SQL generado por IA: por qué falla en silencio

Leonardo Gurgitano7 minLeer en inglés

Filas de asientos idénticos que se repiten hacia el fondo, en blanco y negro.
Filas de asientos idénticos que se repiten hacia el fondo, en blanco y negro.

Cuando un modelo de lenguaje escribe código en casi cualquier lenguaje, algo detecta el error antes de que llegue a producción: el compilador rechaza lo que no tipa, la prueba falla, el programa lanza una excepción.

Con SQL esa red no existe. Una consulta mal escrita se ejecuta sin problemas y devuelve filas. Las filas parecen razonables. El número que sale en el informe también. Nadie se entera hasta que alguien compara ese número con otra fuente, semanas después.

Esto no es un argumento para no usar modelos de lenguaje con SQL: ahorran mucho tiempo, sobre todo en consultas largas de las que uno sabe qué quiere pero no recuerda la sintaxis exacta. Es un argumento para verificarlas de otra manera.

Los dos errores que más se repiten

No son casos raros. Son los dos que aparecen una y otra vez, y los dos se ven perfectamente correctos al leerlos.

El JOIN que multiplica las filas

Tenés pedidos, y cada pedido tiene varios artículos. Querés el total facturado por cliente:

SELECT   o.customer_id,
         SUM(o.total) AS facturado
FROM     orders o
JOIN     order_items i ON i.order_id = o.id
GROUP BY o.customer_id;

Se ejecuta. Devuelve un cliente por fila y un número en cada uno. Y está mal.

El JOIN produce una fila por cada artículo. Un pedido de 300 pesos con tres artículos aparece tres veces, así que SUM(o.total) suma 900. El total del informe queda inflado en proporción a la cantidad de artículos por pedido — un factor que además varía entre clientes, así que el ranking también sale mal.

El JOIN con order_items ni siquiera hacía falta para esta pregunta. Es lo que suele pasar: se mencionan los artículos al describir el problema, y aparecen en la consulta.

El NOT IN que se anula con un solo NULL

Querés los clientes que nunca compraron:

SELECT * FROM customers
WHERE  id NOT IN (SELECT customer_id FROM orders);

Si una sola fila de orders tiene customer_id en NULL, esta consulta devuelve cero filas siempre. No un resultado incompleto: cero, en todos los casos.

El motivo es cómo funciona NULL en SQL. id NOT IN (1, 2, NULL) se evalúa como id <> 1 AND id <> 2 AND id <> NULL, y esa última comparación no da verdadero ni falso: da desconocido. Y verdadero AND desconocido es desconocido, que no pasa el filtro.

La versión que no tiene ese problema:

SELECT   c.* FROM customers c
WHERE    NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);

Un modelo de lenguaje escribe la primera versión con frecuencia, porque es la que más aparece en el material del que aprendió. Y “cero clientes sin compras” es un resultado que puede sonar plausible.

Cinco comprobaciones antes de darla por buena

Ninguna lleva más de un minuto, y detectan la mayor parte de estos casos.

1. Contá las filas antes y después del JOIN

Es la comprobación más rentable de todas, y sirve exactamente para el primer error:

SELECT COUNT(*) FROM orders;                                  -- 12.480
SELECT COUNT(*) FROM orders o JOIN order_items i ON i.order_id = o.id;  -- 41.902

Si el número crece, el JOIN está multiplicando filas. Eso no siempre está mal —a veces es justo lo que querés—, pero si después hay un SUM o un AVG sobre una columna de la tabla izquierda, entonces está mal seguro.

2. Pedile a la base el plan, y leé las filas reales

EXPLAIN ANALYZE SELECT ...;

EXPLAIN a secas muestra lo que la base estima. Con ANALYZE ejecuta la consulta y muestra lo que pasó de verdad. Lo importante no es el costo: es comparar las filas estimadas con las reales en cada paso. Una diferencia de varios órdenes de magnitud indica que la base está trabajando con supuestos equivocados, y suele ser la causa de que una consulta tarde de más.

En SQL Server el equivalente es SET STATISTICS PROFILE ON o el plan de ejecución real.

3. Toda escritura, dentro de una transacción que revertís

Para cualquier UPDATE o DELETE que no escribiste vos entero:

