Read me first. Throughput is records/sec = 1,000,000 rows ÷ mean of 3 runs (2 warmups) via hyperfine. Cross-version numbers are not strictly apples-to-apples — commands gain features over time, so a command's benchmark in an old release may do less work than today's. Read the historical charts (the trend, the flagship deep-dives and the heatmap) as broad trajectory, not exact speedup. `count` gets its own panel below (not the shared-scale charts) because with an index it just reads a stored row count (tens of millions of records/sec) and would flatten every other bar.

## The index advantage

Build an index once and qsv can skip the opening scan on every run after. The payoff is lopsided — a ~6x jump for stats and ~3x for search, but next to nothing for streaming commands — so only the standouts are shown here.

see the qsv viz command

    figs.append(viz("bar", index_src,
                    ["--x", "command", "--y", "recs_per_sec", "--series", "index_status",
                     "--title", "The index advantage", "--y-title", "records/sec"],
                    "index_advantage", "The index advantage",
                    "Build an index once and qsv can skip the opening scan on every run after. "
                    "The payoff is lopsided — a ~6x jump for stats and ~3x for search, but next "
                    "to nothing for streaming commands — so only the standouts are shown here."))

view on GitHub ↗ (gen_benchmark_viz.py, lines 576–582)

## count with an index is effectively instant

The index advantage at its extreme. With an index, count doesn't scan at all — it reads a row count qsv already stored, a ~10x jump from ~9M to ~90M rows/sec. It sits on its own axis precisely because that number would flatten every other bar on the page.

see the qsv viz command

    figs.append(viz("bar", prep_count(),
                    ["--x", "index_status", "--y", "recs_per_sec",
                     "--title", "count: the index-read superpower", "--y-title", "records/sec"],
                    "count_callout", "count with an index is effectively instant",
                    "The index advantage at its extreme. With an index, count doesn't scan at all — "
                    "it reads a row count qsv already stored, a ~10x jump from ~9M to ~90M rows/sec. "
                    "It sits on its own axis precisely because that number would flatten every "
                    "other bar on the page."))

view on GitHub ↗ (gen_benchmark_viz.py, lines 583–590)

## The index superpowers

Some commands don't just skip the opening scan with an index — they skip almost all the work. slice and sample seek straight to the rows they need; schema reuses cached statistics. That's a different order of magnitude: schema runs 78x faster, and slice and sample tens of times over — versus the low single digits for the scan-skippers above.

see the qsv viz command

    figs.append(viz("bar", sp_src,
                    ["--x", "command", "--y", "speedup",
                     "--title", "The index superpowers — times faster with an index",
                     "--y-title", "× faster with an index"],
                    "index_superpowers", "The index superpowers",
                    "Some commands don't just skip the opening scan with an index — they skip almost "
                    "all the work. slice and sample seek straight to the rows they need; schema reuses "
                    f"cached statistics. That's a different order of magnitude: {sp_top_cmd} runs "
                    f"{sp_top_x:.0f}x faster, and slice and sample tens of times over — versus the "
                    "low single digits for the scan-skippers above."))

view on GitHub ↗ (gen_benchmark_viz.py, lines 593–602)

## sqlp tuning: the schema-cache knob

sqlp infers a schema before it runs. Cache that schema and the re-inference cost disappears on the next query — worth about a third more throughput on these aggregations, for a one-line option.

see the qsv viz command

    figs.append(viz("bar", prep_sqlp(),
                    ["--x", "name", "--y", "recs_per_sec",
                     "--title", "sqlp tuning: the Polars schema-cache knob",
                     "--y-title", "records/sec"],
                    "sqlp_tuning", "sqlp tuning: the schema-cache knob",
                    "sqlp infers a schema before it runs. Cache that schema and the re-inference "
                    "cost disappears on the next query — worth about a third more throughput on "
                    "these aggregations, for a one-line option."))

view on GitHub ↗ (gen_benchmark_viz.py, lines 603–610)

## The long view — every release

Records/sec for the marquee commands across all 59 releases since 0.113.0. Each line follows the fastest path available at the time: the plain scan early on, then the indexed variant once search and searchset learned to use an index at 10.0.0 — the visible step up. stats, frequency and validate carried their index from launch, so those lines run flat-to-up with no step. Broad trajectory only (see the note above); count is omitted for scale.

