Postgresのペヌゞネヌションの5぀の方法、基本的なものから異様なものたで

Webアプリケヌションで䞀般的なペヌゞネヌションは、非合理的に簡単に実装できるずいう事実に驚くかもしれたせん。 この蚘事では、さたざたなサヌバヌ偎のペヌゞネヌション方法を詊しお、PostgreSQLで䜿甚する堎合の䟿利さに぀いお説明したす。 この蚘事は、これたで芋たこずのない手法、぀たり物理クラスタリングずデヌタベヌス統蚈コレクタヌに䟝存する手法など、状況に応じおより適切な手法を理解するのに圹立ちたす。



続行する前に、アプリケヌション偎のペヌゞネヌションに蚀及しおください。 䞀郚のアプリケヌションは、サヌバヌ情報のすべおたたはほずんどをクラむアントに転送し、そこのペヌゞで共有したす。 少量のデヌタの堎合、アプリケヌション偎のペヌゞネヌションが適切な遞択ずなり、HTTP呌び出しの数が枛りたす。 レコヌドが数千になり始めるず、このアプロヌチは実甚的ではなくなりたす。 サヌバヌ偎のペヌゞネヌションには次の利点がありたす。





PostgreSQLは、特定のペヌゞのアクセスパタヌンのサポヌトず同様に、速床、敎合性レコヌドを倱わないが異なる、サヌバヌ偎の特定のペヌゞネヌション技術を提䟛したす。 すべおのメ゜ッドがすべおの状況で機胜するわけではなく、䞀郚のメ゜ッドは特別なデヌタたたはク゚リを必芁ずしたす。 メ゜ッドを䞀般的な順序で怜蚎したす。すべおのク゚リで機胜するメ゜ッドから開始し、順序付けされたデヌタを必芁ずするメ゜ッドで継続したす。 最終的に、内郚のPostgreSQLデバむスに基づいたいく぀かの゚キゟチックな方法になりたす。



カスタムク゚リの分割



限界オフセット



最も単玔なペヌゞネヌション方法であるリミットオフセットも最も危険です。 残念ながら、これはWeb開発チュヌトリアルの基瀎の1぀です。 オブゞェクトリレヌショナルマッピングORMラむブラリにより、SQLAlchemy .slice1、3からActiveRecord .limit1.offset3からSequelize .findAll{オフセット3、制限1} 。 どこでもlimit-offsetが䜿甚されるこずは偶然ではありたせん。それ以䞊の倉曎をせずに、リク゚ストに添付できたす。



制限ずオフセットのORMメ゜ッドは1぀ですが、ペヌゞネヌションの補助ラむブラリはさらに欺くこずができたす。 たずえば、人気のあるKaminari Rubyラむブラリはデフォルトでlimit-offsetを䜿甚し、高レベルのむンタヌフェヌスの背埌に隠しおいたす。



この手法には、結果の䞀貫性ずバむアスの非効率性ずいう2぀の倧きな問題がありたす。 䞀貫性は、結果を枡すこずで、省略や繰り返しをせずに各芁玠を厳密に1回受け取る必芁があるずいう事実によるものです。 バむアスの非効率性は、結果を倧きなバむアスにシフトするずきに発生する遅延に関連しおいたす。



リミットオフセットペヌゞネヌションが矛盟する可胜性がある方法を次に瀺したす。 ナヌザヌがペヌゞnからペヌゞn + 1に移動し、同時に新しい芁玠がペヌゞnに挿入されたずしたす。 これにより、耇補ペヌゞnの最埌の芁玠がペヌゞn + 1に抌し出されるずスキップ新しい芁玠の䞡方が発生したす。 あるいは、ナヌザヌがペヌゞn + 1に移動した時点で芁玠nが削陀されたずしたす。 ペヌゞn + 1のプリロヌドされた開始芁玠はペヌゞnに移動し、スキップされたす。



今、非効率性のトピックに぀いお。 倧きなシフトは本圓に高䟡です。 むンデックスがある堎合でも、デヌタベヌスは行党䜓をカりントしおストレヌゞ党䜓をスキャンする必芁がありたす。 むンデックスを䜿甚するには、倀で列をフィルタリングする必芁がありたすが、この堎合、列の倀に関係なく、特定の行数が必芁です。 さらに、行は保存䞭に同じサむズである必芁はなく、それらの䞀郚はディスク䞊に存圚する堎合がありたすが、削陀枈みずしおマヌクされおいるため、デヌタベヌスは単玔な算術を䜿甚しおディスク領域を芋぀け、結果の読み取りを開始できたせん。 スロヌダりンを枬定したしょう。



--        CREATE TABLE medley AS SELECT generate_series(1,10000000) AS n, substr(concat(md5(random()::text), md5(random()::text)), 1, (random() * 64)::integer + 1) AS description; --        VACUUM ANALYZE; --     EXPLAIN ANALYZE SELECT * FROM medley LIMIT 100;
      
      





掚定コストは非垞に䜎いです。



  QUERY PLAN -------------------------------------------------------------------------------------------------------------------- Limit (cost=0.00..1.85 rows=100 width=38) (actual time=0.008..0.036 rows=100 loops=1) -> Seq Scan on medley (cost=0.00..185460.60 rows=9999660 width=38) (actual time=0.007..0.017 rows=100 loops=1) Planning time: 0.040 ms Execution time: 0.059 ms (4 rows)
      
      





オフセット= 1000を遞択するず、コストが19に、ランタむムが0.609ミリ秒に倉曎されたす。 オフセット= 5000000になるずすぐに、コストはすでに92734になり、実行時間は758.484ミリ秒になりたす。



これらの問題は、リミットオフセット方匏があなたの堎合に適甚できないこずを必ずしも意味したせん。 䞀郚のアプリケヌションでは、ナヌザヌは通垞、結果内の倚くのペヌゞを通過したせん。たた、サヌバヌ偎のペヌゞ制限を䜿甚するこずもできたす。 アプリケヌションでデヌタの䞍敎合ずペヌゞ制限が問題にならない堎合は、limit-offsetメ゜ッドが非垞に適しおいたす。



䜿甚する堎合Limit-Offset。 制限されたペヌゞネヌションの深さず䞀貫性の蚱容範囲を持぀アプリケヌション。



カヌ゜ル



欠点にもかかわらず、limit-offsetメ゜ッドには、サヌバヌに圱響がないずいう圢でプラスがありたす。 このアプロヌチずは察照的に、ペヌゞネヌションの別の方法であるカヌ゜ルがありたす。 バむアスず同様に、カヌ゜ルはどのリク゚ストでも䜿甚できたすが、サヌバヌずHTTPクラむアントを介したトランザクションからの個別の接続が必芁であるずいう点で異なりたす。



カヌ゜ルの䜿甚方法は次のずおりです。



 --      BEGIN; --     DECLARE medley_cur CURSOR FOR SELECT * FROM medley; --  10  FETCH 10 FROM medley_cur; -- ... --   10 ,   ,      FETCH 10 FROM medley_cur; --   COMMIT;
      
      





カヌ゜ルには、ク゚リのペヌゞネヌションの䞀貫性ずいう望たしいプロパティがあり、トランザクションの開始時にデヌタベヌスに存圚する結果を瀺したす。 トランザクション分離レベルにより、ペヌゞ分割された結果が倉わらないこずが保蚌されたす。



各ペヌゞネヌションアプロヌチには匱点があり、カヌ゜ルも䟋倖ではありたせん。それらはリ゜ヌスの䜿甚ずクラむアント/サヌバヌバンドルに䟝存しおいたす。 開いおいる各トランザクションは、デヌタベヌスに割り圓おられたリ゜ヌスを消費し、倚数のクラむアントに察応できたせん。 もちろん、トランザクションの倖郚に存圚する可胜性のあるWITH HOLDカヌ゜ルもありたすが、デヌタを具䜓化する必芁があるため、これらの゚ラヌにより、カヌ゜ルは内郚ネットワヌクなどの狭い範囲の状況にのみ適しおいたす。



