PostgreSQL 中那些 MySQL 没有的数据类型怎么玩

PostgreSQL 中那些 MySQL 没有的数据类型怎么玩

•约 3.9 千字

很多人从 MySQL 切到 PostgreSQL,第一反应是:语法差不多,SELECT、JOIN、索引也差不多,应该很快就能上手。然后某天他打开 PostgreSQL 的类型文档,看见了数组、范围、多范围、复合类型、域、inet、tsvector 和一串几何类型,感觉像误入了数据库的后厨。

这些东西到底有什么用?难道只是 PostgreSQL 把「一列只能放一个值」这条规矩也给折腾没了?

先把标题里的话说严谨一点:下面有些类型是 MySQL 没有对应物,有些是 PostgreSQL 把同一个问题做得更深入,还有些只是 MySQL 也有、但使用方式完全不同。 JSON、ENUM 和空间类型不能算 PostgreSQL 独占。比较它们的目的,也不是给数据库排座次,而是看看一个类型系统能替应用代码承担多少约束和查询工作。

本文以 PostgreSQL 18 为准。完整清单可以参考 PostgreSQL 18 数据类型文档;版本升级后,别把这篇文章当成正在运行实例的最终说明书。

一、先建立一张地图

PostgreSQL 18 的常用内置类型,大致可以按下面几类理解:

类别代表类型它替你表达了什么
数值与时间numeric、interval、timestamptz精确金额、时间长度、带时区的时间点
半结构化jsonb、xml可查询的文档和 XML 数据
容器数组、复合类型一列中的有序集合,或一个结构化值
约束与集合enum、domain、Range、Multirange允许的值、可复用约束、连续区间
网络与标识inet、cidr、macaddr、uuidIP、网段、硬件地址、全局标识
检索与计算tsvector、tsquery、bit全文检索文档、检索表达式、位掩码
几何point、box、circle、polygon平面上的几何对象

这张表里最重要的词不是「稀奇」,而是语义。如果一列存的是 IP,就让数据库知道它是 IP;如果一列表示一段营业时间,就别把两个时间戳塞进 JSON 再让每个应用各自解释。

二、jsonb:不是把数据库变成 MongoDB

MySQL 也有 JSON,所以它不属于「MySQL 没有」的类型。但 PostgreSQL 的 jsonb 很值得单独说:它不是原样保存 JSON 文本,而是解析后以二进制形式保存,适合索引和查询;代价是会丢掉空白、对象键顺序和重复键。

postgresql.sql
sql
CREATE TABLE orders (
  id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  customer_id bigint NOT NULL,
  attributes jsonb NOT NULL DEFAULT '{}'
);

INSERT INTO orders (customer_id, attributes)
VALUES (42, '{"channel":"mobile", "coupon":"new-user"}');

SELECT id
FROM orders
WHERE attributes @> '{"channel":"mobile"}';

@> 是包含判断:左边的 jsonb 是否包含右边给出的结构。需要按键查询时,可以建立 GIN 索引:

postgresql.sql
sql
CREATE INDEX orders_attributes_gin
ON orders USING gin (attributes);

我的建议是:稳定、需要关联、排序或单独约束的字段,仍然建成普通列;变化频繁但结构仍有意义的附加属性,再放进 jsonb。 把整张业务表压成一个 JSON,短期少写几行 ALTER TABLE,长期却会失去类型检查、列级统计和清晰的约束边界。JSON 是逃生门,不是地基。

三、数组:一列存多个值,但不是让表格变成 Excel

PostgreSQL 原生支持数组,而且数组可以是多维的:

postgresql.sql
sql
CREATE TABLE articles (
  id       bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  title    text NOT NULL,
  tags     text[] NOT NULL DEFAULT '{}',
  matrix   integer[][]
);

INSERT INTO articles (title, tags)
VALUES ('PostgreSQL 类型漫游', ARRAY['postgresql', 'database']);

SELECT title
FROM articles
WHERE 'postgresql' = ANY (tags);

ANY、ALL、unnest、数组切片和 array_position 让它不只是「一串逗号分隔的字符串」。如果需要判断数组是否包含某些元素,也可以使用 @>,再配合 GIN 索引。

