-- WHERE clauses the partition bounds imply (DESIGN.md section 16, "Clauses -- the partition bounds imply"). -- -- A count over a partitioned table needs a lion index for every WHERE clause -- in every partition it counts - except for a clause the partition's bounds, -- its ancestors' included, imply. Every row of such a partition satisfies it, -- so it selects nothing there, needs no index, and is left out of that -- partition's count. What has to be checked: the clause is left out exactly -- where core's own implication proof says so (sub-partitions, a default -- partition, a partition that accepts NULL, a clause implied by some of the -- partitions counted and not by others), the query is still declined where -- it is not, and every answer is the ordinary plan's. \set VERBOSITY terse SET client_min_messages = warning; LOAD 'pg_lion'; SET synchronous_commit = on; SET max_parallel_workers_per_gather = 0; CREATE EXTENSION IF NOT EXISTS pg_lion; /* * lion_ip() runs one query with the pushdown on and off, checks that the two * results are the same multiset, and says whether the node ran - and, when a * partition left a clause out, which one. */ CREATE FUNCTION lion_ip(q text) RETURNS text LANGUAGE plpgsql AS $$ DECLARE ln text; pushed boolean := false; implied text := NULL; nrows bigint; ndiff bigint; BEGIN PERFORM set_config('pg_lion.enable_count_pushdown', 'on', true); FOR ln IN EXECUTE 'EXPLAIN (COSTS OFF) ' || q LOOP IF ln LIKE '%Custom Scan (LionCount)%' THEN pushed := true; END IF; IF ln LIKE '%Implied by Partition Bounds:%' THEN implied := btrim(split_part(ln, 'Implied by Partition Bounds:', 2)); END IF; END LOOP; EXECUTE format('CREATE TEMP TABLE lion_ip_on AS %s', q); PERFORM set_config('pg_lion.enable_count_pushdown', 'off', true); EXECUTE format('CREATE TEMP TABLE lion_ip_off AS %s', q); PERFORM set_config('pg_lion.enable_count_pushdown', 'on', true); EXECUTE 'SELECT count(*) FROM lion_ip_on' INTO nrows; EXECUTE 'SELECT (SELECT count(*) FROM (SELECT * FROM lion_ip_on EXCEPT ALL SELECT * FROM lion_ip_off) a)' ' + (SELECT count(*) FROM (SELECT * FROM lion_ip_off EXCEPT ALL SELECT * FROM lion_ip_on) b)' INTO ndiff; EXECUTE 'DROP TABLE lion_ip_on, lion_ip_off'; IF ndiff <> 0 THEN RETURN format('MISMATCH: %s rows differ', ndiff); END IF; RETURN format('%s, %s rows%s', CASE WHEN pushed THEN 'pushed down' ELSE 'not pushed down' END, nrows, CASE WHEN implied IS NULL THEN '' ELSE '; implied: ' || implied END); END $$; /* * The plan of one query with the pushdown on - without the "Disabled: true" * PostgreSQL 18 prints under a disabled node, so that every release prints * the same plan. */ CREATE FUNCTION lion_ipplan(q text) RETURNS SETOF text LANGUAGE plpgsql AS $$ DECLARE ln text; BEGIN PERFORM set_config('pg_lion.enable_count_pushdown', 'on', true); FOR ln IN EXECUTE 'EXPLAIN (COSTS OFF) ' || q LOOP CONTINUE WHEN ln ~ '^\s*Disabled: true$'; RETURN NEXT ln; END LOOP; END $$; -- ---- the table ---------------------------------------------------------- /* * LIST-partitioned by kind, which no lion index covers: 'a' sub-partitioned * by year on ts, 'b' alone, 'c' and 'd' together (the one partition with a * lion index on kind itself), NULL with 'n', and a default partition for the * rest. Every leaf has a lion index on (tags, ts, fk), and x is indexed on * its own. */ CREATE TABLE ip ( id int NOT NULL, kind text, tags text[], ts timestamptz, fk int, x int ) PARTITION BY LIST (kind); CREATE TABLE ip_a PARTITION OF ip FOR VALUES IN ('a') PARTITION BY RANGE (ts); CREATE TABLE ip_a_2025 PARTITION OF ip_a FOR VALUES FROM ('2025-01-01') TO ('2026-01-01'); CREATE TABLE ip_a_2026 PARTITION OF ip_a FOR VALUES FROM ('2026-01-01') TO ('2027-01-01'); CREATE TABLE ip_b PARTITION OF ip FOR VALUES IN ('b'); CREATE TABLE ip_cd PARTITION OF ip FOR VALUES IN ('c', 'd'); CREATE TABLE ip_n PARTITION OF ip FOR VALUES IN (NULL, 'n'); CREATE TABLE ip_def PARTITION OF ip DEFAULT; INSERT INTO ip SELECT i, CASE i % 8 WHEN 0 THEN 'a' WHEN 1 THEN 'a' WHEN 2 THEN 'b' WHEN 3 THEN 'c' WHEN 4 THEN 'd' WHEN 5 THEN 'n' WHEN 6 THEN NULL ELSE 'e' END, ARRAY['x' || (i % 7), 'y' || (i % 11)], timestamptz '2025-01-01' + ((i * 7) % 700) * interval '1 day', CASE WHEN i % 13 = 0 THEN NULL ELSE i % 400 END, i % 5 FROM generate_series(1, 32000) i; CREATE INDEX ip_tsf ON ip USING lion (tags, ts, fk); CREATE INDEX ip_x ON ip USING lion (x); CREATE INDEX ip_cd_kind ON ip_cd USING lion (kind); VACUUM (FREEZE, ANALYZE) ip; /* Force the node wherever it has a path, so that every answer is its own. */ SET enable_seqscan = off; SET enable_bitmapscan = off; SET pg_lion.enable_plain_scan = off; -- ---- implied by every partition counted ---------------------------------- /* * kind = 'a' leaves the two sub-partitions of ip_a, whose own bounds are on * ts: it is their parent's bound that implies it. No index on kind is needed. */ SELECT lion_ipplan($q$SELECT count(*) FROM ip WHERE kind = 'a' AND tags && '{x1}'$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'a' AND tags && '{x1}'$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'b' AND tags && '{x1}'$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'a' AND tags && '{x1}' AND ts >= '2025-06-01'$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'a' AND tags && '{x1}' AND ts < '2025-03-01'$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'a' AND tags @> '{x1,y2}' AND x = 3$q$); /* a stable expression, which only run-time pruning could use */ SELECT lion_ipplan($q$SELECT count(*) FROM ip WHERE kind = 'a' AND tags && '{x1}' AND ts >= now() - interval '100 years'$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'a' AND tags && '{x1}' AND ts >= now() - interval '100 years'$q$); /* an IN list, the partitions it leaves each implying it */ SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind IN ('a', 'b') AND tags && '{x1}'$q$); /* the null test a LIST bound without NULL carries */ SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind IS NOT NULL AND kind = 'b' AND tags && '{x2}'$q$); /* * Ranges. One on the key keeps the default partition, which could hold * values between any two of the others' (the planner does not prune it), and * which implies nothing of it: tested on that partition's rows, and left out * of ip_b's (DESIGN.md §16, "Residual clauses over a partitioned table"). * One on ts that ip_a_2026's own bound implies: a range every partition * implies does not drive the count while another clause selects the rows, * and is left out; with nothing else it still drives, and only kind is left * out. */ SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind >= 'b' AND kind <= 'b' AND tags && '{x3}'$q$); SELECT lion_ipplan($q$SELECT count(*) FROM ip WHERE kind = 'a' AND ts >= '2026-01-01' AND tags && '{x1}'$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'a' AND ts >= '2026-01-01' AND tags && '{x1}'$q$); SELECT lion_ipplan($q$SELECT count(*) FROM ip WHERE kind = 'a' AND ts >= '2026-01-01'$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'a' AND ts >= '2026-01-01'$q$); /* * A range across the sub-partitions' bound, which ip_a_2026's alone implies: * it is not implied everywhere, so it drives the count, and a range that * bounds the driving walk is never left out. */ SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'a' AND ts >= '2025-06-01' AND tags && '{x1}'$q$); /* * No posting set answers an OR one of whose arms is not a plain column, but * every partition kind = 'a' leaves implies it: it is left out, and so is * what its first arm had been taken as. Without kind = 'a' nothing prunes, * and the partitions that do not imply it test it on their rows. */ SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'a' AND (kind = 'a' OR upper(kind) = 'Z') AND tags && '{x1}'$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE (kind = 'a' OR upper(kind) = 'Z') AND tags && '{x1}'$q$); /* grouped: the partial counts of every partition, with the clause left out */ SELECT lion_ipplan($q$SELECT fk, count(*) FROM ip WHERE kind = 'b' AND tags && '{x1}' GROUP BY fk$q$); SELECT lion_ip($q$SELECT fk, count(*) FROM ip WHERE kind = 'b' AND tags && '{x1}' GROUP BY fk$q$); SELECT lion_ip($q$SELECT x, count(*) FROM ip WHERE kind = 'a' GROUP BY x$q$); SELECT lion_ip($q$SELECT x, count(*) FROM ip WHERE kind = 'a' AND ts >= '2026-03-01' GROUP BY x$q$); SELECT lion_ip($q$SELECT fk, x, count(*) FROM ip WHERE kind = 'a' AND tags && '{y3}' GROUP BY fk, x$q$); -- ---- implied by some of the partitions and not by others ---------------- /* * ip_cd holds 'd' too, so its bounds do not imply the list: it answers it from * its own index on kind, while ip_a's two leaves and ip_b leave it out. */ SELECT lion_ipplan($q$SELECT count(*) FROM ip WHERE kind IN ('a', 'b', 'c') AND tags && '{x1}'$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind IN ('a', 'b', 'c') AND tags && '{x1}'$q$); SELECT lion_ip($q$SELECT fk, count(*) FROM ip WHERE kind IN ('a', 'b', 'c') AND tags && '{x1}' GROUP BY fk$q$); /* ... and a partition that answers the clause itself is not affected */ SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'c' AND tags && '{x1}'$q$); /* * A range taken as a source (a GROUP BY drives the count): ip_a_2026's bound * implies it, ip_b's does not, and ip_b reads it from its index. */ SELECT lion_ipplan($q$SELECT fk, count(*) FROM ip WHERE kind IN ('a', 'b') AND ts >= '2026-01-01' GROUP BY fk$q$); SELECT lion_ip($q$SELECT fk, count(*) FROM ip WHERE kind IN ('a', 'b') AND ts >= '2026-01-01' GROUP BY fk$q$); SELECT lion_ip($q$SELECT x, count(*) FROM ip WHERE kind IN ('a', 'b') AND ts >= '2026-01-01' AND ts < '2026-06-01' GROUP BY x$q$); /* * an OR across columns, implied as a whole by ip_a_2026 alone; ip_a_2025's * bound refutes its first arm, and leaves it fk = 3 */ SELECT lion_ipplan($q$SELECT count(*) FROM ip WHERE kind IN ('a', 'b') AND (ts >= '2026-01-01' OR fk = 3) AND tags && '{x1}'$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind IN ('a', 'b') AND (ts >= '2026-01-01' OR fk = 3) AND tags && '{x1}'$q$); -- ---- OR arms the partition bounds refute --------------------------------- /* * A filter per kind, ORed: in a partition of one kind the arms of the others * are never true and are left out, and in the arm it keeps `kind = ...` is * implied and left out too. ip_a's leaves count tags && '{x1}' AND tags && * '{y2}', ip_b counts tags && '{x3}', and ip_cd, whose two-value bound * implies what is left of the OR (kind = 'c' OR kind = 'd'), leaves it out * whole. Without the refutation no partition but ip_cd has an index for * `kind = ...`, and the query was declined. */ SELECT lion_ipplan($q$SELECT count(*) FROM ip WHERE ((kind = 'a' AND tags && '{x1}' AND tags && '{y2}') OR (kind = 'b' AND tags && '{x3}') OR kind = 'c' OR kind = 'd') AND ts >= '2025-06-01'$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE ((kind = 'a' AND tags && '{x1}' AND tags && '{y2}') OR (kind = 'b' AND tags && '{x3}') OR kind = 'c' OR kind = 'd') AND ts >= '2025-06-01'$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE ((kind = 'a' AND tags && '{x1}' AND tags && '{y2}') OR (kind = 'b' AND tags && '{x3}') OR kind = 'c' OR kind = 'd') AND ts >= now() - interval '100 years'$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE ((kind = 'a' AND tags && '{x1}' AND tags && '{y2}') OR (kind = 'b' AND tags && '{x3}') OR kind = 'c' OR kind = 'd') AND tags && '{y5}'$q$); SELECT lion_ip($q$SELECT x, count(*) FROM ip WHERE ((kind = 'a' AND tags && '{x1}' AND tags && '{y2}') OR (kind = 'b' AND tags && '{x3}') OR kind = 'c' OR kind = 'd') AND ts >= '2025-06-01' GROUP BY x$q$); SELECT lion_ip($q$SELECT fk, x, count(*) FROM ip WHERE (kind = 'a' AND tags && '{x1}') OR (kind = 'b' AND tags && '{x3}') GROUP BY fk, x$q$); /* * An arm a sub-partition's bound refutes: ip_a_2025 keeps only the second * arm, ip_a_2026 both. The first arm's range carries no pin (§32), so where * it is kept the count needs another clause to select rows by: tags && * '{y1}', or, with ip_a_2025 the only partition left, the range on ts that * then drives the count. With neither, ip_a_2026 declines the query. */ SELECT lion_ipplan($q$SELECT count(*) FROM ip WHERE kind = 'a' AND tags && '{y1}' AND ((ts >= '2026-03-01' AND tags && '{x1}') OR x = 2)$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'a' AND tags && '{y1}' AND ((ts >= '2026-03-01' AND tags && '{x1}') OR x = 2)$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'a' AND ts < '2026-01-01' AND ((ts >= '2026-03-01' AND tags && '{x1}') OR x = 2)$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'a' AND ((ts >= '2026-03-01' AND tags && '{x1}') OR x = 2)$q$); /* * The two-value bound of ip_cd refutes the arm on 'a' and implies neither * `kind = 'c'` nor `kind = 'd'`: it answers them from its own index on kind. */ SELECT lion_ipplan($q$SELECT count(*) FROM ip WHERE (kind = 'c' AND tags && '{x1}') OR (kind = 'a' AND tags && '{x2}')$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE (kind = 'c' AND tags && '{x1}') OR (kind = 'a' AND tags && '{x2}')$q$); SELECT lion_ip($q$SELECT fk, count(*) FROM ip WHERE (kind = 'c' AND tags && '{x1}') OR (kind = 'd' AND x = 1) OR (kind = 'b' AND tags && '{x2}') GROUP BY fk$q$); /* * The default partition refutes the arm on 'a', but not `kind = 'e'`, which * it has no index for: declined, as ip_n is for the arm `kind IS NULL`. */ SELECT lion_ip($q$SELECT count(*) FROM ip WHERE (kind = 'e' AND tags && '{x1}') OR (kind = 'a' AND tags && '{x2}')$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE (kind IS NULL AND tags && '{x1}') OR (kind = 'b' AND tags && '{x2}')$q$); /* * Without plan-time pruning every partition is counted, and one whose bounds * refute every arm has nothing to count: it is not counted at all. */ SET enable_partition_pruning = off; SELECT lion_ipplan($q$SELECT count(*) FROM ip WHERE (kind = 'a' AND tags && '{x1}') OR (kind = 'b' AND tags && '{x3}')$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE (kind = 'a' AND tags && '{x1}') OR (kind = 'b' AND tags && '{x3}')$q$); RESET enable_partition_pruning; -- ---- the value a stable expression has while the plan is made ------------ /* * An IN list over a stable expression is priced over the array the * expression has at plan time, which a cached plan does not keep: it runs * under every value the expression takes later, and the executor evaluates * it each time. So no partition's bounds refute an arm, or imply a leaf of * one, on the strength of that estimate - in a generic plan made under * lion_test.ks = '1' the arm k = ANY (...) is neither refuted in ipk_2 nor * implied in ipk_1, and each partition answers it from its own index on k. * lion_ipk() runs both prepared statements under another value and compares * them with the query planned afresh with the pushdown off. */ CREATE TABLE ipk (id int, k int, b int) PARTITION BY LIST (k); CREATE TABLE ipk_1 PARTITION OF ipk FOR VALUES IN (1); CREATE TABLE ipk_2 PARTITION OF ipk FOR VALUES IN (2); INSERT INTO ipk SELECT i, 1 + i % 2, (i / 2) % 5 + 5 * (i % 2) FROM generate_series(1, 4000) i; CREATE INDEX ipk_k ON ipk USING lion (k); CREATE INDEX ipk_b ON ipk USING lion (b); VACUUM (FREEZE, ANALYZE) ipk; CREATE FUNCTION lion_ipk(ks text) RETURNS text LANGUAGE plpgsql AS $$ DECLARE arr text := $a$string_to_array(current_setting('lion_test.ks'), ',')::int[]$a$; node_or bigint; node_arm bigint; plain_or bigint; plain_arm bigint; BEGIN PERFORM set_config('lion_test.ks', ks, false); EXECUTE 'EXECUTE ipk_or' INTO node_or; EXECUTE 'EXECUTE ipk_arm' INTO node_arm; PERFORM set_config('pg_lion.enable_count_pushdown', 'off', true); EXECUTE format('SELECT count(*) FROM ipk WHERE k = ANY (%s) OR b = 5', arr) INTO plain_or; EXECUTE format('SELECT count(*) FROM ipk WHERE (k = ANY (%s) AND b = 4) OR b = 5', arr) INTO plain_arm; PERFORM set_config('pg_lion.enable_count_pushdown', 'on', true); RETURN format('ks %L: %s, %s (the ordinary plan %s, %s)%s', ks, node_or, node_arm, plain_or, plain_arm, CASE WHEN node_or <> plain_or OR node_arm <> plain_arm THEN ' MISMATCH' ELSE '' END); END $$; SET lion_test.ks = '1'; SET plan_cache_mode = force_generic_plan; PREPARE ipk_or AS SELECT count(*) FROM ipk WHERE k = ANY (string_to_array(current_setting('lion_test.ks'), ',')::int[]) OR b = 5; PREPARE ipk_arm AS SELECT count(*) FROM ipk WHERE (k = ANY (string_to_array(current_setting('lion_test.ks'), ',')::int[]) AND b = 4) OR b = 5; SELECT lion_ipplan('EXECUTE ipk_or'); SELECT lion_ipplan('EXECUTE ipk_arm'); SELECT lion_ipk('1'); SELECT lion_ipk('2'); SELECT lion_ipk('1,2'); SELECT lion_ipk(''); RESET plan_cache_mode; DEALLOCATE ipk_or; DEALLOCATE ipk_arm; RESET lion_test.ks; DROP FUNCTION lion_ipk(text); DROP TABLE ipk; -- ---- NULLs and the default partition ------------------------------------- /* * ip_n holds NULL as well as 'n', so neither `kind = 'n'` nor `kind IS NULL` * is implied there and, with no index on kind, it tests the clause on its * rows; the OR of the two is implied, and is left out. */ SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'n' AND tags && '{x1}'$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind IS NULL AND tags && '{x1}'$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE (kind IS NULL OR kind = 'n') AND tags && '{x1}'$q$); /* * The default partition's bound is the negation of every other one's, and * implies no equality: 'e' could share it with anything no partition lists, * and is tested on its rows. It does imply the NOT IN of every listed * value, which is left out whole. `kind IS NOT NULL` is implied by every * partition but ip_n, which holds the NULLs (the default partition would, if * no other partition took them) and has no index on kind: tested there, and * left out everywhere else. */ SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'e' AND tags && '{x1}'$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind NOT IN ('a', 'b', 'c', 'd', 'n') AND tags && '{x1}'$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind IS NOT NULL AND tags && '{x1}'$q$); -- ---- counted whole -------------------------------------------------------- /* * Nothing left in the partitions to select rows by: a clause no partition * indexes and every one implies is left out before anything else, and each * partition is counted whole, by the entries of one of its columns, as a * table without a WHERE is (DESIGN.md §35, "Every row"). */ SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'a'$q$); SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'a' AND fk IS NOT NULL$q$); -- ---- still declined ------------------------------------------------------ /* a pinned column the target list prints comes out of the index's key */ SELECT lion_ip($q$SELECT kind, count(*) FROM ip WHERE kind = 'b' AND tags && '{x1}' GROUP BY kind$q$); /* a volatile clause is the query's to run per row */ SELECT lion_ip($q$SELECT count(*) FROM ip WHERE (kind = 'a' OR random() < 0) AND tags && '{x1}'$q$); /* a parameter proves nothing at plan time: a generic plan declines */ PREPARE ip_kind(text) AS SELECT count(*) FROM ip WHERE kind = $1 AND tags && '{x1}'; SET plan_cache_mode = force_generic_plan; SELECT lion_ipplan('EXECUTE ip_kind(''a'')'); EXECUTE ip_kind('a'); SET plan_cache_mode = force_custom_plan; SELECT lion_ipplan('EXECUTE ip_kind(''a'')'); EXECUTE ip_kind('a'); RESET plan_cache_mode; DEALLOCATE ip_kind; SET pg_lion.enable_count_pushdown = off; SELECT count(*) FROM ip WHERE kind = 'a' AND tags && '{x1}'; RESET pg_lion.enable_count_pushdown; -- ---- a dirty heap --------------------------------------------------------- DELETE FROM ip WHERE kind = 'a' AND id % 5 = 0; UPDATE ip SET tags = ARRAY['x1'] WHERE kind = 'b' AND id % 9 = 0; SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'a' AND tags && '{x1}'$q$); SELECT lion_ip($q$SELECT fk, count(*) FROM ip WHERE kind IN ('a', 'b', 'c') AND tags && '{x1}' GROUP BY fk$q$); SELECT lion_ip($q$SELECT x, count(*) FROM ip WHERE ((kind = 'a' AND tags && '{x1}' AND tags && '{y2}') OR (kind = 'b' AND tags && '{x3}') OR kind = 'c' OR kind = 'd') AND ts >= '2025-06-01' GROUP BY x$q$); VACUUM (FREEZE) ip; SELECT lion_ip($q$SELECT count(*) FROM ip WHERE kind = 'a' AND tags && '{x1}'$q$); SELECT lion_ip($q$SELECT fk, count(*) FROM ip WHERE kind IN ('a', 'b', 'c') AND tags && '{x1}' GROUP BY fk$q$); SELECT lion_ip($q$SELECT x, count(*) FROM ip WHERE ((kind = 'a' AND tags && '{x1}' AND tags && '{y2}') OR (kind = 'b' AND tags && '{x3}') OR kind = 'c' OR kind = 'd') AND ts >= '2025-06-01' GROUP BY x$q$); RESET enable_seqscan; RESET enable_bitmapscan; RESET pg_lion.enable_plain_scan; DROP TABLE ip; DROP FUNCTION lion_ip(text); DROP FUNCTION lion_ipplan(text);