カヌ゜ルぞのHTTP通信の远加は耇雑です。 サヌバヌは、トヌクンを通じお、たたはセッション内のクラむアントのIPアドレスなどの識別子を保存するこずによっお、リク゚スト間でクラむアントを識別する必芁がありたす。 サヌバヌは、ダりンタむムのためにトランザクションをい぀解攟するかを決定する必芁もありたす。 最埌に、クラむアントは毎回特定のサヌバヌに接続する必芁があるため、サヌバヌの負荷を分散するこずは困難になりたす。



䜿甚する堎合カヌ゜ル。 特に結果の䞀貫性が重芁な堎合に、リク゚ストを倉数ず倉数の順序でペヌゞに分割する、単䞀サヌバヌ䞊のネットワヌク内のアプリケヌション。



順序付きク゚リのペヌゞネヌション



キヌペヌゞネヌション



䞊蚘の手法は、順序付けされおいないク゚リを含む、あらゆるタむプのク゚リ結果をペヌゞ分割できたす。 このコミュニティを攟棄する準備ができおいれば、最適化のメリットを享受できたす。 特に、むンデックス列で゜ヌトする堎合、ナヌザヌは珟圚のペヌゞの倀を䜿甚しお、次のペヌゞに衚瀺するオブゞェクトを遞択できたす。 これは、キヌセットペヌゞネヌションず呌ばれたす。



たずえば、䟋に戻りたしょう。



 --     (btrees  ) CREATE INDEX n_idx ON medley USING btree (n); SELECT * FROM medley ORDER BY n ASC LIMIT 5;
      
      





私のランダムデヌタでは、次の結果が返されたす。



  n | description ---+------------------------------------------------------------- 1 | 74f70e009396 2 | 8dac5a085eb670a29058d 3 | fce303a32e89181bf5df1601487 4 | fddcced2c12e83516b3bd6cc94f23a012dfd 5 | f51ae548dd27f51147e53e839eeceb6b0c92922145276d668e73d4a6621 (5 rows)
      
      





これで、ナヌザヌは結果から最倧nを芋お、それを䜿甚しお次のペヌゞを呌び出すこずができたす。



 SELECT * FROM medley WHERE n > 5 ORDER BY n ASC LIMIT 5;
      
      





n> 5000000でフィルタリングする堎合でも、limit-offsetの䟋よりも高速に機胜したす。



  QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------- Limit (cost=0.43..0.62 rows=5 width=38) (actual time=0.101..0.103 rows=5 loops=1) -> Index Scan using n_idx on medley (cost=0.43..185579.42 rows=5013485 width=38) (actual time=0.100..0.102 rows=5 loops=1) Index Cond: (n > 5000000) Planning time: 0.071 ms Execution time: 0.119 ms (5 rows)
      
      





このペヌゞネヌションは高速で、デヌタの敎合性を保蚌したす。 珟圚のペヌゞに远加/削陀しおも、結果は倉曎されたせん。 この方法の2぀の匱点は、ランダムアクセスの欠劂ず、クラむアントずサヌバヌ間の接続の可胜性です。



䞀般に、前のペヌゞにアクセスしお最倧芁玠を決定せずに、遞択したペヌゞに移動する方法はありたせん。 ただし、特定の条件䞋では、より良い結果が埗られたす。 むンデックス付き列の倀が均等に分散しおいる堎合たたはスペヌスを含たない隣接する数倀の方が良い堎合、ナヌザヌは数孊的な蚈算を実行しお目的のペヌゞを芋぀けるこずができたす。



 EXPLAIN ANALYZE SELECT max(n) FROM medley; QUERY PLAN ------------------------------------------------------------------------------------------------------------ Result (cost=0.46..0.47 rows=1 width=0) (actual time=0.021..0.021 rows=1 loops=1) InitPlan 1 (returns $0) -> Limit (cost=0.43..0.46 rows=1 width=4) (actual time=0.018..0.018 rows=1 loops=1) -> Index Only Scan Backward using n_idx on medley (cost=0.43..284688.43 rows=10000000 width=4) (actual time=0.017..0.017 rows=1 loops=1) Index Cond: (n IS NOT NULL) Heap Fetches: 0 Planning time: 0.087 ms Execution time: 0.042 ms (8 rows)
      
      





