Avanzado 13 minSQL

Text-to-SQL en local: consultar tu base de datos en lenguaje naturel

El text-to-SQL con un LLM local te permite hacer una pregunta en francés («¿cuál fue la facturación por región el mes pasado?») y obtener una consulta SQL ejecutable en tu PostgreSQL o MySQL, sin que el esquema ni los datos salgan de tu infraestructura. Esta guía cubre el funcionamiento real: introducir el esquema en el contexto, construir un pipeline Python con Ollama y, sobre todo, establecer las salvaguardas (solo lectura, validación, límites) sin las cuales ningún sistema text-to-SQL puede desplegarse en producción.

Por Mohamed Meguedmi·Actualización 2026-08-27·Probado en Windows, macOS y Linux

#¿Por qué hacer text-to-SQL con un LLM local?

Las soluciones de text to SQL en la nube (asistentes de BI, copilotos de data warehouse) envían tu esquema —nombres de tablas, de columnas y, a veces, muestras de filas— a un servidor de terceros. Para una base de datos de clientes, de recursos humanos o financiera, esto suele ser inaceptable: el esquema por sí solo ya revela la estructura de tu actividad, y las muestras contienen datos personales.

Un LLM local resuelve este problema de raíz: el modelo se ejecuta en tu máquina mediante Ollama, el esquema permanece en la memoria local y la consulta generada se ejecuta sobre tu base de datos sin que ningún byte pase por internet. Además, su uso es gratuito y no depende de ningún límite de tasa de solicitudes de una API.

Confidencialidad
El esquema y los datos nunca abandonan tu red, lo que facilita el cumplimiento del RGPD y la protección de los secretos empresariales.
Coste
Sin costo por consulta. Un analista de datos puede iterar cientos de veces sin factura.
Accesibilidad
Usuarios profesionales que no conocen SQL consultan la base en lenguaje natural.
Control
Tú eliges el modelo, el prompt y las medidas de protección: no hay una caja negra remota.
!
El text-to-SQL no es mágico
Un LLM genera SQL plausible, pero no garantiza que sea correcto. En esquemas complejos (múltiples uniones JOIN, columnas ambiguas), la tasa de error sigue siendo real. Trata la salida como una propuesta que debe validarse, nunca como una fuente de verdad, sobre todo si una persona sin conocimientos técnicos se basa en ella para tomar decisiones.

#¿Cómo funciona concretamente?

El kit Copiloto Local

Esta guía te lleva al modelo. El kit te lleva al copiloto que programa en tu editor.

  • Espacio en línea de por vida
  • PDF + archivos
  • Actualizaciones de por vida

