Módulo 3: Promptear para SQL correcto
Fijar el dialecto SQLite
Descripción
SQL no es un solo idioma: es una familia de dialectos. SQLite, PostgreSQL, MySQL y SQL Server comparten el núcleo (SELECT, JOIN, GROUP BY), pero difieren en funciones, sintaxis y detalles que rompen una consulta al pasarla de un motor a otro. El modelo vio todos esos dialectos en su entrenamiento y, si no le dices contra cuál trabajas, mezcla —genera DATE_TRUNC('month', ...), que es de PostgreSQL y en SQLite no existe—. Esta cápsula profundiza en la pieza [2] del system prompt: fijar el dialecto. Verás ejecutado el error real que produce una función de otro motor, y la función de SQLite que la reemplaza.
Conexión con el módulo
La cápsula 02 introdujo la pieza [2] (dialecto) en una línea. Esta la desarrolla: por qué el modelo mezcla motores, qué funciones son las trampas frecuentes, y cómo la instrucción de dialecto —reforzada con few-shot— evita el SQL de otro motor. Es la diferencia entre un SQL que corre en tu SQLite y uno que falla con no such function.
Analogía: la misma palabra, otro idioma
En inglés británico, el maletero de un coche es el boot; en inglés estadounidense, el trunk. Si le pides a un mecánico estadounidense que abra el boot, te mira raro: la palabra existe, pero significa otra cosa (una bota). Los dos hablan "inglés", pero un término perfectamente normal en un dialecto es un sinsentido en el otro.
Los dialectos de SQL son iguales. DATE_TRUNC('month', fecha) es una palabra perfectamente normal en PostgreSQL: trunca una fecha al inicio del mes. En SQLite esa palabra no existe: el motor responde no such function: DATE_TRUNC. El modelo, que aprendió de textos en todos los dialectos, a veces te habla en "SQL británico" cuando tu motor solo entiende "SQL estadounidense". Fijar el dialecto es decirle, antes de empezar: "aquí hablamos SQLite; usa sus palabras."
Por qué el modelo mezcla dialectos
El modelo aprendió SQL de millones de ejemplos en internet: tutoriales de PostgreSQL, respuestas de MySQL, documentación de SQL Server, snippets de SQLite. No hay una etiqueta que separe "esto es Postgres" de "esto es SQLite" en su cabeza; hay un continuo de "cosas que se parecen a SQL". Cuando genera, produce la continuación más probable —y si en su entrenamiento DATE_TRUNC apareció mucho junto a "ingreso por mes", puede elegirlo aunque tu motor sea SQLite.
El esquema (M2) no arregla esto: el esquema le dice qué tablas hay, no qué funciones tiene tu motor. La corrección del dialecto es una instrucción aparte, explícita, en el system prompt.
Las trampas frecuentes: SQLite vs. otros motores
Estas son las diferencias que más se cuelan en el SQL generado. La columna "otros motores" es SQL que el modelo puede producir por inercia; la de SQLite es lo correcto para Reservo:
| Necesidad | Otros motores (Postgres/MySQL/SQL Server) | SQLite (lo correcto aquí) |
|---|---|---|
| Truncar fecha a mes | DATE_TRUNC('month', start_at) (PG) | strftime('%Y-%m', start_at) |
| Extraer el mes | EXTRACT(MONTH FROM start_at) (PG) | strftime('%m', start_at) |
| Fecha/hora actual | NOW() (PG/MySQL) | datetime('now') |
| Comparar sin distinguir mayúsculas | name ILIKE 'foc%' (PG) | lower(name) LIKE 'foc%' |
| Limitar filas | SELECT TOP 3 ... (SQL Server) | SELECT ... LIMIT 3 |
| Concatenar texto | CONCAT(a, b) (MySQL) | `a |
Varias de estas no solo dan otro resultado: fallan con error de sintaxis o de función inexistente. Comprobémoslo ejecutándolas contra Reservo.
Ejemplo trabajado: ingreso por mes, con y sin dialecto fijado
La pregunta:
"¿Cuánto ingresó cada mes?"
Sin fijar el dialecto. El system prompt tiene rol, esquema y reglas, pero no dice "esto es SQLite". El modelo, por inercia de Postgres, elige DATE_TRUNC. Supón que claude-sonnet-5 devolvió:
-- SQL generado SIN fijar el dialecto (ejemplo realista, NO ejecutado por el modelo)
SELECT DATE_TRUNC('month', start_at) AS month, SUM(price_cents) AS revenue
FROM bookings
WHERE status = 'confirmed'
GROUP BY month
ORDER BY month;
Con el dialecto fijado ("El motor es SQLite; usa strftime, nunca DATE_TRUNC"). Supón que devolvió:
-- SQL generado CON el dialecto fijado (ejemplo realista)
SELECT strftime('%Y-%m', start_at) AS month, SUM(price_cents) AS revenue
FROM bookings
WHERE status = 'confirmed'
GROUP BY month
ORDER BY month;
Ejecutemos los dos contra Reservo y veamos qué pasa:
import sqlite3
con = sqlite3.connect("reservo.db")
def run(label, sql):
print(f"== {label} ==")
try:
cur = con.execute(sql)
print(" | ".join(d[0] for d in cur.description))
for row in cur.fetchall():
print(" | ".join(str(v) for v in row))
except Exception as e:
print(f"{type(e).__name__}: {e}")
print()
run("sin dialecto (DATE_TRUNC, de Postgres)", """
SELECT DATE_TRUNC('month', start_at) AS month, SUM(price_cents) AS revenue
FROM bookings WHERE status = 'confirmed'
GROUP BY month ORDER BY month
""")
run("con dialecto (strftime, de SQLite)", """
SELECT strftime('%Y-%m', start_at) AS month, SUM(price_cents) AS revenue
FROM bookings WHERE status = 'confirmed'
GROUP BY month ORDER BY month
""")
con.close()
Qué esperar:
== sin dialecto (DATE_TRUNC, de Postgres) ==
OperationalError: no such function: DATE_TRUNC
== con dialecto (strftime, de SQLite) ==
month | revenue
2026-01 | 47100
2026-02 | 36900
2026-03 | 55400
2026-04 | 27600
2026-05 | 28900
2026-06 | 16000
El SQL sin dialecto ni siquiera corre: SQLite no conoce DATE_TRUNC y lanza no such function. El SQL con el dialecto fijado responde el ingreso de los seis meses. Aquí el error de dialecto es del tipo "ruidoso" —falla en voz alta—, que es mejor que el error silencioso de un número equivocado. Pero un asistente que devuelve SQL que no corre es un asistente roto; fijar el dialecto lo evita de raíz.
Más funciones de otro motor, ejecutadas
DATE_TRUNC no es la única trampa. Veamos otras tres fallar, para que reconozcas el patrón:
import sqlite3
con = sqlite3.connect("reservo.db")
def run(label, sql):
print(f"== {label} ==")
try:
cur = con.execute(sql)
print(cur.fetchall())
except Exception as e:
print(f"{type(e).__name__}: {e}")
run("EXTRACT (Postgres)", "SELECT EXTRACT(MONTH FROM start_at) FROM bookings LIMIT 3")
run("NOW (Postgres/MySQL)", "SELECT NOW()")
run("ILIKE (Postgres)", "SELECT name FROM rooms WHERE name ILIKE 'foc%'")
run("TOP (SQL Server)", "SELECT TOP 3 name FROM rooms")
con.close()
Qué esperar:
== EXTRACT (Postgres) ==
OperationalError: near "FROM": syntax error
== NOW (Postgres/MySQL) ==
OperationalError: no such function: NOW
== ILIKE (Postgres) ==
OperationalError: near "ILIKE": syntax error
== TOP (SQL Server) ==
OperationalError: near "3": syntax error
Cuatro dialectos ajenos, cuatro fallos. EXTRACT e ILIKE y TOP fallan con error de sintaxis (SQLite no reconoce la construcción); NOW falla con función inexistente. Cada uno tiene su reemplazo en SQLite: strftime('%m', ...), lower(name) LIKE ..., LIMIT 3. La instrucción de dialecto en el system prompt es lo que empuja al modelo hacia el reemplazo correcto.
Cómo fijar el dialecto en el prompt (y reforzarlo con few-shot)
Dos capas, que se combinan:
Capa 1 — la instrucción explícita (pieza [2] del system prompt). Nombra el motor, su versión, y las trampas concretas:
El motor es SQLite 3.50. Usa exclusivamente funciones de SQLite:
- Para fechas: strftime(...) y date(...). NUNCA DATE_TRUNC, EXTRACT ni NOW.
- Para limitar filas: LIMIT n. NUNCA TOP n.
- Para comparar sin distinguir mayúsculas: lower(col) LIKE '...'. NUNCA ILIKE.
No uses sintaxis de PostgreSQL, MySQL ni SQL Server.
Nombrar las trampas por su nombre (DATE_TRUNC, EXTRACT, NOW) es más efectivo que un genérico "usa SQLite": le das al modelo la lista exacta de palabras a evitar.
Capa 2 — el refuerzo por ejemplo (few-shot). Como viste en la cápsula 03, un ejemplo enseña por imitación. Un ejemplo few-shot cuyo SQL usa strftime('%Y-%m', ...) refuerza el dialecto mejor que cualquier prohibición: el modelo copia el patrón de SQLite que acaba de ver. Instrucción + ejemplo es la combinación robusta.
Un matiz de honestidad: incluso con las dos capas, el modelo puede colarte una función de otro motor de vez en cuando. Por eso M4 valida el SQL antes de correrlo —y un SQL que usa DATE_TRUNC falla ruidosamente al ejecutarse, lo que M4 aprovecha para el loop de auto-corrección—. Fijar el dialecto reduce la frecuencia del error; validar lo atrapa cuando se cuela.
Por qué esto importa más allá de SQLite
Toda esta guía usa SQLite, pero el problema del dialecto es universal. Si tu asistente corre sobre PostgreSQL en producción, la pieza [2] cambia de contenido pero no de idea: le dirías "usa DATE_TRUNC, no strftime", porque en Postgres strftime no existe. Lo que se mantiene es la disciplina: nombrar el motor y sus funciones en el system prompt, porque el modelo, librado a su inercia, mezcla.
Es más: si tu asistente debe soportar varios motores, el dialecto pasa a ser un parámetro del prompt —generas el bloque [2] según el motor de destino—. El patrón (schema-as-context + dialect-pinning + reglas) es idéntico; solo cambian las palabras del dialecto.
Errores comunes
-
Asumir que "sabe SQL" implica "sabe tu dialecto". El modelo sabe SQL en general, que es justo el problema: sabe demasiados dialectos y los mezcla. Hay que decirle cuál usar.
-
Un "usa SQLite" genérico. Ayuda poco. Nombra las trampas concretas:
DATE_TRUNC,EXTRACT,NOW,TOP,ILIKE. La lista específica de palabras a evitar es mucho más efectiva. -
Olvidar reforzar con ejemplos. La instrucción sola es más débil que instrucción + few-shot. Un ejemplo que usa
strftimeenseña el dialecto por imitación. -
Creer que el error de dialecto siempre es ruidoso.
DATE_TRUNCfalla en voz alta, sí. Pero algunas diferencias de dialecto dan otro resultado sin fallar: en SQLite la división de dos enteros con/puede truncar, y ciertas funciones de fecha aceptan formatos distintos. No todos los errores de dialecto se ven; por eso M4 valida y M7 evalúa. -
Fijar un dialecto y olvidar la versión. Las funciones cambian entre versiones del motor.
CONCATllegó a SQLite en la 3.44; funciones nuevas no existen en instalaciones viejas. Nombrar la versión (SQLite 3.50) ancla el modelo a lo disponible.
Ejercicios
Ejercicio 1: Traducir del dialecto ajeno (Fácil)
El modelo generó SELECT name FROM rooms WHERE name ILIKE 'stu%' (Postgres). Reescríbelo en SQLite y córrelo.
Ver solución
import sqlite3
con = sqlite3.connect("reservo.db")
cur = con.execute("SELECT name FROM rooms WHERE lower(name) LIKE 'stu%'")
print(cur.fetchall())
con.close()
Salida esperada:
[('Studio',)]
Explicación: ILIKE (comparación sin distinguir mayúsculas) no existe en SQLite y falla con error de sintaxis. El reemplazo idiomático es bajar la columna a minúsculas con lower(name) y comparar con LIKE. Devuelve Studio, la única sala que empieza por "stu".
Ejercicio 2: El error de EXTRACT (Medio)
Ejecuta SELECT EXTRACT(YEAR FROM start_at) FROM bookings LIMIT 1 contra Reservo, observa el error, y escribe la versión SQLite que extrae el año.
Ver solución
import sqlite3
con = sqlite3.connect("reservo.db")
try:
con.execute("SELECT EXTRACT(YEAR FROM start_at) FROM bookings LIMIT 1").fetchall()
except Exception as e:
print("falla:", type(e).__name__, e)
cur = con.execute("SELECT strftime('%Y', start_at) FROM bookings LIMIT 1")
print("SQLite:", cur.fetchone()[0])
con.close()
Salida esperada:
falla: OperationalError near "FROM": syntax error
con: 2026
Explicación: EXTRACT(YEAR FROM ...) es sintaxis de PostgreSQL; SQLite no la reconoce y falla al llegar al FROM interno. El equivalente en SQLite es strftime('%Y', start_at), que devuelve el año como texto ('2026'). Nota que strftime siempre devuelve texto —si necesitas un entero, envuélvelo en CAST(... AS INTEGER)—.
Ejercicio 3: Escribir la pieza [2] para una pregunta con fecha actual (Difícil)
Un usuario pregunta "¿cuántas reservas hay a partir de hoy?". Un modelo sin dialecto fijado devuelve SELECT COUNT(*) FROM bookings WHERE start_at >= NOW(). Explica por qué falla, escribe la versión SQLite, y redacta la línea de la pieza [2] que lo habría evitado.
Ver solución
import sqlite3
con = sqlite3.connect("reservo.db")
try:
con.execute("SELECT COUNT(*) FROM bookings WHERE start_at >= NOW()").fetchall()
except Exception as e:
print("falla:", type(e).__name__, e)
# Versión SQLite (datetime('now') da la fecha/hora UTC actual como texto ISO)
cur = con.execute("SELECT COUNT(*) FROM bookings WHERE start_at >= datetime('now')")
print("SQLite corre; filas a partir de hoy:", cur.fetchone()[0])
con.close()
Salida esperada (el conteo depende de la fecha en que lo corras; lo relevante es que corre):
falla: OperationalError no such function: NOW
SQLite corre; filas a partir de hoy: 0
Explicación: NOW() es de Postgres/MySQL; en SQLite no existe y falla con no such function. El equivalente es datetime('now'), que devuelve la fecha/hora UTC actual como texto ISO —comparable directamente con start_at, que es texto ISO—. Como todas las reservas de Reservo son de 2026 y hoy es una fecha posterior a las del seed, el conteo da 0 (ninguna reserva futura respecto a hoy), pero lo importante es que la consulta corre. La línea de la pieza [2] que lo evita:
Para la fecha/hora actual usa datetime('now'). NUNCA NOW().
Resumen y siguiente paso
- SQL es una familia de dialectos. El modelo aprendió todos y los mezcla si no le fijas cuál usar. El esquema (M2) no arregla esto: dice qué tablas hay, no qué funciones tiene tu motor.
- Ejecutado:
DATE_TRUNC('month', ...)(Postgres) falla en SQLite conno such function;strftime('%Y-%m', ...)responde el ingreso de los seis meses.EXTRACT,NOW,ILIKEyTOPtambién fallan; cada uno tiene su reemplazo SQLite. - Se fija en dos capas: la instrucción explícita (pieza [2] del system prompt, nombrando las trampas por su nombre) y el refuerzo por few-shot (ejemplos que usan
strftime,LIMIT). - El problema es universal: en Postgres fijarías lo contrario (
DATE_TRUNC, nostrftime). El patrón no cambia; cambian las palabras del dialecto. Fijar el dialecto reduce el error; M4 lo valida cuando se cuela.
Siguiente cápsula: Instrucciones-restricción y formato — Desarrollaremos la pieza [4]: "genera solo una sentencia SELECT", "usa solo estas tablas", "responde solo el SQL". Verás por qué restringir la forma de la salida hace que parsearla y validarla después sea trivial.
Recursos adicionales
- SQLite — Date and time functions —
strftime,date,datetime: los reemplazos deDATE_TRUNC/EXTRACT/NOW. - SQLite — Core functions —
lower,||,CAST: el vocabulario de SQLite que fijamos como dialecto. - SQLite — Query language: SELECT (LIMIT clause) —
LIMIT, el reemplazo deTOP. - Claude — Giving Claude a role with a system prompt — Dónde vive la instrucción de dialecto (la pieza [2]).