# 本番規模の前提が変わったことによる見直し（2026-09-17）

**作成: 2026-09-17 / ブランチ: `develop` / HEAD: `d4751fc` / 作成者: 調査担当**

> ★**本書は調査と文書化だけです。コードは1行も変更していません。**★
> ★**`lib/` `app/` `components/` `prisma/` `d-watch-api.yaml` は読んだだけです。**★
> ★**`docs/decisions.md` `docs/handoff.md` `CLAUDE.md` も読んだだけです。**★
> ★**本書は決定ではありません。決めるのはユーザーです（7章に判断が要る点をまとめました）。**★

---

## 0. ★★きっかけ — ユーザーからの新情報（2026-09-17）★★

> ★**現状のロボット台数は 30台未満**★（★施設数ではなくロボットの台数★）
> ★**索引は TeamWorks が対応中**★

これまで、詳細ログの期間上限・CSVの行数上限・実行計画の劣化の見積もりは、
すべて ★**「施設200件」という DFA 側の仮置き**★ か ★**「本番規模は不明」**★ を前提に置いていました。

- ★「施設200件」の仮置きの出典★: `docs/decisions.md:10276`（D-136 2節）/ `:10296`（同3節）/
  `docs/handoff/TeamWorks確認事項_第9回_20260915.md:551`（8-1「施設200件という当方の仮置き」）
- ★「見積もりが資料に1件も無い」ことの記録★: `docs/decisions.md:9596-9605`（D-132 8節）/
  `docs/handoff.md:4214`（追記46 の⑧）

★**その前提が現実と大きく違うことが分かったので、何が変わるかを洗い出したものが本書です。**★

---

## 1. ★★結論（先に3行）★★

| # | 結論 |
|---|---|
| ★1★ | ★★`MAX_CSV_ROWS = 20,000` は、★**「全件」表示 × 全店舗 × 2年** のときだけ超えます★★（★約2.2万〜3.3万行★）。★★画面の既定（△×のみ）なら2年分が全部溜まっても超えません（約1.2万行）★★。★ただし2年分が溜まるのは運用開始の2年後なので、いま落ちるわけではありません★ |
| ★2★ | ★★`MAX_DATE_RANGE_DAYS = 731` は、規模の観点では**変える必要がありません**★★。読む行数は旧外挿の ★約26万行 → 約2.8万〜3.3万行（約1/9）★ になります |
| ★3★ | ★★「施設200件の壁」（`eq_range_index_dive_limit = 200`）は**当面問題になりません**★★。★1エリア=1ロボットなので施設数はロボット台数以下＝最大29件★で、★壁の1/7★です。★現実化するのはロボット201台以上★ |

---

## 2. ★★本書の根拠（2026-09-17 のテストDB実測）★★

★実測環境★: テストDB `133.125.238.144:13306 / clean_watch`（★読み取りのみ。書き込みは一切していません★）。
`SELECT VERSION()` → ★`10.11.19-MariaDB-ubu2204`★。接続情報は `.env.local`（★値は本書に書きません★）。

### 2-1. 件数（`SELECT COUNT(*)`）

| テーブル | 行数 |
|---|---|
| `facilities` | ★12★ |
| `sections` | ★14★ |
| `robots` | ★14★ |
| `controls`（D-S制御盤） | ★11★ |
| `customers` | 1 |
| `clean_schedules` | ★21★（★うち `inactive=1` が1本 → ★**有効20本**★） |
| `clean_timetables` | ★1,044★（内訳: ★`precheck=0`（本清掃）= **538**★ / `precheck=30` = 253 / `precheck=60` = 253） |
| `clean_status_histories` | ★538★ ← ★★本清掃と **1:1**★★ |
| `robot_error_reports` | 49 |
| `control_reports` | ★14★ |
| `link_schedules` | ★★0★★ ← ★だから `intervals` の実運用値はDBから取れません（4章）★ |

### 2-2. ★★1日あたりの清掃回数（＝CSVの行数の根拠）★★

★**推測ではなく `clean_timetables` の実データです**★:

```sql
SELECT DATE(start_at) d, COUNT(*) n FROM clean_timetables WHERE precheck=0 GROUP BY 1 ORDER BY 1;
```

| 期間 | 1日あたりの本清掃 |
|---|---|
| 2026-08-14 〜 2026-09-06（★24日間★） | ★★毎日ちょうど 18件★★ |
| 2026-09-07 〜 2026-09-13 | 11 / 12 / 14 / 14 / 19 / 18（★投入途中とみられる★） |

データの範囲は ★2026-08-14 〜 2026-09-13（JST）の31日ぶん★（`MIN/MAX(start_at)` 実測）。

★**18件/日は `clean_schedules` から再現できます**★（＝偶然の数字ではありません）:

| 施設 | 有効スケジュール | 毎日ぶん | 曜日限定ぶん |
|---|---|---|---|
| 1, 2, 5, 9, 10 | 各2本（`weeks="1111111"`） | 2 × 5 = ★10★ | — |
| 6 | 2本のうち1本が `inactive=1` | ★1★ | — |
| 7 | 3本（うち1本が `weeks="1010101"`） | ★2★ | +1本（4/7日） |
| 3 | 2本（うち1本が `weeks="0111110"`） | ★1★ | +1本（5/7日） |
| 4 | 2本（うち1本が `weeks="0101010"`） | ★1★ | +1本（3/7日） |
| 8 | 2本（うち1本が `weeks="1000001"`） | ★1★ | +1本（2/7日） |
| 11 | ★スケジュールなし★ | 0 | — |
| ★合計★ | ★有効20本★ | ★16★ | ★(4+5+3+2)/7 = **2**★ |

★16 + 2 = **18件/日**★。★実測と一致します★。

### 2-3. ★★1台あたりの回数（比の取り方を3通り示します）★★

| 分母 | 計算 | 1台1日あたり |
|---|---|---|
| ★スケジュールを持つ section（12）★ | 18 ÷ 12 | ★★1.50回★★ |
| ★`robots` テーブルの登録台数（14）★ | 18 ÷ 14 | ★1.29回★ |
| ★最も控えめな仮定（1台1晩1回）★ | — | ★1.00回★ |

★★以降の試算はこの3通りをすべて並べます★★ — ★どれが本番の実態かは DFA しか知らないためです★
（`CLAUDE.md` §7「仕様が不明な箇所を推測で実装しない」と同じ姿勢で、★1つに決め打ちしません★）。

### 2-4. ★判定の分布（既定の絞り込みがどれだけ効くか）★

`clean_status_histories.clean_ratio` を `GRADE_THRESHOLDS`（`lib/config/index.ts:18-25`）で分けた実測:

| 判定 | 件数 | 割合 |
|---|---|---|
| ◎（90以上） | 232 | 43.1% |
| ○（70以上） | 107 | 19.9% |
| △（10以上） | 79 | 14.7% |
| ×（10未満） | 120 | 22.3% |
| ★★△× 合計（＝画面の既定の絞り込み）★★ | ★199★ | ★★37.0%★★ |

★★これはテストデータの分布です★★ — ★本番の問題発生率がこれほど高いとは考えにくく、
実際にはもっと小さくなる（＝安全側に振れる）見込みです★。★本書では 37% をそのまま使います★。

### 2-5. ★索引の現状（2026-09-17 実測）★

`information_schema.STATISTICS` を引いた結果、★★第9回1章で依頼した7件は、テストDBに**1本も入っていません**★★:

| 依頼 | 対象 | 現状 |
|---|---|---|
| ① | `clean_status_histories.clean_timetable_id` | ★索引なし★（あるのは `id`(unique) / `facility_id` / `section_id` の3本のみ） |
| ② | `robot_error_reports.clean_timetable_id` | ★索引なし★（あるのは `error_id`(unique) / `facility_id` / `robot_id` / `section_id`） |
| ③ | `clean_timetables (precheck, start_at)` | ★なし★（6本すべて単一列） |
| ④ | `clean_timetables (facility_id, start_at)` | ★なし★（同上） |
| ⑤ | `clean_status_histories.started_at` | ★なし★ |
| ⑥ | `control_reports (control_id, reported_at)` | ★なし★（`control_id` / `facility_id` / `section_id` / PRIMARY のみ） |
| ⑦ | `control_error_histories.occurred_at` | ★なし★ |

★★「TeamWorks が対応中」＝**テストDBにはまだ反映されていません**★★。
★本書の所要時間の数字は「索引が無い状態」のものです★。

### 2-6. ★所要時間（2026-09-17 の実測。参考）★

現状のデータ量（538行）での実測です。★★行数が少ないので、これ自体は本番の予測には使えません★★
（D-131 申し送り④「ローカル538件では差が出ない」と同じ状況）。

| クエリ | 行数 | 時間 |
|---|---|---|
| 主クエリ（`precheck=0` + 2年範囲 + `ORDER BY start_at DESC, id ASC`） | 538 | ★30ms★ |
| `clean_status_histories WHERE clean_timetable_id IN (538件)` | 538 | ★85ms★ |
| `robot_error_reports WHERE clean_timetable_id IN (538件)` | 33 | ★28ms★ |

---

## 3. ★★調査1: `MAX_CSV_ROWS = 20,000`（`lib/config/index.ts:384`）★★ ←★最重要★

### 3-1. 【いまの前提】

★出典★: `docs/decisions.md:10233-10366`（D-136）/ `docs/handoff.md:4389-4400`（追記48 の5節）/
`lib/config/index.ts:361-384` のコメント。

| 記録されている数値 | 出典 |
|---|---|
| ★全店舗（施設200件）の2年 = 約25万行★ | `docs/decisions.md:10296` |
| ★1店舗の2年 = 約1,300行★ | `docs/decisions.md:10295` |
| ★根拠は「31日・502行で0.73秒」という**実測1点からの外挿**★ | `docs/decisions.md:10308` / `docs/handoff.md:4391-4393` |
| ★1行あたり111バイト（実測） → 20,000行 ≒ 2.2MB★ | `lib/config/index.ts:378-379` |
| ★★「この値は暫定です。実データと索引が入ったら測り直すこと」★★ | ★`docs/decisions.md:10306-10310`（D-136 3節）/ `:10360-10362`（同8節）★ |

★25万行の内訳★: `docs/spec.md:45`「データ量: 約360行/日、年間約13万行、保持2年」 × 731日 = 263,160行。
★つまり「1施設あたり1.8回/日 × 200施設」という外挿です★。

### 3-2. 【新しい事実】

★ロボット30台未満★。★★1エリア = 1ロボット（`CLAUDE.md` §8）★★なので、
★★CSVの1行 = `clean_timetables` の `precheck=0` の1行 = 1ロボットの1回の清掃★★です
（`app/api/clean-logs/export/route.ts:48-55` が `db.cleanLogs()` を呼び、
`lib/db/index.ts:706-708` が `precheck: CLEAN_TIMETABLE_PRECHECK.main` で絞っています）。

### 3-3. 【変わること】★★計算過程★★

```
CSVの行数 = ロボット台数 × 1台1日あたりの清掃回数 × 日数(731)
```

★★「全件」を選んだ場合（絞り込みなし）★★:

| 1台1日あたり（2-3節） | ★29台★（30台未満の最大） | ★30台★（切りのよい上限） |
|---|---|---|
| ★1.50回★（スケジュール保有台で割った実測比） | 1.50 × 29 × 731 = ★31,799行★ | 1.50 × 30 × 731 = ★★32,895行★★ |
| ★1.29回★（登録台数で割った実測比） | 1.29 × 29 × 731 = ★27,256行★ | 1.29 × 30 × 731 = ★28,196行★ |
| ★1.00回★（最も控えめ） | 1.00 × 29 × 731 = ★21,199行★ | 1.00 × 30 × 731 = ★21,930行★ |

★★どの取り方でも 20,000行を超えます★★。

