CHAR / VARCHAR / TEXT
本节讲解 PostgreSQL 的三种字符类型 CHAR、VARCHAR 与 TEXT。你将理解它们的语义差异、长度检查行为、存储方式,以及"该用哪一种"这个长期争论的正确答案。
前置知识
阅读本节前,建议先了解:数值类型选择指南
基础概念
PostgreSQL 提供三种字符类型,它们在存储方式上完全一致,差异只在约束与填充语义。
| 类型名 | 别名 | 存储空间 | 长度限制 | 超长时行为 | 说明 |
|---|---|---|---|---|---|
CHARACTER(n) | CHAR(n)、BPCHAR(n) | 变长(最短 1 字节 + 头) | n 个字符 | 报错 | 定长,不足时用空格补齐 |
CHARACTER VARYING(n) | VARCHAR(n) | 变长 | n 个字符 | 报错 | 变长,有长度上限 |
CHARACTER VARYING | VARCHAR | 变长 | 无限 | — | 变长,无长度上限 |
TEXT | — | 变长 | 无限 | — | 变长,无长度上限 |
关键术语:
- 字符(Character):这里指"字符"而非"字节",长度限制按字符计数,与编码无关。
- 填充语义(Padding Semantics):
CHAR(n)会把短值右侧补空格到 n 个字符。 - 变长类型(Variable-Length Type):只存实际内容,不补齐。
varlena头(Var-length Header):所有变长类型的前 1~4 字节元数据,记录长度。
最重要的一个事实:PostgreSQL 的 CHAR(n) 在磁盘上并不定长,它只是"语义上定长"(比较时会忽略尾部空格),存储上仍然是变长的。这让 CHAR(n) 相对 VARCHAR(n) 毫无空间优势。
SELECT
pg_column_size('a'::CHAR(100)) AS char100_size,
pg_column_size('a'::VARCHAR(100)) AS varchar100_size,
pg_column_size('a'::TEXT) AS text_size; char100_size | varchar100_size | text_size
--------------+-----------------+-----------
2 | 2 | 2CHAR(100) 里只放一个字符,实际只占 2 字节(1 字节内容 + 1 字节头),不是 100 字节。
语法与用法
列名 CHARACTER(n) | CHAR(n) | CHARACTER VARYING(n) | VARCHAR(n) | VARCHAR | TEXT-- 最简可运行示例
CREATE TABLE customers (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
code CHAR(4) NOT NULL, -- 定长业务码,如 'A001'
name VARCHAR(64) NOT NULL, -- 有上限的姓名
email TEXT, -- 长度不可预估
remark VARCHAR DEFAULT '' -- 无上限变长(等于 TEXT)
);
INSERT INTO customers (code, name, email, remark) VALUES
('A001', '张三', 'zhangsan@example.com', 'VIP 客户'),
('B02', '李四', 'lisi@example.com', NULL);
SELECT id, code, '[' || code || ']' AS code_bracket, name, email, remark
FROM customers ORDER BY id; id | code | code_bracket | name | email | remark
----+------+--------------+------+---------------------+----------
1 | A001 | [A001] | 张三 | zhangsan@example.com| VIP 客户
2 | B02 | [B02 ] | 李四 | lisi@example.com |
(2 rows)注意 B02 被存成 'B02 '(后面带一个空格),在拼接时被 [B02 ] 暴露了出来——这正是 CHAR(n) 填充语义的直观体现。
详细说明
CHAR(n) 的填充与空格处理
CHAR(n) 有两个容易误导人的行为:
- 写入时补齐:短值右侧补空格到 n 个字符。
- 比较时忽略尾部空格:
'abc'::CHAR(5) = 'abc '::CHAR(5)为真。
SELECT
length('abc'::CHAR(5)) AS raw_length,
length(trim(trailing from 'abc'::CHAR(5))) AS trimmed_length,
('abc'::CHAR(5) = 'abc') AS eq_without_pad,
('abc'::CHAR(5) = 'abc ') AS eq_with_pad; raw_length | trimmed_length | eq_without_pad | eq_with_pad
------------+----------------+----------------+-------------
3 | 3 | t | t
(1 row)length() 返回 3 而不是 5——PostgreSQL 内部会忽略 CHAR 的尾部空格。但一旦把值转成 TEXT 或参与字符串拼接,空格又"回来了":
SELECT
length('abc'::CHAR(5)::TEXT) AS to_text_length,
length('abc'::CHAR(5) || '') AS concat_length,
octet_length('abc'::CHAR(5)) AS octets; to_text_length | concat_length | octets
----------------+---------------+--------
3 | 3 | 3
(1 row)这说明尾部空格在 PostgreSQL 内部是被"抹掉"的,CHAR(n) 并不真的在磁盘上存了补白空格(这也解释了为什么它没有空间优势)。
长度检查行为
三种类型的长度检查规则不同,这是实际开发中最容易踩的地方。
| 类型 | 写入超长值 | 写入含尾部空格的值 |
|---|---|---|
CHAR(n) | 报错(除非超出部分全是空格) | 补到 n 个字符 |
VARCHAR(n) | 报错 | 原样保留空格 |
VARCHAR / TEXT | 不检查 | 原样保留空格 |
-- CHAR(n):超长报错
SELECT 'abcdef'::CHAR(3);ERROR: value too long for type character(3)-- CHAR(n):超出部分全是空格时不报错,直接截掉
SELECT 'ab '::CHAR(3) AS padded_ok; padded_ok
-----------
ab
(1 row)-- VARCHAR(n):超长报错
SELECT 'abcdef'::VARCHAR(3);ERROR: value too long for type character varying(3)-- TEXT:无限制
SELECT length('abcdef'::TEXT) AS n; n
---
6
(1 row)注意 VARCHAR(n) 的长度检查按字符数而非字节数:
SELECT length('中文测试'::TEXT) AS chars, octet_length('中文测试'::TEXT) AS bytes; chars | bytes
-------+-------
4 | 12
(1 row)4 个中文字符在 UTF-8 下占 12 字节,但 VARCHAR(4) 能装下,因为限制针对 4 个字符。
SELECT '中文测试'::VARCHAR(4) AS ok; ok
----------
中文测试
(1 row)存储方式:完全一致
这是最关键的一点:三种类型在存储层用的是同一套机制。
CREATE TABLE storage_cmp (
a CHAR(10),
b VARCHAR(10),
c TEXT
);
INSERT INTO storage_cmp VALUES ('abc', 'abc', 'abc');
SELECT
pg_column_size(a) AS char_bytes,
pg_column_size(b) AS varchar_bytes,
pg_column_size(c) AS text_bytes
FROM storage_cmp; char_bytes | varchar_bytes | text_bytes
------------+---------------+------------
4 | 4 | 4
(1 row)三者都是 4 字节(3 字节内容 + 1 字节头)。差别只在:
CHAR(n)写入时补空格(但内部会 trim)VARCHAR(n)写入前做长度检查TEXT不做任何检查
长度限制是"约束",不是"存储格式",这是理解这三种类型的关键。
长度限制的实际影响
VARCHAR(n) 的 n 只影响写入时是否报错,不改变存储。那么它有没有用?
有用的场景:
- 防御性约束:字段在业务上确实有上限(如手机号 20 字符),
VARCHAR(20)能挡住脏数据。 - 保护下游系统:下游系统的字段宽度固定,
VARCHAR(n)能保证不超。 - 表达业务语义:
code VARCHAR(8)明确告诉读者这个字段的长度预期。
没有用的场景:
- 性能优化:
VARCHAR(50)与TEXT性能完全一样,没有"定长更快"这回事。 - 空间优化:同上,存储完全一致。
代价:
-- 改长度需要 ALTER TABLE,在旧版本会锁表重写
ALTER TABLE customers ALTER COLUMN name TYPE VARCHAR(128);ALTER TABLE从 PostgreSQL 9.2 起,增大 VARCHAR(n) 的 n 是元数据变更,不重写表;但缩小 n 需要全表扫描校验。
-- 缩小长度:需要扫描全表检查是否有超长值
ALTER TABLE customers ALTER COLUMN name TYPE VARCHAR(16);ALTER TABLE如果表里有超长值,会直接失败:
ERROR: value too long for type character varying(16)三者该怎么选
| 场景 | 推荐 | 理由 |
|---|---|---|
| 任意业务文本(名称、描述、地址、备注) | TEXT | 无约束、灵活、与 VARCHAR 等价 |
| 已知业务上限且需要防御(手机号、邮编、业务码) | VARCHAR(n) | 用数据库兜底 |
| 严格定长且语义上是"定长码" | CHAR(n) | 极少数场景,如固定长度哈希片段 |
| 需要与外部系统定长字段对齐 | CHAR(n) | 但要小心空格填充 |
| PostgreSQL 内部使用 | TEXT 为主 | 社区共识 |
一个普遍被接受的社区共识是:在 PostgreSQL 中默认使用 TEXT,只有在确实需要长度上限时使用 VARCHAR(n),几乎不用 CHAR(n)。
原因:
- 三者性能与存储完全相同,
TEXT没有劣势。 TEXT没有长度上限,业务变化时不需要ALTER TABLE。VARCHAR(n)的长度上限经常成为"业务演进"的阻碍(如用户昵称从 16 改到 64)。CHAR(n)的填充语义导致的 bug 多于它带来的价值。
长度单位与编码的关系
长度限制按字符计数,与 server_encoding 有关:
SHOW server_encoding; server_encoding
-----------------
UTF8
(1 row)在 UTF8 编码下,一个字符最多占 4 字节,所以 VARCHAR(n) 理论上最多占 4n 字节:
SELECT
'a'::VARCHAR(10) AS ascii_val,
pg_column_size('a'::VARCHAR(10)) AS ascii_bytes,
pg_column_size('中'::VARCHAR(10)) AS cjk_bytes,
pg_column_size('𝄞'::VARCHAR(10)) AS emoji_bytes; ascii_val | ascii_bytes | cjk_bytes | emoji_bytes
-----------+-------------+-----------+-------------
a | 2 | 4 | 5
(1 row)一个 ASCII 字符 1 字节,一个中文字 3 字节,一个 4 字节的 emoji 占 4 字节 + 1 字节头 = 5 字节。n 是字符数,不是字节数,设计存储容量时要按最坏情况(4 倍)估算。
类型转换
三者之间可以自由转换:
SELECT
'abc'::TEXT::VARCHAR(5) AS text_to_varchar,
'abc '::VARCHAR(10)::CHAR(5) AS varchar_to_char,
'abcdef'::TEXT::CHAR(3) AS will_error;ERROR: value too long for type character(3)SELECT
'abc'::TEXT::VARCHAR(5) AS text_to_varchar,
'abc '::VARCHAR(10)::CHAR(5) AS varchar_to_char; text_to_varchar | varchar_to_char
-----------------+----------------
abc | abc
(1 row)VARCHAR → CHAR 会在转换时补空格,再转换回 TEXT 时尾部空格被 trim。
与其它类型的转换
字符串类型是所有类型的"通用入口",绝大多数类型都能从文本解析:
SELECT
'123'::TEXT::INTEGER AS to_int,
'2026-01-15'::TEXT::DATE AS to_date,
'{"a":1}'::TEXT::JSONB AS to_jsonb,
'true'::TEXT::BOOLEAN AS to_bool; to_int | to_date | to_jsonb | to_bool
--------+------------+--------------+---------
123 | 2026-01-15 | {"a": 1} | t
(1 row)反向转换也支持:
SELECT
pg_typeof(123::TEXT) AS int_to_text,
pg_typeof(true::TEXT) AS bool_to_text,
pg_typeof(now()::TEXT) AS ts_to_text; int_to_text | bool_to_text | ts_to_text
-------------+--------------+------------
text | text | text
(1 row)实战示例
下面通过一个用户中心表的设计,对比三种类型的实际差异。
第 1 步:建表并插入边界数据
DROP TABLE IF EXISTS user_center;
CREATE TABLE user_center (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
country CHAR(2) NOT NULL, -- ISO 3166 国家码,严格 2 位
nickname VARCHAR(32) NOT NULL, -- 有业务上限
bio TEXT, -- 个人简介,长度不可预估
mobile VARCHAR(20) NOT NULL, -- 有上限且需校验
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO user_center (country, nickname, bio, mobile) VALUES
('CN', '张三', '热爱开源。', '13800138000'),
('US', 'John', NULL, '+1-202-555-0100'),
('DE', 'Anna', repeat('简介内容。', 500), '+49-30-1234567');
SELECT id, country, nickname, length(bio) AS bio_len, mobile FROM user_center ORDER BY id; id | country | nickname | bio_len | mobile
----+---------+----------+---------+------------------
1 | CN | 张三 | 5 | 13800138000
2 | US | John | | +1-202-555-0100
3 | DE | Anna | 2500 | +49-30-1234567
(3 rows)第 2 步:验证 CHAR(2) 的填充行为
SELECT
country,
'[' || country || ']' AS bracketed,
length(country) AS len,
octet_length(country) AS octets
FROM user_center WHERE id IN (1, 2) ORDER BY id; country | bracketed | len | octets
---------+-----------+-----+--------
CN | [CN] | 2 | 2
US | [US] | 2 | 2
(2 rows)两位值刚好填满,看不出问题。但如果插入 1 位值:
INSERT INTO user_center (country, nickname, bio, mobile)
VALUES ('C', 'CharTest', NULL, '000');
SELECT country, '[' || country || ']' AS bracketed, length(country) AS len
FROM user_center WHERE nickname = 'CharTest'; country | bracketed | len
---------+-----------+-----
C | [C ] | 1
(1 row)'C' 被存为 'C '(含一个空格),拼接后能看到空格,但 length() 报 1。这个不一致会在导出、拼接、哈希时造成困扰。
第 3 步:验证 VARCHAR(32) 的容量边界
-- 32 个字符刚好通过
SELECT length('一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二') AS n; n
----
32
(1 row)INSERT INTO user_center (country, nickname, bio, mobile)
VALUES ('CN', '一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二', NULL, '001');INSERT 0 1-- 33 个字符会失败
INSERT INTO user_center (country, nickname, bio, mobile)
VALUES ('CN', '一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三', NULL, '002');ERROR: value too long for type character varying(32)第 4 步:验证 TEXT 的无上限特性
-- 插入 2 万字符的长文本
INSERT INTO user_center (country, nickname, bio, mobile)
VALUES ('CN', 'LongBio', repeat('长文本内容。', 4000), '003');
SELECT nickname, length(bio) AS bio_len, octet_length(bio) AS bio_bytes,
pg_column_size(bio) AS stored_bytes
FROM user_center WHERE nickname = 'LongBio'; nickname | bio_len | bio_bytes | stored_bytes
----------+---------+-----------+--------------
LongBio | 24000 | 72000 | 72004
(1 row)2.4 万字符占 72004 字节(72000 内容 + 4 字节头)。TEXT 会自动切换到 4 字节 varlena 头来容纳超过 126 字节的值。
第 5 步:验证三者存储等价
CREATE TEMP TABLE eq_check (a CHAR(20), b VARCHAR(20), c TEXT);
INSERT INTO eq_check VALUES ('hello world', 'hello world', 'hello world');
SELECT
pg_column_size(a) AS char_b,
pg_column_size(b) AS varchar_b,
pg_column_size(c) AS text_b
FROM eq_check; char_b | varchar_b | text_b
--------+-----------+--------
12 | 12 | 12
(1 row)CHAR(20) 与 TEXT 存同样的字符串,字节数完全一致——"CHAR 定长所以省空间"是彻底的误解。
第 6 步:观察 TOAST 机制
当一个值超过约 2 KB,PostgreSQL 会把它压缩或挪到独立的 TOAST 表:
SELECT
nickname,
length(bio) AS bio_len,
pg_column_size(bio) AS in_row_size
FROM user_center
WHERE nickname = 'LongBio'; nickname | bio_len | in_row_size
----------+---------+-------------
LongBio | 24000 | 72004
(1 row)长文本的压缩效果可以直接观察:
SELECT
pg_column_size(repeat('长文本内容。', 4000)) AS raw,
pg_column_size(repeat('aaaa', 20000)::TEXT) AS repeatable; raw | repeatable
-------+------------
72004 | 20004重复度高的内容会被压缩得更小。这是 TEXT 的额外好处:长度无上限的同时,还有自动压缩。
第 7 步:清理环境
DROP TABLE IF EXISTS user_center, customers, storage_cmp;注意事项
- 不要以为
CHAR(n)/VARCHAR(n)更省空间。三者存储机制相同,长度限制只是写入约束。
-- 不推荐(除非确实需要长度校验)
-- name VARCHAR(50)
-- 推荐
-- name TEXTCHAR(n)的填充语义会污染字符串拼接与导出,这是它最大的坑:
SELECT 'ID-' || 'A1'::CHAR(6) AS concat_result; concat_result
---------------
ID-A1
(1 row)length() 之外的场景(如 LIKE、正则、导出)都可能看到尾部空格。
CHAR(n)与VARCHAR(n)比较时不报错但结果可能反直觉:
SELECT 'ab'::CHAR(5) = 'ab'::VARCHAR(5) AS cross_eq; cross_eq
----------
t
(1 row)VARCHAR(n)的n不是字节数,含中文/emoji 时实际字节数可达4n。- 缩小
VARCHAR(n)会全表扫描,大表上要评估锁影响。 - 不要用
VARCHAR(n)做"性能优化"。它与TEXT性能完全一致。
-- 错误认识:以为 VARCHAR(10) 比 TEXT 快
-- 实际:两者执行计划与耗时一致TEXT的CHECK约束同样能做长度校验,比VARCHAR(n)更灵活(可以写复杂条件):
CREATE TEMP TABLE flexible_len (
v TEXT CHECK (length(v) BETWEEN 1 AND 64)
);CHAR(0)和VARCHAR(0)在 PostgreSQL 中合法但无意义,会拒绝多数非空值:
SELECT 'a'::VARCHAR(0);ERROR: value too long for type character varying(0)VARCHAR不带(n)与TEXT完全等价,但写法不统一会降低可读性,建议统一用TEXT。bpchar是CHAR的内部类型名,出现在系统目录与错误信息里,不要与VARCHAR混淆。
SELECT pg_typeof('a'::CHAR(3)) AS char_type, pg_typeof('a'::VARCHAR(3)) AS varchar_type; char_type | varchar_type
-----------+------------------
bpchar | character varying
(1 row)char这个类型名有歧义:"char"(带引号)是 1 字节的内部特殊类型,与CHAR(n)完全不同,日常不要使用。
最佳实践
- 默认用
TEXT。这是 PostgreSQL 社区的主流实践,也是本教程的推荐。 - 只在需要"数据库层兜底"时用
VARCHAR(n):外部系统有硬性长度限制、或字段有明确业务上限。
CREATE TABLE pragmatic (
url_hash CHAR(64) NOT NULL, -- 严格定长哈希,适合 CHAR
mobile VARCHAR(20) NOT NULL, -- 有业务上限,需要兜底
nickname TEXT NOT NULL, -- 长度不可预估,用 TEXT
bio TEXT
);- 几乎不要用
CHAR(n),除非字段语义上就是定长码(如 MD5/SHA 哈希片段、ISO 国家码)。 - 用
CHECK替代VARCHAR(n)做长度校验,语义更清晰且易于演进:
CREATE TABLE check_len (
nickname TEXT NOT NULL CHECK (length(nickname) BETWEEN 1 AND 32)
);- 纯 ASCII 字段(如 email 域名、token)可以用
CHECK限制字符集:
CREATE TABLE ascii_only (
token TEXT NOT NULL CHECK (token ~ '^[A-Za-z0-9_\-]+$')
);- 空字符串与
NULL语义要明确区分。PostgreSQL 允许''与NULL共存,业务上要么统一用NULL,要么统一用'':
CREATE TABLE null_policy (
a TEXT NOT NULL DEFAULT '', -- 策略 A:禁止 NULL,空值用 ''
b TEXT -- 策略 B:允许 NULL 表示"未知"
);- 文本列适合建
GIN全文索引或pg_trgm模糊索引,而不是 B-tree:
-- 前缀查询可以走 B-tree 索引(需 text_pattern_ops)
CREATE INDEX idx_name_prefix ON customers (name text_pattern_ops);- 大文本字段考虑分离到独立表,避免主表行过宽影响顺序扫描:
CREATE TABLE articles (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
title TEXT NOT NULL,
body TEXT
);- 导出数据时统一
trim()一次,防止历史CHAR(n)数据带尾部空格。 - 别把 JSON 塞进
TEXT,用JSONB;别把二进制塞进TEXT,用BYTEA。 - 字符串排序受 collation 影响,需要确定性排序时显式
COLLATE "C":
SELECT name FROM customers ORDER BY name COLLATE "C";下一节
继续学习:定长与变长类型