Postgres 9.5を使用しています。複数のWebサイトからのページヒットを記録するテーブルがあります。このテーブルには、2016年1月1日から2016年6月30日までの約3200万行が含まれています。
CREATE TABLE event_pg (
timestamp_ timestamp without time zone NOT NULL,
person_id character(24),
location_Host varchar(256),
location_path varchar(256),
location_query varchar(256),
location_fragment varchar(256)
);
特定の一連のページヒットを実行したユーザーの数をカウントするクエリを調整しようとしています。このクエリは、「ホームページを表示してから、ヘルプサイトにアクセスして、お礼ページを表示した人」などの質問に答えることを目的としています。結果はこのようになります
╔════════════╦════════════╦═════════════╗
║ home-page ║ help site ║ thankyou ║
╠════════════╬════════════╬═════════════╣
║ 10000 ║ 9800 ║1500 ║
╚════════════╩════════════╩═════════════╝
ホームページ9800を表示した10000人がヘルプサイトにアクセスし、1500人がお礼ページにアクセスしたため、数字が減少していることに注意してください。
3ステップシーケンスのSQLでは、次のようにラテラル結合を使用します。
SELECT
sum(view_homepage) AS view_homepage,
sum(use_help) AS use_help,
sum(thank_you) AS thank_you
FROM (
-- Get the first time each user viewed the homepage.
SELECT X.person_id,
1 AS view_homepage,
min(timestamp_) AS view_homepage_time
FROM event_pg X
WHERE X.timestamp_ between '2016-04-23 00:00:00.0' and timestamp '2016-04-30 23:59:59.999'
AND X.location_Host like '2015.testonline.ca'
GROUP BY X.person_id
) e1
LEFT JOIN LATERAL (
SELECT
Y.person_id,
1 AS use_help,
timestamp_ AS use_help_time
FROM event_pg Y
WHERE
Y.person_id = e1.person_id AND
location_Host = 'helpcentre.testonline.ca' AND
timestamp_ BETWEEN view_homepage_time AND timestamp '2016-04-30 23:59:59.999'
ORDER BY timestamp_
LIMIT 1
) e2 ON true
LEFT JOIN LATERAL (
SELECT
1 AS thank_you,
timestamp_ AS thank_you_time
FROM event_pg Z
WHERE Z.person_id = e2.person_id AND
location_fragment = '/file/thank-you' AND
timestamp_ BETWEEN use_help_time AND timestamp '2016-04-30 23:59:59.999'
ORDER BY timestamp_
LIMIT 1
) e3 ON true;
timestamp_
、person_id
、location
列にインデックスがあります。数日または数週間の日付範囲のクエリは非常に高速です(1〜10秒)。遅くなるのは、1月1日から7月30日までの間にすべてのクエリを実行しようとしたときです。1分以上かかります。以下の2つの説明を比較すると、timestamp_インデックスを使用しておらず、代わりにSeq Scanを実行していることがわかります。これは、「常時」に対してクエリを実行しているため、インデックスは何も購入しないため、テーブル内のほぼすべてのレコード。
これで、ラテラル結合のネストされたループの性質により、ループする必要のあるレコードが遅くなることに気付きましたが、このクエリを高速化して、より適切にスケーリングできるようにする方法はありますか?
奇数のデータ型を使用しています。 character(24)
? char(n)
は古いタイプであり、ほとんどの場合間違った選択です。 person_id
にインデックスがあり、繰り返し結合する。 integer
は、いくつかの理由ではるかに効率的です。 (またはbigint
、テーブルの存続期間中に20億を超える行を書き込む予定の場合。)関連:
LIKE
はワイルドカードなしでは無意味です。代わりに=
を使用してください。もっと早く。x.location_Host LIKE '2015.testonline.ca'
x.location_Host = '2015.testonline.ca'
各サブクエリに値1
のダミー列を追加する代わりに、count(e1.*)
またはcount(*)
を使用します。 (最後の(e3
)を除いて、実際のデータは必要ありません。)
文字列リテラルをtimestamp
にキャストする場合と、しない場合があります(timestamp '2016-04-30 23:59:59.999'
)。それが理にかなっている場合は、それをall実行するか、実行しないか、実行しないでください。
そうではありません。 timestamp
列と比較すると、とにかく文字列リテラルはtimestamp
に強制変換されます。したがって、明示的なキャストは必要ありません。
Postgresデータ型timestamp
は、最大6桁の小数桁を含みます。あなたのBETWEEN
式は、例外的なケースを残しています。エラーを起こしにくい表現に置き換えました。
重要:パフォーマンスを最適化するには、 multicolumn index を作成します。
最初のサブクエリhp
の場合:
CREATE INDEX event_pg_location_Host_timestamp__idx
ON event_pg (location_Host, timestamp_);
または、インデックスのみのスキャンを実行できる場合は、インデックスにperson_id
を追加します。
CREATE INDEX event_pg_location_Host_timestamp__person_id_idx
ON event_pg (location_Host, timestamp_, person_id);
テーブルの大部分またはすべてにわたる非常にlarge時間範囲の場合、このインデックスが望ましい-hlp
サブクエリもサポートしているため、次のいずれかの方法で作成します。
CREATE INDEX event_pg_location_Host_person_id_timestamp__idx
ON event_pg (location_Host, person_id, timestamp_);
tnk
の場合:
CREATE INDEX event_pg_location_fragment_timestamp__idx
ON event_pg (location_fragment, person_id, timestamp_);
location_Host
およびlocation_fragment
の述語が定数の場合、特にlocation_*
列が大きく見えるため、代わりにはるかに安価な部分インデックスを使用できます。
CREATE INDEX event_pg_hp_person_id_ts_idx ON event_pg (person_id, timestamp_)
WHERE location_Host = '2015.testonline.ca';
CREATE INDEX event_pg_hlp_person_id_ts_idx ON event_pg (person_id, timestamp_)
WHERE location_Host = 'helpcentre.testonline.ca';
CREATE INDEX event_pg_tnk_person_id_ts_idx ON event_pg (person_id, timestamp_)
WHERE location_fragment = '/file/thank-you';
考慮してください:
繰り返しますが、person_id
のinteger
またはbigint
を使用すると、これらのインデックスはすべて大幅に小さく、高速になります。
通常、新しいインデックスを作成した後は、テーブルをANALYZE
する必要があります。または、autovacuumが起動するまで待機してください。
インデックスのみのスキャン を取得するには、テーブルが十分にVACUUM
されている必要があります。概念実証としてVACUUM
の直後にテストします。 インデックスのみのスキャン に慣れていない場合は、リンクされたPostgres Wikiページで詳細をお読みください。
私が議論したことを実行する。小さい範囲のクエリ(person_id
ごとに(few行)):
SELECT count(*)::int AS view_homepage
, count(hlp.hlp_ts)::int AS use_help
, count(tnk.yes)::int AS thank_you
FROM (
SELECT DISTINCT ON (person_id)
person_id, timestamp_ AS hp_ts
FROM event_pg
WHERE timestamp_ >= '2016-04-23'
AND timestamp_ < '2016-05-01'
AND location_Host = '2015.testonline.ca'
ORDER BY person_id, timestamp_
) hp
LEFT JOIN LATERAL (
SELECT timestamp_ AS hlp_ts
FROM event_pg y
WHERE y.person_id = hp.person_id
AND timestamp_ >= hp.hp_ts
AND timestamp_ < '2016-05-01'
AND location_Host = 'helpcentre.testonline.ca'
ORDER BY timestamp_
LIMIT 1
) hlp ON true
LEFT JOIN LATERAL (
SELECT true AS yes -- we only need existence
FROM event_pg z
WHERE z.person_id = hp.person_id -- we can use hp here
AND location_fragment = '/file/thank-you'
AND timestamp_ >= hlp.hlp_ts -- this introduces dependency on hlp anyways.
AND timestamp_ < '2016-05-01'
ORDER BY timestamp_
LIMIT 1
) tnk ON true;
DISTINCT ON
は、person_id
あたりの数行に対して、多くの場合、より安価です。詳細な説明:
Ifperson_id
ごとに多くの行がある場合(より大きな時間範囲の可能性が高い)、章のこの回答で説明されている再帰CTE 1aは(はるかに)高速にできます:
以下に統合されているのをご覧ください。
それは古い難問です。1つのクエリ手法は、小規模なセットに最適で、別のクエリ技術は大きなセットに最適です。あなたの特定のケースでは、最初から非常に優れた指標があります-指定された期間の長さ-を決定するために使用できます。
それをすべてPL/pgSQL関数でラップします。私の実装では、指定された期間が設定されたしきい値よりも長い場合にDISTINCT ON
からrCTEに切り替わります。
CREATE OR REPLACE FUNCTION f_my_counts(_ts_low_inc timestamp, _ts_hi_excl timestamp)
RETURNS TABLE (view_homepage int, use_help int, thank_you int) AS
$func$
BEGIN
CASE
WHEN _ts_hi_excl <= _ts_low_inc THEN
RAISE EXCEPTION 'Timestamp _ts_hi_excl (1st param) must be later than _ts_low_inc!';
WHEN _ts_hi_excl - _ts_low_inc < interval '10 days' THEN -- example value !!!
-- DISTINCT ON for few rows per person_id
RETURN QUERY
WITH hp AS (
SELECT DISTINCT ON (person_id)
person_id, timestamp_ AS hp_ts
FROM event_pg
WHERE timestamp_ >= _ts_low_inc
AND timestamp_ < _ts_hi_excl
AND location_Host = '2015.testonline.ca'
ORDER BY person_id, timestamp_
)
, hlp AS (
SELECT hp.person_id, hlp.hlp_ts
FROM hp
CROSS JOIN LATERAL (
SELECT timestamp_ AS hlp_ts
FROM event_pg
WHERE person_id = hp.person_id
AND timestamp_ >= hp.hp_ts
AND timestamp_ < _ts_hi_excl
AND location_Host = 'helpcentre.testonline.ca' -- match partial idx
ORDER BY timestamp_
LIMIT 1
) hlp
)
SELECT (SELECT count(*)::int FROM hp) -- AS view_homepage
, (SELECT count(*)::int FROM hlp) -- AS use_help
, (SELECT count(*)::int -- AS thank_you
FROM hlp
CROSS JOIN LATERAL (
SELECT 1 -- we only care for existence
FROM event_pg
WHERE person_id = hlp.person_id
AND location_fragment = '/file/thank-you'
AND timestamp_ >= hlp.hlp_ts
AND timestamp_ < _ts_hi_excl
ORDER BY timestamp_
LIMIT 1
) tnk
);
ELSE
-- rCTE for many rows per person_id
RETURN QUERY
WITH RECURSIVE hp AS (
( -- parentheses required
SELECT person_id, timestamp_ AS hp_ts
FROM event_pg
WHERE timestamp_ >= _ts_low_inc
AND timestamp_ < _ts_hi_excl
AND location_Host = '2015.testonline.ca' -- match partial idx
ORDER BY person_id, timestamp_
LIMIT 1
)
UNION ALL
SELECT x.*
FROM hp, LATERAL (
SELECT person_id, timestamp_ AS hp_ts
FROM event_pg
WHERE person_id > hp.person_id -- lateral reference
AND timestamp_ >= _ts_low_inc -- repeat conditions
AND timestamp_ < _ts_hi_excl
AND location_Host = '2015.testonline.ca' -- match partial idx
ORDER BY person_id, timestamp_
LIMIT 1
) x
)
, hlp AS (
SELECT hp.person_id, hlp.hlp_ts
FROM hp
CROSS JOIN LATERAL (
SELECT timestamp_ AS hlp_ts
FROM event_pg y
WHERE y.person_id = hp.person_id
AND location_Host = 'helpcentre.testonline.ca' -- match partial idx
AND timestamp_ >= hp.hp_ts
AND timestamp_ < _ts_hi_excl
ORDER BY timestamp_
LIMIT 1
) hlp
)
SELECT (SELECT count(*)::int FROM hp) -- AS view_homepage
, (SELECT count(*)::int FROM hlp) -- AS use_help
, (SELECT count(*)::int -- AS thank_you
FROM hlp
CROSS JOIN LATERAL (
SELECT 1 -- we only care for existence
FROM event_pg
WHERE person_id = hlp.person_id
AND location_fragment = '/file/thank-you'
AND timestamp_ >= hlp.hlp_ts
AND timestamp_ < _ts_hi_excl
ORDER BY timestamp_
LIMIT 1
) tnk
);
END CASE;
END
$func$ LANGUAGE plpgsql STABLE STRICT;
コール:
SELECT * FROM f_my_counts('2016-01-23', '2016-05-01');
定義により、rCTEはCTEと連携します。また、 CTEs でDISTINCT ON
クエリを入力しました(コメントの@Lennartで説明した のように )。これにより、CROSS JOIN
の代わりにLEFT JOIN
を使用して、各CTEを個別にカウントできるため、各ステップで設定します。これには、反対方向に作用する効果があります。
どちらが他よりも優れているかをテストする必要があります。