Skip to content

Commit 80cf0a5

Browse files
committed
Fix set-returning functions in the sort key of ColumnarScan
Sorting a compressed chunk on a set-returning function failed with "set-valued function called in context that cannot accept a set". The function ended up in the sort key pushed down to the per-chunk sorted scan, but a set-returning function can only be expanded above the scan. Stop collecting per-chunk sort keys at the first set-returning function so the sort happens above it. Fixes #9927
1 parent fd3c049 commit 80cf0a5

4 files changed

Lines changed: 162 additions & 0 deletions

File tree

.unreleased/pr_9981

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixes: #9981 Fix set-returning functions in the sort key of ColumnarScan

tsl/src/nodes/columnar_scan/columnar_scan.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3077,6 +3077,17 @@ build_sortinfo(PlannerInfo *root, const Chunk *chunk, RelOptInfo *chunk_rel,
30773077
break;
30783078
}
30793079

3080+
/*
3081+
* We can't sort the ColumnarScan on a set-returning function. It is
3082+
* expanded by a ProjectSet node above the scan, so stop here and let
3083+
* the sort happen there. The leading sort keys collected before it are
3084+
* still usable to sort the ColumnarScan.
3085+
*/
3086+
if (expression_returns_set((Node *) chunk_em_expr))
3087+
{
3088+
break;
3089+
}
3090+
30803091
sort_pathkeys = lappend(sort_pathkeys, pk);
30813092
sort_pathkey_exprs = lappend(sort_pathkey_exprs, chunk_em_expr);
30823093
}

tsl/test/expected/compress_sort_transform.out

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,3 +381,121 @@ group by 1
381381
reset max_parallel_workers_per_gather;
382382
reset work_mem;
383383
reset enable_hashagg;
384+
-- Sorting on a set-returning function.
385+
set max_parallel_workers_per_gather to 0;
386+
create table srf_sort(time timestamptz not null, dev int, seg int);
387+
select create_hypertable('srf_sort', 'time', chunk_time_interval => interval '1 day');
388+
create_hypertable
389+
-----------------------
390+
(3,public,srf_sort,t)
391+
392+
insert into srf_sort values
393+
('2025-01-01', 3, 1),
394+
('2025-01-02', 2, 1),
395+
('2025-01-03', 4, 2);
396+
alter table srf_sort set (timescaledb.compress, timescaledb.compress_segmentby='seg', timescaledb.compress_orderby='time');
397+
select count(compress_chunk(c)) from show_chunks('srf_sort') c;
398+
count
399+
-------
400+
3
401+
402+
-- Sorting by the set-returning function with a LIMIT.
403+
select generate_series(1, dev) g from srf_sort order by g limit 5;
404+
g
405+
---
406+
1
407+
1
408+
1
409+
2
410+
2
411+
412+
explain (costs off) select generate_series(1, dev) g from srf_sort order by g limit 5;
413+
--- QUERY PLAN ---
414+
Limit
415+
-> Sort
416+
Sort Key: (generate_series(1, srf_sort.dev))
417+
-> ProjectSet
418+
-> Append
419+
-> Custom Scan (ColumnarScan) on _hyper_3_7_chunk
420+
-> Seq Scan on compress_hyper_4_10_chunk
421+
-> Custom Scan (ColumnarScan) on _hyper_3_8_chunk
422+
-> Seq Scan on compress_hyper_4_11_chunk
423+
-> Custom Scan (ColumnarScan) on _hyper_3_9_chunk
424+
-> Seq Scan on compress_hyper_4_12_chunk
425+
426+
-- Without a LIMIT it produces the same data.
427+
select generate_series(1, dev) g from srf_sort order by g;
428+
g
429+
---
430+
1
431+
1
432+
1
433+
2
434+
2
435+
2
436+
3
437+
3
438+
4
439+
440+
-- A set-returning function only in the output, sorting on a real column.
441+
explain (costs off) select generate_series(1, dev) g, time from srf_sort order by time limit 3;
442+
--- QUERY PLAN ---
443+
Limit
444+
-> ProjectSet
445+
-> Custom Scan (ChunkAppend) on srf_sort
446+
Order: srf_sort."time"
447+
-> Custom Scan (ColumnarScan) on _hyper_3_7_chunk
448+
-> Sort
449+
Sort Key: compress_hyper_4_10_chunk._ts_meta_v2_first_time
450+
-> Seq Scan on compress_hyper_4_10_chunk
451+
-> Custom Scan (ColumnarScan) on _hyper_3_8_chunk
452+
-> Sort
453+
Sort Key: compress_hyper_4_11_chunk._ts_meta_v2_first_time
454+
-> Seq Scan on compress_hyper_4_11_chunk
455+
-> Custom Scan (ColumnarScan) on _hyper_3_9_chunk
456+
-> Sort
457+
Sort Key: compress_hyper_4_12_chunk._ts_meta_v2_first_time
458+
-> Seq Scan on compress_hyper_4_12_chunk
459+
460+
-- Sorting on a real column followed by a set-returning function.
461+
select dev, generate_series(1, dev) g from srf_sort order by dev, g limit 4;
462+
dev | g
463+
-----+---
464+
2 | 1
465+
2 | 2
466+
3 | 1
467+
3 | 2
468+
469+
-- Sorting on a segment column followed by a set-returning function.
470+
explain (costs off) select seg, generate_series(1, dev) g from srf_sort order by seg, g limit 4;
471+
--- QUERY PLAN ---
472+
Limit
473+
-> Incremental Sort
474+
Sort Key: srf_sort.seg, (generate_series(1, srf_sort.dev))
475+
Presorted Key: srf_sort.seg
476+
-> ProjectSet
477+
-> Merge Append
478+
Sort Key: srf_sort.seg
479+
-> Sort
480+
Sort Key: _hyper_3_7_chunk.seg
481+
-> Custom Scan (ColumnarScan) on _hyper_3_7_chunk
482+
-> Seq Scan on compress_hyper_4_10_chunk
483+
-> Sort
484+
Sort Key: _hyper_3_8_chunk.seg
485+
-> Custom Scan (ColumnarScan) on _hyper_3_8_chunk
486+
-> Seq Scan on compress_hyper_4_11_chunk
487+
-> Sort
488+
Sort Key: _hyper_3_9_chunk.seg
489+
-> Custom Scan (ColumnarScan) on _hyper_3_9_chunk
490+
-> Seq Scan on compress_hyper_4_12_chunk
491+
492+
select seg, generate_series(1, dev) g from srf_sort order by seg, g limit 4;
493+
seg | g
494+
-----+---
495+
1 | 1
496+
1 | 1
497+
1 | 2
498+
1 | 2
499+
500+
drop table srf_sort cascade;
501+
reset max_parallel_workers_per_gather;

