Repository: duckdb/duckdb (2026-07-05T07:03:11Z)

Issue:
`length` function doesn't prune correctly based on stats
### What happens?

Hi team, DuckDB does store min and max length for strings, but it doesn't seem to be used in `length`. See repro SQL for example, should be straightforward.

### To Reproduce

```sql
memory D CREATE TABLE strings AS
         SELECT 'user_' || lpad(i::VARCHAR, 10, '0') AS v
         FROM range(0, 100000) tbl(i);

-- If stats propagated correctly, we don't need to scan any data.
memory D EXPLAIN SELECT count(*) FROM strings WHERE length(v) > 30;
╭─ Ungrouped Aggregate ──────╮
│ Aggregates: count_star()   │
│ ~1 row                     │
╰──────────────┬─────────────╯
╭─ Seq Scan ───┴─────────────╮
│ Table: memory.main.strings │
│ Type: Sequential Scan      │
│ Filters: length(v) > 30    │
│ ~20,000 rows               │
╰────────────────────────────╯
```

### 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/function/scalar/string/length.cpp b/src/function/scalar/string/length.cpp
index 7555c5d57b08..ec9917645145 100644
--- a/src/function/scalar/string/length.cpp
+++ b/src/function/scalar/string/length.cpp
@@ -1,6 +1,10 @@
 #include "duckdb/common/exception.hpp"
+#include "duckdb/common/numeric_utils.hpp"
 #include "duckdb/common/types/bit.hpp"
 #include "duckdb/common/vector_operations/vector_operations.hpp"
+#include "duckdb/storage/statistics/base_statistics.hpp"
+#include "duckdb/storage/statistics/numeric_stats.hpp"
+#include "duckdb/storage/statistics/string_stats.hpp"
 #include "duckdb/function/scalar/string_common.hpp"
 #include "duckdb/function/scalar/string_functions.hpp"
 #include "duckdb/planner/expression/bound_function_expression.hpp"
@@ -61,11 +65,32 @@ unique_ptr<BaseStatistics> LengthPropagateStats(ClientContext &context, Function
 	auto &child_stats = input.child_stats;
 	auto &expr = input.expr;
 	D_ASSERT(child_stats.size() == 1);
-	// can only propagate stats if the children have stats
-	if (!StringStats::CanContainUnicode(child_stats[0])) {
+	const bool can_contain_unicode = StringStats::CanContainUnicode(child_stats[0]);
+	if (!can_contain_unicode) {
 		expr.FunctionMutable().SetFunctionCallback(ScalarFunction::UnaryFunction<string_t, int64_t, StrLenOperator>);
 	}
-	return nullptr;
+	if (!StringStats::HasMaxStringLength(child_stats[0])) {
+		return nullptr;
+	}
+
+	// String stats are stored as byte length, so we need to convert to character length
+	const auto max_length = NumericCast<int64_t>(StringStats::MaxStringLength(child_stats[0]));
+	int64_t min_length = 0;
+	auto min_string_length = StringStats::MinStringLength(child_stats[0]);
+	if (min_string_length.IsValid()) {
+		if (!can_contain_unicode) {
+			// ASCII-only: the character count is exactly the byte count
+			min_length = NumericCast<int64_t>(min_string_length.GetIndex());
+		} else if (min_string_length.GetIndex() > 0) {
+			// non-empty strings contain at least one character
+			min_length = 1;
+		}
+	}
+	auto result = NumericStats::CreateEmpty(expr.GetReturnType());
+	NumericStats::SetMin(result, Value::BIGINT(min_length));
+	NumericStats::SetMax(result, Value::BIGINT(max_length));
+	result.CopyValidity(child_stats[0]);
+	return result.ToUnique();
 }
 
 //------------------------------------------------------------------


Test patch:
diff --git a/test/optimizer/statistics/statistics_string_length.test b/test/optimizer/statistics/statistics_string_length.test
new file mode 100644
index 000000000000..ad3b61b64a5a
--- /dev/null
+++ b/test/optimizer/statistics/statistics_string_length.test
@@ -0,0 +1,14 @@
+# name: test/optimizer/statistics/statistics_string_length.test
+# description: Statistics propagation for the length function based on string length statistics
+# group: [statistics]
+
+statement ok
+CREATE TABLE strings AS
+SELECT 'user_' || lpad(i::VARCHAR, 10, '0') AS v
+FROM range(0, 100000) tbl(i);
+
+# every string is exactly 15 characters - length(v) > 30 can be pruned entirely
+query II
+EXPLAIN SELECT count(*) FROM strings WHERE length(v) > 30;
+----
+physical_plan	<REGEX>:.*Empty Result.*