<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Oxbow Research]]></title><description><![CDATA[Oxbow Research publishes independent performance and pricing analysis of data platforms. 
Research also reviews market trends, vendor strategy, and deep dives on historical companies.]]></description><link>https://oxbowresearch.com</link><image><url>https://substackcdn.com/image/fetch/$s_!Jrj8!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67131e49-c71d-4f97-b9d3-0079a8c8bd20_512x512.png</url><title>Oxbow Research</title><link>https://oxbowresearch.com</link></image><generator>Substack</generator><lastBuildDate>Thu, 16 Apr 2026 15:41:16 GMT</lastBuildDate><atom:link href="https://oxbowresearch.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Joe Harris]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[joeharris76@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[joeharris76@substack.com]]></itunes:email><itunes:name><![CDATA[Joe Harris]]></itunes:name></itunes:owner><itunes:author><![CDATA[Joe Harris]]></itunes:author><googleplay:owner><![CDATA[joeharris76@substack.com]]></googleplay:owner><googleplay:email><![CDATA[joeharris76@substack.com]]></googleplay:email><googleplay:author><![CDATA[Joe Harris]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Benchmark with AI (pt3/3): How I actually use AI for platform evaluation]]></title><description><![CDATA[AI agents are useful for platform evaluation when execution is delegated carefully and judgment stays with the human.]]></description><link>https://oxbowresearch.com/p/benchmark-with-ai-pt33-how-i-actually</link><guid isPermaLink="false">https://oxbowresearch.com/p/benchmark-with-ai-pt33-how-i-actually</guid><dc:creator><![CDATA[Joe Harris]]></dc:creator><pubDate>Wed, 18 Mar 2026 12:39:42 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!InN9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbec4af65-8743-4d2c-b538-0fca6cf99ed5_1800x692.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>The right mental model</h2><img style="" src="https://substackcdn.com/image/fetch/$s_!InN9!,w_1100,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbec4af65-8743-4d2c-b538-0fca6cf99ed5_1800x692.png" alt="How I actually use AI for platform evaluation" title="" data-component-name="ImageToDOM"><p><strong>TL; DR</strong>: Use agents for research, boilerplate, and benchmark execution. Keep methodology design and result interpretation for yourself; those require judgment the tools can't encode.</p><div><hr></div><p>After cataloging how agents fail and building the tools that prevent it, I should be clear: I use AI coding agents for platform evaluation every day. They're very useful for specific tasks. The previous posts aren't arguments against using agents but for using them <em>correctly</em>.</p><p>The right mental model: an AI agent is a tireless research assistant with excellent reading comprehension, infinite patience, and zero domain judgment. It will read every page of documentation you point it at, generate any boilerplate you describe, and format results beautifully; and it will just as confidently execute any methodology you give it, valid or not, without questioning whether the methodology makes sense.</p><p>The human handles methodology and interpretation while the agent handles execution and comparison. When those roles get confused; when the agent designs the methodology or the human accepts results they haven't validated; the output is unreliable.</p><p>Here's the key insight that took me a while to internalize: the division isn't "human does hard things, agent does easy things." Some of what agents do well is hard (synthesizing documentation across 15 platform pages, for instance). And some of what humans must do is simple (choosing a scale factor appropriate for your hardware). The division is: <em>humans make judgment calls, agents execute defined procedures.</em> Judgment is about context, trade-offs, and "what matters here," while execution is about doing the defined thing correctly and completely.</p><div><hr></div><h2>Where agents excel</h2><p>I'm not going to be coy about this: AI agents are very good at several parts of platform evaluation. Here's where I rely on them heavily.</p><h3>Platform discovery and feature comparison</h3><p>This is the single highest-value use case. When evaluating a new database platform, I need to understand: What data formats does it support? What SQL dialect? What indexing options? What's the concurrency model? What are the scaling limits? How does pricing work?</p><p>Gathering this from documentation used to take me the better part of a day per platform. With an AI agent, it's a 20-minute conversation. "Read the ClickHouse documentation and summarize: supported data types, indexing mechanisms, replication model, and known limitations for analytical workloads." For a first pass, the summaries are usually accurate enough because this is reading comprehension, not judgment, and they're often more thorough than my manual scan would be, because the agent doesn't get bored on page 47 of the docs.</p><p>For building an initial feature matrix across 5 platforms, agents save me 3-4 days of documentation grinding with higher coverage. I still verify specific claims that seem surprising or that I'll rely on for decisions. But the first pass is agent territory.</p><h3>Boilerplate generation</h3><p>Connection setup, schema creation, driver configuration, environment scripts, Docker Compose files for multi-platform testing, all mechanical and well-defined. An agent producing a DuckDB connection class or a PostgreSQL schema from the TPC-H specification is doing translation work, not judgment work. The output is either correct (matches the spec) or incorrect (doesn't match), and correctness is easily verified.</p><h3>Result formatting and documentation</h3><p>After a BenchBox run produces raw timing data, the mechanical work of formatting it into comparison tables, calculating QphH@Size, computing percentage differences, and generating markdown summaries is tedious and error-prone when done manually. I give the agent raw CSV output and ask for a formatted comparison table. When the input is clean BenchBox output, the arithmetic is usually reliable and the formatting is cleaner than my manual first pass. This saves 30-60 minutes per benchmark report.</p><h3>Cross-platform syntax translation</h3><p>"Translate this PostgreSQL query to ClickHouse SQL, noting any semantic differences in how window functions or date arithmetic work." Agents often catch subtle dialect differences, INTERVAL syntax, type casting semantics, and NULL handling variations that I'd miss on a manual first pass. This is linguistic pattern matching, a bounded task they usually handle well.</p><h3>Exploratory queries and environment setup</h3><p>"What happens if I run this query with a hash join hint vs. a merge join hint?" Agents are useful for generating query variants, executing them, and reporting differences. This is exploration, not measurement, so the numbers don't need to be publication-quality. Similarly, "set up a DuckDB instance with the TPC-H extension, generate SF-1 data, verify all tables loaded correctly" is a defined procedure with clear success criteria that agents usually handle well when I verify the output.</p><div><hr></div><h2>Where human judgment is irreplaceable</h2><p>Here's the flip side, the tasks where delegating to an agent produces confident-sounding garbage.</p><h3>Methodology design</h3><p>"What should I measure, and how?" This is the fundamental question of any evaluation, and it has no universal answer. It depends on your workload, your scale, your budget, your team's expertise, and your existing infrastructure.</p><p>An agent asked "what benchmark should I run to evaluate DuckDB vs. PostgreSQL for my use case?" will give you an answer, usually "TPC-H," but it can't know whether your workload is actually decision-support, whether your data volumes match any standard scale factor, or whether concurrency testing matters for your deployment.</p><p>I design methodology myself, every time. Here's what happens when I don't:</p><pre><code>Me: I need to evaluate Snowflake vs. a local DuckDB instance for
    a warehouse-scale analytics workload. What scale factor should I use?

Agent: For a meaningful comparison, I recommend SF-1. This provides
       enough data to exercise the query engine while keeping
       execution times reasonable for iterative testing.</code></pre><p>SF-1 is roughly 1GB of data<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>. On Snowflake, or any distributed system with 128GB of RAM per node, SF-1 often fits entirely in memory on a <em>single</em> node - the distribution mechanism is never exercised. On the DuckDB side, it also understates the scale if the real question is "what happens at warehouse size?" You'd need much larger scale factors before the workload actually stresses the system in a meaningful way. The agent defaulted to "reasonable for testing" when I needed "meaningful for production evaluation."</p><h3>Result interpretation</h3><p>"DuckDB is 2.3x faster than PostgreSQL on TPC-H Q6 at SF10." What does this mean for your production decision? Nothing, yet. Is Q6 representative of your workload? Does SF10 match your data volume? Was PostgreSQL tuned? Does the gap hold at higher concurrency?</p><p>Agents will confidently recommend platforms that are wrong for the stated requirements. I've collected these examples:</p><pre><code>Me: Recommend a database for concurrent-write analytics.
    Multiple users updating dashboards simultaneously.

Agent: DuckDB would be excellent for this. It's optimized for
       analytical queries and has a simple deployment model.</code></pre><p>DuckDB allows multiple readers, but writing from multiple processes to the same database file isn't supported; only one process can read/write at a time<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. The agent matched "analytics" to DuckDB without understanding the architectural constraint.</p><pre><code>Me: I need to analyze 50MB of CSV files on my laptop.

Agent: Snowflake offers excellent performance for analytical
       workloads with minimal setup. Their free trial...</code></pre><p>Snowflake's XS warehouse bills 1 credit per hour<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>. The dollar price per credit varies by edition and region, so the absolute cost depends on your account and cloud. For 50MB of data that DuckDB handles in milliseconds for free, the agent recommended a cloud service because it pattern-matched "analytics" without considering scale appropriateness.</p><p>Result interpretation requires domain expertise and business context intersecting simultaneously, and no tool or template replaces that.</p><h3>Platform-specific tuning decisions</h3><p>How much should I tune each platform? Default settings produce an "out of box" comparison that's easy to reproduce but potentially unfair to well-tuned platforms. Aggressive tuning produces better numbers but introduces expertise-dependence.</p><p>"How fast is Platform X if I invest a week in tuning?" is a completely different question from "How fast is Platform X with my team's current expertise?" Both are valid. Choosing between them requires understanding your audience and purpose, judgment that agents can't provide because they don't know your team.</p><h3>Validity assessment</h3><p>"Are these results actually measuring what I think?" A benchmark that runs 10x faster than expected might mean: the platform is excellent, the data didn't load correctly, a query used a cached result, the scale factor was too small, or a query hit an optimization that won't apply to real data. Distinguishing between these requires understanding what's plausible for the platform's architecture and the specific query pattern.</p><div><hr></div><h2>The workflow I actually use</h2><p>Here's the five-phase workflow I've refined over a year of AI-assisted platform evaluation with BenchBox. Each phase has a clear human/agent division.</p><ol><li><p><strong>Discovery</strong>: Agent reads docs and produces feature comparison tables. I define the candidate list, verify surprising claims, and eliminate deal-breakers. Output: shortlist of 2-4 platforms.</p></li><li><p><strong>Methodology</strong>: No agent involvement. I choose benchmarks, scale factors, tuning level, success criteria, and run protocol. Output: a methodology document specifying exactly what to run and how.</p></li><li><p><strong>Execution</strong>: Agent runs benchmarks via BenchBox MCP. Data generation, qualification, warm-up, substitution parameter rotation, and metric calculation are all handled automatically. The agent can't skip steps because the tool doesn't expose "skip qualification" as an option. I review qualification results and check for anomalies.</p></li><li><p><strong>Interpretation</strong>: Agent formats comparison tables and calculates per-query differences. I interpret results in context (is the winner right <em>for my workload</em>?), assess methodology, identify follow-up questions, and make the recommendation.</p></li><li><p><strong>Documentation</strong>: Agent drafts report structure with data tables and reproducibility section. I write the analysis, verify claims match the data, and add caveats the agent wouldn't know to include.</p></li></ol><div><hr></div><h2>The detection paradox</h2><p>The uncomfortable truth behind everything I just described: users delegate benchmark tasks to AI agents <em>because</em> they lack benchmark expertise. But detecting invalid agent output <em>requires</em> benchmark expertise.</p><p>This creates a circular dependency. If you knew enough about TPC-H methodology to catch the agent's mistakes, you probably wouldn't need the agent to run the benchmark. If you don't know enough, you can't catch the mistakes. The knowledge gap that makes delegation attractive is the same gap that makes validation impossible; which is exactly why tool-level constraints matter more than user-level expertise.</p><p>The SSB test from <em>When AI agents are confident and wrong</em> demonstrates the failure mode directly. That post includes the original prompts, generated scripts, and the methodology breakdown. Both agents would have submitted their results without flagging a single methodology issue. Claude Opus's 679-line script would have printed timing tables for a workload built from random data. Codex's would have reported elapsed milliseconds with no composite metric. If either had been used for a platform decision, the decision would have rested on numbers that measured nothing; and nothing in the output would have indicated that.</p><p>The fundamental issue: <strong>detection requires the expertise users don't have, presented in a format that conceals the absence of that expertise.</strong></p><div><hr></div><h2>A taxonomy of plausible garbage</h2><p>Not all garbage looks the same. Understanding the categories helps explain why detection is so difficult, and what to look for.</p><h3>Structurally plausible</h3><p>The format is correct but the content isn't.</p><p><strong>Example</strong>: A report labeled "QphH@10: 42,150" where the number was calculated as arithmetic mean of elapsed times instead of geometric mean of per-query throughput scaled to hourly rate.</p><p>The label matches what you'd expect. The number has the right magnitude. But the calculation is wrong, and the result isn't comparable to any other TPC-H benchmark. You'd need to know the QphH formula to recognize the error. Most users don't.</p><h3>Numerically plausible</h3><p>The numbers are in reasonable ranges, but they don't measure what they claim to measure.</p><p><strong>Example</strong>: Query times from random data masquerading as TPC-H times. Q6 reports 0.089 seconds instead of 0.34 seconds because the random data produced different selectivity, executing against fewer rows.</p><p>If you see a time like 0.089 seconds for Q6 at SF-10, that's a red flag. It might be real elapsed time, but it likely reflects the wrong workload because the data distributions are wrong. You'd need to know expected ranges for TPC-H queries at SF-10 to notice that.</p><h3>Methodologically plausible</h3><p>The terminology is correct but the procedure is invalid.</p><p><strong>Example</strong>: A report stating "Qualification: PASSED (22/22 queries validated)" where "validation" meant "returned non-empty results" rather than "matched SF-1 reference answers."</p><p>The agent used the right word. Qualification is a real TPC-H concept. But the agent invented a definition that satisfied the linguistic requirement without satisfying the methodological one. You'd need to know what TPC-H qualification actually means.</p><h3>Comparatively plausible</h3><p>The rankings match expectations but the magnitudes are fabricated.</p><p><strong>Example</strong>: A report showing "DuckDB: QphH 42,150 / PostgreSQL: QphH 18,230" where DuckDB really is faster for analytical queries, but neither number was calculated correctly.</p><p>The relative ordering is defensible. DuckDB does outperform PostgreSQL on OLAP workloads. An expert might look at this and think "sounds about right." But "sounds about right" isn't "is right." Confirmation bias makes this category dangerous.</p><div><hr></div><h2>Red flags that something went wrong</h2><p>After a year of this workflow, I've learned to recognize when agent-produced output is noise rather than signal. If you see any of these, stop and investigate before using the results.</p><h3>Suspiciously round numbers</h3><p>Real benchmark results are messy; query times like 7-11ms, 9-17ms, 9-13ms across runs. Nothing rounds to a clean number. Compare to what I got from an unconstrained agent:</p><pre><code>TPC-H Results (SF-10):
  QphH: 10,000
  Q1: 1.00s | Q2: 0.50s | Q3: 0.75s | Q4: 0.25s | ...
  All queries completed successfully.</code></pre><p>Every number is round. The QphH equals the scale factor times 1,000; a formula that doesn't exist in the TPC-H spec. This output was the agent generating plausible-looking data rather than measuring anything.</p><h3>Results that exactly match expectations</h3><p>If every platform performs exactly as you predicted; DuckDB wins everything by exactly 2x, PostgreSQL wins nothing; be skeptical. Real benchmarks produce surprises. In my experience, there's always at least one query where the "slower" platform wins, usually due to a specific optimizer behavior or data access pattern.</p><h3>Other flags from <em>When AI agents are confident and wrong</em></h3><ul><li><p><strong>Missing variance</strong>: Single-run timing with no repetition or standard deviation</p></li><li><p><strong>Scale factor mismatch</strong>: SF-1 on a 128GB machine; you're benchmarking RAM, not the query engine</p></li><li><p><strong>No warm-up or qualification</strong>: Straight from data load to timing, skipping both</p></li><li><p><strong>Identical results across runs</strong>: Cache effects or duplicated output rather than independent measurement</p></li><li><p><strong>Query times that are too fast</strong>: Wrong data distributions producing easier workloads</p></li><li><p><strong>Wrong metric</strong>: Elapsed seconds instead of the benchmark's defined composite (QphH@Size, Power@Size)<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a></p></li></ul><div><hr></div><h2>Series conclusion</h2><p>"Be more careful" isn't a solution; users can't detect what they don't have expertise to recognize. The structural fixes from <em>Giving agents knowledge instead of freedom</em>; validated inputs, workflow templates, and structured errors; address the supply side, and that post walks through the tool constraints in detail. But the detection paradox above explains why the demand side matters too: tools need to validate <em>output</em>, not just input. Mandatory methodology metadata and confidence indicators let users assess reliability without becoming benchmarking experts themselves.</p><p>AI agents optimize for <em>successful execution</em>, not <em>measurement validity</em>. Everything that looks like success; code that runs, numbers that appear, professional formatting; can be achieved without producing valid measurement.</p><p>The workflow that avoids this: use agents for research, boilerplate, formatting, and benchmark execution. Put structured tools between the agent and the results; anything that encodes correct methodology rather than hoping the agent recalls it. And keep the judgment calls for yourself: what to measure, what the numbers mean, what decision they support.</p><p>If you take one thing from this series: write the methodology document before the agent touches anything. An agent executing a defined plan produces valid execution, but an agent designing its own methodology produces confident-sounding garbage.</p><div><hr></div><p><em>This concludes the "AI Agents and Database Benchmarking" series. For more on BenchBox's methodology and how Oxbow Research uses it for platform evaluation, see the "Introducing Oxbow Research" series.</em></p><div><hr></div><h2>Footnotes</h2><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p><a href="https://docs.snowflake.com/en/user-guide/warehouses-overview">Overview of Warehouses</a> - Snowflake, accessed 2026-02-02. X-Small (XS) warehouse consumes 1 credit per hour.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p><a href="https://duckdb.org/docs/connect/concurrency">Concurrency</a> - DuckDB, accessed 2026-02-02. Only one process can write to a database at a time; multiple readers are supported.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p><a href="https://www.tpc.org/tpc_documents_current_versions/pdf/tpc-h_v3.0.1.pdf">TPC-H Specification v3.0.1</a> - TPC, accessed 2026-02-02. Clauses 5.4.1-5.4.3 define Power, Throughput, and the composite QphH@Size metric.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p><a href="https://www.tpc.org/tpc_documents_current_versions/pdf/tpc-h_v3.0.1.pdf">TPC-H Specification v3.0.1</a> - TPC, accessed 2026-02-02. Clause 4.2.5.1 estimates database size by scale factor (SF=1 ~1GB).</p></div></div>]]></content:encoded></item><item><title><![CDATA[Benchmark with AI (pt2/3): Giving agents knowledge instead of freedom]]></title><description><![CDATA[The fix is not a smarter prompt, but tools that make invalid benchmark methodology impossible to run.]]></description><link>https://oxbowresearch.com/p/benchmark-with-ai-pt23-giving-agents</link><guid isPermaLink="false">https://oxbowresearch.com/p/benchmark-with-ai-pt23-giving-agents</guid><dc:creator><![CDATA[Joe Harris]]></dc:creator><pubDate>Mon, 16 Mar 2026 12:37:32 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!rSyh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f26344c-6674-4f4a-a23b-5cb6fe0285cd_1800x692.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Three prompts that didn't work</h2><img style="" src="https://substackcdn.com/image/fetch/$s_!rSyh!,w_1100,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f26344c-6674-4f4a-a23b-5cb6fe0285cd_1800x692.png" alt="Giving agents knowledge instead of freedom" title="" data-component-name="ImageToDOM"><p><strong>TL; DR</strong>: I kept the benchmark task fixed and changed the interface. Unconstrained, the agent fabricated benchmark-shaped output; with BenchBox's MCP server, it followed a validated workflow with schema checks, ground-truth resources, and structured errors. If you're building agent-facing benchmark tooling, constrain the method and let the model handle the explanation.</p><div><hr></div><p>In the previous post, I identified the gap: agents don't fail at benchmarking because they're unintelligent; they fail because nothing in their environment makes invalid methodology impossible. My first instinct was to fix this with better prompts. I'm normalizing the wording below because the actual prompts varied a bit by benchmark, but the pattern was the same every time.</p><p><strong>Attempt 1</strong>: "When running a benchmark, always use the official data generator (<code>dbgen</code>, <code>ssb-dbgen</code>, <code>dsgen</code>). Never generate random data."</p><p>One model followed this instruction for exactly one session. The next time I asked for a benchmark, it checked whether the official generator was available, found it wasn't, and helpfully generated "equivalent" data using NumPy with "the same distributions the generator would produce." That is the same failure mode I showed with SSB in Post 1: once the tool is missing, the agent improvises methodology instead of stopping. Another model was more creative: for a TPC-H run, it wrote a Python script that it <em>named</em> <code>dbgen.py</code>, apparently reasoning that this satisfied the "use the official generator" constraint.</p><p><strong>Attempt 2</strong>: "Always validate benchmark results against the benchmark's reference answers before reporting performance numbers. Qualification is mandatory."</p><p>This one was more interesting. The agent acknowledged the requirement, added a "Qualification" section header to its output, ran the queries once, and wrote "All queries returned non-empty result sets. Qualification: PASSED." That's not what qualification means. For TPC-H it means comparing specific numeric outputs against known-correct answers for SF-1<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>. TPC-DS has the same general answer-validation problem, and Post 1's SSB example needed the same discipline even without the same formal TPC terminology. But the agent had never seen the actual answer set, so it invented a plausible-sounding validation procedure.</p><p><strong>Attempt 3</strong>: "Report the benchmark's defined metric, not elapsed wall-clock time."</p><p>The agent calculated a number it labeled "QphH@Size" but used arithmetic mean instead of geometric mean, didn't convert to hourly rate, and included data loading time in the calculation. The label was specific but the mistake was general: once an agent treats a benchmark metric as just another output string, it can make the same kind of mess with TPC-H, TPC-DS, or anything else.</p><p>The pattern across all three: prompts produced <em>cosmetic compliance</em>: the agent changed its output labels and added section headers, without changing the underlying methodology. It's the difference between telling someone "please don't touch the sterile field" and putting a physical barrier around it; one relies on understanding and compliance, the other makes the error impossible.</p><p>This is when I stopped trying to make agents <em>understand</em> valid methodology and started building tools that <em>enforce</em> it.</p><div><hr></div><h2>What I actually built</h2><p>BenchBox exposes its capabilities to AI agents through an MCP server, Model Context Protocol, a standard for giving AI models access to structured tools, data resources, and workflow templates<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. The key insight isn't the protocol itself, but what the protocol lets me <em>constrain</em>.</p><p>Every tool in BenchBox's MCP server encodes a principle I learned the hard way: <strong>the agent should be able to use a tool correctly without understanding why it's correct.</strong> The domain expertise lives in the tool, not in the agent's training data.</p><h3>Validated inputs: making nonsense impossible</h3><p>Here's what the <code>run_benchmark</code> tool looks like from the agent's perspective:</p><pre><code>Tool: run_benchmark
Inputs:
  platform: string (validated against known platforms)
  benchmark: string (validated against known benchmarks)
  scale_factor: number (min: 0.001, max: 10000)
  queries: array of string (validated against query ID patterns)
  phases: array of enum (generate, load, power, throughput)</code></pre><p>When an agent tries to pass "postgressql" (misspelled) or "mysql-compatible-duckdb" (hallucinated), it gets back:</p><pre><code>Error: VALIDATION_UNKNOWN_PLATFORM
Category: CLIENT (you can fix this)
Message: Platform "postgressql" not found.
Available platforms: duckdb, postgresql, clickhouse, polars-df</code></pre><p>The agent can't invent platform names. It can't use a negative scale factor or a string where a number belongs. It can't request a benchmark phase that doesn't exist. Before I built this, agents would confidently execute benchmarks against platforms that didn't exist in BenchBox, generating creative error-handling code to work around the failures. Now they can't even start down that path.</p><p>Why does bounding scale factor to 0.001-10,000 matter? Without bounds, agents request nonsensical values; zero (empty database), negative numbers, strings, or values requiring petabytes of storage. The validation prevents this by defining what "reasonable" means at the API boundary rather than asking agents to be reasonable.</p><h3>Resources: facts instead of guessing</h3><p>The second problem I needed to solve was hallucinated capabilities. Agents kept reporting platforms, benchmarks, and features that BenchBox didn't have because they were <em>recalling</em> from training data rather than <em>checking</em> reality.</p><p>BenchBox's MCP server exposes tools that provide ground truth. Here's actual output from the <code>list_platforms</code> tool (captured 2026-02-02):</p><pre><code>{
  "count": 35,
  "summary": {
    "available": 7,
    "sql_platforms": 30,
    "dataframe_platforms": 17
  }
}</code></pre><p>And from <code>system_profile</code> (captured 2026-02-02 on my local dev machine):</p><pre><code>{
  "cpu": {"cores": 10, "architecture": "arm64"},
  "memory": {"total_gb": 16, "available_gb": 3.41},
  "recommendations": {
    "max_scale_factor": 0.1,
    "notes": [
      "Scale factor 0.01 requires ~10MB RAM",
      "Scale factor 1 requires ~1GB RAM",
      "Scale factor 10 requires ~10GB RAM"
    ]
  }
}</code></pre><p>When an agent queries <code>list_platforms</code>, it gets the definitive list of 35 platforms: 7 available, 28 requiring installation. When it checks <code>system_profile</code>, it gets concrete recommendations ("max_scale_factor: 0.1" for this 16GB machine) instead of defaulting to whatever scale factor appeared in its training data. Resources replace recall with system state. That's the point.</p><h3>Workflow templates: encoding the correct sequence</h3><p>The third problem was sequence violations. Even when agents used the right tools with valid inputs, they'd skip steps, running queries before loading data, reporting results without validation, or executing a timed run without warm-up.</p><p>BenchBox's MCP server exposes prompt templates that encode complete workflows. The <code>benchmark_run</code> template defines:</p><ol><li><p>Check system resources (can this machine handle the scale factor?)</p></li><li><p>Validate platform availability (is the database installed?)</p></li><li><p>Generate data using the benchmark's official generator (not random rows that merely look plausible)</p></li><li><p>Load data with correct settings (bulk load, referential integrity)</p></li><li><p>Run result validation before timing (qualification, answer checks, whatever the workload requires)</p></li><li><p>Execute the benchmark's timed protocol (warm-up, substitution parameters, repeated runs, as applicable)</p></li><li><p>Calculate the benchmark's defined metric (correct metric, not elapsed time)</p></li><li><p>Report results with methodology metadata</p></li></ol><p>An agent following this template produces valid results not because it <em>understands</em> why validation comes before timing, but because the template puts validation before timing. The expertise is in the sequence, not the agent's comprehension.</p><p>I built a set of these templates covering the full workflow: analysis, platform comparison, regression detection, failure diagnosis, benchmark planning, execution, and platform tuning. Each encodes a procedure I'd follow manually, but in a form that agents can execute without understanding the domain rationale.</p><h3>Structured errors: teaching through failure</h3><p>The fourth problem was error recovery. Without structured feedback, agents hallucinated fixes, retried blindly, and generated workarounds that made things worse.</p><p>When BenchBox rejects an invalid request, the error tells the agent exactly what category of problem occurred:</p><pre><code>Error: BENCHMARK_VALIDATION_FAILED
Category: EXECUTION
Message: Query Q6 returned 0 rows. Expected: 1 row.
         Reference answer: 123,141,078.23
         Likely cause: Data generation used wrong distributions.
         Action: Re-run data generation with dbgen, then retry.</code></pre><p>Without structured errors, the agent sees "0 rows returned," decides the query might have a syntax issue, rewrites it three times, eventually gets a non-empty result by removing a WHERE clause, and reports the garbage number as a benchmark result. CLIENT, PLATFORM, EXECUTION, and SERVER tell it whether to fix the input, stop and ask for setup, or retry a failed run without inventing a workaround.</p><div><hr></div><h2>The before and after</h2><p>I ran the same DuckDB/TPC-H task twice on the same machine with the same agent model. I kept TPC-H for the before-and-after because BenchBox already had a clean DuckDB path instrumented for it. But the control problem is the same one Post 1 exposed with SSB, and it generalizes cleanly to TPC-DS as benchmark workflows get more elaborate.</p><p>To make the comparison clean, I held the target platform and the request constant: benchmark DuckDB on TPC-H. The only thing I changed was the interface. One run had plain tool access and no benchmark-specific guardrails; the other had BenchBox's MCP tools. I am not presenting the two numbers as a fair performance shootout. I am showing that one path produced benchmark-shaped garbage and the other produced a validated run.</p><p><strong>Without tools</strong> (unconstrained agent):</p><p>Post 1 used SSB on DataFusion because the lack of a built-in extension made the failure obvious. I disabled DuckDB's TPC-H extension here for the same reason: to show the unconstrained path agents take when the guardrail is missing. What follows is still the common case for most systems.</p><pre><code>Agent: I'll create a TPC-H benchmark for DuckDB.
[Writes tpch_bench.py using Faker for data generation]

Results:
- Data generation: 12 seconds (random data, not dbgen)
- Query execution: 3.1 seconds total
- QphH: 25,548

