I'm running PostgreSQL 11 with shared_buffers
set to 3 GB on my Mac. I have a table job
with 5 million rows. The table structure is
Table "public.job"
Column | Type | Collation | Nullable | Default
------------+--------------------------+-----------+----------+---------
id | uuid | | not null |
name | text | | |
created_on | timestamp with time zone | | |
updated_on | timestamp with time zone | | |
Indexes:
"job_pkey" PRIMARY KEY, btree (id)
"job_created_on_idx" btree (created_on)
"job_name_idx" btree (name)
"job_updated_on_idx" btree (updated_on)
"job_updated_on_name_compound_asc_idx" btree (updated_on, upper(name))
"job_updated_on_name_compound_desc_idx" btree (updated_on DESC, upper(name))
Note I've created compound index on updated_on
and name
columns.
When I running query select name, created_on from job where created_on >= '2023-10-08 00:00:00+08'::timestamp with time zone AND created_on < '2023-10-16 00:00:00+08' ORDER BY updated_on ASC, UPPER(name::text) ASC limit 25
, PostgreSQL uses the compound index job_updated_on_name_compound_asc_idx
and took more than 4 seconds.
Execution plan
Limit (cost=0.43..102.29 rows=25 width=61) (actual time=4549.668..4550.235 rows=25 loops=1)
Buffers: shared hit=4859940
-> Index Scan using job_updated_on_name_compound_asc_idx on job (cost=0.43..416764.16 rows=102293 width=61) (actual time=4549.667..4550.230 rows=25 loops=1)
Filter: ((created_on >= '2023-10-08 00:00:00+08'::timestamp with time zone) AND (created_on < '2023-10-16 00:00:00+08'::timestamp with time zone))
Rows Removed by Filter: 4828894
Buffers: shared hit=4859940
Planning Time: 0.218 ms
Execution Time: 4550.260 ms
There's an index on the created_on
column, but it's not used. I can force PostgreSQL to use the index of created_on
column by appending id
to the order by clause. The query is select name, created_on from job where created_on >= '2023-10-08 00:00:00+08'::timestamp with time zone AND created_on < '2023-10-16 00:00:00+08' ORDER BY updated_on ASC, UPPER(name::text) ASC, id limit 25;
. This time, PostgreSQL uses the index on the created_on
column and returns the result very fast.
Execution plan
Limit (cost=52190.61..52193.52 rows=25 width=77) (actual time=125.192..138.055 rows=25 loops=1)
Buffers: shared hit=42788
-> Gather Merge (cost=52190.61..62136.44 rows=85244 width=77) (actual time=125.191..138.049 rows=25 loops=1)
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=42788
-> Sort (cost=51190.58..51297.14 rows=42622 width=77) (actual time=119.359..119.362 rows=20 loops=3)
Sort Key: updated_on, (upper(name)), id
Sort Method: top-N heapsort Memory: 30kB
Worker 0: Sort Method: top-N heapsort Memory: 31kB
Worker 1: Sort Method: top-N heapsort Memory: 31kB
Buffers: shared hit=42788
-> Parallel Bitmap Heap Scan on job (cost=2512.94..49987.82 rows=42622 width=77) (actual time=19.915..109.984 rows=36562 loops=3)
Recheck Cond: ((created_on >= '2023-10-08 00:00:00+08'::timestamp with time zone) AND (created_on < '2023-10-16 00:00:00+08'::timestamp with time zone))
Heap Blocks: exact=24557
Buffers: shared hit=42738
-> Bitmap Index Scan on job_created_on_idx (cost=0.00..2487.36 rows=102293 width=0) (actual time=16.909..16.909 rows=109685 loops=1)
Index Cond: ((created_on >= '2023-10-08 00:00:00+08'::timestamp with time zone) AND (created_on < '2023-10-16 00:00:00+08'::timestamp with time zone))
Buffers: shared hit=395
Planning Time: 0.168 ms
Execution Time: 138.115 ms
The difference of execution time becomes larger if the database is busy on updating a large column of rows.
The compound index was created to improve sorting performance and is very useful in some cases. Because my system generates the SQL dynamically based on the user selection, so the query condition and sorting can vary. In this specific case, adding id
to the order by clause to avoid using a compound index can improve performance, but maybe in some other cases using the compound index is better, so I cannot just simply remove the compound index.
I also checked the pg_stats table and here's the result:
attname | inherited | n_distinct | most_common_vals
------------+-----------+------------+------------------
id | f | -1 |
name | f | -1 |
created_on | f | -0.908167 |
updated_on | f | -1 |
I have two questions:
- For the above query, it's obviously using the index of
created_on
is better. Why does PostgreSQL choose the compound index of the order by clause? Is there anything I can configure on PostgreSQL to let it use the correct index? - It looks like PostgreSQL won't use both indexes of columns in the query condition and order by. It's
Filter
under the compound index although the column used in theFilter
is indexed. Is it possible for PostgreSQL to use the compound index for order by and the index for the query condition column together in a single query?