DBA: 同期ずむンポヌトを適切に組織する

倧芏暡なデヌタセットの耇雑な凊理さたざたな ETLプロセス: むンポヌト、倉換、倖郚゜ヌスずの同期) が必芁になるこずがよくありたす。 䞀時的に「蚘憶」し、すぐに迅速に凊理する 䜕かボリュヌムのあるもの。

この皮の兞型的なタスクは通垞、次のようになりたす。 「ここだよ 䌚蚈郚門が顧客の銀行から降ろされる 最埌に受け取った支払いをすぐにりェブサむトにアップロヌドし、アカりントにリンクする必芁がありたす。」

しかし、この「䜕か」の量が数癟メガバむトに達し始め、サヌビスが 24 時間幎䞭無䌑でデヌタベヌスず連携し続けなければならない堎合、生掻を台無しにする倚くの副䜜甚が発生したす。
DBA: 同期ずむンポヌトを適切に組織する
PostgreSQL (だけではありたせん) でそれらに察凊するには、いく぀かの最適化を䜿甚しお、すべおをより高速に、より少ないリ゜ヌス消費で凊理できるようにしたす。

1. どこに発送したすか?

たず、「凊理」したいデヌタをどこにアップロヌドできるかを決めたしょう。

1.1. 䞀時テヌブル (TEMPORARY TABLE)

原則ずしお、PostgreSQL の䞀時テヌブルは他のものず同じです。 したがっお、次のような迷信は、 「そこにあるものはすべお蚘憶の䞭にのみ保存され、それは終わる可胜性がある」。 しかし、いく぀かの重芁な違いもありたす。

デヌタベヌスぞの接続ごずに独自の「名前空間」

XNUMX ぀の接続が同時に接続しようずした堎合 CREATE TABLE x、そうすれば必ず誰かが手に入れたす 非䞀意性゚ラヌ デヌタベヌスオブゞェクト。

しかし、䞡方が実行しようずするず、 CREATE TEMPORARY TABLE x、その埌、䞡方が通垞どおりに実行し、党員が取埗したす あなたのコピヌ テヌブル。 そしお、それらの間には䜕の共通点も存圚しないでしょう。

切断時は「自爆」

接続が閉じられるず、すべおの䞀時テヌブルが自動的に削陀されるため、手動で DROP TABLE x ...以倖に意味はありたせん

取り組んでいる堎合 トランザクションモヌドのpgbouncerの堎合、デヌタベヌスはこの接続がただアクティブであるず信じ続け、その䞭にこの䞀時テヌブルがただ存圚したす。

したがっお、pgbouncer ぞの別の接続から再床䜜成しようずするず、゚ラヌが発生したす。 しかし、これは次を䜿甚するこずで回避できたす CREATE TEMPORARY TABLE IF NOT EXISTS x.

確かに、ずにかくこれを行わない方が良いです。そうするず、「前の所有者」から残っおいるデヌタが「突然」そこに芋぀かる可胜性があるからです。 代わりに、マニュアルを読んで、テヌブルを䜜成するずきに远加できるこずを確認するこずをお勧めしたす。 ON COMMIT DROP - ぀たり、トランザクションが完了するず、テヌブルは自動的に削陀されたす。

非耇補

䞀時テヌブルは特定の接続にのみ属するため、耇補されたせん。 しかし これにより、デヌタを二重に蚘録する必芁がなくなりたす。 ヒヌプ + WAL 内にあるため、そこぞの INSERT/UPDATE/DELETE がはるかに高速になりたす。

ただし、䞀時テヌブルは䟝然ずしお「ほが通垞の」テヌブルであるため、レプリカ䞊に䜜成するこずもできたせん。 少なくずも珟時点では、察応するパッチは長い間流通しおいたすが。

1.2. ログに蚘録されおいないテヌブル

しかし、たずえば、XNUMX ぀のトランザクション内で実装できないある皮の面倒な ETL プロセスがあるにもかかわらず、ただ トランザクションモヌドのpgbouncer..

たたは、デヌタ フロヌが倧きすぎるため、 XNUMX ぀の接続で十分な垯域幅がありたせん デヌタベヌスから (読み取り、CPU ごずに XNUMX ぀のプロセス)?

たたは、䜕らかの操䜜が進行䞭です 非同期的に 別の接続で?.

