We have a table that may contain 1M-5M or more rows. It has an assortment of string, numeric and date columns.
The UI presents a table view, with "infinite scroll" paging, and various filters and sort options on many of the columns.
In order to support consistent result ordering for the paging functionality, the UI always adds a "unique ID" column as the last sort (or the default sort if the user doesn't select a sort).
We are struggling to define the indexes so that the various filtering and sorting combinations perform well in UI terms. Sometimes queries take 90s or more. We need sub-second or worst case 2-3s queries.
An example query might look like:
select _columns_
where numeric_col >= 80
order by other_col desc, unique_id_col desc
limit 100 offset 0
This will perform poorly, with EXPLAIN saying something like
-> Index Scan Backward using idx_on_other_col on my_table (cost=0.43..676590.40 rows=575273 width=33)
Filter: (numeric_col >= 80)
If, however, the sorting is the same as the filtering, the query behaves much better, with the EXPLAIN showing Index Cond in place of the Filter. I am not sure if that is what explains [sic] the difference in performance.
I also don't know how much the "range" (>=) filter affects performance vs. equality filters.
But if we need to support filters (range and equality) for maybe 10-15 columns (one or more filters), and a single sort on either just the unique ID (for results order consistency) or on 1 of 6 or so fields plus the unique ID, what can we do in terms of indexes to make these queries faster? We can also consider adding more terms to the range indexes (even though naturally they usually have only a single bound, it might not be hard to add the other side of the range in some cases, if this would help).
EDIT based on comments
Here is full EXPLAIN output:
clustering.a1ab960a-7596-4519-9a56-52723ac35500=> explain (analyze, buffers) select * from "appsessions" where whole_risk > 80 order by app_session_id desc limit 100;
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.43..192.46 rows=100 width=1557) (actual time=21376.632..21376.633 rows=0 loops=1)
Buffers: shared hit=1324199 read=422192
I/O Timings: read=18567.241
-> Index Scan Backward using pk_appsessions on appsessions (cost=0.43..1104707.17 rows=575273 width=1557) (actual time=21376.630..21376.631 rows=0 loops=1)
Filter: (whole_risk > 80)
Rows Removed by Filter: 1725820
Buffers: shared hit=1324199 read=422192
I/O Timings: read=18567.241
Planning Time: 0.197 ms
Execution Time: 21376.668 ms
(10 rows)
clustering.a1ab960a-7596-4519-9a56-52723ac35500=> explain (analyze, buffers) select * from "appsessions" where whole_risk > 80 order by app_session_id desc limit 100;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.43..192.46 rows=100 width=1557) (actual time=2238.998..2238.999 rows=0 loops=1)
Buffers: shared hit=1745176 read=1215
I/O Timings: read=5.251
-> Index Scan Backward using pk_appsessions on appsessions (cost=0.43..1104707.17 rows=575273 width=1557) (actual time=2238.997..2238.997 rows=0 loops=1)
Filter: (whole_risk > 80)
Rows Removed by Filter: 1725820
Buffers: shared hit=1745176 read=1215
I/O Timings: read=5.251
Planning Time: 0.195 ms
Execution Time: 2239.039 ms
(10 rows)
2 seconds, while not great, is doable, but the 21s can be 50s or more. The problem is that the table is not going to be in the buffers most of the time - we need the performance to be acceptable also in the first case.
EDIT again (Nov 30)
Based on helpful follow-ups from @jjanes in the comments, I looked into ANALYZE. It appeared to me that pg_stat_user_tables.last_autoanalyze showed that the table was analyzed after it was last updated. However I ran ANALYZE myself, and now the EXPLAIN output is quite different:
clustering.a1ab960a-7596-4519-9a56-52723ac35500=> explain (analyze, buffers) select * from "appsessions" where whole_risk > 80 order by app_session_id desc limit 100;
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=61.25..61.32 rows=29 width=1629) (actual time=0.022..0.023 rows=0 loops=1)
Buffers: shared hit=3
-> Sort (cost=61.25..61.32 rows=29 width=1629) (actual time=0.022..0.022 rows=0 loops=1)
Sort Key: app_session_id DESC
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=3
-> Index Scan using sessions_by_whole_risk on appsessions (cost=0.43..60.55 rows=29 width=1629) (actual time=0.016..0.016 rows=0 loops=1)
Index Cond: (whole_risk > 80)
Buffers: shared hit=3
Planning Time: 0.181 ms
Execution Time: 0.053 ms
(11 rows)
So, is the autoanalyze not doing its job? Do we need to run ANALYZE ourselves here?
ANALYZEthe table - isn't there some auto vacuum or analyze job that takes care of this? This is RDS. It looked to me like thelast_autoanalyzewas after the table was last updated. However, running it myself does seem to have changed things - I'm updating my questionwhole_risk > 80, then that subpopulation of rows might have changed a lot even though the table as a whole hasn't changed enough to trigger autoanalyze. You might need to change some of the table-specific analyze settings. It is hard to know when the last update to a table was (it isn't automatically recorded anywhere unless you created a system to do that), so maybe you were just mistaken about that.