---
title: "MySQL 1146「#sql... doesn't exist」を追って、複数参照CTEとTempTableに行き着いた"
slug: "mysql-1146-cte-temptable-investigation"
description: "MySQL 1146「Table '#sql...' doesn't exist」を、複数参照CTEとTempTable fallbackの観点から切り分け、有力な原因仮説と回避策を整理します。"
publishedAt: "2026-07-17"
updatedAt: null
status: "published"
category: "engineering"
tags:
  - "mysql"
  - "aurora-mysql"
  - "database"
  - "debugging"
  - "cte"
  - "incident-investigation"
articleType: "technical-note"
source: "manual"
publicRisk: "medium"
reviewNotes: "公開情報とPoCで確認できた範囲を観測事実・原因仮説・未確定事項に分け、SQL例と対照実験を技術レビュー済み。"
series: null
episode: null
audience: "engineers"
lang: "ja"
aiAssisted: true
humanReviewed: true
canonicalUrl: null
coverImage: null
external:
  qiita: null
  zenn: null
  note: null
  x: null
  youtube: null
  podcast: null
---

## 先に結論: materializeされた複数参照CTEとTempTable fallbackを確認する

MySQL 1146 `Table './tmp/#sql...' doesn't exist` が断続的に発生し、次の条件が重なるなら、複数参照 CTE と TempTable fallback の組み合わせを確認する価値があります。

- 実テーブルではなく、`#sql...` という内部一時テーブルが見つからない
- 同じクエリでも単発では成功し、高並行時だけ失敗する
- 失敗しても retry では成功する
- 同じ CTE が複数回参照され、実行計画上で merge されず materialize・再利用されている

本番で観測した条件と再現 PoC は、materialize[^materialization] された複数参照 CTE が TempTable の RAM と mmap[^mmap] の上限を使い切り、InnoDB へ fallback[^fallback] する経路で 1146 が発生する、という説明と強く整合しました。

