Skip to content

Commit 28bdab5

Browse files
Enable Batch Sorted Merge for nullable order by with firstlast index
1 parent 763e7b1 commit 28bdab5

5 files changed

Lines changed: 103 additions & 36 deletions

File tree

tsl/src/nodes/columnar_scan/columnar_scan.c

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2844,6 +2844,8 @@ is_var_notnull(const CompressionInfo *compression_info, Var *var)
28442844
#if PG17_LT
28452845
notnull = ts_get_attnotnull(compression_info->chunk_rte->relid, var->varattno);
28462846
#else
2847+
/* Since PG18 "notnullattnums" contain only NOT NULL columns with validated NOT NULL constraints
2848+
*/
28472849
notnull = bms_is_member(var->varattno, compression_info->chunk_rel->notnullattnums);
28482850
#endif
28492851

@@ -2895,8 +2897,8 @@ is_var_notnull(const CompressionInfo *compression_info, Var *var)
28952897
static bool
28962898
match_pathkeys_to_compression_orderby(List *pathkeys, List *chunk_em_exprs,
28972899
int starting_pathkey_offset,
2898-
const CompressionInfo *compression_info, bool for_bsm,
2899-
bool *out_reverse)
2900+
const CompressionInfo *compression_info,
2901+
bool for_batch_sorted_merge, bool *out_reverse)
29002902
{
29012903
int compressed_pk_index = 0;
29022904
for (int i = starting_pathkey_offset; i < list_length(pathkeys); i++)
@@ -2925,13 +2927,13 @@ match_pathkeys_to_compression_orderby(List *pathkeys, List *chunk_em_exprs,
29252927
return false;
29262928
}
29272929

2928-
if (for_bsm)
2930+
/* Special handling for Batch Sorted Merge with minmax-only index */
2931+
if (for_batch_sorted_merge &&
2932+
orderby_sparse_kind(compression_info->settings, orderby_index) !=
2933+
ORDERBY_SPARSE_FIRSTLAST)
29292934
{
2930-
/* Bail out on Batch Sorted Merge if orderby column is nullable,
2931-
* as at the moment the minmax metadata we have doesn't include NULLs,
2932-
* so it's difficult to use it for null-sensitive ordering.
2933-
* But this restriction can be lifted in the future on new type of chunks
2934-
* with NULL-handling metadata.
2935+
/* Bail out on Batch Sorted Merge if orderby column is nullable
2936+
* and does not have firstlast index
29352937
*/
29362938
if (!is_var_notnull(compression_info, var))
29372939
{
@@ -2946,9 +2948,7 @@ match_pathkeys_to_compression_orderby(List *pathkeys, List *chunk_em_exprs,
29462948
* will be sorted before [(1,1) .. (1,19)] with min(1),(1)
29472949
* but it should be sorted after as (1,20) > (1,1): correct with firstlast index.
29482950
*/
2949-
if (compressed_pk_index > 1 &&
2950-
orderby_sparse_kind(compression_info->settings, orderby_index) !=
2951-
ORDERBY_SPARSE_FIRSTLAST)
2951+
if (compressed_pk_index > 1)
29522952
{
29532953
return false;
29542954
}
@@ -3167,7 +3167,7 @@ build_sortinfo(PlannerInfo *root, const Chunk *chunk, RelOptInfo *chunk_rel,
31673167
chunk_em_exprs,
31683168
/* starting_pathkey_offset = */ 0,
31693169
compression_info,
3170-
/* for_bsm = */ true,
3170+
/* for_batch_sorted_merge = */ true,
31713171
&sort_info.reverse);
31723172
}
31733173
return sort_info;
@@ -3199,7 +3199,7 @@ build_sortinfo(PlannerInfo *root, const Chunk *chunk, RelOptInfo *chunk_rel,
31993199
chunk_em_exprs,
32003200
/* starting_pathkey_offset = */ 0,
32013201
compression_info,
3202-
/* for_bsm = */ true,
3202+
/* for_batch_sorted_merge = */ true,
32033203
&sort_info.reverse);
32043204
}
32053205
return sort_info;
@@ -3215,12 +3215,13 @@ build_sortinfo(PlannerInfo *root, const Chunk *chunk, RelOptInfo *chunk_rel,
32153215
* loop over the rest of pathkeys
32163216
* this needs to exactly match the configured compress_orderby
32173217
*/
3218-
sort_info.use_compressed_sort = match_pathkeys_to_compression_orderby(pathkeys,
3219-
chunk_em_exprs,
3220-
i,
3221-
compression_info,
3222-
/* for_bsm = */ false,
3223-
&sort_info.reverse);
3218+
sort_info.use_compressed_sort =
3219+
match_pathkeys_to_compression_orderby(pathkeys,
3220+
chunk_em_exprs,
3221+
i,
3222+
compression_info,
3223+
/* for_batch_sorted_merge = */ false,
3224+
&sort_info.reverse);
32243225

32253226
return sort_info;
32263227
}

tsl/test/expected/compression_sorted_merge.out

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -941,8 +941,8 @@ SELECT 1 as one, 2 as two, 3 as three, x2, x1, c2, time FROM test1 ORDER BY time
941941
1 | 2 | 3 | 3 | 1 | 43 | Fri Dec 31 17:00:00 1999 PST
942942
1 | 2 | 3 | 2 | 1 | 43 | Fri Dec 31 16:00:00 1999 PST
943943

944-
-- Test with null values: should not optimize
945-
set timescaledb.debug_require_batch_sorted_merge to 'forbid';
944+
-- Test with null values: should optimize with firstlast index
945+
set timescaledb.debug_require_batch_sorted_merge to 'force';
946946
SELECT time, x2 FROM test_with_defined_null ORDER BY x2 ASC NULLS FIRST;
947947
time | x2
948948
------------------------------+----
@@ -959,6 +959,8 @@ SELECT time, x2 FROM test_with_defined_null ORDER BY x2 DESC NULLS LAST;
959959
Sat Jan 01 00:00:00 2000 PST |
960960
Sat Jan 01 00:00:00 2000 PST |
961961

962+
-- should not optimize (NULL order wrong)
963+
set timescaledb.debug_require_batch_sorted_merge to 'forbid';
962964
SELECT time, x2 FROM test_with_defined_null ORDER BY x2 ASC NULLS LAST;
963965
time | x2
964966
------------------------------+----

tsl/test/expected/compression_sorted_merge_unordered.out

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
-- This file and its contents are licensed under the Timescale License.
22
-- Please see the included NOTICE for copyright information and
33
-- LICENSE-TIMESCALE for a copy of the license.
4+
\c :TEST_DBNAME :ROLE_SUPERUSER
45
-- Increase the working memory limit slightly, otherwise the batch sorted merge
56
-- will be penalized for segmentby cardinalities larger than 100, where it is
67
-- still faster than sort.
@@ -1295,8 +1296,8 @@ SELECT 1 as one, 2 as two, 3 as three, x2, x1, c2, time FROM test1 ORDER BY time
12951296
1 | 2 | 3 | 2 | 1 | 43 | Fri Dec 31 16:00:00 1999 PST
12961297
1 | 2 | 3 | 2 | 1 | 43 | Fri Dec 31 16:00:00 1999 PST
12971298

1298-
-- Test with null values in x2
1299-
set timescaledb.debug_require_batch_sorted_merge to 'forbid';
1299+
-- Test with null values in x2: batch sorted merge supported with firstlast indexes
1300+
set timescaledb.debug_require_batch_sorted_merge to 'force';
13001301
SELECT time, x2 FROM test_with_defined_null ORDER BY x2 ASC NULLS FIRST;
13011302
time | x2
13021303
------------------------------+----
@@ -1329,6 +1330,8 @@ SELECT time, x2 FROM test_with_defined_null ORDER BY x2 DESC NULLS LAST;
13291330
Sat Jan 01 00:00:00 2000 PST |
13301331
Sat Jan 01 00:00:00 2000 PST |
13311332

1333+
-- Should not be optimized (NULL order wrong)
1334+
set timescaledb.debug_require_batch_sorted_merge to 'forbid';
13321335
SELECT time, x2 FROM test_with_defined_null ORDER BY x2 ASC NULLS LAST;
13331336
time | x2
13341337
------------------------------+----
@@ -1589,7 +1592,7 @@ SELECT * FROM test_segby ORDER BY segby, time;
15891592
-> Seq Scan on _timescaledb_internal.compress_hyper_8_8_chunk (actual rows=2.00 loops=1)
15901593
Output: compress_hyper_8_8_chunk._ts_meta_count, compress_hyper_8_8_chunk.segby, compress_hyper_8_8_chunk._ts_meta_min_1, compress_hyper_8_8_chunk._ts_meta_max_1, compress_hyper_8_8_chunk._ts_meta_v2_first_time, compress_hyper_8_8_chunk._ts_meta_v2_last_time, compress_hyper_8_8_chunk."time", compress_hyper_8_8_chunk.val
15911594

1592-
-- Tests for #9445: forbid BatchSortedMerge on nullable orderby columns
1595+
-- Tests for #9445: forbid BatchSortedMerge on nullable orderby columns with no firstlast index
15931596
CREATE TABLE t(time int NOT NULL, device int, val int);
15941597
SELECT create_hypertable('t', 'time', chunk_time_interval => 10000);
15951598
create_hypertable
@@ -1613,9 +1616,36 @@ SELECT compress_chunk(show_chunks('t'));
16131616
SET timescaledb.enable_direct_compress_insert = true;
16141617
INSERT INTO t SELECT 1, 1, g FROM generate_series(500, 800) g;
16151618
INSERT INTO t SELECT 1, 1, g FROM generate_series(900, 1000) g;
1616-
SET timescaledb.debug_require_batch_sorted_merge = 'forbid';
1617-
-- In DESC order NULLs come first; a NULL after a non-NULL is wrong.
1619+
-- Batches with NULLs in orderby columns are correctly sorted with firstlast:
1620+
-- Batch Sorted Merge is allowed
1621+
SET timescaledb.debug_require_batch_sorted_merge = 'force';
1622+
-- Should return 0
1623+
SELECT count(*) AS wrong_rows FROM (
1624+
SELECT val, lag(val) OVER (ORDER BY val DESC) AS prev FROM t
1625+
) t WHERE val IS NULL AND prev IS NOT NULL;
1626+
wrong_rows
1627+
------------
1628+
0
1629+
1630+
-- Remove firstlast index from order by columns
1631+
update _timescaledb_catalog.compression_settings
1632+
set index = '[{"type": "minmax", "column": "val", "source": "orderby"}, {"type": "minmax", "column": "time", "source": "orderby"}]'
1633+
where relid = 't'::regclass;
1634+
update _timescaledb_catalog.compression_settings
1635+
set index = '[{"type": "minmax", "column": "val", "source": "orderby"}, {"type": "minmax", "column": "time", "source": "orderby"}]'
1636+
where compress_relid = (select format('%I.%I', schema_name, table_name)::regclass AS chunk_regclass from _timescaledb_catalog.chunk
1637+
where id = (select compressed_chunk_id from _timescaledb_catalog.chunk
1638+
where hypertable_id = (select id from _timescaledb_catalog.hypertable
1639+
where table_name = 't') limit 1));
1640+
-- Use minmax index on (val DESC, time DESC) instead
1641+
select schema_name || '.' || table_name comp_chunk from _timescaledb_catalog.chunk
1642+
where id = (select compressed_chunk_id from _timescaledb_catalog.chunk
1643+
where hypertable_id = (select id from _timescaledb_catalog.hypertable
1644+
where table_name = 't') limit 1)
1645+
\gset
1646+
create index compressed_index_minmax on :comp_chunk (device, _ts_meta_min_1 DESC, _ts_meta_max_1 DESC, _ts_meta_min_2 DESC, _ts_meta_max_2 DESC);
16181647
-- Should not use BatchSortedMerge here, should return 0
1648+
SET timescaledb.debug_require_batch_sorted_merge = 'forbid';
16191649
SELECT count(*) AS wrong_rows FROM (
16201650
SELECT val, lag(val) OVER (ORDER BY val DESC) AS prev FROM t
16211651
) t WHERE val IS NULL AND prev IS NOT NULL;
@@ -1634,6 +1664,7 @@ SELECT val, lag(val) OVER (ORDER BY val DESC NULLS FIRST) AS prev FROM t where v
16341664
997 | 998
16351665
996 | 997
16361666

1667+
drop table t cascade;
16371668
-- Tests for BatchSortedMerge over one-segment data
16381669
--------------------------------------
16391670
-- Should be optimized (all segmentby columns are pinned to a Const, orderby columns match)
@@ -1834,6 +1865,5 @@ drop table test1 cascade;
18341865
drop table test2 cascade;
18351866
drop table test_segby cascade;
18361867
drop table test_nosegby cascade;
1837-
drop table t cascade;
18381868
RESET timescaledb.enable_direct_compress_insert;
18391869
RESET timescaledb.debug_require_batch_sorted_merge;

tsl/test/sql/compression_sorted_merge.sql

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,11 +331,13 @@ SELECT x2, x1, c2, time FROM test1 ORDER BY time DESC;
331331
SELECT 1 as one, 2 as two, 3 as three, x2, x1, c2, time FROM test1 ORDER BY time DESC;
332332
SELECT 1 as one, 2 as two, 3 as three, x2, x1, c2, time FROM test1 ORDER BY time DESC;
333333

334-
-- Test with null values: should not optimize
335-
set timescaledb.debug_require_batch_sorted_merge to 'forbid';
334+
-- Test with null values: should optimize with firstlast index
335+
set timescaledb.debug_require_batch_sorted_merge to 'force';
336336
SELECT time, x2 FROM test_with_defined_null ORDER BY x2 ASC NULLS FIRST;
337337
SELECT time, x2 FROM test_with_defined_null ORDER BY x2 DESC NULLS LAST;
338338

339+
-- should not optimize (NULL order wrong)
340+
set timescaledb.debug_require_batch_sorted_merge to 'forbid';
339341
SELECT time, x2 FROM test_with_defined_null ORDER BY x2 ASC NULLS LAST;
340342
SELECT time, x2 FROM test_with_defined_null ORDER BY x2 DESC NULLS FIRST;
341343

tsl/test/sql/compression_sorted_merge_unordered.sql

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
-- Please see the included NOTICE for copyright information and
33
-- LICENSE-TIMESCALE for a copy of the license.
44

5+
\c :TEST_DBNAME :ROLE_SUPERUSER
6+
57
-- Increase the working memory limit slightly, otherwise the batch sorted merge
68
-- will be penalized for segmentby cardinalities larger than 100, where it is
79
-- still faster than sort.
@@ -384,12 +386,13 @@ SELECT x2, x1, c2, time FROM test1 ORDER BY time DESC;
384386
SELECT 1 as one, 2 as two, 3 as three, x2, x1, c2, time FROM test1 ORDER BY time DESC;
385387
SELECT 1 as one, 2 as two, 3 as three, x2, x1, c2, time FROM test1 ORDER BY time DESC;
386388

387-
-- Test with null values in x2
388-
set timescaledb.debug_require_batch_sorted_merge to 'forbid';
389-
389+
-- Test with null values in x2: batch sorted merge supported with firstlast indexes
390+
set timescaledb.debug_require_batch_sorted_merge to 'force';
390391
SELECT time, x2 FROM test_with_defined_null ORDER BY x2 ASC NULLS FIRST;
391392
SELECT time, x2 FROM test_with_defined_null ORDER BY x2 DESC NULLS LAST;
392393

394+
-- Should not be optimized (NULL order wrong)
395+
set timescaledb.debug_require_batch_sorted_merge to 'forbid';
393396
SELECT time, x2 FROM test_with_defined_null ORDER BY x2 ASC NULLS LAST;
394397
SELECT time, x2 FROM test_with_defined_null ORDER BY x2 DESC NULLS FIRST;
395398

@@ -479,7 +482,7 @@ SELECT * FROM test_segby ORDER BY time ASC NULLS FIRST;
479482
:PREFIX
480483
SELECT * FROM test_segby ORDER BY segby, time;
481484

482-
-- Tests for #9445: forbid BatchSortedMerge on nullable orderby columns
485+
-- Tests for #9445: forbid BatchSortedMerge on nullable orderby columns with no firstlast index
483486
CREATE TABLE t(time int NOT NULL, device int, val int);
484487
SELECT create_hypertable('t', 'time', chunk_time_interval => 10000);
485488
ALTER TABLE t SET (timescaledb.compress,
@@ -498,10 +501,38 @@ SET timescaledb.enable_direct_compress_insert = true;
498501
INSERT INTO t SELECT 1, 1, g FROM generate_series(500, 800) g;
499502
INSERT INTO t SELECT 1, 1, g FROM generate_series(900, 1000) g;
500503

501-
SET timescaledb.debug_require_batch_sorted_merge = 'forbid';
504+
-- Batches with NULLs in orderby columns are correctly sorted with firstlast:
505+
-- Batch Sorted Merge is allowed
506+
SET timescaledb.debug_require_batch_sorted_merge = 'force';
507+
508+
-- Should return 0
509+
SELECT count(*) AS wrong_rows FROM (
510+
SELECT val, lag(val) OVER (ORDER BY val DESC) AS prev FROM t
511+
) t WHERE val IS NULL AND prev IS NOT NULL;
512+
513+
-- Remove firstlast index from order by columns
514+
update _timescaledb_catalog.compression_settings
515+
set index = '[{"type": "minmax", "column": "val", "source": "orderby"}, {"type": "minmax", "column": "time", "source": "orderby"}]'
516+
where relid = 't'::regclass;
517+
518+
update _timescaledb_catalog.compression_settings
519+
set index = '[{"type": "minmax", "column": "val", "source": "orderby"}, {"type": "minmax", "column": "time", "source": "orderby"}]'
520+
where compress_relid = (select format('%I.%I', schema_name, table_name)::regclass AS chunk_regclass from _timescaledb_catalog.chunk
521+
where id = (select compressed_chunk_id from _timescaledb_catalog.chunk
522+
where hypertable_id = (select id from _timescaledb_catalog.hypertable
523+
where table_name = 't') limit 1));
524+
525+
-- Use minmax index on (val DESC, time DESC) instead
526+
select schema_name || '.' || table_name comp_chunk from _timescaledb_catalog.chunk
527+
where id = (select compressed_chunk_id from _timescaledb_catalog.chunk
528+
where hypertable_id = (select id from _timescaledb_catalog.hypertable
529+
where table_name = 't') limit 1)
530+
\gset
531+
create index compressed_index_minmax on :comp_chunk (device, _ts_meta_min_1 DESC, _ts_meta_max_1 DESC, _ts_meta_min_2 DESC, _ts_meta_max_2 DESC);
502532

503-
-- In DESC order NULLs come first; a NULL after a non-NULL is wrong.
504533
-- Should not use BatchSortedMerge here, should return 0
534+
SET timescaledb.debug_require_batch_sorted_merge = 'forbid';
535+
505536
SELECT count(*) AS wrong_rows FROM (
506537
SELECT val, lag(val) OVER (ORDER BY val DESC) AS prev FROM t
507538
) t WHERE val IS NULL AND prev IS NOT NULL;
@@ -510,6 +541,8 @@ SELECT count(*) AS wrong_rows FROM (
510541
SET timescaledb.debug_require_batch_sorted_merge = 'force';
511542
SELECT val, lag(val) OVER (ORDER BY val DESC NULLS FIRST) AS prev FROM t where val > 995;
512543

544+
drop table t cascade;
545+
513546
-- Tests for BatchSortedMerge over one-segment data
514547
--------------------------------------
515548

@@ -583,7 +616,6 @@ drop table test1 cascade;
583616
drop table test2 cascade;
584617
drop table test_segby cascade;
585618
drop table test_nosegby cascade;
586-
drop table t cascade;
587619

588620
RESET timescaledb.enable_direct_compress_insert;
589621
RESET timescaledb.debug_require_batch_sorted_merge;

0 commit comments

Comments
 (0)