メルカリにおけるTiDB改善の取り組み:IN句内100万件クエリへの対応

先日、メルカリのCoreDBをMySQLからTiDBへ移行した振り返り という記事で、MySQLからTiDBへの移行完了に関する報告と、大枠の振り返りについてお伝えしました。これから3つの記事にわたって、私たちDBRE(Database Reliability Engineering)チームが、2026年3月末までに行った改善について紹介します。

1つ目の本記事では、メルカリの最も大きなクラスタ切り替え直前に発覚した、IN句に多数の要素を含むクエリがTiDBクラスタ全体のレイテンシを悪化させた事例を紹介します。この事例は、MySQLとTiDBを並行運用し、一部のクエリに対して先行してエンドポイントをTiDBへ切り替えていた期間に発生しました。

私たちDBREチームは、ProxySQLによる一時的な切り戻し、TiDB Cloudサポートとの原因調査、Temporaryテーブルを使ったアプリケーション改修を通じて、この問題に対処しました。本記事では、巨大なIN句が分散データベースに与えた影響、リプレイ検証で事前に検出できなかった理由、そしてTemporaryテーブルを導入する際に注意すべきポイントを共有します。

問題発生時の状況

様々な事前の動作確認を終え、全体として下記の順序で順にMySQLからTiDBへ切り替えを行う計画でした。

  1. 読み取りエンドポイント(OLAP: Online Analytical Processing トラフィック)の切り替え
  2. 読み取りエンドポイント(OLTP: Online Transaction Processing トラフィック)の切り替え
  3. 書き込みエンドポイントの切り替え

問題が発生した時点では、私たちは手順1を実施していました。具体的には、読み取りエンドポイントのうち、比較的高負荷なトラフィックを含むエンドポイント(バッチエンドポイントと呼びます)をMySQLからTiDBへ切り替えていました。切り替えの前には、ProxySQL経由でMySQLに流通するトラフィックを内製ツールによるクエリリプレイによりTiDBへ発行し、OLAPエンドポイントに関してもクエリの互換性の問題や、TiDBの負荷のチェックを事前に行い必要な問題には対処していました。

切り替え構成図

また、同時にMySQLからTiDBへ切り替える前に、別のブログ記事で紹介した大量のデータ削除ツールを利用して、1日1億件以上のデータ削除を実行していました。

発生した問題 – クラスタ全体の遅延

バッチエンドポイントをMySQLからTiDBへ切り替えた後、毎時クエリ速度が著しく低下する時間が発生しました。

クエリ速度低下のグラフ

また、MySQLからTiDBへの同期遅延が、このレイテンシ拡大と同じタイミングで定期的に発生していることを観測しました。あわせて、更新のCommit所要時間も同時に上昇していることを確認しました。

この事象の特筆すべきことはクエリの速度の低下が、特定のクエリにとどまっておらず、クラスタ全体に波及していることです。

通常、特定のクエリに問題がある場合、その影響は当該クエリや関連する処理に閉じることを期待します。しかし今回の事象では、原因となったクエリ以外の読み書きにも遅延が波及していました。この点が、単なるクエリチューニングではなく、クラスタ全体の安定性に関わる問題として扱う必要があった理由です。

初期の対応 – 原因となっているクエリの特定/特定クエリの切戻し

遅延の発生にデータ削除は関係ないと考えられた一方で、まずはそれを一時的に停止した後、何がこの事象を発生させているのかの調査を行いました。

スロークエリのリスト精査により、原因となるクエリを直ちに特定し、まずは影響を一時的に抑えるためにProxySQLで「当該クエリのみ」再度MySQLへルーティングするように変更することとしました。この「問題あるクエリを特定し、そのクエリ(クエリダイジェスト)のみ実行先を変更する」オペレーションは、移行の計画の中である程度想定されていたオペレーションで、現在も有効に活用されています。

この一部のクエリに対して、実行先のホストを変更する処理は、ProxySQLの mysql_query_rules というテーブルに対して所定のルールを記述することで実現できます。

ProxySQLによるクエリダイジェストルーティング

なお、この機能でトランザクション中のあるクエリだけ実行先ホストを変える、ということはできません。

