I have a large table, which I want to get a count of some subset. The filter parameter is on a column that has an index on it. If I was just writing a normal query, I'd do it like so:
SELECT count(*) FROM foo WHERE ts > '2022-07-01';
However, it's possible to use attribute notation with the count
function:
SELECT foo.count FROM foo WHERE ts > '2022-07-01';
My understanding is that second version should be semantically identical to the first. However, in practice it results in a different query plan with significantly worse performance characteristics (parallel index-only scan
vs parallel seq scan
).
I'm unfortunately locked into the second version, because it's how PostgREST implements count
queries. The situation is actually worse than this toy example, because in most cases I'm selecting against a view, not a table, and in the functional ('normal') case, the query planner is smart enough to use indices, whereas in the attribute case, it looks like the planner executes the entirety of the view (e.g. approximately identical performance as doing SELECT * FROM myview WHERE ts > '2022-07-01'
) and then just counts the result set. In situations where some columns are derived from moderately expensive inline function calls, this is the difference between a query running in 2.5 seconds vs 2.5 minutes.
I'm interested in
- understanding why these two apparently-equivalent queries produce different query plans, and
- if there's a way of nudging the query planner to use the more-optimised plan for the attribute-notated form of the query.
Minimal reproduction:
CREATE TABLE bar (id PRIMARY KEY)
CREATE TABLE foo (
id bigint PRIMARY KEY,
ts timestamptz UNIQUE,
-- for whatever reason I can only get a replication if I add a foreign key.
-- Not sure if this is significant...
bar_id bigint REFERENCES bar(id)
);
INSERT INTO bar (id) SELECT generate_series(1, 1e6);
WITH t AS (
select *
from generate_series('2022-01-01 00:00'::timestamptz, '2022-12-31 23:59:59', '1 second') ts
)
INSERT INTO foo (id, ts, bar_id)
SELECT rank, ts, CASE WHEN rank <= 1e6 THEN rank ELSE NULL END
FROM (SELECT rank() over (order by ts), ts FROM t) a
;
EXPLAIN ANALYZE SELECT count(*) FROM foo WHERE ts > '2022-07-01';
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ QUERY PLAN │
├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Finalize Aggregate (cost=587875.34..587875.35 rows=1 width=8) (actual time=4789.976..4802.692 rows=1 loops=1) │
│ -> Gather (cost=587875.13..587875.34 rows=2 width=8) (actual time=4789.778..4802.645 rows=3 loops=1) │
│ Workers Planned: 2 │
│ Workers Launched: 2 │
│ -> Partial Aggregate (cost=586875.13..586875.14 rows=1 width=8) (actual time=4740.529..4740.530 rows=1 loops=3) │
│ -> Parallel Index Only Scan using foo_ts_key on foo (cost=0.56..563750.57 rows=9249822 width=0) (actual time=0.856..4503.148 rows=5212800 loops=3) │
│ Index Cond: (ts < '2022-07-01 00:00:00+00'::timestamp with time zone) │
│ Heap Fetches: 24110439 │
│ Planning Time: 0.344 ms │
│ JIT: │
│ Functions: 11 │
│ Options: Inlining true, Optimization true, Expressions true, Deforming true │
│ Timing: Generation 3.898 ms, Inlining 77.703 ms, Optimization 30.218 ms, Emission 20.267 ms, Total 132.086 ms │
│ Execution Time: 4806.155 ms │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
SELECT foo.count FROM foo WHERE ts > '2022-07-01';
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ QUERY PLAN │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Finalize Aggregate (cost=721287.60..721287.61 rows=1 width=8) (actual time=2051.126..2059.868 rows=1 loops=1) │
│ -> Gather (cost=721287.38..721287.59 rows=2 width=8) (actual time=2051.004..2059.858 rows=3 loops=1) │
│ Workers Planned: 2 │
│ Workers Launched: 2 │
│ -> Partial Aggregate (cost=720287.38..720287.39 rows=1 width=8) (actual time=2020.404..2020.405 rows=1 loops=3) │
│ -> Parallel Seq Scan on foo (cost=0.00..697162.83 rows=9249822 width=81) (actual time=49.001..1761.162 rows=5212800 loops=3) │
│ Filter: (ts < '2022-07-01 00:00:00+00'::timestamp with time zone) │
│ Rows Removed by Filter: 5299200 │
│ Planning Time: 0.403 ms │
│ JIT: │
│ Functions: 17 │
│ Options: Inlining true, Optimization true, Expressions true, Deforming true │
│ Timing: Generation 2.818 ms, Inlining 76.615 ms, Optimization 38.902 ms, Emission 30.954 ms, Total 149.289 ms │
│ Execution Time: 2061.402 ms │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Note that in this minimal reproduction the execution time is actually better for the seq scan example, but it serves to demonstrate that different query plans can be generated, and when I try on a real database, the index-based plan tends to be drastically better.
SELECT foo.count FROM foo WHERE ts > '2022-07-01';
– fixed