see the qsv viz command

    figs.append(viz("line", prep_trend(),
                    ["--x", "version", "--y", "recs_per_sec", "--series", "command",
                     "--title", f"Throughput across every release ({first_release} → {latest_release})",
                     "--y-title", "records/sec"],
                    "trend", "The long view — every release",
                    f"Records/sec for the marquee commands across all {n_releases} releases since "
                    f"{first_release}. Each line follows the fastest path available at the time: the "
                    "plain scan early on, then the indexed variant once search and searchset learned "
                    "to use an index at 10.0.0 — the visible step up. stats, frequency and validate "
                    "carried their index from launch, so those lines run flat-to-up with no step. "
                    "Broad trajectory only (see the note above); count is omitted for scale."))

view on GitHub ↗ (gen_benchmark_viz.py, lines 611–621)

## Flagship deep-dive: stats

qsv's most-used command, indexed to its own launch speed. Over the years stats kept adding statistics — cardinality, quartiles, MAD, skewness — yet never got slower: plain stats now runs 1.8x its first-release speed, and the full --everything pass with an index 5.7x. Features grew; the curve still points up. (The odd single-release dip is a failed benchmark run, not a regression.)

see the qsv viz command

    figs.append(viz("line", stats_src,
                    ["--x", "version", "--y", "rel", "--series", "name",
                     "--title", "Flagship deep-dive: stats got richer AND faster",
                     "--y-title", "speed vs first release (1.0 = launch)"],
                    "stats_growth", "Flagship deep-dive: stats",
                    "qsv's most-used command, indexed to its own launch speed. Over the years stats "
                    "kept adding statistics — cardinality, quartiles, MAD, skewness — yet never got "
                    f"slower: plain stats now runs {s_base:.1f}x its first-release speed, and the "
                    f"full --everything pass with an index {s_heavy:.1f}x. Features grew; the curve "
                    "still points up. (The odd single-release dip is a failed benchmark run, not a "
                    "regression.)"))

view on GitHub ↗ (gen_benchmark_viz.py, lines 632–642)

## Flagship deep-dive: frequency

The same story for qsv's second flagship. As frequency gained sorted, case-insensitive and unlimited modes, throughput climbed rather than eroded — base frequency is now 1.5x its launch speed and the indexed run 2.0x. Newer modes join partway, each measured from its own debut; the transient dips are measurement artifacts, not regressions.

see the qsv viz command

    figs.append(viz("line", freq_src,
                    ["--x", "version", "--y", "rel", "--series", "name",
                     "--title", "Flagship deep-dive: frequency held its ground",
                     "--y-title", "speed vs first release (1.0 = launch)"],
                    "freq_growth", "Flagship deep-dive: frequency",
                    "The same story for qsv's second flagship. As frequency gained sorted, "
                    "case-insensitive and unlimited modes, throughput climbed rather than eroded — "
                    f"base frequency is now {f_base:.1f}x its launch speed and the indexed run "
                    f"{f_idx:.1f}x. Newer modes join partway, each measured from its own debut; the "
                    "transient dips are measurement artifacts, not regressions."))

view on GitHub ↗ (gen_benchmark_viz.py, lines 643–652)

## Flagship deep-dive: validate

qsv's data-quality workhorse, each variant indexed to its own launch speed. As validate gained modes — the fast no-schema structural pass, batch validation, dynamic-enum lookups, newer JSON Schema drafts — full JSON-Schema validation kept pace: validate_index now runs 1.3x its first-release speed. The no-schema structural path — the most parse-bound variant — dipped ~40% below launch from 17.0.0 through 21.0.0, when qsv's CSV parser (the csv-nose fork) moved to its 1.0.x line, then recovered sharply in 21.1.0 to 1.1x (back above launch) once csv-nose 1.1.0 landed SIMD UTF-8 validation and memchr-based scanning. Full-schema and no-schema paths are shown base vs index, each normalized to its own debut. (The sharp single-release spikes down — e.g. at 1.0.0 and 2.2.1 — are failed benchmark runs, not regressions.)

