LENGTH / CHAR_LENGTH / OCTET_LENGTH
长度函数家族共 5 个成员(含 bit_length、length(bytea)),它们看似都叫「长度」,实际回答的是三个不同问题:有几个字符、占几个字节、占几个比特。选错一个,业务校验就会在中文、Emoji、变长编码上翻车。本节将把字符与字节的边界讲透,并给出校验、截断、估算存储三类场景的正确写法。
前置知识
阅读本节前,建议先了解:SUBSTRING / LEFT / RIGHT / TRIM
基础概念
| 概念 | 术语 | 说明 |
|---|---|---|
| 字符(Character) | Char | 解码后的最小可见单元,一个汉字算 1 个 |
| 码点(Code Point) | Code Point | Unicode 编号,如 U+4E2D 表示「中」 |
| 字节(Byte) | Octet | 存储单位,UTF-8 下汉字通常占 3 字节 |
| 比特(Bit) | Bit | 位,1 字节 = 8 比特 |
| 变长编码 | Variable-Length Encoding | UTF-8 中不同字符占不同字节数 |
| 组合字符 | Combining Character | 由基础字符 + 附加符号组成,字符数为 1 但码点多于 1 |
| 字符长度语义 | Character Length | 业务常说的「长度限制」 |
| 字节长度语义 | Octet Length | 协议、存储、网络传输关心的长度 |
五个长度函数的行为对照(以 '中国' 和 'a' 为例):
| 函数 | 返回值含义 | '中国' | 'a' | 典型用途 |
|---|---|---|---|---|
length(text) | 字符数 | 2 | 1 | 业务长度校验 |
char_length(text) | 字符数(SQL 标准名) | 2 | 1 | 同上,可移植性更好 |
character_length(text) | 同 char_length | 2 | 1 | 同上,全拼写法 |
octet_length(text) | 字节数 | 6 | 1 | 存储估算、协议对账 |
bit_length(text) | 比特数 | 48 | 8 | 极少使用 |
length(bytea) | 字节数 | — | — | 二进制数据长度 |
关键要点:length 是重载函数。当入参是 text 时它算字符,当入参是 bytea 时它算字节:
SELECT
length('中国'::text) AS len_text, -- 字符数:2
length('中国'::bytea) AS len_bytea, -- 字节数:6
octet_length('中国'::text) AS octet_text, -- 字节数:6
bit_length('中国'::text) AS bit_text; -- 比特数:48 len_text | len_bytea | octet_text | bit_text
----------+-----------+------------+----------
2 | 6 | 6 | 48语法与用法
-- 字符长度
length(str) -- text 入参时等价于 char_length
char_length(str) -- SQL 标准
character_length(str) -- SQL 标准全拼
-- 字节长度
octet_length(str) -- 对 text / bytea 均返回字节数
length(str::bytea) -- 转二进制后再取长度
-- 比特长度
bit_length(str) -- 等价于 octet_length * 8
-- 二进制长度
length(bytea)最小可运行示例:
SELECT
length('PostgreSQL') AS en_len,
length('数据库') AS cn_len,
octet_length('PostgreSQL') AS en_bytes,
octet_length('数据库') AS cn_bytes,
char_length('数据库') AS cn_chars,
bit_length('数据库') AS cn_bits; en_len | cn_len | en_bytes | cn_bytes | cn_chars | cn_bits
--------+--------+----------+----------+----------+---------
10 | 3 | 10 | 9 | 3 | 72准备本节贯穿使用的示例表:
CREATE TABLE users (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username TEXT NOT NULL,
full_name TEXT,
bio TEXT, -- 简介,限 100 字符
phone TEXT,
avatar_note TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO users (username, full_name, bio, phone) VALUES
('zhangsan', '张三', 'DBA,专注 PostgreSQL 18 性能优化', '13800000001'),
('lisi', '李四', '后端开发', '13800000002'),
('wangwu', '王五👨💻', '全栈工程师,热爱开源与写作,喜欢咖啡', NULL),
('zhaoliu', '赵六', NULL, '13800000004'),
('sunqi', '孙七', '数据库内核研发(PG / MySQL)', '13800000005');-- 对比:同一个 bio 字段的字符数与字节数
SELECT
id,
length(bio) AS chars,
octet_length(bio) AS bytes,
round(octet_length(bio)::numeric / nullif(length(bio), 0), 2) AS bytes_per_char
FROM users
ORDER BY id; id | chars | bytes | bytes_per_char
----+-------+-------+----------------
1 | 26 | 62 | 2.38
2 | 4 | 12 | 3.00
3 | 17 | 51 | 3.00
4 | | |
5 | 16 | 48 | 3.00第 1 行 bytes_per_char = 2.38——因为混合了 ASCII 字符(1 字节)与汉字(3 字节)。这个指标是判断文本「偏英文还是偏中文」的快捷方式。
详细说明
字符数:length 与 char_length
两者对 text 入参完全等价,选择取决于是否需要 SQL 标准可移植性:
SELECT
length('数据库') AS by_length,
char_length('数据库') AS by_char_length,
character_length('数据库') AS by_full,
length('abc') AS ascii_len,
length('') AS empty_len,
length(NULL) AS null_len; by_length | by_char_length | by_full | ascii_len | empty_len | null_len
-----------+----------------+---------+-----------+-----------+----------
3 | 3 | 3 | 3 | 0 |三个必须记住的结论:
length('')返回0,而length(NULL)返回NULL。做非空校验时,length(col) = 0与col IS NULL是两件不同的事;length按字符计,length('中国')= 2,不是 6;length与char_length都是IMMUTABLE,可用于表达式索引。
验证易变性:
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 ('length', 'char_length', 'character_length', 'octet_length', 'bit_length')
AND p.pronamespace = 'pg_catalog'::regnamespace
AND pg_get_function_arguments(p.oid) LIKE '%text%'
ORDER BY p.proname, pg_get_function_arguments(p.oid); proname | volatility
------------------+------------
bit_length | IMMUTABLE
char_length | IMMUTABLE
character_length | IMMUTABLE
length | IMMUTABLE
octet_length | IMMUTABLE字节数:octet_length
octet_length 是判断「存储与传输成本」的唯一正确工具:
SELECT
octet_length('a') AS ascii_1, -- 1
octet_length('中') AS cn_1, -- 3(UTF-8)
octet_length('€') AS euro, -- 3(U+20AC)
octet_length('😀') AS emoji, -- 4(U+1F600,超出 BMP)
octet_length(E'\n') AS newline, -- 1
octet_length('') AS empty_len; -- 0 ascii_1 | cn_1 | euro | emoji | newline | empty_len
---------+------+------+-------+---------+-----------
1 | 3 | 3 | 4 | 1 | 0UTF-8 的编码长度规律:
| 码点范围 | 字节数 | 示例 |
|---|---|---|
U+0000 ~ U+007F | 1 | a、0、_ |
U+0080 ~ U+07FF | 2 | é、ß、西里尔字母 |
U+0800 ~ U+FFFF | 3 | 汉字、日文、韩文 |
U+10000 ~ U+10FFFF | 4 | Emoji、生僻汉字 |
若数据库编码不是 UTF-8(如 GBK 或 SQL_ASCII),字节数会完全不同,务必先确认:
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-8varchar(n) 的 n 按字符计
这是最容易被误解的一处:VARCHAR(10) 允许 10 个字符,而不是 10 个字节。
CREATE TEMP TABLE t_len (a VARCHAR(10), b CHAR(4));
-- 可以插入 10 个汉字(占 30 字节)
INSERT INTO t_len (a, b) VALUES ('十个汉字测试长度呀', '中国');
SELECT length(a) AS chars, octet_length(a) AS bytes FROM t_len; chars | bytes
-------+-------
10 | 30-- 错误!超过 10 个字符会被拒绝(不是按字节)
INSERT INTO t_len (a) VALUES ('这是十一个汉字测试呀');ERROR: value too long for type character varying(10)而 CHAR(4)(定长)的 n 同样按字符计,且存储时会用空格补齐:
SELECT
length(b) AS char_len_trimmed, -- 去尾空格后
octet_length(b) AS octet, -- 补齐后
b || '|' AS appended; -- 拼接时尾空格通常被裁剪
FROM t_len; char_len_trimmed | octet | appended
------------------+-------+----------
2 | 4 | 中国|注意 CHAR(4) 存 '中国'(2 字符)时占用 4 个字符位,但 length(b) 返回 2(尾空格被裁剪),octet_length(b) 返回 4(补齐为 4 字节)。char 类型的这一双重性历史上造成过很多 bug,现代建模应优先用 text + CHECK 约束。
length 与 Emoji、组合字符
字符长度的边界在于「组合字符」:某些可见符号由多个码点组成。
SELECT
'👨💻' AS zwj_emoji,
length('👨💻') AS len_zwj,
octet_length('👨💻') AS octet_zwj,
length('é') AS len_precomposed, -- U+00E9,单码点
length('e' || U&'\0301') AS len_combined_char; -- e + 组合重音 zwj_emoji | len_zwj | octet_zwj | len_precomposed | len_combined_char
-----------+---------+-----------+-----------------+-------------------
👨💻 | 3 | 11 | 1 | 2'👨💻'(程序员 Emoji)由「男人」+ 零宽连接符 + 「电脑」3 个码点组成,length返回 3 而不是 1;'é'若用组合形式(e+ 组合重音)输入,length返回 2 而不是 1。
这意味着「字符数」不等于「人类感知的可见字符数」(字素,Grapheme)。若业务严格按用户感知计数(如微博字数限制),PostgreSQL 内置函数无法直接满足,需要应用层用 ICU 的字素切分能力处理。
一个实用的近似手段是 normalize 做 Unicode 规范化,把组合字符合并为预组合形式:
SELECT
length('e' || U&'\0301') AS before_norm, -- 2
length(normalize('e' || U&'\0301', NFC)) AS after_nfc, -- 1
length(normalize('é', NFD)) AS to_nfd; -- 2 before_norm | after_nfc | to_nfd
-------------+-----------+--------
2 | 1 | 2NFC(规范合成)把 e + 组合重音合并为单码点 é,长度从 2 变 1。处理用户输入的长度校验前,先规范化能显著减少边界争议。
length 与 trim 的组合陷阱
长度校验前是否修剪,会导致完全不同的结果:
SELECT
length(' abc ') AS raw_len, -- 7
length(btrim(' abc ')) AS trimmed_len, -- 3
length(nullif(btrim(' '), '')) AS empty_to_null;-- NULL raw_len | trimmed_len | empty_to_null
---------+-------------+---------------
7 | 3 |校验顺序固定为「先修剪 → 再判空 → 再算长度」:
-- 推荐的长度校验写法
SELECT
id,
btrim(bio) AS clean_bio,
length(btrim(bio)) AS char_len,
length(btrim(bio)) BETWEEN 1 AND 100 AS len_ok
FROM users
WHERE bio IS NOT NULL
ORDER BY id; id | clean_bio | char_len | len_ok
----+--------------------------------------+----------+--------
1 | DBA,专注 PostgreSQL 18 性能优化 | 26 | t
2 | 后端开发 | 4 | t
3 | 全栈工程师,热爱开源与写作,喜欢咖啡 | 17 | t
5 | 数据库内核研发(PG / MySQL) | 16 | t长度与索引:表达式索引与 CHECK 约束
length 是 IMMUTABLE,因此可以在表达式索引与 CHECK 约束中使用:
-- 用 CHECK 约束把长度规则下沉到数据库层
ALTER TABLE users
ADD CONSTRAINT ck_users_bio_len CHECK (bio IS NULL OR length(btrim(bio)) <= 100);
ALTER TABLE users
ADD CONSTRAINT ck_users_username_len CHECK (length(username) BETWEEN 3 AND 32);-- 用表达式索引支撑「按长度筛选」的查询
CREATE INDEX idx_users_bio_len ON users ((length(bio)));
ANALYZE users;
EXPLAIN (COSTS OFF)
SELECT id FROM users WHERE length(bio) > 15; QUERY PLAN
----------------------------------------------------------
Index Scan using idx_users_bio_len on users
Index Cond: (length(bio) > 15)注意约束与索引都要保证表达式完全一致——上面索引用 length(bio),而约束用 length(btrim(bio)),两者不能互相复用。
用 CHECK 约束拦截超长输入,比在应用层「尽量校验」可靠得多:
-- 验证约束生效
UPDATE users SET bio = repeat('长', 101) WHERE id = 2;ERROR: new row for relation "users" violates check constraint "ck_users_bio_len"
DETAIL: Failing row contains (2, lisi, 李四, 长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长, 13800000002, null, 2026-09-24 10:00:00+08).长度与性能:对大文本调用有何代价
length(text) 需要遍历整个字符串解码计数,时间复杂度 O(n)。对少量小文本毫无影响,但在大表上做全表 length 计算会有明显代价:
-- 不推荐:对 100 万行的大文本逐行计算长度
SELECT count(*) FROM big_docs WHERE length(content) > 1000;
-- 推荐:若只是判断「是否超长」,可先用便宜的短路条件
SELECT count(*) FROM big_docs
WHERE content IS NOT NULL AND length(content) > 1000;PostgreSQL 对 text 长度有一个便宜的短路优化——varlena 头里存了总字节数,因此 octet_length 是 O(1) 的,而 length 需要真正扫描计数:
| 函数 | 复杂度 | 说明 |
|---|---|---|
octet_length(text) | O(1) | 直接读取 varlena 头中的长度字段 |
bit_length(text) | O(1) | 基于 octet_length |
length(text) | O(n) | 需逐字符解码计数 |
char_length(char(n)) | O(1) | 定长类型长度已知 |
若业务只关心「是否超过某个字节阈值」,用 octet_length 更快。验证两者差异:
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) FROM users WHERE octet_length(bio) > 20;
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) FROM users WHERE length(bio) > 20;-- octet_length
Aggregate (actual rows=1 loops=1)
-> Seq Scan on users (actual rows=1 loops=1)
Filter: (octet_length(bio) > 20)
-- length
Aggregate (actual rows=1 loops=1)
-> Seq Scan on users (actual rows=1 loops=1)
Filter: (length(bio) > 20)小表下差异不明显,但在 TB 级文本表上,octet_length 的 O(1) 特性可以带来显著差异。
数组的长度:cardinality 与 array_length
length 只接受字符串与 bytea,没有数组重载,对数组要换用专门的函数:
-- 错误!length 没有数组重载,会尝试隐式转换后报错
SELECT length(ARRAY['a', 'b', 'c']);ERROR: function length(text[]) does not exist
HINT: No function matches the given name and argument types. You might need to add explicit type casts.数组长度要用 cardinality(基数)或 array_length:
| 函数 | 返回值 | 空数组时 | 说明 |
|---|---|---|---|
cardinality(anyarray) | 所有维度的元素总数 | 0 | 推荐,最直观 |
array_length(anyarray, dim) | 指定维度的长度 | NULL | 维度越界也返回 NULL |
array_ndims(anyarray) | 维度数 | NULL | 一维数组返回 1 |
两者在空数组上的差异是最经典的陷阱:
-- 空数组下 cardinality 返回 0,array_length 返回 NULL
SELECT
cardinality(ARRAY[]::text[]) AS card_empty,
array_length(ARRAY[]::text[], 1) AS arrlen_empty,
cardinality(ARRAY['a', 'b', 'c']) AS card_three,
array_length(ARRAY['a', 'b', 'c'], 1) AS arrlen_three; card_empty | arrlen_empty | card_three | arrlen_three
------------+--------------+------------+--------------
0 | | 3 | 3用 array_length 做 > 0 判断时,空数组因 NULL 会被静默过滤掉,与字符串那套「NULL 与 '' 分开处理」的教训完全一致:
-- 错误!空数组因 array_length 返回 NULL 而被漏掉
SELECT count(*) FROM products WHERE array_length(tags, 1) > 0;
-- 正确:用 cardinality,空数组返回 0 能正确参与比较
SELECT count(*) FROM products WHERE cardinality(tags) > 0;多维数组时 cardinality 是各维度长度之积,需按维度取值时只能用 array_length:
-- 多维数组:cardinality 返回总元素数,array_length 取单维
SELECT
cardinality(ARRAY[[1, 2, 3], [4, 5, 6]]) AS card_2d,
array_length(ARRAY[[1, 2, 3], [4, 5, 6]], 1) AS dim1,
array_length(ARRAY[[1, 2, 3], [4, 5, 6]], 2) AS dim2,
array_length(ARRAY[[1, 2, 3], [4, 5, 6]], 3) AS dim3_out_of_range; card_2d | dim1 | dim2 | dim3_out_of_range
---------+------+------+-------------------
6 | 2 | 3 |integer[3] 这种声明里的长度会被 PostgreSQL 完全忽略,它不构成约束,真正的限制要自己写:
-- 不推荐:声明里的 3 只是文档,不产生任何约束
CREATE TABLE bad_tags (id integer, tags text[3]);
-- 推荐:用 cardinality 写成 CHECK 约束,才是真约束
CREATE TABLE products (
id integer PRIMARY KEY,
name text NOT NULL,
tags text[] NOT NULL DEFAULT '{}',
CONSTRAINT ck_products_tags_len CHECK (cardinality(tags) <= 3)
);-- 验证约束生效
INSERT INTO products (id, name, tags)
VALUES (1, 'T 恤', ARRAY['服装', '上衣', '夏季', '促销']);ERROR: new row for relation "products" violates check constraint "ck_products_tags_len"
DETAIL: Failing row contains (1, T 恤, {服装,上衣,夏季,促销}).同理,索引与聚合也走数组自己的路径:数组元素计数用 cardinality,展开成行统计用 unnest 后 count(*),二者语义不同。
实战示例
场景一:校验并统计用户输入
需求:bio 限制 100 字符,username 限制 3~32 字符,需要一次性找出所有不合规数据。
-- 步骤 1:定义校验规则并分类统计
SELECT
count(*) AS total,
count(*) FILTER (WHERE length(username) < 3) AS username_too_short,
count(*) FILTER (WHERE length(username) > 32) AS username_too_long,
count(*) FILTER (WHERE bio IS NOT NULL AND length(btrim(bio)) > 100) AS bio_too_long,
count(*) FILTER (WHERE bio IS NOT NULL AND btrim(bio) = '') AS bio_blank
FROM users; total | username_too_short | username_too_long | bio_too_long | bio_blank
-------+--------------------+-------------------+--------------+-----------
5 | 0 | 0 | 0 | 0-- 步骤 2:列出可能的「隐形超长」——字符数合规但字节数惊人
SELECT
id,
username,
length(bio) AS chars,
octet_length(bio) AS bytes,
mb_bytes
FROM (
SELECT id, username, bio, round(octet_length(bio) / 1024.0, 3) AS mb_bytes FROM users
) t
WHERE octet_length(bio) > 30
ORDER BY bytes DESC; id | username | chars | bytes | mb_bytes
----+----------+-------+-------+----------
1 | zhangsan | 26 | 62 | 0.061
3 | wangwu | 17 | 51 | 0.050
5 | sunqi | 16 | 48 | 0.047-- 步骤 3:估算整列存储占用(含 varlena 头部开销)
SELECT
pg_size_pretty(sum(octet_length(bio))::bigint) AS raw_text,
pg_size_pretty(sum(octet_length(bio) + 4)::bigint) AS with_header,
pg_size_pretty(pg_total_relation_size('users')) AS table_total,
pg_size_pretty(pg_relation_size('users')) AS table_main
FROM users; raw_text | with_header | table_total | table_main
----------+-------------+-------------+------------
161 bytes | 173 bytes | 8192 kB | 8192 kB小表下「1 页 = 8 KB」是最小单位,所以 pg_relation_size 直接显示 8192 kB。
场景二:按字节长度截断以适配外部系统
需求:对接一个只接受 64 字节的旧系统字段,必须按字节截断,且不能把 UTF-8 字符切成半个。
-- 步骤 1:先观察错位截断的后果(用 bytea 演示)
SELECT
convert_from(substring(convert_to('中文测试内容', 'UTF8') FROM 1 FOR 4), 'UTF8') AS bad_cut; bad_cut
---------
中只取了 4 字节(1 个完整汉字 3 字节 + 1 个残缺字节),convert_from 丢弃了残缺部分。这种做法不可靠。
-- 步骤 2:正确的按字节截断函数(保证不切坏字符)
CREATE OR REPLACE FUNCTION truncate_bytes(str text, max_bytes integer)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
result text := '';
ch text;
BEGIN
IF str IS NULL OR max_bytes <= 0 THEN
RETURN NULL;
END IF;
FOR ch IN SELECT regexp_split_to_table(str, '') LOOP
EXIT WHEN octet_length(result) + octet_length(ch) > max_bytes;
result := result || ch;
END LOOP;
RETURN result;
END $$;
SELECT
'中文测试内容abc' AS raw,
octet_length('中文测试内容abc') AS raw_bytes,
truncate_bytes('中文测试内容abc', 8) AS cut8,
octet_length(truncate_bytes('中文测试内容abc', 8)) AS cut8_bytes,
truncate_bytes('中文测试内容abc', 64) AS cut64; raw | raw_bytes | cut8 | cut8_bytes | cut64
----------------+-----------+--------+------------+-----------------
中文测试内容abc | 21 | 中文 | 6 | 中文测试内容abccut8 得到 中文(6 字节),因为再加一个汉字会超过 8 字节,函数正确地停止在字符边界上。
-- 步骤 3:在表上验证截断效果
SELECT
id,
bio AS raw,
length(bio) AS chars,
octet_length(bio) AS bytes,
truncate_bytes(bio, 20) AS cut,
octet_length(truncate_bytes(bio, 20)) AS cut_bytes
FROM users
WHERE bio IS NOT NULL
ORDER BY id; id | raw | chars | bytes | cut | cut_bytes
----+-------------------------------+-------+-------+--------------------+-----------
1 | DBA,专注 PostgreSQL 18 性能优化 | 26 | 62 | DBA,专注 P | 13
2 | 后端开发 | 4 | 12 | 后端开发 | 12
3 | 全栈工程师,热爱开源与写作,喜欢咖啡 | 17 | 51 | 全栈工程师,热爱开 | 24
5 | 数据库内核研发(PG / MySQL) | 16 | 48 | 数据库内核研发(PG | 21注意 cut_bytes 都 ≤ 20,且都是完整字符。
注意事项
length(NULL)返回NULL,length('')返回0,做校验时这两个分支必须分别处理:sql-- 错误!NULL 行会被静默漏掉 SELECT * FROM users WHERE length(bio) > 0; -- 正确 SELECT * FROM users WHERE bio IS NOT NULL AND length(btrim(bio)) > 0;length是重载函数,length(text)算字符、length(bytea)算字节,混用会得到相反结论。- 不要用
length判断字节是否超限,该用octet_length。 VARCHAR(n)的n是字符数不是字节数,VARCHAR(10)能存 10 个汉字(30 字节)。CHAR(n)的长度语义有双重性:length会裁剪尾空格,octet_length则是补齐后长度,容易对不上账。- Emoji 与组合字符会让「字符数」大于「可见字符数」,
'👨💻'的length是 3,严格按用户感知计数需应用层处理字素。 - 组合形式的
é(e+ 重音)长度为 2,做长度校验前建议先normalize(..., NFC)。 length是 O(n) 而octet_length是 O(1),大文本表上做阈值筛选优先用后者。- 校验前必须先
btrim,否则首尾空格会虚增长度、也会让「全是空格」通过校验。 CHECK约束与表达式索引的表达式必须逐字符一致,length(bio)与length(btrim(bio))互不复用。length不要写在WHERE的索引列上,需要按长度筛选时建表达式索引。- 数据库编码非 UTF-8 时字节规律不成立,用
pg_encoding_to_char(encoding)先确认。 length不能用于数组,数组长度用cardinality;用array_length判断时空数组返回NULL,> 0会把空数组静默漏掉。text[3]声明里的长度会被 PostgreSQL 忽略,要真正限制元素个数需写CHECK (cardinality(col) <= 3)。
最佳实践
- 业务长度校验统一用
char_length(SQL 标准名),可移植性优于length,含义对审核者更直观。 - 协议对账、存储估算、网络传输用
octet_length,并记住octet_length是 O(1)。 - 长度校验固定顺序:
btrim→nullif(..., '')→length→ 比较阈值。 - 把长度规则写成
CHECK约束,让数据库成为最后一道防线,而不是只依赖应用层。 - 建模优先
text+CHECK,避免char(n)的补齐语义带来的长度歧义。 - 阈值筛选建表达式索引(
((length(bio)))),并让查询表达式与索引定义完全一致。 - 需要按字节截断时用
convert_to/convert_from或字符遍历,不要用substring(col::bytea, ...)直接切,会切坏多字节字符。 - 外部系统对接前先做字符集对账,确认双方编码一致(UTF-8 / GBK),否则字节数无法对比。
- 统计文本「英文还是中文」用
octet_length / length的比值,接近 1 是纯 ASCII,接近 3 是纯汉字。 - 把长度校验放在数据入口(导入、表单提交)而非出口,越早拦截成本越低。
下一节