レコードのセットを返すこの関数があり、それらのレコードをテーブルに永続化する必要があります。私はそれを一日に百回しなければなりません。
私の最初のアプローチは、テーブルからデータをクリアし、すべてのレコードを再度挿入することでした。
-- CLEAR MY TABLE
DELETE FROM MY_TABLE;
-- POPULATE MY TABLE WITH MY FUNCTION'S RESULT
INSERT INTO MY_TABLE (COLUMN1, COLUMN2, COLUMN3)
SELECT COLUMN1, COLUMN2, COLUMN3
FROM MY_FUNCTION(PARAM1, PARAM2, PARAM3);
ここまでは順調ですね。しかし、私のテーブルには多くのトリガーがあり、関数が数千のレコードを返す場合、このアプローチは非常に非効率的です。
次に、私はこのアプローチに移動しました:
-- CREATE A TEMPORARY TABLE
CREATE GLOBAL TEMPORARY TABLE MY_TEMP_TABLE
(COLUMN1 TEXT, COLUMN2 TEXT, COLUMN3 TEXT);
-- POPULATE MY TEMP TABLE WITH MY FUNCTION'S RESULT
INSERT INTO MY_TEMP_TABLE (COLUMN1, COLUMN2, COLUMN3)
SELECT COLUMN1, COLUMN2, COLUMN3
FROM MY_FUNCTION(PARAM1, PARAM2, PARAM3);
-- CREATE AN INDEX FOR HELP PERFORMANCE
CREATE INDEX MY_TEMP_TABLE_INDEX ON MY_TEMP_TABLE (COLUMN1, COLUMN2, COLUMN3);
-- DELETE FROM MY TABLE WHERE NOT EXISTS IN MY TEMP TABLE
DELETE FROM MY_TABLE T
WHERE NOT EXISTS (SELECT 1
FROM MY_TEMP_TABLE T2
WHERE T2.COLUNN1 = T.COLUMN1);
-- UPDATE MY TABLE WHERE COLUMNS ARE DIFFERENT IN MY TEMP TABLE
UPDATE MY_TABLE T
SET COLUMN2 = T2.COLUMN2,
COLUMN3 = T2.COLUMN3
FROM MY_TEMP_TABLE T2
WHERE T2.COLUNN1 = T.COLUMN1
AND (T2.COLUMN2 <> T.COLUMN2 OR T2.COLUMN3 <> T.COLUMN3);
-- INSERT INTO MY TABLE WHER EXISTS IN MY TEMP TABLE
INSERT INTO FROM MY_TABLE T (COLUMN1, COLUMN2, COLUMN3)
(SELECT COLUMN1, COLUMN2, COLUMN3
FROM MY_TEMP_TABLE T2
WHERE NOT EXISTS (SELECT 1 FROM TABLE T3 WHERE T3.COLUNN1 = T2.COLUMN1);
しかし、まだパフォーマンスの問題があります。このtemp_tableを作成すると、非常に多くのリソースが消費されると思います。それに、これは最善の方法ではないと思います。
皆さんは別のアプローチを提案できますか?それともこれが最善の方法だと思いますか?
編集:
テストのために、上記のスクリプトを実行できます。
これは、テーブル/トリガー/関数/などを作成するスクリプトです...
-- THIS TABLE CONTAINS INFORMATION THAT USERS NEED
CREATE TABLE USER_INFO (USER_ID TEXT, INFO_ID TEXT, INFO1 TEXT, INFO2 TEXT, INFO3 TEXT, INFO4 TEXT, INFO5 TEXT);
ALTER TABLE USER_INFO ADD CONSTRAINT USER_INFO_PK PRIMARY KEY (USER_ID, INFO_ID);
-- THIS TABLE CONTAINS A KIND OF FLAG, INDICATING FOR USERS THEIR INFORMATION HAS BEEN "REFRESHED" AND THEY SHOULD GET ROWS FROM "USER_INFO"
CREATE TABLE USER_HAS_NEW_INFO (USER_ID TEXT, INFO_DATE TIMESTAMP);
ALTER TABLE USER_HAS_NEW_INFO ADD CONSTRAINT USER_HAS_NEW_INFO_PK PRIMARY KEY (USER_ID, INFO_DATE);
-- CREATE TRIGGER FUNCTION
CREATE OR REPLACE FUNCTION TF_USER_INFO()
RETURNS trigger AS
$BODY$
begin
-- IF SOME INFO HAS CHANGED
if (TG_OP = 'INSERT')
OR
(
(TG_OP = 'UPDATE')
AND
(
(COALESCE(NEW.INFO1,'') <> COALESCE(OLD.INFO1,'')) OR
(COALESCE(NEW.INFO2,'') <> COALESCE(OLD.INFO2,'')) OR
(COALESCE(NEW.INFO3,'') <> COALESCE(OLD.INFO3,'')) OR
(COALESCE(NEW.INFO4,'') <> COALESCE(OLD.INFO4,'')) OR
(COALESCE(NEW.INFO5,'') <> COALESCE(OLD.INFO5,''))
)
)
then
-- INSERT A NEW ROW INTO USER_HAS_NEW_INFO
INSERT INTO USER_HAS_NEW_INFO (USER_ID, INFO_DATE)
SELECT NEW.USER_ID, CURRENT_TIMESTAMP
WHERE NOT EXISTS (SELECT 1
FROM USER_HAS_NEW_INFO
WHERE USER_ID = NEW.USER_ID
AND INFO_DATE = CURRENT_TIMESTAMP
);
end if;
RETURN NEW;
end;
$BODY$
LANGUAGE plpgsql VOLATILE;
-- CREATE TRIGGER
CREATE TRIGGER T_USER_INFO
AFTER INSERT OR UPDATE OR DELETE
ON USER_INFO
FOR EACH ROW
EXECUTE PROCEDURE TF_USER_INFO();
CREATE OR REPLACE FUNCTION CALCULATE_USERS_INFO()
RETURNS SETOF USER_INFO AS
$BODY$
DECLARE
vUSER_INFO USER_INFO%rowtype;
BEGIN
-- HERE GOES A COMPLEX QUERY PLUS SOME CALCS AND VALIDATIONS
-- BUT, FOR TESTING PORPOUSES, WE CAN DO FOLLOWING:
FOR vUSER_INFO IN
SELECT USER_ID,
INFO_ID,
'A=' || TRUNC(RANDOM() * 1000) || '|' ||
'B=' || TRUNC(RANDOM() * 1000) || '|' ||
'C=' || TRUNC(RANDOM() * 1000) AS INFO1,
'A=' || TRUNC(RANDOM() * 1000) || '|' ||
'B=' || TRUNC(RANDOM() * 1000) || '|' ||
'C=' || TRUNC(RANDOM() * 1000) AS INFO2,
'A=' || TRUNC(RANDOM() * 1000) || '|' ||
'B=' || TRUNC(RANDOM() * 1000) || '|' ||
'C=' || TRUNC(RANDOM() * 1000) AS INFO3,
'A=' || TRUNC(RANDOM() * 1000) || '|' ||
'B=' || TRUNC(RANDOM() * 1000) || '|' ||
'C=' || TRUNC(RANDOM() * 1000) AS INFO4,
'A=' || TRUNC(RANDOM() * 1000) || '|' ||
'B=' || TRUNC(RANDOM() * 1000) || '|' ||
'C=' || TRUNC(RANDOM() * 1000) AS INFO5
FROM GENERATE_SERIES(1,1500) AS USER_ID
CROSS JOIN GENERATE_SERIES(1,500) AS INFO_ID
LOOP
RETURN NEXT vUSER_INFO;
END LOOP;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
そして、私が1日に何度も実行するスクリプトは次のとおりです。
-- CREATE A TEMPORARY TABLE
CREATE GLOBAL TEMPORARY TABLE USER_INFO_TEMP
(USER_ID TEXT, INFO_ID TEXT, INFO1 TEXT, INFO2 TEXT, INFO3 TEXT, INFO4 TEXT, INFO5 TEXT)
ON COMMIT DROP;
-- POPULATE MY TEMP TABLE WITH MY FUNCTION'S RESULT
INSERT INTO USER_INFO_TEMP (USER_ID, INFO_ID, INFO1, INFO2, INFO3, INFO4, INFO5)
SELECT USER_ID, INFO_ID, INFO1, INFO2, INFO3, INFO4, INFO5
FROM CALCULATE_USERS_INFO();
-- CREATE AN INDEX FOR HELP PERFORMANCE
CREATE INDEX USER_INFO_TEMP_INDEX ON USER_INFO_TEMP (USER_ID, INFO_ID);
-- DELETE FROM MY TABLE WHERE NOT EXISTS IN MY TEMP TABLE
DELETE FROM USER_INFO T
WHERE NOT EXISTS (SELECT 1
FROM USER_INFO_TEMP T2
WHERE T2.USER_ID = T.USER_ID
AND T2.INFO_ID = T.INFO_ID);
-- UPDATE MY TABLE WHERE COLUMNS ARE DIFFERENT IN MY TEMP TABLE
UPDATE USER_INFO T
SET INFO1 = T2.INFO1,
INFO2 = T2.INFO2,
INFO3 = T2.INFO3,
INFO4 = T2.INFO4,
INFO5 = T2.INFO5
FROM USER_INFO_TEMP T2
WHERE T2.USER_ID = T.USER_ID
AND T2.INFO_ID = T.INFO_ID
AND (T2.INFO1 <> T.INFO1 OR
T2.INFO2 <> T.INFO2 OR
T2.INFO3 <> T.INFO3 OR
T2.INFO4 <> T.INFO4 OR
T2.INFO5 <> T.INFO5
);
-- INSERT INTO TABLE WHERE EXISTS IN TEMP AND NOT EXISTS IN TABLE
INSERT INTO USER_INFO (USER_ID, INFO_ID, INFO1, INFO2, INFO3, INFO4, INFO5)
(SELECT USER_ID, INFO_ID, INFO1, INFO2, INFO3, INFO4, INFO5
FROM USER_INFO_TEMP T2
WHERE NOT EXISTS (SELECT 1
FROM USER_INFO T3
WHERE T3.USER_ID = T2.USER_ID
AND T3.INFO_ID = T2.INFO_ID
)
);
それはカーディナリティに大きく依存します。
古いテーブルと新しいテーブルの行数は?これらのうちいくつがDELETE
/UPDATE
/INSERT
になりますか?
TRUNCATE
が最速一般に、テーブルの大部分が変更される場合、TRUNCATE
/INSERT
from関数がおそらく最も速い方法です。同じトランザクションで行われる場合、PostgresはWALを記述する必要はありません(とにかくゼロから始めるため)。また、膨張のない元のテーブルを取得します。これは、このプロセスの次の反復に積極的に反映されます。大きなテーブルの場合は、インデックスを削除して再作成します。この関連する回答の詳細:
残りは、何らかの理由で既存の行を所定の位置に保持したい場合にのみ適用されます。
トリガーが邪魔になっている場合(あなたが書いていて、私が確信していないが、今のところは仮定しましょう)。または、テーブル内に失われない追加の列がある場合。
変更セットに含まれている行数に応じて(関数から返されます)...
〜1000未満、それは多くの要因に依存します。 data-modifying CTE (関数結果用の自動で安価な内部一時テーブルを使用)はおそらく最速です:
WITH x AS (SELECT * FROM calculate_users_info())
, del AS (
DELETE FROM user_info t
WHERE NOT EXISTS (
SELECT 1 FROM x
WHERE user_id = t.user_id
AND info_id = t.info_id
)
, upd AS (
UPDATE user_info t
SET (info1, info2, info3, info4, info5)
= (x.info1, x.info2, x.info3, x.info4, x.info5)
FROM x
WHERE x.user_id = t.user_id
AND x.info_id = t.info_id
AND (x.info1 <> t.info1 OR
x.info2 <> t.info2 OR
x.info3 <> t.info3 OR
x.info4 <> t.info4 OR
x.info5 <> t.info5)
)
INSERT INTO user_info
(user_id, info_id, info1, info2, info3, info4, info5)
SELECT user_id, info_id, info1, info2, info3, info4, info5
FROM x
WHERE NOT EXISTS (
SELECT 1
FROM user_info t3
WHERE t3.user_id = t2.user_id
AND t3.info_id = t2.info_id
)
;
この場合、一時テーブルで作成するスクリプトはほとんど問題ありません。主な利点は、一時テーブルのインデックスです-欠落した統計が原因で、適切に活用できませんでした。
他にもいくつか提案があります:
CREATE TEMP TABLE user_info_tmp ON COMMIT DROP AS -- directly from SELECT
SELECT * FROM calculate_users_info();
CREATE INDEX user_info_tmp_idx ON user_info_tmp (user_id, info_id);
ANALYZE user_info_tmp; -- !!!
DELETE FROM user_info t -- with EXISTS semi-anti-join
WHERE NOT EXISTS (
SELECT 1 FROM user_info_tmp
WHERE user_id = t.user_id
AND info_id = t.info_id
);
ANALYZE user_info; -- only if large parts have been removed
UPDATE user_info t -- with short syntax
SET (info1, info2, info3, info4, info5)
= (x.info1, x.info2, x.info3, x.info4, x.info5) -- shorter, not faster
FROM user_info_tmp x
WHERE x.user_id = t.user_id
AND x.info_id = t.info_id
AND (x.info1 <> t.info1 OR x.info2 <> t.info2 OR x.info3 <> t.info3
OR x.info4 <> t.info4 OR x.info5 <> t.info5);
INSERT INTO user_info -- with join syntax
(user_id, info_id, info1, info2, info3, info4, info5)
SELECT user_id, info_id, info1, info2, info3, info4, info5
FROM user_info_tmp x
LEFT JOIN user_info u USING (user_id, info_id)
WHERE u.user_id IS NULL; -- shorter, maybe faster
一時テーブルは自動的に分析されません。また、同じトランザクションで作成され、すぐに使用されるテーブルは、通常、autovacuum
が起動する機会を与えません。詳細:
9.1でも通常のVACUUM ANALYZEは推奨されますか?
どちらの理由でも、テーブルでANALYZE
を手動で実行する必要があります。これにより、クエリプランが大幅に誤解されることを回避できます。マイナーな追加の最適化:無関係な列の統計ターゲットを0に設定する場合があります-例では(user_id, info_id)
を除くすべて。
一時テーブルにGLOBAL
を使用しないでください。 ドキュメントごと:
GLOBAL
またはLOCAL
互換性のために無視されます。これらのキーワードの使用は非推奨です。詳細については、
CREATE TABLE
を参照してください。
関数の結果から一時テーブルを自動的に作成できます。はるかに短いコードと少し速いも。
短い構文のバリエーションを検討してください。これは主にコードを短縮し、パフォーマンスに大きな変化はありません。
work_mem
/ temp_bufers
どちらの方法でも、高速にするために十分なRAM=が必要です。小さなカーディナリティにはほとんど関係ありませんが、大きなテーブルには重要です。中程度の設定では、数千がメモリ制限に触れてはなりません。さらに、CTEには十分なwork_mem
を、一時テーブルにはtemp_bufers
を割り当てるようにしてください。