★★画面の既定（△×のみ）の場合★★（`components/features/detail-log/DetailLogScreen.tsx:62`
の `useState<GradeFilter>("problem")`。★API 側の既定は `all`★ — `lib/clean-logs/query.ts:119`）:

| 1台1日あたり | 30台・2年・全店舗 | ★2-4節の 37.0% を掛けた行数★ |
|---|---|---|
| ★1.50回★ | 32,895行 | ★★12,171行 → 通る★★ |
| ★1.29回★ | 28,196行 | ★10,433行 → 通る★ |
| ★1.00回★ | 21,930行 | ★8,114行 → 通る★ |

★★1店舗だけなら★★: 1.50 × 1 × 731 = ★1,097行★（旧外挿の「約1,300行」とほぼ同じ）。★余裕で落とせます★。

### 3-4. ★★判定: 「常に422で落ちる」わけではありません★★

★★3つの条件が**すべて**揃ったときだけ 422 になります★★:

1. ★店舗で絞らない（全店舗）★
2. ★判定フィルタを「全件」に切り替える★（★既定の「△×のみ」のままでは超えません★）
3. ★★2年ぶんのデータが実際に溜まっている★★

★★3 が重要です★★ — `app/api/clean-logs/export/route.ts:79` の判定は
★`filtered.length`（＝**実在する行数**）★を見ており、★指定した期間の長さではありません★。
★2年を指定しても、DBに1か月ぶんしか無ければ1か月ぶんの行しか出ません★。

★★422が出はじめるまでの日数★★（運用開始からの経過日数。上限20,000行のまま・「全件」表示の場合）:

| 1日あたりの本清掃 | 20,000行に達するまで |
|---|---|
| 45件/日（30台 × 1.50回） | ★445日 ≒ **1年3か月**★ |
| 38.6件/日（30台 × 1.29回） | ★519日 ≒ **1年5か月**★ |
| 30件/日（30台 × 1.00回） | ★667日 ≒ **1年10か月**★ |

★★つまり「今すぐ壊れる」問題ではありません★★。★ただし放置すれば必ず来ます★
（★保持期間が2年なので、2年経つと行数は頭打ちになり、そこで恒久的に422になります★）。

### 3-5. ★上限に触れない最大の規模（閾値）★

```
20,000 ÷ 731 = 27.4 行/日  ← これが「2年分が20,000行に収まる」1日あたりの上限
```

| 1台1日あたり | ★2年が通る最大のロボット台数★ |
|---|---|
| 1.50回 | ★18台★ |
| 1.29回 | ★21台★ |
| 1.00回 | ★27台★ |

★★30台未満という新しい事実は、この3つの閾値を**すべて上回っています**★★。

### 3-6. ★重さ（時間・サイズ）はどう変わるか★

★★以下は外挿です。実測ではありません★★（★行数を増やすにはDBへの書き込みが要り、本作業では禁止されています★）。

| | ★旧外挿（施設200件）★ | ★新しい前提（30台 × 1.50回）★ | 比 |
|---|---|---|---|
| 2年の行数 | 約263,000行 | ★約32,900行★ | ★1/8★ |
| `IN` の塊数（`DB_IN_CLAUSE_MAX_ITEMS = 1000`） | ★264塊★ | ★33塊★ | 1/8 |
| `clean_status_histories` の走査行数（★索引が無いので塊ごとに全表走査★） | ★264 × 263,000 ≒ **6,950万行**★ | ★33 × 32,900 ≒ **109万行**★ | ★★1/64★★ |
| CSVのサイズ（111バイト/行） | 約28MB | ★約3.7MB★ | 1/8 |
| CSV生成時間（D-136 2節の「約6分（下限）」を走査量で割り戻した参考値） | 約6分 | ★★約6秒（★参考値。要実測★）★★ | — |

★★nginx の既定タイムアウト（60秒）に対する見通しは、**旧前提より大幅に楽になります**★★
（D-136 2節は「2年分は落とせない（504）」と書いていましたが、★その前提は施設200件でした★）。
★★ただし実測していないので、断定はしません★★（8章のチェックリストへ）。

### 3-7.【誰が何を決めるか】→ ★7章の判断1★

---

## 4. ★★調査2: `MAX_DATE_RANGE_DAYS = 731`（`lib/config/index.ts:359`）★★

### 4-1. 【いまの前提】

- `lib/config/index.ts:356-357` の申し送り: ★「索引が入るまで、2年を選ぶと重い（本番想定で約25万行）」★
- `lib/db/index.ts:681` / `:1768` / `lib/db/consecutive-source.ts:214` 等が ★「保持2年で約26万行」★ を前提にコメントしています
- `lib/config/index.ts:433-435`（`DB_IN_CLAUSE_MAX_ITEMS`）: ★「`in` の要素数は約26万になりえます」★

### 4-2. 【新しい事実 → 変わること】

| | 旧外挿 | ★新しい前提（30台）★ |
|---|---|---|
| 主クエリが読む行数（2年） | 約26万行 | ★約2.8万〜3.3万行★ |
| `in` の要素数 | 約26万 | ★約3.3万★ |
| 塊数 | 264 | ★33★ |

★★`take` / `skip` は SQL に降りていません★★（D-131 決定 D-3 / 申し送り⑥。`lib/db/index.ts:692-694`）。
つまり ★2年を選ぶと、その行数がまるごと Node のメモリに載ります★。
★★26万行なら危険でしたが、3.3万行なら現実的です★★。

★D-131 申し送り⑥（`docs/decisions.md:9390`）★は
★「施設数が増えて1日の行数が跳ねたら見直すこと」★と書いていますが、
★★1日の行数は跳ねるどころか 360件/日 → 45件/日 に下がりました★★。★見直しの必要性は下がります★。

### 4-3. ★★判定: 変える必要はありません★★

- ★規模の観点では 731 を変える理由がありません★
- ★索引が入ればさらに楽になります★（5章）
- ★★変えるとしたら理由は「重いから」ではなく「運用上そんなに長く見せたいか」です★★ —
  ★それは D-136 1節でユーザーが「絞り込むときの上限を2年」と確定済み★なので、★動かす理由がありません★