Agent: DuckDB shows excellent TPC-H performance!</code></pre><p>What actually happened: the agent recreated the same pattern from Post 1's SSB scripts. It calculated "QphH" as arithmetic mean of elapsed times (wrong formula), used random data (wrong distributions), ran each query once (no warm-up or repetition), and used hardcoded parameters (cache effects invisible). The number 25,548 is meaningless.</p><p><strong>With BenchBox MCP</strong> (same model, structured tools):</p><p>Here's actual output from a BenchBox run (captured 2026-02-01, <code>tpch_sf001_duckdb_sql_20260201_132319_mcp_566116fd.json</code>):</p><pre><code>{
  "run": {
    "id": "mcp_566116fd",
    "timestamp": "2026-02-01T13:23:19",
    "iterations": 3
  },
  "platform": {
    "name": "DuckDB",
    "version": "1.4.4"
  },
  "summary": {
    "queries": {"total": 66, "passed": 66, "failed": 0},
    "timing": {
      "total_ms": 758,
      "geometric_mean_ms": 11.1,
      "stdev_ms": 4.9
    },
    "validation": "passed",
    "tpc_metrics": {"power_at_size": 2848.75}
  },
  "phases": {
    "data_generation": {"status": "SUCCESS"},
    "validation": {"status": "PASSED", "duration_ms": 50},
    "power_test": {"status": "COMPLETED", "duration_ms": 1057}
  }
}</code></pre><p>Notice what's present that the unconstrained agent lacked:</p><ul><li><p><strong>Validation phase</strong>: <code>"status": "PASSED"</code> confirming queries produce correct results</p></li><li><p><strong>Multiple iterations</strong>: 3 runs with variance tracking (<code>stdev_ms: 4.9</code>)</p></li><li><p><strong>Proper TPC metric</strong>: <code>power_at_size: 2848.75</code> (geometric mean, not arithmetic)</p></li><li><p><strong>Full methodology metadata</strong>: Every phase recorded with timing and status</p></li></ul><p>The BenchBox result is <em>lower</em> than the fabricated one. That's expected: random data often produces faster queries (wrong selectivities, smaller intermediate results, empty joins). But unlike the fabricated number, the BenchBox result actually <em>means something</em>. Post 1 showed the fake-data side of this with SSB; this example shows the other half of the story, where the same model stops improvising once the workflow is encoded in tools.</p><div><hr></div><h2>The CLI: a second layer</h2><p>Beyond the MCP server, agents can invoke BenchBox directly via CLI, and the same structural enforcement applies.</p><p>The difference between <code>benchbox run --platform duckdb --benchmark tpch --scale 0.01</code> and "please run TPC-H at SF0.01 on DuckDB" isn't just syntax. It's the difference between invoking a validated workflow and asking an agent to reason about methodology from scratch. The same distinction shows up if the workload is SSB or TPC-DS: the command narrows the valid path, while the natural-language request invites improvisation. That improvisation is where every failure mode from Post 1 lives.</p><p>What surprised me was how many failure modes disappeared simply by defining the <em>vocabulary of valid operations</em>. <code>--benchmark tpch</code> is a valid option; <code>--benchmark my-custom-benchmark</code> isn't. <code>--scale 0.001</code> is valid; <code>--scale "ten"</code> rejects at argument parsing. When an agent can only invoke commands that accept valid arguments, it can't hallucinate benchmarks that don't exist. When the workflow definition includes validation, the agent can't skip it; not through ignorance, not through "efficiency," not through creative reasoning. The constraint is structural, not persuasive.</p><p>The design principle: the CLI defines the <em>space of valid operations</em>, not just a convenience layer for common tasks. An agent using these commands can't produce the failures from Post 1, because those failures require operations the CLI doesn't expose.</p><div><hr></div><h2>What this generalizes to</h2><p>SSB, TPC-H, and TPC-DS differ in query shapes, data generators, and scoring rules. The control problem is the same in all three: if the agent is free to improvise methodology, it will.</p><p>If you're building agent-facing tooling for any rigorous domain, I think three constraints matter more than prompt cleverness:</p><ol><li><p><strong>Put valid inputs and ground truth behind tools</strong>: Whitelists, bounds, and live system resources beat instructions every time.</p></li><li><p><strong>Encode the execution sequence</strong>: If validation must happen before timing, the tool should require that order instead of hoping the model remembers it.</p></li><li><p><strong>Make failure explicit</strong>: Categorized, actionable errors stop the agent from papering over a broken run with made-up fixes.</p></li></ol><p>I don't try to constrain everything. I constrain <em>methodology</em>, not <em>analysis</em>. I constrain <em>inputs</em>, not <em>presentation</em>. That's the split that has held up in practice: agents are useful for explaining results, comparing runs, and surfacing anomalies, but not for inventing the measurement protocol.</p><p>My recommendation is simple: if you're going to publish or act on benchmark numbers, give the agent a constrained runner or keep it out of the execution path. Let it summarize, compare, and explain. Don't let it improvise the methodology. If the valid path is not encoded in the tool, treat every benchmark number it produces as untrusted until a human verifies it.</p><div><hr></div><h2>Footnotes</h2><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p><a href="https://www.tpc.org/tpc_documents_current_versions/pdf/tpc-h_v3.0.1.pdf">TPC-H Specification v3.0.1</a> - TPC, accessed 2026-02-02. Clause 2.3.1 defines the qualification database and output validation; Clause 4.1.2.2 requires SF=1 for qualification.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p><a href="https://modelcontextprotocol.io/">Model Context Protocol</a> - Anthropic, accessed 2026-02-02. Open standard for connecting AI models to external tools and data sources.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Benchmark with AI (pt1/3): When AI agents are confident and wrong]]></title><description><![CDATA[AI agents can produce benchmark-shaped output with total confidence, even when the methodology is invalid.]]></description><link>https://oxbowresearch.com/p/benchmark-with-ai-pt13-when-ai-agents</link><guid isPermaLink="false">https://oxbowresearch.com/p/benchmark-with-ai-pt13-when-ai-agents</guid><dc:creator><![CDATA[Joe Harris]]></dc:creator><pubDate>Fri, 13 Mar 2026 12:35:38 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!5-A4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b68684e-f3df-4627-8fac-2f1d1e6bdc83_1800x692.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Creating a benchmarking script</h2><img style="" src="https://substackcdn.com/image/fetch/$s_!5-A4!,w_1100,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b68684e-f3df-4627-8fac-2f1d1e6bdc83_1800x692.png" alt="When AI agents are confident and wrong" title="" data-component-name="ImageToDOM"><p><strong>TL; DR</strong>: AI agents generate fake data, skip result validation, hardcode query filters, and report meaningless metrics; all with complete confidence. Without guardrails, their benchmark "results" are just numbers the agent printed.</p><div><hr></div><p>AI coding agents often make subtle mistakes when asked to assist with benchmarking tasks. To demonstrate the phenomenon for this post, I asked both Claude Code and Codex to create a Star Schema Benchmark script for DataFusion. Not "run the benchmark," just "create the script." I wanted to inspect what they produced before executing anything. Both ran in clean sessions; no project context, no MCP servers, no prompt history, no benchmarking tools available.</p><p>Claude Code wrote 679 lines of well-structured Python. Correct SSB table schemas, all 13 queries organized by flight, CLI argument parsing, multiple runs with best/median/average reporting. Here's how it generated the data:</p><pre><code>def generate_lineorder_table(sf, date_keys, num_parts, num_supps, num_custs):
    """~6_000_000 * SF rows (1_500_000 orders * ~4 lines each)."""
    rng = make_rng(789)
    num_orders = 1_500_000 * sf

    for ok in range(1, num_orders + 1):
        nlines = rng.randint(1, 7)
        odate = rng.choice(date_keys)
        ckey = rng.randint(1, num_custs)
        # ... fills every column with random.Random() values</code></pre><p>This generator looks like it's creating benchmark data but <strong>it isn't.</strong> The comment says "deterministic PRNG seeded per-table so results are reproducible." and it creates tables with the right column names, plausible value ranges, and correct cardinalities. However, SSB specifies a data generator (<code>ssb-dbgen</code>) that produces data with designed distributions and correlations<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a> that the agent's <code>random.randint()</code> calls do not reproduce. The result is that foreign key relationships become statistical noise rather than designed joins, and filter selectivities end up arbitrary rather than calibrated; so the queries run against a fundamentally different workload than SSB intends, making every timing result meaningless.</p><p>The agent expressed zero uncertainty about any of this. The script had no placeholder markers, no "this is an approximation" caveats, no suggestion that <code>ssb-dbgen</code> exists. It treated data generation as a solved problem: generate rows that fit the schema, then move on to printing timing tables without any validation phase, composite metric, or run protocol.</p><p>Codex produced a different script: instead of generating fake data, it cloned the <code>ssb-dbgen</code> repository from GitHub and built the official data generator. It included a dedicated warm-up phase and reported standard deviation. The worst failure mode; fake data; was absent.</p><p>But Codex still failed three of my five methodology checks: it didn't validate results against reference answers, it hardcoded all 13 queries with fixed filter values, and it reported raw elapsed milliseconds instead of a composite metric. Better methodology than Claude's attempt, but still not a benchmark result.</p><p>Both agents got the same prompt under the same constraints, yet one generated fake data with complete confidence while the other found the real data generator but skipped half the methodology. Neither flagged what it got wrong.</p><div><hr></div><h2>The overconfidence gap</h2><p>Why do AI agents express such certainty about benchmark methodology when their knowledge is so unreliable?</p><p>Benchmarking sits in a dangerous middle ground for language models. There's enough online content about database performance testing to pattern-match convincingly, but the corpus is mostly informal, incomplete, and frequently wrong.</p><p>Benchmark specifications like TPC-H<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> and SSB<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a> are dense technical documents that define exact data generation procedures, validation requirements, timing protocols, and composite metrics. Almost none of this detail appears in the training corpus in a form agents can reproduce. What <em>does</em> appear are simplified summaries; "TPC-H has 22 queries," "SSB uses a star schema"; that are true but incomplete for actually running the benchmark.</p><p>Search for any benchmark tutorial online and you'll see the pattern: run the queries, report elapsed times. Data generator requirements, qualification steps, composite metrics; almost none of it appears. The posts aren't wrong about what they describe; they're just describing something much simpler than the actual specification. This is the corpus agents train on, and it means the vocabulary is well-represented while the methodology isn't. The result is an agent that sounds fluent in benchmarking but doesn't actually understand the methodology; creating an illusion of competence that's invisible to anyone with the same knowledge gap.</p><p>Claude's SSB script is a direct example. It used the right table names, correct column schemas, and appropriate cardinalities because that vocabulary is all over the training data. But it used <code>random.randint()</code> for data generation because the detail that SSB requires <code>ssb-dbgen</code> with designed distributions is buried in a 2009 academic paper<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>, not in the blog posts and tutorials agents learn from.</p><p>Three factors compound the problem:</p><ul><li><p>Most online benchmark content is informal; "I ran TPC-H on my laptop" usually means "I ran some queries and reported times."</p></li><li><p>Vendor marketing dominates the rest, publishing performance numbers without methodology details.</p></li><li><p>The actual specifications are dense PDFs poorly represented in web-crawled training data. The simplified summaries that <em>are</em> well-represented omit exactly the details that matter.</p></li></ul><p>What makes this dangerous is that the output <em>looks right</em>. Garbage Python fails visibly, but invalid benchmark methodology just runs, prints numbers, and looks professional. The two scripts I collected demonstrate this: both are well-structured, well-commented, production-quality Python. Both would run without errors and print professional-looking timing tables, even though one generates entirely fake data and the other uses the real generator but skips half the methodology. Neither failure is visible in the output.</p><p>Here's the counterintuitive part: more capable agents don't produce <em>fewer</em> methodology failures; they produce <em>harder-to-detect</em> ones. Claude's script with deterministic seeding and reproducible random generation is more sophisticated than a quick <code>random.uniform()</code> shortcut. The sophistication makes the fake data <em>harder</em> to spot, not easier. The failure mode shifts from "obviously broken" to "subtly wrong," and detection requires domain expertise the user may not have.</p><div><hr></div><h2>Why agents default to building</h2><p>The overconfidence isn't random; it has root causes that matter if you want to use agents productively.</p><h3>They've seen more "build it" than "use it"</h3><p>There are far more code-generation examples in training corpora than tool-usage examples. The internet is full of "here's how to build X" and sparse on "here's how to correctly invoke this tool," so agents naturally default to the pattern they've seen most.</p><p>When I ask an agent to run a benchmark, it can generate Python and execute it fast. Confirming whether <code>benchbox</code> is installed, learning its CLI, and invoking it correctly takes exploration the agent may not know how to do. The path of least resistance is generation. Claude's SSB script; 679 lines of custom data generation; versus Codex's one <code>git clone</code> of <code>ssb-dbgen</code> is the split in action.</p><h3>Building is one step; tool discovery is many</h3><p>An agent can <em>verify</em> that its generated code runs. It cannot easily verify that a specialized tool exists on the system, is installed correctly, and will do what the task requires. Generation is one step with immediate feedback, while tool discovery is multiple steps with uncertain outcomes.</p><h3>Wrong output that runs is still "success"</h3><p>Here's the fundamental issue: agents aren't penalized for reinventing wheels that produce incorrect results, as long as those results <em>look</em> correct. If the script runs and prints a table, the agent considers the task complete. Whether the data was generated correctly or the methodology was valid isn't in the feedback loop. The agent optimizes for "runs successfully," not "produces valid measurement."</p><div><hr></div><h2>Why I tested with SSB, not TPC-H</h2><p>My earlier tests used TPC-H; the most widely discussed benchmark in the training data. When I asked agents to "run TPC-H at SF10 against DuckDB," every agent I tested found DuckDB's built-in TPC-H extension and used it correctly:</p><pre><code>import duckdb
