Skip to content

Commit 4c9a4a4

Browse files
committed
Fix wrong results with IS NULL and minmax pushdown
A qual like (v > 0) IS NULL wrapped an operator whose minmax pushdown is a lossy, false-positive-only approximation. Wrapping that lossy result in a NullTest could turn a false positive into a false negative and skip a batch that actually holds matching rows, so the query returned wrong results. Only push down NullTest and the other transforming nodes when their child is pushed down exactly, never when the child still needs a recheck. Fixes #9921
1 parent 0a51c86 commit 4c9a4a4

4 files changed

Lines changed: 99 additions & 3 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixes: #9921 Fix wrong results with IS NULL and minmax sparse index pushdown

tsl/src/nodes/columnar_scan/qual_pushdown.c

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1767,8 +1767,10 @@ qual_pushdown_mutator(Node *orig_node, QualPushdownContext *context)
17671767
}
17681768

17691769
/*
1770-
* These nodes do not influence the pushdown by themselves, so we
1771-
* recurse.
1770+
* These nodes transform their child value, so we can only push them
1771+
* down when every child pushes down exactly. A child that needs a
1772+
* recheck is only approximate, and the transform could flip its result
1773+
* (e.g. IS NULL), giving wrong results.
17721774
*/
17731775
case T_FuncExpr:
17741776
case T_CoerceViaIO:
@@ -1782,8 +1784,14 @@ qual_pushdown_mutator(Node *orig_node, QualPushdownContext *context)
17821784
case T_CaseWhen:
17831785
case T_ArrayExpr:
17841786
{
1787+
QualPushdownContext tmp_context = copy_context(context);
17851788
Node *pushed_down =
1786-
expression_tree_mutator((Node *) orig_node, qual_pushdown_mutator, context);
1789+
expression_tree_mutator((Node *) orig_node, qual_pushdown_mutator, &tmp_context);
1790+
if (!tmp_context.can_pushdown || tmp_context.needs_recheck)
1791+
{
1792+
context->can_pushdown = false;
1793+
return orig_node;
1794+
}
17871795
return pushed_down;
17881796
}
17891797

tsl/test/expected/compress_qualpushdown_complex.out

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,3 +406,58 @@ blm1 = blm2 and segby in (1,2,3);
406406
-> Index Scan using _hyper_1_1_chunk_compressed_segby__ts_meta_v2_first_ts__ts__idx on _hyper_1_1_chunk_compressed
407407
Index Cond: (segby = ANY ('{1,2,3}'::integer[]))
408408

409+
-- NullTest over a minmax-pushdown column
410+
CREATE TABLE sparse_isnull(ts timestamptz NOT NULL, v int)
411+
WITH (
412+
tsdb.hypertable,
413+
tsdb.partition_column = 'ts',
414+
tsdb.compress,
415+
tsdb.orderby = 'ts, v'
416+
);
417+
INSERT INTO sparse_isnull VALUES ('2024-01-01', NULL), ('2024-01-02', 1);
418+
SELECT count(compress_chunk(c)) FROM show_chunks('sparse_isnull') c;
419+
count
420+
-------
421+
1
422+
423+
-- IS NULL is not pushed to the compressed scan
424+
explain (buffers off, costs off)
425+
SELECT * FROM sparse_isnull WHERE (v > 0) IS NULL;
426+
--- QUERY PLAN ---
427+
Custom Scan (ColumnarScan) on _hyper_2_2_chunk
428+
Filter: ((v > 0) IS NULL)
429+
-> Seq Scan on _hyper_2_2_chunk_compressed
430+
431+
SELECT * FROM sparse_isnull WHERE (v > 0) IS NULL;
432+
ts | v
433+
------------------------------+---
434+
Mon Jan 01 00:00:00 2024 PST |
435+
436+
-- IS NOT NULL is not pushed to the compressed scan
437+
explain (buffers off, costs off)
438+
SELECT * FROM sparse_isnull WHERE (v > 0) IS NOT NULL;
439+
--- QUERY PLAN ---
440+
Custom Scan (ColumnarScan) on _hyper_2_2_chunk
441+
Filter: ((v > 0) IS NOT NULL)
442+
-> Seq Scan on _hyper_2_2_chunk_compressed
443+
444+
SELECT * FROM sparse_isnull WHERE (v > 0) IS NOT NULL;
445+
ts | v
446+
------------------------------+---
447+
Tue Jan 02 00:00:00 2024 PST | 1
448+
449+
-- plain minmax range pushdown
450+
explain (buffers off, costs off)
451+
SELECT * FROM sparse_isnull WHERE v > 0;
452+
--- QUERY PLAN ---
453+
Custom Scan (ColumnarScan) on _hyper_2_2_chunk
454+
Vectorized Filter: (v > 0)
455+
-> Seq Scan on _hyper_2_2_chunk_compressed
456+
Filter: (_ts_meta_max_2 > 0)
457+
458+
SELECT * FROM sparse_isnull WHERE v > 0;
459+
ts | v
460+
------------------------------+---
461+
Tue Jan 02 00:00:00 2024 PST | 1
462+
463+
DROP TABLE sparse_isnull;

tsl/test/sql/compress_qualpushdown_complex.sql

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,3 +234,35 @@ explain (buffers off, costs off)
234234
SELECT * FROM complex_pushdown WHERE
235235
blm1 = blm2 and segby in (1,2,3);
236236

237+
-- NullTest over a minmax-pushdown column
238+
CREATE TABLE sparse_isnull(ts timestamptz NOT NULL, v int)
239+
WITH (
240+
tsdb.hypertable,
241+
tsdb.partition_column = 'ts',
242+
tsdb.compress,
243+
tsdb.orderby = 'ts, v'
244+
);
245+
246+
INSERT INTO sparse_isnull VALUES ('2024-01-01', NULL), ('2024-01-02', 1);
247+
SELECT count(compress_chunk(c)) FROM show_chunks('sparse_isnull') c;
248+
249+
-- IS NULL is not pushed to the compressed scan
250+
explain (buffers off, costs off)
251+
SELECT * FROM sparse_isnull WHERE (v > 0) IS NULL;
252+
253+
SELECT * FROM sparse_isnull WHERE (v > 0) IS NULL;
254+
255+
-- IS NOT NULL is not pushed to the compressed scan
256+
explain (buffers off, costs off)
257+
SELECT * FROM sparse_isnull WHERE (v > 0) IS NOT NULL;
258+
259+
SELECT * FROM sparse_isnull WHERE (v > 0) IS NOT NULL;
260+
261+
-- plain minmax range pushdown
262+
explain (buffers off, costs off)
263+
SELECT * FROM sparse_isnull WHERE v > 0;
264+
265+
SELECT * FROM sparse_isnull WHERE v > 0;
266+
267+
DROP TABLE sparse_isnull;
268+

0 commit comments

Comments
 (0)