TiDB CloudのデータをTiDB Cloud Lakeへ移してみた

TiDB CloudのデータをTiDB Cloud Lakeへ移してみた

TiDB CloudのデータをTiDB Cloud Lakeへ移行することに挑戦してみました。Dumplingを使ったエクスポートから取り込み、データ検証までの全プロセスと、コンソール機能では取り込めなかった理由を詳しく紹介します。
2026.09.15

こんにちは、ゲームソリューション部のsoraです。
今回は、TiDB CloudのクラスタにあるデータをTiDB Cloud Lakeへ移してみたことについて書いていきます。

TiDB Cloudにダミーデータを入れる

TiDB Cloud Starterのクラスタを1つ作り、同じデータベースにappdb.ordersappdb.app_logsの2つのテーブルを置きました。
app_logsの500,000行だけをLakeへ移して、ordersの50,000行はTiDBに残します。

app_logsの定義は以下です。

CREATE TABLE appdb.app_logs (
    log_id     BIGINT PRIMARY KEY,
    level      VARCHAR(16) NOT NULL,
    service    VARCHAR(64) NOT NULL,
    message    TEXT,
    attributes JSON,
    logged_at  DATETIME NOT NULL,
    KEY idx_logged_at (logged_at)
);

データはLOAD DATA LOCAL INFILEで入れました。
app_logsの500,000行で約54秒、投入後のRow-based Storageはorders込みのクラスタ全体で199.24 MiBです。

TiDB CloudからS3へエクスポートする

TiDB Cloud Lakeへの取り込みはS3を経由します。
TiDB CloudからS3へファイルを出して、それをLake側に読ませる、という流れです。

エクスポートにはDumplingを使いました。
TiDB Cloudのコンソールにもエクスポート機能があるのですが、そちらで出したファイルは取り込めませんでした。
その話は後半に書きます。

Dumplingそのものの使い方は、こちらの記事にて紹介されています。
https://dev.classmethod.jp/articles/export-tidb-data-with-dumpling/

実行したコマンドがこちらです。

tiup dumpling \
  -h gateway01.ap-northeast-1.prod.aws.tidbcloud.com -P 4000 \
  -u '<prefix>.root' -p '<パスワード>' \
  --ca /etc/ssl/cert.pem \
  --filter 'appdb.app_logs' \
  --filetype csv \
  --escape-backslash=false \
  --csv-output-dialect snowflake \
  --csv-line-terminator $'\n' \
  -c no-compression \
  -o 's3://<バケット名>/tidb-cloud-lake/dumpling/' \
  --s3.region ap-northeast-1

--filterでテーブルを絞れるので、app_logsだけが出てordersは出ません。
-oにS3のURIをそのまま書けるため、ローカルに落とす必要もありませんでした。

そして、後ろの4つがLakeに取り込ませるための条件です。

フラグ 無いとどうなるか
--filetype csv デフォルトはsql。Parquetで出したファイルは取り込まれなかった
--escape-backslash=false デフォルトのtrue\"になり、列数が合わずFailedする
--csv-line-terminator $'\n' デフォルトの\r\nで出て、取り込み側の\nと合わない
-c no-compression デフォルトは無圧縮だが、圧縮するとファイル自体を認識しない

--csv-output-dialect snowflakeも付けていますが、これはクォートの話ではありませんでした。
公式の説明では、バイナリ型を16進表記にする(0xプレフィックスを除去する)オプションです。

https://docs.pingcap.com/tidb/stable/dumpling-overview/

今回のテーブルにバイナリ列は無いので、実質効果があったのは--escape-backslash=falseだけです。

500,000行・140.1MBのダンプに1分57秒かかり、4ファイルが出ました。

