web-dev-qa-db-ja.com

オフセット制限付きの選択クエリが遅すぎる

インターネットリソースから、オフセットが増加するとクエリが遅くなることを読みました。しかし、私の場合は遅すぎると思います。 _postgres 9.3_を使用しています

これがクエリです(idは主キーです):

_select * from test_table offset 3900000 limit 100;
_

_10 seconds_あたりのデータを返します。そして、私はそれが遅すぎると思います。テーブルに約_4 million_レコードがあります。データベースの全体的なサイズは_23GB_です。

マシン構成:

_RAM: 12 GB
CPU: 2.30 GHz
Core: 10
_

私が変更した_postgresql.conf_ファイルのいくつかの値は次のとおりです。その他はデフォルトです。

_shared_buffers = 2048MB
temp_buffers = 512MB
work_mem = 1024MB
maintenance_work_mem = 256MB
dynamic_shared_memory_type = posix
default_statistics_target = 10000
autovacuum = on
enable_seqscan = off   ## its not making any effect as I can see from Analyze doing seq-scan
_

これらとは別に、_random_page_cost = 2.0_と_cpu_index_Tuple_cost = 0.0005_の値を変更することも試みましたが、結果は同じです。

クエリに対するExplain (analyze, buffers)結果は次のとおりです。

_"Limit  (cost=10000443876.02..10000443887.40 rows=100 width=1034) (actual time=12793.975..12794.292 rows=100 loops=1)"
"  Buffers: shared hit=26820 read=378984"
"  ->  Seq Scan on test_table  (cost=10000000000.00..10000467477.70 rows=4107370 width=1034) (actual time=0.008..9036.776 rows=3900100 loops=1)"
"        Buffers: shared hit=26820 read=378984"
"Planning time: 0.136 ms"
"Execution time: 12794.461 ms"
_

世界中の人々がpostgresでこの問題についてどのように交渉していますか?他の解決策も私にとっては役に立ちます。

PDATE :: _order by id_を追加します(他のインデックス付けされた列でも試行されました)。これが説明です。

_"Limit  (cost=506165.06..506178.04 rows=100 width=1034) (actual time=15691.132..15691.494 rows=100 loops=1)"
"  Buffers: shared hit=110813 read=415344"
"  ->  Index Scan using test_table_pkey on test_table  (cost=0.43..533078.74 rows=4107370 width=1034) (actual time=38.264..11535.005 rows=3900100 loops=1)"
"        Buffers: shared hit=110813 read=415344"
"Planning time: 0.219 ms"
"Execution time: 15691.660 ms"
_
18
Sabuj Hassan

最上位のoffset行を見つけて次の100行をスキャンする必要があるため、処理が遅くなります。巨大なオフセットを処理している場合、最適化の量は変化しません。

これは、クエリが文字どおりinstructDBエンジンにoffset 3900000を使用して多数の行にアクセスするように指示するためです。つまり、3.9M行です。これをいくらか高速化するオプションは多くありません。

超高速RAM、SSDなどが役立ちます。ただし、そうすることで得られるのは一定の要因だけです。つまり、十分に大きなオフセットに到達するまで、缶を蹴り下げるだけです。

テーブルがメモリに収まることを保証し、十分に余裕を持たせておくと、定数係数が大きくなります- 初回を除く 。しかし、これは十分に大きなテーブルまたはインデックスでは不可能かもしれません。

インデックスのみのスキャンを確実に実行することで、ある程度の効果が得られます。 (velisの回答を参照してください。これには多くのメリットがあります。)ここでの問題は、すべての実用的な目的で、インデックスをディスクの場所とインデックス付きフィールドを格納するテーブルと考えることができることです。 (それよりも最適化されていますが、妥当な最初の近似です。)十分な行があれば、十分に大きなオフセットで問題が発生します。

行の正確な位置を格納および維持しようとすると、コストのかかるアプローチになりがちです(これは、たとえばbenjistによって示唆されています)。データの大きなチャンクを一緒に更新する必要があるような方法でノードが挿入、更新、または削除されると、読み取りは大幅に増加しますが、書き込み時間が過剰になります。

うまくいけばもっとはっきりしているように、これほど大きなオフセットを扱っているとき、本当の魔法の弾丸はありません。多くの場合、別のアプローチを検討する方が良いでしょう。

ID(または日付フィールド、またはその他のインデックス可能なフィールドのセット)に基づいてページ番号を付ける場合、潜在的なトリック(たとえば、blogspotで使用される)は、クエリをインデックスの任意のポイントから開始することです。

代わりに別の言い方をすると:

example.com?page_number=[huge]

次のようなことをしてください:

example.com?page_following=[huge]

こうすることで、インデックス内のどこにいるかを追跡でき、膨大な数の行を調べなくても正しい開始点に直接進むことができるため、クエリは非常に高速になります。

select * from foo where ID > [huge] order by ID limit 100

当然、ジャンプする能力は失われます。しかし、これについて正直に考えてみてください。毎月のアーカイブを直接検索したり、検索ボックスを使用したりするのではなく、サイトの巨大なページ番号に最後にジャンプしたのはいつですか。

ページネーションをしているが、何らかの方法でページをオフセットしたい場合、さらに別のアプローチは、より大きなページ番号の使用を禁止することです。ばかげているわけではありません。Googleが検索結果に対して行っていることです。検索クエリを実行すると、Googleは推定結果数を提供し(explainを使用して妥当な数を取得できます)、上位数千の結果を閲覧できます。特に、パフォーマンス上の理由からそうなります-まさにあなたが遭遇しているものです。

48

私はデニスの答えを賛成しましたが、提案を自分で追加します。おそらく、それはあなたの特定のユースケースにとっていくつかのパフォーマンス上の利点になる可能性があります:

実際のテーブルがtest_tableではなく、複数の結合を使用した巨大な複合クエリであると想定します。最初に、必要な開始IDを決定できます。

select id from test_table order by id offset 3900000 limit 1

これは、テーブル全体に対してインデックスをスキャンするだけでよいため、元のクエリよりもはるかに高速です。このIDを取得すると、完全なフェッチのための高速なインデックス検索オプションが開きます。

select * from test_table where id >= (what I got from previous query) order by id limit 100
6
velis

データが主に読み取り専用であるか、頻繁に更新されるかは言いませんでした。一度にテーブルを作成し、時々(たとえば、数分ごとに)のみ更新することができれば、問題は簡単に解決できます。

  • 新しい列「offset_id」を追加します
  • ID順に並べられた完全なデータセットの場合、単純に数値を増分することにより、offset_idを作成します。1,2,3,4...
  • 「offset ... limit 100」の代わりに「where offset_id> = 3900000 limit 100」を使用します
3
benjist

2つのステップで最適化できます

最初に3900000レコードから最大IDを取得します

select max(id) (select id from test_table order by id limit 3900000);

次に、この最大IDを使用して、次の100レコードを取得します。

select * from test_table id > {max id from previous step) order by id limit 100 ;

両方のクエリがIDによるインデックススキャンを実行するため、より高速になります。

1
Manvendra Jina

この方法で、セミランダムな順序で行を取得します。クエリの結果を並べ替えていないため、結果として、ファイルに格納されているデータが取得されます。問題は、行を更新すると、行の順序が変わる可能性があることです。

これを修正するには、クエリにorder byを追加する必要があります。このようにして、クエリは同じ順序で行を返します。さらに、インデックスを使用してクエリを高速化できます。

つまり、インデックスを追加すること、クエリにorder byを追加することです。両方とも同じ列に。 id列を使用する場合は、インデックスを追加せず、クエリを次のように変更します。

select * from test_table order by id offset 3900000 limit 100;
1

オフセット/制限ではなくIDに基づいてページネーションを行う場合はどうでしょうか?

次のクエリは、すべてのレコードをサイズper_pageのチャンクに分割するIDを提供します。レコードが削除されたかどうかには依存しません

SELECT id AS from_id FROM (
  SELECT id, (ROW_NUMBER() OVER(ORDER BY id DESC)) AS num FROM test_table
) AS rn
WHERE num % (per_page + 1) = 0;

これらのfrom_IDを使用して、ページにリンクを追加できます。 :from_idsをインデックスで反復処理し、次のリンクをページに追加します。

<a href="/test_records?from_id=:from_id">:from_id_index</a>

ユーザーがページにアクセスすると、リクエストされた:from_idよりも大きいIDのレコードを取得します。

SELECT * FROM test_table WHERE ID >= :from_id ORDER BY id DESC LIMIT :per_page

from_id=0の最初のページへのリンクは機能します

<a href="/test_records?from_id=0">1</a>
0
Hirurg103

まず、order by句を使用して制限とオフセットを定義する必要があります。そうしないと、一貫性のない結果が得られます。

クエリを高速化するために、計算されたインデックスを持つことができますが、これらの条件に対してのみです:

  1. 新しく挿入されたデータは厳密にID順になっています
  2. 列IDの削除も更新もありません

方法は次のとおりです。

  1. 行位置関数を作成する

create or replace function id_pos (id) returns bigint as 'select count(id) from test_table where id <= $1;' language sql immutable;

  1. Id_pos関数で計算されたインデックスを作成する

create index table_by_pos on test_table using btree(id_pos(id));

あなたがそれを呼び出す方法は次のとおりです(オフセット3900000制限100):

select * from test_table where id_pos(id) >= 3900000 and sales_pos(day) < 3900100;

この方法では、クエリは3900000オフセットデータを計算しませんが、100データのみを計算するため、はるかに高速になります。

このアプローチを実行できる、または位置が変わる2つの条件に注意してください。

0
Soni Harriz

データの詳細すべてを知っているわけではありませんが、400万行は少し重いかもしれません。テーブルを分割し、本質的に小さなテーブルに分割するための合理的な方法がある場合、それは有益かもしれません。

これを説明するために、例を挙げましょう。データベースにsurvey_answerというテーブルがあり、非常に大きく、非常に遅くなっているとします。ここで、これらの調査の回答はすべて、異なるクライアントのグループからのものであるとしましょう(これらのクライアントを追跡するクライアントテーブルもあります)。次に、私ができることは、データが含まれていないsurvey_answerというテーブルがあるようにすることですが、親テーブルであり、実際に次のデータを含む多数の子テーブルがあります名前付けフォーマットsurvey_answer_ <clientid>、つまり、クライアントごとに1つずつ、子テーブルsurvey_answer_1、survey_answer_2などがあることになります。次に、そのクライアントのデータを選択する必要がある場合は、そのテーブルを使用します。すべてのクライアントからデータを選択する必要がある場合、親のsurvey_answerテーブルから選択できますが、遅くなります。しかし、個々のクライアントのデータを取得する場合、これは私が主に行うことなので、高速です。

これはデータを分割する方法の1つの例であり、他にもたくさんあります。別の例は、survey_answerテーブルがクライアントによって簡単に分割されなかった場合ですが、代わりに、通常は一度に1年間のデータにのみアクセスしていることを知っているため、年に基づいて子テーブルを作成できる可能性があります。たとえば、survey_answer_2014、survey_answer_2013など。一度に1年以上アクセスしないことがわかっている場合、必要なすべてのデータを取得するには、子テーブルのうち2つにアクセスするだけで十分です。

あなたの場合、私に与えられたのはおそらくidだけです。それで分割することもできます(ただし、理想的ではないかもしれません)。テーブルごとに約1000000行のみになるように分割するとします。したがって、子テーブルはtest_table_0000001_1000000、test_table_1000001_2000000、test_table_2000001_3000000、test_table_3000001_4000000などになります。したがって、オフセット3900000を渡す代わりに、少し計算を行って、目的のテーブルがオフセット900000のテーブルtest_table_3000001_4000000であると判断します。代わりに。したがって、次のようなもの:

SELECT * FROM test_table_3000001_4000000 ORDER BY id OFFSET 900000 LIMIT 100;

テーブルのシャーディングが問題にならない場合は、部分インデックスを使用して同様のことを実行できる場合がありますが、ここでも、最初にシャーディングすることをお勧めします。部分インデックスの詳細 こちら

お役に立てば幸いです。 (また、ORDER BYが必要であることはSzymon Guzに同意します)。

編集:100の結果を取得する前に行を削除するか、行を選択的に除外する必要がある場合、IDによるシャーディングは処理が非常に難しくなることに注意してください(Denisが指摘したように、IDによるシャーディングはそもそも素晴らしいものではありません)。しかし、データを「ページ分け」するだけで、挿入または編集のみを行う場合(一般的なことではありませんが、ログが頭に浮かびます)、IDによるシャーディングは合理的に行うことができます(ただし、他のものを選択します)シャードする)。

0
Trevor Young