web-dev-qa-db-ja.com

大きなテーブルの効率的なページネーション

PostgreSQL 10.5を使用します。ユーザーがさまざまな結果の間を行き来できるページネーションシステムを作成しようとしています。

OFFSETを使用しないようにするために、前のページの最後の行のidp(prevId)というパラメーターで渡します。次に、idpパラメーターで渡された数よりも大きい最初の3つの行を選択します。 ( この記事 で説明されています)

たとえば、前のページの最後の行のidが5だった場合、idが5より大きい最初の3行を選択します。

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

これはうまく機能し、タイミングもそれほど悪くありません:

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

一方、ユーザーが前のページに戻りたい場合は、状況が少し異なります。

最初に、最初の行にidを渡してから、その前にマイナス記号を付けて、idが(正の)pパラメーターより小さい行を選択する必要があることを示します。つまり、最初の行のidが6の場合、pパラメータは-6。同様に、私のクエリは次のようになります。

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;

上記のクエリでは、最初にidが6未満の最後の3行を選択し、最初に説明した最初のクエリと同じ方法で表示するためにそれらを逆にします。

これは正常に機能しますが、データベースはほとんどすべての行を通過するため、パフォーマンスが低下しています。

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

これが言われて、私はあなたがここまで読んで時間を割いてくれたことに感謝します、そして私の質問はどのようにページネーションをより効率的にすることができますか?

5
David

パフォーマンスの鍵は、次の形式の一致するマルチカラムインデックスです。

CREATE UNIQUE INDEX ON people (firstname, id);

UNIQUE、それがないとソート順があいまいになる可能性があるため、ピアから任意の結果を得ることができます。

UNIQUEまたはPRIMARY KEY制約も機能します。

最初の列は例のように等しいかどうか確認されます(またはクエリと同じ方向に並べ替えられます)が、このインデックスはページングupおよびdownに適していますが、少しですページングupに適しています。

インデックスを配置すると(およびテーブルでANALYZEを実行した後)、シーケンシャルスキャンは表示されなくなります(テーブルがsmallでない限り)。データベースは「ほとんどすべての行を通過」しなくなりました。

リンクしているすばらしい Markus Winandによるプレゼンテーション を読んでください。

複数のfirstnameにわたるページングが必要な場合は、ROW値を使用します。ページングの例:

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;

関連:

IfSELECTリストは、例のようにlastnameのみを追加します。その列をインデックスに追加しようとする場合があります。 インデックスのみのスキャン を取得するには:

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

この順序でインデックス式。

今後の Postgres 11では、INCLUDEカラム 。これにより、インデックスサイズが小さくなり、パフォーマンスが向上し、より多くの状況に適用できます。お気に入り:

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