★★もし変えるなら★★: `lib/clean-logs/__tests__/query.test.ts:76` が
★`expect(MAX_DATE_RANGE_DAYS).toBe(731)` と直接固定しています★ので、★テストも一緒に直す必要があります★。
`d-watch-api.yaml` の文言も追随させること（`CLAUDE.md` §6-1）。

---

## 5. ★★調査3: 施設200件超で実行計画が劣化する懸念★★

### 5-1. 【いまの前提】

- `docs/decisions.md:9395`（D-131 申し送り⑫）: ★「施設が200件を超えると `facility_id: { in: [...] }` の実行計画が静かに劣化する」★。
  ★★「ローカル MySQL 8.4 で実測。**MariaDB 10.11 の値は未確認**」★★
- `docs/decisions.md:9609-9611`（D-132 8節）: ★「唯一出てくる『300施設』は出典・根拠の記載が無い」★
  「★上の『300施設』が本当なら、本番でこの線に触れる★」
- `docs/decisions.md:10677-10681`（D-138 4節）: ★「200件の壁は今回は素通しした」「測れない相手に対策を入れても効果測定ができない」★

### 5-2. 【新しい事実】★本調査で MariaDB 側も実測しました★

```
SHOW VARIABLES LIKE 'eq_range_index_dive_limit';   → 200
SHOW VARIABLES LIKE 'optimizer_use_condition_selectivity'; → 4
```

★★テストDB（MariaDB 10.11.19）でも `eq_range_index_dive_limit = 200` でした★★。
★これで「MariaDB 10.11 の値は未確認」という申し送りは**解消**します★
（★ただし本番（ステージング／商用）で同じ設定とは限らないので、8章のチェックリストに残します★）。

### 5-3. 【変わること】★いつ現実化するか★

★★1エリア = 1ロボット（`CLAUDE.md` §8）で、1施設には1つ以上のエリアがある★★ので:

```
施設数 ≤ セクション数 = ロボット台数 < 30
```

★★つまり施設は最大でも29件★★。★壁（200）の 1/7 です★。
実測でも 12施設 / 14セクション / 14ロボット で、★施設 ≤ 台数★ が成り立っています。

| 問い | 答え |
|---|---|
| ★いつ現実化するか★ | ★★`facility_id: { in: [...] }` の要素数が **201以上**になったとき★★ |
| ★★何台で現実化するか★★ | ★★1施設1台なら **ロボット201台**★★（現在の ★7倍以上★）。★1施設2台なら402台★ |
| ★当面問題になるか★ | ★★なりません★★ |

★同じ線にある既知の潜在バグ★:

| # | 内容 | 場所 | ★30台未満なら★ |
|---|---|---|---|
| ★A★ | ★レポート画面の「全{n}施設を表示 →」が200件で切れる★（D-120 申し送り② / D-131 申し送り⑨。`docs/decisions.md:9392`） | `components/features/reports/ReportsScreen.tsx:55`（`SHOW_ALL_LIMIT = PAGINATION.maxPerPage`） | ★★発火しません★★（施設29件以下） |
| ★B★ | ★制御盤が200台を超えたら `control_id IN (...)` を再測定★（`docs/handoff.md:4214` の⑦） | `lib/db/` の D-S 系 `groupBy` | ★★発火しません★★（制御盤 ≤ 施設数 ≤ 29） |
| ★C★ | ★屋号が数百件規模になったら判断を測り直す★ | `lib/masters/options.ts:26` | ★★発火しません★★（顧客1社・P1） |

★★A は「直さなくてよい」という意味ではありません★★ —
★施設数の表示（n）と実際に出る件数が食い違う不整合は残ります★。★ただし優先度は下がります★。

---

## 6. ★★調査4: C-9 — `control_reports` が1,100万行か7,000万行か★★

### 6-1. 【いまの前提】

★出典★: `docs/handoff.md:4214`（追記46 の5節 ⑧）/ `docs/handoff/TeamWorks確認事項_第9回_20260915.md:562-569`（8章）

> ★★本番の行数見積もりが資料に存在しません★★ — `link_schedules.intervals`（心拍の報告間隔。秒）の
> **実運用値**を確認しないと、`control_reports` が ★★1,100万行なのか7,000万行なのかが決まりません★★
> （定義書の例は60秒 → 2年で約1,100万行 / TeamWorks 第2回回答の例は10〜30秒 → 10秒なら約7,000万行。
> ★★11台での試算★★）

★★「11台」はテストDBの `controls` の件数であって、本番の見込みではありません★★。

### 6-2. 【新しい事実で、幅はどう狭まるか】

★★狭まるのは「台数の上限が初めて決まった」ことだけです★★。

```
2年 = 731日 = 63,158,400 秒
1台あたり:  60秒間隔 → 1,052,640行 /  10秒間隔 → 6,315,840行
```

| 制御盤の台数 | ★60秒間隔★ | ★10秒間隔★ |
|---|---|---|
| ★11台★（テストDB実測。C-9 の試算の前提） | ★約1,158万行★ | ★約6,948万行★ |
| ★29台★（★★新しい上限★★。制御盤 ≤ 施設数 ≤ ロボット台数） | ★約3,053万行★ | ★約1億8,316万行★ |

★★注意: 上限は**狭まるどころか上がりました**★★ —
★C-9 は「11台」で試算していましたが、その11台は根拠のある本番見込みではなかった★ためです。
★★新しい事実が与えたのは「上限が29台で止まる」という保証であって、下方修正ではありません★★。

★制御盤はもっと少ない可能性があります★ — D-S制御盤は ★D-Security 契約のある施設にだけ★ 置かれます
（`facilities.contract` の1桁目。`lib/config/index.ts:632-635` の `CONTRACT_DIGITS.dSecurity`）。
テストDBの実測では ★D-Security 契約は6施設（`1000000000` が1件 + `1100000000` が5件）★ですが、
★`controls` は11台★あり、★★テストデータの中で整合していません★★
（★テストデータなので本番の比率にはなりません。ここから本番を推定しないでください★）。

