From 85da22ed03d05edda5cbe9756ea1d38319824e1a Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:28:09 -0500 Subject: [PATCH] bench: add projection_subquery SQL benchmark suite Adds a SQL benchmark suite for IN, NOT IN and EXISTS subqueries that sit in the SELECT list instead of a filter. A projected IN must return NULL, and not false, when there is no match and the inner side holds a NULL, so the decorrelation turns one projected IN into more than one mark join. A mark join with no hashable join predicate runs as a nested-loop join, and the cost then grows with the outer row count times the inner row count. See https://github.com/apache/datafusion/issues/25341. The suite has seven queries, one per shape: bare uncorrelated IN, the same IN wrapped in COALESCE, IN correlated on an equality, two independent IN subqueries in one SELECT list, NOT IN, correlated EXISTS, and IN correlated on a less-than. The last one has no equality to hash on, so it keeps the nested-loop plan. It is the control: its time must not change when the hashable shapes get faster. Every query aggregates the projected boolean, so the result is two numbers and the measured cost is the plan and not the size of the output. The counts are checked in under results/, so --result-mode validate also proves that a change to the decorrelation still returns the same rows. The two tables are built inline by the suite load SQL, so there is no data step. PSQ_ROWS sets the row count in each table. The default is 30000, which keeps the nested-loop plans at a few hundred milliseconds. The checked-in result files hold the counts for that default. Co-Authored-By: Claude Fable 5.1 --- benchmarks/README.md | 26 +++++++++++ benchmarks/bench.sh | 29 ++++++++++++ benchmarks/sql_benchmarks/README.md | 2 + .../benchmarks/q01.benchmark | 3 ++ .../benchmarks/q02.benchmark | 3 ++ .../benchmarks/q03.benchmark | 3 ++ .../benchmarks/q04.benchmark | 3 ++ .../benchmarks/q05.benchmark | 3 ++ .../benchmarks/q06.benchmark | 3 ++ .../benchmarks/q07.benchmark | 3 ++ .../projection_subquery/init/cleanup.sql | 2 + .../projection_subquery/load/tables.sql | 25 +++++++++++ .../projection_subquery.benchmark.template | 45 +++++++++++++++++++ .../projection_subquery.suite | 23 ++++++++++ .../projection_subquery/queries/q01.sql | 7 +++ .../projection_subquery/queries/q02.sql | 6 +++ .../projection_subquery/queries/q03.sql | 6 +++ .../projection_subquery/queries/q04.sql | 12 +++++ .../projection_subquery/queries/q05.sql | 6 +++ .../projection_subquery/queries/q06.sql | 6 +++ .../projection_subquery/queries/q07.sql | 7 +++ .../results/q01_in_bare.csv | 2 + .../results/q02_in_coalesce.csv | 2 + .../results/q03_in_correlated_eq.csv | 2 + .../results/q04_in_two_columns.csv | 2 + .../results/q05_not_in_bare.csv | 2 + .../results/q06_exists_correlated.csv | 2 + .../results/q07_in_correlated_residual.csv | 2 + 28 files changed, 237 insertions(+) create mode 100644 benchmarks/sql_benchmarks/projection_subquery/benchmarks/q01.benchmark create mode 100644 benchmarks/sql_benchmarks/projection_subquery/benchmarks/q02.benchmark create mode 100644 benchmarks/sql_benchmarks/projection_subquery/benchmarks/q03.benchmark create mode 100644 benchmarks/sql_benchmarks/projection_subquery/benchmarks/q04.benchmark create mode 100644 benchmarks/sql_benchmarks/projection_subquery/benchmarks/q05.benchmark create mode 100644 benchmarks/sql_benchmarks/projection_subquery/benchmarks/q06.benchmark create mode 100644 benchmarks/sql_benchmarks/projection_subquery/benchmarks/q07.benchmark create mode 100644 benchmarks/sql_benchmarks/projection_subquery/init/cleanup.sql create mode 100644 benchmarks/sql_benchmarks/projection_subquery/load/tables.sql create mode 100644 benchmarks/sql_benchmarks/projection_subquery/projection_subquery.benchmark.template create mode 100644 benchmarks/sql_benchmarks/projection_subquery/projection_subquery.suite create mode 100644 benchmarks/sql_benchmarks/projection_subquery/queries/q01.sql create mode 100644 benchmarks/sql_benchmarks/projection_subquery/queries/q02.sql create mode 100644 benchmarks/sql_benchmarks/projection_subquery/queries/q03.sql create mode 100644 benchmarks/sql_benchmarks/projection_subquery/queries/q04.sql create mode 100644 benchmarks/sql_benchmarks/projection_subquery/queries/q05.sql create mode 100644 benchmarks/sql_benchmarks/projection_subquery/queries/q06.sql create mode 100644 benchmarks/sql_benchmarks/projection_subquery/queries/q07.sql create mode 100644 benchmarks/sql_benchmarks/projection_subquery/results/q01_in_bare.csv create mode 100644 benchmarks/sql_benchmarks/projection_subquery/results/q02_in_coalesce.csv create mode 100644 benchmarks/sql_benchmarks/projection_subquery/results/q03_in_correlated_eq.csv create mode 100644 benchmarks/sql_benchmarks/projection_subquery/results/q04_in_two_columns.csv create mode 100644 benchmarks/sql_benchmarks/projection_subquery/results/q05_not_in_bare.csv create mode 100644 benchmarks/sql_benchmarks/projection_subquery/results/q06_exists_correlated.csv create mode 100644 benchmarks/sql_benchmarks/projection_subquery/results/q07_in_correlated_residual.csv diff --git a/benchmarks/README.md b/benchmarks/README.md index 5a555e15abda2..151f1e7df5fb8 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -1009,6 +1009,32 @@ Several queries are included to test sort merge joins under various workloads. ./bench.sh run smj ``` + +## Projection Subquery + +This benchmark measures `IN`, `NOT IN` and `EXISTS` subqueries that sit in the `SELECT` list instead of a filter +(see ). + +A projected `IN` must return `NULL`, and not `false`, when there is no match and the inner side holds a `NULL`. +The decorrelation therefore turns one projected `IN` into more than one mark join. +If a mark join has no hashable join predicate, it runs as a nested-loop join, and the cost grows with the outer row count times the inner row count. + +Every query projects the boolean subquery result and aggregates it, so the measured cost is the plan and not the size of the output. +Query `q07` correlates on `<` instead of `=`, so it stays on the nested-loop path. +It is the control: its time must not change when the hashable shapes get faster. + +The two tables are built inline by the suite's load SQL, so there is no data step. +Set `PSQ_ROWS` to change the row count in each table. +The default is 30,000, which keeps the nested-loop plans at a few hundred milliseconds. +The checked-in result files hold the counts for that default, so `--result-mode validate` needs it. + +### Example Run + +```bash +# No need to generate data: the suite's load SQL builds the two tables inline + +./bench.sh run projection_subquery +``` ## Cancellation Test performance of cancelling queries. diff --git a/benchmarks/bench.sh b/benchmarks/bench.sh index 05ede43178c15..523e566c3ded2 100755 --- a/benchmarks/bench.sh +++ b/benchmarks/bench.sh @@ -115,6 +115,10 @@ parquet_row_filter_skip: Per-RG fully-matched RowFilter skip on Parquet (apache/ null_aware_join: Null-aware (NOT IN) hash join micro-benchmarks: uncorrelated, non-equality-correlated and equality-correlated NOT IN across NULL fractions, to measure the per-pair join-filter work the correlated cases do (data generated inline by the suite's load SQL from range(); knobs: NAJ_ROWS, NAJ_LARGE_ROWS) +projection_subquery: IN / NOT IN / EXISTS subqueries in the SELECT list (see https://github.com/apache/datafusion/issues/25341); each query projects the + boolean subquery result and aggregates it, so the cost is the decorrelation plan and not the output size + (q07 correlates on '<' instead of '=', so it keeps the nested-loop plan and acts as the control) + (data generated inline by the suite's load SQL; knob: PSQ_ROWS) # ClickBench Benchmarks clickbench_1: ClickBench queries against a single parquet file @@ -279,6 +283,10 @@ main() { # Data is generated inline by the suite's load SQL from range(). echo "null_aware_join: no external data to generate" ;; + projection_subquery) + # Data is generated inline by the suite's load SQL. + echo "projection_subquery: no external data to generate" + ;; asof_join) data_asof_join ;; @@ -530,6 +538,9 @@ main() { null_aware_join) run_null_aware_join ;; + projection_subquery) + run_projection_subquery + ;; asof_join) run_asof_join ;; @@ -969,6 +980,24 @@ run_null_aware_join() { bash -c "$SQL_CARGO_COMMAND" } +# Runs the projection_subquery suite: IN / NOT IN / EXISTS subqueries that sit +# in the SELECT list instead of a filter (see +# https://github.com/apache/datafusion/issues/25341). The load SQL builds the +# two tables inline, so there is no data step. Each query projects the boolean +# subquery result and aggregates it, so the measured cost is the decorrelation +# plan and not the size of the output. Query 07 correlates on '<' instead of +# '=', so it keeps the nested-loop plan and acts as the control. +# Knob (string-substituted into the load SQL, not engine config): +# PSQ_ROWS rows in each of the two tables (default 30_000; the checked-in +# result files hold the counts for that value) +run_projection_subquery() { + echo "Running projection_subquery benchmark (rows=${PSQ_ROWS:-30000})..." + debug_run env BENCH_NAME=projection_subquery \ + PSQ_ROWS="${PSQ_ROWS:-30000}" \ + ${QUERY:+BENCH_QUERY="${QUERY}"} \ + bash -c "$SQL_CARGO_COMMAND" +} + # Runs the tpch in memory (needs tpch parquet data) run_tpch_mem() { SCALE_FACTOR=$1 diff --git a/benchmarks/sql_benchmarks/README.md b/benchmarks/sql_benchmarks/README.md index fb09c95daa12f..0bf3dbd86280e 100644 --- a/benchmarks/sql_benchmarks/README.md +++ b/benchmarks/sql_benchmarks/README.md @@ -47,6 +47,7 @@ in the community: | `wide_schema` | Small-projection queries on a wide (1024-col, 256-file) synthetic dataset; runs `wide` + `narrow` subgroups for comparison | | `predicate_eval` | Conjunctive (AND) filter-evaluation micro-benchmarks; each subgroup is a different predicate pattern, to test how an adaptive predicate-ordering system behaves across them ([#11262](https://github.com/apache/datafusion/issues/11262)). Subgroups (`--subgroup`): `costsel`, `cost`, `selectivity`, `cardinality`, `width`, `scale`, `neutral`, `correlation`, `drift`, `nulls`. The suite sets no engine config of its own, so by default it measures DataFusion's built-in left-deep `AND` short-circuit; point it at a system under test by exporting that system's own DataFusion setting (the harness builds its `SessionConfig` with `SessionConfig::from_env`). Every query is a `count(*)`, and the counts are checked in under `predicate_eval/results/`, so `--result-mode validate` also checks that a reordering under test still returns the same rows; the checked-in counts were persisted at the suite defaults (`PRED_ROWS=1000000`, `PRED_FILL=30`), so validation assumes those (the `scale` and `width` subgroups pin their own values per query and validate at any setting). | | `parquet_row_filter_skip` | Micro-benchmark for the per-row-group fully-matched RowFilter skip on Parquet scans ([#23696](https://github.com/apache/datafusion/issues/23696)). Subgroups (`--subgroup`): `skip` (clustered key, most row groups fully matched by statistics so the per-row filter is skipped), `control` (scrambled key, no row group is ever fully matched). Size the data with `PRED_ROWS` and the row-group size with `RG_SIZE`. | +| `projection_subquery` | `IN`, `NOT IN` and `EXISTS` subqueries that sit in the `SELECT` list instead of a filter ([#25341](https://github.com/apache/datafusion/issues/25341)). Every query projects the boolean subquery result and aggregates it, so the measured cost is the decorrelation plan and not the size of the output. `q07` correlates on `<` instead of `=`, so it keeps the nested-loop plan and is the control that must not change. Size the two inline tables with `PSQ_ROWS` (default `30000`). The counts are checked in under `projection_subquery/results/`, so `--result-mode validate` also proves that a change to the decorrelation still returns the same rows; the checked-in counts were persisted at the default `PSQ_ROWS`, so validation assumes that value. | # Running Benchmarks @@ -174,6 +175,7 @@ Some benchmarks use custom environment variables as outlined below: | PRED_ROWS | Used in the predicate_eval benchmark to size the synthetic table (the `scale` subgroup overrides this per query), and in the parquet_row_filter_skip benchmark to size the generated Parquet datasets (default `10000000` there). | `1000000` | | PRED_FILL | Used in the predicate_eval benchmark as the string-column width knob (filler chars per marker). | `30` | | RG_SIZE | Used in the parquet_row_filter_skip benchmark as the Parquet `max_row_group_size` for the generated datasets. | `1000000` | +| PSQ_ROWS | Used in the projection_subquery benchmark to size the two inline tables. | `30000` | ## How it works diff --git a/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q01.benchmark b/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q01.benchmark new file mode 100644 index 0000000000000..587150c4bf06f --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q01.benchmark @@ -0,0 +1,3 @@ +template sql_benchmarks/projection_subquery/projection_subquery.benchmark.template +QPAD=01 +NAME=q01_in_bare diff --git a/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q02.benchmark b/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q02.benchmark new file mode 100644 index 0000000000000..0b855f3feaf0c --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q02.benchmark @@ -0,0 +1,3 @@ +template sql_benchmarks/projection_subquery/projection_subquery.benchmark.template +QPAD=02 +NAME=q02_in_coalesce diff --git a/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q03.benchmark b/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q03.benchmark new file mode 100644 index 0000000000000..de302e8d6d5a8 --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q03.benchmark @@ -0,0 +1,3 @@ +template sql_benchmarks/projection_subquery/projection_subquery.benchmark.template +QPAD=03 +NAME=q03_in_correlated_eq diff --git a/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q04.benchmark b/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q04.benchmark new file mode 100644 index 0000000000000..d82b3ecad9e2d --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q04.benchmark @@ -0,0 +1,3 @@ +template sql_benchmarks/projection_subquery/projection_subquery.benchmark.template +QPAD=04 +NAME=q04_in_two_columns diff --git a/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q05.benchmark b/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q05.benchmark new file mode 100644 index 0000000000000..3c47bad916967 --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q05.benchmark @@ -0,0 +1,3 @@ +template sql_benchmarks/projection_subquery/projection_subquery.benchmark.template +QPAD=05 +NAME=q05_not_in_bare diff --git a/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q06.benchmark b/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q06.benchmark new file mode 100644 index 0000000000000..76979e1ac336d --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q06.benchmark @@ -0,0 +1,3 @@ +template sql_benchmarks/projection_subquery/projection_subquery.benchmark.template +QPAD=06 +NAME=q06_exists_correlated diff --git a/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q07.benchmark b/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q07.benchmark new file mode 100644 index 0000000000000..541b77db3a71d --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/benchmarks/q07.benchmark @@ -0,0 +1,3 @@ +template sql_benchmarks/projection_subquery/projection_subquery.benchmark.template +QPAD=07 +NAME=q07_in_correlated_residual diff --git a/benchmarks/sql_benchmarks/projection_subquery/init/cleanup.sql b/benchmarks/sql_benchmarks/projection_subquery/init/cleanup.sql new file mode 100644 index 0000000000000..29a35be6b4d99 --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/init/cleanup.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS outer_t; +DROP TABLE IF EXISTS inner_t; diff --git a/benchmarks/sql_benchmarks/projection_subquery/load/tables.sql b/benchmarks/sql_benchmarks/projection_subquery/load/tables.sql new file mode 100644 index 0000000000000..5e57de826cd55 --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/load/tables.sql @@ -0,0 +1,25 @@ +-- Two small integer tables for the projected IN / EXISTS subquery benchmarks. +-- +-- `outer_t.id` is dense on 1..PSQ_ROWS and `inner_t.id` is the even numbers, +-- so about half of the outer keys have a match. Every 97th inner key is NULL, +-- which keeps three-valued logic in play: a projected `IN` must return NULL, +-- not false, when there is no match and the inner side holds a NULL. That is +-- the reason the decorrelation needs more than one mark join. +-- +-- `z` is a low-cardinality column (1000 groups) used by the correlated +-- queries, once with an equality correlation and once with a `<` correlation. +-- +-- The tables are deliberately small. The plans under test are quadratic in +-- outer rows times inner rows, so a larger PSQ_ROWS makes the suite take +-- minutes per query instead of seconds. +CREATE TABLE outer_t AS +SELECT + CAST(value AS INT) AS id, + CAST(value % 1000 AS INT) AS z +FROM generate_series(1, ${PSQ_ROWS:-30000}); + +CREATE TABLE inner_t AS +SELECT + CASE WHEN value % 97 = 0 THEN NULL ELSE CAST(value * 2 AS INT) END AS id, + CAST(value % 1000 AS INT) AS z +FROM generate_series(1, ${PSQ_ROWS:-30000}); diff --git a/benchmarks/sql_benchmarks/projection_subquery/projection_subquery.benchmark.template b/benchmarks/sql_benchmarks/projection_subquery/projection_subquery.benchmark.template new file mode 100644 index 0000000000000..f91c61dfc5e67 --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/projection_subquery.benchmark.template @@ -0,0 +1,45 @@ +# Shared template for every projection_subquery benchmark. Each qNN.benchmark +# includes this template with parameters: +# QPAD zero-padded query id / query file stem (e.g. 01) +# NAME criterion display name (e.g. q01_in_bare) +# Optional, read by the load script as ${...:-default}: +# PSQ_ROWS rows in each of the two tables (default 30000) +# +# The tables are always named `outer_t` and `inner_t`, so the load, the asserts +# and the cleanup are the same for every query. +# +# Every query is an aggregate over the projected subquery result, so the output +# is two numbers and the measurement is the plan and not the result transfer. +# The counts are checked in under results/, so --result-mode validate also +# proves that a change to the decorrelation still returns the same rows. The +# checked-in counts hold for the default PSQ_ROWS only. + +load sql_benchmarks/projection_subquery/load/tables.sql + +name ${NAME} +group projection_subquery + +# Both tables have exactly PSQ_ROWS rows. +assert I +SELECT count(*) = ${PSQ_ROWS:-30000} FROM outer_t; +---- +true + +assert I +SELECT count(*) = ${PSQ_ROWS:-30000} FROM inner_t; +---- +true + +# The inner key must keep its NULLs, because three-valued logic is what forces +# the extra mark joins. A load that loses them makes the suite measure the +# wrong plan. +assert I +SELECT count(*) > 0 FROM inner_t WHERE id IS NULL; +---- +true + +run sql_benchmarks/projection_subquery/queries/q${QPAD}.sql + +result sql_benchmarks/projection_subquery/results/${NAME}.csv + +cleanup sql_benchmarks/projection_subquery/init/cleanup.sql diff --git a/benchmarks/sql_benchmarks/projection_subquery/projection_subquery.suite b/benchmarks/sql_benchmarks/projection_subquery/projection_subquery.suite new file mode 100644 index 0000000000000..f5f419b50aa1e --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/projection_subquery.suite @@ -0,0 +1,23 @@ +description = "IN / NOT IN / EXISTS subqueries that appear in the SELECT list instead of a filter (apache/datafusion#25341). Each query projects the boolean result of a subquery and aggregates it, so the measured work is the decorrelation plan and not the output size. The decorrelation turns one projected IN into mark joins; when the join predicate is not hashable, or the correlation is not an equality, the plan keeps a nested-loop mark join and the cost grows with outer rows times inner rows. Query 07 correlates on `<` and stays on that path, so it is the control that must not change. The two tables are built inline by the load SQL and are sized with PSQ_ROWS." + +query_pattern = "q{QUERY_ID_PADDED}.benchmark" + +[[options]] +name = "rows" +short = "r" +env = "PSQ_ROWS" +default = "30000" +values = ["30000", "..."] +help = "Sets the number of rows in each of the two generated tables. The checked-in result files hold the counts for the default value, so --result-mode validate needs the default." + +[[examples]] +command = "cargo run --release --bin benchmark_runner -- projection_subquery" +description = "Run every query with the default 30,000 rows per table." + +[[examples]] +command = "cargo run --release --bin benchmark_runner -- projection_subquery --query 1 --result-mode validate" +description = "Run query 01 and compare the counts with the checked-in result file." + +[[examples]] +command = "cargo run --release --bin benchmark_runner -- projection_subquery -r 5000" +description = "Run every query on smaller tables." diff --git a/benchmarks/sql_benchmarks/projection_subquery/queries/q01.sql b/benchmarks/sql_benchmarks/projection_subquery/queries/q01.sql new file mode 100644 index 0000000000000..70cd5ee4737b1 --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/queries/q01.sql @@ -0,0 +1,7 @@ +-- Shape: bare uncorrelated IN in the SELECT list. The subquery has no +-- correlation, so the only join key is the outer key against the inner key. +-- This is the plain form of the quadratic plan. +SELECT + count(*) FILTER (WHERE m) AS true_count, + count(*) FILTER (WHERE m IS NULL) AS null_count +FROM (SELECT id, id IN (SELECT id FROM inner_t) AS m FROM outer_t); diff --git a/benchmarks/sql_benchmarks/projection_subquery/queries/q02.sql b/benchmarks/sql_benchmarks/projection_subquery/queries/q02.sql new file mode 100644 index 0000000000000..11b934fe23f79 --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/queries/q02.sql @@ -0,0 +1,6 @@ +-- Shape: the same bare IN, wrapped in COALESCE so the NULL result becomes +-- false. The wrapper is the common way to use a projected IN, and it must not +-- stop the decorrelation. +SELECT + count(*) FILTER (WHERE m) AS true_count +FROM (SELECT id, COALESCE((id IN (SELECT id FROM inner_t))::boolean, false) AS m FROM outer_t); diff --git a/benchmarks/sql_benchmarks/projection_subquery/queries/q03.sql b/benchmarks/sql_benchmarks/projection_subquery/queries/q03.sql new file mode 100644 index 0000000000000..e21953305f180 --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/queries/q03.sql @@ -0,0 +1,6 @@ +-- Shape: IN correlated on an equality. The correlation adds a second join key, +-- so the whole join condition stays hashable. +SELECT + count(*) FILTER (WHERE m) AS true_count, + count(*) FILTER (WHERE m IS NULL) AS null_count +FROM (SELECT id, id IN (SELECT i.id FROM inner_t i WHERE i.z = o.z) AS m FROM outer_t o); diff --git a/benchmarks/sql_benchmarks/projection_subquery/queries/q04.sql b/benchmarks/sql_benchmarks/projection_subquery/queries/q04.sql new file mode 100644 index 0000000000000..838c36d7e63e6 --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/queries/q04.sql @@ -0,0 +1,12 @@ +-- Shape: two independent projected IN subqueries in one SELECT list. Each one +-- is decorrelated on its own, so this measures whether the cost of the first +-- one simply doubles. +SELECT + count(*) FILTER (WHERE a) AS a_count, + count(*) FILTER (WHERE b) AS b_count +FROM ( + SELECT + id IN (SELECT id FROM inner_t) AS a, + id IN (SELECT id FROM inner_t WHERE z < 500) AS b + FROM outer_t +); diff --git a/benchmarks/sql_benchmarks/projection_subquery/queries/q05.sql b/benchmarks/sql_benchmarks/projection_subquery/queries/q05.sql new file mode 100644 index 0000000000000..e429d50853bb0 --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/queries/q05.sql @@ -0,0 +1,6 @@ +-- Shape: NOT IN in the SELECT list. It is the negation of q01 and it has the +-- same three-valued logic, so it must plan the same way. +SELECT + count(*) FILTER (WHERE m) AS true_count, + count(*) FILTER (WHERE m IS NULL) AS null_count +FROM (SELECT id, id NOT IN (SELECT id FROM inner_t) AS m FROM outer_t); diff --git a/benchmarks/sql_benchmarks/projection_subquery/queries/q06.sql b/benchmarks/sql_benchmarks/projection_subquery/queries/q06.sql new file mode 100644 index 0000000000000..89414bd282cd1 --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/queries/q06.sql @@ -0,0 +1,6 @@ +-- Shape: correlated EXISTS in the SELECT list. EXISTS has no NULL result, so +-- one mark join is enough. It is the reference for what the IN queries should +-- cost. +SELECT + count(*) FILTER (WHERE e) AS true_count +FROM (SELECT id, EXISTS (SELECT 1 FROM inner_t i WHERE i.id = o.id) AS e FROM outer_t o); diff --git a/benchmarks/sql_benchmarks/projection_subquery/queries/q07.sql b/benchmarks/sql_benchmarks/projection_subquery/queries/q07.sql new file mode 100644 index 0000000000000..54299f0cbdaf4 --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/queries/q07.sql @@ -0,0 +1,7 @@ +-- Shape: IN correlated on `<` instead of `=`. There is no equality to hash on, +-- so this query keeps the nested-loop mark joins. It is the control: its cost +-- must stay the same when the hashable cases get faster. +SELECT + count(*) FILTER (WHERE m) AS true_count, + count(*) FILTER (WHERE m IS NULL) AS null_count +FROM (SELECT id, id IN (SELECT i.id FROM inner_t i WHERE i.z < o.z) AS m FROM outer_t o); diff --git a/benchmarks/sql_benchmarks/projection_subquery/results/q01_in_bare.csv b/benchmarks/sql_benchmarks/projection_subquery/results/q01_in_bare.csv new file mode 100644 index 0000000000000..fc5b81602d14b --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/results/q01_in_bare.csv @@ -0,0 +1,2 @@ +true_count|null_count +14846|15154 diff --git a/benchmarks/sql_benchmarks/projection_subquery/results/q02_in_coalesce.csv b/benchmarks/sql_benchmarks/projection_subquery/results/q02_in_coalesce.csv new file mode 100644 index 0000000000000..c0af10b38460f --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/results/q02_in_coalesce.csv @@ -0,0 +1,2 @@ +true_count +14846 diff --git a/benchmarks/sql_benchmarks/projection_subquery/results/q03_in_correlated_eq.csv b/benchmarks/sql_benchmarks/projection_subquery/results/q03_in_correlated_eq.csv new file mode 100644 index 0000000000000..f966c83888735 --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/results/q03_in_correlated_eq.csv @@ -0,0 +1,2 @@ +true_count|null_count +15|9270 diff --git a/benchmarks/sql_benchmarks/projection_subquery/results/q04_in_two_columns.csv b/benchmarks/sql_benchmarks/projection_subquery/results/q04_in_two_columns.csv new file mode 100644 index 0000000000000..2b9f129316284 --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/results/q04_in_two_columns.csv @@ -0,0 +1,2 @@ +a_count|b_count +14846|7423 diff --git a/benchmarks/sql_benchmarks/projection_subquery/results/q05_not_in_bare.csv b/benchmarks/sql_benchmarks/projection_subquery/results/q05_not_in_bare.csv new file mode 100644 index 0000000000000..9e3764aaa3e1c --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/results/q05_not_in_bare.csv @@ -0,0 +1,2 @@ +true_count|null_count +0|15154 diff --git a/benchmarks/sql_benchmarks/projection_subquery/results/q06_exists_correlated.csv b/benchmarks/sql_benchmarks/projection_subquery/results/q06_exists_correlated.csv new file mode 100644 index 0000000000000..c0af10b38460f --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/results/q06_exists_correlated.csv @@ -0,0 +1,2 @@ +true_count +14846 diff --git a/benchmarks/sql_benchmarks/projection_subquery/results/q07_in_correlated_residual.csv b/benchmarks/sql_benchmarks/projection_subquery/results/q07_in_correlated_residual.csv new file mode 100644 index 0000000000000..f9cf5f10be5d9 --- /dev/null +++ b/benchmarks/sql_benchmarks/projection_subquery/results/q07_in_correlated_residual.csv @@ -0,0 +1,2 @@ +true_count|null_count +7408|22487