但数组有一条很硬的边界:数组适合属性集合,不适合关系。 用户的多个地址、订单的多个商品、需要独立更新和关联的标签,应该是子表;文章的少量标签、设备支持的协议列表、固定维度的坐标,才是数组的舒适区。数组不是把一对多关系偷偷塞进一列的许可证。

四、enum、domain 与复合类型:把规则放回类型系统

enum:稳定且确实有限的集合

MySQL 也有 ENUM,而 PostgreSQL 的枚举通常通过独立类型定义:

postgresql.sql
sql
CREATE TYPE order_status AS ENUM ('pending', 'paid', 'cancelled');

CREATE TABLE payments (
  order_id bigint PRIMARY KEY,
  status order_status NOT NULL DEFAULT 'pending'
);

枚举值有顺序,默认按声明顺序比较。它适合状态很稳定、值很少的概念。状态经常新增、需要后台配置或携带描述时,我更倾向于使用字典表;枚举不是免费的配置中心,修改类型也需要认真安排迁移。

domain:给既有类型套一个可复用约束

domain 不是新存储格式,而是「某种基础类型 + 默认值或检查约束」:

postgresql.sql
sql
CREATE DOMAIN positive_money AS numeric(12, 2)
  CHECK (VALUE >= 0);

CREATE TABLE products (
  price positive_money NOT NULL
);

如果多个表都要表达「非负金额」「合法的中国手机号」或「非空短文本」,域可以让约束集中定义。它比每张表复制一份 CHECK 更不容易漂移。

复合类型:一行记录也可以成为一个值

postgresql.sql
sql
CREATE TYPE address AS (
  city text,
  detail text,
  postal_code text
);

CREATE TABLE users (
  id bigint PRIMARY KEY,
  home_address address
);

复合类型可以作为列类型、函数参数和返回值。它在数据库函数、视图和领域模型里很方便,但对 ORM、部分客户端驱动和迁移工具不如普通列友好。能用普通列讲清楚的结构,不必为了复合类型而复合。

五、Range 和 Multirange:时间段终于不必拆成两个孤儿字段

这是 PostgreSQL 最有辨识度的一组能力。Range 表示一个区间,例如 [2026-08-22 10:00, 2026-08-22 11:00);Multirange 则表示多个不相交区间的集合。

内置类型包括:

RangeMultirange常见含义
int4rangeint4multirange整数编号区间
int8rangeint8multirange大整数区间
numrangenummultirange数值区间
tsrangetsmultirange不带时区的时间区间
tstzrangetstzmultirange带时区的时间区间
daterangedatemultirange日期区间

日期预约可以这样写:

postgresql.sql
sql
CREATE TABLE bookings (
  room_id  bigint NOT NULL,
  during   tstzrange NOT NULL,
  EXCLUDE USING gist (
    room_id WITH =,
    during WITH &&
  )
);

INSERT INTO bookings
VALUES (7, '[2026-08-22 10:00+08, 2026-08-22 11:00+08)');

&& 表示两个区间有重叠,EXCLUDE 约束则保证同一个房间不能出现重叠预约。这里的关键不是少写了两个字段,而是把「区间不能重叠」变成数据库可以拒绝的事实。

Range 的上下界可以是包含、排除或无限边界。离散类型如日期还有规范化行为:[2026-08-22, 2026-08-23) 表示一天,右边界通常用排除形式,做连续时间计算时尤其不容易出现「23:59:59 到底算不算」的争论。

如果一个员工每周有多段可用时间,Multirange 可以把这些段作为一个值保存;如果需要对每一段分别查询、排序或记录来源,仍然应该建明细表。类型能表达集合,不代表明细表就失业了。

六、网络地址:别再把 IP 当字符串

inet 表示主机地址或带掩码的网络地址,cidr 表示网络;它们支持 IPv4 和 IPv6,并且能做包含判断:

postgresql.sql
sql
CREATE TABLE access_logs (
  client_ip inet NOT NULL,
  allowed_network cidr
);

SELECT client_ip
FROM access_logs
WHERE client_ip <<= '10.0.0.0/8'::cidr;