El principio del texto a SQL con un LLM se reduce a tres pasos. Primero se describe el esquema de la base de datos al modelo (el DDL de las tablas relevantes). Luego se le envía la pregunta del usuario con una instrucción estricta: generar únicamente una consulta SQL para el dialecto objetivo. Por último, se recupera la consulta, se valida y se ejecuta en modo de solo lectura.

  1. 01
    Introspección del esquema
    Se extrae la estructura de las tablas (columnas, tipos, claves) de la base de datos, automáticamente en lugar de a mano, para mantener la sincronización.
  2. 02
    Construcción del prompt
    Se crea un prompt de sistema que incluye el dialecto SQL, el esquema pertinente y las reglas (solo SELECT, LIMIT obligatorio, sin comentarios).
  3. 03
    Generación
    El LLM local devuelve una consulta. La limpiamos (eliminación de los posibles delimitadores Markdown ```sql).
  4. 04
    Validación + ejecución
    Se verifica que sea realmente un SELECT, se ejecuta mediante un rol de base de datos de solo lectura y se devuelven las filas.

#Prerrequisitos

Ollama instalado
El demonio debe escuchar en http://localhost:11434. Verifica con «ollama ps».
Un modelo capaz
Un modelo de código reciente (Qwen3-Coder 30B-A3B, Devstral 24B) da resultados mucho mejores en SQL que un pequeño generalista (ver la sección modelos).
Python 3.10+
Con el cliente de base de datos adecuado: psycopg2-binary (PostgreSQL) o PyMySQL (MySQL).
Acceso a la base de datos de solo lectura
Idealmente, un rol SQL específico que solo permita ejecutar SELECT: la medida de protección más importante.
Terminal
# Récupérer un modèle adapté au SQL
ollama pull qwen3-coder:30b

# Dépendances Python
pip install ollama psycopg2-binary sqlparse

#Proporcionar al modelo el esquema de tu base de datos

Es el paso que determina un 80 % de la calidad del resultado. El modelo no puede generar una consulta correcta si no conoce los nombres exactos de las tablas y columnas, sus tipos y las relaciones entre ellas. Dos enfoques: pegar el DDL bruto o introspeccionar la base para construir una descripción compacta.

Para una base de datos pequeña (menos de unas veinte tablas), se puede introducir todo. Por encima de ese tamaño, el esquema supera el contexto útil y abruma al modelo: entonces hay que seleccionar las tablas pertinentes para la pregunta (mediante una primera fase de búsqueda o un mapeo del dominio de negocio). A continuación se muestra una introspección de PostgreSQL que genera un esquema legible para el LLM.

schema.py
import psycopg2

def get_schema(conn):
    """Retourne le schéma sous forme de CREATE TABLE simplifiés."""
    query = """
        SELECT table_name, column_name, data_type
        FROM information_schema.columns
        WHERE table_schema = 'public'
        ORDER BY table_name, ordinal_position;
    """
    tables = {}
    with conn.cursor() as cur:
        cur.execute(query)
        for table, col, dtype in cur.fetchall():
            tables.setdefault(table, []).append(f"{col} {dtype}")

    lines = []
    for table, cols in tables.items():
        cols_str = ", ".join(cols)
        lines.append(f"TABLE {table} ({cols_str});")
    return "\n".join(lines)
→
Añade comentarios de negocio
Una columna «ca_ht» resulta ambigua para el modelo. Enriquece el esquema con anotaciones: «ca_ht (facturación sin impuestos, en euros)». Estas pocas palabras reducen drásticamente los errores de selección de columnas. En PostgreSQL, los comentarios COMMENT ON COLUMN se pueden recuperar mediante information_schema y pg_description.

#Pipeline Python completo con Ollama

Aquí tienes un pipeline mínimo pero funcional: esquema → prompt → generación → limpieza → validación → ejecución. Utiliza el cliente oficial de Python de Ollama y un rol de base de datos de solo lectura.

text_to_sql.py
import re
import ollama
import psycopg2
import sqlparse

MODEL = "qwen3-coder:30b"

SYSTEM_PROMPT = """Tu es un expert PostgreSQL. Génère UNE seule requête SQL
qui répond à la question de l'utilisateur, en respectant ces règles :
- Uniquement des requêtes SELECT (jamais INSERT/UPDATE/DELETE/DROP).
- Utilise exactement les noms de tables et colonnes du schéma fourni.
- Ajoute toujours LIMIT 100 si la question ne précise pas de limite.
- Réponds UNIQUEMENT avec le SQL, sans explication ni balise Markdown.

Schéma de la base :
{schema}"""

def generate_sql(question, schema):
    resp = ollama.chat(
        model=MODEL,
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT.format(schema=schema)},
            {"role": "user", "content": question},
        ],
        options={"temperature": 0},  # déterminisme : crucial pour du SQL
    )
    return clean_sql(resp["message"]["content"])

def clean_sql(raw):
    # Retire les fences Markdown ```sql ... ``` si le modèle en ajoute
    raw = re.sub(r"```(?:sql)?", "", raw).strip()
    return raw.rstrip(";") + ";"

La parte de ejecución separa deliberadamente la validación de la llamada a la base de datos. Se rechaza todo lo que no sea un único SELECT antes incluso de abrir el cursor.

text_to_sql.py (continuación)
def is_read_only(sql):
    statements = sqlparse.parse(sql)
    if len(statements) != 1:
        return False  # une seule requête, pas d'empilement
    stmt = statements[0]
    if stmt.get_type() != "SELECT":
        return False
    forbidden = ("insert", "update", "delete", "drop",
                 "alter", "truncate", "grant", "create")
    lowered = sql.lower()
    return not any(kw in lowered for kw in forbidden)

def run_query(sql):
    if not is_read_only(sql):
        raise ValueError(f"Requête refusée (non lecture seule) : {sql}")
    # Rôle 'readonly' : ne dispose QUE du privilège SELECT côté base
    conn = psycopg2.connect(
        dbname="analytics", user="readonly",
        password="...", host="localhost",
    )
    with conn.cursor() as cur:
        cur.execute("SET statement_timeout = '5s';")  # anti-requête folle
        cur.execute(sql)
        cols = [d[0] for d in cur.description]
        rows = cur.fetchall()
    conn.close()
    return cols, rows

if __name__ == "__main__":
    from schema import get_schema
    ro = psycopg2.connect(dbname="analytics", user="readonly",
                          password="...", host="localhost")
    schema = get_schema(ro)
    question = "Combien de commandes par mois en 2025 ?"
    sql = generate_sql(question, schema)
    print("SQL généré :", sql)
    cols, rows = run_query(sql)
    print(cols)
    for r in rows:
        print(r)
i
temperature = 0
Para text-to-SQL, siempre ajusta la temperatura a 0. No queremos creatividad: queremos la consulta más probable y reproducible. Una temperatura alta introduce variaciones de columnas y joins que hacen fallar la ejecución.

#Mejorar la fiabilidad y la seguridad del SQL generado

Esta es la sección que distingue una demostración de un despliegue real. Un LLM puede generar una consulta destructiva si se le pide, o por accidente mediante una inyección en la pregunta. La defensa nunca debe basarse únicamente en el prompt: debe aplicarse en profundidad, en la base de datos.

  1. 01
    Rol de base de datos de solo lectura (defensa principal)
    Crea un rol SQL que tenga SOLO el privilegio SELECT. Aunque el modelo genere un DROP TABLE, la base lo rechaza. Este es el único mecanismo realmente confiable: 'GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;' y nada más.
  2. 02
    Validación en la aplicación
    Antes, analiza el SQL con sqlparse y descarta todo lo que no sea una única sentencia SELECT. Doble barrera junto con el rol de la base de datos.
  3. 03
    Tiempo límite de la consulta
    SET statement_timeout impide que una consulta mal formada (producto cartesiano sobre millones de filas) sature la base de datos.
  4. 04
    LIMIT forzado
    Impón un LIMIT tanto en el prompt como en el código para no cargar nunca tablas completas en memoria.
  5. 05
    Bucle de corrección
    Si la ejecución devuelve un error SQL, envía el mensaje de error al modelo y pide una consulta corregida (1 o 2 intentos máximo).
Rol de PostgreSQL de solo lectura
-- À exécuter une fois par un admin
CREATE ROLE readonly WITH LOGIN PASSWORD '...';
GRANT CONNECT ON DATABASE analytics TO readonly;
GRANT USAGE ON SCHEMA public TO readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;
-- Les tables créées plus tard héritent aussi du SELECT
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT ON TABLES TO readonly;
!
Nunca interpolar la pregunta en SQL
La pregunta del usuario va en el prompt del LLM, nunca concatenada en una consulta. El SQL ejecutado es el que produce el modelo, se valida y se ejecuta tal cual mediante cur.execute(sql), sin ningún parámetro del usuario inyectado. Por tanto, el riesgo de inyección clásica se desplaza hacia la validación de solo lectura; de ahí la importancia del rol en la base de datos.

El ciclo de corrección mejora significativamente el porcentaje de éxito. Muchos errores son triviales (nombre de columna ligeramente incorrecto, función de fecha propia del dialecto) y el modelo los corrige en el segundo intento si ve el mensaje de error del motor.

Bucle de corrección
def answer(question, schema, max_retries=2):
    sql = generate_sql(question, schema)
    for attempt in range(max_retries + 1):
        try:
            return sql, run_query(sql)
        except Exception as e:
            if attempt == max_retries:
                raise
            # On renvoie l'erreur au modèle pour correction
            fix_prompt = (
                f"La requête suivante a échoué :\n{sql}\n\n"
                f"Erreur PostgreSQL : {e}\n"
                f"Corrige la requête. SQL uniquement."
            )
            resp = ollama.chat(model=MODEL, messages=[
                {"role": "system", "content": SYSTEM_PROMPT.format(schema=schema)},
                {"role": "user", "content": fix_prompt},
            ], options={"temperature": 0})
            sql = clean_sql(resp["message"]["content"])

#¿Qué modelos locales destacan en SQL?

El SQL es una tarea de programación: los modelos especializados en código superan claramente a los generalistas del mismo tamaño. En 2026, un modelo de código reciente como Qwen3-Coder 30B-A3B lo cambia todo: los modelos pequeños de 2B a 8B improvisan consultas sencillas, pero fallan en cuanto hacen falta varias uniones o una agregación con funciones de ventana. (Codestral 22B, citado durante mucho tiempo para SQL, ahora tiene una licencia que no permite su uso en producción: hay que descartarlo en empresas.)

Qwen3-Coder 30B-A3B
La opción por defecto en 2026. Modelo MoE especializado en código con 3B de parámetros activos: rápido, 256k de contexto para esquemas grandes, ≈19 GB en Q4 en una RTX 4090 o un Mac reciente. Licencia Apache 2.0.
Devstral 24B
Especialista en código de Mistral AI (Apache 2.0), ≈14 GB en Q4: cabe en una tarjeta de 16 GB como la RTX 4080. El mejor equilibrio para SQL en una estación de trabajo modesta.
Qwen 3.8 27B
Modelo generalista reciente con razonamiento sólido en consultas con uniones complejas (≈18 GB, 262k de contexto, visión). Pon su ajuste de razonamiento en «low»: en una tarea tan estructurada como el SQL, tiende a razonar en exceso con el ajuste predeterminado.
Modelos pequeños 2B–8B (Qwen 3.5 4B, Granite 4.2 8B)
Para esquemas muy simples y preguntas directas únicamente. Evitar estos modelos en cuanto la base de datos tenga relaciones no triviales.
→
Cuantización Q4_K_M
Para text-to-SQL, Q4_K_M ofrece la mejor relación calidad/VRAM. La pérdida de precisión frente a Q8 es despreciable en esta tarea estructurada, mientras que el ahorro de VRAM permite pasar a un modelo más grande — y el tamaño del modelo cuenta mucho más que la cuantización para la precisión del SQL.

#Solución de problemas

El modelo inventa columnas
El esquema es incompleto o demasiado grande. Limítalo a las tablas pertinentes y añade comentarios sobre la lógica de negocio en las columnas ambiguas.
Respuestas con texto alrededor del SQL
Refuerza la instrucción «Solo SQL, sin explicaciones» y conserva la eliminación de los delimitadores de bloques de código Markdown en clean_sql.
Errores en la función de fecha
Especifica el dialecto en el prompt de sistema (PostgreSQL y MySQL difieren en DATE_TRUNC, YEAR(), etc.). El ciclo de corrección subsana los errores restantes.
Consultas lentas o que agotan el tiempo de espera
statement_timeout hace su trabajo. Añade «filtrar siempre en un rango de fechas razonable» al prompt para las grandes tablas.
«Connection refused» en Ollama
El daemon no está en ejecución. Comprueba «ollama ps» y que el servicio esté escuchando en http://localhost:11434.

#Para ir más allá

El text-to-SQL reutiliza varios componentes ya tratados en el sitio. Estas guías amplían esta guía:

Integrar Ollama en una aplicación Python mediante la API REST
Para exponer este pipeline detrás de una API FastAPI, gestionar el streaming y el modo JSON.
Function calling y salidas JSON estructuradas con Ollama
Una alternativa para estructurar la salida (consulta + explicación) de forma garantizada en lugar de mediante limpieza de texto.
Elegir tu cuantización (Q4, Q5, Q8, FP16)
Para encontrar el equilibrio entre el tamaño del modelo SQL y la VRAM disponible en tu tarjeta.
¿Esta guía te ha ayudado?

¿Un comentario, un error, una precisión? Avísanos, eso mejora la guía para todos.