PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Obtenez des lignes paginées et le nombre total en une seule requête

Tout d'abord :vous pouvez utiliser les résultats d'un CTE plusieurs fois dans la même requête, c'est une fonctionnalité principale de CTE .) Ce que vous avez fonctionnerait comme ceci (tout en utilisant le CTE une seule fois) :

WITH cte AS (
   SELECT * FROM (
      SELECT *, row_number()  -- see below
                OVER (PARTITION BY person_id
                      ORDER BY submission_date DESC NULLS LAST  -- see below
                             , last_updated DESC NULLS LAST  -- see below
                             , id DESC) AS rn
      FROM  tbl
      ) sub
   WHERE  rn = 1
   AND    status IN ('ACCEPTED', 'CORRECTED')
   )
SELECT *, count(*) OVER () AS total_rows_in_cte
FROM   cte
LIMIT  10
OFFSET 0;  -- see below

Mise en garde 1 :rank()

rank() peut renvoyer plusieurs lignes par person_id avec rank = 1 . DISTINCT ON (person_id) (comme Gordon fourni) est un remplacement applicable pour row_number() - qui fonctionne pour vous, en tant qu'informations supplémentaires clarifiées. Voir :

Mise en garde 2 :ORDER BY submission_date DESC

Ni submission_date ni last_updated sont définis NOT NULL . Peut être un problème avec ORDER BY submission_date DESC, last_updated DESC ... Voir :

Ces colonnes doivent-elles vraiment être NOT NULL ?

Vous avez répondu :

Les chaînes vides ne sont pas autorisées pour le type date . Gardez les colonnes nullables. NULL est la valeur appropriée pour ces cas. Utilisez NULLS LAST comme démontré pour éviter NULL étant trié en haut.

Mise en garde 3 :OFFSET

Si OFFSET est égal ou supérieur au nombre de lignes renvoyées par le CTE, vous obtenez aucune ligne , donc pas de décompte total non plus. Voir :

Solution provisoire

En tenant compte de toutes les mises en garde jusqu'à présent, et sur la base d'informations supplémentaires, nous pourrions arriver à cette requête :

WITH cte AS (
   SELECT DISTINCT ON (person_id) *
   FROM   tbl
   WHERE  status IN ('ACCEPTED', 'CORRECTED')
   ORDER  BY person_id, submission_date DESC NULLS LAST, last_updated DESC NULLS LAST, id DESC
   )
SELECT *
FROM  (
   TABLE  cte
   ORDER  BY person_id  -- ?? see below
   LIMIT  10
   OFFSET 0
   ) sub
RIGHT  JOIN (SELECT count(*) FROM cte) c(total_rows_in_cte) ON true;

Maintenant, le CTE est en fait utilisé deux fois. Le RIGHT JOIN garantit que nous obtenons le nombre total, quel que soit le OFFSET . DISTINCT ON devrait fonctionner correctement pour les quelques lignes seulement par (person_id) dans la requête de base.

Mais vous avez de larges rangées. Quelle largeur en moyenne ? La requête entraînera probablement une analyse séquentielle sur l'ensemble de la table. Les index n'aideront pas (beaucoup). Tout cela restera extrêmement inefficace pour la pagination . Voir :

Vous ne pouvez pas impliquer un index pour la pagination car celui-ci est basé sur la table dérivée du CTE. Et vos critères de tri réels pour la pagination ne sont toujours pas clairs (ORDER BY id ?). Si la pagination est l'objectif, vous avez désespérément besoin d'un style de requête différent. Si vous n'êtes intéressé que par les premières pages, vous avez encore besoin d'un style de requête différent. La meilleure solution dépend des informations encore manquantes dans la question...

Rapidement plus rapide

Pour votre objectif mis à jour :

(Ignorer "pour les critères de filtre spécifiés, le type, le plan, le statut" pour plus de simplicité.)

Et :

Sur la base de ces deux indices spécialisés :

CREATE INDEX ON tbl (submission_date DESC NULLS LAST, last_updated DESC NULLS LAST, id DESC NULLS LAST)
WHERE  status IN ('ACCEPTED', 'CORRECTED'); -- optional

CREATE INDEX ON tbl (person_id, submission_date DESC NULLS LAST, last_updated DESC NULLS LAST, id DESC NULLS LAST);

Exécutez cette requête :

WITH RECURSIVE cte AS (
   (
   SELECT t  -- whole row
   FROM   tbl t
   WHERE  status IN ('ACCEPTED', 'CORRECTED')
   AND    NOT EXISTS (SELECT FROM tbl
                      WHERE  person_id = t.person_id 
                      AND   (  submission_date,   last_updated,   id)
                          > (t.submission_date, t.last_updated, t.id)  -- row-wise comparison
                      )
   ORDER  BY submission_date DESC NULLS LAST, last_updated DESC NULLS LAST, id DESC NULLS LAST
   LIMIT  1
   )

   UNION ALL
   SELECT (SELECT t1  -- whole row
           FROM   tbl t1
           WHERE ( t1.submission_date, t1.last_updated, t1.id)
               < ((t).submission_date,(t).last_updated,(t).id)  -- row-wise comparison
           AND    t1.status IN ('ACCEPTED', 'CORRECTED')
           AND    NOT EXISTS (SELECT FROM tbl
                              WHERE  person_id = t1.person_id 
                              AND   (   submission_date,    last_updated,    id)
                                  > (t1.submission_date, t1.last_updated, t1.id)  -- row-wise comparison
                              )
           ORDER  BY submission_date DESC NULLS LAST, last_updated DESC NULLS LAST, id DESC NULLS LAST
           LIMIT  1)
   FROM   cte c
   WHERE  (t).id IS NOT NULL
   )
SELECT (t).*
FROM   cte
LIMIT  10
OFFSET 0;

Chaque jeu de parenthèses ici est obligatoire.

Ce niveau de sophistication devrait récupérer un ensemble relativement petit de lignes supérieures radicalement plus rapidement en utilisant les indices donnés et sans analyse séquentielle. Voir :

submission_date devrait très probablement être de type timestamptz ou date , pas character varying(255) - qui est une définition de type étrange dans Postgres dans tous les cas. Voir :

Beaucoup plus de détails pourraient être optimisés, mais cela devient incontrôlable. Vous pourriez envisager de consulter un professionnel.