MySQL中字符串的MAX()/MIN()按字典序逐字符比较Unicode码点,首字符不同时直接决定结果;存数字字符串时需显式转换(如CAST或乘1)或重构字段结构,不可依赖默认排序规则。

这个话题,看起来基础,但踩坑的人绝对不少。字符串的比较,靠的不是长度,也不是你脑子里理解的“语义”,而是每一位字符的Unicode码点。这事在MySQL里尤其关键,也是不少人在日常开发中栽过的地方。
字符串比较按字典序逐字符进行,不是看长度或语义
MAX() 和 MIN() 对 CHAR、VARCHAR 类型字段求极值时,底层的逻辑就是逐字符比对 Unicode(或 ASCII)码点。首字符不同,结果直接定;首字符一样,才接着比第二位,以此类推。举个例子,MIN('v1.2', 'v1.10', 'v2.1') 返回的结果是 'v1.10',而不是很多人直觉以为的 'v1.2'。原因很简单:'1' 的 ASCII 码(49)小于 '2'(50),所以 'v1.10' “小于” 'v2.1'。这是字符串比较的必然行为,不是bug。
这里有几个常见的误判场景,值得留意:
- 把版本号、IP 地址、编号类字符串直接丢给
MAX(),结果往往和业务预期南辕北辙。 - 以为中文会按笔画、部首或常用度来排序,但实际上默认走的是拼音首字母的Unicode码点。比如
'李四'会“小于”'张三',因为'李'的 Unicode 码点 U+674E 小于'张'的 U+5F20。 - 忽略 collation 设置的影响。MySQL 里
utf8mb4_0900_as_cs区分大小写且严格按码点比较,而utf8mb4_general_ci则忽略大小写,做更宽松的匹配。同一个查询,在不同 collation 下结果可能完全不同。
所以,碰到字符串范围统计,第一反应不是写 SQL,而是先搞清楚字段的 collation 和实际存储的是什么。
GROUP BY 分组统计字符串范围时,NULL 和全 NULL 要分开判断
执行类似 SELECT category, MIN(name), MAX(name) FROM products GROUP BY category 这样的查询时,MIN() 和 MAX() 会跳过每组内的 NULL 值。但关键问题是,如果某组的所有 name 都是 NULL,结果就会返回两个 NULL,既不会报错,也不会给你留个空字符串。
这种“静默”行为很容易掩盖数据质量问题。比如“图书”分类下本来应该有书名,结果录入时全丢了,报表里只显示一片空白,你可能会误以为这个分类下没有数据,实际上问题出在数据完整性上。这其实是很多人容易忽略的陷阱。
一个实用的补救方法是,在查询中加上 COUNT(name) 作为辅助判断:
COUNT(name) = 0意味着该组无有效字符串值,数据极可能全部缺失。COUNT(name) > 0但MIN(name)仍为NULL的情况不会发生,因为COUNT()已经过滤掉了NULL。- 如果想用默认值填充,可以用
COALESCE(name, '[unknown]'),但要注意这样会污染真实范围,因为'[unknown]'这个字符串的字典序,很可能比所有合法值都小,从而影响后续的业务逻辑判断。
想按数值逻辑比较字符串?必须显式转换或重构存储结构
当字段里存的其实是数字字符串(比如 '123'、'45'),或者带前缀的编号(比如 'ID001'、'ID100'),直接上 MAX() 按字典序返回的结果大概率是错的。这时候,改 collation 解决不了根本问题,必须动真格的。
业内常用的三条路径:
- 运行时转换:在 MySQL 里用
MAX(score * 1)或者MAX(CAST(score AS SIGNED));在 PostgreSQL 里则是MAX(score::integer)。简单粗暴,适合临时应急。 - 标准化前缀:如果编号有固定前缀,就提取纯数字部分再转换。比如
MAX(CAST(SUBSTRING(code, 3) AS UNSIGNED))来处理'ID001'这种格式。但这只适用于前缀长度固定的情况。 - 根本性修复:把版本号拆成
major、minor、patch三列,存成整数;或者把 IP 地址用INET_ATON()转换成无符号整数来存储。这才是对长远维护最友好的方案。
别指望数据库能自动“理解”你的业务意图——它只认字节和 collation,不要对它有不切实际的幻想。
不同数据库对中文排序的实际表现差异很大
这个问题在跨数据库场景下尤其突出。MySQL 默认按 collation 决定中文比较方式:utf8mb4_unicode_ci 走的是 Unicode 码点,相当于拼音序;utf8mb4_zh_0900_as_cs(MySQL 8.0+ 支持)才能按真正的中文字典序来比较。SQL Server 的情况是,如果你用 Chinese_PRC_CI_AS,它会按《GB2312》字典序来排,否则默认走拼音。而 PostgreSQL 完全依赖系统级的 LC_COLLATE,建库时定死后运行时就无法切换,灵活性最差。
这意味着,同样的 SQL,在开发环境、测试环境和生产环境,甚至在不同数据库之间,可能返回完全不同的 MIN() 结果。最稳妥的应对思路是:只要涉及中文排序逻辑,就不要依赖数据库的默认行为。要么在应用层做标准化(比如预存一个拼音字段 name_pinyin),要么在 SQL 层显式调用转换函数(像 MySQL 里的 CONVERT(name USING gbk) 配合指定 collation)。
说到底,真正的麻烦不在于 SQL 语法怎么写,而在于你清不清楚当前数据库到底在用哪一套规则在做比较。所以,查一下 SHOW CREATE TABLE 和 SELECT COLLATION(column_name),不是可选项,而是第一步。
