- Los antipatrones de SQL dificultan el mantenimiento de las consultas y los pipelines de datos, y provocan un rendimiento más lento de lo esperado
- Abuso de
CASE WHEN, aplicar funciones a columnas indexadas, SELECT *, abuso de DISTINCT, vistas y subconsultas anidadas, y estructuras de dependencias profundas son algunos de los casos más representativos
- La mayoría de estos problemas surgen de soluciones improvisadas por presión de tiempo y de fechas de entrega, y a largo plazo dañan tanto la confiabilidad de los datos como la velocidad de desarrollo
- Como soluciones, se necesitan definiciones claras de joins, uso de tablas de dimensión, eliminación de anidamientos innecesarios y limpieza periódica de vistas
- SQL no debe tratarse como un simple script, sino como código de producción gestionado en equipo, y un diseño inicial orientado a la legibilidad reduce el retrabajo
Introducción
- Hoy se explican varios casos frecuentes y de alto impacto entre los antipatrones de SQL
- Estos problemas provocan un círculo vicioso de menor confiabilidad de los datos y menor velocidad de desarrollo de consultas
- La lista de abajo no cubre todos los casos, y si quieres una comprensión más profunda, se recomienda el libro de Bill Karwin
Uso excesivo de sentencias CASE WHEN complejas
- En sistemas grandes, las sentencias
CASE WHEN se usan con frecuencia para convertir códigos de estado (por ejemplo, 1=sin inventario) a formatos legibles para las personas
- Agregar esa lógica de
CASE WHEN solo en una vista para desarrollar rápido un dashboard o reporte es, a largo plazo, un antipatrón
- Esto provoca lógica duplicada por copiar y pegar, problemas de interpretación inconsistente y vuelve desordenado todo el entorno de consultas
- La solución es crear y reutilizar por separado una tabla de dimensión (dimension table) o una vista compartida que convierta los códigos de estado
Uso de funciones sobre columnas indexadas
- Si aplicas una función a una columna indexada, como en “WHERE UPPER(name) = 'ABC'”, se pierde la eficiencia del índice
- En SQL Server y otros sistemas, esto puede causar un full table scan innecesario
- La solución es indexar por separado la columna transformada o transformar el valor de entrada para simplificar la condición de la consulta
Uso de SELECT * en vistas
- Al desarrollar una vista, usar
SELECT * puede parecer cómodo, pero si cambia la estructura (schema), la vista puede romperse fácilmente
- Además, incluye columnas innecesarias, lo que genera dependencias no intencionales y problemas de rendimiento, por lo que se deben seleccionar las columnas de forma explícita
Abusar de DISTINCT para “resolver” duplicados
- Cuando aparecen resultados duplicados por un join incorrecto, usar
SELECT DISTINCT como solución temporal oculta un problema de integridad de datos
- La causa raíz es una condición de join incompleta o un error en la definición de relaciones (1:1, 1:N, etc.)
- La solución correcta es reforzar la lógica del join para aclarar la definición de las relaciones y garantizar la consistencia relacional antes de agregar o generar reportes
Anidamiento de vistas (Excessive View Layer Stacking)
- Cuando varios equipos reutilizan vistas existentes y siguen apilando nuevas vistas encima, la cadena de dependencias se vuelve compleja y el rendimiento se degrada con rapidez
- Se vuelve difícil depurar, y expandir una consulta llega a sentirse como una “excavación arqueológica”
- Se necesita una estrategia para aplanar (flatten) periódicamente la lógica de transformación y materializar (materialize) operaciones complejas en vistas base o tablas claramente definidas
Subconsultas con profundidad excesiva
- Las subconsultas profundamente anidadas, con más de 3 o 4 niveles, reducen la legibilidad y dificultan la depuración
- Incluso existen casos con subconsultas de más de 5000 líneas
- Usar CTE (Common Table Expression) facilita separar las etapas lógicas y mejora la claridad (readability) de la consulta
Conclusión
- SQL parece simple en la superficie, pero a medida que el sistema crece, su complejidad también aumenta
- La mayoría de los antipatrones no surgen por mala intención, sino como resultado de compromisos para obtener “resultados rápidos” (velocidad, fechas límite, soluciones temporales)
- Si gestionas SQL como código (control de versiones, code review, diseño claro), a largo plazo puedes asegurar al mismo tiempo productividad y confiabilidad
- Invertir unos minutos en el diseño inicial, pensando en la claridad y la consistencia, reduce enormemente el retrabajo y la confusión futura
5 comentarios
Como no hay tiempo ahora, solo apagamos el incendio y decimos que lo reescribiremos después, pero cuando eso se acumula termina convirtiéndose en un terrible infierno de consultas. Yo también he creado varias así, aun sabiendo que ese “después” para reescribirlas nunca llega.
Mmm...
"La mayoría de los problemas son soluciones temporales causadas por la presión de la velocidad y las fechas límite"
Snif...
Opiniones de Hacker News
addresses_citiessolo para evitar que se registren nombres de ciudades duplicadosLIMIT 1a una consulta porque esperaba que como máximo devolviera un resultado. Pero en tablas grandes (ensqlite,mysqlypostgresql) la DB suele seguir recorriendo toda la tabla incluso después de encontrar el registro deseadoSELECT x FROM tse puede omitir DISTINCT de forma segura. Aunque en el esquema detvea quextiene una restricción PRIMARY o UNIQUE, alguien podría quitar esa restricción enseguida. Entonces empezarían a aparecer duplicados y uno se quedaría preguntándose por qué. SQL no es un lenguaje de conjuntos (set), sino de bolsas (bag). En runtime, si encuentra la relaciónty el atributox, simplemente los devuelve. Puede haber duplicados, o hasta cambiar el tipo. Si quieres un Set, tienes que especificar DISTINCT sí o sí. Si en runtime el query planner ve que es UNIQUE o PRIMARY, entonces no hará deduplicaciónhttps://kb.databasedesignbook.com/posts/systematic-design-of-join-queries/
!=oNOT IN (...)casi siempre es ineficiente (aunque puede estar bien si otras condiciones ya redujeron lo suficiente el conjunto de resultados). Y es importante entender cómo manejanullla DB. Sinully la cadena vacía son lo mismo, sinull == null, eso puede variar según la base de datosnulle indexación, las DB que he usado no indexan valoresnull, así que una consulta comoWHERE col IS NULLfunciona de forma ineficiente aunquecoltenga índice. Si de verdad hace falta, recomiendo crear una columnachar(1)obitque indique sicoles null, e indexar ese campo!=oNOT IN (...)casi siempre son ineficientes, pero me da curiosidad por qué. Si el valor del lado derecho es constante, debería resolverse con un lookup en tabla hash y en general ser eficiente. Me pregunto si hay una alternativa más eficientehttps://en.wikipedia.org/wiki/Sargable
https://www.brentozar.com/blitzcache/non-sargable-predicates/
sargable, encontré útil esta respuesta de StackOverflow https://dba.stackexchange.com/a/217983La palabra “sargable” es un portmanteau de “Search ARGument ABLE”
CASE WHENse pueden resolver consolidando la lógica en un UDF (User Defined Function)Usar funciones sobre columnas indexadas es una señal de que la consulta no es sargable
En vez de abusar de DISTINCT, una consulta útil para deduplicar el fanout derivado de joins según el grano de la tabla es algo como Algunas DB soportan una función llamada QUALIFY, y eso deja la consulta mucho más limpia
explicación de sargable
QUALIFY en Redshift
sqlite, yo diría que síwheredistintas para cada tabla y manejar cada vez condiciones como void/devolución/cancelación, así que cuando hay cambios terminarías corrigiendo decenas de views/procedures. En nuestro caso, usar views anidadas es mucho más prácticoUPPER(name)) al menos en MS SQL Server no es la mejor. No sé qué tanto lo soporten otras DB, pero una solución mejor es crear directamente una computed column con COLLATE que no distinga mayúsculas y minúsculas (ajústalo a tu gusto)Falta lo más importante.