### 6-3. 【★決まらない部分は決まらないと書きます★】

| 決まるか | 内容 |
|---|---|
| ★★決まった★★ | ★制御盤の台数の**上限**が29台★（＝ロボット台数の上限から導ける） |
| ★★決まらない★★ | ★★`link_schedules.intervals` の実運用値★★ — ★第9回 Q8-3 で照会中、**未回答**★。
★★テストDBの `link_schedules` は **0行**（2026-09-17 実測）なので、DBからも取れません★★ |
| ★★決まらない★★ | ★実際の制御盤の台数★（第9回 Q8-2 で照会中、未回答） |
| ★★決まらない★★ | ★`control_reports` の最終的な行数★ — ★6倍の開き（60秒 vs 10秒）はそのまま残ります★ |

★★帰結: 索引⑥（`control_reports (control_id, reported_at)`）の重要度は下がりません★★ —
★心拍は清掃回数と無関係に積み上がるので、ロボット台数が減っても `control_reports` は減りません★。
★むしろ台数の上限が11→29に上がったぶん、**上振れの余地が増えました**★。

---

## 7. ★★調査5: 索引が入ることで変わるもの★★

★出典★: `docs/handoff/TeamWorks確認事項_第9回_20260915.md` 1章（`:58-198`）/
`docs/handoff.md:4364-4387`（追記48 の4節）。★2026-09-17 時点でテストDBに1本も入っていません（2-5節）★。

### 7-1. 何が速くなるか

| # | 索引 | ★何が速くなるか★ | ★30台未満での効き方★ |
|---|---|---|---|
| ★①★ | `clean_status_histories.clean_timetable_id` | ★★詳細ログの本体と連続日数の算出（**2経路**）★★。塊ごとの全表走査 → レンジスキャン | ★走査 約109万行 → 必要行だけ★。★最も効く★ |
| ★②★ | `robot_error_reports.clean_timetable_id` | ★同上（エラー種別の引き直し）★ | ①と同じ形。★行数は少ない（本清掃の1割弱）★ |
| ★③★ | `clean_timetables (precheck, start_at)` | ★主クエリ（★画面の既定＝施設で絞らない経路★）の `type=ALL` + `filesort`★ | ★2年で約3.3万行を読むところが索引レンジ1本になる★ |
| ★④★ | `clean_timetables (facility_id, start_at)` | ★施設で絞る経路★ | ★施設29件以下なので `IN` の要素数は少ない。★③より効きは小さい★ |
| ★⑤★ | `clean_status_histories.started_at` | ★★総清掃面積の期間集計（F-新画面。`lib/clean-area/query.ts`）★★ | ★期間で絞っても全行走査になっていたのが解消★ |
| ★⑥★ | `control_reports (control_id, reported_at)` | ★★D-Sログ画面の「最終通信時刻」（`MAX(reported_at)`）★★。`index`（フルスキャン）→ `Using index for group-by` | ★★規模の影響が最大（6章）。3,000万〜1億8,000万行が制御盤数オーダーに落ちる★★ |
| ★⑦★ | `control_error_histories.occurred_at` | ★D-Secアラートの期間絞り込み★ | — |

### 7-2. ★★索引が入っても直らないもの★★（★混同しないこと★）

`docs/decisions.md:10280-10283`（D-136 2節）/ `docs/handoff.md:4404-4410`（追記48 の6節）:

- ★★`LIMIT`（`take` / `skip`）が SQL に降りていない★★ — ★索引では直りません★
- ★★`ORDER BY start_at DESC, id ASC` の向きが混在していて `filesort` が消えない★★ — ★同上★
- ★★この2つを同時にやらないこと★★（★切り分けができなくなります★）

---

## 8. ★★調査6: ほかに規模を前提にした判断はないか★★

`lib/config/index.ts` を ★全文（705行）読みました★。★規模（行数・施設数・台数）に依存する設定値★は次の9件です。

### 8-1. ★規模に依存する設定値の一覧★

| # | 定数 | 場所 | 現在値 | ★何に依存するか★ | ★30台未満で変わること★ |
|---|---|---|---|---|---|
| ★1★ | `MAX_CSV_ROWS` | `lib/config/index.ts:384` | ★20,000★ | ★★行数（台数 × 回数 × 日数）★★ | ★★「全件」×全店舗×2年で超える（3章）。★判断が要る★★★ |
| ★2★ | `MAX_DATE_RANGE_DAYS` | `:359` | 731 | ★行数（読み込み量・メモリ）★ | ★★変えなくてよい★★（4章）。★コメント `:356`「本番想定で約25万行」は**古くなった**★ |
| ★3★ | `DB_IN_CLAUSE_MAX_ITEMS` | `:463` | 1,000 | ★`in` の要素数（＝行数）★ | ★塊数 264 → 33。★値は変えなくてよい★（分割の必要性は残る）。★コメント `:434-435`「約26万」は**古くなった**★ |
| ★4★ | `PAGINATION.maxPerPage` | `:326` | 200 | ★施設数★（`SHOW_ALL_LIMIT` 経由） | ★★施設29件以下なので「全{n}施設を表示」は切れない（5-3節 A）★★ |
| ★5★ | `PAGINATION.defaultPerPage` | `:325` | 50 | ★1日あたりの行数★ | ★360件/日 → 45件/日。★1日ぶんが1ページに収まりやすくなる★（変更は不要） |
| ★6★ | `RESCUE_INITIAL_VISIBLE` | `:102` | 5 | ★施設数（「もっと表示」で開く全件の上限）★ | ★コメント `:89`「施設数（P1 の想定で200件程度）が自然な上限」が**古くなった**（実際は最大29件）。★値は変えなくてよい★ |
| ★7★ | `CLEAN_AREA_INITIAL_VISIBLE` | `:704` | 10 | ★施設数★ | ★同上（`:701`「施設数が上限」）。★値は変えなくてよい★ |
| ★8★ | `CONSECUTIVE_DAYS_DISPLAY.maxMax` | `:139` | 365 | ★保持2年（＝読み込み範囲の歯止め）★ | ★保持期間は変わらないので影響なし★ |
| ★9★ | `CONSECUTIVE_LOOKBACK.calendarDaysPerScheduleDay` | `:169` | 7 | ★★「どんなに疎な施設でも7カレンダー日に1回は清掃予定がある」という前提★★ | ★★実データで検証しました → 前提は成立しています★★（下記） |

