四个产品:Access 是「文件即库」,SQL Server 是「Windows 上的企业级」, Oracle 是「大型商用」,MySQL 是「开源部署最广」。 它们共用一套 SQL 标准,方言只差在边角:分页、拼串、空值、时间、自增列。 换库的成本主要在那些边角上,不在主体语法上——所以学会一个再换另一个,没有想象中贵。
教材 1.4 分四小节介绍四个产品。
这一节读起来像广告,但它其实在回答一个问题:「我手上这个项目该用哪个」。
这一节也是全书唯一一次把四个产品摆在一起比。后面十九章全在讲 SQL Server,
不会再回头讲别人。
演示里能跑的是「同一个需求四种写法」:取前 3 名,SQL Server 用 TOP、Oracle 用 ROWNUM、
MySQL 用 LIMIT。
这说明「标准 SQL」是真的,但「标准 SQL 不够用」也是真的。
一、四个产品,四段场景
| 产品 | 引擎与文件 | 典型场景 | 一条硬约束 |
|---|---|---|---|
| Access | Jet/ACE,整个库是一个文件 | 小团队报表、教学 | 单文件上限 2 GB |
| SQL Server | 独立服务,1433 端口 | Windows 上的业务系统 | Express 版单库 10 GB |
| Oracle | 独立服务,1521 端口 | 银行、电信核心 | 按 CPU 核数收许可费 |
| MySQL | 独立服务,3306 端口 | 互联网后端 | 开源版无硬限制 |
第一行「整个库是一个文件」是 Access 最大的特点和最大的局限。好处是拷一个文件就搬走了整个库; 坏处是三个人同时写就开始锁,而且 2 GB 一到就涨不上去。 所以它适合「一个人做报表」,不适合「一个团队录业务」。 第二行和第三行都是「要装服务、占端口、有实例」的重量级产品。 它们的共同点是:安装、备份、权限、日志都得有人管。这也是为什么小项目更愿意用 MySQL—— 不是因为它快,是因为它「管起来省事」。 第三行那句许可费不是玩笑:Oracle 按 CPU 核数收费,一套下来几十万起。所以「选型」这件事, 性能往往不是第一位的,成本和人手才是。
二、同一个需求,四种写法
需求:取成绩最高的前 3 名。
这在 SQL Server 上是一行 TOP 3,在 Oracle 上要套一层子查询用 ROWNUM,在 MySQL 上是 LIMIT 3。
| 产品 | 写法 | 能不能直接跑 |
|---|---|---|
| SQL Server | SELECT TOP 3 ... ORDER BY score DESC |
能 |
| Oracle | SELECT * FROM (...) WHERE ROWNUM <= 3 |
必须套子查询 |
| MySQL | SELECT ... ORDER BY score DESC LIMIT 3 |
能 |
| SQLite | SELECT ... ORDER BY score DESC LIMIT 3 |
能 |
第二行的「必须套子查询」不是啰嗦,是语义决定的。ROWNUM 在「取到行」的那一刻就编号,
所以必须先排好序、再限行。
直接写 WHERE ROWNUM <= 3 ORDER BY score DESC,会先取三行、再排序。
拿到的是「随便三行里排个序」,不是「最好的三个」。
这个坑很经典:同一句 SQL 在 Oracle 上不报错,但结果是错的。所以换库的时候,
「能跑通」和「跑得对」是两件事。
第三、四行一样,是因为 MySQL 的 LIMIT 被 SQLite 抄了去。
这也是本课能在 SQLite 上演示 MySQL 写法的原因——两者在这条语法上是一致的。
三、换库要改的六件事
| 要干的事 | SQL Server | Oracle | MySQL |
|---|---|---|---|
| 取前 n 行 | TOP n |
ROWNUM |
LIMIT n |
| 拼字符串 | + |
|| |
CONCAT() |
| 空值兜底 | ISNULL(a,b) |
NVL(a,b) |
IFNULL(a,b) |
| 当前时间 | GETDATE() |
SYSDATE |
NOW() |
| 自增列 | IDENTITY(1,1) |
SEQUENCE |
AUTO_INCREMENT |
| 语句结束 | ; 加 GO |
; 加 / |
; |
六行里有五行是「换个名字」,函数干的事一模一样。
只有第五行是真不一样:Oracle 用序列(SEQUENCE)这个独立对象来发号,另外三家是列上的属性。
所以「迁移脚本」最麻烦的往往不是 SELECT,是自增列和存储过程。
第六行的 GO 也不是 SQL 语句,是客户端的分隔符——它告诉 SSMS「把上面这些发过去」。
这就是为什么 GO 不能写在存储过程体里面:存储过程是整段发过去的。这一点在第 6.6 节还会细讲。
实际迁移的时候,这六件事之外还有两类东西要看:
数据类型(NVARCHAR 对应什么)和隐式转换('1' = 1 在哪些库上成立)。
前者影响存储,后者影响结果对不对。
四、本课的落地边界
| 要干的事 | 本课怎么写 | 本沙箱 | 真实例 |
|---|---|---|---|
| 建表、增删改、查询、连接 | 照 T-SQL 写 | 能跑 | 能跑 |
| 视图、索引、约束、事务 | 照 T-SQL 写 | 能跑 | 能跑 |
| 存储过程、触发器、游标 | Python 复刻语义 | 能演示 | 能跑 |
| 安装、SSMS、备份恢复 | 讲清机制 | 不能 | 能跑 |
第二行的「能跑」有个前提:把 TOP 改成 LIMIT、IDENTITY 改成 AUTOINCREMENT、
NVARCHAR 改成 TEXT。
这些改写每一处都会在正文里标出来,不会偷偷换掉。
第三行是这一课最诚实的一处:存储过程、触发器、游标,SQLite 一个都没有。
所以那几章的做法是「T-SQL 原码逐句贴出来,再用 Python 把它的语义复刻一遍」。复刻的是语义,
不是那个产品——正文会写清这一点。
第四行干脆不演。安装、SSMS 界面、备份还原这些,写清「真实例上是什么样、
关键参数在哪」就够了,硬造一个假的界面反而误导。
五、动手 Demo:四种关系数据库,同一个需求四种写法
这一段先把四个产品的定位摆成一张表,然后把「取成绩最高的前 3 名」的四种写法并排贴出来, 其中 MySQL/SQLite 那一种真跑一遍。最后列出换库要改的六件事,以及本课在沙箱里的落地边界。
import sqlite3
c = sqlite3.connect(u":memory:", isolation_level=None)
c.execute(u"CREATE TABLE tb_score (student_id INTEGER, subject TEXT, score REAL)")
c.executemany(u"INSERT INTO tb_score VALUES (?,?,?)", [
(1, u"高等数学", 88.0), (2, u"高等数学", 92.0), (3, u"高等数学", 61.0),
(4, u"高等数学", 55.5), (5, u"大学英语", 91.0), (6, u"高等数学", 79.0),
])
# SQL Server 的写法(教材原码,本沙箱不执行)
# SELECT TOP 3 student_id, score FROM tb_score ORDER BY score DESC;
# Oracle 的写法(教材原码,本沙箱不执行)
# SELECT * FROM (SELECT student_id, score FROM tb_score
# ORDER BY score DESC) WHERE ROWNUM <= 3;
# MySQL / SQLite 的写法:这一段真跑
for r in c.execute(u"SELECT student_id, score FROM tb_score "
u"ORDER BY score DESC LIMIT 3"):
print(r)
# 四种库用 UNION ALL 拼成一张结果集
for r in c.execute(u"SELECT 'Access' AS 产品, '文件即库' AS 特征 "
u"UNION ALL SELECT 'SQL Server', 'Windows 上的企业级' "
u"UNION ALL SELECT 'Oracle', '大型商用' "
u"UNION ALL SELECT 'MySQL', '开源部署最广'"):
print(r)
实跑输出:
--------------------------------------------------------------------
四种关系数据库,同一个需求四种写法
--------------------------------------------------------------------
① 四个产品的定位
--------------------------------------------------------------------
| 产品 | 定位 | 典型场景 |
|---|---|---|
| Access | 桌面单机库,文件即库 | 小团队报表、教学演示 |
| SQL Server | 微软的企业级库 | Windows 上的业务系统 |
| Oracle | 大型商用库 | 银行、电信的核心账务 |
| MySQL | 开源库,部署最广 | 互联网后端 |
这四个都不是「更好的那个」——Access 装完就能用,Oracle 装完要调参数。
选库先看场景,再看许可,最后才看性能。
② 同一个需求:取成绩最高的前 3 名
--------------------------------------------------------------------
SQL Server 用 TOP:
[教材原码 · 本沙箱不执行]
| SELECT TOP 3 student_id, score FROM tb_score ORDER BY score DESC;
Oracle 用 ROWNUM 套一层:
[教材原码 · 本沙箱不执行]
| SELECT * FROM (SELECT student_id, score FROM tb_score ORDER BY score DESC)
| WHERE ROWNUM <= 3;
MySQL 和 SQLite 用 LIMIT——这一段本沙箱能跑:
1> SELECT student_id, score FROM tb_score ORDER BY score DESC LIMIT 3
student_id score
-----------------
2 92
5 91
1 88
(3 行受影响)
三种写法的结果一样,语法三个样。这就是「标准 SQL 之上还有方言」。
③ 换库要改的不止分页
--------------------------------------------------------------------
| 要干的事 | SQL Server | Oracle | MySQL |
|---|---|---|---|
| 取前 n 行 | `TOP n` | `ROWNUM` | `LIMIT n` |
| 拼字符串 | `+` | `||` | `CONCAT()` |
| 空值兜底 | `ISNULL(a,b)` | `NVL(a,b)` | `IFNULL(a,b)` |
| 当前时间 | `GETDATE()` | `SYSDATE` | `NOW()` |
| 自增列 | `IDENTITY(1,1)` | `SEQUENCE` | `AUTO_INCREMENT` |
| 语句结束符 | `;` / `GO` | `;` / `/` | `;` |
六行里有五行是「换个名字」,只有自增列是真不一样——
Oracle 用序列对象,另外三家用列属性。
④ 在本沙箱里,哪些能跑
--------------------------------------------------------------------
| 要干的事 | 本课写法 | 本沙箱 | 真实例 |
|---|---|---|---|
| 建表、增删改、查询、连接 | 能 | 改写后能 | 能 |
| 视图、索引、约束、事务 | 能 | 改写后能 | 能 |
| 存储过程、触发器、游标 | 不能 | Python 复刻语义 | 能 |
| 安装、SSMS、备份恢复 | 不能 | 讲清机制 | — |
「改写后能」是指 TOP 改 LIMIT、IDENTITY 改 AUTOINCREMENT、
NVARCHAR 改 TEXT 这类——每一处改写正文里都会标出来。
⑤ 一句话记住这四个
--------------------------------------------------------------------
1> SELECT 'Access' AS 产品, '文件即库' AS 特征
UNION ALL
SELECT 'SQL Server', 'Windows 上的企业级'
UNION ALL
SELECT 'Oracle', '大型商用'
UNION ALL
SELECT 'MySQL', '开源部署最广'
产品 特征
------------------------------
Access 文件即库
SQL Server Windows 上的企业级
Oracle 大型商用
MySQL 开源部署最广
(4 行受影响)
UNION ALL 那一段本身就是第 8 章要讲的,这里先用它把四行拼成一张结果集。
**四个产品共用一套 SQL 标准,方言只差在「边角」——分页、拼串、空值、时间。**
**所以学会一个再换另一个,成本主要在那些边角上,不在主体语法上。**
第二段是这一节的重点:三段写法在各自的产品上结果一样,语法三个样。
注意 TOP 和 ROWNUM 那两段在本沙箱里只能贴出来、跑不了——SQLite 只认 LIMIT。
这也是全书唯一一处「三种写法并排但不都跑」的地方,标清楚比硬凑更重要。
第三段的表看起来像广告,但第五行(自增列)是真差别:
Oracle 的 SEQUENCE 是一个独立对象,你可以先 NEXTVAL 拿到号、再决定插不插;
另外三家是列属性,插的时候才发号。
这个差别会直接影响「批量插入要先知道 id」这类写法。
第五段那个 UNION ALL 本身是第 8 章的内容,这里先借用一下:
它把四行常量拼成一张结果集,省得建临时表。注意每一段的列数和类型要一致,
这是 UNION 的硬要求。
最后要说清一件事:这一节列的「能不能跑」,不是「好不好」。本课选 SQLite 当替身,只因为它够小、
够快、语法够像——不是因为它是生产选择。
30 秒实验
- 把第三段的表抄到纸上,然后遮住右边三列,只看「要干的事」,自己默写 SQL Server 那一列。 能默出五行就算过关,剩下那行(自增列)记住「是列属性」就够了。
- 在
tb_score上加一个subject分组,取每门课的第一名。 SQLite 上没有窗口函数的老版本只能用相关子查询;这一题第 9.3 节会正式做。 - 把
LIMIT 3改成LIMIT 3 OFFSET 3,看取出的是哪三行。 这就是分页的雏形;第 9.3 节会讲它为什么在深翻页时会变慢。
常见误区
- 「SQL 是标准,所以换库不用改代码」。标准只覆盖主体语法。 分页、拼串、空值、时间、自增列这五样,每家都不一样。真正迁移的时候, 这几样占掉大部分工作量。
- 「
ROWNUM <= 3和TOP 3是一回事」。不是。ROWNUM在取行时编号,所以要先排序再套一层; 直接写WHERE ROWNUM <= 3 ORDER BY会先取三行再排序,结果是错的,而且不报错。 - 「MySQL 快所以选 MySQL」。选型的顺序是: 场景(单机还是联网)→ 成本(许可和人手)→ 生态(驱动、工具)→ 性能。 性能排在最后,因为对大多数业务系统来说,瓶颈不在数据库。
自测三问(点开对答案)
1. 为什么 `ROWNUM <= 3` 必须套一层子查询? —— **因为 `ROWNUM` 是在「取到行」的那一刻赋值的,不是排序之后才赋。** **`WHERE ROWNUM <= 3 ORDER BY score DESC` 的执行顺序是先取三行(随便三行)、** **再对这三行排序。** 套一层子查询的意思是「先排好序,再给排好的结果编号」。 2. `GO` 是什么?为什么不能写在存储过程体里? —— **`GO` 不是 SQL 语句,是 SSMS 和 sqlcmd 的批处理分隔符。** **它告诉客户端「把上面这一批发过去」。** **存储过程是整段作为一个批次发过去的,里面出现 `GO` 会把这段切断,语法就错了。** 3. 本课为什么用 SQLite 当 SQL Server 的替身? —— **因为沙箱里装不了 SQL Server 实例,而 SQLite 单文件、免安装、语法够接近。** **但它是替身不是等价物:`TOP` 要改 `LIMIT`、`IDENTITY` 要改 `AUTOINCREMENT`、** **存储过程和触发器根本没有。** 每一处改写正文都会标出来。第一篇到此结束。 下一篇进第 2 章 2.1,开始装库——从「SQL Server 有哪几个版本」讲到「Express 版到底能干什么」, 也把这一课为什么不能真装说清楚。
- ☐ (本篇完,下一篇 2.1 SQL Server 数据库简介)