Skip to content

Commit 1e865cc

Browse files
committed
Merge phase22b-vectorized-windows: window arc complete
2 parents b97717a + 5340099 commit 1e865cc

10 files changed

Lines changed: 815 additions & 91 deletions

File tree

‎ARCHITECTURE.md‎

Lines changed: 8 additions & 6 deletions
Large diffs are not rendered by default.

‎ROADMAP.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
19. Boolean-mask filter kernels. Delivered in Phase 19: vectorized Filter now evaluates WHERE/HAVING predicate trees as paired `is_true`/`is_known` byte masks using SQL Kleene 3VL algebra, with comparison leaves scanning pre-resolved int64 or string values column-at-a-time and `IS [NOT] NULL` leaves reading validity directly. Scan-adjacent identity selections use a dense full-batch domain, while filtered/non-identity inputs use dense selection-position masks that map back to the immutable input selection. Join residual predicates deliberately remain on the existing row-pair path. Release benchmark evidence in `docs/benchmarks.md` shows the three scan/filter vectorized medians improving with byte-identical correctness checks and no material non-target regression.
2222
20. LEFT/RIGHT OUTER JOIN arc. Delivered across Phase 20a/20b: the parser accepts `LEFT [OUTER] JOIN` and `RIGHT [OUTER] JOIN`; binding normalizes RIGHT to LEFT while stable identities preserve SQL-visible projection order; both interpreted and vectorized engines implement deterministic left-row-major NULL-extension, NULL-key non-matching, and residual-aware matchedness for hash and non-equi joins. `LeftJoinToInnerRule` additively simplifies a filtered LEFT join only under a proof-bearing conservative 3VL null-rejection test, unlocking the existing INNER commute/associate/pushdown exploration without weakening their guards. The deterministic fuzzer now mixes INNER/LEFT/RIGHT chains with actual NULL-bearing key data and verifies standalone rewrites, every memo alternative, and `extract_best` through both engines. The Phase 20a LEFT cardinality rule remains sufficient; no 20b cost-model change was required. The outer-join lowering guard and its verifier path are gone.
2323
21. Complete subquery arc. Delivered across Phase 21a/21b/21c: scalar-comparison, IN/NOT IN, and EXISTS/NOT EXISTS use immutable bound subplans with explicit lexical-depth correlation metadata. Empty correlation retains the eager once-per-query 3VL/cardinality model; correlated forms use the per-outer-row interpreted specification with nested/grandparent frames and row-ordered scalar errors. Explicit left SEMI/ANTI algebra has left-only output identity, deterministic interpreted/vectorized hash/NLJ execution, memo identity/printers/EXPLAIN/costing, and conservative guards. Uncorrelated and immediate equality-correlated top-level EXISTS, NOT EXISTS, and IN decorrelate through proof-bearing memo rules. Residual correlated forms remain oracle-defined and fail once at physical lowering; uncorrelated residuals still run vectorized recursively. Exact EXPLAIN goldens, eager/per-row error equivalence, and deterministic uncorrelated/correlated fuzz lanes close the story.
24-
22. Window functions arc. Phase 22a is delivered: whole-SELECT-item `ROW_NUMBER`, `RANK`, and `DENSE_RANK` plus whole-partition `SUM`, `COUNT`, `MIN`, and `MAX` parse and bind into one post-HAVING Window node; the interpreted oracle defines first-appearance partitioning, input-order preservation, stable ranking ties, NULL partition/peer equality, and replicated NULL-aware aggregate results; Window participates in memo identity, EXPLAIN, extraction, and conservative costing while remaining a filter-pushdown barrier, with ROW_NUMBER and checked SUM additionally pinning order-changing child transforms because stable ties and intermediate overflow expose input order; physical lowering fails with one stable unsupported error. Phase 22b will add the real vectorized Window operator and deterministic fuzzer generation. Running aggregate windows and explicit `ROWS`/`RANGE` frames remain future work after 22b.
24+
22. Window functions arc. Delivered across Phase 22a/22b: whole-SELECT-item `ROW_NUMBER`, `RANK`, and `DENSE_RANK` plus whole-partition `SUM`, `COUNT`, `MIN`, and `MAX` parse and bind into one post-HAVING Window node; both interpreted and vectorized execution implement first-appearance partitioning, input-order preservation, stable ranking ties, NULL partition/peer equality, checked/NULL-skipping partition aggregates, and replicated results; Window participates in physical lowering, memo identity, EXPLAIN, extraction, conservative costing, golden coverage, deterministic fuzzing, and a Release benchmark workload while remaining a filter-pushdown barrier, with ROW_NUMBER and checked SUM additionally pinning order-changing child transforms because stable ties and intermediate overflow expose input order. Running aggregate windows and explicit `ROWS`/`RANGE` frames remain future work.
2525
23. Null-aware anti join (future). Extend algebra and execution before considering NOT IN decorrelation; ordinary Anti remains intentionally insufficient for a NULL-bearing right set, where unmatched NOT IN is UNKNOWN rather than TRUE.
2626

