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
Thursday, 18 December 2025
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ä 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 Nämä kaksi artikkelia muodostavat lyhyen sarjan käytännön paikallisista LLM + kyselymalleista.
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.
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.
Ilmiselvissä lähestymistavoissa on sama kohtalokas puute:
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**Lisäksi LLM:n luoman mielivaltaisen koodin suorittaminen on turvallisuuspainajainen - SQL on deklaratiivinen ja hiekkalaatikkomainen, Python ei.
DuckDB on erilainen. Se kyselee CSV-tiedostoja suoraan - ei tuontiaskelta, ei latausta muistiin:
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 Saat täyden ADO.NET-tuen - se tuntuu miltä tahansa muulta tietokannasta, paitsi että kyselet tiedostoja.
Komponentti Miksi tämä?
|-----------|--------------|
| DuckDB CSV:n suora kysely, ei tuontivaihetta
| DuckDB.NET Täysi ADO.NET-tuki, tuntuu alkuperäiseltä ADO.NET-tuelta
| Ollama Paikallinen päätelmä, ei API-avaimia, ei pilviä
| Bogus Realistisia testitietoja missä tahansa mittakaavassa
| qwen2.5-coder:7b 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 Lisää keskustelua profiilien muuttamisesta LLM-rajapintaan ja CLI:hen, joka toteuttaa nämä kuviot, katso seurakappaleesta Datakehittäjä: Nopea paikallinen dataprofilointi.
Tehdään näyteprojekti. Asenna NuGet-paketit:
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ä:
ollama pull qwen2.5-coder:7b
Näin palaset sopivat yhteen:
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 otostiedotTä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.
Onko CSV-tietoja jo olemassa? Siirry kohteeseen Vaihe 2: Muodostelman kontekstin rakentaminen.
Ennen kuin voimme testata LLM-käyttöistä CSV-analysaattoriamme, tarvitsemme dataa analysoitavaksi. Kehittämistä ja testausta varten synteettinen data päihittää todellisen datan:
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() → "[email protected]" (asianmukaisesti muotoiltu)f.Date.Between(start, end) → Realistinen treffijakeluf.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.
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; }
}
Bogus käyttää sujuvaa API-rajapintaa määritelläkseen tuotantosäännöt:
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 tilaustunnisteeseenf.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 CategoryErytropoietiini (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.
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.
Ennen kuin LLM pystyy luomaan SQL:n, sen on ymmärrettävä datarakennetta. Otamme tämän DuckDB:stä:
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.
DuckDB voi kuvata minkä tahansa CSV:n lataamatta kaikkea:
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äyterivit auttavat LLM:ää ymmärtämään dataformaatteja (päivämäärät, henkilötunnukset jne.):
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.
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.
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.
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)}}}");
}
}
Jos aiempi yritys epäonnistui, mainitse virhe:
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ä.
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.
LLM:t käärivät usein SQL:n markdown-koodin lohkoihin, vaikka niitä on kielletty:
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');
}
DuckDB:n EXPLAIN Tarkistetaan SQL-syntaksi ilman kyselyä:
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).
Lopuksi suorita kysely ja muotoile tuloste:
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.
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.
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"
}
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".
SQL-sukupolvelle koodauspainotteiset mallit toimivat parhaiten. Ollaman mallikirjasto:
Malli Koko Nopeus Laatu Linkki
| ------- | ------ | ------- | --------- | ------ |
|---|---|---|---|---|
deepseek-coder-v2:16b Keskikokoinen 9GB Paras Ollama |
||||
codellama:7b 4GB Nopeaa hyvää Ollama |
||||
llama3.2:3b 2GB Erittäin nopea Hyväksyttävä Ollama |
Suurimmassa osassa käyttökohteita: qwen2.5-coder:7b osuma makeaan paikkaan - tarkka SQL, hyvä vauhti, kulkee vaatimattomalla laitteella (8GB+ RAM).
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.
Yli 1GB:n tiedostojen osalta: Parquet-muoto on 10-100x nopeampi:
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.
Suorittaessaan LLM:n tuottamaa SQL:tä:
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.
Näin asiat järjestyvät:
// 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?"));
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:
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 - sisältää CsvQueryService, ConversationalCsvAnalyser, ja Bogus-pohjainen tiedontuotanto.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.