PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Ottieni righe impaginate e conteggio totale in una singola query

Per prima cosa:tu puoi utilizzare i risultati di un CTE più volte nella stessa query, questa è una funzione di CTE .) Quello che hai funzionerebbe in questo modo (mentre usi ancora il CTE una sola volta):

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

Avvertimento 1:rank()

rank() può restituire più righe per person_id con rank = 1 . DISTINCT ON (person_id) (come fornito da Gordon) è un sostituto applicabile per row_number() - che funziona per te, come ulteriori informazioni chiarite. Vedi:

Attenzione 2:ORDER BY submission_date DESC

submission_datelast_updated sono definiti NOT NULL . Può essere un problema con ORDER BY submission_date DESC, last_updated DESC ... Vedi:

Queste colonne dovrebbero essere davvero NOT NULL ?

Hai risposto:

Non sono consentite stringhe vuote per il tipo date . Mantieni le colonne annullabili. NULL è il valore corretto per quei casi. Usa NULLS LAST come dimostrato per evitare NULL ordinato in cima.

Avvertimento 3:OFFSET

Se OFFSET è uguale o maggiore del numero di righe restituite dal CTE, ottieni nessuna riga , quindi anche nessun conteggio totale. Vedi:

Soluzione provvisoria

Affrontando tutti gli avvertimenti finora e sulla base delle informazioni aggiunte, potremmo arrivare a questa domanda:

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;

Ora il CTE è in realtà usato due volte. Il RIGHT JOIN garantisce che otteniamo il conteggio totale, indipendentemente dal OFFSET . DISTINCT ON dovrebbe funzionare in modo corretto per le poche righe per (person_id) nella query di base.

Ma hai righe larghe. Quanto è largo in media? La query risulterà probabilmente in una scansione sequenziale sull'intera tabella. Gli indici non aiuteranno (molto). Tutto ciò rimarrà estremamente inefficiente per il paging . Vedi:

Non è possibile coinvolgere un indice per il paging poiché è basato sulla tabella derivata dal CTE. E i tuoi criteri di ordinamento effettivi per il paging non sono ancora chiari (ORDER BY id ?). Se il paging è l'obiettivo, hai un disperato bisogno di uno stile di query diverso. Se sei interessato solo alle prime pagine, hai ancora bisogno di uno stile di query diverso. La soluzione migliore dipende dalle informazioni ancora mancanti nella domanda...

Radialmente più veloce

Per il tuo obiettivo aggiornato:

(Ignorando "per criteri di filtro, tipo, piano, stato specificati" per semplicità.)

E:

Basato su questi due indici specializzati :

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);

Esegui questa query:

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;

Ogni serie di parentesi qui è obbligatoria.

Questo livello di sofisticazione dovrebbe recuperare un insieme relativamente piccolo di righe superiori in modo radicalmente più veloce utilizzando gli indici forniti e nessuna scansione sequenziale. Vedi:

submission_date molto probabilmente dovrebbe essere di tipo timestamptz o date , non character varying(255) - che in ogni caso è una definizione di tipo dispari in Postgres. Vedi:

Molti altri dettagli potrebbero essere ottimizzati, ma questo sta sfuggendo di mano. Potresti prendere in considerazione una consulenza professionale.