2团
Published on 2026-05-06 / 2 Visits
0
0

SQL中的疑似Bug:utf8mb4_unicode_ci导致所有Emoji被GROUP BY视为相同

摘要:近期在TiDB Cloud中实现需求,发现utf8mb4_unicode_ci排序规则将所有emoji视作等价字符,导致GROUP BY mood把13条不同 emoji的记录全部折叠成一行,心情统计结果完全错误。本文记录了该问题的复现过程、实机验证数据以及修复方案。


1. 问题现象

最近在开发一个应用时,需要根据用户在某时间段内设置的不同Emoji表情进行GROUP BY统计。但在实现过程中遇到了一个诡异问题:GROUP BY竟将截然不同的emoji表情(如😊/😴/😢/🤩等)全聚合成了同一种心情。

例如,某用户2026年4月共有13条记录,实际分布如下:

mood

总和

😴

4

😊

2

🤩

2

😤

1

😢

1

😰

1

😌

1

😔

1

但API却只返回一行数据(返回内容为第一条记录的emoji表情,计数却是所有表情次数的总和)。

2. 原因分析

2.1 建表时列的排序规则继承了utf8mb4_unicode_ci

当前mood_records表的建表语句如下:

CREATE TABLE IF NOT EXISTS mood_records (
    id      BIGINT PRIMARY KEY AUTO_RANDOM,
    user_id BIGINT NOT NULL,
    date    DATE NOT NULL,
    mood    VARCHAR(32) NOT NULL,          -- 没有显式声明 COLLATE
    ...
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

mood列没有显式指定COLLATE,因此继承了表级的默认值utf8mb4_unicode_ci。这正是Bug的直接根源——没有正确设置该字段的排序规则。

2.2 utf8mb4_unicode_ci对emoji排序的影响

utf8mb4_unicode_ci基于Unicode标准的排序算法(DUCET)。在此算法中,许多emoji被分配了完全相同的 Unicode Collation Element(排序元素),导致数据库认为它们彼此“相等”。当执行GROUP BY mood时,所有排序权重相同的 emoji会被归入同一分组,并以该分组第一个读取到的emoji表情作为返回值。

3. 实机验证

在 TiDB Cloud上创建了两个临时测试表,分别插入相同的13行数据(包含8种不同的emoji)来复现问题:

-- 测试表 1:使用 utf8mb4_unicode_ci(Bug 状态)
CREATE TABLE _test_emoji_unicode_ci (
    id   INT AUTO_INCREMENT PRIMARY KEY,
    mood VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL
) ENGINE=InnoDB;

-- 测试表 2:使用 utf8mb4_bin(修复状态)
CREATE TABLE _test_emoji_bin (
    id   INT AUTO_INCREMENT PRIMARY KEY,
    mood VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL
) ENGINE=InnoDB;

-- 插入相同的 13 行数据(与生产环境 Bug 现场完全一致)
INSERT INTO _test_emoji_unicode_ci (mood) VALUES
    ('😴'),('😴'),('😴'),('😴'),
    ('😊'),('😊'),
    ('🤩'),('🤩'),
    ('😤'),('😢'),('😰'),('😌'),('😔');
-- _test_emoji_bin 表插入相同数据(略)

通过SHOW CREATE TABLE可以验证各列的最终排序规则:

-- _test_emoji_unicode_ci
CREATE TABLE `_test_emoji_unicode_ci` (
  `id`   int NOT NULL AUTO_INCREMENT,
  `mood` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL,  -- 显式覆盖了表默认值
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

-- _test_emoji_bin(mood 列无显式 COLLATE,继承表默认 utf8mb4_bin)
CREATE TABLE `_test_emoji_bin` (
  `id`   int NOT NULL AUTO_INCREMENT,
  `mood` varchar(32) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

3.1 直接比较:emoji在unicode_ci下等价

SELECT '😴' = '😊' COLLATE utf8mb4_unicode_ci;  -- 返回 1(相等!)
SELECT '😴' = '😊' COLLATE utf8mb4_bin;          -- 返回 0(不相等)

进一步验证:上述8种 emoji(😴/😊/🤩/😤/😢/😰/😌/😔)共28对组合,在utf8mb4_unicode_ci全部两两相等

3.2 GROUP BY行为对比

有Bug(对unicode_ci 表进行GROUP BY):

SELECT mood, COUNT(*) AS cnt
FROM _test_emoji_unicode_ci
GROUP BY mood
ORDER BY cnt DESC;
mood  | cnt
------+----
😴    | 13     ← 13条记录全部折叠为一行,代表值为插入顺序中的第一个emoji

修复尝试1:在GROUP BY时强制指定 COLLATE(失败):

SELECT mood, COUNT(*) AS cnt
FROM _test_emoji_unicode_ci
GROUP BY mood COLLATE utf8mb4_bin
ORDER BY cnt DESC;
ERROR 1055: Expression #1 of SELECT list is not in GROUP BY clause
and contains nonaggregated column '...mood' which is not functionally
dependent on columns in GROUP BY clause;
this is incompatible with sql_mode=only_full_group_by

修复尝试2:ANY_VALUE + COLLATE utf8mb4_bin(成功):

SELECT ANY_VALUE(mood) AS mood, COUNT(*) AS cnt
FROM _test_emoji_unicode_ci
GROUP BY mood COLLATE utf8mb4_bin
ORDER BY cnt DESC;
mood  | cnt
------+----
😴    | 4
🤩    | 2
😊    | 2
😰    | 1
😤    | 1
😢    | 1
😔    | 1
😌    | 1

ANY_VALUE用于抑制ONLY_FULL_GROUP_BY模式下的报错,可从每个分组中返回一个不确定的值。
MySQL 5.7正式支持ANY_VALUE,TiDB在设计中兼容MySQL,因此也继承了该函数。

迁移后(utf8mb4_bin表,普通GROUP BY):

ALTER TABLE mood_records
    MODIFY COLUMN mood VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL;

SELECT mood, COUNT(*) AS cnt
FROM _test_emoji_bin
GROUP BY mood
ORDER BY cnt DESC;
mood  | cnt
------+----
😴    | 4
😊    | 2
🤩    | 2
😤    | 1
😢    | 1
😰    | 1
😔    | 1
😌    | 1

mood列的排序规则从继承的utf8mb4_unicode_ci改为显式指定utf8mb4_bin,可从根本上解决问题,无需使用ANY_VALUE,结果完全正确。

5. 经验教训

  1. 列的排序规则必须显式声明:不要依赖表级默认值的继承。对于emoji或符号类字段,应在列定义中明确写上 COLLATE utf8mb4_bin

  2. utf8mb4_unicode_ci 不适合将emoji作为标识符的场景:DUCET权重会将所有普通emoji视为等价,只有utf8mb4_bin(按字节逐一比较)才能正确区分不同的emoji。


Comment