postgres Mauvaises performances sur ORDER BY « id » DESC LIMIT 1
-
29-09-2020 - |
Question
j'ai une table items
avec le schéma suivant (dans postgres v9.3.5) :
Column | Type | Modifiers | Storage
-----------+--------+----------------------------------------------------+----------
id | bigint | not null default nextval('items_id_seq'::regclass) | plain
data | text | not null | extended
object_id | bigint | not null | plain
Indexes:
"items_pkey" PRIMARY KEY, btree (id)
"items_object_id_idx" btree (object_id)
Has OIDs: no
Lorsque j'exécute une requête, elle se bloque pendant très longtemps :
SELECT * FROM "items" WHERE "object_id" = '123' ORDER BY "id" DESC LIMIT 1;
Après VACUUM ANALYZE, l’exécution des requêtes s’est beaucoup améliorée, mais toujours pas parfaite.
# EXPLAIN ANALYZE SELECT * FROM "items" WHERE "object_id" = '123' ORDER BY "id" DESC LIMIT 1;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.44..1269.14 rows=1 width=63) (actual time=873796.061..873796.061 rows=0 loops=1)
-> Index Scan Backward using items_pkey on items (cost=0.44..1164670.11 rows=918 width=63) (actual time=873796.059..873796.059 rows=0 loops=1)
Filter: (object_id = 123::bigint)
Rows Removed by Filter: 27942522
Total runtime: 873796.113 ms
(5 rows)
Ce qui est étrange, c'est que lorsque j'exécute
SELECT * FROM "items" WHERE "object_id" = '123' LIMIT 1;
il renvoie 0 ligne et je peux le faire dans mon code pour optimiser les performances de mon application Web, mais pourquoi cela peut-il être fait par Postgres lui-même ?Je suis arrivé à Postgres depuis MySQL et je n'y ai jamais vu de choses aussi étranges.
=====
J'ai constaté qu'il utilise un plan de requête différent, un index différent, mais pourquoi ?
# EXPLAIN ANALYZE SELECT * FROM "items" WHERE "object_id" = '123' LIMIT 1;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.56..3.34 rows=1 width=63) (actual time=0.014..0.014 rows=0 loops=1)
-> Index Scan using items_object_id_operation_idx on items (cost=0.56..2579.16 rows=929 width=63) (actual time=0.013..0.013 rows=0 loops=1)
Index Cond: (object_id = 123::bigint)
Total runtime: 0.029 ms
(4 rows)
La solution 2
Pour optimiser la requête
SELECT * FROM "items" WHERE "object_id" = '123' ORDER BY "id" DESC LIMIT 1;
j'ai fait suivre
SELECT * FROM
(SELECT * FROM "items"
WHERE "object_id" = '123'
ORDER BY "id" DESC) AS "items"
ORDER BY "id" DESC LIMIT 1;
Cela a aidé sans ajouter d'index (object_id asc, id desc)
ce qui a été suggéré par @mustaccio.
# EXPLAIN SELECT * FROM
(SELECT * FROM "items"
WHERE "object_id" = '123'
ORDER BY "id" DESC) AS "items"
ORDER BY "id" DESC LIMIT 1;
QUERY PLAN
--------------------------------------------------------------------------------------------------------
Limit (cost=16629.84..16629.86 rows=1 width=59)
-> Sort (cost=16629.84..16640.44 rows=4239 width=59)
Sort Key: items.id
-> Bitmap Heap Scan on items (cost=125.42..16374.45 rows=4239 width=59)
Recheck Cond: (object_id = 123::bigint)
-> Bitmap Index Scan on items_object_id_idx (cost=0.00..124.36 rows=4239 width=0)
Index Cond: (object_id = 123::bigint)
(7 rows)
Autres conseils
Essayer d'expliquer pourquoi il existe une différence de performances entre les deux requêtes.
Celui-ci: SELECT * FROM "items" WHERE "object_id" = '123' LIMIT 1
est satisfait par n'importe lequel une ligne avec la correspondance object_id
, donc l'index sur object_id
est un choix naturel.La requête nécessite un minimum d'E/S :analyse d'index pour trouver la première valeur correspondante plus une lecture de tas pour récupérer la ligne entière.
L'alternative: SELECT * FROM "items" WHERE "object_id" = '123' ORDER BY "id" DESC LIMIT 1
a besoin tous lignes avec la correspondance object_id
être trié par une autre colonne, id
, puis la ligne avec la valeur maximale de id
être retourné.Si vous deviez utiliser l'index sur object_id
vous devrez effectuer les opérations suivantes :parcourez l'index pour trouver chaque correspondant à object_id
;pour chaque correspondance, allez chercher la ligne réelle ;puis triez toutes les lignes récupérées par id
et renvoie celui avec le plus grand id
.
L'alternative choisie par l'optimiseur, vraisemblablement basée sur la object_id
histogramme, est :scanner l'index sur id
à l'envers, dans son intégralité ;pour chaque valeur, allez chercher la ligne et vérifiez si la valeur de object_id
allumettes;renvoie la première ligne correspondante, qui aura le maximum possible id
valeur.Cette alternative évite de trier les lignes, donc je suppose que l'optimiseur la préfère à l'utilisation de l'index sur object_id
.
La présence d'un index sur (object_id asc, id desc)
permet encore une autre alternative :scannez ce nouvel index pour la première entrée correspondant au fourni object_id
valeur, qui par définition aura la valeur la plus élevée id
valeur;allez chercher une ligne correspondante et revenez.C’est évidemment l’approche la plus efficace.