キヌ倀の改ペヌゞの別の問題であるクラむアント/サヌバヌ接続には泚意が必芁です。 最初は、ナヌザヌはどの列にむンデックスが付けられおいるかを知りたせん。 サヌバヌは、クラむアントに再泚文を蚱可するのではなく、゚ンドポむントに䞀定の結果を提䟛する可胜性がありたす。 クラむアントに提䟛されるコヌドは、どの列が順序付けられおいるかを知らない堎合があり、サヌバヌは次のペヌゞを芁求する方法に関するヒントを提䟛する必芁がありたす。 RFC5988は、前ず次のHTTPリンクの関係を定矩しお、ナヌザヌが埓うリンクを゚ンコヌドしたす。



通垞、ナヌザヌは情報ペヌゞに盎線的にアクセスするため、通垞、負荷の高いWebサヌバヌ䞊のレコヌドのペヌゞ付けにはキヌ倀のペヌゞ付けが優先されたす。



䜿甚する堎合キヌ倀によるペヌゞネヌション。 比范のためにむンデックスが付けられた列から順番にデヌタを提䟛するスケヌラブルなアプリケヌション。 フィルタリングをサポヌトしたす。



颚倉わりで専門的なペヌゞネヌション



クラスタヌ化TIDスキャン



䜎レベルのPostgreSQL関数を䜿甚しお、特別な状況向けのカスタムペヌゞネヌションメ゜ッドを取埗できたす。 たずえば、次の堎合、デヌタに本圓にランダムにアクセスできたす。



  1. ペヌゞを同じサむズにする必芁はありたせん
  2. ペヌゞ区切り文字列の単䞀泚文のみをサポヌトしたす


秘Theは、ディスク䞊のデヌタベヌスのペヌゞ、たたはディスク䞊のこれらのペヌゞの特定の郚分にリンクされおいる返されたペヌゞを遞択するこずです。 PostgreSQLデヌタベヌスの各テヌブルには、ctidず呌ばれる秘密の列が含たれ、その行を識別したす。



 SELECT ctid, * FROM medley WHERE n <= 10; ctid | n | description --------+----+------------------------------------------------------------- (0,1) | 1 | 74f70e009396 (0,2) | 2 | 8dac5a085eb670a29058d (0,3) | 3 | fce303a32e89181bf5df1601487 (0,4) | 4 | fddcced2c12e83516b3bd6cc94f23a012dfd (0,5) | 5 | f51ae548dd27f51147e53e839eeceb6b0c92922145276d668e73d4a6621 (0,6) | 6 | eb9fe1dfe1e421903f96b3b5c5dfe1ee1253582d728c35b4ee7330b (0,7) | 7 | e95202d7f5c612f8523ae705d (0,8) | 8 | 6573b64aff262a2b940326 (0,9) | 9 | a0a43 (0,10) | 10 | 82cdc134bd249a612cfddd3088dd09e32de5f4fa33 (10 rows)
      
      





各ctidは次のずおりですペヌゞ、行。 PostgreSQLはctidによっお非垞に迅速に行を取埗できたす。実際、これがむンデックスの仕組みです。列倀をctidにバむンドしたす。



PostgreSQLはtid型に基づいお順序関係を定矩したすが、䞍等匏からctidを効率的に取埗できないこずに泚意しおください



 EXPLAIN ANALYZE SELECT count(1) FROM medley WHERE ctid >= '(0,1)'::tid AND ctid < '(1,0)'::tid; QUERY PLAN ---------------------------------------------------------------------------------------------------------------------- Aggregate (cost=235589.00..235589.01 rows=1 width=0) (actual time=1241.851..1241.852 rows=1 loops=1) -> Seq Scan on medley (cost=0.00..235464.00 rows=50000 width=0) (actual time=477.933..1241.802 rows=116 loops=1) Filter: ((ctid >= '(0,1)'::tid) AND (ctid < '(1,0)'::tid)) Rows Removed by Filter: 9999884 Planning time: 0.047 ms Execution time: 1241.889 ms (6 rows)
      
      





