web-dev-qa-db-fra.com

Comprendre la priorisation de composite Btree + gin_trgm_ops à index et comportement inférieur impair ()

en espérant que quelqu'un puisse essayer de m'aider à déchiffrer un comportement d'index. Je travaille sur l'activation de quelques recherches simples contains de type sur diverses colonnes de données utilisateur (~ Varchar <255) et essayant de comprendre le comportement de l'index, ainsi que d'avoir une idée de savoir s'il y a une meilleure approche Globalement (peut-être le texte intégral? - Bien que j'imagine que nous aurons probablement besoin d'une application de recherche plus large à un moment donné, mais en passant à cet effet pour notre application pour le moment)

Anyhoo, dans mon cas, nous recherchons principalement tous les utilisateurs de ce tableau commençant par la catégorie/type tuple (en raison d'un héritage à table unique avec des rails)

Exemple de table et d'index, utilisant Postgres11:

CREATE TABLE people (
    id SERIAL,
    email character varying(255) not null,
    first_name character varying(255) not null,
    last_name character varying(255) not null,
    user_category integer not null,
    user_type character varying(255) not null
);

-- Dummy Data
INSERT INTO people (email, first_name, last_name, user_category, user_type)
SELECT
  concat(md5(random()::text), '@not-real-email-plz.com'),
  md5(random()::text), 
  md5(random()::text), 
  ceil(random() * 3), 
  ('{Grunt,Peon,Ogre}'::text[])[ceil(random()*3)]
FROM
  (SELECT * FROM generate_series(1,1000000) AS id) AS x;

-- Standard, existing lookup
CREATE INDEX index_people_category_type ON people USING btree (user_category, user_type);

-- taken from https://niallburkley.com/blog/index-columns-for-like-in-postgres/
CREATE INDEX idx_people_gin_user_category_and_user_type_and_full_name 
ON people
USING GIN(user_category, user_type, (first_name || ' ' || last_name) gin_trgm_ops);    

-- first name
CREATE INDEX idx_people_gin_user_category_and_user_type_and_first_name 
ON people
USING GIN(user_category, user_type, first_name gin_trgm_ops);

-- last name
CREATE INDEX idx_people_gin_user_category_and_user_type_and_last_name 
ON people
USING GIN(user_category, user_type, last_name gin_trgm_ops);

-- email
CREATE INDEX idx_people_gin_user_category_and_user_type_and_email 
ON people
USING GIN(user_category, user_type, email gin_trgm_ops);

-- non-composite email (had for testing and raised more questions)
CREATE INDEX idx_people_gin_email 
ON people
USING GIN(email gin_trgm_ops);

J'ai lu que cet ordre n'a pas d'importance dans les index Gin, alors je suppose que ma première question est de savoir si elle est également possible de créer un index comprenant plusieurs colonnes qui permettraient une combinaison de leur utilisation? Je suppose que non, car les index varient définitivement en taille mais n'étaient pas sûr de l'implication des détails de commande.

Quoi qu'il en soit, sur ce que j'ai observé!

L'une des premières choses que j'ai remarquées, c'est que cela semble que l'indice Gin est immédiatement compatible le premier index B-Tree lors de la simple recherche par catégorie et par type

EXPLAIN ANALYZE VERBOSE

SELECT DISTINCT id
FROM people
WHERE user_category = 2
  AND (user_type != 'Ogre')

Résultats:

Unique  (cost=52220.05..53334.71 rows=222932 width=4) (actual time=251.070..339.769 rows=222408 loops=1)
  Output: id
  ->  Sort  (cost=52220.05..52777.38 rows=222932 width=4) (actual time=251.069..285.652 rows=222408 loops=1)
        Output: id
        Sort Key: people.id
        Sort Method: external merge  Disk: 3064kB
        ->  Bitmap Heap Scan on public.people  (cost=3070.23..29368.23 rows=222932 width=4) (actual time=35.156..198.549 rows=222408 loops=1)
              Output: id
              Recheck Cond: (people.user_category = 2)
              Filter: ((people.user_type)::text <> 'Ogre'::text)
              Rows Removed by Filter: 111278
              Heap Blocks: exact=21277
              ->  Bitmap Index Scan on idx_people_gin_user_category_and_user_type_and_email  (cost=0.00..3014.50 rows=334733 width=0) (actual time=32.017..32.017 rows=333686 loops=1)
                    Index Cond: (people.user_category = 2)
Planning Time: 0.293 ms
Execution Time: 359.247 ms

L'arbre B d'origine est-il totalement redondant à ce stade? Je m'attendais à ce que le planificateur puisse toujours être cueilli par le planificateur si seulement ces deux colonnes étaient utilisées si le B-Tree était plus rapide pour ces types de données, mais cela semble que ce ne soit pas le cas.

Ensuite, j'avais remarqué que nos requêtes existantes dépendaient de lower() et semblait ignorer complètement les indices de gin, ou plutôt qu'il semblait utiliser la dernière étant la dernière créée même si cette colonne n'était pas utilisée dans le requete:

EXPLAIN ANALYZE VERBOSE

SELECT DISTINCT id
FROM people
WHERE user_category = 2
  AND (user_type != 'Ogre')
  AND (LOWER(last_name) LIKE LOWER('%a62%'))

Résultats (comparer Last_Name mais en utilisant l'index de messagerie):

HashAggregate  (cost=28997.16..29086.33 rows=8917 width=4) (actual time=175.204..175.554 rows=1677 loops=1)
  Output: id
  Group Key: people.id
  ->  Gather  (cost=4016.73..28974.87 rows=8917 width=4) (actual time=39.947..181.936 rows=1677 loops=1)
        Output: id
        Workers Planned: 2
        Workers Launched: 2
        ->  Parallel Bitmap Heap Scan on public.people  (cost=3016.73..27083.17 rows=3715 width=4) (actual time=22.037..156.233 rows=559 loops=3)
              Output: id
              Recheck Cond: (people.user_category = 2)
              Filter: (((people.user_type)::text <> 'Ogre'::text) AND (lower((people.last_name)::text) ~~ '%a62%'::text))
              Rows Removed by Filter: 110670
              Heap Blocks: exact=7011
              Worker 0: actual time=13.573..147.844 rows=527 loops=1
              Worker 1: actual time=13.138..147.867 rows=584 loops=1
              ->  Bitmap Index Scan on idx_people_gin_user_category_and_user_type_and_email  (cost=0.00..3014.50 rows=334733 width=0) (actual time=35.445..35.445 rows=333686 loops=1)
                    Index Cond: (people.user_category = 2)
Planning Time: 7.546 ms
Execution Time: 189.186 ms

Alors que la passation de ILIKE

EXPLAIN ANALYZE VERBOSE

SELECT DISTINCT id
FROM people
WHERE user_category = 2
  AND (user_type != 'Ogre')
  AND (last_name ILIKE '%A62%')

Les résultats sont très rapides et en utilisant l'indice attendu. Qu'est-ce que c'est à propos de l'appel lower() semble rendre le planificateur sauter un battement?

Unique  (cost=161.51..161.62 rows=22 width=4) (actual time=27.144..27.570 rows=1677 loops=1)
  Output: id
  ->  Sort  (cost=161.51..161.56 rows=22 width=4) (actual time=27.137..27.256 rows=1677 loops=1)
        Output: id
        Sort Key: people.id
        Sort Method: quicksort  Memory: 127kB
        ->  Bitmap Heap Scan on public.people  (cost=32.34..161.02 rows=22 width=4) (actual time=16.470..26.798 rows=1677 loops=1)
              Output: id
              Recheck Cond: ((people.user_category = 2) AND ((people.last_name)::text ~~* '%A62%'::text))
              Filter: ((people.user_type)::text <> 'Ogre'::text)
              Rows Removed by Filter: 766
              Heap Blocks: exact=2291
              ->  Bitmap Index Scan on idx_people_gin_user_category_and_user_type_and_last_name  (cost=0.00..32.33 rows=33 width=0) (actual time=16.058..16.058 rows=2443 loops=1)
                    Index Cond: ((people.user_category = 2) AND ((people.last_name)::text ~~* '%A62%'::text))
Planning Time: 10.577 ms
Execution Time: 27.746 ms

Ensuite, ajoutant un autre champ dans les choses ...

EXPLAIN ANALYZE VERBOSE

SELECT DISTINCT id
FROM people
WHERE user_category = 2
  AND (user_type != 'Ogre')
  AND (last_name ILIKE '%A62%')
  AND (first_name ILIKE '%EAD%')

Est toujours assez rapide globalement

Unique  (cost=161.11..161.11 rows=1 width=4) (actual time=10.854..10.860 rows=12 loops=1)
  Output: id
  ->  Sort  (cost=161.11..161.11 rows=1 width=4) (actual time=10.853..10.854 rows=12 loops=1)
        Output: id
        Sort Key: people.id
        Sort Method: quicksort  Memory: 25kB
        ->  Bitmap Heap Scan on public.people  (cost=32.33..161.10 rows=1 width=4) (actual time=3.895..10.831 rows=12 loops=1)
              Output: id
              Recheck Cond: ((people.user_category = 2) AND ((people.last_name)::text ~~* '%A62%'::text))
              Filter: (((people.user_type)::text <> 'Ogre'::text) AND ((people.first_name)::text ~~* '%EAD%'::text))
              Rows Removed by Filter: 2431
              Heap Blocks: exact=2291
              ->  Bitmap Index Scan on idx_people_gin_user_category_and_user_type_and_last_name  (cost=0.00..32.33 rows=33 width=0) (actual time=3.173..3.173 rows=2443 loops=1)
                    Index Cond: ((people.user_category = 2) AND ((people.last_name)::text ~~* '%A62%'::text))
Planning Time: 0.257 ms
Execution Time: 10.897 ms

Pourtant, revenant à cet indice supplémentaire non tuple créé au bas et filtrant sur le courrier électronique semble utiliser un autre index dans le cadre des choses?

EXPLAIN ANALYZE VERBOSE

SELECT DISTINCT id
FROM people
WHERE user_category = 2
  AND (user_type != 'Ogre')
  AND (last_name ILIKE '%A62%')
  AND (email ILIKE '%0F9%')

A un chemin différent:

Unique  (cost=140.37..140.38 rows=1 width=4) (actual time=4.180..4.184 rows=7 loops=1)
  Output: id
  ->  Sort  (cost=140.37..140.38 rows=1 width=4) (actual time=4.180..4.180 rows=7 loops=1)
        Output: id
        Sort Key: people.id
        Sort Method: quicksort  Memory: 25kB
        ->  Bitmap Heap Scan on public.people  (cost=136.34..140.36 rows=1 width=4) (actual time=4.145..4.174 rows=7 loops=1)
              Output: id
              Recheck Cond: ((people.user_category = 2) AND ((people.last_name)::text ~~* '%A62%'::text) AND ((people.email)::text ~~* '%0F9%'::text))
              Filter: ((people.user_type)::text <> 'Ogre'::text)
              Rows Removed by Filter: 4
              Heap Blocks: exact=11
              ->  BitmapAnd  (cost=136.34..136.34 rows=1 width=0) (actual time=4.125..4.125 rows=0 loops=1)
                    ->  Bitmap Index Scan on idx_people_gin_user_category_and_user_type_and_last_name  (cost=0.00..32.33 rows=33 width=0) (actual time=3.089..3.089 rows=2443 loops=1)
                          Index Cond: ((people.user_category = 2) AND ((people.last_name)::text ~~* '%A62%'::text))
                    ->  Bitmap Index Scan on idx_people_gin_email  (cost=0.00..103.76 rows=10101 width=0) (actual time=0.879..0.879 rows=7138 loops=1)
                          Index Cond: ((people.email)::text ~~* '%0F9%'::text)
Planning Time: 0.291 ms
Execution Time: 4.217 ms

Le coût semble négliger, mais se demandant ce que cela signifie pour une quantité assez dynamique de colonnes pouvant être filtrées par? Serait-il idéal pour faire un index non tuple pour tous les champs aussi?

Désolé pour la longueur, j'ai filé mes roues pendant un moment essayant de comprendre tout cela, mais toute idée serait plutôt géniale (et il semble que ce ne soit pas une tonne d'index de gin comme celle-ci, bien que je manque peut-être quelque chose de plus global fondamental)

1
Jeff B.

L'arbre B d'origine est-il totalement redondant à ce stade? Je m'attendais à ce que le planificateur puisse toujours être cueilli par le planificateur si seulement ces deux colonnes étaient utilisées si le B-Tree était plus rapide pour ces types de données, mais cela semble que ce ne soit pas le cas.

Pas totalement. L'indice BTTREE peut être utilisé pour la commande (bien que pour 3 valeurs distinctes dans chaque colonne, ce n'est pas clair combien d'appel y aurait pour cela). Dans mes mains, l'indice B-Tree est effectivement plus rapide, mais pas par beaucoup. Je m'attendais à ce que cela soit plus rapide de plus. Même si je change le test! = Test sur = (qui est où B-Tree brille), l'index B-Tree n'est toujours que légèrement plus rapide. L'avantage Bree a dans les tests d'égalité multicolonneuse a été principalement annulé par l'avantage de Gin dans la compression du stockage des listes TDD, qui sont utiles lorsque chacune des 3 valeurs montre 333 333 heures. Ne vous attendez pas à ce que cela porte sur d'autres situations.

Les résultats sont très rapides et en utilisant l'indice attendu. Qu'est-ce que c'est à propos de l'appel inférieur () qui semble rendre le planificateur sauter un battement?

Vous devez construire l'index sur les choses que vous souhaitez rechercher. Si vous aviez construit l'index sur (user_category, user_type, lower(last_name) gin_trgm_ops);, Cela l'utiliserait. PostgreSQL sait juste que le texte inférieur () prend du texte et crache du texte. Il ne sait pas que lower(a) LIKE lower(b) implique que a ILIKE b.

Le coût semble négliger, mais se demandant ce que cela signifie pour une quantité assez dynamique de colonnes pouvant être filtrées par? Serait-il idéal pour faire un index non tuple pour tous les champs aussi?

Je ne sais pas ce que vous entendez par un index non tuple. Lorsque vous établissez un indice de gin N-Colonne, il s'agit de la même chose que de créer N index de gin à colonne. Le planificateur peut combiner des index individuels avec bitmapand et bitmapor, et Gin peut combiner plusieurs colonnes d'un indice de colonne MULI dans de la même manière, mais sans la transparence.

2
jjanes