<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Melih Birim</title>
    <description>The latest articles on DEV Community by Melih Birim (@melih_birim_8363f43966da2).</description>
    <link>https://dev.to/melih_birim_8363f43966da2</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4054380%2Fbcc4e0c4-4c9a-4a88-bbca-b10273fd4709.jpg</url>
      <title>DEV Community: Melih Birim</title>
      <link>https://dev.to/melih_birim_8363f43966da2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/melih_birim_8363f43966da2"/>
    <language>en</language>
    <item>
      <title>For Structured Data, Retrieval Accuracy Isn't a Model Problem</title>
      <dc:creator>Melih Birim</dc:creator>
      <pubDate>Thu, 13 Aug 2026 13:30:03 +0000</pubDate>
      <link>https://dev.to/melih_birim_8363f43966da2/for-structured-data-retrieval-accuracy-isnt-a-model-problem-54h3</link>
      <guid>https://dev.to/melih_birim_8363f43966da2/for-structured-data-retrieval-accuracy-isnt-a-model-problem-54h3</guid>
      <description>&lt;p&gt;MongoDB shipped three retrieval features this week: context-aware embeddings, hybrid search, and a native reranker. &lt;/p&gt;

&lt;p&gt;The pitch is the same one every vector database is making right now: &lt;strong&gt;retrieval accuracy is the thing that determines whether an agent's next step is right or wrong, and getting it wrong is expensive twice over, once for the bad action and again for the retry.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That framing is correct. It's also a framing built entirely around unstructured data, and it quietly assumes retrieval has to be approximate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why retrieval is probabilistic at all
&lt;/h2&gt;

&lt;p&gt;Vector search returns the &lt;em&gt;k&lt;/em&gt; passages whose embeddings are closest to the query's embedding. &lt;/p&gt;

&lt;p&gt;"Closest" is a distance in a learned space, not a guarantee of relevance. &lt;/p&gt;

&lt;p&gt;Two chunks can sit near each other in that space and still not answer the question, which is exactly why reranking exists as a second pass: a bigger model re-scores the same candidates because the first pass wasn't reliable enough on its own. &lt;/p&gt;

&lt;p&gt;Hybrid search exists because vector similarity alone misses exact terms like a product SKU or an error code. &lt;/p&gt;

&lt;p&gt;Context-aware chunking exists because splitting a document into pieces throws away the surrounding meaning each piece depended on.&lt;/p&gt;

&lt;p&gt;All three of those are real, useful fixes. They're also all patches on the same underlying fact*&lt;em&gt;&lt;em&gt;: when your data is unstructured text, there is no query that deterministically identifies "the right answer." You can only get closer to it.&lt;/em&gt;&lt;/em&gt;*&lt;/p&gt;

&lt;h2&gt;
  
  
  A table doesn't have that problem
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SELECT dept, AVG(salary) FROM employees WHERE region = 'West' GROUP BY dept&lt;/code&gt; doesn't return the four passages that are probably relevant. &lt;/p&gt;

&lt;p&gt;It returns the rows that satisfy the predicate, with 100% recall and 100% precision, every time, because the question and the data share the same structure. &lt;/p&gt;

&lt;p&gt;There's no embedding to be almost-right in the middle of. There's no reranking step, because there's nothing to rerank, the WHERE clause already decided.&lt;/p&gt;

&lt;p&gt;This is why &lt;a href="https://github.com/melihbirim/csvql" rel="noopener noreferrer"&gt;csvql&lt;/a&gt;'s benchmarking has always focused on two things: does the query return the exact same result set DuckDB does, and how many tokens does an agent spend getting that result. &lt;/p&gt;

&lt;p&gt;Accuracy was never the axis we had to optimize, because for tabular data, exact retrieval isn't a hard problem someone solved with a bigger model. &lt;/p&gt;

&lt;p&gt;It's what a WHERE clause has always done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the two worlds actually meet
&lt;/h2&gt;

&lt;p&gt;None of this means vector search is wrong for what it's for. &lt;/p&gt;