BEGIN;
UPDATE orders SET status = 'cancelled' WHERE ...;
-- ¿Cuántas filas dice que afectó? ¿Es el número que esperabas?
ROLLBACK;   -- COMMIT solo si el número cierra

El recuento de filas afectadas es la comprobación más directa que existe. Un WHERE con una condición de más devuelve cero; uno con una de menos devuelve todas. Los dos son evidentes en ese número, y ninguno de los dos lanza un error.

4. Dale el esquema, no lo dejes suponer

Casi todos los errores de columnas y relaciones vienen de que el modelo tuvo que adivinar la estructura. Pegar la definición de las tablas involucradas cuesta diez segundos:

-- PostgreSQL: \d orders    en psql
-- SQL Server:
SELECT column_name, data_type, is_nullable
FROM   information_schema.columns
WHERE  table_name IN ('orders', 'order_items');

El dato que más cambia el resultado es cuáles columnas admiten NULL, que es justo lo que nunca se menciona al describir el problema y lo que provoca el segundo error de arriba.

5. Verificá contra una consulta simple que sí entendés

Si la consulta generada es larga, comprobá una parte contra algo que puedas razonar de memoria:

-- La consulta compleja dice que el cliente 42 facturó 18.400
SELECT SUM(total) FROM orders WHERE customer_id = 42;  -- ¿coincide?

Un solo caso conocido descarta la mayoría de los errores estructurales.

Lo que sí conviene delegar

Con esas comprobaciones, hay trabajo donde un modelo de lenguaje rinde muy bien y el riesgo es bajo:

  • Traducir entre dialectos. De la sintaxis de SQL Server a la de PostgreSQL, o al revés. Es un trabajo mecánico y tedioso donde los errores se ven al ejecutar.
  • Explicar una consulta heredada. Pedirle que describa en texto qué hace una consulta de doscientas líneas que nadie recuerda. Verificás leyendo, no ejecutando.
  • Escribir la ventana o el CTE recursivo que sabés que existe pero cuya sintaxis exacta nunca recordás.
  • Generar datos de prueba que respeten las restricciones del esquema.

Lo nuevo: búsqueda semántica dentro de la base

Hay un cambio reciente que vale la pena conocer, porque elimina una pieza completa de infraestructura.

Hasta hace poco, buscar por significado —encontrar documentos parecidos a una pregunta, no que contengan las mismas palabras— requería una base de datos especializada aparte, con su propia sincronización y su propio despliegue.

Eso cambió. PostgreSQL 18 incorporó el tipo vectorial y los índices HNSW e IVFFlat al motor, junto a los índices tradicionales; antes había que instalar la extensión pgvector. SQL Server 2025 trae un tipo VECTOR(n) nativo y funciones como VECTOR_DISTANCE, con un índice aproximado integrado al optimizador.

La técnica consiste en convertir cada texto en una lista de números que representa su significado —lo que se llama un embedding— y buscar los que están más cerca entre sí.

Pero lo interesante no es la búsqueda semántica en sí, que ya existía. Es que ahora se combina con filtros relacionales en una sola consulta:

SELECT   d.titulo,
         d.embedding <=> :consulta AS distancia
FROM     documentos d
JOIN     proyectos p ON p.id = d.proyecto_id
WHERE    p.cliente_id = 42          -- filtro relacional de siempre
  AND    d.publicado_en > '2026-01-01'
  AND    d.estado = 'publicado'
ORDER BY distancia                   -- ordenado por parecido semántico
LIMIT    10;

Con una base vectorial separada, esto son dos consultas y un cruce en el código de la aplicación: traer los cien más parecidos, filtrar por permisos y fechas, y esperar que queden diez. Acá el motor resuelve todo junto, con las restricciones de integridad y los permisos que ya tenías.

Antes de agregar una base de datos vectorial a tu sistema, conviene comprobar si la que ya usás alcanza. Para volúmenes medianos, casi siempre alcanza.

En resumen

SQL es el peor lenguaje para aceptar código generado sin revisar, porque es el único donde el error no se manifiesta: la consulta se ejecuta, devuelve filas verosímiles, y el problema aparece semanas después en un informe que no cierra.

Las dos comprobaciones que más devuelven por el tiempo que cuestan: contar las filas antes y después de cada JOIN, y envolver toda escritura en una transacción que revertís hasta que el recuento de filas afectadas sea el que esperabas.

Comentarios