<<= 表示左侧地址或网络被右侧网络包含。inet 和 cidr 还提供掩码长度、地址族、网络地址等运算。这样做的好处很朴素:数据库知道 10.0.0.2 是地址,不会把 10.0.0.100 错排在 10.0.0.20 前面,也不需要应用层手写 CIDR 判断。

macaddr 和 macaddr8 则用于 MAC 地址。它们的使用频率没有 inet 高,但在网络设备、资产管理和 DHCP 记录中比 varchar(17) 更诚实。

七、uuid:标识符不必都是自增整数

UUID 也不是 PostgreSQL 独有类型,但 PostgreSQL 18 已经内置了 uuidv7(),可以生成带时间排序特征的 UUID:

postgresql.sql
sql
CREATE TABLE events (
  id         uuid PRIMARY KEY DEFAULT uuidv7(),
  happened_at timestamptz NOT NULL DEFAULT now(),
  payload    jsonb NOT NULL DEFAULT '{}'
);

UUID 的优势是可以在应用侧生成、跨系统合并时不容易冲突,也不直接暴露「这是第几条数据」。传统随机 UUID v4 写入 B-tree 时可能带来索引局部性问题;UUIDv7 把时间信息放进高位,通常更适合按生成时间逐渐增长的场景,但它仍然不是严格的业务时间戳,也不应拿来替代 created_at。

如果只是单库单表、内部管理系统、自增整数没有隐私和扩展问题,我不会为了显得分布式就把所有主键换成 UUID。类型选择解决的是边界问题,不是架构信仰。

八、tsvector 和 tsquery:全文检索不是 LIKE '%关键词%'

PostgreSQL 内置了全文检索类型。tsvector 是规范化后的文档词项,tsquery 是检索表达式:

postgresql.sql
sql
CREATE TABLE notes (
  id       bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  title    text NOT NULL,
  body     text NOT NULL,
  document tsvector GENERATED ALWAYS AS (
    to_tsvector('simple', coalesce(title, '') || ' ' || coalesce(body, ''))
  ) STORED
);

CREATE INDEX notes_document_gin
ON notes USING gin (document);

SELECT id, title
FROM notes
WHERE document @@ plainto_tsquery('simple', 'vacuum');

@@ 表示文档匹配查询。PostgreSQL 可以记录词项位置、做词干化、权重排序,并用 GIN 索引加速检索。中文分词则是另一件事:内置配置并不等于中文搜索方案,通常需要合适的分词扩展或外部搜索引擎。

这组类型适合站内搜索、后台文档和日志检索。搜索需求如果已经包括复杂分词、拼写纠错、聚合和海量全文,别因为数据库「也能搜」就拒绝专业搜索系统;数据库内置能力的价值,是让中小规模全文查询少依赖一个额外服务。

九、几何类型:小型平面计算的便宜工具

PostgreSQL 内置 point、line、lseg、box、path、polygon 和 circle:

postgresql.sql
sql
CREATE TABLE stores (
  id       bigint PRIMARY KEY,
  location point NOT NULL
);

SELECT id
FROM stores
WHERE location <@ circle '<(0,0), 10>';

这里的 <@ 表示点位于圆内。它们适合简单的二维平面计算、绘图标注和非常轻量的空间关系。经纬度、投影、距离单位、坐标系一旦复杂起来,应考虑 PostGIS;不要把地球表面粗暴地当成一张无限大的方格纸。

顺便说一句:MySQL 也有空间类型,所以它们不属于 PostgreSQL 独占。但 PostgreSQL 的扩展接口和 GiST 索引让这条路非常自然,Range 的排他约束也正是这种可扩展索引系统的一个漂亮产物。

十、还有一些不那么常用、但确实有用的类型

bit 与 bit varying

bit(n) 是固定长度位串,bit varying(n) 是可变长度位串,适合权限位图、协议字段和需要逐位运算的场景:

postgresql.sql
sql
SELECT B'1010' & B'1100'; -- 1000

权限复杂到需要频繁解释每一位时,位图可能比一串布尔列紧凑;但它的可读性很差,权限名称仍然应该在代码或字典表中有对应关系。

money

money 表示带本地化格式的货币金额,能做一些方便的格式化和运算,但它受 lc_monetary 影响,跨环境输出和多币种业务都容易踩坑。金融金额通常使用带明确精度和舍入规则的 numeric(p, s),而不是看起来很顺手的 money。