[mysql/mysql-server PR #672](https://github.com/mysql/mysql-server/pull/672) では、fallback 後も CTE clone[^cte-clone] が古い TempTable handler[^handler] を保持することが原因候補として説明されています。ただし、2026-07-17 時点で、対応する [MySQL Bug #112704](https://bugs.mysql.com/bug.php?id=112704) は `Verified`、PR #672 は `Open` です。本番の Aurora MySQL で同じ内部不整合が起きていたことまで確認できたわけではないため、この記事では公開情報と PoC に最も整合する原因仮説として扱います。

該当しそうな場合は、次の順序で切り分けます。

1. 実テーブルの削除や名前間違いなど、通常の 1146 を先に除外する
2. 対象クエリに複数参照 CTE がないか確認する
3. 実行計画や optimizer trace で、対象 CTE の materialization と再利用を確認する
4. `Created_tmp_disk_tables` だけでなく、Performance Schema と補助的な status counter を確認する
5. 緩和策として、TempTable 用の RAM と mmap-backed なローカルストレージに余裕を持たせる
6. 構造的回避策として、問題の CTE を単一参照化する

すべての MySQL 1146 がこの Bug に該当するわけではありません。また、高並行は Bug の必須条件ではなく、この事例で共有上限を消費して fallback を起こしやすくした条件です。ただ、上の特徴が揃う場合は、闇雲に chunk を小さくするより、CTE の参照構造と TempTable fallback を先に確認した方が調査を進めやすくなります。

以下では、この結論に至るまでに何を観測し、どの仮説を捨て、どう再現したのかを順に書きます。

## 背景: 本番バッチで MySQL 1146 が断続的に出た

ある日次集計バッチで、特定の集計だけが断続的に失敗していました。失敗していたのは `metric_a` の集計で、`metric_b` は成功していました。

エラーは MySQL 1146 でした。

```txt
Table './tmp/#sql...' doesn't exist
```

対象の処理は、tenant ごとに snapshot 時点の集計値を計算する `INSERT ... SELECT` です。Aurora MySQL v3 系の writer instance 上で動いていました。

ここで重要だったのは、失敗が恒常的ではなかったことです。クエリそのものが常に壊れているなら単発でも落ちるはずですが、実際にはそうではありませんでした。

## 最初の観察: 単発では成功し、並行時だけ失敗する

最初に見えた特徴はかなり偏っていました。

- 単発実行では成功する
- 高並行 worker で historical aggregation を流したときだけ多発する
- エラーは短時間に集中する
- retry すると最終的には成功する

つまり、永続的なデータ不整合というより、一過性の実行時問題に見えました。

この時点では、テーブルそのものが消えているというより、「その瞬間だけ内部一時テーブルを正しく開けていないのではないか」という見立てが自然でした。後から振り返ると、この観察はかなり重要でした。

## 最初の仮説: 一時テーブルが大きすぎるのではないか

対象クエリは、CTE、window function、`GROUP BY` を含む重めの `INSERT ... SELECT` でした。内部一時テーブルを使っていても不思議ではありません。

そのため、最初は「一時テーブルが大きすぎて、TempTable が苦しくなっているのではないか」と考えました。よくある話に見えたからです。

この仮説から、まずは chunk 分割を試しました。1 回あたりの処理量を減らせば、一時テーブルも小さくなり、安全側へ寄るはずだと考えたためです。

ただ、この仮説を単純に支持する結果にはなりませんでした。

## chunkを小さくした後、総失敗件数は増えた

期待としては、chunk を小さくすれば 1 回あたりの一時テーブルサイズは下がるはずでした。

ところが本番では再発し、小さめの chunk にした後は 1146 の総発生件数が増えました。

ここで比較できたのは総発生件数であり、SQL 実行回数を分母にしたエラー率ではありません。chunk を小さくすると同じデータ量を処理するための実行回数も増えるため、この観測だけで「1 クエリあたりの一時テーブルサイズは無関係だった」とまでは言えません。

それでも、「1 回あたりを小さくすればバッチ全体も安全になる」という単純な期待とは合いませんでした。サイズだけでなく、次の要素を含めて見直すきっかけになりました。

代わりに怪しくなったのは、こちらです。

- 一時テーブルの生成回数
- 高並行時の共有リソース競合
- クエリ構造そのもの

この観測はかなり大きかったです。「大きすぎるから危ない」という直感だけでは説明できず、実行回数と並行度を含む別の仮説へ進めたからです。後続の PoC で複数参照と fallback の条件を個別に変えたことで、初めて仮説を絞り込めました。

## 公開Bugを調べて、複数参照CTEにたどり着く

そこで、`#sql... doesn't exist` と CTE / TempTable の観点で公開情報を調べました。ここでたどり着いたのが [MySQL Bug #112704](https://bugs.mysql.com/bug.php?id=112704) です。

この Bug は、複数回参照される CTE で internal table not found が起きるという内容でした。2026-07-17 時点で確認した範囲では、この Bug は `Verified` のままで、修正提案の [mysql/mysql-server PR #672](https://github.com/mysql/mysql-server/pull/672) も `Open` の状態でした。

自分たちのクエリを見直すと、ちょうど複数参照される非再帰 CTE がありました。ただし、複数参照だけでは条件として十分ではありません。MySQL は CTE を外側のクエリへ merge する場合と、内部一時テーブルへ materialize する場合を選びます。[MySQL の CTE 最適化ドキュメント](https://dev.mysql.com/doc/refman/8.0/en/derived-table-optimization.html)にある通り、`DISTINCT`、集約、window function、`GROUP BY` などが merge を妨げ、materialize された CTE が複数参照されると一時テーブルが再利用されます。

対象 CTE には SELECT リスト内のサブクエリなど、merge を妨げる構造が含まれていました。別のクエリを切り分ける場合は、構造だけで決めつけず、`EXPLAIN FORMAT=JSON` と optimizer trace で materialization と再利用を確認します。optimizer trace では、`creating_tmp_table` と `reusing_tmp_table` が手掛かりになります。ここで初めて、

- 単発ではなく並行時に偏ること
- `#sql... doesn't exist` というエラー文
- 対象クエリが複数参照 CTE を含んでいたこと

が一本につながりました。

### materializeされる同じCTEを、INとJOINから参照していた

対象クエリは `INSERT ... SELECT`、window function、`GROUP BY` を含みます。複数参照の構造を簡略化すると、次のような形です。

```sql
WITH latest_snapshot_per_resource AS (
  SELECT
    r.resource_key,
    (
      SELECT MAX(s2.snapshot_id)
      FROM snapshots AS s2
      WHERE s2.resource_key = r.resource_key
        AND s2.captured_at <= :target_at
    ) AS snapshot_id
  FROM resources AS r
  WHERE r.tenant_key = :tenant_key
),
ranked_config AS (
  SELECT
    c.resource_key,
    c.config_value,
    ROW_NUMBER() OVER (
      PARTITION BY c.resource_key
      ORDER BY c.updated_at DESC
    ) AS row_num
  FROM configs AS c
  WHERE c.resource_key IN (
    SELECT resource_key
    FROM latest_snapshot_per_resource -- 参照1: INサブクエリ
  )
),
metric_rows AS (
  SELECT
    latest.resource_key,
    source.metric_key,
    config.config_value
  FROM metric_source AS source
  JOIN latest_snapshot_per_resource AS latest -- 参照2: JOIN
    ON latest.snapshot_id = source.snapshot_id
  LEFT JOIN ranked_config AS config
    ON config.resource_key = latest.resource_key
   AND config.row_num = 1
)
SELECT
  resource_key,
  COUNT(*) AS metric_a,
  MAX(config_value) AS latest_config_value
FROM metric_rows
GROUP BY resource_key;
```

`latest_snapshot_per_resource` は、一度は `ranked_config` の対象を絞るために、もう一度は `metric_rows` の JOIN 相手として使われています。`ranked_config` 自体も `metric_rows` から参照されるため、どちらの参照も実行上必要です。クエリ全体の重さだけを見ていた段階では、この「materialize された同じ CTE を 2 回参照している」という条件を見落としていました。

この時点で「TempTable を使っているらしい」から、「materialize される複数参照 CTE という構造が条件として重要なのではないか」へ、仮説の焦点が変わりました。

## Created_tmp_disk_tables を信じすぎてはいけなかった

この調査で一度かなり遠回りしたのが、`Created_tmp_disk_tables` の読み方です。

最初は、disk temp table が増えていないなら安全側なのではないか、と見ていました。ところがこれは誤りでした。[MySQL の内部一時テーブルに関するドキュメント](https://dev.mysql.com/doc/refman/8.0/en/internal-temporary-tables.html)と [MySQL Bug #98741](https://bugs.mysql.com/bug.php?id=98741) を読むと、TempTable の mmap 利用は `Created_tmp_disk_tables` に出ないことが分かります。

つまり、

- `Created_tmp_disk_tables` は InnoDB 側へ変換された internal on-disk temp table を主に見ている
- TempTable が mmap を使っただけでは、このカウンタは増えない
- `Created_tmp_files` は mmap 利用時にも増えうるが、TempTable 以外の一時ファイルも含む
- TempTable に絞るには Performance Schema の `memory/temptable/physical_ram` と `memory/temptable/physical_disk` を見る

ということでした。

調査では、負荷をかける前後で次の値を取得し、差分を見ました。

```sql
SHOW GLOBAL STATUS
WHERE Variable_name IN (
  'Created_tmp_tables',
  'Created_tmp_disk_tables',
  'Created_tmp_files'
);

SELECT
  EVENT_NAME,
  CURRENT_COUNT_USED,
  CURRENT_NUMBER_OF_BYTES_USED,
  HIGH_NUMBER_OF_BYTES_USED
FROM performance_schema.memory_summary_global_by_event_name
WHERE EVENT_NAME IN (
  'memory/temptable/physical_ram',
  'memory/temptable/physical_disk'
);
```

`Created_tmp_files` は補助的な変化を見るには使えますが、単独では TempTable の使用量や fallback を判断できません。`Created_tmp_disk_tables` が増えていないことだけでも、TempTable の mmap 利用や共有上限の余裕までは分かりません。Performance Schema の `physical_ram` と `physical_disk` を中心に、status counter と実行計画を組み合わせる必要がありました。

また、`HIGH_NUMBER_OF_BYTES_USED` はその負荷だけの消費量ではなく、サーバー起動または統計リセット後の高水位です。過去のピークを超えなければ前後で変化しません。比較するときは `CURRENT_NUMBER_OF_BYTES_USED` と併記し、テスト専用環境のベースラインを取る必要があります。

Aurora MySQL v3 の [TempTable behavior ドキュメント](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/ams3-temptable-behavior.html) でも、TempTable は共有 RAM 枠 `temptable_max_ram` を使い、次に mmap-backed なローカルストレージ枠 `temptable_max_mmap` を使うことが説明されています。Aurora MySQL v3.04 以降では、`aurora_tmptable_enable_per_table_limit=OFF` の場合、`tmp_table_size` は TempTable の per-table 制限としては効きません。

上限到達後の挙動は writer と reader で異なります。writer は InnoDB の内部一時テーブルへ変換できますが、reader は InnoDB fallback を使えず、条件によって `ERROR 1114` になります。この事例は `INSERT ... SELECT` を writer で実行していたため、以降の InnoDB fallback の説明も writer を前提にしています。

指標は名前だけで判断してはいけない、というのは本当にその通りでした。`disk_tables` という名前だけで「ディスクに行っていないから大丈夫」と読んでしまうと、調査を誤ります。

## 再現実験: 単発ではなく、並行で共有上限を消費する

最初の dev 実験では、なかなか再現できませんでした。

- 単発実行では落ちない
- 低並行でも落ちない
- 別経路の CTE では落ちない
- 一度は「これは本番固有で再現できないかもしれない」と考えた

ただ、条件を見直すと抜けていたものがありました。この事例の負荷に合わせ、並行実行で TempTable の共有上限を消費する、という条件です。

高並行そのものは Bug #112704 の必須条件ではありません。公開 Bug には、`tmp_table_size` を極端に小さくして単発で再現する例もあります。ここでは本番で「単発は成功し、高並行時だけ失敗した」条件を再現するために並行度を上げました。

そこから、`information_schema` だけで組める PoC を作り、高並行 + 小さな TempTable 共有上限で再現を試しました。スクリプト全文は付録に載せますが、比較の中心は次の 2 クエリです。

複数参照版では、materialize される CTE `v` を 2 つのスカラーサブクエリから参照します。

```sql
WITH v AS (
  SELECT DISTINCT
    c.table_name,
    c.column_name,
    c.column_type,
    x.n
  FROM information_schema.columns AS c
  CROSS JOIN (
    SELECT 1 AS n
    UNION ALL SELECT 2
    UNION ALL SELECT 3
    UNION ALL SELECT 4
    UNION ALL SELECT 5
  ) AS x
)
SELECT
  (SELECT COUNT(*) FROM v) AS row_count,
  (SELECT MAX(CONCAT(table_name, column_name, column_type)) FROM v) AS max_value;
```

単一参照版は CTE 本体と 2 つの集約を変えず、`v` の参照だけを 1 回に減らします。

```sql
WITH v AS (
  SELECT DISTINCT
    c.table_name,
    c.column_name,
    c.column_type,
    x.n
  FROM information_schema.columns AS c
  CROSS JOIN (
    SELECT 1 AS n
    UNION ALL SELECT 2
    UNION ALL SELECT 3
    UNION ALL SELECT 4
    UNION ALL SELECT 5
  ) AS x
)
SELECT
  COUNT(*) AS row_count,
  MAX(CONCAT(table_name, column_name, column_type)) AS max_value
FROM v;
```

この 2 クエリを使い、次の 3 条件を比較しました。

| 条件 | CTE参照 | mmap上限 | 確認したいこと |
| --- | --- | --- | --- |
| 再現条件 | 複数参照 | 小さい | 1146 が再現するか |
| 緩和条件 | 複数参照 | 大きい | fallback を遠ざけると止まるか |
| 構造的回避条件 | 単一参照 | 小さい | 参照構造だけを変えると止まるか |

この事例の再現で見るべきだったのは単発の成功可否ではなく、並行時に TempTable の共有上限を消費したときの振る舞いでした。ここを揃えると、

- 高並行 + 小さな TempTable 共有上限で再現する
- mmap 上限を大きくすると止まる
- CTE を単一参照にすると止まる

という結果が出ました。

この段階で、緩和策と構造的回避策の両方を同じ PoC 上で比較できるようになりました。ただし、この比較で確認できるのは発生条件との整合性と回避の有効性までです。MySQL 内部でどのオブジェクトが不整合になったかを直接観測する実験ではありません。

## 公開情報とPoCから得た最有力仮説

観測結果に最もよく当てはまったのは、PR #672 が説明する次の流れです。

1. materialize される CTE が複数参照されると CTE clone が関わる
2. TempTable の materialization が共有 RAM と mmap-backed なローカルストレージの上限に達し、InnoDB 側へ fallback することがある
3. PR #672 の説明では、clone 側の handler と、実体として作られた table 種別が不整合になる
4. 後続の open で `Table doesn't exist` が出る

PR #672 が提示する原因仮説を図にすると、次のようになります。

```mermaid
flowchart LR
  A["materializeされた複数参照CTE"] --> B["cloneがTempTable handlerを保持"]
  C["この事例では高並行で共有上限を消費"] --> D["InnoDBへfallback"]
  B --> E["handlerと実体の種別が不整合"]
  D --> E
  E --> F["後続openでMySQL 1146"]
```

言い換えると、「一時テーブルが重いから落ちる」という説明より、「複数参照 CTE が shared materialization される経路で、TempTable fallback 時の状態整合が崩れる」という説明の方が、今回の観測にはよく当てはまりました。

[PR #672](https://github.com/mysql/mysql-server/pull/672) には、TempTable materialization が `RECORD_FILE_FULL` で InnoDB に fallback したとき、CTE clone が古い handler を持ち続ける、という説明があります。自分たちの再現結果も、この説明と整合していました。

一方、Bug #112704 のスレッドには、共有一時テーブルの open count が不足し、利用中のファイルが早く削除されるという別の低レベルな説明も投稿されています。両者の関係は公開ページだけでは確定できません。handler 不整合は、現時点で本番事象と PoC に最も整合する仮説ですが、ベンダーにより確定された内部原因としては扱いません。

MySQL 内部に詳しくない場合は、「複数参照 CTE が共有する一時テーブルで、fallback 後の参照状態が正しく揃わない可能性がある」と捉えると、調査対象を絞りやすくなります。

## 対策: TempTable枠の拡大による緩和と、CTE単一参照化による構造的回避

対策は二段階で考えました。

まず緩和策です。

- TempTable 用の共有 RAM と mmap-backed なローカルストレージの余裕を増やす
- これにより fallback に到達しにくくする

これは即効性がありましたが、負荷が上がれば再発しうる種類の対策です。問題の経路に入りにくくするだけで、クエリ構造そのものを変えるわけではありません。

設定値を増やすときは、別の枯渇を起こさないことも重要です。`temptable_max_ram` を大きくすると DB instance の空きメモリを圧迫し、`temptable_max_mmap` を大きくするとローカルストレージを使います。Aurora では `FreeableMemory`、`FreeLocalStorage` と Performance Schema の現在値・高水位を確認しながら調整します。

次に構造的回避策です。

- 問題の CTE を単一参照化する
- clone が作られない形に寄せる

こちらは、バグ経路そのものに乗らないようにする考え方です。MySQL Server の不具合自体を直すわけではないため「根治」とは呼びませんが、設定値に依存しにくい恒久的な回避として、可能ならこちらを本線にした方がよいと判断しました。サーバー側の根本修正は、修正が取り込まれた MySQL または Aurora MySQL バージョンへの更新です。

単一参照化後は、`latest_snapshot_per_resource` を `joined_input` の中で 1 回だけ参照し、必要な候補をそこで合流させる形です。

```sql
WITH latest_snapshot_per_resource AS (
  SELECT
    r.resource_key,
    (
      SELECT MAX(s2.snapshot_id)
      FROM snapshots AS s2
      WHERE s2.resource_key = r.resource_key
        AND s2.captured_at <= :target_at
    ) AS snapshot_id
  FROM resources AS r
  WHERE r.tenant_key = :tenant_key
),
ranked_config AS (
  SELECT
    c.resource_key,
    c.config_value,
    ROW_NUMBER() OVER (
      PARTITION BY c.resource_key
      ORDER BY c.updated_at DESC
    ) AS row_num
  FROM configs AS c
),
joined_input AS (
  SELECT
    latest.resource_key,
    source.metric_key,
    config.config_value
  FROM latest_snapshot_per_resource AS latest -- 参照はここだけ
  JOIN metric_source AS source
    ON source.snapshot_id = latest.snapshot_id
  LEFT JOIN ranked_config AS config
    ON config.resource_key = latest.resource_key
   AND config.row_num = 1
)
SELECT
  resource_key,
  COUNT(*) AS metric_a,
  MAX(config_value) AS latest_config_value
FROM joined_input
GROUP BY resource_key;
```

書き換え前との違いは、`ranked_config` の候補絞り込みに `latest_snapshot_per_resource` を使わず、最後の `joined_input` で対象 resource と合流させる点です。最新 config を `row_num = 1` で 1 行に絞る処理は残しているため、config の複数行による集計値の増幅を避けながら、`latest_snapshot_per_resource` の参照だけを 1 回にできます。

単一参照化を適用するときは、書き換え前後の行数、resource ごとの集計値、NULL、重複行、snapshot 選択結果を比較します。実行結果を変えずに参照構造だけを変えられて初めて回避策になります。

調査の途中では「まず設定で止まるならそれでよいのでは」とも思いましたが、再現実験で単一参照化が効くことを確認できたので、緩和と構造的回避を分けて考えられるようになりました。

## 調査で学んだこと

- もっともらしい仮説ほど、実測で反証する
- chunk を小さくすることが、常に安全とは限らない
- DB の status counter は、名前だけで意味を判断しない
- 単発で再現できない問題は、並行度や共有リソースを疑う
- 公開 Bug と自分たちのクエリ構造を照らし合わせる
- 緩和策、構造的回避策、サーバー側の根本修正を分けて考える
- 誤った仮説を撤回することも調査の一部

特に、`Created_tmp_disk_tables` を安全性の proxy として見てしまったのは反省点でした。調査では「何を数えている指標なのか」を確認しないまま使うと、かなり遠回りします。

## まとめ: もっともらしい仮説ほど、実測で壊す

今回の MySQL 1146 は、単なる「クエリが重い」問題として見ると分かりにくく、materialize された複数参照 CTE、TempTable fallback、この事例では高並行時の共有上限消費をまとめて見る必要がありました。

最初は「一時テーブルが大きすぎるのではないか」と考えました。しかし、小さめの chunk で総失敗件数が増えた観測だけでは、実行回数の増加と 1 実行あたりの失敗率を分離できません。そこで単純なサイズ仮説に固定せず、公開 Bug と条件をそろえた PoC で、複数参照、fallback、並行度を切り分けました。

その結果、本番事象は Bug #112704 と強く整合し、TempTable の共有上限を広げると緩和し、CTE を単一参照化すると問題経路を回避できることを確認しました。一方、Aurora 内部で PR #672 の handler 不整合そのものが起きていたかは未確定です。

もっともらしい仮説ほど、観測の分母をそろえ、条件を一つずつ変えて確かめる。確定したことと、最も整合する説明を分けて残しておくと、同じ症状を再調査するときの出発点になります。

## 付録: 再現PoC全文（information_schemaのみ）

以下は、`information_schema` だけを参照する再現 PoC です。

`SET GLOBAL` で TempTable と接続数の設定を変更し、高並行のクエリを実行します。**本番DBや共有DBでは実行せず、必ず使い捨ての Docker コンテナで実行してください。** TempTable の上限値は fallback を起こしやすくするため、意図的に小さくしています。再現条件を固定するため、Docker image は結果を確認した MySQL 8.0.46 に固定しています。

```bash
#!/usr/bin/env bash
#MySQL Bug #112704 の再現PoC。使い捨てコンテナを起動し、終了時に削除する。
#mmapは常に有効にし、temptable_max_mmapだけを変えて3条件を比較する。
set -uo pipefail

IMAGE="${IMAGE:-mysql:8.0.46}"
CONC="${CONC:-100}"
ITER="${ITER:-30}"
NAME="poc1146-$$"
PW="pocpass"
WORKDIR="$(mktemp -d)"

cleanup() {
  docker rm -f "$NAME" >/dev/null 2>&1 || true
  rm -rf "$WORKDIR"
}
trap cleanup EXIT

mx() {
  docker exec -e MYSQL_PWD="$PW" "$NAME" mysql -uroot -N -e "$1" 2>&1
}

docker run --rm -d \
  --name "$NAME" \
  -e MYSQL_ROOT_PASSWORD="$PW" \
  "$IMAGE" >/dev/null

echo ">> image=$IMAGE id=$(docker image inspect --format '{{.Id}}' "$IMAGE")"
echo ">> docker_cpus=$(docker info --format '{{.NCPU}}') docker_memory_bytes=$(docker info --format '{{.MemTotal}}')"

echo -n ">> waiting"
for _ in $(seq 1 90); do
  if docker exec -e MYSQL_PWD="$PW" "$NAME" \
    mysql -uroot -e "SELECT 1" >/dev/null 2>&1; then
    break
  fi
  printf .
  sleep 2
done
echo
echo ">> $(mx 'SELECT VERSION();')"

#DISTINCTでmaterializeさせ、CROSS JOINで行を増やしてfallbackを起こしやすくする。
#MULTIは同じCTEを2回参照するため、cloneが作られる。
MULTI="$(
  cat <<'SQL'
WITH v AS (
  SELECT DISTINCT
    c.table_name,
    c.column_name,
    c.column_type,
    x.n
  FROM information_schema.columns AS c
  CROSS JOIN (
    SELECT 1 AS n
    UNION ALL SELECT 2
    UNION ALL SELECT 3
    UNION ALL SELECT 4
    UNION ALL SELECT 5
  ) AS x
)
SELECT
  (SELECT COUNT(*) FROM v) AS row_count,
  (SELECT MAX(CONCAT(table_name, column_name, column_type)) FROM v) AS max_value;
SQL
)"

SINGLE="$(
  cat <<'SQL'
WITH v AS (
  SELECT DISTINCT
    c.table_name,
    c.column_name,
    c.column_type,
    x.n
  FROM information_schema.columns AS c
  CROSS JOIN (
    SELECT 1 AS n
    UNION ALL SELECT 2
    UNION ALL SELECT 3
    UNION ALL SELECT 4
    UNION ALL SELECT 5
  ) AS x
)
SELECT
  COUNT(*) AS row_count,
  MAX(CONCAT(table_name, column_name, column_type)) AS max_value
FROM v;
SQL
)"

run() {
  find "$WORKDIR" -name 'w_*' -delete 2>/dev/null || true

  for i in $(seq 1 "$CONC"); do
    docker exec \
      -e MYSQL_PWD="$PW" \
      -e Q="$2" \
      -e ITER="$ITER" \
      "$NAME" \
      sh -c 'for j in $(seq 1 "$ITER"); do mysql -uroot -N -e "$Q" 2>&1; done' \
      > "$WORKDIR/w_$i" 2>&1 &
  done
  wait

  printf '   %-34s 1146=%s / %s\n' \
    "$1" \
    "$(cat "$WORKDIR"/w_* | grep -cE "ERROR 1146 .*doesn.t exist")" \
    "$((CONC * ITER))"
}

mx "SET GLOBAL max_connections=1000; SET GLOBAL temptable_use_mmap=ON; SET GLOBAL temptable_max_ram=2097152; SET GLOBAL tmp_table_size=16777216;" >/dev/null

mx "SET GLOBAL temptable_max_mmap=2097152;" >/dev/null
run "multi-ref / mmap 2MB (reproduce)" "$MULTI"

mx "SET GLOBAL temptable_max_mmap=1073741824;" >/dev/null
run "multi-ref / mmap 1GiB (mitigate)" "$MULTI"

mx "SET GLOBAL temptable_max_mmap=2097152;" >/dev/null
run "single-ref / mmap 2MB (avoid)" "$SINGLE"
```

手元で MySQL 8.0.46 を使って 1 回実行した例では、次の結果になりました。使用した image ID は `sha256:7dcddc01f13bab2f15cde676d44d01f61fc9f99fe7785e86196dfc07d358ae2b`、Docker の割り当ては 16 CPU、約 8.2 GB です。この件数は再現率の推定値ではありません。同じ CTE 本体と 2 つの集約を使い、複数参照のまま mmap 上限を増やすと止まり、同じ小さな上限でも単一参照化すると止まる、という差を見るための smoke test です。

```txt
multi-ref / mmap 2MB (reproduce)  1146=2918 / 3000
multi-ref / mmap 1GiB (mitigate)  1146=0 / 3000
single-ref / mmap 2MB (avoid)     1146=0 / 3000
```

実行環境や MySQL のバージョンによって再現率は変わりえます。再現しない場合は、まず `CONC`、`ITER`、TempTable の上限値を含め、共有上限を消費する条件になっているかを確認します。比較を厳密にする場合は、各条件を複数回実行し、実行順も入れ替え、`Created_tmp_*` と Performance Schema の前後値を一緒に記録します。

## 参考リンク

- [MySQL Bug #112704](https://bugs.mysql.com/bug.php?id=112704)
- [mysql/mysql-server PR #672](https://github.com/mysql/mysql-server/pull/672)
- [MySQL Bug #98741](https://bugs.mysql.com/bug.php?id=98741)
- [MySQL 8.0: Optimizing CTEs with merging or materialization](https://dev.mysql.com/doc/refman/8.0/en/derived-table-optimization.html)
- [MySQL 8.0: Internal Temporary Table Use](https://dev.mysql.com/doc/refman/8.0/en/internal-temporary-tables.html)
- [New temporary table behavior in Aurora MySQL version 3](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/ams3-temptable-behavior.html)

[^materialization]: **materialization**: CTE や派生テーブルの問い合わせ結果を内部一時テーブルへ保存し、後続の処理から参照できる形にすることです。materialize された複数参照 CTE では、同じ結果を複数の参照元から共有します。
[^mmap]: **mmap**: memory-mapped file の略です。TempTable では RAM 枠を超えたデータをローカルストレージ上の一時ファイルへ割り当てるために使われます。RAM 容量を増やす仕組みではありません。
[^fallback]: **fallback**: TempTable で処理を続けられなくなったときに、内部一時テーブルを InnoDB へ切り替える動きです。この記事では、writer instance で RAM と mmap-backed なローカルストレージの上限に達した後の切り替えを指します。
[^cte-clone]: **CTE clone**: materialize された同じ CTE を複数箇所から参照するときに、MySQL が参照ごとに用意する内部の `TABLE` オブジェクトです。結果データそのものを複製するのではなく、1 つの内部一時テーブルを複数の clone で共有します。
[^handler]: **handler**: MySQL Server がストレージエンジン経由でテーブルを操作するためのオブジェクトです。PR #672 の原因仮説では、clone 側に TempTable 用 handler が残ったまま、実体が InnoDB へ変わることが不整合につながると説明されています。
