ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SQL Server与MySQL语法差异全解析:分页、锁、迁移避坑

SQL Server与MySQL语法差异全解析:分页、锁、迁移避坑 如果有人问SQL Server和MySQL不都是关系型数据库吗SQL是不是都差不多我一般会讲一个自己翻车的例子。某个项目里我在SQL Server上写了一条SELECT TOP 100 ... OFFSET 50 ROWS到了MySQL环境里想当然改成LIMIT 50, 100结果两条语句拿到的数据完全是两个意思——前者是“取前100条里跳过前50条”后者是“跳过50条后取100条”。这种“看着差不多、用起来正好相反”的细节就是两套数据库最磨人的地方。我这份笔记不敢说多系统主要是自己在同时维护两套库时的踩坑汇总。如果你也在做数据迁移、双库兼容开发或者刚从其中一个转到另一个那这篇文章能帮你少走不少弯路。1. 为什么“会一种数据库”不等于“会另一种”两套方言的坐标系1.1 SQL标准之下各自长出了一套方言先建立一个大前提SQL Server说的是T-SQLMySQL虽然也是SQL但它在标准SQL基础上做了大量自己的扩展。两边都支持SELECT、INSERT、UPDATE、DELETE、JOIN这些主干语法可一旦你往下沉到具体函数、分页写法、事务行为、系统命令就会发现两边已经不是“换个数据库连接串”就能直接跑的程度了。我把MySQL对SQL的扩展理解为“怎么方便怎么来”比如LIMIT分页、REPLACE INTO、ON DUPLICATE KEY UPDATE都很务实。而SQL Server更像一个“规矩很多的学院派”比如它要求你写SELECT TOP、用[方括号]转义存储过程的返回值和输出参数区分得很细。两种设计哲学没有优劣但你要是在一个项目里来回切就得时刻提醒自己现在到底是在给哪家写代码。1.2 这份“自用笔记”的三种使用姿势我整理这份笔记时是按三类需求来分维度的你也好对照自己的场景去用日常开发排查写某个查询、报表、存储过程时快速确认另一个库的等价写法。数据迁移从MySQL迁到SQL Server或者反着来最怕函数和语法在边界处悄悄变形。面试和方案选型很多人被问到“两个数据库的区别”时只能说出“MySQL免费、SQL Server收费”这种空话。能说出分页、锁、隔离级别、字符串处理的具体差异才是真正证明你摸过这两套库。下面开始进入干货。2. 建表和连接的细节从端口、引号到自增主键的第一道分岔2.1 端口、实例名和连接字符串先把这个对上SQL Server默认端口是1433MySQL默认端口是3306。这是基础中的基础但迁移时最容易栽跟头的是连接串结构不同。SQL Server的连接串通常长这样Server192.168.1.10,1433;Databasetestdb;User Idsa;Passwordxxx;注意中间是英文逗号而且SQL Server经常用“实例名”来区分同一台机器上的多个实例格式是主机名\\实例名。MySQL则简单得多直接是host192.168.1.10;port3306;databasetestdb;userroot;passwordxxx。还有图形化管理工具的差异SQL Server官方对应的是SSMSMySQL最常用的是Workbench或者命令行。不少人在Windows上装了某个版本之后发现“宝塔面板里识别不到SQL Server”之类的问题本质就是一台机器上同时存在多个数据库实例管理工具扫描的目标可能对不上。遇到这种情况优先检查服务有没有启动、端口有没有被占用再用连接串手动测试别一上来就怀疑安装坏了。2.2 引号、字符串拼接和大小写敏感第一行SQL就不同这是一个非常容易被忽略的差异。SQL Server默认用[ ]包住关键字或特殊列名也接受双引号取决于QUOTED_IDENTIFIER设置。MySQL用反引号就是键盘数字1左边那个键包住表名、列名。如果你在MySQL里用方括号或者在SQL Server里用反引号轻则语法报错重则被当成普通字符串查了半天不知道错在哪。字符串拼接的差异更著名SQL ServerSELECT hello , worldMySQLSELECT CONCAT(hello, ,, world)这里有个经典坑SQL Server里如果用拼接只要有一个字符串是NULL整个结果就变成NULLMySQL的CONCAT同样遇到NULL会返回NULL但MySQL还有CONCAT_WS可以指定分隔符跳过NULL。两边的行为逻辑不一样你在拼接用户地址、商品标签这种可能为空的字段时一定要先处理NULL。大小写敏感方面SQL Server默认的排序规则不区分大小写具体看collation配置MySQL在Linux上表名是区分大小写的受lower_case_table_names参数影响。Windows上的MySQL默认不区分Linux上默认区分。所以从Windows开发环境搬到Linux线上环境最容易出现的就是“本地查询正常线上报表不存在”。2.3 自增主键和默认值写表结构时的差异自增主键是最常见的建表需求两者写法如下-- SQL Server CREATE TABLE dbo.users ( id INT IDENTITY(1,1) PRIMARY KEY, name NVARCHAR(50) NOT NULL ); -- MySQL CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL );插入后取自增ID的方式也不一样。SQL Server用SELECT IDENTITY或者SCOPE_IDENTITY()MySQL用SELECT LAST_INSERT_ID()。注意IDENTITY在某些触发器场景下会拿到其他表生成的ID所以SQL Server更推荐SCOPE_IDENTITY()。MySQL的LAST_INSERT_ID()只跟当前会话有关不受其他连接影响这一点倒是很直白。默认值这边SQL Server写GETDATE()MySQL写CURRENT_TIMESTAMP。SQL Server还有NEWID()生成GUIDMySQL对应的是UUID()。如果你习惯在MySQL里用DEFAULT 2024-01-01这种固定日期SQL Server也能写但SQL Server对datetime默认值用函数更常见。2.4 BOOLEAN的伪装一个容易被忽视的坑MySQL里有BOOLEAN类型但它其实是TINYINT(1)的别名你可以写入0或1也可以写TRUE/FALSE会被翻译成1/0。SQL Server没有真正的BOOLEAN列类型在表里存布尔值通常用BIT值为0和1。这个差异带来的坑是“表里是否有这个字段类型”在迁移工具里经常不兼容。从MySQL导出的TINYINT(1)到SQL Server若不手动改成BIT后面写查询条件时还得小心WHERE active 1和WHERE active TRUE的行为差异。我自己就吃过这个亏觉得两个库都能用“1”表达真值结果在映射工具里把整张表的字段类型全部打乱了。3. 数据类型与函数族字符串转数字、日期处理和多行合并的对照3.1 字符串转数字两边都会转但失败时的表现不同热搜词里专门有人搜“sqlserver 字符串转数字”说明这确实是个高频需求。SQL Server一般这样写SELECT CAST(123 AS INT); SELECT CONVERT(INT, 123);MySQL也差不多SELECT CAST(123 AS SIGNED); SELECT CONVERT(123, SIGNED);但微妙的地方在转型失败。SQL Server如果遇到CAST(abc AS INT)直接抛错整个查询中断。MySQL在使用CAST(abc AS SIGNED)时不会报错而是返回0同时给出一条warning。这对数据处理来说是个天壤之别你从文件导入一批“看起来是数字”的字符串SQL Server里一条脏数据就能让整个批处理挂掉但你能立刻发现MySQL里数据静默变成0等你算平均值时才发现一批脏数据已经混进去了。所以我的习惯是SQL Server侧写一个明确的正则或ISNUMERIC()做前置过滤MySQL侧用REGEXP ^[0-9]$先把不合规的剔掉绝对不指望隐式转换帮我把脏数据“安全”处理掉。3.2 日期函数对照参数顺序可能让你怀疑人生日期函数是两边差异最大的重灾区之一。下面这张表我经常放在手边功能SQL ServerMySQL当前日期时间GETDATE()NOW()取年份YEAR(date)/DATEPART(year, date)YEAR(date)/EXTRACT(YEAR FROM date)日期差DATEDIFF(day, start, end)DATEDIFF(start, end)注意参数顺序加天数DATEADD(day, 5, date)DATE_ADD(date, INTERVAL 5 DAY)格式化日期FORMAT(date, yyyy-MM-dd)DATE_FORMAT(date, %Y-%m-%d)这里面最大的坑是DATEDIFF。SQL Server的语法是DATEDIFF(datepart, startdate, enddate)两个日期先后顺序不能反不然会算出负数。MySQL的DATEDIFF只返回天数差语法是DATEDIFF(date1, date2)是拿第一个参数减第二个参数。你从SQL Server迁到MySQL第一反应可能是把SQL写成DATEDIFF(2024-06-01, 2024-05-01)这倒没问题但你要是从MySQL迁到SQL Server还沿袭MySQL的参数顺序就会得到完全相反的天数。另一个坑是搜索热词里的DATEPART。SQL Server里它是一个通用函数用DATEPART(weekday, date)取星期几MySQL也有DATEPART吗严格来说没有MySQL里想取星期几一般用WEEKDAY(date)或DAYOFWEEK(date)两者返回的索引基准不一样很容易搞混。我今天写代码前都会确认一下取星期的语义绝不靠记忆硬写。3.3 字符串函数从LEN/LENGTH到GROUP_CONCAT字符串函数里最容易记错的是“返回字符长度”的函数。SQL Server用LEN它返回的是字符数MySQL用LENGTH它返回的是字节数所以中文字符在UTF-8下用MySQL的LENGTH会得到3倍字符数得用CHAR_LENGTH才对。这直接影响你截取字符串、校验字段长度时的逻辑。多行合并成一行更是经典需求。SQL Server 2017以上用STRING_AGGSELECT department, STRING_AGG(employee_name, ,) FROM employees GROUP BY department;MySQL用GROUP_CONCATSELECT department, GROUP_CONCAT(employee_name) FROM employees GROUP BY department;两者语法看着像但默认长度限制完全不同MySQL的GROUP_CONCAT受group_concat_max_len限制默认值不长合并一堆标签时容易被截断需要SET SESSION group_concat_max_len 10240;之类的调整。SQL Server的STRING_AGG没有类似的默认长度问题不过你要显式指定分隔符和排序SELECT STRING_AGG(employee_name, ,) WITHIN GROUP (ORDER BY employee_name) ...如果你是在老版本SQL Server上没有STRING_AGG还得靠FOR XML PATH这种能看明白但写起来别扭的方案。所以“多行合并成一行”这个热词背后是一种非常典型的两库差异。3.4 空值的脾气ISNULL、IFNULL和COALESCE处理NULL是数据库开发永远的日常。SQL Server有ISNULL(expr, default)MySQL有IFNULL(expr, default)两者用法类似但别以为这是同一个函数。它们的行为有一个重要区别SQL Server的ISNULL返回值的类型受第一个参数类型主导如果第一个参数是INT第二个参数写一个很大的BIGINT或VARCHAR结果可能被隐式转换截断。MySQL的IFNULL相对宽松类型推导更接近表达式整体。两个库都支持标准的COALESCE这个函数在两边行为基本一致取第一个非NULL值。所以我做跨库开发时能写COALESCE就尽量用COALESCE而不是ISNULL或IFNULL就是为了少记一套差异。4. 增删改查里的语法分叉分页、JOIN更新、MERGE与排序行为4.1 分页查询TOP、LIMIT和OFFSET的心智模型分页语法是两库最外显的区别也是热搜词里“sqlserver offset 后再 top 20 查到的是什么”这个问题的来源。SQL Server旧派写法SELECT * FROM ( SELECT ROW_NUMBER() OVER (ORDER BY id) AS rn, * FROM users ) t WHERE rn BETWEEN 1 AND 20;SQL Server 2012以后可以用OFFSET FETCHSELECT * FROM users ORDER BY id OFFSET 20 ROWS FETCH NEXT 20 ROWS ONLY;MySQL长期以来的写法SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 20; -- 等价于 SELECT * FROM users ORDER BY id LIMIT 20, 20;关键词“offset 后再 top 20”的坑在于有些人在SQL Server里写SELECT TOP 20 * FROM users ORDER BY id OFFSET 20 ROWS;这条SQL是错误的或者说语义混乱。OFFSET是跳过前20行然后再取结果但如果你在前面加了TOP 20执行计划会按“先按TOP取前20再OFFSET跳过20”来解析吗并不会实际上这是非法组合正确理解是要么用TOP做限制要么用OFFSET FETCH做跳过限制你不能把“跳过20行”和“只取20行”写成一个词序颠倒的句子。OFFSET 20 ROWS FETCH NEXT 20 ROWS ONLY表示“从第21行开始往后拿20行”而TOP 20加上OFFSET的组合一旦写错查出来的数据就不是人话。我处理的很多线上分页慢查询问题也都出在这MySQL的LIMIT 100000, 20不是“跳过10万行”这么简单它实际上还得把前10万行都读出来只是不返回。SQL Server的OFFSET FETCH同样需要排序支持如果ORDER BY的字段没有索引翻页越深越慢。所以无论哪个库深度分页的优化思路都是先缩小范围再说。4.2 UPDATE JOIN 和 DELETE JOIN一个能写一个不能写我在做数据订正时经常需要“按另一张表来更新当前表”。SQL Server的写法是UPDATE ... FROM ... JOINMySQL是UPDATE ... JOIN ... SET。举个例子你要把订单表里的城市名称按照城市编码表更新成最新名称。SQL ServerUPDATE o SET o.city_name c.new_city_name FROM orders o INNER JOIN city_dict c ON o.city_code c.city_code;MySQLUPDATE orders o INNER JOIN city_dict c ON o.city_code c.city_code SET o.city_name c.new_city_name;可以看到语法结构差很多SQL Server是先写要更新的目标表别名再写FROM关联MySQL是先把JOIN写完再写SET。如果你在SQL Server里按MySQL的写法写或者反过来数据库会直接报语法错误没有商量的余地。DELETE关联删除也是类似SQL Server用DELETE FROM o FROM orders o JOIN tmp t ON ...MySQL用DELETE o FROM orders o JOIN tmp t ON ...。这种语法差异在ORM里通常被隐藏了但一旦你手写SQL批量清理数据就会立刻暴露。4.3 批量插入和MERGE别让“看起来更强大”的语法骗了批量插入在两者都很好用-- SQL Server INSERT INTO users (name) VALUES (a), (b), (c); -- MySQL INSERT INTO users (name) VALUES (a), (b), (c);这类基础写法两边高度一致但再进阶一点就分叉了。SQL Server有MERGE语法可以一条语句同时处理插入、更新、删除。MySQL直到8.0都还没有完全对标的MERGE最常用的替代是INSERT ... ON DUPLICATE KEY UPDATEINSERT INTO users (id, name) VALUES (1, 张三) ON DUPLICATE KEY UPDATE name VALUES(name);注意这个写法有两个容易踩的坑。第一它依赖唯一索引或主键来判断“重复”而SQL Server的MERGE可以自己写ON条件灵活性差很多。第二ON DUPLICATE KEY UPDATE即使只更新数据不插入也会消耗自增ID因为InnoDB在冲突时已经为新的候选行分配了自增值。如果你在高并发下依赖自增ID做业务排序就会发现ID空洞比预想的多。所以不要简单地把这两者划等号——MERGE是通用同步工具ON DUPLICATE KEY UPDATE是“MySQL特有的一次性补丁”需要精准同步时我会选择“先查再决定插入/更新”的显式逻辑虽然多写几行但行为透明。4.4 排序的隐藏差异NULL和中文排序排序看似同构但NULL的默认位置两库不同。SQL Server的ORDER BY col ASC时默认NULL排在最前MySQL的ORDER BY col ASC时默认NULL排在最后。写分页和数据对比时如果没意识到这个差异你看到的“第一行”完全不是同一个逻辑。更麻烦的是中文排序。SQL Server默认按中文排序规则按拼音排序MySQL的默认排序规则取决于collation常见的是utf8mb4_general_ci中文大体按Unicode编码排序拼音排序并不完全符合直觉。如果你要“按首字母排序”或“按拼音搜索”两边都需要单独指定collation。比如MySQL可以ORDER BY name COLLATE utf8mb4_zh_0900_as_csSQL Server可以建表时指定Chinese_PRC_CI_AS。这类问题在开发环境很难暴露一上生产发现“本地排对了线上排不对”多半就是两边collation不一致。5. 事务、锁与索引并发场景下最容易忽略的差异5.1 默认隔离级别不同体验天差地别SQL Server默认隔离级别是READ COMMITTEDMySQL InnoDB默认是REPEATABLE READ。这会带来几个直接后果。在SQL Server的READ COMMITTED下同一个事务里两次SELECT可能拿到不同的数据因为其他事务可能在两次查询之间提交了修改。在MySQL的REPEATABLE READ下同一个事务内多次SELECT看到的是同一个快照读一致性更强。但别以为MySQL默认级别高就“更好”。REPEATABLE READ的实现依赖间隙锁它锁的不只是命中的行还有索引范围里的“空隙”所以并发写入冲突的概率更大死锁也更难排查。SQL Server的READ COMMITTED锁范围相对小但你可能要应对“不可重复读”带来的报表数据抖动。我在两套库上做并发压测的粗略体感是MySQL上同样的UPDATE并发间隙锁导致的等待明显更多SQL Server上则更容易遇到“一个慢查询拖着整个表锁”的情况。没有绝对优势只有不同的并发模型。5.2 锁的分类叫法不同本质要能对上热词里有“mysql锁的分类”我简单梳理一下并和SQL Server的锁概念做对应方便两边切换时脑内翻译。MySQL InnoDB的锁常按这几个维度分全局锁、表级锁、行级锁行锁又分共享锁S、排他锁X间隙锁、临键锁next-key lock、意向锁自增锁SQL Server那边的锁资源类型五花八门常见的有共享锁S排他锁X更新锁U意向锁IS/IX等架构锁Sch-M/Sch-S两者的“共享/排他”概念是一致的但SQL Server在更新一个数据页时会有“更新锁”这个中间状态先拿更新锁真正写的时候再升级为排他锁MySQL里没有这个显式概念。至于间隙锁SQL Server没有直接对应的东西它只能通过锁粒度页锁/表锁和隔离级别来近似。对日常开发来说你不需要把两边锁的每种细则都背下来关键是能看懂死锁报错MySQL死锁报错会直接显示deadlock found然后打印被回滚的事务SQL Server死锁报错通常是一个错误号1205其中一个会话被选为死锁牺牲品。处理思路都是“缩短事务时间、保持一致的访问顺序、避免大范围扫描”这个放之四海皆准。5.3 索引语法和建索引行为索引的基础创建语法两边几乎一样CREATE INDEX idx_users_name ON users(name);但细节有差SQL Server支持包含列索引CREATE INDEX idx_users_name ON users(name) INCLUDE (phone, email);这个INCLUDE的意思是非索引列也放进索引页的叶子节点但不用来排序这样可以减少回表查询。MySQL没有INCLUDE语法想实现覆盖索引只能把所有查询列都放进去比如CREATE INDEX idx_users_name_phone_email ON users(name, phone, email)但是放进去的列都参与索引排序性能和维护成本会更敏感。另一个差异是索引键长度限制。MySQL的InnoDB对索引键长度限制受参数和行格式影响常见的UTF-8字符串列做长索引时经常会碰到“Specified key was too long; max key length is 3072 bytes”的报错解决办法是用前缀索引比如CREATE INDEX idx_name ON users(name(20))。SQL Server也有最大键长度限制默认是900字节老版本或1700字节新版本但处理手段更复杂通常你会考虑改用NVARCHAR的合理长度或者用哈希值列做索引。5.4 自增ID回滚和事务截断自增ID还有一个行为差异我把它单独列出来是因为太常踩了。MySQL的InnoDB在事务回滚后自增ID不会回退也就是说你开启一个事务插入一条记录回滚再插入一条ID会从2开始而不是1。SQL Server的IDENTITY在创建表后可以直接DBCC CHECKIDENT(table, RESEED, 0)重置但日常回滚后同样不会自动回退。但MySQL还有另一个坑删除最大ID之后如果没有重启实例AUTO_INCREMENT不会自动降到当前最大值以下除非你显式ALTER TABLE users AUTO_INCREMENT 1。SQL Server的行为相对“好猜”但这好猜也是相对的。总之如果你要依赖自增ID做“物理顺序”的业务两个库都会让你失望还是老老实实加一个created_at字段做业务排序吧。6. 存储过程、运维命令与迁移排错自用笔记里最值钱的几段6.1 存储过程的方言差异写存储过程时两库的“骨架”差别非常明显。SQL Server存储过程CREATE PROCEDURE dbo.GetUsers city NVARCHAR(50) AS BEGIN SET NOCOUNT ON; SELECT * FROM users WHERE city city; END;MySQL存储过程DELIMITER $$ CREATE PROCEDURE GetUsers(IN city VARCHAR(50)) BEGIN SELECT * FROM users WHERE city city; END$$ DELIMITER ;差异主要体现在参数声明SQL Server用city并且不需要写IN/OUT默认输入参数MySQL变量可以直接用IN、OUT、INOUT。流程控制SQL Server是IF ... ELSE ... BEGIN ... ENDMySQL是IF ... THEN ... ELSE ... END IF。循环语句也不一样SQL Server用WHILEMySQL也是WHILE但MySQL还有REPEAT、LOOP游标声明和打开方式完全不同。结果返回SQL Server存储过程直接执行多行SELECT就能返回结果集也可以加RETURN返回整数状态码MySQL存储过程不能用RETURN返回结果集只能用SELECT结果或者通过OUT参数带出。有一次我把SQL Server的存储过程直接丢给同事让他改到MySQL他改到一半崩溃了原因是不但语法要改连“改名存储过程”的语句都不同。SQL Server用sp_renameMySQL要DROP旧过程再CREATE新过程。6.2 常用运维命令对照查版本、看表结构、看进程、执行计划开发到一定阶段你一定会用命令行或脚本做运维操作。以下是我整理的常用命令速查表查的时候直接照抄操作SQL ServerMySQL查看版本SELECT VERSION;SELECT VERSION();查看数据库列表SELECT name FROM sys.databases;SHOW DATABASES;查看表结构sp_help users;DESCRIBE users;查看当前进程sp_who2;/ DMVSHOW PROCESSLIST;执行计划SET SHOWPLAN_ALL ON;EXPLAIN SELECT ...;备份BACKUP DATABASE testdb TO DISK...;mysqldump -u root -p testdb testdb.sql退出命令行EXITQUIT或exit这里面有一个很实用的习惯MySQL里遇到慢查询第一步就是SHOW PROCESSLIST看有没有Sleep时间很长的连接占着连接数SQL Server则是查sys.dm_exec_requests或者sys.dm_exec_sessions看阻塞头和wait_type。两个库的排查思路其实一样都是“找到源头会话再看它在等什么资源”只是命令长得完全不一样。6.3 迁移和排错时最容易翻车的几个点最后集中写几个我在实际迁移中踩过的坑基本都是双库迁移时的高频雷区。第一语法层面。把SQL Server代码迁到MySQL时TOP要先改成LIMITGETDATE()改成NOW()ISNULL改成IFNULL。把MySQL迁到SQL Server则是反过来的。很多人靠全局替换来做非常危险因为有些单词是关键字比如 MySQL里表名列名可以用order、key、group但在SQL Server里这些也是保留字你得加[ ]转义。两边的保留字集合并不完全重合全局替换前先跑一遍语法解析器比人肉检查靠谱。第二NULL行为差异。前面提过字符串拼接、排序位置、聚合函数结果这些都是迁移后“结果不对但不报错”的典型。我的做法是迁移后不拿总数对比拿“行列级差异对比”比如用EXCEPT或NOT EXISTS双向比对两张表的数据才能发现NULL带来的隐性差异。第三时间精度和时区。SQL Server的DATETIME精度到毫秒级MySQL的DATETIME默认到秒要精确到毫秒得专门指定DATETIME(3)。迁移时如果不调整字段类型时间值会丧精度。更麻烦的是时区MySQL的TIMESTAMP受系统时区影响会自行转换SQL Server的DATETIME2不带时区语义跨时区业务要统一约定存UTC。第四中文排序和字符集。MySQL侧务必用utf8mb4SQL Server侧最好用NVARCHAR而不是VARCHAR否则存储生僻字时SQL Server的VARCHAR编码可能丢字。在建表阶段就把这些定好后面省很多事。我一直跟团队说“会写SQL”和“会迁移数据库”是两种能力。SQL标准给了你一把钥匙但每个库都在锁芯上加了自己的拨片你光会插钥匙没用还得知道往哪个方向拧。这份笔记与其说是总结不如说是我在两边来回切换时用来“校准手感”的参照物。你也不妨把自己踩过的坑记下来慢慢就会形成一套属于自己的双库切换心法。
返回列表