Pourquoi un index gin sur une colonne jsonb ralentit ma requête et que puis-je faire à ce sujet?

10

Initialiser les données de test:

CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE docs (data JSONB NOT NULL DEFAULT '{}');
-- generate 200k documents, ~half with type: "type1" and another half with type: "type2", unique incremented index and random uuid per each row
INSERT INTO docs (data)
SELECT json_build_object('id', gen_random_uuid(), 'type', (CASE WHEN random() > 0.5 THEN 'type1' ELSE 'type2' END) ,'index', n)::JSONB
FROM generate_series(1, 200000) n;
-- inset one more row with explicit uuid to query by it later
INSERT INTO docs (data) VALUES (json_build_object('id', '30e84646-c5c5-492d-b7f7-c884d77d1e0a', 'type', 'type1' ,'index', 200001)::JSONB);

Première requête - filtrer par données-> type et limite:

-- FAST ~19ms
EXPLAIN ANALYZE
SELECT * FROM docs
WHERE data @> '{"type": "type1"}'::JSONB
LIMIT 25;
/* "Limit  (cost=0.00..697.12 rows=25 width=90) (actual time=0.029..0.070 rows=25 loops=1)"
   "  ->  Seq Scan on docs  (cost=0.00..5577.00 rows=200 width=90) (actual time=0.028..0.061 rows=25 loops=1)"
   "        Filter: (data @> '{"type": "type1"}'::jsonb)"
   "        Rows Removed by Filter: 17"
   "Planning time: 0.069 ms"
   "Execution time: 0.098 ms" 
*/

Deuxième requête - filtrer par données-> type, classer par données-> index et limite

-- SLOW ~250ms
EXPLAIN ANALYZE
SELECT * FROM docs
WHERE data @> '{"type": "type1"}'::JSONB
ORDER BY data->'index' -- added ORDER BY
LIMIT 25;

/* "Limit  (cost=5583.14..5583.21 rows=25 width=90) (actual time=236.750..236.754 rows=25 loops=1)"
   "  ->  Sort  (cost=5583.14..5583.64 rows=200 width=90) (actual time=236.750..236.750 rows=25 loops=1)"
   "        Sort Key: ((data -> 'index'::text))"
   "        Sort Method: top-N heapsort  Memory: 28kB"
   "        ->  Seq Scan on docs  (cost=0.00..5577.50 rows=200 width=90) (actual time=0.020..170.797 rows=100158 loops=1)"
   "              Filter: (data @> '{"type": "type1"}'::jsonb)"
   "              Rows Removed by Filter: 99842"
   "Planning time: 0.075 ms"
   "Execution time: 236.785 ms"
*/

Troisième requête - identique à la deuxième (précédente) mais avec un index btree sur data-> index:

CREATE INDEX docs_data_index_idx ON docs ((data->'index'));

-- FAST ~19ms
EXPLAIN ANALYZE
SELECT * FROM docs
WHERE data @> '{"type": "type1"}'::JSONB
ORDER BY data->'index' -- added BTREE index on this field
LIMIT 25;
/* "Limit  (cost=0.42..2473.98 rows=25 width=90) (actual time=0.040..0.125 rows=25 loops=1)"
   "  ->  Index Scan using docs_data_index_idx on docs  (cost=0.42..19788.92 rows=200 width=90) (actual time=0.038..0.119 rows=25 loops=1)"
   "        Filter: (data @> '{"type": "type1"}'::jsonb)"
   "        Rows Removed by Filter: 17"
   "Planning time: 0.127 ms"
   "Execution time: 0.159 ms"
*/

Quatrième requête - filtrer maintenant par data-> id et limit = 1:

-- SLOW ~116ms
EXPLAIN ANALYZE
SELECT * FROM docs
WHERE data @> ('{"id": "30e84646-c5c5-492d-b7f7-c884d77d1e0a"}')::JSONB -- querying by "id" field now
LIMIT 1;
/* "Limit  (cost=0.00..27.89 rows=1 width=90) (actual time=97.990..97.990 rows=1 loops=1)"
   "  ->  Seq Scan on docs  (cost=0.00..5577.00 rows=200 width=90) (actual time=97.989..97.989 rows=1 loops=1)"
   "        Filter: (data @> '{"id": "30e84646-c5c5-492d-b7f7-c884d77d1e0a"}'::jsonb)"
   "        Rows Removed by Filter: 189999"
   "Planning time: 0.064 ms"
   "Execution time: 98.012 ms"
*/ 