conn = duckdb.connect()
conn.execute("LOAD tpch")
conn.execute("CALL dbgen(sf=10)")
# Then: SELECT * FROM tpch_queries() or PRAGMA tpch(N)</code></pre><p>This produces valid TPC-H data; correct distributions, correct correlations, correct cardinalities. The agents didn't need to know the TPC-H specification because DuckDB abstracted the hard part away. Three lines of code, and the worst failure mode (fake data) was impossible.</p><p>That's a genuine improvement. DuckDB's team deserves credit for building benchmark extensions that make correct methodology the path of least resistance; exactly the kind of guardrail I argue for in this post. I wrote about the difference between DuckDB's extensions and full benchmark methodology in <a href="https://benchbox.dev/blog/2026-03-03-duckdb-tpch-extension-vs-benchbox.html">"DuckDB tpch Extension vs BenchBox TPC-H"</a>.</p><p>But it created a problem for testing the blog's thesis. If agents always use DuckDB's extension for TPC-H, I can't demonstrate the fake data failure mode. So I switched to SSB on DataFusion; a benchmark with no built-in extension on a platform that requires the agent to solve data generation itself. The fake data problem reappeared immediately.</p><p>This tells you something important about the failure boundary: agents don't understand benchmark methodology, but they can <em>use tools that encode it</em>. When the right tool exists and is discoverable, agents find it; but when it doesn't, they generate fake data with complete confidence. The gap isn't in agent intelligence but in the tools we give them, and that's something we can close.</p><div><hr></div><h2>The failure modes that matter most</h2><p>I've spent months watching AI coding agents attempt benchmark tasks during BenchBox development. The two SSB scripts above are representative, not cherry-picked; I tested multiple frontier models across TPC-H and SSB with consistent results. The catalog of mistakes is long, but four failure modes dominate and they're the hardest to detect.</p><h3>Fake data generation</h3><p>As the opening section demonstrated, this is the hardest failure to catch. Standard benchmarks define specific data generators (<code>dbgen</code> for TPC-H, <code>ssb-dbgen</code> for SSB) that produce data with designed distributions, correlations, and cardinalities<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. These properties are <em>the point</em>; the queries are designed to stress specific patterns in this specific data.</p><p>The distortion from random data hits every query flight differently. Flight 1's discount/quantity filters depend on specific distributions. Flight 3 queries (Q3.1-Q3.4) filter on customer and supplier regions; with random keys, join selectivities change by orders of magnitude and queries that should stress the engine finish instantly. Flight 4's profit calculations depend on correlated <code>lo_revenue</code> and <code>lo_supplycost</code> values. Random data usually makes queries <em>faster</em>, not slower, because the workload gets easier; and faster feels like success, not a red flag.</p><p>Codex avoided this failure by cloning <code>ssb-dbgen</code> and building the official generator. But that makes the failure mode more dangerous: you can't predict which agents will fake the data and which won't. The only safe assumption is to verify.</p><h3>Missing result validation</h3><p>Standard benchmarks include mechanisms to verify query correctness. TPC-H requires a formal "qualification database" step at SF-1 with reference answers<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>. SSB defines expected result characteristics through its data generator's deterministic output<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>. The principle is the same: confirm your queries compute what the benchmark intends before you start timing.</p><p>Neither agent validated results; not Claude, not Codex, and not any other agent I've tested across dozens of attempts. They all go straight from "create tables" to "run queries and report times," which means you have no evidence that the queries are computing correctly. Speed is meaningless without correctness.</p><p>Why does this matter beyond pedantry? Because without validation, nothing catches a query that returns wrong answers. Claude's script records the row count for each query; but never checks whether that count is <em>correct</em>. With its randomly generated data, join queries could return zero rows or millions, and the script would report both as "success" with equal confidence. Codex's script does the same. The only difference is Codex's data would at least have the right distributions; the queries still run against unvalidated output.</p><h3>No warm-up or repetition protocol</h3><p>Without a defined protocol, performance measurement is just noise measurement. The first run on a cold system tells you about storage and buffer behavior, not the query engine. Benchmark specifications address this: TPC-H defines power run sequences and a composite metric (QphH@Size)<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>; SSB's original paper reports geometric means across multiple runs<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>.</p><p>This is where the two agents diverged interestingly. Claude's script ran each query 3 times and reported best/median/average; better than a single run, but with no dedicated warm-up phase. The first measured run includes cold-cache effects mixed into the "best" timing. Codex's script included an explicit <code>--warmup</code> parameter (default 1 run) before measured runs; a methodological improvement.</p><p>Neither agent addressed the deeper issue: without a defined protocol, which number is "the result"? Best-of-3? Median? Geometric mean? The choice matters, and the benchmark spec makes it for you. Agents pick whatever feels reasonable.</p><h3>Hardcoded filter values</h3><p>TPC-H defines each query as a <em>template</em> with substitution parameters; Q1 has a date parameter, Q6 has a discount range; precisely to prevent query result caching from dominating measurements<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>. SSB's 13 queries use fixed filter values in the original spec, but the principle still applies: if you run identical SQL on every iteration, the database can return memoized results and you're timing cache lookup, not query processing.</p><p>Both agents hardcoded every filter value. Claude's Q1.1 filters on <code>d_year = 1993</code>, <code>lo_discount BETWEEN 1 AND 3</code>, <code>lo_quantity &lt; 25</code>; every run, identical. Codex's queries use the same fixed values. Neither agent varied filters across runs, discussed cache effects, or acknowledged the limitation. For SSB this is defensible if disclosed; for TPC-H it violates the spec. Neither agent made the distinction.</p><div><hr></div><h2>The rest of the catalog</h2><p>Beyond the big four, these failures appear frequently enough to mention:</p><ul><li><p><strong>Inventing scale factors.</strong> Agent uses SF-5, SF-25, or other non-standard values. Can't validate against reference data; can't compare to published results. Data generators <em>allow</em> arbitrary SFs, but defined benchmark sizes (1, 10, 30, 100, etc.)<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a> have specific validation data.</p></li><li><p><strong>Mixing benchmarks.</strong> Agent references "Query 23" in a TPC-H context, or uses SSB filters in TPC-DS queries. Different benchmarks with different schemas and protocols. Agents synthesize from all benchmark-related training data simultaneously.</p></li><li><p><strong>Ignoring isolation requirements.</strong> Agent doesn't set or validate isolation. Violates TPC-H/TPC-DS isolation requirements and undermines ACID assumptions.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a></p></li><li><p><strong>Platform-blind scale factors.</strong> Agent uses SF-1 for a distributed system or SF-1000 for a laptop. SF-1 fits entirely in RAM on most modern hardware; you're testing cache behavior, not the query engine.</p></li><li><p><strong>Reporting elapsed time instead of the defined metric.</strong> Agent reports "45.2 seconds total" as the result. Each benchmark defines its performance metric. TPC-H uses QphH@Size (composite of Power and Throughput)<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>; SSB uses geometric mean across flights. Elapsed time isn't a benchmark metric; it's a wall-clock sum that depends on query execution order.</p></li></ul><div><hr></div><h2>Why "it runs" isn't enough</h2><p>A benchmark isn't a program; it's a <em>measurement protocol</em>. Think of the difference between "a thermometer that displays a number" and "a calibrated instrument that accurately measures temperature." The display showing "72.3F" doesn't mean the room is 72.3 degrees; it just means the device produced output. What makes the reading <em>mean something</em> is calibration, placement, equilibration time, and environmental controls.</p><p>An agent that skips result validation, uses random data, hardcodes query filters, and reports raw elapsed times hasn't produced a wrong benchmark result; it hasn't produced a benchmark result <em>at all</em>, just a number that happens to have units of seconds attached.</p><div><hr></div><h2>So what do you do about it?</h2><p>None of this argues against using AI agents for database evaluation. It argues for understanding <em>where the failure boundary is</em>; and acting on it before you trust any agent-generated performance claim.</p><p>Here's my immediate advice, applicable whether or not you use BenchBox:</p><p><strong>Before trusting any AI-generated benchmark result, verify these five things:</strong></p><ol><li><p><strong>Data origin</strong>: Was the benchmark's official data generator used (<code>dbgen</code> for TPC-H, <code>ssb-dbgen</code> for SSB, <code>dsgen</code> for TPC-DS)? If the agent generated data with Python, Faker, or any random generation, the results are invalid, full stop.</p></li><li><p><strong>Result validation</strong>: Were query results checked against expected outputs? If not, you don't know whether the queries computed correctly. Speed without correctness is meaningless.</p></li><li><p><strong>Filter variation</strong>: Were query filter values varied across runs (required for TPC-H/TPC-DS, good practice for SSB)? If the same values were used every time, cache effects dominate and the results are suspect.</p></li><li><p><strong>Repetition and variance</strong>: How many runs? What's the standard deviation? A single data point is not a measurement. Demand at least 3 runs with reported variance.</p></li><li><p><strong>Metric calculation</strong>: Is the result reported using the benchmark's defined metric (QphH@Size for TPC-H, Power@Size for TPC-DS) or just "total seconds"? The latter isn't a benchmark metric; it's a wall-clock number that depends on query execution order.</p></li></ol><p>If any of these checks fail, the "benchmark results" aren't benchmark results. They're numbers the agent printed. Treat them like any unverified claim.</p><p><strong>How I operationalize those checks.</strong> The three that need concrete verification beyond "ask the agent":</p><ul><li><p><strong>Data source</strong>: Look for data generator output files on disk. Check row counts against expected cardinalities; at SF-1, SSB's LINEORDER should have ~6 million rows; TPC-H's LINEITEM should have ~6 million at SF-1 or ~60 million at SF-10.</p></li><li><p><strong>Timing variance</strong>: If all runs are identical to the millisecond, something is wrong. If runs 2 and 3 are 10x faster than run 1, you're measuring cache warm-up. Real variance on a stable benchmark run: standard deviation of ~5ms on queries averaging 11ms.</p></li><li><p><strong>Scale</strong>: If the dataset fits in RAM, you're benchmarking your memory subsystem, not the query engine.</p></li></ul><p><strong>Three rules for working with AI agents on benchmarks:</strong></p><ol><li><p><strong>Plan first, execute second.</strong> Define your methodology before the agent touches anything; benchmark, scale factor, repetition count, which phases to run, success criteria. The agent's job is to execute your plan, not design one; give it a blank canvas and it'll generate plausible-looking methodology, but give it a specific plan and it follows it.</p></li><li><p><strong>Use tools with guardrails.</strong> If a tool exists that encodes correct methodology; DuckDB's TPC-H extension, BenchBox's MCP server, any validated benchmark runner; use it. The DuckDB finding is clear: when agents had a three-line invocation that handled data generation correctly, they used it and produced valid data. When they were left to figure it out themselves, they invented something that looked right and wasn't. Tool constraints beat prompt instructions every time.</p></li><li><p><strong>Verify artifacts, not prose.</strong> Don't ask the agent "did you use the official data generator?"; check for its output files on disk. Don't ask "did validation pass?"; look for the validation report. Agent-generated prose about methodology is unreliable. A script that says "using ssb-dbgen" in a comment but uses <code>random.randint()</code> in the implementation is exactly the kind of cosmetic compliance you'll miss if you take the comment at face value.</p></li></ol><p>The two scripts tell the whole story: Claude's passed zero of my five checks and Codex's passed two, yet neither agent flagged what it got wrong, and both would have reported results with full confidence.</p><p>But the DuckDB finding points toward the fix. When the right tool exists and handles methodology correctly, agents use it. BenchBox's MCP server is built on exactly that principle: validated inputs, structured errors, and workflow constraints that make invalid methodology impossible to execute rather than merely inadvisable to attempt.</p><div><hr></div><h2>Footnotes</h2><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p><a href="https://www.tpc.org/tpc_documents_current_versions/pdf/tpc-h_v3.0.1.pdf">TPC-H Specification v3.0.1</a>; Transaction Processing Performance Council, accessed 2026-02-02. Clause 2.4.1.3 (substitution parameters), Clause 2.3.1 and Appendix C (qualification database and reference answers), Clause 4.1.2.2 (SF=1 for qualification).</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p><a href="https://www.tpc.org/tpc_documents_current_versions/pdf/tpc-h_v3.0.1.pdf">TPC-H Specification v3.0.1</a>; TPC, accessed 2026-02-02. Clause 4.2.1.2 (DBGen data generation requirements), Clause 4.2.5.2 (authorized scale factors and LINEITEM cardinalities). SSB uses an analogous generator (<code>ssb-dbgen</code>) derived from TPC-H's <code>dbgen</code> with modified distributions for the star schema.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p><a href="https://www.tpc.org/tpc_documents_current_versions/pdf/tpc-h_v3.0.1.pdf">TPC-H Specification v3.0.1</a>; TPC, accessed 2026-02-02. Clause 5.4.3 (composite QphH@Size metric from Power and Throughput), Clause 3.4 (isolation requirements).</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>P. O'Neil, E. O'Neil, X. Chen, S. Revilak, "Star Schema Benchmark" (2009). Defines the SSB data generator, 13 queries across 4 flights, and expected result characteristics for validation. The <code>ssb-dbgen</code> tool (<a href="https://github.com/electrum/ssb-dbgen">github.com/electrum/ssb-dbgen</a>) is the standard open-source implementation.</p></div></div>]]></content:encoded></item><item><title><![CDATA[How Much Faster is DuckDB 1.5 vs 1.0? A lot]]></title><description><![CDATA[I benchmarked every DuckDB minor release from 1.0.0 through the 1.5.0 dev build on TPC-H, TPC-DS, ClickBench, and SSB. The results tell a clear story of DuckDB quickly improving.]]></description><link>https://oxbowresearch.com/p/how-much-faster-is-duckdb-15-vs-10</link><guid isPermaLink="false">https://oxbowresearch.com/p/how-much-faster-is-duckdb-15-vs-10</guid><dc:creator><![CDATA[Joe Harris]]></dc:creator><pubDate>Fri, 27 Feb 2026 21:29:40 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!dtJ_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F245093ed-1b41-4a35-a14f-398a4f9e4067_4967x2319.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>TL;DR</strong>: Is upgrading from DuckDB v1.0.0 worth it? Yes. v1.5.0-dev is 1.67&#215; faster on TPC-H, 1.84&#215; faster on ClickBench, and 1.45&#215; faster on SSB, with a 1.73&#215; higher TPC-DS Power@Size score.</p><h2>Introduction</h2><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|l|l|l|}\n\\hline\n\\textbf{Benchmark} &amp; \\textbf{Metric} &amp; \\textbf{v1.0.0} &amp; \\textbf{v1.5.0-dev} &amp; \\textbf{Improvement} &amp; \\textbf{Highlight} \\\\\n\\hline\n\\text{TPC-H} &amp; \\text{Total runtime} &amp; \\text{15,228ms} &amp; \\text{9,142ms} &amp; \\text{1.67&#215; faster} &amp; \\text{Q7: 4.3&#215; single-query improvement} \\\\\n\\hline\n\\text{TPC-DS} &amp; \\text{Power@Size} &amp; \\text{385,807} &amp; \\text{669,328} &amp; \\text{1.73&#215; higher} &amp; \\text{v1.2.2 regression hump, full recovery} \\\\\n\\hline\n\\text{ClickBench} &amp; \\text{Total runtime} &amp; \\text{729ms} &amp; \\text{396ms} &amp; \\text{1.84&#215; faster} &amp; \\text{LIKE queries +73\\%} \\\\\n\\hline\n\\text{SSB} &amp; \\text{Total runtime} &amp; \\text{1,356ms} &amp; \\text{938ms} &amp; \\text{1.45&#215; faster} &amp; \\text{v1.3/v1.4 regressed, v1.5 full recovery} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;ERZYNKWYRZ&quot;}" data-component-name="LatexBlockToDOM"></div><div><hr></div><p><strong>This post evaluates the last six DuckDB versions (1.0.0 through 1.5.0-dev) on the TPC-H, TPC-DS, ClickBench, and Star Schema (SSB) benchmarks.</strong></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://oxbowresearch.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Oxbow Research is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>DuckDB is now common infrastructure for SQL analytics work. It runs in-process with no server, handles billion-row workloads on a laptop, and embeds into Python, R, and dozens of other runtimes. Since v1.0.0 shipped in June 2024, it has become a standard tool for data engineering, data science, and ad hoc analytics.</p><p>DuckDB is typically embedded in applications, notebooks, and scripts with the version locked to ensure consistent behavior. So it's worth highlighting version-level performance improvements that locked version embeds may be missing out on.</p><p>MotherDuck estimated a cumulative 2&#215; improvement since v1.0.0<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>. Because DuckDB is open source, I can easily compare the six major versions since 1.0, at the query level, and trace performance shifts to specific PRs and issues: not just "it's faster," but <em>which execution changes produced which gains</em>.</p><div class="image-gallery-embed" data-attrs="{&quot;gallery&quot;:{&quot;images&quot;:[{&quot;type&quot;:&quot;image/png&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/245093ed-1b41-4a35-a14f-398a4f9e4067_4967x2319.png&quot;},{&quot;type&quot;:&quot;image/png&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/22a8aea0-c00f-402e-9540-5e12ef8782d9_4967x2319.png&quot;},{&quot;type&quot;:&quot;image/png&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7b63fb66-9562-41ce-904f-5492eb98e4c0_4967x2319.png&quot;},{&quot;type&quot;:&quot;image/png&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bbeb7af3-9da7-4a5f-a3d9-9b31a5974455_4967x2320.png&quot;}],&quot;caption&quot;:&quot;Query time distributions across all four benchmarks and six versions&quot;,&quot;alt&quot;:&quot;Query time distributions across all four benchmarks and six versions&quot;,&quot;staticGalleryImage&quot;:{&quot;type&quot;:&quot;image/png&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/706e584c-f145-4b13-8e47-ccf70062fd8d_1456x1456.png&quot;}},&quot;isEditorNode&quot;:true}"></div><p></p><h3>Versions Tested</h3><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|l|}\n\\hline\n\\textbf{Version} &amp; \\textbf{Codename} &amp; \\textbf{Release Date} &amp; \\textbf{Key Performance Features} \\\\\n\\hline\n\\textbf{1.0.0} &amp; \\text{-} &amp; \\text{June 2024} &amp; \\text{First stable release (baseline)} \\\\\n\\hline\n\\textbf{1.1.3} &amp; \\text{Eatoni} &amp; \\text{October 2024} &amp; \\text{Filter pushdown, join optimizations} \\\\\n\\hline\n\\textbf{1.2.2} &amp; \\text{Histrionicus} &amp; \\text{February 2025} &amp; \\text{CSV parser rewrite (+15\\%)} \\\\\n\\hline\n\\textbf{1.3.2} &amp; \\text{Ossivalis} &amp; \\text{June 2025} &amp; \\text{Parquet reader/writer rewrite} \\\\\n\\hline\n\\textbf{1.4.4} &amp; \\text{Andium (1.4.x LTS line)} &amp; \\text{January 2026} &amp; \\text{Sorting rewrite (2x+), 1.4.x patch line} \\\\\n\\hline\n\\textbf{1.5.0-dev} &amp; \\text{Variegata} &amp; \\text{Pre-GA build tested} &amp; \\text{Pre-release build (1.5.0.dev311)} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;WYRLVRKGIL&quot;}" data-component-name="LatexBlockToDOM"></div><p>I tested the last patch release of each minor version to capture cumulative improvements. For v1.5, I used a pre-release dev build (1.5.0.dev311). DuckDB's release calendar lists 1.5.0 as upcoming on March 2, 2026, and GitHub Releases still shows v1.4.4 as latest GA<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>.</p><div><hr></div><h2>DuckDB's Performance Evolution</h2><h3>Versions 1.1 through 1.3: Execution Engine, I/O, and Parquet</h3><p>The first three post-1.0 releases moved from core execution to storage. v1.1<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a> shipped optimizer work (filter and join improvements) and produced the biggest single-version TPC-H jump in this matrix (+39.4% Power@Size). v1.2<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a> focused on I/O (CSV parser rewrite, Parquet bloom filters) and delivered the largest single-version ClickBench drop (17.7% over v1.1). v1.3<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a> rewrote Parquet reader/writer paths (deferred column fetching and stronger pushdown), changes that matter most in Parquet-heavy workloads rather than these in-memory runs.</p><h3>Version 1.4: Sorting Rewrite</h3><p>The v1.4<a href="https://github.com/duckdb/duckdb/pull/17459">^5] sorting rewrite (PR-17584) replaced DuckDB's sort implementation with a K-way merge sort, delivering 1.7-2.7&#215; improvement on random data and up to 10&#215; on pre-sorted data in isolated ORDER BY benchmarks[^8]. TPC-H shows modest gains (+1.4% on my sorting proxy queries) because its ORDER BY operations are one component of multi-join queries, not isolated sorts. DuckDB 1.4 also changed CTE behavior to materialize by default ([PR-17459</a>), with the release notes reporting performance and correctness improvements for repeated CTE references<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>. In this matrix, TPC-DS Power@Size increased from 614,882 (v1.3.2) to 630,854 (v1.4.4).</p><h3>Version 1.5.0-dev: Continued Acceleration</h3><p>After major rewrites in v1.3 and v1.4, v1.5.0-dev shows another round of gains: +4.7% TPC-H Power@Size, +6.1% TPC-DS Power@Size, 0.5% faster ClickBench, and SSB recovering fully from the v1.3/v1.4 regression to become the fastest version (938ms). Because GA release notes aren't final, I focus on observed deltas rather than attributed features. Concurrent upstream work mapped from <code>v1.5-variegata</code><a href="https://github.com/duckdb/duckdb/pull/21022">^7] includes join memory improvements ([PR-21022</a>), window optimizer extensions (<a href="https://github.com/duckdb/duckdb/pull/21021">PR-21021</a>), and plan correctness tightening (<a href="https://github.com/duckdb/duckdb/pull/21014">PR-21014</a>). These are the most active performance-relevant threads on the branch at test time. The per-query delta table in the TPC-H results section shows where the gains landed.</p><div><hr></div><h2>Results: TPC-H (SF=10)</h2><h3>Overall Version Progression</h3><p><strong>TPC-H Power@Size</strong>, the TPC standard metric for single-stream query performance: <code>3600 &#215; Scale_Factor / geometric_mean(per-query times)</code>. Higher is better. The geometric mean weights all queries equally, so a 2&#215; improvement on any single query contributes the same regardless of absolute runtime.</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|l|}\n\\hline\n\\textbf{Version} &amp; \\textbf{Power@Size} &amp; \\textbf{vs. v1.0.0} &amp; \\textbf{vs. Previous} \\\\\n\\hline\n\\text{v1.0.0} &amp; \\text{194,231} &amp; \\text{-} &amp; \\text{-} \\\\\n\\hline\n\\text{v1.1.3} &amp; \\text{270,754} &amp; \\text{+39.4\\% higher} &amp; \\text{+39.4\\% higher} \\\\\n\\hline\n\\text{v1.2.2} &amp; \\text{274,287} &amp; \\text{+41.2\\% higher} &amp; \\text{+1.3\\% higher} \\\\\n\\hline\n\\text{v1.3.2} &amp; \\text{277,920} &amp; \\text{+43.1\\% higher} &amp; \\text{+1.3\\% higher} \\\\\n\\hline\n\\text{v1.4.4} &amp; \\text{282,440} &amp; \\text{+45.4\\% higher} &amp; \\text{+1.6\\% higher} \\\\\n\\hline\n\\text{v1.5.0-dev} &amp; \\text{295,792} &amp; \\text{+52.3\\% higher} &amp; \\text{+4.7\\% higher} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;PVPZRSBRSF&quot;}" data-component-name="LatexBlockToDOM"></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!CGsB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25b6f7eb-6262-4ac7-b9e8-e19464d100f3_4967x724.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!CGsB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25b6f7eb-6262-4ac7-b9e8-e19464d100f3_4967x724.heic 424w, https://substackcdn.com/image/fetch/$s_!CGsB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25b6f7eb-6262-4ac7-b9e8-e19464d100f3_4967x724.heic 848w, https://substackcdn.com/image/fetch/$s_!CGsB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25b6f7eb-6262-4ac7-b9e8-e19464d100f3_4967x724.heic 1272w, https://substackcdn.com/image/fetch/$s_!CGsB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25b6f7eb-6262-4ac7-b9e8-e19464d100f3_4967x724.heic 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!CGsB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25b6f7eb-6262-4ac7-b9e8-e19464d100f3_4967x724.heic" width="1456" height="212" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/25b6f7eb-6262-4ac7-b9e8-e19464d100f3_4967x724.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:212,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:85046,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://oxbowresearch.com/i/189402596?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25b6f7eb-6262-4ac7-b9e8-e19464d100f3_4967x724.heic&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!CGsB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25b6f7eb-6262-4ac7-b9e8-e19464d100f3_4967x724.heic 424w, https://substackcdn.com/image/fetch/$s_!CGsB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25b6f7eb-6262-4ac7-b9e8-e19464d100f3_4967x724.heic 848w, https://substackcdn.com/image/fetch/$s_!CGsB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25b6f7eb-6262-4ac7-b9e8-e19464d100f3_4967x724.heic 1272w, https://substackcdn.com/image/fetch/$s_!CGsB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25b6f7eb-6262-4ac7-b9e8-e19464d100f3_4967x724.heic 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p><strong>Total Runtime (all 22 queries)</strong>:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|}\n\\hline\n\\textbf{Version} &amp; \\textbf{Total (ms)} &amp; \\textbf{vs. v1.0.0} \\\\\n\\hline\n\\text{v1.0.0} &amp; \\text{15,228} &amp; \\text{-} \\\\\n\\hline\n\\text{v1.1.3} &amp; \\text{10,273} &amp; \\text{32.5\\% faster} \\\\\n\\hline\n\\text{v1.2.2} &amp; \\text{10,085} &amp; \\text{33.8\\% faster} \\\\\n\\hline\n\\text{v1.3.2} &amp; \\text{9,906} &amp; \\text{34.9\\% faster} \\\\\n\\hline\n\\text{v1.4.4} &amp; \\text{9,683} &amp; \\text{36.4\\% faster} \\\\\n\\hline\n\\text{v1.5.0-dev} &amp; \\text{9,142} &amp; \\text{40.0\\% faster} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;IXYSEDMFON&quot;}" data-component-name="LatexBlockToDOM"></div><h3>Per-Query Analysis</h3><p><strong>Biggest winners</strong> (largest improvement v1.0.0 to v1.5.0-dev):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|l|l|}\n\\hline\n\\textbf{Query} &amp; \\textbf{v1.0.0 (ms)} &amp; \\textbf{v1.5.0-dev (ms)} &amp; \\textbf{Speedup} &amp; \\textbf{Notes} \\\\\n\\hline\n\\text{Q7} &amp; \\text{429} &amp; \\text{99} &amp; \\text{4.3&#215;} &amp; \\text{Join + aggregation heavy} \\\\\n\\hline\n\\text{Q18} &amp; \\text{828} &amp; \\text{247} &amp; \\text{3.4&#215;} &amp; \\text{GROUP BY with large aggregation} \\\\\n\\hline\n\\text{Q17} &amp; \\text{192} &amp; \\text{96} &amp; \\text{2.0&#215;} &amp; \\text{Subquery-heavy} \\\\\n\\hline\n\\text{Q15} &amp; \\text{159} &amp; \\text{87} &amp; \\text{1.8&#215;} &amp; \\text{View + aggregation} \\\\\n\\hline\n\\text{Q5} &amp; \\text{184} &amp; \\text{102} &amp; \\text{1.8&#215;} &amp; \\text{Multi-table join + aggregation} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;HLSRUFIUTS&quot;}" data-component-name="LatexBlockToDOM"></div><p>Q7's 4.3&#215; improvement is the headline number from this analysis, and it surprised me. A 4.3&#215; speedup from iterative algorithmic improvements alone (no schema changes, no index tricks, same hardware) is unusually large for a query that was already completing successfully.</p><h3>Regressions</h3><p>I found no query slower in v1.5.0-dev than in v1.0.0 across the full 22-query TPC-H suite. Adjacent-version regressions do appear (see Analysis section), but the cumulative direction is consistently positive.</p><h3>Query Category Breakdown</h3><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|l|l|}\n\\hline\n\\textbf{Category} &amp; \\textbf{Queries} &amp; \\textbf{v1.0.0 Mean (ms)} &amp; \\textbf{v1.5.0-dev Mean (ms)} &amp; \\textbf{Improvement} \\\\\n\\hline\n\\text{Full scan} &amp; \\text{Q1, Q6} &amp; \\text{169.5} &amp; \\text{149.0} &amp; \\text{+12.1\\%} \\\\\n\\hline\n\\text{Join-heavy} &amp; \\text{Q9, Q21} &amp; \\text{384.5} &amp; \\text{284.0} &amp; \\text{+26.1\\%} \\\\\n\\hline\n\\text{Aggregation} &amp; \\text{Q5, Q18} &amp; \\text{506.0} &amp; \\text{174.5} &amp; \\text{+65.5\\%} \\\\\n\\hline\n\\text{Sorting} &amp; \\text{Q3, Q4, Q10, Q16} &amp; \\text{175.5} &amp; \\text{116.5} &amp; \\text{+33.6\\%} \\\\\n\\hline\n\\text{Subquery} &amp; \\text{Q17, Q20} &amp; \\text{158.5} &amp; \\text{103.5} &amp; \\text{+34.7\\%} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;WNDWPZOSBA&quot;}" data-component-name="LatexBlockToDOM"></div><p><strong>Key finding</strong>: Aggregation queries improved the most (65.5%), driven primarily by Q18's dramatic improvement (3.4&#215;). Full-scan queries improved the least (+12.1%): if your workload is dominated by Q1-style full table scans, the cumulative v1.0&#8594;v1.5 improvement is real but not a compelling reason to rush an upgrade.</p><p>The v1.4 sorting rewrite (PR-17584) measured 1.7-2.7&#215; on random data in isolated benchmarks<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a>, but its TPC-H impact is modest. Using Q3, Q4, Q10, and Q16 as a sorting proxy (the four queries most dominated by ORDER BY):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|}\n\\hline\n\\textbf{Version} &amp; \\textbf{Proxy Mean (ms)} &amp; \\textbf{vs. v1.3.2} \\\\\n\\hline\n\\text{v1.3.2} &amp; \\text{126.50} &amp; \\text{- (baseline)} \\\\\n\\hline\n\\text{v1.4.4} &amp; \\text{124.75} &amp; \\text{1.4\\% faster} \\\\\n\\hline\n\\text{v1.5.0-dev} &amp; \\text{116.50} &amp; \\text{7.9\\% faster} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;ZGHCVYEAFV&quot;}" data-component-name="LatexBlockToDOM"></div><p>That's expected: TPC-H sorting queries are multi-join queries where ORDER BY is one component, not isolated sorts where the rewrite's full gains apply.</p><h3>v1.4.4 to v1.5.0-dev: Per-Query Deltas</h3><p>Query-level movement from v1.4.4 to v1.5.0-dev is mixed but net-positive. The most-moved queries (both directions):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|l|}\n\\hline\n\\textbf{Query} &amp; \\textbf{v1.4.4 (ms)} &amp; \\textbf{v1.5.0-dev (ms)} &amp; \\textbf{Change} \\\\\n\\hline\n\\text{Q8} &amp; \\text{141} &amp; \\text{95} &amp; \\text{32.6\\% faster} \\\\\n\\hline\n\\text{Q7} &amp; \\text{119} &amp; \\text{99} &amp; \\text{16.8\\% faster} \\\\\n\\hline\n\\text{Q17} &amp; \\text{112} &amp; \\text{96} &amp; \\text{14.3\\% faster} \\\\\n\\hline\n\\text{Q5} &amp; \\text{117} &amp; \\text{102} &amp; \\text{12.8\\% faster} \\\\\n\\hline\n\\text{Q9} &amp; \\text{343} &amp; \\text{300} &amp; \\text{12.5\\% faster} \\\\\n\\hline\n\\text{Q14} &amp; \\text{94} &amp; \\text{100} &amp; \\text{6.4\\% slower} \\\\\n\\hline\n\\text{Q18} &amp; \\text{234} &amp; \\text{247} &amp; \\text{5.6\\% slower} \\\\\n\\hline\n\\text{Q6} &amp; \\text{59} &amp; \\text{62} &amp; \\text{5.1\\% slower} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;UIPRRBIEVS&quot;}" data-component-name="LatexBlockToDOM"></div><p>The full six-version heatmap shows the cumulative per-query trajectory. Darker cells are faster; look for the v1.1 row (the biggest single jump) and the Q7/Q18 columns (the steepest per-query improvement over the full range):</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RasQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1698a16c-a67a-4864-8a80-5d570babb66e_512x512.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RasQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1698a16c-a67a-4864-8a80-5d570babb66e_512x512.heic 424w, https://substackcdn.com/image/fetch/$s_!RasQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1698a16c-a67a-4864-8a80-5d570babb66e_512x512.heic 848w, https://substackcdn.com/image/fetch/$s_!RasQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1698a16c-a67a-4864-8a80-5d570babb66e_512x512.heic 1272w, https://substackcdn.com/image/fetch/$s_!RasQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1698a16c-a67a-4864-8a80-5d570babb66e_512x512.heic 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RasQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1698a16c-a67a-4864-8a80-5d570babb66e_512x512.heic" width="1456" height="664" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1698a16c-a67a-4864-8a80-5d570babb66e_512x512.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:664,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:431831,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://oxbowresearch.com/i/189402596?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1698a16c-a67a-4864-8a80-5d570babb66e_512x512.heic&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!RasQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1698a16c-a67a-4864-8a80-5d570babb66e_512x512.heic 424w, https://substackcdn.com/image/fetch/$s_!RasQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1698a16c-a67a-4864-8a80-5d570babb66e_512x512.heic 848w, https://substackcdn.com/image/fetch/$s_!RasQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1698a16c-a67a-4864-8a80-5d570babb66e_512x512.heic 1272w, https://substackcdn.com/image/fetch/$s_!RasQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1698a16c-a67a-4864-8a80-5d570babb66e_512x512.heic 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><h2>Results: TPC-DS (SF=10)</h2><p>TPC-DS has 99 queries (run as 103 individual variants), testing window functions, CTEs, correlated subqueries, and other advanced SQL features that TPC-H doesn't cover.</p><h3>Overall Version Progression</h3><p><strong>TPC-DS Power Score (Power@Size)</strong>:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|l|}\n\\hline\n\\textbf{Version} &amp; \\textbf{Power@Size} &amp; \\textbf{Query Records Passed} &amp; \\textbf{vs. v1.0.0} \\\\\n\\hline\n\\text{v1.0.0} &amp; \\text{385,807} &amp; \\text{308/309} &amp; \\text{-} \\\\\n\\hline\n\\text{v1.1.3} &amp; \\text{594,666} &amp; \\text{309/309} &amp; \\text{+54.1\\% higher} \\\\\n\\hline\n\\text{v1.2.2} &amp; \\text{500,512} &amp; \\text{309/309} &amp; \\text{+29.7\\% higher} \\\\\n\\hline\n\\text{v1.3.2} &amp; \\text{614,882} &amp; \\text{309/309} &amp; \\text{+59.4\\% higher} \\\\\n\\hline\n\\text{v1.4.4} &amp; \\text{630,854} &amp; \\text{309/309} &amp; \\text{+63.5\\% higher} \\\\\n\\hline\n\\text{v1.5.0-dev} &amp; \\text{669,328} &amp; \\text{309/309} &amp; \\text{+73.5\\% higher} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;GYIMSTOBER&quot;}" data-component-name="LatexBlockToDOM"></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!CTaT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb00fef09-1377-41fb-8c1d-722379b24c00_4967x724.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!CTaT!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb00fef09-1377-41fb-8c1d-722379b24c00_4967x724.heic 424w, https://substackcdn.com/image/fetch/$s_!CTaT!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb00fef09-1377-41fb-8c1d-722379b24c00_4967x724.heic 848w, https://substackcdn.com/image/fetch/$s_!CTaT!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb00fef09-1377-41fb-8c1d-722379b24c00_4967x724.heic 1272w, https://substackcdn.com/image/fetch/$s_!CTaT!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb00fef09-1377-41fb-8c1d-722379b24c00_4967x724.heic 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!CTaT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb00fef09-1377-41fb-8c1d-722379b24c00_4967x724.heic" width="1456" height="212" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b00fef09-1377-41fb-8c1d-722379b24c00_4967x724.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:212,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:86601,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://oxbowresearch.com/i/189402596?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb00fef09-1377-41fb-8c1d-722379b24c00_4967x724.heic&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!CTaT!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb00fef09-1377-41fb-8c1d-722379b24c00_4967x724.heic 424w, https://substackcdn.com/image/fetch/$s_!CTaT!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb00fef09-1377-41fb-8c1d-722379b24c00_4967x724.heic 848w, https://substackcdn.com/image/fetch/$s_!CTaT!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb00fef09-1377-41fb-8c1d-722379b24c00_4967x724.heic 1272w, https://substackcdn.com/image/fetch/$s_!CTaT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb00fef09-1377-41fb-8c1d-722379b24c00_4967x724.heic 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p><strong>v1.2.2 Dip:</strong> <code>Power@Size</code> dropped 16% from v1.1.3 before recovering in v1.3.2. The largest regression was Query 22, a <code>GROUP BY ROLLUP</code> over inventory data, which went from 881ms to 10,032ms, an 11&#215; slowdown. Queries 67, 23A, 14A, 49, and 27 also regressed (43-136%). By v1.3.2, overall TPC-DS <code>Power@Size</code> exceeded v1.1.3 levels. But Q22 itself never came back. My plan/profiling evidence supports two compounding causes: details in the Q22 deep-dive below.</p><p>In my matrix summary artifacts, v1.0.0 shows 308/309 query records passed while v1.1.3+ shows 309/309; all versions report zero timeouts. Across versions, the dominant change is execution speed, not broad query-correctness drift.</p><h3>The v1.2 Regression: What Happened to Query 22?</h3><p>TPC-DS Query 22 runs a four-column <code>GROUP BY ROLLUP</code> over <code>inventory</code>, one of the largest tables in the schema. <code>ROLLUP</code> expands to five grouping sets: the full combination plus four progressively coarser subtotals. It's one of the most aggregation-intensive queries in TPC-DS, and it's where the v1.2 regression hit hardest.</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|l|}\n\\hline\n\\textbf{Version} &amp; \\textbf{Q22 median (ms)} &amp; \\textbf{vs. v1.1.3} &amp; \\textbf{Root cause state} \\\\\n\\hline\n\\text{v1.1.3} &amp; \\text{881} &amp; \\text{baseline} &amp; \\text{Good hash table sizing, column pruning working} \\\\\n\\hline\n\\text{v1.2.2} &amp; \\text{10,032} &amp; \\text{11.4&#215; slower} &amp; \\text{Hash aggregation reworked for high-cardinality single-group workloads} \\\\\n\\hline\n\\text{v1.3.2} &amp; \\text{1,319} &amp; \\text{50\\% slower} &amp; \\text{HLL-based hash table sizing fixes aggregation; column pruning now disabled under ROLLUP} \\\\\n\\hline\n\\text{v1.4.4} &amp; \\text{1,414} &amp; \\text{60\\% slower} &amp; \\text{Same two-factor state} \\\\\n\\hline\n\\text{v1.5.0-dev} &amp; \\text{1,593} &amp; \\text{81\\% slower} &amp; \\text{Column pruning fix merged ([PR-20781](https://github.com/duckdb/duckdb/pull/20781)) but may not be in this build} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;ZJHYRCZGVW&quot;}" data-component-name="LatexBlockToDOM"></div><p>The strongest hypothesis from plan/profiling evidence is two independent changes that compound against each other.</p><ul><li><p><strong>v1.2: hash aggregation rework</strong></p><p>Between v1.1.3 and v1.2.0, DuckDB made hash aggregation performance improvements (<a href="https://github.com/duckdb/duckdb/pull/15251">PR-15251</a>, <a href="https://github.com/duckdb/duckdb/pull/15321">PR-15321</a>) targeting high-cardinality single-group-set workloads. These changes added row-width-aware partitioning thresholds and a "skip lookups if mostly unique" heuristic, both good for single-GROUP-BY queries, both bad for <code>ROLLUP</code>. <code>ROLLUP</code> produces NULL-padded rows across multiple grouping sets, which inflates the apparent uniqueness rate and triggers wider partitioning for the wider tuples. On Q22, this created a perfect storm: high base cardinality &#215; five grouping sets &#215; heuristics tuned for a different data pattern.</p></li><li><p><strong>v1.3: one step forward, one step back</strong></p><p>DuckDB v1.3.0 added HyperLogLog-based adaptive hash table sizing (<a href="https://github.com/duckdb/duckdb/pull/17236">PR-17236</a>), which improves hash table cardinality estimates. DuckDB's own benchmarks showed TPC-DS Q67, another <code>ROLLUP</code> query, running ~2&#215; faster with this optimization. But the same release also added a correctness fix (<a href="https://github.com/duckdb/duckdb/pull/17259">PR-17259</a>) that disabled <em>all</em> column pruning below <code>ROLLUP</code> and <code>CUBE</code> operators. Instead of scanning the two <code>inventory</code> columns Q22 actually needs it scanned all of them<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-8" href="#footnote-8" target="_self">8</a>.</p></li></ul><p>These two forces (fixed hash tables, broken column pruning) net out to ~50% slower than the v1.1.3 baseline. An improved column pruning fix landed in <a href="https://github.com/duckdb/duckdb/pull/20781">PR-20781</a>, which re-enabled column pruning while keeping a targeted guard only in <code>RemoveDuplicateGroups</code>. That fix ships with v1.5.0 GA. When it does, Q22 should return toward its v1.1.3 speed, or better. To my knowledge, this is the only public, per-query, multi-version TPC-DS benchmark of DuckDB; if you've seen another, I'd like to know about it.</p><div><hr></div><h2>Results: ClickBench</h2><p>ClickBench tests scan-heavy web analytics patterns on a single 100M-row table.</p><h3>Overall Version Progression</h3><p><strong>ClickBench Total Runtime (ms)</strong>:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|}\n\\hline\n\\textbf{Version} &amp; \\textbf{Total (ms)} &amp; \\textbf{vs. v1.0.0} \\\\\n\\hline\n\\text{v1.0.0} &amp; \\text{729} &amp; \\text{-} \\\\\n\\hline\n\\text{v1.1.3} &amp; \\text{651} &amp; \\text{10.7\\% faster} \\\\\n\\hline\n\\text{v1.2.2} &amp; \\text{536} &amp; \\text{26.5\\% faster} \\\\\n\\hline\n\\text{v1.3.2} &amp; \\text{555} &amp; \\text{23.9\\% faster} \\\\\n\\hline\n\\text{v1.4.4} &amp; \\text{398} &amp; \\text{45.4\\% faster} \\\\\n\\hline\n\\text{v1.5.0-dev} &amp; \\text{396} &amp; \\text{45.7\\% faster} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;BPOXUVEOUY&quot;}" data-component-name="LatexBlockToDOM"></div><p>Note that v1.3.2 shows a slight regression vs. v1.2.2 in total ClickBench runtime (555ms vs 536ms). Given observed ClickBench variance in this matrix, I treat this as directional rather than a strong signal.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!kjtm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff94f656a-5071-4f9b-a26f-08ddc508f125_4967x724.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!kjtm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff94f656a-5071-4f9b-a26f-08ddc508f125_4967x724.heic 424w, https://substackcdn.com/image/fetch/$s_!kjtm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff94f656a-5071-4f9b-a26f-08ddc508f125_4967x724.heic 848w, https://substackcdn.com/image/fetch/$s_!kjtm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff94f656a-5071-4f9b-a26f-08ddc508f125_4967x724.heic 1272w, https://substackcdn.com/image/fetch/$s_!kjtm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff94f656a-5071-4f9b-a26f-08ddc508f125_4967x724.heic 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!kjtm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff94f656a-5071-4f9b-a26f-08ddc508f125_4967x724.heic" width="1456" height="212" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f94f656a-5071-4f9b-a26f-08ddc508f125_4967x724.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:212,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:80442,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://oxbowresearch.com/i/189402596?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff94f656a-5071-4f9b-a26f-08ddc508f125_4967x724.heic&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!kjtm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff94f656a-5071-4f9b-a26f-08ddc508f125_4967x724.heic 424w, https://substackcdn.com/image/fetch/$s_!kjtm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff94f656a-5071-4f9b-a26f-08ddc508f125_4967x724.heic 848w, https://substackcdn.com/image/fetch/$s_!kjtm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff94f656a-5071-4f9b-a26f-08ddc508f125_4967x724.heic 1272w, https://substackcdn.com/image/fetch/$s_!kjtm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff94f656a-5071-4f9b-a26f-08ddc508f125_4967x724.heic 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h3>Query Pattern Analysis</h3><p>ClickBench queries are categorized by pattern:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|l|}\n\\hline\n\\textbf{Pattern} &amp; \\textbf{v1.0.0 Mean (ms)} &amp; \\textbf{v1.5.0-dev Mean (ms)} &amp; \\textbf{Improvement} \\\\\n\\hline\n\\text{COUNT(*)} &amp; \\text{0.67} &amp; \\text{&#8764;0} &amp; \\text{+100\\%} \\\\\n\\hline\n\\text{GROUP BY (low cardinality)} &amp; \\text{4.63} &amp; \\text{3.25} &amp; \\text{+29.7\\%} \\\\\n\\hline\n\\text{GROUP BY (high cardinality)} &amp; \\text{7.68} &amp; \\text{4.42} &amp; \\text{+42.5\\%} \\\\\n\\hline\n\\text{String matching (LIKE)} &amp; \\text{5.86} &amp; \\text{1.57} &amp; \\text{+73.2\\%} \\\\\n\\hline\n\\text{ORDER BY with LIMIT} &amp; \\text{6.90} &amp; \\text{3.77} &amp; \\text{+45.4\\%} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;FDUMIVUPTG&quot;}" data-component-name="LatexBlockToDOM"></div><p><strong>Key finding</strong>: String matching (<code>LIKE</code>) is the biggest winner at 73.2%, and high-cardinality <code>GROUP BY</code> improved more than low-cardinality (42.5% vs 29.7%). String hash caching (<a href="https://github.com/duckdb/duckdb/pull/18580">PR-18580</a>) is the most direct match for the LIKE gains: string processing changes have an obvious path to <code>LIKE</code> query performance. Dictionary-aware insertion (<a href="https://github.com/duckdb/duckdb/pull/15152">PR-15152</a>) and fewer aggregation allocations (<a href="https://github.com/duckdb/duckdb/pull/16849">PR-16849</a>) align with the high-cardinality <code>GROUP BY</code> gains concentrated in v1.2-v1.3. Higher-load-factor probing (<a href="https://github.com/duckdb/duckdb/pull/17718">PR-17718</a>) fits the continued ORDER BY improvement through v1.4. I haven't run micro-benchmarks to isolate each contribution, but the per-pattern distribution matches the change history well.</p><div><hr></div><h2>Results: SSB (SF=10)</h2><p>The Star Schema Benchmark tests classic dimensional model queries.</p><h3>Overall Version Progression</h3><p><strong>SSB Total Runtime (ms)</strong>:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|}\n\\hline\n\\textbf{Version} &amp; \\textbf{Total (ms)} &amp; \\textbf{vs. v1.0.0} \\\\\n\\hline\n\\text{v1.0.0} &amp; \\text{1,356} &amp; \\text{-} \\\\\n\\hline\n\\text{v1.1.3} &amp; \\text{1,005} &amp; \\text{25.9\\% faster} \\\\\n\\hline\n\\text{v1.2.2} &amp; \\text{1,006} &amp; \\text{25.8\\% faster} \\\\\n\\hline\n\\text{v1.3.2} &amp; \\text{1,054} &amp; \\text{22.3\\% faster} \\\\\n\\hline\n\\text{v1.4.4} &amp; \\text{1,071} &amp; \\text{21.0\\% faster} \\\\\n\\hline\n\\text{v1.5.0-dev} &amp; \\text{938} &amp; \\text{30.8\\% faster} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;VPRPWNNAHF&quot;}" data-component-name="LatexBlockToDOM"></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!DcUc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2ad43f-0cef-4b20-b85d-5dbb0fde1f92_4967x724.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DcUc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2ad43f-0cef-4b20-b85d-5dbb0fde1f92_4967x724.heic 424w, https://substackcdn.com/image/fetch/$s_!DcUc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2ad43f-0cef-4b20-b85d-5dbb0fde1f92_4967x724.heic 848w, https://substackcdn.com/image/fetch/$s_!DcUc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2ad43f-0cef-4b20-b85d-5dbb0fde1f92_4967x724.heic 1272w, https://substackcdn.com/image/fetch/$s_!DcUc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2ad43f-0cef-4b20-b85d-5dbb0fde1f92_4967x724.heic 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DcUc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2ad43f-0cef-4b20-b85d-5dbb0fde1f92_4967x724.heic" width="1456" height="212" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ac2ad43f-0cef-4b20-b85d-5dbb0fde1f92_4967x724.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:212,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:73866,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://oxbowresearch.com/i/189402596?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2ad43f-0cef-4b20-b85d-5dbb0fde1f92_4967x724.heic&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!DcUc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2ad43f-0cef-4b20-b85d-5dbb0fde1f92_4967x724.heic 424w, https://substackcdn.com/image/fetch/$s_!DcUc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2ad43f-0cef-4b20-b85d-5dbb0fde1f92_4967x724.heic 848w, https://substackcdn.com/image/fetch/$s_!DcUc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2ad43f-0cef-4b20-b85d-5dbb0fde1f92_4967x724.heic 1272w, https://substackcdn.com/image/fetch/$s_!DcUc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2ad43f-0cef-4b20-b85d-5dbb0fde1f92_4967x724.heic 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The per-query heatmap makes the uneven progression visible. v1.1/v1.2 show broad improvement (bluer cells), v1.3/v1.4 regress on Flight 3-4 joins (warmer cells), and v1.5 recovers to the best result across most queries:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!E3A4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd54de6c-13ad-4638-a3d1-9f45e98022bb_512x512.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!E3A4!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd54de6c-13ad-4638-a3d1-9f45e98022bb_512x512.heic 424w, https://substackcdn.com/image/fetch/$s_!E3A4!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd54de6c-13ad-4638-a3d1-9f45e98022bb_512x512.heic 848w, https://substackcdn.com/image/fetch/$s_!E3A4!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd54de6c-13ad-4638-a3d1-9f45e98022bb_512x512.heic 1272w, https://substackcdn.com/image/fetch/$s_!E3A4!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd54de6c-13ad-4638-a3d1-9f45e98022bb_512x512.heic 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!E3A4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd54de6c-13ad-4638-a3d1-9f45e98022bb_512x512.heic" width="1456" height="486" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bd54de6c-13ad-4638-a3d1-9f45e98022bb_512x512.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:486,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:221552,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://oxbowresearch.com/i/189402596?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd54de6c-13ad-4638-a3d1-9f45e98022bb_512x512.heic&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!E3A4!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd54de6c-13ad-4638-a3d1-9f45e98022bb_512x512.heic 424w, https://substackcdn.com/image/fetch/$s_!E3A4!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd54de6c-13ad-4638-a3d1-9f45e98022bb_512x512.heic 848w, https://substackcdn.com/image/fetch/$s_!E3A4!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd54de6c-13ad-4638-a3d1-9f45e98022bb_512x512.heic 1272w, https://substackcdn.com/image/fetch/$s_!E3A4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd54de6c-13ad-4638-a3d1-9f45e98022bb_512x512.heic 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>SSB is the one benchmark where improvement isn't consistent across every version. v1.3.2 and v1.4.4 are both <em>slower</em> than v1.1.3/v1.2.2, but v1.5.0-dev fully recovers and is the fastest version overall (938ms vs 1,005ms for v1.1.3). Per-subquery plan and profiling diffs show that the v1.3/v1.4 slowdown is concentrated in specific Flight 3-4 join queries rather than spread evenly.</p><p>One outlier worth noting: <code>Q2.2</code> jumps from 4ms to 53ms in v1.4.4 (and 44ms in v1.5.0-dev), a large percentage regression but small in absolute terms. Because <code>Q2.2</code> contributes &lt;50ms to total runtime in every version, the Flight 3-4 joins are where the macro story plays out:</p><p>The persistent regression is mostly <strong>Flights 3 and 4</strong>, especially <code>Q4.1</code> and <code>Q4.2</code>:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|l|l|l|}\n\\hline\n\\textbf{Query} &amp; \\textbf{v1.2.2 (ms)} &amp; \\textbf{v1.3.2 (ms)} &amp; \\textbf{v1.4.4 (ms)} &amp; \\textbf{v1.5.0-dev (ms)} &amp; \\textbf{Key movement} \\\\\n\\hline\n\\text{Q3.1} &amp; \\text{60} &amp; \\text{64} &amp; \\text{58} &amp; \\text{39} &amp; \\text{Temporary +6.7\\% in v1.3.2, then -32.8\\% in v1.5} \\\\\n\\hline\n\\text{Q3.2} &amp; \\text{60} &amp; \\text{64} &amp; \\text{56} &amp; \\text{37} &amp; \\text{Temporary +6.7\\% in v1.3.2, then -38.3\\% in v1.5} \\\\\n\\hline\n\\text{Q4.1} &amp; \\text{77} &amp; \\text{89} &amp; \\text{78} &amp; \\text{64} &amp; \\text{Persistent regression in v1.3.2 (+15.6\\%), partial recovery in v1.4/v1.5} \\\\\n\\hline\n\\text{Q4.2} &amp; \\text{56} &amp; \\text{58} &amp; \\text{54} &amp; \\text{54} &amp; \\text{Modest regression in v1.3.2 (+3.6\\%), stabilizes from v1.4 onward} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;UFCHRQRTHN&quot;}" data-component-name="LatexBlockToDOM"></div><p>I traced the SSB slowdown to a two-stage execution story, similar to the Q22 analysis approach.</p><ul><li><p><strong>Stage 1, v1.2.2 -&gt; v1.3.2/v1.4.4: regression without plan-shape change</strong></p><p>For representative regressors (<code>Q3.2</code>, <code>Q4.1</code>), the <code>EXPLAIN</code> plans keep the same join skeleton, join predicates, and lineorder full-scan shape across v1.2.2, v1.3.2, and v1.4.4. That makes a structural join-plan rewrite an unlikely primary cause for this regression pattern, including <a href="https://github.com/duckdb/duckdb/pull/16443">PR #16443</a> as the dominant driver here.</p></li><li><p><strong>Stage 2, v1.4.4 -&gt; v1.5.0-dev: targeted recovery from scan-time filtering</strong></p><p>In v1.5.0-dev, <code>EXPLAIN ANALYZE</code> for <code>Q3.2</code> and <code>Q4.1</code> shows <code>Dynamic Filters</code> on lineorder scan keys, consistent with the Bloom/SIP work in <a href="https://github.com/duckdb/duckdb/pull/19502">PR #19502</a>. This aligns with the strong recovery in <code>Q3.1</code> and <code>Q3.2</code>, and partial recovery in <code>Q4.1</code>.</p></li></ul><p>The scan-time filtering recovery is strong enough that total SSB runtime in v1.5.0-dev (938ms) beats the previous best (v1.1.3 at 1,005ms). <code>Q4.2</code> remains slightly slower than v1.2.2, but the gains in Flight 1 and Flight 3 queries more than compensate.</p><div><hr></div><h2>Analysis and Insights</h2><h3>Regression Analysis</h3><p>Suite-level improvements don't tell the whole story. Optimizations for one pattern can regress another, and the per-query data shows where.</p><p><strong>Adjacent-version TPC-H regressions (&gt;10% slower)</strong>:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|l|l|}\n\\hline\n\\textbf{Version Jump} &amp; \\textbf{Query} &amp; \\textbf{Older (ms)} &amp; \\textbf{Newer (ms)} &amp; \\textbf{Slower by} \\\\\n\\hline\n\\text{v1.3.2 &#8594; v1.4.4} &amp; \\text{Q12} &amp; \\text{98} &amp; \\text{115} &amp; \\text{+17.3\\%} \\\\\n\\hline\n\\text{v1.3.2 &#8594; v1.4.4} &amp; \\text{Q20} &amp; \\text{95} &amp; \\text{110} &amp; \\text{+15.8\\%} \\\\\n\\hline\n\\text{v1.2.2 &#8594; v1.3.2} &amp; \\text{Q19} &amp; \\text{140} &amp; \\text{156} &amp; \\text{+11.4\\%} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;NCHHIDSUEK&quot;}" data-component-name="LatexBlockToDOM"></div><p>All three are adjacent-version regressions; none accumulate to v1.5.0-dev, where every TPC-H query is faster or equal to v1.0.0.</p><h3>How I Ran These Benchmarks</h3><p>All benchmarks used BenchBox for reproducibility.</p><p><strong>Environment details</strong>:</p><ul><li><p>Hardware: Mac Mini (M4, 10 cores, 16 GB unified memory)</p></li><li><p>OS captured in benchmark artifacts: Darwin 25.3.0</p></li><li><p>Python runtime: 3.10.17</p></li><li><p>BenchBox CLI version in this environment: 0.1.3</p></li><li><p>DuckDB config: <code>threads=10</code>, <code>memory_limit='12GB'</code>, <code>enable_progress_bar=false</code>, <code>result_cache_enabled=false</code></p></li></ul><p><strong>Run protocol</strong>:</p><ul><li><p>Data generation ran once per benchmark.</p></li><li><p>Load phase ran once per version and benchmark.</p></li><li><p>Power phase ran 3 times per version and benchmark; median reported in all tables.</p></li><li><p>No explicit OS page-cache flush between power runs, so measurements reflect warm filesystem cache behavior with DuckDB result cache disabled.</p></li></ul><p><strong>Aggregation method</strong>: BenchBox computes per-run per-query medians and per-run Power@Size. I report the median of three runs for each published metric (per-query time, total runtime, Power@Size). I do not recompute Power@Size from cross-run per-query medians.</p><p>Representative commands:</p><pre><code># TPC-H SF=10
uv run benchbox run --platform duckdb \
  --benchmark tpch --scale 10 \
  --phases generate,load,power \
  --output results/duckdb-v150dev-tpch-sf10</code></pre><p>Each version was tested using isolated Python environments:</p><pre><code>uv pip install duckdb==1.0.0  # Baseline
