Counting Distinct Rows Using Recursive Cte Over Non-distinct Index
Solution 1:
Several notes:
Simple query on table day
SELECTCOUNT(DISTINCTday)
FROM days
WHEREdayBETWEEN'2010-01-01'AND'2011-01-01';While day is defined as PK, DISTINCT is just expensive noise.
Recursive CTE with correlated suquery
This is the alternative if there is no day table with unique entries. The technique pays if there are multiple to many rows per day, so that the equivalent of a loose index scan is actually faster than a simple DISTINCT on the base table:
WITHRECURSIVE cte AS (
( -- parentheses required because of LIMITSELECTdayFROM data
WHEREday>='2010-01-01'-- exclude irrelevant rows earlyORDERBY1
LIMIT 1
)
UNIONALLSELECT (SELECTdayFROM data
WHEREday> c.day
ANDday<'2011-01-01'-- see belowORDERBY1
LIMIT 1)
FROM cte c
WHEREdayISNOTNULL-- necessary because corr. subq. always returns row
)
SELECTcount(*) AS ct
FROM cte
WHEREdayISNOTNULL;Index
Only makes sense in combination with a matching index on data:
CREATE INDEX data_day_idx ON data (day);
day must be the leading column. The index you have in the question on (id, day) can be used too, but is far less efficient:
Notes
It is much cheaper to exclude irrelevant rows early. I integrated your predicate into the query.
Detailed explanation:
The case at hand is even simpler - the simplest possible actually.
Your original time frame was day BETWEEN '2010-01-01' AND '2011-01-01'. But BETWEEN .. AND ..includes upper and lower bound, so you'd get all of 2010 plus 2011-01-01. You probably want to exclude the upper bound. Use d.day < '2011-01-01' (not <=). See:
EXISTS for this special case
Since you are testing for a range of enumerable days (as opposed to a range with an infinite number of possible values), you can test this alternative with an EXISTS semi-join:
SELECTcount(*) AS ct
FROM generate_series(timestamp'2010-01-01'
, timestamp'2010-12-31'
, interval'1 day') AS d(day)
WHEREEXISTS (SELECTFROM data WHEREday= d.day::date);
Why is this form of generate_series() optimal?
The same simple index is essential again.
db<>fiddle here demonstrating both with big test table. Old sqlfiddle
Solution 2:
Try creating an index on data(day) and then running the first query:
SELECTCOUNT(DISTINCTday)
FROM data
WHEREdayBETWEEN'2010-01-01'AND'2011-01-01';
You might find the performance sufficient for your purposes.
Solution 3:
I'm not really sure why the index on data(day) is slower, that would seem the simplest option. But if that's too slow, you could try creating a materialised view of your days. Basically just:
create materialized view days asselectdayfrom data
groupbyday;
I don't believe postgres updates materialised views automatically, but at least then all the maintenance you need to do is periodically refresh it. Or perhaps create a trigger on data which refreshes the view. Bear in mind of course that refreshing this view might take some time depending on the size of the data table, you might only want to do it hourly or nightly if you can get away with it.
Alternatively if this table gets a lot of updates and you need the distinct day count to be consistent at all times, you could consider going back to your original separate days table, but reduce the maintenance overhead by creating a trigger on the data table to update it.
Post a Comment for "Counting Distinct Rows Using Recursive Cte Over Non-distinct Index"