また余談となりますが、ここで利用しているクエリダイジェストとは、SQL文からリテラル値(数値や文字列などの具体的な値)をプレースホルダに置き換え、空白やコメントを正規化した「クエリの型」と、それをもとに計算されるハッシュ値のことです。たとえば SELECT * FROM items WHERE id = 123SELECT * FROM items WHERE id = 456 は、正規化するとどちらも SELECT * FROM items WHERE id = ? となり、同一クエリ(同一クエリダイジェスト)とみなせます。値だけが異なる同型のクエリを1つのグループとして扱えるため、同じ種類のクエリの実行状況の集計や、クエリのルーティングの単位として利用できます。このProxySQLで利用するダイジェストは、pt-query-digest といった他のツールで生成されるダイジェストやTiDBで生成されるものと互換がないものとなります。ProxySQL内でクエリがどのようなダイジェストで表現されているかを確認し、ルールを設定する必要があります。ProxySQLが認識しているダイジェストは、Adminインターフェースの統計テーブル stats_mysql_query_digest から取得できます。digest_text(正規化後のクエリ文字列)で対象のクエリを探し、対応する digest カラムの値を mysql_query_rules に設定する、という流れになります。

根本原因の調査 – Protobufのデシリアライズが問題と特定

特定のクエリをMySQLで処理するように切り戻したことで、ユーザー影響を抑えながら調査を継続できる状態になりました。しかし、特定クエリをMySQLへ戻すだけではTiDB移行を完了できませんので、続いてTiDB Cloudのサポートと連携し、このクエリがなぜクラスタ全体に影響したのかを調査しました。

最初はどのクエリが問題かに対する認識をメルカリとPingCAP社で合わせ、「当該クエリの実行時に Index Join の内部処理で生成される IN リストが 100 万件近くに達している」という事実を確認しました。また、当該クエリは結合条件を含む大量の行を処理するものだったため、あわせてPingCAP社からは、クラスタ全体がダウンするリスクを抑えるとともに、当該クエリの並列度を下げて処理を平滑化する1次対処の提案を受けました。具体的には、スキャン範囲のメモリ上限を制御する tidb_opt_range_max_size と、Index Joinの並列度を制御する tidb_index_lookup_join_concurrency の調整です。(現時点ではdeprecatedで tidb_executor_concurrency を代わりに利用することが推奨されています)

-- クラスタの安定性の確保
SET GLOBAL tidb_opt_range_max_size = 1048576;
-- 並列度抑制
SET SESSION tidb_index_lookup_join_concurrency = 2;

続いて調査が進み、根本原因はTiKVのgRPCスレッドにおけるProtobufデシリアライズ処理にあることが分かりました。

https://github.com/pingcap/tidb/issues/55845

TiDBのTiDB – TiKV間を含めたノード間の通信はgRPCで行われているのですが、大きなIN句の発行によりTiKVのgRPC スレッドが Protobufのデシリアライズ処理に過度に占有され、その結果、同じ gRPCスレッドを使用する後続のすべてのリクエスト(他の読み書き操作)に待ち状態が発生し、クラスター全体のレイテンシが急上昇する結果となっていたのです。

問題解決への対処 – Temporaryテーブルの活用

根本原因の特定に至るまでの間、私たちは下記に記載したアプリケーションの改修が不要な対処候補を事前に検討していました。

  1. リソースコントロールにより該当クエリの優先度を下げる
  2. TiKVをスケールアップする
  3. TiDBのシステム変数を調整する(tidb_opt_range_max_size, tidb_index_lookup_join_concurrency)

一方、根本対処としてはPingCAP社から「100万件のIN句の要素をTemporaryテーブルに保存し、SQLとして渡さないようにする」方法の提示を受けました。gRPCスレッド経由で実際の100万件のIDが渡されなくなる、ということで、根本的かつ実効性が見込める改修手段です。

IN句からTemporaryテーブルへの変更イメージ

しかし、SQLの修正やロジック修正を含むアプリケーション改修は、開発チームの現行の優先度に割り込んで変更を実施する必要があり、直ちに完了する保証がなかったため、その他のワークアラウンドに対しても、実際にどの程度の効果が見積もれるかを、検証・確認しました(結果的には1-3のアプリケーション改修を行わない暫定対処の緩和策は問題解決への寄与は限定的という結論でした)。

