Skip to content

Build1 publisher3 min readPublished

A million-element IN clause MySQL absorbed took Mercari's TiDB cluster down with it

Mercari's DBRE team found the query only after switching a batch read endpoint to TiDB. Query replay had run first and had not flagged it.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

Illustration accompanying A million-element IN clause MySQL absorbed took Mercari's TiDB cluster down with it
Generated illustration

What happened

  • Mercari completed a migration of its CoreDB from MySQL to TiDB, and its DBRE team is publishing three articles on improvements made through the end of March 2026.
  • The incident was discovered immediately before Mercari's largest cluster switchover, during a period when MySQL and TiDB were operated in parallel and endpoints for some queries had already been switched to TiDB ahead of the rest.
  • After switching the batch endpoint from MySQL to TiDB, there were periods every hour in which query speed degraded markedly.
  • The planned cutover order was: read endpoints for OLAP traffic, then read endpoints for OLTP traffic, then write endpoints.
  • At the time the problem occurred the team was executing step 1, specifically switching a comparatively high-load read endpoint called the batch endpoint from MySQL to TiDB.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

Mercari's Database Reliability Engineering team moved a high-load batch read endpoint from MySQL to TiDB and then watched query speed degrade sharply on an hourly cycle, with MySQL-to-TiDB sync lag and commit durations rising at the same moments [3][6][7]. The cause was a single query whose Index Join generated an internal IN list of close to one million elements, and the damage did not stay inside that query [8][11].

That last point is the operational one. Normally a bad query hurts itself and its neighbours; here reads and writes unrelated to the offending statement slowed too, which is why the team treated it as a cluster stability problem rather than a tuning ticket [9][10]. TiDB Cloud support traced it to Protobuf deserialization in TiKV's gRPC threads: the oversized IN clause monopolised those threads, so every subsequent request sharing them queued, and cluster latency spiked [12].

The timing was unhelpful. The incident surfaced immediately before Mercari's largest cluster switchover, during a period when MySQL and TiDB ran in parallel and some endpoints had already been pointed at TiDB [2]. The cutover plan had three steps in order: OLAP read endpoints, OLTP read endpoints, then writes [4]. The team was still on step one [5], which leaves two of the three phases ahead of them [20]. And before switching, they had replayed ProxySQL's live MySQL traffic against TiDB with an in-house tool, checking both query compatibility and TiDB load, and fixed what that found [c5a]. The post says it will explain why replay did not catch this one; the published excerpt stops before the explanation [19].

Containment was fast because it had been planned for. The team read the slow query list, identified the query, and used ProxySQL's mysql_query_rules to route that one query digest back to MySQL, an operation they had anticipated in the migration plan and still use [13][14]. Two caveats worth copying into your own runbook: you cannot redirect a single query inside a transaction this way [15], and ProxySQL's digests are not compatible with those from pt-query-digest or TiDB, so you have to read the digest out of stats_mysql_query_digest and paste that exact value into the rule [16].

PingCAP's first-aid was to cap scan-range memory with tidb_opt_range_max_size = 1048576, which is one mebibyte [17][21], and to reduce Index Join parallelism with tidb_index_lookup_join_concurrency = 2, a variable now deprecated in favour of tidb_executor_concurrency [17]. The team also priced up resource control to deprioritise the query and scaling TiKV up, and concluded that all of the no-code-change workarounds contributed only marginally [c18a][18]. The actual fix was to stop shipping a million IDs through gRPC at all: store them in a temporary table instead of embedding them in SQL [c18b]. That required application work, which meant interrupting a product team's priorities with no guaranteed completion date [c18c].

Reproducing the problem cost real effort too. Building an independent cluster carrying the same data hit resource exhaustion, repeated restore failures against bandwidth quotas, and a data pipeline that fell behind while catching up, because a new TiCDC changefeed's load was not isolated from the changefeed already in production [22][23].

Watch for the follow-up posts: the changefeed isolation writeup, and whether the OLTP read and write cutovers turn up more query shapes that only misbehave at production scale.

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories