J'ai une table comme celle-ci:
CREATE TABLE products (
id serial PRIMARY KEY,
category_ids integer[],
published boolean NOT NULL,
score integer NOT NULL,
title varchar NOT NULL);
Un produit peut appartenir à plusieurs catégories. category_ids
La colonne contient une liste des identifiants de toutes les catégories de produits.
La requête typique ressemble à ceci (toujours à la recherche d'une seule catégorie):
SELECT * FROM products WHERE published
AND category_ids @> ARRAY[23465]
ORDER BY score DESC, title
LIMIT 20 OFFSET 8000;
Pour l'accélérer, j'utilise l'index suivant:
CREATE INDEX idx_test1 ON products
USING GIN (category_ids gin__int_ops) WHERE published;
Celui-ci aide beaucoup à moins qu'il n'y ait trop de produits dans une catégorie. Il filtre rapidement les produits qui appartiennent à cette catégorie, mais il y a ensuite une opération de tri qui doit être effectuée à la dure (sans index).
Une btree_gin
extension installée m'a permis de construire un index GIN multi-colonnes comme ceci:
CREATE INDEX idx_test2 ON products USING GIN (
category_ids gin__int_ops, score, title) WHERE published;
Mais Postgres ne veut pas l'utiliser pour le tri . Même lorsque je supprime le DESC
spécificateur dans la requête.
Toute approche alternative pour optimiser la tâche est la bienvenue.
Information additionnelle:
- PostgreSQL 9.4, avec extension intarray
- le nombre total de produits est actuellement de 260 000 mais devrait croître de manière significative (jusqu'à 10 millions, il s'agit d'une plateforme de commerce électronique multi-locataire)
- produits par catégorie 1..10000 (peut atteindre 100 000), la moyenne est inférieure à 100 mais les catégories avec un grand nombre de produits ont tendance à attirer beaucoup plus de demandes
Le plan de requête suivant a été obtenu à partir d'un système de test plus petit (4680 produits dans la catégorie sélectionnée, 200k produits au total dans le tableau):
Limit (cost=948.99..948.99 rows=1 width=72) (actual time=82.330..82.341 rows=20 loops=1)
-> Sort (cost=948.37..948.99 rows=245 width=72) (actual time=80.231..81.337 rows=4020 loops=1)
Sort Key: score, title
Sort Method: quicksort Memory: 928kB
-> Bitmap Heap Scan on products (cost=13.90..938.65 rows=245 width=72) (actual time=1.919..16.044 rows=4680 loops=1)
Recheck Cond: ((category_ids @> '{292844}'::integer[]) AND published)
Heap Blocks: exact=3441
-> Bitmap Index Scan on idx_test2 (cost=0.00..13.84 rows=245 width=0) (actual time=1.185..1.185 rows=4680 loops=1)
Index Cond: (category_ids @> '{292844}'::integer[])
Planning time: 0.202 ms
Execution time: 82.404 ms
Remarque # 1 : 82 ms peuvent ne pas sembler si effrayantes, mais c'est parce que le tampon de tri tient dans la mémoire. Une fois que j'ai sélectionné toutes les colonnes de la table des produits ( SELECT * FROM ...
et dans la vie réelle, il y a environ 60 colonnes), le Sort Method: external merge Disk: 5696kB
temps d'exécution double. Et ce n'est que pour 4680 produits.
Point d'action n ° 1 (provient de la note n ° 1): afin de réduire l'empreinte mémoire de l'opération de tri et donc d'accélérer un peu, il serait sage de récupérer, trier et limiter les identifiants de produit d'abord, puis de récupérer les enregistrements complets:
SELECT * FROM products WHERE id IN (
SELECT id FROM products WHERE published AND category_ids @> ARRAY[23465]
ORDER BY score DESC, title LIMIT 20 OFFSET 8000
) ORDER BY score DESC, title;
Cela nous ramène à Sort Method: quicksort Memory: 903kB
et ~ 80 ms pour 4680 produits. Peut encore être lent lorsque le nombre de produits atteint 100 000.
la source
score
peut être NULL, mais vous triez toujours parscore DESC
, nonscore DESC NULLS LAST
. L'un ou l'autre ne semble pas correct ...score
en fait, ce n'est PAS NULL - j'ai corrigé la définition de la table.Réponses:
J'ai fait beaucoup d'expérimentations et voici mes résultats.
GIN et tri
L'index GIN actuellement (à partir de la version 9.4) ne peut pas aider à la commande .
work_mem
Merci Chris d'avoir signalé ce paramètre de configuration . La valeur par défaut est 4 Mo, et dans le cas où votre jeu d'enregistrements est plus grand, l'augmentation
work_mem
à la valeur appropriée (disponible à partir deEXPLAIN ANALYSE
) peut accélérer considérablement les opérations de tri.Redémarrez le serveur pour que les modifications prennent effet, puis revérifiez:
Requête d'origine
J'ai rempli ma base de données avec 650k produits avec certaines catégories contenant jusqu'à 40k produits. J'ai simplifié un peu la requête en supprimant la
published
clause:Comme nous pouvons le voir, ce
work_mem
n'était pas suffisant, nous en avionsSort Method: external merge Disk: 29656kB
(le nombre ici est approximatif, il a besoin d'un peu plus de 32 Mo pour le tri rapide en mémoire).Réduisez l'empreinte mémoire
Ne sélectionnez pas les enregistrements complets pour le tri, utilisez les identifiants, appliquez le tri, le décalage et la limite, puis chargez seulement 10 enregistrements dont nous avons besoin:
Remarque
Sort Method: quicksort Memory: 7396kB
. Le résultat est bien meilleur.JOIN et index B-tree supplémentaire
Comme Chris l'a conseillé, j'ai créé un index supplémentaire:
J'ai d'abord essayé de rejoindre comme ceci:
Le plan de requête diffère légèrement mais le résultat est le même:
En jouant avec différents décalages et nombre de produits, je n'ai pas pu faire en sorte que PostgreSQL utilise un index B-tree supplémentaire.
Je suis donc allé de façon classique et j'ai créé une table de jonction :
N'utilisant toujours pas l'indice B-tree, l'ensemble de résultats ne correspondait pas
work_mem
, d'où de mauvais résultats.Mais dans certaines circonstances, ayant un grand nombre de produits et un petit décalage, PostgreSQL décide maintenant d'utiliser l'index B-tree:
Ceci est en fait assez logique car l'index B-tree ici ne produit pas de résultat direct, il est uniquement utilisé comme guide pour le scan séquentiel.
Comparons avec la requête GIN:
Le résultat de GIN est bien meilleur. J'ai vérifié avec différentes combinaisons de nombre de produits et de décalage, en aucun cas l'approche de la table de jonction n'était meilleure .
La puissance de l'indice réel
Pour que PostgreSQL utilise pleinement l'index pour le tri, tous les
WHERE
paramètres de requête ainsi que lesORDER BY
paramètres doivent résider dans un index B-tree unique. Pour ce faire, j'ai copié les champs de tri du produit vers la table de jonction:Et c'est le pire scénario avec un grand nombre de produits dans la catégorie choisie et un grand décalage. Lorsque offset = 300, le temps d'exécution n'est que de 0,5 ms.
Malheureusement, le maintien d'une telle table de jonction nécessite un effort supplémentaire. Cela peut être accompli via des vues matérialisées indexées, mais cela n'est utile que lorsque vos données sont rarement mises à jour, car l'actualisation de cette vue matérialisée est une opération assez lourde.
Donc, je reste avec l'index GIN jusqu'à présent, avec une
work_mem
requête d'empreinte mémoire augmentée et réduite.la source
work_mem
paramètre général dans postgresql.conf. Le rechargement suffit. Et permettez-moi de mettre en garde contre un réglagework_mem
trop élevé à l'échelle mondiale dans un environnement multi-utilisateurs (pas trop bas non plus). Si vous avez des questions plus besoinwork_mem
, réglez plus pour la session uniquement avecSET
- ou tout simplement la transaction avecSET LOCAL
. Voir: dba.stackexchange.com/a/48633/3684Voici quelques conseils rapides qui peuvent vous aider à améliorer vos performances. Je vais commencer par la pointe la plus simple, qui est presque sans effort de votre part, et passer à la pointe la plus difficile après la première.
1.
work_mem
Donc, je vois tout de suite qu'un type signalé dans votre plan d'explication
Sort Method: external merge Disk: 5696kB
consomme moins de 6 Mo, mais se répand sur le disque. Vous devez augmenter votrework_mem
paramètre dans votrepostgresql.conf
fichier pour qu'il soit suffisamment grand pour que le tri puisse tenir en mémoire.EDIT: En outre, après une inspection plus approfondie, je vois qu'après avoir utilisé l'index pour vérifier celui
catgory_ids
qui correspond à vos critères, l'analyse d'index bitmap est forcée de devenir "avec perte" et doit revérifier la condition lors de la lecture des lignes à partir des pages de tas pertinentes . Reportez-vous à cet article sur postgresql.org pour une explication meilleure que celle que j'ai donnée. : P Le point principal est que votrework_mem
est trop bas. Si vous n'avez pas réglé les paramètres par défaut sur votre serveur, cela ne fonctionnera pas bien.Cette correction ne vous prendra essentiellement pas de temps. Un changement pour
postgresql.conf
, et c'est parti! Reportez-vous à cette page de réglage des performances pour plus de conseils.2. Changement de schéma
Ainsi, vous avez pris la décision dans votre conception de schéma de dénormaliser le
category_ids
en un tableau entier, ce qui vous oblige ensuite à utiliser un index GIN ou GIST pour obtenir un accès rapide. D'après mon expérience, votre choix d'un index GIN sera plus rapide pour les lectures qu'un GIST, donc dans ce cas, vous avez fait le bon choix. Cependant, GIN est un index non trié; penser plus comme une valeur clé, où l'égalité prédicats sont faciles à vérifier, mais des opérations telles queWHERE >
,WHERE <
ouORDER BY
ne sont pas facilitées par l'indice.Une approche décente serait de normaliser votre conception en utilisant une table de pont / table de jonction , utilisée pour spécifier les relations plusieurs-à-plusieurs dans les bases de données.
Dans ce cas, vous avez plusieurs catégories et un ensemble d'entiers correspondants
category_id
, et vous avez de nombreux produits et leursproduct_id
s correspondants . Au lieu d'une colonne dans votre table de produit qui est un tableau entier decategory_id
s, supprimez cette colonne de tableau de votre schéma et créez une table en tant queEnsuite, vous pouvez générer des indices B-tree sur les deux colonnes de la table bridge,
Juste mon humble avis, mais ces changements peuvent faire une grande différence pour vous. Essayez ce
work_mem
changement en premier lieu, à tout le moins.Bonne chance!
ÉDITER:
Créer un index supplémentaire pour faciliter le tri
Donc, si au fil du temps votre gamme de produits se développe, certaines requêtes peuvent renvoyer de nombreux résultats (des milliers, des dizaines de milliers?), Mais qui ne peuvent être qu'un petit sous-ensemble de votre gamme de produits totale. Dans ces cas, le tri peut même être assez coûteux s'il est effectué en mémoire, mais un index conçu de manière appropriée peut être utilisé pour faciliter le tri.
Voir la documentation officielle de PostgreSQL décrivant les index et ORDER BY .
Si vous créez un index correspondant à vos
ORDER BY
exigencesPostgres optimisera et décidera alors si l'utilisation de l'index ou l'exécution d'un tri explicite sera plus rentable. Gardez à l'esprit qu'il n'y a aucune garantie que Postgres utilisera l'index; il cherchera à optimiser les performances et à choisir entre l'utilisation de l'index ou le tri explicite. Si vous créez cet index, surveillez-le pour voir s'il est suffisamment utilisé pour justifier sa création et supprimez-le si la plupart de vos tris sont effectués de manière explicite.
Pourtant, à ce stade, votre plus grand rapport qualité-prix en utilisera probablement plus
work_mem
, mais il existe des cas où l'index peut prendre en charge le tri.la source
work_mem
configuration était destinée à corriger votre problème de «tri sur le disque», ainsi que votre problème de vérification de l'état. À mesure que le nombre de produits augmente, vous devrez peut-être disposer d'un index supplémentaire pour trier. Veuillez consulter mes modifications ci-dessus pour obtenir des éclaircissements.