$ aws s3 ls s3://<バケット名>/tidb-cloud-lake/dumpling/ --recursive --human-readable
  133 Bytes tidb-cloud-lake/dumpling/appdb-schema-create.sql
  442 Bytes tidb-cloud-lake/dumpling/appdb.app_logs-schema.sql
  133.6 MiB tidb-cloud-lake/dumpling/appdb.app_logs.000000000.csv
  146 Bytes tidb-cloud-lake/dumpling/metadata

データ本体のほかにCREATE DATABASE文とCREATE TABLE文が出ています。
これがあとでLake側のテーブル自動作成に使われます。

CSVの中身はこうなりました。

"log_id","level","service","message","attributes","logged_at"
1,"DEBUG","search-api","span exported (req=204b5f50)","{""az"": ""ap-northeast-1a"", ""customer_id"": 3890, ""duration_ms"": 46.5, ""http"": {""method"": ""GET"", ""path"": ""/v1/inventory"", ""status"": 200}, ""trace_id"": ""11398b037a11a01e""}","2026-07-25 10:34:50"

--escape-backslash=falseにすると"""の2個重ねになります。
この挙動は公式ドキュメントに明記が見つけられなかったのですが、実測で確認できました。

ちなみに、TiDB Cloud Starterに対してDumplingを流すと、information_schema.cluster_infomysql.tidbinformation_schema.placement_policiesへの権限エラーが3つ出ます。
Starterがシステムテーブルへのアクセスを制限しているためで、3つとも警告止まりでダンプ自体は成功します。

https://docs.pingcap.com/tidbcloud/limited-sql-features/

TiDB Cloud Lakeにデータソースを登録する

ここからはTiDB Cloud Lake側のコンソールでの作業です。
Data > Data Sources > CreateのServiceにTiDBという選択肢があるので、これを選びます。

01-sr-datasource-tidb-top

開いてみると、入力欄にホスト名もユーザー名もパスワードもありません。
並んでいたのはS3バケットとRole ARNでした。

Storage Providerの下には、この一文があります。

Where TiCDC / Dumpling stages the data that Databend loads.

つまりTiDBデータソースは、TiDBクラスタへ接続するためのものではなく、TiCDCやDumplingがS3に吐いたファイルの置き場を指すものでした。
LakeはTiDBを見ません。
TiDBが出力したファイルが配置されているS3を見ます。

Authentication MethodのデフォルトはRole ARNで、アクセスキーを持たせずにIAMロールで繋げました。

なお、公式ドキュメントのData Source TypesTiDBのページはありません。

https://docs.pingcap.com/tidbcloudlake/data-sources/

掲載されているのはAmazon S3・Amazon SQS(S3)・MySQL・PostgreSQL・FeiShuBot・Kafkaの6種類だけで、TiDBはコンソールにしか存在しない選択肢でした。

02-sr-datasource-service-list

TiDB Cloud LakeのIntegration Taskで取り込む

TiDB Cloud LakeのコンソールのData > Data Integrationsでタスクを作ります。

03-sr-task-filled-top

Table Rulesappdb.app_logsとだけ書けば、そのテーブルだけが対象になります。

04-sr-task-filled-bottom

Sync Modeは3択でした。

05-sr-sync-mode-options

Snapshot / CDC / Snapshot + CDCで、Snapshotに置かれるのがDumplingの出力、CDCに置かれるのがTiCDCの変更ログ、という対応です。
今回はSnapshotモードで進めます。

Preview Matched Tablesを押すと、作る前にマッチ結果を確認できます。

06-sr-preview-matched-tables

appdb.app_logsだけがマッチし、ordersは含まれていません。
このボタンはS3を実際にスキャンして-schema.sqlを探しているので、タスクを作らずWarehouseも起動させずにプレフィックスと出力形式の当たりを確認できます。

詳細画面のSync Configurationには、作成フォームに無かった項目が3つ出ていました。

07-sr-sync-configuration

Allow Delete: Yes / Poll Interval: 10 s / Merge Interval: 3 sの3つで、デフォルトで10秒ごとにS3を見に行くようになっています。

作成しただけでは動かないので、Startを押します。

08-sr-dumpling-run-success

Rows Syncedが500,000になりました。
Auto Create TableがデフォルトでYesなので、Lake側にテーブルも自動で作られています。

09-sr-lake-table-loaded

500,000行で18.5MB、EngineFUSEです。

TiDB Cloud Lakeで取り込んだデータを確認する

TiDB Cloud LakeのWorksheetで検証クエリを流して、TiDB Cloud側で取っておいた期待値と突き合わせます。

10-sr-verify-count-checksum

SELECT COUNT(*) AS cnt, SUM(log_id) AS sum_id FROM appdb.app_logs;

COUNT(*)が500,000、SUM(log_id)が125,000,250,000で、どちらもTiDB Cloud側と一致しました。
level別の内訳もHTTPステータス別の集計も同じ値です。
チェックサムが合っているので、欠けも重複もなく入っています。

jsonVARIANTになる

TiDB Cloud Lake側に作られたテーブル定義です。

11-sr-verify-create-table

CREATE TABLE app_logs (
  log_id     BIGINT   NULL,
  level      VARCHAR  NOT NULL,
  service    VARCHAR  NOT NULL,
  message    VARCHAR  NULL,
  attributes VARIANT  NOT NULL,
  logged_at  TIMESTAMP NOT NULL
) ENGINE=FUSE
TiDB Cloud側 Lake側
level varchar(16) VARCHAR 長さ指定が落ちる
message text VARCHAR TEXTとVARCHARの区別が無い
attributes json VARIANT 狙いどおり
logged_at datetime TIMESTAMP

jsonVARIANTになったので、ネストしたJSONをそのまま辿れます。

12-sr-verify-variant

SELECT attributes['http']['status'] AS status, COUNT(*) AS cnt
FROM appdb.app_logs GROUP BY status ORDER BY status;

200が356,519、429が71,939、500が71,542で、これもTiDB Cloud側と一致しました。

ただし書き方は変わります。
TiDB Cloud側がattributes->>'$.http.status'なのに対して、Lake側はattributes['http']['status']です。
同じ結果が出ますが、JSON列を触っているSQLはそのままでは移植できません。

主キーとセカンダリインデックスが消えるのは分析用ストアとして妥当だと思いますが、log_idNOT NULLだったのにNULLになり、attributesは逆にNULL許容だったのにNOT NULLになっています。
-schema.sqlのDDLをそのまま反映しているわけではなく実データから推定しているのではないかと思われますが、そこは確認できていません。

ストレージは約1/11になった

サイズ
TiDB Cloud側のRow-based Storage 199.24 MiB
Dumplingが出したCSV 133.6 MiB
Lake側のテーブル(FUSE) 18.5MB

TiDB Cloud側の199.24 MiBはordersを含むクラスタ全体の値ですが、ほとんどがapp_logsの分です。
Lake側には主キーもセカンダリインデックスも引き継がれないので、その分が大きそうです。

試行錯誤: TiDB Cloudのエクスポート機能では取り込めなかった

ここからは本筋から外れますが、Dumplingを持ち出すまでの記録です。
TiDB Cloudのコンソールにもエクスポート機能があり、最初はそちらで出したファイルを取り込もうとしていました。

13-sr-export-settings

画面としてはこちらの方が楽です。
Exported Dataのツリーからテーブル単位でチェックできるので、app_logsだけを選んでordersを外せます。
認証もRole ARNで、CloudFormationのリンクからロールを作れます。

形式はSQL / CSV / Parquet、圧縮はZstd / Gzip / Snappy / Noneから選べます。
ParquetとCSVをそれぞれ圧縮あり・なしで試しましたが、どれも取り込めませんでした。

Parquetでは取り込まれなかった

まずParquetで出してタスクを実行しました。

14-sr-run-success

StatusSuccessLast Messageは空です。
ところがRows Syncedが0、Data Syncedが0 B、Chunksが0/0でした。
S3には26.4 MBのParquetが置いてあるのに、1行も取り込まれていません。

エラーではなく、「対象ファイルが0件だった」という結果として成功扱いになっています。

ここで大事なのは、取り込まれたかどうかはStatusではなくRows Syncedを見ないと分からないということです。
定期実行させてSuccessだけを監視していると、実は何も入っていないことに気づけません。

切り分けのためTiDB Cloud Lake側のData > Databasesを見ると、テーブルだけは作られていました。

15-sr-lake-table-created-empty

Rowsが0、Bytesが0Bです。
-schema.sqlは読めていて、データファイルだけが対象から外れています。

圧縮を外しても結果は同じでした。
ファイル名に.zstが入るのが原因ではなさそうです。
チャンクとして認識されたのはCSVのときだけだったので、SnapshotタスクがParquetを対象にしていないように見えます。
実際、作成フォームにはファイル形式を選ぶ欄が無く、CSV Separator / Skip Header Rows / Export Escaped BackslashesとCSV前提の設定ばかりが並んでいます。

なお、同じLakeでもAmazon S3 Integration TaskはCSV / Parquet / NDJSONに対応しています。
https://docs.pingcap.com/tidbcloudlake/integrate-with-amazon-s3/

TiDBタスクで通ったのはCSVだけでした。

圧縮するとスキーマファイルまで.zstになる

CSVで出し直します。
CompressionをZstdのままにしたところ、今度はPreview Matched Tablesの段階で弾かれました。

These rules matched no table under the S3 prefix.
No database was found under the prefix at all. Check the S3 prefix.

S3に出たファイルを見ると理由が分かります。

appdb-schema-create.sql.zst              131 B
appdb.app_logs-schema.sql.zst            303 B
appdb.app_logs.0000000010000.csv.zst    22.5 MB

スキーマSQLまで.zstで圧縮されています。

Parquetのときは圧縮がファイル内部のコーデックだったので.sqlは素のまま残っていました。
CSVにすると圧縮が外側のラッパーになり、.sqlも巻き込まれます。
データベースの定義そのものが見つからないので、「DBが1つも無い」というメッセージになるわけです。

非圧縮CSVはエスケープと改行がズレている

Compression: Noneで出し直すとPreviewは通り、実行するとこうなりました。

16-sr-run-failed

Chunksが0/0から0/1になっています。
1チャンクを認識して、読みにいって、失敗しました。

Last Message1 table(s) failed during full sync: ap...で切れていて全文が読めません。
ここで役に立ったのが、TiDB Cloud Lake側のMonitoring > SQL Historyでした。

17-sr-sql-history-error

BadBytes. Code: 1046, Text = Number of columns in file (12) does not match that of the corresponding table (6)
at file 'tidb-cloud-lake/appdb.app_logs.0000000010000.csv', line 1.

Integration Taskの失敗はRun Historyでは途中で切れますが、実体はWarehouse上で走るSQLなのでSQL Historyに全部残ります。

さて、app_logsは6列です。
ファイルが12列に見えているので、差は6です。

実際のCSVを見てみます。

1,"DEBUG","search-api","span exported (req=204b5f50)","{\"az\": \"ap-northeast-1a\", \"customer_id\": 3890, \"duration_ms\": 46.5, \"http\": {\"method\": \"GET\", \"path\": \"/v1/inventory\", \"status\": 200}, \"trace_id\": \"11398b037a11a01e\"}","2026-07-25 10:34:50"

attributesのダブルクォートがバックスラッシュでエスケープされています。
Dumplingのデフォルト--escape-backslash=true(MySQL方言)そのものです。

そして差の6は、attributesのJSON内部にあるカンマの数と一致します。
\"のエスケープが解釈されないため値が1つのフィールドとして閉じず、JSON内部のカンマが列区切りとして扱われていました。

タスクが投げているSQLもSQL Historyに出ていました。

COPY INTO `appdb`.`app_logs` (`log_id`, `level`, `service`, `message`, `attributes`, `logged_at`)
FROM 's3://<バケット名>/tidb-cloud-lake/appdb.app_logs.0000000010000.csv'
CONNECTION=(external_id='la***xl', role_arn='ar***le')
FILE_FORMAT=(field_delimiter=',', null_display='\\N', record_delimiter='\n', skip_header=1, type=CSV)
PURGE=false FORCE=false DISABLE_VARIANT_CHECK=false ON_ERROR=abort RETURN_FAILED_ONLY=false

FILE_FORMATescapequoteもないので、Databendのデフォルト(RFC 4180の"")で読まれます。
さらにrecord_delimiter='\n'ですが、エクスポートはDumplingのデフォルトどおり\r\nで出ます。
エスケープ方式と改行コードの2つがズレていました。

出し直すためのオプションがコンソールに無い

タスクのExport Escaped BackslashesYesにすれば読めるだろうと思ったのですが、赤字が出ました。

18-sr-escape-backslash-warning

Re-export with --escape-backslash=false --csv-output-dialect=snowflake, then select No.
Existing export settings must not be changed without re-exporting.

「エスケープ済みも読める」という設定ではなく、「そのファイルは読めないので出し直してからNoを選べ」という指示でした。
しかもYesにするとUpdateボタンが押せなくなるので、そもそも保存できません。

では出し直せばいいのかというと、TiDB Cloudのエクスポート機能にそのオプションはありません。
コンソールのEdit CSV Configurationで変えられるのはSeparator・Delimiter・Null value・Skip headerの4つだけで、エスケープ方式も改行コードも触れません。

CLIも同じでした。

https://docs.pingcap.com/tidbcloud/ticloud-serverless-export-create/

指定できるのは--csv.delimiter--csv.separator--csv.null-value--csv.skip-header--parquet.compressionの5つだけで、--escape-backslash--csv-output-dialectもありません。

ここまでで、前半のDumplingに--escape-backslash=false--csv-line-terminator $'\n'を付けた理由がつながります。
画面に書かれているとおりに出し直すには、Dumplingを自分で回すしかありませんでした。

ちなみにTiDB Cloudのエクスポートと自前のDumplingで-schema.sqldiffすると差分なしで、バイト単位で一致します。
エクスポート機能の実体もDumplingで、違うのはフラグだけでした。

変換すれば通る

Dumplingを持ち出さずに済ませたい場合は、出したCSVを直す手もあります。
直すのは2か所だけです。

$ LC_ALL=C sed 's/\\"/""/g' original.csv | tr -d '\r' > converted.csv

\"""に、\r\n\nにしているだけです。
134MBの変換に4.9秒でした。

これをS3へ戻して同じタスクを実行すると、通りました。

1回前の実行はRows Synced 0だったので、設定を一切変えずファイルの中身だけを変えて成否が分かれています。

ただし、値そのものにバックスラッシュを含むデータでは、この単純置換は壊れます。
今回は事前にgrepで確認して、データにバックスラッシュもNULL表現も含まれていないことを確かめたうえでやっています。

最後に

今回は、TiDB CloudのクラスタのデータをS3経由でTiDB Cloud Lakeへ移し、取り込んだデータが一致するところまで確認してみました。
TiDBデータソースは自前でDumplingを回す前提で作られていて、TiDB Cloudのコンソールのエクスポート機能だけでは完結しないというのが正直なところです。
この記事がどなたかの参考になれば幸いです。


TiDB Cloudの導入・サポートはクラスメソッドにお任せください

クラスメソッドでは、TiDB Cloudの導入から運用支援まで、豊富なノウハウでお客様をサポートしています。パフォーマンスの最適化やスケーラビリティに課題を抱えている方は、ぜひご相談ください。
詳細な導入事例やサービス内容について知りたい方は、こちらからご確認いただけます。

TiDB Cloudのサポート詳細を見る

この記事をシェアする

関連記事