私はページ区切りのためにLIMITとOFFSETの前にORDER BYを実行しているテーブルがあります。
ORDER BY列にインデックスを追加すると、パフォーマンスに大きな違いが生じます(小さなLIMITと組み合わせて使用した場合)。 500,000行のテーブルでは、小さなLIMITがある限り、インデックスを追加すると10,000倍の改善が見られました。
ただし、インデックスは高いオフセット(つまり、ページネーションの後のページ)には影響しません。これは理解できます。Bツリーインデックスを使用すると、最初から反復するのが簡単になりますが、n番目の項目は見つかりません。
役立つのはcounted b-tree indexのようですが、PostgreSQLでこれらがサポートされていることは知りません。別の解決策はありますか?大きなオフセット(特にページネーションの使用例)の最適化は、それほど珍しいことではないようです。
残念ながら、PostgreSQLのマニュアルでは、「OFFSET句でスキップされた行はサーバー内で計算する必要があるため、大きなOFFSETは非効率的である可能性があります」と単純に述べています。
計算されたインデックスが必要になる場合があります。
テーブルを作成しましょう:
create table sales(day date, amount real);
そして、ランダムなものでそれを埋めます:
insert into sales
select current_date + s.a as day, random()*100 as amount
from generate_series(1,20);
日ごとにインデックスを作成します。ここでは特別なことは何もありません。
create index sales_by_day on sales(day);
行位置関数を作成します。他の方法がありますが、これが最も簡単です。
create or replace function sales_pos (date) returns bigint
as 'select count(day) from sales where day <= $1;'
language sql immutable;
機能するかどうかを確認します(ただし、大規模なデータセットではこのように呼び出さないでください)。
select sales_pos(day), day, amount from sales;
sales_pos | day | amount
-----------+------------+----------
1 | 2011-07-08 | 41.6135
2 | 2011-07-09 | 19.0663
3 | 2011-07-10 | 12.3715
..................
ここでトリッキーな部分:sales_pos関数値で計算された別のインデックスを追加します。
create index sales_by_pos on sales using btree(sales_pos(day));
使い方は次のとおりです。 5は「オフセット」、10は「制限」です。
select * from sales where sales_pos(day) >= 5 and sales_pos(day) < 5+10;
day | amount
------------+---------
2011-07-12 | 94.3042
2011-07-13 | 12.9532
2011-07-14 | 74.7261
...............
このように呼び出すと、Postgresはインデックスから事前計算された値を使用するため、高速です。
explain select * from sales
where sales_pos(day) >= 5 and sales_pos(day) < 5+10;
QUERY PLAN
--------------------------------------------------------------------------
Index Scan using sales_by_pos on sales (cost=0.50..8.77 rows=1 width=8)
Index Cond: ((sales_pos(day) >= 5) AND (sales_pos(day) < 15))
それが役に立てば幸い。
「カウントされたbツリーインデックス」については何も知りませんが、アプリケーションでこれを支援するために行った1つのことは、おそらくサブクエリを使用して、クエリを2つに分割することです。既にこれを行っている場合は、時間を無駄にしてしまったことをお詫びします。
SELECT *
FROM massive_table
WHERE id IN (
SELECT id
FROM massive_table
WHERE ...
LIMIT 50
OFFSET 500000
);
ここでの利点は、すべての適切な順序を計算する必要がありますが、行全体ではなくid列のみを順序付けることです。
大きなオフセット(特にページネーションの使用例)の最適化は、それほど珍しいことではないようです。
私には少し珍しいようです。ほとんどの人は、ほとんどの場合、非常に多くのページを読み飛ばしていないようです。それは私がサポートするものですが、最適化するために一生懸命働くことはありません。
とにかく 。 。 。
アプリケーションコードは、すでに表示されている順序付けされた値を認識しているため、WHERE句でそれらの値を除外することにより、結果セットを削減し、オフセットを削減できるはずです。単一の列を注文し、それが昇順で並べ替えられている場合、アプリのコードはページの最後の値を格納し、適切な方法でAND your-ordered-column-name > last-value-seen
をWHERE句に追加できます。
OFFSETを使用する代わりに、非常に効率的な方法は、一時テーブルを使用することです。
CREATE TEMPORARY TABLE just_index AS
SELECT ROW_NUMBER() OVER (ORDER BY myID), myID
FROM mytable;
10 000 000行の場合、作成には約10秒必要です。次に、テーブルをSELECTまたはUPDATEしたい場合は、次のようにします。
SELECT * FROM mytable INNER JOIN (SELECT just_index.myId FROM just_index WHERE row_number >= *your offset* LIMIT 1000000) indexes ON mytable.myID = indexes.myID
Just_indexのみによるmytableのフィルタリングは、WHERE myID IN(SELECT ...)よりもINNER JOINの方が(私の場合)効率的です。
このようにして、最後のmyId値を保存する必要はありません。オフセットを、インデックスを使用するWHERE句に置き換えるだけです。
最近、私はこのような問題に取り組み、その問題にどう対処するかについてブログを書きました。非常に似ています、私は誰にとっても役立つことを願っています。部分的な取得で遅延リストアプローチを使用します。 iクエリの制限とオフセットまたはページネーションを手動のページネーションに置き換えました。私の例では、selectは1000万件のレコードを返します。それらを取得して、「テンポラルテーブル」に挿入します。
create or replace function load_records ()
returns VOID as $$
BEGIN
drop sequence if exists temp_seq;
create temp sequence temp_seq;
insert into tmp_table
SELECT linea.*
FROM
(
select nextval('temp_seq') as ROWNUM,* from table1 t1
join table2 t2 on (t2.fieldpk = t1.fieldpk)
join table3 t3 on (t3.fieldpk = t2.fieldpk)
) linea;
END;
$$ language plpgsql;
その後、各行を数えることなくページ番号を付けることができますが、割り当てられたシーケンスを使用します。
select * from tmp_table where counterrow >= 9000000 and counterrow <= 9025000
Javaパースペクティブから、私は遅延リストを使用した部分取得を通じてこのページネーションを実装しました。これは、抽象リストから拡張され、get()メソッドを実装するリストです。getメソッドはデータアクセスを使用できます。次のデータセットを引き続き取得し、メモリヒープを解放するインターフェイス:
@Override
public E get(int index) {
if (bufferParcial.size() <= (index - lastIndexRoulette))
{
lastIndexRoulette = index;
bufferParcial.removeAll(bufferParcial);
bufferParcial = new ArrayList<E>();
bufferParcial.addAll(daoInterface.getBufferParcial());
if (bufferParcial.isEmpty())
{
return null;
}
}
return bufferParcial.get(index - lastIndexRoulette);<br>
}
一方、データアクセスインターフェイスはクエリを使用してページ分割を行い、1つのメソッドを実装して段階的に反復します。各25000レコードはそれをすべて完了します。
このアプローチの結果はここで見ることができます http://www.arquitecturaysoftware.co/2013/10/laboratorio-1-iterar-millones-de.html