Skip to content

refactor(query): select if branches by mask word for all types - #20904

Open
SkyFan2002 wants to merge 7 commits into
databendlabs:mainfrom
SkyFan2002:bp/n01-if
Open

SkyFan2002 wants to merge 7 commits into
databendlabs:mainfrom
SkyFan2002:bp/n01-if

Conversation

@SkyFan2002

@SkyFan2002 SkyFan2002 commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

I hereby agree to the terms of the CLA available at: https://docs.databend.com/dev/policies/cla/

Summary

Follow-up to #20881, which added a typed row selection for a binary numeric/decimal if. This PR applies three further improvements to if evaluation, suggested by sundy-li after #20232 and used by other engines.

1. Skip branches that no enabled row reaches (Evaluator::eval_if, all result types)

Each branch already runs under validity & flag, and an all-unset validity means the branch runs for nobody (errors are dropped, see expand_scalar_error). Before this PR such a branch was still evaluated over the whole block. Now:

  • a condition or branch whose validity has no set bit is not evaluated, as a branch behind a constant false condition already was. Before selection it is replaced by a reached branch of the same type, which is only read on rows that are not enabled; this also avoids Scalar::default_value, which has no Opaque value;
  • when every enabled row reaches a single branch (e.g. a column condition that is all true, all false or NULL on the block, or an if nested inside a masked branch), that branch is returned directly without row selection.

Row errors keep their existing behaviour: errors on rows that are not enabled were never reported, and the skipped branches have no enabled row. The result type, nullability and the values on enabled rows are unchanged; only the content of NULL slots in the output can differ (two golden files show this in evaluation (internal)).

Prior art: DuckDB skips a THEN with no selected rows and runs THEN/ELSE directly when a WHEN covers all rows (src/execution/expression_executor/execute_case.cpp); DataFusion checks has_true/has_false in CaseExpr::expr_or_expr (datafusion/physical-expr/src/expressions/case.rs); ClickHouse's short-circuit maskedExecute does not run a branch whose mask has no ones (src/Columns/MaskOperations.cpp).

2. Word-adaptive selection for every result type (new kernels::select_if_then_else)

The mask is read one 64-row word at a time (Bitmap::chunks::<u64>). Consecutive all-set or all-unset words are merged into runs and copied from one side in bulk (push_item_repeat for a scalar, append_column of a slice for a column); the rows of a mixed word are selected one by one. This replaces the #20881 selector and extends it from numbers/decimals to every type:

  • Nullable(T) is split into inner values (selected as T) and validity (selected as a bitmap), so every nullable type is covered. When one side is a NULL scalar the other side provides the inner values, so the column is reused without a copy;
  • Boolean and validity are combined with word-wide bit operations, (m & t) | (!m & f);
  • String, Binary and Variant go through their concrete ValueType; any other type uses AnyType.

Decimal branches with a non-strict width (e.g. Decimal(10, 2) carried by Decimal128) are converted to the strict width instead of falling back to the generic path, keeping the regression test from #20881 passing.

Prior art: run-based copy in Arrow zip (arrow-select/src/zip.rs, SlicesIterator + MutableArrayData::extend), Polars if_then_else_extend (polars-compute/src/if_then_else/mod.rs), ClickHouse's per-64-row prefixToCopy in filters (src/Columns/ColumnVector.cpp). Deciding per word avoids a block-level density heuristic: a block of a clustered table often has one or two long runs even when half of its rows are true.

3. Branch-free select for fixed-width types on mixed words

Numbers, decimals (64/128/256), Date and Timestamp write into a Vec<N>; on a mixed word both sides are loaded and one is picked without a branch, as in Polars' if_then_else_loop and ClickHouse's fillVectorVector (src/Functions/if.cpp).

Also: ColumnBuilder::push_repeat gains the Opaque arm that push already had; the bulk path needs it.

Benchmark

