Skip to content

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 İ Problemi 与 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)NULLNULL空值传染

关键认知:initcap 的效果常与直觉不符

sql
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;
text
       normal        |  with_dash   | with_apostrophe |   multi_space    | with_digit |   email_case
---------------------+--------------+-----------------+------------------+------------+----------------
 Postgresql Tutorial | Hello-World  | O'Brien         | Hello   World    | 3rd Place  | E-Mail Address

initcap 的单词边界是任何非字母数字字符,因此 hello-world 变成 Hello-World(连字符后的 w 也大写)、e-mail 变成 E-Mail。这在处理英文人名时往往是错的(McDonald、van der Berg、O'Brien 都会出问题)。

语法与用法 ​

sql
-- 三个经典函数
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")   -- 指定土耳其语规则

最小可运行示例:

sql
SELECT
  upper('PostgreSQL 18')     AS up,
  lower('PostgreSQL 18')     AS low,
  initcap('postgresql tutorial') AS ini,
  upper('中文abc')            AS mixed_cn,
  lower('ÄÖÜ')                AS umlaut;
text
       up       |       low        |         ini          | mixed_cn |  umlaut
----------------+------------------+----------------------+----------+----------
 POSTGRESQL 18  | postgresql 18    | Postgresql Tutorial  | 中文ABC   | äöü

准备本节贯穿使用的示例表:

sql
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');
sql
-- 大小写不敏感的用户名查找:三种写法的对比
SELECT id, username
FROM users
WHERE lower(username) = lower('LISI')
ORDER BY id;
text
 id | username
----+----------
  2 | LiSi

详细说明 ​

upper / lower 依赖 locale ​

这是最容易被忽略的一点:upper / lower 的结果可能随排序规则变化。

sql
-- 当前数据库的 locale 设置
SELECT
  current_database()   AS db,
  pg_encoding_to_char(encoding) AS encoding,
  datcollate,
  datctype
FROM pg_database
WHERE datname = current_database();
text
  db  | encoding |  datcollate  |   datctype
------+----------+--------------+--------------
 demo | UTF8     | zh_CN.UTF-8  | zh_CN.UTF-8

datctype(即 lc_ctype)决定 upper / lower 处理非 ASCII 字符的方式。在 C locale 下,只有 ASCII 字母会被转换:

sql
-- 对比不同排序规则下的德语 ß(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;
text
 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 可能让结果超长:

sql
SELECT
  upper('straße')                AS val,
  length('straße')               AS len_before,   -- 6
  length(upper('straße'))        AS len_after;    -- 7
text
  val   | len_before | len_after
--------+------------+-----------
 STRASSE |          6 |         7

土耳其语 i 问题 ​

土耳其语(tr_TR)中 i 的大写是 İ(带点),I 的小写是 ı(无点)。这与其它语言完全不同:

sql
-- 假设数据库已安装 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;        -- ı
text
 default_upper | tr_upper | default_lower | tr_lower
---------------+----------+---------------+----------
 I             | İ        | i             | ı

如果两个系统使用不同的 locale 做大小写归一,会出现「同一个用户在小写后不相等」的问题:

sql
-- 跨 locale 比较会失败
SELECT
  lower('I' COLLATE "tr_TR") = lower('I' COLLATE "C") AS same;
text
 same
------
 f

规避办法:在排序规则敏感的场景不要依赖 lower 做归一,改用 casefold 或把列统一存为规范形式。

查看可用排序规则:

sql
SELECT collname, collprovider, collcollate
FROM pg_collation
WHERE collname IN ('C', 'POSIX', 'default') OR collname LIKE 'tr_TR%'
ORDER BY collname
LIMIT 10;
text
 collname | collprovider | collcollate
----------+--------------+------------
 C        | c            | C
 POSIX    | c            | POSIX
 default  | d            |

casefold:PostgreSQL 18 的新成员 ​

casefold 是 PostgreSQL 18 新增的函数,用于「更彻底的大小写折叠」——目标不是转成小写,而是把在大小写不敏感比较下应当等价的形式统一。

sql
-- PostgreSQL 18+
SELECT
  casefold('Straße')      AS folded_cn,        -- 完整折叠
  lower('Straße')         AS lowered,          -- 仅转小写
  casefold('İstanbul')    AS folded_tr,        -- 土耳其语大写 I 带点
  casefold('ΣΊΣΥΦΟΣ')     AS folded_greek;     -- 希腊语
text
 folded_cn | lowered | folded_tr | folded_greek
-----------+---------+-----------+--------------
 strasse   | straße  | i̇stanbul  | σίσυφος

差异关键点:

输入lower()casefold()说明
Straßestraßestrasseß 折叠为 ss
İ(U+0130)i̇(两码点)i̇ 或 i依赖实现与排序规则
Σ(词尾 sigma)σσ

casefold 在 libc provider 下等价于 lower,只有在支持完整 Unicode 折叠的 provider(如 PG_UNICODE_FAST 或 ICU)下才展现差异。它仅支持 UTF8 编码的数据库。

sql
-- 检查当前 provider
SELECT
  datname,
  daticulocale,
  datcollversion
FROM pg_database
WHERE datname = current_database();
text
 datname | daticulocale | datcollversion
---------+--------------+----------------
 demo    |              |
sql
-- 大小写不敏感的唯一约束:用 casefold 而不是 lower(PostgreSQL 18+)
CREATE UNIQUE INDEX idx_users_email_fold ON users (casefold(email));
text
CREATE INDEX

用 lower 做不到这一点——Straße 与 STRASSE 在 lower 下分别为 straße 与 strasse,不相等;而 casefold 下都是 strasse,会正确判重。

initcap 的正确用法与局限 ​

initcap 的定位是「规范化英文标题」,而不是「规范化人名」。它的单词边界规则是:任何非字母数字字符都算分隔符,且只处理 ASCII 字母与 locale 定义的大小写字母对。

sql
-- 适合 initcap 的场景:规范化英文标题
SELECT
  initcap('the quick brown fox')          AS title_case,
  initcap('postgresql 18 是 数据库')       AS mixed_title,
  initcap('user profile settings')         AS ui_label;
text
      title_case       |     mixed_title      |      ui_label
-----------------------+----------------------+--------------------
 The Quick Brown Fox   | Postgresql 18 是 数据库 | User Profile Settings
sql
-- 不适合 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;       -- 应该是 李小龙
text
  dutch_name   | irish_name | irish_name2 | cn_name
---------------+------------+-------------+----------
 Van Der Berg  | Mcdonald   | O'Brien     | 李 小龙

人名处理需要专门的规则库,不能靠 initcap。中文人名更要注意 initcap 会把拼音首字母大写:

sql
SELECT
  initcap('zhang san')   AS pinyin,     -- Zhang San
  initcap('zhangsan')    AS pinyin2;    -- Zhangsan
text
  pinyin   | pinyin2
-----------+----------
 Zhang San | Zhangsan

大小写不敏感匹配的三种手段 ​

手段语法索引支持适用场景
lower 归一WHERE lower(col) = lower(:v)表达式索引通用,兼容所有版本
ILIKEWHERE col ILIKE 'a%'pg_trgm GIN / 不支持 B-tree模糊匹配
citextWHERE col = :v(列类型 citext)直接支持表设计阶段可决定
casefold 索引WHERE casefold(col) = casefold(:v)表达式索引PostgreSQL 18+,最严谨
sql
-- 手段 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');
text
                           QUERY PLAN
-----------------------------------------------------------------
 Index Scan using idx_users_username_lower on users
   Index Cond: (lower(username) = 'lisi'::text)
sql
-- 手段 2:ILIKE
EXPLAIN (COSTS OFF)
SELECT id FROM users WHERE username ILIKE 'li%';
text
              QUERY PLAN
----------------------------------------
 Seq Scan on users
   Filter: (username ~~* 'li%'::text)

ILIKE 默认走顺序扫描——它无法使用普通 B-tree 索引。若数据量大,需要 pg_trgm:

sql
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%';
text
                                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)
sql
-- 手段 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');
sql
-- citext 列直接比较即可,无需 lower
SELECT id, username FROM users_ci WHERE username = 'lisi';
text
 id | username
----+----------
  1 | LiSi

转换与索引:常见错误 ​

错误 1:在索引列上做转换,导致索引失效

sql
-- 错误!username 上有普通索引,但 lower() 让它失效
CREATE INDEX idx_users_username ON users (username);

EXPLAIN (COSTS OFF)
SELECT id FROM users WHERE lower(username) = 'lisi';
text
              QUERY PLAN
----------------------------------------
 Seq Scan on users
   Filter: (lower(username) = 'lisi'::text)

错误 2:表达式索引的表达式与查询不一致

sql
-- 建了 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';
text
              QUERY PLAN
----------------------------------------
 Seq Scan on users
   Filter: (lower(btrim(username)) = 'lisi'::text)

索引用 lower(username),查询用 lower(btrim(username)),二者不同,索引无法匹配。必须保持一致:

sql
EXPLAIN (COSTS OFF)
SELECT id FROM users WHERE lower(username) = lower(btrim('  LISI  '));
text
                           QUERY PLAN
-----------------------------------------------------------------
 Index Scan using idx_users_username_lower on users
   Index Cond: (lower(username) = 'lisi'::text)

错误 3:对索引列调用 upper 做「规范化存储」,破坏了原值

sql
-- 不推荐!存储时转大写会丢失原始信息,且不可逆
UPDATE users SET username = upper(username);

-- 推荐:保持原值,用转换只做比较
SELECT id FROM users WHERE lower(username) = lower('LiSi');

大小写转换与性能 ​

upper / lower / initcap 都是 IMMUTABLE,可用于表达式索引,但都有 O(n) 的解码与逐字符处理成本:

sql
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;
text
 proname  | volatility
----------+------------
 casefold | IMMUTABLE
 initcap  | IMMUTABLE
 lower    | IMMUTABLE
 upper    | IMMUTABLE

initcap 比 upper / lower 更贵,因为它还需要做单词边界识别:

sql
-- 在大表上比较三者成本
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%';
text
-- 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 倍,能不用就不用。

实战示例 ​

场景一:统一邮箱存储并建立不区分大小写唯一约束 ​

需求:邮箱必须全局唯一且不区分大小写,但展示时要保留用户输入的原始形式。

sql
-- 步骤 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;
text
 email_lower | cnt | ids | raw_emails
-------------+-----+-----+------------

本例数据无冲突。制造一个便于观察的冲突场景:

sql
-- 步骤 2:插入一条仅大小写不同的邮箱,验证是否会冲突
INSERT INTO users (username, email, full_name, country, status)
VALUES ('lisi2', 'LISI@example.com', 'li si', 'CN', 'active');
text
INSERT 0 1
sql
-- 步骤 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;
text
 email_lower     | cnt |  ids  |         raw_emails
-----------------+-----+-------+----------------------------
 lisi@example.com |   2 | {2,6} | {lisi@example.com,"LISI@example.com"}
sql
-- 步骤 4:清理冲突数据后建立不区分大小写的唯一索引
DELETE FROM users WHERE id = 6;

CREATE UNIQUE INDEX idx_users_email_lower ON users (lower(email));
text
CREATE INDEX
sql
-- 步骤 5:验证约束生效
INSERT INTO users (username, email, full_name, country, status)
VALUES ('lisi3', 'LISI@example.com', 'li si', 'CN', 'active');
text
ERROR:  duplicate key value violates unique constraint "idx_users_email_lower"
DETAIL:  Key (lower(email))=(lisi@example.com) already exists.
sql
-- 步骤 6:展示时保留原值,仅比较时归一
SELECT
  id,
  email              AS raw_email,
  lower(email)       AS normalized
FROM users
ORDER BY id;
text
 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.com

PostgreSQL 18 下更严谨的写法是 casefold:

sql
-- 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;
text
 by_eq | by_casefold
-------+--------------
 f     | t

场景二:统计数据质量问题 ​

需求:检查 status 字段的大小写是否统一(业务要求全大写),并批量修复。

sql
-- 步骤 1:按原值分布统计,暴露不一致
SELECT
  status            AS raw,
  count(*)          AS cnt
FROM users
GROUP BY status
ORDER BY cnt DESC, raw;
text
  raw   | cnt
--------+-----
 active |    2
 ACTIVE |    2
 Active |    1
sql
-- 步骤 2:按归一后的值统计,确认归一能收敛
SELECT
  upper(status)  AS normalized,
  count(*)       AS cnt,
  count(DISTINCT status) AS variants
FROM users
GROUP BY upper(status)
ORDER BY cnt DESC;
text
 normalized | cnt | variants
------------+-----+----------
 ACTIVE     |   5 |        3

三种写法收敛为 1 个值,variants = 3 说明数据不干净。

sql
-- 步骤 3:先预览修复影响行数(不要直接 UPDATE)
SELECT count(*) AS rows_to_fix
FROM users
WHERE status <> upper(status);
text
 rows_to_fix
-------------
            3
sql
-- 步骤 4:加 CHECK 约束把规则固化,防止再次污染
ALTER TABLE users
  ADD CONSTRAINT ck_users_status_upper CHECK (status = upper(status));

-- 此时 UPDATE 会因约束失败,必须先修复数据
text
ERROR:  check constraint "ck_users_status_upper" of relation "users" is violated by some row
sql
-- 步骤 5:修复数据后约束即可加上
UPDATE users SET status = upper(status) WHERE status <> upper(status);

ALTER TABLE users
  ADD CONSTRAINT ck_users_status_upper CHECK (status = upper(status));
text
ALTER TABLE
sql
-- 步骤 6:验证
SELECT status, count(*) FROM users GROUP BY status;
text
 status | cnt
--------+-----
 ACTIVE |   5

注意 CHECK (status = upper(status)) 这个写法的巧妙之处:它同时约束了「必须全大写」与「不能含无法转换的字符」,且因 upper 是 IMMUTABLE,约束合法。

场景三:生成规范化展示名 ​

需求:把 full_name(拼音小写)转成合适的展示名,中文人名不做首字母大写。

sql
-- 步骤 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;
text
 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
sql
-- 步骤 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;
text
   es_name    |    fr_name     |   de_name
--------------+----------------+--------------
 María García | Élève Français | Über Alles

initcap 在 UTF-8 locale 下能正确处理带变音符号的欧洲语言字母——但前提是 lc_ctype 不是 C:

sql
SELECT
  initcap('maría' COLLATE "C")     AS c_locale,     -- 变音字符不转换
  initcap('maría')                  AS utf8_locale;
text
 c_locale | utf8_locale
----------+-------------
 maría    | María

注意事项 ​

  1. upper / lower 依赖 lc_ctype,C locale 下只处理 ASCII,非 ASCII 字符不转换。
  2. upper('straße') 可能返回 STRASSE,长度会变,对定长字段或被约束的列要格外小心:
    sql
    -- 错误!VARCHAR(6) 装不下 upper 后的 7 个字符
    CREATE TABLE t (code VARCHAR(6) CHECK (code = upper(code)));
  3. 土耳其语 locale 下 upper('i') 是 İ 而非 I,跨系统做大小写归一必须统一 locale。
  4. initcap 的单词边界是任何非字母数字字符,hello-world → Hello-World、e-mail → E-Mail,与多数人的预期不符。
  5. initcap 不适合处理人名,van der berg → Van Der Berg、mcdonald → Mcdonald 都是错的。
  6. initcap 会把中文字符当分隔符的邻接字母也大写,处理中文姓名要避免使用。
  7. lower(NULL) 返回 NULL,拼接或聚合时要考虑空值传染。
  8. 在 WHERE 的索引列上套 lower / upper 会让索引失效,需要建表达式索引。
  9. 表达式索引的表达式必须与查询完全一致,lower(col) 与 lower(btrim(col)) 不能互相复用。
  10. ILIKE 默认不走 B-tree 索引,大表模糊匹配需要 pg_trgm + GIN。
  11. casefold 是 PostgreSQL 18 新增且仅支持 UTF8 编码;在 libc provider 下它等价于 lower,只有 PG_UNICODE_FAST / ICU 下才体现完整折叠能力。
  12. 不要把转换结果写回列来「规范化数据」,转换是不可逆的(Straße 与 STRASSE 都变成 STRASSE),会永久丢失原始信息。

最佳实践 ​

  1. 存储保持原值,比较时才归一:展示用户输入的原始形式,用表达式索引支撑大小写不敏感查询。
  2. 大小写不敏感唯一约束用 lower(col) 或 casefold(col) 表达式索引,本地化要求高时优先 18 版的 casefold。
  3. 排序规则敏感的场景显式指定 COLLATE,如 lower(col COLLATE "tr_TR"),避免隐式依赖库默认值。
  4. 写转换前先确认 lc_ctype:用 SELECT datctype FROM pg_database WHERE datname = current_database()。
  5. 需要区分语言做转换时用 CASE 分支,不要指望一个 initcap 通吃所有语言。
  6. 约束式规范化用 CHECK (col = upper(col)),让数据库强制格式统一,比事后清洗可靠。
  7. initcap 只用于英文标题、UI 标签,人名与专有名词必须走规则库或人工维护映射表。
  8. 大表避免在 WHERE / ORDER BY 中对列做转换,改用表达式索引或预先计算并存储归一列。
  9. 比较前先修剪再转换,顺序为 lower(btrim(col)),并让索引与之保持一致。
  10. 升级到 18 后优先评估 casefold,它能在不改变存储的前提下解决 ß / İ / 希腊语 sigma 等长期存在的边缘问题。

下一节

继续学习:REPLACE / TRANSLATE / REGEXP_REPLACE

参考链接 ​