# Come analizzare grandi file CSV con LLM locali in C#

<!--category-- AI, LLM, Data Analysis, DuckDB, C#, Ollama -->
<datetime class="hidden">2025-12-18T10:00</datetime>

**Serie: LLM locali per i dati - Parte 1 di 2**

Ecco l'errore che tutti commettono: cercano di alimentare il loro CSV in un LLM. Non farlo. **I LLM dovrebbero generare query, non consumare dati.**

Hai un file CSV da 500MB e vuoi chiedere "Qual è il valore medio dell'ordine per regione?" [Copilota in Excel](https://support.microsoft.com/en-gb/copilot-excel) può farlo, ma se i tuoi dati sono troppo sensibili per i servizi cloud? Cosa succede se hai bisogno di costruirli da soli?

Questo articolo mostra come - localmente, privatamente, in C#.

> Usa DuckDB per interrogare direttamente i file CSV. Usa un LLM locale per generare SQL. Il LLM non vede mai i tuoi dati - solo lo schema. Risultato: query sub-100ms su file da milioni di righe, completamente offline.

Per un trattamento complementare e più focalizzato su CLI che si espande sull'utilizzo di profili statistici come interfaccia LLM (e mostra uno strumento completo che implementa queste idee - profilazione, modalità SQL sicura, clonazione sintetica e rilevamento della deriva), vedere l'articolo seguente: **[DataSummarizer: Profilazione rapida dei dati locali](/blog/datasummarizer-how-it-works)** - in particolare la sezione "The Key Upgrade: Statistics as the Interface." Questi due articoli formano una breve serie sui modelli pratici locali LLM + query.

[TOC]

## Il Core Insight

Usare un LLM come archivio dati è l'astrazione sbagliata. I LLM sono fondamentalmente incapaci di scansionare milioni di righe per calcolare una media - non è quello che servono. Anche una finestra di contesto token da 200K misura forse 50.000 righe. Il tuo CSV da 500MB ha milioni.

Il modello corretto: **Motivi dell'LLM, calcolo del database.**

```mermaid
flowchart LR
    A[User Question] --> B[LLM]
    B --> C[SQL Query]
    C --> D[DuckDB]
    D --> E[Results]
    
    style B stroke:#333,stroke-width:4px
    style D stroke:#333,stroke-width:4px
```

Notate cosa sta succedendo: l'LLM genera una query SQL basata sulla vostra domanda e sullo schema. DuckDB la esegue contro i dati reali. L'LLM non tocca mai i vostri dati - vede solo i nomi delle colonne e i tipi. Ecco perché è veloce, privato e preciso.

Per maggiori informazioni sul trattamento dei profili come interfaccia LLM (e una CLI concreta che implementa la prima narrazione del profilo, il Q&A protetto da SQL, le sessioni supportate dal registro e la clonazione sintetica), vedere **[DataSummarizer: Profilazione rapida dei dati locali](/blog/datasummarizer-how-it-works#the-key-upgrade-statistics-as-the-interface)**.

## Perché non caricarlo nella memoria?

Gli approcci ovvi hanno tutti lo stesso difetto fatale:

**[CsvHelper](https://joshclose.github.io/CsvHelper/) / DataFrames**: Caricare l'intero file in RAM. Un CSV da 500MB diventa 2-4GB di oggetti. Un file da 5GB? Crash OOM.

**SQLite / PostgreSQL**: Richiede un passo lento di importazione (minuti per file di grandi dimensioni), definizioni di schema upfront, e gestione del database overhead.

**[PandasAICity name (optional, probably does not need a translation)](https://github.com/Sinaptik-AI/pandas-ai)**: Ancora carica tutto in memoria. Inoltre, l'esecuzione di codice arbitrario generato da LLM è un incubo di sicurezza - SQL è dichiarativo e sandboxable; Python non lo è.

## Perché DuckDB

[AnatraDBunit synonyms for matching user input](https://duckdb.org/) è diverso. Interroga i file CSV *direttamente* - nessun passaggio di importazione, nessun caricamento in memoria:

```csharp
using var connection = new DuckDBConnection("DataSource=:memory:");
connection.Open();
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT Region, SUM(Amount) FROM 'sales.csv' GROUP BY Region";
// Executes directly against the file - no import, no memory explosion
```

La caratteristica killer: **tratta i file come tabelle**. Puntalo su un file CSV, Parquet o JSON e chiedi immediatamente. Nessun CREATE TABLE, nessun inserto sfuso, nessuna attesa.

| Fattore | CsvHelper | SQLite | DuckDB |
|--------|-----------|--------|--------|
| **Memoria** | Carica tutto il file | Carica durante l'importazione | Streams from disc |
| **Configurazione** |Nessuno |Schema + importazione |Nessuno |
| **File 500MB** | ~2GB RAM | Minuti di importazione | Istantanea |
| **File 5GB** | Crash OOM | Molto lento | Funziona bene |
| **Parquet** | No | No | Sì (10-100x più veloce) |

DuckDB è ciò che gli ingegneri di dati utilizzano in Python per esattamente questo caso d'uso. [Legame .NET](https://github.com/Giorgi/DuckDB.NET) dare il supporto completo ADO.NET - si sente come qualsiasi altro database, tranne che si sta interrogando i file.

## Lo StackCity name (optional, probably does not need a translation)

| Componente | Perché questo |
|-----------|--------------|
| [AnatraDBunit synonyms for matching user input](https://duckdb.org/) | Queries CSV directly, no import step |
| [DuckDB.NET](https://github.com/Giorgi/DuckDB.NET) | Supporto completo ADO.NET, si sente nativo |
| [OllamaCity name (optional, probably does not need a translation)](https://ollama.ai/) | Inferenza locale, nessuna chiave API, nessuna nuvola |
| [BogusCity name (optional, probably does not need a translation)](https://github.com/bchavez/Bogus) | Dati di prova realistici su qualsiasi scala |
| [`qwen2.5-coder:7b`](https://ollama.ai/library/qwen2.5-coder) |Migliore precisione SQL a dimensione 7B |

> **Nota sulla sicurezza**: Stiamo eseguendo SQL generato da LLM. Questo è più sicuro del codice arbitrario, ma richiede comunque la convalida. [Sezione Sicurezza](#security-considerations) Per una più ampia discussione sulla trasformazione dei profili nell'interfaccia LLM e in una CLI che implementa questi modelli, vedere il pezzo di accompagnamento **[DataSummarizer: Profilazione rapida dei dati locali](/blog/datasummarizer-how-it-works)**.

## Configurazione progetto

Creiamo un progetto di esempio. Installa i pacchetti NuGet:

```bash
dotnet add package DuckDB.NET.Data.Full
dotnet add package OllamaSharp
dotnet add package Bogus
```

Tirare un modello focalizzato sulla codifica che è bravo in SQL:

```bash
ollama pull qwen2.5-coder:7b
```

## L'architettura

Ecco come i pezzi si adattano insieme:

```mermaid
flowchart TB
    subgraph Input
        Q[User Question]
        CSV[CSV File]
    end
    
    subgraph Processing
        Schema[Extract Schema]
        Sample[Get Sample Rows]
        Context[Build LLM Context]
        LLM[Generate SQL]
        Validate[Validate SQL]
        Execute[Execute Query]
    end
    
    subgraph Output
        Results[Query Results]
    end
    
    CSV --> Schema
    CSV --> Sample
    Schema --> Context
    Sample --> Context
    Q --> Context
    Context --> LLM
    LLM --> Validate
    Validate -->|Error| LLM
    Validate -->|OK| Execute
    CSV --> Execute
    Execute --> Results
    
    style LLM stroke:#333,stroke-width:4px
    style Execute stroke:#333,stroke-width:4px
```

L'intuizione chiave: diamo l'LLM **schema e dati del campione**, non i dati reali. Questo mantiene il contesto piccolo e le risposte veloci.

**Perche' questo e' importante?**: Il LLM genera intent (SQL). DuckDB lo esegue. Il passo di validazione cattura gli errori di sintassi prima dell'esecuzione. Il loop di riprova gestisce l'errore occasionale. Questa separazione è ciò che rende il sistema sia sicuro che preciso. Per un riferimento più dettagliato, CLI-centrato (incluso profilo-primo narrazione e sicuri limiti di esecuzione SQL) vedere **[DataSummarizer: Profilazione rapida dei dati locali](/blog/datasummarizer-how-it-works#the-key-upgrade-statistics-as-the-interface)**.

## Passo 1: Generare i dati di prova con Bogus

> **Hai già i dati CSV?** Skip to [Passo 2: Costruire il contesto schema](#step-2-build-the-schema-context).

Prima di poter testare il nostro analizzatore CSV alimentato da LLM, abbiamo bisogno di dati da analizzare. Per lo sviluppo e il test, i dati sintetici battono i dati reali:

1. **Prova su scala** - Generare righe 100K, 1M o 10M per verificare le prestazioni in diverse dimensioni
2. **Privacy** - Nessun rischio di esporre i dati reali cliente / business in demo o screenshot
3. **Riproducibilità** - Stesso seme = stessi dati, rendendo riproducibili i bug
4. **Casi di bordo** - Controllare la distribuzione (ad esempio, forza 5% ritorni, intervalli di date specifici)

### Che cos'è Bogus?

[BogusCity name (optional, probably does not need a translation)](https://github.com/bchavez/Bogus) è una porta .NET della libreria popolare faker.js. Genera dati falsi realistici - nomi, indirizzi, e-mail, date, numeri - con un adeguato supporto locale. Invece di file CSV di test artigianali o utilizzando dati casuali della spazzatura, Bogus ti dà dati che *sguardi* reale:

- `f.Name.FullName()` → "John Smith" (non "asdf1234")
- `f.Internet.Email()` → "john.smith@gmail.com" (proprio formattato)
- `f.Date.Between(start, end)` → Distribuzione realistica delle date
- `f.Commerce.ProductName()` → "Formaggio di Granito artigianale" (divertente, ma riconoscibile)

Questo è importante perché dati realistici ti aiutano a individuare problemi - formattazione strana, aggregazioni inaspettate, casi di bordo nella gestione delle date - che stringhe casuali si nascondono.

### Definire il modello di dati

```csharp
internal class SaleRecord
{
    public string OrderId { get; set; } = "";
    public DateTime OrderDate { get; set; }
    public string CustomerId { get; set; } = "";
    public string CustomerName { get; set; } = "";
    public string Region { get; set; } = "";
    public string Category { get; set; } = "";
    public string ProductName { get; set; } = "";
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
    public decimal Discount { get; set; }
    public bool IsReturned { get; set; }
}
```

### Configura il Faker

Bogus utilizza un'API fluente per definire le regole di generazione:

```csharp
var categories = new[] { "Electronics", "Clothing", "Home & Garden", "Sports", "Books" };
var regions = new[] { "North", "South", "East", "West", "Central" };

var faker = new Faker<SaleRecord>()
    .RuleFor(s => s.OrderId, f => f.Random.Guid().ToString()[..8].ToUpper())
    .RuleFor(s => s.OrderDate, f => f.Date.Between(
        new DateTime(2022, 1, 1), 
        new DateTime(2024, 12, 31)))
    .RuleFor(s => s.CustomerId, f => $"CUST-{f.Random.Number(10000, 99999)}")
    .RuleFor(s => s.CustomerName, f => f.Name.FullName())
    .RuleFor(s => s.Region, f => f.PickRandom(regions))
    .RuleFor(s => s.Category, f => f.PickRandom(categories))
    .RuleFor(s => s.ProductName, (f, s) => GenerateProductName(f, s.Category))
    .RuleFor(s => s.Quantity, f => f.Random.Number(1, 20))
    .RuleFor(s => s.UnitPrice, f => f.Random.Decimal(9.99m, 299.99m))
    .RuleFor(s => s.Discount, f => f.Random.Bool(0.3f) ? f.Random.Decimal(0.05m, 0.25m) : 0m)
    .RuleFor(s => s.IsReturned, f => f.Random.Bool(0.05f));
```

Distruggiamo quello che sta succedendo:

- **`f` (Faker)** - L'istanza generatore con accesso a tutti i moduli di dati (Nome, Data, Casuale, ecc.)
- **`f.Random.Guid().ToString()[..8]`** - Generare un GUID ma prendere solo i primi 8 caratteri per un ID ordine leggibile
- **`f.Date.Between()`** - Data casuale entro un intervallo realistico (non anno 9999)
- **`f.PickRandom(array)`** - Selezionare in modo casuale da opzioni predefinite (esprime categorie valide)
- **`f.Random.Bool(0.3f)`** - 30% di probabilità di Vero (30% di ordini ottenere uno sconto)
- **`(f, s)` sintassi** - Accedi sia al falso che al disco parzialmente costruito. `ProductName` dipende da `Category`

La `(f, s)` pattern è potente - significa che gli ordini "Electronics" ottengono i nomi dei prodotti elettronici, non oggetti casuali. Questa coerenza rende i dati generati molto più realistici per testare le aggregazioni come "revenue per categoria."

### Genera e scrivi su CSV

```csharp
var records = faker.Generate(100_000); // Adjust for your testing needs

await using var writer = new StreamWriter(csvPath, false, Encoding.UTF8);
await writer.WriteLineAsync("OrderId,OrderDate,CustomerId,CustomerName,Region,Category,...");

foreach (var record in records)
{
    var total = record.Quantity * record.UnitPrice * (1 - record.Discount);
    await writer.WriteLineAsync($"{record.OrderId},{record.OrderDate:yyyy-MM-dd},...");
}
```

100K righe genera circa 15MB di CSV - abbastanza per testare, ma si può facilmente scalare a milioni. La generazione è veloce (~ 2 secondi per 100K righe) perché Bogus è ottimizzato per la generazione all'ingrosso.

> **Suggerimento**: Imposta `Randomizer.Seed = new Random(12345)` prima di generare per ottenere dati riproducibili. Stesso seme = stesso "casuale" registra ogni volta, che è inestimabile per il debug.

## Passo 2: Costruire il contesto schema

Prima che l'LLM possa generare SQL, deve capire la struttura dei dati. Lo estraiamo da DuckDB:

### Il modello contestuale

```csharp
public class DataContext
{
    public string CsvPath { get; set; } = "";
    public List<ColumnInfo> Columns { get; set; } = new();
    public List<Dictionary<string, string>> SampleRows { get; set; } = new();
    public long RowCount { get; set; }
}

public class ColumnInfo
{
    public string Name { get; set; } = "";
    public string Type { get; set; } = "";  // VARCHAR, DOUBLE, TIMESTAMP, etc.
}
```

Questo cattura tutto ciò di cui l'LLM ha bisogno: nomi delle colonne, tipi e alcune righe di esempio per capire il formato dei dati.

### Estrazione dello schema

DuckDB può descrivere qualsiasi CSV senza caricare tutto:

```csharp
private DataContext BuildContext(DuckDBConnection connection, string csvPath)
{
    var context = new DataContext { CsvPath = csvPath };

    // Get schema - DuckDB infers types from the CSV
    using var cmd = connection.CreateCommand();
    cmd.CommandText = $"DESCRIBE SELECT * FROM '{csvPath}'";
    using var reader = cmd.ExecuteReader();

    while (reader.Read())
    {
        context.Columns.Add(new ColumnInfo
        {
            Name = reader.GetString(0),  // Column name
            Type = reader.GetString(1)   // Inferred type
        });
    }

    return context;
}
```

La `DESCRIBE` comando legge solo l'intestazione del file più alcune righe per l'inferenza di tipo - è istantaneo anche su file enormi.

### Ottenere i dati del campione

Le righe di esempio aiutano l'LLM a comprendere i formati dei dati (date, ID, ecc.):

```csharp
using var cmd = connection.CreateCommand();
cmd.CommandText = $"SELECT * FROM '{csvPath}' LIMIT 3";
using var reader = cmd.ExecuteReader();

while (reader.Read())
{
    var row = new Dictionary<string, string>();
    for (int i = 0; i < reader.FieldCount; i++)
    {
        var value = reader.IsDBNull(i) ? "NULL" : reader.GetValue(i)?.ToString() ?? "";
        row[reader.GetName(i)] = value;
    }
    context.SampleRows.Add(row);
}
```

Tre righe sono di solito sufficienti - mostra al LLM quali formati aspettarsi senza sprecare gettoni.

## Passo 3: Generare SQL con il LLM

**Questa è la parte difficile.** L'ingegneria rapida qui non è negoziabile - senza regole rigorose, LLM locali produrrà SQL creativo ma rotto. L'obiettivo è il determinismo, non la creatività.

### Costruire il Prompt

```csharp
private string BuildPrompt(DataContext context, string question, string? previousError)
{
    var sb = new StringBuilder();

    sb.AppendLine("You are a SQL expert. Generate a DuckDB SQL query to answer the user's question.");
    sb.AppendLine();
    sb.AppendLine("IMPORTANT RULES:");
    sb.AppendLine("1. The table is accessed directly from the CSV file path");
    sb.AppendLine("2. Use single quotes around the file path in FROM clause");
    sb.AppendLine("3. DuckDB syntax - use LIMIT not TOP, use || for string concat");
    sb.AppendLine("4. Return ONLY the SQL query, no explanation, no markdown");
    sb.AppendLine();
    
    sb.AppendLine($"CSV File: '{context.CsvPath}'");
    sb.AppendLine($"Row Count: {context.RowCount:N0}");
    sb.AppendLine();
    
    // Schema
    sb.AppendLine("Schema:");
    foreach (var col in context.Columns)
    {
        sb.AppendLine($"  - {col.Name}: {col.Type}");
    }
```

La sezione regole è cruciale - dice al LLM esattamente come formattare la query per DuckDB. Essere espliciti sulla sintassi (LIMIT vs TOP, concatenazione delle stringhe) impedisce errori comuni.

### Aggiunta dei dati del campione

```csharp
    if (context.SampleRows.Count > 0)
    {
        sb.AppendLine();
        sb.AppendLine("Sample data (first 3 rows):");
        foreach (var row in context.SampleRows)
        {
            var values = row.Select(kv => $"{kv.Key}='{kv.Value}'");
            sb.AppendLine($"  {{{string.Join(", ", values)}}}");
        }
    }
```

### Errore di recupero

Se un tentativo precedente non è riuscito, includere l'errore:

```csharp
    if (previousError != null)
    {
        sb.AppendLine();
        sb.AppendLine("YOUR PREVIOUS QUERY HAD AN ERROR:");
        sb.AppendLine(previousError);
        sb.AppendLine("Please fix the query based on this error.");
    }

    sb.AppendLine();
    sb.AppendLine($"Question: {question}");
    sb.AppendLine();
    sb.AppendLine("SQL Query (no markdown, no explanation):");

    return sb.ToString();
}
```

Questo meccanismo di riprovazione è importante - gli LLM locali a volte commettono errori di sintassi, e dando loro l'errore di solito lo corregge al secondo tentativo.

### Chiamare l'LLM

```csharp
var request = new GenerateRequest { Model = _model, Prompt = prompt };
var response = await _ollama.GenerateAsync(request).StreamToEndAsync();
var sql = CleanSqlResponse(response?.Response ?? "");
```

La `StreamToEndAsync()` Aspetta la risposta completa. Per un UX migliore, si potrebbe trasmettere i gettoni quando arrivano.

### Pulire la risposta

I LLM spesso avvolgono SQL nei blocchi di codice markdown nonostante sia stato detto di non:

```csharp
private string CleanSqlResponse(string response)
{
    var sql = response.Trim();

    // Remove markdown code blocks if present
    if (sql.StartsWith("```"))
    {
        var lines = sql.Split('\n').ToList();
        lines.RemoveAt(0);  // Remove opening ```sql
        if (lines.Count > 0 && lines[^1].Trim().StartsWith("```"))
        {
            lines.RemoveAt(lines.Count - 1);  // Remove closing ```
        }
        sql = string.Join('\n', lines);
    }

    return sql.Trim('`', ' ', '\n', '\r');
}
```

## Passo 4: Convalida prima dell'esecuzione

DUCKDB'S `EXPLAIN` ci permette di controllare la sintassi SQL senza eseguire la query:

```csharp
private string? ValidateSql(DuckDBConnection connection, string sql)
{
    try
    {
        using var cmd = connection.CreateCommand();
        cmd.CommandText = $"EXPLAIN {sql}";
        cmd.ExecuteNonQuery();
        return null; // Valid
    }
    catch (Exception ex)
    {
        return ex.Message;
    }
}
```

Se la convalida fallisce, rimettiamo l'errore alla LLM e riproviamo (fino a un limite).

## Passo 5: Eseguire e Formattare i risultati

Infine, eseguire la query e formattare l'output:

```csharp
private QueryResult ExecuteQuery(DuckDBConnection connection, string sql)
{
    var result = new QueryResult { Sql = sql };

    try
    {
        using var cmd = connection.CreateCommand();
        cmd.CommandText = sql;
        using var reader = cmd.ExecuteReader();

        // Capture column names
        for (int i = 0; i < reader.FieldCount; i++)
        {
            result.Columns.Add(reader.GetName(i));
        }

        // Capture rows
        while (reader.Read())
        {
            var row = new List<object?>();
            for (int i = 0; i < reader.FieldCount; i++)
            {
                row.Add(reader.IsDBNull(i) ? null : reader.GetValue(i));
            }
            result.Rows.Add(row);
        }

        result.Success = true;
    }
    catch (Exception ex)
    {
        result.Success = false;
        result.Error = ex.Message;
    }

    return result;
}
```

La `QueryResult` class (mostrato per intero nel progetto campione) include un `ToString()` metodo che formatta risultati come una tabella leggibile.

## Aggiunta del contesto di conversazione

Per l'analisi interattiva, gli utenti spesso vogliono porre domande di follow-up:

```
"What's the total revenue?"
→ "Break that down by region"
→ "Show the top 5 regions"
```

La seconda e la terza questione hanno senso solo con il contesto dalla prima.

### Cronologia delle conversazioni di monitoraggio

```csharp
public class ConversationTurn
{
    public string Question { get; set; } = "";
    public string Sql { get; set; } = "";
    public bool Success { get; set; }
    public int RowCount { get; set; }
    public string Summary { get; set; } = "";  // "Single value: 1234567.89"
}
```

### Compresa la storia nei prompt

```csharp
if (_history.Count > 0)
{
    sb.AppendLine();
    sb.AppendLine("CONVERSATION HISTORY (for context):");
    
    foreach (var turn in _history.TakeLast(5))  // Last 5 turns
    {
        sb.AppendLine($"Q: {turn.Question}");
        sb.AppendLine($"SQL: {turn.Sql}");
        if (turn.Success)
        {
            sb.AppendLine($"Result: {turn.Summary}");
        }
        sb.AppendLine();
    }
}
```

La storia dà al contesto LLM per comprendere riferimenti come "quello," "quelli risultati," o "disgregarlo ulteriormente."

## Quale modello usare?

Per la generazione SQL, i modelli focalizzati sulla codifica funzionano meglio. Ecco le opzioni disponibili tramite [La libreria modello di Ollama](https://ollama.ai/library):

| Modello | Dimensioni | Velocità | Qualità | Collegamento |
|-------|------|-------|---------|------|
| `qwen2.5-coder:7b` | 4.7GB | Fast | Excellent | [OllamaCity name (optional, probably does not need a translation)](https://ollama.ai/library/qwen2.5-coder) |
| `deepseek-coder-v2:16b` | 9GB | Medio | Migliore | [OllamaCity name (optional, probably does not need a translation)](https://ollama.ai/library/deepseek-coder-v2) |
| `codellama:7b` | 4GB | Fast | Good | [OllamaCity name (optional, probably does not need a translation)](https://ollama.ai/library/codellama) |
| `llama3.2:3b` | 2GB | Molto veloce | Accettabile | [OllamaCity name (optional, probably does not need a translation)](https://ollama.ai/library/llama3.2) |

Per la maggior parte dei casi d'uso, **`qwen2.5-coder:7b`** colpisce il punto dolce - SQL accurato, buona velocità, funziona su hardware modesto (8GB + RAM).

## Prestazioni

### Benchmark del mondo reale

Test su un file CSV a riga 100K (14MB) su una macchina dev standard (Ryzen 5, NVMe SSD, 32GB RAM):

| Tipo di domanda | Tempo |
|------------|------|
| COUNT semplice | 65ms |
| GROUP BY with SUM | 58-68ms |
| Aggregazione complessa con FILTRO | 63ms |
| GROUPO multitavola BY con RISPARMIO | 71ms |

Sotto-100ms per le query analitiche su righe 100K - senza alcun passaggio di importazione. La complessità di query conta più del numero di righe; il motore colonnare di DuckDB gestisce le aggregazioni in modo efficiente indipendentemente dalla dimensione del file.

### Converti file di grandi dimensioni in parquet

Per file superiori a 1GB, [Formato del parquet](https://parquet.apache.org/) è 10-100x più veloce:

```csharp
using var cmd = connection.CreateCommand();
cmd.CommandText = $"COPY (SELECT * FROM '{csvPath}') TO '{parquetPath}' (FORMAT PARQUET)";
cmd.ExecuteNonQuery();
```

Anche il file Parquet compresso è molto più piccolo.

### Considerazioni in materia di sicurezza

Quando si esegue SQL generato da LLM:

```csharp
private bool IsSafeQuery(string sql)
{
    var dangerous = new[] { "DROP", "DELETE", "TRUNCATE", "UPDATE", "INSERT", "ALTER", "CREATE" };
    var upperSql = sql.ToUpperInvariant();
    return !dangerous.Any(d => upperSql.Contains(d));
}
```

La modalità in-memory di DuckDB fornisce anche l'isolamento naturale - non può influenzare i database di produzione.

## Esempio completo

Ecco come si fondono le cose:

```csharp
// Generate test data
await GenerateSalesCsvAsync("sales.csv", 100_000);

// Simple query
using var service = new CsvQueryService("qwen2.5-coder:7b", verbose: true);
var result = await service.QueryAsync("sales.csv", "What are total sales by region?");
Console.WriteLine(result);

// Conversational analysis
using var analyser = new ConversationalCsvAnalyser("sales.csv", "qwen2.5-coder:7b");

Console.WriteLine(await analyser.AskAsync("What's the total revenue?"));
Console.WriteLine(await analyser.AskAsync("Break that down by category"));
Console.WriteLine(await analyser.AskAsync("Which category has the most returns?"));
```

## Sommario

Il modello mentale da mantenere: **LLMs ragione; basi di dati calcolare.**

Non caricare i dati sull'LLM. Lo schema, lascialo generare SQL, esegue SQL contro un motore di query corretto. Questa separazione è il motivo per cui l'approccio funziona in scala.

L'attuazione:

1. **AnatraDBunit synonyms for matching user input** query CSV direttamente - nessuna importazione, flussi da disco
2. **Schema + campioni** dare all'LLM un contesto sufficiente senza esporre i dati
3. **Regole rigorose e rapide** forza deterministica SQL, prosa non creativa
4. **Convalida tramite EXPLAIN** errori di cattura prima dell'esecuzione
5. **Riprova con il feedback degli errori** gestisce lo slip di sintassi occasionale

Il risultato: query analitiche sub-100ms su file million-row, completamente offline, con dati che non lasciano mai la tua macchina.

Il progetto completo del campione è disponibile all'indirizzo: [Per lo più Lucid.CsvLlm](https://github.com/scottgal/mostlylucidweb) - comprende: `CsvQueryService`, `ConversationalCsvAnalyser`, e generazione di dati basati su Bogus.

## Risorse

### AnatraDBunit synonyms for matching user input

- [Documentazione DuckDB](https://duckdb.org/docs/) - Riferimento completo
- [Importa CSV DuckDB](https://duckdb.org/docs/data/csv/overview.html) - Caratteristiche specifiche del CSV
- [Riferimento DuckDB SQL](https://duckdb.org/docs/sql/introduction) - differenze di sintassi SQL da altri database
- [DuckDB.NET GitHub](https://github.com/Giorgi/DuckDB.NET) - Legature C#
- [DuckDB.NET NuGet](https://www.nuget.org/packages/DuckDB.NET.Data.Full) - Pacchetto completo con binari nativi

### Ollama & LLMs

- [OllamaCity name (optional, probably does not need a translation)](https://ollama.ai/) - Runtime LLM locale
- [Biblioteca modelli Ollama](https://ollama.ai/library) - Modelli disponibili
- [OllamaSharpCity name (optional, probably does not need a translation)](https://github.com/awaescher/OllamaSharp) - Libreria C# client
- [OllamaSharp NuGet](https://www.nuget.org/packages/OllamaSharp/)

### Generazione dei dati di prova

- [Bogus GitHub](https://github.com/bchavez/Bogus) - Generatore di dati falso
- [Riferimento API Bogus](https://github.com/bchavez/Bogus#bogus-api-support) - Tipi di dati disponibili

### Alternative citate

- [CsvHelper](https://joshclose.github.io/CsvHelper/) - Libreria di analisi CSV
- [Microsoft.Data.Analisi](https://www.nuget.org/packages/Microsoft.Data.Analysis) - DataFrame per .NET
- [PandasAICity name (optional, probably does not need a translation)](https://github.com/Sinaptik-AI/pandas-ai) - Integrazione Python LLM + panda

### Articoli correlati

- [DataSummarizer: Profilazione rapida dei dati locali](/blog/datasummarizer-how-it-works) - Articolo Companion riguardante la profilazione dei dati basata su CLI