ODPS / MaxCompute SQL 调优
MaxCompute 按扫描量计费。最贵的一类事故不是「语句报错」,而是语句跑通了、账单炸了——
分区表没写分区过滤,或 WHERE SUBSTR(dt, 1, 6) = '202601' 把函数套在分区字段上,
分区裁剪直接失效,一次查询扫遍全部分区。这类写法不报错,只静悄悄地烧钱。
本工具对 .sql 做纯静态文本分析,交付四样东西:风险清单(78 条规则,其中 33 条是
「提交即报错」级官方硬约束,逐条带官方依据)、确定性改写后的可提交 SQL、扫描量与费用估算、
Markdown / CSV / 自包含 HTML 报告(含目录批量体检)。可选接入 EXPLAIN 做算子级实证。
执行原则
- 只读不连库。 只解析你给的文本:不连接 MaxCompute、不执行任何 SQL、不读表元数据;不要声称查过真实数据量、真实分区或真实执行计划。
- 版本先确认。 结论依赖
--odps-edition(默认2.0);不要在没确认项目版本时下版本相关结论——OE32/OE33极易把合规的 2.0 写法误改成 1.0 写法。 - 能确定才改。 只对语义可确定推导的写法落盘(分区裁剪区间、白名单内方言函数、
TOP→LIMIT);凡可能有第二种解释的一律只给建议,不擅自改动业务语义。 - 凭据只掩码。 疑似 AccessKey / 口令的说明行与语句片段一律掩码;不要在报告、日志或对话里回写明文,并提示轮换——原始脚本仍含明文。
- 不夸大证据。 静态结论只是「写法疑点」,只有显式提供
--explain才有算子级实证;不要把「疑似」说成「已证实」,也不要把估算费用说成实际账单。
何时使用
CRITICAL: 只要用户提出「这条 MaxCompute / ODPS SQL 为什么这么贵、扫了多少数据 / 分区裁剪有没有失效、能不能改 / 账单超标是哪几条语句在扫全表」,就必须使用本 skill,即使看起来可以用别的方式解决。
- 数仓作业上线前、日常调度脚本改完要提交前,先过一次体检。
- 提交前想先排掉「提交即报错」:作业跑不起来往往不是业务逻辑错,而是触发了官方语法
与使用限制(JOIN 无等值条件、CROSS JOIN、GROUP BY用输出别名、HAVING非布尔、
窗口函数与同层聚合混用、DISTRIBUTE BY次序颠倒…)——这一类由OE41–OE73覆盖。 - 脚本里用了
WITH/ 集合操作 / UDTF / 事务表 /SET调参:想知道 CTE 会不会被重复计算、
UDTF 该怎么写才不报错、UPDATE能不能直接跑、调参写对了没有——
对应OE61–OE73这一组。 - 收到账单异常或「SQL 成本超标」告警,想定位是哪几条语句在扫全表。
- 从 MySQL / Hive / Spark / SQL Server 迁到 MaxCompute,脚本里混着旧方言和旧语法。
- 迁移或换项目:老项目 1.0 数据类型版本、新项目 2.0,想知道脚本能不能直接搬。
- 想把问题改完:不想只看清单,希望直接拿到一份改好的 SQL 与一份能交出去的优化报告。
- 手上有
EXPLAIN输出:怀疑某条语句扫了全表、或想确认某个 JOIN 是不是退化成笛卡尔积,
可以把计划一起喂进来做算子级实证(--explain),拿Data size说话而不是靠猜。 - 做成本专项:想知道「把这几个写法改掉,一个月能省多少 GB」。
- 做存量治理:手上一整个目录的
.sql,想按风险排出改造优先级。
怎么用
# 体检(只看问题清单)
python3 scripts/odps_sql_optimizer.py 作业.sql
# 改完:套用确定性改写,落盘一份可直接提交的 SQL
python3 scripts/odps_sql_optimizer.py 作业.sql --rewrite --out 作业_odps.sql
# 出报告 + 方案:生成 Markdown 报告,终端同时打印分优先级改造步骤
python3 scripts/odps_sql_optimizer.py 作业.sql --report 体检报告.md --plan
# 带表元信息,估算扫描量与费用
python3 scripts/odps_sql_optimizer.py 作业.sql --meta 表元信息.json
# 存量治理:整个目录批量体检,出一份合并报告
python3 scripts/odps_sql_optimizer.py --dir 数仓作业目录/ --report 全量体检.md
# 上线前只盯「提交即报错」与全表扫描的高危项
python3 scripts/odps_sql_optimizer.py 作业.sql --min-severity 高
# 档位预设:quick 只看高危 / standard 看高+中 / strict 全部(默认)
python3 scripts/odps_sql_optimizer.py 作业.sql --profile quick
# 有 EXPLAIN 输出时叠加算子级实证;不提供则结果与此前完全一致
python3 scripts/odps_sql_optimizer.py 作业.sql --explain 计划.txt --report 体检报告.md
# 丢给工单 / 看板:导出 CSV,或生成一份自包含 HTML 报告(可直接发出去)
python3 scripts/odps_sql_optimizer.py 作业.sql --format csv > 问题清单.csv
python3 scripts/odps_sql_optimizer.py 作业.sql --html 体检报告.html
# 查规则表:全部 78 条规则码、严重度、官方依据与核对日期
python3 scripts/odps_sql_optimizer.py --list-rules
# 冒烟:跑内置演示样例
python3 scripts/odps_sql_optimizer.py --sample
退出码:0 = 未发现问题;1 = 发现问题或存在改写项;2 = 输入/参数错误。
全部命令行参数(含 --rules / --ignore / --min-severity / --profile 过滤与 --list-rules)见 08 §1。
按需读取(先查 00,再只读命中章节)
> 读法:先读 00-路由索引 定位到「文件号 + 章节 + 行号区间」,
> 再用 read_file(offset, limit) 只读命中的那一章节;禁止整文件读取任何 reference,
> 也不要一次把 01~10 全读进来。规则码、严重度、官方依据与核对日期以脚本 --list-rules 为准,
> 不要凭记忆复述。
| 当前任务 | 读哪里 |
|---|---|
| 不确定读哪个 / 要查章节行号 | 00 |
版本基线:OE20/OE21/OE25/OE32/OE33 怎么解释 |
01 §1 |
OE1–OE40 的含义、严重度与官方依据 |
02 §1–§4 |
OE41–OE60「提交即报错」与去噪约定 |
03 §1–§2 |
OE61–OE73 集合操作 / CTE / UDTF / 事务表 / SET |
04 §1–§2 |
有 EXPLAIN 输出,做 XP1–XP5 算子级实证 |
05 §1–§3 |
| 哪些能自动改、哪些必须人工 | 06 §1–§3 |
| 扫描量费用估算 / 参数建议与改造步骤 | 07 §1–§2 |
| 命令行参数与过滤 | 08 §1–§2 |
| 输出示例、报告与输入切分 | 09 §1–§4 |
| 误报边界、凭据处置、排错 | 10 §1–§3 |
高频规则摘要
完整 78 条以 --list-rules 为准(含严重度、官方依据与依据核对日期)。最常命中的几条:
| 规则码 | 一句话 | 严重度 |
|---|---|---|
OE1 / OE2 / OE40 |
分区表没过滤 / 分区列被函数包裹 / 用 LIKE 前缀——裁剪失效,按全表计费 |
高 |
OE52 |
分区过滤写在 ON 子句:主表仍全表扫描,语句能跑通、账单照付 |
高 |
OE41 / OE53 |
JOIN 无等值条件 / 显式 CROSS JOIN——提交即报 ODPS-0130071 |
高 |
OE3 / OE39 |
INSERT OVERWRITE 未指定分区 / 目标表同时是源表——可能一次覆盖全部分区 |
高 |
OE4 / OE5 |
SELECT * 全列扫描 / ORDER BY 无 LIMIT 全局单点排序 |
中 |
OE14 |
疑似硬编码 AccessKey / 口令(说明与片段均掩码,并提示轮换) | 中 |
OE37 |
NOT IN (子查询):子查询含 NULL 时静默返回空结果 |
中 |
常见错误 vs 正确做法
| 常见错误做法 | 正确做法 |
|---|---|
没给 --meta 就说「这条 SQL 扫了 500 GB」 |
只说「写法上分区裁剪失效」,补一句「给 --meta 才能估算扫描量」 |
不问版本就判 OE32,让 2.0 项目去改数据类型 |
先确认 --odps-edition;2.0 下 OE32 不触发 |
把 OE25(GROUPING SETS)说成「语法不支持」 |
说是开销提示:2020-02 起支持,早期文档的「不支持」表述已过时 |
| 把自动改写产物直接当交付提交 | 只保证按确定规则替换;必须人工复核 + 回归后再提交 |
| 报告里回填疑似凭据的原文 | 一律掩码并提示轮换;报告已掩码但原始脚本仍含明文 |
没给 --explain 却输出 XP* 结论 |
XP1–XP5 只在显式提供 --explain 时参与,不给计划就不提 |
| 把估算费用说成实际账单 | 明说是量级估算,实际以官方计费页为准 |
输出与交付
- 终端文本按 高 / 中 / 低 分级,逐条给行号、语句片段、建议与官方依据;另有
--json/--format csv/--html三种可机读与可分发的形态。 --rewrite --out落盘改写后的 SQL,文件头注释写明「未自动替换的项及原因」;--report出 Markdown 报告,--dir出目录批量体检汇总。- 交付时必须说清:命中条数与分级、哪些已自动改写、哪些必须人工确认、本次版本基线,以及未纳入估算的表;不要把估算费用说成实际账单。
- 需要人工组织或补充时,用交付物模板 assets/体检报告模板.md:复制后填充,不适用项写「不适用」,不要留空。
能力边界与红线
- 纯静态文本分析:不连库、不执行 SQL、不评估真实数据分布与倾斜;规则基于文本模式,存在少量误报与漏报(逐条说明见
10§1)。 - 版本基线是「声明值」而非探测值:不填
--odps-edition时按2.0判断,1.0 专属问题(OE32)不会被报出。 --explain只按文本解析你给的执行计划;不是EXPLAIN输出会明确跳过,不硬解析出误报。- 凭据红线:疑似凭据一律掩码输出,不回写明文;报告已掩码但原始脚本仍含明文,须提示轮换。
- 自动改写产物必须人工复核后再提交——工具只保证按确定规则替换,不判断业务意图是否正确。
免责声明
本技能给出的是写法层面的风险提示与量级估算,不是最终结论,也不构成费用承诺:
扫描量与费用是按你提供的表元信息推算的量级值,不是实际账单,实际口径与折扣以官方计费页为准;
版本相关结论取决于你填写的 --odps-edition,不是对线上项目的探测结果;
自动改写产物必须经人工复核与回归验证后再提交——因未复核直接提交造成的数据覆盖、结果异常或费用损失,不由本工具承担。
依赖
Python 3.12+ 标准库,无第三方包;只读输入文件,无网络请求,无 subprocess / eval / exec。