做数据库开发和运维这些年,我身边几乎每个人都喊过同一句话:“这三个库的SQL怎么又不一样?” 上午在MySQL里写得好好的分页语句,拿到Oracle直接报错;下午把一段SQL Server的日期处理搬到MySQL,结果日期格式全乱。要说语法差异,网上一搜一大把,但大部分是零散笔记,今天我想系统性地把MySQL、Oracle、SQL Server这三大数据库的语法区别梳理一遍,结合我自己的踩坑经历,从数据类型到DML、从分页到存储过程,把最实用、最容易被忽略的差异点一次性讲透。
这篇文章适合三类人:一是刚入行需要同时接触多个数据库的开发,二是做数据迁移或ETL任务的工程师,三是经常写跨库工具脚本的人。读完之后不敢说你能闭眼写三库通用SQL,但至少看到报错能第一时间反应过来是哪里的语法不兼容。
1. 先搞明白一件事:三大数据库为什么各自为政
1.1 出身和生态决定了语法走向
很多新手不理解,SQL明明有国际标准,为什么三大数据库还是各写各的。说白了,SQL标准只是给了一个基础框架,各家在实现时都有自己的历史包袱和商业考量。
MySQL出生在开源社区,早期主打轻量、快速、易用,语法上强调短平快,能省就省,LIMIT直接加在查询后面,写起来非常直觉化。Oracle是商业数据库里的老大哥,企业级应用几十年的积累,PL/SQL语言极其强大,它更追求严谨和可扩展性,所以语法偏“重型”,处处体现数据字典、角色权限、事务控制的体系化设计。SQL Server则是微软生态的宠儿,T-SQL在易用性上做得最好,和Windows开发体系无缝衔接,很多语法细节(比如GETDATE()、ISNULL()、+号拼接字符串)都和C#语言风格一脉相承,对从应用开发转过来的程序员特别友好。
这三个库的“性格”差异,决定了它们的SQL语法必然走向不同分支。MySQL的朴实、Oracle的严谨、SQL Server的易用,都直接反映在每一句SQL的写法上。
1.2 标准SQL的“标准”其实有限
很多人问:那我全部写标准SQL不就行了?理论上可以,但实际做项目的人都明白,标准SQL覆盖的只是最基础的SELECT、INSERT、UPDATE、DELETE、WHERE、JOIN等核心能力。一旦涉及分页、字符串处理、日期计算、自增字段、批量插入这些日常高频操作,三大数据库就会各走各路。
比如标准SQL里规定了||作为字符串连接符,Oracle和PostgreSQL都遵守了,但MySQL默认用CONCAT()函数,SQL Server默认用+号,三者直接对不上。标准SQL里没有定义类似LIMIT的通用分页子句(现代SQL标准后来加了OFFSET...FETCH,但Oracle在12c之前一直不支持),所以MySQL用LIMIT、SQL Server用TOP或OFFSET FETCH、Oracle用ROWNUM或FETCH FIRST,各有各的祖传写法。
1.3 一张表看懂三库性格
| 对比维度 | MySQL | Oracle | SQL Server |
|---|---|---|---|
| 语言继承 | MySQL原生SQL | PL/SQL | T-SQL |
| 设计理念 | 轻量简洁 | 严谨厚重 | 易用友好 |
| 典型场景 | 互联网应用、读写频繁 | 银行、电信等核心交易系统 | 企业ERP、微软生态 |
| 自增方案 | AUTO_INCREMENT | SEQUENCE序列 | IDENTITY |
| 分页习惯 | LIMIT | ROWNUM/OFFSET | TOP/OFFSET FETCH |
| 字符串拼接 | CONCAT() | || | + |
| 日期处理 | DATE_FORMAT | TO_CHAR/TO_DATE | CONVERT |
这张表基本就是全文的浓缩版,后面每一个差异点,我都会展开讲原理和使用细节。
2. 建表与数据类型:第一个分岔路口
2.1 字符串类型:VARCHAR、VARCHAR2和NVARCHAR的距离
这是建表时最容易被坑的地方。
MySQL里最常用的字符串类型是VARCHAR(n)和CHAR(n),n代表字符数,不是字节数。也就是说VARCHAR(10)在utf8mb4字符集下最多能存10个汉字,即使一个汉字占3个字节也无所谓,因为MySQL按字符计数。
Oracle里没有VARCHAR,只有VARCHAR2(n),而且这个n默认是字节数,不是字符数。很多从MySQL转过来的人建表时写VARCHAR2(10),以为能存10个汉字,结果插入中文直接报“ORA-12899: value too large for column”,就是因为10个字节在UTF-8下只能放3个汉字。解决办法是写成VARCHAR2(10 CHAR)显式指定按字符计数,或者在数据库层面设置NLS_LENGTH_SEMANTICS=CHAR。Oracle新版虽然也支持VARCHAR,但官方文档明确建议始终使用VARCHAR2,这一点我建议直接照做。
SQL Server则是另一个流派,它有VARCHAR(n)和NVARCHAR(n)两种。VARCHAR按非Unicode字符存储,一个英文字符1字节,一个中文占2字节(取决于代码页);NVARCHAR按Unicode存储,所有字符统一2字节,最大长度为4000字符。实际项目里我强烈建议SQL Server的表尽量用NVARCHAR,否则接口对接时一旦传入中文或特殊符号,很容易出现乱码或溢出。
2.2 数字与日期:精确到小数点背后的理念差异
数字类型方面,MySQL用的是TINYINT/SMALLINT/INT/BIGINT加DECIMAL(p,s)、FLOAT/DOUBLE的分类方式;Oracle则是统一用NUMBER(p,s)搞定所有数字场景,无论是整数还是小数都收在这个类型里,NUMBER的精度范围可以从NUMBER(1)到NUMBER(38);SQL Server则和MySQL类似,有INT/BIGINT/SMALLINT/TINYINT、DECIMAL(p,s)、NUMERIC(p,s)、FLOAT/REAL,它的DECIMAL和NUMERIC基本等价。
日期时间类型差异更大。MySQL有DATE、TIME、DATETIME、TIMESTAMP四种常用类型,DATETIME存的是普通时间,TIMESTAMP会自动跟随时区且在2038年有溢出问题。Oracle的日期核心是DATE(精确到秒)和TIMESTAMP(精确到纳秒,支持时区),而且Oracle的DATE本身就带时分秒,和MySQL的DATE只存年月日完全不同,这个不特别注意的话,直接通过SYSDATE对比时间会把人绕晕。SQL Server有DATE(纯年月日)和DATETIME(精确到3.33毫秒)以及DATETIME2(精确到100纳秒)、DATETIMEOFFSET(带时区偏移),其中DATETIME2是后来自主推荐使用的。
说到数字,还有一个SQL Server特有的大坑:INT除法永远返回整数。写SELECT 5/2在MySQL和Oracle里结果都是2.5,在SQL Server里结果是2,因为两个整数相除会走整数除法。如果业务里必须保留小数,得写成SELECT 5.0/2或者SELECT CAST(5 AS DECIMAL(10,2))/2。这种隐含转换的坑,最容易在报表计算里冒出来。
2.3 自增字段:AUTO_INCREMENT、IDENTITY与SEQUENCE
这是建表时最直观的语法分叉。
MySQL的自增最简单,字段定义时加AUTO_INCREMENT,然后指定这个字段是主键即可:
CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) );SQL Server用的是IDENTITY(起始值, 步长):
CREATE TABLE user ( id INT IDENTITY(1,1) PRIMARY KEY, name NVARCHAR(50) );Oracle呢?它没有自增关键字,老版本必须用序列加触发器来实现所谓的自增ID,12c之后虽然支持IDENTITY列,但很多老项目还是沿用序列的习惯:
CREATE SEQUENCE seq_user_id START WITH 1 INCREMENT BY 1; -- 插入时手动取序列值 INSERT INTO user (id, name) VALUES (seq_user_id.NEXTVAL, '张三'); -- 或者用触发器在插入前自动取序列 CREATE OR REPLACE TRIGGER trg_user_id BEFORE INSERT ON user FOR EACH ROW BEGIN SELECT seq_user_id.NEXTVAL INTO :NEW.id FROM DUAL; END;我遇到过不少从MySQL迁到Oracle的项目,程序里用了SELECT LAST_INSERT_ID()去拿新插入记录的ID,在Oracle里根本没有这个函数,白折腾了好几天。要拿序列当前值,Oracle用的是seq_user_id.CURRVAL,但前提是当前会话已经调用过NEXTVAL,否则会报“ORA-08002: sequence CURRVAL is not yet defined in this session”,这也是一个常见坑。
2.4 默认值与NULL的底层分歧
默认值和NULL的处理,三库的哲学也不同。
MySQL和SQL Server都支持在字段定义处直接写DEFAULT 0、DEFAULT '无'等,Oracle也支持DEFAULT,但12c之前对默认值的处理有个限制:如果插入时没有指定该列,并且默认值是一个序列的NEXTVAL或者复杂表达式,会有一些怪问题,需要靠触发器绕过去。
NULL的处理更是分歧明显。MySQL里''空字符串和NULL是两个概念,字符串列插入''和NULL在COUNT、GROUP BY里表现完全不同。Oracle更极端:它的VARCHAR2空字符串会被当成NULL,也就是说INSERT INTO t(name) VALUES ('')之后,你查出来的是NULL而不是空字符串,这一度让无数从其他库迁移过来的开发懵圈。SQL Server的分界相对清楚一些,''就是'',NULL就是NULL,但CONCAT有特殊处理,后面函数篇我会讲到。
这里提醒新手一个反直觉的点:在Oracle里判断一个字段是否有值,WHERE col != ''永远查不出任何行,因为col != ''会被推导为col != NULL,结果是未知,行被过滤掉了。正确写法必须是WHERE col IS NULL或IS NOT NULL。
3. 查询语法:分页、排序与字符串处理的三重体验
3.1 分页:LIMIT、ROWNUM、OFFSET FETCH
分页是语法差异最明显、报错率最高的操作。
MySQL从入门就是LIMIT一统天下:
SELECT * FROM employees ORDER BY emp_id LIMIT 10 OFFSET 20; -- 也可以简写成 LIMIT 20, 10这个写法简单直观,LIMIT offset, count先偏移后取数量,从0开始算偏移。
Oracle在12c之前根本没有LIMIT,传统写法是用ROWNUM伪列。但ROWNUM有个著名陷阱:直接在WHERE里写WHERE ROWNUM > 10是取不到数据的,因为ROWNUM是在结果集产生过程中逐行分配的,查询出来的第一行永远是1,大于10的条件根本轮不到后续行。正确写法通常要套一层子查询:
SELECT * FROM ( SELECT e.*, ROWNUM rn FROM ( SELECT * FROM employees ORDER BY emp_id ) e WHERE ROWNUM <= 30 ) WHERE rn > 20;注意内层必须先排序,再取ROWNUM <= 30,外层再过滤rn > 20,顺序错了数据就不对。Oracle 12c之后引入了OFFSET...FETCH NEXT,写法上和SQL Server接近了:
SELECT * FROM employees ORDER BY emp_id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;但这个语法在Oracle里要求必须有ORDER BY,否则直接报错。
SQL Server的经典分页是TOP加子查询:
SELECT TOP 10 * FROM employees ORDER BY emp_id; -- 跨页需要配合 NOT IN 或 EXCEPT SELECT TOP 10 * FROM employees WHERE emp_id NOT IN ( SELECT TOP 20 emp_id FROM employees ORDER BY emp_id ) ORDER BY emp_id;SQL Server 2012之后终于支持了标准的OFFSET FETCH:
SELECT * FROM employees ORDER BY emp_id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;这里必须给OFFSET FETCH点个赞——它和Oracle 12c以后的写法几乎一样,算是现代版本里最接近统一的方案。所以如果你写跨库工具,能控制数据库版本的话,尽量引导项目升级到支持OFFSET FETCH的版本,代码可移植性会好很多。
3.2 排序的隐藏差异:排序规则与大小写
排序看起来都一样,但细节差异能坑到人。
MySQL的排序规则由字符集和collation决定,默认的utf8mb4_general_ci不区分大小写,所以'abc'、'ABC'、'Abc'排在一起时会被当成同一个键值处理,ORDER BY name的顺序完全取决于索引建立时的字节序,很多人觉得“乱乱”的。如果想区分大小写,得指定utf8mb4_bin排序规则或者用BINARY转换。
Oracle的排序规则默认由NLS_SORT和NLS_COMP参数控制,通常默认也是不区分大小写的,但它支持语言化排序,比如中文按拼音顺序排序需要设置NLS_SORT=SCHINESE_PINYIN_M。写ORDER BY时如果期望的是拼音顺序,实际输出却可能是按ASCII码或者笔画排的,这种差异在报表场景里可大可小。
SQL Server的排序规则由COLLATE子句直接控制,比如Chinese_PRC_CI_AS表示中文、不区分大小写、区分重音。SQL Server的排序规则是可以在列级别覆盖的,灵活性最高。跨库迁移时,如果排序要求严格一致,最省事的办法是都加上COLLATE或等价机制显式指定,别指望默认值一致。
3.3 字符串拼接与转数字的经典陷阱
字符串拼接是日常需求里频率最高的操作,三大库的写法正好三种风格。
MySQL用CONCAT()函数,可以传多个参数,比如CONCAT(first_name, '-', last_name)。MySQL的+号是纯粹的算术加号,不能用它拼字符串,否则会做隐式数字转换,拼出离谱的结果。
Oracle用||符号,这是SQL标准写法,first_name || '-' || last_name。Oracle虽然也有CONCAT(),但它只接受两个参数,想要拼三个以上的字符串得嵌套调用,特别麻烦,所以大家都直接用||。
SQL Server用+号拼接字符串,first_name + '-' + last_name。但它有个著名坑:只要有一个操作数是NULL,整个结果就是NULL。所以拼接前通常要写ISNULL(first_name, '') + ISNULL(last_name, '')。
再说说热搜词里提到的“sqlserver 字符串转数字”,这个坑也是经典之中的经典。SQL Server把一个字符串转数字,最常见的是CAST和CONVERT:
SELECT CAST('123' AS INT); -- 123 SELECT CONVERT(INT, '123'); -- 123 SELECT TRY_CAST('abc' AS INT); -- NULL,不报错MySQL转数字更直接,写'123'+0就能得到数字123,CAST('123' AS UNSIGNED)也可以。Oracle转数字则是TO_NUMBER('123'),它同样没有TRY机制,字符串里带一个非数字字符就直接报ORA-01722: invalid number。所以跨库写转换逻辑时,必须先想好“遇到非法数据时希望报错还是回退为NULL”,再选对应函数,这件事没人替你做。
4. DML操作:插、改、删的语法细节和坑
4.1 INSERT:多行插入与RETURNING
三条SQL的基础INSERT INTO table (col1, col2) VALUES (v1, v2)都一样,但批量插入就有明显差异。
MySQL支持INSERT INTO后面直接跟多组VALUES:
INSERT INTO user (name, age) VALUES ('张三', 20), ('李四', 21), ('王五', 22);这种写法在Oracle的老版本里不支持,Oracle语法是INSERT ALL配合INTO多个子句,或者用INSERT INTO ... SELECT ... UNION ALL的方式:
INSERT ALL INTO user (name, age) VALUES ('张三', 20) INTO user (name, age) VALUES ('李四', 21) INTO user (name, age) VALUES ('王五', 22) SELECT 1 FROM DUAL;SQL Server默认支持多行VALUES,写法和MySQL基本一致,但批量插入的行数如果超过阈值可能影响日志空间,需要注意。
另一个易被忽略的点是RETURNING子句。Oracle插入后要拿回自增ID或某些计算值,直接写:
INSERT INTO user (name, age) VALUES ('张三', 20) RETURNING id INTO :out_id;这里:out_id是绑定变量,在PL/SQL块里声明。SQL Server要用OUTPUT子句:
INSERT INTO user (name, age) OUTPUT INSERTED.id VALUES ('张三', 20);MySQL也可以拿LAST_INSERT_ID(),但局限性很大——它只返回当前连接最近一次插入的自增值,如果一次插入多行,只拿得到第一行ID,想拿全部ID就得靠别的手段了。
4.2 UPDATE与DELETE的多表关联写法
这个差异几乎能把每个跨库开发的折磨一轮。
MySQL支持在UPDATE里直接JOIN别的表:
UPDATE employees e JOIN departments d ON e.dept_id = d.dept_id SET e.dept_name = d.dept_name WHERE d.location = '北京';SQL Server也支持类似的写法,只是语法结构略有不同:
UPDATE e SET e.dept_name = d.dept_name FROM employees e JOIN departments d ON e.dept_id = d.dept_id WHERE d.location = '北京';Oracle就比较轴了,它不支持UPDATE...JOIN这种直观写法,要么用相关子查询:
UPDATE employees e SET e.dept_name = ( SELECT d.dept_name FROM departments d WHERE d.dept_id = e.dept_id AND d.location = '北京' ) WHERE EXISTS ( SELECT 1 FROM departments d WHERE d.dept_id = e.dept_id AND d.location = '北京' );要么用MERGE INTO来实现带关联的更新。Oracle每次遇到这种需求都让我觉得“杀鸡用牛刀”,但没办法,它的语法体系就是不允许多表直接UPDATE。
DELETE的多表关联差异与此类似。MySQL支持DELETE FROM employees USING employees JOIN departments ...这种写法;SQL Server支持DELETE FROM e FROM employees e JOIN departments d ...;Oracle则必须先写子查询确定要删的ID列表,再DELETE FROM employees WHERE dept_id IN (SELECT dept_id FROM departments WHERE location='北京')。总之,Oracle世界里,“一步到位”的思维要收敛一下。
4.3 MERGE与UPSERT的取舍
很多业务场景需要“有则更新,无则插入”,三库的处理思路完全不同。
Oracle的MERGE最成熟,是官方推荐的写法:
MERGE INTO employees e USING (SELECT 1001 AS id, '张三' AS name FROM DUAL) s ON (e.id = s.id) WHEN MATCHED THEN UPDATE SET e.name = s.name WHEN NOT MATCHED THEN INSERT (id, name) VALUES (s.id, s.name);SQL Server也有MERGE,语法基本一致,但有几个著名的坑。一是WHEN NOT MATCHED之外的AND条件如果写复杂了容易产生“多行匹配导致更新同一行”的运行时错误;二是MERGE在事务隔离级别下有可能引发锁争议,社区甚至有人建议SQL Server下用单独的UPDATE加INSERT替代MERGE以获得更好的可预测性。
MySQL没有MERGE,它的UPSERT写法和前两者完全不同,用INSERT ... ON DUPLICATE KEY UPDATE:
INSERT INTO employees (id, name) VALUES (1001, '张三') ON DUPLICATE KEY UPDATE name = '张三';注意这里触发更新逻辑的前提是唯一键或主键冲突。还有一个REPLACE INTO,它本质上先删后插,副作用是自增ID会变、外键关联可能会断,非特殊情况不建议在生产里用REPLACE。
一句话总结:Oracle和SQL Server选MERGE,MySQL选ON DUPLICATE KEY UPDATE,迁移时这两类逻辑不能直接等价替换,必须逐一评审。
5. 函数与日期处理:报账、报表和ETL最常用的战场
5.1 字符串函数对照
日常用到的字符串函数,三库差异集中在一张表里:
| 操作 | MySQL | Oracle | SQL Server |
|---|---|---|---|
| 字符串拼接 | CONCAT(a,b,c) | a||b||c | a+b |
| 字符串长度 | CHAR_LENGTH(s) | LENGTH(s) | LEN(s) |
| 字节长度 | LENGTH(s) | LENGTHB(s) | DATALENGTH(s) |
| 截取子串 | SUBSTRING(s,pos,len) | SUBSTR(s,pos,len) | SUBSTRING(s,pos,len) |
| 替换字符 | REPLACE(s,a,b) | REPLACE(s,a,b) | REPLACE(s,a,b) |
| 转大写/小写 | UPPER/LOWER | UPPER/LOWER | UPPER/LOWER |
| 去空格 | TRIM(s) | TRIM(s) | TRIM(s) |
| 聚合拼接 | GROUP_CONCAT | LISTAGG | STRING_AGG |
注意MySQL的GROUP_CONCAT和Oracle的LISTAGG在连接大量元素时有长度限制;SQL Server的STRING_AGG从2017年才有,之前只能用FOR XML PATH这种歪招拼接,那一长串写法能背下来的人都是老僵尸级的了。
还有一个容易忽略的细节:Oracle的SUBSTR的起始位置支持负数,从末尾开始数,MySQL和SQL Server的SUBSTRING起始位置不支持负数(MySQL的可以从0开始,SQL Server有类似模拟写法),写跨库代码时截取逻辑必须自己多套一层。
5.2 日期函数对照
日期处理是跨库迁移的深水区,我每次做这种任务都先问一句:“你们的日期逻辑集中在哪几个函数里?”这里列最核心的几个。
获取当前日期时间:
- MySQL:
NOW()、CURDATE()、SYSDATE() - Oracle:
SYSDATE、CURRENT_TIMESTAMP - SQL Server:
GETDATE()、SYSDATETIME()
格式化:
- MySQL:
DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s') - Oracle:
TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') - SQL Server:
CONVERT(VARCHAR(19), GETDATE(), 120)
解析字符串:
- MySQL:
STR_TO_DATE('2024-01-15', '%Y-%m-%d') - Oracle:
TO_DATE('2024-01-15', 'YYYY-MM-DD') - SQL Server:
CONVERT(DATE, '2024-01-15', 120)或CAST('2024-01-15' AS DATE)
日期差:
- MySQL:
DATEDIFF(end, start)直接返回天数 - Oracle:
end - start返回相差天数,但两个DATE相减得到的是小数(含时分秒换算) - SQL Server:
DATEDIFF(DAY, start, end)必须先指定日期part
日期加减:
- MySQL:
DATE_ADD(date, INTERVAL 1 DAY) - Oracle:
date + 1直接加天数,ADD_MONTHS(date, 1)加月份 - SQL Server:
DATEADD(DAY, 1, date)
这些差异如果靠脑子硬记很容易混,最好是维护一张自己项目内部的函数映射表,迁移时直接查表翻译,比现场查文档快得多。
5.3 类型转换与IF逻辑的差异
类型转换上,SQL Server的CAST和CONVERT双雄最直观,CONVERT还能指定样式码(比如日期格式的系列编号)。Oracle的TO_CHAR、TO_NUMBER、TO_DATE三件套在字符串和日期之间来回横跳,语义清晰但写起来啰嗦。MySQL的CAST基本够用,CONVERT函数则可以选择字符集,比如CONVERT(s USING utf8mb4)。
IF逻辑的差异更值得说道:
MySQL有IF(条件, 真值, 假值)这个简洁函数,很受大家喜爱;Oracle没有IF()函数,只能用CASE WHEN或DECODE(),DECODE是Oracle的特色写法:
SELECT DECODE(status, 'A', '正常', 'B', '禁用', '未知') FROM orders;SQL Server也没有IF函数,条件取值用CASE WHEN或IIF()(从2012年开始支持)。
写跨库SQL时,尽量把业务里所有IF()都改成标准CASE WHEN,这样三库通用,虽然啰嗦一点,但换库的时候省心到偷笑。
6. 存储过程、游标与事务控制
6.1 存储过程的外观差异
存储过程的热搜词很高,必然有大量同学在这些语法细节上栽过跟头。
MySQL存储过程的结构:
DELIMITER // CREATE PROCEDURE get_user(IN uid INT, OUT uname VARCHAR(50)) BEGIN SELECT name INTO uname FROM user WHERE id = uid; END // DELIMITER ;Oracle用PL/SQL,参数要区分IN/OUT/IN OUT,变量定义放在IS后面,赋值用:=:
CREATE OR REPLACE PROCEDURE get_user(uid IN NUMBER, uname OUT VARCHAR2) IS BEGIN SELECT name INTO uname FROM user WHERE id = uid; END get_user;PL/SQL里有个和T-SQL完全不同的点:SELECT ... INTO必须有且只能有一行返回,返回多行直接报TOO_MANY_ROWS,返回零行报NO_DATA_FOUND,处理起来要写异常块,这点很多从MySQL转过来的人第一次写就懵了。
SQL Server的存储过程:
CREATE PROCEDURE get_user @uid INT, @uname NVARCHAR(50) OUTPUT AS BEGIN SELECT @uname = name FROM user WHERE id = @uid; END变量用@前缀表示,赋值用SELECT @变量 = 列 FROM ...或SET @变量 = 值。还有一个经典差异:T-SQL里如果不需要在存储过程里显示中间结果,不要随意写SELECT,因为驱动层ExecuteReader会把结果集当成返回给客户端的检索结果。
6.2 游标和循环:PL/SQL和T-SQL的经典差异
写复杂的逐行处理逻辑时,三库的循环语法也完全不同。
MySQL里常用WHILE ... DO ... END WHILE配合游标DECLARE cur CURSOR FOR SELECT ...,LOOP里通过FETCH ... INTO逐行取值,然后用done标志位退出。
Oracle的PL/SQL游标最简洁:
FOR rec IN (SELECT id FROM user) LOOP DBMS_OUTPUT.PUT_LINE(rec.id); END LOOP;它直接用隐式游标把整个SELECT循环处理干净了,不需要显式声明变量和退出条件,效率也高。
SQL Server则要用CURSOR显式声明:
DECLARE @id INT; DECLARE cur CURSOR FOR SELECT id FROM user; OPEN cur; FETCH NEXT FROM cur INTO @id; WHILE @@FETCH_STATUS = 0 BEGIN PRINT @id; FETCH NEXT FROM cur INTO @id; END CLOSE cur; DEALLOCATE cur;T-SQL的@@FETCH_STATUS = 0判断退出条件是一个特色,很多初学者忘了在循环体末尾再加一次FETCH NEXT,结果死循环,这是SQL Server游标最常见的bug。
6.3 事务、异常处理和动态SQL
事务控制基础语句三库都支持BEGIN TRANSACTION、COMMIT、ROLLBACK,但细节有差异。
MySQL默认每条SQL自动提交,要手动事务必须显式写:
START TRANSACTION; ... COMMIT; -- 或 ROLLBACK;Oracle默认也不是自动提交,隐式提交由DDL语句触发,DML语句执行后如果不COMMIT会一直持有锁,而且其他会话可能看到未提交的数据版本(读一致性由Undo机制保证)。这一点和我们平时在MySQL里的直觉完全不一样,在Oracle里操作完数据不提交就关掉客户端,数据直接没了,但表锁可能被占住一段时间。
SQL Server的TRANSACTION支持命名事务、保存点(SAVE TRANSACTION),配合TRY...CATCH做异常回滚是最顺手的:
BEGIN TRY BEGIN TRANSACTION; ... COMMIT; END TRY BEGIN CATCH ROLLBACK; END CATCH;Oracle的异常处理用EXCEPTION WHEN OTHERS THEN ROLLBACK;,MySQL则用DECLARE EXIT HANDLER FOR SQLEXCEPTION或SIGNAL声明错误处理,风格差异巨大。
动态SQL也是重头戏。Oracle用EXECUTE IMMEDIATE加绑定变量,SQL Server用EXEC sp_executesql @sql, N'@p1 INT', @p1 = 1,MySQL用PREPARE加EXECUTE或存储过程中的CONCAT拼SQL。无论在哪一个库里,我都要强调:动态SQL一定要用绑定变量,尽量不要直接拼接字符串,防SQL注入只是一方面,更重要的是Oracle和SQL Server绑定变量能有效复用执行计划,性能差距在复杂语句里能到几十倍。
7. 元数据与系统表:三库各自的“户口本”
7.1 MySQL:information_schema的开放性
MySQL的元数据查询最开放,中央枢纽就是information_schema数据库。想查某张表的结构,用DESC 表名或者查COLUMNS视图:
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'my_db' AND TABLE_NAME = 'user';MySQL还提供SHOW CREATE TABLE这种人性化命令,一键导出建表语句,做备份或迁移时最常用。
7.2 Oracle:ALL_TABLES与DBA视角
Oracle的数据字典就厚重多了。普通用户能查的是USER_TABLES(用户自己的表)、ALL_TABLES(有权限访问的所有表)、DBA_TABLES(全部表,需要高权限)。查字段信息用ALL_TAB_COLUMNS:
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, DATA_LENGTH FROM ALL_TAB_COLUMNS WHERE OWNER = 'SCOTT' AND TABLE_NAME = 'EMPLOYEES';新手最容易犯的错是:在Oracle里执行SELECT * FROM sys.tables或者去看别的数据库的information_schema,发现根本没有这个库——Oracle的元数据是数据字典,不是数据库架构。
7.3 SQL Server:sys架构与系统视图
SQL Server的元数据集中在sys架构下,最常用的是sys.tables、sys.columns、sys.indexes:
SELECT t.name AS table_name, c.name AS column_name, ty.name AS data_type FROM sys.tables t JOIN sys.columns c ON t.object_id = c.object_id JOIN sys.types ty ON c.user_type_id = ty.user_type_id WHERE t.name = 'User';它还提供了sp_help这种存储过程快速查看表结构,和sp_who2查看阻塞进程,是老DBA手边最顺手的工具。SQL Server还支持INFORMATION_SCHEMA.COLUMNS标准视图,这一点和MySQL一致,写跨库查询工具时优先用INFORMATION_SCHEMA标准视图会把兼容性拉高不少。
8. 我踩过的跨库迁移坑与通用语法建议
最后这部分不列什么高深原理,就说我在真实项目里搬数据、改代码时撞过的一些南墙,以及现在沉淀下来的操作习惯。
第一个坑是“想当然的等价替换”。有一年我把一套用户积分模块从MySQL迁到Oracle,本地一测,发现所有分页查询全部报错,我花了半天把所有LIMIT改成了FETCH FIRST。改完后另一批报表SQL又报错,一看是用了IF(score >= 100, 'VIP', 'NORMAL'),Oracle根本不认识IF函数,只能改成CASE WHEN。这两批SQL看似都是“换个写法就行”,但每一处都要仔细比对NULL语义、隐式转换、排序规则,不是能手动批量替换的。
第二个坑是日期类型和空字符串。从SQL Server往Oracle导数据时,空字符串在Oracle里变成NULL,业务逻辑判断全乱;日期字段从SQL Server的DATETIME2搬到Oracle,精度差异导致某些比较条件差了一毫秒,对不上账。这类问题只能在迁移前做字段级映射分析,没有捷径。
第三个经验是:写新功能时,尽量让代码贴近标准SQL。具体做法是——字符串拼接优先用CONCAT的多参版本,不要依赖某种库独有的拼法;条件取值只用CASE WHEN,别用IF和DECODE;分页如果版本允许,统一用OFFSET FETCH;UUID主键比自增主键更容易在三库之间平移;日期比较尽量用>=和<而非BETWEEN,避免精度边界问题。
第四个经验是善用ORM或方言层。现在主流的ORM框架都支持多数据库方言配置,MyBatis有databaseIdProvider,Hibernate有方言类,EF Core有多数据库提供程序。如果项目本身就在多个数据库上跑,老老实实引入方言层,让框架去拼不同语法的SQL,比自己维护一套SQL模板要省心得多。
最后一个建议关乎心态:不要试图背下所有差异,这不现实,也没必要。数据库语法差异不是死记硬背的知识点,而是一套可以整理成映射表的工具。我在自己的工程文档里维护了一张速查表,从数据类型、字符串函数、日期函数、分页语法到存储过程模板,全都在一行行对照着写。这个表是每次迁移、每次新项目开工都翻出来对一遍的,比临时看官方文档能省下大半天时间。
如果你现在正被某个三库语法差异卡住,建议按这个顺序排查:先确认数据库版本和兼容模式,再用最小案例分别在三库中执行对比,最后把结论补进自己的映射表。这比你翻一百篇零散笔记都有用。