1. 从一次线上报警开始:1267报错到底在说什么
先交代一下背景。某天下午我负责的一个报表服务突然出现了一批超时报警,查慢查询日志发现罪魁祸首是一条多表关联的SQL,执行计划也没啥问题,索引都走了,就是卡在了一个诡异的报错上:ERROR 1267 (HY000): Illegal mix of collations (utf8mb4_general_ci,IMPLICIT) and (utf8mb4_unicode_ci,IMPLICIT) for operation '='。
这个报错你Ifyou在MySQL里做过一点稍微复杂一点的查询,大概率都见过。它翻译过来就是:“排序规则混用了,没法比较。”大白话讲,两个字符串字段在数据库眼里不是一个“语言规则体系”的,MySQL不允许你直接拿它们做等值比较、关联、排序这类操作。
这个报错坑就坑在:大多数情况下你写的SQL语句语法完全正确,字段也都有索引,但就是执行不了。尤其是团队协作的项目里,有人建表时用了默认排序规则,有人建表时特意指定了另一种,两边的字段一JOIN,1267就冒出来了。
这篇文章我打算从上到下把这个报错吃透,包括它产生的底层原因、最典型的触发场景、几种可行的解决办法,以及怎么从工程规范层面彻底避免它再次出现。内容不绕弯子,直接按实际排查的思路来写。
2. 字符集和排序规则的基本概念:不懂这个就没法根治
先补一点基础知识,因为我从不少同事的排查过程里发现,很多人卡住不是因为不会写SQL,而是对字符集和排序规则的层级关系没搞清。MySQL里有两个层面经常被搞混:一个是字符集(charset),一个是排序规则(collation)。
字符集决定的是“字符怎么存储”。比如utf8mb4就是用1到4个字节去编码一个字符,它支持emoji、生僻汉字等各种Unicode字符。utf8mb4是目前主流建库选择。排序规则则决定的是“字符之间怎么比较大小、怎么排序”,它依附于字符集存在。同一个字符集下可以有多种排序规则,比如utf8mb4下常见的有:
| 排序规则 | 特点 |
|---|---|
| utf8mb4_general_ci | 比较宽松,性能较好,早期默认 |
| utf8mb4_unicode_ci | 基于Unicode排序算法,更精确,5.7+较推荐 |
| utf8mb4_0900_ai_ci | MySQL 8.0默认,大小写不敏感、口音不敏感 |
| utf8mb4_bin | 区分大小写,按二进制逐字节比较 |
这些排序规则虽然都属于utf8mb4字符集,但它们在MySQL内部是不同的数据类型标识。两个字段即便都叫“字符串类型”,只要排序规则不同,MySQL就认为它们的“可比较性”不存在,直接拒绝执行。
打个比方,字符集像是语言(中文、英文),排序规则像是语言内的方言或习惯(大陆中文、繁体中文习惯)。同样是中文,你用简体字和注音符号规则去排序,结果顺序可能不同,系统不敢保证一致性,索性不允许直接比。
实际开发中常见的一个现象是:数据库表结构里写入的时候通常不会报错,因为字符集不同只影响存储;但到了查询、关联、比较环节,MySQL必须确定一个统一的规则来做判断,一旦发现两头不一致,就走到了1267。
还有一点很多人容易忽略:排序规则不仅仅存在于表字段上,还存在于连接会话、字符串字面量、表达式结果之中。连接层的排序规则、SQL里直接写的字符串默认使用的排序规则、函数返回值自带的排序规则,都可能成为“第三者”,把原本一致的比较搅乱。这也是为什么有时候你明明检查了两个字段都是utf8mb4_unicode_ci,还是报了1267——可能是连接层的默认排序规则和字段不一样,也可能是字符集不同导致的连带问题。
所以解决1267,不光是“改一个字段”的局部动作,而是要从字段-表-库-连接四个层级全看一遍,定位谁才是那个“搅局者”。
3. 复现一次典型场景:多表JOIN里的隐性“异类”排序规则
理论讲完了,直接进入实战复现。我这边项目用的是MySQL 5.7,库表结构是这样的:
用户表t_user,字段username用的是utf8mb4_unicode_ci;订单表t_order,有个冗余字段buyer_name,建表的时候没注意,被设成了utf8mb4_general_ci(早期模板遗留)。
当时要求统计一段时间内下单用户的名单,SQL大概长这样:
SELECT u.id, u.username, o.order_sn FROM t_user u JOIN t_order o ON u.username = o.buyer_name WHERE o.created_at BETWEEN '2024-01-01 00:00:00' AND '2024-12-31 23:59:59';这条SQL一执行,MySQL立刻甩出1267错误。第一次看到这个报错,人很容易懵:“我条件字段不都是varchar吗?怎么就不能相等了?”但只要你去查一下information_schema,把两边的排序规则拉出来对比,一切都很清楚:
SELECT table_name, column_name, character_set_name, collation_name FROM information_schema.COLUMNS WHERE table_schema = 'your_db' AND column_name IN ('username', 'buyer_name');查询结果非常直观:username的排序规则是utf8mb4_unicode_ci,buyer_name的排序规则是utf8mb4_general_ci。这俩没法比。
那为什么MySQL不在JOIN的时候自动转换一下排序规则呢?理论上可以做隐式转换,但MySQL选择不这么做。原因之一是隐式转换在不同排序规则之间不总是可逆的,比如大小写敏感性规则不一致、特殊字符权重不同,直接自动转换可能导致结果不准确。为了避免“看似正确但实际排序/匹配有偏差”这种更难排查的问题,MySQL选择了一条更安全的路:报错,提醒人工介入。
这里也顺带补充一个容易踩的坑:有时候两字段字符集都是utf8mb4,但一个带_bin,一个带_unicode_ci,同样会报错。不要因为“字符集一样”就觉得万事大吉,核心看**排序规则(collation)**是否一致。
如果遇到三个表以上关联,排查范围就更大了。我建议每次都走一遍information_schema查询,把参与JOIN和WHERE比较的字段都拉出来,建立一张对照表再逐个处理。靠肉眼扫建表语句在表多的时候实在太慢,也容易漏。
4. 解决1267的四种实操套路:从简单粗暴到釜底抽薪
定位到了排序规则不一致,剩下的就是动手解决。解决的办法不少,但各有适用场景,选错了会有隐性代价。我按从“临时能用”到“长期干净”的顺序,把几种最常用的做法一一说清楚。
4.1 直接修改表字段的排序规则
如果你确认全表所有字段都应该遵循同一套规则,直接改表是最痛快的做法。对于上面那个例子,有两种改法:
一种是把t_order.buyer_name改成和t_user.username一样的utf8mb4_unicode_ci:
ALTER TABLE t_order MODIFY buyer_name VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;另一种是如果全表都用了utf8mb4_general_ci,只是用户表特殊,那就改用户表。总之思路就是让不一致的字段向主流规则靠拢。
这个操作的风险点在于:改字段排序规则会触发表重建,对大表来说是个不小的DDL操作。在我那个项目里t_order当时已经有两千多万行,直接ALTER会锁表,影响线上写入。所以我当时没有立即在线上执行,而是先用了第4.2节的临时方案,等业务低峰期才做的字段变更。
4.2 使用CONVERT函数在查询时转换
不改表结构、只在SQL层面解决的方案,首选是CONVERT()函数:
SELECT u.id, u.username, o.order_sn FROM t_user u JOIN t_order o ON u.username = CONVERT(o.buyer_name USING utf8mb4) COLLATE utf8mb4_unicode_ci WHERE o.created_at BETWEEN '2024-01-01 00:00:00' AND '2024-12-31 23:59:59';这段SQL的本质是:把buyer_name从原本的utf8mb4_general_ci转换成utf8mb4_unicode_ci后再与username比较。转换过后两边排序规则一致,MySQL就不再报1267了。
要注意的是,在JOIN条件中对字段使用函数,会让该字段上的索引失效。如果buyer_name上原本有索引,这条SQL会转向全表扫描加逐行转换,性能可能反而更差。所以这个方案适合临时止血,不适合长期高频查询。
4.3 用COLLATE子句直接指定比较规则
另一种常见的写法是用COLLATE子句,这种写法不用转换整个字段的内容,只声明“本次比较请按我指定的规则来”:
SELECT u.id, u.username, o.order_sn FROM t_user u JOIN t_order o ON u.username = o.buyer_name COLLATE utf8mb4_unicode_ci WHERE o.created_at BETWEEN '2024-01-01 00:00:00' AND '2024-12-31 23:59:59';它的执行逻辑是:表面上仍然是buyer_name和username比较,但MySQL被告知这次比较采用utf8mb4_unicode_ci规则,于是两边就以这个统一规则进行比较。这里必须注意一个坑:COLLATE子句的优先级非常高,它会把另一个字段也“拉”入自己的规则体系。比如上面这条SQL,o.buyer_name COLLATE utf8mb4_unicode_ci会和u.username比较,MySQL会尝试将u.username也转换为utf8mb4_unicode_ci的上下文。只有另一个字段的排序规则与此兼容(比如也是utf8mb4族内),才能成功。
从索引角度讲,COLLATE加在JOIN条件的某个字段上,同样会使该字段索引在本次查询中失效。所以第4.3方案适合小表关联或者低频报表,不适合大表在线查询。
4.4 统一数据库、表、字段、连接的排序规则,彻底根除
说完临时方案,再讲根治方案。真正从根上避免1267,必须让一个系统里的所有字符集和排序规则保持统一。我的实际做法分三步走:
第一步,先定标准。新项目统一使用utf8mb4字符集和utf8mb4_unicode_ci排序规则(5.7环境),或者是utf8mb4_0900_ai_ci(8.0环境)。定标准的时候要考虑你的业务语言场景,中文业务一般用utf8mb4_unicode_ci或utf8mb4_0900_ai_ci即可。
第二步,把旧表全部迁移到标准上来。迁移顺序建议先小表后大表,先核心后边缘,每一张表改动前先确认没有依赖此排序规则的视图、存储过程。
第三步,固定连接层参数。Java、Python等连接串里明确指定characterEncoding=utf8mb4和连接排序规则,不要让连接的默认值“飘”。MySQL连接层默认排序规则会随着服务端配置或驱动版本变化,有时候你以为两边字段都统一了,但连接层又冒出来一个不一样的规则,1267依旧会出现。
做完这三步,95%以上的1267问题都能根除。剩下5%是历史遗留的极端场景,比如同一张表内既有utf8mb4又有latin1字段的情况,这种就需要单独评估迁移方案。
5. 几个容易被忽略的隐藏触发源:不只是表字段不一致
很多人以为1267只会出现在表字段JOIN时,但其实有几个隐藏触发源非常反直觉。排查时如果只盯表字段,容易被误导。
第一个隐藏源是字符串字面量。SQL里直接写死的字符串,比如WHERE status = 'active',这个'active'自身会有一个排序规则。如果它和status字段的排序规则不一致,一样会报1267。解决办法是给字面量显式加上COLLATE,或干脆改字段。
第二个隐藏源是表达式结果。比如CONCAT()、SUBSTRING()这类函数返回的字符串,其排序规则可能跟源字段不同,再拿这个结果去和其他字段比较,可能就会触发1267。这种场景在复杂报表SQL里尤其容易遇到,报错信息里的“for operation”会指向某个表达式而不是某个具体表字段,排查时不要死盯列名。
第三个隐藏源是视图和派生表。视图内部的字段排序规则与视图外部的字段不一致时,外层查询一JOIN就会报错。我在某个统计项目里遇到过这样的问题:视图本身没问题,但视图里有一个字段是latin1_swedish_ci,外层一关联就炸。这种视图排查起来更隐蔽,因为你在外层SQL里压根看不见这个字段的排序规则定义。
第四个隐藏源是存储过程/函数里的变量。有些存储过程定义了局部变量,赋值时继承了某种排序规则,后续与该变量比较的字段若规则不一致,也会报1267。这种问题通常只在夜深人静跑定时任务时炸出来,调试起来十分痛苦。
针对这些隐藏源,一个比较有效的排查技巧是使用COLLATION()函数看某个字符串表达式的实际排序规则。比如:
SELECT COLLATION(CONCAT(u.username, '-suffix')) FROM t_user u LIMIT 1;如果发现结果和预期不一致,那么后续比较时就得多加小心。这个技巧在排查视图、表达式场景时特别有用。
6. 实战排查命令清单和我的踩坑记录
最后这部分我给一套可以直接复制粘贴的排查命令清单,都是实际验证过有效的。遇到1267时按照顺序跑一圈,基本能在十分钟内定位问题。
6.1 查看当前连接和库的排序规则
-- 查看连接级别排序规则 SHOW VARIABLES LIKE 'collation_%'; -- 查看库级别的排序规则 SELECT default_character_set_name, DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE schema_name = 'your_db';6.2 列出某张表所有字段的字符集和排序规则
SELECT COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'your_db' AND TABLE_NAME = 't_order' ORDER BY ORDINAL_POSITION;这一步能快速发现表里有没有“异类”字段,尤其是那种混用了utf8mb4和latin1的新老字段并存的表。
6.3 查看SQL中各部分表达式继承的排序规则
SELECT COLLATION(u.username) AS col_user, COLLATION(o.buyer_name) AS col_buyer, COLLATION('literal') AS col_str FROM t_user u LEFT JOIN t_order o ON u.id = o.user_id LIMIT 1;这个方法直观展示三个关键元素的排序规则,方便判断到底谁和谁不一致。
6.4 我实际踩过的两次比较典型的坑
第一次是处理一个老报表,表结构里字段全是utf8mb4_general_ci,唯独一张维度表用了默认的utf8mb4_0900_ai_ci(数据库是从8.0迁移到5.7的,迁移工具把排序规则换了一部分)。当时我查了两三次都没发现,因为老报表涉及的字段实在太多。后来靠第6.2步把两张表的字段全部拉出来,写了个脚本对比排序规则差异,才找到问题。
第二次是存储过程的坑。某个存储过程里用了一个局部变量DECLARE v_status VARCHAR(20),没显式指定排序规则。它在存储过程中与一张utf8mb4_general_ci的表字段比较没问题,后来我把存储过程升级,在内部拼接了几个字符串,又把结果和另一张utf8mb4_unicode_ci的表字段比较,1267就出现了。当时也是排查了半天,最后发现局部变量在拼接字符串后排序规则变成了utf8mb4_unicode_ci,这才炸的锅。
6.5 批量修改表的推荐流程
如果确认要全库统一排序规则,不建议手写ALTER语句逐表执行,太容易漏。我推荐用动态SQL生成的方式:
SELECT CONCAT( 'ALTER TABLE `', TABLE_NAME, '` ', 'CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;' ) AS alter_statement FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'your_db' AND TABLE_COLLATION != 'utf8mb4_unicode_ci';CONVERT TO CHARACTER SET会同时改掉表中所有字符串字段的字符集和排序规则,并且不会动到字段的其他属性,比逐字段MODIFY要稳妥高效很多。但同样要提醒:大表执行前务必评估锁表影响,尽量安排在维护窗口。
我当年在线上库执行这个语句时就吃过亏。一张接近千万行的日志表,直接CONVERT TO跑了将近半个小时,期间该表不可写。从那以后我再也不在业务高峰期做这种操作,都是先改从库、再主从切换,或者用在线DDL工具来处理。
7. 从工程规范上彻底消灭1267
前面讲的都是“出了问题怎么修”,但真正成熟的团队应该把1267这类问题扼杀在源头。根据我这些年带项目的经验,工程规范做的好的团队,一个月也遇不到几次1267;规范混乱的团队,几乎每周都要被它折腾一轮。
我建议从以下几个维度入手:
7.1 建表规范里强制指定字符集和排序规则
不要让建表语句依赖数据库默认值。每个建表语句都显式写明:
CREATE TABLE t_example ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(128) NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='示例表';很多1267问题的根源,其实是有人图省事,建表时没写COLLATE,用了服务器默认值。后来服务器配置一升级,新表和老表排序规则就不一致了。这个教训我在不止一个项目里见过。
7.2 代码评审时把排序规则纳入检查项
每次代码评审时,凡是涉及JOIN、子查询、字符串函数的地方,都要抽查一下参与字段的排序规则是否一致。这不需要特别深厚的数据库知识,只要会用information_schema查就行。关键是团队要有这个意识,而不是等到上线报错再来补课。
7.3 连接串显式指定字符集和排序规则
使用连接池时,JDBC URL或Python连接参数里显式写明:
jdbc:mysql://localhost:3306/your_db?characterEncoding=utf8mb4&connectionCollation=utf8mb4_unicode_ci这样做的好处是,无论数据库默认配置怎么变,应用侧始终固定用一种排序规则连接,避免“应用和数据库默认值不一致导致的隐式转换问题”。
7.4 定期巡检
可以用一条SQL定期巡检整个实例下排序规则异常的表:
SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_COLLATION NOT LIKE 'utf8mb4%';把结果接上监控告警,一旦发现非utf8mb4系列的新表出现,立刻提示整改。这比等用户投诉报错要主动得多。
回到文章开头那个案例。我后来怎么处理的呢?先用第4.3节的COLLATE方案把线上查询恢复了,当天报表服务不再报警;然后在下一个维护窗口,把t_order表的排序规则统一到utf8mb4_unicode_ci,顺便把同批次其他有隐患的表也做了一遍CONVERT TO。之后再也没出现过1267。
这次排查让我养成了一个习惯:凡是多表关联的SQL,先查一遍参与字段的排序规则,再写JOIN条件。这种习惯看起来慢,但能省掉很多查询报错和半夜起来的紧急修复。如果你也经常和MySQL打交道,不妨把上面第6节的排查命令存下来,下次遇到1267直接套用。踩过这个坑,以后就不会再被它绊倒了。