Cinquième requête - identique à la quatrième mais avec un index gin (json_path_ops) sur les données:

CREATE INDEX docs_data_idx ON docs USING GIN (data jsonb_path_ops);

-- FAST ~17ms
EXPLAIN ANALYZE
SELECT * FROM docs
WHERE data @> '{"id": "30e84646-c5c5-492d-b7f7-c884d77d1e0a"}'::JSONB -- added gin index with json_path_ops
LIMIT 1;
/* "Limit  (cost=17.55..20.71 rows=1 width=90) (actual time=0.027..0.027 rows=1 loops=1)"
   "  ->  Bitmap Heap Scan on docs  (cost=17.55..649.91 rows=200 width=90) (actual time=0.026..0.026 rows=1 loops=1)"
   "        Recheck Cond: (data @> '{"id": "30e84646-c5c5-492d-b7f7-c884d77d1e0a"}'::jsonb)"
   "        Heap Blocks: exact=1"
   "        ->  Bitmap Index Scan on docs_data_idx  (cost=0.00..17.50 rows=200 width=0) (actual time=0.016..0.016 rows=1 loops=1)"
   "              Index Cond: (data @> '{"id": "30e84646-c5c5-492d-b7f7-c884d77d1e0a"}'::jsonb)"
   "Planning time: 0.095 ms"
   "Execution time: 0.055 ms"
*/

Sixième (et dernière) requête - identique à la troisième requête (requête par données-> type, ordre par données-> index, limite):

-- SLOW AGAIN! ~224ms
EXPLAIN ANALYZE
SELECT * FROM docs
WHERE data @> '{"type": "type1"}'::JSONB
ORDER BY data->'index'
LIMIT 25;
/* "Limit  (cost=656.06..656.12 rows=25 width=90) (actual time=215.927..215.932 rows=25 loops=1)"
   "  ->  Sort  (cost=656.06..656.56 rows=200 width=90) (actual time=215.925..215.925 rows=25 loops=1)"
   "        Sort Key: ((data -> 'index'::text))"
   "        Sort Method: top-N heapsort  Memory: 28kB"
   "        ->  Bitmap Heap Scan on docs  (cost=17.55..650.41 rows=200 width=90) (actual time=33.134..152.618 rows=100158 loops=1)"
   "              Recheck Cond: (data @> '{"type": "type1"}'::jsonb)"
   "              Heap Blocks: exact=3077"
   "              ->  Bitmap Index Scan on docs_data_idx  (cost=0.00..17.50 rows=200 width=0) (actual time=32.468..32.468 rows=100158 loops=1)"
   "                    Index Cond: (data @> '{"type": "type1"}'::jsonb)"
   "Planning time: 0.157 ms"
   "Execution time: 215.992 ms"
*/

Il semble donc que la sixième requête (identique à la troisième) soit beaucoup plus lente lorsqu'il y a un index gin sur la colonne de données. C'est probablement parce qu'il n'y a pas beaucoup de valeurs distinctes pour le champ data-> type (seulement "type1" ou "type2")? Qu'est-ce que je peux y faire? J'ai besoin de gin index pour faire d'autres requêtes qui en profitent ...

user606521
la source

Réponses:

5

Il semble que vous ayez rencontré le problème que les jsonbcolonnes ont un taux de statistiques plat de 1%, comme indiqué ici Vous contournez le manque de statistiques de jsonb? . En regardant vos plans de requête, les différences entre les estimations et les exécutions réelles sont énormes. Les estimations indiquent qu'il y a probablement 200 lignes et le retour réel 100158 lignes, ce qui amène le planificateur à privilégier certaines stratégies par rapport à d'autres.

Étant donné que le choix dans la sixième requête semble se résumer à favoriser un balayage d'index bitmap par rapport à un balayage d'index, vous pouvez pousser le planificateur avec SET enable_bitmapscan=offpour essayer de le faire revenir au comportement que vous aviez dans votre troisième exemple.

Voici comment cela a fonctionné pour moi:

postgres@[local]:5432:postgres:=# EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM docs
WHERE data @> '{"type": "type1"}'::JSONB
ORDER BY data->'index'
LIMIT 25;
                                                                QUERY PLAN                                                                 