この問題に対処しないままMySQLからTiDBへ切り替えることは困難でした。TiDB Cloudのサポートチームには、調査と対策の検討において多大な協力をいただきました。

再現環境の構築と改修

本番と同じようなクエリが流れている状況で再現・改修確認を行うため、本番のクラスタとは独立した同じデータが流通するクラスタを作成することにしました。

この再現環境の構築では、次のような問題が発生しました。

  1. リソース枯渇問題がありクラスタが作成できない
  2. クラスタ作成後のリストア処理で、帯域のQuota制限などでリストア処理が何回も失敗
  3. リストア後のデータが最新データに追いつく際にデータパイプラインに遅延が発生

再現環境構築時の構成図

特に3のデータパイプライン遅延については大きな問題でした。新規にTiCDC(TiDBの変更データをダウンストリームへ同期するコンポーネント)の同期タスクであるChangefeed 2を作成し、データを下流のTiDBに伝播させようとしたところ、データパイプラインの負荷が隔離されないケースがあり、結果として既存の運用中であるChangefeed 1の同期に影響が発生しました。本事象の詳細とその解決については、TiDB関連のデータパイプラインをまとめたブログ記事を別途作成し、そこで紹介する予定です。

改修確認 – 消えたクエリ

実環境でクエリの修正により問題が解消したことを確認する際には、前提条件として事前に改修前後のクエリを新しい検証用のクラスタで実行し改善を確認していました。しかし、開発環境でリリースし、いざ本番環境でリリースしたところ、当初クエリの存在が確認できませんでした。

旧MySQLのバッチエンドポイントにクエリが発行されている可能性、などを調査しましたが、見つからず… 検証用TiDB、切り替え用TiDBにも見つからず、最終的には、書き込みエンドポイント向けにクエリが発行されていることが確認できました。これは、クエリ改修時に、Temporaryテーブルの作成およびTemporaryテーブルへの挿入、といった処理がアプリケーションで読み取りエンドポイントへの発行が禁止されており、結果として意図せぬ発行先に(MySQLのWriterへ)クエリが発行されることとなっていました。これは、アプリケーションがReplicaに書き込みを行うとデータ不整合を起こす可能性があるため、ReplicaへのINSERTを禁止するテストが存在していたためです。検証用に独立したクラスタを準備したにもかかわらず、結果的に本番環境への影響リスクを十分に排除できていませんでした。

開発環境と、本番環境でエンドポイントの構成に差分があったこともこの事象が発覚しなかった原因の一つでした。

最終的には、再度アプリケーションを改修し、バッチエンドポイントに対して、Temporaryテーブルの処理をするようになり、TiDBで問題なく動作することが確認できました。

なお、このIN句の要素をTemporaryテーブルとして渡すようアプリケーションを改修する対処が必要になったのはメルカリではこのケースを含めて2件のみです。

事後振り返り – リプレイで検出できなかったクエリ

本バッチエンドポイントに対しても切り替え前に、事前にクエリリプレイによる負荷の再現検証を行なっていました。このエンドポイントに対しては、数時間に数個の取りこぼしがあったものの、ほぼ100%のクエリはリプレイできていました。しかしながら当該クエリについては、切り替え前の検証時点では、(a)ツールが「取りこぼし」として検知・報告した漏れに含まれていたのか、あるいは(b)そもそもツールが取りこぼしとしても検知できない形で漏れていたのかが判別できませんでした。結果として、実際にエンドポイントを切り替えるまで、このクエリがもたらすリスクを検出できませんでした。

本問題対処完了後に、パケットのキャプチャサイズの上限や、パケットのフラグメンテーションなどの扱い、その他の可能性を事後に検討し、なぜリプレイできなかったか、振り返りを行いましたが、解明には至りませんでした。再現は容易なので解決できない問題ではないと思うのですが、一定の時間をかけて解決しなかったので、現時点では追加調査を保留しています。

Temporaryテーブル導入時のポイント

最後にTemporaryテーブルを導入する際の細かいポイントをいくつかまとめます。実際に類似事例に対処が必要になった際に、考慮すべき事項としてご活用ください。

Temporaryテーブルへのデータ挿入は大胆に

TiDBのTemporaryテーブルは、メモリ上で処理が完結します。

