← 行间 技术 / TiDB
技术

TiDB系列(II)-一次字符串比较发现的 TiDB 排序规则“时空裂缝”

一、问题是如何被发现的

事情的起点并不复杂, 用户反馈在两个 TiDB 集群上执行了同一条 INSERT:

INSERT INTO job_status
(process_time, name, message, run_id, status, create_time)
VALUES
('2025-09-17 13:21:55', 'test', '', '2025-09-17', 'START ', '2025-09-17 13:21:55');

# 对应的表结构如下:
CREATE TABLE job_status (
  process_time datetime NOT NULL,
  name varchar(255) NOT NULL,
  message text DEFAULT NULL,
  run_id varchar(255) DEFAULT NULL,
  status enum('START','IN_PROGRESS','SUCCESS','FAILURE') NOT NULL,
  create_time datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (process_time,name,status) /*T![clustered_index] NONCLUSTERED */
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
PARTITION BY RANGE (MONTH(process_time))
(PARTITION JAN VALUES LESS THAN (2),
 PARTITION FEB VALUES LESS THAN (3),
 PARTITION MAR VALUES LESS THAN (4),
 PARTITION APR VALUES LESS THAN (5),
 PARTITION MAY VALUES LESS THAN (6),
 PARTITION JUN VALUES LESS THAN (7),
 PARTITION JUL VALUES LESS THAN (8),
 PARTITION AUG VALUES LESS THAN (9),
 PARTITION SEP VALUES LESS THAN (10),
 PARTITION OCT VALUES LESS THAN (11),
 PARTITION NOV VALUES LESS THAN (12),
 PARTITION DEC VALUES LESS THAN (MAXVALUE));

然而得到的结果却完全不同:

  • 新部署的 TiDB 8.5.0 集群:插入成功
  • 由老版本升级到 8.1.1 的集群:报错 Data truncated for column 'status'

这让我非常困惑:

  • sql_mode 完全一致(严格模式)
  • 表结构一致
  • 字符集和排序规则一致(utf8mb4_bin)

为什么仅仅因为多了一个尾随空格,行为会完全不同?

二、问题的根源:TiDB 4.0 前后的排序规则断层

答案最终指向了一段 TiDB v8.5.0 官方文档中看似不起眼的一句话:

在 v4.0 之前,TiDB 仅提供旧排序规则框架。在此框架下,TiDB 支持大部分 MySQL 排序规则的语法解析,但语义上将所有排序规则视为二进制排序。

这句话的含义,其实非常“重”。

1. 什么叫“支持语法解析,但语义上视为二进制排序”?

在 TiDB v4.0 之前:

  • 你可以在表定义中写:VARCHAR(20) COLLATE utf8mb4_general_ci
  • 元数据里也会显示正确的排序规则

但在真正执行比较时:

  • 不区分大小写
  • 不忽略尾随空格
  • 不做任何 collation 语义
  • 一律按字节(binary)比较

换句话说: 排序规则在旧框架中是“装饰性的”,并不参与执行逻辑。

2. TiDB 4.0 之后发生了什么?

从 TiDB 4.0 开始,引入了新的排序规则框架: 真正实现了 MySQL 8 的 collation 语义, 支持:

  • 大小写不敏感
  • 重音不敏感
  • PAD SPACE / NO PAD
  • utf8mb4_bin 开始真的忽略尾随空格

于是:

SELECT 'START ' = 'START' COLLATE utf8mb4_bin;
  • 旧框架集群:FALSE(binary 行为)
  • 新框架集群:TRUE(PAD SPACE 生效)

3. 为什么升级的老集群没有“自动切换”?

这是 TiDB 非常克制、也非常工程化的一点:

是否启用新排序规则框架,是在集群初始化时决定的,升级不会自动切换。

原因很现实: 切换排序规则语义 = 语义级破坏性变更,会影响:

  • = / <>
  • UNIQUE KEY
  • JOIN
  • GROUP BY
  • ENUM 匹配

一旦自动切换,已有数据可能立刻变成“逻辑错误”。

于是 TiDB 选择了:

  • 新建集群:启用新框架
  • 老集群升级:保持旧语义,保证行为不变

这也正是我遇到“同 SQL 不同行为”的根本原因。

三、一个有意思的延伸:工程世界里的“时间裂缝”

这个问题让我意识到一个以前很少被认真对待的事实:

数据库的“语义”,是有时间维度的。

在 TiDB 中:

  • 两个版本号相同的集群
  • 两个 schema 完全一致的表
  • 甚至两个相同的 SQL

可能因为“历史初始化时间不同”,而生活在不同的语义宇宙里。

这不是 TiDB 的疏忽,而是一种取舍:

  • 自动修复语义 → 高风险
  • 保持历史行为 → 高复杂度

TiDB 选择了后者。

当我们谈“兼容 MySQL”时,兼容的不只是语法,还有语义,甚至历史。

如果你在 TiDB 中遇到“说不清道不明的字符串行为差异”, 不妨问自己一句:

这个集群,是不是来自另一个时代?