54 puntos por GN⁺ 2025-10-19 | 5 comentarios | Compartir por WhatsApp
  • 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

 
aer0700 2025-10-20

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.

 
firefoxsaiko123 2025-10-20

Mmm...

 
ilikeall 2025-10-20

"La mayoría de los problemas son soluciones temporales causadas por la presión de la velocidad y las fechas límite"
Snif...

 
GN⁺ 2025-10-19
Opiniones de Hacker News
  • Cuando veo que se usa DISTINCT en una consulta, sospecho que quien la escribió no entiende bien el modelo de datos o la teoría de conjuntos, o quizá ninguna de las dos
    • A veces DISTINCT también puede ser señal de un esquema excesivamente normalizado. Por ejemplo, no creo que haga falta crear una tabla como addresses_cities solo para evitar que se registren nombres de ciudades duplicados
    • Mi experiencia ha sido casi igual. Pero recientemente hubo un caso donde, aunque todos los joins estaban bien hechos, meter DISTINCT dentro de un CTE mejoró mucho el rendimiento. Parece que el query planner optimiza cuando se garantiza la unicidad de los registros
    • Me dijeron algo parecido, que no era buena práctica, cuando agregué LIMIT 1 a una consulta porque esperaba que como máximo devolviera un resultado. Pero en tablas grandes (en sqlite, mysql y postgresql) la DB suele seguir recorriendo toda la tabla incluso después de encontrar el registro deseado
    • Pregunto cómo saber si en una consulta SELECT x FROM t se puede omitir DISTINCT de forma segura. Aunque en el esquema de t vea que x tiene 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ón t y el atributo x, 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ón
    • En Cypher es más bien al revés. Al trabajar con datos complejos en neo4j, es facilísimo que entren nodos duplicados en el resultado, así que DISTINCT es indispensable. Sobre todo cuando usas relaciones de longitud variable: sin DISTINCT se vuelve más lento y aparecen muchos duplicados
  • Escribí un tutorial de unas 9000 palabras, en dos partes, sobre cómo diseñar la estructura correcta de consultas sin usar DISTINCT
    https://kb.databasedesignbook.com/posts/systematic-design-of-join-queries/
    • Buen artículo. Lo guardé en marcadores. Y luego me di cuenta de que en realidad también es un libro
  • Algo que casi no se menciona son las consultas para encontrar “lo que no existe”. Por ejemplo, usar != o NOT 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 maneja null la DB. Si null y la cadena vacía son lo mismo, si null == null, eso puede variar según la base de datos
    • Sobre null e indexación, las DB que he usado no indexan valores null, así que una consulta como WHERE col IS NULL funciona de forma ineficiente aunque col tenga índice. Si de verdad hace falta, recomiendo crear una columna char(1) o bit que indique si col es null, e indexar ese campo
    • Dijiste que != o NOT 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 eficiente
  • No creo que todos los “antipatrones” que se presentan sean realmente antipatrones. El problema de que las condiciones de una consulta no calcen con los índices al final viene de no entender bien cómo funcionan los índices. Y muchos de los problemas que se mencionan aquí están más relacionados con el diseño del esquema de la base de datos que con SQL como tal. Si hace falta DISTINCT, puede que el diseño de la primary key no sea el correcto. Si terminas apilando demasiadas views, probablemente el diseño de las tablas base está mal. Un buen modelado de DB previene todos estos problemas desde el principio
  • Estos “antipatrones” en realidad no son más que soluciones de compromiso por las limitaciones de diseño de SQL (o por cosas que nunca se diseñaron). Estoy creando un lenguaje nuevo que funciona sobre bases de datos SQL, y quiero ofrecer mejores alternativas para cada uno de estos problemas. Todavía está incompleto y le falta documentación, pero si a alguien le interesa me gustaría recibir feedback en https://lutra-lang.org
    • La expresión “bases de datos SQL” es ambigua. SQL se ha implementado no solo en DB relacionales sino también en algunas no relacionales. Los expertos ya reconocían los problemas de SQL desde hace mucho, y hubo alternativas como Tutorial D de Chris Date y Hugh Darwen. Aun así, por todo el código SQL y las herramientas acumuladas durante décadas, los reemplazos no han logrado despegar. Gracias a SQL he tenido estabilidad laboral e ingresos constantes durante décadas, así que aunque hace falta un lenguaje mejor, por otro lado veo esta situación con buenos ojos
    • El proyecto se ve bien. Si llega a estar más maduro, definitivamente lo seguiré
  • El mayor antipatrón es no reconocer que SQL no es solo un lenguaje de consultas, sino un lenguaje de programación de verdad. Recomiendo mantener un estilo de código consistente con la indentación y agrupar juntas las partes lógicamente relacionadas. Conviene convertir subconsultas en CTE. También es importante dejar comentarios de forma efectiva. Mi estilo de referencia: https://bentilly.blogspot.com/2011/02/sql-formatting-style.html
    • Creo que estas discusiones sobre estilo de código casi no tienen sentido si no hay una herramienta de linting adecuada
  • El mayor truco que me ha servido para acelerar mis consultas y reducir el uso de recursos del servidor es hacerlas más sargable
    https://en.wikipedia.org/wiki/Sargable
    https://www.brentozar.com/blitzcache/non-sargable-predicates/
    • Me pregunto en qué comunidades se usa realmente la palabra “sargable”. Llevo más de 20 años trabajando con SQL, pero casi nunca la he visto en manuales, Stack Overflow o HN. Me da curiosidad si se usa mucho en alguna DB, empresa o comunidad open source en particular
    • Buscando la etimología de sargable, encontré útil esta respuesta de StackOverflow https://dba.stackexchange.com/a/217983
      La palabra “sargable” es un portmanteau de “Search ARGument ABLE”
  • Muchos problemas por abusar de CASE WHEN se 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
    ROW_NUMBER() OVER (PARTITION BY <grain> ORDER BY <deterministic sort>) = 1
    
    Algunas DB soportan una función llamada QUALIFY, y eso deja la consulta mucho más limpia
    explicación de sargable
    QUALIFY en Redshift
    • Los problemas por no ser sargable se pueden resolver fácilmente con un expression index. Al menos en sqlite, yo diría que sí
  • También hay casos donde sí hace falta anidar views. En nuestro software POS usamos muchas views anidadas para crear una view backbone que permita ver las transacciones de forma clara y completa. Si no, habría que escribir cláusulas where distintas 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áctico
  • El problema de usar funciones en columnas indexadas necesita una explicación más clara. Si aplicas una función a una columna indexada, la eficiencia del índice desaparece y en la práctica termina ocurriendo un full scan, así que se vuelve lento. Eso lo aprendí viviéndolo directamente
    • Hay una referencia conocida sobre esto https://use-the-index-luke.com/sql/where-clause/obfuscation
    • La solución propuesta (por ejemplo, agregar un índice sobre una columna UPPER(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
      ALTER TABLE example ADD name_ci AS name COLLATE SQL_Latin1_General_CI_AS;
      
      (ajústalo a tu gusto)
    • El blog relacionado tiene un typo. La primera línea debería ir en mayúsculas. Si ya tienes el índice puesto sobre los datos con la función aplicada, entonces al consultar no hay full scan. Claro, en este ejemplo desde el principio habría sido mejor usar una collation case-insensitive, pero en general la idea sí es válida
    • “Lo aprendí viviéndolo directamente” parece el lema de los desarrolladores SQL. De todos modos, SQL ha cambiado de forma estable durante bastante tiempo, así que conocer estas trampas de antemano sigue siendo útil por muchos años
 
ahwjdekf 2025-10-21

Falta lo más importante.

  • usar ORM