web-dev-qa-db-fra.com

Pagination efficace pour les grandes tables

Utilisation de PostgreSQL 10.5 . J'essaie de créer un système de pagination où l'utilisateur peut aller et venir entre divers résultats.

Pour ne pas utiliser OFFSET, je passe le id de la dernière ligne de la page précédente dans un paramètre appelé p (prevId). Je sélectionne ensuite les trois premières lignes dont id est supérieur au nombre passé dans le paramètre p. (comme décrit dans cet article )

Par exemple, si le id pour la dernière ligne de la page précédente était 5, je sélectionnerais les 3 premières lignes avec un id supérieur à 5:

SELECT 
  id, 
  firstname, 
  lastname 
FROM 
  people 
WHERE 
  firstname = 'John'
  AND id > 5 
ORDER BY 
  ID ASC 
LIMIT 
  3;

Cela fonctionne très bien et le timing n'est pas très mal non plus:

Limit  (cost=0.00..3.37 rows=3 width=17) (actual time=0.046..0.117 rows=3 loops=1)
   ->  Seq Scan on people  (cost=0.00..4494.15 rows=4000 width=17) (actual time=0.044..0.114 rows=3 loops=1)
         Filter: ((id > 5) AND (firstname = 'John'::text))
         Rows Removed by Filter: 384
 Planning time: 0.148 ms
 Execution time: 0.147 ms

Si, en revanche, l'utilisateur souhaite revenir à la page précédente, les choses semblent un peu différentes:

Tout d'abord, je passerais le id pour la première ligne, puis je mettrais le signe moins devant pour indiquer que je devrais sélectionner les lignes avec un id inférieur à (positif) p paramètre. A savoir, si le id pour la première ligne est 6, le paramètre p serait -6. De même, ma requête ressemblerait à ceci:

SELECT 
  * 
FROM 
  (
    SELECT 
      id, 
      firstname, 
      lastname 
    FROM 
      people 
    WHERE 
      firstname = 'John' 
      AND id < 6 
    ORDER BY 
      id DESC 
    LIMIT 
      3
  ) as d 
ORDER BY 
  id ASC;

Dans la requête ci-dessus, je sélectionne d'abord les 3 dernières lignes avec un id inférieur à 6, puis je les inverse pour les présenter de la même manière que la première requête décrite au début.

Cela fonctionne comme il se doit, mais comme la base de données parcourt presque toutes mes lignes, les performances en souffrent:

Sort  (cost=4252.75..4252.76 rows=1 width=17) (actual time=194.464..194.464 rows=0 loops=1)
   Sort Key: people.id
   Sort Method: quicksort  Memory: 25kB
   ->  Limit  (cost=4252.73..4252.73 rows=1 width=17) (actual time=194.460..194.460 rows=0 loops=1)
         ->  Sort  (cost=4252.73..4252.73 rows=1 width=17) (actual time=194.459..194.459 rows=0 loops=1)
               Sort Key: people.id DESC
               Sort Method: quicksort  Memory: 25kB
               ->  Gather  (cost=1000.00..4252.72 rows=1 width=17) (actual time=194.448..212.010 rows=0 loops=1)
                     Workers Planned: 1
                     Workers Launched: 1
                     ->  Parallel Seq Scan on people  (cost=0.00..3252.62 rows=1 width=17) (actual time=18.132..18.132 rows=0 loops=2)
                           Filter: ((id < 13) AND (firstname = 'John'::text))
                           Rows Removed by Filter: 100505
Planning time: 0.116 ms
Execution time: 212.057 ms

Cela étant dit, j'apprécie que vous ayez pris le temps de lire jusqu'ici et ma question est, comment puis-je rendre la pagination plus efficace?

5
David

La clé de la performance est un index multicolonne correspondant de la forme:

CREATE UNIQUE INDEX ON people (firstname, id);

UNIQUE, car l'ordre de tri peut être ambigu sans lui, et vous pouvez obtenir des résultats arbitraires de vos pairs.

A UNIQUE ou PRIMARY KEY la contrainte sert aussi.

Alors que la première colonne est vérifiée pour l'égalité comme dans votre exemple (ou triée dans le même sens que la requête), cet index est bon pour la pagination de haut en bas, bien qu'il soit un peu mieux pour la pagination en haut.

Avec l'index en place (et après avoir exécuté ANALYZE sur la table), vous ne verrez plus d'analyses séquentielles (sauf si votre table est petite). La base de données ne "passe plus par presque toutes vos lignes".

Lisez l'amende présentation de Markus Winand vous liez.

Si vous souhaitez paginer sur plusieurs firstname, utilisez les valeurs ROW. Exemple de pagination vers le bas:

SELECT *
FROM  (
   SELECT id, firstname, lastname 
   FROM   people
   WHERE  (firstname, id) < ('John', 6)  -- ROW values
   ORDER  BY firstname DESC, id DESC 
   LIMIT  3
   ) d 
ORDER BY firstname, id;

En relation:

Si la liste SELECT n'ajoute que lastname comme dans votre exemple, vous pouvez essayer d'ajouter cette colonne à l'index pour obtenir analyses d'index uniquement hors de celui-ci:

CREATE UNIQUE INDEX ON people (firstname, id, lastname);

Indexez les expressions dans cet ordre.

Le prochain Postgres 11 permet aux index de INCLUDE colonnes , ce qui se traduit par une taille d'index plus petite, de meilleures performances et est applicable dans plus de situations. Comme:

CREATE UNIQUE INDEX ON people (firstname, id) INCLUDE (lastname);
7
Erwin Brandstetter