Avanzato 13 minSQL

Text-to-SQL in locale: interrogare il proprio database in linguaggio naturel

Il text-to-SQL con un LLM locale permette di porre una domanda in francese (« qual è il fatturato per regione del mese scorso? ») e di ottenere una query SQL eseguibile sul tuo PostgreSQL o MySQL — senza che né lo schema né i dati lascino la tua infrastruttura. Questa guida illustra il funzionamento concreto: inserire lo schema nel contesto, costruire una pipeline Python con Ollama e soprattutto predisporre le misure di protezione (sola lettura, validazione, limiti) senza le quali nessun sistema text-to-SQL può essere messo in produzione.

Di Mohamed Meguedmi·Agg. 2026-08-27·Testato su Windows, macOS e Linux

#Perché usare text-to-SQL con un LLM locale

Le soluzioni cloud di text to SQL (assistenti BI, copiloti di data warehouse) inviano il tuo schema — nomi di tabelle e colonne, talvolta campioni di righe — a un server di terzi. Per un database di clienti, risorse umane o dati finanziari, questo è spesso inaccettabile: lo schema da solo rivela già la struttura della tua attività e i campioni contengono dati personali.

Un LLM locale risolve questo problema alla radice: il modello gira sulla tua macchina tramite Ollama, lo schema rimane nella memoria locale e la query generata viene eseguita sul tuo database senza che un solo byte transiti su internet. È anche gratuito da usare e non è soggetto ad alcun limite di frequenza delle richieste API.

Privacy
Lo schema e i dati non lasciano mai la tua rete — è più facile rispettare il GDPR e tutelare i segreti commerciali.
Costo
Nessun costo per richiesta. Un analista dati può iterare centinaia di volte senza pagare.
Accessibilità
Gli utenti aziendali che non conoscono SQL interrogano il database in linguaggio naturale.
Controllo
Sei tu a scegliere il modello, il prompt e le misure di salvaguardia: nessuna scatola nera remota.
!
Il text-to-SQL non è magico
Un LLM genera SQL plausibile, senza garantirne la correttezza. Su schemi complessi (join multipli, colonne ambigue), il tasso di errore resta concreto. Tratta l'output come una proposta da verificare, mai come una fonte di verità — soprattutto se una persona senza competenze tecniche vi si basa per prendere decisioni.

#Come funziona concretamente

Il kit Copilota Locale

Questa guida ti porta al modello. Il kit ti porta al copilota che scrive codice nel tuo editor.

  • Spazio online a vita
  • PDF + file
  • Aggiornamenti a vita

