数组、复合类型与其他特殊类型
本教程共 50 篇 · 第 17 篇 · 更新于 2026-07-31 · 约 9 分钟阅读
17. 数组、复合类型与其他特殊类型
本节目标:学完认识 PostgreSQL 的数组、复合类型、网络地址、几何、二进制、范围等非常规类型,知道各自适用场景。
除了前面的常规类型,PostgreSQL 还有一批「特殊类型」。它们不是每天都要用,但遇到对的场景能省很多事。比如一个用户有多个标签,用数组就比另开一张表简单。本章挨个过一遍,知道有这些能力、啥时候该用即可。
数组(ARRAY)
任何类型都能加 [] 变成数组,比如 TEXT[]、INT[]、INTEGER[]。元素类型必须一致,不能混装。
CREATE TABLE users (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(120),
age SMALLINT,
tags TEXT[],
created_at TIMESTAMPTZ DEFAULT now()
);
INSERT INTO users (username, tags) VALUES ('alice', ARRAY['vip','new']);
数组字面量还能直接写花括号形式:'{vip,new}'::TEXT[]。
取下标(从 1 开始)和按值搜索:
SELECT username, tags[1] FROM users; -- 第一个标签
SELECT * FROM users WHERE 'vip' = ANY(tags); -- 包含 vip 的用户
数组还能做很多运算:连接 ||、长度 array_length、判断是否重叠 &&、包含 @> 等:
SELECT array_append(tags, 'old') FROM users WHERE username='alice';
SELECT array_length(tags, 1) FROM users;
SELECT ARRAY[1,2,3] && ARRAY[3,4]; -- true,有重叠元素
把数组拆成多行用 unnest:
SELECT username, unnest(tags) FROM users;
Note数组下标从 1 开始,不是 0,这是 PostgreSQL 数组的约定,别和很多编程语言搞混。需要频繁按数组元素建索引或做复杂关联时,往往拆成子表更规范;数组适合简单、固定的一对多。数组上也能建 GIN 索引加速
@>查询。
复合类型(Composite)
把多个字段打包成一个自定义类型,类似把「一行」当作一个值。比如地址由省、市、街道组成。
CREATE TYPE address AS (
street TEXT,
city TEXT,
zip TEXT
);
CREATE TABLE users (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username VARCHAR(50) NOT NULL,
addr address
);
INSERT INTO users (username, addr) VALUES ('alice', ROW('南京路','上海','200001'));
取复合类型的某个字段,注意括号:
SELECT username, (addr).city FROM users; -- 上海
更新某个字段:
UPDATE users SET addr.city = '北京' WHERE username = 'alice';
Note建表时 PostgreSQL 会隐式生成一个同名的复合类型,所以表也能当复合类型用。复合类型适合把「一组相关字段」当整体传递,但查询单个字段不如拆成独立列直观,别滥用。
网络地址(inet / cidr)
存 IP 和网段,比用文本更专业:带格式校验,还有专门的运算符(比如判断 IP 是否属于某网段)。
inet:主机地址,可带子网,如192.168.1.10。cidr:网络段,如192.168.1.0/24,不能带主机位。
CREATE TABLE devices (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
ip INET
);
INSERT INTO devices (ip) VALUES ('192.168.1.10');
判断某个 IP 是否落在网段内:
SELECT '192.168.1.10'::inet << '192.168.1.0/24'::cidr; -- true
Tip存 IP 地址优先用
inet,它会校验格式并支持网段运算,比varchar靠谱得多。需要严格表示「网络」时用cidr。
几何类型
存二维空间对象:point 点、line 线、box 矩形、circle 圆等。适合简单几何运算。
SELECT '((1,1),(4,4))'::box AS 矩形; -- 一个矩形
SELECT '(3,3)'::point AS 点; -- 一个点
SELECT '<(0,0), 5>'::circle AS 圆; -- 圆心(0,0)半径5
几何类型之间能做距离、包含、相交等运算,做地图、图形相关时才派上用场。日常业务用得少。
二进制(bytea)
bytea 存原始二进制,比如图片、文件片段。默认以十六进制输入输出。
CREATE TABLE files (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
data BYTEA
);
INSERT INTO files (data) VALUES ('\xDEADBEEF');
Warning大文件别直接塞
bytea,通常存文件系统、数据库只存路径。小图标这类才适合放库里——大对象会拖慢备份、膨胀表体积,还不好做缓存。
范围类型(Range)
PostgreSQL 还有一组范围类型,表示「一段连续的区间」:int4range(整数区间)、int8range、daterange(日期区间)、numrange、tsrange(时间戳区间)等。常用来表达「有效期」「排班时段」。
CREATE TABLE events (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT,
during daterange
);
INSERT INTO events (name, during)
VALUES ('促销', daterange('2026-08-01', '2026-08-31'));
SELECT * FROM events WHERE during @> DATE '2026-08-15'; -- 该日期在区间内
范围类型自带去重、重叠判断,比自己用两个字段加条件优雅得多。
hstore 键值对
hstore 把「键 => 值」存进一个字段,键和值都是文本。它是扩展,需要先启用。
CREATE EXTENSION hstore;
CREATE TABLE books (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
title VARCHAR(120),
attr hstore
);
INSERT INTO books (title, attr)
VALUES ('PG 教程', '"publisher" => "码上学", "weight" => "500g"');
SELECT title, attr -> 'publisher' FROM books;
hstore 也支持 ?(键存在)、@>(包含)等操作符,和 jsonb 类似但功能更弱。
Tip新项目如果要用键值结构,优先考虑
jsonb,它已内置且功能更全。hstore主要在维护老库时遇到。
先想清楚要不要特殊类型
在介绍具体类型前,先说一句总原则:特殊类型是用来解决「常规列不好表达」的场景的。能用普通列清晰表达、又要频繁查询的,就别上特殊类型。下面这些类型是「工具箱里的备选」,而不是默认选项。
数组与范围的更多玩法
数组还能用 array_agg 把多行聚合成一个数组,和它反向的 unnest 把数组拆成多行,两者配合很常用:
-- 先把每行数组拆成多行,再按用户聚合成一个数组
SELECT username, array_agg(t) AS all_tags
FROM (SELECT username, unnest(tags) AS t FROM users) s
GROUP BY username;
-- 反过来,把数组直接拆成多行
SELECT username, unnest(tags) FROM users;
范围类型除了算「是否在区间内」,还能算重叠、相邻、取交集:
SELECT int4range(1,10) && int4range(5,15); -- true,有重叠
SELECT int4range(1,10) * int4range(5,15); -- [5,10),交集
范围类型自带「排除约束(EXCLUDE)」能力,能优雅地防止时间段重叠,比如会议室排班不能撞期,这是它独到的用处。
何时不该用特殊类型
这些特殊类型虽好,但不要为了用而用。判断标准很简单:如果数据固定、要频繁关联或聚合,拆成普通列或独立表更规范、约束更强、查询优化器也更容易挑对索引。数组、jsonb、范围适合「结构灵活或整体作为一个值处理」的场景。一旦你发现自己在对数组元素频繁做复杂关联查询,多半该拆表了。
一句话总结
- 同类型多值 → 数组
[]。 - 多字段打包 → 复合类型
CREATE TYPE ... AS ()。 - IP / 网段 →
inet/cidr。 - 图形 → 几何类型。
- 一段区间 → 范围类型
daterange等。 - 原始字节 →
bytea(仅小数据)。 - 老式键值 →
hstore(新项目用jsonb)。
这些特殊类型不必强记。知道「PostgreSQL 有这能力」,等真遇到对应场景,回来翻这一章就够了。