We have a PostgreSQL table with ~5 billion rows that has developed a nasty habit of missing the proper indices and doing a Primary Key scan on certain LIMIT
operations.
The problem generally manifests on an ORDER BY .. LIMIT ..
clause (a common pattern in Django pagination) where the LIMIT
is some relatively small subset of the results matched by the index. An extreme example is this:
SELECT * FROM mcqueen_base_imagemeta2
WHERE image_id IN ( 123, ... )
ORDER BY id DESC
LIMIT 1;
where the items in that IN
clause are ~20 and total rows matched by the index on image_id
is 16.
The EXPLAIN
shows that it misses the image_id
index and instead does a PK scan of 5B rows:
Limit (cost=0.58..4632.03 rows=1 width=28) -> Index Scan Backward using mcqueen_base_imagemeta2_pkey on mcqueen_base_imagemeta2 (cost=0.58..364597074.75 rows=78722 width=28) Filter: (image_id = ANY ('{123, ...}'::bigint[]))
If the LIMIT
is increased to 2
, it works as expected:
Limit (cost=7585.92..7585.93 rows=2 width=28) -> Sort (cost=7585.92..7782.73 rows=78722 width=28) Sort Key: id DESC -> Index Scan using mcqueen_base_imagemeta2_image_id_616fe89c on mcqueen_base_imagemeta2 (cost=0.58..6798.70 rows=78722 width=28) Index Cond: (image_id = ANY ('{123, ...}'::bigint[]))
This also happens on queries where the index matches ~3000 rows and the limit is set to 100, so something that easily happens in real world REST API pagination.
The table definition is:
mcqueen=# \d mcqueen_base_imagemeta2
Table "public.mcqueen_base_imagemeta2"
Column | Type | Modifiers
-------------------+--------------------------+----------------------------------------------------------------------
id | bigint | not null default nextval('mcqueen_base_imagemeta2_id_seq'::regclass)
created_at | timestamp with time zone | not null
image_id | bigint | not null
key_id | smallint | not null
source_version_id | smallint | not null
Indexes:
"mcqueen_base_imagemeta2_pkey" PRIMARY KEY, btree (id)
"mcqueen_base_imagemeta2_image_id_616fe89c" btree (image_id)
"mcqueen_base_imagemeta2_key_id_a4854581" btree (key_id)
"mcqueen_base_imagemeta2_source_version_id_f9b0513e" btree (source_version_id)
Foreign-key constraints:
"mcqueen_base_imageme_image_id_616fe89c_fk_mcqueen_b" FOREIGN KEY (image_id) REFERENCES mcqueen_base_image(id) DEFERRABLE INITIALLY DEFERRED
"mcqueen_base_imageme_key_id_a4854581_fk_mcqueen_b" FOREIGN KEY (key_id) REFERENCES mcqueen_base_metakey(id) DEFERRABLE INITIALLY DEFERRED
"mcqueen_base_imageme_source_version_id_f9b0513e_fk_mcqueen_b" FOREIGN KEY (source_version_id) REFERENCES mcqueen_base_metasourceversion(id) DEFERRABLE INITIALLY DEFERRED
I'm a novice at best when it comes to tuning, but I figure that the defaults for statistics are not up to that table's size and so it naively thinks that a PK scan is faster than an index scan.
select * from pg_stats where tablename ='mcqueen_base_imagemeta2' and attname='image_id'\x\g\x
? Extremely rare values (which these must be if 20 of them only find 16 rows) are often estimated poorly. And what is the actual number of distinct values of image_id in the table?select count(*) from (select distinct image_id from mcqueen_base_imagemeta2) foo