xml

xml 能够检查 XML 文档的格式,并提供 XML 相关函数。它适合确实要保存 XML 的接口交换、配置和遗留系统数据;新项目若没有 XML 生态约束,不必为了「类型齐全」而选它。

pg_lsn 与系统类型

pg_lsn 表示 PostgreSQL WAL(预写式日志,Write-Ahead Log)中的日志序列号,适合复制、备份和恢复工具记录进度。pg_snapshot、OID 等类型也能在系统目录和诊断工具里见到,但它们不是普通业务表的万能主键。

另外还有伪类型,例如 record、anyelement 和 void。它们主要用于声明函数参数和返回值,不能像普通列类型一样保存业务数据。看到一个类型出现在文档里,不代表应该把它放进 CREATE TABLE。

十一、从 MySQL 迁移时,最容易想错的几件事

serial 不是魔法类型

PostgreSQL 的 serial、bigserial 是创建序列、默认值和整数列的一组便利语法,并不是真正意义上的独立数据类型。新表更推荐使用 SQL 标准的 identity column:

postgresql.sql
sql
CREATE TABLE users (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY
);

它的意图比隐式创建序列更清楚,也更适合迁移和权限审查。

timestamp 不等于「带时区」

PostgreSQL 的 timestamp without time zone 不保存时区;timestamptz 保存的是一个绝对时间点,输出时按当前会话时区显示。它不是把原始时区名字永远存下来。

postgresql.sql
sql
SET TIME ZONE 'Asia/Shanghai';
SELECT '2026-08-22 10:00+08'::timestamptz;

记录事件发生的时刻通常用 timestamptz;记录「每天 9 点营业」这种与地点和规则相关的墙上时间,则要单独设计时区和重复规则。时间类型选错,往往比少建一个索引更难补救。

不要把 PostgreSQL 特性当 ORM 特性

数组、复合类型、Range 和 Multirange 在 SQL 里很好看,但 ORM、迁移工具、BI 软件和客户端驱动可能只支持其中一部分。真正采用之前,至少验证四件事:

  • 驱动能否正确读写该类型,而不是返回一串未经解析的文本;
  • ORM 是否支持迁移、查询条件和变更检测;
  • 备份恢复、跨语言服务和 CDC(Change Data Capture,变更数据捕获)是否能处理它;
  • 团队是否愿意长期维护这条不那么主流的路径。

数据库不是孤岛。一个类型在 PostgreSQL 里很优雅,出了数据库边界之后可能就要靠你写适配器。

十二、我会怎么选

我会先把不变量放进类型系统:金额用 numeric,时间点用 timestamptz,IP 用 inet,全局标识用 uuid,有限状态用 enum 或字典表,重复约束用 domain,不重叠区间用 Range 加 EXCLUDE。这些选择不是为了显摆 PostgreSQL,而是因为错误数据一旦写入,后面每个服务都要重复防守。

然后才考虑容器:少量有序属性用数组,结构不稳定的附加字段用 jsonb,数据库函数之间传递结构用复合类型,全文检索用 tsvector。如果一个类型让迁移、驱动和分析链路都变得痛苦,那就算它再漂亮,也应该退回普通列和关联表。

说到底,PostgreSQL 的厉害之处不是「类型比 MySQL 多」,而是它愿意把更多领域概念变成可组合、可索引、可约束的数据库对象。好的类型让错误在入口处失败;坏的类型只是把复杂度推迟到所有调用者身上。

我喜欢这些类型,但不会见到新玩具就往表里塞。先问数据到底是什么,再选最能诚实描述它的类型。数据库知道得越多,应用需要猜的就越少;但数据库承担得越多,迁移和生态的成本也越真实。这个取舍,才是 PostgreSQL 类型系统真正好玩的地方。

PostgreSQL 中那些 MySQL 没有的数据类型怎么玩https://aoang.x2ox.com/2026/postgresql-types/
作者Aoang
发布于2026-07-13
更新于2026-07-13
许可协议CC BY-NC-SA 4.0
转载或引用本文时请遵守许可协议,并注明出处。