see the qsv viz command

    figs.append(viz("line", validate_src,
                    ["--x", "version", "--y", "rel", "--series", "name",
                     "--title", "Flagship deep-dive: validate",
                     "--y-title", "speed vs first release (1.0 = launch)"],
                    "validate_growth", "Flagship deep-dive: validate",
                    "qsv's data-quality workhorse, each variant indexed to its own launch speed. As "
                    "validate gained modes — the fast no-schema structural pass, batch validation, "
                    "dynamic-enum lookups, newer JSON Schema drafts — full JSON-Schema validation kept "
                    f"pace: validate_index now runs {v_idx:.1f}x its first-release speed. The no-schema "
                    "structural path — the most parse-bound variant — dipped ~40% below launch from "
                    "17.0.0 through 21.0.0, when qsv's CSV parser (the csv-nose fork) moved to its 1.0.x "
                    f"line, then recovered sharply in 21.1.0 to {v_ns_idx:.1f}x (back above launch) once "
                    "csv-nose 1.1.0 landed SIMD UTF-8 validation and memchr-based scanning. Full-schema "
                    "and no-schema paths are shown base vs index, each normalized to its own debut. (The "
                    "sharp single-release spikes down — e.g. at 1.0.0 and 2.2.1 — are failed benchmark "
                    "runs, not regressions.)"))

view on GitHub ↗ (gen_benchmark_viz.py, lines 653–668)

## Flagship deep-dive: moarstats

qsv's advanced-statistics command — moarstats is designed to run right AFTER stats, extending the stats CSV that stats produces. Only its bivariate variants are charted here. The two univariate variants (moarstats and moarstats --advanced) are left out because their benchmark numbers don't reflect real throughput: moarstats persists its computed columns into the .stats.csv and skips any statistic whose column is already present, so the suite's repeated timed runs short-circuit the work and report a nominal ~90M recs/sec that measures skipped work, not per-row cost. Those univariate paths do genuinely scan the original data — always for the outlier statistics, and for kurtosis, Gini/Atkinson and entropy under --advanced. The bivariate computation, by contrast, is recomputed on every run, so it carries a real throughput-over-time story. Each charted variant is indexed to its own launch speed. moarstats does real pairwise work — the default bivariate pass runs at hundreds of thousands of records/sec and the full --bivariate-stats all battery at a few thousand — so unlike stats or frequency there is genuine per-row cost to hold down. It held: the default bivariate pass now runs 1.7x its first-release speed, stepping up at 17.0.0 and again at 20.0.0. The full all battery is heavier and choppier — it climbed to a ~6k-recs/sec peak around 19.x-20.x, dipped at 21.0.0, and recovered partway in 21.1.0 to 1.3x launch. All four lines are base vs --advanced, each normalized to its own debut.

see the qsv viz command

    figs.append(viz("line", moarstats_src,
                    ["--x", "version", "--y", "rel", "--series", "name",
                     "--title", "Flagship deep-dive: moarstats",
                     "--y-title", "speed vs first release (1.0 = launch)"],
                    "moarstats_growth", "Flagship deep-dive: moarstats",
                    "qsv's advanced-statistics command — moarstats is designed to run right AFTER stats, "
                    "extending the stats CSV that stats produces. Only its bivariate variants are charted "
                    "here. The two univariate variants (moarstats and moarstats --advanced) are left out "
                    "because their benchmark numbers don't reflect real throughput: moarstats persists its "
                    "computed columns into the .stats.csv and skips any statistic whose column is already "
                    "present, so the suite's repeated timed runs short-circuit the work and report a "
                    "nominal ~90M recs/sec that measures skipped work, not per-row cost. Those univariate "
                    "paths do genuinely scan the original data — always for the outlier statistics, and "
                    "for kurtosis, Gini/Atkinson and entropy under --advanced. The bivariate computation, "
                    "by contrast, is recomputed on every run, so it carries a real throughput-over-time "
                    "story. Each charted variant is indexed to its own launch speed. moarstats does real "
                    "pairwise work — the default bivariate pass runs at hundreds of thousands of "
                    "records/sec and the full --bivariate-stats all battery at a few thousand — so unlike "
                    f"stats or frequency there is genuine per-row cost to hold down. It held: the default "
                    f"bivariate pass now runs {m_biv:.1f}x its first-release speed, stepping up at 17.0.0 "
                    "and again at 20.0.0. The full all battery is heavier and choppier — it climbed to a "
                    f"~6k-recs/sec peak around 19.x-20.x, dipped at 21.0.0, and recovered partway in 21.1.0 "
                    f"to {m_all:.1f}x launch. All four lines are base vs --advanced, each normalized to its "
                    "own debut."))

