バージョン管理された行のマスターテーブルがあります。
CREATE TABLE master (
id SERIAL PRIMARY KEY,
rec_id integer,
val text,
valid_on date[],
valid_during daterange
);
INSERT INTO master (rec_id, val, valid_on, valid_during) VALUES
(1, 'a', '{2015-01-01,2015-01-05}', '[2015-01-01,infinity)'),
(2, 'b', '{2015-01-01,2015-01-05}', '[2015-01-01,infinity)'),
(3, 'c', '{2015-01-01,2015-01-05}', '[2015-01-01,infinity)');
SELECT * FROM master ORDER BY rec_id, id;
/*
id | rec_id | val | valid_on | valid_during
----+--------+-----+-------------------------+-----------------------
1 | 1 | a | {2015-01-01,2015-01-05} | [2015-01-01,infinity)
2 | 2 | b | {2015-01-01,2015-01-05} | [2015-01-01,infinity)
3 | 3 | c | {2015-01-01,2015-01-05} | [2015-01-01,infinity)
*/
rec_id
はレコードの自然キー、valid_on
はレコードが有効だった日付の配列、valid_during
はレコードが存在する期間を表す日付範囲です有効。 (同じrec_id
で最新のvalid_on
値を持つレコードがない場合、valid_duringの上限は「無限大」です。)
更新されたレコードの2番目のテーブルと、各レコードが有効であった新しい日付を指定します。
CREATE TABLE updates (id SERIAL PRIMARY KEY, rec_id integer, val text, valid_on date);
INSERT INTO updates (rec_id, val, valid_on) VALUES
(1, 'a', '2015-01-03'), -- (1) same "val" for id 1, just add valid_on date
(2, 'd', '2015-01-06'), -- (2) different val for id 2,
(3, 'e', '2015-01-03'); -- (3) different val for id 3 with new date
-- intersecting old date range
SELECT * FROM updates;
/*
id | rec_id | val | valid_on
----+--------+-----+------------
1 | 1 | a | 2015-01-03
2 | 2 | d | 2015-01-06
3 | 3 | e | 2015-01-03
*/
マスターテーブルを挿入/更新して、次のようなものに仕上げたいと思います:
-- The goal
SELECT rec_id, val, valid_on, valid_during FROM master ORDER BY rec_id, id;
/*
rec_id | val | valid_on | valid_during
--------+-----+------------------------------------+-----------------------
1 | a | {2015-01-01,2015-01-05,2015-01-03} | [2015-01-01,infinity)
2 | b | {2015-01-01,2015-01-05} | [2015-01-01,2015-01-06)
2 | d | {2015-01-06} | [2015-01-06,infinity)
3 | c | {2015-01-01} | [2015-01-01,2015-01-03)
3 | e | {2015-01-03} | [2015-01-03,2015-01-05)
3 | c | {2015-01-05} | [2015-01-05,infinity)
*/
具体的には:
rec_id
が同じval
でマスターテーブルに存在するが、新しいvalid_on
の日付がマスターのvalid_on
配列にない場合は、単にマスターテーブルのvalid_on
フィールドの新しい日付(rec_id
1を参照)rec_id
に別のval
が存在する場合は、新しいレコードをマスターテーブルに挿入します。マスターテーブルの古いレコードのvalid_during
値は、新しいレコードのvalid_on
の日付で終了する必要があります(rec_id
2を参照)valid_on
の日付が古いレコードのvalid_during
の範囲と交差する場合、古いレコードは更新されたレコードの両方の「側」に表示されます(rec_id
3を参照)mostがそこにあります。最初のケースは簡単です。マスターテーブルのvalid_on
フィールドを更新するだけです(valid_during
フィールドについては、別のステップで一時的に心配します)。
UPDATE master m
SET valid_on = m.valid_on || u.valid_on
FROM updates u
WHERE m.rec_id = u.rec_id
AND m.val = u.val
AND NOT m.valid_on @> ARRAY[u.valid_on];
SELECT * FROM master ORDER BY rec_id, id;
/*
id | rec_id | val | valid_on | valid_during
----+--------+-----+------------------------------------+-----------------------
1 | 1 | a | {2015-01-01,2015-01-05,2015-01-03} | [2015-01-01,infinity)
2 | 2 | b | {2015-01-01,2015-01-05} | [2015-01-01,infinity)
3 | 3 | c | {2015-01-01,2015-01-05} | [2015-01-01,infinity)
*/
ケース#2の場合、単純な挿入を実行できます。
INSERT INTO master (rec_id, val, valid_on)
SELECT u.rec_id, u.val, ARRAY[u.valid_on]
FROM updates u
LEFT JOIN master m ON u.rec_id = m.rec_id AND u.val = m.val
WHERE m.id IS NULL;
SELECT * FROM master ORDER BY rec_id, id;
/*
id | rec_id | val | valid_on | valid_during
----+--------+-----+------------------------------------+-----------------------
1 | 1 | a | {2015-01-01,2015-01-05,2015-01-03} | [2015-01-01,infinity)
2 | 2 | b | {2015-01-01,2015-01-05} | [2015-01-01,infinity)
4 | 2 | d | {2015-01-06} |
3 | 3 | c | {2015-01-01,2015-01-05} | [2015-01-01,infinity)
5 | 3 | e | {2015-01-03} |
*/
これで、同じvalid_during
を持つレコードの次の有効な日付をチェックするウィンドウ関数を使用するサブクエリに参加することにより、1回のパスでrec_id
の範囲を修正できます。
-- Helper function...
CREATE OR REPLACE FUNCTION arraymin(anyarray)
RETURNS anyelement AS $$
SELECT min($1[i])
FROM generate_series(array_lower($1,1), array_upper($1,1)) g(i);
$$ language sql immutable strict;
UPDATE master m
SET valid_during = daterange(arraymin(valid_on), new_valid_until)
FROM (
SELECT
id,
lead(arraymin(valid_on), 1, 'infinity'::date)
OVER (partition by rec_id ORDER BY arraymin(valid_on)) AS new_valid_until
FROM master ) t
WHERE
m.id = t.id;
SELECT * FROM master ORDER BY rec_id, id;
/*
id | rec_id | val | valid_on | valid_during
----+--------+-----+------------------------------------+-------------------------
1 | 1 | a | {2015-01-01,2015-01-05,2015-01-03} | [2015-01-01,infinity)
2 | 2 | b | {2015-01-01,2015-01-05} | [2015-01-01,2015-01-06)
4 | 2 | d | {2015-01-06} | [2015-01-06,infinity)
3 | 3 | c | {2015-01-01,2015-01-05} | [2015-01-01,2015-01-03)
5 | 3 | e | {2015-01-03} | [2015-01-03,infinity)
*/
そして、ここで私は行き詰まっています:rec_id
1と2はまさに私が欲しいものですが、rec_id
3を再度挿入する必要があります。その挿入を実行するために配列操作に頭を抱えているようには見えません。マスターテーブルのネストを解除しないアプローチについての考えはありますか?それとも、これが唯一の/最善のアプローチですか?
私はPostgreSQL 9.3を使用しています(ただし、新しいバージョンでこれを行うための適切な方法がある場合は、幸いにも9.4にアップグレードします)。
valid_during
の範囲を忘れているようです。 3番目のケースが示すように、(rec_id, val)
ごとに複数のエントリが存在する可能性があるため、正しいエントリを選択する必要があります。
UPDATE master m
SET valid_on = f_array_sort(m.valid_on || u.valid_on) -- sorted array, see below
FROM updates u
WHERE m.rec_id = u.rec_id
AND m.valid_during @> u.valid_on -- additional check
AND m.val = u.val
AND NOT m.valid_on @> ARRAY[u.valid_on];
whole可能な日付範囲は常に既存のrec_id
ごとにカバーされており、valid_during
はrec_id
ごとに重複してはならないか、それ以上する必要があります。
追加モジュール btree_Gist
をインストールした後、重複する日付範囲がない場合は 除外制約 を追加して重複する日付範囲を除外します。
ALTER TABLE master ADD CONSTRAINT EXCLUDE
USING Gist (rec_id WITH =, valid_during WITH &&) -- disallow overlap
これが実装されている要旨インデックスもクエリに完全に一致します。詳細:
everyの日付範囲は、(現在ソートされている)配列の最小の日付から始まると仮定します:lower(m.valid_during) = m.valid_on[1]
CHECK
制約でそれを強制します。
ここでは、1つまたは2つの新しい行を作成する必要があります。2番目のケースでは、古い行の範囲を縮小して1つの新しい行を挿入するだけで十分です。3番目のケースでは、配列と範囲の左半分で古い行を更新し、新しい行と最後に、配列と範囲の右半分を挿入します。
簡単にするために、新しい制約を導入します。すべての配列がソートされます。このヘルパー関数を使用する
-- sort array
CREATE OR REPLACE FUNCTION f_array_sort(anyarray)
RETURNS anyarray LANGUAGE sql IMMUTABLE AS
$$SELECT ARRAY (SELECT unnest($1) ORDER BY 1)$$;
ヘルパー関数arraymin()
はもう必要ありませんが、次のように簡略化できます。
CREATE OR REPLACE FUNCTION f_array_min(anyarray)
RETURNS anyelement LANGUAGE sql IMMUTABLE AS
$$SELECT min(a) FROM unnest($1) a$$;
配列の左半分と右半分を特定の要素で分割するためにさらに2つ:
-- split left array at given element
CREATE OR REPLACE FUNCTION f_array_left(anyarray, anyelement)
RETURNS anyarray LANGUAGE sql IMMUTABLE AS
$$SELECT ARRAY (SELECT * FROM unnest($1) a WHERE a < $2 ORDER BY 1)$$;
-- split right array at given element
CREATE OR REPLACE FUNCTION f_array_right(anyarray, anyelement)
RETURNS anyarray LANGUAGE sql IMMUTABLE AS
$$SELECT ARRAY (SELECT * FROM unnest($1) a WHERE a >= $2 ORDER BY 1)$$;
これはall残りを行います:
WITH u AS ( -- identify candidates
SELECT m.id, rec_id, m.val, m.valid_on, m.valid_during
, u.val AS u_val, u.valid_on AS u_valid_on
FROM master m
JOIN updates u USING (rec_id)
WHERE m.val <> u.val
AND m.valid_during @> u.valid_on
FOR UPDATE -- lock for update
)
, upd1 AS ( -- case 2: no overlap, no split
UPDATE master m -- shrink old row
SET valid_during = daterange(lower(u.valid_during), u.u_valid_on)
FROM u
WHERE u.id = m.id
AND u.u_valid_on > m.valid_on[array_upper(m.valid_on, 1)]
RETURNING m.id
)
, ins1 AS ( -- insert new row
INSERT INTO master (rec_id, val, valid_on, valid_during)
SELECT u.rec_id, u.u_val, ARRAY[u.u_valid_on]
, daterange(u.u_valid_on, upper(u.valid_during))
FROM upd1
JOIN u USING (id)
)
, upd2 AS ( -- case 3: overlap, need to split row
UPDATE master m -- shrink to first half
SET valid_during = daterange(lower(u.valid_during), u.u_valid_on)
, valid_on = f_array_left(u.valid_on, u.u_valid_on)
FROM u
LEFT JOIN upd1 USING (id)
WHERE upd1.id IS NULL -- all others
AND u.id = m.id
RETURNING m.id, f_array_right(u.valid_on, u.u_valid_on) AS arr_right
)
INSERT INTO master (rec_id, val, valid_on, valid_during)
-- new row
SELECT u.rec_id, u.u_val, ARRAY[u.u_valid_on]
, daterange(u.u_valid_on, upd2.arr_right[1])
FROM upd2
JOIN u USING (id)
UNION ALL -- second half of old row
SELECT u.rec_id, u.val, upd2.arr_right
, daterange(upd2.arr_right[1], upper(u.valid_during))
FROM upd2
JOIN u USING (id);
これに触れる前に、 data-modifying CTEs (writeable CTEs)の概念を理解する必要があります。あなたが提供したコードから判断すると、Postgresの使い方はわかります。
FOR UPDATE
は、同時書き込みアクセスによる競合状態を回避するためのものです。あなたがテーブルに書き込む唯一のユーザーであれば、それは必要ありません。
私は一枚の紙を取り、タイムラインを描きました。
各行は更新/挿入onceのみであり、操作は単純で、大まかに最適化されています。高価なウィンドウ関数はありません。これはperformとなるはずです。いずれにしても、以前のアプローチよりもはるかに高速です。
u.valid_on
とm.valid_on
にdistinct column namesを使用すると、関連はありますが異なるものになりますが、少し混乱しにくくなります。
CTE upd2
:f_array_right(u.valid_on, u.u_valid_on) AS arr_right
のRETURNING
句で分割配列の右半分を計算します。これは、次のステップで数回必要になるためです。これは、もう1つのCTEを節約する(法的)トリックです。
don't involve unnesting the master table
のソリューションの場合:少なくとも並べ替えられていない限り、配列を分割するために、配列valid_on
を/ネスト解除する必要があります。また、ヘルパー関数arraymin()
は、すでにそれをネスト解除しています。