
说实话这个标题一看就知道是干嘛的——“sqlserver和mysql用法上的区别~自用”。我当初在项目里同时维护两套系统一套是SQL Server 2016另一套是MySQL 8.0每天光记语法差异就够喝一壶的了。时间久了踩过的坑、查过的文档、改过的SQL都堆在笔记里这次干脆整理成一份可以直接参考的对比笔记既能当自用速查也方便遇到类似情况的朋友直接“抄作业”。这篇文章不打算从零教安装也不会把官方文档搬一遍重点放在“同样一个需求在两个数据库里到底怎么写、为什么这么写、坑在哪”。不管你是在写迁移脚本、维护老系统还是刚入职的公司恰好两套库都在用这个笔记应该都能帮上忙。1. 先聊最直接的差异安装部署与日常运维很多人以为SQL Server只能在Windows上用MySQL只能在Linux上用这个印象其实过时了。SQL Server从2017开始支持Linux容器部署MySQL在Windows上的安装包也越来越完善微软官方和MySQL官方都在互相“跨界”。但跨过来之后细节上的坑才是真正让人头疼的地方。1.1 Windows/Linux 安装姿势对比MySQL的安装主要分两种方式在Windows下用官方安装包MSI或ZIP在Linux下一般用rpm或tar.gz。这里有个非常明显的时间节点MySQL 5.7和8.0的安装过程差别很大5.7.44是老版本的最终维护版本安装过程相对简单选了Server only基本一路下一步8.0则引入了caching_sha2_password默认认证插件稍老一些的客户端工具连上去就会报“SSL连接错误”。这个我后面单独说。SQL Server安装时最容易栽跟头的是“无法找到数据库引擎启动句柄”这个报错在SQL Server 2016、2017、2019上都可能出现。我第一次遇到时以为是安装包坏了重装了三遍才反应过来问题大多出在服务账户权限或实例残留上。解决顺序建议这样试打开SQL Server配置管理器查看“SQL Server服务”里对应实例是否处于“运行”状态如果是“已停止”先手动启动看具体报错。如果启动时报句柄无效检查SQL Server服务账户是否有安装目录的完全控制权限尤其是C:\Program Files\Microsoft SQL Server\目录。在注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\实例ID下检查是否存在残留实例项有就清掉再装。使用sqlservr.exe -f冷启动方式尝试绕过部分配置问题这个办法我在很多帖子里看到过实测对部分句柄问题有效。提示Windows下同时装了多个版本实例时安装前用netstat -ano | findstr 1433检查端口是否被占用避免SQL Server默认实例端口冲突。1.2 图形化工具选择与效率问题MySQL官方有WorkbenchSQL Server官方有SSMS这是最正统的组合。但如果你和我一样电脑上两个库都要连我强烈建议用DBeaver或Navicat统一管理。Navicat对SQL Server和MySQL的语法提示都做得不错DBeaver则是免费开源跨平台还能直接预览ER图。不过要注意一点Navicat老版本连接MySQL 8.0时默认认证插件不兼容会出现“Authentication plugin caching_sha2_password cannot be loaded”或者“SSL连接错误”。解决方法是把用户的认证插件改成mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;这在开发环境用问题不大生产环境还是建议升级客户端直接支持caching_sha2_password更安全。这个坑属于“不是你的SQL有问题是认证方式变了”的典型case。1.3 大小写敏感这是新手第一个认知冲击MySQL在Linux下默认大小写敏感在Windows下默认不敏感这个差异让很多人吃过亏。表名User和user在Windows上能共存在Linux上会直接当成两张表。SQL Server的默认排序规则一般是SQL_Latin1_General_CP1_CI_ASCI就是Case Insensitive即大小写不敏感但如果你建库时用了CS结尾的排序规则大小写就又敏感了。判断MySQL当前是否区分大小写执行SHOW VARIABLES LIKE lower_case_table_names;值为0区分大小写值为1不区分大小写Windows默认值为2存储时保留原始大小写但比较时不区分这个主要在macOS和部分Linux上使用SQL Server则可以通过查询数据库排序规则来确认SELECT DATABASEPROPERTYEX(数据库名, Collation);自用笔记里我最常写的一句话是跨库迁移时所有表名和字段名一律小写字符串比较用统一的collation能省掉90%的隐性bug。2. SQL语法差异用法对比才是重头戏说到SQL本身两个数据库的“方言”差异非常大。我平时维护的报表系统经常把同一套查询逻辑在两套库上各写一遍语法上几乎没有能直接复制粘贴的。下面按几个高频场景逐一对比。2.1 数据类型映射别在字符串转数字上吃亏热搜里一直有人搜“sqlserver 字符串转数字”说明这个是高频需求。SQL Server里最常用的是CAST和CONVERT-- SQL Server SELECT CAST(123 AS INT); SELECT CONVERT(INT, 123);MySQL里也能用CAST但多了一个可以直接指定字符集和类型转换的写法-- MySQL SELECT CAST(123 AS SIGNED INTEGER); SELECT CONVERT(123, SIGNED INTEGER);这里有个最容易忽略的差异SQL Server的CONVERT是原生内置函数第一个参数是目标类型第二个是被转换值MySQL的CONVERT更接近一个“表达式转换”第一个参数是表达式第二个参数才是目标类型。两边的参数顺序正好是反的一旦记混迁移代码时必然报错。两个数据库共同的语言是转换非数字字符串时两边都会报错如果想要“失败就返回默认值”的容错效果SQL Server可以用TRY_PARSE或TRY_CONVERTMySQL可以用REGEXP先筛或者CAST配合异常处理。2.2 字符串拼接的“空值陷阱”SQL Server的字符串拼接用号MySQL默认也支持号但在MySQL里是数学运算符用于字符串时会进行隐式转换不是纯拼接。判断两个库最标准的方法是看CONCAT函数。SQL Server从2012版本开始引入了CONCAT它的特点是可以自动忽略NULL参数-- SQL Server SELECT CONCAT(a, NULL, b); -- 结果 ab SELECT a NULL b; -- 结果 NULLMySQL的CONCAT遇到NULL会整个返回NULL-- MySQL SELECT CONCAT(a, NULL, b); -- 结果 NULL所以如果某个字段可能为NULLMySQL里必须用IFNULL或COALESCE包一层SQL Server用拼接时也得包ISNULL否则整条结果就是NULL。千万不要在两个库里用同一条拼接语句直接跑。2.3 分页查询LIMIT 与 OFFSET FETCH 的大坑MySQL的分页写法大家都熟悉SELECT * FROM orders ORDER BY id DESC LIMIT 10 OFFSET 20;SQL Server没有LIMIT但可以参考两种写法老版本的ROW_NUMBER()窗口函数以及2012版本的OFFSET FETCH-- SQL Server 2012 SELECT * FROM orders ORDER BY id DESC OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;这里有个热搜问题“sqlserver offset 后再 top 20 查到的是什么”。我特意验证过OFFSET和TOP同时出现在一条语句里时语义非常容易误解。你可能会写出SELECT TOP 20 * FROM table ORDER BY id DESC OFFSET 10 ROWS但这个写法在SQL Server里会报语法错误。正确理解是TOP在最外层限制返回行数OFFSET...FETCH只负责跳过和取出区间两者不是“分页工具”和“限制工具”的组合关系。如果一定要实现“跳过前10行再取前20行”应该写成SELECT TOP 20 * FROM ( SELECT *, ROW_NUMBER() OVER (ORDER BY id DESC) AS rn FROM orders ) t WHERE rn 10;或者干脆统一用OFFSET 10 ROWS FETCH NEXT 20 ROWS ONLY。自用经验是如果不是在维护SQL Server 2008等老环境全部改成OFFSET FETCH可读性好执行计划也比ROW_NUMBER()更稳定。2.4 CASE WHEN两边都能用但注意类型CASE WHEN是标准SQL语法两个数据库都支持很完整基本可以直接复用。真正的坑出现在THEN分支返回值的类型一致性上。SQL Server要求所有THEN分支的数据类型必须能隐式转换到同一个类型比如一个分支返回INT另一个分支返回VARCHARSQL Server会尝试把INT转成VARCHAR而不是报错但可能会导致隐式转换和性能问题。MySQL则灵活很多混用数字和字符串时自动转为字符串。另一个差异是CASE的简写形式MySQL支持CASE配合WHEN 条件但SQL Server也支持这在标准里其实都有。真正需要注意的是MySQL里有个IF(expr, a, b)函数SQL Server没有SQL Server里有IIF(expr, a, b)从2012版本开始提供MySQL则没有。两个函数功能类似但名字不同迁移代码时容易踩“函数不存在”的错。-- SQL Server SELECT IIF(score 60, 及格, 不及格); -- MySQL SELECT IF(score 60, 及格, 不及格);2.5 多行合并成一行GROUP_CONCAT 与 STRING_AGG“sqlserver多行合并成一行”也是高频搜索词。MySQL里大家习惯用GROUP_CONCATSELECT dept_id, GROUP_CONCAT(user_name) FROM employee GROUP BY dept_id;SQL Server的传统写法是用FOR XML PATH()做拼接这是让我每次写都觉得五味杂陈的语法SELECT dept_id, STUFF(( SELECT , user_name FROM employee e2 WHERE e2.dept_id e1.dept_id FOR XML PATH() ), 1, 1, ) FROM employee e1 GROUP BY dept_id;SQL Server 2017以后新加了STRING_AGG这让问题简单多了SELECT dept_id, STRING_AGG(user_name, ,) FROM employee GROUP BY dept_id;注意两个函数在处理NULL上的差异MySQL的GROUP_CONCAT默认排除NULL值SQL Server的STRING_AGG默认也会忽略NULL但FOR XML PATH方式不会自动忽略需要额外加上WHERE user_name IS NOT NULL或者在子查询里过滤。2.6 排序规则和中文排序“mysql排序”这个热搜词下面最常见的问题就是中文排序乱序。MySQL默认的utf8mb4排序规则是utf8mb4_0900_ai_ci8.0版本或utf8mb4_general_ci5.7版本中文排序默认按Unicode编码不是按拼音。如果想按拼音排序需要指定ORDER BY CONVERT(name USING gbk)但这招在8.0以上还能用只是不太推荐。SQL Server的中文排序取决于排序规则中的LCID比如Chinese_PRC_CI_AS会按拼音排序Japanese_CI_AS则可能按日文假名优先规则。因此同一个排序需求两个数据库的写法可能完全不一样-- MySQL 按拼音排 SELECT name FROM user ORDER BY CONVERT(name USING gbk); -- SQL Server 按拼音排取决于排序规则 SELECT name FROM user ORDER BY name COLLATE Chinese_PRC_CI_AS;这块我的建议是涉及中文排序需求时不要在SQL里硬处理前端排序或预先存拼音首字母字段更可控。数据库的collation是全局属性改一个字段的排序规则容易引起索引失效得不偿失。3. 事务、锁与并发控制谁更像“正经数据库”很多从MySQL转过来的开发第一次看SQL Server的事务日志和锁等待时会觉得“这系统怎么这么重”。反过来从SQL Server看MySQL时又会觉得“InnoDB的锁机制好抽象”。这两个数据库的事务模型差别其实远比SQL语法大。3.1 事务隔离级别默认值不同行为就不同MySQL InnoDB的默认隔离级别是REPEATABLE READ可重复读SQL Server的默认隔离级别是READ COMMITTED读已提交。这是两者最大的“隐藏差异”。REPEATABLE READ意味着在同一个事务里多次执行同样的SELECT结果是快照级别的稳定。这个特性在报表系统里非常好用能让一个事务内的多次统计保持一致。但代价是更容易产生间隙锁Gap Lock在高并发插入场景下死锁概率会更高。READ COMMITTED则是每次读都看到最新已提交数据一致性不如可重复读但并发能力和锁开销相对更平衡。如果你在MySQL里遇到奇怪的死锁但SQL逻辑看起来没问题可以先看看是不是隔离级别设置的锅-- 查看MySQL当前隔离级别 SELECT transaction_isolation;SQL Server查询事务隔离级别DBCC USEROPTIONS;常见的修改方法两边都支持-- SQL Server SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- MySQL SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;我个人在报表库上会主动降级隔离级别用“允许脏读”换查询性能这对统计类需求影响不大但对财务类事务是绝对不行的这一点自己心里要有数。3.2 锁粒度与死锁排查思路MySQL InnoDB支持行级锁同时还有Record Lock、Gap Lock、Next-Key Lock几种模式。SQL Server也有行级锁、页级锁、表级锁但它还有独特的“锁升级”机制——当单个事务获取的锁数量超过阈值时会把行锁自动升级为表锁。这个机制有时候会让人很意外本来以为并发很高结果某条语句瞬间变成全表锁性能急转直下。排查死锁时两个库的思路也不一样MySQLSHOW ENGINE INNODB STATUS可以看到最近一次死锁的详细信息和涉及的SQL语句配合performance_schema里的data_locks和data_lock_waits表能定位锁等待链条。SQL Server更常用的是系统视图sys.dm_exec_requests查看阻塞信息sys.dm_tran_locks查看锁资源靠wait_resource字段定位具体等待的资源。-- SQL Server 查看当前阻塞 SELECT session_id, blocking_session_id, wait_type, wait_resource FROM sys.dm_exec_requests WHERE blocking_session_id 0;自用笔记里我记了一条遇到死锁第一时间看“是否所有事务都先按同一个顺序访问表”。大多数死锁都是业务代码里事务A先查订单表再查用户表事务B先查用户表再查订单表互相等锁导致的。统一加锁顺序比调数据库参数有效得多。3.3 备份恢复完全两个世界SQL Server的备份恢复基本靠管理工具BACKUP DATABASE和RESTORE DATABASE是所有数据库管理员都会用到的标准命令。MySQL则流行mysqldump逻辑备份和xtrabackup/clone plugin物理备份恢复方式的差异非常大。SQL Server的完整备份语句BACKUP DATABASE [数据库名] TO DISK ND:\backup\db.bak WITH INIT, COMPRESSION;MySQL的典型备份命令mysqldump -uroot -p --single-transaction --routines --triggers --databases 库名 backup.sql注意--single-transaction是做InnoDB在线备份的关键不加的话会锁住所有表业务直接阻塞。这个参数在SQL Server的备份里没有直接对应物因为SQL Server在线备份是默认行为。恢复方面SQL Server恢复时经常要处理“恢复模式”问题如果数据库是FULL恢复模式但日志链断裂恢复不到指定时间点会很痛苦。MySQL的逻辑备份恢复就简单多了直接把SQL文件灌进去就行但遇到超大库逻辑备份的恢复速度会让人崩溃这时候必须上物理备份工具。4. 常用管理操作和查询技巧的细节差异除了SQL语法和事务模型两个数据库的日常管理和辅助查询也各有各的脾气。这个部分我集中写一些“两侧实在不一样”的操作细节都是自用频率极高的。4.1 查看执行计划工作量大比拼MySQLEXPLAIN SELECT ...输出一个结果表展示访问类型、使用索引情况、预估行数配合EXPLAIN ANALYZE还能看到实际执行耗时。SQL Server图形化执行计划快捷键是CtrlM控制台命令是SET STATISTICS PROFILE ON或SET SHOWPLAN_XML ON。两边都支持“估计执行计划”和“实际执行计划”两种模式但MySQL的EXPLAIN默认是估计计划必须要EXPLAIN ANALYZE才是真实执行计划SQL Server则默认就是真实执行计划除非提前设置SHOWPLAN。自用经验是MySQL先看type列从const到ref到range到ALL覆盖索引和回表的区别全在这个字段上SQL Server则更依赖图形化计划里的“扫描”和“查找”图标表扫描的估算成本远高于索引查找。两边的优化思路都离不开“让WHERE条件能用上索引”这一点倒是完全一致。4.2 自增主键IDENTITY 与 AUTO_INCREMENTSQL Server的自增字段靠IDENTITY(起始值, 步长)定义插入数据时不能直接给自增列指定值除非先SET IDENTITY_INSERT 表名 ON。MySQL的AUTO_INCREMENT则宽松很多可以手动指定值甚至指定一个比当前更大或更小的值数据库会自动调整自增指针。获取刚插入记录的自增ID这个点非常关键-- SQL Server 推荐用 SCOPE_IDENTITY() SELECT SCOPE_IDENTITY(); -- MySQL SELECT LAST_INSERT_ID();不推荐SQL Server里用IDENTITY因为触发器插入其他表时IDENTITY会被触发器里的自增值覆盖而SCOPE_IDENTITY()只返回当前会话、当前作用域的自增值。我踩过一次这个坑某个数据同步脚本因为触发器插入了日志表结果拿到的ID是日志表的导致业务数据错乱排查了一整天才找到根因。4.3 存储过程和批处理GO 与 DELIMITERSQL Server用GO作为批处理分隔符MySQL默认用分号但创建存储过程时要用DELIMITER临时改分隔符否则CREATE PROCEDURE内部的;会被客户端提前识别成语句结束导致整个存储过程解析失败。-- MySQL 创建存储过程 DELIMITER $$ CREATE PROCEDURE test_proc() BEGIN SELECT 1; END$$ DELIMITER ;SQL Server创建存储过程则简单到令人怀疑CREATE PROCEDURE test_proc AS BEGIN SELECT 1; END GO在自动化脚本或迁移工具里这个DELIMITER的处理经常是一个大坑。最稳妥的办法是先查一遍存储过程里的所有分号再用工具自动转换不要手动改。4.4 临时表与表变量SQL Server里临时表和表变量是两个不同的东西。临时表#temp会真实创建在tempdb中支持索引和统计信息表变量table则更像一个内存型的变量集合没有统计信息数据量大时性能下降严重。MySQL里没有表变量只有临时表CREATE TEMPORARY TABLE会话结束自动删除。日常使用中SQL Server开发者很容易滥用表变量只要数据量超过1万行表变量的性能就开始明显差于临时表。MySQL的临时表则要注意在同一个会话里临时表名不能和普通表重名否则会覆盖普通表。还有一点MySQL的临时表在连接断开后自动删除SQL Server的临时表在会话结束或显式DROP后删除生命周期逻辑差不多但清理时机的微妙差异在长连接池里会被放大。5. 移植、同步与日常问题排查实录最后这部分我把自己在真实项目里遇到的“典型事故”整理成对比清单每一条都是花了时间才弄明白的直接照着排查就行。5.1 常见“SQL迁移报错”对照速查表问题SQL Server表现MySQL表现处理思路字符串拼接空值aNULL结果为NULLCONCAT(a,NULL)结果为NULL两边都要用ISNULL/IFNULL预处理分页查询无LIMIT用OFFSET FETCH支持LIMIT x OFFSET y迁移工具自动改写SQL自增ID获取SCOPE_IDENTITY()LAST_INSERT_ID()名字不同语义都限定在当前会话布尔类型BIT类型0/1TINYINT(1)惯用无原生布尔MySQL的BOOLEAN只是TINYINT(1)别名字符串转数字CAST(123 AS INT)CAST(123 AS SIGNED)空字符串在两个库都报错先做过滤多行合并STRING_AGG或FOR XML PATHGROUP_CONCAT注意分隔符转义和长度限制查看版本SELECT VERSIONSELECT VERSION()命名不同语义类似这个表相当于我的“两库方言对照字典”遇到报错先来查表八九不离十能定位到是语法差异还是语义差异。5.2 安装失败与启动故障排查清单SQL Server侧最常见的启动失败原因数据库引擎服务没有启动配置管理器里检查服务状态。端口1433被占用用netstat -ano排查。无法找到数据库引擎启动句柄尝试修复安装或重新配置服务账户。安装日志路径C:\Program Files\Microsoft SQL Server\160\Setup Bootstrap\Log里能看到详细错误。MySQL侧常见的安装失败原因Windows安装时选了“Developer Default”但系统缺少Visual C运行库装到一半报错。Linux下rpm安装后初始密码不知道在哪查看/var/log/mysqld.log里的temporary password。Docker安装失败最常见原因是目录映射权限不足给容器加-v /my/custom/path:/var/lib/mysql时保证宿主机目录属主和MySQL的uid一致。AMI/云镜像里预装的MySQL已经占用3306端口卸载不干净导致重装失败。遇到“无法找到数据库引擎启动句柄”时我建议按这个顺序排查服务状态 - 服务账户权限 - 端口占用 - 冷启动测试 - 重装。别一上来就卸了重装大概率白忙一场。5.3 数据同步与迁移的自用指南这两年项目里比较火的做法是用Flink做MySQL同步到ClickHouse的数据管道。这里有个核心前提MySQL要开启binlog并且使用ROW格式否则Flink CDC拿不到变更数据。很多人在这一步卡住检查了半天SQL最后发现是MySQL的binlog_row_image设置成了MINIMAL导致同步工具拿不到完整的行数据。如果你只是做SQL Server到MySQL的日常数据迁移我建议分几步走先用工具生成两个库的表结构差异报告Ingy或Navicat都有这功能。手动核对每一张表的数据类型把SQL Server的NVARCHAR对应成MySQL的VARCHAR和合适的字符集把DATETIME2对应成DATETIME或TIMESTAMP。批量转换SQL脚本时把ISNULL、GETDATE()、TOP、[方括号]这些语法逐项替换成MySQL版本。小表用mysqldump大表用分批导出SELECT ... WHERE id BETWEEN ...避免一次性导出超大结果集。迁移后做数据对比可以两边各算COUNT(*)和关键表的CHECKSUM。同步过程中最常见的类型问题是SQL Server的NVARCHAR(MAX)和MySQL的LONGTEXT映射很多工具默认会生成TEXT导致超长内容被截断。宁可多留空间也不要让数据落地时悄悄丢失。5.4 几个虽然小但致命的区别还有一些零碎细节看起来不起眼出问题时定位特别费劲字符串引号两个数据库都认单引号但SQL Server默认不认双引号作为标识符除非开启QUOTED_IDENTIFIERMySQL则可以配置ANSI_QUOTES让双引号表示列名。跨库迁移脚本时我一般规定全部用方括号给SQL Server列名加引号MySQL里全部用反引号避免歧义。关键字冲突rank、level、system在两边都是保留字但保留字集合不完全一样。写脚本前先用SELECT * FROM 表名 WHERE 列名 1做最小测试。空字符串与NULLMySQL把和NULL严格区分SQL Server也一样但排序规则或字符集不同会导致WHERE 字段 在某些情况下查不出来最好统一用IFNULL/ISNULL转换成同一语义。日期函数SQL Server的GETDATE()在MySQL里不存在最接近的是NOW()。日期格式化方面SQL Server用FORMAT(date, yyyy-MM-dd)MySQL用DATE_FORMAT(date, %Y-%m-%d)占位符风格完全不同。最后再分享一个小习惯我自己在实际操作中养成了一个非常笨但有效的习惯每当我需要在两个数据库上写同一套业务逻辑时先在Notepad里建一个对照表左边写SQL Server版本右边写MySQL版本两边逐行对比。看起来费时间但比起线上报错再回来改效率高十倍。比如过滤空值的通用写法我会写成-- SQL Server WHERE ISNULL(字段, ) -- MySQL WHERE IFNULL(字段, ) 这个对照小卡片已经积累了好几千行基本覆盖了我日常用的90%语法差异。你要是也经常两头跑不妨从这篇文章里的几个高频差异开始建自己的对照表。遇到新坑先写进表里下次就少踩一次。