PostgreSQL的复合类型提供了一种将多个字段打包成一个值的方式,适合需要返回多列数据、复用行结构或构建复杂参数对象的场景。创建复合类型的语法并不复杂,但访问字段时的小括号规则经常被忽略,导致查询报错。下面从基础定义开始,逐步展开复合类型在真实业务中的用法。

一、创建复合类型与基础表结构
复合类型通过CREATE TYPE命令定义。语法为:CREATE TYPE type_name AS (字段名 数据类型, 字段名 数据类型, ...);。字段可以包含大部分PostgreSQL内置类型,也可以嵌套其他复合类型。例如定义一个地址类型:
CREATE TYPE address_type AS (
province TEXT,
city TEXT,
street TEXT,
zip_code TEXT
);
执行成功后,address_type就成为一个可复用的数据类型。可以把复合类型放在普通表中作为列类型,也可以直接创建基于该类型的表。例如:
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
home_address address_type,
work_address address_type
);
这样users表就拥有两个复合类型字段。向表中插入数据时,可以通过ROW表达式构造复合值:
INSERT INTO users (name, home_address)
VALUES ('张三', ROW('浙江省', '杭州市', '文一西路', '310000'));
也可以使用字符串字面量形式,但推荐ROW表达式,因为它能正确处理类型转换和引号嵌套。复合类型创建后还可以通过ALTER TYPE ADD ATTRIBUTE增加字段,但需要注意已有数据不会自动填充,且某些场景下修改复合类型需要先删除依赖对象。
二、字段访问语法与括号规则
访问复合类型字段的标准写法是(composite_value).field_name。括号用于消除解析歧义。如果直接写composite_value.field_name,PostgreSQL可能把它解释成表名加列名,而不是复合类型字段访问。例如查询用户的城市:
SELECT name, (home_address).city AS home_city FROM users;
如果省略外层括号写成home_address.city,数据库会尝试寻找名为home_address的表并读取其city列,结果通常报错。当复合类型字段出现在表达式、函数或子查询中时,括号同样不能少。比如在WHERE条件里比较城市:
SELECT name FROM users WHERE (home_address).city = '杭州市';
对于多个字段同时访问,可以在SELECT列表中多次使用相同复合列并分别取不同字段,也可以使用子查询展开。需要注意的是,如果复合类型本身为NULL,那么访问任何字段都会得到NULL,不会报错。例如home_address为NULL时,(home_address).city返回NULL。
当复合类型来自函数返回值时,同样必须把函数调用整体放进括号。例如有一个返回复合类型的函数get_user_address(user_id),访问城市要写成(get_user_address(1)).city。如果缺少外层括号,解析器会认为get_user_address是表名,city是列名,从而报错。这个规则在自定义函数、触发器NEW/OLD引用中都非常关键。
三、复合类型与函数返回多列
PostgreSQL函数如果需要返回多个相关值,可以把复合类型作为返回类型。这样调用方可以用一个函数拿到结构化结果,而不必定义多条OUT参数。例如创建一个函数返回用户的默认地址:
CREATE FUNCTION get_user_home_address(p_user_id INT)
RETURNS address_type
LANGUAGE sql
AS $$
SELECT home_address
FROM users
WHERE user_id = p_user_id;
$$;
调用函数并访问字段时,语法为(get_user_home_address(1)).city。还可以把整行结果直接作为复合类型插入另一张表:
INSERT INTO address_log (user_id, addr) SELECT user_id, get_user_home_address(user_id) FROM users WHERE home_address IS NOT NULL;
除了命名复合类型,PostgreSQL还提供record类型。record可以表示任意行结构,但无法持久化存储,通常需要配合列定义列表或RETURNS TABLE使用。命名复合类型更适合需要重复引用的场景,而record适合临时返回动态结构。两者的字段访问语法相同,都依赖小括号。
如果函数返回复合类型数组,可以在FROM子句中使用unnest展开,再访问字段。例如返回地址数组的函数,可以这样查询:
SELECT a.province, a.city FROM unnest(get_user_addresses(1)) AS a;
这里unnest把复合类型数组转换为行集,别名a代表单个复合值,在SELECT中通过别名加字段名访问不需要括号,因为此时a已经是表别名,符合表.列规则。这也是复合类型与表行类型之间的一个重要联系。
四、复合类型的更新、比较与JSON转换
更新复合类型字段时,不能只更新单个属性,需要重新构造整个复合值。常见做法是使用ROW表达式或jsonb_populate_record函数。例如只修改城市字段:
UPDATE users
SET home_address = ROW(
(home_address).province,
'宁波市',
(home_address).street,
(home_address).zip_code
)
WHERE user_id = 1;
这种方法需要列出所有字段,维护成本较高。PostgreSQL 9.5之后可以利用jsonb_populate_record更灵活地只覆盖指定字段,但需要先转换为jsonb:
UPDATE users
SET home_address = jsonb_populate_record(
home_address,
'{"city": "宁波市"}'::jsonb
)
WHERE user_id = 1;
复合类型与JSON互转很方便。使用to_json或to_jsonb可以把复合值转换为JSON对象:
SELECT to_json(home_address) AS home_address_json FROM users WHERE user_id = 1;
反过来,json_populate_record可以把JSON对象还原为复合类型。这个能力在接口数据交换和日志记录场景中非常实用。复合类型也支持比较操作,但需要注意:两个复合值相等意味着所有字段都相等,比较顺序遵循字段定义顺序。NULL字段参与比较时,PostgreSQL使用IS NOT DISTINCT FROM语义,即NULL与NULL视为相等。
五、常见误区与注意事项
第一个误区是在访问字段时漏掉括号。尤其在JOIN、GROUP BY、ORDER BY等子句中,复合列容易和表别名混淆。正确的写法始终是(composite).field。当复合列名与表名相同时,括号更不可少。第二个误区是试图修改复合类型内部字段的排序或重命名,却不考虑依赖对象。ALTER TYPE RENAME ATTRIBUTE可以重命名字段,但使用该字段的索引、约束、函数可能失效,需要检查pg_depend。
第三个误区是过度使用复合类型替代关系表。复合类型适合封装少量强相关属性,如地址、坐标、计量单位组合。如果字段会被频繁独立查询、建立索引或与其他表关联,拆分成普通列更合适。复合类型列上的内部字段通常无法直接创建独立索引,虽然可以通过表达式索引实现,但维护成本更高。
性能方面,复合类型作为函数返回值和变量传递并不会带来显著额外开销,因为它本质上是行结构。但是把复合类型用于大量行存储时,需要注意TOAST策略和行宽度。例如包含多个TEXT字段的复合类型可能触发TOAST压缩,影响更新性能。合理控制字段数量和类型宽度,比单纯依赖复合类型更能保证写入效率。
PostgreSQL复合类型字段访问CREATE TYPE修改时间:2026-10-01 18:45:33