字符集管「怎么存」,校对规则管「怎么比」,时区管「现在几点」。这三件事平时不出问题,
一跨语言、跨时区就全暴露。
Demo 里 'a' < 'B' 默认给 0、加 COLLATE NOCASE 给 1;
四个汉字是 4 个字符、12 个字节;
date('now') 给 10-29 而本地时间已是 10-30——UTC 与本地差了一天。
前面所有章节都在「英文数据 + 单一时区」的假设下工作,这一章把这个假设拆掉。 它是全书唯一一章「不改逻辑、只改配置」的内容, 但配置错了,中文会变成问号,时间会错一天。
一、字符集:库里到底用什么编码存字
字符集决定「一个字怎么变成字节」。 SQLite 只有一种:UTF-8,
所以它没有 SET NAMES 这种设置。
MySQL 有三层:库、表、列各有一个字符集,再加上连接自己的字符集。
四层里有任何一层不一致,中文就会变成问号。
老版本 MySQL 的 utf8 只有三字节,放不下 emoji,
要存就得用 utf8mb4(真正的四字节 UTF-8)。 这是本课唯一必须记住的 MySQL 常识。
二、校对顺序:怎么比大小、怎么排序
校对规则(collation)决定两件事:两个字符串谁大、排序时谁在前。
它和字符集是两个概念:一个管存,一个管比。
默认按字节比:小写 a 是 97、大写 B 是 66,
所以 'a' < 'B' 是假。 加上 COLLATE NOCASE 忽略大小写,a 就小于 B 了。
同一个查询,加不加 COLLATE 结果可能完全不同:prod_name = 'carrots' 给 0 行,
... COLLATE NOCASE 给 1 行——因为数据里存的是 Carrots。
MySQL 里那一串后缀(如 utf8mb4_0900_ai_ci):ai 是重音不敏感、ci 是大小写不敏感、
bin 是纯字节比较。知道这三个词就够用了。
三、中文:字符数与字节数不是一回事
UTF-8 里一个汉字占三字节,所以「四个汉字」是 4 个字符、12 个字节。
length() 数字符,转成 BLOB 才数字节。
MySQL 的两个函数名字正好反过来:CHAR_LENGTH() 数字符、LENGTH() 数字节。
从 MySQL 转过来最容易在这一处犯错。
为什么在意这个:VARCHAR(50) 是按字符算的,
但索引长度、网络包大小、显示宽度都跟字节有关。 算不准就会出现「明明只有十个字却存不下」。
四、日期与时区:两个 now 差一天
'now' 给的是 UTC 时间,'localtime' 才换成本地时间。
Demo 里这台机器本地已是 30 日凌晨,UTC 还停在 29 日。
这是全球化里最实际的坑:服务器在 UTC、用户在 +8,
如果直接把 UTC 当本地时间显示,用户看到的日期会差一天。
稳的做法只有一条:存的时候一律存 UTC,显示的时候再换时区。
千万别把「本地时间字符串」当时间存——用户一跨时区,排序和比较全乱。
日期格式可以做本地化:strftime('%Y年%m月%d日', ...) 就是中文写法。
但那是「显示」,存储永远用 ISO 那种标准格式。
五、三条实操建议
一,全链路统一 UTF-8(MySQL 用 utf8mb4):库、表、连接、应用都统一, 只要有一层不一致,中文就会变成问号。 二,时间一律存 UTC,显示时再换时区。这一条能省掉后面所有时区 bug。 三,排序与比较显式写 COLLATE,别赌默认值——默认值在不同版本、不同库里都可能不一样。
六、小结
教材小结:
字符集管存储、校对管比较,两者都要在库、表、列、连接上保持一致;日期要按不同地区的格式显示。
本课补三条:一是中文一个字符三字节,字符数与字节数是两回事;
二是 'now' 是 UTC,别当本地时间用;三是排序比较要显式写 COLLATE。
七、动手 Demo:大小写、汉字、时区,三处真坑
Demo 六段:先看库的编码,再比大小写(同一个比较两个答案),然后数汉字(4 个字符 12 个字节), 最后看 UTC 与本地时间差一天。 每一段都是「同一个东西,两种看法」——这正是本地化问题的本质。
import sqlite3
con = sqlite3.connect(":memory:")
con.execute("PRAGMA encoding") # UTF-8
# 校对顺序:怎么比大小、怎么排序
con.execute("SELECT 'a' < 'B' AS binary_order, "
"'a' < 'B' COLLATE NOCASE AS nocase_order")
con.execute("SELECT COUNT(*) FROM products WHERE prod_name = 'carrots'")
con.execute("SELECT COUNT(*) FROM products "
"WHERE prod_name = 'carrots' COLLATE NOCASE")
# 字符数与字节数:UTF-8 里一个汉字三字节
con.execute("SELECT length('中文测试') AS chars, "
"length(CAST('中文测试' AS BLOB)) AS bytes")
# 时区:'now' 是 UTC,'localtime' 才是本地
con.execute("SELECT date('now') AS utc_date, "
"strftime('%Y-%m-%d %H:%M', 'now', 'localtime') AS local_now")
# 显示时本地化,存放时用 ISO
con.execute("SELECT order_date, strftime('%Y年%m月%d日', order_date) AS cn "
"FROM orders WHERE order_num = 20005")
实跑输出:
--------------------------------------------------------------------
第二十七课:全球化与本地化
--------------------------------------------------------------------
【一】字符集:库里到底用什么编码存字
SQL> PRAGMA encoding
encoding
---------
UTF-8
(1 行)
**SQLite 的库默认 UTF-8**,从头到尾只有这一种编码,所以它没有什么 `SET NAMES` 之类的设置。
**MySQL 就复杂得多**:库、表、列各有一层字符集,
**老版本默认 utf8(只存三字节,放不下 emoji),要存就得用 utf8mb4。**
**这是本课唯一必须记住的 MySQL 常识:需要 emoji 或生僻字,就用 utf8mb4。**
【二】校对顺序:a 和 B 谁大
SQL> SELECT 'a' < 'B' AS binary_order, 'a' < 'B' COLLATE NOCASE AS nocase_order
binary_order nocase_order
------------------------------
0 1
(1 行)
**同一个比较,两种答案。** 默认按字节比:小写 a 是 97、大写 B 是 66,
**所以 a 大于 B,结果是 0(假)。**
**加上 `COLLATE NOCASE` 就忽略大小写**,a 与 B 按字母序比,**a 小于 B,结果是 1(真)。**
**「校对顺序」(collation)决定两件事:怎么比大小、怎么排序。** **它和字符集是两个概念**:
字符集管「怎么存」,校对管「怎么比」。
SQL> SELECT COUNT(*) AS n_bin FROM products WHERE prod_name = 'carrots'
n_bin
------
0
(1 行)
SQL> SELECT COUNT(*) AS n_nocase FROM products WHERE prod_name = 'carrots' COLLATE NOCASE
n_nocase
---------
1
(1 行)
**不加 COLLATE 给 0,加了给 1。** 数据里是 `Carrots`,大写 C。
**这个差别在「用户输入大小写不定」的场景里非常要命。**
【三】中文:字符数与字节数不是一回事
SQL> SELECT length('中文测试') AS chars, length(CAST('中文测试' AS BLOB)) AS bytes
chars bytes
----------------
4 12
(1 行)
**四个汉字,12 个字节**——UTF-8 里一个汉字占三字节。 **`length()` 数字符,转成二进制才数字节。**
**MySQL 里对应两个函数**:`CHAR_LENGTH()` 数字符、`LENGTH()` 数字节。
**名字正好反过来,这是从 MySQL 转过来最容易错的一处。**
SQL> SELECT prod_name, length(prod_name) AS n FROM products WHERE prod_id IN ('ANV01',
'JP2000')
prod_name n
---------------------------
.5 ton anvil 12
JetPack 2000 12
(2 行)
**`.5 ton anvil` 与 `JetPack 2000` 都是 12 个字符**,长度完全相同。
**但换成中文,12 个字要占 24 格显示宽度**——**字符数与显示宽度是两回事。**
【四】日期与时区:两个 now 差一天
SQL> SELECT date('now') AS utc_date, strftime('%Y-%m-%d %H:%M', 'now',
'localtime') AS local_now
utc_date local_now
--------------------------------
2026-10-29 2026-10-30 00:23
(1 行)
**`'now'` 给的是 UTC,`'localtime'` 才换成本地时间。** 这台机器现在是凌晨 0 点多,
UTC 还停在前一天——**两个值就差了一天。**
**这是全球化里最实际的坑**:服务器在 UTC、用户在 +8,**存的时候用 UTC、显示的时候换时区**,
是唯一稳的做法。
**别把「本地时间字符串」当时间存**——用户一跨时区,排序和比较全乱。
SQL> SELECT order_date, strftime('%Y年%m月%d日',
order_date) AS cn FROM orders WHERE order_num = 20005
order_date cn
------------------------------
2005-09-01 2005年09月01日
(1 行)
**日期格式能按语言拼**,`%Y年%m月%d日` 就是中文写法。
**但存的时候一定存标准格式(ISO 那种),显示才做本地化。**
【五】MySQL 侧的对照写法(原码,不执行)
`SHOW VARIABLES LIKE 'character%';` 看各层字符集
`SET NAMES utf8mb4;` 告诉服务端「我这条连接用 utf8mb4」
`CREATE TABLE t (name VARCHAR(50)) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;`
`SELECT ... ORDER BY name COLLATE utf8mb4_bin;` 按字节序排
**`ai_ci` 是「重音不敏感、大小写不敏感」的缩写**,`bin` 是纯字节比较。
**这一串后缀是本课最像「密码」的东西,看一眼知道是什么就行。**
【六】三条实操建议
**一,全链路统一 UTF-8(MySQL 用 utf8mb4)**:库、表、连接、应用都统一,
**只要有一层不一致,中文就会变成问号。**
**二,时间一律存 UTC**,显示时再换时区;**别存本地时间字符串。**
**三,排序和比较要显式写 COLLATE**,别赌默认值——**默认值在不同版本、不同库里都可能不一样。**
第一段编码。 只有 UTF-8 一种,所以 SQLite 没有 SET NAMES。
第二段大小写。 默认 0(a 大于 B),加 NOCASE 才 1。
第三段同一列两种比法。 不加 COLLATE 给 0 行,加了给 1 行——数据里是大写 Carrots。
第四段数汉字。 4 与 12,字符与字节。
第五段时区。 UTC 是 10-29,本地已是 10-30,整整差一天。
第六段本地化显示。 日期拼成 2005年09月01日,但存的仍是 2005-09-01。
30 秒实验
- 把
COLLATE NOCASE换成COLLATE BINARY,'a' < 'B'结果是多少? - 用
length('🙂')数一个 emoji,是 1 还是 2?为什么? - (先想再跑)
strftime('%s', 'now')给的是秒数,它受时区影响吗?
常见误区
- 把字符集和校对规则当一回事。前者管怎么存,后者管怎么比,两个都要设。
- MySQL 用 utf8 存 emoji。三字节放不下,必须 utf8mb4。
- 把 UTC 当本地时间显示。跨时区时日期会差一天,用户直接看到错的。
自测三问(点开对答案)
1. 字符集和校对规则有什么区别? —— 字符集决定一个字怎么存成字节;校对规则决定两个字符串怎么比大小、怎么排序。 2. 为什么必须用 utf8mb4 而不是 utf8? —— 老版 utf8 最多三字节,放不下四字节的 emoji 与部分生僻字;utf8mb4 才是完整 UTF-8。 3. 时间为什么建议一律存 UTC? —— 本地时间会随用户时区变化,同一时刻在不同地区字符串不同,排序和比较都会错; 存 UTC、显示时转换才稳。写到这一章, 数据的「内容」和「地区差异」都覆盖了。 剩下三章讲「谁来管、怎么修、怎么快」——安全、维护、性能。
- ☐ (本章完,下一章 第28章 安全管理)