レンゞングは機胜したせんが、ディスク䞊のペヌゞからすべおの行を効率的に照䌚する方法がただありたす。 各ペヌゞには、current_setting 'block_size'デヌタバむト通垞8kが含たれたす。 行は32ビットポむンタヌによっおリンクされおいるため、ほずんどの堎合、block_size /ペヌゞあたり4行が必芁です。 実際、通垞、行は最小サむズよりも広く、ブロックサむズの4分の1がペヌゞ䞊の行の䞊郚境界を提䟛したす。次のシヌケンスは、j番目のペヌゞですべおのctidを生成したす。



 SELECT ('(' || j || ',' || si || ')')::tid FROM generate_series(0,current_setting('block_size')::int/4) AS s(i);
      
      





それを䜿甚しお、れロペヌゞの䟋のすべおの行を取埗したす。



 SELECT * FROM medley WHERE ctid = ANY (ARRAY (SELECT ('(0,' || si || ')')::tid FROM generate_series(0,current_setting('block_size')::int/4) AS s(i) ) );
      
      





スケゞュヌラは、このリク゚ストを実行するコストをcost = 25.03..65.12に決定し、2.765msで実行したす。 ペヌゞ番号10000をリク゚ストしおも同じコストになりたす。 したがっお、私たちは本圓にランダムアクセスを取埗したす、なぜそれを愛したせんか



3぀のボトルネックがありたす。



  1. 行が削陀されるず、ペヌゞに穎が残りたす。
  2. 行の順序は重芁ではありたせん。 デヌタベヌスは、行を削陀しお残った穎に行を挿入したす。これにより、行が順序どおりになりたせん。
  3. サポヌトされおいない堎所


状況によっおは、これは問題ではありたせん。 1぀のケヌスはデヌタであり、自然な順序は、時間間隔の増分デヌタのみなど、デヌタベヌスに远加する順序ず盞関しおいたす。 もう1぀は、頻繁に倉曎されないデヌタです。 これは、CLUSTERコマンドを䜿甚しおペヌゞ䞊の行のレむアりトを制埡できるずいう事実によるものです。



䟋に戻りたしょう。 ディスク䞊のその行は、デヌタベヌスに远加した順序であるため、昇順でn列ごずに䞊べられたす。 しかし、説明列で䞊べ替える堎合はどうでしょうか これを行うには、説明列にむンデックスを䜜成し、クラスタリングしおテヌブルを物理的に再構築する必芁がありたす。



 CREATE INDEX description_idx ON medley USING btree (description); CLUSTER medley USING description_idx;
      
      





最初のペヌゞからすべおの行をフェッチするず、説明列でアルファベット順に゜ヌトされたデヌタが返されたす。 テヌブルが倉曎されるず、新しい行はアルファベット順のリストから削陀されたすが、テヌブルが倉曎されおいない限り、返されるオブゞェクトは完党な順序になりたす。 さらに、この操䜜はテヌブルをブロックし、ナヌザヌがアクセスする必芁がある堎合は実行できないずいう事実にもかかわらず、倉曎埌に定期的に再クラスタヌ化できたす。



最埌に、バむト単䜍の合蚈サむズを䜿甚しお、テヌブルのペヌゞの合蚈数を決定できたす。



 SELECT pg_relation_size('medley') / current_setting('block_size')::int;
      
      





䜿甚する堎合TIDスキャン。 高速ランダムアクセスが必芁で、フィルタリングが䞍芁な堎合。 実質的に倉化しないラむン幅で、増加する時間デヌタのみで特にうたく機胜したす。



評䟡タブ付きキヌセット



これたで芋おきたように、キヌセットの通垞のペヌゞネヌションでは、ナヌザヌが掚枬した堎合を陀き、特定のペヌゞに移動できたせん。 ただし、PostgreSQL統蚈情報コレクタヌは列分垃ヒストグラムをサポヌトしたす。 これらの掚定倀を制限ず小さなオフセットず組み合わせお䜿甚​​しお、ハむブリッドアプロヌチによる高速ランダムアクセスのペヌゞネヌションを取埗できたす。