通常は、データの更新は大量の更新を一括で行うと、データ同期の遅延などの可能性があるため、少しずつ更新/挿入したり、トランザクションサイズの制約があります。しかし、ローカルTemporaryテーブルを利用すると接続しているTiDBのメモリ上に全データが格納されるため、100万件のデータ挿入であっても遅延発生の心配はなく、データを過度に分割して挿入したり挿入の間にsleepを入れたりする必要はありません。

Temporaryテーブル作成権限付与忘れに注意

Temporaryテーブルの作成には、当該ユーザーに所定の権限(CREATE TEMPORARY TABLE 権限)が必要です。MySQLでもTiDBでも同じ権限が必要です。

https://dev.mysql.com/doc/refman/8.4/en/privileges-provided.html#priv_create-temporary-tables

Temporaryテーブルのサイズ制限を確認する

すべてのデータをメモリで扱うという都合上、サイズの制約を通常より気にする必要があります。

idのみを一時的に保持するというユースケースでは、100万件レベルでは通常特に問題にならないことが多いと思われます。TiDBではTemporaryテーブルサイズの上限はデフォルトでは64MBで tidb_tmp_table_max_size 変数でセッション単位でも変更可能ですので必要に合わせて設定をしてご利用ください。

Temporaryテーブルへの読み書きが読み取りノードでできるかに注意

Temporaryテーブルの作成、Temporaryテーブルへのレコード挿入は一種の書き込みクエリですが、読み取りノードに対しても発行可能です。一方で、アプリケーションやライブラリの作りにより、読み取りノードにはTemporaryテーブルを含めた書き込みクエリを発行できない、書き込みエンドポイントへクエリを自動的に発行するといった挙動になっていることがあります。このような制約の影響でクエリ発行先が予期せず変わらないように注意が必要です。

MySQL / TiDB間の互換についての考慮

MySQLからTiDBへの移行期間中であれば、MySQLとTiDBの両方のデータベースで動作させる可能性があり、双方で動作するように互換性を考慮する必要があります。

下記の場合に、SQLレベルで非互換となりアプリケーションによりロジックを分岐する必要が発生する場合があります。

  • MySQLにおいて binlog_format=STATEMENT で運用している場合
    • Temporaryテーブルへの操作によってGTID(Global Transaction Identifier)と呼ばれるIDが発行され、これにより不整合の可能性として記録されます。
  • 一時テーブルのサイズが個別に指定が必要な場合

まとめ

本記事では、MySQLからTiDBへの切り替え前に取り組んだIN句に100万件の要素を含むクエリがクラスタ全体に影響を与えた問題とTemporaryテーブルによる根本対処の方法、そして実際にTemporaryテーブルを利用しアプリケーションを改修する際の注意点についてお伝えしました。TiDBのリリース予定日は、外部に切り替え日を連絡済みだったり、万が一のためCXOの予定を押さえたりしている中で、再現試験環境を作成するための比較的時間のかかるリストアが繰り返し失敗するなどといったこともあり、とても印象深い事例でした。クエリリプレイなどを行っていたにもかかわらず、リプレイでは事前の問題抽出ができませんでしたが、バッチエンドポイントの先行切り替えにより、若干早く問題を発見できたため結果的に当初予定通りの切り替えが達成できました。

本記事ではメルカリが2026年3月末までに行ってきた改善を3つに分けた1つ目を紹介しました。続いて2つ記事を公開しますので、引き続きご期待ください。

  1. IN句内に多数の要素を含むSQLで全体影響が発生した事例共有(本記事)
  2. 次の改善事例の共有
    1. リソースグループの不具合に対する対応
    2. リージョン数の調整
    3. プランキャッシュの導入/調整
    4. In Memory Engineの導入
  3. MySQL/TiDBにおけるインデックス非互換への対応

おしらせ

最後に、現在メルカリでは、この記事の発行者の所属する DBREチーム の EM(Engineering Manager)および、IC(Individual Contributor)を募集しています。

この記事を読んで興味を持たれた方は、その旨をお知らせください。

詳しくは

をご覧ください。

また、メルカリにおけるTiDBやデータベース関連の取り組みについては、TiDB関連の記事一覧データベース関連の記事一覧もぜひご覧ください。

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加