uv pip install duckdb==1.1.3  # Last v1.1
uv pip install duckdb==1.2.2  # Last v1.2
uv pip install duckdb==1.3.2  # Last v1.3
uv pip install duckdb==1.4.4  # Current LTS
uv pip install duckdb==1.5.0.dev311  # Pre-GA v1.5 dev build</code></pre><p>Three runs per benchmark per version, median reported.</p><p>Run-to-run spread (max within-version spread across the matrix, non-zero power runtimes): TPC-H 2.7%, TPC-DS 72.7%, ClickBench 54.1%, SSB 13.0%. TPC-DS v1.0.0 is the primary outlier (one run at 734s vs median 441s), and ClickBench v1.1.3 had one anomalous run at 999ms vs median 651ms.</p><p>Interpretation threshold used in this post:</p><ul><li><p><code>&lt;2%</code> runtime deltas are directional unless variance is low for that suite/version.</p></li><li><p><code>&gt;5%</code> shifts are treated as stronger signals when also visible in per-query tables.</p></li></ul><p>Reproducibility artifacts:</p><ul><li><p><a href="https://gist.github.com/joeharris76/179602f9bebf96228717e7e39937615e">BenchBox Test Matrix Runner</a></p></li><li><p><a href="https://gist.github.com/joeharris76/6752d2d9c41a5df8f47da4d6cabd74c4">Test Matrix Result Analysis</a></p></li><li><p><a href="https://gist.github.com/joeharris76/4ad526c9da361aba9baab3a6c40f943c">Version Matrix Summary CSV</a></p></li><li><p><a href="https://gist.github.com/joeharris76/78415595e8aa0702de75642197bf28a7">Version Matrix Metrics JSON</a></p></li></ul><div><hr></div><h2>Conclusions: v1.0.0 to v1.5.0-dev</h2><ol><li><p><strong>Cumulative improvement is real</strong>: 1.67&#215; TPC-H, 1.73&#215; TPC-DS, 1.84&#215; ClickBench, 1.45&#215; SSB</p></li><li><p><strong>Major rewrites delivered</strong>: v1.1 lifted TPC-H Power@Size by 39%; overall I/O and runtime improvements reduced ClickBench runtime 46%</p></li><li><p><strong>Regressions are contained</strong>: Regressions exist but are modest (11-17%) and don't accumulate. No TPC-H query is slower in v1.5.0-dev than v1.0.0.</p></li><li><p><strong>Workload differences matter</strong>: SSB regressed in v1.3/v1.4, but v1.5.0-dev fully recovers to the best result overall (1.45&#215;)</p></li><li><p><strong>Open source enables attribution</strong>: specific PRs can be mapped to specific benchmark shifts, which is rare in database benchmarking</p></li></ol><p>The bottom line: upgrade. DuckDB has earned your trust and, if you're on older versions, you're likely leaving meaningful performance on the table. In this matrix, v1.5.0-dev improves runtime by 40.0% on TPC-H, 45.7% on ClickBench, and 30.8% on SSB versus v1.0.0, with a 73.5% higher TPC-DS Power@Size score. v1.4.4 is the safe GA choice today. v1.5.0 ships March 2, 2026; run your critical queries against the pre-release build now so you know what to expect before upgrading.</p><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://oxbowresearch.com/p/how-much-faster-is-duckdb-15-vs-10?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Thanks for reading Oxbow Research! This post is public so feel free to share it.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://oxbowresearch.com/p/how-much-faster-is-duckdb-15-vs-10?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://oxbowresearch.com/p/how-much-faster-is-duckdb-15-vs-10?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p></div><div><hr></div><h3>Limitations and Caveats</h3><ul><li><p>Direct evidence is cited inline. Plausible hypotheses are explicitly noted as such.</p></li><li><p>This uses a pre-GA DuckDB 1.5 build (<code>1.5.0.dev311</code>), so final GA behavior may differ.</p></li><li><p>Results are from one hardware profile and may not extrapolate to x86 server environments.</p></li><li><p>Power-phase timings were collected without forced OS cache eviction, so this is a warm-cache profile.</p></li><li><p>TPC-DS v1.0.0 shows high run-to-run variance (one run at 734s vs median 441s), driven by Q23A/Q23B instability.</p><ul><li><p>Later versions are stable (&lt;5% spread). Median aggregation absorbs these outliers.</p></li></ul></li></ul><div><hr></div><h3>References &amp; Resources</h3><ul><li><p><a href="https://duckdb.org/docs/stable/guides/performance/benchmarks">DuckDB Official Benchmarks Guide</a></p></li><li><p><a href="https://github.com/duckdb/duckdb/releases">DuckDB GitHub Releases</a></p></li><li><p><a href="https://github.com/duckdb/duckdb/pull/17584">New Sorting Implementation PR-17584</a></p></li><li><p><a href="https://duckdb.org/release_calendar">DuckDB Release Calendar</a></p></li><li><p><a href="https://github.com/elementalconductor/benchbox">BenchBox GitHub Repository</a></p></li></ul><div><hr></div><h2>Footnotes</h2><div><hr></div><p><em>This post is part of the DuckDB Performance series at Oxbow Research. I track DuckDB's evolution with systematic benchmarks and technical analysis across each release.</em></p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p><a href="https://motherduck.com/blog/faster-ducks/">Faster Ducks</a> - MotherDuck Blog, 2025. Performance analysis of DuckDB evolution.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p><a href="https://duckdb.org/2024/09/09/announcing-duckdb-110">Announcing DuckDB 1.1.0 "Eatoni"</a> - DuckDB Blog, September 2024.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p><a href="https://duckdb.org/2025/02/05/announcing-duckdb-120">Announcing DuckDB 1.2.0 "Histrionicus"</a> - DuckDB Blog, February 2025.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p><a href="https://duckdb.org/2025/05/21/announcing-duckdb-130">Announcing DuckDB 1.3.0 "Ossivalis"</a> - DuckDB Blog, May 2025.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p><a href="https://duckdb.org/2025/09/16/announcing-duckdb-140">Announcing DuckDB 1.4.0 "Andium"</a> - DuckDB Blog, September 2025.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p><a href="https://duckdb.org/release_calendar">DuckDB Release Calendar</a> and <a href="https://github.com/duckdb/duckdb/releases">DuckDB GitHub Releases</a> - accessed February 25, 2026. Release calendar lists 1.5.0 as upcoming on March 2, 2026; latest GA listed in releases is v1.4.4 (January 26, 2026).</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p><a href="https://duckdb.org/2025/09/24/sorting-again">Redesigning DuckDB's Sort, Again</a> - DuckDB Blog, September 2025. Benchmarks on M1 Max MacBook Pro (10 cores, 64 GB RAM): 1.7-2.7&#215; on random data, up to 10&#215; on pre-sorted data, with wide-table sorting 2-3.4&#215; faster at SF10-SF100.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-8" href="#footnote-anchor-8" class="footnote-number" contenteditable="false" target="_self">8</a><div class="footnote-content"><p>The column pruning issue under ROLLUP/CUBE was independently documented by GitHub user heldeo, who measured 9.3&#215; more columns scanned on TPC-DS Q36 (another ROLLUP query) when running on S3-backed Parquet. See <a href="https://gist.github.com/heldeo/d11057c83d76e6d4eee7bf9a05136a72">Column pruning disabled for GROUP BY ROLLUP/CUBE/GROUPING SETS</a>. The underlying correctness fix was <a href="https://github.com/duckdb/duckdb/pull/17259">PR-17259</a>; the targeted performance fix is <a href="https://github.com/duckdb/duckdb/pull/20781">PR-20781</a>, shipping in v1.5.0.</p></div></div>]]></content:encoded></item><item><title><![CDATA[The Egress Tax: how cloud providers engineer data gravity]]></title><description><![CDATA[Free ingress, cheap storage, 18x egress markup. The asymmetry is intentional.]]></description><link>https://oxbowresearch.com/p/the-egress-tax-how-data-gravity-subsidizes</link><guid isPermaLink="false">https://oxbowresearch.com/p/the-egress-tax-how-data-gravity-subsidizes</guid><dc:creator><![CDATA[Joe Harris]]></dc:creator><pubDate>Wed, 18 Feb 2026 16:46:40 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!RSVi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb47e097a-4ec1-4c4b-89a9-2827662e608f_1024x358.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>TL;DR</strong>: Cloud egress is priced at 18-24x wholesale cost to create switching costs, not recover bandwidth expenses. But internet egress is only part of the story: the cross-AZ and cross-region charges baked into every HA deployment and pipeline run are continuous and often larger. The hyperscalers' offer to waive egress only applies if you fully close your account. Design for locality, negotiate egress into contracts, and budget for intra-cloud transfer as a line item.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RSVi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb47e097a-4ec1-4c4b-89a9-2827662e608f_1024x358.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RSVi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb47e097a-4ec1-4c4b-89a9-2827662e608f_1024x358.png 424w, https://substackcdn.com/image/fetch/$s_!RSVi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb47e097a-4ec1-4c4b-89a9-2827662e608f_1024x358.png 848w, https://substackcdn.com/image/fetch/$s_!RSVi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb47e097a-4ec1-4c4b-89a9-2827662e608f_1024x358.png 1272w, https://substackcdn.com/image/fetch/$s_!RSVi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb47e097a-4ec1-4c4b-89a9-2827662e608f_1024x358.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RSVi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb47e097a-4ec1-4c4b-89a9-2827662e608f_1024x358.png" width="1024" height="358" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b47e097a-4ec1-4c4b-89a9-2827662e608f_1024x358.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:358,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:131074,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://oxbowresearch.com/i/188337021?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb47e097a-4ec1-4c4b-89a9-2827662e608f_1024x358.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!RSVi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb47e097a-4ec1-4c4b-89a9-2827662e608f_1024x358.png 424w, https://substackcdn.com/image/fetch/$s_!RSVi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb47e097a-4ec1-4c4b-89a9-2827662e608f_1024x358.png 848w, https://substackcdn.com/image/fetch/$s_!RSVi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb47e097a-4ec1-4c4b-89a9-2827662e608f_1024x358.png 1272w, https://substackcdn.com/image/fetch/$s_!RSVi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb47e097a-4ec1-4c4b-89a9-2827662e608f_1024x358.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>The economics of data gravity</h2><p>Move 1 TB of data into AWS: free. Move it out: $90. The actual cost of that bandwidth to the provider? Roughly $0.005/GB, based on wholesale transit pricing<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>.</p><p>That's an 18x markup. Not 2x, not 5x. Eighteen times the actual cost.</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|l|l|}\n\\hline\n\\textbf{Direction} &amp; \\textbf{AWS} &amp; \\textbf{GCP} &amp; \\textbf{Azure} &amp; \\textbf{Wholesale} \\\\\n\\hline\n\\text{Ingress (in)} &amp; \\text{Free} &amp; \\text{Free} &amp; \\text{Free} &amp; \\text{&#8764;\\$0.005/GB} \\\\\n\\hline\n\\text{Egress (out)} &amp; \\text{\\$0.09/GB} &amp; \\text{\\$0.12/GB} &amp; \\text{\\$0.087/GB} &amp; \\text{&#8764;\\$0.005/GB} \\\\\n\\hline\n\\text{Markup} &amp; \\text{&#8764;18x} &amp; \\text{&#8764;24x} &amp; \\text{&#8764;17x} &amp; \\text{n/a} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;EZVMAVDRSD&quot;}" data-component-name="LatexBlockToDOM"></div><p>I don't think this is price gouging in the traditional sense. It's something more deliberate: egress fees are designed to create switching costs. The "roach motel" model of cloud economics: data checks in but doesn't check out.</p><div><hr></div><h3>What data gravity means</h3><p>The concept of "data gravity" was coined by Dave McCrory in 2010<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. The basic insight: data attracts applications, services, and more data. The larger your dataset, the harder it becomes to move, not because of technical limitations but because of the ecosystem that forms around it.</p><p>In my mental model, teams hit a "gravity threshold" somewhere between 10 TB and 100 TB. Below that, migration is annoying but feasible. Above it, the conversation shifts from "should we move?" to "can we afford to move?"</p><h3>The math of switching costs</h3><p><strong>Example: 100 TB analytics workload</strong></p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|}\n\\hline\n\\textbf{Action} &amp; \\textbf{AWS Cost} &amp; \\textbf{Transfer Time (1 Gbps)} \\\\\n\\hline\n\\text{Move data in} &amp; \\text{\\$0} &amp; \\text{&#8764;10 days} \\\\\n\\hline\n\\text{Move data out} &amp; \\text{\\$9,000} &amp; \\text{&#8764;10 days} \\\\\n\\hline\n\\text{Monthly S3 storage} &amp; \\text{&#8764;\\$2,300} &amp; \\text{n/a} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;BRLNHFBLYZ&quot;}" data-component-name="LatexBlockToDOM"></div><p>The egress cost equals roughly 4 months of storage. That's your "tax" for leaving. And that's just the transfer fee: add migration engineering, testing, and risk, and the real switching cost is multiples higher. Even at volume discount tiers ($0.05/GB at 150 TB+ on AWS<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>), the markup over wholesale is still 10x.</p><div><hr></div><h2>The "roach motel" business model</h2><h3>How free ingress creates lock-in</h3><p>The cloud acquisition model works like this:</p><p><strong>Stage 1: Land</strong> (free ingress + cheap storage)</p><ul><li><p>"Try us out! Data transfer is free!"</p></li><li><p>Storage is cheap: $0.023/GB/month for S3 Standard<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a></p></li><li><p>Low barrier to entry, easy first step</p></li></ul><p><strong>Stage 2: Expand</strong> (add compute, services, integrations)</p><ul><li><p>Now you're running queries, building pipelines</p></li><li><p>Add Lambda, Athena, SageMaker</p></li><li><p>Interconnections multiply</p></li></ul><p><strong>Stage 3: Lock</strong> (data gravity + egress costs)</p><ul><li><p>50 TB and growing</p></li><li><p>15 services connected</p></li><li><p>Egress would cost $4,500+</p></li><li><p>Too expensive to leave</p></li></ul><p>Free ingress is a marketing expense. Cheap storage is a retention mechanism. Expensive egress is a switching cost.</p><h3>Example: A migration that won't happen</h3><p><strong>Scenario</strong>: A mid-size company considers migrating 500 TB from Redshift on AWS to BigQuery on GCP.</p><p><strong>The first calculation</strong>:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|}\n\\hline\n\\textbf{Item} &amp; \\textbf{Monthly Cost} \\\\\n\\hline\n\\text{Redshift (current)} &amp; \\text{\\$85,000} \\\\\n\\hline\n\\text{BigQuery (estimate)} &amp; \\text{\\$68,000} \\\\\n\\hline\n\\text{Monthly savings} &amp; \\text{\\$17,000} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;MIUDSKXEYG&quot;}" data-component-name="LatexBlockToDOM"></div><p>"A 20% savings! Let's migrate!"</p><p><strong>The real calculation</strong>:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|}\n\\hline\n\\textbf{Item} &amp; \\textbf{One-Time Cost} \\\\\n\\hline\n\\text{AWS egress (500 TB &#215; \\$0.09/GB)} &amp; \\text{\\$45,000} \\\\\n\\hline\n\\text{GCS ingress} &amp; \\text{\\$0} \\\\\n\\hline\n\\text{Staging/intermediate storage} &amp; \\text{\\$5,000} \\\\\n\\hline\n\\text{Migration engineering (6 weeks)} &amp; \\text{\\$60,000} \\\\\n\\hline\n\\text{Validation and testing} &amp; \\text{\\$15,000} \\\\\n\\hline\n\\textbf{Total migration cost} &amp; \\textbf{\\$125,000} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;NGQRBDMRQC&quot;}" data-component-name="LatexBlockToDOM"></div><p><strong>Break-even</strong>: $125,000 / $17,000 = 7.4 months, <em>if</em> nothing goes wrong during migration.</p><p>The customer stays. Data gravity wins. And "next year" never comes, because by then the dataset has grown and the math is even worse.</p><p>It's not just the hyperscalers. Analytics vendors inherit or create their own gravity. Snowflake's storage sits on the underlying cloud provider (S3, Azure Blob, GCS)<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>, so leaving Snowflake still triggers cloud egress. Databricks avoids direct egress exposure by keeping data in the customer's account, but Unity Catalog creates its own form of gravity: governance metadata is harder to migrate than raw data. And as platforms like ClickHouse Cloud mature, they add transfer-based charges that look a lot like the hyperscaler playbook<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>.</p><div><hr></div><h2>Multi-cloud multiplies egress</h2><h3>Why multi-cloud costs more than single-cloud</h3><p>The promise of multi-cloud: "Avoid lock-in by spreading across clouds."</p><p>The reality: multi-cloud multiplies egress costs.</p><p><strong>Example: Cross-cloud analytics pipeline</strong></p><pre><code>Data lands in AWS S3
     &#8595; ($0.09/GB egress)
