去年下半年,我们把一个多门店零售的报表系统从 SQLServer 换到了 MySQL。
时间大头不在改 SQL 上,在"数字怎么对不上"上。报错好办,改完就不报了。不报错的那些,得一行行去核。
流水账记一下,顺序大概是:先让能跑,再让算对,最后才管快不快。
一、最开始那半天,报错是成片的
第一轮 SQL 跑下来,报错一片。基本就四类。
GROUP BY 的写法不允许了。 原来 SELECT 里可以带没在 GROUP BY 里的字段,MySQL 严格模式下直接报错。我们改了一批之后发现,有几个报表是故意省写的——分组字段和展示字段本来就一一对应,改起来反而让执行计划变差。最后统一把这项检查关掉了,在代码层做校验。
这个处理不算标准做法,但当时没更好的选择。
TOP 全部改成 LIMIT。 没难度,但量太大,漏一个就跑不过,反复编译反复试。
ISNULL 改成 IFNULL。 这个简单,注意参数顺序别搞反就行。
字符串拼接。 这个不报错,放到下一节说。
二、不报错的才是真坑
DATEDIFF 顺序反了,环比全成负数
报表里有"近 30 天""环比上期",都在用 DATEDIFF 算天数差。
SQLServer 是三个参数,单位放第一位:
MySQL 只有两个参数,没有单位。迁移的时候把单位删了,参数顺序没动,结果全反了,算出来是负数。
报表照常能跑,只是所有时间差都是负的,环比显示成一两百的负增长。
这个错了快一周才发现,因为负增长在报表里太正常了。 没人会盯着一张"本月比上月下降"的表怀疑代码。
后来加了个笨办法:所有日期函数改完,拿一个已知答案的日期手工对一遍。
加号变成加法,标题列全是 0
报表导出要拼标题,原来是这么写的:
到 MySQL,加号就是加法。前面那个字符串转成数字是 0,0 加什么都没了。
结果标题列变成纯数字,或者干脆是 0。一个错都不报。
改成 CONCAT 之后又冒出个小问题:CONCAT 里只要有 NULL,整个结果就是 NULL。原来在 SQLServer 上碰巧没遇到,迁过来发现多了一批空白行,也是查了一阵才反应过来。
时间差 8 小时,日报晚上八点才有数据
上线第二天,运营说今天的数据是空的,晚上八点之后才慢慢出来。
原因不复杂:MySQL 那边的服务器时区设的是 UTC,取当前时间比北京时间晚 8 小时。SQLServer 取的是服务器本地时间,从来没为这个操心过。
连接串和实例两处都改了才正常。
改完还有个后遗症:库里有两种时间类型,一种会跟着时区转,一种不会。改完之后历史数据得重新看一遍,不能只测新写进去的。
翻页翻出重复数据
报表导出按页取,一页五百条。翻到第三页,发现两条和第一页一模一样。
问题不在分页,在排序。排序那个字段重复值太多(同一天几十个门店),相同值之间谁前谁后,数据库不保证。两次执行顺序一变,翻页就会重复或者漏。
解决办法就是把排序字段后面再挂一个唯一的列兜底。这个在 SQLServer 上其实也有,只是当时数据少,没露出来。
三、字段类型,几个不出声的地方
字符串前缀。 SQLServer 里中文串前面习惯带个 N,迁过来这个写法不认,得批量清掉。用正则扫要小心,别把正文里的字母也删了。
时间精度。 原来那边时间是带毫秒的,MySQL 默认只到秒。迁完同一秒里的两条记录时间戳一模一样,按时间排序就乱了,按时间去重还会把两笔合成一笔,报表少算。后来把精度调回去了,顺带发现 ORM 那边也要跟着调。
超长的处理方式变了。 SQLServer 是截断,给个警告但数据进去了。MySQL 严格模式直接报错。导历史数据时一次报了几百条,最后把几个备注字段加长了事。
金额字段。 别顺手把小数位加上去。小数位一多,报表里做除法会有尾差,汇总就对不上。
布尔字段。 底层存的是 0/1,但驱动读出来是布尔还是数字,跟连接配置有关。我们这边出现过前端显示 true/false 的,查了半天是连接串没配。
四、ROW_NUMBER 改写,这段最费劲
报表里最常用的一招是"取每个门店最新的一条"。
SQLServer 一个 ROW_NUMBER 加 PARTITION BY 就完了。目标库有一部分版本不支持这个,得自己想办法。
第一种写法是用户变量。 在 SELECT 里用变量累加行号,按门店分组重置。
能跑,但不稳。新版本里这种写法已经不推荐了,而且优化器不保证 SELECT 里表达式的计算顺序,行号经常乱。老版本上勉强能用,等于留了个升级的雷。
第二种是先把每个门店的最大时间取出来,再关联回去。
看着没毛病,实际出了大问题:同一门店同一秒有两笔的时候,关联会出两条。
表本来几十万行,结果多出来一截,报表总额也偏高了一些。没有报错,就是数字大了一点。
这个查了一天多。一开始怀疑源库有重复数据,去查,没有;怀疑关联条件写错,换了三种写法结果一样;最后把中间结果导出来逐行比,才发现是时间戳相同导致一对多。
最后的解法是拿唯一键代替时间。 取每个门店最大的那条 ID,因为 ID 唯一,不会再出一对多。
前提是"最新的"和"ID 最大的"是同一件事。我们这个场景成立,因为 ID 是递增的。如果业务上有补录,这个改法就不行了,得另外想办法。
五、锁:原来那些写法搬不过来
SQLServer 那套代码里,几乎每个报表查询后面都跟着 NOLOCK。
原因很实际:报表跑起来会卡住业务写入,加了就不卡了,代价是可能读到还没提交的数据。对报表来说这笔交易划算。
迁过来第一件事就是把 NOLOCK 全删了。MySQL 没这个写法,而且它读的时候本来就不加锁、不阻塞写入。
但反过来出了个坑。
有个过程,为了"读的时候数据别被人改",原来写的是带更新锁的查询。我们照着改了,换成了 FOR UPDATE。
结果这个报表一跑,那张门店表像被锁住一样,下单接口大面积超时。
后来才搞明白:MySQL 在某些隔离级别下,范围条件加锁锁的不是命中的那几行,是整个区间。SQLServer 默认级别下没这个行为,所以从来没人注意过。
最后那个 FOR UPDATE 直接删了——报表本来就不需要锁。
六、存储过程,最后决定不迁
三十来个存储过程,平均两百行,最长的一个七百多行。
本来打算一个一个翻译:变量去掉 @、临时表换写法、异常处理换写法、IF 语法调一调。
翻到第五个就发现方向不对。三个原因:
一是 MySQL 的存储过程不好调试,出错只能临时加查询打点,改完自己都不敢确认逻辑对不对;二是嵌套 IF 一多,翻完的代码没人看得懂;三是这套东西后面还要维护。
最后全部拆成 Java。
拆法是按阶段拆,每个阶段落一张中间表:取基础数据 → 算中间指标 → 汇总 → 输出。每步能单独跑,能看中间结果,某个指标算错只改那一步。
代价是事务边界变了。原来一个过程是一个事务,失败全回滚;拆成几步之后,第三步失败,前两步的结果已经写进去了,重跑会重复累加。
处理办法是每张中间表加个批次号,重跑前按批次号先删。当时觉得多余,后来救过两次——上游数据补录要重算几个月的数据,直接按批次重跑,没碰生产数据。
七、多表关联:3 秒变成好几分钟
有个区域汇总报表,五张表关联,三层分组。
SQLServer 上三秒出结果,迁过来第一版跑了好几分钟。
老版本的 MySQL 在关联上的优化比 SQLServer 弱不少,几张表一关联,驱动表选错就是成倍地扫。
三步处理,按效果排:
先把明细聚合,再去关联。 原来是明细表直接和门店、区域、商品表关联,最后才分组。改成先把明细按门店和日期汇总成中间表,再跟维度表关联。参与关联的行数一下就下来了。
补索引。 原来只有单列索引,换成联合索引之后能直接走索引,不用再回表。
把子查询改成关联。 IN (子查询) 这种写法,MySQL 有时候会先把子查询算成临时表再全表扫,SQLServer 一般能处理好。改成 JOIN 之后差别很明显。
最后降到几秒,能接受。
八、其他零碎的
- UPDATE 带关联的写法不一样,改错顺序会把整表更新掉,而且不报错
- MERGE 换成 INSERT ... ON DUPLICATE KEY UPDATE
- 几个字符串函数换了名字,注意有的算字符数、有的算字节数,中文会算错
- UNION 里的排序和限制,原来的写法要改成每个分支加括号,不然限制作用在整个结果上
- 排序规则默认值不同,卡号、单号这种查询查不到,八成是这个
1002



被折叠的 条评论
为什么被折叠?



