news 2026/11/6 1:55:30

MySQL、Oracle、SQL Server三大数据库语法差异详解与踩坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL、Oracle、SQL Server三大数据库语法差异详解与踩坑指南

做数据库开发和运维这些年,我身边几乎每个人都喊过同一句话:“这三个库的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 一张表看懂三库性格

对比维度MySQLOracleSQL Server
语言继承MySQL原生SQLPL/SQLT-SQL
设计理念轻量简洁严谨厚重易用友好
典型场景互联网应用、读写频繁银行、电信等核心交易系统企业ERP、微软生态
自增方案AUTO_INCREMENTSEQUENCE序列IDENTITY
分页习惯LIMITROWNUM/OFFSETTOP/OFFSET FETCH
字符串拼接CONCAT()||+
日期处理DATE_FORMATTO_CHAR/TO_DATECONVERT

这张表基本就是全文的浓缩版,后面每一个差异点,我都会展开讲原理和使用细节。

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 字符串函数对照

日常用到的字符串函数,三库差异集中在一张表里:

操作MySQLOracleSQL Server
字符串拼接CONCAT(a,b,c)a||b||ca+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/LOWERUPPER/LOWERUPPER/LOWER
去空格TRIM(s)TRIM(s)TRIM(s)
聚合拼接GROUP_CONCATLISTAGGSTRING_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模板要省心得多。

最后一个建议关乎心态:不要试图背下所有差异,这不现实,也没必要。数据库语法差异不是死记硬背的知识点,而是一套可以整理成映射表的工具。我在自己的工程文档里维护了一张速查表,从数据类型、字符串函数、日期函数、分页语法到存储过程模板,全都在一行行对照着写。这个表是每次迁移、每次新项目开工都翻出来对一遍的,比临时看官方文档能省下大半天时间。

如果你现在正被某个三库语法差异卡住,建议按这个顺序排查:先确认数据库版本和兼容模式,再用最小案例分别在三库中执行对比,最后把结论补进自己的映射表。这比你翻一百篇零散笔记都有用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/25 1:54:25

STM32本质是嵌入式操作系统底座,不是高级51单片机

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/28 11:43:38

Lerobot+飞特舵机:低成本搭建智能机械臂实战全攻略

我最早接触Lerobot&#xff0c;是冲着Hugging Face团队在开源机器人领域搞的那套东西去的。当时第一反应是&#xff1a;一个搞大模型的开源社区&#xff0c;怎么跑来折腾机械臂了&#xff1f;结果细看之后发现&#xff0c;Lerobot这个项目比我想象中务实得多——它把数据采集、…

作者头像 李华
网站建设 2026/10/28 20:22:43

Linux环境下MySQL学习路径:从安装到锁与事务实战

很多刚开始接触Linux的朋友都会在同一个地方卡住&#xff1a;系统装好了&#xff0c;命令也敲得挺溜&#xff0c;但一说到“装个数据库用用”&#xff0c;立刻没了方向。MySQL作为互联网行业最普及的关系型数据库&#xff0c;基本是运维和开发绕不开的第一站。这篇博文就围绕在…

作者头像 李华
网站建设 2026/10/27 23:12:49

QEMU+RISC-V搭建虚拟AI芯片实验台:AGENT辅助软件栈验证实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/28 4:48:37

Altium Designer差分对设计实战:规则设置与高效走线全解析

1. 差分对设计前的思路梳理&#xff1a;为什么要花力气折腾规则做高速PCB设计的人&#xff0c;早晚都会碰到差分信号。USB、HDMI、PCIe、千兆网、LVDS&#xff0c;这些接口清一色都是差分对&#xff0c;跑起来动不动就是几百Mbps甚至几个Gbps。以前我刚接触Altium Designer&…

作者头像 李华