&lt;p&gt;A support ticket, a contract, a Slack thread, those are unstructured, and "closest passage" is a genuinely reasonable strategy when there's no schema to query against. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mistake is applying that strategy to data that already has one.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;A CSV of orders, a Parquet file of transactions, a SQL export of user events, these don't need semantic retrieval at all. &lt;/p&gt;

&lt;p&gt;They need a query engine an agent can call directly, cheaply, and get back an exact answer.&lt;/p&gt;

&lt;p&gt;The token cost of a wrong retrieval compounds the same way in both worlds, an extra round trip, a retry, a chain of tokens that never needed to be spent. &lt;/p&gt;

&lt;p&gt;The difference is what fixes it. &lt;/p&gt;

&lt;p&gt;For unstructured data, the fix is a better model. &lt;/p&gt;

&lt;p&gt;For structured data, the fix already existed. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's SQL.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>sql</category>
      <category>csv</category>
      <category>agents</category>
    </item>
    <item>
      <title>One dev.to Comment on a Benchmark Post Turned Into 18 Filed Bugs</title>
      <dc:creator>Melih Birim</dc:creator>
      <pubDate>Mon, 03 Aug 2026 21:00:00 +0000</pubDate>
      <link>https://dev.to/melih_birim_8363f43966da2/one-devto-comment-on-a-benchmark-post-turned-into-18-filed-bugs-5f8l</link>
      <guid>https://dev.to/melih_birim_8363f43966da2/one-devto-comment-on-a-benchmark-post-turned-into-18-filed-bugs-5f8l</guid>
      <description>&lt;p&gt;I published a &lt;a href="https://dev.to/melih_birim_8363f43966da2/i-benchmarked-my-csv-engine-against-duckdb-on-its-own-dataset-it-found-two-bugs-in-mine-e3e"&gt;benchmark post&lt;/a&gt; comparing csvql against DuckDB on the NYC taxi dataset. &lt;a href="https://dev.to/mads_hansen_27b33ebfee4c9"&gt;Mads Hansen &lt;/a&gt;left this comment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The most valuable result here may be the two correctness bugs, because differential testing gives you an oracle while the implementation is still small enough to reason about. I'd keep DuckDB in CI as a semantic reference and generate adversarial CSV fixtures: quoted newlines, escaped delimiters, empty versus NULL, Unicode, exponent notation, NaN/Infinity, timestamp offsets, and group cardinalities large enough to force spills. Compare normalized result sets and error classes, not just elapsed time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I checked what csvql's existing correctness suite actually covered. It diffed csvql against DuckDB, but only on business-style data: names, cities, departments, salaries. No quotes, no embedded newlines, no unicode, nothing adversarial. Every check passed because nothing in the fixtures could fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  What five test rows found
&lt;/h2&gt;

&lt;p&gt;I built a small CSV with a quoted field containing an embedded newline, a doubled quote, and a unicode name, and ran it through csvql instead of the usual clean data. Two bugs showed up before I'd even finished writing the fixture.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LENGTH()&lt;/code&gt; on a field like &lt;code&gt;"has ""quotes"" inside"&lt;/code&gt; came back wrong. The zero-copy parser found the correct closing quote but never collapsed the doubled &lt;code&gt;""&lt;/code&gt; down to a single &lt;code&gt;"&lt;/code&gt;, so the stored value was longer than it should have been.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SUM()&lt;/code&gt; was worse. A row with an embedded newline in a quoted field caused the aggregate to silently drop that row's value from the total. No error, no crash, just a wrong number. Plain projection of the same column returned the correct value, so the bug was specific to how the aggregation scan split rows, not how it read them.&lt;/p&gt;

&lt;p&gt;Both went into a CI job that runs DuckDB as an oracle on every push, so they can't come back unnoticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second pass found more, and worse
&lt;/h2&gt;