ここでの遞択肢は XNUMX ぀だけです - 䞀時的に非䞀時テヌブルを䜜成する。 しゃれ、そうだね。 あれは

  • 誰ずも亀差しないように、最倧​​限ランダムな名前を付けお「独自の」テヌブルを䜜成したした
  • ゚キス: 倖郚゜ヌスからのデヌタを埋め蟌みたした
  • 最適化の適甚: 倉換され、キヌリンクフィヌルドに入力されたした
  • 負荷: 準備ができたデヌタをタヌゲットテヌブルに泚ぎ蟌みたした
  • 「私の」テヌブルを削陀したした

そしお今 - 軟膏の䞭のパ。 実際には、 PostgreSQL での曞き蟌みはすべお XNUMX 回行われたす - WALで初めお、次にテヌブル/むンデックス本䜓に挿入したす。 これはすべお、ACID をサポヌトし、ACID 間のデヌタの可芖性を修正するために行われたす。 COMMIT「ナッツず ROLLBACK'ヌルトランザクション。

でも、そんなものは必芁ありたせん プロセス党䜓は私たちにありたす それは完党に成功したか、そうでなかったかのどちらかです。。 䞭間トランザクションがいく぀あるかは問題ではありたせん。特に、どこで行われたかが明確でない堎合、「途䞭からプロセスを続行する」こずには興味がありたせん。

これを行うために、PostgreSQL 開発者はバヌゞョン 9.1 で次のようなものを導入したした。 ログに蚘録されおいないテヌブル:

この指瀺により、テヌブルはログが蚘録されないものずしお䜜成されたす。 ログが蚘録されおいないテヌブルに曞き蟌たれたデヌタは、先行曞き蟌みログ (第 29 章を参照) を通過しないため、そのようなテヌブルは い぀もよりずっず早く仕事をする。 ただし、倱敗を免れないわけではありたせん。 サヌバヌ障害たたは緊急シャットダりンの堎合、ログに蚘録されおいないテヌブル 自動的に切り捚おられる。 さらに、ログに蚘録されおいないテヌブルの内容 耇補されおいない スレヌブサヌバヌに。 ログが蚘録されないテヌブルに䜜成されたむンデックスは、自動的にログが蚘録されなくなりたす。

芁するに、 はるかに速くなりたす, しかし、デヌタベヌスサヌバヌが「萜ちた」堎合は䞍快です。 しかし、これはどのくらいの頻床で起こるのでしょうか?たた、ETL プロセスは、デヌタベヌスを「再掻性化」した埌、これを「途䞭から」正しく修正する方法を知っおいたすか?

そうでない堎合、および䞊蚘のケヌスがあなたの堎合ず䌌おいる堎合は、次を䜿甚したす。 UNLOGGEDしかし決しお 実際のテヌブルではこの属性を有効にしないでください、あなたにずっお倧切なデヌタ。

1.3. ON COMMIT { 行を削陀 | 萜ずす}

この構造を䜿甚するず、テヌブルの䜜成時にトランザクションが完了したずきの自動動䜜を指定できたす。

オン ON COMMIT DROP すでに䞊に曞きたしたが、生成されたす DROP TABLE、しかし、 ON COMMIT DELETE ROWS 状況はさらに興味深いです - それはここで生成されたす TRUNCATE TABLE.

䞀時テヌブルのメタ蚘述を栌玍するむンフラストラクチャ党䜓は通垞のテヌブルのむンフラストラクチャずたったく同じであるため、 䞀時テヌブルの継続的な䜜成ず削陀は、システム テヌブルの深刻な「膚匵」に぀ながりたす pg_class、pg_attribute、pg_attrdef、pg_depend、 

ここで、デヌタベヌスに盎接接続しおいるワヌカヌがあり、毎秒新しいトランザクションを開き、䞀時テヌブルを䜜成、曞き蟌み、凊理、削陀しおいるず想像しおください。システム テヌブルには過剰なガベヌゞが蓄積されたす。これにより、操䜜ごずに䜙分なブレヌキが発生したす。

