web-dev-qa-db-fra.com

Table partitionnée d'index pour empêcher les postgres de faire une analyse séquentielle

J'ai une base de données Postgres avec une table partitionnée qui contiendra environ 2 000 000 000 entrées.

J'ai défini une base de données partitionnée basée sur la première lettre de "Identifiant" - ceci est divisée en 37 sous-tables, [0-9, A-Z, défaut (Catchall pour tout le reste)].

La base de données est très simple et directe, et est définie ci-dessous.

create table entries (
  id                bigserial,
  identifier        text null,
  password          text null,
  additional_fields jsonb
)
  partition by list (lower(left(identifier, 1)));
ALTER DATABASE credentials SET constraint_exclusion=on;

CREATE TABLE entries_0 PARTITION OF entries for values in ('0');
CREATE TABLE entries_1 PARTITION OF entries for values in ('1');
CREATE TABLE entries_2 PARTITION OF entries for values in ('2');
CREATE TABLE entries_3 PARTITION OF entries for values in ('3');
...
CREATE TABLE entries_z PARTITION OF entries for values in ('z');

ALTER TABLE entries_0 ADD CONSTRAINT first_letter  CHECK (lower(left(identifier, 1)) = '0');
ALTER TABLE entries_1 ADD CONSTRAINT first_letter  CHECK (lower(left(identifier, 1)) = '1');
ALTER TABLE entries_2 ADD CONSTRAINT first_letter  CHECK (lower(left(identifier, 1)) = '2');
ALTER TABLE entries_3 ADD CONSTRAINT first_letter  CHECK (lower(left(identifier, 1)) = '3');
...
ALTER TABLE entries_z ADD CONSTRAINT first_letter  CHECK (lower(left(identifier, 1)) = 'z');

CREATE INDEX ident_idx on entries(identifier);

Cependant, lorsque j'exécute un EXPLAIN, il dit que cela fait toujours une analyse séquentielle.

EXPLAIN SELECT * FROM entries where identifier = 'some_identifier_from_subtable_s' LIMIT 1;

Production:

Limit  (cost=0.00..140.92 rows=1 width=104)
  ->  Append  (cost=0.00..43418531.72 rows=308113 width=104)
        ->  Seq Scan on entries_0  (cost=0.00..23239.38 rows=2 width=68)
              Filter: (identifier = 'some_identifier_from_subtable_s'::text)
        ->  Seq Scan on entries_1  (cost=0.00..150187.81 rows=6 width=68)
              Filter: (identifier = 'some_identifier_from_subtable_s'::text)
        ->  Seq Scan on entries_2  (cost=0.00..94694.38 rows=4 width=67)
              Filter: (identifier = 'some_identifier_from_subtable_s'::text)
        ->  Seq Scan on entries_3  (cost=0.00..81656.71 rows=3 width=67)
              Filter: (identifier = 'some_identifier_from_subtable_s'::text)

        ... etc.

        ->  Seq Scan on entries_z  (cost=0.00..579207.95 rows=13 width=69)
              Filter: (identifier = 'some_identifier_from_subtable_s'::text)
        ->  Seq Scan on entries_default  (cost=0.00..15582.36 rows=4 width=69)
              Filter: (identifier = 'some_identifier_from_subtable_s'::text)

Qu'est-ce que je fais mal? Le partitionnement intelligent devrait être capable de rediriger la requête à la partition entries_s, ne devrait-elle pas? Et puis la fonction CREATE INDEX ident_idx on entries(identifier); devrait faire passer la requête dans l'index?

Après avoir ajouté la requête explicite, le nouveau plan ressemble à ceci:

FROM   entries
WHERE  identifier = 'some_identifier_from_subtable_s'
AND    lower(left(identifier, 1)) = 's'  -- 1st letter of above identifier
LIMIT  1;

Production:

Limit  (cost=1000.00..2583053.76 rows=1 width=71)
  ->  Gather  (cost=1000.00..2583053.76 rows=1 width=71)
        Workers Planned: 2
        ->  Parallel Append  (cost=0.00..2582053.66 rows=1 width=71)
              ->  Parallel Seq Scan on entries_s  (cost=0.00..2582053.66 rows=1 width=71)
                    Filter: ((identifier = 'some_identifier_from_subtable_s'::text) AND (lower("left"(identifier, 1)) = 's'::text))

Cela fait toujours une analyse séquentielle sur entries_s. Déclarant l'index avec CREATE INDEX ident_idx on entries(identifier); non propagez-la à toutes les partitions?

3
JonLuca

Essayez d'ajouter la clé de partition explicitement (redondante!). Comme:

SELECT *
FROM   entries 
WHERE  identifier = 'some_identifier_from_subtable_s'
AND    lower(left(identifier, 1)) = 's'  -- 1st letter of above identifier
LIMIT  1;

Cela devrait permettre aux postgres de comprendre qu'il peut tailler toutes les autres partitions de la requête.

Peut être dérivé de $1 bien sûr:

AND    lower(left(identifier, 1)) = lower(left($1, 1))

Vous verrez toujours une analyse séquentielle sur la partition unique, sauf si vous créez un index comme si vous aviez à l'esprit:

CREATE INDEX ident_idx on entries(identifier);

Cela fonctionne à Postgres 11 ou plus tard car, citant le manuel :

Lorsque CREATE INDEX est invoqué sur une table partitionnée, le comportement par défaut consiste à recueillir toutes les partitions pour s'assurer qu'elles disposent d'index correspondants.

Dans Postgres 10 ou plus, vous devez créer des index par partition.

Vous devrez peut-être exécuter ANALYZE sur la table après avoir créé l'index si vous suivez la requête immédiatement, avant que Autovacuum ait eu le temps de lancer.

2
Erwin Brandstetter