I'm experiencing a strange variation of filter performance depending on the value in WHERE
condition with this query:
SELECT MIN(revenue) AS min_revenue,
MAX(revenue) AS max_revenue
FROM v_premises_filter
WHERE area IS NOT NULL
AND v_premises_filter.scat_group_code =1
Query with =1
condition executes much faster than =23
.
I have noticed that the drastic slowdown happens when aggregated column (revenue
in the example below) has only NULL values for rows matching the WHERE
condition.
I would understand if this was happening when using index lookup, but it happens when using filter - which to my knowledge should scan all rows.
With scat_group_code =1
, the query executes quickly:
Result (cost=2.92..2.93 rows=1 width=0) (actual time=0.239..0.241 rows=1 loops=1)
InitPlan 1 (returns $0)
-> Limit (cost=0.43..1.46 rows=1 width=8) (actual time=0.134..0.135 rows=1 loops=1)
-> Index Scan using v_premises_filter_revenue_idx on v_premises_filter (cost=0.43..310356.33 rows=301212 width=8) (actual time=0.130..0.130 rows=1 loops=1)
Index Cond: (revenue IS NOT NULL)
Filter: ((area IS NOT NULL) AND (scat_group_code = 1))
Rows Removed by Filter: 20
InitPlan 2 (returns $1)
-> Limit (cost=0.43..1.46 rows=1 width=8) (actual time=0.094..0.096 rows=1 loops=1)
-> Index Scan Backward using v_premises_filter_revenue_idx on v_premises_filter v_premises_filter_1 (cost=0.43..310356.33 rows=301212 width=8) (actual time=0.091..0.091 rows=1 loops=1)
Index Cond: (revenue IS NOT NULL)
Filter: ((area IS NOT NULL) AND (scat_group_code = 1))
Rows Removed by Filter: 17
Planning time: 0.359 ms
Execution time: 0.276 ms
However, with scat_group_code =23
, the same query with the same execution plan takes much more time:
Result (cost=129.15..129.16 rows=1 width=0) (actual time=8138.644..8138.669 rows=1 loops=1)
InitPlan 1 (returns $0)
-> Limit (cost=0.43..64.58 rows=1 width=8) (actual time=4094.036..4094.036 rows=0 loops=1)
-> Index Scan using v_premises_filter_revenue_idx on v_premises_filter (cost=0.43..310356.33 rows=4838 width=8) (actual time=4094.032..4094.032 rows=0 loops=1)
Index Cond: (revenue IS NOT NULL)
Filter: ((area IS NOT NULL) AND (scat_group_code = 23))
Rows Removed by Filter: 1631527
InitPlan 2 (returns $1)
-> Limit (cost=0.43..64.58 rows=1 width=8) (actual time=4044.569..4044.569 rows=0 loops=1)
-> Index Scan Backward using v_premises_filter_revenue_idx on v_premises_filter v_premises_filter_1 (cost=0.43..310356.33 rows=4838 width=8) (actual time=4043.757..4043.757 rows=0 loops=1)
Index Cond: (revenue IS NOT NULL)
Filter: ((area IS NOT NULL) AND (scat_group_code = 23))
Rows Removed by Filter: 1631527
Planning time: 0.260 ms
Execution time: 8138.770 ms
What would be the best way to optimise this query (aside from getting more memory to persuade query planner to use indexes instead of filters)?
Additional information
Here is the view structure from \d+
command:
Materialized view "public.v_premises_filter"
Column | Type | Modifiers | Storage | Stats target | Description
-----------------+------------------+-----------+----------+--------------+-------------
uarn | bigint | | plain | |
lat | double precision | | plain | |
long | double precision | | plain | |
la_code | text | | extended | |
la_desc | text | | extended | |
msoa_code | text | | extended | |
msoa_desc | text | | extended | |
lsoa_code | text | | extended | |
lsoa_desc | text | | extended | |
postcode | text | | extended | |
street | text | | extended | |
empty | boolean | | plain | |
rateable_value | bigint | | plain | |
area | numeric | | main | |
employees | numeric | | main | |
employee_cost | numeric | | main | |
break_even | numeric | | main | |
margin | double precision | | plain | |
revenue | double precision | | plain | |
ba_ref | text | | extended | |
scat_code | smallint | | plain | |
scat_group_code | smallint | | plain | |
sic_code | character(1)[] | | extended | |
Indexes:
"v_premises_filter_area_idx" btree (area)
"v_premises_filter_ba_ref_idx" btree (ba_ref)
"v_premises_filter_break_even_idx" btree (break_even)
"v_premises_filter_employee_cost_idx" btree (employee_cost)
"v_premises_filter_employees_idx" btree (employees)
"v_premises_filter_empty_idx" btree (empty)
"v_premises_filter_la_code_la_desc_idx" btree (la_code, la_desc)
"v_premises_filter_lat_long_idx" btree (lat, long)
"v_premises_filter_lsoa_code_lsoa_desc_idx" btree (lsoa_code, lsoa_desc)
"v_premises_filter_margin_idx" btree (margin)
"v_premises_filter_msoa_code_msoa_desc_idx" btree (msoa_code, msoa_desc)
"v_premises_filter_postcode_idx" btree (postcode text_pattern_ops)
"v_premises_filter_postcode_street_idx" btree (postcode, street)
"v_premises_filter_rateable_value_idx" btree (rateable_value)
"v_premises_filter_revenue_idx" btree (revenue)
"v_premises_filter_scat_code_idx" btree (scat_code)
"v_premises_filter_scat_group_code_idx" btree (scat_group_code)
"v_premises_filter_scat_group_code_idx1" btree (scat_group_code) WHERE area IS NOT NULL
"v_premises_filter_sic_code_idx" gin (sic_code)
"v_premises_filter_uarn_idx" btree (uarn)
Regarding cardinality, I'm confident that there is nothing obvious happening like fan-trap in the view creation because we do consistency checks on a few column totals and the row count is matching number of rows in master source table of the JOIN
.
The count of filtered rows differs so much because of genuine difference in amount of NULL values in the groups:
=> SELECT scat_group_code, COUNT(*), COUNT(area), COUNT(revenue)
-> FROM v_premises_filter
-> WHERE scat_group_code IN (1,23)
-> GROUP BY scat_group_code;
scat_group_code | count | count | count
-----------------+--------+--------+--------
23 | 6513 | 6513 | 0
1 | 359813 | 359813 | 359813
(2 rows)
=> SELECT COUNT(*), COUNT(revenue) FROM v_premises_filter;
count | count
---------+---------
1909175 | 1631527
(1 row)
I also noticed that a makeshift MAX()
function works much better than the MAX function itself:
=> EXPLAIN ANALYZE SELECT revenue FROM v_premises_filter
WHERE area IS NOT NULL AND scat_group_code = 23
ORDER BY revenue DESC LIMIT 1;
Limit (cost=0.43..55.28 rows=1 width=8) (actual time=0.222..0.223 rows=1 loops=1)
-> Index Scan Backward using v_premises_filter_revenue_idx on v_premises_filter (cost=0.43..314199.84 rows=5728 width=8) (actual time=0.218..0.218 rows=1 loops=1)
Filter: ((area IS NOT NULL) AND (scat_group_code = 23))
Rows Removed by Filter: 117
Planning time: 0.161 ms
Execution time: 0.245 ms
(6 rows)
However, it appears to run slower for scat_group_code
values like 1
, that match bigger count of rows, so not an ideal solution in my use case. Besides, at this point I'm very interested to find out why MAX()
/MIN()
do not run fast enough.
I use PostgreSQL 9.5.6.