cargo bench -p databend-common-functions --bench bench (bench profile, i.e. release with debug assertions), groups if_selection (new, 65,536 rows) and numeric_if (from #20881). The timed section is Evaluator::run: condition, branches and selection. The base (dc7efce824) and this PR were built as separate binaries, pinned to one core of an AMD Ryzen 9 9950X, and run three times interleaved; the table shows the median of the three Divan medians.

Conditions: alternating = x % 2 = 0 (every mask word mixed), clustered = x < 32768 (two runs), all_true = x >= 0.

Case Base This PR Speedup
if(<cond>, x, x * 3) alternating 0.369 ms 0.285 ms 1.29x
if(<cond>, x, x * 3) clustered 0.257 ms 0.146 ms 1.76x
if(<cond>, x, x * 3) all_true 0.254 ms 0.105 ms 2.41x
if(<cond>, x, NULL) alternating 0.660 ms 0.280 ms 2.36x
if(<cond>, x, NULL) clustered 0.536 ms 0.134 ms 4.01x
if(<cond>, x, NULL) all_true 0.555 ms 0.107 ms 5.17x
if(<cond>, s, 'none') alternating 1.426 ms 0.875 ms 1.63x
if(<cond>, s, 'none') clustered 1.394 ms 0.426 ms 3.27x
if(<cond>, s, 'none') all_true 1.499 ms 0.104 ms 14.4x
if(<cond>, x > 10, x < 10) alternating 1.575 ms 0.439 ms 3.59x
if(<cond>, x > 10, x < 10) clustered 1.425 ms 0.339 ms 4.20x
if(<cond>, x > 10, x < 10) all_true 1.636 ms 0.222 ms 7.38x
numeric_if/if_contains 65,536 0.600 ms 0.500 ms 1.20x
numeric_if/if_modulo 65,536 0.616 ms 0.526 ms 1.17x
numeric_if/if_boolean 65,536 1.550 ms 0.439 ms 3.53x

For int64 alternating the condition x % 2 = 0 and x * 3 dominate: a standalone loop with the same release settings (opt-level = "s") selects 65,536 i64 rows from two columns with an alternating mask in about 21 µs with the branch-free form, against 44 µs with a per-row if + push.

Tests

  • kernel::test_select_if_then_else (databend-common-expression): every type of get_all_test_data_types(All) plus Decimal256, Tuple and 11 nullable types; lengths 0–700; random, run, all-set and all-unset masks with a random bit offset; column/column, scalar/column, column/scalar, scalar/scalar and NULL scalars; compared row by row with a per-row reference.
  • kernel::test_select_if_then_else_non_strict_decimal: a Decimal128 column of type Decimal(10, 2) is read at the Decimal64 width.
  • scalars::control::test_if_with_opaque_result: Opaque and Nullable(Opaque) results with all-true, all-false and mixed column conditions and a NULL branch.
  • scalars::control::test_control: golden cases for all-true and all-false/NULL column conditions with a division by zero in the other branch, a multi-branch if whose first condition takes every row, a nested if whose inner else is reached by no enabled row, and String, nullable String and Boolean results.
  • Local: cargo test -p databend-common-expression --test it, cargo test -p databend-common-functions --test it, make lint.

No setting or default change.

Tests

  • Unit Test
  • Logic Test
  • Benchmark Test
  • No Test - Explain why

Type of change

  • Bug Fix (non-breaking change which fixes an issue)
  • New Feature (non-breaking change which adds functionality)
  • Breaking Change (fix or feature that could cause existing functionality not to work as expected)
  • Documentation Update
  • Refactoring
  • Performance Improvement
  • Other (please describe):

AI assistance

  • AI usage: An AI coding agent compared the if/CASE implementations of other engines, implemented the branch skipping in eval_if, the selection kernel, its tests and the benchmark, ran the benchmark and drafted this PR description
  • Responsible human: @SkyFan2002
  • The responsible human has read every line of this diff and can explain each change

This change is Reviewable

Follow-up to databendlabs#20881.

- `eval_if` no longer evaluates a condition or branch that no enabled
  row reaches (a column condition that is all true, all false or NULL on
  the enabled rows, or an `if` nested in a masked branch). When every
  enabled row reaches one branch, that branch is returned without row
  selection.
- The selection of a binary `if` moves to `kernels::select_if_then_else`
  and covers every result type. The mask is read one 64-row word at a
  time: runs of all-set or all-unset words are copied from one side in
  bulk, and the rows of mixed words are selected one by one. Numbers,
  decimals, dates and timestamps use a branch-free select on mixed
  words; booleans and validity are combined with word-wide bit
  operations; String, Binary and Variant use their concrete value type;
  other types use `AnyType`. Non-strict decimal branches are read at the
  strict width.
- `ColumnBuilder::push_repeat` handles `Opaque` values, which the bulk
  path needs.

Add kernel tests over all test data types, mask shapes and
scalar/column combinations, golden cases for uniform conditions,
nested `if`, String, Boolean and nullable results, and an
`if_selection` benchmark group.
@SkyFan2002 SkyFan2002 added the hold-ci Pause heavy CI builds/tests while iterating on review feedback label Oct 30, 2026
@github-actions github-actions Bot added the pr-refactor this PR changes the code base without new features or bugfix label Oct 30, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T13:10:10.655811Z f4bd000 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f9cec06d72

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/query/expression/src/evaluator.rs Resolved
`Scalar::default_value` has no Opaque value. A branch that no enabled
row reaches is now left unevaluated without a placeholder scalar and is
replaced by a reached branch before selection; with no reached branch the
result is `ColumnBuilder::repeat_default`. A NULL scalar side of a
nullable selection takes its inner values from the other side.
@SkyFan2002

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: 4e97eb3dcf

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@SkyFan2002 SkyFan2002 removed the hold-ci Pause heavy CI builds/tests while iterating on review feedback label Oct 30, 2026
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

⏱ CI timing · Dev

run 41059987934 · attempt 3 (wall time includes re-run gap) · 59.1 min (main p50 71.1 min, −12.0 min)

stage this run main p50 Δ
total 59.1 min 71.1 min −12.0 min
build (aarch64, ci profile) 35.1 min 26.6 min +8.5 min ⚠️
├ deps + crates (parallel) 18.4 min — —
├ databend-query codegen 3.1 min — —
├ databend-binaries link 11.8 min — —
└ sccache hit rate 95.8 % — —
test_unit 21.0 min 17.4 min +3.6 min ⚠️
mac_check — 12.8 min —
tests after build (49 jobs) 23.3 min 35.1 min −11.8 min
├ queue p50 / max 1.6 min / 14.6 min 5.0 min / 29.9 min −3.3 min
├ sqllogic standalone (8 jobs, max) 5.0 min 4.3 min +0.8 min
├ sqllogic cluster (8 jobs, max) 7.3 min 7.2 min +0.2 min
├ sqllogic stage (2 jobs, max) 5.5 min 5.0 min +0.5 min
├ sqllogic other (10 jobs, max) 6.0 min 5.8 min +0.2 min
├ stateless (2 jobs, max) 7.3 min 6.7 min +0.7 min
├ stateful (4 jobs, max) 5.1 min 4.8 min +0.3 min
├ compat (6 jobs, max) 5.7 min 5.7 min +0.0 min
├ meta / logs / lineage / udf (5 jobs, max) 4.6 min 4.4 min +0.2 min
├ ee (2 jobs, max) 1.5 min 1.4 min +0.1 min
├ private_tasks (1 jobs, max) 8.8 min 8.8 min −0.1 min
└ fuzz (1 jobs, max) 1.1 min 1.1 min +0.0 min

Minutes unless stated; durations exclude queue time unless stated; "max" = longest job in the group. Baseline = median of the last 10 successful runs (Dev: Main on main; Cloud: recent Cloud runs). Build breakdown has no baseline. Updated on each push.

start: 0,
run: None,
};
let mut chunks = mask.chunks::<u64>();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could call BitChunksExact if offset is 0

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c76068b.

