
R
イベント
マガジン
技術ブログ
MySQLと高い互換性を持つデータベースのTiDBでは、DDLが高速かつオンラインで実施されとても有用です。メルカリの運用における気付きとして得られた、主に実行の速度制御とmodify columnの完了時間見積もりの学びについてお伝えします。 背景 メルカリではMySQLと高い互換性を持つTiDBを利用しているため、DDLはオンラインで実行でき、現状のところ大きな問題なく動作しています。 先日、数十億レコード程度のテーブルのALTERを実施した際、実行の完了時刻が予測できない、と感じた事象がありました。この記事ではこの問題について調査して得られた学びを共有したいと思います。DDLにはさまざまなバリエーションがあり、本記事が適用できないケースがあることにご留意ください。 なお、本記事中についてはTiDB CloudのDedicated(Serverlessでない方)を前提にしております。TiDB Cloudではなく、TiDBのSoftware版でもおそらく同じことがいえると思います。 実行中のTiDB障害 本記事では、DDLの実行完了の予測について取り扱いたいのですが、その前にDDLの進捗情報の永続化について整理をするために、まずはTiDBのDDLの障害耐性について整理したいと思います。 長時間のDDL実行における関心事項として、DDL中に障害が発生するとどうなるか、という懸念があげられます。 結論から言えば、障害が発生してもDDLは継続しますが、これにはDDLをどこまで実行したかの状態を永続化して保持し、かつ障害発生時に他のノードで実行を再開する仕組みが必要です。 TiDBでのDDLおよびDDL owner TiDBでのDDL実行においてはDDL ownerという概念があり、あるTiDBがDDL Ownerの役割を担い、そのTiDBが主体となり実際にデータを保存するTiKVに対するDDLの実処理を実行します https://docs.pingcap.com/best-practices/ddl-introduction/#tidb-ddl-module https://pingcap.github.io/tidb-dev-guide/understand-tidb/ddl.html#execution-in-the-tidb-ddl-owner DDL実行中にDDL ownerであるTiDBに障害が発生した場合どうなるでしょうか? DDL ownerは、etcdベースで選出がおこなわれ、通常はDDL ownerが定期的にKeepaliveのような死活情報をetcdに報告することにより、etcdリース期間が延長されています。したがって、DDL owner障害時には、このKeepaliveが途絶えることによりetcdリースが失効し、新ownerが選出されます。 したがって、TiDB障害時にもDDLの処理は継続します。 メルカリでも、長時間のDDL中に(意図せず)TiDBのスケールダウンを行ってしまう事象が発生しましたが、その際にも実際に処理が正常に継続することを確認できました。 ただし、TiDBで実行中のプロセス一覧( information_schema.cluster_processlist )からは、ALTERのプロセスは確認できなくなる一方で、ADMIN SHOW DDL JOBSを確認すると処理が継続している、という状況でした。 DDLの実行状況の永続化 次に、DDLの状態の永続化です。DDLに関連するテーブルとしては https://docs.pingcap.com/tidb/stable/mysql-schema/#system-tables-related-to-ddl-statements があり、このうちバックフィルの情報はmysql.tidb_ddl_reorgに以下のように保持されます。 mysql> DESC mysql.tidb_ddl_reorg; +-------------+----------+------+------+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-------------+----------+------+------+---------+-------+ | job_id | bigint | NO | MUL | NULL | | | ele_id | bigint | YES | | NULL | | | ele_type | blob | YES | | NULL | | | start_key | blob | YES | | NULL | | | end_key | blob | YES | | NULL | | | physical_id | bigint | YES | | NULL | | | reorg_meta | longblob | YES | | NULL | | +-------------+----------+------+------+---------+-------+ 7 rows in set (0.00 sec) これは次に処理すべきキーの値をstart_keyとして永続化する、という方法によっているようです。これにより処理が中断された場合、この情報を元に再開、継続することが可能となります。 また、実行中の設定や進捗の確認に関しては、 mysql.tidb_ddl_job に永続化されます。 こちらの詳細は記事の後半で再度内容を紹介します。 なお、こちらはDDLの実行が完了すると、mysql.tidb_ddl_historyに移動するため注意が必要です。 mysql> desc mysql.tidb_ddl_job; +------------+------------+------+------+---------+-------+ | Field | Type | Null | Key | Default | Extra | +------------+------------+------+------+---------+-------+ | job_id | bigint | NO | PRI | NULL | | | reorg | int | YES | | NULL | | | schema_ids | mediumtext | YES | | NULL | | | table_ids | mediumtext | YES | | NULL | | | job_meta | longblob | YES | | NULL | | | type | int | YES | | NULL | | | processing | int | YES | | NULL | | +------------+------------+------+------+---------+-------+ 7 rows in set (0.00 sec) mysql> desc mysql.tidb_ddl_history; +-------------+------------+------+------+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-------------+------------+------+------+---------+-------+ | job_id | bigint | NO | PRI | NULL | | | job_meta | longblob | YES | | NULL | | | db_name | char(64) | YES | | NULL | | | table_name | char(64) | YES | | NULL | | | schema_ids | mediumtext | YES | | NULL | | | table_ids | mediumtext | YES | | NULL | | | create_time | datetime | YES | | NULL | | +-------------+------------+------+------+---------+-------+ 7 rows in set (0.00 sec) DDL実行中の速度制御 大容量テーブルに対する物理的なDDL(Reorg DDL)は、TiKVのリソース(CPUやI/O)を消費しながらデータのバックフィルを行うため、実行に時間がかかったり、リソースを過剰に消費するので、これらのコントロールが必要になることがあります。 すでに実行中のDDLジョブの速度を動的にコントロールするにはADMIN ALTER DDL JOBSを使用します。 ADMIN ALTER DDL JOBS 現在の設定値や、どのような操作をするかにより設定可能な項目などは異なりますが、MODIFY COLUMNに区分される実態の再構築(Full Reorg)が必要なDDLでは、 THREAD: DDLジョブのワーカー数 BATCH_SIZE: ワーカーが1回のバッチで処理する行数 の変更が有効です。 ADMIN ALTER DDL JOBS <job_id> THREAD = 16, BATCH_SIZE = 1024; これらのパラメータはTiKVのリソースの消費量と、DDLの実行速度に影響し、負荷を抑えたい場合は、一時的なDDLの中断/再開も可能ですし、スレッド数を減らして実行することもできます。逆にTiKVのキャパシティーに余裕があり、高速に完了させたい場合にスレッド数やバッチサイズを増やしたりすることが有効です。 また、これらの初期値として、 tidb_ddl_reorg_worker_cnt tidb_ddl_reorg_batch_size を変更することが可能です。 通常、ADMIN ALTER DDL JOBS を実行するとその設定変更は即時で反映されることが期待されます。1点注意が必要なのは、特定のバージョン(v8.5.2, v8.5.3 など)において、特定のDDL(MODIFY COLUMN のcharからvarcharへの変更など)に対してADMIN ALTER DDL JOBSを実行しても、ワーカー数などの設定が実質的に反映されない不具合が報告されています。 関連Issue: #63201 ADMIN ALTER DDL JOBS can’t adjust the modify column job concurrency 関連PR: #63605 ddl: fix dynamic parameter adjustment failure in txn and local ingest mode こちらに関しては、設定変更後に、PAUSE/RESUMEをすれば設定が反映されることを確認しています。 PAUSE: https://docs.pingcap.com/tidb/stable/sql-statement-admin-pause-ddl/ RESUME: https://docs.pingcap.com/tidb/stable/sql-statement-admin-resume-ddl/ DDLを実行の際はご利用のバージョンを確認し、必要に応じてPAUSE/RESUMEを試してみてください。 また、現状設定している値の確認に関しては先ほどの、 mysql.tidb_ddl_job から確認できます。 mysql> SELECT job_id, JSON_EXTRACT(CONVERT(UNHEX(TRIM(LEADING '0x' FROM HEX(job_meta))) USING utf8mb4), '$.reorg_meta.concurrency') AS concurrency, JSON_EXTRACT(CONVERT(UNHEX(TRIM(LEADING '0x' FROM HEX(job_meta))) USING utf8mb4), '$.reorg_meta.batch_size') AS batch_size FROM mysql.tidb_ddl_job WHERE job_id = 149; +--------+-------------+------------+ | job_id | concurrency | batch_size | +--------+-------------+------------+ | 149 | 8 | 1024 | +--------+-------------+------------+ 1 row in set (0.00 sec) DDL完了時間の見積もり DDLについては実行完了時間の見積もりができることが望ましいと思います。 まず、前提条件としてTiDBでDDLを発行した際、 ADMIN SHOW DDL JOBS コマンドで、 information_schema.ddl_jobs のROW_COUNTという値が観測可能であり、これはDDLの実行に伴って値が増えていきます。 : ALTER文の実行 mysql> ALTER TABLE test_alter_row_count MODIFY COLUMN value INT NOT NULL; Query OK, 0 rows affected (0.28 sec) : ADMIN SHOW DDL JOBSコマンドで実行結果/実行中の状況が確認可能 mysql> admin show ddl jobs 1; +--------+---------+----------------------+---------------+--------------+-----------+----------+-----------+----------------------------+----------------------------+----------------------------+--------+----------+ | JOB_ID | DB_NAME | TABLE_NAME | JOB_TYPE | SCHEMA_STATE | SCHEMA_ID | TABLE_ID | ROW_COUNT | CREATE_TIME | START_TIME | END_TIME | STATE | COMMENTS | +--------+---------+----------------------+---------------+--------------+-----------+----------+-----------+----------------------------+----------------------------+----------------------------+--------+----------+ | 151 | mercari | test_alter_row_count | modify column | public | 116 | 143 | 100000000 | 2026-02-27 01:45:35.549000 | 2026-02-27 01:45:35.549000 | 2026-02-27 02:03:25.748000 | synced | | +--------+---------+----------------------+---------------+--------------+-----------+----------+-----------+----------------------------+----------------------------+----------------------------+--------+----------+ 1 row in set (0.00 sec) この値を利用してどのように、完了時刻見積もりができるか、をバージョンごとに確認したのが本エントリの内容です。 v8.5.3まで TiDB v8.5.3で下記のテーブルを作成し、1000行のデータにインデックスを作成していきました CREATE TABLE test_alter_row_count ( id BIGINT UNSIGNED NOT NULL, value INT UNSIGNED NOT NULL, label VARCHAR(255) NOT NULL DEFAULT '', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) CLUSTERED ); -- 1000行を挿入後、value カラムを参照するセカンダリインデックスを 0〜4 本作成 インデックス作成後に、INT UNSIGNEDから INTへの変更を伴う次のDDLを発行した際のROW_COUNTの変化を観測します。この変更は、UNSIGNEDとSIGNEDではバイト列のエンコーディングが異なるため、行データとインデックスの両方を書き換える必要があり、TiDB内部で、ModifyTypeReorg (Full Reorg)に分類されるものとなります。 ALTER TABLE test_alter_row_count MODIFY COLUMN value INT NOT NULL; 結果としては、テーブルの行数をR、変更対象のカラムを含むインデックス数をnとした際に、下記のように変更対象のカラムに関連するインデックス数の2乗のオーダーのROW_COUNTとなること(R x (1 + n²))が観測されました。 インデックス数 (n) ROW_COUNTの値 R x (1 + n²) 0 1,000 1,000 1 2,000 2,000 2 5,000 5,000 3 10,000 10,000 4 17,000 17,000 複合インデックス(例: (value, created_at, updated_at))におけるROW_COUNTに対する寄与も確認し、これは単一カラムインデックスと同じ寄与で、インデックスに含まれるカラム数は無関係であることを確認しました。 処理の実態と発生条件 ROW_COUNTの観測により、DDLの完了時刻をなるべく正確に推測することを目標とすると、 この R x (1 + n²) という式が、ROW_COUNT の集計上のバグなのか、それとも実際に n² に比例する処理が走っているのかを切り分ける必要があります。 OSS の最新のmasterブランチを解析すると、masterのMODIFY COLUMNはカラム書き換えとインデックス再構築が別ステージに分離されており、n² パターンを説明できるコードパスは見つかりませんでした。 8.5.3に関してはMODIFY COLUMN(Full Reorg)はupdateCurrentElementという関数で処理され、この関数にはインデックスを逐次的に処理するループがありました。 https://github.com/pingcap/tidb/blob/release-8.5-20251107-v8.5.3/pkg/ddl/column.go#L523-L597 // pkg/ddl/column.go L523-596 (v8.5.3) func (w *worker) updateCurrentElement(ctx context.Context, t table.Table, reorgInfo *reorgInfo) error { // Phase 1: カラム書き換え(全 R 行の値を変換) if bytes.Equal(reorgInfo.currElement.TypeKey, meta.ColumnElementKey) { err := w.updatePhysicalTableRow(ctx, t.(table.PhysicalTable), reorgInfo) } // Phase 2: インデックス再構築ループ(n 回実行) for i := startElementOffset; i < len(reorgInfo.elements[1:]); i++ { reorgInfo.currElement = reorgInfo.elements[i+1] // 現在の要素を idx_i に設定 err = w.addTableIndex(ctx, t, reorgInfo) // インデックスを再構築 } } ここからインデックス再構築でRx n²の操作が発生することになっている実装上の要因2点を説明します。細かい話なので読み飛ばしていただいて構いません。 1. Worker が「現在のインデックス」ではなく「全インデックス」を受け取る addTableIndexの内部ではbackfill workerが生成されます。このとき、reorgInfo.elements(カラム+全 n インデックスを含む完全なリスト)がそのまま渡されます。 // pkg/ddl/backfilling_scheduler.go L280-281 (v8.5.3) idxWorker, err := newAddIndexTxnWorker(b.decodeColMap, b.tbl, backfillCtx, job.ID, reorgInfo.elements, reorgInfo.currElement.TypeKey) // ^^^^^^^^^^^^^^^^^^ // 「全要素リスト」が渡される Workerはこのリストからインデックス型の要素をすべて抽出して保持します。 // pkg/ddl/index.go L1892-1900 (v8.5.3) allIndexes := make([]table.Index, 0, len(elements)) for _, elem := range elements { if !bytes.Equal(elem.TypeKey, meta.IndexElementKey) { continue } indexInfo := model.FindIndexInfoByID(t.Meta().Indices, elem.ID) index := tables.NewIndex(t.GetPhysicalID(), t.Meta(), indexInfo) allIndexes = append(allIndexes, index) // 全 n インデックスが入る } つまり、ループの i 番目のイテレーションでcurrElementがidx_iを指していても、Workerは idx₁ からidx_nまでの全インデックスを処理します。 その結果、テーブルの全行をスキャンする際に各行からn個のidxRecordが生成されます。 // pkg/ddl/index.go L2027-2032 (v8.5.3) for _, index := range w.indexes { // w.indexes は全 n 個 idxRecord, _ := w.getIndexRecord(index.Meta(), handle, recordKey) w.idxRecords = append(w.idxRecords, idxRecord) } R行 × nインデックス = n×R 個のレコード。これがn回のイテレーションそれぞれで生成されるため、合計 n × n × R = n²R 個になります。 2. DupKeyCheckSkip による重複書き込みの黙認 仮に1があっても、2 回目以降のイテレーションでは「すでに同じインデックスエントリが存在する」ため、重複キーエラーで処理がスキップされれば実害は小さいはずです。しかし、BackfillDataではDupKeyCheckSkipフラグが指定されています。 // pkg/ddl/index.go L2349-2354 (v8.5.3) handle, err := w.indexes[i%len(w.indexes)].Create( w.tblCtx, txn, idxRecord.vals, idxRecord.handle, idxRecord.rsData, table.WithIgnoreAssertion, table.FromBackfill, table.DupKeyCheckSkip, // 重複チェックを完全にスキップ ) DupKeyCheckSkipはindex.Create内部で以下のように作用します。 // pkg/table/tables/index.go L259-278 (v8.5.3) skipCheck := opt.DupKeyCheck() == table.DupKeyCheckSkip // true if !distinct || skipCheck || untouched { err = txn.GetMemBuffer().Set(key, val) // 既存エントリを無言で上書き continue // ErrKeyExists は発生しない } 重複チェックをスキップしているため、MemBufferへのSetは既存エントリを上書きするだけでエラーになりません。BackfillDataに戻るとtaskCtx.addedCount++が無条件に実行され、全 n×Rレコードがカウントされます。 v8.5.4での修正(2025年11月) PR #63970 "modify column: support ingest/DXF mode to recreate indexes" により、MODIFY COLUMNの処理アーキテクチャが刷新されたことにより、この問題は解消しました。 TiDBの変更にはいくつかの種類があり、通常のインデックス追加はデフォルトではIngestという物理ImportであるTiDB lightningと同じ方式の高速な処理方法が適用されます。DXFというのは分散実行の仕組みで、これも通常Indexの追加に利用されます。 https://docs.pingcap.com/tidb/stable/tidb-distributed-execution-framework/ このPRの変更内容は、カラムの変更であるmodify columnの際のインデックス再構築にこの処理を利用可能にしたものです。 これにより、修正後のアーキテクチャでは、MODIFY COLUMNが 3 つの明示的なステージに分離されました。 Stage 1: ReorgStageModifyColumnUpdateColumn → カラム書き換えのみ Stage 2: ReorgStageModifyColumnRecreateIndex → 全インデックスを一括再構築 Stage 3: ReorgStageModifyColumnCompleted → 完了 updateCurrentElementのインデックスループは完全に削除され、modifyTableColumnにリネームされた関数はカラム書き換えのみを担当します。インデックス再構築は doReorgWorkForCreateIndexに委譲され、全インデックスを1 回の呼び出しで一括処理します。 // v8.5.4 pkg/ddl/column.go func (w *worker) modifyTableColumn(...) error { if bytes.Equal(reorgInfo.currElement.TypeKey, meta.ColumnElementKey) { err := w.updatePhysicalTableRow(ctx, t, reorgInfo) // カラムのみ } return nil // インデックスループは存在しない } この修正により n² 問題は解消され、doReorgWorkForCreateIndexはpickBackfillType でIngest / TxnMergeを動的に選択できるため、MODIFY COLUMNでもIngest(Lightning ベース)パイプラインが利用可能になりました。 また、詳細は割愛しますが、本問題は7.4.0から発生していたように思われます。 v8.5.4以降 さて、8.5.4以降でのバージョンで、modify columnで必要以上に時間がかかる構造は解消されていそうなことがわかりました。どのようにカウントされるようになったのでしょうか。 インデックス数 (n) ROW_COUNTの値 0 1,000 1 1,000 2 1,000 3 1,000 4 1,000 インデックス数に依存しないレコード数に等しい値となることが確認できました。 それでは、これのROW_COUNTの値を利用して、どのように実行の完了時間を予測できるでしょうか。 これを確認するため、1億レコードのテスト用のテーブルと、4つのインデックスを事前に作成、これらに対してDDLを実行し、ROW_COUNTの推移を実際に観測してみました。 ROW_COUNTは最終的にはレコード数Rになるのですが、最初のカラムの書き換えがおわり、インデックスの再構成が発生する際に一度0にクリアされ、n本のindexが同時構築されるため再度増加しRに達する、という挙動が観測されました。また、Step 2のIngestは高速な処理が行われることも合わせて確認できると思います。 また、ROW_COUNTがRに達した後も何らかの処理が継続されますが、現状この状態の進捗を何らかの方法で確認する方法は確認できておりません。 なお、このステージおよび、ROW_COUNTについては、先ほどの mysql.tidb_ddl_job により確認可能で、今回検証でこのDDLの進捗確認に利用したスクリプトを共有します。 import pymysql import time import csv import sys JOB_ID = 151 INTERVAL = 5 conn = pymysql.connect( host="127.0.0.1", port=3306, user="root", password="", database="test", autocommit=True ) cursor = conn.cursor() cursor.execute("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED") log_file = f"ddl_job_{JOB_ID}.csv" with open(log_file, "w", newline="") as f: writer = csv.writer(f) writer.writerow(["elapsed_sec", "timestamp", "row_count", "state", "schema_state", "stage", "reorg_tp", "concurrency", "batch_size", "task_id", "task_state", "subtask_row_count"]) META_QUERY = """ SELECT JSON_EXTRACT(CONVERT(UNHEX(TRIM(LEADING '0x' FROM HEX(job_meta))) USING utf8mb4), '$.reorg_meta.stage'), JSON_EXTRACT(CONVERT(UNHEX(TRIM(LEADING '0x' FROM HEX(job_meta))) USING utf8mb4), '$.reorg_meta.reorg_tp'), JSON_EXTRACT(CONVERT(UNHEX(TRIM(LEADING '0x' FROM HEX(job_meta))) USING utf8mb4), '$.reorg_meta.concurrency'), JSON_EXTRACT(CONVERT(UNHEX(TRIM(LEADING '0x' FROM HEX(job_meta))) USING utf8mb4), '$.reorg_meta.batch_size') FROM mysql.tidb_ddl_job WHERE job_id = %s """ print(f"Monitoring JOB_ID={JOB_ID} every {INTERVAL}s → {log_file}") print(f"{'elapsed':>8} {'row_count':>14} {'state':<10} {'stage':>5} {'reorg_tp':>8} {'thr':>3} {'batch':>6} {'task_id':>7} {'task_state':<10} {'subtask_rows':>14}") print("-" * 110) start = time.time() while True: try: # ROW_COUNT (real-time from memory) cursor.execute(""" SELECT JOB_ID, ROW_COUNT, STATE, SCHEMA_STATE, NOW() FROM information_schema.ddl_jobs WHERE JOB_ID = %s LIMIT 1 """, (JOB_ID,)) ddl_row = cursor.fetchone() # Stage/config from job_meta (persisted) stage = reorg_tp = conc = bs = "" try: cursor.execute(META_QUERY, (JOB_ID,)) meta_row = cursor.fetchone() if meta_row: stage = meta_row[0] if meta_row[0] is not None else "" reorg_tp = meta_row[1] if meta_row[1] is not None else "" conc = meta_row[2] if meta_row[2] is not None else "" bs = meta_row[3] if meta_row[3] is not None else "" except Exception: pass # Dist task progress (only exists during Stage 2) task_id = "" task_state = "" subtask_row_count = "" try: cursor.execute(""" SELECT id, state FROM mysql.tidb_global_task WHERE task_key LIKE CONCAT('ddl/%%/', %s, '%%') ORDER BY id DESC LIMIT 1 """, (JOB_ID,)) task_row = cursor.fetchone() if task_row: task_id, task_state = task_row cursor.execute(""" SELECT COALESCE(SUM(summary->'$.row_count'), 0) FROM mysql.tidb_background_subtask WHERE task_key LIKE CONCAT('ddl/%%/', %s, '%%') """, (JOB_ID,)) st_row = cursor.fetchone() if st_row: subtask_row_count = st_row[0] except Exception: pass elapsed = int(time.time() - start) if ddl_row: job_id, row_count, state, schema_state, ts = ddl_row row_count = row_count or 0 line = (f"{elapsed:>7}s {row_count:>14,} {state:<10} {stage:>5} {reorg_tp:>8} " f"{conc:>3} {bs:>6} {str(task_id):>7} {str(task_state):<10} {str(subtask_row_count):>14}") print(line) with open(log_file, "a", newline="") as f: writer = csv.writer(f) writer.writerow([elapsed, ts, row_count, state, schema_state, stage, reorg_tp, conc, bs, task_id, task_state, subtask_row_count]) if state in ("synced", "cancelled", "paused"): print(f"\nJob finished: state={state}, final ROW_COUNT={row_count:,}") break else: print(f"{elapsed:>7}s job not found") except Exception as e: print(f" error: {e}") conn.ping(reconnect=True) cursor = conn.cursor() cursor.execute("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED") time.sleep(INTERVAL) cursor.close() conn.close() なお、 mysql.tidb_ddl_job テーブルのjob_meta属性にはJSONの値が格納されていますが、この中の構造に現れるreorg_meta.stageは8.5.4による、DDLの実行方式の変更に伴い新設されています。これにより、8.5.4以降ではStage 1完了後に一度ROW_COUNTがリセットされるものの、現状どちらのStageを処理しているかの判別ができます。 最終的にバージョン間で差分が見られるインデックス再構築の箇所のみに焦点を当て、それぞれのバージョンでどうなっていたか、という結論を図にまとめたものが以下になります。 まとめ 今回はDDL実行中の速度制御の方法と、完了時間の見積もりについて紹介しました。 速度制御については、 ADMIN ALTER DDL JOBS で可能である一方、コマンド実行により変更が反映されないバグが含まれるバージョンがあり、PAUSE/RESUMEにてこれを反映できることを説明しました。 完了時間見積もりについては、DDLがmodify columnに分類される場合、ROW_COUNTの値で見積もりが可能です。TiDB 8.5.3までは、対象のカラムを含むインデックス数の2乗のオーダーに比例する不具合があり、また、ROW_COUNTが統計上の値が増えているだけではなく、実際に発生するI/Oなどにも直接影響し注意が必要です。 また、TiDB 8.5.4以降では、インデックス再構築が、従来とは異なる方式で実施されるようになり、重複して実行される不具合は解消し、かつ高速になりました。ROW_COUNTは最終的には、レコード数に近い値になるものの、インデックス構築フェーズで値が一度ゼロにリセットされ、再度カウントアップする挙動となっていた検証結果を共有しました。 マイナーバージョンによる不具合などについては、公式ドキュメントをよく読んでいるだけではわからないことが多く、他のDBMSなどをみても、この類の情報が充実してくると、利用者としても安心できる状況に近づくのではないかと考えています。 メルカリではTiDBの運用を進めており、このような想定と異なる挙動の遭遇はまだまだ多く、避けられないものです。このような、想定外の挙動に対し、生成AIなどを活用し状況を理解し、世の中に発信しユーザーや製品に還元していけるような仲間を求めています。 また、現在当チームのマネージャーポジションが募集中となります。興味ある方はぜひご応募ください。 https://apply.workable.com/mercari/j/7AD4EF9218/
.table-of-contents > li > ul > li > ul { display: none; } はじめに こんにちは、データサイエンス部コーディネートサイエンスブロックの 大川 です。私たちは、WEARにおける「似合う」をユーザーに届けるため、LLMやマルチモーダルAIを活用してコーディネートの特徴抽出や似合うに関する独自の判定処理のR&Dを行っています。 LLMが台頭して以降、LLMに構造化出力を要求するタスクは増えています。数百件のテストでは問題なく動いていたシステムが、本番運用で10万件・100万件規模の推論を回すと思わぬエラーに直面することがあります。 本記事では、ファッション画像から柄の特徴を抽出するタスクを本番運用する過程で直面した課題と、その解決策を共有します。具体的には、エラー内容をプロンプトにフィードバックしてリトライする手法により、87%のエラー削減を達成しました。この手法はLLMの構造化出力タスク全般に応用可能です。 目次 はじめに 目次 サマリー 前提条件 発生した問題 問題1: 不正な出力(68件) 問題2: トークンが繰り返される出力(9件) 原因分析 原因1: 不正な出力 原因2: トークンが繰り返される出力 解決策 解決策1: バリデーション&リトライの追加 解決策2: プロンプトへのエラーフィードバック追加 ValueErrorの場合 JSONDecodeErrorの場合 GoogleAPIError(APIエラー)の場合 結果 エラー削減効果 性能への影響 まとめ おわりに サマリー LLMの構造化出力で発生する「不正な値の出力」と「トークン繰り返し」問題に対し、バリデーション+エラーフィードバックプロンプトで87%のエラー削減を達成(68件→9件) エラー内容だけでなく、リトライ回数とtemperatureもフィードバックに含めると効果が大きい(21件→9件) F1スコアへの影響は約0.02の低下にとどまり、安心して導入できる 前提条件 前提条件を揃えるため、タスク内容とLLMの仕様を共有します。 項目 内容 タスク ファッション画像から複数の柄の特徴を抽出するタスク(マルチラベル分類) 推論規模 約10万件の全身コーディネート画像 使用モデル gemini-2.5-flash-lite 出力形式 JSON(許可された値のリストから選択) リトライ 最大3回 構造化出力では、柄の種類( pattern_type )などの特徴に対して、事前定義された値のみを出力するようLLMに指示しています。例えば、ニュアンス柄( nuance_pattern )やグレンチェック柄( glen_check_pattern )などが定義済みの値です。この制約の実装にはGemini APIの response_schema パラメータを利用しています 1 。 ただし、 response_schema はJSONの 構文的な正しさ(型やフィールド名)は保証しますが、値の意味的な正しさは保証しません 。公式ドキュメントでも「最終的な出力は、使用する前に必ずアプリケーションコードで検証してください」と明記されています 2 。この仕様上の限界が、後述する「不正な出力」問題の背景にあります。 LLMにより画像から抽出されたpattern_typeのマルチラベル分類例 発生した問題 約10万件の画像を推論したところ、以下の2つの問題が発生しました。 問題1: 不正な出力(68件) 定義外の値が出力されるケースです。例えば pattern_type に対して、 logo 、 patchwork_pattern 、 graphic_pattern のような、あらかじめ指定したリストに含まれない値が返ってきました。 不正な値 件数 logo 26 patchwork_pattern 17 graphic_pattern 14 camouflage_pattern 6 その他 5 これらの不正な値は、いずれもファッション領域では実在する概念です。LLMが持つ一般知識から「もっともらしい値」を生成してしまったと考えられます。 問題2: トークンが繰り返される出力(9件) 同じトークンが無限に繰り返され、JSONパースに失敗するケースです。Gemini API公式ドキュメントでも「トークンの繰り返しに関する問題」として同様の事象が取り上げられています 3 。 実際のトークン繰り返しエラーのログ(Langfuse) この問題が厄介なのは、JSONパースエラーでリトライしても同様の事象が繰り返される点です。その結果、以下の影響が生じます。 出力が得られない : 3回リトライしても正常な結果を取得できない レイテンシーの悪化 : 1件あたり10分程度かかるケースも発生 コストの増加 : 無駄なトークンを大量に消費する この問題をスケールで考えると深刻さが分かります。10万件中9件の発生率(0.009%)は一見小さく見えますが、本番の全件推論で400万件を処理する場合、約360件でこの問題が発生する計算です。1件あたり10分の遅延とすると、トークン繰り返し問題だけで 約60時間(2.5日分)の遅延 が発生します。 原因分析 原因1: 不正な出力 不正な出力の原因は、出力値のバリデーションとリトライの仕組みが不十分だったことです。前述のとおり、Geminiの response_schema はJSONの構文を制約するものであり、enum値の完全な制約までは保証しません。従来の実装ではこれを検知してリトライする機能がなく、不正な出力がそのまま通過していました。 原因2: トークンが繰り返される出力 この問題の背景には、 再現性とトークン繰り返しのトレードオフ があります。分類タスクでは temperature=0 で出力を安定させたい一方、それがトークン繰り返し問題を引き起こします。実際、Gemini API公式のトラブルシューティングガイドでも、temperatureを低く設定すると「ループや性能劣化などの予期しない動作を引き起こす可能性がある」と警告されています 4 。 技術的には、 temperature=0 の貪欲デコーディングにより、特定の入力に対して同じ出力トークンが延々と選ばれ、適切にEOSトークンで終了できない状態に陥ります。この問題に対処するため、リトライ時にtemperatureを0.1ずつ増やす施策を導入していましたが、それだけでは完全には回避できませんでした。 解決策 2つのアプローチを組み合わせて改善を図りました。 解決策1: バリデーション&リトライの追加 不正な値が出力された際に、許可された値のリストと照合してバリデーションし、失敗時はリトライする機能を追加しました。 解決策2: プロンプトへのエラーフィードバック追加 単にリトライするのではなく、前回のエラー内容をプロンプトの末尾にフィードバックして再試行させることでLLMの注意を問題点に向けさせました。このとき、エラーの種類によってフィードバック内容を変えるように設計しました。 ValueErrorの場合 ValueErrorの場合、問題1(不正な出力)の発生が予想されます。どの値が不正で、どの値が許可されているかをエラーメッセージとしてそのままフィードバックするようにしました。 前回の推論で以下のようなエラーが発生しましたので注意してください。 ** 前回のConfig・エラー情報 ** - 試行: {N} 回目 - temperature: {current_temp} - 前回エラー: ValueError: invalid result for feature=pattern_type: 'logo' (allowed: ['ethnic_pattern', 'geometric_pattern', ...]) JSONDecodeErrorの場合 JSONDecodeErrorの場合、トークンが繰り返されている可能性が高いと判断し、通常のプロンプトの末尾に以下のフィードバックを追加しました。この問題は公式ドキュメントでも言及されており、「同じことを繰り返さないでください」という指示を追記することが推奨されています 5 。 前回の推論で以下のようなエラーが発生しましたので注意してください。 ** 前回の Config・エラー情報 ** - 試行: {N} 回目 - temperature: {current_temp} - 前回エラー: JSONDecodeError: ... 無限にトークンが繰り返される問題が発生している可能性があります。**同じことを繰り返さないでください。** GoogleAPIError(APIエラー)の場合 GoogleAPIError(APIエラー)の場合、レート制限やネットワークエラーが主な原因となるため、プロンプトを改善しても解決しません。この場合はフィードバックを追加せず、指数バックオフによるリトライのみとしました。 結果 エラー削減効果 解決策の効果を検証するため、不正な出力を起こした68件を評価データとして用い、施策前後での改善度合いを比較しました。なお、トークンが繰り返される問題については、エラーの再現ができなかったため今回は評価データから除外しています。 3つの条件を用意して比較実験を行いました。 解決策1: バリデーションのみを追加 解決策2-1: バリデーションとエラーフィードバック(エラー内容のみ) 前回の推論で以下のようなエラーが発生しましたので注意してください。 - 前回エラー: ValueError: invalid result for feature=pattern_type: 'logo' (allowed: ['ethnic_pattern', 'geometric_pattern', ...]) 解決策2-2: バリデーションとエラーフィードバック(エラー内容 + リトライ数 + temperature) 前回の推論で以下のようなエラーが発生しましたので注意してください。 ** 前回のConfig・エラー情報 ** - 試行: {N} 回目 - temperature: {current_temp} - 前回エラー: ValueError: invalid result for feature=pattern_type: 'logo' (allowed: ['ethnic_pattern', 'geometric_pattern', ...]) 施策 バリデーションの有無 エラーFBの有無 エラー件数 削減率 ベースライン ✗ ✗ 68件 — 解決策1 ✓ ✗ 40件 41% 解決策2-1 ✓ ✓ 21件 69% 解決策2-2 ✓ ✓ 9件 87% 実験結果として、 87%のエラー削減 (68件 → 9件)を達成しました。 重要な発見として、 エラー内容だけでなく、リトライ数とtemperatureも付与したほうが効果的である ことを確認しました(解決策2-1の21件→解決策2-2の9件)。これらの情報を付与することで、LLMが「何回目の試行で、どのような生成条件なのか」を把握でき、前回と異なる出力を生成しやすくなったと推察されます。 トークンが繰り返される問題についても、定量的な評価には至っていないものの、定性的には出現頻度の低下と出力の安定化を確認しています。 性能への影響 エラーフィードバックを追加することで性能への悪影響がないか検証しました。柄の評価データセットを用意し、エラーFBの有無で3回ずつ実行した平均値を比較しました。リトライ時にtemperatureを0.1ずつ増やす運用を想定し、temperature 0.0〜0.2の範囲で検証しています。 モデル temperature エラーFBの有無 F1スコア gemini-2.5-flash-lite 0.0 ✗ 0.8417 gemini-2.5-flash-lite 0.0 ✓ 0.8208 gemini-2.5-flash-lite 0.1 ✗ 0.8434 gemini-2.5-flash-lite 0.1 ✓ 0.8208 gemini-2.5-flash-lite 0.2 ✗ 0.8425 gemini-2.5-flash-lite 0.2 ✓ 0.8217 性能への大きな影響はないことを確認しました 。数値上ではF1スコアに約0.02の低下が見られますが、エラーフィードバックが適用されるのはバリデーション失敗時のリトライのみです。正常に出力された大多数のケースではフィードバックが付与されないため、システム全体への影響は軽微です。 まとめ 本記事では、LLMの構造化出力で発生するエラーを87%削減した手法を紹介しました。 本記事の貢献は以下のとおりです。 バリデーション+エラーフィードバック をプロンプトに含めることでエラー件数を87%削減できる エラー内容だけでなく、 リトライ数とtemperatureも付与すると効果が高い フィードバックを追加してもF1スコアへの大きな悪影響はなく、安心して導入できる この手法はGeminiに限らず、 LLMの構造化出力タスク全般 に応用可能 LLMの構造化出力や、Gemini APIの出力の安定化(トークン繰り返し問題の回避)に悩むエンジニアの方々にとって、本手法が何らかのヒントになれば幸いです。 おわりに ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com Generative AI on Vertex AI - 構造化出力 ↩ Gemini API - 構造化出力 ↩ トラブルシューティング ガイド - トークンの繰り返しに関する問題 ↩ 朗読に関する問題 ↩ トークンの繰り返しに関する問題 ↩
はじめに 皆さんこんにちは、InsightEdgeのDataScientistのSugaです。最近は徒歩圏内にサウナが新しく出来たのでリフレッシュのため、そこにばっかりに通っています。 さて、今回は最近話題のブロードリスニングについての記事です。 「ブロードリスニング」とは、大量の意見データを AI で構造化・分析し、全体像を俯瞰する手法です。従来のアンケート分析やインタビューでは拾いきれない多様な声を、LLM(大規模言語モデル)とクラスタリング技術を組み合わせて一気に可視化します。 本記事では、 乾燥機付きドラム洗濯機 をサンプルテーマに取り上げ、以下の3ステップで分析を行いました。 意見(VOC)の生成とクラスタリング分析 — 1000件の消費者意見を AI で生成し、論点を抽出・クラスタリング ペルソナ推定とクラスタリング分析 — 各意見からペルソナを推定し、10タイプに分類 マーケティング検証 — 推定したペルソナに対してアンケート・購買判断・購入理由の分析を実施 使用技術 項目 技術 LLM Azure OpenAI GPT-5.2 Embedding text-embedding-3-large(3072次元) 次元削減 UMAP クラスタリング HDBSCAN + BERTopic + SpectralClustering / KMeans 日本語トークナイザ janome 可視化 matplotlib, Next.js(インタラクティブ散布図) 1. 意見(VOC)の生成とクラスタリング分析 1.1 意見データの生成 まず、分析対象となる消費者の意見データ(VOC: Voice of Customer)を用意します。今回は LLM を使って、乾燥機付きドラム洗濯機に関する 1000件の意見 を生成しました。 各意見は実際のクチコミに近い形で、購入動機・使い方・満足点・不満点などが含まれています。 例: 「共働きで小学生2人。体操服と給食エプロンを毎日乾燥まで回している。夜21時に予約して朝6時に取り出せるのが助かる。ただ、電気代が月3000円くらい上がった気がする。フィルター掃除は週1でやっているが面倒…」 1.2 論点の抽出 1000件の意見を直接クラスタリングするのではなく、まず各意見から 独立した論点 を抽出します。1つの意見には複数の論点(時短、電気代、騒音など)が含まれるためです。 GPT-5.2 で各意見から2〜5個の論点を抽出し、合計 5,447件の論点 を得ました。 1.3 クラスタリングの処理フロー 抽出した論点に対して、以下のパイプラインでクラスタリングを行います。 論点テキスト → text-embedding-3-large → 3072次元ベクトル → UMAP(2次元に削減) → HDBSCAN + BERTopic → SpectralClustering → ラベル付け → 要約 → 散布図レポート この結果、 15個の主要クラスタ に分類されました。 1.4 クラスタリング結果 ID ラベル #0 花粉雨雪でも外干し不要で臭い減少 #1 共働き育児介護で洗濯増え時短導入 #2 乾燥の電気代増と手入れ負担の不満 #3 乾燥フィルターと排水口の掃除負担と怠ると不調 #4 夜間中心に週3〜5回洗濯乾燥を回す習慣 #5 賃貸での搬入経路狭さと設置追加費用問題 #6 夜回して朝には乾く時短と段取り安定 #7 縦型や部屋干しで乾かず洗濯が回らない悩み #8 乾燥で衣類のシワ縮み傷みが気になる #9 夜間運転の騒音振動で近隣に気を遣う #10 来客前の寝具カバー類を即日洗乾燥できる便利さ #11 花粉アレルギーや部屋干し臭対策の導入理由 #12 洗剤自動投入と投入口のぬめり詰まり掃除負担 #13 ドアパッキンの水滴残りと臭い掃除負担 #14 夜間予約で毎日洗濯乾燥し朝完了運用 VOCクラスタリングの散布図 1.5 VOCクラスタリングからわかったこと 全体の傾向を一文でまとめると: 乾燥まで自動で完結し、雨雪・梅雨・花粉でも外干し不要で時短と快適さが高評価。共働き・育児介護や賃貸で夜回して朝仕上げる運用が定番。一方、電気代増や騒音振動、搬入設置の難しさが不満。さらにフィルター・排水口・パッキン等の掃除負担やシワ縮み対策など手入れ前提が目立つ。 クラスタリングの結果を大きく分類すると、以下の4つのテーマに整理できます。 満足・導入理由(#0, #1, #6, #7, #10, #11, #14) - 花粉・梅雨・雪でも外干し不要 → 生乾き臭からの解放 - 「夜回して朝に乾く」段取りの安定性 - 共働き・育児・介護で洗濯量が増えた家庭の時短ニーズ - 縦型や部屋干しから乗り換えた人の満足感が高い 不満・課題(#2, #3, #8, #9, #12, #13) - 乾燥の電気代増(月2000〜3000円の上昇感) - フィルター・排水口・ドアパッキンの定期清掃の手間 - 衣類のシワ・縮み・傷み - 夜間運転の騒音・振動(集合住宅での近隣配慮) 使い方の工夫(#4, #14) - 週3〜5回の高頻度運転が一般的 - タオル・寝具は乾燥まで、デリケート衣類は洗いのみと使い分け 設置の壁(#5) - 賃貸では搬入経路の狭さや設置追加費用が障壁 1.6 生成AIによる意見生成についての考察 今回は LLM を使って1000件の消費者意見を生成しました。この手法にはいくつかの論点があります。 なぜ実データではなく生成データを使ったのか 本記事の目的は「ブロードリスニングの分析手法を検証すること」であり、分析パイプラインの構築と検証が主眼です。実際のクチコミデータを使う場合、収集・クレンジング・匿名化などの前処理が必要になりますが、生成データであればすぐに分析パイプラインの検証に着手できます。 また、実データには偏りがつきものです。特定の ECサイトのレビューだけを集めると、そのサイトの購買層に偏った意見になります。LLM による生成では「こういう属性の人はどういう意見を持つか」というシナリオを幅広く網羅できるため、分析手法の検証には適しています。 生成データの限界 一方で、LLM が生成する意見には「きれいすぎる」という問題があります。実際のクチコミは文法が崩れていたり、複数の話題が混在していたり、感情的な表現が含まれていたりします。LLM 生成の意見はそれらに比べると整った文章になりがちで、論点抽出やクラスタリングが実データよりうまくいく可能性があります。 また、LLM の学習データに含まれる情報に基づいて生成されるため、学習データに存在しない新しいトレンドや、ニッチな使い方(例: 特定の地域特有の事情)は反映されにくい傾向があります。 実務での使い分け 実務では、以下のような使い分けが現実的です。 手法の検証・プロトタイピング : 生成データで分析パイプラインを構築・検証し、実データへの適用前にワークフローを固める 実データの補完 : 実データが少ないセグメント(例: 介護世帯やペット世帯)の意見を生成データで補い、クラスタの偏りを軽減する 仮説生成 : 「こういう層はこういう意見を持つのではないか」という仮説を生成し、実データで検証する 生成AIによる意見データは「答えそのもの」ではなく、「分析の出発点」として位置づけるのが適切です。 2. ペルソナ推定とクラスタリング分析 2.1 ペルソナ推定の考え方 VOCのクラスタリングでは「何が語られているか(論点)」を分析しました。次のステップでは、 「誰が語っているか(ペルソナ)」 に焦点を当てます。 各意見から、以下の4つの観点でペルソナを推定します。 購買価値観 — 何を重視して選ぶか 嗜好 — どんな製品・ブランドを好むか ライフスタイル — 日常の生活パターン ライフステージ — 家族構成・年代・住居形態 GPT-5.2 で1000件の意見それぞれから300〜500文字のペルソナテキストを生成し、同じパイプライン(embedding → UMAP → KMeans)で 10クラスタ に分類しました。 2.2 ペルソナクラスタリング結果 ID ラベル 件数 C0 退職後シニアの省力家事と外干し回避ニーズ 91 C1 単身若手の時短家事自動化ニーズ層 111 C2 夜間運転の静音性と花粉冬対策重視層 119 C3 ペットの毛対策と時短を求める30〜40代層 74 C4 花粉アレルギー対策で外干し回避する家庭 79 C5 賃貸集合住宅で夜回し時短と節約両立層 74 C6 子育て共働きの夜洗濯乾燥で朝支度時短層 142 C7 花粉外干し回避と夜洗乾の共働き層 115 C8 介護で増える汚れ物を夜間に洗乾完結したい層 78 C9 子育て期のひとり親多世代の時短洗乾燥需要層 117 2.3 各ペルソナクラスタの特徴 C0: 退職後シニア(91件) 40〜70代の退職後世帯(単身・夫婦)で、体力低下や外干しの負担から「洗い〜乾燥で完結」を求める実用派。ブランド信頼性を重視し、電気代や手入れ負担には敏感だが、乾燥は雨・冬・花粉期中心に使い分ける冷静な運用を行う。 C1: 単身若手(111件) 20〜40代の単身〜同棲前後の多忙層。干す手間やコインランドリー通い、部屋干しの生乾き臭を解消したい「時短・実用」志向が中心。集合住宅では騒音・振動への配慮が必要で、電気代増にも敏感。 C3: ペット同居世帯(74件) 犬猫などペットと暮らす30〜40代。毛がフィルターにまとまる点、部屋干しゼロ、朝までに乾く点に満足。ただし騒音/振動、パッキンの毛掃除などの手入れ負担に不満も。 C6: 子育て共働き(142件・最大クラスタ) 30〜40代の共働き・子育て世帯が中心で、洗濯量増と干し場不足から「夜回して朝完了」を狙う。雨・花粉のストレス解消と生活空間の整頓を評価するが、騒音・振動や電気代には敏感。 C8: 介護世帯(78件) 在宅介護を担う40〜60代中心。洗濯回数増・突発汚れ・天候不安から「干す工程を消して朝までに乾かす」時短と衛生を最優先。便利さは高評価だがコストとメンテ負荷に揺れる。 C9: 子育て期のひとり親・多世代(117件) 30〜40代の子育て世帯(共働き・ひとり親・三世代同居)。洗濯量増と時間不足から「夜に回して朝に乾いている」確実性と時短を最優先する。 2.4 ペルソナ分析からわかったこと VOCの論点クラスタリングでは「時短」「電気代」「騒音」といったテーマ別の分類でしたが、ペルソナクラスタリングによって 同じ「時短」でもライフステージによって意味が異なる ことが見えてきます。 C1(単身若手)の時短 = コインランドリー通いの代替。干す手間からの解放 C6(子育て共働き)の時短 = 夜に予約→朝回収の段取り。家事全体の最適化 C8(介護世帯)の時短 = 突発的な汚れ物への即応。衛生と時短の両立 C9(多世代子育て)の時短 = 大量洗濯物のまとめ処理。確実に朝までに乾く安心感 また、 C3(ペット層) や C4(花粉アレルギー層) のように、特定の生活課題がドラム洗濯機導入の強い動機になっているセグメントも明確になりました。 2.5 生成AIによるペルソナ推定の可能性 従来のペルソナ設計は、マーケターやリサーチャーが定性調査の結果をもとに手作業で行うものでした。「30代共働き夫婦」「単身社会人」といった典型像を数パターン作り、それをチーム内で共有するのが一般的です。この手法には、作成者の主観やバイアスが入りやすく、またペルソナの数が限られるという課題がありました。 LLM によるペルソナ推定が変えること 今回の手法では、1000件の意見それぞれに対して LLM がペルソナを推定しています。つまり、1000人分の個別ペルソナが存在し、それをクラスタリングすることで「データから浮かび上がるペルソナ像」を得ています。これは従来の「仮説としてのペルソナ」とは根本的に異なるアプローチです。 この手法の特徴は以下の通りです。 網羅性 : 人手では見落としがちなニッチセグメント(例: ペット世帯、介護世帯)が自動的に浮かび上がる 粒度の柔軟性 : クラスタ数を変えることで、粗い分類(5タイプ)から細かい分類(20タイプ)まで自在に調整できる 再現性 : 同じ入力データに対して同じパイプラインを実行すれば、類似の結果が得られる スケーラビリティ : 1000件でも10万件でも、処理時間は増えるが手法は同じ 従来手法との組み合わせ LLM によるペルソナ推定は、従来の定性調査を代替するものではなく、補完するものです。LLM が推定したペルソナクラスタを「仮説」として使い、その仮説を実際のインタビューやアンケートで検証するというワークフローが有効です。 例えば、今回の分析で「C3: ペットの毛対策」というクラスタが浮かび上がりました。従来のマーケティング調査では「ペット世帯」を独立したセグメントとして設計しない可能性がありますが、データからこのセグメントの存在が示唆されたことで、追加調査の方向性が明確になります。 応用の可能性 この手法は洗濯機に限らず、以下のような分野に応用できます。 商品開発 : 大量のユーザーフィードバックからペルソナを推定し、未充足ニーズを発見する コンテンツマーケティング : SNS上の投稿からペルソナを推定し、セグメント別のコンテンツ戦略を設計する カスタマーサポート : 問い合わせ内容からペルソナを推定し、FAQ やチャットボットの応答をセグメント別に最適化する 政策立案 : 市民の意見データからペルソナを推定し、政策の影響を受けるグループを事前に把握する ペルソナ推定の自動化により、「誰の声を聞いているのか」を構造的に理解できるようになります。これは意見データの分析精度を上げるだけでなく、施策の優先順位付けにも直結する重要な技術です。 3. マーケティング検証 ペルソナが推定できたので、次はそのペルソナを使って 具体的な製品に対するマーケティング検証 を行います。対象製品は 乾燥機付きドラム洗濯機 BD-STX130M (洗濯13kg/乾燥7kg)です。 (※特定の製品の方が説明がしやすかったので、製品を限定しましたが、本来の分析としてはどのような製品でも問題ありません) 主な特徴: - 乾燥フィルターレス + 3つの自動おそうじ(洗濯槽・乾燥経路・ドアパッキン) - ヒートポンプ式「らくはや 風アイロン」乾燥(洗濯〜乾燥7kg 約93分) - ナイアガラ循環2段シャワー(高濃度洗剤液で洗浄) - 省エネ効果: 消費電力約26%減、水量約24%減 3.1 アンケート分析(1000件) 1000件の各ペルソナに対して、洗濯機購入時に重視する10項目の重要度(1〜5のスケール)を LLM で回答させました。 アンケート項目と全体平均 # 項目 全体平均 Q1 本体価格の安さ(初期費用) 2.87 Q2 ランニングコストの低さ(電気代・水道代) 3.99 Q3 洗浄力(汚れ落ち) 4.33 Q4 乾燥の仕上がり(生乾き・シワの少なさ・ふんわり感) 4.77 Q5 洗濯〜乾燥までの所要時間(時短) 4.83 Q6 静音性・振動の少なさ 4.13 Q7 お手入れの楽さ(フィルター掃除・槽洗浄など) 4.16 Q8 容量(洗濯kg・乾燥kgの大きさ) 3.91 Q9 設置・使い勝手(サイズ・操作性など) 4.21 Q10 信頼性・サポート(故障しにくさ・ブランド安心感) 4.34 Q5(時短)が4.83で最も高く、Q1(本体価格)が2.87で最も低い という結果は、乾燥機付きドラム洗濯機のユーザーが「価格よりも機能・時短」を重視していることを示しています。 クラスタ別の特徴的な差異 クラスタ別 洗濯機購入価値観ヒートマップ まず前提として、Q4(乾燥の仕上がり)と Q5(時短)は 全クラスタで4.4〜4.9の高スコア です。これは乾燥機付きドラム洗濯機を検討する層に共通するニーズであり、クラスタ間での差が小さい項目です。 一方で、Q3(洗浄力)、Q8(容量)、Q10(信頼性)などはクラスタ間で0.5〜1.0以上の差があり、ペルソナごとの特性が反映されています。以下の表では、 他クラスタとの比較で特徴的な項目 を取り上げています。 クラスタ 他クラスタと比較して特徴的な傾向 C0(シニア) Q5(時短)が4.4で全クラスタ中最低 。退職後で時間に余裕があるため、時短への切迫感が薄い。一方 Q10(信頼性)は4.6 で、ブランド安心感を相対的に重視する C1(単身若手) Q8(容量)が3.4で全クラスタ中最低 。一人暮らしでは大容量は不要。また Q1(本体価格)が3.2で全クラスタ中最高 で、価格感度が最も高い C3(ペット層) Q3(洗浄力)が4.6で全クラスタ中トップタイ 。ペットの毛・汚れへの強いニーズ。また Q1(本体価格)が2.6で全クラスタ中最低 で、価格より機能を優先する傾向 C4(花粉アレルギー) Q4(乾燥の仕上がり)が4.9で全クラスタ中最高 。花粉対策として「確実に室内で乾く」仕上がり品質を最重視 C8(介護) Q10(信頼性)が4.8で全クラスタ中最高 。介護中の故障は致命的なため、信頼性・サポートを最重視。 Q3(洗浄力)も4.6でトップタイ C9(子育て期) Q8(容量)が4.5で全クラスタ中最高 。大家族のまとめ洗い需要が反映されている クラスタ別 洗濯機購入価値観アンケート(平均値) レーダーチャートを見ると、全クラスタで「Q4 乾燥の仕上がり」「Q5 時短」が突出して高い一方、「Q1 本体価格」だけが全体的に低いことがわかります。これは乾燥機付きドラム洗濯機のユーザーが、価格よりも機能性を重視する層であることを裏付けています。 注目すべきは、Q4・Q5 のような「共通して高い項目」ではなく、Q3・Q8・Q10 のような「クラスタ間で差が開く項目」です。これらの差分こそが、ペルソナごとに異なるニーズを示しており、マーケティング訴求を出し分ける根拠になります。 3.2 製品購買判断(1000件) 各ペルソナに対して、BD-STX130M の製品情報を提示し、「この製品を購入するか(Yes/No)」を判断させました。 BD-STX130M 購入判断(1000件集計) 全体結果: 購入する 719件(71.9%)、購入しない 281件(28.1%) クラスタ別 BD-STX130M 購入率 クラスタ別購入率 クラスタ ペルソナ 購入率 C9 子育て期のひとり親多世代の時短洗乾燥需要層 87% C3 ペットの毛対策と時短を求める30〜40代層 85% C8 介護で増える汚れ物を夜間に洗乾完結したい層 81% C2 夜間運転の静音性と花粉冬対策重視層 80% C6 子育て共働きの夜洗濯乾燥で朝支度時短層 78% C4 花粉アレルギー対策で外干し回避する家庭 68% C7 花粉外干し回避と夜洗乾の共働き層 65% C5 賃貸集合住宅で夜回し時短と節約両立層 64% C0 退職後シニアの省力家事と外干し回避ニーズ 59% C1 単身若手の時短家事自動化ニーズ層 50% 購入率が高いクラスタ(80%以上)の共通点: - 洗濯量が多い (子育て・介護・ペット) - 乾燥フィルターレスの価値を感じやすい (毛・汚れ・手入れ負担の軽減) - 13kg大容量が活きる 購入率が低いクラスタ(50〜64%)の共通点: - C1(単身若手): 価格帯が高すぎる&大容量がオーバースペック - C0(シニア): 操作の複雑さやサイズへの不安 - C5(賃貸): 設置スペースの制約 3.3 購入/非購入理由のクラスタリング(1000件) 購買判断の「理由」そのものもテキストデータとして価値があります。1000件のペルソナそれぞれについて購入/非購入の理由を LLM で生成し、その理由テキストを embedding → クラスタリングして 10個の理由クラスタ に分類しました。 理由クラスタはペルソナクラスタ(C0-C9)と区別するため、 R0-R9 と命名しています。 BD-STX130M 購入/非購入理由 クラスタ散布図 BD-STX130M 購入/非購入 散布図 散布図を見ると、右側に固まっている赤い点(非購入)のクラスタと、左側に広がる緑の点(購入)のクラスタがはっきり分離しています。 理由クラスタ一覧 理由クラスタ別 購入/非購入比率 ID ラベル 購入率 概要 R0 三世代の大量洗濯を大容量洗乾で時短 100% 三世代同居・共働きの大量洗濯に13kgが活きる R1 日立指名の信頼感で選ぶ時短省エネ乾燥 62% 日立ブランドで購入する層がいる一方、パナ/東芝指名で見送りも R2 花粉寒冷地の外干し回避で夜洗乾時短 100% 花粉・雪・梅雨で外干し不可。室内完結で解消 R3 夜回して朝乾く93分洗乾と手入れ軽減 100% 93分の時短+フィルターレスで手入れも楽 R4 夜回し中心の時短洗乾と手入れルール化 99% 自動投入・通知で家事をルール化。振動は懸念 R5 高価格大容量で設置騒音不安の見送り 0% 非購入理由が集中。価格高・サイズ大・騒音不安 R6 大家族の大量洗濯を夜に洗乾で時短完結 100% 体操服・寝具のまとめ洗い。梅雨・花粉対策 R7 介護で毎日洗乾を時短完結し手入れも省力化 100% 介護の汚れ物に大容量+自動おそうじが有効 R8 時短段取り化と省エネで洗乾ストレス軽減 100% 共働き・介護の段取り化。賃貸では設置・振動が懸念 R9 ペット毛とアレルギー対策の洗乾完結需要 99% ペット毛・花粉対策で室内完結。フィルターレスで集約 3.4 マーケティング検証からわかったこと BD-STX130Mの強み(購入理由の分析から) 「93分で洗乾完結」の時短訴求が最も刺さる (R3, R4)。夜回して朝に取り出す生活パターンと完全に合致 乾燥フィルターレスが差別化要因 (R3, R7, R9)。特にペット毛や介護の汚れ物で手入れ負担が減る点が高評価 13kg大容量が大家族・三世代に有効 (R0, R6)。体操服・寝具のまとめ洗いに対応 花粉・寒冷地対策として「確実に乾く」安心感 (R2)。外干し回避のニーズに合致 BD-STX130Mの課題(非購入理由の分析から) R5(購入率0%)に非購入理由が集中 : 本体価格が高い(上位機のため) 幅630×奥行720mm のサイズが賃貸に収まらない 夜間運転の騒音・振動の不安(スペック上の根拠が弱い) 単身・夫婦には13kgがオーバースペック R1(購入率62%)にブランド競合の影響 : パナソニック・東芝・シャープの指名買い層では決め手に欠ける 他ブランドの独自機能(除菌・空気ケアなど)との比較 ペルソナ×購買判断のクロス分析 アンケート結果と購買判断を重ねると、マーケティング施策のヒントが見えます。 ターゲット 購入率 施策の方向性 C9 子育て期(87%) 既に高い 「13kg大容量+93分」の時短訴求を継続 C3 ペット層(85%) 既に高い 「フィルターレスで毛が集約」をペット向けに訴求 C1 単身若手(50%) 改善余地大 小容量モデルの展開 or コストパフォーマンス訴求 C0 シニア(59%) 改善余地あり 操作の簡便さ・ブランド信頼性の訴求 C5 賃貸(64%) 改善余地あり 設置サイズの明示・防振対策の訴求 3.5 推定ペルソナに対するアンケート調査のメリット 今回の分析では、LLM で推定した1000件のペルソナに対してアンケートや購買判断を「回答させる」という手法を用いました。この「AIペルソナへの調査」は、従来のアンケート調査とは異なるメリットを持っています。 従来のアンケート調査の課題 従来の消費者アンケートには、以下のような構造的な課題があります。 コスト : パネル調査やインタビュー調査には1件あたり数百円〜数千円のコストがかかる。1000件のアンケートを実施するだけで数十万円〜数百万円の費用が発生する 時間 : 調査設計・配信・回収・集計に数週間〜数か月を要する 回答バイアス : 「社会的に望ましい回答」をする傾向や、報酬目的の不誠実な回答が混入する セグメント不足 : 特定のニッチセグメント(介護世帯、ペット世帯など)の回答者を十分に集められない AIペルソナ調査のメリット 推定ペルソナに対する調査は、これらの課題を以下のように緩和します。 高速な仮説検証 : 調査設計からクラスタ別集計まで、数時間で完了する。「この訴求はどのセグメントに刺さるか」という仮説を即座にテストでき、アンケート項目や訴求文言の試行錯誤が容易になる セグメント別の深堀り : 1000件のペルソナがクラスタに分類されているため、「C3(ペット層)は洗浄力をどれだけ重視するか」といったセグメント別分析が自動的に得られる。従来の調査ではクロス集計のために追加のサンプル数が必要だった 製品コンセプトの事前スクリーニング : 実際の製品を消費者に提示する前に、AIペルソナに対して複数の製品コンセプトを評価させることで、有望なコンセプトを絞り込める。今回の BD-STX130M の購入率が50%〜87%とペルソナクラスタごとに大きく異なったことは、ターゲティングの精度を上げるための有力な示唆となった 非購入理由の構造化 : 従来の調査では「なぜ買わないか」を自由記述で聞いても、回答の質にばらつきがある。AIペルソナはペルソナの文脈に基づいた具体的な理由を生成するため、理由のクラスタリング(R0-R9)のような構造化分析が可能になる 注意点と限界 AIペルソナへの調査は、あくまで「LLM が推定した消費者像に基づくシミュレーション」であり、実際の消費者の回答とは異なる可能性があります。特に以下の点に注意が必要です。 LLM は学習データに含まれる消費者行動パターンをもとに回答を生成するため、新製品カテゴリや革新的な機能に対する反応は実態と乖離する可能性がある 価格感度やブランド選好は、地域・時期・経済状況によって変動するため、LLM の回答が現在の市場を正確に反映しているとは限らない 回答の分布が実際の消費者調査と一致するかどうかは、別途検証が必要である 実務での位置づけ AIペルソナ調査は、従来の消費者調査を「代替する」ものではなく、「準備段階で仮説を磨くためのツール」として位置づけるのが適切です。具体的なワークフローとしては以下が考えられます。 AIペルソナ調査で仮説を構築(どのセグメントに何を訴求するか) 仮説に基づいて本調査のアンケート設計を最適化(無駄な質問を減らし、深堀りすべきポイントを絞る) 本調査の結果とAIペルソナ調査の結果を比較し、AIペルソナの精度を検証・改善する このサイクルを回すことで、調査の精度と効率を同時に向上させることが可能になります。 4. まとめ 本記事では、ブロードリスニングとペルソナ推定分析を組み合わせて、乾燥機付きドラム洗濯機の消費者インサイトを3段階で深掘りしました。 Step 1: 意見のクラスタリング → 「何が語られているか」 1000件の意見から5,447件の論点を抽出し、15個のクラスタに分類。 時短・外干し不要の満足 と 電気代・騒音・手入れの不満 という二極構造が明確になりました。 Step 2: ペルソナのクラスタリング → 「誰が語っているか」 同じ意見をペルソナ視点で再分析し、10タイプのペルソナに分類。 同じ「時短」ニーズでも、単身若手・子育て共働き・介護世帯ではその意味と優先度が異なる ことが可視化されました。 Step 3: マーケティング検証 → 「どう製品を届けるか」 推定したペルソナに対してアンケート・購買判断・理由分析を実施。 全体の71.9%が購入意向を示す中、ペルソナごとに50%〜87%の幅 がありました。購入率の高いセグメント(子育て期・ペット層・介護世帯)への訴求強化と、低いセグメント(単身若手・賃貸)へのアプローチ改善という具体的な施策の方向性が得られました。 ブロードリスニングの手法を使えば、大量の消費者の声から 構造的なインサイト を効率的に抽出し、ペルソナ推定と組み合わせることで 誰に・何を・どう伝えるか というマーケティング戦略に直結する分析が可能になります。
動画
該当するコンテンツが見つかりませんでした














