# Health Check TableFullScan 报告问题分析

报告信息：

- Org ID: `1372813089196930499`
- Cluster ID: `10989049060142230334`
- Report ID: `83883563-f4f4-43ac-a5ae-fa20cb6bd3e1`
- 时间范围：`2026-05-13T01:00:22.251Z` ~ `2026-05-14T01:00:22.251Z`

## 核心问题

这次报告暴露出 4 个产品问题：

1. **TableFullScan top 100 不是 100 个同类问题。**
   这里混在一起的有批量扫描、报表聚合、复杂 join、普通查询扫描放大，不能统一给“加索引/优化 SQL”的建议。

2. **只按 digest 展示会误导用户。**
   多租户 schema 里的 `dh_app_*` 会让同一个扫描模板拆成多个 digest，用户看到的是几十条重复问题，而不是一个 workload family。

3. **很多高风险 query 不是业务在线查询，而是批量扫描任务。**
   例如安全扫描/数据画像类 SQL，会扫完整列内容。它们的治理方式应该是隔离、限流、错峰、离线化，而不是优先建议建普通索引。

4. **当前报告缺少“是否真的有风险、是什么风险、该怎么做”的分层。**
   现在主要展示原始表格，用户还要自己判断哪些 query 需要处理。

## 这次报告里的关键数字

`tablefullscan_slow_query` 返回了 top 100 digest。仅这 100 条就已经很多：

- `93/100` 同时出现在 `slow_query_hugeResourceUsage`。
- `82/100` 的 `p95_latency_s >= 10`。
- `82/100` 的 `max_scanned_rows >= 50,000,000`。
- top 100 累计扫描约 `127,647,052,759` 行。
- 最大单次扫描 `1,869,828,367` 行。

粗略分类：

- `73` 条：数据画像/安全扫描类 batch SQL。
- `21` 条：报表聚合类 SQL。
- `2` 条：复杂 join 或 latest snapshot 查询。
- `4` 条：普通查询形态，但扫描明显放大。

结论：这 100 条都有资源风险，但处理优先级和建议动作不同。

## 问题一：digest 维度不够

这类 SQL 会因为 schema 名不同拆成不同 digest：

```sql
FROM `dh_app_5395`.`dh_user_vip_gift`
FROM `dh_app_5532`.`dh_user_vip_gift`
FROM `dh_app_5567`.`dh_user_vip_gift`
```

TiDB digest 会归一化常量，但不会把 schema/table identifier 归一化。因此，同一个逻辑扫描模板会被拆成多个 digest。

这次 top 100 里，`dh_user_vip_gift / site_code` 这个 family 就有 `17` 个 digest。

产品上应该增加 `query_family` 视角，例如：

```text
schema_pattern = dh_app_*
table_name = dh_user_vip_gift
column_name = site_code
predicate_pattern = lower(column) like leading-wildcard security keywords
```

用户应该先看到“一个 family 有多少 digest、总共扫了多少行、影响多大”，再展开看 digest 明细。

## 问题二：data profile batch 不该按普通慢 SQL 处理

一个完整 case：

```sql
SELECT 'dh_app_5395' AS db_name,
       'dh_user_vip_gift' AS table_name,
       id,
       'site_code' AS column_name,
       LEFT(`site_code`, 500) AS column_value
FROM `dh_app_5395`.`dh_user_vip_gift`
WHERE `site_code` IS NOT NULL
  AND (
    LOWER(`site_code`) LIKE '%<script%'
    OR LOWER(`site_code`) LIKE '%eval(%'
    OR LOWER(`site_code`) LIKE '%fetch(%'
    OR LOWER(`site_code`) LIKE '%xmlhttprequest%'
    OR LOWER(`site_code`) LIKE '%navigator.sendbeacon%'
    OR LOWER(`site_code`) LIKE '%onerror=%'
    OR LOWER(`site_code`) LIKE '%onload=%'
    OR LOWER(`site_code`) LIKE '%onfocus=%'
    OR LOWER(`site_code`) LIKE '%onmouseover=%'
    OR LOWER(`site_code`) LIKE '%onclick=%'
  );
```

证据：

- digest: `bba1b754b0589ffcd7d194c8942d598ad12e1f48d2583fd7544528276696f874`
- `tablefullscan_slow_query.slow_count = 1`
- `slow_query_hugeResourceUsage.hit_records = 3`
- `max_query_time_s = 180.02`
- `max_scanned_rows = 1,869,828,367`
- sample `process_keys = 1,869,410,682`
- sample `result_rows = 0`

这条 SQL 的问题不是普通业务查询没有走好索引，而是它本来就在做大范围内容扫描：

- `LOWER(column)` 包住了列。
- `LIKE '%xxx%'` 是前导通配符。
- 查询目标是扫描列内容，检查是否包含一批安全关键词。
- 扫了约 `1.87B` keys，最后返回 `0` 行。

因此建议不应该是“加普通索引”。更合理的建议是：

- 放到离线链路、只读副本或专门的分析环境跑。
- 如果必须跑在线集群，需要限流、错峰、Resource Group 隔离。
- 按 `query_family` 聚合类似 `dh_app_*` 扫描，不要让用户逐条处理重复 digest。

## 问题三：没有 LLM 也可以打标

第一版不需要 LLM，可以用确定性规则。

### Query 类型标签

`data_profile_batch`：

- 包含 `AS db_name`、`AS table_name`、`AS column_name`。
- 选择 `LEFT(column, N) AS column_value`。
- 包含多个 `LOWER(column) LIKE '%...%'`。

`reporting_aggregate`：

- 包含 `COUNT/SUM/AVG`。
- 有 `GROUP BY`。
- 通常有时间范围、大 `IN` 列表、join 维表。

`join_latest_snapshot`：

- join 子查询。
- 子查询里有 `MAX(day/time)` 和 `GROUP BY`。

`pagination_or_search`：

- 有 `ORDER BY ... LIMIT`。
- 或 `LIKE '%keyword%'` 搜索。

`unknown_table_scan`：

- 是 TableFullScan，但没命中以上规则。

### 风险标签

| 标签 | 规则 |
| --- | --- |
| `high_latency` | `p95_latency_s >= 10` 或 `max_query_time_s >= 40` |
| `large_single_scan` | `max_scanned_rows >= 50,000,000` |
| `high_frequency` | `slow_count >= 100` |
| `extreme_slow_overlap` | digest 出现在 `slow_query_hugeResourceUsage` |
| `wasteful_scan` | `process_keys` 很高但 `result_rows` 很低 |
| `family_repeated` | 同一个 normalized family 下有多个 digest |

## 建议的产品形态

报告首页不要直接给 100 行表格。建议先展示 `Findings`：

- query family
- 类型标签
- 风险标签
- 影响 digest 数
- 总扫描行数
- 最大单次扫描行数
- 最大或 P95 latency
- 推荐动作

原始 digest 表格保留，但放在 evidence/detail 里。

推荐动作按类型区分：

| 类型 | 建议 |
| --- | --- |
| `data_profile_batch` | 隔离、限流、错峰、离线化 |
| `reporting_aggregate` | 复合索引、预聚合、汇总表 |
| `join_latest_snapshot` | 改写 latest-row 查询，补 grouping/lookup 索引 |
| `pagination_or_search` | 避免前导通配符，使用搜索索引或前缀搜索设计 |
| `unknown_table_scan` | 展示 plan、stats、谓词、索引证据，让用户继续 drill down |

核心目标：把“发现很多 query”变成“告诉用户先处理哪几类问题，以及每类该怎么处理”。
