Cinco patrones SQL que sustituyen un bucle de Python — y por qué ganan
El Python más caro que reviso es for order in orders tras un SELECT *. Última fila por grupo, mes a mes, recuentos filtrados y upserts son consultas. Cómo las escribo en Postgres en vez de bajar la tabla.
El Python más caro que reviso no es el handler de FastAPI sin tipos. Es el for order in orders que existe porque nadie escribió una función de ventana. El bucle parece honesto. Así un informe de cuatro segundos pasa a cuarenta, y el lunes tras un buen fin de semana de ventas se convierte en timeout.
Ya escribí cómo elijo un tipo de base. Esta es la capa siguiente: el SQL que escribo de verdad cuando la pregunta del producto es «último pedido por cliente», «ingresos de este mes frente al anterior» o «inserta esta reserva si el hueco está libre». Postgres responde en un viaje. Python también — después de bajarse la tabla. El artículo hermano: cómo esa consulta debe salir de Python sin traer el montón de vuelta.
1. El bucle que se baja la tabla
Un script típico: SELECT * FROM orders, luego agrupar en un dict, luego el created_at más reciente por customer_id. Con 800 filas se siente bien. Con 800.000 es un ticket de soporte. La base ya tiene el índice, el orden y la memoria para esto. Eso lo pagó. Usar Python como segundo motor de consultas es cómo el coste aparece en latencia, no en una factura prevista.
- Si el resultado es más pequeño que el origen y la regla tiene nombre — last, sum, rank, exists — es una consulta.
- Si la regla es un ensayo de negocio — reembolsos con excepción humana, un párrafo de política — puede quedarse en Python.
2. Última fila por grupo — DISTINCT ON y ROW_NUMBER
Suscripción actual por espacio de trabajo. Último estado de factura por pedido. El conductor que hizo ping por última vez. Es el primer bucle que borro. En Postgres escribo DISTINCT ON (workspace_id) y ORDER BY workspace_id, created_at DESC. El primo portable es ROW_NUMBER() OVER (PARTITION BY workspace_id ORDER BY created_at DESC), luego dejar rn = 1.
DISTINCT ON es más corto y lo uso cuando Postgres es mío. ROW_NUMBER lo escribo cuando la misma consulta debe sobrevivir un traslado, o cuando necesito rango 1 y 2 — el plan anterior, el segundo. No traiga todas las suscripciones para reducir en Python. Empatará mal y traerá columnas que nunca pinta.
3. Totales acumulados y «desde el mes pasado» — SUM OVER y LAG
Mes a mes no es un join de la tabla consigo en un bucle. LAG(revenue) OVER (PARTITION BY workspace_id ORDER BY month) pone el mes pasado en la misma fila. SUM(amount) OVER (PARTITION BY customer_id ORDER BY created_at) es un saldo acumulado. Las funciones de ventana guardan el grano de la fila y aún ven el vecindario. GROUP BY colapsa. Un bucle en Python reconstruye ese vecindario en un dict y olvida los husos.
Si necesita la fila de detalle y el total del grupo, SUM(amount) OVER (PARTITION BY order_id) gana a una segunda consulta y gana al N+1 del ORM. El truco: conservar el grano que va a pintar y calcular el vecindario en el motor.
4. Recuentos condicionales — FILTER en vez de if
COUNT(*) FILTER (WHERE status = paid) junto a COUNT(*) FILTER (WHERE status = refunded) es un solo barrido. La versión en Python es una pasada y dos contadores — hasta que alguien añade un tercer estado en un segundo bucle, o el export crece y barre dos veces. FILTER se lee. Un CASE dentro de SUM funciona en motores sin FILTER. En Postgres prefiero FILTER porque la intención está en la página: este recuento tiene predicado.
5. Upsert en vez de leer y luego insertar
La carrera: leer «¿este email está libre?» e insertar. Dos pestañas, dos workers, una unique que descubre en producción. INSERT ... ON CONFLICT (email) DO UPDATE — o DO NOTHING — es la transacción que la base ya sabe correr. Misma familia: UPDATE ... WHERE version = $version para un bloqueo optimista. Check-then-write en Python es una base peor escrita a mano.
- Huecos de reserva, claves de idempotencia, «crea el cliente si falta» — son upserts, no bloques if.
- Un índice único es parte del producto. Sin él, ON CONFLICT es teatro.
6. Cómo compruebo que no inventé una consulta más lenta
EXPLAIN (ANALYZE, BUFFERS) en el camino lento. Si ve un sequential scan en una tabla que creció, necesitaba un índice que coincida con la partición y el orden — (workspace_id, created_at DESC) para DISTINCT ON. Una función de ventana no es gratis. Sigue siendo más barata que mandar el montón a la app. Si el plan ordena dos millones de filas para un dashboard que muestra veinte, ponga un tope de fecha. El SQL que sustituye un bucle aún puede ser un mal bucle dentro del motor.
Conclusión: nombre el resultado, escriba la consulta
Python es un buen sitio para HTTP, la política y la frase que ve el usuario. Un mal sitio para redescubrir GROUP BY. Cinco formas — last-per-group, lag, suma acumulada, agregados filtrados, upsert — y la mayoría de los scripts «listos» que reviso se vuelven una vista o una sola consulta en una función de repositorio. El otro artículo es el Python que se niega a deshacer ese trabajo.
¿Hablamos de tu proyecto?
Soy ingeniera web senior, especializada en React y Next.js - disponible para proyectos freelance en cualquier país.
Ubicación
Kyiv, Ucrania
Upwork
Ver perfilTelegram
ContáctameViber
Contáctame