128 lines
6.8 KiB
Markdown
128 lines
6.8 KiB
Markdown
---
|
|
name: sql-review
|
|
description: 对执行前生成好的 PySpark SQL 字符串做静态评审时使用。触发词包括"review this SQL"、"SQL 评审"、"静态审查"、"risk level"、"check join cardinality"、"数据膨胀"、"重复聚合"、"分区裁剪"。消费 SQL 字符串及其产出的 SQL Context,产出按风险分级的评审结果(HIGH / MEDIUM / LOW)以及具体修复建议。**不修改 SQL** — 只报告发现。
|
|
---
|
|
|
|
# SQL Review(SQL 评审)
|
|
|
|
## 概述(Overview)
|
|
|
|
对一条生成好的 PySpark SQL 字符串做执行前的静态评审。评审者**不**改写 SQL — 它只输出发现项、严重程度和建议修复。由 SQL 的作者决定采纳哪些。
|
|
|
|
核心原则:**对照 SQL Context 评审,而不是凭直觉。** SQL 中引用的每一个字段、alias、join key 都必须存在于产出它的 SQL Context。如果不存在,这就是一项 `HIGH` 风险(SQL 绕过了契约)。
|
|
|
|
## 何时使用(When to Use)
|
|
|
|
**使用场景:**
|
|
|
|
- 一条 SQL 字符串已经从 `sql_context` 生成出来,即将执行
|
|
- 用户粘贴一条 SQL 字符串,要求评审、优化或风险评估
|
|
- 代码改动为 PySpark 脚本新增了 `spark.sql(...)` 调用
|
|
|
|
**不要使用场景:**
|
|
|
|
- 这条 SQL 不是 Spark SQL(例如纯 Postgres、HiveQL 在 Spark 之外)— 至少要把 Spark 特有的检查替换成对应引擎的等价检查,并标注出来
|
|
- 是一次性、不会被复用的交互查询 — 用户可以主动选择跳过
|
|
|
|
## 输入(Inputs)
|
|
|
|
1. 要评审的 **SQL 字符串**
|
|
2. 产出这条 SQL 的 **SQL Context**(如果缺失,需要显式声明"该 SQL 是不可信 / 临时拼凑的")
|
|
|
|
如果 SQL Context 缺失,抛出一项 `HIGH` 风险:*"SQL 没有对应的 SQL Context;下游 skill 无法保证字段 / join 的正确性。"* 然后继续做结构性检查。
|
|
|
|
## 严重程度模型(Severity Model)
|
|
|
|
| 等级 | 含义 | 处置 |
|
|
|---|---|---|
|
|
| `HIGH` | 会导致数字错误、执行失败或违反策略 | 阻断执行,必须修。 |
|
|
| `MEDIUM` | 大概率对,但低效、脆弱或大规模场景有风险 | 上线前修,开发环境可放过。 |
|
|
| `LOW` | 风格 / 最佳实践小瑕疵 | 可选修。 |
|
|
|
|
按以下顺序输出发现项:`HIGH` → `MEDIUM` → `LOW`。任一 `HIGH` 让整体评审结论为 `FAIL`。
|
|
|
|
## 工作流(Workflow)
|
|
|
|
```dot
|
|
digraph sql_review {
|
|
"解析 SQL(逻辑层面,不只是正则)" [shape=box];
|
|
"对照 SQL Context 交叉校验 alias、字段、join key" [shape=box];
|
|
"检测多语句 / 禁用 verb(防护栏)" [shape=box];
|
|
"分析 join 基数与膨胀风险" [shape=box];
|
|
"检测同一度量的重复聚合" [shape=box];
|
|
"检测全表扫描与缺失分区过滤" [shape=box];
|
|
"校验 GROUP BY 与 SELECT 一致" [shape=box];
|
|
"检查 join key 与度量的空值处理" [shape=box];
|
|
"检查字段类型与运算的匹配" [shape=box];
|
|
"汇总成评审结论" [shape=box];
|
|
|
|
"解析 SQL(逻辑层面,不只是正则)" -> "对照 SQL Context 交叉校验 alias、字段、join key";
|
|
"对照 SQL Context 交叉校验 alias、字段、join key" -> "检测多语句 / 禁用 verb(防护栏)";
|
|
"检测多语句 / 禁用 verb(防护栏)" -> "分析 join 基数与膨胀风险";
|
|
"分析 join 基数与膨胀风险" -> "检测同一度量的重复聚合";
|
|
"检测同一度量的重复聚合" -> "检测全表扫描与缺失分区过滤";
|
|
"检测全表扫描与缺失分区过滤" -> "校验 GROUP BY 与 SELECT 一致";
|
|
"校验 GROUP BY 与 SELECT 一致" -> "检查 join key 与度量的空值处理";
|
|
"检查 join key 与度量的空值处理" -> "检查字段类型与运算的匹配";
|
|
"检查字段类型与运算的匹配" -> "汇总成评审结论";
|
|
}
|
|
```
|
|
|
|
## 检查项(Checks,全部执行)
|
|
|
|
1. **引用完整性(Reference integrity)** — SQL 中的每个 `<alias>.<column>` 都必须存在于 SQL Context。不匹配即 `HIGH`
|
|
2. **Join key 匹配** — 每个 `ON` 谓词都必须对得上 SQL Context 中已固化的 join。凭空发明的 join 算 `HIGH`
|
|
3. **禁用 verb / 多语句** — 交给 `pyspark-sql-guardrails`。任何 DDL/DML/维护 verb,或任何 `;` 堆叠的语句,均为 `HIGH`
|
|
4. **Join 基数风险** — 对每个 join,如果 SQL Context 标记基数为 `1:N` 或 `N:N` 且没有去重,分别抛 `MEDIUM`(1:N)或 `HIGH`(N:N)
|
|
5. **重复聚合** — 同一度量被聚合两次(例如子查询里一次、外层再一次)且非有意重新聚合,抛 `HIGH`
|
|
6. **分区 / 时间过滤** — 对任何有已知时间字段的事实表,缺少对该字段的 `WHERE` 过滤即为 `HIGH`(全表扫描风险)
|
|
7. **GROUP BY 完整性** — SELECT 中每个未聚合的列都必须出现在 `GROUP BY` 中。不匹配为 `HIGH`
|
|
8. **空值处理** — 可空 join key 上做 `INNER JOIN` 会静默丢行(`MEDIUM`);可空度量未用 `COALESCE` 聚合,可能产出 `NULL` 进而破坏下游 `WHERE ... > 0`(`MEDIUM`)
|
|
9. **类型不匹配** — 字符串列未做 cast 就参与数值聚合(`MEDIUM`);日期字符串和 `TIMESTAMP` 字面量比较(`MEDIUM`)
|
|
10. **Spark 最佳实践** — `COUNT(*)` vs `COUNT(1)` 属于风格(`LOW`);生产环境用 `SELECT *` 算 `MEDIUM`;数据倾斜 join 缺 `DISTRIBUTE BY` / `CLUSTER BY` 算 `LOW` 到 `MEDIUM`
|
|
|
|
## 输出契约(Output Contract)
|
|
|
|
```yaml
|
|
sql_review:
|
|
verdict: <PASS | FAIL>
|
|
highest_severity: <HIGH | MEDIUM | LOW>
|
|
summary: "<一句话总结>"
|
|
findings:
|
|
- id: F1
|
|
severity: HIGH
|
|
check: reference_integrity
|
|
location: "<行/列号或 SQL 片段>"
|
|
message: "<错在哪>"
|
|
fix: "<具体的建议修改>"
|
|
- id: F2
|
|
severity: MEDIUM
|
|
check: join_cardinality
|
|
location: "JOIN customer_info"
|
|
message: "1:N join without pre-aggregation; risk of row inflation"
|
|
fix: "Aggregate loan_order by customer_id before joining"
|
|
notes:
|
|
- "SQL Context provided: yes (sql_context.aliases = [...])"
|
|
- "Reviewed by: sql-review v1"
|
|
```
|
|
|
|
## 禁止行为(Forbidden Behaviors)
|
|
|
|
| 自我说服 | 现实 |
|
|
|---|---|
|
|
| "SQL 看起来没问题,不用对一遍 context" | 字段可能"看起来对"但源表里根本没有,必须做交叉校验。 |
|
|
| "我顺手在评审时把 SQL 修了" | 评审只报告,修是作者的事。职责分离。 |
|
|
| "没给 SQL Context,那字段检查就跳过" | 缺 context 本身就是 `HIGH`,绝不能悄悄跳过。 |
|
|
| "只是开发查询,分区检查跳过" | 这一项成本极低,跳过就会留下静默的全表扫描。 |
|
|
| "1:N join 是常态,没风险" | 在保留粒度的前提下 1:N 是 OK 的,但评审必须验证,不能假设。 |
|
|
| "用正则就够了,能查 GROUP BY" | GROUP BY 成员关系是结构性的,要用解析器,不要用正则。 |
|
|
|
|
## 完成判定(Completion Criteria)
|
|
|
|
评审完成的条件:
|
|
|
|
- 全部 10 项检查都已评估
|
|
- 每一条 `HIGH` 风险都有 `fix`
|
|
- 当且仅当 `highest_severity` 为 `MEDIUM` 或 `LOW` 时,`verdict` 才为 `PASS`
|
|
- 输出的 YAML 可直接贴到 PR 评论,无需修改
|