El Modelo de Capas de Base de Datos en Python
Un servicio Python que se comunica con una base de datos en realidad pasa por varias capas apiladas, incluso cuando una sola línea session.query(...) hace que parezca un solo paso.
En la parte inferior se encuentra un driver de base de datos que habla el protocolo de red real de esa base de datos; encima de él se encuentra un pool de conexiones que decide cuántas de esas conexiones a nivel de red permanecen abiertas a la vez; encima de eso se encuentra SQLAlchemy Core, que traduce expresiones Python a SQL; y encima de Core se encuentra el ORM, que rastrea la identidad del objeto y genera ese SQL para ti.
Conceptos Básicos de Bases de Datos muestra el código de trabajo para cada una de esas capas; esta página trata sobre por qué la pila tiene esta forma, y dónde se sitúan realmente la sincronización frente a la asincronía y el pooling de conexiones dentro de ella.
Resumen
- El acceso a bases de datos en Python es una pila de capas - un driver DB-API, un pool de conexiones, el lenguaje de expresiones de SQLAlchemy Core y el ORM - cada una añadiendo estructura sobre la capa inferior en lugar de reemplazarla.
- Por Qué Importa: Confundir qué capa posee qué trabajo (¿es el ORM lento, o el pool está infra-dimensionado, o el driver está bloqueando?) es la fuente más común de problemas de rendimiento de bases de datos mal diagnosticados en servicios Python.
- Conceptos Clave: DB-API 2.0, pool de conexiones, SQLAlchemy Core, unidad de trabajo del ORM, drivers síncronos vs asíncronos, consultas N+1.
- Cuándo Usar: Elegir entre DB-API crudo, Core y el ORM para un nuevo servicio; diagnosticar si una ralentización es a nivel de driver, a nivel de pool, o a nivel de patrón de consulta; decidir si una reescritura asíncrona realmente ayudará.
- Limitaciones / Compensaciones: Cada capa añadida proporciona conveniencia y portabilidad a costa de cierta visibilidad del SQL exacto y la temporización que ocurren debajo.
- Temas Relacionados: dimensionamiento del pool de conexiones, el patrón de consulta N+1, migraciones Alembic, diferencias de drivers síncronos a asíncronos.
Fundamentos
PEP 249, comúnmente llamado DB-API 2.0, es una especificación, no una biblioteca: define una forma común - connect(), un objeto cursor(), .execute(), .fetchall() - que la mayoría de los drivers de bases de datos Python (psycopg, sqlite3, mysqlclient) implementan de forma independiente.
Esa forma compartida es la razón por la que el código de aplicación que habla con Postgres a través de psycopg se parece estructuralmente al código que habla con MySQL a través de un driver diferente, aunque los dos drivers no compartan código y las bases de datos hablen protocolos de red completamente diferentes por debajo.
Una analogía útil es la forma de un enchufe estándar: DB-API no hace que todas las bases de datos sean idénticas, al igual que una forma de toma de corriente común no hace que todos los electrodomésticos sean idénticos, pero significa que cualquier cosa construida para enchufarse en esa forma - como SQLAlchemy - no necesita una integración separada para las peculiaridades de cada driver.
SQLAlchemy Core se sitúa directamente encima de los drivers DB-API: añade un engine (que posee un pool de conexiones) y un lenguaje de expresiones Python (select(), insert(), Table) que se compila al dialecto SQL específico que espera una base de datos determinada.
El ORM es una capa adicional construida sobre Core, no un camino separado a la base de datos: las clases declarativas todavía se ejecutan a través del engine y las conexiones de Core, pero el ORM añade una Session que rastrea qué objetos Python corresponden a qué filas de la base de datos (el mapa de identidad) y agrupa los cambios en un solo flush en lugar de una sentencia por cambio de atributo.
Mecánica e Interacciones
El pooling de conexiones existe porque abrir una conexión a la base de datos es costoso en comparación con ejecutar una consulta: típicamente implica un handshake TCP, a veces TLS, y un intercambio de autenticación antes de que la primera sentencia pueda ejecutarse.
El pool por defecto de SQLAlchemy mantiene un pequeño número de conexiones DB-API reales abiertas y las entrega a quien llama engine.connect() o ejecuta una consulta, devolviendo la conexión al pool (sin cerrarla) cuando el bloque with o la sesión circundante terminan.
requests ──▶ engine.connect() ──▶ [pool: N conexiones] ──▶ driver ──▶ database
▲ prestada / inactiva
request N+1 ── espera en cola ──────┘ (todas las N actualmente prestadas)
Eso significa que el tamaño del pool es un techo de concurrencia que tú estableces, no un dial de velocidad: un pool de 10 limita las consultas concurrentes en curso a 10 independientemente de la rapidez con la que se ejecute el código circundante, y ese techo se multiplica en cada proceso worker: cinco workers de Gunicorn, cada uno con un pool de 10, pueden presentar 50 conexiones simultáneas a una base de datos que quizás solo permita 100 en total.
El acceso asíncrono es donde esta pila adquiere una nueva complejidad, no solo una versión más rápida del mismo camino.
asyncpg es un driver nativo asíncrono con su propia implementación de protocolo; no implementa DB-API 2.0 en absoluto, porque DB-API es inherentemente una especificación síncrona.
El create_async_engine de SQLAlchemy puentea esto usando greenlet internamente: permite que el código de unidad de trabajo del ORM, tradicionalmente de aspecto síncrono, se ejecute contra un driver asíncrono cambiando transparentemente el contexto en los puntos de E/S, por lo que await session.execute(...) no es literalmente el mismo camino de código que la Session síncrona, sino una versión puenteada de la misma.
# create_async_engine habla con asyncpg (nativo asíncrono, no DB-API)
engine = create_async_engine("postgresql+asyncpg://user:pass@host/db")
async with AsyncSession(engine) as session:
result = await session.execute(select(Item))
# greenlet permite que esta llamada estilo ORM espere a un driver nativo asíncrono debajoEse puente es precisamente la razón por la que SQLAlchemy asíncrono tiene sus propios problemas (la carga perezosa de una relación fuera de un contexto await explícito genera un error, en lugar de bloquear silenciosamente) - la API del ORM de estilo síncrono y el driver asíncrono subyacente son reconciliados por SQLAlchemy, no son naturalmente compatibles por sí mismos.
Consideraciones Avanzadas y Aplicaciones
La elección entre driver, Core u ORM realmente se trata de dónde quieres que resida la visibilidad, y vale la pena separarla de la elección entre síncrono y asíncrono, porque las dos decisiones interactúan de manera diferente en cada capa.
DB-API crudo proporciona control total sobre exactamente qué SQL se ejecuta y cuándo, a costa de escribir y mantener ese SQL manualmente, incluida su parametrización.
SQLAlchemy Core mantiene ese nivel de control SQL mientras añade portabilidad de dialecto y protege contra inyecciones a través de su lenguaje de expresiones en lugar de formato de cadenas.
El ORM intercambia parte de esa visibilidad por velocidad de desarrollo y código tipado con forma de objeto, a costa de ocultar patrones potencialmente costosos - el más notorio es el problema de la consulta N+1, donde acceder a una relación cargada perezosamente dentro de un bucle convierte una consulta prevista en N+1 viajes de ida y vuelta.
| Capa | Fortaleza | Debilidad | Mejor Ajuste |
|---|---|---|---|
| Driver DB-API crudo | Máximo control sobre el SQL exacto y la temporización | Sin portabilidad de dialecto; disciplina manual de parametrización requerida | Scripts estrechos y críticos para el rendimiento o una herramienta de base de datos única |
| SQLAlchemy Core | SQL portable por dialecto, gestión de pool, parametrización segura | Todavía requiere escribir la lógica de consulta manualmente, solo que en objetos Python | Operaciones masivas, metadatos de migraciones, consultas sensibles al rendimiento |
| SQLAlchemy ORM | Mapa de identidad, modelos tipados, menos código repetitivo para código CRUD intensivo | Puede ocultar patrones N+1 y viajes de ida y vuelta adicionales detrás de una sintaxis conveniente | Código de aplicación con ricas relaciones de objetos y flujos de trabajo intensivos en CRUD |
| SQLModel | Ergonomía del ORM más modelos de solicitud/respuesta basados en Pydantic en una sola clase | Proyecto más joven, acoplamiento más estrecho entre el esquema de la API y el esquema de la BD | Servicios FastAPI que desean una definición de modelo para ambas capas |
Ni síncrono ni asíncrono es universalmente "el rápido": el asíncrono ayuda cuando un servicio está limitado por E/S y necesita manejar muchas solicitudes concurrentes sin bloquear un hilo worker en cada una, pero no hace nada por el trabajo limitado por CPU, y un engine asíncrono con un pool sobredimensionado todavía alcanza el mismo techo de conexiones a la base de datos que alcanzaría un engine síncrono.
Las herramientas de migración (Alembic) se sitúan junto a esta pila en lugar de dentro de ella: leen los mismos metadatos de Core/ORM para generar diferencias de esquema y aplicarlas como migraciones.
Conceptos Erróneos Comunes
- "El acceso asíncrono a bases de datos es siempre más rápido." Ayuda a la concurrencia bajo carga limitada por E/S al liberar un worker mientras espera la base de datos, pero no aporta ningún beneficio al trabajo limitado por CPU y no eleva el techo de conexiones de la propia base de datos.
- "El ORM reemplaza a Core, y Core reemplaza al driver." Cada capa se asienta sobre la de abajo: la
Sessiondel ORM todavía se ejecuta a través del engine de Core y una conexión DB-API (o nativa asíncrona) con pool debajo. - "Un pool de conexiones más grande es siempre más seguro." Más allá de un punto, simplemente traslada el cuello de botella al límite de conexiones de la propia base de datos, que todos los procesos worker comparten: los pools sobredimensionados en varios workers son una causa común de incidentes de "base de datos rechazando conexiones".
- "
asyncpges solo una versión asíncrona depsycopg." Son drivers implementados independientemente con diferente manejo de protocolos;psycopg(v3) soporta modos síncrono y asíncrono en un solo driver, mientras queasyncpges solo asíncrono y nunca fue una implementación de DB-API en primer lugar. - "La
Sessionde SQLAlchemy es segura para compartir entre solicitudes concurrentes." UnaSessionno es segura para hilos por defecto y está pensada para ser limitada a una unidad de trabajo (típicamente una solicitud); compartir una entre solicitudes concurrentes corre el riesgo de corromper su mapa de identidad.
Preguntas Frecuentes
¿Qué es DB-API 2.0 y por qué es importante?
Es PEP 249, una especificación que define una forma común (connect(), cursor(), .execute()) que la mayoría de los drivers de bases de datos Python implementan de forma independiente. Es la razón por la que herramientas como SQLAlchemy pueden soportar muchas bases de datos sin una integración a medida para los detalles internos de cada driver.
¿SQLAlchemy Core reemplaza al driver de la base de datos?
No, Core se asienta sobre un driver DB-API, añadiendo un engine, un pool de conexiones y un lenguaje de expresiones SQL. El driver todavía realiza la comunicación real del protocolo de red por debajo.
¿Es el ORM una forma separada de acceder a la base de datos desde Core?
No, la Session del ORM se ejecuta a través del mismo engine y conexiones con pool que usa Core. Añade un mapa de identidad y seguimiento de unidad de trabajo por encima, no una ruta de conexión alternativa.
¿Por qué el acceso asíncrono a bases de datos necesita algo como greenlet?
Porque la API del ORM de SQLAlchemy está escrita en un estilo tradicionalmente síncrono, pero los drivers nativos asíncronos como asyncpg no son implementaciones de DB-API en absoluto. Greenlet permite que ese código del ORM de estilo síncrono se ejecute contra un driver asíncrono cambiando el contexto en los puntos de E/S, en lugar de que los dos sean naturalmente compatibles.
¿Qué determina realmente cuánta carga de base de datos concurrente puede realizar mi servicio?
Generalmente el tamaño del pool de conexiones, no la velocidad bruta del driver o de la consulta: un pool de N conexiones limita las consultas en curso a N independientemente de la rapidez con la que se ejecute el código circundante. Ese techo también se multiplica en cada proceso worker que ejecuta su propio pool.
¿Por qué cinco workers con un tamaño de pool de 10 todavía pueden abrumar una base de datos que permite 100 conexiones?
Porque 5 workers por un pool de 10 cada uno son 50 conexiones posibles, y eso es antes de tener en cuenta otros servicios o réplicas que comparten el límite de conexiones de la misma base de datos. Las matemáticas deben tener en cuenta las instancias totales, no solo la configuración del pool de un solo proceso.
¿Qué es concretamente el problema de la consulta N+1?
Obtener una lista de N filas y luego obtener los datos relacionados de cada fila por separado, una a una, convirtiendo una consulta prevista en N+1 viajes de ida y vuelta. Es más común cuando una relación del ORM cargada perezosamente se accede dentro de un bucle sin carga anticipada.
¿Debería siempre optar por el ORM en lugar de Core o DB-API crudo?
No necesariamente: el ORM intercambia algo de visibilidad del SQL exacto por menos código repetitivo en código intensivo en CRUD y con ricas relaciones de objetos. Las operaciones masivas, los metadatos de migraciones o las consultas críticas para el rendimiento a menudo encajan mejor con Core o DB-API crudo.
¿Es `psycopg` el mismo tipo de driver que `asyncpg`?
No, psycopg (versión 3) implementa DB-API 2.0 y soporta modos síncrono y asíncrono en un solo driver, mientras que asyncpg es un driver nativo asíncrono que nunca implementó DB-API en absoluto, ya que DB-API es inherentemente síncrono.
¿Puedo compartir una `Session` de SQLAlchemy entre múltiples solicitudes concurrentes?
No, una Session no es segura para hilos por defecto y está pensada para limitarse a una unidad de trabajo, típicamente una solicitud o una tarea. Compartirla entre solicitudes concurrentes corre el riesgo de corromper su mapa de identidad y estado de transacción.
¿El pooling de conexiones significa que no necesito cerrar sesiones o conexiones explícitamente?
No, todavía necesitas usar gestores de contexto o cerrar explícitamente una sesión/conexión para que sea devuelta al pool. Omitir eso deja una conexión prestada indefinidamente, lo que agota el pool para otras solicitudes, aunque el socket subyacente permanezca abierto.
¿Dónde encaja Alembic en este modelo de capas?
Se sitúa junto a la pila en lugar de dentro de ella, leyendo los mismos metadatos de Core u ORM para generar diferencias de esquema y aplicarlas como migraciones. No cambia cómo la aplicación se comunica con la base de datos en tiempo de ejecución.
Relacionados
- Conceptos Básicos de Bases de Datos - ejemplos prácticos de DB-API, engine y ORM
- SQLAlchemy Core - el lenguaje de expresiones y el pool de conexiones en detalle
- SQLAlchemy ORM 2.0 - Session, mapa de identidad y modelos declarativos
- Acceso Asíncrono a Bases de Datos - engines asíncronos, sessions asíncronas y puenteo con greenlet
- Gestión de Conexiones - dimensionamiento de pools, reintentos y alcance de transacciones
- Patrones de Consulta y N+1 - diagnóstico y corrección del patrón N+1
Versiones de la pila: Esta página fue escrita para Python 3.14.0 (estable 3.14, mantenimiento 3.13).