web-dev-qa-db-ja.com

多くのテーブルの列を使用して、order byのパフォーマンスを向上させる

PostgreSQL 8.4を使用して、2つのテーブルのインデックス付き列でorder byを使用して、100万レコードの2つのテーブルを調べようとしています。カラムには5分かかります)。例えば。:

select r.code, r.test_code, r.sample_code, s.barcode, s.registry_date
from requests r
inner join samples s on (s.code = r.sample_code)
order by s.barcode  asc , r.code asc
limit 21;

テーブル情報:

CREATE TABLE public.samples (
  code BIGINT NOT NULL,
  barcode VARCHAR(40) NOT NULL,
  registry_date TIMESTAMP WITH TIME ZONE NOT NULL,
  CONSTRAINT samples_pkey PRIMARY KEY(code)
);

CREATE INDEX idx_samp_barcode ON public.samples (barcode);
CREATE INDEX idx_samp_barcode_code ON public.samples (barcode, code);
CREATE INDEX idx_samp_barcode_code_desc ON public.samples (barcode DESC, code DESC);
CREATE INDEX idx_test_identifier_desc ON public.samples (barcode DESC);


CREATE TABLE public.requests (
  code BIGINT NOT NULL,
  test_code INTEGER NOT NULL,
  sample_code BIGINT NOT NULL,
  CONSTRAINT requests_pkey PRIMARY KEY(code),
  CONSTRAINT "Requests_S_fk" FOREIGN KEY (sample_code)
    REFERENCES public.samples(code)
    ON DELETE NO ACTION ON UPDATE NO ACTION NOT DEFERRABLE,
  CONSTRAINT "Requests_T_fk" FOREIGN KEY (test_code)
    REFERENCES public.tests(code)
    ON DELETE NO ACTION ON UPDATE NO ACTION NOT DEFERRABLE
);

CREATE INDEX idx_req_test_code ON public.requests (test_code);
CREATE INDEX idx_req_test_code_desc ON public.requests (test_code DESC);
CREATE INDEX request_sample_code_index ON public.requests (sample_code);
CREATE INDEX requests_sample_code_desc_idx ON public.requests (sample_code DESC, code DESC);
CREATE INDEX requests_sample_code_idx ON public.requests (sample_code, code);
  1. 各テーブルには100万行があります。
  2. 両方のテーブルのすべての行は異なります。
  3. どちらかの列のみで注文した場合、実行時間は約30 msです。
  4. ご要望がないサンプルはありません。
  5. サンプルには多数のs.code for smaes.barcode

パフォーマンスを向上させるにはどうすればよいですか?

6

問題

これは一目でわかるよりも複雑な問題です。他の2つの列を結合しながら、それぞれが異なるテーブルの2つの列で並べ替えています。これはPostgresが提供されたインデックスを使用することを不可能にし、そして(非常に)高価な順次スキャンをデフォルトにしなければなりません。以下は、dba.SEの関連ケースです。

インデックス

最高のパフォーマンスを得るには、2つのインデックスが必要です。どちらもすでに存在しています。

CREATE INDEX idx_samp_barcode_code ON public.samples (barcode, code);
CREATE INDEX requests_sample_code_idx ON public.requests (sample_code, code);

しかし、あなたが持っているプレーンなクエリはそれらを利用することはできません。

クエリ

Postgres 8.4はかなりのハードルです。しかし、私は再帰的なCTEの方法を見つけたと思います。

WITH RECURSIVE cte AS (
   ( -- all parentheses are required
   SELECT r.code, r.test_code, r.sample_code, s.barcode, s.registry_date
   FROM  (
      SELECT s.barcode
      FROM   samples s
      -- WHERE  EXISTS (SELECT 1 FROM requests WHERE r.sample_code = s.code)
      ORDER  BY s.barcode
      LIMIT  1  -- get smallest barcode to start with
      ) s0
   JOIN   samples  s USING (barcode)  -- join all samples with same barcode
   JOIN   requests r ON r.sample_code = s.code
   ORDER  BY r.code  -- start with ordered list
   )

   UNION ALL
   (
   SELECT r.code, r.test_code, r.sample_code, s.barcode, s.registry_date
   FROM  (
      SELECT s.barcode
      FROM   cte c
      JOIN   samples s ON s.barcode > c.barcode 
      -- WHERE  EXISTS (SELECT 1 FROM requests WHERE r.sample_code = s.code)
      ORDER  BY s.barcode
      LIMIT  1  -- get next higher barcode
      ) s0
   JOIN   samples  s USING (barcode)  -- join all samples with same barcode
   JOIN   requests r ON r.sample_code = s.code
   ORDER  BY r.code  -- again, ordered list
   )
   )
TABLE cte
LIMIT 21;

テスト済みで、9.4ページで動作します。インデックスを使用します(一部はpg 9.2+のインデックスのみのスキャンで)。

