web-dev-qa-db-ja.com

Postgres 9.xの複雑なクエリからテーブルレコードを挿入/更新/削除する効率的な方法

レコードのセットを返すこの関数があり、それらのレコードをテーブルに永続化する必要があります。私はそれを一日に百回しなければなりません。

私の最初のアプローチは、テーブルからデータをクリアし、すべてのレコードを再度挿入することでした。

-- 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
                   )
);
4
Christian

それはカーディナリティに大きく依存します。
古いテーブルと新しいテーブルの行数は?これらのうちいくつが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を割り当てるようにしてください。

5