@SkyFan2002 SkyFan2002 added the hold-ci Pause heavy CI builds/tests while iterating on review feedback label Oct 30, 2026
@SkyFan2002

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c76068b866

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/query/expression/src/evaluator.rs Resolved
A decimal inside an array, map or tuple with a non-strict width can only
be converted row by row. `if` results whose type nests a decimal skip
the single-branch return and use per-row selection.
@SkyFan2002

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5675a794a7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@SkyFan2002

Copy link
Copy Markdown
Member Author

@codex review

@sundy-li sundy-li left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review of f9f9f54. The branch-skipping/word-adaptive selection design looks sound in the paths examined. The nested non-strict decimal issue identified on 4e97eb3 is fixed in the current revision: both independent review probes now pass (128 rows split into a 64-row THEN run and a 64-row ELSE run, plus single-branch tuple normalization). The two selection-kernel tests and all four scalars::control tests also pass.

No additional blocking issue found in this focused recheck. Two non-blocking suggestions below: retain the full-word/multiple-run regression case in the test suite, and clarify the deliberately conservative nested-decimal fallback. Benchmarks and generated assembly were not revalidated.

Comment thread src/query/expression/tests/it/kernel.rs Resolved
/// Whether `data_type` has a decimal inside an array, map or tuple (also
/// through the physical type of an aggregate state). A non-strict decimal (e.g.
/// `Decimal(10, 2)` carried by `Decimal128`) nested there can only be converted
/// to the strict width row by row, so such types skip the bulk paths.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: please describe this as a conservative type-based fallback, rather than saying nested decimals can only be converted row by row. has_nested_decimal checks the logical type, not the actual child widths, so even already-strict nested decimal values intentionally lose both bulk selection and single-branch reuse. That is a reasonable correctness-first choice here, but the comment should make the scope explicit. A future layout-aware check or recursive pre-normalization could retain the fast paths; no need to add that complexity to this fix. The current benchmarks do not measure this nested-type case.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f4bd000.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f9f9f54ef0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@SkyFan2002

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: dfa860dd26

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@SkyFan2002 SkyFan2002 added hold-ci Pause heavy CI builds/tests while iterating on review feedback and removed hold-ci Pause heavy CI builds/tests while iterating on review feedback labels Oct 30, 2026
@SkyFan2002

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: f4bd000f8f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@SkyFan2002 SkyFan2002 removed the hold-ci Pause heavy CI builds/tests while iterating on review feedback label Oct 30, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-refactor this PR changes the code base without new features or bugfix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants