web-dev-qa-db-ja.com

PostgreSQLでのオフセットパフォーマンスの向上

私はページ区切りのためにLIMITとOFFSETの前にORDER BYを実行しているテーブルがあります。

ORDER BY列にインデックスを追加すると、パフォーマンスに大きな違いが生じます(小さなLIMITと組み合わせて使用​​した場合)。 500,000行のテーブルでは、小さなLIMITがある限り、インデックスを追加すると10,000倍の改善が見られました。

ただし、インデックスは高いオフセット(つまり、ページネーションの後のページ)には影響しません。これは理解できます。Bツリーインデックスを使用すると、最初から反復するのが簡単になりますが、n番目の項目は見つかりません。

役立つのはcounted b-tree indexのようですが、PostgreSQLでこれらがサポートされていることは知りません。別の解決策はありますか?大きなオフセット(特にページネーションの使用例)の最適化は、それほど珍しいことではないようです。

残念ながら、PostgreSQLのマニュアルでは、「OFFSET句でスキップされた行はサーバー内で計算する必要があるため、大きなOFFSETは非効率的である可能性があります」と単純に述べています。

39
James Tauber

計算されたインデックスが必要になる場合があります。

テーブルを作成しましょう:

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

それが役に立てば幸い。

40
Mike Ivanov

「カウントされたbツリーインデックス」については何も知りませんが、アプリケーションでこれを支援するために行った1つのことは、おそらくサブクエリを使用して、クエリを2つに分割することです。既にこれを行っている場合は、時間を無駄にしてしまったことをお詫びします。

SELECT *
FROM massive_table
WHERE id IN (
    SELECT id
    FROM massive_table
    WHERE ...
    LIMIT 50
    OFFSET 500000
);

ここでの利点は、すべての適切な順序を計算する必要がありますが、行全体ではなくid列のみを順序付けることです。

3
Flimzy

大きなオフセット(特にページネーションの使用例)の最適化は、それほど珍しいことではないようです。

私には少し珍しいようです。ほとんどの人は、ほとんどの場合、非常に多くのページを読み飛ばしていないようです。それは私がサポートするものですが、最適化するために一生懸命働くことはありません。

とにかく 。 。 。

アプリケーションコードは、すでに表示されている順序付けされた値を認識しているため、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句に置き換えるだけです。

1
Rolintocour

最近、私はこのような問題に取り組み、その問題にどう対処するかについてブログを書きました。非常に似ています、私は誰にとっても役立つことを願っています。部分的な取得で遅延リストアプローチを使用します。 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

1
user2928872