&lt;p&gt;Fixing those two led to a closer look at how CSV quoting was handled everywhere else in the codebase. The same class of bug showed up repeatedly, because quote-handling logic had been implemented separately in several places instead of shared:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The mmap and bulk scanners toggled quote state on &lt;em&gt;any&lt;/em&gt; quote character, not just one at the start of a field, so a value like &lt;code&gt;abc"def&lt;/code&gt; inside an unquoted field could throw off everything after it.&lt;/li&gt;
&lt;li&gt;Header parsing didn't respect quotes at all when splitting column names.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IN (...)&lt;/code&gt; list parsing split on raw commas, breaking on any value like &lt;code&gt;'New York, NY'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A fixed 8KB stack buffer for DISTINCT deduplication silently truncated longer rows and could falsely collapse two different rows into one.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;parseIntFast&lt;/code&gt; had no overflow check, so a 20+ digit number could silently corrupt under a release build instead of failing.&lt;/li&gt;
&lt;li&gt;An ORDER BY sort key was recorded as a live slice into a growable arena buffer, a genuine use-after-free once the arena reallocated mid-scan.&lt;/li&gt;
&lt;li&gt;A duplicate header column name (two columns both called &lt;code&gt;id&lt;/code&gt;) silently returned whichever one happened to be inserted into the lookup table last, not the first, with no indication anything was ambiguous.&lt;/li&gt;
&lt;li&gt;A UTF-8 BOM, which Excel writes by default when you export "CSV UTF-8", glued onto the first column name and broke every reference to that column.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these needed a large file or a strange query. All of them needed data slightly outside what the original test suite had ever generated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;p&gt;18 issues filed since that comment, 12 already fixed and shipped across two point releases. 6 are still open: a documentation-only issue about intentional differences from DuckDB (byte length vs character count, that kind of thing), a known gap where the four parallel worker threads used for files over 10MB still don't have the same quote-aware parsing as the single-threaded paths, and a handful of smaller ones (&lt;code&gt;SUM&lt;/code&gt;/&lt;code&gt;AVG&lt;/code&gt; returning 0 instead of NULL on an empty result set, a bare &lt;code&gt;CASE WHEN&lt;/code&gt; outside an aggregate failing with a misleading error, a negative &lt;code&gt;LIMIT&lt;/code&gt; silently being treated as unlimited).&lt;/p&gt;

&lt;p&gt;The parallel-path gap is the one I'm least comfortable rushing. Fixing it means touching four independent threaded scan functions, each with its own I/O buffer boundary handling, and getting that wrong under load is worse than leaving it as a known, documented limitation for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual lesson
&lt;/h2&gt;

&lt;p&gt;None of this was exotic. It was one reader pointing out that a correctness suite built entirely from clean, well-formed business data will pass no matter how many real bugs are sitting in the parser, because it never exercises the code paths those bugs live in. The fix wasn't a smarter test, it was a meaner one.&lt;/p&gt;

&lt;p&gt;If you want to see the current adversarial fixtures or run them yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/melihbirim/csvql
&lt;span class="nb"&gt;cd &lt;/span&gt;csvql &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; zig build &lt;span class="nt"&gt;-Doptimize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ReleaseFast
./bench/verify_correctness.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It runs on every push now. If you find a fixture it should have but doesn't, that's exactly the kind of comment that started this.&lt;/p&gt;

</description>
      <category>csvql</category>
      <category>duckdb</category>
      <category>development</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Benchmarked My CSV Engine Against DuckDB on Its Own Dataset. It Found Two Bugs in Mine.</title>
      <dc:creator>Melih Birim</dc:creator>
      <pubDate>Thu, 30 Jul 2026 07:03:05 +0000</pubDate>
      <link>https://dev.to/melih_birim_8363f43966da2/i-benchmarked-my-csv-engine-against-duckdb-on-its-own-dataset-it-found-two-bugs-in-mine-e3e</link>
      <guid>https://dev.to/melih_birim_8363f43966da2/i-benchmarked-my-csv-engine-against-duckdb-on-its-own-dataset-it-found-two-bugs-in-mine-e3e</guid>
      <description>&lt;p&gt;&lt;em&gt;csvql queries raw CSV ~2.8x faster than DuckDB on an 8 GB file, using ~6x less memory and zero extra disk — and the benchmark that proved it also surfaced two real bugs I'd never have found otherwise.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup: query the same raw CSV, on the same machine
&lt;/h2&gt;