★★#9 の検証（本調査で初めて実データで確認しました）★★:

`clean_schedules.weeks`（有効20本）の実測分布:

| `weeks` | 本数 | 週あたりの清掃日 | ★最大の間隔★ |
|---|---|---|---|
| `1111111` | 16 | 7日 | 1日 |
| `0111110` | 1 | 5日 | 3日 |
| `1010101` | 1 | 4日 | 2日 |
| `0101010` | 1 | 3日 | 3日 |
| ★`1000001`★ | 1 | ★2日★ | ★★6日★★ |

★★最も疎いスケジュールでも、清掃予定日の最大間隔は6カレンダー日★★ → ★安全係数7は成立（余裕1日）★。
★★ただし余裕は1日しかありません★★ — ★週1回（`1000000` のような）スケジュールが1本でも入ると
最大間隔が7日になり、★★遡りが足りず連続日数が実際より短く出ます（例外もテスト失敗も出ません）★★★
（`lib/config/index.ts:150-155` の警告のとおり）。★8章のチェックリストに入れました★。

### 8-2. ★規模に依存**しない**設定値（確認済み。念のため列挙）★

`GRADE_THRESHOLDS` / `ESCALATION_THRESHOLDS` / `COMPLETION_DROP` / `RESCUE_THRESHOLDS` /
`LAUNCH_GRACE_MINUTES` / `DB_DATETIME_OFFSET_MINUTES` / `PRECHECK_MINUTES_BEFORE` /
`CLEAN_TIMETABLE_PRECHECK` / `CLEAN_ERROR` / `CLEAN_ERROR_LABELS` / `ROBOT_STATUS_STANDBY` /
`REPORT_YEAR` / `FACILITY_STATUS_THRESHOLDS` / `FACILITY_TABLE_THRESHOLDS` / `SUCCESS_RATE_TARGET` /
`PATH_ID_MAX` / `PATH_SERIAL_NUMBER_MAX_LENGTH` / `MAKER_NAMES` / `ROBOT_MODELS` /
`CONTRACT_DIGITS` / `GROUP_ID_NONE` / `ROBOT_STATUS_TONES` / `AREA_UNIT` /
`DEVICE_NOTE_MAX_LENGTH` / `ADVICE_*`

★`DEVICE_NOTE_MAX_LENGTH`（`:503`）と `ADVICE_*`（`:531-537`）だけは「JSONストアの全件読み込み→書き戻し」
という保存方式への配慮ですが、★件数は制御盤台数（≤29）とルール数なので、規模の心配はありません★。

### 8-3. ★★同じ前提に乗っているコード内コメント（★**古くなりました**★）★★

★★本書ではコードを1行も直していません★★。★次に触る人が直す場所の一覧です★:

| 場所 | 古くなった記述 |
|---|---|
| `lib/config/index.ts:356-357` | 「本番想定で約25万行」 |
| `lib/config/index.ts:368` | 「全店舗（施設200件）なら約25万行で、200倍違う」 |
| `lib/config/index.ts:372-376` | 「20,000行なら 20塊 × 20,000行 = 40万行の走査で収まる見込み」 |
| `lib/config/index.ts:89` | 「施設数（P1 の想定で200件程度）が自然な上限」 |
| `lib/config/index.ts:434-435` | 「本番想定は約360行/日 …`in` の要素数は約26万になりえます」 |
| `lib/db/index.ts:176` / `:681` / `:1767-1768` / `:1785` / `:292` | 「保持2年で約26万行」「最大26万行」「200行程度」 |
| `lib/db/consecutive-source.ts:214` / `:220` / `:230` | 「保持2年・約26万行」「増分を『26万行 × バイト数』で見積もり」 |
| `lib/db/facility-name-source.ts:60-61` | 「清掃ログ本体（本番想定で約26万行）」「施設が200件を超えたら」 |
| `lib/db/clean-area-source.ts:44` | 「施設は実測12件、本番想定でも200件程度」 |
| `app/api/dashboard/route.ts:52` / `:82` | 「本番想定で `clean_timetables` 約26万行」「約26万行の読み込みが約360行に」 |
| `lib/facilities/resolve-by-name.ts:25` | 「本番想定で約26万行」 |
| `lib/masters/options.ts:26` | 「屋号が数百件規模になったら」 |
| `docs/spec.md:45` | 「データ量: 約360行/日、年間約13万行、保持2年」 ← ★★この1行がすべての外挿の源です★★ |
| `lib/db/__tests__/chunked.test.ts:36` | テスト名「本番想定（31日 × 約360行/日 ≒ 11,000件）」 |

★★`docs/spec.md:45` を直すかどうかは、第9回 Q8-1 / Q8-2 への TeamWorks の回答を待つか、
DFA 社内で確定させるかの判断になります★★（D-132 8節が「★これは TeamWorks ではなく DFA 側で決めること★」
と書いています）。

---

## 9. ★★ユーザー判断が要る点★★

> ★★ここに書いた選択肢はどれも「案」です。★決めるのはユーザーです★★★。
> ★★実装は含みません。決まったら指示書を起こしてください★★。

### 判断1（★最重要★）: `MAX_CSV_ROWS = 20,000` をどうするか

★前提★: 上限に触れるのは ★「全件」表示 × 全店舗 × 2年分が溜まった後★（3-4節）。★運用開始から1年3か月〜1年10か月後★。