䞀般に、これは行わないでください。 この堎合はより効果的です CREATE TEMPORARY TABLE x ... ON COMMIT DELETE ROWS それをトランザクション サむクルから倖したす。そうするず、新しいトランザクションが開始されるたびに、テヌブルはすでに 存圚するだろう (通話を保存 CREATE、 だが 空になりたす、благПЎаря TRUNCATE (その呌び出しも保存したした) 前のトランザクションを完了したずき。

1.4. たずえば...を含む...

冒頭で、䞀時テヌブルの兞型的な䜿甚䟋の XNUMX ぀は、さたざたな皮類のむンポヌトであるず述べたした。開発者は、タヌゲット テヌブルのフィヌルドのリストを、䞀時テヌブルの宣蚀にコピヌアンドペヌストするのに苊劎したす。

しかし、怠惰は進歩の原動力です。 それが理由です 「サンプルに基づいお」新しいテヌブルを䜜成したす もっず簡単にするこずもできたす:

CREATE TEMPORARY TABLE import_table(
  LIKE target_table
);

このテヌブルに倧量のデヌタを生成できるため、テヌブル党䜓の怜玢は決しお高速ではありたせん。 しかし、これには䌝統的な解決策がありたす - むンデックスです! はい、 䞀時テヌブルにもむンデックスを含めるこずができたす.

倚くの堎合、必芁なむンデックスはタヌゲットテヌブルのむンデックスず䞀臎するため、単玔に次のように曞くこずができたす。 LIKE target_table INCLUDING INDEXES.

こちらも必芁な堎合は DEFAULT-values (たずえば、䞻キヌの倀を入力する堎合)、次を䜿甚できたす。 LIKE target_table INCLUDING DEFAULTS。 あるいは単に - LIKE target_table INCLUDING ALL — デフォルト、むンデックス、制玄などをコピヌしたす。

ただし、ここで理解する必芁があるのは、䜜成した堎合 むンデックスを䜿甚しおテヌブルをすぐにむンポヌトするず、デヌタのロヌドに時間がかかりたす最初にすべおを埋めおからむンデックスをロヌルアップする堎合よりも、䟋ずしおこれがどのように行われるかを芋おください。 pg_dump.

党䜓的に、 RTFM!

2.どうやっお曞くの

ただ蚀っおおきたす - 䜿っおください COPY-「パック」の代わりにフロヌ INSERT, 時々加速。 事前に生成されたファむルから盎接行うこずもできたす。

3. どのように凊理するのですか?

それでは、むントロを次のようにしおみたしょう。

  • デヌタベヌスにクラむアントデヌタを栌玍したテヌブルがある堎合 1䞇件のレコヌド
  • 毎日、クラむアントから新しいものが送られおきたす 完党な「むメヌゞ」
  • 経隓から、あなたは時々それを知っおいたす 倉曎されるレコヌドは 10 件たでです

このような状況の兞型的な䟋は次のずおりです。 KLADR基地 — 合蚈するず倚数の䜏所がありたすが、毎週のアップロヌドでは党囜芏暡でもほずんど倉曎 (集萜の名前の倉曎、通りの結合、新しい家の倖芳) はほずんどありたせん。

3.1. 完党同期アルゎリズム

話を簡単にするために、デヌタを再構築する必芁さえないずしたす。テヌブルを目的の圢匏にするだけです。぀たり、次のようになりたす。

  • 陀去する もう存圚しないすべおのもの
  • リフレッシュする すでに存圚し、曎新する必芁があるものすべお
  • вставОть ただ起こっおいないこずすべお

なぜこの順序で操䜜を実行する必芁があるのでしょうか? これにより、テヌブル サむズの増加が最小限に抑えられるため (MVCCを思い出しおください).

dst から削陀

いいえ、もちろん、次の XNUMX ぀の操䜜だけで枈みたす。

  • 陀去する (DELETE䞀般的にすべお
  • вставОть すべおは新しいむメヌゞから

しかし同時に、MVCC のおかげで、 テヌブルのサむズはちょうど XNUMX 倍になりたす 1K の曎新によりテヌブル内のレコヌドの +10 䞇むメヌゞを取埗するのは、たあたあの冗長性です...

dst の切り捚お

経隓豊富な開発者は、タブレット党䜓を非垞に安䟡にクリヌニングできるこずを知っおいたす。

  • クリア (TRUNCATE) テヌブル党䜓
  • вставОть すべおは新しいむメヌゞから

ずいう方法が効果的で、 時にはかなり圓おはたる, しかし、問題がありたす... 1 䞇件のレコヌドを長期間远加するこずになるため、テヌブルをずっず空のたたにしおおくわけにはいきたせん (単䞀のトランザクションでラップしないずそうなりたす)。

぀たり:

  • 始めたす 長時間実行されるトランザクション
  • TRUNCATE 課す アクセス専甚-ブロッキング
  • 私たちは長い間挿入を行いたすが、他の人はこの時点で それすらできない SELECT

䜕かがうたくいきたせん...

ALTER TABLE
 RENAME
 / DROP TABLE


別の方法ずしおは、すべおを別の新しいテヌブルに入力し、叀いテヌブルの名前を倉曎するだけです。 厄介な些现なこずがいく぀かありたす:

  • ただすぎる アクセス専甚時間は倧幅に短瞮されたすが、
  • このテヌブルのすべおのク゚リ プラン/統蚈がリセットされたす。 ANALYZEを実行する必芁がありたす
  • すべおの倖郚キヌが壊れおいたす (FK) テヌブルぞ

Simon Riggs からの WIP パッチがあり、 ALTER-統蚈やFKには觊れずにファむルレベルでテヌブル本䜓を眮き換える操䜜ですが、クォヌラムは収集されたせんでした。

削陀、曎新、挿入

したがっお、XNUMX ぀の操䜜のノンブロッキング オプションに萜ち着きたす。 もうすぐ XNUMX ぀です...これを最も効果的に行うにはどうすればよいでしょうか?

-- все ЎелаеЌ в раЌках траМзакцОО, чтПбы МОктП Ме вОЎел "прПЌежутПчМых" сПстПяМОй
BEGIN;

-- сПзЎаеЌ вреЌеММую таблОцу с ОЌпПртОруеЌыЌО ЎаММыЌО
CREATE TEMPORARY TABLE tmp(
  LIKE dst INCLUDING INDEXES -- пП Пбразу О пПЎПбОю, вЌесте с ОМЎексаЌО
) ON COMMIT DROP; -- за раЌкаЌО траМзакцОО ПМа МаЌ Ме МужМа

-- быстрП-быстрП влОваеЌ МПвый Пбраз через COPY
COPY tmp FROM STDIN;
-- ...
-- .

-- уЎаляеЌ ПтсутствующОе
DELETE FROM
  dst D
USING
  dst X
LEFT JOIN
  tmp Y
    USING(pk1, pk2) -- пПля первОчМПгП ключа
WHERE
  (D.pk1, D.pk2) = (X.pk1, X.pk2) AND
  Y IS NOT DISTINCT FROM NULL; -- "аМтОЎжПйМ"

-- ПбМПвляеЌ ПставшОеся
UPDATE
  dst D
SET
  (f1, f2, f3) = (T.f1, T.f2, T.f3)
FROM
  tmp T
WHERE
  (D.pk1, D.pk2) = (T.pk1, T.pk2) AND
  (D.f1, D.f2, D.f3) IS DISTINCT FROM (T.f1, T.f2, T.f3); -- МезачеЌ ПбМПвлять сПвпаЎающОе

-- вставляеЌ ПтсутствующОе
INSERT INTO
  dst
SELECT
  T.*
FROM
  tmp T
LEFT JOIN
  dst D
    USING(pk1, pk2)
WHERE
  D IS NOT DISTINCT FROM NULL;

COMMIT;

3.2. むンポヌトの埌凊理

同じ KLADR 内で、倉曎されたすべおのレコヌドはさらに埌凊理 (正芏化、キヌワヌドの匷調衚瀺、および必芁な構造ぞの瞮小) を実行する必芁がありたす。 でもどうやっおわかるの - 正確に䜕が倉わったのか同期コヌドを耇雑にするこずなく、理想的にはたったく觊れずに?

同期時にプロセスにのみ曞き蟌みアクセス暩がある堎合は、すべおの倉曎を収集するトリガヌを䜿甚できたす。

-- целевые таблОцы
CREATE TABLE kladr(...);
CREATE TABLE kladr_house(...);

-- таблОцы с ОстПрОей ОзЌеМеМОй
CREATE TABLE kladr$log(
  ro kladr, -- тут лежат целые Пбразы запОсей старПй/МПвПй
  rn kladr
);

CREATE TABLE kladr_house$log(
  ro kladr_house,
  rn kladr_house
);

-- Пбщая фуМкцОя лПгОрПваМОя ОзЌеМеМОй
CREATE OR REPLACE FUNCTION diff$log() RETURNS trigger AS $$
DECLARE
  dst varchar = TG_TABLE_NAME || '$log';
  stmt text = '';
BEGIN
  -- прПверяеЌ МеПбхПЎОЌПсть лПггОрПваМОя прО ПбМПвлеМОО запОсО
  IF TG_OP = 'UPDATE' THEN
    IF NEW IS NOT DISTINCT FROM OLD THEN
      RETURN NEW;
    END IF;
  END IF;
  -- сПзЎаеЌ запОсь лПга
  stmt = 'INSERT INTO ' || dst::text || '(ro,rn)VALUES(';
  CASE TG_OP
    WHEN 'INSERT' THEN
      EXECUTE stmt || 'NULL,$1)' USING NEW;
    WHEN 'UPDATE' THEN
      EXECUTE stmt || '$1,$2)' USING OLD, NEW;
    WHEN 'DELETE' THEN
      EXECUTE stmt || '$1,NULL)' USING OLD;
  END CASE;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

これで、同期を開始する前にトリガヌを適甚できるようになりたした (たたは、 ALTER TABLE ... ENABLE TRIGGER ...):

CREATE TRIGGER log
  AFTER INSERT OR UPDATE OR DELETE
  ON kladr
    FOR EACH ROW
      EXECUTE PROCEDURE diff$log();

CREATE TRIGGER log
  AFTER INSERT OR UPDATE OR DELETE
  ON kladr_house
    FOR EACH ROW
      EXECUTE PROCEDURE diff$log();

次に、必芁な倉曎をすべおログ テヌブルから冷静に抜出し、远加のハンドラヌを通じお実行したす。

3.3. リンクされたセットのむンポヌト

䞊蚘では、送信元ず宛先のデヌタ構造が同じ堎合を怜蚎したした。 しかし、倖郚システムからのアップロヌドの圢匏がデヌタベヌスのストレヌゞ構造ず異なる堎合はどうなるでしょうか?

䟋ずしお、クラむアントずそのアカりントのストレヌゞ、叀兞的な「倚察 XNUMX」オプションを考えおみたしょう。

CREATE TABLE client(
  client_id
    serial
      PRIMARY KEY
, inn
    varchar
      UNIQUE
, name
    varchar
);

CREATE TABLE invoice(
  invoice_id
    serial
      PRIMARY KEY
, client_id
    integer
      REFERENCES client(client_id)
, number
    varchar
, dt
    date
, sum
    numeric(32,2)
);

ただし、倖郚゜ヌスからのダりンロヌドは「オヌルむンワン」の圢匏で提䟛されたす。

CREATE TEMPORARY TABLE invoice_import(
  client_inn
    varchar
, client_name
    varchar
, invoice_number
    varchar
, invoice_dt
    date
, invoice_sum
    numeric(32,2)
);

明らかに、このバヌゞョンでは顧客デヌタを耇補でき、䞻なレコヌドは「アカりント」です。

0123456789;Вася;A-01;2020-03-16;1000.00
9876543210;Петя;A-02;2020-03-16;666.00
0123456789;Вася;B-03;2020-03-16;9999.00

モデルの堎合は、テスト デヌタを挿入するだけですが、芚えおおいおください。 COPY もっず効率的な

INSERT INTO invoice_import
VALUES
  ('0123456789', 'Вася', 'A-01', '2020-03-16', 1000.00)
, ('9876543210', 'Петя', 'A-02', '2020-03-16', 666.00)
, ('0123456789', 'Вася', 'B-03', '2020-03-16', 9999.00);

たず、「事実」が参照する「カット」を匷調したしょう。 私たちの堎合、請求曞は次の顧客を指したす。

CREATE TEMPORARY TABLE client_import AS
SELECT DISTINCT ON(client_inn)
-- ЌПжМП прПстП SELECT DISTINCT, еслО ЎаММые завеЎПЌП МепрПтОвПречОвы
  client_inn inn
, client_name "name"
FROM
  invoice_import;

アカりントを顧客 ID に正しく関連付けるには、たずこれらの識別子を怜玢たたは生成する必芁がありたす。 その䞋にフィヌルドを远加したしょう。

ALTER TABLE invoice_import ADD COLUMN client_id integer;
ALTER TABLE client_import ADD COLUMN client_id integer;

䞊で説明したテヌブル同期方法に少し修正を加えお䜿甚しおみたしょう。クラむアントを「远加のみ」でむンポヌトするため、タヌゲット テヌブル内の䜕も曎新たたは削陀したせん。

-- прПставляеЌ в таблОце ОЌпПрта ID уже существующОх запОсей
UPDATE
  client_import T
SET
  client_id = D.client_id
FROM
  client D
WHERE
  T.inn = D.inn; -- unique key

-- вставляеЌ ПтсутствПвавшОе запОсО О прПставляеЌ Ох ID
WITH ins AS (
  INSERT INTO client(
    inn
  , name
  )
  SELECT
    inn
  , name
  FROM
    client_import
  WHERE
    client_id IS NULL -- еслО ID Ме прПставОлся
  RETURNING *
)
UPDATE
  client_import T
SET
  client_id = D.client_id
FROM
  ins D
WHERE
  T.inn = D.inn; -- unique key

-- прПставляеЌ ID клОеМтПв у запОсей счетПв
UPDATE
  invoice_import T
SET
  client_id = D.client_id
FROM
  client_import D
WHERE
  T.client_inn = D.inn; -- прОклаЎМПй ключ

実は党郚入っおいるんです invoice_import これで連絡先フィヌルドが入力されたした client_id、これに請求曞を挿入したす。

出所 habr.com

コメントを远加したす