From e9467c4fe0d5dae420f04d830b6a34dc7ecc5874 Mon Sep 17 00:00:00 2001 From: Leonid Krugliak Date: Wed, 20 May 2026 12:50:13 +0300 Subject: [PATCH] Add StructColumnData::Select to propagate selection to children Previously, struct columns had no Select override and fell back to ColumnData::Select which scans all 2048 rows per vector then slices to the selected rows. For wide tables with many struct columns and selective filters, this caused 98% wasted work. The new Select implementation: - Scans struct validity (detects all-NULL instantly) - Propagates Select to child columns, which use compression-specific select functions to read only selected rows - Falls back to generic path for pushdown-extract mode On a 559-column table with dynamic bloom filter selecting ~37 rows per 2048-row chunk, this reduces scan time by 12-15% on release builds and up to 70% on non-LTO builds. Co-Authored-By: Claude Sonnet 4.6 --- .../storage/table/struct_column_data.hpp | 2 + src/storage/table/struct_column_data.cpp | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/include/duckdb/storage/table/struct_column_data.hpp b/src/include/duckdb/storage/table/struct_column_data.hpp index bc0d1c24b79b..98f50b55dbcd 100644 --- a/src/include/duckdb/storage/table/struct_column_data.hpp +++ b/src/include/duckdb/storage/table/struct_column_data.hpp @@ -47,6 +47,8 @@ class StructColumnData : public ColumnData { idx_t scan_count) override; idx_t ScanCount(ColumnScanState &state, Vector &result, idx_t count, idx_t result_offset = 0) override; + void Select(TransactionData transaction, idx_t vector_index, ColumnScanState &state, Vector &result, + SelectionVector &sel, idx_t count) override; void Skip(ColumnScanState &state, idx_t count = STANDARD_VECTOR_SIZE) override; void InitializeAppend(ColumnAppendState &state) override; diff --git a/src/storage/table/struct_column_data.cpp b/src/storage/table/struct_column_data.cpp index f49240a49520..2d756c0d1cb6 100644 --- a/src/storage/table/struct_column_data.cpp +++ b/src/storage/table/struct_column_data.cpp @@ -200,6 +200,56 @@ idx_t StructColumnData::ScanCount(ColumnScanState &state, Vector &result, idx_t return scan_count; } +void StructColumnData::Select(TransactionData transaction, idx_t vector_index, ColumnScanState &state, Vector &result, + SelectionVector &sel, idx_t sel_count) { + if (state.storage_index.IsPushdownExtract()) { + // Pushdown extract mode — use generic path + ColumnData::Select(transaction, vector_index, state, result, sel, sel_count); + return; + } + auto target_count = GetVectorCount(vector_index); + validity->Scan(transaction, vector_index, state.child_states[0], result, target_count); + if (result.GetVectorType() == VectorType::CONSTANT_VECTOR) { + if (!ConstantVector::IsNull(result)) { + throw InternalException("StructColumnData::Select returned a constant but not NULL vector"); + } + auto struct_children = GetStructChildren(state); + for (auto &child : struct_children) { + if (child.should_scan) { + child.col.Skip(child.state); + } + } + return; + } + auto struct_children = GetStructChildren(state); + for (auto &child : struct_children) { + auto &target_vector = GetFieldVectorForScan(result, child.vector_index); + if (!child.should_scan) { + ConstantVector::SetNull(target_vector, count_t(sel_count)); + continue; + } + if (child.state.expression_state) { + ScanChild(child.state, target_vector, [&](Vector &child_result) { + return child.col.Scan(transaction, vector_index, child.state, child_result, target_count); + }); + target_vector.Slice(sel, sel_count); + } else { + child.col.Select(transaction, vector_index, child.state, target_vector, sel, sel_count); + } + } + auto &vmask = FlatVector::ValidityMutable(result); + if (vmask.IsMaskSet()) { + ValidityMask new_mask(sel_count); + for (idx_t i = 0; i < sel_count; i++) { + if (!vmask.RowIsValid(sel.get_index(i))) { + new_mask.SetInvalid(i); + } + } + vmask = std::move(new_mask); + } + FlatVector::SetSize(result, sel_count); +} + void StructColumnData::Skip(ColumnScanState &state, idx_t count) { validity->Skip(state.child_states[0], count);