2727
## Phase 1 first tasks

‎benchmarks/sql_bench.cpp‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,12 @@ std::vector<Workload> make_workloads(const execution::Catalog& catalog) {
360360
"fact=200000,groups=50000",
361361
"SELECT group_many, SUM(value) AS total "
362362
"FROM fact GROUP BY group_many ORDER BY group_many");
363+
add("window_row_number_sum",
364+
"fact=200000,partitions=100",
365+
"SELECT bucket, value, "
366+
"ROW_NUMBER() OVER (PARTITION BY bucket ORDER BY sort_key) AS rn, "
367+
"SUM(value) OVER (PARTITION BY bucket) AS bucket_sum "
368+
"FROM fact");
363369
add("sort",
364370
"fact=200000",
365371
"SELECT a, value FROM fact ORDER BY sort_key, a LIMIT 1000");

‎docs/benchmarks.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,16 @@ The checksum match pins left-only output and duplicate-preserving Semi
235235
semantics before timing. The vectorized lookup-only hash implementation is
236236
2.301x faster on median than the interpreted left-row-major nested-loop oracle
237237
for this selective workload.
238+
239+
## Phase 22b Window Baseline (2026-07-10)
240+
241+
Same optimized build, five-repetition min/median, deterministic-data, and
242+
checksum-before-timing methodology as above. This adds one correctness-first
243+
Window workload over the 200,000-row `fact` table with 100 partitions:
244+
`ROW_NUMBER()` ordered inside each partition plus `SUM(value)` replicated over
245+
the partition. No Window-specific performance tuning was attempted in this
246+
phase.
247+
248+
| workload | rows | correctness | interpreted min ms | interpreted median ms | vectorized min ms | vectorized median ms | median speedup |
249+
|---|---:|---|---:|---:|---:|---:|---:|
250+
| window_row_number_sum | fact=200000,partitions=100 | match rows=200000 checksum=0x1f5b8dc9ce10a38c | 725.385 | 732.916 | 108.253 | 109.029 | 6.722x |

