Skip to content

Commit cdaf229

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 39e368d commit cdaf229

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
@@ -3081,6 +3081,17 @@ build_sortinfo(PlannerInfo *root, const Chunk *chunk, RelOptInfo *chunk_rel,
30813081
if (!ec->ec_has_volatile)
30823082
{
30833083
em_expr = ts_find_em_expr_for_rel(pk->pk_eclass, compression_info->chunk_rel);
3084+
3085+
/*
3086+
* We can't sort the ColumnarScan on a set-returning function. It is
3087+
* expanded by a ProjectSet node above the scan, so treat it as no
3088+
* match and let the sort happen there. The leading sort keys
3089+
* collected before it are still usable to sort the ColumnarScan.
3090+
*/
3091+
if (em_expr && expression_returns_set((Node *) em_expr))
3092+
{
3093+
em_expr = NULL;
3094+
}
30843095
}
30853096
chunk_em_exprs = lappend(chunk_em_exprs, em_expr);
30863097
}

tsl/test/expected/compress_sort_transform.out

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