简介:SQL-99标准是关系数据库管理系统发展中的重要里程碑。这份PDF文档系统整理并讲解了SQL-99的核心内容与设计思路,面向数据库开发人员、运维工程师、高校数据库课程学习者,以及需要从旧版标准平滑迁移的团队。文档从SQL-92的关系模型讲起,逐一介绍SQL-99新增的布尔类型、二进制大对象(BLOB)、字符大对象(CLOB)、枚举类型、行类型、集合类型、多重集类型、列表类型与抽象数据类型,并结合图像、音频、视频等多媒体数据的实际存储需求,解释了这些类型在复杂查询、逻辑判断、数据校验等场景中的应用。操作符与函数方面,文档还重点说明了时间间隔运算、日期时间精确处理,以及大对象数据上的连接、替换、长度校验等能力,便于读者理解新语法在生产环境中的具体用法。文档同时兼顾与传统关系数据库的兼容性,使既有系统在升级过程中保持平稳过渡。资源包内仅包含一个PDF文档,大小约483KB,便于离线查阅和按需检索;已有558人学习下载。通过这份资料,读者能够快速对照SQL-92看清SQL-99的关键变化,掌握新增数据类型与操作符的使用场景,并将其用于数据库建模、查询优化和复杂业务逻辑设计,是一份紧贴实际、性价比很高的标准速查参考。
1. SQL-99.pdf 到底是什么:从一份规范文档到少踩三年数据库兼容性的坑
手里出现 SQL-99.pdf 的时候,多半不是因为你闲下来想读规范,而是线上数据库开始闹脾气了。同一个递归查询在 PostgreSQL 里跑得顺顺当当,换到 MySQL 8.0 之前直接语法报错;一张表加了个时间字段之后,NATURAL JOIN 的结果集悄无声息少了一截。SQL-99 是 ISO/IEC 9075:1999 标准,定义了 SQL 语言的数据类型、查询表达式、连接语义、游标、触发器和存储过程的行为边界,也是现代主流数据库在 SQL 语法上对齐的公共基准。这份文档适合数据平台工程师、DBA 和写复杂 SQL 的后端开发,解决的不只是某个语句怎么写,而是当你面对两个库行为不一致时,能回到标准原文找到判断依据。这篇笔记就按我实际读这份 PDF 的路径展开:先抓住标准引入的核心能力,再把它映射到手上的数据库,最后落到验证方法上。
2. SQL-99 的三大硬核能力:连接扩展、窗口函数与递归查询
2.1 连接语义从口头约定变成标准定义:ON、USING、NATURAL 怎么选
SQL-92 已经把显式 JOIN 写进标准,但 SQL-99 把连接类型和条件形式补齐了:INNER、LEFT/RIGHT/FULL OUTER、CROSS 一类,条件上分 ON、USING、NATURAL 三条路。很多人把这三者混着用,实际标准对它们的约束完全不同。
ON 是最通用的形式,两边任意列、任意表达式都能作为连接条件。它和 WHERE 里写等值条件有一个关键区别:ON 在左连接时决定右表保留哪些行,WHERE 是在连接全部完成之后再做过滤,这个执行顺序差异是排查行数对不上的第一现场。USING 要求两边列名完全一致,连接列在结果里只保留一份,适合两张表都用 department_id 这种命名一致的场景。NATURAL 则把两边所有同名列自动拿去等值连接,标准定义它等价于把这些列逐一展开放入 USING 列表。
-- 方案一:ON 显式指定连接条件 SELECT d.dept_name, e.emp_name FROM department d LEFT JOIN employee e ON d.dept_id = e.dept_id; -- 方案二:USING 要求两列同名 SELECT dept_name, emp_name FROM department d LEFT JOIN employee e USING (dept_id); -- 方案三:NATURAL 自动匹配所有同名列 SELECT dept_name, emp_name FROM department d NATURAL LEFT JOIN employee e;三条语句在 dept_id 是唯一同名列时结果一致。差异在表结构变更时暴露:如果 department 和 employee 都增加了 created_at 字段,NATURAL JOIN 的连接条件会自动多一个 created_at,结果集会比预想少很多行,而 ON/USING 不受影响。我在生产环境从不用 NATURAL,原因就一句话:连接条件应该被人看见,而不是被数据库猜。
CROSS JOIN 在标准里是笛卡尔积,不需要条件;自连接是把同一张表用不同别名连接,常用于逐层展开的树形结构。虽然在递归查询普及后自连接的频率下降,但在处理“取每条记录之后最近一条记录”这类场景时仍然是干净的做法。选连接方式时,把 ON 作为默认,USING 作为同名列简写,NATURAL 直接排除,是少踩坑的保守策略。
2.2 OLAP 函数与 OVER 子句:SQL-99 里窗口函数的原始形态
窗口函数在 SQL-99 里不叫窗口函数,标准文本的术语是 OLAP 函数,核心语法是函数名后面跟一个 OVER 子句。OVER 由 PARTITION BY 和 ORDER BY 组成,PARTITION BY 把结果集拆成若干分区,ORDER BY 决定每个分区内的计算顺序。它和 GROUP BY 最大的差异是:窗口函数不折叠行,查询返回的行数不变,只在每行旁边多出一列计算结果。
SELECT department_id, emp_name, salary, RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS dept_rank, ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS row_seq FROM employee;这里 PARTITION BY department_id 指定按部门拆窗,ORDER BY salary DESC 让高薪排在前。RANK 对并列工资赋予相同名次且后续名次跳跃,例如两个第二名并列时下一位是第四名;ROW_NUMBER 即使值相同也强制给出递增序号,适合分页或去重场景;如果既不想跳跃又要并列,用 DENSE_RANK。三个函数在标准里各有精确定义,实际报表需求不同就选不同函数。
窗口函数还有一个容易忽略的执行时点:它在 WHERE、GROUP BY、HAVING 之后,ORDER BY 之前计算。这意味着 WHERE 里不能直接写 dept_rank = 1 这种过滤,SQL 标准不允许在 WHERE 阶段引用 SELECT 列表中生成的别名,必须把窗口查询包一层子查询再过滤。很多业务报表的“取每个部门工资最高的人”需求,正确写法是先算窗口列,再在外层 WHERE 过滤,而不是在同一个查询里直接过滤窗口结果。我一般会先写内层查询把窗口列算出来,再包一层做过滤,这样既绕开标准限制,也让执行计划更可控。
2.3 WITH RECURSIVE 递归查询:终止条件才是标准真正约束的地方
递归查询是 SQL-99 引入的另一个范式级能力。它把“从一张表里反复 JOIN 自己直到不再产生新行”的过程封装成 WITH RECURSIVE 表达式。标准把它分成两部分:锚点成员先执行一次,产出初始结果集;递归成员反复引用上一轮结果,与基表进行连接,每轮输出新行;当一轮执行后没有新行产生,查询即终止。
WITH RECURSIVE emp_tree (emp_id, manager_id, depth, emp_name) AS ( -- 锚点成员:找出顶级管理者 SELECT emp_id, manager_id, 1, emp_name FROM employee WHERE manager_id IS NULL UNION ALL -- 递归成员:每轮连接上一轮结果 SELECT e.emp_id, e.manager_id, t.depth + 1, e.emp_name FROM employee e JOIN emp_tree t ON e.manager_id = t.emp_id ) SELECT emp_id, emp_name, depth FROM emp_tree ORDER BY depth;两个关键参数:depth 用于记录层级,既能输出路径深度,也是排查死循环的抓手;UNION ALL 表示每轮结果全部保留,如果换成 UNION 则自动去重,但代价是每轮要做一次去重比较。递归查询最常见的翻车点不是语法,而是数据里存在环,比如 A 的 manager 是 B,B 的 manager 又是 A。标准文本对这种情况没有魔法处理,数据库实现通常也会陷入无限循环。解决方法有两招:一是在递归成员里维护一个 visited_path 数组,把当前访问过的节点集合带进去,遇到重复节点就终止该分支;二是干脆限制递归深度,很多数据库提供递归上限参数,例如 PostgreSQL 里可以在递归成员加 WHERE depth < 10。生产环境用递归查询前,我习惯先写一段脚本扫描目标表确认不存在环,再放线上跑。
3. 拿到 SQL-99.pdf 该从哪读起:把标准拆成可执行的语法骨架
3.1 标准文档的卷结构:Foundation、CLI、PSM 各管什么
SQL-99 不是一份单文件规范,ISO/IEC 9075:1999 由多个部分组成。做应用开发最常打交道的是第二部分 Foundation,它定义了数据类型、表、视图、查询表达式、数据修改语句、游标和完整性约束;第四部分 Persistent Stored Modules 定义存储过程和过程化语言;第三部分 Call Level Interface 是 CLI 接口,也就是 ODBC 那套调用层定义;第五部分 Language Binding 处理 SQL 嵌入宿主语言的绑定规则。
第一次读这份 PDF 的人最容易犯的错是从头往后逐页读。我在给团队做标准导读时给出的阅读顺序是:先读 Foundation 里的数据类型和谓词,理解 NULL、三值逻辑、字符串比较规则;再读查询表达式,重点看 SELECT 的完整语法图和连接、子查询的约束;接着读数据修改语句和触发器;需要写存储过程时再翻 PSM。CLI 和 Language Binding 是给驱动开发和嵌入式场景准备的,后端工程师可以先放着。
| 卷编号 | 内容 | 日常使用频率 |
|---|---|---|
| 9075-1 Framework | 总体框架、概念、术语 | 低,但不读术语会看不懂其他卷 |
| 9075-2 Foundation | 核心 SQL:数据类型、查询、DML、游标、断言 | 最高,几乎每个问题都在这卷找答案 |
| 9075-3 Call Level Interface | CLI 调用层接口 | 低,驱动开发才需要 |
| 9075-4 Persistent Stored Modules | 存储过程与过程化 SQL | 中,写数据库函数时翻 |
| 9075-5 Language Binding | SQL 嵌入宿主语言的绑定 | 低,传统嵌入式开发场景 |
3.2 看懂语法图:尖括号、::=、方括号与竖线的含义
标准文本用文法方式描述 SQL 语法,第一次接触会觉得像在读编译原理。核心符号只有几个:::= 表示“定义为”,尖括号里的内容是语法类别名,方括号表示可选,竖线表示多选一,花括号表示可重复。理解这套符号之后,PDF 里的语法定义就能翻译成实际 SQL。
<query specification> ::= SELECT [ <set quantifier> ] <select list> <table expression>这个简化定义说的是:一个查询规格由 SELECT 关键字开头,后面可以跟一个可选的选择限定符,再跟选择列表和表表达式。set quantifier 就是 DISTINCT 和 ALL,select list 是输出列,table expression 包含了 FROM、WHERE、GROUP BY、HAVING 这些子句的排列。标准里完整的 SELECT 定义比这个复杂得多,但读法是一样的:先拆语法类别,再逐层替换成真实标识符。
看语法图还有一个技巧:当一个类别名出现两次以上时,说明这个位置支持递归结构,比如表达式可以嵌套表达式。这意味着你在写子查询、CASE 表达式多层嵌套时,标准早就预留了合法空间,数据库报语法错误往往不是标准不允许,而是实现没跟上。
3.3 一个 SELECT 的落地流程:从语法骨架到测试库验证
读完语法定义到真正跑通,中间隔着一个验证步骤。我的做法是三步走:先在 PDF 对应章节定位语法定义,再把占位符替换成实际表名和列名,最后在一个专门建的标准验证库里执行并记录差异。
-- 按 <query specification> 骨架替换后的实际语句 SELECT DISTINCT department_id, COUNT(*) AS emp_cnt FROM employee WHERE hire_date >= '2020-01-01' GROUP BY department_id HAVING COUNT(*) > 5 ORDER BY emp_cnt DESC;这条语句覆盖了 DISTINCT、聚合、过滤、分组、分组后过滤和排序六个环节。执行时关注三个参数:DISTINCT 是否参与行去重,HAVING 是否只能引用分组列或聚合结果,ORDER BY 能不能直接引用别名。不同数据库对 ORDER BY 别名、HAVING 里允许的表达式范围处理并不一致,这些差异都能在标准里找到对应条款。我在验证库跑完一遍后,会把“标准要求”和“本库行为”两列写进测试记录,后续换版本、换引擎就直接复用这套对照。
4. 把 SQL-99 映射到真实数据库:兼容性矩阵与两个行为差异实例
4.1 用兼容性矩阵回答“我该不该用这个特性”
读标准的直接产出是一张特性清单,而落到技术选型时需要把清单映射到具体数据库版本。我维护着一张常用特性兼容性表,按 SQL-99 定义逐项记录 PostgreSQL、MySQL、SQL Server 的真实行为。这张表的作用是在代码评审和架构设计时快速回答“这个特性能不能用”。
| SQL-99 特性 | PostgreSQL 15 | MySQL 8.0 | SQL Server 2019 |
|---|---|---|---|
| BOOLEAN 类型 | BOOLEAN 原生支持 | TINYINT(1) 模拟 | BIT 模拟 |
| 递归 CTE | WITH RECURSIVE | WITH RECURSIVE | WITH,不需要 RECURSIVE 关键字 |
| FULL OUTER JOIN | 支持 | 不支持 | 支持 |
| NULLS FIRST / LAST 排序 | 支持 | 不支持 | 不支持 |
| 窗口函数 RANK 系列 | 支持 | 支持 | 支持 |
这张表说明了一个现象:没有任何数据库实现完整的 SQL-99,每个产品都做了取舍。MySQL 至今不提供 FULL OUTER JOIN,窗口函数直到 8.0 才补齐;SQL Server 用 BIT 充当布尔类型,递归 CTE 不写 RECURSIVE 关键字;PostgreSQL 在语义细节上覆盖最全,但存储过程方言又和标准 PSM 差得很远。选型策略不是追求“完全符合标准”,而是识别出业务里真正依赖的标准特性,再确认当前引擎版本是否支持。
4.2 同一段标准语义在两套引擎上的差异实例
递归 CTE 是差异最明显的场景之一。同一份语义,在 PostgreSQL 和 MySQL 里都要写 RECURSIVE 关键字,到了 SQL Server 里写 WITH 就行,多写 RECURSIVE 反而报错。这类语法差异不是厂商随意改动,而是标准演进留下的版本岔路:SQL Server 的 CTE 实现在早期版本就固化了,之后没有跟进关键字的写法。
-- PostgreSQL / MySQL 写法 WITH RECURSIVE emp_tree AS ( SELECT emp_id, manager_id, 1 AS depth FROM employee WHERE manager_id IS NULL UNION ALL SELECT e.emp_id, e.manager_id, t.depth + 1 FROM employee e JOIN emp_tree t ON e.manager_id = t.emp_id ) SELECT * FROM emp_tree; -- SQL Server 写法 WITH emp_tree AS ( SELECT emp_id, manager_id, 1 AS depth FROM employee WHERE manager_id IS NULL UNION ALL SELECT e.emp_id, e.manager_id, t.depth + 1 FROM employee e JOIN emp_tree t ON e.manager_id = t.emp_id ) SELECT * FROM emp_tree;FULL OUTER JOIN 是另一个实例。标准定义了三个连接方向,MySQL 8.0 仍然不直接支持右外连接与左外连接的并集场景,替代方案是把左连接结果和右连接结果通过 UNION 合并:
-- MySQL 模拟 FULL OUTER JOIN SELECT d.dept_name, e.emp_name FROM department d LEFT JOIN employee e ON d.dept_id = e.dept_id UNION SELECT d.dept_name, e.emp_name FROM department d RIGHT JOIN employee e ON d.dept_id = e.dept_id;这个替代方案有一个需要留意的边界:UNION 会去重,如果业务要求保留重复行,要改成 UNION ALL,并把左连接里右表为空时的补 NULL 逻辑显式写出来。替换方案能跑通,但性能会比原生 FULL JOIN 差,因为相当于执行了两遍连接再加合并。
4.3 标准文档是压测与评审的依据
我拿标准文本参与技术评审时,从来不引用标准原句,而是用标准定义判断两个数据库谁的行为更合理。比如说窗口函数的执行顺序,只要确认某个数据库允许在 WHERE 里直接引用窗口列别名,基本可以判定它做了非标准扩展,这个扩展很可能会在跨库迁移时断掉;相反,如果某个语法标准里明确允许而数据库不支持,记录成该库的能力缺口,比争论谁对谁错更有价值。
压测场景同样需要标准做支撑。排查慢 SQL 时把一条复杂查询拆成标准子句,逐个环节压测,就能定位是连接顺序优化不行、窗口分区引起内存溢出,还是递归没有触发终止条件。标准在这里的作用是给拆解提供依据:每个子句的语义边界清楚了,压测结果才解释得通。
5. 读 SQL-99 避坑指南:5 个标准误读与排查经验
5.1 NATURAL JOIN 的结果集随表结构悄悄变化
现象:一段 NATURAL JOIN 查询上线后运行正常,某次加字段后数据莫名变少,排查业务逻辑找不到原因。
原因:NATURAL JOIN 自动匹配所有同名同类型的列。表结构新增一个两边都有的字段,连接条件就新增一条等值约束,能把匹配的行过滤掉,结果集会明显减少。
解决:改写为 USING 或 ON,把连接条件固定下来。生产里不要用 NATURAL,至少在关键链路上排除它。排查时先用SHOW COLUMNS确认两表当前的同名列集合,对照结果集差异。
5.2 FULL OUTER JOIN 在 MySQL 上直接语法报错
现象:同一段 SQL 在开发环境的 PostgreSQL 里跑通,上线时在 MySQL 里执行报语法错误。
原因:MySQL 8.0 仍未实现 FULL OUTER JOIN,标准定义了连接语义,但 MySQL 一直没补这个实现。
解决:确认目标数据库版本后,用左连接 UNION 右连接的方式改写。改写时注意 UNION 默认去重,需要保留重复行时换 UNION ALL,并显式补齐 NULL 列。
5.3 递归查询死循环,数据库 CPU 被打满
现象:执行递归 CTE 后查询长时间不返回,数据库 CPU 飙升,最终会话被 kill。
原因:数据里存在环。锚点成员和递归成员不断互相引用同一批节点,每一轮都能产出新行,查询永远不会终止,标准并没有给出环检测的自动处理。
解决:在递归成员里增加 visited_path 数组记录已访问节点,发现重复节点立即切断该分支;同时设置递归深度上限作为兜底。生产环境跑递归前,先用脚本扫描目标表确认无环。
5.4 WHERE 里直接引用窗口函数别名报错
现象:SELECT 里写了RANK() OVER (...) AS rnk,紧接着在 WHERE 里写rnk = 1,数据库报“列不存在”。
原因:窗口函数在 SELECT 阶段计算,执行顺序在 WHERE 之后。WHERE 阶段别名还没生成,标准不允许在这个位置引用窗口结果。
解决:包一层子查询,把窗口计算结果投影到外层再过滤:
SELECT dept_id, emp_name, salary, rnk FROM ( SELECT department_id AS dept_id, emp_name, salary, RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rnk FROM employee ) t WHERE rnk = 1;5.5 把 SQL/PSM 当成 Oracle PL/SQL 用,迁移时语法全错
现象:从 Oracle 迁移到 PostgreSQL,存储过程代码大量报错,关键字、变量声明、异常捕获都无法解析。
原因:SQL/PSM 是标准过程化 SQL,而 Oracle PL/SQL、SQL Server T-SQL、PostgreSQL PL/pgSQL 都是厂商方言,它们共同继承了标准的过程化思想,但语法细节差异巨大。
解决:把存储过程业务逻辑改写成标准 SQL 步骤,再分别用目标库方言实现。不要指望一份过程代码跨库直接跑。评审时把标准 PSM 当作抽象层,方言封装在适配层。
6. 把 SQL-99.pdf 变成一套可重复的验证清单:维护标准行为测试库
读标准最怕读完之后什么都记不住。我的习惯是把 PDF 里的每个关键特性拆成一条验证 SQL,放进统一的测试目录,用脚本批量跑,结果归档。这样每次数据库升级或换引擎,都能快速知道哪些标准行为变了。
cd ~/sql99-lab for f in tests/*.sql; do echo "===== $f =====" >> reports/run.log psql -X -U tester -d sql99_lab -a -f "$f" >> reports/run.log 2>&1 if [ $? -ne 0 ]; then echo "FAIL: $f" >> reports/fail.list fi done脚本里-X跳过用户配置文件,保证每次跑测试的环境一致;-a回显 SQL 原文,方便对照日志;输出写到 reports 目录,失败清单单独落在 fail.list。每条测试 SQL 开头用注释写明标准条款号和本库预期行为,例如:
-- 条款:SQL-99 Foundation, 7.9 <sort specification> -- 预期:PostgreSQL 支持 NULLS FIRST SELECT 1 AS ok FROM (VALUES (1), (NULL)) v(a) ORDER BY a NULLS FIRST;验证项目按优先级排序:先跑数据类型、NULL 语义、连接类型,再跑窗口函数、递归查询、触发器。每个季度或者每次数据库小版本升级后跑一遍,结果差异直接对照标准判断是行为修正还是回归 bug。我自己刚入行时拿到这份 PDF,把框架卷从头翻了两遍也没留下多少印象,后来改成按特性拆开逐条验证,标准才真正变成了手里能用的判断依据。希望这套方法也能帮到你。
本文还有配套的精品资源,点击获取