pg 8.4にはすでに再帰的なCTEがあります。残りは基本的なSQLです(TABLEもpg 8.4で使用できます(そしてSELECT * FROMで置き換えることができます)。パーティーを台無しにする制限がないことを願っています。インストールされていません。 pg 8.4以降。

説明

コメントされた部分:

-- WHERE  EXISTS (SELECT 1 FROM requests WHERE r.sample_code = s.code)

コメントで除外 のように、リクエストがまったくないサンプルがある場合に必要になります。

クエリはこれに依存します 文書化された再帰CTEの動作

ヒント:再帰クエリ評価アルゴリズムは、出力をbreadth-first検索順序で生成します。

大胆な強調鉱山。そして これも

これは、PostgreSQLの実装がWITHクエリの行を親クエリによって実際にフェッチされるのと同じ数だけ評価するため、機能します。他のシステムでは動作が異なる可能性があるため、このトリックを本番環境で使用することはお勧めしません。また、外部クエリで再帰クエリの結果を並べ替えたり、他のテーブルに結合したりしても、通常は機能しません。

大胆な強調鉱山。したがって、PostgreSQLでの動作が保証されるのは、外部クエリに別のORDER BYを追加しない場合のみです。

これは、外部クエリの(比較的)小さいLIMITの場合は高速です。当面のケースでは、非常に高速である必要があります。クエリは大規模なLIMITには適していませんになります。

PL/pgSQL関数

私が考えることができるもう1つのオプションは、plpgsqlを使用した手続き型ソリューションです。特に繰り返し呼び出しの場合は、まだ少し速いはずです:

CREATE OR REPLACE FUNCTION f_demo(_limit int)
  RETURNS TABLE (code int8, test_code int, sample_code int8
               , barcode varchar, registry_date timestamptz) AS
$func$
DECLARE
   _barcode text;
   _n       int;
   _rest    int := _limit;  -- init with _limit parameter
BEGIN
   FOR _barcode IN
      SELECT DISTINCT s.barcode
      FROM   samples s
      -- WHERE  EXISTS (SELECT 1 FROM requests WHERE r.sample_code = s.code)
      ORDER  BY s.barcode

   LOOP
      RETURN QUERY
      SELECT r.code, r.test_code, r.sample_code, s.barcode, s.registry_date
      FROM   samples  s
      JOIN   requests r ON r.sample_code = s.code
      WHERE  s.barcode = _barcode
      ORDER  BY r.code
      LIMIT  _limit;

      GET DIAGNOSTICS _n = ROW_COUNT;
      _rest := _rest - _n;
      EXIT WHEN _rest < 1;
   END LOOP;
END
$func$ LANGUAGE plpgsql;

コール:

SELECT * FROM f_demo(21); 

MATERIALIZED VIEW

大きなOFFSETは、大きなLIMITと同様の効果があります。スキップされたすべての行はI/Oに追加されませんが、最初の行afterオフセットを決定するには、それらを計算する必要があります。それが必要な場合は、行番号とそのインデックスを追加した MATERIALIZED VIEW を使用します。

Postgres 9.3以降には組み込みの機能があり、古いソフトウェアを使用して手動でソリューションを作成する必要があります。ただし、それほど複雑ではありません。基本的に、事前定義されたSELECTステートメントまたはVIEWなどから入力されたスナップショットテーブル。基本的に、次のようにテーブルを作成します。

CREATE TABLE req_sample AS
SELECT row_number() OVER (ORDER BY s.barcode, r.code) AS rn
     , r.code, r.test_code, r.sample_code, s.barcode, s.registry_date
FROM   requests r
JOIN   samples s on (s.code = r.sample_code)
ORDER  by s.barcode, r.code;

ALTER TABLE req_sample ADD CONSTRAINT req_sample_rk PRIMARY KEY (code);
CREATE INDEX foo ON  req_sample (rn);

そのSELECTを関数、ビュー、またはプレーンクエリテキストとして保存します。 1つのトランザクションでの更新(高価):

BEGIN;
-- drop PK & index
TRUNCATE req_sample;
INSERT INTO req_sample SELECT ... ;
-- add PK & index
COMMIT;

冗長なbarcode

基になるテーブルが大幅に変更され、「最新の」結果が必要な場合、MVはより高価になります。もう1つ中程度の安価なオプション:barcoderequestsテーブルに保存します冗長。これをsamples"Requests_S_fk")へのFK制約の2番目の列として含め、そのデータをON UPDATE CASCADEにすることで、古いデータや競合するデータを回避できます。次に、requests(barcode, code)のインデックスを操作できます。

最後の2つのオプションについては、前のページの値のWHERE句をOFFSETに置き換えて、直接適用可能なインデックスを使用してページ付けを簡略化できます。 use-the-index-luke.comの "Pagination done the PostgreSQL way"に関するこのプレゼンテーションを検討してください

4