# Miten analysoida suuria CSV-tiedostoja paikallisilla LLM-tiedostoilla C#

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

**Sarja: Paikalliset LLM:t datalle - Osa 1 2**

Tämän virheen kaikki tekevät: he yrittävät syöttää CSV:nsä LLM:ään. **LLM:ien pitäisi tuottaa kyselyjä, ei kuluttaa tietoja.**

Sinulla on 500MB CSV-tiedosto ja haluat kysyä "Mikä on keskimääräinen tilausarvo alueittain?" Työkalut kuten [Copilot Excelissä](https://support.microsoft.com/en-gb/copilot-excel) voi tehdä näin, mutta entä jos datasi on pilvipalveluille liian herkkää? Entä jos sinun täytyy rakentaa se itse?

Tässä artikkelissa kerrotaan, miten - paikallisesti, yksityisesti, C#:ssä.

> Käytä DuckDB:tä CSV-tiedostojen suoraan kyselyyn. Käytä paikallista LLM:tä SQL:n tuottamiseen. LLM ei koskaan näe tietojasi - vain skeemaa. Tulos: ali-100 ms kyselyjä miljoonan rivin tiedostoista, kokonaan offline-muodossa.

Täydentävä, CLI-painotteisempi hoito, joka laajenee käyttämään tilastollisia profiileja LLM-rajapintana (ja näyttää täyden työkalun näiden ideoiden toteuttamiseksi – profilointi, turvallinen SQL-tila, synteettinen kloonaus ja ajelehtimisen toteaminen), ks. seuraava artikkeli: **[Datakehittäjä: Nopea paikallinen dataprofilointi](/blog/datasummarizer-how-it-works)** Nämä kaksi artikkelia muodostavat lyhyen sarjan käytännön paikallisista LLM + kyselymalleista.

[TOC]

## Keskeistä näkemystä

LLM:n käyttäminen datavarastona on väärä abstraktio. LLM:t ovat pohjimmiltaan kykenemättömiä skannaamaan miljoonia rivejä keskiarvon laskemiseen - sitä varten ne eivät ole. Jopa 200K:n kuvaikkunaan mahtuu ehkä 50 000 riviä. 500MB CSV:ssä on miljoonia.

Oikea kuvio: **LLM syistä, tietokanta laskee.**

```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
```

Huomaa, mitä tapahtuu: LLM tuottaa SQL-kyselyn, joka perustuu kysymykseesi ja skeemaan. DuckDB suorittaa sen todellisia tietoja vastaan. LLM ei koskaan koske dataasi - se näkee vain sarakkeen nimiä ja tyyppejä. Siksi se on nopea, yksityinen ja tarkka.

Lisätietoja profiilien käsittelystä LLM-rajapintana (ja konkreettisena CLI:nä, joka toteuttaa profiilien ensimmäisen kuvauksen, turvallisen SQL-tuen, rekisterien tukemien istuntojen ja synteettisen kloonauksen), ks. **[Datakehittäjä: Nopea paikallinen dataprofilointi](/blog/datasummarizer-how-it-works#the-key-upgrade-statistics-as-the-interface)**.

## Mikset vain ladannut sitä muistiin?

Ilmiselvissä lähestymistavoissa on sama kohtalokas puute:

**[CsvHelper](https://joshclose.github.io/CsvHelper/) / DataFrames**: Lataa koko tiedosto RAM-muistiin. 500MB CSV:stä tulee 2-4GB esineitä. 5GB-tiedosto? OOM-romahdus.

**SQLite / PostgreSQL**: Vaatii hitaan tuontivaiheen (minuutteja suurille tiedostoille), etukäteismääritelmät ja tietokannan hallinnan yläpuolella.

**[PandasAI](https://github.com/Sinaptik-AI/pandas-ai)**Lisäksi LLM:n luoman mielivaltaisen koodin suorittaminen on turvallisuuspainajainen - SQL on deklaratiivinen ja hiekkalaatikkomainen, Python ei.

## Miksi DuckDB

[DuckDB](https://duckdb.org/) on erilainen. Se kyselee CSV-tiedostoja *suoraan* - ei tuontiaskelta, ei latausta muistiin:

```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
```

Tappajan ominaisuus: **se käsittelee tiedostoja taulukoina**. Osoita se CSV-, Parquet- tai JSON-kansioon ja tiedustele välittömästi. Ei CREATE TABLETTIA, ei irtonumeroa, ei odottelua.

Apulainen SQLite DuckDB
|--------|-----------|--------|--------|
| **Muisti** Lataa koko tiedoston Lataukset tuonnin aikana Virrat levyltä
| **Asetukset** Ei mitään Schema + tuonti Ei yhtään
| **500MB-tiedosto** ~2GB RAM Minuuttia pikatuontiin
| **5GB-tiedosto** OOM-romahdus Erittäin hidas Toimii hyvin
| **Parketti** Kyllä (10-100x nopeammin)

DuckDB on se, mitä datainsinöörit käyttävät Pythonissa juuri tässä käyttökotelossa. [.NET-sidokset](https://github.com/Giorgi/DuckDB.NET) Saat täyden ADO.NET-tuen - se tuntuu miltä tahansa muulta tietokannasta, paitsi että kyselet tiedostoja.

## Stack

Komponentti Miksi tämä?
|-----------|--------------|
| [DuckDB](https://duckdb.org/) CSV:n suora kysely, ei tuontivaihetta
| [DuckDB.NET](https://github.com/Giorgi/DuckDB.NET) Täysi ADO.NET-tuki, tuntuu alkuperäiseltä ADO.NET-tuelta
| [Ollama](https://ollama.ai/) Paikallinen päätelmä, ei API-avaimia, ei pilviä
| [Bogus](https://github.com/bchavez/Bogus) Realistisia testitietoja missä tahansa mittakaavassa
| [`qwen2.5-coder:7b`](https://ollama.ai/library/qwen2.5-coder) Paras SQL-tarkkuus 7B-koossa

> **Turvallisuutta koskeva huomautus**: Suoritamme LLM:n tuottamaa SQL:tä. Tämä on turvallisempaa kuin mielivaltainen koodi, mutta vaatii silti vahvistusta. [Turvallisuusosasto](#security-considerations) Lisää keskustelua profiilien muuttamisesta LLM-rajapintaan ja CLI:hen, joka toteuttaa nämä kuviot, katso seurakappaleesta **[Datakehittäjä: Nopea paikallinen dataprofilointi](/blog/datasummarizer-how-it-works)**.

## Projektin asetukset

Tehdään näyteprojekti. Asenna NuGet-paketit:

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

Ota käyttöön koodattu malli, joka on hyvä SQL:ssä:

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

## Arkkitehtuuri

Näin palaset sopivat yhteen:

```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
```

Avainnäkemys: annamme LLM:lle **Skeema- ja otostiedot**Tämä pitää kontekstin pienenä ja reagoi nopeasti.

**Miksi tällä on merkitystä**: LLM luo aiesopimuksen (SQL). DuckDB toteuttaa sen. Validointivaihe nappaa syntaksivirheet ennen suoritusta. Uudelleenryhmittelysilmukka käsittelee satunnaisen virheen. Tämä ero tekee järjestelmästä sekä turvallisen että tarkan. Tarkemman, CLI-keskeisen viittauksen (mukaan lukien profiili-ensikerronta ja turvalliset SQL-toteutusrajat). **[Datakehittäjä: Nopea paikallinen dataprofilointi](/blog/datasummarizer-how-it-works#the-key-upgrade-statistics-as-the-interface)**.

## Vaihe 1: Luo testitiedot Boguksella

> **Onko CSV-tietoja jo olemassa?** Siirry kohteeseen [Vaihe 2: Muodostelman kontekstin rakentaminen](#step-2-build-the-schema-context).

Ennen kuin voimme testata LLM-käyttöistä CSV-analysaattoriamme, tarvitsemme dataa analysoitavaksi. Kehittämistä ja testausta varten synteettinen data päihittää todellisen datan:

1. **Mittakaavatestaus** - Luo 100K-, 1M- tai 10M-rivit erikokoisille suorituksille
2. **Yksityisyys** - Ei riskiä paljastaa todellista asiakas-/yritystietoa demoissa tai kuvakaappauksissa
3. **Toistuvuus** - Sama siemen = sama data, mikä tekee vioista toistettavissa
4. **Edge-kotelot** - Hallita jakelua (esim. pakottaa 5% palautukset, tietyt päivämäärät)

### Mikä Bogus on?

[Bogus](https://github.com/bchavez/Bogus) Se tuottaa realistisen näköisiä väärennettyjä tietoja - nimiä, osoitteita, sähköposteja, päivämääriä, numeroita - oikealla paikallisella tuella. Sen sijaan, että Bogus käyttäisi käsintestattavia CSV-tiedostoja tai satunnaisia jätetietoja, hän antaa sinulle tietoja, jotka *näyttää* Real:

- `f.Name.FullName()` → "John Smith" (ei "asdf1234")
- `f.Internet.Email()` → "john.smith@gmail.com" (asianmukaisesti muotoiltu)
- `f.Date.Between(start, end)` → Realistinen treffijakelu
- `f.Commerce.ProductName()` → "Handcrafted Granite Cheese" (hauska, mutta tunnistettava)

Tällä on merkitystä, koska realististen tietojen avulla huomaat asioita - outoa muotoilua, odottamattomia yhdistelmiä, treffien reuna-asioita - joita satunnaiset narut piilottaisivat.

### Määrittele datamalli

```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; }
}
```

### Määrittele Faker-asetukset

Bogus käyttää sujuvaa API-rajapintaa määritelläkseen tuotantosäännöt:

```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));
```

Selvitetään, mitä tapahtuu:

- **`f` (Faker)** - Generaattorin instanssi, jossa on pääsy kaikkiin datamoduuleihin (Nimi, Päiväys, Random jne.)
- **`f.Random.Guid().ToString()[..8]`** - Luo GUID, mutta ota vain ensimmäiset 8 merkkiä luettavaan tilaustunnisteeseen
- **`f.Date.Between()`** - Satunnaispäivämäärä realistisella vaihteluvälillä (ei vuosi 9999)
- **`f.PickRandom(array)`** - Valitse ennalta määritellyistä vaihtoehdoista sattumanvaraisesti (varmistaa kelvolliset luokat)
- **`f.Random.Bool(0.3f)`** - 30 prosentin todennäköisyys pitää paikkansa (30 % tilauksista saa alennusta)
- **`(f, s)` syntaksi** Tämä mahdollistaa sekä väärennöksen että osittain rakennetun levyn käytön. `ProductName` riippuu `Category`

Erytropoietiini `(f, s)` Malli on tehokas - se tarkoittaa, että "Electronicsin" tilaukset saavat elektroniikan tuotenimiä, eivät satunnaisia kohteita. Johdonmukaisuus tekee tuotetusta datasta paljon realistisempaa aggregaatioiden, kuten "tulot luokittain", testaamiseen.

### Luo ja kirjoita CSV:lle

```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-rivit tuottavat noin 15MB CSV:tä, mutta voit helposti skaalautua miljooniin. Sukupolvi on nopea (noin 2 sekuntia 100K-riville), koska Bogus on optimoitu irtotavarasukupolveen.

> **Vinkki**: Aseta `Randomizer.Seed = new Random(12345)` Ennen kuin saadaan toistettavissa olevaa dataa. Sama siemen = sama "satunnainen" tietueet joka kerta, mikä on korvaamatonta vianetsintää varten.

## Vaihe 2: Muodostelman kontekstin rakentaminen

Ennen kuin LLM pystyy luomaan SQL:n, sen on ymmärrettävä datarakennetta. Otamme tämän DuckDB:stä:

### Kontekstimalli

```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.
}
```

Tämä kuvaa kaikkea LLM:n tarvitsemaa: sarakenimiä, tyyppejä ja muutamia näyterivejä datamuodon ymmärtämiseksi.

### Hermostoa vedetään pois

DuckDB voi kuvata minkä tahansa CSV:n lataamatta kaikkea:

```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;
}
```

Erytropoietiini `DESCRIBE` Komento lukee vain tiedoston otsikon ja muutaman rivin tyypin päättelyä varten - se on instant jopa valtavista tiedostoista.

### Näytetietojen saaminen

Näyterivit auttavat LLM:ää ymmärtämään dataformaatteja (päivämäärät, henkilötunnukset jne.):

```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);
}
```

Kolme riviä riittää yleensä - se näyttää LLM:lle, mitä formaatteja odottaa tuhlaamatta rahakkeita.

## Vaihe 3: Luo SQL LLM:llä

**Tämä on vaikeinta.** Nopeasta suunnittelusta täällä ei neuvotella - ilman tiukkoja sääntöjä paikalliset LLM:t tuottavat luovaa mutta rikkinäistä SQL:tä. Tavoitteena on determinismi, ei luovuus.

### Promptin rakentaminen

```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}");
    }
```

Sääntöosio on ratkaisevan tärkeä - se kertoo LLM:lle tarkalleen, miten DuckDB:n kysely muotoillaan. Selkeys syntaksista (LIMIT vs. TOP, merkkijono konketaatio) estää yhteiset virheet.

### Näytetietojen lisääminen

```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)}}}");
        }
    }
```

### Virheen toipuminen

Jos aiempi yritys epäonnistui, mainitse virhe:

```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();
}
```

Tämä uudelleenyrittämismekanismi on tärkeä - paikalliset LLM:t tekevät joskus syntaksivirheitä, ja virheiden antaminen heille yleensä korjaa sen toisella yrityksellä.

### LLM:n kutsuminen

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

Erytropoietiini `StreamToEndAsync()` Saat paremman UX:n, voit streamata kuponkeja, kun ne saapuvat.

### Vastauksen puhdistaminen

LLM:t käärivät usein SQL:n markdown-koodin lohkoihin, vaikka niitä on kielletty:

```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');
}
```

## Vaihe 4: Validointi ennen toimeenpanoa

DuckDB:n `EXPLAIN` Tarkistetaan SQL-syntaksi ilman kyselyä:

```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;
    }
}
```

Jos validointi epäonnistuu, syötämme virheen takaisin LLM:ään ja yritämme uudelleen (rajaan asti).

## Vaihe 5: Suorita ja muotoile tulokset

Lopuksi suorita kysely ja muotoile tuloste:

```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;
}
```

Erytropoietiini `QueryResult` Luokkaan (joka esitetään kokonaisuudessaan otantaprojektissa) kuuluu `ToString()` menetelmä, joka muotoilee tuloksia luettavana taulukona.

## Keskustelukontekstin lisääminen

Vuorovaikutteista analyysia varten käyttäjät haluavat usein esittää jatkokysymyksiä:

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

Toisessa ja kolmannessa kysymyksessä on järkeä vain ensimmäisen kysymyksen kontekstin kanssa.

### Keskustelun historian seuraaminen

```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"
}
```

### Historia mukaan lukien pikavippeinä

```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();
    }
}
```

Historia antaa LLM:n kontekstille mahdollisuuden ymmärtää sellaisia viittauksia kuin "that", "ne tulokset" tai "hajota sitä pidemmälle".

## Mitä mallia tulee käyttää?

SQL-sukupolvelle koodauspainotteiset mallit toimivat parhaiten. [Ollaman mallikirjasto](https://ollama.ai/library):

Malli Koko Nopeus Laatu Linkki
|-------|------|-------|---------|------|
| `qwen2.5-coder:7b` "4.7GB" "Nopeasti" "Erinomaisesti" [Ollama](https://ollama.ai/library/qwen2.5-coder) |
| `deepseek-coder-v2:16b` Keskikokoinen 9GB Paras [Ollama](https://ollama.ai/library/deepseek-coder-v2) |
| `codellama:7b` 4GB Nopeaa hyvää [Ollama](https://ollama.ai/library/codellama) |
| `llama3.2:3b` 2GB Erittäin nopea Hyväksyttävä [Ollama](https://ollama.ai/library/llama3.2) |

Suurimmassa osassa käyttökohteita: **`qwen2.5-coder:7b`** osuma makeaan paikkaan - tarkka SQL, hyvä vauhti, kulkee vaatimattomalla laitteella (8GB+ RAM).

## Suorituskyky

### Reaalimaailman esikuvat

Testi 100K-rivin CSV-tiedostolla (14MB) tavallisella dev-koneella (Ryzen 5, NVME SSD, 32GB RAM):

Kyselyn tyyppi Aika
|------------|------|
Yksinkertaista summaa 65 ms
YHDISTYNYT KUNINGASKUNTA 58-68
Monimutkaista aggregaatiota FILTERin kanssa 63 ms
Monipöytäinen ryhmä 71 ms:n kanssa

Alle 100 ms analyyttisiin kyselyihin 100K-riveillä - ilman tuontivaihetta. Kyselyn monimutkaisuudella on muutakin merkitystä kuin rivilaskenta; DuckDB:n sarakemoottori käsittelee yhdistelmiä tehokkaasti tiedostojen koosta riippumatta.

### Muuta suuret tiedostot parquetiksi

Yli 1GB:n tiedostojen osalta: [Parquet-muoto](https://parquet.apache.org/) on 10-100x nopeampi:

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

Pakattu Parquet-tiedosto on myös paljon pienempi.

### Turvallisuusnäkökohtia

Suorittaessaan LLM:n tuottamaa SQL:tä:

```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));
}
```

DuckDB:n muistitila tarjoaa myös luonnollisen eristyksen - se ei voi vaikuttaa tuotantotietokantoihin.

## Täydellinen esimerkki

Näin asiat järjestyvät:

```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?"));
```

## Yhteenveto

Mielenterveysmalli, joka pitää: **LLM:n järki; tietokannat laskevat.**

Älä syötä dataa LLM:lle. Syötä se schema, anna sen tuottaa SQL, suorita SQL oikean kyselymoottorin kanssa. Tämän vuoksi lähestymistapa toimii mittakaavassa.

Täytäntöönpano:

1. **DuckDB** kysyy suoraan CSV:ltä - ei tuonti, virrat levyltä
2. **Schema + näytteet** antaa LLM:lle riittävästi kontekstia paljastamatta tietoja
3. **Tiukat ja nopeat säännöt** Voimadeterministinen SQL, ei luova proosa
4. **Validointi EXPLAIN-sovelluksella** saalisvirheet ennen toteutusta
5. **Yritä uudelleen virhepalautteella** käsittelee satunnaista syntaksiliuskaa

Tulos: alle 100 ms:n analyyttiset kyselyt miljoonan rivin tiedostoista, täysin pois päältä, ja dataa, joka ei koskaan poistu koneesta.

Koko otosprojekti on saatavilla osoitteessa [Enimmäkseen lucid.CsvLlm](https://github.com/scottgal/mostlylucidweb) - sisältää `CsvQueryService`, `ConversationalCsvAnalyser`, ja Bogus-pohjainen tiedontuotanto.

## Resurssit

### DuckDB

- [DuckDB-dokumentaatio](https://duckdb.org/docs/) - Täydet referenssit
- [DuckDB:n CSV-tuonti](https://duckdb.org/docs/data/csv/overview.html) - CSV-ominaisuudet
- [DuckDB:n SQL-viite](https://duckdb.org/docs/sql/introduction) - SQL-syntaksien erot muista tietokannoista
- [DuckDB.NET GitHub](https://github.com/Giorgi/DuckDB.NET) - C#-sidokset
- [DuckDB.NET NuGet](https://www.nuget.org/packages/DuckDB.NET.Data.Full) - Täysi paketti alkuperäiskansojen binäärien kanssa

### Ollama & LLMs

- [Ollama](https://ollama.ai/) - Paikallinen LLM-ajoaika
- [Ollaman mallikirjasto](https://ollama.ai/library) - Saatavilla olevat mallit
- [OllamaSharp](https://github.com/awaescher/OllamaSharp) C#-asiakaskirjasto
- [OllamaSharp NuGet](https://www.nuget.org/packages/OllamaSharp/)

### Testaa datan muodostusta

- [Bogus GitHub](https://github.com/bchavez/Bogus) - Väärennetyn datan generaattori
- [Bogus API -viite](https://github.com/bchavez/Bogus#bogus-api-support) - Saatavilla olevat tietotyypit

### Vaihtoehtoja mainittu

- [CsvHelper](https://joshclose.github.io/CsvHelper/) - CSV:n jäsennyskirjasto
- [Microsoft.Data.Analyysi](https://www.nuget.org/packages/Microsoft.Data.Analysis) DataFrame for .NET
- [PandasAI](https://github.com/Sinaptik-AI/pandas-ai) - Python LLM + pandojen integraatio

### Aiheeseen liittyvät artikkelit

- [Datakehittäjä: Nopea paikallinen dataprofilointi](/blog/datasummarizer-how-it-works) - Companion-artikkeli, jossa käsitellään CLI-pohjaista dataprofilointia