‎include/plan/physical_plan.hpp‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
namespace plan {
1212

13-
enum class PhysicalKind { Scan, Join, Filter, Project, Aggregate, Distinct, Sort, Limit };
13+
enum class PhysicalKind { Scan, Join, Filter, Project, Aggregate, Window, Distinct, Sort, Limit };
1414

1515
struct PhysicalPlan {
1616
PhysicalKind kind{PhysicalKind::Scan};
@@ -21,6 +21,7 @@ struct PhysicalPlan {
2121
std::vector<Projection> projections;
2222
std::vector<BoundColumnRef> group_keys;
2323
std::vector<AggregateExpression> aggregate_expressions;
24+
std::vector<WindowExpression> window_expressions;
2425
std::vector<SortKey> sort_keys;
2526
std::vector<BoundPredicate> predicates;
2627
JoinKind join_kind{JoinKind::Inner};
@@ -81,6 +82,14 @@ struct PhysicalPlan {
8182
return p;
8283
}
8384

85+
static PhysicalPlan window(std::vector<WindowExpression> window_expressions, PhysicalPlan child) {
86+
PhysicalPlan p;
87+
p.kind = PhysicalKind::Window;
88+
p.window_expressions = std::move(window_expressions);
89+
p.input = std::make_shared<PhysicalPlan>(std::move(child));
90+
return p;
91+
}
92+
8493
static PhysicalPlan distinct(PhysicalPlan child) {
8594
PhysicalPlan p;
8695
p.kind = PhysicalKind::Distinct;

‎src/execution/vectorized.cpp‎

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,6 +1471,32 @@ TypedCell finalize_aggregate(const AggregateValue& value, const plan::AggregateE
14711471
throw std::logic_error("unreachable aggregate function");
14721472
}
14731473

1474+
sql::AggregateFunction aggregate_function_for_window(sql::WindowFunction function) {
1475+
switch (function) {
1476+
case sql::WindowFunction::Count:
1477+
return sql::AggregateFunction::Count;
1478+
case sql::WindowFunction::Sum:
1479+
return sql::AggregateFunction::Sum;
1480+
case sql::WindowFunction::Min:
1481+
return sql::AggregateFunction::Min;
1482+
case sql::WindowFunction::Max:
1483+
return sql::AggregateFunction::Max;
1484+
case sql::WindowFunction::RowNumber:
1485+
case sql::WindowFunction::Rank:
1486+
case sql::WindowFunction::DenseRank:
1487+
break;
1488+
}
1489+
throw std::logic_error("ranking window is not an aggregate");
1490+
}
1491+
1492+
plan::AggregateExpression aggregate_expression_for_window(const plan::WindowExpression& window) {
1493+
return plan::AggregateExpression{window.output_name,
1494+
aggregate_function_for_window(window.function),
1495+
window.argument,
1496+
window.position,
1497+
window.type};
1498+
}
1499+
14741500
AggregateGroupState make_aggregate_group(HashKey key, const plan::PhysicalPlan& plan) {
14751501
AggregateGroupState group;
14761502
group.key = std::move(key);
@@ -1721,6 +1747,7 @@ BatchView execute_filter(const plan::PhysicalPlan& plan, ExecutionContext& conte
17211747
BatchView execute_join(const plan::PhysicalPlan& plan, ExecutionContext& context);
17221748
BatchView execute_project(const plan::PhysicalPlan& plan, ExecutionContext& context);
17231749
BatchView execute_aggregate(const plan::PhysicalPlan& plan, ExecutionContext& context);
1750+
BatchView execute_window(const plan::PhysicalPlan& plan, ExecutionContext& context);
17241751
BatchView execute_distinct(const plan::PhysicalPlan& plan, ExecutionContext& context);
17251752
BatchView execute_sort(const plan::PhysicalPlan& plan, ExecutionContext& context);
17261753
BatchView execute_limit(const plan::PhysicalPlan& plan, ExecutionContext& context);
@@ -1737,6 +1764,8 @@ BatchView execute_to_view(const plan::PhysicalPlan& plan, ExecutionContext& cont
17371764
return execute_project(plan, context);
17381765
case plan::PhysicalKind::Aggregate:
17391766
return execute_aggregate(plan, context);
1767+
case plan::PhysicalKind::Window:
1768+
return execute_window(plan, context);
17401769
case plan::PhysicalKind::Distinct:
17411770
return execute_distinct(plan, context);
17421771
case plan::PhysicalKind::Sort:
@@ -2065,6 +2094,156 @@ BatchView execute_aggregate(const plan::PhysicalPlan& plan, ExecutionContext& co
20652094
return view;
20662095
}
20672096

2097+
struct WindowPartition {
2098+
std::vector<std::size_t> rows;
2099+
};
2100+
2101+
std::vector<WindowPartition> build_window_partitions(const plan::WindowExpression& window,
2102+
const BatchView& input) {
2103+
validate_view(input);
2104+
const auto partition_keys = compile_columns(window.partition_keys, *input.batch);
2105+
std::unordered_map<HashKey, std::size_t, HashKeyHash> partition_index_by_key;
2106+
std::vector<WindowPartition> partitions;
2107+
2108+
for (auto row : *input.selection) {
2109+
auto key = make_key(row, partition_keys);
2110+
const auto found = partition_index_by_key.find(key);
2111+
if (found == partition_index_by_key.end()) {
2112+
const auto index = partitions.size();
2113+
partition_index_by_key.emplace(std::move(key), index);
2114+
partitions.push_back(WindowPartition{});
2115+
partitions.back().rows.push_back(row);
2116+
} else {
2117+
partitions.at(found->second).rows.push_back(row);
2118+
}
2119+
}
2120+
return partitions;
2121+
}
2122+
2123+
bool window_rows_are_peers(const std::vector<CompiledSortKey>& order_keys,
2124+
std::size_t left,
2125+
std::size_t right) {
2126+
for (const auto& key : order_keys) {
2127+
if (!(sort_key_cell(key, left) == sort_key_cell(key, right))) {
2128+
return false;
2129+
}
2130+
}
2131+
return true;
2132+
}
2133+
2134+
std::int64_t checked_window_ordinal(std::size_t ordinal, const std::string& output_name) {
2135+
if (ordinal > static_cast<std::size_t>(std::numeric_limits<std::int64_t>::max())) {
2136+
throw std::runtime_error(output_name + " overflowed int64");
2137+
}
2138+
return static_cast<std::int64_t>(ordinal);
2139+
}
2140+
2141+
void evaluate_ranking_window(const plan::WindowExpression& window,
2142+
const storage::ColumnarBatch& batch,
2143+
const std::vector<WindowPartition>& partitions,
2144+
std::vector<TypedCell>& values) {
2145+
const auto order_keys = compile_sort_keys(window.order_keys, batch);
2146+
for (const auto& partition : partitions) {
2147+
auto ordered_rows = partition.rows;
2148+
std::stable_sort(ordered_rows.begin(), ordered_rows.end(), [&](std::size_t left, std::size_t right) {
2149+
for (const auto& key : order_keys) {
2150+
const auto left_value = sort_key_cell(key, left);
2151+
const auto right_value = sort_key_cell(key, right);
2152+
if (left_value == right_value) {
2153+
continue;
2154+
}
2155+
return sort_cell_less(left_value, right_value, key.direction);
2156+
}
2157+
return false;
2158+
});
2159+
2160+
std::size_t rank = 1;
2161+
std::size_t dense_rank = 1;
2162+
for (std::size_t index = 0; index < ordered_rows.size(); ++index) {
2163+
if (index != 0 && !window_rows_are_peers(order_keys, ordered_rows[index - 1], ordered_rows[index])) {
2164+
rank = index + 1;
2165+
++dense_rank;
2166+
}
2167+
const auto ordinal = window.function == sql::WindowFunction::RowNumber
2168+
? index + 1
2169+
: (window.function == sql::WindowFunction::Rank ? rank : dense_rank);
2170+
values.at(ordered_rows[index]) = int_cell(checked_window_ordinal(ordinal, window.output_name));
2171+
}
2172+
}
2173+
}
2174+
2175+
void evaluate_aggregate_window(const plan::WindowExpression& window,
2176+
const storage::ColumnarBatch& batch,
2177+
const std::vector<WindowPartition>& partitions,
2178+
std::vector<TypedCell>& values) {
2179+
const auto aggregate = aggregate_expression_for_window(window);
2180+
const auto compiled = compile_aggregate_expression(aggregate, batch);
2181+
for (const auto& partition : partitions) {
2182+
AggregateValue state;
2183+
for (auto row : partition.rows) {
2184+
update_aggregate(state, compiled, row);
2185+
}
2186+
const auto value = finalize_aggregate(state, aggregate);
2187+
for (auto row : partition.rows) {
2188+
values.at(row) = value;
2189+
}
2190+
}
2191+
}
2192+
2193+
void add_window_column(storage::ColumnarBatch& out,
2194+
const plan::WindowExpression& window,
2195+
const BatchView& input) {
2196+
const auto partitions = build_window_partitions(window, input);
2197+
std::vector<TypedCell> values(input.batch->row_count(), null_cell(window.type));
2198+
2199+
switch (window.function) {
2200+
case sql::WindowFunction::RowNumber:
2201+
case sql::WindowFunction::Rank:
2202+
case sql::WindowFunction::DenseRank:
2203+
evaluate_ranking_window(window, *input.batch, partitions, values);
2204+
break;
2205+
case sql::WindowFunction::Count:
2206+
case sql::WindowFunction::Sum:
2207+
case sql::WindowFunction::Min:
2208+
case sql::WindowFunction::Max:
2209+
evaluate_aggregate_window(window, *input.batch, partitions, values);
2210+
break;
2211+
}
2212+
2213+
TypedColumnBuilder column(window.type);
2214+
column.reserve(input.selection->size());
2215+
for (auto row : *input.selection) {
2216+
append_cell(column, values.at(row));
2217+
}
2218+
std::move(column).add_to(out, window.output_name);
2219+
}
2220+
2221+
storage::ColumnarBatch materialize_window(const plan::PhysicalPlan& plan, const BatchView& input) {
2222+
validate_view(input);
2223+
2224+
storage::ColumnarBatch out;
2225+
for (const auto& name : input.batch->column_names()) {
2226+
add_materialized_selected_column(out, input, name, name);
2227+
}
2228+
for (const auto& window : plan.window_expressions) {
2229+
add_window_column(out, window, input);
2230+
}
2231+
return out;
2232+
}
2233+
2234+
BatchView execute_window(const plan::PhysicalPlan& plan, ExecutionContext& context) {
2235+
auto input = execute_to_view(require_input(plan), context);
2236+
auto materialized = std::make_shared<const storage::ColumnarBatch>(materialize_window(plan, input));
2237+
2238+
BatchView view;
2239+
view.owned_batch = materialized;
2240+
view.batch = materialized.get();
2241+
view.selection = identity_selection(materialized->row_count());
2242+
view.selection_rows_match_positions = true;
2243+
validate_view(view);
2244+
return view;
2245+
}
2246+
20682247
BatchView execute_distinct(const plan::PhysicalPlan& plan, ExecutionContext& context) {
20692248
auto input = execute_to_view(require_input(plan), context);
20702249
validate_view(input);
@@ -2216,6 +2395,22 @@ bool plan_requires_typed_dispatch(const plan::PhysicalPlan& plan, const Catalog&
22162395
return true;
22172396
}
22182397
}
2398+
for (const auto& window : plan.window_expressions) {
2399+
if (window.type == catalog::ColumnType::String ||
2400+
(window.argument.has_value() && window.argument->type == catalog::ColumnType::String)) {
2401+
return true;
2402+
}
2403+
for (const auto& key : window.partition_keys) {
2404+
if (key.type == catalog::ColumnType::String) {
2405+
return true;
2406+
}
2407+
}
2408+
for (const auto& key : window.order_keys) {
2409+
if (key.column.type == catalog::ColumnType::String) {
2410+
return true;
2411+
}
2412+
}
2413+
}
22192414
for (const auto& key : plan.sort_keys) {
22202415
if (key.column.type == catalog::ColumnType::String) {
22212416
return true;
@@ -2236,6 +2431,7 @@ bool plan_requires_typed_dispatch(const plan::PhysicalPlan& plan, const Catalog&
22362431
case plan::PhysicalKind::Filter:
22372432
case plan::PhysicalKind::Project:
22382433
case plan::PhysicalKind::Aggregate:
2434+
case plan::PhysicalKind::Window:
22392435
case plan::PhysicalKind::Distinct:
22402436
case plan::PhysicalKind::Sort:
22412437
case plan::PhysicalKind::Limit:

0 commit comments

Comments
 (0)