干了这么多年数据库开发,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 加上allowPublicKeyRetrieval=true&useSSL=false,二是在 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\\SQLEXPRESS;MySQL 默认端口是 3306,没有实例名的概念,只有host和port。JDBC 连接串对比如下:
| 数据库 | JDBC 连接串示例 |
|---|---|
| SQL Server | jdbc:sqlserver://localhost:1433;databaseName=testdb;user=sa;password=xxx |
| MySQL | jdbc:mysql://localhost:3306/testdb?useSSL=false&serverTimezone=Asia/Shanghai |
.NET 环境里,SQL Server 用SqlConnection,连接串通常是Server=localhost;Database=testdb;User Id=sa;Password=xxx;,MySQL 用MySqlConnection,连接串是Server=localhost;Database=testdb;Uid=root;Pwd=xxx;。上面这些差异虽然小,但项目里一配错就是半小时起步的排查时间,我建议直接存到一个常用配置清单里,别靠脑子记。
还有一个很容易被忽略的点:MySQL 的连接用户名是带 host 的,'root'@'localhost'和'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')。更麻烦的是拼接时遇到 NULL:SQL Server 里'a' + NULL结果是NULL,MySQL 的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_CAST(SQL 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 的DATETIME;SQL Server 的UNIQUEIDENTIFIER对应 MySQL 的CHAR(36)或UUID()。特别注意TIMESTAMP:SQL 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 INT,MySQL 用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_CONCAT:
SELECT GROUP_CONCAT(name ORDER BY name SEPARATOR ',') FROM sys.all_objects;这里有几个注意点。第一,FOR XML PATH会对 XML 特殊字符做转义,比如内容里有<、&会变成<、&,处理用户生成内容时要注意还原。第二,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 / ROLLBACK,MySQL 用START TRANSACTION / COMMIT / ROLLBACK,都能实现回滚。但默认隔离级别差异很大:SQL Server 默认是READ COMMITTED,MySQL 默认是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 + 1,MySQL 日常命令行里用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 ;调用必须用CALL:
CALL GetUser(1);除了语法,参数体系差异很大。SQL Server 的存储过程参数默认是输入参数,要输出得加OUTPUT;MySQL 的存储过程参数有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 Server | MySQL |
|---|---|---|
| 查看所有表 | 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(); |
| 查看当前连接 ID | SELECT @@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 Server | MySQL |
|---|---|---|
| 取前 N 行 | SELECT TOP (10) ... | SELECT ... LIMIT 10 |
| 分页 | OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY | LIMIT 20, 10 |
| 字符串拼接 | a + b,注意 NULL | CONCAT(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 PATH | GROUP_CONCAT(name) |
| 当前时间 | GETDATE() | NOW() |
| 自增列 | IDENTITY(1,1) | AUTO_INCREMENT |
| 获取自增 ID | SCOPE_IDENTITY() | LAST_INSERT_ID() |
| 条件表达式 | CASE WHEN、IIF() | CASE WHEN、IF() |
| 行号排名 | ROW_NUMBER() OVER(...) | 8.0 支持,5.7 用变量模拟 |
| 临时表 | #temp/@table | CREATE TEMPORARY TABLE |
| 默认隔离级别 | READ COMMITTED | REPEATABLE READ |
| 存储过程调用 | EXEC proc @p = 1 | CALL proc(1) |
| 修改列类型 | ALTER TABLE t ALTER COLUMN c INT | ALTER TABLE t MODIFY COLUMN c INT |
| 元数据查看 | sys.tables/sys.columns | SHOW TABLES/SHOW COLUMNS |
| 日期格式化 | FORMAT(d, 'yyyy-MM-dd') | DATE_FORMAT(d, '%Y-%m-%d') |
| 多表 UPDATE | UPDATE a SET ... FROM a JOIN b ON ... | UPDATE a JOIN b ON ... SET ... |
| 多表 DELETE | DELETE 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/1;MySQL 里有TINYINT(1)和BOOL关键词。虽然应用层拿到的结果都是 0/1,但如果你在 SQL 里写WHERE is_active = TRUE,两边解析方式会有差异,SQL Server 会把它当等于 1,MySQL 则看具体版本和类型,容易出幺蛾子。
第四,导入导出工具。SQL Server 的bcp和BACPAC适合大批量迁移,MySQL 的mysqldump更适合中小规模数据。我踩过的坑是直接用 mysqldump 导出的 SQL 里有UNLOCK TABLES和LOCK TABLES,在只读账号上跑会失败,需要用--skip-lock-tables。
第五,自增 ID 重置。把旧库数据导到新库后,如果没注意自增列当前值,插入新数据时可能主键冲突。SQL Server 用DBCC CHECKIDENT,MySQL 用ALTER TABLE ... AUTO_INCREMENT,两条命令都必须在数据导入完成后手动执行,别指望系统自动帮你续上。
最后分享一点我的实际操作习惯
这些年在两个库之间来回切换,我最开始也试图写一套“通用 SQL”来兼容两边,后来发现完全走不通。现在我的做法是:先明确当前在哪个数据库,再用对应的方言写,写完对照速查表过一遍。每次从 SQL Server 切换到 MySQL,我最先提醒自己的三件事是分页要改成 LIMIT、字符串拼接要改成 CONCAT、事务隔离级别默认值不一样;从 MySQL 切回 SQL Server,我就反过来检查 OFFSET FETCH、ISNULL、IDENTITY 这些点。这套方法不敢说能覆盖所有差异,但应对我日常 80% 的项目需求已经够了。建议你也找一个固定的切换顺序,每次都按这个顺序逐项检查,踩坑次数会明显降下来。