Skip to content

CHAR / VARCHAR / TEXT ​

本节讲解 PostgreSQL 的三种字符类型 CHAR、VARCHAR 与 TEXT。你将理解它们的语义差异、长度检查行为、存储方式,以及"该用哪一种"这个长期争论的正确答案。

前置知识

阅读本节前,建议先了解:数值类型选择指南

基础概念 ​

PostgreSQL 提供三种字符类型,它们在存储方式上完全一致,差异只在约束与填充语义。

类型名别名存储空间长度限制超长时行为说明
CHARACTER(n)CHAR(n)、BPCHAR(n)变长(最短 1 字节 + 头)n 个字符报错定长,不足时用空格补齐
CHARACTER VARYING(n)VARCHAR(n)变长n 个字符报错变长,有长度上限
CHARACTER VARYINGVARCHAR变长无限—变长,无长度上限
TEXT—变长无限—变长,无长度上限

关键术语:

  • 字符(Character):这里指"字符"而非"字节",长度限制按字符计数,与编码无关。
  • 填充语义(Padding Semantics):CHAR(n) 会把短值右侧补空格到 n 个字符。
  • 变长类型(Variable-Length Type):只存实际内容,不补齐。
  • varlena 头(Var-length Header):所有变长类型的前 1~4 字节元数据,记录长度。

最重要的一个事实:PostgreSQL 的 CHAR(n) 在磁盘上并不定长,它只是"语义上定长"(比较时会忽略尾部空格),存储上仍然是变长的。这让 CHAR(n) 相对 VARCHAR(n) 毫无空间优势。

sql
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;
text
 char100_size | varchar100_size | text_size
--------------+-----------------+-----------
            2 |               2 |         2

CHAR(100) 里只放一个字符,实际只占 2 字节(1 字节内容 + 1 字节头),不是 100 字节。

语法与用法 ​

text
列名 CHARACTER(n) | CHAR(n) | CHARACTER VARYING(n) | VARCHAR(n) | VARCHAR | TEXT
sql
-- 最简可运行示例
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;
text
 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) 有两个容易误导人的行为:

  1. 写入时补齐:短值右侧补空格到 n 个字符。
  2. 比较时忽略尾部空格:'abc'::CHAR(5) = 'abc '::CHAR(5) 为真。
sql
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;
text
 raw_length | trimmed_length | eq_without_pad | eq_with_pad
------------+----------------+----------------+-------------
          3 |              3 | t              | t
(1 row)

length() 返回 3 而不是 5——PostgreSQL 内部会忽略 CHAR 的尾部空格。但一旦把值转成 TEXT 或参与字符串拼接,空格又"回来了":

sql
SELECT
  length('abc'::CHAR(5)::TEXT)                    AS to_text_length,
  length('abc'::CHAR(5) || '')                    AS concat_length,
  octet_length('abc'::CHAR(5))                    AS octets;
text
 to_text_length | concat_length | octets
----------------+---------------+--------
              3 |             3 |      3
(1 row)

这说明尾部空格在 PostgreSQL 内部是被"抹掉"的,CHAR(n) 并不真的在磁盘上存了补白空格(这也解释了为什么它没有空间优势)。

长度检查行为 ​

三种类型的长度检查规则不同,这是实际开发中最容易踩的地方。

类型写入超长值写入含尾部空格的值
CHAR(n)报错(除非超出部分全是空格)补到 n 个字符
VARCHAR(n)报错原样保留空格
VARCHAR / TEXT不检查原样保留空格
sql
-- CHAR(n):超长报错
SELECT 'abcdef'::CHAR(3);
text
ERROR:  value too long for type character(3)
sql
-- CHAR(n):超出部分全是空格时不报错,直接截掉
SELECT 'ab   '::CHAR(3) AS padded_ok;
text
 padded_ok
-----------
 ab
(1 row)
sql
-- VARCHAR(n):超长报错
SELECT 'abcdef'::VARCHAR(3);
text
ERROR:  value too long for type character varying(3)
sql
-- TEXT:无限制
SELECT length('abcdef'::TEXT) AS n;
text
 n
---
 6
(1 row)

注意 VARCHAR(n) 的长度检查按字符数而非字节数:

sql
SELECT length('中文测试'::TEXT) AS chars, octet_length('中文测试'::TEXT) AS bytes;
text
 chars | bytes
-------+-------
     4 |    12
(1 row)

4 个中文字符在 UTF-8 下占 12 字节,但 VARCHAR(4) 能装下,因为限制针对 4 个字符。

sql
SELECT '中文测试'::VARCHAR(4) AS ok;
text
    ok
----------
 中文测试
(1 row)

存储方式:完全一致 ​

这是最关键的一点:三种类型在存储层用的是同一套机制。

sql
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;
text
 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 性能完全一样,没有"定长更快"这回事。
  • 空间优化:同上,存储完全一致。

代价:

sql
-- 改长度需要 ALTER TABLE,在旧版本会锁表重写
ALTER TABLE customers ALTER COLUMN name TYPE VARCHAR(128);
text
ALTER TABLE

从 PostgreSQL 9.2 起,增大 VARCHAR(n) 的 n 是元数据变更,不重写表;但缩小 n 需要全表扫描校验。

sql
-- 缩小长度:需要扫描全表检查是否有超长值
ALTER TABLE customers ALTER COLUMN name TYPE VARCHAR(16);
text
ALTER TABLE

如果表里有超长值,会直接失败:

text
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)。

原因:

  1. 三者性能与存储完全相同,TEXT 没有劣势。
  2. TEXT 没有长度上限,业务变化时不需要 ALTER TABLE。
  3. VARCHAR(n) 的长度上限经常成为"业务演进"的阻碍(如用户昵称从 16 改到 64)。
  4. CHAR(n) 的填充语义导致的 bug 多于它带来的价值。

长度单位与编码的关系 ​

长度限制按字符计数,与 server_encoding 有关:

sql
SHOW server_encoding;
text
 server_encoding
-----------------
 UTF8
(1 row)

在 UTF8 编码下,一个字符最多占 4 字节,所以 VARCHAR(n) 理论上最多占 4n 字节:

sql
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;
text
 ascii_val | ascii_bytes | cjk_bytes | emoji_bytes
-----------+-------------+-----------+-------------
 a         |           2 |         4 |           5
(1 row)

一个 ASCII 字符 1 字节,一个中文字 3 字节,一个 4 字节的 emoji 占 4 字节 + 1 字节头 = 5 字节。n 是字符数,不是字节数,设计存储容量时要按最坏情况(4 倍)估算。

类型转换 ​

三者之间可以自由转换:

sql
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;
text
ERROR:  value too long for type character(3)
sql
SELECT
  'abc'::TEXT::VARCHAR(5)       AS text_to_varchar,
  'abc  '::VARCHAR(10)::CHAR(5) AS varchar_to_char;
text
 text_to_varchar | varchar_to_char
-----------------+----------------
 abc             | abc
(1 row)

VARCHAR → CHAR 会在转换时补空格,再转换回 TEXT 时尾部空格被 trim。

与其它类型的转换 ​

字符串类型是所有类型的"通用入口",绝大多数类型都能从文本解析:

sql
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;
text
 to_int |   to_date  |   to_jsonb   | to_bool
--------+------------+--------------+---------
    123 | 2026-01-15 | {"a": 1}     | t
(1 row)

反向转换也支持:

sql
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;
text
 int_to_text | bool_to_text | ts_to_text
-------------+--------------+------------
 text        | text         | text
(1 row)

实战示例 ​

下面通过一个用户中心表的设计,对比三种类型的实际差异。

第 1 步:建表并插入边界数据 ​

sql
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;
text
 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) 的填充行为 ​

sql
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;
text
 country | bracketed | len | octets
---------+-----------+-----+--------
 CN      | [CN]      |   2 |      2
 US      | [US]      |   2 |      2
(2 rows)

两位值刚好填满,看不出问题。但如果插入 1 位值:

sql
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';
text
 country | bracketed | len
---------+-----------+-----
 C       | [C ]      |   1
(1 row)

'C' 被存为 'C '(含一个空格),拼接后能看到空格,但 length() 报 1。这个不一致会在导出、拼接、哈希时造成困扰。

第 3 步:验证 VARCHAR(32) 的容量边界 ​

sql
-- 32 个字符刚好通过
SELECT length('一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二') AS n;
text
 n
----
 32
(1 row)
sql
INSERT INTO user_center (country, nickname, bio, mobile)
VALUES ('CN', '一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二', NULL, '001');
text
INSERT 0 1
sql
-- 33 个字符会失败
INSERT INTO user_center (country, nickname, bio, mobile)
VALUES ('CN', '一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三', NULL, '002');
text
ERROR:  value too long for type character varying(32)

第 4 步:验证 TEXT 的无上限特性 ​

sql
-- 插入 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';
text
 nickname | bio_len | bio_bytes | stored_bytes
----------+---------+-----------+--------------
 LongBio  |   24000 |     72000 |        72004
(1 row)

2.4 万字符占 72004 字节(72000 内容 + 4 字节头)。TEXT 会自动切换到 4 字节 varlena 头来容纳超过 126 字节的值。

第 5 步:验证三者存储等价 ​

sql
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;
text
 char_b | varchar_b | text_b
--------+-----------+--------
     12 |        12 |     12
(1 row)

CHAR(20) 与 TEXT 存同样的字符串,字节数完全一致——"CHAR 定长所以省空间"是彻底的误解。

第 6 步:观察 TOAST 机制 ​

当一个值超过约 2 KB,PostgreSQL 会把它压缩或挪到独立的 TOAST 表:

sql
SELECT
  nickname,
  length(bio)          AS bio_len,
  pg_column_size(bio)  AS in_row_size
FROM user_center
WHERE nickname = 'LongBio';
text
 nickname | bio_len | in_row_size
----------+---------+-------------
 LongBio  |   24000 |       72004
(1 row)

长文本的压缩效果可以直接观察:

sql
SELECT
  pg_column_size(repeat('长文本内容。', 4000))  AS raw,
  pg_column_size(repeat('aaaa', 20000)::TEXT)   AS repeatable;
text
  raw  | repeatable
-------+------------
 72004 |      20004

重复度高的内容会被压缩得更小。这是 TEXT 的额外好处:长度无上限的同时,还有自动压缩。

第 7 步:清理环境 ​

sql
DROP TABLE IF EXISTS user_center, customers, storage_cmp;

注意事项 ​

  • 不要以为 CHAR(n) / VARCHAR(n) 更省空间。三者存储机制相同,长度限制只是写入约束。
sql
-- 不推荐(除非确实需要长度校验)
-- name VARCHAR(50)

-- 推荐
-- name TEXT
  • CHAR(n) 的填充语义会污染字符串拼接与导出,这是它最大的坑:
sql
SELECT 'ID-' || 'A1'::CHAR(6) AS concat_result;
text
 concat_result
---------------
 ID-A1
(1 row)

length() 之外的场景(如 LIKE、正则、导出)都可能看到尾部空格。

  • CHAR(n) 与 VARCHAR(n) 比较时不报错但结果可能反直觉:
sql
SELECT 'ab'::CHAR(5) = 'ab'::VARCHAR(5) AS cross_eq;
text
 cross_eq
----------
 t
(1 row)
  • VARCHAR(n) 的 n 不是字节数,含中文/emoji 时实际字节数可达 4n。
  • 缩小 VARCHAR(n) 会全表扫描,大表上要评估锁影响。
  • 不要用 VARCHAR(n) 做"性能优化"。它与 TEXT 性能完全一致。
sql
-- 错误认识:以为 VARCHAR(10) 比 TEXT 快
-- 实际:两者执行计划与耗时一致
  • TEXT 的 CHECK 约束同样能做长度校验,比 VARCHAR(n) 更灵活(可以写复杂条件):
sql
CREATE TEMP TABLE flexible_len (
  v TEXT CHECK (length(v) BETWEEN 1 AND 64)
);
  • CHAR(0) 和 VARCHAR(0) 在 PostgreSQL 中合法但无意义,会拒绝多数非空值:
sql
SELECT 'a'::VARCHAR(0);
text
ERROR:  value too long for type character varying(0)
  • VARCHAR 不带 (n) 与 TEXT 完全等价,但写法不统一会降低可读性,建议统一用 TEXT。
  • bpchar 是 CHAR 的内部类型名,出现在系统目录与错误信息里,不要与 VARCHAR 混淆。
sql
SELECT pg_typeof('a'::CHAR(3)) AS char_type, pg_typeof('a'::VARCHAR(3)) AS varchar_type;
text
 char_type |   varchar_type
-----------+------------------
 bpchar    | character varying
(1 row)
  • char 这个类型名有歧义:"char"(带引号)是 1 字节的内部特殊类型,与 CHAR(n) 完全不同,日常不要使用。

最佳实践 ​

  • 默认用 TEXT。这是 PostgreSQL 社区的主流实践,也是本教程的推荐。
  • 只在需要"数据库层兜底"时用 VARCHAR(n):外部系统有硬性长度限制、或字段有明确业务上限。
sql
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) 做长度校验,语义更清晰且易于演进:
sql
CREATE TABLE check_len (
  nickname TEXT NOT NULL CHECK (length(nickname) BETWEEN 1 AND 32)
);
  • 纯 ASCII 字段(如 email 域名、token)可以用 CHECK 限制字符集:
sql
CREATE TABLE ascii_only (
  token TEXT NOT NULL CHECK (token ~ '^[A-Za-z0-9_\-]+$')
);
  • 空字符串与 NULL 语义要明确区分。PostgreSQL 允许 '' 与 NULL 共存,业务上要么统一用 NULL,要么统一用 '':
sql
CREATE TABLE null_policy (
  a TEXT NOT NULL DEFAULT '',           -- 策略 A:禁止 NULL,空值用 ''
  b TEXT                                -- 策略 B:允许 NULL 表示"未知"
);
  • 文本列适合建 GIN 全文索引或 pg_trgm 模糊索引,而不是 B-tree:
sql
-- 前缀查询可以走 B-tree 索引(需 text_pattern_ops)
CREATE INDEX idx_name_prefix ON customers (name text_pattern_ops);
  • 大文本字段考虑分离到独立表,避免主表行过宽影响顺序扫描:
sql
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":
sql
SELECT name FROM customers ORDER BY name COLLATE "C";

下一节

继续学习:定长与变长类型

参考链接 ​