-------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=656.06..656.12 rows=25 width=90) (actual time=117.338..117.343 rows=25 loops=1)
   Buffers: shared hit=3096
   ->  Sort  (cost=656.06..656.56 rows=200 width=90) (actual time=117.336..117.338 rows=25 loops=1)
         Sort Key: ((data -> 'index'::text))
         Sort Method: top-N heapsort  Memory: 28kB
         Buffers: shared hit=3096
         ->  Bitmap Heap Scan on docs  (cost=17.55..650.41 rows=200 width=90) (actual time=12.838..80.584 rows=99973 loops=1)
               Recheck Cond: (data @> '{"type": "type1"}'::jsonb)
               Heap Blocks: exact=3077
               Buffers: shared hit=3096
               ->  Bitmap Index Scan on docs_data_idx  (cost=0.00..17.50 rows=200 width=0) (actual time=12.469..12.469 rows=99973 loops=1)
                     Index Cond: (data @> '{"type": "type1"}'::jsonb)
                     Buffers: shared hit=19
 Planning time: 0.088 ms
 Execution time: 117.405 ms
(15 rows)

Time: 117.813 ms
postgres@[local]:5432:postgres:=# SET enable_bitmapscan = off;
SET
Time: 0.130 ms
postgres@[local]:5432:postgres:=# EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM docs
WHERE data @> '{"type": "type1"}'::JSONB
ORDER BY data->'index'
LIMIT 25;
                                                               QUERY PLAN                                                               
----------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.42..1320.48 rows=25 width=90) (actual time=0.017..0.050 rows=25 loops=1)
   Buffers: shared hit=4
   ->  Index Scan using docs_data_index_idx on docs  (cost=0.42..10560.94 rows=200 width=90) (actual time=0.015..0.045 rows=25 loops=1)
         Filter: (data @> '{"type": "type1"}'::jsonb)
         Rows Removed by Filter: 27
         Buffers: shared hit=4
 Planning time: 0.083 ms
 Execution time: 0.071 ms
(8 rows)

Time: 0.402 ms
postgres@[local]:5432:postgres:=#

Si vous cherchez à suivre cette voie, veillez à désactiver cette analyse uniquement pour les requêtes qui affichent un comportement comme celui-ci, sinon, vous obtiendrez également un mauvais comportement sur d'autres plans de requête. Faire quelque chose comme ça devrait très bien fonctionner:

BEGIN;
SET enable_bitmapscan=off;
SELECT * FROM docs
WHERE data @> '{"type": "type1"}'::JSONB
ORDER BY data->'index'
LIMIT 25;
SET enable_bitmapscan=on;
COMMIT;

J'espère que ça aide =)

Kassandry
la source
Je ne sais pas si je vous comprends bien (je ne connais pas les internes PG) - ce comportement est causé par une faible cardinalité sur le champ "type" dans la colonne jsonb (et en interne causé par un taux de statistiques plat), non? Et cela signifie également que, si je veux que ma requête soit optimisée, je dois connaître la cardinalité approximative du ou des champs jsonb par lesquels je recherche pour décider si je dois activer_bitmapscan ou non, non?
user606521
1
Oui, vous semblez comprendre cela sur les deux plans. La sélectivité de base de 1% favorise la recherche du champ dans la WHEREclause de l'index gin, car elle pense qu'elle renverra moins de lignes, ce qui n'est pas vrai. Étant donné que vous pouvez mieux estimer le nombre de lignes, vous pouvez voir que, puisque vous le faites ORDER BY data->'index' LIMIT 25, que l'analyse des premières entrées de l'autre index (50 environ, avec des lignes jetées) se traduira par encore moins de lignes, donc révélateur le planificateur, il ne devrait vraiment pas essayer d'utiliser un résultat bitmapscan dans un plan de requête plus rapide utilisé. J'espère que cela clarifie les choses. =)
Kassandry
1
Il y a des informations de clarification supplémentaires ici: databasesoup.com/2015/01/tag-all-things-part-3.html et dans cette présentation thebuild.com/presentations/json2015-pgconfus.pdf pour vous aider également.
Kassandry
1
Le seul travail que je connaisse est d'Oleg Bartunov, Tedor Sigaev et Alexander Kotorov sur l' extension JsQuery et ses améliorations de sélectivité. Avec un peu de chance, il arrive dans le noyau PostgreSQL en 9.6 ou version ultérieure.
Kassandry
1
J'ai cité le chiffre de 1% de l'e-mail dans ma réponse de Josh Berkus, membre de l'équipe PostgreSQL Core. D'où cela nécessite une compréhension beaucoup plus approfondie des éléments internes que je ne possède actuellement, désolé. = (Vous pouvez essayer de répondre à l' [email protected]IRC de freenode ou de vérifier #postgresqld'où vient exactement ce chiffre.
Kassandry