tsl/test/sql/compress_sort_transform.sql

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,35 @@ reset max_parallel_workers_per_gather;
9696

9797
reset work_mem;
9898
reset enable_hashagg;
99+
100+
-- Sorting on a set-returning function.
101+
set max_parallel_workers_per_gather to 0;
102+
103+
create table srf_sort(time timestamptz not null, dev int, seg int);
104+
select create_hypertable('srf_sort', 'time', chunk_time_interval => interval '1 day');
105+
insert into srf_sort values
106+
('2025-01-01', 3, 1),
107+
('2025-01-02', 2, 1),
108+
('2025-01-03', 4, 2);
109+
alter table srf_sort set (timescaledb.compress, timescaledb.compress_segmentby='seg', timescaledb.compress_orderby='time');
110+
select count(compress_chunk(c)) from show_chunks('srf_sort') c;
111+
112+
-- Sorting by the set-returning function with a LIMIT.
113+
select generate_series(1, dev) g from srf_sort order by g limit 5;
114+
explain (costs off) select generate_series(1, dev) g from srf_sort order by g limit 5;
115+
116+
-- Without a LIMIT it produces the same data.
117+
select generate_series(1, dev) g from srf_sort order by g;
118+
119+
-- A set-returning function only in the output, sorting on a real column.
120+
explain (costs off) select generate_series(1, dev) g, time from srf_sort order by time limit 3;
121+
122+
-- Sorting on a real column followed by a set-returning function.
123+
select dev, generate_series(1, dev) g from srf_sort order by dev, g limit 4;
124+
125+
-- Sorting on a segment column followed by a set-returning function.
126+
explain (costs off) select seg, generate_series(1, dev) g from srf_sort order by seg, g limit 4;
127+
select seg, generate_series(1, dev) g from srf_sort order by seg, g limit 4;
128+
129+
drop table srf_sort cascade;
130+
reset max_parallel_workers_per_gather;

0 commit comments

Comments
 (0)