金仓数据库字符集与国际化支持:多语言环境下的编码处理方案
前言做过国际化项目的开发者大概都有这样的经历——系统上线后用户反馈数据全变成问号了。字符编码这东西平时没人关心可一旦出问题影响的是整条数据链路。轻则页面乱码重则排序错乱、数据截断甚至索引直接失效。这篇文章就来聊聊金仓数据库在字符集与国际化方面的完整技术方案。从选哪种编码到怎么配排序规则再到客户端和服务器编码不一致怎么办把实际开发中容易踩的坑一次性讲清楚。一、先搞清楚几个基本概念1.1 字符集和编码——它们不是一回事简单说字符集是一张字符清单规定了这个集合里有哪些符号编码则是把这些符号变成计算机能存储的字节序列的规则。一个字符集可以对应好几种编码方式。拿 Unicode 来说它就有 UTF-8、UTF-16 两种常见编码。金仓数据库能处理两大类字符集单字节字符集一个字符占 1 个字节。像 LATIN1 ~ LATIN10 这类 ISO 8859 系列主要服务西欧、中欧等语言环境简单高效。多字节字符集一个字符占 1~4 个字节。UTF-8、GBK、GB18030、EUC 系列都属于这一类专门应对中日韩等字多的语言。在金仓的文档和日常交流中字符集和编码经常混着用指的其实是同一件事。1.2 字符集该怎么选字符集在数据库初始化initdb的时候就定下来了后面所有新建的数据库都会继承这个默认值。选的时候想清楚四件事就够了你的数据要说几种语言只有中文和英文GBK 就够用。要是还得塞进日文、阿拉伯文、俄文……那 UTF-8 是唯一靠谱的选择。存储空间敏感吗UTF-8 存一个中文字符占 3 字节GBK 只要 2 字节。纯中文环境下 GBK 确实省空间。对接的系统和工具多不多UTF-8 几乎是万能钥匙所有主流编程语言和中间件都原生支持省心。排序和区域有没有特殊要求字符集得跟数据库的LC_CTYPE字符分类和LC_COLLATE排序规则兼容否则后患无穷。一句话建议没有特殊约束就选 UTF-8一劳永逸。二、手把手配置字符集2.1 初始化时把基调定好用initdb初始化数据库集簇时加个-E参数就能指定默认字符集# 指定 UTF-8 作为默认编码 initdb -D /data/kingbase -E UTF8 # 也可以写完整选项名 initdb -D /data/kingbase --encodingUTF8 # 同时指定中文区域让排序和分类也一步到位 initdb -D /data/kingbase -E UTF8 --localezh_CN.UTF-8如果什么都不指定initdb会去读操作系统的区域设置自动猜一个编码。比如系统区域是zh_CN.UTF-8它就会默认用UTF8。大多数情况下这个自动猜是靠谱的但生产环境建议显式指定别给未来埋雷。2.2 单个数据库也可以另起炉灶有时候整个集簇用 UTF-8但某个数据库需要用别的编码这没问题。关键是两条编码得和区域设置兼容并且必须从template0模板创建。-- 创建一个韩文数据库编码用 EUC_KR CREATE DATABASE korean WITH ENCODING EUC_KR LC_COLLATEko_KR.euckr LC_CTYPEko_KR.euckr TEMPLATEtemplate0;命令行也一样createdb -E EUC_KR -T template0 \ --lc-collateko_KR.euckr \ --lc-ctypeko_KR.euckr korean一条铁律如果模板不是template0编码和区域就不能改。强行改的话数据很可能就毁了。2.3 怎么查看当前编码进了数据库随时可以查-- 方法一ksql 里的快捷命令 \l -- 方法二直接查系统表 SELECT datname, pg_encoding_to_char(encoding) AS encoding, datcollate, datctype FROM pg_database; -- 方法三查看当前数据库编码 SHOW server_encoding;输出大概是这个样子Name | Encoding | Collate | Ctype -------------------------------------------- kingbase | UTF8 | zh_CN.UTF8 | zh_CN.UTF8 korean | EUC_KR | ko_KR.euckr| ko_KR.euckr prod | UTF8 | en_US.UTF8 | en_US.UTF8三、区域Locale——不只是语言那么简单3.1 区域到底管什么很多人以为区域就是选个语言其实远不止于此。区域决定了数据库对文化习惯的方方面面怎么响应——字符串怎么排、大小写怎么转、货币符号长什么样、日期格式用哪一套。金仓数据库把区域拆成了六个类别类别管什么LC_COLLATE字符串怎么排序LC_CTYPE哪些算大写、哪些算小写、哪些是数字LC_MESSAGES系统报错和提示用什么语言LC_MONETARY货币怎么显示LC_NUMERIC小数点用逗号还是句号LC_TIME日期格式怎么写重点提醒LC_COLLATE和LC_CTYPE这两个在数据库创建时就锁死了创建之后改不了。原因是它们直接决定了索引的排序方式中途修改的话索引会直接废掉。其他几个类别倒是可以在运行时调整。3.2 配置区域——两种时机时机一初始化集簇时# 全局设成中文区域 initdb -D /data/kingbase --localezh_CN.UTF-8 # 也可以混搭中文排序 美式货币 initdb -D /data/kingbase --localezh_CN.UTF-8 --lc-monetaryen_US.UTF-8时机二创建单个数据库时CREATE DATABASE prod TEMPLATE template0 LC_CTYPEzh_CN.UTF-8 LC_COLLATEzh_CN.UTF-8;创建之后想确认配置对不对一条命令就够SHOW lc_collate; -- 应该输出 zh_CN.UTF-8 SHOW lc_ctype; -- 应该输出 zh_CN.UTF-8 SHOW lc_messages; -- 看看提示信息用的什么语言3.3 区域会悄悄影响这些 SQL 行为改了区域之后你可能不会立刻感知到变化但下面这些操作的结果确实不一样了ORDER BY的排序结果upper()、lower()这些大小写函数的行为LIKE和正则表达式的匹配规则to_char()格式化出来的日期、数字长什么样LIKE查询能不能走索引一个性能相关的忠告如果你用了非 C/POSIX 的区域字符处理会变慢LIKE也用不上普通索引。如果业务允许可以给需要LIKE查询的列单独建一个使用 C 排序规则的索引来弥补性能。-- 给 name 列创建一个走 C 排序规则的索引加速 LIKE 查询 CREATE INDEX idx_products_name_c ON products (name COLLATE C);四、金仓数据库支持哪些字符集下面这张表覆盖了最常用的字符集够绝大多数场景使用了字符集一句话描述适用语言字节/字符支持ICUUTF8万能编码覆盖全部 Unicode所有语言1-4是GBK简体中文国家标准扩展简体中文1-2是GB18030中文国家标准含生僻字中文1-4是EUC_CN中文扩展 Unix 编码简体中文1-3是EUC_JP日文扩展 Unix 编码日文1-3是EUC_KR韩文扩展 Unix 编码韩文1-3是LATIN1西欧语言编码西欧1是4.1 一个特殊角色SQL_ASCIISQL_ASCII不是一种真正的字符集。它做的事情很简单只认 0~127 的 ASCII 字符128 以上的字节统统不管也不做任何编码转换。-- 除非你 100% 确定只有纯 ASCII 数据否则别这么干 CREATE DATABASE mydb WITH ENCODING SQL_ASCII;听起来不管不顾好像很自由实际上是个大坑。数据库既不校验也不转换非 ASCII 字符出了问题还很难排查。只有纯英文环境才适合用它其他场景一律避开。五、客户端和服务器编码不一样怎么办5.1 数据库帮你自动转金仓数据库内置了客户端/服务器之间的自动编码转换。你的数据库用 UTF-8但某个老系统只能吐 GBK 数据没关系数据库会自动完成 GBK → UTF-8 的转换查询结果也会自动从 UTF-8 转回 GBK 返回给客户端。转换规则全部记录在系统目录sys_conversion里查一查就知道当前支持哪些转换组合-- 查看系统内置的编码转换规则 SELECT conname, conforencoding, contoencoding FROM pg_conversion;5.2 告诉数据库我用的什么编码自动转换有个前提你得告诉数据库你的客户端在用什么编码。四种方式按需选择在命令行工具里临时改\encoding UTF8用 SQL 语句设置-- 方式一 SET client_encoding TO UTF8; -- 方式二标准 SQL 写法 SET NAMES UTF8; -- 查看当前客户端编码 SHOW client_encoding; -- 恢复默认 RESET client_encoding;通过环境变量适合批量配置# 设了这个变量连接后自动生效 export KINGBASE_CLIENTENCODINGUTF8在 JDBC 连接字符串里指定Java 应用最常用String url jdbc:kingbase8://localhost:54321/mydb ?characterEncodingUTF8; Connection conn DriverManager.getConnection(url, user, password);5.3 转换失败了会怎样并不是所有字符都能从一个编码翻译到另一个编码。比如数据库编码是 EUC_JP日文客户端用的是 LATIN1西欧那些日文独有的字符根本没法用 LATIN1 表示数据库就会直接报错。遇到这种情况怎么办三条对策最省事服务器和客户端都用 UTF-8从根本上消除转换问题。退而求其次确保源字符集是目标字符集的子集。比如从 GBK 转到 UTF-8 永远没问题UTF-8 是 GBK 的超集反过来就不一定了。千万别踩的坑别把客户端编码设成SQL_ASCII这会直接关掉自动转换功能等于所有字符都裸奔。5.4 内置转换不够用自己造一个如果系统内置的转换规则覆盖不了你的场景可以写一个自定义转换函数然后注册-- 先创建转换函数PL/pgSQL 示例 CREATE OR REPLACE FUNCTION utf8_to_gbk_custom(INTEGER, INTEGER, CSTRING, INTERNAL, INTEGER) RETURNS VOID AS $$ BEGIN -- 这里实现具体的编码转换逻辑 -- 通常调用数据库内部的转换 API END; $$ LANGUAGE plpgsql; -- 注册为可用的编码转换 CREATE CONVERSION my_utf8_to_gbk FOR UTF8 TO GBK FROM utf8_to_gbk_custom;六、排序规则——让字符串按你的想法排6.1 为什么排序还需要规则不同语言对字符串排序的理解不一样。中文可以按拼音排也可以按笔画排。德语里ä有人觉得排在a后面有人觉得它等于ae。这些差异就是排序规则Collation要解决的问题。在金仓数据库里排序规则本质上是一个 SQL 对象它把一个名字映射到操作系统或 ICU 库提供的排序行为。6.2 两种排序引擎libc 和 ICU金仓数据库支持两种排序规则的提供者libc直接用操作系统自带的 C 库。好处是开箱即用坏处是不同操作系统的 libc 版本行为可能略有差异同一套数据在 Linux 和 Windows 上排序结果可能不一样。另外libc 排序规则和字符集编码是绑定的。icu用外部 ICU 库International Components for Unicode。排序行为跨平台一致而且跟编码无关。不过需要在编译数据库时启用 ICU 支持。如果追求排序结果的一致性比如多地部署的系统优先选 ICU。6.3 开箱即用的标准排序规则不管你用什么平台这三个排序规则一定有名称怎么排default数据库创建时指定的 LC_COLLATE 和 LC_CTYPEC/POSIX只认 ASCII 的 A~Z按字节值硬排速度快ucs_basic仅限 UTF8 编码按 Unicode 编码点排序6.4 一张表里也能各排各的排序规则可以精确到列级别甚至单个表达式上也能临时指定。这在多语言混合的业务场景里特别好用-- 同一张表不同列用不同排序规则 CREATE TABLE international_products ( id INTEGER PRIMARY KEY, name_cn TEXT COLLATE zh_CN, -- 中文按拼音排 name_de TEXT COLLATE de_DE, -- 德语规则 name_ja TEXT COLLATE ja_JP, -- 日语规则 description TEXT -- 跟随数据库默认 );查询时也能临时切换排序规则-- 按德语规则排序 SELECT * FROM international_products ORDER BY name_de COLLATE de_DE; -- 比较时指定用中文排序规则 SELECT * FROM international_products WHERE name_cn COLLATE zh_CN 苹果;一个容易犯的错当两列有不同的排序规则时直接比较会报错-- 报错两列排序规则冲突数据库不知道听谁的 SELECT * FROM t1, t2 WHERE t1.name t2.name; -- 修好它显式告诉数据库用哪个规则 SELECT * FROM t1, t2 WHERE t1.name COLLATE zh_CN t2.name;6.5 内置的不够用自己创建排序规则-- 基于 libc 创建中文排序规则 CREATE COLLATION chinese (provider libc, locale zh_CN.utf8); -- 基于 ICU 创建德语电话簿风格排序 CREATE COLLATION de-u-co-phonebk-x-icu (provider icu, locale de-u-co-phonebk); -- 数字自然排序A-21 排在 A-123 前面 CREATE COLLATION numeric_sort (provider icu, locale en-u-kn-true); -- 先排大写再排小写默认反过来 CREATE COLLATION upper_first (provider icu, locale en-u-kf-upper); -- 把数字排到字母后面默认数字在前 CREATE COLLATION digits_last (provider icu, locale en-u-kr-latn-digit); -- 从已有排序规则复制一个新名字方便跨平台统一 CREATE COLLATION german FROM de_DE;6.6 不区分大小写的排序——非确定性排序规则默认情况下只有字节完全一样的字符串才算相等。但实际业务中经常需要模糊比较——比如忽略大小写、忽略重音符号。这时就要用到非确定性排序规则了关键是那个deterministic false-- 创建不区分大小写的排序规则 CREATE COLLATION case_insensitive (provider icu, locale und-u-ks-level2, deterministic false); -- 测试一下Hello 和 hello 被视为相等 SELECT Hello COLLATE case_insensitive hello; -- 输出ttrue -- 创建不区分重音的排序规则 CREATE COLLATION ignore_accents (provider icu, locale und-u-ks-level1-kc-true, deterministic false); SELECT café COLLATE ignore_accents cafe; -- 输出ttrue注意非确定性排序规则有两个代价——性能会比普通排序慢一些而且不支持LIKE、正则这类模式匹配操作。所以只在确实需要模糊比较的场景下使用。6.7 SQLServer 兼容模式下的排序规则如果数据库运行在 SQLServer 兼容模式下排序规则的后缀有特定含义后缀含义_CS区分大小写Case Sensitive_CI不区分大小写Case Insensitive_AS区分重音Accent Sensitive_AI不区分重音Accent Insensitive_KS区分假名日文片假名/平假名_WS区分全角/半角-- 创建一个按笔画排序、不区分大小写和重音的列 CREATE TABLE t3 ( id INT, name VARCHAR(20) COLLATE Chinese_PRC_Stroke_CI_AI ); INSERT INTO t3 VALUES (1, 人), (2, 大), (3, 金), (4, 仓); -- 按笔画排序人(2画) 大(3画) 仓(4画) 金(8画) SELECT * FROM t3 ORDER BY name;七、国际化数据类型——给多语言留后路7.1 NCHAR 和 NVARCHAR2 是干什么的普通的CHAR/VARCHAR2用的是数据库字符集。但如果你用的数据库字符集是 GBK突然要存一个日文字符怎么办这时候就需要NCHAR和NVARCHAR2了——它们走的是独立的 Unicode 国际字符集UTF-16 或 UTF-8跟数据库本身的字符集互不干扰。NCHAR定长 Unicode 字符串用国际字符集存储。NVARCHAR2变长 Unicode 字符串用国际字符集存储。数据在普通字符类型和国际字符类型之间赋值时数据库会自动做字符集转换不需要手动处理。-- 一个典型的国际化表设计 CREATE TABLE multilingual_docs ( id INTEGER PRIMARY KEY, title VARCHAR2(200), -- 数据库字符集 title_i18n NVARCHAR2(200), -- Unicode什么语言都能存 content CLOB, -- 大文本数据库字符集 content_i18n NCLOB -- 大文本Unicode ); -- 正常插入中文 INSERT INTO multilingual_docs (id, title, title_i18n) VALUES (1, 产品说明, 产品说明); -- title_i18n 可以存任何语言的文字 INSERT INTO multilingual_docs (id, title, title_i18n) VALUES (2, Product Guide, 製品ガイド); -- NCHAR 类型的长度限制取决于国际字符集的编码方式 -- AL16UTF16 编码最多 1000 个字符 -- UTF8 编码最多 2000 个字符7.2 “4 个字符和4 个字节”——差别大了在多字节编码下字符和字节完全是两码事。一个中文字符在 UTF-8 里占 3 个字节在 GBK 里占 2 个字节。如果建表时写VARCHAR2(4)到底是指 4 个字符还是 4 个字节金仓数据库用NLS_LENGTH_SEMANTICS参数来控制这个行为-- 模式一按字符计算 SET NLS_LENGTH_SEMANTICS CHAR; CREATE TABLE test_char (col VARCHAR2(4)); INSERT INTO test_char VALUES (一二三四); -- 成功正好4个字符 INSERT INTO test_char VALUES (一二三四五); -- 报错超出4个字符 SELECT col, length(col) FROM test_char; -- col | length -- ----------------- -- 一二三四 | 4 -- 模式二按字节计算 SET NLS_LENGTH_SEMANTICS BYTE; CREATE TABLE test_byte (col VARCHAR2(4)); INSERT INTO test_byte VALUES (1234); -- 成功4字节 INSERT INTO test_byte VALUES (一二三); -- 报错UTF-8下一个中文3字节光一二就6字节了 INSERT INTO test_byte VALUES (一); -- 成功3字节没超过4 SELECT col, length(col) FROM test_byte; -- col | length -- ------------ -- 1234| 4 -- 一 | 1更推荐的做法是直接在列定义里写清楚省得依赖全局参数CREATE TABLE products ( name_char VARCHAR2(20 CHAR), -- 明确说20个字符 name_byte VARCHAR2(60 BYTE), -- 明确说60个字节 code CHAR(10) -- 不写的话跟随 NLS_LENGTH_SEMANTICS );7.3 常用字符处理函数速查日常开发中这几个函数用得最多收藏备用-- 按字符数计数 SELECT char_length(你好World); -- 77个字符 -- 按字节数计量 SELECT octet_length(你好World); -- 9UTF-8: 2×359字节 -- 大小写转换受 LC_CTYPE 区域影响 SELECT upper(hello world); -- HELLO WORLD SELECT lower(HELLO WORLD); -- hello world SELECT initcap(hello world); -- Hello World -- 截取子串按字符 SELECT substring(你好世界, 1, 2); -- 你好 -- 字符集转换把 UTF-8 编码的中国转成 GBK 字节序列 SELECT UTL_RAW.CONVERT( UTL_RAW.CAST_TO_RAW(中国), ZHS16GBK, -- 目标字符集 AL32UTF8 -- 源字符集 ); -- 输出D6D0B9FA中国的 GBK 编码 -- 查看字符串在当前编码下的十六进制表示 SELECT encode(convert_to(中国, UTF8), hex); -- 输出e4b8ade59bbd八、实战多语言场景下的完整方案8.1 第一原则——全链路统一 UTF-8如果你的项目还在技术选型阶段把这一条钉在墙上数据库、应用服务器、客户端工具、操作系统的默认编码全部统一成 UTF-8。这样做的好处是彻底消除编码转换带来的性能损耗和潜在错误全球所有语言的字符都能直接存储不用操心这个字能不能存应用层的编码处理逻辑大幅简化# 1. 初始化数据库 initdb -D /data/kingbase -E UTF8 --localezh_CN.UTF-8 # 2. 设置环境变量 export KINGBASE_CLIENTENCODINGUTF8 export LANGzh_CN.UTF-8 # 3. 启动数据库 sys_ctl -D /data/kingbase -l logfile start8.2 Java 应用的完整编码配置import java.sql.*; public class I18nDemo { public static void main(String[] args) { // 连接 URL 里直接指定编码 String url jdbc:kingbase8://localhost:54321/mydb ?characterEncodingUTF8 clientEncodingUTF8; try (Connection conn DriverManager.getConnection(url, system, password)) { // 双重保险连接后再显式设置一次 try (Statement stmt conn.createStatement()) { stmt.execute(SET client_encoding TO UTF8); } // 建一张多语言表 try (Statement stmt conn.createStatement()) { stmt.execute( CREATE TABLE IF NOT EXISTS i18n_data ( id SERIAL PRIMARY KEY, lang VARCHAR(10), text VARCHAR(500) ) ); } // 批量插入多语言数据 String[] langs {zh, ja, en, de}; String[] texts {你好世界, こんにちは, Hello World, Hallo Welt}; try (PreparedStatement ps conn.prepareStatement( INSERT INTO i18n_data (lang, text) VALUES (?, ?))) { for (int i 0; i langs.length; i) { ps.setString(1, langs[i]); ps.setString(2, texts[i]); ps.addBatch(); } ps.executeBatch(); } // 查询并打印——中文不会乱码 try (Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(SELECT * FROM i18n_data ORDER BY id)) { while (rs.next()) { System.out.printf([%s] %s%n, rs.getString(lang), rs.getString(text)); } } } catch (SQLException e) { e.printStackTrace(); } } }8.3 Python 应用的编码处理# 使用 psycopg2 风格的驱动连接金仓数据库 import sys def query_i18n_data(): conn None try: # 连接时确保客户端编码正确 conn connect( hostlocalhost, port54321, databasemydb, usersystem, passwordpassword ) with conn.cursor() as cur: # 显式设置客户端编码 cur.execute(SET client_encoding TO UTF8) # 查询多语言数据 cur.execute(SELECT lang, text FROM i18n_data ORDER BY id) for lang, text in cur.fetchall(): # Python 3 默认使用 Unicode直接输出即可 print(f[{lang}] {text}) except Exception as e: print(f数据库操作失败: {e}, filesys.stderr) finally: if conn: conn.close() if __name__ __main__: query_i18n_data()8.4 数据迁移——编码转换是最容易翻车的环节从一个数据库搬到另一个数据库编码兼容性必须提前确认-- 第一步搞清楚源库用的什么编码 SELECT datname, pg_encoding_to_char(encoding) AS encoding FROM pg_database; -- 第二步确保目标库的字符集是源库的超集 -- GBK → UTF-8安全UTF-8 包含 GBK 的所有字符 -- UTF-8 → GBK可能丢字符GBK 不包含某些 Unicode 字符 -- 第三步用 COPY 命令导出显式指定编码 COPY products TO /tmp/products_utf8.csv WITH (ENCODING UTF8, FORMAT csv, HEADER); -- 第四步导入时也可以指定编码数据库自动转换 COPY products FROM /tmp/products_gbk.csv WITH (ENCODING GBK, FORMAT csv, HEADER);如果数据量很大建议先用小批量数据做一次试导入确认没有乱码或截断后再全量迁移。8.5 常见问题——排查清单乱码了按这个顺序查-- 1. 服务端用的什么编码 SHOW server_encoding; -- 2. 客户端用的什么编码 SHOW client_encoding; -- 3. 两个一样吗不一样就统一 SET client_encoding TO UTF8;中文排序不对-- 看看排序规则是不是中文的 SHOW lc_collate; -- 临时用中文排序规则查一下 SELECT * FROM users ORDER BY name COLLATE zh_CN;插入中文报值太长-- 大概率是字节/字符语义搞混了 SHOW nls_length_semantics; -- 改成按字符计算 SET nls_length_semantics CHAR; -- 或者建表时直接写明 CREATE TABLE t (name VARCHAR2(50 CHAR)); -- 50个字符不是50个字节LIKE 查询走不上索引-- 非C区域下 LIKE 默认不用索引单独建一个 C 排序规则的索引 CREATE INDEX idx_name_c ON users (name COLLATE C); -- 然后查询时也指定 C 排序规则 SELECT * FROM users WHERE name COLLATE C LIKE 张%;九、收尾记住这五条就够了字符编码这种事提前花一小时规划好能省掉后面无数个深夜排查乱码的小时。统一 UTF-8服务器、客户端、应用全链路 UTF-8省去 90% 的编码问题。初始化时就定好区域LC_COLLATE和LC_CTYPE创建后改不了别等建完库才想起来。灵活使用排序规则列级别、表达式级别的COLLATE子句让不同语言各自排序互不干扰。分清字符和字节多字节编码下建表时用CHAR限定符避免中文被意外截断。迁移前先确认编码兼容性目标字符集必须是源字符集的超集否则可能丢数据。