ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

SQL Server与MySQL语法差异对照:从连接配置到迁移踩坑全指南

SQL Server与MySQL语法差异对照:从连接配置到迁移踩坑全指南 干了这么多年数据库开发SQL Server 和 MySQL 我几乎每天都在切着用。最烦的不是 SQL 写不出来而是同一个思路在两个库里的写法看起来差不多跑起来却一个对、一个错查半天才发现是方言差异。这次我把平时自己备忘的那些点重新整理了一遍从安装连接、查询语法、表结构、事务锁到存储过程和迁移踩坑按“自用”的标准写成这篇东西顺便分享给需要在两个库之间来回切换的朋友。不想讲太深的存储引擎原理重点就是“怎么写才对、为什么这么写、出错了怎么查”。1. 装环境、选工具两边第一印象就完全不同1.1 安装和认证那些坑第一次装 SQL Server 的人很容易被它的安装流程搞懵。虽然 2016、2017、2019、2022 基本都是下一步下一步但经常在最后一步报“无法找到数据库引擎启动句柄”。我遇到过很多次原因其实就三类一是旧实例残留安装程序检测到原来的 SQL Server 服务还挂在那但服务又启动不了二是防火墙把 1433 端口挡了服务起没起来你根本不知道三是安装时选了默认实例但机器上已经有同名实例服务名冲突。我自己的排查顺序是先打开services.msc看有没有SQL Server (MSSQLSERVER)服务接着用管理员身份跑一下net start mssqlserver再去看安装目录下的ERRORLOG文件。如果日志里提示端口被占用就改端口或者把旧实例卸干净。实在不行用安装中心里的“修复”功能重来一遍多数能救回来。MySQL 这边安装通常顺很多但 8.0 的认证插件caching_sha2_password是个经典坑。你装完 8.0用老版本的 JDBC 驱动或者旧客户端一连接直接报 SSL 连接错误或者提示Public Key Retrieval is not allowed。这并不是网络问题是驱动不支持新认证方式。解决起来两个思路一是把 JDBC URL 加上allowPublicKeyRetrievaltrueuseSSLfalse二是在 MySQL 里把 root 用户改回mysql_native_password。对我这种要兼容老项目的人来说第二种方法更省心但安全等级确实低一些自己评估。要是还在用 5.7 的同学安装相对宽松默认认证就是mysql_native_password这也是很多生产环境到现在还没升 8.0 的原因。另外MySQL 装完之后去my.ini改basedir、datadir、port这些基础配置是常规操作而 SQL Server 一般不需要手动改配置文件靠“SQL Server 配置管理器”来控制端口、服务、网络协议。两边管理思路不一样别用习惯硬套。1.2 图形化工具与连接串差异工具选择上SQL Server 官方提供 SSMS功能全、免费缺点是启动慢、吃内存。MySQL 官方有 MySQL Workbench能用但交互体验一般。如果你经常两边切我建议直接上 DBeaver 或者 Navicat一套工具通吃所有数据库避免装两个客户端、记两套快捷键。命令行方面SQL Server 的sqlcmd和 MySQL 的mysql客户端都存在但语法有点差别日常调试小数据量还行复杂报表我还是喜欢图形化查。连接串是第一个真正体现差异的地方。SQL Server 默认端口是 1433实例名可以跟在服务器名后面比如localhost\\SQLEXPRESSMySQL 默认端口是 3306没有实例名的概念只有host和port。JDBC 连接串对比如下数据库JDBC 连接串示例SQL Serverjdbc:sqlserver://localhost:1433;databaseNametestdb;usersa;passwordxxxMySQLjdbc:mysql://localhost:3306/testdb?useSSLfalseserverTimezoneAsia/Shanghai.NET 环境里SQL Server 用SqlConnection连接串通常是Serverlocalhost;Databasetestdb;User Idsa;Passwordxxx;MySQL 用MySqlConnection连接串是Serverlocalhost;Databasetestdb;Uidroot;Pwdxxx;。上面这些差异虽然小但项目里一配错就是半小时起步的排查时间我建议直接存到一个常用配置清单里别靠脑子记。还有一个很容易被忽略的点MySQL 的连接用户名是带 host 的rootlocalhost和root%是两个账号。你在本机能连、服务器上连不上很多时候不是密码错是没给远程 host 授权。SQL Server 没有这套概念登录名和服务器地址是分开的。第一次从 MySQL 转过来的朋友遇到“能连本地连不了远程”时优先往这个方向想。2. SQL 写法差异CRUD 和查询里的“同理但不同法”2.1 查询语句的骨架差异终止符、别名、多表更新最直观的差异是语句结束符和批处理分隔符。SQL Server 的 SSMS 里常用GO作为批处理分隔MySQL 里没有GO默认用分号;作为语句结束。还有一个细节SQL Server 的GO并不是 T-SQL 语句它只是客户端工具识别的一个批处理标记。如果你拿一个带GO的脚本去跑比如在代码里用 SqlCommand 执行直接报语法错误。反过来MySQL 的存储过程定义里需要自己处理DELIMITER $$SQL Server 完全不需要这个我们后面讲存储过程时再展开。大小写敏感这块也要格外小心。默认情况下SQL Server 的表名、列名不区分大小写关键是数据库的排序规则Collation而 MySQL 在 Linux 上数据库名和表名默认区分大小写Windows 上又基本不区分。我有一阵子把 Linux 上开发好的建表脚本直接拿到 Windows 环境跑结果一切正常反过来又出问题就是大小写导致的。所以迁移时先确认lower_case_table_names这个系统参数是什么值。多表 UPDATE 和 DELETE 的语法差别比较隐蔽。SQL Server 的 UPDATE 多表联更是UPDATE a SET ... FROM 主表 a JOIN 关联表 b ON ...MySQL 则习惯把 JOIN 放在 UPDATE 和 SET 之间写成UPDATE 主表 a JOIN 关联表 b ON ... SET ...。DELETE 也一样SQL Server 里DELETE a FROM ...是合法的MySQL 里DELETE a FROM ...也支持但如果你不带别名直接写DELETE FROM 主表 WHERE EXISTS (...)在两边都能跑通。我的建议是多表删除更新统一写子查询 EXISTS尽量少用连接语法这样移植性最好。2.2 字符串处理拼接、空值、转数字字符串拼接是我踩过最多坑的地方。SQL Server 里abc def直接拼MySQL 里在字符串上下文里会当数字运算得用CONCAT(abc, def)。更麻烦的是拼接时遇到 NULLSQL Server 里a NULL结果是NULLMySQL 的CONCAT(a, NULL)结果也是NULL但如果用CONCAT_WS它会跳过 NULL。我一般在需要拼接列表字段时先用ISNULL或者IFNULL把 NULL 处理掉再拼避免结果整列为空。字符串转数字也是高频需求。SQL Server 最简单的是CAST(123 AS INT)或者CONVERT(INT, 123)两者都能用。MySQL 同样有CAST(123 AS SIGNED)也有CONVERT(123, SIGNED)这种写法。但这里有个坑MySQL 的CONVERT有两种完全不同的用法一个是转字符集比如CONVERT(name USING utf8mb4)一个是转数据类型比如CONVERT(123, SIGNED)我第一次用的时候把参数顺序写反直接报错。另外SQL Server 对字符串转数字的容忍度很低12a直接转会报错MySQL 的隐式转换相对宽松12a转出来往往是 12但这反而容易掩盖数据问题我建议两边转数字都尽量先用正则或者TRY_CASTSQL Server 2012做校验。空值处理函数也要区分SQL Server 用ISNULL(a, b)MySQL 用IFNULL(a, b)两边共同支持COALESCE(a, b, c)。我自己的习惯是统一用COALESCE这样在两边都不用改。别忘了NULLIF(a, b)两边都有做除法防除零很有用比如SELECT a / NULLIF(b, 0) FROM t。2.3 分页与排名OFFSET FETCH 与 LIMIT分页可以说是切换数据库时最容易写错的地方。SQL Server 2012 之前的版本没有 OFFSET老项目里全是ROW_NUMBER() OVER (ORDER BY id)套一层子查询又长又绕。2012 之后终于支持标准写法SELECT * FROM t ORDER BY id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;MySQL 简单直接SELECT * FROM t ORDER BY id LIMIT 20, 10;两个库现在都支持窗口函数所以ROW_NUMBER()在两边都能用。不过注意MySQL 8.0 之前是不支持窗口函数的老版本只能用rownum这种用户变量模拟性能一言难尽。如果你还在维护 MySQL 5.7遇到“分组后取每组前 N 条”的需求别硬写窗口函数老老实实做关联子查询或者自增变量。有个冷门问题热搜里也有人问“SQL Server OFFSET 后再 TOP 20 查到的是什么”。这里要特别提醒TOP和OFFSET混用非常危险。因为SELECT TOP(20) ... ORDER BY id OFFSET 10 ROWS的执行顺序里OFFSET 是在 ORDER BY 之后先对结果集做跳过TOP 是在查询最后取行所以它的真实含义是“先排好序跳过前 10 条在剩余行里取前 20 条”。虽然逻辑上能自洽但读代码的人很容易理解成“先取前 20再跳过 10”建议分页就老老实实用OFFSET FETCH不要跟TOP混着写。2.4 CASE WHEN 与分支逻辑CASE WHEN表达式在 SQL Server 和 MySQL 里基本可以无缝切换复杂条件聚合、行转列都靠它。比如统计每个用户满足不同条件的数量两边写法几乎一样SELECT user_id, SUM(CASE WHEN status A THEN 1 ELSE 0 END) AS a_cnt, SUM(CASE WHEN status B THEN 1 ELSE 0 END) AS b_cnt FROM orders GROUP BY user_id;唯一的坑是 NULL 判断在CASE WHEN里判断 NULL必须写WHEN col IS NULL不能写WHEN col NULL这个规则两边通用但新手很容易踩。另外SQL Server 提供了IIF(condition, true_value, false_value)MySQL 对应的是IF(condition, true_value, false_value)函数名不一样迁移的时候直接替换即可。NULLIF(a, b)两边通用适合做“除数为零返回 NULL”的保护。3. 进阶必备表结构、事务、锁、多行合并3.1 创建表自增、默认值、字段类型差异建表时最明显的差异是自增列。SQL Server 用IDENTITY(1,1)只允许在创建表或修改表时定义插入时不能给自增列显式赋值除非临时打开SET IDENTITY_INSERT ON。MySQL 用AUTO_INCREMENT插入时如果想指定某个 ID可以直接写后续自增会接着你指定的最大值继续走。这个习惯差异看起来很细但在做数据迁移、导入导出时影响挺大的。重置自增值的语法也不一样。SQL Server 用DBCC CHECKIDENT (表名, RESEED, 0);MySQL 用ALTER TABLE 表名 AUTO_INCREMENT 1;字段类型映射方面SQL Server 的NVARCHAR在 MySQL 里一般用VARCHAR就够SQL Server 的DATETIME2对应 MySQL 的DATETIMESQL Server 的UNIQUEIDENTIFIER对应 MySQL 的CHAR(36)或UUID()。特别注意TIMESTAMPSQL Server 里的TIMESTAMP是行版本号跟时间没关系MySQL 里的TIMESTAMP是真时间并且有可能自动更新。如果迁移时直接搬运建表语句这会是一个隐蔽的坑。默认值写法也不一样。SQL Server 当前时间默认值是DEFAULT GETDATE()MySQL 是DEFAULT CURRENT_TIMESTAMP并且TIMESTAMP类型还能配ON UPDATE CURRENT_TIMESTAMP自动改时间。修改表结构时SQL Server 用ALTER TABLE t ALTER COLUMN c INTMySQL 用ALTER TABLE t MODIFY COLUMN c INT关键字都不一样别直接套。3.2 多行合并成一行STUFF FOR XML PATH 与 GROUP_CONCAT多行合并是报表开发里的高频需求。SQL Server 7.0 时代的老写法到现在还经常见用FOR XML PATH配合STUFF去掉开头分隔符SELECT STUFF(( SELECT , name FROM sys.all_objects ORDER BY name FOR XML PATH() ), 1, 1, ) AS names;SQL Server 2017 开始有了官方函数STRING_AGG写法简洁很多SELECT STRING_AGG(name, ,) FROM sys.all_objects;MySQL 对应的是GROUP_CONCATSELECT GROUP_CONCAT(name ORDER BY name SEPARATOR ,) FROM sys.all_objects;这里有几个注意点。第一FOR XML PATH会对 XML 特殊字符做转义比如内容里有、会变成lt;、amp;处理用户生成内容时要注意还原。第二MySQL 的GROUP_CONCAT结果长度受group_concat_max_len限制默认是 1024 字节数据多了会静默截断可以在会话级别SET SESSION group_concat_max_len 102400;。第三STRING_AGG和GROUP_CONCAT都支持DISTINCT但语法位置略有不同我已经记错过好几次写的时候留意一下。3.3 事务与隔离级别默认值就能坑人事务的基本写法两边很像SQL Server 用BEGIN TRAN / COMMIT / ROLLBACKMySQL 用START TRANSACTION / COMMIT / ROLLBACK都能实现回滚。但默认隔离级别差异很大SQL Server 默认是READ COMMITTEDMySQL 默认是REPEATABLE READ。这意味着同一个业务代码在 SQL Server 里两次查询可能读到别的事务刚提交的数据在 MySQL 里同一个事务内两次普通查询结果却是一致的快照读。如果迁移时没注意很可能会出现“同一时间点统计结果对不上”的诡异问题。事务里的错误处理差异更大。SQL Server 里习惯用BEGIN TRY / BEGIN CATCH / THROW包住事务出错自动回滚MySQL 里更常见的是先做校验再执行或者在存储过程里用DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK。还有一点千万记住MySQL 的 DDL 语句有隐式提交一旦你执行了CREATE TABLE、ALTER TABLE等语句当前事务就自动提交了无法回滚SQL Server 对 DDL 是支持事务性回滚的。所以那种“先建临时表、再更新正式表、出错全回滚”的脚本在 MySQL 里很可能达不到预期效果。定位事务问题时SQL Server 可以查sys.dm_tran_locks结合sys.dm_exec_requests看阻塞MySQL 8.0 用SHOW ENGINE INNODB STATUS看锁信息或者查performance_schema.data_locks。这两个系统的排查思路完全不同但第一步都是找到那个“卡住”的事务或会话再往前追它持有的锁。3.4 锁与查询提示NOLOCK 别乱用SQL Server 里很多人喜欢在查询里加WITH (NOLOCK)本质是让语句在READ UNCOMMITTED级别下读好处是不容易被写事务阻塞坏处是会读到未提交的脏数据。MySQL 里没有一句完全等价的查询提示LOCK IN SHARE MODE和FOR UPDATE反而都是加锁读跟 NOLOCK 方向完全相反。如果你在 SQL Server 里用惯了 NOLOCK切到 MySQL 后想提高并发读性能正确的做法是修改事务隔离级别SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;然后执行普通 SELECT。但最好想清楚这个操作是否真的需要脏读报表、缓存、统计类场景可以接受涉及资金、库存的就别赌了老老实实让读写排队。显式加锁方面SQL Server 可以用WITH (UPDLOCK, HOLDLOCK)模拟“先查后改”过程中的范围锁MySQL 里对应的是SELECT ... FOR UPDATE。两边都支持NOWAIT但语义不完全一样MySQL 的FOR UPDATE NOWAIT在 8.0 中遇到锁直接报错SQL Server 的NOWAIT是用于指定等待时间。这些微妙的差异多库并行开发时特别容易埋雷。4. 存储过程、函数、系统表和迁移兼容4.1 变量、临时表与批处理写脚本时SQL Server 里随手就是DECLARE i INT 0然后SET i i 1MySQL 日常命令行里用SET i 0直接声明用户变量。但 MySQL 的DECLARE关键字只能出现在存储过程、函数、触发器里不能直接在 SQL 脚本里DECLARE x。这个差异会导致你从 SQL Server 复制一段查询到 MySQL 客户端第一行就报语法错误。临时表也有差异。SQL Server 里会话级临时表用#temp全局临时表用##temp还有表变量table生命周期和作用域分得很细。MySQL 的临时表就是CREATE TEMPORARY TABLE只对当前会话可见断开连接自动删除。在连接池环境下要注意MySQL 的临时表会跟着同一个物理连接走如果连接复用临时表可能残留到下一次请求里这是很隐蔽的问题。SQL Server 的#temp虽然也会跟着会话但连接池封装通常更严格一般不会出现交叉污染。批处理GO这个点值得再说一下。SQL Server 里变量作用域跟在批处理边界有关DECLARE在第一个GO之前定义GO之后就不能用了MySQL 没有批处理边界变量从定义到会话结束都有效。所以迁移长脚本时如果原脚本里有很多GO不能简单删掉完事要检查变量作用域和事务边界是否被改变。4.2 存储过程写法差异存储过程是最能体现“两边都在写 SQL但完全不是一个物种”的地方。SQL Server 创建存储过程CREATE PROCEDURE usp_GetUser id INT 0 AS BEGIN SELECT * FROM dbo.users WHERE id id; END; GO调用时可以用命名参数EXEC usp_GetUser id 1;MySQL 创建同样功能DELIMITER $$ CREATE PROCEDURE GetUser(IN p_id INT) BEGIN SELECT * FROM users WHERE id p_id; END$$ DELIMITER ;调用必须用CALLCALL GetUser(1);除了语法参数体系差异很大。SQL Server 的存储过程参数默认是输入参数要输出得加OUTPUTMySQL 的存储过程参数有IN、OUT、INOUT三种模式OUT参数必须在调用时传入用户变量比如先CALL sp_xxx(result)再SELECT result取值。另外SQL Server 的存储过程里可以直接执行动态 SQLEXEC(sql)MySQL 需要三段式SET sql SELECT * FROM users WHERE id ?; PREPARE stmt FROM sql; EXECUTE stmt USING id; DEALLOCATE PREPARE stmt;动态 SQL 一定要做参数化处理无论哪个库直接字符串拼接都容易出 SQL 注入这个没得商量。4.3 系统表与元数据查询sys.objects vs information_schema查表结构、查索引、查当前连接两边差别也明显。SQL Server 有sys.tables、sys.columns、sys.indexes这一套系统视图DBA 都喜欢直接查sys.MySQL 更多用information_schema或者SHOW命令比如SHOW TABLES和SHOW CREATE TABLE。information_schema两边都有所以做通用工具时我优先用它。下面的对照表可以收藏需求SQL ServerMySQL查看所有表SELECT name FROM sys.tables;SHOW TABLES;查看表字段SELECT * FROM sys.columns WHERE object_id OBJECT_ID(t);SHOW COLUMNS FROM t;查看建表语句sp_help t;SHOW CREATE TABLE t;查看当前版本SELECT VERSION;SELECT VERSION();查看当前连接 IDSELECT SPID;SELECT CONNECTION_ID();备份和恢复也是迁移必做的。SQL Server 的常规操作是BACKUP DATABASE生成.bak文件再用RESTORE DATABASE恢复MySQL 则是用mysqldump导出 SQL 文件再用mysql dump.sql导入。要注意.bak文件基本不能跨版本降级恢复而 MySQL 的 SQL 导出文件只要注意字符集和存储引擎兼容性相对好一点。碰到大数据量迁移SQL Server 可以拆分多个备份文件MySQL 则适合用--single-transaction保证一致性备份细节不少。5. 常见问题速查表与迁移踩坑记录5.1 语法差异速查表把上面所有内容浓缩成一张表遇到拿不准的先查这张表再动手功能SQL ServerMySQL取前 N 行SELECT TOP (10) ...SELECT ... LIMIT 10分页OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLYLIMIT 20, 10字符串拼接a b注意 NULLCONCAT(a, b)字符串转数字CAST(123 AS INT)CAST(123 AS SIGNED)空值替代ISNULL(a, b)/COALESCE(a,b)IFNULL(a, b)/COALESCE(a,b)多行合并STRING_AGG(name, ,)老版本用STUFF...FOR XML PATHGROUP_CONCAT(name)当前时间GETDATE()NOW()自增列IDENTITY(1,1)AUTO_INCREMENT获取自增 IDSCOPE_IDENTITY()LAST_INSERT_ID()条件表达式CASE WHEN、IIF()CASE WHEN、IF()行号排名ROW_NUMBER() OVER(...)8.0 支持5.7 用变量模拟临时表#temp/tableCREATE TEMPORARY TABLE默认隔离级别READ COMMITTEDREPEATABLE READ存储过程调用EXEC proc p 1CALL proc(1)修改列类型ALTER TABLE t ALTER COLUMN c INTALTER TABLE t MODIFY COLUMN c INT元数据查看sys.tables/sys.columnsSHOW TABLES/SHOW COLUMNS日期格式化FORMAT(d, yyyy-MM-dd)DATE_FORMAT(d, %Y-%m-%d)多表 UPDATEUPDATE a SET ... FROM a JOIN b ON ...UPDATE a JOIN b ON ... SET ...多表 DELETEDELETE a FROM a JOIN b ON ...DELETE a FROM a JOIN b ON ...也可用这张表并不完整但它覆盖了日常 CRUD、报表、事务开发里 80% 以上的高频需求。我自己的代码片段库里常年放着这份对照每次换库先过一眼能省很多试错时间。5.2 迁移时最容易被忽略的 5 个坑第一字符集和排序规则。MySQL 5.7 默认utf8其实不是真正的 UTF-8很多 emoji 和生僻字存不进去8.0 默认utf8mb4好一些。SQL Server 默认排序规则类似SQL_Latin1_General_CP1_CI_AS大小写不敏感但同样的数据迁到 MySQL 的utf8mb4_bin或utf8mb4_unicode_ci后排序规则变了查询结果顺序和大小写匹配行为都可能变。第二零日期。MySQL 在严格模式下0000-00-00这种日期会直接报错而 SQL Server 的DATETIME默认就是1900-01-01几乎没有零值概念。迁移旧数据时先跑一遍SELECT ... WHERE date_col 0000-00-00把脏数据清洗干净再导。第三布尔类型。SQL Server 没有真正的 BOOLEAN用BIT表示 0/1MySQL 里有TINYINT(1)和BOOL关键词。虽然应用层拿到的结果都是 0/1但如果你在 SQL 里写WHERE is_active TRUE两边解析方式会有差异SQL Server 会把它当等于 1MySQL 则看具体版本和类型容易出幺蛾子。第四导入导出工具。SQL Server 的bcp和BACPAC适合大批量迁移MySQL 的mysqldump更适合中小规模数据。我踩过的坑是直接用 mysqldump 导出的 SQL 里有UNLOCK TABLES和LOCK TABLES在只读账号上跑会失败需要用--skip-lock-tables。第五自增 ID 重置。把旧库数据导到新库后如果没注意自增列当前值插入新数据时可能主键冲突。SQL Server 用DBCC CHECKIDENTMySQL 用ALTER TABLE ... AUTO_INCREMENT两条命令都必须在数据导入完成后手动执行别指望系统自动帮你续上。最后分享一点我的实际操作习惯这些年在两个库之间来回切换我最开始也试图写一套“通用 SQL”来兼容两边后来发现完全走不通。现在我的做法是先明确当前在哪个数据库再用对应的方言写写完对照速查表过一遍。每次从 SQL Server 切换到 MySQL我最先提醒自己的三件事是分页要改成 LIMIT、字符串拼接要改成 CONCAT、事务隔离级别默认值不一样从 MySQL 切回 SQL Server我就反过来检查 OFFSET FETCH、ISNULL、IDENTITY 这些点。这套方法不敢说能覆盖所有差异但应对我日常 80% 的项目需求已经够了。建议你也找一个固定的切换顺序每次都按这个顺序逐项检查踩坑次数会明显降下来。
返回列表