| 案 | 値 | ★この選択をするとこうなる★ |
|---|---|---|
| ★A. 据え置き★ | 20,000 | ★運用開始から1年半ほど経つと、「全件・全店舗・2年」のCSVが ★★422★★ になります。画面ではCSVボタンが押せなくなり、理由が同じ場所に出ます（D-136 4節 / 5-3）。★利用者は「期間を短くする」か「店舗で絞る」で回避できます★。★`details` に `{ total, max }` が入るので「あと何件減らせばよいか」は分かります★。★★「年に1度、全店舗2年ぶんを一括でCSVに落とす」運用があると詰みます★★ — ★それがあるかは DFA しか知りません★ |
| ★B. 35,000 にする★ | 35,000 | ★30台 × 1.50回 × 731日 = 32,895 を包む最小の丸い値★。★全店舗2年が通ります★。CSVは ★約3.9MB★。走査は ★35塊 × 3.3万行 ≒ 116万行★。★★索引①②が入る前だと nginx の60秒に間に合うか未実測★★。★台数が30を超えたら、また超えます★ |
| ★C. 50,000 にする★ | 50,000 | ★台数が45台程度まで増えても持ちます★（50,000 ÷ 731 ÷ 1.5 ≒ 45台）。CSVは ★約5.6MB★。★余裕を取るぶん、重い要求を通してしまう幅も広がります★ |
| ★D. 行数上限をやめる★ | — | ★★採らないことを推奨します★★ — ★D-136 4節が「25万行ぶんの文字列を作ってから捨てるのではメモリを使い切る危険が残る」として、組み立て前に数える設計にしています★。★上限を消すとその歯止めが消えます★。★ユーザー自身が「CSV出力できませんとわかるようにしてください」と指示しています★（D-136 0節） |

★★どの案でも一緒に必要になること★★:

- ★`d-watch-api.yaml:406`（「上限(MAX_CSV_ROWS = 20,000行)を超えるときは 422」）の文言を先に直す★（`CLAUDE.md` §6-1）
- ★★`app/api/clean-logs/export/__tests__/route.test.ts:87-100` が `MAX_CSV_ROWS ± 1` 行を実際に組み立てます★★ —
  ★値を大きくするとテストが重くなります★（★20,001行 → 50,001行なら2.5倍★）。★定数を直接使っているので**壊れはしません**が、遅くなります★

### 判断2: いつ測り直すか

| 案 | ★この選択をするとこうなる★ |
|---|---|
| ★A. 索引が入るのを待ってから測り、それから決める★ | ★★最も根拠が強い★★。★ただし索引は第9回1章で依頼中・未回答（2-5節で未投入を実測）★。★待っている間は 20,000 のまま★ |
| ★B. いま暫定で上げておき、索引が入ってから測り直す★ | ★上限に触れるのは1年以上先なので、急ぐ必要はありません★。★「暫定を暫定で上書きする」ことになり、D-136 8節の申し送りが増えます★ |
| ★C. 触らないと決める（申し送りを閉じる）★ | ★★根拠は本書で揃いました★★ — ★「30台未満なら既定の絞り込みでは超えない。全件を選んだときだけ超える。それは許容する」と決め切れば、申し送りを閉じられます★ |

### 判断3: `docs/spec.md:45`（約360行/日・年間約13万行）を書き換えるか

★★この1行がすべての外挿の源です★★（8-3節）。

| 案 | ★この選択をするとこうなる★ |
|---|---|
| ★A. いま DFA の数字で書き換える★ | ★「約45行/日・年間約1.6万行・保持2年」（30台 × 1.50回）★。★以後の見積もりが現実に合います★。★TeamWorks の回答（Q8-1/Q8-2）と食い違ったら、また直すことになります★ |
| ★B. TeamWorks の回答を待つ★ | ★D-132 8節は「★これは TeamWorks ではなく DFA 側で決めること★」と書いています★ — ★★待っても TeamWorks からは出てこない可能性があります★★ |
| ★C. 両方書く★ | ★「DFA 実績ベース: 約45行/日 / TeamWorks 未回答」と併記する★ |

### 判断4: 第9回 8章（本番規模の見積もり）の扱い

★★`docs/handoff/TeamWorks確認事項_第9回_20260915.md` は別のエージェントが編集中のため、本書では触っていません★★。

- ★Q8-1（施設数）/ Q8-2（台数）は、★★DFA 側で答えが出ました（30台未満）★★★ → ★取り下げるか、DFA の数字を添えて「確認」に変えるか★
- ★★Q8-3（`intervals`）は取り下げないこと★★ — ★6章のとおり、ここだけは未回答のままで、6倍の開きが残ります★
- ★1章の走査行数の表（「施設200件で外挿」）は、★★30台前提に直すと桁が2つ下がります★★★ —
  ★索引依頼の説得力が落ちる可能性があります★。★★ただし⑥（`control_reports`）は規模が落ちない★★ので、
  ★そこを前面に出すという手があります★

---

## 10. ★★索引が入った後に測り直す項目（チェックリスト）★★

> ★★次の担当者がそのまま実行できる粒度で書いています★★。
> ★★テストDBは読み取りのみ。書き込み禁止です★★。

### 10-1. まず索引が入ったことを確認する（★誰: DFA / いつ: TeamWorks から連絡が来たら★）

```sql
SELECT TABLE_NAME, INDEX_NAME, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) cols
  FROM information_schema.STATISTICS
 WHERE TABLE_SCHEMA = DATABASE()
   AND TABLE_NAME IN ('clean_status_histories','robot_error_reports','clean_timetables',
                      'control_reports','control_error_histories')
 GROUP BY 1,2 ORDER BY 1,2;
```

★期待★: 7件が現れること（2-5節の表の①〜⑦）。★★2026-09-17 時点では0件でした★★。

### 10-2. チェックリスト

