Módulo 4 · Lección 41 de 45
De SQLite a PostgreSQL
El database is locked que no aparece hasta que hay usuarios, el nombre de 500 letras que cabe en un campo de 100, y las búsquedas en español que por fin funcionan.
Todo el módulo ha ido sobre SQLite, que es lo que startproject deja puesto, y no ha estorbado en ningún momento. Pero han quedado tres cosas apuntadas por el camino que apuntan al mismo sitio: la lección 12 midió que las búsquedas con tilde no funcionan, la 37 tuvo que inventar un campo normalizado para arreglarlo, y la 12 dijo que PostgreSQL era la razón de peso para cambiar.
Esta lección hace el cambio y mide qué se gana. Con ejemplos reales, porque casi todo lo que se lee sobre esto son afirmaciones sin números.
El cambio#
Sorprendentemente poco:
pip install "psycopg[binary]"DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'agenda',
'USER': 'agenda',
'PASSWORD': os.environ['CLAVE_BD'],
'HOST': 'localhost',
'PORT': '5432',
}
}Applying contactos.0001_initial... OK
Applying contactos.0002_etiqueta_contacto_etiquetas_ficha... OK
Applying contactos.0003_alter_contacto_options_... OK
Applying contactos.0004_contacto_adjunto_contacto_foto... OK
Applying contactos.0005_contacto_busqueda... OKLas mismas migraciones, sin cambiar ni una línea. Ni los modelos, ni las vistas, ni las consultas. Eso es lo que hace el ORM de la lección 11, y es la primera vez en el módulo que se ve de verdad para qué sirve esa capa de abstracción.
psycopg es la biblioteca que habla con PostgreSQL. El [binary] trae los binarios compilados y evita necesitar un compilador; en producción se suele usar psycopg[c], que es algo más rápido y sí compila.
Y lo que no se migra solo son los datos. Para eso, dumpdata y loaddata si son pocos, o herramientas de PostgreSQL si son muchos. Es la parte aburrida y la que hay que ensayar.
La primera diferencia: escrituras a la vez#
Esta es la razón principal, y es fácil de enseñar. Dos hilos, uno que escribe durante siete segundos y otro que intenta escribir medio segundo después:
SQLite: OperationalError tras 5.2s: database is locked
PostgreSQL: escrito en 0.0sAhí está todo. SQLite permite un escritor a la vez y bloquea el fichero entero; el segundo espera y, pasado su tiempo de espera, se rinde con un error que tira la petición.
Ese tiempo de espera es un ajuste con un valor por defecto que explica el 5.2s:
journal_mode: delete | busy_timeout: 5000Y ojo, porque sin llegar al error el problema ya está ahí. Ocho hilos escribiendo doscientas cuarenta filas, con trabajo dentro de cada transacción:
SQLite: 1.70s
PostgreSQL: 0.21sOcho veces más lento, sin un solo error. SQLite no falló: puso a los ocho hilos en fila de uno en uno. Eso es exactamente lo que pasa en producción antes de que aparezcan los database is locked: el sitio se va poniendo lento a medida que llega gente, y los registros no dicen nada porque nada falló.
Y por qué no se nota en desarrollo: porque desarrollando eres un usuario, y nunca hay dos escrituras a la vez.
La segunda: PostgreSQL te dice que no#
nombre de 500 letras en un campo de 100:
SQLite: guardado, longitud real: 500
PostgreSQL: DataError: value too long for type character varying(100)Esto es más grave de lo que parece. SQLite se traga el dato y lo devuelve entero, porque no aplica los límites de longitud: para él, varchar(100) es una sugerencia.
O sea que max_length en SQLite solo existe en la validación de Django, que es la del bloque D. Todo lo que no pase por un formulario —una importación, un update, un bulk_create, el intérprete— puede meter lo que quiera.
Y la consecuencia real: un proyecto desarrollado sobre SQLite puede tener datos que no caben en producción, y enterarse al migrar.
Esta es la diferencia que menos se menciona y la que más tiempo cuesta cuando aparece. El motivo de fondo es que SQLite tiene tipos flexibles por diseño, mientras que PostgreSQL comprueba todo en la base de datos.
La moraleja práctica es la de siempre: desarrollar con el mismo motor que producción. Es la misma idea de la lección 40 sobre minimizar las diferencias entre entornos, aplicada a lo que más duele.
La tercera: el español#
Aquí es donde el cambio se nota de verdad. Primero, el orden alfabético, que la lección 12 midió mal en SQLite:
PostgreSQL: ['Álvaro Ruiz', 'ana lopez', 'Beatriz', 'Íñigo Ñuño', 'Óscar Paz', 'Zacarías']Correcto. La A con tilde entre las A, la I con tilde donde va, sin mayúsculas antes que minúsculas. SQLite dejaba los acentos al final, detrás de la Z. Esto sale gratis, sin tocar nada, porque PostgreSQL ordena según la configuración regional de la base de datos.
Las búsquedas, en cambio, mejoran a medias:
icontains('íñigo') -> ['Íñigo Ñuño'] (en SQLite: [])
icontains('oscar') -> []La primera línea es una mejora real: PostgreSQL sí entiende mayúsculas y minúsculas en Unicode, así que íñigo encuentra a Íñigo. SQLite no podía.
La segunda sigue fallando, y es normal: quitar tildes no lo hace ninguna base de datos por defecto, porque o y ó son letras distintas. Lo que pasa es que PostgreSQL trae una extensión para eso:
CREATE EXTENSION unaccent;Contacto.objects.filter(nombre__unaccent__icontains=termino)unaccent__icontains('oscar') -> ['Óscar Paz']
unaccent__icontains('inigo') -> ['Íñigo Ñuño']
unaccent__icontains('NUNO') -> ['Íñigo Ñuño']
unaccent__icontains('alvaro') -> ['Álvaro Ruiz']
unaccent__icontains('Óscar') -> ['Óscar Paz']Los cinco. Y fíjate en lo que eso significa: el campo normalizado de la lección 37 sobra. Todo aquello —el campo extra, el save() que lo rellena, la migración de datos, el cuidado con bulk_create— era un apaño para suplir lo que SQLite no hace. Con PostgreSQL se borra y se escribe una palabra en la consulta.
Hace falta poner 'django.contrib.postgres' en INSTALLED_APPS, y la extensión se instala con una migración (CreateExtension) para que esté también en la base de pruebas.
Búsqueda de texto completo#
Esto ya no es "lo mismo pero mejor": es algo que SQLite no tiene. Y tiene un detalle que casi nadie cuenta. Mira cómo trocea PostgreSQL la misma frase con dos configuraciones:
frase: 'Reunión sobre los presupuestos de marketing y las campañas'
english: 'campaña':9 'de':5 'las':8 'los':3 'market':6 'presupuesto':4 'reunión':1 'sobr':2 'y':7
spanish: 'campañ':9 'marketing':6 'presupuest':4 'reunion':1Nueve piezas contra cuatro, y la diferencia lo explica todo:
Las palabras vacías. La configuración en español tira de, los, las y y, que no aportan nada. La inglesa las guarda como si fueran términos buscables, y además se come la e de sobre aplicándole reglas inglesas.
Las raíces. En español, presupuestos se reduce a presupuest y campañas a campañ, así que buscar presupuesto encuentra presupuestos y al revés. En inglés no, porque no conoce los plurales españoles.
Las tildes. reunión se guarda como reunion. O sea que en español la búsqueda de texto completo ya es insensible a tildes, sin unaccent.
Por eso el ajuste importa:
from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank
vector = SearchVector('nombre', 'notas', config='spanish')
consulta = SearchQuery(termino, config='spanish')
Contacto.objects.annotate(v=vector).filter(v=consulta).annotate(
relevancia=SearchRank(vector, consulta)
).order_by('-relevancia')Ese config='spanish' hay que ponerlo en los dos sitios, en el vector y en la consulta, o se troceará el texto con unas reglas y la búsqueda con otras, y no coincidirán. Es el fallo número uno de esta parte, y falla en silencio: devuelve menos resultados de los que debería.
Con eso se llega a lo que la lección 37 no podía: relevancia. Y para las erratas, TrigramSimilarity, que necesita la extensión pg_trgm y encuentra a Álvarez buscando Alvares.