🐬 第27章 全球化与本地化
📄 一句话概括

字符集管「怎么存」,校对规则管「怎么比」,时区管「现在几点」。这三件事平时不出问题, 一跨语言、跨时区就全暴露。 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 秒实验

  1. 把 COLLATE NOCASE 换成 COLLATE BINARY,'a' < 'B' 结果是多少?
  2. 用 length('🙂') 数一个 emoji,是 1 还是 2?为什么?
  3. (先想再跑)strftime('%s', 'now') 给的是秒数,它受时区影响吗?

常见误区

  1. 把字符集和校对规则当一回事。前者管怎么存,后者管怎么比,两个都要设。
  2. MySQL 用 utf8 存 emoji。三字节放不下,必须 utf8mb4。
  3. 把 UTC 当本地时间显示。跨时区时日期会差一天,用户直接看到错的。
自测三问(点开对答案) 1. 字符集和校对规则有什么区别? —— 字符集决定一个字怎么存成字节;校对规则决定两个字符串怎么比大小、怎么排序。 2. 为什么必须用 utf8mb4 而不是 utf8? —— 老版 utf8 最多三字节,放不下四字节的 emoji 与部分生僻字;utf8mb4 才是完整 UTF-8。 3. 时间为什么建议一律存 UTC? —— 本地时间会随用户时区变化,同一时刻在不同地区字符串不同,排序和比较都会错; 存 UTC、显示时转换才稳。

写到这一章, 数据的「内容」和「地区差异」都覆盖了。 剩下三章讲「谁来管、怎么修、怎么快」——安全、维护、性能。

相关笔记