Consejos y trucos de SQL
(github.com/ben-n93)- Una lista de hábitos al escribir SQL y patrones de consultas que se usan con frecuencia en el trabajo de análisis de datos, con la premisa de que no todo aplica igual a todos los RDBMS
- En términos de legibilidad, se recomienda usar comas al inicio,
WHERE 1=1, sangría, CTE, comentarios yUSINGpara que las consultas sean más fáciles de leer y modificar - En procesamiento de datos, se muestran ejemplos de sintaxis como anti-join,
QUALIFY,GROUP BY ROLLUPyEXCEPT, usadas en la práctica para filtrar resultados, generar totales y verificar diferencias entre tablas - En rendimiento y exactitud,
NOT INconNULL, la conversión implícita de tipos y los conflictos de alias en campos calculados pueden afectar el resultado o la velocidad de una consulta - En consultas complejas, hábitos básicos como entender el orden de ejecución, revisar la documentación, indicar el origen de las columnas y usar buenos nombres para consultas guardadas son importantes para mejorar la depuración y la reutilización
Hábitos para mejorar la legibilidad al escribir SQL
- Este repositorio es una lista organizada de consejos y trucos de SQL aprendidos a lo largo de varios años, centrada en lo útil para el trabajo diario de una analista o un analista de datos y en lo que habría sido bueno saber al comenzar a usar SQL
- Algunos consejos pueden no aplicar a todos los RDBMS
-
Comas al inicio y
ANDal inicio- En la cláusula
SELECT, se recomienda usar comas al inicio en lugar de comas al final para separar campos - Hace más claro si se trata de una nueva columna o de una línea continuada
- Aunque cambie la longitud de las líneas, es más fácil detectar si falta una coma
- Por la misma razón, también se puede poner
ANDal inicio de las condiciones en la cláusulaWHERE
- En la cláusula
-
Facilitar pruebas de condiciones con
WHERE 1=1- Si se agrega la condición dummy
1=1en la cláusulaWHERE, la consulta no se rompe aunque se comenten condiciones durante las pruebas - Incluso si se comentan todas las condiciones,
1=1permanece y la consulta puede seguir ejecutándose
- Si se agrega la condición dummy
-
Sangría y formateadores
-
Considerar CTE en consultas complejas
- Si se anidan inline views en 2 o 3 niveles o más, es fácil que unas semanas después la consulta se vuelva difícil de entender
- Los CTE se presentan como una forma de organizar mejor consultas largas y ayudar con la reutilización y la depuración
-
Que los comentarios expliquen el “por qué”
- Después de un tiempo, puede ser difícil recordar por qué se hizo un procesamiento específico
- En general, es mejor que los comentarios expliquen por qué se hizo algo, más que cómo funciona el código
- El ejemplo agrega un comentario a una condición que excluye contenido archive porque un CMS nuevo no puede procesar el formato de video archive
-
Usar
USINGal unir columnas con el mismo nombre- Si se hace un join entre dos tablas usando columnas con el mismo nombre,
USINGpermite expresarlo de forma más simple queON USINGelimina duplicados de la columna común en el resultado y devuelve solo una- Si se usa
ONy no se indica explícitamente la columna común, puede aparecer un error deambiguous column name
- Si se hace un join entre dos tablas usando columnas con el mismo nombre,
Sintaxis útil para procesamiento de datos
-
Encontrar filas ausentes en otra tabla con anti-join
- El anti-join se usa para devolver filas que existen en una tabla pero no tienen coincidencia en otra
- El ejemplo trata el caso de obtener solo los
video_idde contenido que no fue archivado - Hay varias formas de implementarlo
- Filtrar solo las filas donde la clave de la tabla emparejada sea
NULLdespués de unLEFT JOIN - Usar
NOT INcon una subconsulta - Usar
NOT EXISTScon una subconsulta correlacionada - No se recomienda
NOT INporque puede no comportarse como se espera debido a valoresNULL
-
Filtrar resultados de funciones de ventana con
QUALIFYQUALIFYpermite filtrar resultados de consultas con base en el resultado de una función de ventana- Permite filtrar sin usar inline views, reduciendo la cantidad de líneas de código
- El ejemplo selecciona los 10 principales mercados por producto con
DENSE_RANK()y luego filtra conQUALIFY - Existe la limitación de que
QUALIFYparece estar disponible solo en grandes data warehouses como Snowflake, Amazon Redshift y Google BigQuery
-
GROUP BYyORDER BYbasados en posición de columnas- Se puede escribir
GROUP BY 1uORDER BY 2usando la posición de la columna en vez del nombre - Puede ser útil en consultas temporales o de un solo uso
- Para código de producción, se recomienda siempre referenciar directamente los nombres de las columnas
- Se puede escribir
-
Generar totales con
GROUP BY ROLLUPGROUP BY ROLLUPpuede usarse para generar subtotales y totales generales- El ejemplo calcula la suma de salarios por departamento y además genera una fila con la suma total de salarios
- La documentación de Transact-SQL explica que
ROLLUPcrea grupos por combinaciones de expresiones de columnas y reduce la cantidad de grupos de derecha a izquierda para generar subtotales y totales - Si se aplica
COALESCE, la fila de total puede mostrarse comoTotal - Hay que cuidar la columna de ordenamiento para que la fila de total quede al final del resultado
-
Encontrar diferencias entre dos conjuntos de resultados con
EXCEPTEXCEPTdevuelve filas que están en el resultado de la primera consulta pero no en el de la segunda- Si se usan juntos
EXCEPTyUNION ALL, se puede validar si dos tablas tienen los mismos datos - Si no se devuelve ninguna fila, ambas tablas son iguales
- Si se devuelven filas, esas filas son la causa de la diferencia
Patrones que dañan el rendimiento y la exactitud
-
En columnas que pueden ser
NULL,NOT EXISTSes mejor queNOT IN- Si la columna comparada permite
NULL,NOT INnormalmente puede ser más lento queNOT EXISTS - Se menciona haber vivido este problema en Snowflake, y Don’t Do This del PostgreSQL Wiki indica que
NOT IN (SELECT ...)no se optimiza bien NOT INno funciona como se espera si hayNULLen los valores comparados- Que una columna permita
NULLno significa que realmente tenga valoresNULL, pero al trabajar con tablas que no se pueden modificar,NOT EXISTSpuede ayudar a mejorar la velocidad
- Si la columna comparada permite
-
La conversión implícita de tipos puede ralentizar o fallar
- Si en una condición se pone un valor de un tipo de dato distinto al de la columna, la base de datos puede intentar una conversión implícita de tipos
- El ejemplo trata el caso de comparar el entero
200050contra una columnavideo_idde tipo cadena - Depender de una conversión implícita puede causar problemas
- Si hay valores que no se pueden convertir, puede producirse un error
- La consulta puede volverse más lenta por el trabajo extra de convertir cada valor al tipo indicado
- Se puede usar el mismo tipo de dato que la columna o, para evitar errores, funciones como
TRY_TO_NUMBERde Snowflake - El impacto en velocidad depende del tamaño del dataset procesado
Errores frecuentes
-
NOT INyNULLNOT INno funciona si entre los valores comparados hayNULL- Como
NULLrepresenta Unknown, el motor SQL no puede verificar que el valor evaluado no esté en la lista - En ese caso, una alternativa es usar
NOT EXISTS
-
Conflictos de alias en campos calculados
- Si el nombre de un campo calculado queda igual al de una columna existente, puede haber comportamientos inesperados
- La documentación de
GROUP BYde Snowflake indica que si un nombre en la cláusulaGROUP BYcoincide tanto con un nombre de columna como con un alias, se usa el nombre de la columna - En el ejemplo, si se crea el alias
LEFT(product, 1) AS producty luego se usaGROUP BY product, la agrupación se hace por la columna originalproducty no por la primera letra, por lo que se devuelven 3 filas - Hay dos soluciones
- Usar un alias único como
product_letter - Indicar explícitamente la expresión, como
GROUP BY LEFT(product, 1) - Los alias también pueden causar problemas en funciones de ventana
- En el ejemplo, un
CASEcambia a 0 el revenue deRobot, pero como se aplica después de ejecutar la función de ventana, el ranking sale distinto a lo esperado - Cuando sea posible, se necesita usar un alias único o poner directamente la expresión calculada dentro del
ORDER BYde la función de ventana
-
Indicar a qué tabla pertenece cada columna
- En consultas complejas con varios joins, debe ser posible rastrear un problema de valores hasta la tabla de origen
- Si dos tablas comparten el mismo nombre de columna y no se indica a cuál pertenece, el RDBMS puede lanzar un error
- El ejemplo deja claro el origen de las columnas usando alias de tabla como
vc.video_idymetadata.season
Orden de ejecución, documentación y nombres guardados
-
Entender el orden de ejecución de SQL
- Se señala que uno de los consejos más importantes para quienes aprenden SQL es entender el orden de ejecución de las cláusulas
- Conocer ese orden puede cambiar mucho la forma de escribir consultas
- Como material de referencia se propone A beginner’s guide to the true order of SQL operations
-
Leer la documentación hasta el final
- Se menciona un caso en Snowflake donde se usó
GREATEST()para devolver la fecha más reciente entre varias columnas de fecha GREATEST()devuelveNULLsi uno de sus argumentos esNULL- Si se hubiera leído más la documentación, se habría podido usar
GREATEST_IGNORE_NULLS()en lugar deCOALESCE(GREATEST(...), ...) - En muchos casos, revisar la documentación toma menos de un minuto y puede ahorrar el esfuerzo de encontrar por qué algo se comporta distinto de lo esperado
- Se menciona un caso en Snowflake donde se usó
-
Usar nombres descriptivos para consultas guardadas
- Para evitar no encontrar una consulta que luego haya que volver a ejecutar o consultar, conviene guardarla con un nombre descriptivo
- El nombre guardado suele incluir el tema de la consulta, el mes de ejecución y el nombre de quien la pidió
- El ejemplo tiene el formato
Lapsed users analysis - 2023-09-01 - Olivia Roberts
2 comentarios
Las comas iniciales en esta publicación están escritas todas como comas finales. En el original están ingresadas como iniciales.
Opiniones de Hacker News
El consejo que agregaría es este: aprende bien tu servidor de DB y revisa con frecuencia los planes de ejecución. Pueden salir resultados inesperados, así que conviene ajustar y volver a revisar.
Por lo general,
EXISTSes más rápido queIN, yNOT EXISTSse comporta distinto queEXCEPTal manejarNULL. A veces es mucho más rápido usar columnas de subconsulta en la lista deSELECTque unir tablas y luego filtrar filas con algo comoDISTINCT. Esto puede ser cierto incluso si traes más de 10 valores de la misma tabla, y también aunque el servidor de DB soporte lateral join. Eso sí, la subconsulta debe devolver como máximo una fila.Las consultas que no sean de una sola vez no deberían hacer un escaneo completo de tabla. El escaneo de tabla de hoy puede ser la caída de mañana, así que hay que agregar índices. Recuerda también que la cláusula
GROUP BYsuele determinar el uso de índices.Si necesitas filtrar por una expresión, por ejemplo comprobar si una subcadena es igual a cierto valor, puedes agregar una columna calculada e indexarla. Algunas DB soportan directamente índices sobre expresiones. Usar
UNION ALLen lugar deORa menudo puede hacer que consultas complejas o con varias condicionesORsean mucho más rápidas.Cuando la DB no elige de forma inteligente el orden de filtrado, también es útil forzar el orden haciendo
JOINcon una subconsulta.Algo que me pareció interesante en Postgres —y es posible que también ocurra en otras DB— es que una operación
INSERT (SELECT ...)puede acelerarse de forma casi lineal si la particionas manualmente según la cantidad de núcleos de CPU. Incluso funcionó con unas 10 uniones. Primero mirasEXPLAINy encuentras el join más interno o más externo; luego ejecutas consultas paralelas separadas para cada rango de filas (id >= start AND id < end). En un trabajo de hace 6 años usamos mucho este método por razones raras. Postgres 10+ agregó paralelismo, pero por lo que sé todavía no es tan avanzado como esto.SELECT.Por ejemplo, si ejecuto
SELECT column1, (SELECT column2, column3, ... FROM table_b WHERE table_a.id = table_b.a_id) FROM table_a, como era de esperarse aparece “subquery must return only one column”. ¿La idea es devolver varias columnas como un record/tipo compuesto?No me resultó obvio por qué la cláusula
GROUP BYsuele determinar el uso de índices, pero para quien tenga curiosidad, este artículo lo explica paso a paso: https://www.brentozar.com/archive/2015/06/indexing-for-group...EXPLAINy aprender a interpretarlo con la herramienta que prefieras. También hay que monitorear las consultas.En una startup anterior instalamos PgHero, y nos ayudó muchísimo con la optimización de rendimiento y la priorización.
Hay consultas en las que un escaneo completo de tabla es la estrategia de acceso más eficiente. Normalmente son consultas analíticas/de agregación que leen toda la tabla, y a veces incluso cuando solo se recupera el 50% de las filas, un escaneo de tabla es mejor.
Tampoco veo bien cómo un escaneo de tabla de solo lectura terminaría causando una caída, porque no bloquea el acceso concurrente. La única desventaja es que genera mucha carga de I/O, pero si el servidor no puede soportar eso, diría que desde el principio está gravemente subdimensionado.
Los 3 ejemplos de la sección “legibilidad” son raros. Los primeros 2 literalmente sacrifican legibilidad para que sea más fácil escribirlos, y el último es un monstruo difícil de leer que casi ni la indentación puede salvar.
Creo que los desarrolladores miran el historial de commits casi tanto como el código fuente real.
Las he visto con suficiente frecuencia como para que ya no me molesten mucho.
No veo el problema
y no parece haber nada incorrecto.
SELECTuna por línea y aun así deja líneas de 150 caracteres? Esa es una definición rota de legibilidad. Y ni siquiera voy a empezar con lo de las comas.En las revisiones de código, nadie mira bien las líneas largas. Ese fue el mayor problema de AngularJS. Los merges se resolvían mal y todo se rompía porque, alrededor de la columna 90, la vista se te empieza a nublar. He pasado por más de media docena de equipos con code review y siempre fue igual. Incluso siendo muy consciente de este problema e intentando evitarlo, yo también sigo cometiendo errores, aunque más o menos con la mitad de frecuencia que otros.
Divídanlo un poco. Sobre todo si van a mostrar ejemplos a otras personas.
Estos son algunos consejos para lidiar con procedimientos almacenados complejos
WHERE. Si intentas hacerJOINu operaciones complejas, es muy probable que haya timeoutsSi haces el punto 5 sin el 6, es muy probable que no veas que estás haciendo algo no óptimo. Mi consejo es evitar la optimización prematura, escribir primero de la forma más intuitiva y optimizar solo cuando haga falta. Lo más importante es no escribir SQL de forma procedural. Se trata de describir los datos que quieres, no de ordenarle al motor cómo traerlos
En cambio, Microsoft sigue poniendo advertencias de no intentar ajustarlo, como si el planificador de consultas siempre supiera mejor
La versión de la DB también puede influir
No me gusta el desarrollo hecho “por si acaso”. Lo mismo aplica a las interfaces y a placeholders como
where 1=1Hazlo cuando lo necesites. No lo hagas solo porque quizá algún día en el futuro lo necesites. El código de producción no es un lugar para dejar helpers de desarrollo. Durante el desarrollo puedes hacer lo que quieras, pero en código de producción la legibilidad y una intención clara son mucho más importantes
Una cosa más sobre los “anti joins”. Si solo quieres comprobar si existe una fila que cumpla una condición en otra tabla grande o en una subconsulta, conviene usar EXISTS en vez de
INoLEFT JOINEXISTSdevuelve verdadero en cuanto encuentra una coincidencia. En el caso deLEFT JOINeIN, el motor reúne todos los resultados antes de evaluarlos(NOT) EXISTSgeneró un plan de ejecución mejor que(LEFT) JOINo(NOT) IN, o bien el mismo planAdemás, la intención queda más clara
En relación con “comenta tu código”, al menos en MSSQL se suele recomendar usar
/**/en los comentarios en lugar de--. Es porque funciones como Query Store muchas veces guardan las consultas sin saltos de línea, así que cuando traes una consulta desde ahí tienes que arreglarla toda manualmente en vez de poder usar directamente el formateador del IDEOBJECT_DEFINITIONselect name,cast((select OBJECT_DEFINITION(object_id) for xml path('')) as xml) from sys.proceduresComo se preservan los saltos de línea, puede ser más fácil ordenarlo. Eso sí, otros caracteres XML se rompen, como cuando
>se convierte en>. Otra opción es usarVARBINARYy algo que lo vuelva a decodificarTodos se alteran con la sugerencia de las comas, pero ¿les parece buena idea lo de
1=1en la cláusulaWHERE? Si veo eso en una revisión de código, no sé qué pensar del autorWHEREno afecte otras líneas, haciendo que la revisión de código sea más fácilPero si la razón es agregar condiciones dinámicas, como en este caso, donde trabajo definitivamente te despedirían
¿Alguien podría compartir una guía general sobre dónde trazar la línea entre aumentar la velocidad mediante la configuración de la DB, algo casi cercano a “comprar”, y una “construcción” que en la práctica se implementa manualmente? En mi experiencia limitada, como los DBA competentes ganan mucho más y trabajan en otros lugares, este trabajo a menudo termina cayéndole al desarrollador de la app. Como se dijo arriba, es importante conocer la DB
Un ejemplo típico son los datos que se acumulan masivamente con el tiempo y donde los datos más recientes son los que se consultan con más frecuencia. Un DBA puede mantener rápido el acceso con particionamiento o índices parciales, pero un desarrollador de apps también podría mover registros en segundo plano a tablas de archivo separadas, sin dejar de soportar funciones como la búsqueda final sobre todo el conjunto de datos. También me da la sensación de que, en casos como cuando uno está limitado por la falta de funciones de una DB en la nube, una herramienta podría automatizar bastante el trabajo inicial de dividir una tabla en varias en el momento adecuado
Otra opción de gestión es guardar todos los blobs/archivos grandes en una base de datos separada, o en el sistema de archivos, para usar otra configuración de almacenamiento. Esto también puede ser soportado por la DB o manejarse manualmente
En el extremo, creo que se podría llegar incluso a implementar índices por cuenta propia. Sería tener una tabla enorme con una clave primaria autoincremental y muchísimas columnas, y crear aparte una tabla con ese ID y algunas columnas buscables. Incluso se podría llegar a búsqueda de texto completo o vectores
Un tip útil al implementar manualmente el patrón de vistas materializadas en MSSQL 2016+ es usar también el cambio de particiones. Está bien explicado e implementado en https://github.com/cajuncoding/SqlBulkHelpers?tab=readme-ov-.... Fue una pequeña biblioteca que encontré por casualidad, comercialmente la más útil para mí, pero con bajo ranking en búsquedas y pocas estrellas; está enfocada en inserciones masivas en MSSQL con .NET. Creo que es un buen ejemplo de trazar correctamente la línea entre comprar y construir mediante la automatización del cambio de particiones
Algo que falta: hay que dejar de usar
SELECT *. Casi seguro no necesitas todo el ancho de la tabla; al hacerlo aumentas los datos que hay que filtrar y transferir, y además bloqueas una función muy buena: los semi joinsSi eres desarrollador, sí.
SELECT *tiene trampas, y casi siempre deberías especificar las columnas o usar un query builder que lo haga por tiPero si eres analista, la vida es corta y a veces no quieres tipear todas las columnas.
SELECT *está bienPuede que me salga un poco del tema, pero ¿es aceptable que el administrador simplemente cierre un pull request sin ningún comentario ni discusión?
Lo pregunto como alguien que ha contribuido, o intentado contribuir, ocasionalmente al repositorio
Ejemplo: https://github.com/ben-n93/SQL-tips-and-tricks/pulls?q=is%3A...