This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Hur man analyserar stora CSV-Dater med Lokala LLM i C#
Thursday, 18 December 2025
Serien: Lokala LLMs för data - Del | 1 | |
Här är det misstag som alla gör. De försöker mata sin CSV till en LLM. LLMs borde generera frågor , inte konsumera data.
Du' har en 500MB CSV-Datei och vill fråga: " Vad är den genomsnittliga inställningsvaluten per region? Kopilot i Excel kan göra detta, men vad händer om dina data är för känsliga för molntjänster?
Den här artikeln visar hur - lokalt , privatt
Använd DuckDB för att fråga CSV-Dater direkt. Använd en lokal LLM för att generera SQL . Den LLM ser aldrig dina data | - bara schemat |. Resultat ♫ : ♫ sub ♫ МSK5 ♫ ms frågor på miljontals ♫
För en komplementär treatment som expanderar på användandet av statistikprofiler som LLM-gränssnittet ( och visar ett komplett verktyg för att implementera dessa idéer | - | profilering |, | säker SQL-modus | МSK5 | syntetiskt kloning och driftdetektion DataSummarizer: Vinniga lokaldataprofilering - i synnerhet i sektionen "Kysyluppgraderingen | : | Statistik som gränssnittet | МSK3 | De här två artiklarna utgör en kort serie om praktisk lokal LLM
Kerninsikten
Att använda en LLM som dataspeicher är fel abstraktion.
Det korrekta mönstret: LLM anledningar , databasberäkningar.
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
Lägg märke till vad som händer.', :, LLM genererar en SQL-fråga baserat på din fråga och schemat.., DuckDB löser den mot den faktiska datan.M SK3, LDM rör aldrig vid dina data. -, det ser bara kolumnernas namn och typer.
För mer information om hur man hanterar profile som LLM-gränssnittet ( och en konkret CLI som implementerar profilen -first narration, säkra SQLM SK3bevarad Q~&A+,registret-bevarade sessioner och syntetiskt kloning~ ),~ se DataSummarizer: Vinniga lokaldataprofilering.
Varför inte bara ladda in den i minnet?
De uppenbara tillvägagångssätten har samma dödliga brist
CsvHelper / Dataframer: Lade hela filen in i RAM. En 500MB CSV blir 2-4GB av objekt
SQLite / PostgreSQL: kräver en långsam importerad steg (minuter för stora filer M SK2 framfronts schemadefinitioner , och databashantering överhuvudtaget
PandasAI: Den laddar fortfarande allt i minnet . Plus
Varför DuckDB
DuckDB är annorlunda. Den frågar CSV-filer direkt - ingen importerad steg , inget laddande i minnet
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
Killer Featuren: den behandlar filer som tabeller. Närma den till en CSV , Parquet M SK2 eller JSON-fiel och fråga direkt . Inga skapadestablar
| Faktor | CsvHelper | SQLite m | DuckDB M | ||||
|---|---|---|---|---|---|---|---|
| minne | Lader hela filen | Lader under import | Stromer från disken | ||||
| Konfiguration | Inget | Schema | + | Import | МSK3 | Inget | |
| 500MB-Datei | ~2 GB RAM | Mina minuter att importera | |||||
| 5GB-Datei | OOMkraschen | Väldigt långsamt ♫ ♫ | ♫ Fungerar bra ♫ | ||||
| Parquet | Nej | Nej |
DuckDB är vad dataingenjörer använder i Python för exakt detta användandefall .NET bindinger ge dig full ADO.NET support - det känns som vilken annan databas som helst
Stanken
| Komponent ♫ | ♫ Varför den här ♫ |
|---|---|
| DuckDB | Fråga CSV direkt , ingen införingsteg |
| DuckDB.NET | Full ADO. Net supportM SK2 känns inhemskt |
| Ollama | Lokalt utgångssätt , inga API-kyler M SK2 ingen moln |
| Bogus | Realistiska tester på alla skalor |
qwen2.5-coder:7b |
Den bästa SQL-tegenskapen på 7B-storlek |
Beteckning om säkerhet: Vi executerar LLM-genererad SQLM SK3 Det är säkrare än godtyckligt kodt , men kräver fortfarande validering säkerhetssektionen för säkerhetsåtgärder . För en bredare diskussion om att omvandla profiler till LLM-gränssnittet och en CLI som implementerar dessa mönster DataSummarizer: Vinniga lokaldataprofilering.
Projektuppsättning
Låt oss skapa ett exempelprojekt.
dotnet add package DuckDB.NET.Data.Full
dotnet add package OllamaSharp
dotnet add package Bogus
Ta fram en kodning, -, som fokuserar på modellen och som är bra på SQL.
ollama pull qwen2.5-coder:7b
Architekturen
Här' är hur de delar passar ihop
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
Den viktigaste insikten vi ger LLM schema och proverdata, inte den faktiska datan . Detta gör att kontexten är liten och reaktioner snabbar
Varför är detta viktigt?: LLM genererar intentionen | ( | SQL | МSK2 | DuckDB löser det | . | Valideringsstegen fångar syntaktiska fel innan Executionen ♫ . | Retry-loopet hanterar slumpmässiga fel |. | Det är denna separation som gör systemet både säkert och korrekt ♫ DataSummarizer: Vinniga lokaldataprofilering.
steg 1: Generera testerdata med Bogus
Har redan CSV data? Spring till steg 2: bygga schemakontexten.
Innan vi kan testa vår LLM, behöver vi data att analysera.
- Skala tester - Generera ♫ 100 ♫ K ♫ , ♫ ♫ МSK3 ♫ mm ♫M, ♫ eller ♫ M10 ♫ rader för att kontrollera effekten på olika storlekar
- Privatliv - Inga risker för att exponera riktiga kunder / affärsdata i demo eller screenshots
- Reproduktivitet - Samma frö = samma data, gör att bugs är återskapbara
- Randfall - Kontrollera fördelningen | | e | . | g | мSK3 | kraft | МSK4 | återspeglar |, | specifika datumsträngar
Vad är Bogus?
Bogus är en .NET port av den populära fakern.js biblioteketM SK2 Den genererar realistiskaMST3liknande falska data MST4 namnM ST5 adressernaM st6 e-posterMst7 datumM St8 siffror | MST9 med korrekt lokalisering | МST10 | Istället för att göra test CSV-Dater eller använda slumpmässiga skräpdata |MST12 | Bogus ger dig data som ser ut riktiga:
f.Name.FullName()→ "John SmithM SK2 ♫ ♫ ( ♫f.Internet.Email()→f.Date.Between(start, end)→ Distribution av realistiska datumf.Commerce.ProductName()→ "Handgjorda granitiskt ost" | | ( roligtM SK4 men igenkännande
Detta spelar roll, eftersom realistiska data hjälper dig att upptäcka problem - konstiga formateringar , oväntade aggregationer
Definiera datamodellen
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; }
}
Konfigurera Fakern
Bogus använder en flytande API för att definiera generregler:
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));
Låt oss bryta ner vad som händer
f(Faker) - Genereringsinstancen med tillgång till alla datamoduler | | namn | , | datum |, | slumpmässigt | , etc | SMK5f.Random.Guid().ToString()[..8]- Generera en GUID men ta bara först 8 tecken för en läsbar ordningsIDf.Date.Between()- Lukraket datum inom en realistisk intervall inte årf.PickRandom(array)- Välj slumpmässigt från förutdefinierade alternativ bestämdar giltiga kategorierf.Random.Bool(0.3f)- 30% chans att riktiga order blir beskömda(f, s)syntax - Zugriff både på faker och på de delvis konstruerade rekorden -ProductNameberoende avCategory
Den (f, s) mönster är kraftfulla - det betyder "Elektronics" order får elektroniksprodukternas namnM SK3 inte slumpmässiga prylarMSC4 Den här konsekvencen gör datan som genereras mycket mer realistisk för att testa aggregationer som |
Generera och skriva till CSV
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 rader genererar ungefär 15MB av CSV | - | tillräckligt för att testa med |, | men man kan enkelt skala upp till miljoner |
Tipp: Set
Randomizer.Seed = new Random(12345)innan man genererar för att få återskapbara data
steg 2: bygga schemakontexten
Innan LLM kan generera SQL, måste den förstå datastrukturen.
Kontextmodellen
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.
}
Det fångar allt som LLM behöver, columns namn, ,, typer och några mönster rader för att förstå dataformatet.
Att extrahera schemat
DuckDB kan beskriva vilken CSV som helst utan att ladda den alla
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;
}
Den DESCRIBE kommandon läser bara fileheader plus några rader för typsuppfattning - det 's omedelbart även på stora filer.
Att få proverdata
Sammlingraden hjälper LLM att förstå dataformat (dataM SK1 IDs , etc.):
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 rader är vanligtvis tillräckligt. - den visar LLM vilka format man ska förvänta sig utan att slösa bort token.
steg 3: Generera SQL med LLM
Det här är den hårda delen Det snabbaste ingenjörskonst här är inte
Att bygga upp springen
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äcken om regler är avgörande - den berättar för LLM exakt hur man formaterar queryn för DuckDB . Att vara tydlig med syntaxen | | ( | LIMIT vs TOP
Att lägga till proverdata
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)}}}");
}
}
Fout återhämta
Om ett tidigare försök misslyckades
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();
}
Den här upprepningsmekanismen är viktig. Lokala LLM gör ibland syntaktiska fel och genom att ge dem felet löser den ofta vid det andra försöket
Att kalla LLM
var request = new GenerateRequest { Model = _model, Prompt = prompt };
var response = await _ollama.GenerateAsync(request).StreamToEndAsync();
var sql = CleanSqlResponse(response?.Response ?? "");
Den StreamToEndAsync() väntar på den kompletta svaret. För en bättre UX, skulle man kunna skicka ut token när de kommer fram
Att rena reaktionen
LLM wrapar ofta SQL i markerdown-kodblock trots att de fått höra att inte göra det.
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');
}
steg 4: Validatera före att utföra
DuckDB's EXPLAIN låter oss kontrollera SQL syntax utan att köra query:
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;
}
}
Om bekräftelset misslyckas, skickar vi felet tillbaka till LLM och testar igen (till en gräns
steg 5: utföra och formatera resultat
Slutligen, köra queryn och formatera utgången
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;
}
Den QueryResult klass ( fullkomligt visat i exemplet projekt ToString() metod som formaterar resultat som en läsbar tabell.
Att lägga till konversationskontext
När det gäller interaktiv analys vill användare ofta ställa frågor som följer:
"What's the total revenue?"
→ "Break that down by region"
→ "Show the top 5 regions"
De andra och tredje frågorna är logiska bara i sammanhanget från den första
Att följa konversationers historia
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"
}
Inklusive historia i prompt
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();
}
}
Historien ger LLM: s kontext för att förstå referenser som "som ", | | " dessa resultat
Vilken modell att använda?
För SQL-generationen, kodering - fokuserade modeller fungerar bäst. Här är de tillgängliga alternativen via Ollamas modellbibliotek:
| Modell | Storlek | Rymden | МSK3 | Jakt | ||||
|---|---|---|---|---|---|---|---|---|
qwen2.5-coder:7b |
4.7GB | snabb | Utmärkt | Ollama | ||||
deepseek-coder-v2:16b |
9 GB | Medel | bäst | Ollama | ||||
codellama:7b |
4GB | snabb | Bra | Ollama | ||||
llama3.2:3b |
Ollama |
För de flesta användande fall, qwen2.5-coder:7b träffar den bästa punkten. - korrekt SQL.
Förmågor
Real-World Benchmarks
Testande på en 100K rad CSV-Datei (14MB) på en standard- dev-maskin | | (Ryzen | МSK4 | NVMe SSD |
| frågeställningstyp | tid |
|---|---|
| Enkel siffra ♫ ♫ | ♫ |
| GROUP BY med SUM | 58-68ms |
| Kompleks aggregation med FILTER | 63ms |
| Multi -table GROUP BY with HAVING |
Sub-100ms för analytiska queryer på | 100 | K rader |- | utan någon importerad steg | МSK3 | Query-komplexiteten är viktigare än radsräkningen ♫ ; | DuckDB | ' | vår pilarmotor hanterar agregationer effektivt oavsett filestorlek ♫
Konvertera stora filer till Parquet
För filer över 1GB, Parquet-format är 10-100x snabbareM SK1
using var cmd = connection.CreateCommand();
cmd.CommandText = $"COPY (SELECT * FROM '{csvPath}') TO '{parquetPath}' (FORMAT PARQUET)";
cmd.ExecuteNonQuery();
Den komprimerade Parquet-fiilen är också mycket mindre.
säkerhetsfrågor
När man sätter igång LLM
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's i memory-modus ger också naturlig isolering - den kanM SK3 inte påverka dina produktionsdatabaser
Ett komplett exempel
Här är "'", det är hur allt går ihop.
// 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?"));
sammanfattning
Den mentala modellen för att hålla LLMs skäl; databasernas beräkningar
Sorg inte data till LLM. Sorg det schemat. Schick det ,. Sorg att det genererar SQL. ,. Släng det SQL mot en äkta sökmotor.
Implementeringen:
- DuckDB queries CSV direkt - inga importer , strömmar från disket
- Schema + prover ge LLM tillräckligt med kontext utan att avslöja data
- Strict prompt-regler kraft deterministisk SQL, inte kreativ prosa
- Validering via EXPLAIN fångar fel före uitvoeringen
- Försök igen med fel återkoppling hanterar den slumpmässiga syntaxen
Resultatet: sub-100 ms analytiska queryr på miljontals filer - rader files M SK3 helt offline , med data som aldrig lämnar din maskin
Det kompletta exemplaret är tillgängligt Mostlylucid.CsvLlm - inkluderar CsvQueryService, ConversationalCsvAnalyser, och Bogus
resurser
DuckDB
- DuckDB Dokumentation - Hela referensen
- DuckDB CSV-import Specifiska kännetecken
- DuckDB SQL-referens - SQL-syntaxer skiljer sig från andra databaser
- DuckDB.NET GitHub - CM SK1 bindningar
- DuckDB.NET NuGet - Hela paketet med binärsystem
Ollama & LLMs
- Ollama - Lokal LLM körtid
- Ollama Modellbiblioteket - tillgängliga modeller
- Ollamascharp - C# klientbibliotek
- OllamaSharp NuGet
Testa datagenerering
- Bogus GitHub - Fake datagenerator
- Bogus API-referens - tillgängliga datatyper
Alternativer Beskrivna
- CsvHelper - CSV-testningsbibliotek
- Microsoft.Data - Dataframem för .NET
- PandasAI - Python LLM + integration av pander
Related Articles
- DataSummarizer: Vinniga lokaldataprofilering - Kompanionsartikel om CLI-baserad dataprofilering