| # | ★誰が★ | ★何を★ | ★どう測るか★ | ★判断の基準★ |
|---|---|---|---|---|
| ★1★ | DFA | ★索引①②が効いているか★ | `EXPLAIN SELECT * FROM clean_status_histories WHERE clean_timetable_id IN (…)` | ★`type=ALL` / `possible_keys=null` でなくなっていること★（第9回 1-2 の出力と比較） |
| ★2★ | DFA | ★索引③④が効いているか★ | `EXPLAIN` で `precheck=0 AND start_at BETWEEN …`（施設指定あり／なしの2本） | ★`Using filesort` が消えること★。★消えなければ「`ORDER BY` の向き混在」が原因（7-2節）★ |
| ★3★ | DFA | ★★`MAX_CSV_ROWS` の測り直し★★ | ★本番相当の行数が入った環境で `GET /clean-logs/export?date_from=…&date_to=…&result=all`（店舗指定なし）を実行し、★**所要時間とバイト数**★を記録する★ | ★★nginx の既定タイムアウト60秒に対して十分余裕があるか★★。★D-136 は「2年で約6分」を旧前提で見込んでいた★ |
| ★4★ | DFA | ★CSVの1行あたりバイト数の再実測★ | 出力ファイルのバイト数 ÷ 行数 | ★111バイト（旧実測）から大きく変わっていないか★。★列を足したら変わります★ |
| ★5★ | DFA | ★詳細ログ2年の画面表示の所要時間★ | 画面で `date_from`/`date_to` を2年にして開く（★既定の「△×のみ」と「全件」の両方★） | ★★`take`/`skip` が SQL に降りていないので、行数ぶんがメモリに載ります★★（4-2節）。★体感で待てるか★ |
| ★6★ | DFA | ★`eq_range_index_dive_limit` を★★本番／ステージング★★で実測★ | `SHOW VARIABLES LIKE 'eq_range_index_dive_limit';` | ★200 なら5章の結論がそのまま使えます★。★テストDB（MariaDB 10.11.19）では 200 を実測済み★ |
| ★7★ | DFA | ★★`CONSECUTIVE_LOOKBACK.calendarDaysPerScheduleDay = 7` の前提★★ | `SELECT weeks, COUNT(*) FROM clean_schedules WHERE inactive=0 AND deleted_at IS NULL GROUP BY 1;` | ★★清掃予定日の最大間隔が7カレンダー日以上になる `weeks` が1本でもあれば、値を増やすこと★★（8-1節 #9。★例外もテスト失敗も出ません★） |
| ★8★ | DFA | ★1日あたりの本清掃の件数★ | `SELECT DATE(start_at), COUNT(*) FROM clean_timetables WHERE precheck=0 GROUP BY 1;` | ★本書の 18件/日（12施設・14台）から、本番の台数でどう変わったか★。★★`MAX_CSV_ROWS` の再計算はこの数字1つで足ります★★ |
| ★9★ | DFA | ★`control_reports` の実際の増加速度★ | `SELECT COUNT(*), MIN(reported_at), MAX(reported_at) FROM control_reports;` を ★日を空けて2回★ | ★★1日あたりの増加行数 ÷ 制御盤台数 → 実効の報告間隔が逆算できます★★（★Q8-3 が未回答でも、データが入れば測れます★）。★2026-09-17 時点は14行しかなく測れません★ |
| ★10★ | DFA | ★`link_schedules.intervals` の実値★ | `SELECT section_id, intervals FROM link_schedules;` | ★★2026-09-17 時点で 0行★★。★行が入ったら6章の表のどちらに寄るかが確定します★ |
| ★11★ | DFA | ★D-S制御盤の台数★ | `SELECT COUNT(*) FROM controls;` と `facilities.contract` の1桁目が `1` の件数 | ★6章のとおり、テストDBでは11台と6施設で整合していません★。★本番では一致するはずです★ |
| ★12★ | DFA | ★★コメントの一斉更新★★ | 8-3節の一覧 | ★★測り直した数字が出てから、1つの指示書でまとめて直すこと★★（★バラバラに直すと「26万行」と「3.3万行」が混在して次の人が混乱します★） |

---

## 11. ★★本書でやっていないこと★★

- ❌ ★コードの変更★（★1行も触っていません★）
- ❌ ★`docs/decisions.md` / `docs/handoff.md` / `CLAUDE.md` への追記★（★読んだだけです★）
- ❌ ★`docs/handoff/TeamWorks確認事項_第9回_20260915.md` の更新★（★別のエージェントが編集中★）
- ❌ ★`docs/spec.md:45` の書き換え★（★9章の判断3。ユーザーが決めること★）
- ❌ ★git のコミット・push・`git add`★
- ❌ ★テストDBへの書き込み★（★`SELECT` と `SHOW VARIABLES` と `EXPLAIN` のみ★）
- ❌ ★本番相当の行数での実測★（★行数を増やすには書き込みが要るため。8-2節・10章のとおり申し送りにしました★）

## 12. ★★未確認★★

| # | 内容 |
|---|---|
| ★①★ | ★★3-6節・6-2節の数値は外挿です★★。★実測は2章の「2026-09-17 のテストDB（538行）」と、D-136 が残した「31日・502行・0.73秒」「1行111バイト」のみ★ |
| ★②★ | ★「ロボット30台未満」はユーザーの申告であり、DB では確認していません★（★テストDBの `robots` は14行で、テストデータです★） |
| ★③★ | ★1台1日あたりの清掃回数（1.00 / 1.29 / 1.50）は★★テストDBのスケジュールから取った比★★です。★本番のスケジュールが同じ密度とは限りません★ |
| ★④★ | ★判定の分布（△× = 37.0%）もテストデータです★。★本番の問題発生率はこれより低いと見込まれます（＝安全側）が、確認していません★ |
| ★⑤★ | ★`eq_range_index_dive_limit = 200` は★★テストDB★★での実測です。本番／ステージングは未確認★（10-2節 #6） |
| ★⑥★ | ★nginx の既定タイムアウト60秒という前提は D-136 2節の記述によるもので、本書では確認していません★ |
| ★⑦★ | ★本書は調査担当が書いたもので、★★テスト・lint・型チェックは実行していません★★（★コードを変更していないため★） |
