web-dev-qa-db-fra.com

La clause ORDER BY détruit les performances des requêtes

Contexte:

PostgreSQL 10, avec 3667438 enregistrements dans la table des utilisateurs, la table des utilisateurs a un JSONB appelé social, nous utilisons généralement une stratégie d'indexation des sorties de fonctions calculées, afin que nous puissions agréger des informations dans un seul index. La sortie de la fonction engagement(social) est de type numérique à double précision.

Problème:

La clause problématique est ORDER BY engagement(social) DESC NULLS LAST, il y a aussi un index btree idx_in_social_engagement with DESC NULLS LAST joint à ces données.

Requête rapide:

EXPLAIN ANALYZE
SELECT  "users".* FROM "users"
WHERE (follower_count(social) < 500000)
AND (engagement(social) > 0.03)
AND (engagement(social) < 0.25)
AND (peemv(social) < 533)
ORDER BY "users"."created_at" ASC
LIMIT 12 OFFSET 0;

Limit  (cost=0.43..52.25 rows=12 width=1333) (actual time=0.113..1.625 
rows=12 loops=1)
   ->  Index Scan using created_at_idx on users  (cost=0.43..7027711.55 rows=1627352 width=1333) (actual time=0.112..1.623 rows=12 loops=1)
         Filter: ((follower_count(social) < 500000) AND (engagement(social) > '0.03'::double precision) AND (engagement(social) <  '0.25'::double precision) AND (peemv(social) > '0'::double precision) AND (peemv(social) < '533'::double precision))
         Rows Removed by Filter: 8
 Planning time: 0.324 ms
 Execution time: 1.639 ms

Requête lente:

EXPLAIN ANALYZE 
SELECT  "users".* FROM "users" 
WHERE (follower_count(social) < 500000) 
AND (engagement(social) > 0.03) 
AND (engagement(social) < 0.25) 
AND (peemv(social) > 0.0) 
AND (peemv(social) < 533) 
ORDER BY engagement(social) DESC NULLS LAST, "users"."created_at" ASC 
LIMIT 12 OFFSET 0;

Limit  (cost=2884438.00..2884438.03 rows=12 width=1341) (actual time=68011.728..68011.730 rows=12 loops=1)
->  Sort  (cost=2884438.00..2888506.38 rows=1627352 width=1341) (actual time=68011.727..68011.728 rows=12 loops=1)
        Sort Key: (engagement(social)) DESC NULLS LAST, created_at
        Sort Method: top-N heapsort  Memory: 45kB
        ->  Index Scan using idx_in_social_engagement on users  (cost=0.43..2847131.26 rows=1627352 width=1341) (actual time=0.082..67019.102 rows=1360633 loops=1)
            Index Cond: ((engagement(social) > '0.03'::double precision) AND (engagement(social) < '0.25'::double precision))
            Filter: ((follower_count(social) < 500000) AND (peemv(social) > '0'::double precision) AND (peemv(social) < '533'::double precision))
            Rows Removed by Filter: 85580
Planning time: 0.312 ms
Execution time: 68011.752 ms

La sélection va avec * car j'ai besoin de toutes les données stockées dans chaque ligne.

Mise à jour:

CREATE INDEX idx_in_social_engagement on influencers USING BTREE ( engagement(social) DESC NULLS LAST)

Définition exacte de l'index

4
Imanol Y.

Votre clause ORDER BY Est sur:

engagement(social) DESC NULLS LAST, "users"."created_at" ASC

Mais je soupçonne que votre index est juste sur:

engagement(social) DESC NULLS LAST

L'index n'est donc pas capable de prendre en charge entièrement le ORDER BY.

Vous pouvez reproduire le même problème sans utiliser d'index JSONB ou d'expression. Vous pourriez être en mesure de sauver la situation en créant un index d'expression composite sur les deux colonnes dans votre ORDER BY.

Si le planificateur PostgreSQL était infiniment sage, il pourrait être capable d'utiliser efficacement l'index existant. Il lui faudrait remonter engagement(social) DESC NULLS LAST jusqu'à ce qu'il recueille 12 tuples qui répondent à tous les autres critères de filtrage. Ensuite, il aurait continué à remonter cet index jusqu'à ce qu'il collecte tous les autres tuples qui étaient liés à engagement(social) avec le 12ème tuple (et qui remplissaient les autres critères). Ensuite, il devrait trier à nouveau tous les tuples collectés sur le ORDER BY Complet et appliquer le LIMIT 12 À cet ensemble développé et re-trié. Mais le planificateur PostgreSQL n'est pas infiniment sage.

7
jjanes

Je soupçonne que le coupable ici est le manque de statistiques sur les colonnes JSONB. Postgres ne conserve aucune statistique sur les colonnes JSONB, mais utilise des estimations codées en dur. Si ces estimations sont très éloignées, ce qui est très probable, cela peut conduire à des plans de requête terribles comme votre cas.

Dans votre bon plan, Postgres commande d'abord les données puis les filtre. Ceci est extrêmement rapide pour les requêtes avec des clauses LIMIT et un index sur la colonne triée, si le filtre ne supprime pas de nombreuses lignes. L'index est ordonné, donc obtenir les lignes dans l'ordre est extrêmement bon marché. La clause limit signifie que vous n'avez qu'à récupérer quelques lignes jusqu'à ce que vous en ayez assez pour la satisfaire.

Mais si votre filtre exclut un grand nombre de lignes, par exemple si seulement 0,1% y correspond, le tri en premier vous obligerait à parcourir la plupart de votre tableau pour trouver suffisamment de lignes, car vous les filtrez presque toutes. Dans ce cas, le filtrage d'abord par index puis le tri sont beaucoup plus rapides. C'est ce que fait votre mauvais plan, et ce n'est évidemment pas un bon ajustement pour vos données.

La meilleure option serait de loin de mettre les valeurs que vous utilisez ici dans leurs propres colonnes. JSONB est extrêmement utile pour certaines utilisations, mais si vous n'avez pas besoin de la flexibilité qu'il offre, l'ancienne méthode relationnelle est bien meilleure.

Un index fonctionnel fournit des statistiques, ce qui devrait aider votre requête. Je soupçonne que dans votre cas, cela ne fonctionne pas bien parce que vous passez toute la colonne sociale à la fonction, et cela ne peut pas construire de bonnes statistiques à partir de cela. Vous pouvez essayer un index uniquement sur la clé d'engagement dans la colonne sociale, ce qui pourrait fournir à Postgres les statistiques nécessaires à un meilleur plan de requête. Voir ma propre question sur les statistiques JSONB pour un exemple sur la façon de le faire.

1
Mad Scientist