UPPER / LOWER / INITCAP
大小写转换(Case Conversion)是文本规范化的第一步:统一存储格式、实现大小写不敏感匹配、生成展示文案。PostgreSQL 提供 upper / lower / initcap 三个经典函数,18 版新增了更严谨的 casefold。本节将讲清 locale 对转换结果的影响、Turkish i 问题、以及为什么 lower() 不等于完整的大小写不敏感。
前置知识
阅读本节前,建议先了解:LENGTH / CHAR_LENGTH / OCTET_LENGTH
基础概念
| 概念 | 术语 | 说明 |
|---|---|---|
| 大小写转换 | Case Conversion | 改变字符的大小写形式,不改变语义 |
| 大小写折叠 | Case Folding | 更彻底的大小写归一,一对多(如 ß → ss) |
| 排序规则 | Collation | 决定转换与比较的语言相关行为 |
| C 排序规则 | C / POSIX Locale | 逐字节比较,转换只处理 ASCII |
| 语言环境 | Locale / lc_ctype | 影响 upper / lower / initcap 的结果 |
| 土耳其语 i 问题 | Turkish İ Problem | i 与 I 的映射在土耳其语中不同 |
| 词首大写 | Title Case | 每个单词首字母大写,其余小写 |
| 无差别匹配 | Case-Insensitive Matching | 忽略大小写比较,需特殊索引支持 |
三个经典函数的行为对照:
| 函数 | 输入 | 输出 | 说明 |
|---|---|---|---|
upper('Post') | 混合 | POST | 全部转大写 |
lower('Post') | 混合 | post | 全部转小写 |
initcap('post gre sql') | 小写 | Post Gre Sql | 每个单词首字母大写 |
initcap('pOSt') | 混合 | Post | 其余字母转小写 |
upper('中文abc') | 混合 | 中文ABC | 非字母字符不变 |
lower(NULL) | NULL | NULL | 空值传染 |
关键认知:initcap 的效果常与直觉不符
SELECT
initcap('postgresql tutorial') AS normal,
initcap('hello-world') AS with_dash,
initcap('o''brien') AS with_apostrophe,
initcap('hello world') AS multi_space,
initcap('3rd place') AS with_digit,
initcap('e-mail address') AS email_case; normal | with_dash | with_apostrophe | multi_space | with_digit | email_case
---------------------+--------------+-----------------+------------------+------------+----------------
Postgresql Tutorial | Hello-World | O'Brien | Hello World | 3rd Place | E-Mail Addressinitcap 的单词边界是任何非字母数字字符,因此 hello-world 变成 Hello-World(连字符后的 w 也大写)、e-mail 变成 E-Mail。这在处理英文人名时往往是错的(McDonald、van der Berg、O'Brien 都会出问题)。
语法与用法
-- 三个经典函数
upper(text) → text -- 转大写
lower(text) → text -- 转小写
initcap(text) → text -- 每个单词首字母大写
-- PostgreSQL 18 新增:大小写折叠
casefold(text) → text
-- 相关操作符与谓词
col ILIKE 'pattern' -- 大小写不敏感的 LIKE
col ~* 'regex' -- 大小写不敏感的正则匹配
col !~* 'regex' -- 大小写不敏感的正则不匹配
-- 排序规则指定
lower(col COLLATE "tr_TR") -- 指定土耳其语规则最小可运行示例:
SELECT
upper('PostgreSQL 18') AS up,
lower('PostgreSQL 18') AS low,
initcap('postgresql tutorial') AS ini,
upper('中文abc') AS mixed_cn,
lower('ÄÖÜ') AS umlaut; up | low | ini | mixed_cn | umlaut
----------------+------------------+----------------------+----------+----------
POSTGRESQL 18 | postgresql 18 | Postgresql Tutorial | 中文ABC | äöü准备本节贯穿使用的示例表:
CREATE TABLE users (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username TEXT NOT NULL,
email TEXT NOT NULL,
full_name TEXT,
country TEXT,
status TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO users (username, email, full_name, country, status) VALUES
('zhangsan', 'ZhangSan@Example.COM', 'zhang san', 'CN', 'ACTIVE'),
('LiSi', 'lisi@example.com', 'li si', 'CN', 'active'),
('wangwu', 'WangWu@Example.com', 'wang wu', 'CN', 'Active'),
('JOHN_DOE', 'john.doe@example.com', 'john doe', 'US', 'ACTIVE'),
('MARIA', 'maria@example.com', 'maría garcía', 'ES', 'active');-- 大小写不敏感的用户名查找:三种写法的对比
SELECT id, username
FROM users
WHERE lower(username) = lower('LISI')
ORDER BY id; id | username
----+----------
2 | LiSi详细说明
upper / lower 依赖 locale
这是最容易被忽略的一点:upper / lower 的结果可能随排序规则变化。
-- 当前数据库的 locale 设置
SELECT
current_database() AS db,
pg_encoding_to_char(encoding) AS encoding,
datcollate,
datctype
FROM pg_database
WHERE datname = current_database(); db | encoding | datcollate | datctype
------+----------+--------------+--------------
demo | UTF8 | zh_CN.UTF-8 | zh_CN.UTF-8datctype(即 lc_ctype)决定 upper / lower 处理非 ASCII 字符的方式。在 C locale 下,只有 ASCII 字母会被转换:
-- 对比不同排序规则下的德语 ß(U+00DF)
SELECT
upper('straße') AS default_locale,
upper('straße' COLLATE "C") AS c_locale,
lower('STRASSE') AS lower_default,
lower('STRASSE' COLLATE "C") AS lower_c; default_locale | c_locale | lower_default | lower_c
----------------+----------+---------------+---------
STRASSE | STRAßE | strasse | strasse在 UTF-8 locale 下,upper('straße') 得到 STRASSE(ß 展开为 SS,长度从 6 变 7);而在 C locale 下 ß 不变,得到 STRAßE。
长度会变这一点非常重要——如果表上有 VARCHAR(10) 约束,upper 可能让结果超长:
SELECT
upper('straße') AS val,
length('straße') AS len_before, -- 6
length(upper('straße')) AS len_after; -- 7 val | len_before | len_after
--------+------------+-----------
STRASSE | 6 | 7土耳其语 i 问题
土耳其语(tr_TR)中 i 的大写是 İ(带点),I 的小写是 ı(无点)。这与其它语言完全不同:
-- 假设数据库已安装 tr_TR locale
SELECT
upper('i') AS default_upper, -- I
upper('i' COLLATE "tr_TR") AS tr_upper, -- İ
lower('I') AS default_lower, -- i
lower('I' COLLATE "tr_TR") AS tr_lower; -- ı default_upper | tr_upper | default_lower | tr_lower
---------------+----------+---------------+----------
I | İ | i | ı如果两个系统使用不同的 locale 做大小写归一,会出现「同一个用户在小写后不相等」的问题:
-- 跨 locale 比较会失败
SELECT
lower('I' COLLATE "tr_TR") = lower('I' COLLATE "C") AS same; same
------
f规避办法:在排序规则敏感的场景不要依赖 lower 做归一,改用 casefold 或把列统一存为规范形式。
查看可用排序规则:
SELECT collname, collprovider, collcollate
FROM pg_collation
WHERE collname IN ('C', 'POSIX', 'default') OR collname LIKE 'tr_TR%'
ORDER BY collname
LIMIT 10; collname | collprovider | collcollate
----------+--------------+------------
C | c | C
POSIX | c | POSIX
default | d |casefold:PostgreSQL 18 的新成员
casefold 是 PostgreSQL 18 新增的函数,用于「更彻底的大小写折叠」——目标不是转成小写,而是把在大小写不敏感比较下应当等价的形式统一。
-- PostgreSQL 18+
SELECT
casefold('Straße') AS folded_cn, -- 完整折叠
lower('Straße') AS lowered, -- 仅转小写
casefold('İstanbul') AS folded_tr, -- 土耳其语大写 I 带点
casefold('ΣΊΣΥΦΟΣ') AS folded_greek; -- 希腊语 folded_cn | lowered | folded_tr | folded_greek
-----------+---------+-----------+--------------
strasse | straße | i̇stanbul | σίσυφος差异关键点:
| 输入 | lower() | casefold() | 说明 |
|---|---|---|---|
Straße | straße | strasse | ß 折叠为 ss |
İ(U+0130) | i̇(两码点) | i̇ 或 i | 依赖实现与排序规则 |
Σ(词尾 sigma) | σ | σ |
casefold 在 libc provider 下等价于 lower,只有在支持完整 Unicode 折叠的 provider(如 PG_UNICODE_FAST 或 ICU)下才展现差异。它仅支持 UTF8 编码的数据库。
-- 检查当前 provider
SELECT
datname,
daticulocale,
datcollversion
FROM pg_database
WHERE datname = current_database(); datname | daticulocale | datcollversion
---------+--------------+----------------
demo | |-- 大小写不敏感的唯一约束:用 casefold 而不是 lower(PostgreSQL 18+)
CREATE UNIQUE INDEX idx_users_email_fold ON users (casefold(email));CREATE INDEX用 lower 做不到这一点——Straße 与 STRASSE 在 lower 下分别为 straße 与 strasse,不相等;而 casefold 下都是 strasse,会正确判重。
initcap 的正确用法与局限
initcap 的定位是「规范化英文标题」,而不是「规范化人名」。它的单词边界规则是:任何非字母数字字符都算分隔符,且只处理 ASCII 字母与 locale 定义的大小写字母对。
-- 适合 initcap 的场景:规范化英文标题
SELECT
initcap('the quick brown fox') AS title_case,
initcap('postgresql 18 是 数据库') AS mixed_title,
initcap('user profile settings') AS ui_label; title_case | mixed_title | ui_label
-----------------------+----------------------+--------------------
The Quick Brown Fox | Postgresql 18 是 数据库 | User Profile Settings-- 不适合 initcap 的场景:人名
SELECT
initcap('van der berg') AS dutch_name, -- 应该是 van der Berg
initcap('mcdonald') AS irish_name, -- 应该是 McDonald
initcap('o''brien') AS irish_name2, -- 恰好正确
initcap('李 小龙') AS cn_name; -- 应该是 李小龙 dutch_name | irish_name | irish_name2 | cn_name
---------------+------------+-------------+----------
Van Der Berg | Mcdonald | O'Brien | 李 小龙人名处理需要专门的规则库,不能靠 initcap。中文人名更要注意 initcap 会把拼音首字母大写:
SELECT
initcap('zhang san') AS pinyin, -- Zhang San
initcap('zhangsan') AS pinyin2; -- Zhangsan pinyin | pinyin2
-----------+----------
Zhang San | Zhangsan大小写不敏感匹配的三种手段
| 手段 | 语法 | 索引支持 | 适用场景 |
|---|---|---|---|
lower 归一 | WHERE lower(col) = lower(:v) | 表达式索引 | 通用,兼容所有版本 |
ILIKE | WHERE col ILIKE 'a%' | pg_trgm GIN / 不支持 B-tree | 模糊匹配 |
citext | WHERE col = :v(列类型 citext) | 直接支持 | 表设计阶段可决定 |
casefold 索引 | WHERE casefold(col) = casefold(:v) | 表达式索引 | PostgreSQL 18+,最严谨 |
-- 手段 1:lower + 表达式索引
CREATE INDEX idx_users_username_lower ON users (lower(username));
ANALYZE users;
EXPLAIN (COSTS OFF)
SELECT id FROM users WHERE lower(username) = lower('LISI'); QUERY PLAN
-----------------------------------------------------------------
Index Scan using idx_users_username_lower on users
Index Cond: (lower(username) = 'lisi'::text)-- 手段 2:ILIKE
EXPLAIN (COSTS OFF)
SELECT id FROM users WHERE username ILIKE 'li%'; QUERY PLAN
----------------------------------------
Seq Scan on users
Filter: (username ~~* 'li%'::text)ILIKE 默认走顺序扫描——它无法使用普通 B-tree 索引。若数据量大,需要 pg_trgm:
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_users_username_trgm ON users USING gin (username gin_trgm_ops);
ANALYZE users;
EXPLAIN (COSTS OFF)
SELECT id FROM users WHERE username ILIKE '%LIS%'; QUERY PLAN
---------------------------------------------------------------------------
Bitmap Heap Scan on users
Recheck Cond: (username ~~* '%LIS%'::text)
-> Bitmap Index Scan on idx_users_username_trgm
Index Cond: (username ~~* '%LIS%'::text)-- 手段 3:citext 扩展类型(表设计阶段决定)
CREATE EXTENSION IF NOT EXISTS citext;
CREATE TEMP TABLE users_ci (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username citext NOT NULL UNIQUE
);
INSERT INTO users_ci (username) VALUES ('LiSi'), ('wangwu');-- citext 列直接比较即可,无需 lower
SELECT id, username FROM users_ci WHERE username = 'lisi'; id | username
----+----------
1 | LiSi转换与索引:常见错误
错误 1:在索引列上做转换,导致索引失效
-- 错误!username 上有普通索引,但 lower() 让它失效
CREATE INDEX idx_users_username ON users (username);
EXPLAIN (COSTS OFF)
SELECT id FROM users WHERE lower(username) = 'lisi'; QUERY PLAN
----------------------------------------
Seq Scan on users
Filter: (lower(username) = 'lisi'::text)错误 2:表达式索引的表达式与查询不一致
-- 建了 lower(username) 索引,但查询写成 lower(btrim(username))
CREATE INDEX idx_users_username_lower ON users (lower(username));
EXPLAIN (COSTS OFF)
SELECT id FROM users WHERE lower(btrim(username)) = 'lisi'; QUERY PLAN
----------------------------------------
Seq Scan on users
Filter: (lower(btrim(username)) = 'lisi'::text)索引用 lower(username),查询用 lower(btrim(username)),二者不同,索引无法匹配。必须保持一致:
EXPLAIN (COSTS OFF)
SELECT id FROM users WHERE lower(username) = lower(btrim(' LISI ')); QUERY PLAN
-----------------------------------------------------------------
Index Scan using idx_users_username_lower on users
Index Cond: (lower(username) = 'lisi'::text)错误 3:对索引列调用 upper 做「规范化存储」,破坏了原值
-- 不推荐!存储时转大写会丢失原始信息,且不可逆
UPDATE users SET username = upper(username);
-- 推荐:保持原值,用转换只做比较
SELECT id FROM users WHERE lower(username) = lower('LiSi');大小写转换与性能
upper / lower / initcap 都是 IMMUTABLE,可用于表达式索引,但都有 O(n) 的解码与逐字符处理成本:
SELECT
p.proname,
CASE p.provolatile
WHEN 'i' THEN 'IMMUTABLE'
WHEN 's' THEN 'STABLE'
WHEN 'v' THEN 'VOLATILE'
END AS volatility
FROM pg_proc p
WHERE p.proname IN ('upper', 'lower', 'initcap', 'casefold')
AND p.pronamespace = 'pg_catalog'::regnamespace
AND pg_get_function_arguments(p.oid) LIKE '%text%'
ORDER BY p.proname; proname | volatility
----------+------------
casefold | IMMUTABLE
initcap | IMMUTABLE
lower | IMMUTABLE
upper | IMMUTABLEinitcap 比 upper / lower 更贵,因为它还需要做单词边界识别:
-- 在大表上比较三者成本
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) FROM users WHERE upper(username) LIKE 'Z%';
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) FROM users WHERE initcap(full_name) LIKE 'Z%';-- upper
Aggregate (actual rows=1 loops=1)
-> Seq Scan on users (actual rows=5 loops=1)
Filter: (upper(username) ~~ 'Z%'::text)
Rows Removed by Filter: 4
-- initcap
Aggregate (actual rows=1 loops=1)
-> Seq Scan on users (actual rows=5 loops=1)
Filter: (initcap(full_name) ~~ 'Z%'::text)
Rows Removed by Filter: 4小表看不出差异,但在千万行级表上,initcap 的 CPU 成本约为 lower 的 2~3 倍,能不用就不用。
实战示例
场景一:统一邮箱存储并建立不区分大小写唯一约束
需求:邮箱必须全局唯一且不区分大小写,但展示时要保留用户输入的原始形式。
-- 步骤 1:先找出会冲突的数据(按 lower 分组)
SELECT
lower(email) AS email_lower,
count(*) AS cnt,
array_agg(id ORDER BY id) AS ids,
array_agg(email ORDER BY id) AS raw_emails
FROM users
GROUP BY lower(email)
HAVING count(*) > 1
ORDER BY cnt DESC; email_lower | cnt | ids | raw_emails
-------------+-----+-----+------------本例数据无冲突。制造一个便于观察的冲突场景:
-- 步骤 2:插入一条仅大小写不同的邮箱,验证是否会冲突
INSERT INTO users (username, email, full_name, country, status)
VALUES ('lisi2', 'LISI@example.com', 'li si', 'CN', 'active');INSERT 0 1-- 步骤 3:此时按 lower 分组能发现冲突
SELECT
lower(email) AS email_lower,
count(*) AS cnt,
array_agg(id ORDER BY id) AS ids,
array_agg(email ORDER BY id) AS raw_emails
FROM users
GROUP BY lower(email)
HAVING count(*) > 1; email_lower | cnt | ids | raw_emails
-----------------+-----+-------+----------------------------
lisi@example.com | 2 | {2,6} | {lisi@example.com,"LISI@example.com"}-- 步骤 4:清理冲突数据后建立不区分大小写的唯一索引
DELETE FROM users WHERE id = 6;
CREATE UNIQUE INDEX idx_users_email_lower ON users (lower(email));CREATE INDEX-- 步骤 5:验证约束生效
INSERT INTO users (username, email, full_name, country, status)
VALUES ('lisi3', 'LISI@example.com', 'li si', 'CN', 'active');ERROR: duplicate key value violates unique constraint "idx_users_email_lower"
DETAIL: Key (lower(email))=(lisi@example.com) already exists.-- 步骤 6:展示时保留原值,仅比较时归一
SELECT
id,
email AS raw_email,
lower(email) AS normalized
FROM users
ORDER BY id; id | raw_email | normalized
----+--------------------------+-------------------------
1 | ZhangSan@Example.COM | zhangsan@example.com
2 | lisi@example.com | lisi@example.com
3 | WangWu@Example.com | wangwu@example.com
4 | john.doe@example.com | john.doe@example.com
5 | maria@example.com | maria@example.comPostgreSQL 18 下更严谨的写法是 casefold:
-- PostgreSQL 18+:用 casefold 覆盖 ß / İ 等边缘字符
DROP INDEX idx_users_email_lower;
CREATE UNIQUE INDEX idx_users_email_fold ON users (casefold(email));
SELECT 'Straße@example.com' = 'STRASSE@example.com' AS by_eq,
casefold('Straße@example.com') = casefold('STRASSE@example.com') AS by_casefold; by_eq | by_casefold
-------+--------------
f | t场景二:统计数据质量问题
需求:检查 status 字段的大小写是否统一(业务要求全大写),并批量修复。
-- 步骤 1:按原值分布统计,暴露不一致
SELECT
status AS raw,
count(*) AS cnt
FROM users
GROUP BY status
ORDER BY cnt DESC, raw; raw | cnt
--------+-----
active | 2
ACTIVE | 2
Active | 1-- 步骤 2:按归一后的值统计,确认归一能收敛
SELECT
upper(status) AS normalized,
count(*) AS cnt,
count(DISTINCT status) AS variants
FROM users
GROUP BY upper(status)
ORDER BY cnt DESC; normalized | cnt | variants
------------+-----+----------
ACTIVE | 5 | 3三种写法收敛为 1 个值,variants = 3 说明数据不干净。
-- 步骤 3:先预览修复影响行数(不要直接 UPDATE)
SELECT count(*) AS rows_to_fix
FROM users
WHERE status <> upper(status); rows_to_fix
-------------
3-- 步骤 4:加 CHECK 约束把规则固化,防止再次污染
ALTER TABLE users
ADD CONSTRAINT ck_users_status_upper CHECK (status = upper(status));
-- 此时 UPDATE 会因约束失败,必须先修复数据ERROR: check constraint "ck_users_status_upper" of relation "users" is violated by some row-- 步骤 5:修复数据后约束即可加上
UPDATE users SET status = upper(status) WHERE status <> upper(status);
ALTER TABLE users
ADD CONSTRAINT ck_users_status_upper CHECK (status = upper(status));ALTER TABLE-- 步骤 6:验证
SELECT status, count(*) FROM users GROUP BY status; status | cnt
--------+-----
ACTIVE | 5注意 CHECK (status = upper(status)) 这个写法的巧妙之处:它同时约束了「必须全大写」与「不能含无法转换的字符」,且因 upper 是 IMMUTABLE,约束合法。
场景三:生成规范化展示名
需求:把 full_name(拼音小写)转成合适的展示名,中文人名不做首字母大写。
-- 步骤 1:先按国家区分处理策略
SELECT
id,
country,
full_name AS raw,
CASE
WHEN country IN ('CN', 'JP', 'KR') THEN full_name -- 东亚人名:保持原样
WHEN country = 'ES' THEN initcap(full_name) -- 西语:首字母大写
ELSE initcap(full_name)
END AS display_name
FROM users
ORDER BY id; id | country | raw | display_name
----+---------+------------+----------------
1 | CN | zhang san | zhang san
2 | CN | li si | li si
3 | CN | wang wu | wang wu
4 | US | john doe | John Doe
5 | ES | maría garcía | María García-- 步骤 2:验证 initcap 对多字节字符的处理
SELECT
initcap('maría garcía') AS es_name,
initcap('élève français') AS fr_name,
initcap('über alles') AS de_name; es_name | fr_name | de_name
--------------+----------------+--------------
María García | Élève Français | Über Allesinitcap 在 UTF-8 locale 下能正确处理带变音符号的欧洲语言字母——但前提是 lc_ctype 不是 C:
SELECT
initcap('maría' COLLATE "C") AS c_locale, -- 变音字符不转换
initcap('maría') AS utf8_locale; c_locale | utf8_locale
----------+-------------
maría | María注意事项
upper/lower依赖lc_ctype,Clocale 下只处理 ASCII,非 ASCII 字符不转换。upper('straße')可能返回STRASSE,长度会变,对定长字段或被约束的列要格外小心:sql-- 错误!VARCHAR(6) 装不下 upper 后的 7 个字符 CREATE TABLE t (code VARCHAR(6) CHECK (code = upper(code)));- 土耳其语 locale 下
upper('i')是İ而非I,跨系统做大小写归一必须统一 locale。 initcap的单词边界是任何非字母数字字符,hello-world→Hello-World、e-mail→E-Mail,与多数人的预期不符。initcap不适合处理人名,van der berg→Van Der Berg、mcdonald→Mcdonald都是错的。initcap会把中文字符当分隔符的邻接字母也大写,处理中文姓名要避免使用。lower(NULL)返回NULL,拼接或聚合时要考虑空值传染。- 在
WHERE的索引列上套lower/upper会让索引失效,需要建表达式索引。 - 表达式索引的表达式必须与查询完全一致,
lower(col)与lower(btrim(col))不能互相复用。 ILIKE默认不走 B-tree 索引,大表模糊匹配需要pg_trgm+ GIN。casefold是 PostgreSQL 18 新增且仅支持 UTF8 编码;在libcprovider 下它等价于lower,只有PG_UNICODE_FAST/ ICU 下才体现完整折叠能力。- 不要把转换结果写回列来「规范化数据」,转换是不可逆的(
Straße与STRASSE都变成STRASSE),会永久丢失原始信息。
最佳实践
- 存储保持原值,比较时才归一:展示用户输入的原始形式,用表达式索引支撑大小写不敏感查询。
- 大小写不敏感唯一约束用
lower(col)或casefold(col)表达式索引,本地化要求高时优先 18 版的casefold。 - 排序规则敏感的场景显式指定
COLLATE,如lower(col COLLATE "tr_TR"),避免隐式依赖库默认值。 - 写转换前先确认
lc_ctype:用SELECT datctype FROM pg_database WHERE datname = current_database()。 - 需要区分语言做转换时用
CASE分支,不要指望一个initcap通吃所有语言。 - 约束式规范化用
CHECK (col = upper(col)),让数据库强制格式统一,比事后清洗可靠。 initcap只用于英文标题、UI 标签,人名与专有名词必须走规则库或人工维护映射表。- 大表避免在
WHERE/ORDER BY中对列做转换,改用表达式索引或预先计算并存储归一列。 - 比较前先修剪再转换,顺序为
lower(btrim(col)),并让索引与之保持一致。 - 升级到 18 后优先评估
casefold,它能在不改变存储的前提下解决ß/İ/ 希腊语 sigma 等长期存在的边缘问题。