月曜の朝、営業部長から連絡が来ます。「在庫ダッシュボードの数字が、また昨日のままなんだけど」。基盤担当のあなたは、日次バッチをやめて業務DBの変更をそのまま流す仕組みを入れたいと考えています。しかしDBAは「本番DBに何かをつなぐのは困る」と渋り、情シスは「Kafkaなんて誰が面倒を見るのか」と聞いてきます。経営企画からは「それで月にいくら増えるのか」と確認が入ります。
DBの変更を行単位で拾って、別の場所へ順に流す仕組みをCDC(Change Data Capture、変更データキャプチャ)と呼びます。CDCの道具は似た名前で並んでいますが、得意な場面はまったく違います。どこからどこへ流すのか、何分の遅れまで許されるのかを先に決めれば、候補はほぼ1つに絞れます。
流す元と流す先で、候補はほぼ決まる
| 状況 | 第一候補 | これを選ぶと後で困ること |
|---|---|---|
| MySQLやPostgreSQLなどの業務DBから、DWHや複数の下流へ秒〜分単位で流したい | Debezium(Kafka Connect上、またはDebezium Server) | Kafka Connectかメッセージング基盤の運用が増える。binlog・WALの保持設計を誤ると取りこぼす |
| 送り手も受け手もPostgreSQL | PostgreSQLの論理レプリケーション | DDL(テーブル定義の変更)とシーケンスの値は複製されない。使っていないスロットがWALを溜め続ける |
| Snowflakeに入ったテーブルの差分だけを、下流のテーブルへ反映したい | Snowflake Streams+Tasks、またはDynamic Tables | 外部DBの変更は拾えない。ストリームを読む処理が止まると、変更履歴が期限切れになる |
| 数十分〜1時間の遅れは許され、運用の手間を最小にしたい | マネージドの取り込みサービス(Fivetran、AWS DMSなど) | 件数や行数に応じて費用が増える。コネクタの挙動を細かく変えにくい |
| 1日1回で足り、物理削除がほとんどない | 更新日時での差分取得(クエリ方式)のまま | 物理削除した行を検知できない。更新日時を更新しない処理があると漏れる |
最後の行は見落とされがちです。WHERE updated_at > 前回時刻 で差分を取る方式は、DBから消えた行を見つけられません。消えた行は、もう問い合わせても返ってこないからです。削除を下流に伝えたいなら、DBの変更ログを読む方式(ログベース)が要ります。
取り込みツール全体の比較はデータ取り込みツールの選び方に、バッチとストリーミングのどちらにするかはバッチとストリーミングの選び方にまとめています。
業務DBから流すならDebezium:先にDB側の設定を確かめる
DebeziumはDBの変更ログを読み、行ごとの変更をイベントとして流すOSSです。2026年10月時点の安定版は3.7で、安定版のコネクタはMySQL・MariaDB・PostgreSQL・SQL Server・Oracle・Db2・MongoDB・Cassandraに対応しています。
MySQLとAmazon RDSでは、binlogの保持時間を最初に確かめる
MySQLから読む場合、公式ドキュメントは binlog_format = ROW と binlog_row_image = FULL を求めています。接続ユーザーには SELECT・RELOAD・SHOW DATABASES・REPLICATION SLAVE・REPLICATION CLIENT の権限が要ります。
DBAとの打ち合わせで真っ先に確認したいのは、binlog(変更ログ)が何時間残るかです。MySQL本体の既定は30日(binlog_expire_logs_seconds = 2592000)です。しかしAmazon RDS for MySQLでは事情が違います。binlog retention hours の既定値は NULL で、これは「保持しない(0時間)」を意味します。設定できる上限は168時間(7日)です。
-- RDS for MySQL:binlogを72時間残す(上限は168時間)
CALL mysql.rds_set_configuration('binlog retention hours', 72);
-- 現在の設定を確かめる
CALL mysql.rds_show_configuration;
連休中にDebeziumが止まり、その間にbinlogが消えれば、続きから読む手段はありません。最初から全件を取り直すことになります。保持時間は「Debeziumが止まっても、気づいて直すまでに要る時間」から逆算して決めます。
初回の全件取り込みと、届くイベントの形
コネクタは最初に既存データを読み込みます。PostgreSQLコネクタの snapshot.mode の既定は initial で、オフセット(どこまで読んだかの記録)がまだ無いときに全件を読みます。大きなテーブルは、シグナル用テーブルを使う増分スナップショットで分割して読めます。既定では1回あたり1,024行ずつです。
下流に届くイベントには before(変更前)、after(変更後)、op(操作の種類)が入ります。
| op | 意味 | 下流での扱い |
|---|---|---|
| c | 挿入 | そのまま追加 |
| u | 更新 | 主キーで突き合わせて上書き |
| d | 削除 | 物理削除するか、削除フラグを立てるかを先に決める |
| r | スナップショットでの読み込み | 挿入と同じ扱いにすることが多い |
| t | TRUNCATE(PostgreSQL) | 下流のテーブルも空にするか決める |
d の扱いを決めずに作ると、消したはずの受注が集計に残り続けます。
Kafkaが無くても使える
Debeziumを使うにはKafkaが必要だと思われがちです。しかしDebezium Serverという単体アプリを使えば、Amazon Kinesis、Google Cloud Pub/Sub、Apache Pulsar、Azure Event Hubs、Redis、HTTPなどへ直接送れます。社内にKafkaの運用者がいないなら、まず既に使っているクラウドのメッセージングへ送る構成を検討します。
送り手がPostgreSQLなら、レプリケーションスロットを見張る
PostgreSQLで変更を外へ取り出すには wal_level = logical が要ります。DebeziumもPostgreSQL同士の論理レプリケーションも、PostgreSQL 10以降に標準で入っている pgoutput で変更を読めます。
変更を読む側には、レプリケーションスロット(読み終わるまでWALを消さずに残す目印)が1つずつ作られます。読み手が止まっても取りこぼさずに済むのは、このスロットのおかげです。その代わり、読み手が止まったままだとWALは消えずに溜まり続け、送り手のディスクを圧迫します。
対策は3つあります。1つ目は、PostgreSQL 13以降の max_slot_wal_keep_size でスロットが抱えるWALの上限を決めることです。既定は -1(無制限)です。上限を超えると読み手は続きから読めなくなります。それでも、本番DBのディスクが埋まるよりは被害が小さく済みます。2つ目は、PostgreSQL 18で入った idle_replication_slot_timeout です。一定時間使われていないスロットを無効にでき、既定は 0(無効)です。3つ目は、スロットが抱えているWALの量を定期的に見ることです。
-- スロットごとに、溜まっているWALの量を確かめる
SELECT slot_name,
active,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots;
Debeziumのドキュメントには、もう1つ落とし穴が書かれています。同じPostgreSQLインスタンスに、更新の多いDBと更新の少ないDBが同居している場合です。WALはインスタンス全体で共有されるので、取り込み対象のDBに変更が来ない間もWALは溜まります。heartbeat.interval.ms と heartbeat.action.query で定期的に小さな書き込みを起こし、読んだ位置を進めさせます。
削除イベントの中身にも注意が要ります。テーブルの REPLICA IDENTITY が既定(DEFAULT)のままだと、削除や更新の before に入るのは主キーの値だけです。変更前の全列を下流で使いたいテーブルは FULL にします。主キーの無いテーブルを既定のまま使うと、削除イベントに before 自体が入りません。
PostgreSQL同士の複製では、DDLとシーケンスの値が複製されない点も押さえておきます。列を追加するときは、受け手のテーブルを先に変えておくとエラーを避けやすくなります。設定の手順とPublication・Subscriptionの書き方はPostgreSQL論理レプリケーションで詳しく扱っています。
Snowflakeの中の差分処理は、Streamsの「消費」の仕組みを理解してから
Snowflake Streamsは、テーブルの変更を記録する目印です。ストリームを読むだけでは目印(オフセット)は進まず、INSERTやMERGEなどのDMLの中で使ったときに初めて進みます。この性質を知らないと、テストで SELECT しただけの差分が本番の処理で消えずに残り、二重に反映される原因になります。
| 種類 | 追跡する変更 | 使える主な対象 |
|---|---|---|
| Standard | 挿入・更新・削除・TRUNCATE | 通常のテーブル、Dynamic Tables、ビューなど |
| Append-only | 挿入だけ | 通常のテーブル、Dynamic Tables、ビューなど |
| Insert-only | 挿入だけ | 外部テーブル、外部管理のIcebergテーブルなど |
更新は METADATA$ACTION が DELETE と INSERT の2行の組で届き、METADATA$ISUPDATE が TRUE になります。MERGEで反映するときは、この組を1行として扱います。
CREATE TASK task_sync_orders
WAREHOUSE = WH_TRANSFORM
SCHEDULE = '5 MINUTE'
WHEN SYSTEM$STREAM_HAS_DATA('stream_orders')
AS
MERGE INTO gold.fct_orders t
USING (
SELECT * FROM stream_orders
WHERE NOT (METADATA$ACTION = 'DELETE' AND METADATA$ISUPDATE) -- 更新の「旧行」は除く
) s
ON t.order_id = s.order_id
WHEN MATCHED AND s.METADATA$ACTION = 'DELETE' THEN DELETE
WHEN MATCHED THEN UPDATE SET t.amount = s.amount, t.status = s.status
WHEN NOT MATCHED AND s.METADATA$ACTION = 'INSERT' THEN
INSERT (order_id, amount, status) VALUES (s.order_id, s.amount, s.status);
ALTER TASK task_sync_orders RESUME; -- タスクは作成時に停止状態
運用で詰まるのは次の3点です。1つ目は、1つのストリームを2つの処理で読むと、先に読んだ側だけが差分を受け取ることです。読み手ごとにストリームを作ります。ストリームが持つのはオフセットだけなので、複数作ってもストレージはほとんど増えません。2つ目は期限切れです。読まれないストリームのために、Snowflakeは元テーブルの保持期間を既定で最大14日まで延ばします。それを過ぎるとストリームは古くなり、作り直しになります。3つ目は費用です。自分で指定したウェアハウスで動くタスクは、起動のたびに最低60秒分の課金があります。
同じことを宣言的に書けるのがDynamic Tablesです。SELECT文と目標の遅れ(TARGET_LAG、最小60秒)を書けば、Snowflakeが更新を引き受けます。
| やりたいこと | 向く道具 |
|---|---|
| SELECTで書ける変換を、手順を書かずに最新に保ちたい | Dynamic Tables |
| 削除の反映や外部への通知など、更新の手順を自分で書きたい | Streams+Tasks |
| dbtプロジェクトでテストやドキュメントと一緒に管理したい | dbtのincrementalモデル |
事故になりやすい3つの場面と、その費用
| 症状 | 原因 | 事前の手当て |
|---|---|---|
| 再起動したら続きから読めない | binlog・WALが先に消えた | 保持時間を復旧に要る時間から逆算する。RDSは既定で保持しない |
| 消した行が集計に残る | 削除イベントを下流で無視している | d と METADATA$ACTION = 'DELETE' の扱いを設計書に書く |
| 列を足したらパイプラインが止まる | スキーマの変更が下流に届かない | 列の追加は受け手を先に変える。互換性のルールを決めてから変える |
1つ目の手戻りがどれくらい重いか、例で試算します。年収700万円のエンジニアの会社負担は、給与の約3倍で年2,100万円です。年1,900時間で割ると、時給は約11,000円になります。binlogが消えて全件を取り直し、下流の数字と突き合わせるのに2人で3日(48時間)かかれば、人件費だけで約53万円です。その間、ダッシュボードも止まります。金額はいずれも説明のための例です。
自前で運用するか、マネージドに任せるか
| 選択肢 | 向く状況 | 確かめること |
|---|---|---|
| Debezium+Kafka Connectを自社運用 | Kafkaの運用者がいて、同じ変更を複数の下流へ配りたい | 障害時に誰が起きるか。binlog・WALの保持とスロットの監視 |
| Confluent CloudのDebeziumベースのコネクタ | Kafkaは使いたいが、コネクタの運用は持ちたくない | PostgreSQL CDCはV2コネクタに移行済みか(V1は提供終了) |
| AWS DMS | AWS上のDBから、全件移行と継続的な差分の取り込みをまとめて済ませたい | 取り込み元のDB種別と版が対応しているか |
| Fivetranなどの取り込みサービス | 1本のDBからDWHへ、運用の手間をかけずに入れたい | 件数・行数に応じた費用。削除の扱いの既定 |
自前で運用するかどうかは、ツールの機能より「夜中に止まったとき誰が直すか」で決まります。答えられないうちは、マネージドから始めるほうが安全です。
よくある質問(FAQ)
CDCを入れると、業務DBは重くなりますか?
ログを読む方式は、テーブルへ定期的に問い合わせを投げません。ただし負荷がゼロになるわけではありません。binlogやWALを長く残す分だけディスクを使い、初回の全件取り込みでは大きな読み込みが走ります。全件取り込みは業務時間外に回すか、増分スナップショットで分割して読みます。
更新日時で差分を取る方式では、何が足りないのですか?
物理削除を検知できない点です。消えた行は問い合わせても返ってこないので、下流には残り続けます。更新日時を更新しない一括処理があると、その変更も漏れます。削除がほとんど無く、1日1回で足りるテーブルなら、この方式のままでも構いません。
Debeziumを使うには、Kafkaを立てる必要がありますか?
必須ではありません。Debezium Serverを使えば、Kinesis、Pub/Sub、Pulsar、Event Hubs、Redis、HTTPなどへ直接送れます。
下流で同じ変更が二重に届くことはありますか?
あります。障害からの再開時には、同じイベントがもう一度届くことを前提に作ります。下流の反映は主キーでのMERGEにして、同じイベントを2回適用しても結果が変わらないようにしておきます。
SnowflakeのStreamsとDynamic Tablesは、どちらを使えばよいですか?
SELECT文で書ける変換ならDynamic Tablesが手軽です。目標の遅れを書くだけで、更新の手順はSnowflakeが受け持ちます。削除の反映方法を細かく決めたいときや、変更をきっかけに外部へ通知したいときは、Streams+Tasksで手順を書きます。1分より新しいデータが要る場合、Dynamic Tablesは向きません。
出典
- Debezium公式 Connectors一覧(2026-10-17 参照:安定版・インキュベーティングのコネクタ対応DB)
- Debezium公式 PostgreSQL Connector (3.7)(2026-10-17 参照:安定版3.7、snapshot.mode既定initial、増分スナップショット(シグナル、既定1024行)、op値c/u/d/r/t、pgoutput、WALディスク消費とheartbeat.interval.ms/heartbeat.action.query、REPLICA IDENTITYとbefore)
- Debezium公式 MySQL Connector(2026-10-17 参照:binlog_format=ROW、binlog_row_image=FULL、binlog_expire_logs_seconds既定2592000(30日)、必要権限)
- Debezium公式 Debezium Server(2026-10-17 参照:Kafka以外の送り先(Kinesis、Pub/Sub、Pulsar、Event Hubs、Redis、HTTP等))
- AWS RDSユーザーガイド mysql.rds_set_configuration(2026-10-17 参照:binlog retention hours既定NULL(0時間)、上限168時間、設定コマンド)
- PostgreSQL 18 ドキュメント 19.6 Replication(2026-10-17 参照:max_slot_wal_keep_size既定-1、idle_replication_slot_timeout既定0、max_replication_slots既定10)
- PostgreSQL 13 ドキュメント 19.6 Replication(2026-10-17 参照:max_slot_wal_keep_sizeがPG13に存在(PG12には無い、PG17にidle_replication_slot_timeoutが無いことも確認))
- PostgreSQL ドキュメント Logical Replication Restrictions(2026-10-17 参照:DDL・シーケンスが複製されない、受け手を先に変える)
- Snowflake公式 Introduction to Streams(2026-10-17 参照:ストリームの種類と対象、DMLでのみオフセットが進む、読み手ごとにストリーム、保持延長既定最大14日、METADATA$列と更新の表現)
- Snowflake公式 Introduction to tasks(2026-10-17 参照:タスクは作成時停止、WHEN SYSTEM$STREAM_HAS_DATA、ユーザー管理WHは起動ごと最低60秒課金、サーバーレスのTARGET_COMPLETION_INTERVAL)
- Snowflake公式 Dynamic tables(2026-10-17 参照:TARGET_LAG最小60秒、向く・向かない用途)
- Confluent Cloud PostgreSQL CDC Source V2 (Debezium)(2026-10-17 参照:Debeziumベースのマネージドコネクタ、V1がEnd of Life)
- AWS DMS PostgreSQLソース(2026-10-17 参照:Full load+CDCのタスク種別)
「本番DBに何かつなぐのは困る」と言われたまま止まっているCDCの設計も、binlogやWALの保持時間の決め方から一緒に整理できます。初回相談(30分・無料)で一緒に整理できます。