简介:SQL-99.pdf 是一份围绕 SQL-99 标准核心内容的 PDF 资料,适合数据库管理员、开发人员以及备考学习者深入研读。文档从 SQL-92 的局限性切入,详细介绍了 SQL-99 引入的布尔类型、BLOB/CLOB 大型对象、行类型、集合类型、列表类型及抽象数据类型,并解释这些新特性如何扩展关系数据库的表达范围,帮助读者理解 ORDBMS 的演进思路。
整个资源包仅含 1 个 PDF 文件,大小 483KB,轻量便于保存到手机或电脑随时阅读,可作为数据库理论学习的速查手册。目前已有 558 人浏览学习,内容实用且聚焦。
通过该文档,你能快速掌握 SQL-99 在复杂查询、多媒体数据存储和日期时间处理方面带来的能力提升,同时也能从与传统 SQL 的对比中理清数据库语言的发展脉络,对设计更灵活、更强壮的数据库系统有直接参考价值。
1. SQL-99 标准笔记:从 SQL-92 到对象关系型的转折点
如果你表里只有数字、字符串、日期,SQL-92 完全够用;一旦想把一张照片、一份简历、一个带街道门牌的完整地址直接塞进一行,SQL-92 就明显不够了。SQL-99 正是为补这个短板推出:它在保留关系模型和 SQL-92 全部能力的同时,新增了 boolean、BLOB、CLOB、ROW、集合类型等一批非传统类型,把关系型数据库推向 ORDBMS 时代。这份 PDF 笔记把这些新类型逐个过了一遍——每个类型能做什么、哪些操作被标准明确禁止,还附了 Postgres 里 bytea、text、复合类型的对照写法,适合正在学数据库原理、或者要在 PostgreSQL 里设计含大对象和嵌套结构字段的表的人。整份笔记最值钱的地方是:它把每个新类型的两面都列清楚了,能做什么、不能做什么,边界卡得很死,读一遍就能少踩一半的坑。
2. SQL-99 的类型体系:SQL-92 的短板与九类新类型的取舍
2.1 SQL-92 的能力边界:字段必须原子,图片简历没处放
SQL-92 是纯粹的关系模型标准,默认前提是「字段值必须原子」。这套标准能放心处理的场景基本是文本和数字:订单金额、人名、日期、状态码,都可以用数字、字符、位串、日期时间这几组基础类型覆盖。完整性控制方面 SQL-92 并不弱,它支持 SCHEMA、CREATE DOMAIN、过程化 SQL(块结构、循环、游标),甚至还有 ASSERTION 这种表级整体约束:
CREATE DOMAIN tipo_nombre AS varchar(45); -- 给 varchar(45) 起个别名 CREATE TABLE tabPersona ( cod int, nome tipo_nombre -- 本质还是 varchar ); CREATE ASSERTION limite_salario CHECK (NOT EXISTS ( SELECT * FROM empleado E, empleador M, dpto D WHERE E.salario > M.salario AND E.DNO = D.numero AND D.sgerente = M.ssn ));CREATE DOMAIN 只是给基础类型起了别名,值仍然是原子字符串;ASSERTION 管的是约束而不是类型。问题恰恰出在类型这层:关系模型假设每个属性都是不可拆的标量,所以二进制内容、长文本、嵌套结构在 SQL-92 里没有合适的位置。想存一张证件照,要么把二进制编码成 base64 塞进 TEXT 字段,要么只在库里存文件路径,把真实文件丢在磁盘上。前者查询时要额外编解码、占用空间翻倍;后者等于把数据一致性交给了应用层,数据库的备份、事务、权限都对文件内容无能为力。
结构化的嵌套数据同样尴尬。一个地址包含街道、门牌、区、城市、邮编五个字段,在 SQL-92 里要么拆成五列,要么单独建地址表再外键 JOIN。拆五列的问题是表列越堆越多,简历信息、产品规格、坐标点每个都要摊平;建关联表的问题是完整对象被拆散在多张表,读写都要 JOIN,应用代码量直接翻倍。SQL-92 不提供行内嵌套结构,这是它最明显的天花板。
2.2 SQL-99 类型全家桶:九个新类型逐个过一遍
SQL-99 的应对思路很直接:表仍然是存储和操作的基本结构,但字段类型从基础标量扩成一整族。笔记里列出的新类型有九类:boolean、BLOB、CLOB、枚举类型、国家字符大对象、ROW(行类型)、SET(集合)、MULTISET(多重集)、LIST(列表),外加用户可以自定义的抽象数据类型 ADT。这些类型加入后,一行里的列值就不再局限于原子值,可以嵌 BLOB、嵌 CLOB,甚至嵌一整行结构。
逐个说定位。boolean 是显式加入的逻辑类型,只接受 TRUE 和 FALSE,标准还允许定义接收布尔参数的函数、支持用户自定义的布尔运算。BLOB 是二进制大对象,声明时直接带大小,比如 BLOB(1M),用来承载电子签名、图形、音频这类原始字节。CLOB 是字符大对象,适合存几十 K 的长文本,比如员工简历。ROW 是结构类型,把一组「字段名 + 类型」打包成一个字段值,等价于在一行里再嵌一行。SET、MULTISET、LIST 解决「一个字段存多个值」:SET 去重、MULTISET 允许重复、LIST 强调顺序。ADT 最自由,允许用户自定义类型及其操作。
这些类型不是并列关系,而是按能力从简单标量到复杂结构排列的一条梯度。实际选型时可以这样理解:boolean 解决「逻辑值该不该有专门类型」,BLOB/CLOB 解决「大内容放哪」,ROW 解决「嵌套结构怎么建模」,集合类型解决「一对多能不能不建表」,ADT 解决「标准类型都不够用时怎么办」。业务里通常只会用到前四类,后面几类更多是标准完备性的体现。
2.3 兼容策略:ORDBMS 怎么做到不推翻旧表
SQL-99 的另一条主线是兼容性。它没有推翻 SQL-92 的关系模型,而是在原基础上做加法:表依旧是基本结构,原有 SCHEMA、事务、SQL 语义全部保留,新增类型只是让「列的值」比过去更复杂。直接好处是:一个已经按 SQL-92 建好的库升级到 SQL-99 标准不会因此失效,旧查询照跑,新功能只在需要时才引入。标准里把这种兼顾两边的模式命名为 ORDBMS(对象关系数据库),核心卖点就是「对象能力 + 关系兼容」。
兼容性还体现在存储层次的区分上。简单类型走传统直接存储,大对象走定位器(locator)机制:BLOB 和 CLOB 在表里存一个定位标识,真实数据由数据库引擎单独管理,所以它们在 UNION、ORDER BY、GROUP BY 这些操作上的行为和普通列不一致。理解了「扩展而非革命」这条策略,就明白为什么 SQL-99 加了一堆类型却没有让门槛暴涨——完全可以继续写 SQL-92 代码,新类型只是可选增强工具。下表是三者的对照关系,后面两章会逐个展开:
| 能力维度 | SQL-92 | SQL-99 | Postgres 对照 |
|---|---|---|---|
| 逻辑类型 | 不支持 | boolean | boolean |
| 二进制大对象 | 不支持 | BLOB | bytea |
| 字符大对象 | 只有 varchar | CLOB | text |
| 嵌套结构 | 拆表 + JOIN | ROW | 复合类型 composite |
| 一字段多值 | 不支持 | SET / MULTISET / LIST | array |
| 自定义类型 | 仅 DOMAIN 别名 | ADT | CREATE TYPE |
3. boolean 与大型对象实战:把三类新类型落进真实建表语句
3.1 boolean 的三值逻辑:WHERE 里 TRUE 不等于全部
SQL-99 把 boolean 提升为显式类型,此前 SQL-92 里只能靠 char(1) 存 'Y'/'N' 或靠数字 0/1 模拟。boolean 的建表和查询在笔记里有完整示例,翻译成可执行语句是下面这样:
CREATE TABLE a ( cod int, matriculado boolean -- 是否已注册,未录入时是 NULL ); -- 写法一:直接等值比较,NULL 行不会命中 SELECT * FROM a WHERE matriculado = TRUE; -- 写法二:IS TRUE,语义是「确实是 TRUE 的所有行」 SELECT * FROM a WHERE matriculado IS TRUE;两段查询的结果可能不一样,关键就在 SQL 的三值逻辑:boolean 除了 TRUE 和 FALSE,还有 NULL。NULL 不等于 FALSE,它表示「未录入」这个第三种状态。matriculado = TRUE遇到 NULL 时结果是 UNKNOWN,行被过滤;matriculado IS TRUE则按「真的是 TRUE」来判断,NULL 行依旧被排除。若业务上需要区分「未注册」和「未录入」,就必须保留 NULL 并显式处理;若这两种状态没有区别,更好的做法是建表时加DEFAULT FALSE NOT NULL,让字段只存在两种值,查询逻辑会简单得多。
标准里还提到 boolean 支持函数参数和用户自定义运算,常见做法是配合 CHECK 约束做数据校验,比如出勤标记、审核通过标记这类只有两三个状态的字段直接交给 boolean,避免应用层用它模拟字符串。要注意的是,索引方面 boolean 选择性极低,只有两个取值,单独建索引基本没有意义,这类字段适合放进复合索引作为过滤条件的一部分。
3.2 BLOB 与 CLOB:建表写法、大小定义与操作边界
BLOB 和 CLOB 是这份笔记的重点。BLOB 承载原始二进制,CLOB 承载长文本,建表时都要声明大小:
-- 学生表:照片,上限 1MB CREATE TABLE alumno ( num_matricula char(10), foto BLOB(1M) ); -- 员工表:简历,上限 50K 字符 CREATE TABLE func ( num_func int, curriculo CLOB(50k) );大小参数的单位两者不同:BLOB(1M) 是字节,CLOB(50k) 是字符。这里的 k/M 表示存储上限,实际数据小于上限即可。标准明确允许的操作包括:LIKE、UNION ALL、拼接、替换、长度校验;明确不允许的操作包括:UNION、ORDER BY、大于小于比较、作为主键或索引键、GROUP BY。原因在于 BLOB/CLOB 没有可比较的顺序语义,能比的只有长度,而且它们走 locator 机制存储,整体排序和分组在实现上代价极高。
CLOB 比 BLOB 多一个能力:支持部分读取和整体读取。对大段文本,应用可以先读定位器再按需取片段,不必把整个对象拉进内存。实际使用中,大对象列应该只负责存储和检索,不参与业务排序与分组;如果报表确实需要按某种维度聚合,先在外层把大对象投影成摘要字段(长度、哈希值、分类标记)再聚合,这是最稳妥的写法。
3.3 Postgres 落地对照:BYTEA 和 TEXT 的接法
这套标准类型在 Postgres 里的对应关系很明确:BLOB 对应 bytea,CLOB 对应 text,boolean 直接对应 boolean。表结构照搬过来是这样:
CREATE TABLE alumno ( num_matricula char(10), foto bytea -- Postgres 的二进制大对象类型 ); CREATE TABLE func ( num_func int, curriculo text -- Postgres 的长文本类型,无长度上限 );bytea 存储的是原始字节,喂进去什么字节查出来还是什么字节,但展示形式受参数 bytea_output 控制,默认 hex,psql 里查出来是一串\x开头的十六进制,这是展示方式不是数据本身。应用侧读写二进制建议直接用驱动自带的二进制接口,不要走文本拼接;实在要在 SQL 里转换,用encode(foto, 'base64')和decode(..., 'base64')配对使用。
text 和 varchar(n) 的取舍是 Postgres 里的常见问题:text 没有长度限制,varchar(n) 有;不声明长度时两者内部存储机制几乎一样。从 CLOB 语义迁过来时,直接建 text 最省心,长度校验交给应用层或 CHECK 约束即可。另外一个容易忽略的点:CLOB 的「部分读取」在 Postgres 里对应的是substring(curriculo FROM 1 FOR 200)这类函数,而不是靠 locator 机制,理解这个差异才能写出跨方言靠谱的代码。
4. ROW 与嵌套结构:Postgres 复合类型的建表、插入与查询
4.1 ROW 类型的定位:一个值装下一整行,和拆表 JOIN 是两条路
SQL-99 的 ROW 类型把「行」本身变成了一个可用的字段值。笔记里的例子是学生和地址:地址包含街道、门牌、区、城市、邮编,建表时不再拆五列,也不建独立地址表,而是用一个 ROW 包起来:
CREATE TABLE alumno ( cod int, direccion ROW ( calle varchar(30), num int, distrito varchar(20), ciudad varchar(30), cp char(2) ) );建出来的效果是:alumno 一行里套着 direccion 一整行,读取地址不用 JOIN,整块数据物理上跟着主行走。这和「拆两表 + 外键」是两种设计路线:拆表适合地址要独立维护、要按城市聚合统计的场景;ROW 适合地址跟着主体走、读多写少、希望一次取全的场景。ROW 的代价是:内部字段没有独立的约束和索引能力,不能对 direccion.calle 建外键,完整性要靠应用保证,这点在选型时要想清楚。
4.2 COMPOSITE TYPE 完整链路:类型定义、建表、插入、查询
Postgres 里 ROW 的标准实现是复合类型(composite type)。笔记结尾给了一套完整示例,正是这个思路,跑通它的全链路如下:
-- 第一步:定义复合类型,字段顺序即构造顺序 CREATE TYPE tipodireccion AS ( calle varchar(20), num int, estado char(2), cp char(15) ); -- 第二步:建表,列类型直接用复合类型 CREATE TABLE alumno2 ( matricula char(30) PRIMARY KEY, direccion tipodireccion ); -- 第三步:插入整行结构,字段值按类型定义顺序写在括号里 INSERT INTO alumno2 (matricula, direccion) VALUES ('300', ('Calle XV', 200, 'SC', '89200-100')); -- 第四步:查询子字段,复合列名必须加括号 SELECT matricula, (direccion).calle FROM alumno2 WHERE (direccion).calle LIKE 'Jr XV%';('Calle XV', 200, 'SC', '89200-100')这种写法叫行构造式,括号里的值按 CREATE TYPE 时的字段顺序匹配:calle 是字符串、num 是整数、estado 是两位字符、cp 是字符串,顺序错了会直接报类型不匹配。(direccion).calle里的括号不是装饰,Postgres 解析器看到direccion.calle会把它当成「表 direccion 的 calle 列」,而不是「复合列的 calle 子字段」,丢括号就报错。
还有两个实用操作。修改子字段时最稳的做法是整字段替换:
UPDATE alumno2 SET direccion = ROW('Av. Brasil', 1500, 'SC', '88000-100') WHERE matricula = '300';Postgres 虽然也支持(direccion).calle = '新值'这种子字段更新,但表意不如整体替换清晰,跨数据库迁移时整体替换的兼容性更好。另外,源笔记里这条 INSERT 出现了两遍,应该是抄录时的重复粘贴,实际执行一次即可,不影响语义。
4.3 SET、MULTISET、LIST:集合类型在 SQL-99 里的位置
ROW 解决了「一行嵌一行」,SET、MULTISET、LIST 解决的是「一个字段存多个值」。SQL-99 里三者语义不同:SET 存不重复的集合,MULTISET 允许重复,LIST 在 MULTISET 基础上增加顺序。标准这么设计是为了覆盖「标签集合」「订单明细」「坐标序列」三类不同需求。Postgres 没有直接对应这三个类型的语法,最接近的实践替代是数组:
CREATE TABLE producto ( id int, etiquetas text[] -- 模拟 LIST:有序、允许重复 );数组是有序且允许重复的,行为上更像 LIST 与 MULTISET 的合体;要模拟 SET 的去重语义,插入前在应用层排重,或者查询时用SELECT DISTINCT unnest(etiquetas) FROM producto展开后去重。选型建议是:字段值少于几十个、只做包含判断时用数组很顺手;一旦需要按元素聚合、关联另一张表、或者元素数量可能膨胀,还是老老实实建一张子表加外键,数组在这类场景下反而会成为累赘。SQL-99 的原生集合类型更多是设计完备性的体现,实际商业数据库普遍用数组或 JSON 字段达到了类似效果。
5. SQL-99 落地避坑:五条真实踩过的类型边界
5.1 在 BLOB/CLOB 字段上做 ORDER BY 和 GROUP BY:标准明确禁止
现象:有人想把含大对象的表按照片或简历内容排序,写了ORDER BY foto,执行直接报错;也有人把大对象列入 GROUP BY,同样跑不起来。原因:SQL-99 标准里写得明白,BLOB/CLOB 不允许 UNION、ORDER BY、比较运算、作为键和 GROUP BY。它们没有自然顺序语义,能比的只有长度;加上底层走 locator 存储,对真实内容排序成本极高。解决:排序改用ORDER BY length(foto)、ORDER BY length(curriculo);分组先投影出摘要列(长度、分类、哈希值)再聚合。设计表时就让大对象列只负责存储和按主键存取,不参与任何业务排序与分组。
5.2 boolean 用 = TRUE 查询:NULL 行被悄悄漏掉
现象:线上做数据核对,WHERE matriculado = TRUE和WHERE matriculado = FALSE查出来的行数加起来对不上总数,少了一批没录入的行。原因:SQL 的 boolean 是三值逻辑,字段为 NULL 时NULL = TRUE的结果是 UNKNOWN,两种写法都筛不到。如果建表时没加NOT NULL DEFAULT FALSE,未录入的数据就会掉进查询盲区。解决:语义上要区分「未注册」和「未录入」时,查询里显式处理 NULL,用IS TRUE/IS NOT TRUE或加上OR matriculado IS NULL;不区分时直接给字段加DEFAULT FALSE NOT NULL,让布尔字段只存在两种值,一劳永逸。
5.3 Postgres 的 bytea 查出来是一串 \x 乱码
现象:psql 里SELECT foto FROM alumno返回\x...开头的超长十六进制,拷贝出来不是原图;程序里按字符串处理也拿不到原始字节。原因:bytea_output 参数默认值是 hex,Postgres 把原始字节按十六进制文本展示。bytea 存的是二进制,任何文本化展示都会「变形」,这跟数据是否损坏没关系。解决:先SHOW bytea_output;确认输出格式;应用侧用 PostgreSQL 驱动自带的二进制读写接口,或显式调用encode(foto, 'base64')和decode(x, 'base64');只在 psql 排错时把bytea_output临时设为escape,生产环境不要依赖这个设置。
5.4 访问复合类型子字段漏括号:报 column does not exist
现象:写WHERE direccion.calle = 'Calle XV',Postgres 报错column direccion does not exist,明明表里就有这个复合列。原因:Postgres 解析器把direccion.calle中的 direccion 当成表名或别名来解析。复合类型列访问子字段必须用括号写成(direccion).calle,这不是风格问题,是语法要求。解决:查询、条件、索引表达式里统一写(direccion).calle带括号形式;给查询加表别名时写法是(a.direccion).calle,括号包裹的是「别名.列名」整体,别拆错位置。
5.5 对复合类型列建普通索引:查询条件对不上,索引白建
现象:在 direccion 列上建了普通索引,按(direccion).calle做等值查询,EXPLAIN 一看还是 Seq Scan。原因:普通索引建立在「整个复合值」上,等值条件是「复合值的单个子字段」,两者不是同一个表达式,优化器没有索引可用。解决:建表达式索引,CREATE INDEX idx_alumno2_calle ON alumno2 (( (direccion).calle ));建完再 EXPLAIN 验证是否走 Index Scan。高频按子字段查询的场景,也可以把这些子字段冗余成普通列并建索引,查询走普通索引,展示用复合列。
6. 验证与进阶:用 EXPLAIN 和表达式索引确认优化生效
新类型能跑通不算完,性能边界要靠 EXPLAIN 说话。我拿到一张带复合类型的表,固定动作是先看查询计划。假设现在对 alumno2 按街道名检索:
EXPLAIN SELECT matricula FROM alumno2 WHERE (direccion).calle = 'Calle XV';建索引之前,计划里通常是 Seq Scan,全表扫描。要验证子字段索引是否生效,建表达式索引再对比一次:
CREATE INDEX idx_alumno2_calle ON alumno2 (( (direccion).calle )); -- 表达式必须带双层括号 EXPLAIN SELECT matricula FROM alumno2 WHERE (direccion).calle = 'Calle XV'; -- 计划应变为 Index Scan表达式索引的写法有个易错点:函数或括号表达式外面要再加一层括号,((direccion).calle)整体作为一个表达式,少一层括号就可能报语法错误或建出无用的全值索引。索引建完务必重新 EXPLAIN,别想当然。
大对象字段的验证方式不同。bytea/text 数据量大时会走 TOAST 机制,行内只留指针,可用pg_relation_size和pg_total_relation_size对比确认数据是否真的落库、是否触发了 TOAST 压缩;boolean 列则重点验证 NULL 占比,决定要不要在应用层统一默认值。结构类型还可以用\d alumno2查看字段类型定义,确认复合类型嵌套层次和顺序对不对,避免插入时报类型不匹配。
从那以后,我每次设计含大对象或多段结构的表,都强制走一遍「插一条边界值 → EXPLAIN 看计划 → 建表达式索引再验证」三步,最后用 \d 检查类型定义。这个习惯帮我避开了不少上线后才暴露的慢查询和类型错误。这份 SQL-99 笔记里还有不少标准层面的细节,建议下载后对照原文边读边敲,重点看每个类型「不能做什么」那一栏——那些边界才是真正决定设计成败的地方。希望帮到你。
本文还有配套的精品资源,点击获取