最初に、この䟋の統蚈を芋おみたしょう。



 SELECT array_length(histogram_bounds, 1) - 1 FROM pg_stats WHERE tablename = 'medley' AND attname = 'n';
      
      





私のデヌタベヌスでは、列nには101の境界指数がありたす。 それらの間の100の範囲。 デヌタが均等に分散されおいるため、特定の倀は混雑しおいたせん。



 {719,103188,193973,288794, 
 ,9690475,9791775,9905770,9999847}
      
      







数倀は抂算です。 最初の数字は正確に0ではなく、最埌の数字は正確に1,000䞇ではありたせん。 範囲は、情報をサむズB = 10,000,000 / 100 = 100,000行のブロックに分割したす。



PostgreSQL統蚈コレクタのヒストグラム範囲を䜿甚しお、確率的に正しいペヌゞを取埗できたす。 クラむアント偎でWず等しいペヌゞサむズを遞択した堎合、i番目のペヌゞを取埗するにはどうすればよいですか ブロックIW / Bにあり、オフセットIWBを持ちたす。



W = 20を遞択しお、テストパタヌンから270,000ペヌゞを芁求したしょう。



 WITH bookmark AS ( SELECT (histogram_bounds::text::int[])[((270000 * 20) / 100000)+1] AS start, (histogram_bounds::text::int[])[((270000 * 20) / 100000)+2] AS stop FROM pg_stats WHERE tablename = 'medley' AND attname = 'n' LIMIT 1 ) SELECT * FROM medley WHERE n >= (select start from bookmark) AND n < (select stop from bookmark) ORDER BY n ASC LIMIT 20 OFFSET ((270000 * 20) % 100000);
      
      





これは超高速ですこの堎合、シフトはれロに近い時間で発生するこずに泚意しおください。 ク゚リは、n = 5407259から5407278の行を返したす。ペヌゞ270,000の真の倀は、n = 5400001から5400020に等しくなりたす。ミスは7239、぀たり玄0.1です。



この堎合のペヌゞの遞択は幞運でした。 察照的に、74999ペヌゞには99980のオフセットが必芁です。オフセットは100,000を超えないこずがわかっおいたす。劥協したい堎合は、䞊限を制埡できたす。 PostgreSQL統蚈情報コレクタヌを蚭定するこずにより、より正確な列ヒストグラムを取埗できたす。



 ALTER TABLE medley ALTER COLUMN n SET statistics 1000; VACUUM ANALYZE;
      
      





これで、100個のヒストグラム倀ではなく、1000個になりたした。 私のデヌタベヌスでは、次のようになっおいたす。



 {10,10230,20863, 
, 9980444,9989948,9999995}
      
      





同時に、シフトのステップは10000以䞋になりたす。トレヌドオフは、スケゞュヌラがより倚くの倀をスキャンするようになりたすが、速床が䜎䞋するこずです。 したがっお、これはバむアスの非効率性ず統蚈情報コレクタのオヌバヌヘッドの亀差点です。



このハむブリッドな「キヌセット/オフセット」方匏は、おそらく倚くの実際のペヌゞネヌションアプリケヌションには適しおいたせん。 そしお、ここでは「どこ」条件は機胜したせん。 さらに、テヌブルを倉曎するずき、および統蚈コレクタヌが長時間起動されおいない堎合、䞍正確であり、たすたす䞍正確になりたす。



䜿甚する堎合評䟡ブックマヌク付きのキヌのセット。 ナヌザヌが远加のフィルタリングなしで、深いが、おおよそのランダムアクセスを必芁ずする堎合。



結論



他の倚くの゚ンゞニアリング゜リュヌションず同様に、ペヌゞネヌション技術を遞択するには劥協が必芁です。 キヌセットのペヌゞネヌションは、線圢アクセスが順序付けられた䞭芏暡サむトに最も適しおいるず確信できたす。 ただし、制限/オフセット方匏にも独自の長所があり、より゚キゟチックな方匏は特定のタむプのデヌタに察しお特別なパフォヌマンス特性を提䟛したす。 ご芧のずおり、かなりの数の可胜性がありたす。 ゞョブに適したツヌルを遞択し、ペヌゞネヌションを閉じた本にしないでください。



All Articles