&lt;p&gt;DuckDB publishes &lt;a href="https://duckdb.org/2024/10/16/driving-csv-performance-benchmarking-duckdb-with-the-nyc-taxi-dataset" rel="noopener noreferrer"&gt;its own NYC-taxi CSV benchmark&lt;/a&gt;. That makes it the perfect yardstick: I can't be accused of misconfiguring the competitor when I'm running the competitor's own dataset with the competitor's own queries.&lt;/p&gt;

&lt;p&gt;The dataset lives on DuckDB's own blob storage (&lt;code&gt;blobs.duckdb.org/data/nyc-taxi-dataset&lt;/code&gt;). Each file is 20 million rows, 51 columns, ~8 GB uncompressed. The four queries are the canonical &lt;a href="https://github.com/pdet/taxi-benchmark" rel="noopener noreferrer"&gt;"Billion Taxi Rides"&lt;/a&gt; aggregations — &lt;code&gt;GROUP BY&lt;/code&gt; counts and averages over cab type, passenger count, year, and rounded trip distance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The one rule that makes or breaks a benchmark like this:&lt;/strong&gt; both engines must do the &lt;em&gt;same work&lt;/em&gt;. DuckDB's headline numbers come in two flavors — "with storage" (after loading the CSV into DuckDB's native columnar format) and "without storage" (querying the raw CSV each time). &lt;a href="https://github.com/melihbirim/csvql" rel="noopener noreferrer"&gt;csvql&lt;/a&gt; always queries raw CSV directly; it has no native store. So the fair fight is &lt;strong&gt;both engines reading the raw CSV, cold, every run&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;csvql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;csvql&lt;/span&gt; &lt;span class="nv"&gt;"SELECT ... FROM 'trips.csv' ..."&lt;/span&gt;
&lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_csv_auto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'trips.csv'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;   &lt;span class="c1"&gt;-- NOT a preloaded table&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comparing csvql-on-raw-CSV against DuckDB-on-preloaded-store would be dishonest, and anyone who opened the script would say so. Direct vs direct is the only comparison that survives scrutiny.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Machine:&lt;/strong&gt; Apple Silicon, macOS. &lt;strong&gt;DuckDB:&lt;/strong&gt; v1.4.2. &lt;strong&gt;Method:&lt;/strong&gt; best-of-5 runs per query, warm OS page cache (both engines equally).&lt;/p&gt;




&lt;h2&gt;
  
  
  Speed: ~2.8x on 8 GB
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;csvql&lt;/th&gt;
&lt;th&gt;DuckDB&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q01 &lt;code&gt;COUNT(*) GROUP BY cab_type&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.29 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3.55 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.8x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q02 &lt;code&gt;AVG(total_amount) GROUP BY passenger_count&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.41 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3.81 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.7x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q03 &lt;code&gt;COUNT(*) GROUP BY passenger_count, year&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.36 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4.01 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.9x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q04 &lt;code&gt;GROUP BY passenger_count, year, ROUND(distance)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.38 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3.99 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.9x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Query latency, lower is better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        0s        1s        2s        3s        4s
Q01 csvql  ██████▌ 1.29
    duck   ██████████████████ 3.55
Q02 csvql  ███████ 1.41
    duck   ███████████████████ 3.81
Q03 csvql  ██████▊ 1.36
    duck   ████████████████████ 4.01
Q04 csvql  ██████▉ 1.38
    duck   ████████████████████ 3.99
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the counterintuitive part: &lt;strong&gt;the gap shrinks as files get bigger.&lt;/strong&gt; On a 417 MB / 1M-row sample, csvql is ~10x faster. On 8 GB, ~2.8x. Why? On small files DuckDB's process startup and CSV-reader initialization dominate the wall clock, and csvql's lean startup wins big. On large files the actual parse-and-aggregate throughput dominates, DuckDB's parallel CSV reader catches up, and csvql settles into a steady ~2.8x. Both numbers are true; the honest thing is to publish the whole curve with file sizes attached, not cherry-pick the 10x.&lt;/p&gt;




&lt;h2&gt;
  
  
  Memory: ~6x less
&lt;/h2&gt;

&lt;p&gt;Same 8 GB file, peak memory footprint per query:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;csvql&lt;/th&gt;
&lt;th&gt;DuckDB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q01&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;29 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;178 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q02&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;30 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;210 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q03&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;34 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;208 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q04&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;38 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;219 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Peak memory (MB), 8 GB file
csvql   █▍                          ~30 MB
duckdb  ██████████████████████████  ~200 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;csvql streams the file through a memory-mapped scan and keeps only the group-by hash table in memory — a few dozen groups. It never holds more than a sliver of the 8 GB at once. That's why a 30 MB footprint can chew through an 8 GB file.&lt;/p&gt;




&lt;h2&gt;
  
  
  Storage: the number that actually matters
&lt;/h2&gt;

&lt;p&gt;This is where "raw CSV, no store" stops being a fairness caveat and becomes the whole point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Extra disk required to answer these queries on the 8 GB file:

csvql   0 bytes        — queries the CSV in place, no ingest
duckdb  2.1 GB + 21.7s — must build a native store for its fast path
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DuckDB &lt;em&gt;can&lt;/em&gt; be faster than these raw-CSV numbers — but only via its "with storage" path, which first ingests the CSV into a 2.1 GB native store, a one-time cost of ~22 seconds for a single 8 GB file (and minutes for the full multi-file dataset). csvql needs &lt;strong&gt;zero extra disk and zero ingest&lt;/strong&gt; to hit its numbers. For an ad-hoc query against a CSV that landed on your disk five minutes ago, "no ingest" is the difference between an answer now and an answer after a coffee break.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest summary:&lt;/strong&gt; same answers, ~2.8x faster, ~6x less memory, zero extra disk, zero ingest.&lt;/p&gt;




&lt;h2&gt;
  
  
  The test I &lt;em&gt;couldn't&lt;/em&gt; run — and why that's the honest part
&lt;/h2&gt;

&lt;p&gt;You'll notice I didn't put csvql's numbers next to DuckDB's &lt;em&gt;published&lt;/em&gt; figures (their blog reports Q01–Q04 at 2.45 / 3.89 / 5.21 / 11.2 s without storage). It would have looked great — csvql's ~1.3 s next to their 11.2 s on Q04 is a juicy 8x. It would also have been junk.&lt;/p&gt;

&lt;p&gt;Two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Different dataset size.&lt;/strong&gt; DuckDB's published query numbers are on the &lt;em&gt;full&lt;/em&gt; ~1.1-billion-row dataset (all 65 files, ~500 GB of uncompressed CSV). Mine are on a single 20-million-row file. That's a 55x difference in rows. Putting them in the same table would be comparing csvql-on-20M against DuckDB-on-1.1B — the benchmarking equivalent of a fixed fight. (You can see the size gap in their own Q04: 11.2 s on 1.1B rows vs the ~4 s my local DuckDB takes on 20M.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Different hardware.&lt;/strong&gt; Their numbers are from an M1 Pro; mine are from a different Apple Silicon box. Borrowing a competitor's number measured on other silicon and dividing it by yours isn't a benchmark, it's a wish.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To &lt;em&gt;legitimately&lt;/em&gt; cite DuckDB's published figures, csvql would have to run the identical ~1.1-billion-row dataset — which means ~500 GB of uncompressed CSV on disk. My machine has 275 GB free. It physically won't fit. So instead of borrowing numbers across hardware and data sizes, I ran &lt;strong&gt;both engines on the same 20 M rows on the same machine with the same OS cache state.&lt;/strong&gt; That controls for everything the cross-hardware comparison doesn't. It's a smaller, less flashy result — and a far more trustworthy one.&lt;/p&gt;

&lt;p&gt;If someone with a 1 TB disk wants to run the full 500 GB and report csvql against DuckDB's exact published table, the script is one flag away (&lt;code&gt;bench_taxi.sh 65&lt;/code&gt;). I'd love to see it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The plot twist: the benchmark found two bugs in &lt;em&gt;my&lt;/em&gt; engine
&lt;/h2&gt;

&lt;p&gt;I built the benchmark to measure speed. It ended up as the best test suite csvql ever had, because running real queries on real data immediately broke two of them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug #1 — &lt;code&gt;GROUP BY ROUND(trip_distance)&lt;/code&gt; crashed with &lt;code&gt;ColumnNotFound&lt;/code&gt;.&lt;/strong&gt; Grouping by a &lt;em&gt;string&lt;/em&gt;-producing function (&lt;code&gt;STRFTIME&lt;/code&gt;) worked; grouping by a &lt;em&gt;numeric&lt;/em&gt;-producing one (&lt;code&gt;ROUND&lt;/code&gt;) didn't — the group-key resolver simply had no case for it. Q04 wouldn't run at all until I added one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug #2 — &lt;code&gt;ORDER BY COUNT(*) DESC&lt;/code&gt; silently returned unsorted rows.&lt;/strong&gt; No error, no crash — just wrong output. The &lt;code&gt;GROUP BY&lt;/code&gt; code path sorted results by the internal hash key and ignored the &lt;code&gt;ORDER BY&lt;/code&gt; clause entirely. This is the scariest class of bug: the kind that returns a confident, wrong answer. A benchmark that only checked timings would have sailed right past it; comparing &lt;em&gt;output&lt;/em&gt; against DuckDB caught it in one diff.&lt;/p&gt;

&lt;p&gt;Both are now fixed, each with a regression test, and the four canonical queries produce byte-identical results to DuckDB (modulo &lt;code&gt;1&lt;/code&gt; vs &lt;code&gt;1.0&lt;/code&gt; float formatting). The moral: &lt;strong&gt;a benchmark you check against a trusted oracle is a correctness test wearing a stopwatch.&lt;/strong&gt; If I'd only ever run csvql against itself, both bugs would still be there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reproduce it yourself
&lt;/h2&gt;

&lt;p&gt;Everything is in the repo. No 500 GB required — the sample mode pulls ~417 MB and runs in seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/melihbirim/csvql
&lt;span class="nb"&gt;cd &lt;/span&gt;csvql &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; zig build &lt;span class="nt"&gt;-Doptimize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ReleaseFast

&lt;span class="c"&gt;# quick: 1M-row sample (~417 MB)&lt;/span&gt;
./bench/bench_taxi.sh &lt;span class="nt"&gt;--sample&lt;/span&gt;

&lt;span class="c"&gt;# full: 20M rows (~8 GB download from DuckDB's blobs)&lt;/span&gt;
./bench/bench_taxi.sh 1

&lt;span class="c"&gt;# memory / CPU / storage instead of speed&lt;/span&gt;
./bench/bench_taxi.sh &lt;span class="nt"&gt;--resources&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script downloads DuckDB's own dataset, runs both engines on the raw CSV, and prints the tables above. It's ~130 lines of bash — read it before you trust it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Benchmark direct-vs-direct or don't bother.&lt;/strong&gt; The single most important line in the whole exercise was refusing to compare raw-CSV csvql against preloaded DuckDB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish the whole curve.&lt;/strong&gt; ~10x on 400 MB and ~2.8x on 8 GB are both true. Hiding one to headline the other is how benchmarks earn their bad reputation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The interesting number isn't always speed.&lt;/strong&gt; ~6x less memory and &lt;em&gt;zero ingest&lt;/em&gt; is a bigger deal for ad-hoc CSV work than shaving two seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Point your benchmark at a trusted oracle.&lt;/strong&gt; It doubles as the correctness test you were too lazy to write. It found two bugs I'd have shipped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;csvql is open source (Zig, single static binary, &lt;a href="https://github.com/melihbirim/csvql" rel="noopener noreferrer"&gt;github.com/melihbirim/csvql&lt;/a&gt;). If you run the full dataset, tell me what you get.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Numbers: Apple Silicon M2Pro, macOS, DuckDB v1.4.2, best-of-5, warm cache. Dataset and queries: &lt;a href="https://github.com/pdet/taxi-benchmark" rel="noopener noreferrer"&gt;DuckDB's NYC-taxi benchmark&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>csv</category>
      <category>duckdb</category>
      <category>database</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