Processing in GCP BigQuery
     &#8595; ($0.12/GB egress)
Visualization in Azure Power BI</code></pre><p>Every stage of the pipeline triggers egress. For a workload processing 10 TB/day:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|}\n\\hline\n\\textbf{Path} &amp; \\textbf{Daily Cost} &amp; \\textbf{Monthly Cost} \\\\\n\\hline\n\\text{AWS &#8594; GCP} &amp; \\text{\\$900} &amp; \\text{\\$27,000} \\\\\n\\hline\n\\text{GCP &#8594; Azure} &amp; \\text{\\$1,200} &amp; \\text{\\$36,000} \\\\\n\\hline\n\\textbf{Total egress} &amp; \\textbf{\\$2,100} &amp; \\textbf{\\$63,000} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;JNTYMGOHVZ&quot;}" data-component-name="LatexBlockToDOM"></div><p>That's $63,000/month <em>on top of</em> compute and storage costs, purely from egress.</p><p>Multi-cloud can work if you minimize cross-cloud data movement: separate workloads by region or by function (transactional on Cloud A, analytics on Cloud B) and batch-sync once daily instead of streaming. Active-active replication across clouds is the expensive extreme, justified only for critical availability requirements.</p><div><hr></div><h2>The transfer costs inside your cloud</h2><p>Most egress discussions focus on internet egress: data leaving the cloud entirely. But the transfer costs that actually surprise teams are the ones <em>inside</em> the cloud. Cross-AZ and cross-region transfers are billed quietly, buried in line items that most engineers never see until someone audits the bill.</p><h3>Cross-AZ: the invisible tax on high availability</h3><p>Every cloud provider charges for data that crosses availability zone boundaries. Since multi-AZ deployments are the default for production workloads, this cost is effectively baked into any serious architecture.</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|}\n\\hline\n\\textbf{Provider} &amp; \\textbf{Cross-AZ Cost} &amp; \\textbf{Notes} \\\\\n\\hline\n\\text{AWS} &amp; \\text{\\$0.01/GB each direction} &amp; \\text{\\$0.02/GB round-trip} \\\\\n\\hline\n\\text{GCP} &amp; \\text{\\$0.01/GB} &amp; \\text{Same within a region} \\\\\n\\hline\n\\text{Azure} &amp; \\text{Free within a region} &amp; \\text{But see caveat below} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;UCVRUWLRGH&quot;}" data-component-name="LatexBlockToDOM"></div><p>Sources: <a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a>, <a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-8" href="#footnote-8" target="_self">8</a>, <a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-9" href="#footnote-9" target="_self">9</a></p><p>Azure's free cross-AZ transfer looks like a clear win, but there's a significant catch: as of early 2026, roughly a third of Azure's public regions still don't support availability zones at all<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-10" href="#footnote-10" target="_self">10</a>. That includes GA regions like North Central US, Canada East, UK West, West US, and Australia Southeast. If your workload runs in one of these regions, "multi-AZ" isn't an option. Your HA strategy requires cross-region replication, which costs $0.02-0.08/GB<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-11" href="#footnote-11" target="_self">11</a>, the same range as AWS and GCP. Azure's free cross-AZ advantage only applies if you're in a region that actually has AZs.</p><p>On AWS and GCP, that $0.01/GB sounds trivial until you calculate the volume. The catch is that AWS charges $0.01/GB in <em>each direction</em> for cross-AZ traffic between services like EC2, RDS, and Redshift<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-12" href="#footnote-12" target="_self">12</a>, making the effective cost $0.02/GB round-trip. Notably, S3 transfers within the same region are free regardless of AZ, so services reading from S3 (including Redshift managed storage) don't incur this charge.</p><p>The services that <em>do</em> generate cross-AZ costs are the ones that communicate directly: RDS replication, EC2-to-RDS queries, load balancers distributing across AZs, and NAT gateways.</p><p><strong>Example: Analytics pipeline with cross-AZ overhead</strong></p><p>Consider a typical setup: an RDS instance in multi-AZ with an ETL process running on EC2 in a different AZ.</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|}\n\\hline\n\\textbf{Component} &amp; \\textbf{Calculation} &amp; \\textbf{Monthly Cost} \\\\\n\\hline\n\\text{RDS multi-AZ replication (500 GB/day writes)} &amp; \\text{15,000 GB/mo &#215; \\$0.02/GB} &amp; \\text{\\$300} \\\\\n\\hline\n\\text{ETL reads 5 TB/mo from RDS (cross-AZ)} &amp; \\text{5,000 GB &#215; \\$0.02/GB} &amp; \\text{\\$100} \\\\\n\\hline\n\\text{ETL writes to S3 (same region)} &amp; \\text{Free} &amp; \\text{\\$0} \\\\\n\\hline\n\\text{Redshift reads from S3 managed storage} &amp; \\text{Free} &amp; \\text{\\$0} \\\\\n\\hline\n\\textbf{Monthly cross-AZ total} &amp; \\text{} &amp; \\textbf{\\$400} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;JINKWLHGWY&quot;}" data-component-name="LatexBlockToDOM"></div><p>RDS multi-AZ replication is the main cost driver: it synchronously replicates every write to the standby in another AZ, charged at $0.01/GB in each direction. The downstream S3 and Redshift steps are free because S3 is a regional service.</p><p>That's $4,800/year for a modest workload. Scale the database to 2 TB/day of writes and RDS replication alone hits $1,200/month, or $14,400/year in transfer costs that don't appear in any compute or storage line item.</p><h3>Cross-region: the compliance and DR multiplier</h3><p>Cross-region transfer costs apply to disaster recovery, compliance-driven replication, and serving global users from regional data stores. On AWS, cross-region is double the cross-AZ rate. On GCP and Azure, the range is wider.</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|}\n\\hline\n\\textbf{Provider} &amp; \\textbf{Cross-Region Cost} &amp; \\textbf{Example Route} \\\\\n\\hline\n\\text{AWS} &amp; \\text{\\$0.02/GB} &amp; \\text{us-east-1 &#8594; eu-west-1} \\\\\n\\hline\n\\text{GCP} &amp; \\text{\\$0.01-0.08/GB} &amp; \\text{Varies by continent pair} \\\\\n\\hline\n\\text{Azure} &amp; \\text{\\$0.02-0.08/GB} &amp; \\text{Varies by region pair} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;KGGZKEJUAK&quot;}" data-component-name="LatexBlockToDOM"></div><p>Sources: <a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-13" href="#footnote-13" target="_self">13</a>, <a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-14" href="#footnote-14" target="_self">14</a>, <a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-15" href="#footnote-15" target="_self">15</a></p><p>If you're replicating a 100 TB data lake from US to EU for GDPR compliance, that's a one-time $2,000 transfer cost on AWS, plus ongoing replication costs for new data. A daily sync of 500 GB of changes costs $10/day, or $3,650/year, just for the transfer.</p><h3>Why this matters more than internet egress</h3><p>Here's the thing most people miss: internet egress is a tax you pay <em>occasionally</em>, when migrating or serving external users. Cross-AZ and cross-region transfer costs are taxes you pay <em>continuously</em>, on every read, every replication, every pipeline run. They compound. For analytics workloads with multi-AZ HA and cross-region DR, intra-cloud transfer can easily exceed internet egress in aggregate.</p><div><hr></div><h2>Reducing the tax</h2><h3>Strategy 1: Minimize data movement by design</h3><p>The cheapest data transfer is the one that doesn't happen. This applies to cross-AZ and cross-region transfers just as much as internet egress.</p><p><strong>Design principles</strong>:</p><ul><li><p>Process data where it lives</p></li><li><p>Aggregate before moving</p></li><li><p>Cache at edges</p></li><li><p>Batch instead of stream when possible</p></li></ul><p><strong>Example transformation</strong>:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|}\n\\hline\n\\textbf{Approach} &amp; \\textbf{Daily Transfer} &amp; \\textbf{Monthly Egress} \\\\\n\\hline\n\\text{Stream 1 TB raw data} &amp; \\text{1,000 GB} &amp; \\text{\\$2,700} \\\\\n\\hline\n\\text{Pre-aggregate to 10 GB} &amp; \\text{10 GB} &amp; \\text{\\$27} \\\\\n\\hline\n\\textbf{Savings} &amp; \\textbf{99\\%} &amp; \\textbf{\\$2,673} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;LRXDFGCNMG&quot;}" data-component-name="LatexBlockToDOM"></div><p>If your analytics pipeline can work with pre-aggregated summaries, do the aggregation where the data lives.</p><h3>Strategy 2: Negotiate egress into contracts</h3><p>If you're spending $1M+ annually, negotiate egress relief into your enterprise agreement: committed egress credits, reduced rates tied to spend commitment, or free egress for specific use cases (backup, DR, compliance). GCP's more aggressive egress pricing is a useful leverage point against AWS and Azure.</p><h3>Strategy 3: Use physical transfer for bulk migrations</h3><p>For migrations over 100 TB, physical transfer devices can beat network transfer on both cost and time. AWS Snowball Edge<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-16" href="#footnote-16" target="_self">16</a>, GCP Transfer Appliance<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-17" href="#footnote-17" target="_self">17</a>, and Azure Data Box<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-18" href="#footnote-18" target="_self">18</a> all ship storage devices to your datacenter, letting you move data without touching egress pricing at all. Check each provider's current device specs; capacities and product lines change frequently.</p><h3>Strategy 4: Calculate true TCO including transfer costs</h3><p>Most platform comparisons only account for compute and storage. Your TCO model should also include internet egress, cross-AZ transfer (how many AZs does the architecture span?), cross-region transfer (do you replicate for DR or compliance?), and one-time switching costs. These are often invisible until the first bill arrives.</p><div><hr></div><h2>The pressure on egress pricing</h2><h3>Regulatory pressure</h3><p><strong>EU Digital Markets Act</strong><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-19" href="#footnote-19" target="_self">19</a>:</p><ul><li><p>Designates major cloud providers as "gatekeepers"</p></li><li><p>Requires data portability and interoperability</p></li><li><p>Could force egress price reductions in EU</p></li></ul><p><strong>US regulatory interest</strong>:</p><ul><li><p>FTC published a cloud market study in 2024 identifying egress fees and switching costs as competition concerns</p></li><li><p>No enforcement action yet, but the issue is on the regulatory radar</p></li></ul><h3>The "free to leave" offer that almost nobody can use</h3><p>In March 2024, all three hyperscalers announced they would waive egress fees for customers migrating away. Headlines declared the end of egress lock-in.</p><p>The fine print: AWS issues retroactive credits (not a real-time waiver), requires support approval, and gives you 60 days to complete the migration. The offer targets customers who are switching away entirely. Partial repatriation, moving some workloads back on-prem while keeping your account active, doesn't qualify.</p><p>How many companies fully close their cloud accounts? Almost none. The typical enterprise has dozens of services, hundreds of integrations, and compliance dependencies that make a clean break practically impossible.</p><p>The most prominent company to actually do it is 37signals, makers of Basecamp and HEY. Their CTO, DHH, has documented the entire exit publicly: $3.2M/year in cloud spend reduced to well under $1M, with projected savings over $10M across five years. They migrated 6 petabytes of S3 data to on-prem Pure Storage, and AWS honored the waiver, crediting roughly $250,000 in egress fees. Even DHH noted that getting the credits approved "took a while."</p><p>But 37signals runs a handful of Rails applications with a simple architecture, a CTO who made cloud exit a personal crusade, and their own data center space. For a typical enterprise, full account closure isn't a realistic option, which is exactly the point. The waiver addresses the nuclear option of complete departure while leaving day-to-day egress pricing unchanged. The announcements were a response to the EU Data Act (which mandates zero switching fees by January 2027), not a genuine change in the economics of data gravity.</p><h3>Competitive pressure from new entrants</h3><p><strong>Cloudflare R2</strong>:</p><ul><li><p>S3-compatible storage</p></li><li><p>Zero egress fees</p></li><li><p>Clear competitive attack on data gravity</p></li></ul><p><strong>Oracle Cloud</strong>:</p><ul><li><p>$0.0085/GB egress after a free 10 TB/month tier</p></li><li><p>Targeting migrations from AWS</p></li></ul><p><strong>Wasabi</strong>:</p><ul><li><p>No egress fees</p></li><li><p>Hot storage priced below the hyperscalers' cold tiers</p></li></ul><p>The incumbents have responded with expanded free tiers and volume discounts, but core pricing remains high. Competition is pressuring the edges, not the center.</p><h3>Open formats don't solve location gravity</h3><p>Open table formats (Parquet, Iceberg, Delta Lake) reduce format lock-in, but your Iceberg tables are still in S3. Moving them to GCS still triggers egress. Format portability is not location portability.</p><div><hr></div><h2>What I'd tell a data team today</h2><p>I don't think cloud providers are villains for charging egress. They're rational actors optimizing for retention in a market with high customer lifetime value. Understanding the game lets you play it strategically.</p><p>The key things I'd want any data team to internalize:</p><ol><li><p><strong>Egress pricing is strategic, not cost-based.</strong> An 18-24x markup over wholesale bandwidth tells you this isn't about cost recovery.</p></li><li><p><strong>Data gravity is engineered.</strong> Free ingress, cheap storage, expensive egress. The asymmetry is intentional.</p></li><li><p><strong>Internet egress is only part of the transfer cost story.</strong> Cross-AZ and cross-region charges are continuous and often larger in aggregate for analytics workloads.</p></li><li><p><strong>Multi-cloud multiplies all of these costs.</strong> If you're going multi-cloud to avoid lock-in, model the transfer costs first.</p></li><li><p><strong>Design for locality.</strong> Process data where it lives, aggregate before moving, co-locate compute and storage in the same AZ when possible.</p></li></ol><p>If I were advising a data team making platform decisions today, I'd say: build transfer costs into your TCO model from day one, not as an afterthought. Negotiate egress relief into enterprise agreements. And budget for cross-AZ costs as a line item, because they will surprise you if you don't.</p><p>The egress tax is real. The intra-cloud transfer tax is real and less visible. Plan for both.</p><div><hr></div><h2>Footnotes</h2><div><hr></div><p><em>This post is part of the Business of Analytics series, examining vendor incentives across the data stack to help practitioners make informed technology decisions.</em></p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p><a href="https://datagravity.org/">Data Gravity: In the Clouds</a> - Dave McCrory, 2010. Original concept definition.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p><a href="https://drpeering.net/white-papers/Internet-Transit-Pricing-Historical-And-Projected.php">2024 Internet Transit Pricing</a> - DrPeering. Wholesale bandwidth cost analysis.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p><a href="https://clickhouse.com/pricing">ClickHouse Cloud Pricing Changes</a> - ClickHouse, 2024-2025. Evolution from zero egress to consumption-based egress.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p><a href="https://www.cloudflare.com/products/r2/">Cloudflare R2 Pricing</a> - Cloudflare. Zero egress object storage.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p><a href="https://aws.amazon.com/ec2/pricing/on-demand/#Data_Transfer">AWS Data Transfer Pricing</a> - AWS, December 2024.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p><a href="https://cloud.google.com/vpc/network-pricing">Google Cloud Network Pricing</a> - Google Cloud, December 2024.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p><a href="https://azure.microsoft.com/en-us/pricing/details/bandwidth/">Azure Bandwidth Pricing</a> - Microsoft Azure, December 2024.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-8" href="#footnote-anchor-8" class="footnote-number" contenteditable="false" target="_self">8</a><div class="footnote-content"><p><a href="https://learn.microsoft.com/en-us/azure/reliability/regions-list">List of Azure regions</a> - Microsoft Learn, February 2026. Of ~57 public regions, roughly 38 support availability zones; the remainder, including several non-restricted GA regions, do not.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-9" href="#footnote-anchor-9" class="footnote-number" contenteditable="false" target="_self">9</a><div class="footnote-content"><p><a href="https://aws.amazon.com/s3/pricing/">Amazon S3 Pricing</a> - AWS. $0.023/GB/month for S3 Standard, first 50 TB, US East.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-10" href="#footnote-anchor-10" class="footnote-number" contenteditable="false" target="_self">10</a><div class="footnote-content"><p><a href="https://docs.snowflake.com/en/user-guide/intro-key-concepts">Snowflake Architecture Overview</a> - Snowflake Documentation. Storage layer uses cloud provider object storage (S3, Azure Blob, GCS).</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-11" href="#footnote-anchor-11" class="footnote-number" contenteditable="false" target="_self">11</a><div class="footnote-content"><p><a href="https://aws.amazon.com/snowball/">AWS Snowball Edge</a> - AWS. Physical data transfer devices.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-12" href="#footnote-anchor-12" class="footnote-number" contenteditable="false" target="_self">12</a><div class="footnote-content"><p><a href="https://cloud.google.com/transfer-appliance">Transfer Appliance</a> - Google Cloud. Physical data transfer devices.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-13" href="#footnote-anchor-13" class="footnote-number" contenteditable="false" target="_self">13</a><div class="footnote-content"><p><a href="https://azure.microsoft.com/en-us/products/databox">Azure Data Box</a> - Microsoft Azure. Physical data transfer devices.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-14" href="#footnote-anchor-14" class="footnote-number" contenteditable="false" target="_self">14</a><div class="footnote-content"><p><a href="https://commission.europa.eu/strategy-and-policy/priorities-2019-2024/europe-fit-digital-age/digital-markets-act-ensuring-fair-and-open-digital-markets_en">Digital Markets Act</a> - European Commission. Regulation (EU) 2022/1925, effective May 2023.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-15" href="#footnote-anchor-15" class="footnote-number" contenteditable="false" target="_self">15</a><div class="footnote-content"><p><a href="https://www.ftc.gov/reports/examining-impact-cloud-computing-competition">Examining the Impact of Cloud Computing on Competition</a> - FTC, October 2024. Identifies egress fees and switching costs as barriers to competition.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-16" href="#footnote-anchor-16" class="footnote-number" contenteditable="false" target="_self">16</a><div class="footnote-content"><p><a href="https://www.oracle.com/cloud/networking/pricing/">Oracle Cloud Networking Pricing</a> - Oracle. $0.0085/GB egress after 10 TB/month free.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-17" href="#footnote-anchor-17" class="footnote-number" contenteditable="false" target="_self">17</a><div class="footnote-content"><p><a href="https://wasabi.com/pricing">Wasabi Pricing</a> - Wasabi. No egress fees on hot storage.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-18" href="#footnote-anchor-18" class="footnote-number" contenteditable="false" target="_self">18</a><div class="footnote-content"><p><a href="https://aws.amazon.com/blogs/aws/free-data-transfer-out-to-internet-when-moving-out-of-aws/">Free Data Transfer Out to Internet When Moving Out of AWS</a> - AWS Blog, March 2024. Google Cloud and Azure made similar announcements the same quarter.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-19" href="#footnote-anchor-19" class="footnote-number" contenteditable="false" target="_self">19</a><div class="footnote-content"><p><a href="https://world.hey.com/dhh/our-cloud-exit-savings-will-now-top-ten-million-over-five-years-c7d9b5bd">Our Cloud-Exit Savings Will Now Top Ten Million Over Five Years</a> - DHH, 2024. See also <a href="https://world.hey.com/dhh/it-s-five-grand-a-day-to-miss-our-s3-exit-b8293563">It's Five Grand a Day to Miss Our S3 Exit</a>, March 2025.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Why database benchmarks are broken]]></title><description><![CDATA[A 2018 study examined 16 published database benchmark papers. Not one reported enough information to reproduce the results.]]></description><link>https://oxbowresearch.com/p/why-database-benchmarks-are-broken</link><guid isPermaLink="false">https://oxbowresearch.com/p/why-database-benchmarks-are-broken</guid><dc:creator><![CDATA[Joe Harris]]></dc:creator><pubDate>Mon, 16 Feb 2026 20:42:10 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!HgBU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe00b326-63d2-4da9-bf66-b4a5c03b24d5_1400x538.heic" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>TL;DR: Can you trust database benchmarks? Mostly no. Vendors test their own homework, TPC audits cost $100K, and community benchmarks freeze the day they're published. If you're picking a platform this quarter, the data you need barely exists.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HgBU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe00b326-63d2-4da9-bf66-b4a5c03b24d5_1400x538.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HgBU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe00b326-63d2-4da9-bf66-b4a5c03b24d5_1400x538.heic 424w, https://substackcdn.com/image/fetch/$s_!HgBU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe00b326-63d2-4da9-bf66-b4a5c03b24d5_1400x538.heic 848w, https://substackcdn.com/image/fetch/$s_!HgBU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe00b326-63d2-4da9-bf66-b4a5c03b24d5_1400x538.heic 1272w, https://substackcdn.com/image/fetch/$s_!HgBU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe00b326-63d2-4da9-bf66-b4a5c03b24d5_1400x538.heic 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HgBU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe00b326-63d2-4da9-bf66-b4a5c03b24d5_1400x538.heic" width="1400" height="538" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fe00b326-63d2-4da9-bf66-b4a5c03b24d5_1400x538.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:538,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:106420,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://oxbowresearch.com/i/188183427?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe00b326-63d2-4da9-bf66-b4a5c03b24d5_1400x538.heic&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!HgBU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe00b326-63d2-4da9-bf66-b4a5c03b24d5_1400x538.heic 424w, https://substackcdn.com/image/fetch/$s_!HgBU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe00b326-63d2-4da9-bf66-b4a5c03b24d5_1400x538.heic 848w, https://substackcdn.com/image/fetch/$s_!HgBU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe00b326-63d2-4da9-bf66-b4a5c03b24d5_1400x538.heic 1272w, https://substackcdn.com/image/fetch/$s_!HgBU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe00b326-63d2-4da9-bf66-b4a5c03b24d5_1400x538.heic 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://oxbowresearch.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Oxbow Research is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h2>When the test-taker writes the test</h2><p>Every data platform vendor claims to be the fastest. Browse their benchmark pages: I have yet to find one where the vendor's own product doesn't win. Vendor benchmarks aren't measurement, they're marketing with numbers.</p><p>This matters because platform decisions involve real money. In my experience, a mid-size company spends $500,000 or more annually on their data platform. Enterprises can easily reach $5 million. The cloud data warehouse market is estimated at $12 billion and growing at over 25% annually<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>, and finance teams are asking harder questions about every line item. Making the wrong choice based on misleading benchmarks means wasted spend, painful migrations, and missed opportunities. Yet the data available to inform these decisions is terrible.</p><div><hr></div><p>Vendor benchmarks have an obvious problem: the company selling the product is measuring the product. The incentives are misaligned from the start.</p><p>But the problems go deeper than simple bias. A 2018 peer-reviewed study examined 16 published database benchmark papers and found that none reported the parameters necessary to interpret their results<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. One configuration parameter alone changed transaction throughput by a factor of 28. Vendor benchmarks systematically distort comparisons in ways that make the numbers essentially meaningless.</p><ul><li><p><strong>Configuration optimization</strong>:</p><p>The most common distortion is asymmetric tuning: benchmarking your own product with expert configuration while running competitors on defaults. A skilled engineer can often double or triple performance on any database through careful configuration: index selection, memory allocation, parallelism settings, query hints. When a vendor shows "10x faster than the competition," the real comparison is often "our best versus their default."</p><p>These optimizations require expertise that typical users don't have. The vendor's benchmark team has deep knowledge of their own product. They know which configuration knobs matter. Competitors' products? They might spend an afternoon reading docs. The resulting comparison tells you more about configuration skill than product capability.</p></li><li><p><strong>Workload selection</strong>:</p><p>Every database architecture has strengths and weaknesses. Columnar databases crush analytical aggregations but struggle with point lookups. Row stores handle transactions beautifully but choke on full-table scans. In-memory systems win on small datasets but hit limits at scale.</p><p>Vendors choose benchmarks that highlight their strengths. A columnar vendor runs TPC-H (analytical queries). A transactional vendor runs TPC-C (OLTP). Each shows impressive numbers on their chosen workload. Neither mentions the workloads where they lose.</p><p>The phrase "TPC-H isn't representative of real workloads" appears suspiciously often in marketing from vendors who perform poorly on TPC-H. Workload representativeness only becomes a concern when the benchmark is unflattering.</p></li><li><p><strong>Hardware asymmetry</strong>:</p><p>Cloud benchmarks add another variable: instance selection. Vendors run their product on premium instances, the latest generation, maximum memory, fastest storage. Competitors run on whatever was convenient. The resulting performance gap reflects hardware choices as much as software efficiency.</p><p>Instance selection is particularly insidious because the differences aren't obvious. An r6i.8xlarge and r5.8xlarge sound similar. Both have 32 vCPUs and 256GB RAM. But the r6i has newer processors, faster memory bandwidth, and better storage performance. Small differences in instance generation compound across queries.</p></li><li><p><strong>Methodology opacity</strong>:</p><p>The most damaging practice is simply hiding methodology. "Internal testing" with no details. "Optimized configuration" with no specifics. "Contact us for more information" instead of published scripts.</p><p>When a vendor can't or won't share exactly how they produced their numbers, the numbers are worthless. Without published methodology, you can't reproduce the results, validate the claims, or even understand what scenarios the benchmark represents. Independent verification becomes impossible.</p></li></ul><p>These aren't theoretical concerns. In November 2021, Databricks published an official TPC-DS result at 100TB, audited by the TPC council<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>. Snowflake responded with its own unofficial comparison claiming similar price-performance. Databricks challenged Snowflake's methodology. The dispute played out across competing blog posts and Hacker News threads, illustrating exactly the problem: even when one vendor publishes audited results, a competitor can muddy the water with unofficial claims that can't be independently verified.</p><div><hr></div><h2>$100,000 to publish a number</h2><p>TPC (Transaction Processing Performance Council) benchmarks represent the gold standard for rigor. TPC-H and TPC-DS define precise specifications, exact queries, data generation procedures, and validation requirements. Published results require independent audits. Full disclosure is mandatory. The methodology is exactly right.</p><p>But almost nobody uses it. Certification costs $100,000+<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>, which prices out open-source projects and smaller vendors entirely. And even vendors who can afford it face a perverse incentive: publishing only makes sense if you're confident competitors won't beat your result quickly. Spend six figures on a number that gets topped next quarter, and you've just proved you're second-fastest. Most vendors never publish, or publish once, claim the record, and never update. You can compare Oracle to Microsoft using official TPC numbers, but you can't compare either to DuckDB or ClickHouse.</p><p>The queries themselves remain genuinely useful. TPC-H's 22 queries test joins, aggregations, and sorting, the foundational operations that still dominate analytical workloads. TPC-DS adds 99 queries that stress query optimizers significantly harder, covering subqueries, window functions, and complex join patterns. These are the operations that separate fast platforms from slow ones. No single benchmark covers everything (modern workloads also include JSON processing, ML feature engineering, and time-series analysis), but TPC-H and TPC-DS cover the analytical core well.</p><p>Then there's the lag. TPC results represent a point in time. The audit process takes months. By the time results are published, the tested product version may be outdated. For cloud platforms that update continuously, published results may not reflect current performance.</p><p>The TPC council does important work. Their methodology rigor is exactly right. The problem isn't the benchmarks themselves, it's that the cost and time requirements make them impractical for most comparisons.</p><div><hr></div><h2>Enthusiasm without governance</h2><p>Individual practitioners and community members run their own benchmarks to fill the gap. Mark Litwintschik's tech.marksblogg.com publishes detailed database comparisons. ClickBench, maintained by ClickHouse, provides a single-table benchmark<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>. H2O's db-benchmark tests dataframe operations<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>. Countless blog posts share individual experiences.</p><p>This work is valuable. Community benchmarks provide data points that would otherwise not exist. But they have structural problems that limit their usefulness.</p><p><strong>Methodology varies wildly.</strong> One person runs DuckDB on a laptop. Another runs Snowflake on a Large warehouse. Someone else tests PostgreSQL with default settings while tuning ClickHouse aggressively. Two people testing the same platforms can produce opposite rankings, and neither is wrong; they just measured different things.</p><p><strong>Results go stale immediately.</strong> Someone runs tests, publishes results, and moves on. The platforms improve. New versions ship. But five-year-old benchmark posts still appear in Google searches, citing platform versions that no longer exist. There's no systematic process for keeping results current.</p><p><strong>Expertise is unevenly distributed.</strong> Properly benchmarking a data platform requires deep expertise: configuration options, query optimization, hardware characteristics, statistical analysis. Few individuals have this across multiple platforms. A PostgreSQL expert benchmarking ClickHouse might miss critical configuration options. The results then reflect tester expertise rather than product capability.</p><p><strong>Nobody owns the errors.</strong> When community benchmarks contain errors, there's no correction mechanism. Vendors can point out problems, but doing so looks defensive. Bad data persists because no one is responsible for accuracy.</p><p>The people doing this work are filling a real gap with limited resources. Good intentions can't compensate for missing infrastructure: independent benchmarking without governance produces unreliable results.</p><div><hr></div><h2>Million-dollar decisions, dollar-store data</h2><p>The consequences compound. Data teams facing platform selection have too much information and too little clarity. Vendor benchmarks all claim victory. Community benchmarks contradict each other. Academic benchmarks exclude half the options. The rational response is often to ignore external benchmarks entirely and run your own tests, but proper benchmarking takes weeks or months of engineering time. Most teams can't afford this, so they make decisions based on reputation, sales relationships, or which vendor bought them the nicest dinner.</p><p>When a team picks a platform based on benchmark claims that don't reflect their actual workload, they discover the gap after committing to integration, training, and dependencies. Enterprise migration projects routinely run into the millions<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a>.</p><p>The prevalence of misleading benchmarks breeds cynicism. "All benchmarks are marketing" becomes received wisdom, and teams stop paying attention to performance data entirely.</p><p>This cynicism is understandable but harmful. Real performance differences exist between platforms. Some platforms genuinely are 10x faster for certain workloads. Dismissing all benchmarks means missing genuine optimization opportunities. New data platform vendors struggle to compete on credibility at all: established players have years of TPC results, marketing campaigns, and analyst relationships, while a startup with a genuinely better product cannot simply prove superiority because the benchmarking landscape is too broken to support credible claims. The market rewards marketing spend rather than engineering excellence.</p><div><hr></div><h2>Something better</h2><p>The information available for platform decisions, one of the most consequential technical choices an organization makes, is unreliable. In a previous post, I argued that flawed benchmarks still beat no benchmarks. I stand by that. But "better than nothing" is a low bar, and practitioners deserve actual methodology, not just less-bad marketing. Fixing this requires independence from vendor funding, open methodology anyone can reproduce, and ongoing maintenance instead of one-time publication. I think that's buildable. In fact, I'm building it.</p><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://oxbowresearch.com/p/why-database-benchmarks-are-broken?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Thanks for reading Oxbow Research! This post is public so feel free to share it.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://oxbowresearch.com/p/why-database-benchmarks-are-broken?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://oxbowresearch.com/p/why-database-benchmarks-are-broken?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p></div><div><hr></div><h2>References</h2><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p><a href="https://www.tpc.org/information/benchmarks5.asp">TPC Policies</a> - Transaction Processing Performance Council. Full benchmark certification requires third-party auditing and TPC membership.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p><a href="https://h2oai.github.io/db-benchmark/">db-benchmark</a> - H2O.ai database-like operations benchmark</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p><a href="https://benchmark.clickhouse.com/">ClickBench</a> - ClickHouse single-table benchmark</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p><a href="https://www.mordorintelligence.com/industry-reports/cloud-data-warehouse-market">Cloud Data Warehouse Market Size &amp; Share Analysis</a> - Mordor Intelligence. Estimated at $11.78B in 2025, growing at 27.64% CAGR to $39.91B by 2030.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p><a href="https://www.persistent.com/client-success/from-teradata-to-the-cloud-building-a-future-ready-data-foundation-while-saving-140m/">From Teradata to the Cloud: Building a Future-Ready Data Foundation While Saving $140M</a> - Persistent Systems. One healthcare company's Teradata-to-cloud migration involved 5,000+ users across 25 lines of business.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p>M&#252;hleisen et al., <a href="https://hannes.muehleisen.org/publications/DBTEST2018-performance-testing.pdf">Fair Benchmarking Considered Difficult: Common Pitfalls In Database Performance Testing</a> - DBTEST 2018. Examined 16 benchmark papers; none reported sufficient parameters for reproducibility.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p><a href="https://www.databricks.com/blog/2021/11/02/databricks-sets-official-data-warehousing-performance-record.html">Databricks Sets Official Data Warehousing Performance Record</a> - Databricks, November 2021. Official TPC-DS 100TB result: 32.9M QphDS. <a href="https://www.tpc.org/results/fdr/tpcds/databricks~tpcds~100000~databricks_sql_8.3~fdr~2021-11-02~v01.pdf">Full disclosure report</a>. See also Databricks' <a href="https://www.databricks.com/blog/2021/11/15/snowflake-claims-similar-price-performance-to-databricks-but-not-so-fast.html">response to Snowflake's counter-claims</a> and Snowflake's <a href="https://www.snowflake.com/blog/industry-benchmarks-and-competing-with-integrity/">rebuttal</a>.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Benchmarking is good, actually]]></title><description><![CDATA[Everyone knows benchmarks are flawed. But when you have two weeks to pick a million-dollar platform, flawed data beats no data.]]></description><link>https://oxbowresearch.com/p/benchmarking-is-good-actually</link><guid isPermaLink="false">https://oxbowresearch.com/p/benchmarking-is-good-actually</guid><dc:creator><![CDATA[Joe Harris]]></dc:creator><pubDate>Wed, 11 Feb 2026 02:21:36 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!zlb5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fab8f-a2fa-43a4-90b6-18ad14a14610_1400x538.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>TL;DR: Should you ignore flawed benchmarks? No. Teams get a week or two for platform decisions with multi-year financial consequences, and most migrations fail or exceed their budgets. Imperfect data read with skepticism beats no data at all.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zlb5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fab8f-a2fa-43a4-90b6-18ad14a14610_1400x538.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zlb5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fab8f-a2fa-43a4-90b6-18ad14a14610_1400x538.png 424w, https://substackcdn.com/image/fetch/$s_!zlb5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fab8f-a2fa-43a4-90b6-18ad14a14610_1400x538.png 848w, https://substackcdn.com/image/fetch/$s_!zlb5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fab8f-a2fa-43a4-90b6-18ad14a14610_1400x538.png 1272w, https://substackcdn.com/image/fetch/$s_!zlb5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fab8f-a2fa-43a4-90b6-18ad14a14610_1400x538.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zlb5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fab8f-a2fa-43a4-90b6-18ad14a14610_1400x538.png" width="1400" height="538" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f59fab8f-a2fa-43a4-90b6-18ad14a14610_1400x538.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:538,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:246297,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://oxbowresearch.com/i/187586458?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fab8f-a2fa-43a4-90b6-18ad14a14610_1400x538.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!zlb5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fab8f-a2fa-43a4-90b6-18ad14a14610_1400x538.png 424w, https://substackcdn.com/image/fetch/$s_!zlb5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fab8f-a2fa-43a4-90b6-18ad14a14610_1400x538.png 848w, https://substackcdn.com/image/fetch/$s_!zlb5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fab8f-a2fa-43a4-90b6-18ad14a14610_1400x538.png 1272w, https://substackcdn.com/image/fetch/$s_!zlb5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fab8f-a2fa-43a4-90b6-18ad14a14610_1400x538.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>The case against is already well-known</h2><p>Database benchmarks have problems, and everyone in the industry knows it. Vendor benchmarks serve marketing. Configuration choices can swing results by 10x. Standard benchmarks like TPC-H test workloads that may look nothing like yours. Community benchmarks vary in methodology and rigor.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://oxbowresearch.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Oxbow Research is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>I'm not going to argue against any of that. It's all true.</p><p>But here's the part that gets left out of the critique: in my experience, most data teams don't have the luxury of ignoring benchmarks just because they're flawed. Someone has to make platform decisions, those decisions have real financial consequences, and the people making them need <em>some</em> kind of comparative data to work with.</p><p>I'll make the case in three steps: why teams can't benchmark everything themselves, why migration mandates make comparative data non-optional, and how to read benchmark results without getting fooled.</p><div><hr></div><h2>The two-week platform decision</h2><p>The advice "just benchmark it yourself" sounds reasonable until you look at how platform decisions actually happen.</p><p>Most organizations get a week or two for platform selection before committing to implementations that span 12-36 months<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>. A widely cited Gartner finding from 2009 put migration failure or major-overrun rates at 83%<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. It's old, but still one of the most commonly referenced baseline figures for migration risk.</p><p>The data engineer investigating Snowflake alternatives is usually the same person maintaining the current Snowflake deployment and responding to production incidents. "Run comprehensive benchmarks yourself" competes directly with "keep the business running." For most organizations I've worked with, self-benchmarking at production quality is simply not practical.</p><h3>The configuration trap</h3><p>Even when teams find time to evaluate platforms, they run into a subtler problem: properly configuring each platform is genuinely hard. Snowflake configuration decisions differ radically from DuckDB, which differs from BigQuery. Compute sizing, memory allocation, parallelism settings, query patterns that trigger materialization; each platform has its own arcane lore.</p><p>I've seen Postgres benchmarks that forgot to create indexes. ClickHouse tests running with outdated engine choices. Spark memory settings that force unnecessary spills. Snowflake comparisons using XS warehouses against a competitor with 10x more compute. The difference between "20% slower" and "10x faster" can come down to a single configuration choice.</p><p>This is the trap: without external benchmarks, teams unknowingly run suboptimal configurations on the platforms they're less familiar with, which is precisely the platforms they're evaluating. The resulting comparison reflects configuration skill rather than product capability.</p><h3>What this looks like in practice</h3><p>The scale involved is striking. One healthcare organization maintained Teradata for 20+ years, serving 5,000+ users across 25 lines of business. The cloud migration ultimately saved $140M annually, but required migrating every user and workload to validate the new platform could handle production load<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>. Another Teradata-to-Snowflake migration involved 700+ query translations over 8 months before achieving 60% cost reduction and 85% query efficiency improvement<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>.</p><p>These are partner and vendor-adjacent case studies, so I treat them as directional evidence rather than universal outcomes.</p><p>This tracks with every migration I've worked on. Parallel validation, running legacy and new platforms simultaneously, is standard practice for good reason<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>. These decisions evolve iteratively based on ongoing measurement, not one-time evaluation. Some data comes from internal testing, some from external sources. But "ignore all benchmarks because they're imperfect" was never a viable option.</p><div><hr></div><h2>When someone else picks your database</h2><p>Sometimes the platform decision isn't yours to make. I've been on the receiving end of these mandates more than once.</p><p>"We're moving from AWS to Google Cloud." "We're consolidating to Databricks." "We're replacing Teradata with a modern cloud warehouse." These decisions get made by executives balancing factors you may not see: vendor relationships, strategic partnerships, existing contracts, real estate costs. The mandate arrives as a fait accompli. Your job isn't to choose the best platform; it's to execute the migration.</p><p>But you still need to understand the cost-performance tradeoffs of what you're moving to. That need doesn't go away just because the choice was made above your pay grade.</p><p>This happens more often than the industry admits. 63% of IT decision-makers accelerated their cloud migration efforts in 2024, up from 57% the prior year<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>, and the pace shows no sign of slowing. My expectation is that AI coding agents will further compress migration timelines, which increases pressure to make platform decisions faster.</p><h3>The CFO question</h3><p>Without benchmarks, the CFO asks "Why move to GCP?" and you answer: "Lower cost per compute." But I've learned the hard way that configuration matters enormously. BigQuery on-demand pricing differs dramatically from Flex slot pricing, and Snowflake warehouse sizing creates order-of-magnitude cost differences for identical workloads<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a>. Without comparative data, you're guessing.</p><p>With benchmarks, the same question gets a better answer: "Platform A is 40% cheaper at equivalent performance for our average query size, based on independent testing. The lock-in cost of switching is justified by $X million savings over 3 years."</p><p>The data may be imperfect, but it's data. And when you're asked to justify a platform that costs millions annually, having external data points, even flawed ones, is the difference between a knowing nod and a very uncomfortable meeting. I know which one I prefer.</p><div><hr></div><h2>Cloud spend demands justification</h2><p>Cloud spending reached $723.4 billion in 2025<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-8" href="#footnote-8" target="_self">8</a>. At that scale, "because we've always used it" doesn't survive a finance review. Every year, finance asks the data team: "Why do we spend $X on Platform Y?" They expect performance and cost data. In many organizations, failing to provide that evidence weakens the data team's budget credibility.</p><p>The commitment decisions keep getting harder. Cloud platforms offer 25-40% savings for 1-year commitments and up to 55-72% for 3-year commitments<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-9" href="#footnote-9" target="_self">9</a>, but multi-year commitments lock you in. If your data volume grows 50% year-over-year and independent data suggests your workload would run 30% faster on a competitor, that three-year commitment is a fraught decision. Without external data points, you're extrapolating blindly.</p><div><hr></div><h2>Fifty databases and a deadline</h2><p>The vendor landscape is chaotic. Snowflake, BigQuery, Redshift, Databricks, ClickHouse, DuckDB, Firebolt, StarRocks, SingleStore, Yellowbrick; the list grows every year. Each claims superiority. Nobody can realistically evaluate all of them.</p><p>In practice, benchmarks are the most scalable comparative filtering mechanism I've found. "This platform consistently ranks in the top tier for analytical workloads." "This platform excels at real-time ingestion but struggles with complex joins." Pattern recognition across multiple benchmarks, even imperfect ones, helps narrow 50 options to 3-5 candidates worth deeper evaluation.</p><p>You can't run comprehensive tests on 50 platforms. You can read 50 benchmark reports in an afternoon. I do this regularly, and the patterns are surprisingly consistent across independent sources.</p><p>And increasingly, the people making these decisions aren't database specialists. A Gartner projection, as quoted in a Google Cloud summary of the 2024 Magic Quadrant, says 75% of DBMS purchase decisions will be made by business domain leaders by 2027, up from 55% in 2022<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-10" href="#footnote-10" target="_self">10</a>. For a domain leader, "Platform A is 40% faster and 20% cheaper for workloads like yours" is actionable in a way that "columnar storage with vectorized execution" simply isn't. Benchmarks, despite their flaws, make cross-platform differences visible to the people who increasingly make the calls.</p><div><hr></div><h2>How to read benchmarks without being fooled</h2><p>So yes, benchmarks have real problems. Vendor benchmarks serve marketing. Configuration choices skew results. Standard workloads may not match yours. All valid.</p><p>But "benchmarks are flawed" and "benchmarks are worthless" are different claims. The first doesn't imply the second. Restaurant reviews are written by people with opinions, biases, and incomplete information. Reviews are still useful. You just read them with appropriate skepticism.</p><p>When I evaluate a benchmark report, I use a four-step filter:</p><ul><li><p><strong>Discard</strong> benchmark reports that hide basic configuration, hardware, or workload details.</p></li><li><p><strong>Discount</strong> vendor-funded claims unless independent reports show similar ranking patterns.</p></li><li><p><strong>Compare</strong> at least three sources and look for directional consistency, not tiny deltas.</p></li><li><p><strong>Validate</strong> finalists on my own workload before making a commitment.</p></li></ul><p>Then I ask four questions:</p><ul><li><p><strong>Who funded this?</strong> Vendor-funded means skeptical. Independent means more trust.</p></li><li><p><strong>What configuration?</strong> Default settings favor some platforms, penalize others. Optimized settings may not reflect your team's capability.</p></li><li><p><strong>What workload?</strong> OLAP benchmarks tell you nothing about OLTP. TPC-H tells you nothing about JSON processing.</p></li><li><p><strong>What hardware?</strong> Cloud instance generation matters. On-prem versus cloud matters.</p></li></ul><p>If three independent sources say Platform A is faster for analytical workloads, that's meaningful signal. If only the vendor says it, I discount heavily. Patterns across benchmarks are more reliable than individual results.</p><p>My advice: use benchmarks for initial filtering, reducing 50 options to 3-5 candidates. Then run your own tests on the finalists. Smaller scope, but matched to your reality. And factor in non-performance variables: cost, operational complexity, ecosystem, lock-in risk. Speed isn't everything.</p><div><hr></div><h2>Conclusions</h2><p>I've spent my career watching data practitioners navigate real constraints: a week or two to make platform decisions with multi-year financial implications, accountability for cloud spend in the millions, corporate mandates that remove choice but still require cost-performance analysis, and 50+ platform options that can't all be evaluated in depth.</p><p>Benchmarks are how practitioners navigate these constraints, imperfect as they are. The solution isn't to ignore them. It's to use them with appropriate skepticism, corroborate across sources, and supplement with targeted testing on your actual workload.</p><p>The critique of benchmarks should drive demand for <em>better</em> benchmarks. That's what I'm building with Oxbow Research: open methodology, published data, no vendor funding.</p><p>If you're evaluating platforms this quarter, here's a practical starting point:</p><ul><li><p>Pick 3-5 candidates using independent benchmark patterns.</p></li><li><p>Run a scoped in-house test on your top 2 workloads.</p></li><li><p>Present a short cost-performance tradeoff memo before committing.</p></li></ul><div><hr></div><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://oxbowresearch.com/p/benchmarking-is-good-actually?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Thanks for reading Oxbow Research! This post is public so feel free to share it.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://oxbowresearch.com/p/benchmarking-is-good-actually?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://oxbowresearch.com/p/benchmarking-is-good-actually?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p></div><div><hr></div><h2>Footnotes</h2><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p><a href="https://www.scnsoft.com/data/data-warehouse/enterprise">Enterprise Data Warehouse: A Full Guide for 2025</a> - ScienceSoft. Decision timeline research: goals elicitation 3-20 days, tech stack selection 2-15 days, business case creation 2-15 days, implementation 6-12 months.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>Gartner, "Risks and Challenges in Data Migrations and Conversions" (G00165710, 2009). This widely cited 83% migration failure/overrun figure is nearly two decades old, but remains the most commonly referenced stat in the field. See also <a href="https://www.curiositysoftware.ie/blog/too-many-migration-projects-fail">The Research Is Clear: Too Many Migration Projects Fail</a> - Curiosity Software, which surveys multiple migration failure studies with similar findings.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p><a href="https://www.persistent.com/client-success/from-teradata-to-the-cloud-building-a-future-ready-data-foundation-while-saving-140m/">From Teradata to the Cloud: Building a Future-Ready Data Foundation While Saving $140M</a> - Persistent Systems Client Success Story.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p><a href="https://hakkoda.io/resources/teradata-migration-a-step-by-step-guide/">Teradata Migration: A Step-by-Step Guide</a> - Hakkoda. Case study: 700+ queries migrated, 8-month engagement, 60% cost reduction, 85% query efficiency improvement, 16x faster testing.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p><a href="https://docs.databricks.com/en/migration/index.html">Data Warehouse Migration Best Practices</a> - Databricks Documentation. Parallel validation is a recommended practice across major cloud platform migration guides.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p><a href="https://foundryco.com/news/cloud-computing-study-2024/">Cloud Computing Study 2024</a> - Foundry (formerly IDG), August 2024. Survey of 821 global IT decision-makers: 63% accelerated cloud migration in 2024, up from 57% in 2023.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p><a href="https://cloud.google.com/bigquery/pricing">BigQuery Pricing</a> and <a href="https://www.snowflake.com/en/data-cloud/pricing-options/">Snowflake Pricing</a> - Official vendor documentation. Pricing varies by compute model, commitment level, and workload patterns.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-8" href="#footnote-anchor-8" class="footnote-number" contenteditable="false" target="_self">8</a><div class="footnote-content"><p><a href="https://www.cloudzero.com/blog/cloud-computing-statistics/">90+ Cloud Computing Statistics: A 2025 Market Snapshot</a> - CloudZero, citing Gartner. Global cloud spending $723.4B in 2025.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-9" href="#footnote-anchor-9" class="footnote-number" contenteditable="false" target="_self">9</a><div class="footnote-content"><p>See <a href="https://aws.amazon.com/savingsplans/">AWS Savings Plans</a>, <a href="https://cloud.google.com/compute/docs/instances/signing-up-committed-use-discounts">Google Cloud Committed Use Discounts</a>, and <a href="https://azure.microsoft.com/en-us/pricing/reservations/">Azure Reservations</a>. Discount ranges vary by provider, commitment length, and payment structure.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-10" href="#footnote-anchor-10" class="footnote-number" contenteditable="false" target="_self">10</a><div class="footnote-content"><p><a href="https://cloud.google.com/blog/products/databases/2024-gartner-magic-quadrant-for-cloud-database-management-systems">2024 Gartner Magic Quadrant for Cloud Database Management Systems</a> - Google Cloud Blog, December 2024. Gartner projection (as cited): 75% of DBMS decisions by business domain leaders by 2027.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Explore SQL vs DataFrame performance with the BenchBox MCP and Claude Code]]></title><description><![CDATA[Use the BenchBox MCP to execute benchmarks, compare results, and investigate performance outliers - without leaving Claude Code.]]></description><link>https://oxbowresearch.com/p/explore-sql-vs-dataframe-performance</link><guid isPermaLink="false">https://oxbowresearch.com/p/explore-sql-vs-dataframe-performance</guid><dc:creator><![CDATA[Joe Harris]]></dc:creator><pubDate>Thu, 05 Feb 2026 22:47:10 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!b0WJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2551562d-cc32-49f4-a0ce-d450138f45c2_2292x2994.heic" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>TL;DR: This post shows how the BenchBox MCP server (using Claude Code) automates benchmark discovery, execution, and investigative analysis. Using 5 simple prompts, I run and compare TPC-H at scale factor 1 with DataFusion's SQL and DataFrame support. I discover that TPC-H query 19 is 150% slower in DataFrame mode vs SQL. Root cause: the SQL optimizer extracts common join conditions from OR branches while the DataFrame API's explicit join-then-filter approach prevents the same optimization.</p><h2>What I'm demonstrating</h2><p>BenchBox exposes a Model Context Protocol (MCP) server that lets AI assistants interact with the benchmarking framework conversationally. You ask questions in plain language; the assistant calls BenchBox tools behind the scenes, exploring platforms, running benchmarks, comparing results, investigating anomalies. This post walks through one such session. I start with a question, explore the platform catalog, execute benchmarks, and follow an unexpected result down to its root cause.</p><p>Here are my 5 simple prompts for the agent:</p><ol><li><p><em>"Which BenchBox platforms support both SQL and DataFrame execution and can run locally on this computer?"</em></p></li><li><p><em>"Run TPC-H sf1 Power testing with DataFusion using both SQL and DataFrame modes. Compare the runs and flag differences &gt;10%."</em></p></li><li><p><em>"Show me the BenchBox head-to-head visualization comparing the results."</em></p></li><li><p><em>"Identify the query with largest performance regression. Explain what is different in the DataFrame and SQL executions."</em></p></li><li><p><em>"Write a technical analysis of the SQL vs DataFrame executions for this query. Use a diagram to show where they diverge"</em></p></li></ol><div><hr></div><h2>Install BenchBox and setup MCP server</h2><p>Create a virtual environment and install BenchBox with MCP support:</p><pre><code># Create uv virtual environment
uv venv ~/mcp_test/.venv
# Install benchbox with MCP extra
uv pip install --python ~/mcp_test/.venv/bin/python "benchbox[mcp]"
# Create project-level MCP server config
echo '{"mcpServers":{"benchbox":{"command":"~/mcp_test/.venv/bin/benchbox-mcp","args":[]}}}' &gt; ~/mcp_test/.mcp.json</code></pre><p><em>Sidebar</em>: Claude asks for approval to use the BenchBox MCP tool, either every time (1) or the first time (2).</p><blockquote><pre><code>Tool use
benchbox - Read-only discovery tool() (MCP)
List all available database platforms.
       Returns information about each platform including:&#8230;
Do you want to proceed?
&#10095; 1. Yes
2. Yes, and don't ask again for benchbox - Read-only discovery tool commands in ~/mcp_test
3. No</code></pre></blockquote><div><hr></div><h2>Exploring the platform catalog</h2><h5><code>prompt</code>:   </h5><p><em><strong>"Which BenchBox platforms support both SQL and DataFrame execution and can run locally on this computer?"</strong></em></p><h5><code>response</code>:</h5><blockquote><p>Platforms Supporting Both SQL and DataFrame</p><p>Of BenchBox's 37 platforms, 13 support both SQL and DataFrame execution. Filtering to those that can run locally (not cloud/managed services):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|l|}\n\\hline\n\\textbf{Platform} &amp; \\textbf{Category} &amp; \\textbf{Dependencies} &amp; \\textbf{Status} \\\\\n\\hline\n\\text{**DataFusion**} &amp; \\text{Analytical} &amp; \\text{datafusion, pyarrow} &amp; \\text{**Ready to run**} \\\\\n\\hline\n\\text{**PySpark**} &amp; \\text{DataFrame} &amp; \\text{pyspark} &amp; \\text{Missing pyspark} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;MODTKVGHUB&quot;}" data-component-name="LatexBlockToDOM"></div><p><strong>DataFusion is the only one ready today</strong></p><p>It's installed and available on this system with all dependencies met (<code>datafusion + pyarrow 23.0.0</code>). It runs natively on ARM64 and is Arrow-native, so it's a natural fit for your Apple Silicon Mac (10-core, 16GB).</p><p>PySpark can run locally in standalone mode, but would need <code>pyspark</code> installed (<code>uv pip install "benchbox[dataframe-pyspark]"</code>).</p><p><strong>Why not the others?</strong></p><p>The remaining 11 dual-mode platforms (Databricks, Athena Spark, Dataproc, EMR Serverless, Glue, Fabric Spark, Synapse Spark, Snowpark Connect, Quanton, etc.) are all cloud/managed services &#8212; they require remote infrastructure and credentials, not local execution.</p><p><strong>Notable local-only platforms (single mode)</strong></p><p>For context, you also have these local platforms ready but they only support one mode:</p><ul><li><p><strong>DuckDB</strong> (<code>v1.4.4</code>) &#8212; SQL only</p></li><li><p><strong>Polars</strong> (<code>v1.38.1</code>) &#8212; DataFrame only</p></li></ul><p>So if you want to compare SQL vs. DataFrame execution on the same engine locally, DataFusion is your only option right now.</p></blockquote><p>I'll use DataFusion for this investigation, keeping the SQL-vs-DataFrame comparison on the same local engine, removing cloud infrastructure variance.</p><div><hr></div><h2>Running the benchmarks</h2><h5><code>prompt</code>:</h5><p><em><strong>"Run TPC-H sf1 Power testing with DataFusion using both SQL and DataFrame modes. Compare the runs and flag differences &gt;10%. These benchmarks will run locally so do not run them in parallel."</strong></em></p><h5><code>response</code>:</h5><blockquote><h3>Results: DataFusion SQL vs DataFrame &#8212; TPC-H SF1</h3><p>Both runs completed all 22 queries with 3 measurement iterations on DataFusion 51.0.0.</p><p><strong>Overall</strong></p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|l|}\n\\hline\n\\textbf{Metric} &amp; \\textbf{SQL} &amp; \\textbf{DataFrame} &amp; \\textbf{Delta} \\\\\n\\hline\n\\text{Geo Mean} &amp; \\text{70.9 ms} &amp; \\text{59.5 ms} &amp; \\text{-16.1\\% (DF faster)} \\\\\n\\hline\n\\text{Total Time} &amp; \\text{1,848 ms} &amp; \\text{1,573 ms} &amp; \\text{-14.9\\%} \\\\\n\\hline\n\\text{Power@Size} &amp; \\text{48,940} &amp; \\text{60,279} &amp; \\text{+23.2\\%} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;UJADJXDPHF&quot;}" data-component-name="LatexBlockToDOM"></div><p>DataFrame mode is ~16% faster overall, with a materially higher TPC-H Power metric.</p><p><strong>Queries Exceeding 10% Difference (16 improved, 3 regressed, 3 stable)</strong></p><p><strong>DataFrame wins (16 queries):</strong></p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|l|l|}\n\\hline\n\\textbf{Query} &amp; \\textbf{SQL (ms)} &amp; \\textbf{DF (ms)} &amp; \\textbf{Change} &amp; \\textbf{Notes} \\\\\n\\hline\n\\text{Q18} &amp; \\text{332} &amp; \\text{94} &amp; \\text{-71.7%} &amp; \\text{Largest improvement} \\\\\n\\hline\n\\text{Q22} &amp; \\text{56} &amp; \\text{21} &amp; \\text{-62.5%} &amp; \\text{} \\\\\n\\hline\n\\text{Q5} &amp; \\text{114} &amp; \\text{43} &amp; \\text{-62.3%} &amp; \\text{} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;QPXBLVKFBO&quot;}" data-component-name="LatexBlockToDOM"></div><p><strong>SQL wins (3 queries):</strong></p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|l|l|}\n\\hline\n\\textbf{Query} &amp; \\textbf{SQL (ms)} &amp; \\textbf{DF (ms)} &amp; \\textbf{Change} &amp; \\textbf{Notes} \\\\\n\\hline\n\\text{Q19} &amp; \\text{55} &amp; \\text{140} &amp; \\text{+154.5%} &amp; \\text{Largest regression} \\\\\n\\hline\n\\text{Q12} &amp; \\text{52} &amp; \\text{114} &amp; \\text{+119.2%} &amp; \\text{} \\\\\n\\hline\n\\text{Q21} &amp; \\text{106} &amp; \\text{206} &amp; \\text{+94.3%} &amp; \\text{} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;AXNNZHOIWQ&quot;}" data-component-name="LatexBlockToDOM"></div><p><strong>Key Takeaways</strong></p><ol><li><p><strong>DataFrame mode is broadly faster</strong> &#8212; 16 of 22 queries improved, many substantially. The DataFrame API bypasses SQL parsing and can express optimizations more directly to the execution engine.&gt;</p></li><li><p><strong>Three significant regressions stand out</strong> &#8212; Q19, Q12, and Q21 are all dramatically slower in DataFrame mode (2-2.5x). These likely involve query patterns where the hand-written DataFrame code generates a les&gt;s &gt;optimal plan than what the SQL optimizer produces. Q19</p></li></ol><p>involves complex disjunctive predicates (OR conditions), Q12 has ordered aggregation, and Q21 involves multi-level correlated subqueries &#8212; all patterns where SQL optimization heuristics can outperform manual &gt;DataFrame construction.</p><ol><li><p><strong>Q18 is the standout improvement</strong> &#8212; 332ms (SQL) vs 94ms (DF), a 3.5x improvement. This large-order query with HAVING clause and nested aggregation appears to benefit significantly from the DataFrame API's more direct expression of the computation.</p></li></ol></blockquote><p>The biggest percentage regression is Q19: DataFrame mode is 2.55x slower (+154.5%). I will investigate Q19.</p><div><hr></div><h2>Visual comparison of SQL vs DataFrame</h2><h5><code>prompt</code>:</h5><p><em><strong>"Show me the BenchBox head-to-head visualization comparing the results."</strong></em></p><h5><code>response</code>:</h5><blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!b0WJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2551562d-cc32-49f4-a0ce-d450138f45c2_2292x2994.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!b0WJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2551562d-cc32-49f4-a0ce-d450138f45c2_2292x2994.heic 424w, https://substackcdn.com/image/fetch/$s_!b0WJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2551562d-cc32-49f4-a0ce-d450138f45c2_2292x2994.heic 848w, https://substackcdn.com/image/fetch/$s_!b0WJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2551562d-cc32-49f4-a0ce-d450138f45c2_2292x2994.heic 1272w, https://substackcdn.com/image/fetch/$s_!b0WJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2551562d-cc32-49f4-a0ce-d450138f45c2_2292x2994.heic 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!b0WJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2551562d-cc32-49f4-a0ce-d450138f45c2_2292x2994.heic" width="1456" height="1902" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2551562d-cc32-49f4-a0ce-d450138f45c2_2292x2994.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1902,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:421527,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://oxbowresearch.com/i/187032176?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2551562d-cc32-49f4-a0ce-d450138f45c2_2292x2994.heic&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!b0WJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2551562d-cc32-49f4-a0ce-d450138f45c2_2292x2994.heic 424w, https://substackcdn.com/image/fetch/$s_!b0WJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2551562d-cc32-49f4-a0ce-d450138f45c2_2292x2994.heic 848w, https://substackcdn.com/image/fetch/$s_!b0WJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2551562d-cc32-49f4-a0ce-d450138f45c2_2292x2994.heic 1272w, https://substackcdn.com/image/fetch/$s_!b0WJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2551562d-cc32-49f4-a0ce-d450138f45c2_2292x2994.heic 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Embedded image to show colors, Substack code blocks do not support ANSI color codes</em></p></blockquote><p>Most queries improved in DataFrame mode, but Q19, Q12, and Q21 are large regressions that dominate the risk profile.</p><div><hr></div><h2>Investigating the largest regression (Q19)</h2><h5><code>prompt</code>:</h5><p><em><strong>"Identify the query with largest performance regression. Explain what is different in the DataFrame and SQL executions."</strong></em></p><h5><code>response</code>:</h5><blockquote><h3>Q19: Discounted Revenue &#8212; Root Cause Analysis</h3><p><strong>The Query</strong></p><p>Q19 computes revenue for parts matching three independent sets of conditions joined by OR. Each branch requires a join between lineitem (6M rows) and part (200K rows), plus brand/container/quantity/size filters.</p><p><strong>What the SQL Optimizer Does</strong></p><p>The SQL version uses an implicit comma-join with the join predicate inside the OR:</p><pre><code>FROM lineitem, part
WHERE (p_partkey = l_partkey AND p_brand = 'Brand#12' AND ...)
  OR (p_partkey = l_partkey AND p_brand = 'Brand#23' AND ...)
  OR (p_partkey = l_partkey AND p_brand = 'Brand#34' AND ...)</code></pre><p>DataFusion's SQL optimizer can analyze this structure and:</p><ol><li><p><strong>Extract the common join predicate</strong> &#8212; <code>p_partkey = l_partkey</code> appears in all three OR branches, so it's factored out as the hash join condition</p></li><li><p><strong>Extract common filter predicates</strong> &#8212; <code>l_shipmode IN ('AIR', 'AIR REG')</code> and <code>l_shipinstruct = 'DELIVER IN PERSON'</code> are identical across all branches and get pushed down to a lineitem scan filter</p></li><li><p><strong>Push part-side predicates filter</strong> &#8212; <code>p_brand IN ('Brand#12','Brand#23','Brand#34')</code> and <code>p_size BETWEEN 1 AND 15</code> (the union of all size ranges) can be used to filter the part scan</p></li></ol><p>This dramatically shrinks both sides of the join before it executes.</p><p><strong>What the DataFrame Code Does</strong></p><pre><code>lineitem.join(part, left_on="l_partkey", right_on="p_partkey")
   .filter(
       col("l_shipmode").is_in(ship_modes)
       &amp; (col("l_shipinstruct") == lit("DELIVER IN PERSON"))
       &amp; (  (branch1) | (branch2) | (branch3)  )
   )</code></pre><p>The join is specified correctly, but the entire filter is applied post-join as a single compound expression. The optimizer sees:</p><pre><code>HashJoin(lineitem, part, on=l_partkey=p_partkey)
 &#8594; Filter(complex_disjunction)</code></pre><p>The problem is predicate pushdown through disjunctions is hard. While the top-level l_shipmode and l_shipinstruct predicates sit outside the OR (the DataFrame code does structure this well), the OR branches &gt;themselves contain predicates on both sides of the join (p_brand from part, l_quantity from lineitem). The optimizer can't cleanly decompose and push these mixed-table predicates down through the OR.</p><p><strong>The Result</strong></p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|}\n\\hline\n\\textbf{What} &amp; \\textbf{SQL} &amp; \\textbf{DataFrame} \\\\\n\\hline\n\\text{lineitem rows entering join} &amp; \\text{Pre-filtered (~908K)} &amp; \\text{Full 6M} \\\\\n\\hline\n\\text{part rows entering join} &amp; \\text{Pre-filtered (~small)} &amp; \\text{Full 200K} \\\\\n\\hline\n\\text{Join output to filter} &amp; \\text{Small} &amp; \\text{Large} \\\\\n\\hline\n\\text{Runtime} &amp; \\text{55ms} &amp; \\text{140ms} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;TAYTGXFOLO&quot;}" data-component-name="LatexBlockToDOM"></div><p><strong>The Fix</strong></p><p>The DataFrame code could be restructured to match what the SQL optimizer does automatically &#8212; pre-filter both tables, then join, then apply the residual OR:</p><pre><code># Pre-filter common predicates BEFORE the join
li = lineitem.filter(
   col("l_shipmode").is_in(ship_modes)
   &amp; (col("l_shipinstruct") == lit("DELIVER IN PERSON"))
)
pt = part.filter(
   col("p_brand").is_in([brand1, brand2, brand3])
   &amp; (col("p_size") &gt;= lit(1))
   &amp; (col("p_size") &lt;= lit(15))
)
# Join the reduced inputs, then apply branch-specific filters
result = (
   li.join(pt, left_on="l_partkey", right_on="p_partkey")
   .filter(branch1 | branch2 | branch3)
   ...
)</code></pre><p>This is the fundamental tradeoff: SQL optimizers have decades of research on predicate extraction and pushdown through disjunctions. DataFrame APIs express computation imperatively, putting the optimization burden on the developer. Q19 is a textbook case &#8212; its deeply nested OR-of-AND structure is exactly the pattern where SQL optimizers shine and naive DataFrame construction falls short.</p></blockquote><p>The SQL optimizer extracts the common join condition from OR branches and pushes filters down. In this plan shape, DataFusion's DataFrame path cannot do the same rewrite once the join is explicitly fixed before the filter.</p><h3>The predicate extraction gap</h3><h5><code>prompt</code>: </h5><p><em><strong>"Write a technical analysis of the SQL vs DataFrame executions for this query. Use a diagram to show where they diverge."</strong></em></p><h5><code>response</code>:</h5><blockquote><h3>Compilation Path Divergence</h3><pre><code>        SQL Path                        DataFrame Path
        &#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;                        &#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;
   FROM lineitem, part             lineitem.join(part, partkey)
   WHERE (key=key AND ...)         .filter(common &amp; (b1|b2|b3))
      OR (key=key AND ...)                    &#9474;
      OR (key=key AND ...)                    &#9474;
             &#9660;                                &#9660;
&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;      &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; Parser &#8594; CrossJoin       &#9474;      &#9474; Builder &#8594; InnerJoin      &#9474;
&#9474; Join key INSIDE the OR   &#9474;      &#9474; Join key ALREADY SET     &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;      &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
            &#9660;                                 &#9474;
&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;        (rule does not fire:
&#9474; ExtractEquijoinPredicate &#9474;         join already resolved)
&#9474;                          &#9474;                   &#9474;
&#9474; Finds partkey = partkey  &#9474;                   &#9474;
&#9474; in ALL branches &#8594; hoist  &#9474;                   &#9474;
&#9474; as join key              &#9474;                   &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;                   &#9474;
            &#9660;                                 &#9660;
&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;      &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; PushDownFilter           &#9474;      &#9474; PushDownFilter           &#9474;
&#9474;                          &#9474;      &#9474;                          &#9474;
&#9474; Clean single-table preds &#9474;      &#9474; OR branches mix both     &#9474;
&#9474; after join key removed:  &#9474;      &#9474; tables (p_brand AND      &#9474;
&#9474; &#8226; shipmode &#8594; lineitem    &#9474;      &#9474; l_quantity) so cannot    &#9474;
&#9474; &#8226; shipinstruct &#8594; lineitem&#9474;      &#9474; push to either side      &#9474;
&#9474; &#8226; p_size &gt;= 1 &#8594; part     &#9474;      &#9474;                          &#9474;
&#9474; &#8226; p_brand IN &#8594; part      &#9474;      &#9474; Only top-level ANDs push:&#9474;
&#9474;                          &#9474;      &#9474; &#8226; shipmode &#8594; lineitem    &#9474;
&#9474; Both scans filtered      &#9474;      &#9474; &#8226; shipinstruct &#8594; lineitem&#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;      &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
            &#9660;                                 &#9660;
  HashJoin(partkey)                 Filter(OR(b1,b2,b3))
  &#9500;&#9472; Scan lineitem [2 preds]       &#9492;&#9472; HashJoin(partkey)
  &#9474;    ~908K rows                     &#9500;&#9472; Scan lineitem [2 preds]
  &#9492;&#9472; Scan part [2 preds]              &#9474;    ~908K rows
       ~small subset                  &#9492;&#9472; Scan part [no filter]
                                            200K rows
            &#9660;                                 &#9660;
          55 ms                            140 ms</code></pre><p><strong>Where the gap opens</strong></p><p>The critical moment is <strong>ExtractEquijoinPredicate</strong>. This optimizer rule scans the WHERE clause for equijoin conditions. In SQL mode, it finds <code>p_partkey = l_partkey</code> buried inside all three OR branches and hoists it out as the join key. The remaining predicate, now free of the join condition, becomes a simpler OR-of-filters that PushDownFilter can decompose more aggressively.</p><p>In DataFrame mode, this rule never fires because the join is already explicit. The developer did the right thing by specifying the join key directly, but in doing so, they prevented the optimizer from ever seeing the predicate structure that SQL mode exploits.</p><p>The second gap is in PushDownFilter. DataFusion's filter pushdown can extract common factors from OR branches using the identity <code>(A&#8743;B) &#8744; (A&#8743;C) &#8594; A &#8743; (B&#8744;C)</code>. In SQL mode, after the join condition is extracted, the remaining branches are cleaner, containing single-table predicates that factor neatly. In DataFrame mode, each OR branch still contains predicates from both tables (<code>p_brand</code> from part, <code>l_quantity</code> from lineitem), which prevents the optimizer from attributing them to one side of the join.</p><p>The net effect: SQL mode filters the part table down to a small subset (3 brands, size 1-15) before joining. DataFrame mode joins all 200K part rows against the filtered lineitem, producing a much larger intermediate result that the OR filter must then evaluate row by row.</p><p><strong>External confirmation</strong></p><p>From DataFusion's push_down_filter.rs (<a href="https://github.com/apache/datafusion/blob/main/datafusion/optimizer/src/push_down_filter.rs">push_down_filter.rs</a>)</p><pre><code><em>"For expressions like (A AND B) OR (C AND D), the optimizer transforms to: ((A AND B) OR (C AND D)) AND (A OR C)"</em></code></pre><p>The optimizer can only extract common factors across OR branches. It cannot recognize that a join condition appears in every branch, hoist it out, and convert the pattern to JOIN ON key WHERE (filters1 OR filters2 OR filters3).</p><p>From the DataFusion blog <a href="https://datafusion.apache.org/blog/2025/06/15/optimizing-sql-dataframes-part-one">(optimizing-sql-dataframes-part-one</a>):</p><pre><code><em>Both [SQL and DataFrame] are translated into the same LogicalPlan... the DataFrame API builds the LogicalPlan directly using LogicalPlanBuilder.</em></code></pre><p>The same optimizer processes both paths, but the initial plan structure determines what optimizations are possible. SQL's implicit join gives the optimizer more room to restructure; DataFrame's explicit join is a constraint the optimizer respects.</p></blockquote><p>This gap is caused by the difference in how query APIs are interpreted before execution, it is not caused by any specific DataFrame API overhead.</p><div><hr></div><h2>Learnings</h2><p>The BenchBox MCP enables Claude Code to quickly compare DataFusion's SQL vs DataFrame modes, identify Q19 as a 2.55x regression (+154.5%), and trace the source to predicate pushdown during query planning. A future post will contrast this with letting Claude Code try this _without_ the MCP (spoiler: slow, confident, and wrong).</p><p>This is a DataFusion-specific result, not a blanket statement about DataFrame APIs. Polars, PySpark, etc. all have different optimization capabilities and can make different planning choices on the same logical query shape.</p><p>For Q19 on DataFusion, SQL is faster because the optimizer extracts shared predicates from <code>OR</code> branches and pushes filters earlier. DataFusion's DataFrame path starts from the user-expressed plan shape, and does not get the SQL-only rewrites.</p><p>However, DataFrame is actually faster for relatively straightforward queries. 16 of the 22 TPC-H queries ran more quickly, including a 3.5x improvement on Q18. The impact of the optimization difference is query-specific.</p><p>My recommendation today: on DataFusion, prefer SQL for OR-heavy multi-table predicates like Q19, Q12, and Q21. Use DataFrame mode when query construction ergonomics matter and your workload resembles the 16 queries where DataFrame won.</p><p>Note that this is not a permanent DataFusion limitation. DataFusion is moving forward quickly and the optimizer keeps evolving. This specific gap could close in future releases.</p><div><hr></div><h2>BenchBox test environment</h2><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|}\n\\hline\n\\textbf{Component} &amp; \\textbf{Detail} \\\\\n\\hline\n\\text{**Hardware**} &amp; \\text{Apple Mac mini M4 (10-core, 16GB unified memory)} \\\\\n\\hline\n\\text{**DataFusion**} &amp; \\text{51.0.0} \\\\\n\\hline\n\\text{**BenchBox**} &amp; \\text{0.1.2} \\\\\n\\hline\n\\text{**Python**} &amp; \\text{3.12} \\\\\n\\hline\n\\text{**OS**} &amp; \\text{MacOS Tahoe 26.2} \\\\\n\\hline\n\\text{**Benchmark**} &amp; \\text{TPC-H SF1 (\\textasciitilde 1GB, 8.6M rows)} \\\\\n\\hline\n\\text{**Phases**} &amp; \\text{load, power (3 iterations with warmup)} \\\\\n\\hline\n\\text{**Tuning**} &amp; \\text{None} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;FXCYOGXZLH&quot;}" data-component-name="LatexBlockToDOM"></div><p><strong>BenchBox CLI equivalent</strong>:</p><pre><code>$ benchbox run --platform datafusion --benchmark tpch --scale 1 --phases load,power
