web-dev-qa-db-ja.com

PostgreSQLでの「ランダム」選択の最適化

私は次の表を持っています

CREATE TABLE article (
    id bigserial PRIMARY KEY,
    text TEXT NOT NULL,
    category_id bigint REFERENCES categories (id),
    status TEXT NOT NULL,
    locale TEXT NOT NULL,
    source TEXT NOT NULL
)

たとえば、できるだけ多くの異なるカテゴリから10個のランダムレコードを選択したい場所から(したがって、ORDER BY rn ASC CTEの句)。私はこれを達成するために次のCTEを使用しています:

WITH "partitioned_articles" AS
(
    SELECT
        *,
        ROW_NUMBER() OVER (PARTITION BY "articles"."category_id" 
        ORDER BY RANDOM() ASC) AS "rn"
    FROM
        "articles"
    WHERE
        "articles"."status" = 'PUBLISHED' AND 
        LOWER("articles"."locale") = LOWER('en_US') AND 
        "articles"."source" = 'WIKIPEDIA'
    ORDER BY
        rn ASC
)
SELECT
    *
FROM
    "partitioned_articles"
LIMIT
    10

EXPLAIN ANALYZE元々私に次の出力を与えました:

Limit  (cost=45587.17..45587.37 rows=10 width=473) (actual time=795.552..795.557 rows=10 loops=1)
  CTE partitioned_articles
    ->  Sort  (cost=45586.04..45587.17 rows=452 width=306) (actual time=795.545..795.547 rows=10 loops=1)
          Sort Key: (row_number() OVER (?))
          Sort Method: external merge  Disk: 21984kB
          ->  WindowAgg  (cost=45555.93..45566.10 rows=452 width=306) (actual time=574.822..673.524 rows=89190 loops=1)
                ->  Sort  (cost=45555.93..45557.06 rows=452 width=306) (actual time=574.808..623.847 rows=89190 loops=1)
                      Sort Key: articles.category_id, (random())
                      Sort Method: external merge  Disk: 20368kB
                      ->  Seq Scan on articles  (cost=0.00..45536.00 rows=452 width=306) (actual time=0.190..364.287 rows=89190 loops=1)
                            Filter: ((status = 'PUBLISHED'::text) AND (source = 'WIKIPEDIA'::text) AND (lower(locale) = 'en_us'::text))
                            Rows Removed by Filter: 310810
  ->  CTE Scan on partitioned_articles  (cost=0.00..9.04 rows=452 width=473) (actual time=795.550..795.554 rows=10 loops=1)
Planning time: 0.462 ms
Execution time: 805.519 ms

追加してみました

CREATE INDEX articles__locale ON articles (locale);
CREATE INDEX articles__source ON articles (source);
CREATE INDEX articles__status ON articles (status);

そして、次の実行計画になりました。

Limit  (cost=43393.70..43393.90 rows=10 width=473) (actual time=686.982..686.991 rows=10 loops=1)
  CTE partitioned_articles
    ->  Sort  (cost=43392.57..43393.70 rows=452 width=306) (actual time=686.976..686.978 rows=10 loops=1)
          Sort Key: (row_number() OVER (?))
          Sort Method: external merge  Disk: 21976kB
          ->  WindowAgg  (cost=43362.47..43372.64 rows=452 width=306) (actual time=462.080..562.038 rows=89190 loops=1)
                ->  Sort  (cost=43362.47..43363.60 rows=452 width=306) (actual time=462.064..511.409 rows=89190 loops=1)
                      Sort Key: articles.category_id, (random())
                      Sort Method: external merge  Disk: 20368kB
                      ->  Bitmap Heap Scan on articles  (cost=3102.54..43342.54 rows=452 width=306) (actual time=35.149..216.145 rows=89190 loops=1)
                            Recheck Cond: (status = 'PUBLISHED'::text)
                            Filter: ((source = 'WIKIPEDIA'::text) AND (lower(locale) = 'en_us'::text))
                            Rows Removed by Filter: 44388
                            Heap Blocks: exact=12897
                            ->  Bitmap Index Scan on articles__status  (cost=0.00..3102.42 rows=135200 width=0) (actual time=30.421..30.421 rows=133578 loops=1)
                                  Index Cond: (status = 'PUBLISHED'::text)
  ->  CTE Scan on partitioned_articles  (cost=0.00..9.04 rows=452 width=473) (actual time=686.981..686.988 rows=10 loops=1)
Planning time: 0.621 ms
Execution time: 696.945 ms

私が理解していることから、これはほんのわずかに優れています。このクエリをさらに最適化するために他にできることはありますか?私はPostgreSQL9.4を使用しています(できるだけ早く9.5に移行する予定ですが、残念ながらまだオプションではありません)。現在、articlesテーブルには約400,000レコードがあります。

[〜#〜] ps [〜#〜]-これらの名前は例のために作成されたもので、ウィキペディアや他の場所からは何も廃棄していません;-)

4
Tony E. Stark

インデックスを追加してもあまり役に立ちません。ランダムな値で並べ替えると、エンジンはテーブル全体をスキャンしてディスク上で並べ替えるしかありません。[1]、順序付けがORDER BY句で直接行われるか、ウィンドウ関数を介して行われるか。

[1]ただし、小さなデータセットの場合、実際にはディスクに触れない可能性があります

サンプリング機能が利用できない場合、単純なビューでできることはほとんどありません。整数キー内のテーブルがMSSQL Serverなどの再帰CTEを使用して効率的なものをハッキングする可能性があるかもしれませんが、CTEはPostgresの最適化フェンスであるため、これがここで役立つとは思えません(実際に試さずに、私の腸の反応はパフォーマンスがはるかに悪くなると想定します)。

アプリケーションの別のレイヤーでストアドプロシージャまたはビジネスロジックを使用できる場合は、最高IDと最低IDを読み取り、10が存在するまでランダムな値をループして選択し、それらの行を選択する方が効率的です。これは、データのIDに大きなギャップが多数ある場合(つまり、シーケンスが使用されている数よりもギャップが大きい場合)には適していません。

IO)の量を減らす可能性のある最後のオプションは、CTEの順序でランダムにそれだけを選択し、ベーステーブルに結合して、残りの行を取得することです。インデックスをスキャンし、ソートのためにディスクに書き込む可能性がありますが、テーブル全体をスキャンして書き込むことはありません。

1
David Spillett