Repository: duckdb/duckdb (2026-06-23T07:05:01Z)

Issue:
CSE incorrectly deduplicates duplicate calls to volatile functions (uuid(), random()) inside aggregate/window expressions
### What happens?

When the same volatile-function expression (`uuid()`, `random()`, `gen_random_uuid()`) is used as input to two textually identical aggregate/window calls in the same SELECT list, the planner deduplicates them via common subexpression elimination (CSE) and evaluates the volatile function only once, reusing that single value for both output columns instead of evaluating each occurrence independently.


### To Reproduce

```sql
select g,
    any_value(uuid()::varchar) over (partition by g) as id_a,
    any_value(uuid()::varchar) over (partition by g) as id_b
from (values (1),(1),(2),(2)) t(g);
-- id_a = id_b
```

Actual result — id_a equals id_b on every row:

Expected: id_a and id_b independently generated, no relation between the two columns.

Making the two expressions textually different (but semantically equivalent) removes the collision : 
```sql
select g,
    any_value(uuid()::varchar) over (partition by g) as id_a,
    any_value((uuid()||'')::varchar) over (partition by g) as id_b
from (values (1),(1),(2),(2)) t(g);
-- id_a != id_b
```

The same query pattern on PostgreSQL (gen_random_uuid()) returns two independent, distinct values per row, confirming this is a DuckDB-specific defect rather than a standard SQL ambiguity.


### OS:

Linux (WSL2)

### DuckDB Version:

1.5.2 and 1.5.4

### DuckDB Client:

Python, jdbc

### Hardware:

_No response_

### Full Name:

Benjamin Blanchin

### Affiliation:

stane

### 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/execution/physical_plan/plan_window.cpp b/src/execution/physical_plan/plan_window.cpp
index a85dbcba8706..08ef06297ff0 100644
--- a/src/execution/physical_plan/plan_window.cpp
+++ b/src/execution/physical_plan/plan_window.cpp
@@ -112,15 +112,17 @@ PhysicalOperator &PhysicalPlanGenerator::CreatePlan(LogicalWindow &op) {
 				continue;
 			}
 
-			// CSE Elimination: Search for a previous match
+			// CSE Elimination: Search for a previous match (volatile expressions must not be deduplicated)
 			bool cse = false;
-			for (idx_t i = 0; i < matching.size(); ++i) {
-				const auto match_idx = matching[i];
-				auto &match_expr = op.expressions[match_idx]->Cast<BoundWindowExpression>();
-				if (wexpr.Equals(match_expr)) {
-					projection_map[input_width + expr_idx] = output_pos + i;
-					cse = true;
-					break;
+			if (!wexpr.IsVolatile()) {
+				for (idx_t i = 0; i < matching.size(); ++i) {
+					const auto match_idx = matching[i];
+					auto &match_expr = op.expressions[match_idx]->Cast<BoundWindowExpression>();
+					if (wexpr.Equals(match_expr)) {
+						projection_map[input_width + expr_idx] = output_pos + i;
+						cse = true;
+						break;
+					}
 				}
 			}
 			if (cse) {
diff --git a/src/optimizer/common_aggregate_optimizer.cpp b/src/optimizer/common_aggregate_optimizer.cpp
index d351ccb6a23a..037ae908db05 100644
--- a/src/optimizer/common_aggregate_optimizer.cpp
+++ b/src/optimizer/common_aggregate_optimizer.cpp
@@ -50,6 +50,15 @@ void CommonAggregateOptimizer::ExtractCommonAggregates(LogicalAggregate &aggr) {
 	for (idx_t i = 0; i < aggr.expressions.size(); i++) {
 		ProjectionIndex original_index(i + total_erased);
 		ProjectionIndex new_index(i);
+		// volatile aggregates must not be deduplicated: each call is independent
+		if (aggr.expressions[i]->IsVolatile()) {
+			if (new_index != original_index) {
+				ColumnBinding original_binding(aggr.aggregate_index, original_index);
+				ColumnBinding new_binding(aggr.aggregate_index, new_index);
+				aggregate_map[original_binding] = new_binding;
+			}
+			continue;
+		}
 		auto entry = aggregate_remap.find(*aggr.expressions[i]);
 		if (entry == aggregate_remap.end()) {
 			// aggregate does not exist yet: add it to the map


Test patch:
diff --git a/test/sql/window/test_volatile_independence.test b/test/sql/window/test_volatile_independence.test
index cf5414f34b0a..4dae3701ac12 100644
--- a/test/sql/window/test_volatile_independence.test
+++ b/test/sql/window/test_volatile_independence.test
@@ -8,11 +8,23 @@ require vector_size 2048
 set seed 0.8675309
 
 query II
-SELECT 
-	list(random()) OVER (ORDER BY id), 
-	max(random()) OVER (ORDER BY id) 
+SELECT
+	list(random()) OVER (ORDER BY id),
+	max(random()) OVER (ORDER BY id)
 FROM range(3) t(id);
 ----
 [0.5232026142836782]	0.985522684201432
 [0.5232026142836782, 0.47754610640283557]	0.985522684201432
 [0.5232026142836782, 0.47754610640283557, 0.6947716273694302]	0.985522684201432
+
+# Identical volatile window expressions must not be CSE'd (issue #23367)
+# any_value(uuid()) called twice should produce two independent UUIDs per partition
+query I
+SELECT count(*) FROM (
+	SELECT
+		any_value(uuid()::varchar) OVER (PARTITION BY g) AS id_a,
+		any_value(uuid()::varchar) OVER (PARTITION BY g) AS id_b
+	FROM (VALUES (1),(1),(2),(2)) t(g)
+) WHERE id_a = id_b;
+----
+0