多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

MySQL、Oracle、SQLServer语法差异与迁移实战指南

MySQL、Oracle、SQLServer语法差异与迁移实战指南 1. 数据类型与字符串处理迁移时最先崩的地方干这行越久越觉得MySQL、Oracle、SQLServer 的语法区别就像三个方言极其浓厚的地区明明都是说“中国话”可一到具体表达就谁也不服谁。很多朋友费劲装好 MySQL 或 SQLServer还没高兴两天就把一套脚本从 Oracle 搬过来结果从数据类型开始一路报错。其实这第一步的分岔就发生在字符类型和字符串处理上。1.1 字符类型varchar、varchar2 和 nvarchar 不是一回事先看三家的字符类型命名。MySQL 最常用的是varcharOracle 是varchar2SQLServer 是varchar和nvarchar。光看名字很容易以为它们差不多但实际行为差别很大。MySQL 的varchar(n)里的 n 是字符数不是字节数。比如varchar(50)能存 50 个汉字。但注意它有一个行大小限制一般情况下varchar的最大长度受限于65535字节如果表里还有别的字段实际能用的更少。在 utf8mb4 字符集下一个汉字占 4 字节所以varchar(255)在极端情况下就可能超限。Oracle 的varchar2(n)就比较“拧”这里的 n 默认是字节数。也就是说varchar2(10)在中文库里可能只能存 3 个汉字。Oracle 官方一直强调要用字符集语义去定义字段但我见过太多老系统直接按字节定义导致中文丢数据。而且 Oracle 的varchar2必须显式写长度不能像 MySQL 那样给个默认值。SQLServer 又把事情分成两半varchar是非 Unicode 字符串nvarchar是 Unicode 字符串。如果字段里有中文、韩文、日语这些最好用nvarchar否则容易出现乱码。插入中文时SQLServer 建议加N前缀比如N张三这个细节很多从 Oracle 转过来的人根本不习惯。对比项MySQLOracleSQLServer常用类型varchar(n)varchar2(n)varchar/nvarcharn 的含义字符数字节数默认字符数中文建议utf8mb4 下直接存用 varchar2(n char) 或加大长度用 nvarchar N 前缀空字符串 合法区别于 NULL等于 NULL合法区别于 NULL这里藏着 Oracle 的一个大坑会被当成NULL所以很多从 Oracle 迁出来的表里明明看着是空字符串实际存的是 NULL。写判断语句时如果只判断name 可能查出来的数量对不上。字符串拼接同样不统一。MySQL 里的是算术加号想拼接必须用CONCAT函数。Oracle 跟标准 SQL 走用||。SQLServer 则用而且它很“聪明”会自动把数字转成字符串然后拼接。举一个真实的现场有个同事从 SQLServer 迁到 MySQL原SQL是SELECT id - name结果 MySQL 把当成加法整个表达式直接变成数学运算输出一堆 0 和 1。改成CONCAT(id, -, name)才正常。所以每到新环境先确认拼接符号就能避开一大半低级故障。1.2 字符串转数字CAST、CONVERT 与 TO_NUMBER热搜词里“sqlserver 字符串转数字”反复出现说明这是高频需求。三家的实现各有各的名字MySQLCAST(123 AS SIGNED)或CONVERT(123, SIGNED)也支持直接用字段 0做隐式转换。OracleTO_NUMBER(123)字符串里带货币符号、千分位时需要用格式模板比如TO_NUMBER(1,200, 9,999)。SQLServerCAST(123 AS INT)或CONVERT(INT, 123)。想防止异常直接报错可以用TRY_CAST和TRY_CONVERT转换失败时返回 NULL。注意一个隐藏问题如果字段是varchar类型存的是10和9直接比较会按字符顺序排10会排在9前面。解决办法就是显式转成数字再排序。MySQL 里常见写法是ORDER BY CAST(col AS UNSIGNED)或者是ORDER BY col 0Oracle 写ORDER BY TO_NUMBER(col)SQLServer 写ORDER BY CAST(col AS INT)。这类转换还要考虑 NULL。三家的空值函数分别是MySQLIFNULL(a, b)和COALESCE(a, b)OracleNVL(a, b)和COALESCE(a, b)SQLServerISNULL(a, b)和COALESCE(a, b)它们都能在字段为 NULL 时返回替代值。尽量优先用COALESCE因为它是 SQL 标准函数跨数据库差异最小。不过NVL和IFNULL在各自生态里已经用得铺天盖地迁移时经常要整体替换。2. 查询写法差异分页、排序与条件逻辑查询语句是日常动用最频繁的部分也是语法区别最直观的体现。同样一个“按 id 排序后取第 21 到 30 条”的需求三个数据库的写法可能完全不是一套逻辑。2.1 分页LIMIT、ROWNUM 和 OFFSET FETCHMySQL 最简单直接LIMITSELECT * FROM user ORDER BY id LIMIT 20, 10; -- 或者 SELECT * FROM user ORDER BY id LIMIT 10 OFFSET 20;这里20是跳过多少条第二段是取多少条。MySQL 5.7 及之前的写法很容易把顺序记反写成了LIMIT 10, 20就变成跳过 10 条取 20 条。Oracle 传统写法要绕一个弯。因为ROWNUM是查询结果生成时顺手赋的行号而不是先排序后赋号。想取排序后的区间必须三层嵌套SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT * FROM user ORDER BY id ) t WHERE ROWNUM 30 ) WHERE rn 20;这个写法在 Oracle 11g 及以前尤其常见。如果少了最内层的排序取到的就可能不是按 id 排序后的第 21 到 30 条。Oracle 12c 开始推出了OFFSET ... FETCH跟 SQLServer 的写法接近SELECT * FROM user ORDER BY id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;SQLServer 2008 及更早的年代没有OFFSET最常用ROW_NUMBER()SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (ORDER BY id) rn FROM user ) t WHERE rn BETWEEN 21 AND 30;SQLServer 2012 之后才支持OFFSET...FETCHSELECT * FROM user ORDER BY id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;注意 SQLServer 的OFFSET一定要求有ORDER BY否则直接报错。取单条记录时MySQL 用LIMIT 1Oracle 用ROWNUM 1或者新版的FETCH FIRST 1 ROWS ONLYSQLServer 用TOP 1。很多人不知道 Oracle 老版本里不能直接写WHERE ROWNUM 1之外的范围因为ROWNUM 1永远不成立这是我被问过最多的问题之一。分页性能上的区别也值得说。MySQL 深分页时会越翻越慢比如LIMIT 1000000, 10要先扫描前一百万行再丢弃通常的优化方案是先查主键再用主键关联取值。Oracle 因为ROWNUM在生成时就会终止扫描数据读取量相对可控。SQLServer 的ROW_NUMBER()需要全局排序大数据量下内存和临时库压力大新的OFFSET方式同样有深翻页问题Keyset Paging 是更好的办法。2.2 排序空值与条件逻辑同样一个ORDER BY结果不一样排序时遇到 NULL三家的默认行为不同这是特别容易踩的乱子。数据库升序默认位置如何控制MySQLNULL 最前ORDER BY IFNULL(col, 0)或ORDER BY col IS NULL, colOracleNULL 最后ORDER BY col NULLS LASTSQLServerNULL 最前ORDER BY CASE WHEN col IS NULL THEN 1 ELSE 0 END, col比如排行榜字段score是空的MySQL 和 SQLServer 默认把 NULL 排在最前面Oracle 默认把 NULL 排在最后面。如果不统一规则同一个报表在三个库跑出来排序结果完全不一样。我一般建议在 SQL 里显式写明空值处理不要依赖默认行为。条件逻辑也有差异。MySQL 提供了IF(expr, val1, val2)函数Oracle 喜欢用DECODE(expr, val1, result1, ... DEFAULT)SQLServer 2012 以后也提供了IIF。不过它们都属于各自扩展如果想保持兼容老老实实写标准CASE WHEN最稳妥。布尔类型的表达同样奇怪。MySQL 里BOOLEAN本质是TINYINT(1)可以写WHERE flag。Oracle 没有真正的布尔类型通常用NUMBER(1)或者CHAR(1)所以必须写成WHERE flag 1。SQLServer 有BIT类型但也不能直接写WHERE flag要写WHERE flag 1。从 MySQL 迁出去的人最容易在这里碰运气。模糊查询的通配符也有差别。三家的%和_都一样但 SQLServer 额外支持方括号[0-9]这类字符范围匹配。MySQL 的传统LIKE不支持这种写法可以用REGEXP正则替代。涉及到_本身要匹配时记得用ESCAPE指定转义符不然下划线会被当成通配符。3. 函数家族日期时间与字符串函数的“双胞胎不同命”函数是语法差异的另一大集中地。尤其日期函数因为数据库的存储格式、时区策略、语言环境不同导致写法五花八门。3.1 当前时间和日期格式化NOW、SYSDATE 与 GETDATE获取当前时间MySQL 最常用NOW()Oracle 使用SYSDATE或SYSTIMESTAMPSQLServer 使用GETDATE()。格式化日期就更“散装”了MySQL 写SELECT DATE_FORMAT(NOW(), %Y-%m-%d %H:%i:%s);Oracle 写SELECT TO_CHAR(SYSDATE, YYYY-MM-DD HH24:MI:SS) FROM DUAL;SQLServer 写SELECT FORMAT(GETDATE(), yyyy-MM-dd HH:mm:ss); -- 或者 SELECT CONVERT(VARCHAR(19), GETDATE(), 120);Oracle 的TO_CHAR用的是HH24、MI、SS这套格式符MySQL 则用%H、%i、%s。SQLServer 除了FORMAT函数老项目里更喜欢CONVERT配合 style 参数比如120就代表yyyy-mm-dd hh:mi:ss。这些 format 代码各不相同迁移时改到怀疑人生。日期运算更是经典。Oracle 里日期直接加减数字SYSDATE 1就是明天同一时间。MySQL 需要使用INTERVAL写成DATE_ADD(NOW(), INTERVAL 1 DAY)。SQLServer 用DATEADD(day, 1, GETDATE())。取两个日期之间的天数MySQL 用DATEDIFF(a, b)Oracle 直接a - b得到带小数点的天数SQLServer 用DATEDIFF(day, a, b)。日期类型本身也要注意。MySQL 的DATE只有日期DATETIME包含时分秒TIMESTAMP还受时区影响。Oracle 的DATE其实包含时分秒很多人以为是纯日期结果 JDBC 读取时莫名其妙多了 00:00:00。SQLServer 的DATE是新版的纯日期老式的DATETIME范围到 2079 年而datetime2范围更广且精度更高。3.2 字符串函数LENGTH 和 LENSUBSTR 和 SUBSTRING字符串函数最常见的就是取长度、截取、找位置、补位和去空格。这些函数名字相近但行为和返回值常常不一样。取长度MySQL 的LENGTH返回字节数CHAR_LENGTH返回字符数Oracle 的LENGTH返回字符数LENGTHB返回字节数SQLServer 的LEN返回字符数但会自动去掉尾部空格DATALENGTH返回字节数。拿“中文”这两个字做测试MySQL 在 utf8mb4 下LENGTH可能是 8CHAR_LENGTH是 2。不看清楚就会得到“神奇”结果。截取字符串MySQL 和 Oracle 都习惯用SUBSTRING/SUBSTROracle 更常见的是SUBSTR(str, start, len)SQLServer 同样是SUBSTRING(str, start, len)。注意这三个数据库的截取都是从 1 开始计数完全符合日常直觉但就是有人总认为是 0 开始。查找字符位置MySQL 用LOCATE或INSTROracle 用INSTRSQLServer 用CHARINDEX。返回值都是位置数字但参数顺序不一样。比如 SQLServer 是CHARINDEX(子串, 字符串)Oracle 的INSTR(字符串, 子串)刚好反过来迁移时特别容易把参数写反。补位函数MySQL 有LPAD和RPADOracle 也有SQLServer 原生没有需要用REPLICATE(0, 5 - LEN(col)) col或者RIGHT(REPLICATE(0, 5) col, 5)这种方式模拟。去空格三家里LTRIM、RTRIM都有但 SQLServer 是从 2017 版才提供了标准TRIM老版本只能LTRIM(RTRIM(...))而 MySQL 和 Oracle 直接用TRIM。4. 存储过程、事务与元数据查询的进阶差异函数和查询只是开胃菜真正让迁移工作量直线上升的是存储过程和事务体系。如果业务逻辑大量写在数据库端换库基本等于重写一部分后端代码。4.1 存储过程的框架差异分隔符、参数模式与异常处理先看一段最简单的存储过程作用是按 id 查名字并返回给外部变量。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 写法CREATE OR REPLACE PROCEDURE get_user(uid IN NUMBER, uname OUT VARCHAR2) AS BEGIN SELECT name INTO uname FROM user WHERE id uid; END;SQLServer 写法CREATE PROCEDURE get_user uid INT, uname NVARCHAR(50) OUTPUT AS BEGIN SELECT uname name FROM [user] WHERE id uid; END;看起来结构相似但细节差异不少。MySQL 必须用DELIMITER改分隔符避免存储过程内部的;把整段语句拆碎。Oracle 没有这个苦恼但它要求IN和OUT要明确写出而且没有INOUT用的是IN OUT。SQLServer 的参数名全都要带输出参数还要标OUTPUT。异常处理也各成一派。MySQL 的存储过程里写DECLARE EXIT HANDLER FOR SQLEXCEPTIONOracle 用异常块BEGIN ... EXCEPTION WHEN OTHERS THEN ... END;SQLServer 则是BEGIN TRY ... BEGIN CATCH ... END。游标部分同样不统一Oracle 有隐式游标和FOR ... IN写法SQLServer 要先DECLARE CURSOR再OPEN再FETCHMySQL 也类似。这些都导致存储过程迁移不仅只是替换函数名而是要重新梳理控制流。动态 SQL 也是一个大项。Oracle 常用EXECUTE IMMEDIATESQLServer 常用EXEC sp_executesqlMySQL 用PREPARE和EXECUTE。参数绑定方式MySQL 用?Oracle 用:nameSQLServer 用name代码改造量肉眼可见。4.2 事务控制与锁行为AUTOCOMMIT 的坑事务是三家中差异容易隐藏得很深的部分。MySQL 默认autocommit 1执行一条语句就自动提交。想开启一个事务要写START TRANSACTION或BEGIN并且要注意 DDL 语句会隐式提交当前事务。Oracle 的事务模式不太一样它没有一个显式的“开启事务”动作从第一条 SQL 开始就进入了事务直到执行COMMIT或ROLLBACK。SQLServer 默认也是自动提交模式显式使用BEGIN TRANSACTION。因为默认提交策略不同从 Oracle 迁到 MySQL 的代码里如果原封不动图省事省掉显式事务开头可能出现想回滚却已经提交的情况。反过来从 MySQL 迁到 Oracle如果仍然每个操作后都COMMIT虽然不会出错但事务粒度被拆得很碎性能会差。锁行为差异就更大了。SQLServer 有一种常见的默认读行为是加共享锁很多人处理大表查询时会写WITH (NOLOCK)提示允许脏读。Oracle 的多版本并发控制使普通查询不阻塞写、写不阻塞读所以根本没有必要用那种WITH (NOLOCK)提示。MySQL InnoDB 也是 MVCC普通 SELECT 在可重复读下不会加锁但如果要锁定某一行更新三家的写法都需要考虑SELECT ... FOR UPDATE或应用层控制。隔离级别默认值同样要留意MySQL InnoDB 默认为REPEATABLE READOracle 默认为READ COMMITTEDSQLServer 默认为READ COMMITTED。同一个事务内两次查询MySQL 可能读到一致快照Oracle 每次都会读最新已提交数据。这个差异会导致相同的业务逻辑出现不一样的结果特别是在统计报表和状态判断场景。4.3 元数据与系统表查表结构的方式完全不同开发中经常要判断表是否存在、查某张表的字段信息。三个数据库的系统表命名天差地别。MySQL 可以用SHOW TABLES、DESC 表名、SHOW CREATE TABLE也支持information_schema.COLUMNS。Oracle 强烈依赖数据字典视图比如USER_TABLES、ALL_TABLES、USER_TAB_COLUMNS在 SQL*Plus 里可以用DESCRIBE查结构。SQLServer 常见的是sys.tables、sys.columns或者系统存储过程sp_help、sp_columns。如果想写一套跨库查询列信息的逻辑并不容易。INFORMATION_SCHEMA.COLUMNS在 MySQL 和 SQLServer 里都有但 Oracle 并没有完整提供这套视图更多要靠ALL_TAB_COLUMNS。所以迁移工具大多会针对每个数据库写单独的数据字典查询而不是指望 SQL 完全兼容。删除表也体现风格。MySQL 支持DROP TABLE IF EXISTS test_tableSQLServer 2016 开始也支持同样写法但老版本要写成IF OBJECT_ID(test_table, U) IS NOT NULL DROP TABLE test_table;Oracle 里没有IF EXISTS只能用 PL/SQL 块先判断BEGIN EXECUTE IMMEDIATE DROP TABLE test_table; EXCEPTION WHEN OTHERS THEN NULL; END;对象名的处理方式也需要注意。Oracle 未加双引号的表名会自动转成大写MySQL 的表名大小写敏感度取决于lower_case_table_names参数SQLServer 则取决于排序规则。同一套建表脚本可能在 Oracle 里生成USER_TABLE在 MySQL 里写的是user_table导致 JDBC 查询时找不到对象。5. 迁移实战经验与快速记忆法前面把常用差异拆了一遍可能看着有些碎。我在实际迁移项目里通常不会同时处理所有差异而是先做一轮“方言映射”把高风险点找出来再逐个击破。这里分享几个记忆方法和实际踩坑现场。5.1 三个高频踩坑案例第一个是字符串转数字。某个计数接口原来在 SQLServer 里用ISNULL(SUM(CONVERT(INT, amount)), 0)统计金额迁移到 Oracle 后改成NVL(SUM(TO_NUMBER(amount)), 0)看起来没问题。但实际amount列里有空字符串Oracle 的TO_NUMBER遇到空字符串会直接报ORA-01722。因为 Oracle 把当作 NULL而 SQLServer 的空字符串在转换时没这么严格。最后用NVL(TO_NUMBER(NULLIF(TRIM(amount), )), 0)才绕过去。第二个是分页慢。一个订单列表从 Oracle 迁到 MySQL开发同学直接用LIMIT 600000, 20结果生产环境一次查询耗掉两秒多。后来改成先取出满足条件的主键 id再关联查询SELECT * FROM orders WHERE id IN ( SELECT id FROM orders ORDER BY create_time DESC LIMIT 600000, 20 );虽然写法一样笨但走覆盖索引后速度提升明显。这个经验不一定对所有 MySQL 版本都有效但遇到深分页时值得先试。第三个是空值排序。一个 APP 的商品列表要求“价格相同的优先展示库存高的”库存字段正好有 NULL。MySQL 里默认 NULL 排最前结果一批没有库存的商品反而全部跑到前面。最后统一转成 0ORDER BY price, IFNULL(stock, 0) DESC如果是 Oracle就要写成NVL(stock, 0)如果是 SQLServer则写成ISNULL(stock, 0)。问题很小但排查起来往往要耗半天。5.2 快速记忆法一个映射表吃透大半记住下面这张高频映射表能应对大部分日常开发功能MySQLOracleSQLServer字符串拼接CONCAT||空值替换IFNULLNVLISNULL取字符串长度CHAR_LENGTHLENGTHLEN截取子串SUBSTRINGSUBSTRSUBSTRING当前时间NOW()SYSDATEGETDATE()日期加一天DATE_ADD(... INTERVAL 1 DAY)SYSDATE 1DATEADD(day, 1, GETDATE())分页LIMITROWNUM / FETCHOFFSET FETCH / ROW_NUMBER字符串转数字CASTTO_NUMBERCAST / CONVERT表结构查询SHOW CREATE TABLE / DESCUSER_TAB_COLUMNSsp_help / sys.columns我个人接手跨数据库项目时第一步不是去翻手册而是先根据数据库版本把这张表补齐再扫一遍 SQL 脚本里的关键字优先处理LIMIT、ROWNUM、NVL、SYSDATE、GETDATE这类方言特征明显的词。等这些高危点清完再处理编码和排序规则问题整体迁移就顺利很多。语法差异再多背后其实是三个数据库的设计哲学不一样MySQL 追求轻量和易用Oracle 强调严谨和稳定SQLServer 则是微软生态下的强力整合。理解了它们的习惯很多“为什么这样写”的问题就迎刃而解。我之前帮客户做过一次从 Oracle 到 MySQL 的完整迁移前后花了两周梳理语法真正改代码的时间反而只有三天。希望大家遇到这种活儿时先把对照表和踩坑笔记准备好比我当时少走弯路。
返回列表