本节摘要:SQL 引擎接手一条语句的第一站是解析与语义分析:词法切词、语法建树、语义查目录校验。绝大多数"表不存在、列不存在、类型不匹配"类报错发生在这一站——此时数据库还没碰任何数据。分清报错来自哪一小站,排查速度差一个量级。更实际的是,这一站的报错九成与应用与库结构不同步有关
"column xxx does not exist"与"relation xxx does not exist"看着像双胞胎,其实出生在不同小站:前者是语义分析阶段查系统目录时发现列缺失,后者可能早在语法树构建后解析对象名时就已注定。交付现场的分诊价值在于:解析期的报错与数据无关、与负载无关、与参数基本无关,九成是应用与库结构不同步——发版漏脚本、环境间结构漂移、大小写与引号习惯不一致。把这一站吃透,一大类工单可以直接秒杀。
第一小站词法分析把字符流切成记号:关键字、标识符、常量、运算符。奇怪的报错常出在这里——全角引号、中文逗号、不可见字符,都会让切词直接失败。第二小站语法分析按文法把记号组织成语法树,语法错(syntax error)报在这一站,并贴心地指出出错的字符位置。第三小站语义分析拿着语法树查系统目录:表存在吗、列存在吗、函数签名匹配吗、类型能隐式转换吗、你有权限吗。第四小站查询重写做规则性改写,典型如视图展开——你查的是视图,到这一步已经被替换成底层表的查询树。产出查询树,交给第 4.2 节的优化器。
-- 语法错:第二小站拦截,注意提示里的位置标记 SELECT FROM orders WHERE id = 1; -- ERROR: syntax error at or near "FROM" 位置指向缺失的表达式 -- 语义错:第三小站拦截,语法对但对象不存在 SELECT order_id, user_ids FROM orders WHERE id = 1; -- ERROR: column "user_ids" does not exist -- 权限错:也在第三小站,对象在但你无权看 SELECT * FROM hr.salary; -- ERROR: permission denied for table salary -- 验证大小写陷阱:不带引号一律折叠为小写 SELECT * FROM Orders; -- 实际查的是 orders,成功 SELECT * FROM "Orders"; -- 精确匹配大写 O,报不存在(若建表时未加引号)
大小写这一条值得划重点:从 Oracle 或 MySQL 迁移来的团队常带"建表加引号"的习惯,之后所有不带引号的查询全部失灵。交付规范建议统一小写命名、建表不加引号,把这类分歧行政化消灭在开发规范里。
类型系统在第三小站做一件默默无闻却影响深远的事:隐式类型转换。应用传入字符串参数与整型列比较,内核悄悄把列转成字符串(或反之),转换一旦落在列上,索引的有序性即告失效——计划层面表现为索引扫描退化为全表扫描,问题却报在性能而非语法。所以"这条 SQL 突然变慢"的工单里,有一类要先回 4.1 找原因:绑定参数类型变了、驱动升级改变了参数传递方式、SQL 拼接把数字写成了字符串。预防手段是把参数类型写进开发规范:预编译语句显式指定类型,不让驱动猜。
背景:某系统例行发版后,海量接口同时报"relation order_item does not exist",监控一片红。按本节框架分诊:报错全部在第三小站、且只涉及一张表、生产与预发同时报错——结构漂移的嫌疑最大。核对发版脚本,发现本次新增索引的脚本里混入了一条对临时中间表的清理语句,而预发库的这张表是手工建的、生产从未建过。处置:生产补建该表(本来就该有),发版流程增加"脚本与结构基线比对"环节,任何对象增删必须出现在结构变更清单里。复盘结论:解析期报错的路由价值——凡第三小站的批量报错,先查结构与发版一致性,别去查内核、参数和负载。
把解析期知识嵌入发布流程,能在上线前拦下一类问题。做法是在发版流水线里加两道检查。第一道,对象存在性预检:脚本在目标环境执行前,先解析出脚本引用的全部表、列、函数,与目标库的系统目录比对,缺哪个对象直接拦截——上面那次"批量 relation does not exist"事故若挂了这道检查,根本走不到生产。第二道,方言合规扫描:对脚本做静态扫描,标记函数包列、隐式转换风险、非小写命名这类在 4.2 会发酵的模式,标出来让人工确认。两道检查的成本是一次流水线步骤,收益是把"解析期报错"从生产事故降级为流水线红字。交付项目的发版规范里,建议把这两道检查列为强制门禁——它们的性价比在整个流水线里名列前茅。
语义分析对函数的处理有一处容易踩的细节:同名函数按参数类型选择实现。字符串拼连接操作符在不同方言里写法不同,日期加整数的语义在不同库有差异(加天还是加秒),这些差异都会在解析期"顺利通过"、在运行期给错结果——语法对、语义错,比语法错误危险得多。防御手段:项目级约定公共函数清单,跨方言的写法封装到统一函数里;迁移项目的回归用例必须覆盖日期运算与字符串处理的典型场景。另外一个冷知识:类型转换的优先级规则决定表达式里哪个操作数被转换,复杂表达式显式加转换是最便宜的保险——显式转换不费性能,隐式转换选错方向才费。
问答一:"同一条 SQL 在测试环境正常,到生产就报对象不存在,为什么?"——两环境结构漂移,用 1.3 的环境坐标系卡逐对象核对,重点怀疑大小写引号与模式前缀差异。问答二:"SQL 里写了注释,解析时会被丢掉吗,会不会影响性能?"——注释在解析期被剥离,不进执行路径,长注释没有性能代价;但注释会被部分语句日志保留,注意别在注释里写敏感信息。问答三:"预编译语句的执行计划会被缓存吗,换参数还重新解析吗?"——解析结果与计划在会话或全局范围内有缓存复用机制,这正是预编译快的原因之一;副作用是首次执行要付解析与优化的冷启动成本,压测时要预热典型语句。三则问答分别对应漂移、注释、预编译三个高频困惑,值班群里转发即用。
解析期报错里最冤的一类是"看起来一模一样的 SQL 就是不通过",元凶几乎都是不可见字符。第一类,全角符号:中文输入法的全角引号、全角逗号、全角括号,人眼难辨,词法直接拒绝——从聊天记录或文档里复制 SQL 时高发。第二类,零宽字符与不可见控制符:网页复制粘贴的文本可能夹带零宽空格,粘贴进编辑器毫无痕迹,解析直接报错;处置办法是用文本工具显示不可见字符或重新键入。第三类,行尾序列差异:跨平台编辑留下的混合行尾偶发干扰多行语句的解析。三类问题的共同防御是流程性的:SQL 上线走版本库与发布管道,杜绝"从聊天工具直接粘到生产客户端"的操作路径。这些细节小到不值一提,但它们消耗的排障时间加总起来相当可观——把"可疑就重打一遍"变成条件反射,可以秒杀这一整类工单。
文本变成了合法的查询树,接下来轮到本章的核心角色:优化器如何为它挑选一条执行路径。