Files
greptimedb/tests/cases/standalone/common/aggregate/having_input_bounds.sql
T
Lei, HUANG af396e60a3 perf(query): infer safe aggregate input bounds from HAVING (#9582)
* perf(query): infer safe aggregate input bounds from HAVING

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* test(query): refresh explain snapshots for aggregate bound inference

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* fix(query): avoid reinferring bounds across MergeScan

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* fix(query): validate aggregate bound column provenance

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* test(query): retain zero-match groups for nonpositive HAVING thresholds

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* test(query): assert inferred bounds in remote scan plans

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

---------

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>
2026-10-15 04:46:37 +00:00

127 lines
4.9 KiB
SQL

-- Matching spans and the earliest/latest spans deliberately live in different regions.
CREATE TABLE having_input_bounds (
trace_id STRING,
service_name STRING,
span_name STRING,
product STRING,
duration BIGINT,
ts TIMESTAMP(9) TIME INDEX,
PRIMARY KEY (service_name)
)
PARTITION ON COLUMNS (service_name) (
service_name < 'm',
service_name >= 'm'
)
WITH ('append_mode' = 'true');
INSERT INTO having_input_bounds VALUES
('a', 'other', 'parent', NULL, 30, 5),
('a', 'cart', 'GetCart', NULL, 6, 10),
('a', 'frontend', 'GetProduct', 'product-7', 8, 15),
('a', 'cart', 'GetCart', NULL, 6, 20),
('a', 'other', 'parent', NULL, 30, 25),
('b', 'other', 'parent', NULL, 1, 2),
('b', 'frontend', 'GetProduct', 'product-x', 18, 12),
('b', 'other', 'parent', NULL, 30, 22),
('c', 'cart', 'GetCart', NULL, 5, 30),
('d', 'frontend', NULL, 'product-7', 8, 15),
(NULL, 'other', 'parent', NULL, 2, 3),
(NULL, 'cart', 'GetCart', NULL, 6, 11),
(NULL, 'other', 'parent', NULL, 30, 25);
-- T02 shape: only the upper bound may be inferred; preserve earlier spans and NULL keys.
SELECT trace_id, MIN(CAST(ts AS BIGINT)) AS trace_start
FROM having_input_bounds
GROUP BY trace_id
HAVING SUM(CASE WHEN service_name = 'cart' AND span_name = 'GetCart'
AND CAST(ts AS BIGINT) >= 10 AND CAST(ts AS BIGINT) < 20 THEN 1 ELSE 0 END) > 0
ORDER BY trace_start, trace_id
LIMIT 20;
-- Pin the optimization itself: the remote scan has only the inferred time upper
-- bound, while HAVING stays above aggregation. Result equality alone cannot prove this.
-- SQLNESS REPLACE (-+) -
-- SQLNESS REPLACE (\s\s+) _
-- SQLNESS REPLACE (RepartitionExec:.*) RepartitionExec: REDACTED
-- SQLNESS REPLACE (MergeScanExec:.*) MergeScanExec: REDACTED
EXPLAIN SELECT trace_id, MIN(CAST(ts AS BIGINT)) AS trace_start
FROM having_input_bounds
GROUP BY trace_id
HAVING SUM(CASE WHEN service_name = 'cart' AND span_name = 'GetCart'
AND CAST(ts AS BIGINT) >= 10 AND CAST(ts AS BIGINT) < 20 THEN 1 ELSE 0 END) > 0
ORDER BY trace_start, trace_id
LIMIT 20;
-- T03 shape: a span attribute is part of the existence condition, not an input filter.
SELECT trace_id, MIN(CAST(ts AS BIGINT)) AS trace_start
FROM having_input_bounds
GROUP BY trace_id
HAVING SUM(CASE WHEN service_name = 'frontend' AND span_name = 'GetProduct'
AND product = 'product-7' AND CAST(ts AS BIGINT) >= 10
AND CAST(ts AS BIGINT) < 20 THEN 1 ELSE 0 END) > 0
ORDER BY trace_start, trace_id;
-- T04 shape: bounds on a different expression must not become aggregate input bounds.
SELECT trace_id, MIN(CAST(ts AS BIGINT)) AS trace_start
FROM having_input_bounds
GROUP BY trace_id
HAVING SUM(CASE WHEN service_name = 'frontend' AND span_name = 'GetProduct'
AND duration >= 5 AND duration < 20 AND CAST(ts AS BIGINT) >= 10
AND CAST(ts AS BIGINT) < 20 THEN 1 ELSE 0 END) > 0
ORDER BY trace_start, trace_id;
-- MAX is symmetric: preserve later spans by inferring only the lower bound.
-- Also check that the conditional SUM itself remains unchanged.
SELECT trace_id, MAX(CAST(ts AS BIGINT)) AS trace_end,
SUM(CASE WHEN service_name = 'cart' AND CAST(ts AS BIGINT) >= 10
AND CAST(ts AS BIGINT) < 20 THEN 1 ELSE 0 END) AS matches
FROM having_input_bounds
GROUP BY trace_id
HAVING matches >= 1
ORDER BY trace_end, trace_id;
-- Full-group aggregates forbid pruning, even when the MIN would be preserved.
SELECT trace_id, MIN(CAST(ts AS BIGINT)) AS trace_start,
MAX(CAST(ts AS BIGINT)) AS trace_end, COUNT(*) AS spans
FROM having_input_bounds
GROUP BY trace_id
HAVING SUM(CASE WHEN service_name = 'cart' AND CAST(ts AS BIGINT) >= 10
AND CAST(ts AS BIGINT) < 20 THEN 1 ELSE 0 END) > 0
ORDER BY trace_start, trace_id;
-- OR does not guarantee a matching span in every surviving group.
SELECT trace_id, MIN(CAST(ts AS BIGINT)) AS trace_start
FROM having_input_bounds
GROUP BY trace_id
HAVING SUM(CASE WHEN service_name = 'cart' AND CAST(ts AS BIGINT) < 20 THEN 1 ELSE 0 END) > 0
OR MIN(CAST(ts AS BIGINT)) > 20
ORDER BY trace_start, trace_id;
DROP TABLE having_input_bounds;
-- A derived column must not move fallible arithmetic/casts ahead of its WHERE guard.
CREATE TABLE having_guarded(k STRING, v BIGINT, d BIGINT, text_value STRING, ts TIMESTAMP TIME INDEX);
INSERT INTO having_guarded VALUES ('a', 12, 0, 'bad', 1), ('a', 5, 1, '5', 2), ('b', 25, 1, '25', 3);
SELECT k, MIN(r) FROM (
SELECT k, v / d AS r FROM having_guarded WHERE abs(d) > 0
) s GROUP BY k
HAVING SUM(CASE WHEN r < 20 THEN 1 ELSE 0 END) > 0
ORDER BY k;
SELECT k, MIN(r) FROM (
SELECT k, CAST(text_value AS BIGINT) AS r FROM having_guarded WHERE abs(d) > 0
) s GROUP BY k
HAVING SUM(CASE WHEN r < 20 THEN 1 ELSE 0 END) > 0
ORDER BY k;
CREATE VIEW having_guarded_view AS
SELECT k, v / d AS r FROM having_guarded WHERE abs(d) > 0;
SELECT k, MIN(r) FROM having_guarded_view GROUP BY k
HAVING SUM(CASE WHEN r < 20 THEN 1 ELSE 0 END) > 0
ORDER BY k;
DROP VIEW having_guarded_view;
DROP TABLE having_guarded;