|
| 1 | +-- This file and its contents are licensed under the Timescale License. |
| 2 | +-- Please see the included NOTICE for copyright information and |
| 3 | +-- LICENSE-TIMESCALE for a copy of the license. |
| 4 | +-- Regression test for #10071. |
| 5 | +-- |
| 6 | +-- A real-time continuous aggregate (timescaledb.materialized_only = false) that |
| 7 | +-- is three or more levels deep in a hierarchy (a cagg on a cagg on a cagg) must |
| 8 | +-- prune its real-time branch when refreshed for a window entirely below the |
| 9 | +-- watermark, exactly as a two-level cagg does. A refresh reads the cagg's |
| 10 | +-- internal partial view, so EXPLAINing a below-watermark SELECT on that partial |
| 11 | +-- view reproduces the refresh scan. Before the fix the real-time branch was |
| 12 | +-- pruned only up to two levels; at three or more it re-scanned the whole |
| 13 | +-- un-materialized tail of the base hypertable and discarded every row. |
| 14 | +SET timezone TO PST8PDT; |
| 15 | +SET max_parallel_workers_per_gather TO 0; |
| 16 | +SET client_min_messages TO WARNING; |
| 17 | +CREATE TABLE conditions (ts timestamptz NOT NULL, value double precision NOT NULL); |
| 18 | +SELECT create_hypertable('conditions', by_range('ts', INTERVAL '7 days')); |
| 19 | + create_hypertable |
| 20 | +------------------- |
| 21 | + (1,t) |
| 22 | + |
| 23 | +INSERT INTO conditions |
| 24 | +SELECT ts, 1.0 |
| 25 | +FROM generate_series(TIMESTAMPTZ '2026-01-01', TIMESTAMPTZ '2026-04-29', INTERVAL '1 hour') AS ts; |
| 26 | +-- Level 1: 6-hour buckets on the raw hypertable |
| 27 | +CREATE MATERIALIZED VIEW cagg_l1 |
| 28 | + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS |
| 29 | +SELECT time_bucket('6 hours', ts) AS ts, count(*) AS cnt, sum(value) AS sum_value |
| 30 | +FROM conditions GROUP BY 1 WITH NO DATA; |
| 31 | +-- Level 2: 1-day buckets on cagg_l1 (raw -> 6h -> 1day) |
| 32 | +CREATE MATERIALIZED VIEW cagg_l2 |
| 33 | + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS |
| 34 | +SELECT time_bucket('1 day', ts) AS ts, sum(cnt) AS cnt, sum(sum_value) AS sum_value |
| 35 | +FROM cagg_l1 GROUP BY 1 WITH NO DATA; |
| 36 | +-- Level 3: 7-day buckets on cagg_l2 (raw -> 6h -> 1day -> 7day) |
| 37 | +CREATE MATERIALIZED VIEW cagg_l3 |
| 38 | + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS |
| 39 | +SELECT time_bucket('7 days', ts) AS ts, sum(cnt) AS cnt, sum(sum_value) AS sum_value |
| 40 | +FROM cagg_l2 GROUP BY 1 WITH NO DATA; |
| 41 | +-- Level 4: 28-day buckets on cagg_l3 (raw -> 6h -> 1day -> 7day -> 28day) |
| 42 | +CREATE MATERIALIZED VIEW cagg_l4 |
| 43 | + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS |
| 44 | +SELECT time_bucket('28 days', ts) AS ts, sum(cnt) AS cnt, sum(sum_value) AS sum_value |
| 45 | +FROM cagg_l3 GROUP BY 1 WITH NO DATA; |
| 46 | +-- Materialize everything up to 2026-04-01, leaving an un-materialized tail. |
| 47 | +CALL refresh_continuous_aggregate('cagg_l1', NULL, TIMESTAMPTZ '2026-04-01'); |
| 48 | +CALL refresh_continuous_aggregate('cagg_l2', NULL, TIMESTAMPTZ '2026-04-01'); |
| 49 | +CALL refresh_continuous_aggregate('cagg_l3', NULL, TIMESTAMPTZ '2026-04-01'); |
| 50 | +CALL refresh_continuous_aggregate('cagg_l4', NULL, TIMESTAMPTZ '2026-04-01'); |
| 51 | +-- A refresh reads the cagg's internal partial view, so EXPLAINing a |
| 52 | +-- below-watermark SELECT on that partial view reproduces the refresh scan. Look |
| 53 | +-- up each partial view name. |
| 54 | +SELECT format('%I.%I', partial_view_schema, partial_view_name) AS pv_l2 |
| 55 | +FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'cagg_l2' \gset |
| 56 | +SELECT format('%I.%I', partial_view_schema, partial_view_name) AS pv_l3 |
| 57 | +FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'cagg_l3' \gset |
| 58 | +SELECT format('%I.%I', partial_view_schema, partial_view_name) AS pv_l4 |
| 59 | +FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'cagg_l4' \gset |
| 60 | +\set PREFIX 'EXPLAIN (COSTS OFF, TIMING OFF, SUMMARY OFF)' |
| 61 | +-- For a window entirely below the watermark the real-time branch must be pruned |
| 62 | +-- at every depth: the plan should fold it to "One-Time Filter: false" and touch |
| 63 | +-- only the materialized hypertable, never the base-hypertable (_hyper_1_*) |
| 64 | +-- chunks. |
| 65 | +-- 2 levels (control: pruned before and after the fix) |
| 66 | +:PREFIX SELECT * FROM :pv_l2 WHERE ts >= TIMESTAMPTZ '2026-01-13' AND ts < TIMESTAMPTZ '2026-02-10'; |
| 67 | +--- QUERY PLAN --- |
| 68 | + GroupAggregate |
| 69 | + Group Key: (time_bucket('@ 1 day'::interval, _hyper_2_19_chunk.ts)) |
| 70 | + -> Sort |
| 71 | + Sort Key: (time_bucket('@ 1 day'::interval, _hyper_2_19_chunk.ts)) |
| 72 | + -> Result |
| 73 | + -> Append |
| 74 | + -> Index Scan using _hyper_2_19_chunk__materialized_hypertable_2_ts_idx on _hyper_2_19_chunk |
| 75 | + Index Cond: ((ts < 'Tue Mar 31 23:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Wed Feb 11 00:00:00 2026 PST'::timestamp with time zone)) |
| 76 | + Filter: ((time_bucket('@ 1 day'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 1 day'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) |
| 77 | + -> HashAggregate |
| 78 | + Group Key: time_bucket('@ 6 hours'::interval, ts) |
| 79 | + -> Result |
| 80 | + One-Time Filter: false |
| 81 | + |
| 82 | +-- 3 levels (the bug: real-time branch must be pruned) |
| 83 | +:PREFIX SELECT * FROM :pv_l3 WHERE ts >= TIMESTAMPTZ '2026-01-13' AND ts < TIMESTAMPTZ '2026-02-10'; |
| 84 | +--- QUERY PLAN --- |
| 85 | + GroupAggregate |
| 86 | + Group Key: (time_bucket('@ 7 days'::interval, _hyper_3_21_chunk.ts)) |
| 87 | + -> Sort |
| 88 | + Sort Key: (time_bucket('@ 7 days'::interval, _hyper_3_21_chunk.ts)) |
| 89 | + -> Result |
| 90 | + -> Append |
| 91 | + -> Index Scan using _hyper_3_21_chunk__materialized_hypertable_3_ts_idx on _hyper_3_21_chunk |
| 92 | + Index Cond: ((ts < 'Tue Mar 31 17:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Tue Feb 17 00:00:00 2026 PST'::timestamp with time zone)) |
| 93 | + Filter: ((time_bucket('@ 7 days'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 7 days'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) |
| 94 | + -> HashAggregate |
| 95 | + Group Key: time_bucket('@ 1 day'::interval, (time_bucket('@ 6 hours'::interval, ts))) |
| 96 | + -> Result |
| 97 | + -> HashAggregate |
| 98 | + Group Key: time_bucket('@ 6 hours'::interval, ts) |
| 99 | + -> Result |
| 100 | + One-Time Filter: false |
| 101 | + |
| 102 | +-- 4 levels |
| 103 | +:PREFIX SELECT * FROM :pv_l4 WHERE ts >= TIMESTAMPTZ '2026-01-13' AND ts < TIMESTAMPTZ '2026-02-10'; |
| 104 | +--- QUERY PLAN --- |
| 105 | + GroupAggregate |
| 106 | + Group Key: (time_bucket('@ 28 days'::interval, _materialized_hypertable_4.ts)) |
| 107 | + -> Sort |
| 108 | + Sort Key: (time_bucket('@ 28 days'::interval, _materialized_hypertable_4.ts)) |
| 109 | + -> Result |
| 110 | + -> Append |
| 111 | + -> Append |
| 112 | + -> Index Scan using _hyper_4_22_chunk__materialized_hypertable_4_ts_idx on _hyper_4_22_chunk |
| 113 | + Index Cond: ((ts < 'Sun Mar 29 17:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Tue Mar 10 01:00:00 2026 PDT'::timestamp with time zone)) |
| 114 | + Filter: ((time_bucket('@ 28 days'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 28 days'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) |
| 115 | + -> Index Scan using _hyper_4_23_chunk__materialized_hypertable_4_ts_idx on _hyper_4_23_chunk |
| 116 | + Index Cond: ((ts < 'Sun Mar 29 17:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Tue Mar 10 01:00:00 2026 PDT'::timestamp with time zone)) |
| 117 | + Filter: ((time_bucket('@ 28 days'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 28 days'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) |
| 118 | + -> HashAggregate |
| 119 | + Group Key: time_bucket('@ 7 days'::interval, (time_bucket('@ 1 day'::interval, (time_bucket('@ 6 hours'::interval, ts))))) |
| 120 | + -> Result |
| 121 | + -> HashAggregate |
| 122 | + Group Key: time_bucket('@ 1 day'::interval, (time_bucket('@ 6 hours'::interval, ts))) |
| 123 | + -> Result |
| 124 | + -> HashAggregate |
| 125 | + Group Key: time_bucket('@ 6 hours'::interval, ts) |
| 126 | + -> Result |
| 127 | + One-Time Filter: false |
| 128 | + |
| 129 | +DROP TABLE conditions CASCADE; |
| 130 | +RESET client_min_messages; |
0 commit comments