view on GitHub ↗ (gen_benchmark_viz.py, lines 669–692)

## Relative throughput heatmap

The indexed marquee commands over the recent window, each row normalized to its own peak (1.0 = that command's fastest release). Normalizing per row lets a 90M-rows/sec count and a 600k-rows/sec frequency share one canvas — the colour shows trajectory, not absolute speed.

see the qsv viz command

    figs.append(viz("heatmap", prep_heatmap(hm_versions),
                    ["--x", "version", "--y", "name", "--z", "rel",
                     "--title", "Relative throughput vs each command's recent peak"],
                    "heatmap", "Relative throughput heatmap",
                    "The indexed marquee commands over the recent window, each row normalized to its "
                    "own peak (1.0 = that command's fastest release). Normalizing per row lets a "
                    "90M-rows/sec count and a 600k-rows/sec frequency share one canvas — the colour "
                    "shows trajectory, not absolute speed."))

view on GitHub ↗ (gen_benchmark_viz.py, lines 693–700)

## Where the suite spends its time

The whole suite by wall-clock, family then benchmark. The biggest tiles are where a speedup would move the needle most — a map of where the optimization effort is best spent.

see the qsv viz command

    figs.append(viz("treemap", prep_treemap(),
                    ["--cols", "family,name", "--value", "mean", "--agg", "sum",
                     "--title", "Where the suite spends time (mean run time)"],
                    "time_spent", "Where the suite spends its time",
                    "The whole suite by wall-clock, family then benchmark. The biggest tiles are "
                    "where a speedup would move the needle most — a map of where the optimization "
                    "effort is best spent."))

view on GitHub ↗ (gen_benchmark_viz.py, lines 701–707)

## Biggest speedups this release

The 15 benchmarks that improved most over the previous release. Bigger, brighter bubbles are larger wins — the percentage cut in mean run time from one version to the next.

see the qsv viz command

    figs.append(viz("scatter", prep_gainers(),
                    ["--x", "name", "--y", "delta (%)", "--size", "delta (%)", "--color", "delta (%)",
                     "--title", "Biggest speedups this release", "--y-title", "% faster vs previous version"],
                    "gainers", "Biggest speedups this release",
                    "The 15 benchmarks that improved most over the previous release. Bigger, brighter "
                    "bubbles are larger wins — the percentage cut in mean run time from one version "
                    "to the next."))

view on GitHub ↗ (gen_benchmark_viz.py, lines 708–714)

## Change distribution by family

The wider view behind the speedups: the spread of per-release change within each family (above zero = faster). Most families cluster just north of zero — steady, unglamorous progress. Extreme outliers (|Δ| > 100%, usually measurement noise) are omitted; the y-axis is fixed to [-10, 55] so the boxes stay readable, clipping one bad-CI-run point (frequency_ignorecase, -79%).

see the qsv viz command

    figs.append(viz("box", prep_delta_box(),
                    ["--y", "delta (%)", "--x", "family",
                     "--title", "Release-over-release change by command family",
                     "--y-title", "% faster vs previous version",
                     # the real per-family boxes all sit within [-6, +51]; fix the axis to that
                     # window (with light padding) so a lone bad-CI-run outlier can't squash them
                     "--y-range=-10:55",
                     "--annotation",
                     "1 point clipped: frequency_ignorecase -79% (a bad CI run)"],
                    "change_by_family", "Change distribution by family",
                    "The wider view behind the speedups: the spread of per-release change within each "
                    "family (above zero = faster). Most families cluster just north of zero — steady, "
                    f"unglamorous progress. Extreme outliers (|Δ| > {int(DELTA_CLAMP)}%, usually "
                    "measurement noise) are omitted; the y-axis is fixed to [-10, 55] so the boxes "
                    "stay readable, clipping one bad-CI-run point (frequency_ignorecase, -79%)."))

view on GitHub ↗ (gen_benchmark_viz.py, lines 715–729)