$ benchbox run --platform datafusion-df --benchmark tpch --scale 1 --phases load,power</code></pre><p><strong>BenchBox raw results</strong> (<a href="https://gist.github.com/joeharris76/be6f7069b758163fc111b1f4a6c888a1">gist</a>):</p><ul><li><p><a href="https://gist.github.com/joeharris76/be6f7069b758163fc111b1f4a6c888a1#file-tpch_sf1_datafusion_sql_20260212_172405_mcp_6e9f667a-json">DataFusion SQL</a></p></li><li><p><a href="https://gist.github.com/joeharris76/be6f7069b758163fc111b1f4a6c888a1#file-tpch_sf1_datafusion_df_20260212_172419_mcp_b5db4a85-json">DataFusion DataFrame</a></p></li></ul><p><strong>Test Limitations</strong>:</p><ul><li><p>Single-node, Apple Silicon, default DataFusion configuration, TPC-H Power test only</p></li><li><p>TPC-H DataFrame queries were created for BenchBox and are not officially provided.</p></li><li><p>BenchBox's DataFrame Q19 may not represent the best possible translation of the SQL query.</p></li><li><p>BenchBox's DataFusion integration reads from parquet files and operates in-memory.</p></li></ul><div><hr></div><h2>Try it yourself</h2><p>The full investigation, from platform discovery to query plan analysis, took one session. Connect the BenchBox MCP server to your AI assistant and start with a question like "Which platforms support both SQL and DataFrame execution?"</p><p>Or run directly via CLI:</p><pre><code>$ uv run benchbox run --platform datafusion --benchmark tpch --scale 1
$ uv run benchbox run --platform datafusion-df --benchmark tpch --scale 1
$ uv run benchbox compare --head-to-head --runs {run_id_sql} {run_id_df}</code></pre><p>If you find that you cannot reproduce this, <a href="https://github.com/joeharris76/benchbox/issues">please open an issue</a> with your run result JSON files attached. The key signal to compare is whether Q19 remains above 2x on your hardware and DataFusion version.</p><div><hr></div><h2>Resources</h2><ul><li><p><a href="https://github.com/joeharris76/benchbox">BenchBox GitHub Repository</a>, Benchmarking framework used for this analysis</p></li><li><p><a href="https://datafusion.apache.org/">Apache DataFusion</a>, extensible query engine written in Rust that uses Apache Arrow as its in-memory format</p></li><li><p><a href="https://www.tpc.org/tpch/">TPC-H Benchmark Specification</a>, TPC-H is a decision support benchmark, official documentation</p></li></ul>]]></content:encoded></item><item><title><![CDATA[Does your database allow benchmarks? A 2026 DeWitt clause survey]]></title><description><![CDATA[Since 1983 many database vendors forbade independent benchmarks. That's started to change in 2021. Here's the current status of benchmark publication rights in 2026.]]></description><link>https://oxbowresearch.com/p/does-your-database-allow-benchmarks</link><guid isPermaLink="false">https://oxbowresearch.com/p/does-your-database-allow-benchmarks</guid><dc:creator><![CDATA[Joe Harris]]></dc:creator><pubDate>Thu, 05 Feb 2026 15:15:36 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!TC5N!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc6c2920-6605-451c-91b9-70714c09368a_1400x538.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>TL;DR:</h2><p>For 40 years, "DeWitt clauses" let vendors legally block benchmark publication. Since 2021, that's changed: open source databases have no restrictions, major cloud vendors (AWS, Azure, Google, Databricks, Snowflake) now allow benchmarks with methodology disclosure, and only legacy holdouts (Oracle, SQL Server) still require permission. The pattern is clear: vendors confident in their performance welcome scrutiny.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!TC5N!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc6c2920-6605-451c-91b9-70714c09368a_1400x538.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TC5N!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc6c2920-6605-451c-91b9-70714c09368a_1400x538.png 424w, https://substackcdn.com/image/fetch/$s_!TC5N!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc6c2920-6605-451c-91b9-70714c09368a_1400x538.png 848w, https://substackcdn.com/image/fetch/$s_!TC5N!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc6c2920-6605-451c-91b9-70714c09368a_1400x538.png 1272w, https://substackcdn.com/image/fetch/$s_!TC5N!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc6c2920-6605-451c-91b9-70714c09368a_1400x538.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TC5N!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc6c2920-6605-451c-91b9-70714c09368a_1400x538.png" width="1400" height="538" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fc6c2920-6605-451c-91b9-70714c09368a_1400x538.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:538,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:259438,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://oxbowresearch.com/i/186985096?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc6c2920-6605-451c-91b9-70714c09368a_1400x538.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!TC5N!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc6c2920-6605-451c-91b9-70714c09368a_1400x538.png 424w, https://substackcdn.com/image/fetch/$s_!TC5N!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc6c2920-6605-451c-91b9-70714c09368a_1400x538.png 848w, https://substackcdn.com/image/fetch/$s_!TC5N!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc6c2920-6605-451c-91b9-70714c09368a_1400x538.png 1272w, https://substackcdn.com/image/fetch/$s_!TC5N!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc6c2920-6605-451c-91b9-70714c09368a_1400x538.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><h2>What is the "DeWitt clause"?</h2><p>The DeWitt Clause is part of a software license agreement that prohibits users from publishing benchmark results without the vendor's approval. From Oracle's current license:</p><blockquote><p><em>"You may not disclose results of any program benchmark tests without our prior consent."</em><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a></p></blockquote><p>Microsoft SQL Server's license contains a similar restriction:</p><blockquote><p><em>"You must obtain Microsoft's prior written approval to disclose to a third party the results of any benchmark test of the software."</em><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a></p></blockquote><p>These clauses create a chilling effect on database evaluation. Researchers can't publish comparative studies. Consultants can't share findings with the broader community. Customers have to make purchasing decisions based on vendor marketing rather than independent verification. The only databases that can be rigorously critiqued in public are open source, which puts proprietary vendors at an unfair advantage when their performance is actually competitive, and shields them when it isn't.</p><p>The clauses also have a corrosive, self-reinforcing quality. Once one vendor adopts a DeWitt clause, competitors feel disadvantaged without one. After all, if Vendor A can't be publicly critiqued but Vendor B can, Vendor B faces asymmetric scrutiny regardless of actual performance<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>.</p><h2>The origin of the clause</h2><p>The DeWitt clause story starts in 1983, when David DeWitt and his colleagues created the Wisconsin Benchmark to measure relational database performance<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>. When they published their findings, Oracle's performance stood out as particularly poor. According to DeWitt, Oracle CEO Larry Ellison was furious. He called the department chair and demanded: "You have to fire this guy."<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a></p><p>Oracle didn't succeed in getting DeWitt fired. Instead, they did something with longer-lasting consequences: they added a clause to their license agreement prohibiting customers from publishing benchmark results without Oracle's prior written consent<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>.</p><p>This provision became known as the "DeWitt Clause", somewhat ironic given that DeWitt championed benchmarking. The clause spread throughout the database industry like a virus, adopted by nearly every major commercial vendor. For 40 years, DeWitt's name has been synonymous with preventing the very transparency he fought for<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a>.</p><p>But things are changing. Since 2021, there has been a shift in vendor attitudes toward benchmark publication. This post surveys the current landscape as of early 2026, documenting which vendors restrict benchmarks, which have opened up, and what it means for anyone trying to make informed database decisions.</p><div><hr></div><h2>The 2021 turning point</h2><p>For nearly four decades, DeWitt clauses were just how the database industry worked. But cloud hyperscalers were the quiet exception. AWS and Microsoft Azure never adopted traditional DeWitt clauses. Instead, they used reciprocal terms from the start: you could publish benchmarks as long as you shared your methodology and granted them the same rights to benchmark your products. They didn't make a big deal about it and mostly used it as a shield when competing with each other.</p><p>Then Databricks turned benchmarking transparency into a competitive weapon. In November, 2021, Databricks announced that Databricks SQL had set a new world record on the 100TB TPC-DS benchmark, outperforming the previous record by 2.2x<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-8" href="#footnote-8" target="_self">8</a>. This was significant not just for the result itself, but because it was the first official TPC-audited benchmark from a cloud data warehouse vendor. The results were verified by the Transaction Processing Performance Council in a 37-page disclosure report<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-9" href="#footnote-9" target="_self">9</a>.</p><p>Six days later, Databricks announced they were eliminating the DeWitt Clause from their service terms entirely<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-10" href="#footnote-10" target="_self">10</a>. But they went further, introducing what they called a "DeWitt Embrace Clause":</p><blockquote><p><em>"If a competitor or vendor benchmarks Databricks or instructs a third party to do so, this new provision invalidates the vendor's own DeWitt Clause to allow reciprocal benchmarking."</em></p></blockquote><p>In other words: benchmark us, and we can benchmark you back, regardless of what your license says. The move signaled Databricks was confident in their performance and wanted to force competitors into the open. It worked. Within weeks, Snowflake responded with their own benchmarks and removed their DeWitt clause<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-11" href="#footnote-11" target="_self">11</a>. What followed was a brief benchmark war between the two companies, with competing claims, counter-benchmarks, and accusations of unfair methodology.</p><p>The specifics of who "won" that battle matter less than the outcome: two major cloud data warehouse vendors had permanently abandoned benchmark restrictions. Others followed. By the end of 2023, SingleStore had eliminated their clause<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-12" href="#footnote-12" target="_self">12</a>. AWS and Azure, which had long maintained reciprocal (rather than restrictive) benchmark terms, saw their approach validated.</p><p>The dam hasn't broken entirely. Oracle, Microsoft SQL Server, and several cloud-only services still maintain restrictions. But when I look at the industry now versus 2020, the shift is unmistakable.</p><div><hr></div><h2>Current status by vendor category</h2><p>I surveyed over 25 database vendors and cloud services to document current benchmark publication policies as of January 2026. Here's what I found, and what the patterns tell us about vendor confidence.</p><h3>Open source: no restrictions, of course</h3><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|}\n\\hline\n\\textbf{Database} &amp; \\textbf{License} &amp; \\textbf{Benchmark Status} \\\\\n\\hline\n\\text{PostgreSQL} &amp; \\text{PostgreSQL License (BSD-like)} &amp; \\text{Unrestricted} \\\\\n\\hline\n\\text{DuckDB} &amp; \\text{MIT} &amp; \\text{Unrestricted} \\\\\n\\hline\n\\text{SQLite} &amp; \\text{Public Domain} &amp; \\text{Unrestricted} \\\\\n\\hline\n\\text{ClickHouse} &amp; \\text{Apache 2.0} &amp; \\text{Unrestricted} \\\\\n\\hline\n\\text{Apache Spark} &amp; \\text{Apache 2.0} &amp; \\text{Unrestricted} \\\\\n\\hline\n\\text{Trino} &amp; \\text{Apache 2.0} &amp; \\text{Unrestricted} \\\\\n\\hline\n\\text{PrestoDB} &amp; \\text{Apache 2.0} &amp; \\text{Unrestricted} \\\\\n\\hline\n\\text{Apache DataFusion} &amp; \\text{Apache 2.0} &amp; \\text{Unrestricted} \\\\\n\\hline\n\\text{Polars} &amp; \\text{MIT} &amp; \\text{Unrestricted} \\\\\n\\hline\n\\text{NVIDIA RAPIDS cuDF} &amp; \\text{Apache 2.0} &amp; \\text{Unrestricted} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;EFDWBCCTCI&quot;}" data-component-name="LatexBlockToDOM"></div><p>Open source licenses cannot contain DeWitt clauses by definition. The Apache 2.0 license grants users the right to "use, reproduce, and distribute" the software for any purpose<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-13" href="#footnote-13" target="_self">13</a>. The MIT license permits use "without restriction"<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-14" href="#footnote-14" target="_self">14</a>. The PostgreSQL license explicitly allows use "for any purpose, without fee, and without a written agreement"<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-15" href="#footnote-15" target="_self">15</a>.</p><p>This isn't a loophole, it's fundamental to what open source means. You can benchmark PostgreSQL, publish the results, and PostgreSQL Global Development Group has no legal recourse (nor would they want any).</p><p>DuckDB goes further, actively encouraging benchmarks. Their FAQ recommends using preview releases for fairness, references their academic paper "Fair Benchmarking Considered Difficult" on methodology pitfalls, and asks only that you report version numbers<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-16" href="#footnote-16" target="_self">16</a>.</p><p>If you're benchmarking open source databases, you have nothing to worry about legally. Publish away.</p><h3>Cloud vendors with reciprocal rights (DeWitt embrace)</h3><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|l|}\n\\hline\n\\textbf{Vendor} &amp; \\textbf{Services} &amp; \\textbf{Policy Summary} &amp; \\textbf{When Adopted} \\\\\n\\hline\n\\text{AWS} &amp; \\text{Redshift, Athena, Aurora} &amp; \\text{May benchmark; must provide replication details; AWS may benchmark you} &amp; \\text{Pre-2021} \\\\\n\\hline\n\\text{Microsoft Azure} &amp; \\text{Synapse Analytics} &amp; \\text{May benchmark; competitors must share methodology and waive own restrictions} &amp; \\text{Pre-2021} \\\\\n\\hline\n\\text{Google Cloud} &amp; \\text{BigQuery, Spanner, AlloyDB} &amp; \\text{May benchmark; reciprocal rights; hyperscaler exclusion} &amp; \\text{Updated 2022-2024} \\\\\n\\hline\n\\text{Databricks} &amp; \\text{Databricks SQL, Delta Lake} &amp; \\text{May benchmark; must share methodology; grants reciprocal rights} &amp; \\text{Nov 2021} \\\\\n\\hline\n\\text{Snowflake} &amp; \\text{Snowflake} &amp; \\text{May benchmark; reciprocal rights apply} &amp; \\text{Nov 2021} \\\\\n\\hline\n\\text{SingleStore} &amp; \\text{SingleStore} &amp; \\text{May benchmark; must be reproducible; reciprocal rights} &amp; \\text{Nov 2023} \\\\\n\\hline\n\\text{Firebolt} &amp; \\text{Firebolt} &amp; \\text{May benchmark (no DeWitt clause per terms review)} &amp; \\text{Unknown} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;VQVRWWRSSQ&quot;}" data-component-name="LatexBlockToDOM"></div><p>These vendors permit benchmark publication with reciprocal rights provisions, what Databricks branded the "DeWitt Embrace" approach. Notably, AWS and Azure had these terms in place <em>before</em> Databricks coined the phrase; the hyperscalers never adopted traditional restrictive clauses. The key elements are consistent across vendors:</p><ol><li><p><strong>You may benchmark and publish results</strong></p></li><li><p><strong>You must provide methodology sufficient for reproduction</strong></p></li><li><p><strong>By publishing, you grant the vendor reciprocal benchmarking rights</strong></p></li></ol><p>The AWS Service Terms<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-17" href="#footnote-17" target="_self">17</a> state that you may benchmark and disclose results, but you must include "all information necessary to replicate such Benchmark," and AWS gains the right to benchmark your products in return. Microsoft's Online Services Terms<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-18" href="#footnote-18" target="_self">18</a> use nearly identical language.</p><p>The reciprocal element is clever: if you're a competitor and you publish benchmarks of their service, you've just waived your own benchmark restrictions. This creates mutual assured transparency, at least among those who choose to engage.</p><p>You can benchmark these services freely, provided you document your methodology thoroughly. For most evaluators (who aren't competing database vendors), the reciprocal obligation is irrelevant.</p><h3>Google Cloud: a recent convert</h3><p><strong>Google Cloud quietly removed their DeWitt clause.</strong> Sometime between 2022 and 2024, they updated their benchmark terms, a change that hasn't been widely commented upon.</p><p>The <em>old</em> Google Cloud terms (circa 2022) required customers to "obtain Google's prior written consent" before publishing any benchmark results<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-19" href="#footnote-19" target="_self">19</a>. This was a traditional restrictive DeWitt clause, similar to Oracle's.</p><p>The <em>current</em> terms use the same reciprocal approach as AWS and Azure:</p><blockquote><p><em>"Customer may conduct benchmark tests of the Services (each a 'Test'). Customer may only publicly disclose the results of such Tests if (a) the public disclosure includes all necessary information to replicate the Tests, and (b) Customer allows Google to conduct benchmark tests of Customer's publicly available products or services and publicly disclose the results of such tests."</em><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-20" href="#footnote-20" target="_self">20</a></p></blockquote><p>No prior approval required, just reciprocity.</p><p><strong>The hyperscaler exclusion</strong>: Google does maintain one unique restriction:</p><blockquote><p><em>"Customer may not do either of the following on behalf of a hyperscale public cloud provider without Google's prior written consent: (i) conduct (directly or through a third party) any Test or (ii) disclose the results of any such Test."</em><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-21" href="#footnote-21" target="_self">21</a></p></blockquote><p>This means AWS, Azure, or other hyperscale competitors can't commission benchmarks of Google Cloud services without permission. Independent researchers, enterprises, and consultants are unaffected.</p><p><strong>Service-specific restrictions</strong>: A few Google Cloud services still have full benchmark prohibitions:</p><ul><li><p>Cloud NGFW Enterprise (firewall)</p></li><li><p>Cloud IDS (intrusion detection)</p></li></ul><p>For these security services, customers "will not disclose, publish, or otherwise make publicly-available any benchmark, or performance or comparison tests."</p><p><strong>Note</strong>: Some surveys (including Cube.dev's DeWitt Clause list) still categorize Google Cloud as having a restrictive DeWitt clause. This appears to be based on the older terms or the hyperscaler exclusion. For independent evaluators using current terms, Google Cloud is functionally equivalent to AWS and Azure.</p><p>You can benchmark BigQuery, Spanner, AlloyDB, and other Google Cloud database services freely, same rules as AWS and Azure. The hyperscaler exclusion only matters if you're literally acting as an agent for a competing cloud provider.</p><h3>Still restricted: the holdouts</h3><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|}\n\\hline\n\\textbf{Vendor} &amp; \\textbf{Restriction} \\\\\n\\hline\n\\text{Oracle Database} &amp; \\text{Prior written consent required} \\\\\n\\hline\n\\text{Microsoft SQL Server (on-premises)} &amp; \\text{Prior written approval required} \\\\\n\\hline\n\\text{MariaDB SkySQL/Xpand} &amp; \\text{Restrictions in cloud terms} \\\\\n\\hline\n\\text{Elastic Cloud} &amp; \\text{DeWitt clause in DBaaS terms} \\\\\n\\hline\n\\text{InfluxDB Cloud} &amp; \\text{Publication restrictions} \\\\\n\\hline\n\\text{PlanetScale} &amp; \\text{Benchmark restrictions} \\\\\n\\hline\n\\text{Couchbase Capella} &amp; \\text{Restrictions in cloud offering} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;AOMJXGLXMH&quot;}" data-component-name="LatexBlockToDOM"></div><p>Oracle maintains the original DeWitt clause that started it all. Microsoft SQL Server (the on-premises product, distinct from Azure SQL/Synapse) still requires written approval. Several cloud-only database services restrict benchmarks, particularly for their managed offerings even when the underlying technology is open source.</p><p><strong>Why do these vendors hold out?</strong> I see a few patterns:</p><ul><li><p><strong>Legal inertia</strong>: The clause has always been there; removing it requires someone to affirmatively decide to change it</p></li><li><p><strong>Performance concerns</strong>: If you're not confident in your performance, transparency is risky. The vendors who embrace open benchmarking tend to be the ones winning benchmarks.</p></li><li><p><strong>Customer lock-in</strong>: Existing customers can't easily compare alternatives when they can't see independent comparisons</p></li><li><p><strong>Different market dynamics</strong>: Enterprise sales happen behind closed doors; public benchmarks matter less when you're selling to procurement committees</p></li></ul><p>The pattern is telling: vendors confident in their performance actively encourage benchmarks. Vendors who restrict them are telling you something about their confidence level.</p><div><hr></div><h2>Practical guidance</h2><h3>For database evaluators</h3><p>Here's what I do before publishing any benchmark, and what I recommend you do too:</p><ol><li><p><strong>Check the license first</strong>. Before running any benchmark you plan to publish, read the terms of service. A few minutes of legal review can save significant headaches. I learned this the hard way.</p></li><li><p><strong>Open source is always safe</strong>. PostgreSQL, DuckDB, ClickHouse, Spark: benchmark freely. This is one reason I favor open source for my work.</p></li><li><p><strong>DeWitt embrace vendors require methodology</strong>. For AWS, Azure, Databricks, Snowflake, and similar services, document everything:</p><ul><li><p>Hardware specifications (instance types, CPU, RAM, storage)</p></li><li><p>Software versions (database version, OS, drivers)</p></li><li><p>Configuration (all non-default settings)</p></li><li><p>Data generation process</p></li><li><p>Query execution methodology</p></li><li><p>Commands to reproduce</p></li></ul><p><strong>Note:</strong> Using BenchBox makes it very simple to meet these requirements.</p></li><li><p><strong>Restricted vendors require permission</strong>. For Oracle, SQL Server, and similar products, either get written approval, anonymize results ("Database A" vs "Database B"), or don't publish.</p></li><li><p><strong>When in doubt, ask</strong>. Vendor legal teams can clarify what's permitted. Get it in writing.</p></li></ol><h3>For enterprises</h3><p>Questions to ask during vendor evaluation:</p><ul><li><p>"Can we publish benchmark results comparing your product to alternatives?"</p></li><li><p>"What restrictions apply to sharing performance data with our industry peers?"</p></li><li><p>"Will you provide a benchmarking waiver as part of our contract?"</p></li></ul><p>A vendor's answer tells you something about their confidence in their product.</p><h3>For content creators</h3><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|}\n\\hline\n\\textbf{Vendor Category} &amp; \\textbf{What You Can Publish} \\\\\n\\hline\n\\text{Open source} &amp; \\text{Anything} \\\\\n\\hline\n\\text{DeWitt Embrace} &amp; \\text{Anything, with full methodology} \\\\\n\\hline\n\\text{Conditional (BigQuery)} &amp; \\text{Anything, unless working for a hyperscaler} \\\\\n\\hline\n\\text{Restricted} &amp; \\text{Nothing without permission, or anonymize} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;NOSOGVIAIB&quot;}" data-component-name="LatexBlockToDOM"></div><h2>The vector database exception</h2><p>The rise of AI and vector search has created a new category of databases, and a new set of benchmark restrictions.</p><p>A 2024 survey of vector databases found that several cloud offerings restrict benchmarks even when their open source cores don't:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|l|l|}\n\\hline\n\\textbf{Database} &amp; \\textbf{Open Source Status} &amp; \\textbf{Cloud Benchmark Policy} \\\\\n\\hline\n\\text{Milvus/Zilliz} &amp; \\text{Apache 2.0} &amp; \\text{Unrestricted} \\\\\n\\hline\n\\text{Qdrant} &amp; \\text{Apache 2.0} &amp; \\text{Unrestricted} \\\\\n\\hline\n\\text{Weaviate} &amp; \\text{BSD-3} &amp; \\text{Unrestricted} \\\\\n\\hline\n\\text{Pinecone} &amp; \\text{Proprietary} &amp; \\text{Removed restrictions May 2024} \\\\\n\\hline\n\\text{Elasticsearch} &amp; \\text{SSPL} &amp; \\text{Elastic Cloud restricted} \\\\\n\\hline\n\\text{Couchbase} &amp; \\text{Apache 2.0 (core)} &amp; \\text{Capella cloud restricted} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;LHMEHYEWXQ&quot;}" data-component-name="LatexBlockToDOM"></div><p>The pattern is notable: vendors restrict benchmarks specifically for their managed cloud offerings, even when the underlying database engine is open source. This suggests the restriction is about protecting cloud margins rather than the technology itself.</p><div><hr></div><h2>What should change</h2><p>DeWitt clauses are anti-consumer and anti-competitive. Here's what I think needs to happen:</p><ul><li><p><strong>Academic exemptions should be universal</strong>. Researchers should be able to publish benchmark results without fear of legal action. Some licenses technically permit academic use; this should be explicit and standard across the industry.</p></li><li><p><strong>The market is moving toward transparency, and that's good</strong>. The vendors with the best performance actively encourage benchmarks. The correlation isn't coincidental. Transparency favors the winners. The holdouts should take note.</p></li><li><p><strong>TPC should continue expanding</strong>. The Transaction Processing Performance Council's acceptance of cloud-native benchmarks (starting with Databricks' 2021 TPC-DS submission) has helped legitimize comparative performance testing. More standardized, audited benchmarks across more workloads would benefit everyone.</p></li><li><p><strong>Enterprises should push back</strong>. During your next vendor evaluation, ask: "Can we publish benchmark results comparing your product to alternatives?" A vendor's answer tells you something about their confidence. Consider adding benchmarking rights to your contract negotiations.</p></li></ul><div><hr></div><h2>Conclusion</h2><p>The DeWitt Clause, born from Larry Ellison's fury at unflattering benchmark results in 1983, spread throughout the database industry for four decades. But the landscape has fundamentally shifted since 2021.</p><p>Today, the majority of databases can be benchmarked and published freely:</p><ul><li><p><strong>All major open source databases</strong> have no restrictions</p></li><li><p><strong>All major cloud warehouses</strong> (AWS, Azure, Google Cloud, Databricks, Snowflake) permit benchmarks with methodology disclosure</p></li><li><p><strong>Traditional enterprise vendors</strong> (Oracle, SQL Server) maintain restrictions</p></li></ul><p>For anyone evaluating databases in 2026, this is good news. You can conduct independent, reproducible performance testing of most modern data platforms and share your findings with the community.</p><p>Before your next database evaluation, check the vendor's benchmarking terms. If they restrict publication, ask yourself what they're hiding. And if you're negotiating an enterprise contract, push for benchmarking rights. The more customers demand transparency, the faster the holdouts will fold.</p><p>All of the benchmarking research from Oxbow Research is published with complete reproduction instructions via BenchBox.</p><p>The database industry's four-decade experiment with benchmark censorship is ending. Not with a legal ruling or regulatory mandate, but with competitive pressure from vendors confident enough in their performance to welcome scrutiny.</p><p>I hope David DeWitt approves.</p><div><hr></div><h2>Methodology</h2><p>This survey was conducted in January 2026 by reviewing:</p><ul><li><p>Vendor terms of service and acceptable use policies</p></li><li><p>License agreements and service-specific terms</p></li><li><p>Historical vendor announcements and blog posts</p></li><li><p>Third-party analyses from Cube.dev, benchANT, and others</p></li></ul><p>Terms of service change frequently. I recommend verifying current terms before publishing any benchmark results. Links to primary sources are provided in the footnotes.</p><p>This post reflects my good-faith understanding of vendor policies and does not constitute legal advice.</p><div><hr></div><h2>Footnotes</h2><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Bitton, D., DeWitt, D. J., &amp; Turbyfill, C. (1983). Benchmarking database systems: A systematic approach. <em>VLDB Conference Proceedings</em></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p><a href="https://danluu.com/anon-benchmark/">That time Oracle tried to have a professor fired for benchmarking their database</a> - Dan Luu</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p><a href="https://www.brentozar.com/archive/2018/05/the-dewitt-clause-why-you-rarely-see-database-benchmarks/">The DeWitt Clause: Why You Rarely See Database Benchmarks</a> - Brent Ozar, May 2018</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p><a href="https://cube.dev/blog/dewitt-clause-or-can-you-benchmark-a-database">DeWitt clause, or Can you benchmark a database and get away with it</a> - Cube Blog</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p><a href="https://www.oracle.com/downloads/licenses/standard-license.html">Oracle Technology Network License Agreement</a> - Oracle</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p><a href="https://www.microsoft.com/licensing/terms/">Microsoft SQL Server License Terms</a> - Microsoft</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p><a href="https://dwheeler.com/essays/dewitt-clause.html">The DeWitt clause's censorship should be illegal</a> - David A. Wheeler</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-8" href="#footnote-anchor-8" class="footnote-number" contenteditable="false" target="_self">8</a><div class="footnote-content"><p><a href="https://www.databricks.com/blog/2021/11/02/databricks-sets-official-data-warehousing-performance-record.html">Databricks Sets Official Data Warehousing Performance Record</a> - Databricks Blog, November 2021</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-9" href="#footnote-anchor-9" class="footnote-number" contenteditable="false" target="_self">9</a><div class="footnote-content"><p><a href="https://www.tpc.org/results/fdr/tpcds/databricks~tpcds~100000~databricks_sql_8.3~fdr~2021-11-02~v01.pdf">TPC-DS Full Disclosure Report for Databricks</a> - TPC, November 2021</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-10" href="#footnote-anchor-10" class="footnote-number" contenteditable="false" target="_self">10</a><div class="footnote-content"><p><a href="https://www.databricks.com/blog/2021/11/08/eliminating-the-dewitt-clause-for-database-benchmarking.html">Eliminating the DeWitt Clause for Database Benchmarking</a> - Databricks Blog, November 2021</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-11" href="#footnote-anchor-11" class="footnote-number" contenteditable="false" target="_self">11</a><div class="footnote-content"><p><a href="https://www.linkedin.com/pulse/snowflake-vs-databricks-tpcs-ds-benchmark-wars-who-cares-jacobs/">Snowflake vs Databricks: TPC-DS Benchmark Wars</a> - LinkedIn, November 2021</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-12" href="#footnote-anchor-12" class="footnote-number" contenteditable="false" target="_self">12</a><div class="footnote-content"><p><a href="https://www.singlestore.com/blog/eliminating-the-dewitt-clause-for-greater-transparency-in-benchmarking/">Eliminating the DeWitt Clause for Greater Transparency in Benchmarking</a> - SingleStore Blog, November 2023</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-13" href="#footnote-anchor-13" class="footnote-number" contenteditable="false" target="_self">13</a><div class="footnote-content"><p><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache License 2.0</a> - Apache Software Foundation</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-14" href="#footnote-anchor-14" class="footnote-number" contenteditable="false" target="_self">14</a><div class="footnote-content"><p><a href="https://opensource.org/licenses/MIT">MIT License</a> - Open Source Initiative</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-15" href="#footnote-anchor-15" class="footnote-number" contenteditable="false" target="_self">15</a><div class="footnote-content"><p><a href="https://www.postgresql.org/about/licence/">PostgreSQL License</a> - PostgreSQL Global Development Group</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-16" href="#footnote-anchor-16" class="footnote-number" contenteditable="false" target="_self">16</a><div class="footnote-content"><p><a href="https://duckdb.org/faq">DuckDB FAQ</a> - DuckDB</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-17" href="#footnote-anchor-17" class="footnote-number" contenteditable="false" target="_self">17</a><div class="footnote-content"><p><a href="https://aws.amazon.com/service-terms/">AWS Service Terms</a> - Amazon Web Services. Section on Benchmarking.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-18" href="#footnote-anchor-18" class="footnote-number" contenteditable="false" target="_self">18</a><div class="footnote-content"><p><a href="https://www.microsoft.com/licensing/terms/product/ForOnlineServices/MCA">Microsoft Online Services Terms</a> - Microsoft</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-19" href="#footnote-anchor-19" class="footnote-number" contenteditable="false" target="_self">19</a><div class="footnote-content"><p><a href="https://cloud.google.com/terms/service-terms">Google Cloud Service Specific Terms</a> - Google Cloud. Section 7 (Benchmarking). Note: Terms were updated between 2022-2024 to remove prior written consent requirement.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-20" href="#footnote-anchor-20" class="footnote-number" contenteditable="false" target="_self">20</a><div class="footnote-content"><p><a href="https://benchant.com/blog/vectordb-de-witt">To Benchmark Vector Databases or to Get Sued for breaching a DeWitt Clause?</a> - benchANT, April 2024</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-21" href="#footnote-anchor-21" class="footnote-number" contenteditable="false" target="_self">21</a><div class="footnote-content"><p><a href="https://discuss.google.dev/t/dewitt-clause/90532">DeWitt Clause discussion - Google Developer forums</a> - August 2022. Discusses older Google Cloud terms that required prior written consent.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Introducing Oxbow Research]]></title><description><![CDATA[Independent data platform analysis using open-source methodology.]]></description><link>https://oxbowresearch.com/p/introducing-oxbow-research</link><guid isPermaLink="false">https://oxbowresearch.com/p/introducing-oxbow-research</guid><dc:creator><![CDATA[Joe Harris]]></dc:creator><pubDate>Mon, 02 Feb 2026 21:46:27 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!iPiJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5691fe6-e0f3-4682-96bd-01a979f1de0b_1308x512.heic" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>TL;DR</strong>: What is Oxbow Research? Independent analysis of data platform performance and pricing. I also review market trends, vendor strategy, and post deep dives on historical companies and trends. Performance analysis is based on benchmarking run with <a href="https://benchbox.dev/">BenchBox</a>, the open-source benchmarking framework I created to provide transparency and reproducibility for this effort.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!iPiJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5691fe6-e0f3-4682-96bd-01a979f1de0b_1308x512.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!iPiJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5691fe6-e0f3-4682-96bd-01a979f1de0b_1308x512.heic 424w, https://substackcdn.com/image/fetch/$s_!iPiJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5691fe6-e0f3-4682-96bd-01a979f1de0b_1308x512.heic 848w, https://substackcdn.com/image/fetch/$s_!iPiJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5691fe6-e0f3-4682-96bd-01a979f1de0b_1308x512.heic 1272w, https://substackcdn.com/image/fetch/$s_!iPiJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5691fe6-e0f3-4682-96bd-01a979f1de0b_1308x512.heic 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!iPiJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5691fe6-e0f3-4682-96bd-01a979f1de0b_1308x512.heic" width="1308" height="512" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f5691fe6-e0f3-4682-96bd-01a979f1de0b_1308x512.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:512,&quot;width&quot;:1308,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:27466,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://oxbowresearch.com/i/186633309?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5691fe6-e0f3-4682-96bd-01a979f1de0b_1308x512.heic&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!iPiJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5691fe6-e0f3-4682-96bd-01a979f1de0b_1308x512.heic 424w, https://substackcdn.com/image/fetch/$s_!iPiJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5691fe6-e0f3-4682-96bd-01a979f1de0b_1308x512.heic 848w, https://substackcdn.com/image/fetch/$s_!iPiJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5691fe6-e0f3-4682-96bd-01a979f1de0b_1308x512.heic 1272w, https://substackcdn.com/image/fetch/$s_!iPiJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5691fe6-e0f3-4682-96bd-01a979f1de0b_1308x512.heic 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://oxbowresearch.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://oxbowresearch.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h2><strong>The problem with benchmarks</strong></h2><p>No one trusts data platform benchmarks. Data platform vendors don&#8217;t <em>exactly</em> mislead with their benchmarking efforts but, understandably, they only publish benchmarks if they win. Snowflake publishes benchmarks where they look best, Databricks publishes benchmarks where they look best, and data practitioners are left comparing apples to oranges with no easy way to verify either claim. The industry calls this &#8220;benchmarketing&#8221; and it&#8217;s been the norm for decades.</p><p>The same dynamic explains why there are so few official TPC results. TPC certification costs $100k+<sup>[1]</sup>. It only makes sense to publish a result if you are sure your competitors won&#8217;t beat it quickly. If you <em>know </em>a competitor <em>could </em>publish a better (or even close) result then publishing your TPC result is shooting yourself in the foot. That&#8217;s why most vendors never publish or publish once, claim the crown, and never update it. The incentives guarantee you won&#8217;t see an apples-to-apples comparison unless someone outside the vendor ecosystem creates one.</p><p>For data practitioners this creates a real problem: you need to justify your data platform choice and budget. You have a few outdated vendor benchmarks (apples to oranges), analyst quadrants (expensive and vague), and your own experience (limited to platforms you know). You can run your own benchmarks, but it soaks up engineering time: researching configs, debugging drivers, and fighting with cloud permissions - when you should be shipping useful data products for your business.</p><h4><strong>What about independent benchmarks?</strong></h4><p>They often suffer from a few common problems:</p><p><em>Conflict of Interest: </em>Fivetran&#8217;s cloud data warehouse benchmark was very useful but Fivetran&#8217;s business requires close relationships with cloud data warehouse vendors. The conclusion &#8220;they&#8217;re all pretty good&#8221; might be accurate but it reads differently when their revenue depends on not offending anyone on the list.</p><p><em>Single Platform Experts:</em> Often benchmarks are run by practitioners who are expert in a specific platform but have limited experience of the competing platforms. Doing this well requires considerable effort because it&#8217;s hard to create best case tunings for platforms you don&#8217;t know well. It&#8217;s all too easy to write of an unfamiliar platform as &#8220;slow&#8221; when it&#8217;s just misconfigured.</p><p><em>TPC &#8220;Inspired&#8221;:</em> A common category of problematic benchmarks are TPC-H or TPC-DS &#8220;inspired&#8221;. They avoid the complex TPC official methodology requiring: data generation, query validation, specific query ordering, concurrent testing, refresh operations, and unique measurement logic. These &#8220;inspired&#8221; results can be directionally useful but they&#8217;re not directly comparable to other TPC-H or TPC-DS &#8220;inspired&#8221; results because they don&#8217;t adhere to the spec.</p><p>And finally there&#8217;s governance. Who decides if a benchmark was run fairly? Who handles complaints? Who updates results when new versions ship? Usually nobody. The benchmark gets published, gets shared on Hacker News, and sits there, frozen in time, increasingly outdated, with no process for correction or update.</p><div><hr></div><h2><strong>What I built</strong></h2><p>Vendor Benchmarks vs Oxbow Research:</p><ul><li><p>Funding: Vendor funded <em>vs</em> Subscriber funded</p></li><li><p>Methodology: Custom scripts <em>vs</em> Versioned toolkit</p></li><li><p>Reproducibility: Good luck <em>vs</em> <code>pip install benchbox</code></p></li><li><p>Governance: Trust us <em>vs</em> documented process</p></li><li><p>Analysis: &#8220;We&#8217;re the fastest!&#8221; <em>vs</em> &#8220;Fastest at what, exactly?&#8221;</p></li></ul><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;&quot;,&quot;id&quot;:&quot;OLIYFLUAAY&quot;}" data-component-name="LatexBlockToDOM"></div><p></p><p><strong>BenchBox</strong> is the foundation, an easy-to-use open-source benchmarking toolkit with an MIT license.</p><pre><code><code>uv pip install benchbox
uv run benchbox run --platform duckdb --benchmark tpch --scale 1 --phase power</code></code></pre><p>BenchBox will run a spec-compliant TPC-H Power test: generating data with <code>dbgen</code>, running all 22 queries (1 warmup run + 3 measurement runs), queries in the correct order for each run,  using proper parameterization, and reports the geometric mean performance metric (Power@Size). Scale factor 10? Same methodology, larger dataset, correct parameterization. Scale factor 100? Depends on your hardware, but you&#8217;ll know exactly what configuration produced those numbers, because you ran it.</p><p>Oxbow Research is independent and self-funded. I have no outside investors or employer to keep happy. Every benchmark I publish uses BenchBox, the same open-source tool anyone can run. If you disagree with my results, reproduce them and show me. Methodology debates happen on GitHub - if something is wrong (or missing) we&#8217;ll fix it for everyone in public.</p><div><hr></div><h2><strong>What I&#8217;ll write about</strong></h2><p>Benchmark results with full methodology, TPC-H power tests, TPC-DS, ClickBench. Industry economics and vendor analysis. Technical deep-dives on data platform internals. Historical perspectives on analytics technology. I have opinions. I&#8217;ll tell you what they are and why.</p><div><hr></div><h2><strong>Why &#8220;Oxbow&#8221;?</strong></h2><p>The data industry has gone through numerous cycles where a technology or approach seems to completely dominate the market (or the mindshare) for a few years and then becomes less relevant as the market moves onto a different trend. So that&#8217;s the metaphor for Oxbow Research: understanding the speed and course of the current path for data platforms and thinking about where and why the previous path diverged.</p><p>Here are a few &#8220;oxbows&#8221; that I&#8217;ve seen in my career:</p><ul><li><p>Rowstore + Indexing - Oracle</p></li><li><p>MPP Rowstores - Teradata</p></li><li><p>DW Appliances - Netezza</p></li><li><p>Early Columnstores - Vertica</p></li><li><p>Data Lakes - Hadoop, S3</p></li><li><p>Cloud Data Warehouses - Redshift, Snowflake</p></li><li><p>Lakehouse + Open Table formats - Databricks, Delta Lake, Iceberg</p><ul><li><p><em>The current path</em></p></li></ul></li><li><p>Composable Data Stacks - DuckDB, DataFusion, Polars</p><ul><li><p><em>The next path?</em></p></li></ul></li></ul><div><hr></div><h2><strong>What&#8217;s next</strong></h2><p>Subscribe to the Oxbow Research newsletter to stay informed on upcoming research, analysis, and deep dive posts. <a href="https://benchbox.dev/">BenchBox</a> is freely available today.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://oxbowresearch.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Oxbow Research is reader-supported. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><ol><li><p><a href="https://www.tpc.org/information/about/policies">TPC Policies</a> - TPC, accessed January 2026. Full benchmark certification requires third-party auditing and TPC membership. &#8617;</p></li></ol>]]></content:encoded></item></channel></rss>