Repository: duckdb/duckdb (2026-07-06T05:25:19Z)

Issue:
Rowid filter doesn't prune row group
### What happens?

Hi team, I found queries against rowid doesn't seem to prune row group and do sequential scan; see repro SQL for details, which should be straightforward.

### To Reproduce

```sql
memory D CREATE TABLE t AS SELECT printf('%08d', i) AS s, i::BIGINT AS x FROM range(10000000) tbl(i);

memory D EXPLAIN ANALYZE SELECT count(*) FROM t WHERE rowid = 0;
╭─ Summary ───────────╮
│ Total Time: 0.0035s │
╰─────────────────────╯
╭─ Ungrouped Aggregate ───────╮
│ Aggregates: count_star()    │
│ 1 row                   0µs │
╰──────────────┒┎─────────────╯
╭─ Table Scan ─┚┖─────────────╮
│ Table: memory.main.t        │
│ Type: Sequential Scan       │
│ Filters: rowid = 0          │
│ Row Groups Scanned: 82 / 82 │
│ 1 row                   0µs │
╰─────────────────────────────╯

memory D EXPLAIN ANALYZE SELECT count(*) FROM t WHERE rowid BETWEEN 0 AND 10;
╭─ Summary ───────────╮
│ Total Time: 0.0032s │
╰─────────────────────╯
╭─ Ungrouped Aggregate ───────────────────╮
│ 1 row                               0µs │
╰────────────────────┒┎───────────────────╯
╭─ Table Scan ───────┚┖───────────────────╮
│ Table: memory.main.t                    │
│ Type: Sequential Scan                   │
│ Filters: (rowid >= 0) AND (rowid <= 10) │
│ Row Groups Scanned: 82 / 82             │
│ 11 rows                          10.0ms │
╰─────────────────────────────────────────╯
```

### OS:

macos

### DuckDB Version:

a81547b43c56c671a7ad1a636f83886713efdd8a

### DuckDB Client:

SQL

### Hardware:

_No response_

### Full Name:

dentiny

### Affiliation:

N/A

### Did you include all relevant configuration (e.g., CPU architecture, Linux distribution) to reproduce the issue?

- [x] Yes, I have

### Did you include all code required to reproduce the issue?

- [x] Yes, I have

### Did you include all relevant data sets for reproducing the issue?

Yes


Discussion:

Patch:
diff --git a/src/include/duckdb/storage/table/row_group.hpp b/src/include/duckdb/storage/table/row_group.hpp
index 384d5abf90f1..77606d8b4667 100644
--- a/src/include/duckdb/storage/table/row_group.hpp
+++ b/src/include/duckdb/storage/table/row_group.hpp
@@ -149,7 +149,7 @@ class RowGroup : public SegmentBase<RowGroup> {
 	bool InitializeScanWithOffset(CollectionScanState &state, SegmentNode<RowGroup> &node, idx_t vector_offset);
 	//! Checks the given set of table filters against the row-group statistics. Returns false if the entire row group
 	//! can be skipped.
-	bool CheckZonemap(optional_ptr<ClientContext> context, ScanFilterInfo &filters);
+	bool CheckZonemap(optional_ptr<ClientContext> context, ScanFilterInfo &filters, idx_t row_start);
 	//! Checks the given set of table filters against the per-segment statistics. Returns false if any segments were
 	//! skipped.
 	bool CheckZonemapSegments(CollectionScanState &state);
diff --git a/src/storage/table/row_group.cpp b/src/storage/table/row_group.cpp
index cafa06c760af..6a28732468a0 100644
--- a/src/storage/table/row_group.cpp
+++ b/src/storage/table/row_group.cpp
@@ -384,7 +384,7 @@ void CollectionScanState::Initialize(const QueryContext &context_p, const vector
 bool RowGroup::InitializeScanWithOffset(CollectionScanState &state, SegmentNode<RowGroup> &node, idx_t vector_offset) {
 	auto &column_ids = state.GetColumnIds();
 	auto &filters = state.GetFilterInfo();
-	if (!CheckZonemap(state.context.GetClientContext(), filters)) {
+	if (!CheckZonemap(state.context.GetClientContext(), filters, node.GetRowStart())) {
 		return false;
 	}
 	if (!RefersToSameObject(node.GetNode(), *this)) {
@@ -413,7 +413,7 @@ bool RowGroup::InitializeScanWithOffset(CollectionScanState &state, SegmentNode<
 bool RowGroup::InitializeScan(CollectionScanState &state, SegmentNode<RowGroup> &node) {
 	auto &column_ids = state.GetColumnIds();
 	auto &filters = state.GetFilterInfo();
-	if (!CheckZonemap(state.context.GetClientContext(), filters)) {
+	if (!CheckZonemap(state.context.GetClientContext(), filters, node.GetRowStart())) {
 		return false;
 	}
 	if (!RefersToSameObject(node.GetNode(), *this)) {
@@ -705,7 +705,7 @@ FilterPropagateResult RowGroup::CheckRowIdFilter(const TableFilter &filter, idx_
 	return expr_filter.CheckStatistics(dummy_stats);
 }
 
-bool RowGroup::CheckZonemap(optional_ptr<ClientContext> context, ScanFilterInfo &filters) {
+bool RowGroup::CheckZonemap(optional_ptr<ClientContext> context, ScanFilterInfo &filters, idx_t row_start) {
 	auto &filter_list = filters.GetFilterList();
 	// new row group - label all filters as up for grabs again
 	filters.CheckAllFilters();
@@ -714,7 +714,13 @@ bool RowGroup::CheckZonemap(optional_ptr<ClientContext> context, ScanFilterInfo
 		auto &filter = entry.filter;
 		const auto &base_column_index = entry.table_column_index;
 
-		auto prune_result = GetColumn(base_column_index).CheckZonemap(context, base_column_index, filter);
+		FilterPropagateResult prune_result;
+		if (base_column_index.IsRowIdColumn()) {
+			// the row ids in this row group span exactly [row_start, row_start + count)
+			prune_result = CheckRowIdFilter(filter, row_start, row_start + count);
+		} else {
+			prune_result = GetColumn(base_column_index).CheckZonemap(context, base_column_index, filter);
+		}
 		if (prune_result == FilterPropagateResult::FILTER_ALWAYS_FALSE) {
 			return false;
 		}


Test patch:
diff --git a/test/sql/optimizer/test_rowid_pruning.test b/test/sql/optimizer/test_rowid_pruning.test
new file mode 100644
index 000000000000..d2f308c52c7b
--- /dev/null
+++ b/test/sql/optimizer/test_rowid_pruning.test
@@ -0,0 +1,17 @@
+# name: test/sql/optimizer/test_rowid_pruning.test
+# description: Row-group pruning based on rowid filters (#23625)
+# group: [optimizer]
+
+# 300K rows -> 3 row groups (DEFAULT_ROW_GROUP_SIZE = 122880)
+statement ok
+CREATE TABLE rowid_prune AS SELECT range AS v FROM range(300000);
+
+query II
+EXPLAIN ANALYZE SELECT count(*) FROM rowid_prune WHERE rowid = 0;
+----
+analyzed_plan	<REGEX>:.*Row Groups Scanned: 1 / 3.*
+
+query II
+EXPLAIN ANALYZE SELECT count(*) FROM rowid_prune WHERE rowid BETWEEN 0 AND 10;
+----
+analyzed_plan	<REGEX>:.*Row Groups Scanned: 1 / 3.*