Il principio del text to SQL con un LLM si articola in tre fasi. Prima si descrive al modello lo schema del database (il DDL delle tabelle pertinenti). Poi gli si trasmette la domanda dell'utente con un'istruzione rigorosa: produrre esclusivamente una query SQL nel dialetto di destinazione. Infine si recupera la query, la si valida e la si esegue in sola lettura.

  1. 01
    Introspezione dello schema
    Si estrae la struttura delle tabelle (colonne, tipi, chiavi) dal database — automaticamente anziché a mano, per mantenerla sincronizzata.
  2. 02
    Costruzione del prompt
    Si costruisce un prompt di sistema contenente il dialetto SQL, lo schema pertinente e le regole (solo SELECT, LIMIT obbligatorio, nessun commento).
  3. 03
    Generazione
    Il LLM locale restituisce una query. La ripuliamo (rimuovendo gli eventuali delimitatori Markdown ```sql).
  4. 04
    Validazione + esecuzione
    Verifichiamo che sia un SELECT, lo eseguiamo con un ruolo di database in sola lettura, e restituiamo le righe.

#Prerequisiti

Ollama installato
Il daemon deve ascoltare su http://localhost:11434. Verifica con « ollama ps ».
Un modello capace
Un modello recente specializzato nella programmazione (Qwen3-Coder 30B-A3B, Devstral 24B) dà risultati molto migliori in SQL rispetto a un piccolo modello generalista (vedi la sezione modelli).
Python 3.10+
Con il client database adatto: psycopg2-binary (PostgreSQL) o PyMySQL (MySQL).
Un accesso al database in sola lettura
Idealmente un ruolo SQL dedicato che possa eseguire solo SELECT — la misura di protezione più importante.
Terminale
# Récupérer un modèle adapté au SQL
ollama pull qwen3-coder:30b

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

#Fornire al modello lo schema del proprio database

È la fase che determina l'80 % della qualità del risultato. Il modello può generare una query corretta solo se conosce i nomi esatti delle tabelle e delle colonne, i loro tipi e le relazioni tra di esse. Due approcci: incollare il DDL grezzo oppure esaminare la struttura del database tramite introspezione per costruire una descrizione compatta.

Per un database piccolo (meno di una ventina di tabelle), si può inserire tutto. Oltre questa dimensione, lo schema supera il contesto utile e sommerge il modello di informazioni: occorre quindi selezionare le tabelle pertinenti alla domanda (tramite una prima fase di ricerca o una mappatura del dominio aziendale). Ecco un'introspezione PostgreSQL che produce uno schema leggibile dal 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)
→
Aggiungi commenti sul significato dei dati nel contesto aziendale
Una colonna «ca_ht» è ambigua per il modello. Arricchisci lo schema con annotazioni: «ca_ht (fatturato al netto delle imposte, in euro)». Queste poche parole riducono drasticamente gli errori nella selezione della colonna. In PostgreSQL, i commenti definiti con COMMENT ON COLUMN sono recuperabili tramite information_schema e pg_description.

#Pipeline Python completa con Ollama

Ecco una pipeline minima ma funzionale: schema → prompt → generazione → pulizia → validazione → esecuzione. Utilizza il client Python ufficiale di Ollama e un ruolo del database con accesso in sola lettura.

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 dedicata all'esecuzione separa volutamente la validazione dalla chiamata al database. Si rifiuta tutto ciò che non è una singola istruzione SELECT prima ancora di aprire il cursore.

text_to_sql.py (continuazione)
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
Per il text-to-SQL, imposta sempre la temperatura a 0. Non vogliamo creatività: vogliamo la query più probabile e riproducibile. Una temperatura elevata introduce variazioni nelle colonne e nei join che fanno fallire l'esecuzione.

#Rendere affidabile e sicuro il SQL generato

È la sezione che distingue una demo da un deployment reale. Un LLM può generare una query distruttiva se gli viene chiesto — oppure accidentalmente, tramite un'iniezione nella domanda. La difesa non deve mai basarsi soltanto sul prompt: deve essere strutturata su più livelli, sul lato del database.

  1. 01
    Ruolo del database con accesso in sola lettura (difesa principale)
    Crea un ruolo SQL che abbia SOLO il privilegio SELECT. Anche se il modello genera un DROP TABLE, il database lo rifiuta. È il solo sistema veramente affidabile: 'GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;' e niente di più.
  2. 02
    Validazione applicativa
    Prima dell'esecuzione, analizza il codice SQL con sqlparse e rifiuta tutto ciò che non è una singola istruzione SELECT. Insieme al ruolo nel database, questo crea una doppia barriera.
  3. 03
    Timeout della richiesta
    L'istruzione SET statement_timeout impedisce a una query mal formata (prodotto cartesiano su milioni di righe) di saturare il database.
  4. 04
    LIMIT forzato
    Imposta un LIMIT sia sul lato prompt che sul lato codice, per non caricare mai intere tabelle in memoria.
  5. 05
    Ciclo di correzione
    Se l'esecuzione restituisce un errore SQL, restituisci il messaggio di errore al modello e chiedi una query corretta (1 o 2 tentativi massimi).
Ruolo PostgreSQL di sola lettura
-- À 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;
!
Non interpolare mai la domanda nel codice SQL
La domanda dell'utente va nel prompt del LLM, mai concatenata in una query. Il codice SQL eseguito è quello prodotto dal modello, validato ed eseguito così com'è tramite cur.execute(sql), senza parametri utente iniettati. Il rischio di SQL injection classica si sposta quindi sulla verifica che le operazioni siano di sola lettura: da qui l'importanza del ruolo nel database.

Il ciclo di correzione migliora notevolmente il tasso di successo. Molti errori sono banali (nome di colonna leggermente errato, funzione per le date specifica del dialetto) e il modello li corregge al secondo tentativo se vede il messaggio di errore del motore.

Ciclo di correzione
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"])

#Quali modelli locali eccellono in SQL

Il SQL è un'attività di programmazione: i modelli specializzati «coder» superano nettamente quelli generalisti della stessa dimensione. Nel 2026, un modello recente per la programmazione come Qwen3-Coder 30B-A3B cambia tutto — i piccoli modelli da 2B a 8B mettono insieme alla meglio query semplici, ma falliscono non appena servono più join o un'aggregazione con funzioni finestra. (Codestral 22B, a lungo citato per il SQL, è ora soggetto a una licenza che non consente l'uso in produzione: da escludere in azienda.)

Qwen3-Coder 30B-A3B
La scelta predefinita per il 2026. Modello MoE per il codice con 3B parametri attivi: veloce, 256k di contesto per schemi di grandi dimensioni, ≈19 GB in Q4 su una RTX 4090 o un Mac recente. Licenza Apache 2.0.
Devstral 24B
Specialista del codice firmato Mistral AI (Apache 2.0), ≈14 GB in Q4 — entra nella memoria di una scheda da 16 GB come la RTX 4080. Il miglior compromesso per SQL su una workstation modesta.
Qwen 3.8 27B
Modello generalista recente, con solide capacità di ragionamento sui join complessi (≈18 GB, 262k di contesto, visione). Imposta il livello di ragionamento su «low»: su un compito così strutturato come SQL, tende a ragionare troppo con l'impostazione predefinita.
Modelli piccoli 2B–8B (Qwen 3.5 4B, Granite 4.2 8B)
Solo per schemi molto semplici e domande dirette. Da evitare quando il database presenta relazioni non banali.
→
Quantizzazione Q4_K_M
Per il text-to-SQL, Q4_K_M offre il migliore rapporto qualità/VRAM. La perdita di precisione rispetto a Q8 è trascurabile in questo compito strutturato, mentre il risparmio di VRAM permette di passare a un modello più grande — e la dimensione del modello conta molto più della quantizzazione per la correttezza del SQL.

#Risoluzione dei problemi

Il modello inventa colonne
Lo schema è incompleto o troppo grande. Limitane il contenuto alle tabelle pertinenti e aggiungi commenti che spiegano il significato aziendale delle colonne ambigue.
Risposte con testo intorno al SQL
Rafforza l'istruzione «Solo SQL, nessuna spiegazione» e mantieni in clean_sql la rimozione dei delimitatori dei blocchi di codice Markdown.
Errori nelle funzioni per le date
Specifica il dialetto nel prompt di sistema (PostgreSQL e MySQL differiscono per DATE_TRUNC, YEAR(), ecc.). Il ciclo di correzione corregge gli errori rimanenti.
Query lente o che vanno in timeout
statement_timeout funziona. Aggiungi «filtrare sempre su un intervallo di date ragionevole» al prompt per le grandi tabelle.
« Connection refused » Ollama
Il servizio non è avviato. Verifica « ollama ps » e assicurati che il servizio ascolti su http://localhost:11434.

#Per approfondire

Il text-to-SQL riutilizza diversi componenti già trattati sul sito. Queste guide approfondiscono questa guida:

Integrare Ollama in un'applicazione Python tramite l'API REST
Per esporre questa pipeline tramite un'API FastAPI, gestire lo streaming e la modalità JSON.
Function calling e output JSON strutturati con Ollama
Un'alternativa per garantire la struttura dell'output (query + spiegazione), anziché ottenerla ripulendo il testo.
Scegliere la quantizzazione (Q4, Q5, Q8, FP16)
Per trovare il compromesso tra la dimensione del modello SQL e la VRAM disponibile sulla tua scheda.
Questa guida ti è stata utile?

Un feedback, un errore, una precisazione? Facci sapere, così la guida migliora per tutti.