大对象(Large Objects)概述
在 PostgreSQL 里存一个二进制文件,第一反应往往是 bytea 字段。bytea 简单、直观、跟着表一起走事务,绝大多数场景确实够用。但它有一个绕不开的天花板:单个字段的值最多 1 GB,而且每次读取都会把整个值拉进内存。
当你要存的是一段视频、一个几十 GB 的备份镜像、或者一个需要被反复读取其中一小段的归档文件时,bytea 就不合适了。PostgreSQL 为此提供了另一套机制——大对象(Large Objects,简称 LO)。
大对象把二进制数据拆成固定大小的数据块,存进一张系统表,用一个 4 字节整数(OID)来引用。它单个对象最大可达 4 TB,并且支持按需读写任意片段,不用把整个对象读进内存。本节讲清大对象是什么、底层怎么存、权限与事务怎么管,后续小节展开具体操作函数与 bytea 的选型对比。
前置知识
建议先阅读:自定义扩展开发
基础概念
什么是大对象
大对象是 PostgreSQL 提供的一种在数据库内部存储大规模二进制数据的机制。它不属于任何一张业务表,而是存放在系统目录表里,业务表只保存一个指向它的 OID。
一个典型的用法:photos 表存元数据,真正的图片内容存在大对象里。
CREATE TABLE photos (
id serial PRIMARY KEY,
title text,
filename text,
raster oid -- 这里只存一个 OID,指向真正的大对象
);
-- 从服务器文件系统导入一个文件,返回新的大对象 OID
SELECT lo_import('/tmp/photo.png'); lo_import
-----------
16385
(1 row)把 16385 写进 photos.raster,图片的二进制内容就"存进数据库"了。业务表只增长了一个 4 字节整数,实际数据在系统表里分块存放。
大对象 vs 普通字段:本质区别
大对象和普通表字段最大的差异不在于"容量大",而在于存储位置与访问方式:
| 维度 | 普通字段(含 bytea、text) | 大对象(LO) |
|---|---|---|
| 存储位置 | 表的堆文件(超大值走 TOAST) | 独立系统表 pg_largeobject |
| 值的整体性 | 存进去就是一个完整值 | 拆成多个逻辑页分块存储 |
| 单值上限 | 1 GB | 约 4 TB |
| 读取方式 | 一次性读全值 | 可按偏移、按长度读取片段 |
| 修改方式 | 整值重写(或 TOAST 部分更新) | 可就地按偏移写入片段 |
| 生命周期 | 随行删除而消失 | 独立存在,需显式删除 |
| 引用方式 | 直接是列的值 | 通过 OID 间接引用 |
最后两条是实践中最容易踩坑的地方:大对象的生命周期与业务行解耦。删掉 photos 表里的一行,raster 指向的大对象不会被自动删除,它会变成"孤儿对象",一直占用空间,直到你显式调用 lo_unlink() 或运行 vacuumlo。
关键的四种 OID 相关概念
大对象的引用都用 OID,围绕它有几个容易混淆的概念,先厘清:
| 概念 | 类型 | 说明 |
|---|---|---|
| OID(Object Identifier) | oid | 无符号 4 字节整数,全库范围内唯一标识一个对象 |
| 大对象 OID(loid) | oid | 大对象在 pg_largeobject 中的标识,即 loid 列 |
oid 类型 | oid | 内置的 OID 类型,业务表通常用 oid 列存大对象引用 |
lo 类型 | lo | 由 lo 扩展提供的类型,本质是 oid 的一个 domain |
关于 lo 类型,需要澄清一个常见误解:lo 不是内置数据类型,PostgreSQL 的 OID 别名类型表(官方文档 Table 8.26)里并没有它。它由 contrib/lo 扩展提供,是一个建立在 oid 之上的 domain。这个扩展目前依然存在于 PostgreSQL 18 的官方文档中(附录 F.21),并未被移除。它的价值在于能额外挂一个触发器来自动清理大对象,详见后文。
存储结构:pg_largeobject 与分块
大对象的实际数据放在系统目录表 pg_largeobject 里,这张表只有三列:
| 列名 | 类型 | 说明 |
|---|---|---|
loid | oid | 该行所属的大对象的 OID |
pageno | int4 | 页号,从 0 开始 |
data | bytea | 这一页上承载的数据 |
每个大对象的数据被切成固定大小的逻辑页,每页单独占 pg_largeobject 的一行。虽然这个表叫"表",但它不是普通堆表,而是用 B-tree 索引组织的,索引键是 (loid, pageno)。这个设计让"读取第 N 页"变成一次索引定位,从而支撑随机访问。
页的大小由常量 LOBLKSIZE 决定,它的定义是:
LOBLKSIZE = BLCKSZ / 4BLCKSZ 是数据库块大小,默认 8 kB,因此 LOBLKSIZE 的默认值是 2 kB。数据中偏移量为 offset 的字节,位于大对象的第 offset / LOBLKSIZE 页(整除),页内偏移为 offset % LOBLKSIZE。
-- 查看当前实例的块大小相关参数
SHOW block_size; block_size
------------
8192
(1 row)因为 LOBLKSIZE = BLCKSZ / 4,默认配置下 LOBLKSIZE 就是 2048 字节。这意味着一个 1 MB 的文件会占用约 512 行 pg_largeobject 记录。
稀疏存储:不存在的页读出零
pg_largeobject 允许页号不连续。如果某个页从来没有被写过,它在表里就没有对应的行;当读取操作落在这些"空洞"上时,PostgreSQL 返回的是零字节,而不是报错。
这个行为非常像 Unix 文件系统里的稀疏文件(sparse file):你可以在一个空文件的末尾写入 1 MB 处,只占用实际写入的数据块,中间的空洞在读取时表现为零。
-- 创建一个新的大对象,直接从偏移 1MB 处写入 3 个字节
SELECT lo_put(lo_create(0), 1048576, '\x010203');无需先分配 1 MB,PostgreSQL 只会为真正写入的那一页分配空间。这个特性让大对象适合做"随机位置写入"的场景,比如往一个归档文件里追加或修补某一段。但要小心:无法区分"已写入的零"和"从未写入的零",读到的都是零字节。
元数据:pg_largeobject_metadata
除了数据本身,每个大对象还有一份元数据,存在 pg_largeobject_metadata 表里:
| 列名 | 类型 | 说明 |
|---|---|---|
oid | oid | 该元数据行的标识,即大对象的 OID |
lomowner | oid | 大对象的属主,引用 pg_authid.oid |
lomacl | aclitem[] | 访问权限列表(ACL) |
这张表的存在有历史原因。在 PostgreSQL 9.0 之前,pg_largeobject 是公开可读的,任何用户只要知道 OID 就能读到内容,而且大对象没有属主、没有权限控制。9.0 之后引入了属主与 ACL,把它们放到 pg_largeobject_metadata,同时把 pg_largeobject 限制为只有超级用户可读。
因此,要查看大对象的属主与权限,必须查询 pg_largeobject_metadata,而不是 pg_largeobject。
-- 查看所有大对象及其属主
SELECT oid AS loid,
lomowner::regrole AS owner,
lomacl
FROM pg_largeobject_metadata
ORDER BY oid; loid | owner | lomacl
-------+-----------+---------
16385 | app_owner |
16386 | app_owner | {app_user=rw/app_owner}
(2 rows)lomacl 为 NULL 表示使用默认权限(只有属主可操作);被 GRANT 过则显示为具体的 aclitem 数组。
语法与用法
创建与删除
大对象的创建与删除通过服务端函数完成,核心是三个:
-- 创建一个新的大对象,返回其 OID
-- 参数为该对象的 OID;传 0 表示自动分配
SELECT lo_create(0);
-- 旧式别名,与 lo_create 等价;其 mode 参数自 8.1 起被忽略
SELECT lo_creat(-1);
-- 删除一个大对象
SELECT lo_unlink(16385); lo_create
-----------
16387
(1 row)注意 lo_creat 是旧别名,只是"少一个 e",很容易与 lo_create 混淆。两者功能相同,但 lo_creat 的第二个参数(曾经的访问模式)自 PostgreSQL 8.1 起已被完全忽略,写什么值都没有影响。生产代码应当使用 lo_create。更详细的函数说明见下一节。
读写的最小示例
服务端的读写函数是 loread / lowrite(注意:服务端函数名没有下划线,与客户端 libpq 的 lo_read / lo_write 不同)。这两个函数作用于一个"大对象描述符",描述符由 lo_open 打开、lo_close 关闭。
BEGIN;
-- 打开大对象,返回描述符
SELECT lo_open(16385, x'00060000'::int); -- INV_READ | INV_WRITE
-- 写入数据
SELECT lowrite(0, '\x48656c6c6f');
-- 定位到开头再读
SELECT lo_lseek(0, 0, 0);
SELECT loread(0, 5);
-- 关闭描述符
SELECT lo_close(0);
COMMIT; loread
--------
\x48656c6c6f
(1 row)这个例子里出现的整数常量 x'00020000'::int 并不直观——它就是下面要讲的 INV_READ / INV_WRITE 组合。实际使用中,建议用它们的数值常量或直接在客户端接口里用符号名。
权限授予
大对象支持与表类似的 GRANT / REVOKE,语法里的对象类型是 LARGE OBJECT:
GRANT SELECT ON LARGE OBJECT 16385 TO app_user;
GRANT UPDATE ON LARGE OBJECT 16385 TO app_user;
GRANT ALL PRIVILEGES ON LARGE OBJECT 16385 TO app_owner;
REVOKE SELECT ON LARGE OBJECT 16385 FROM app_user;可授予的权限只有两种:SELECT(读)和 UPDATE(写、截断)。删除、修改属主、添加注释不能通过 GRANT 授予,只有属主或超级用户才能做。
详细说明
大对象权限模型
大对象的权限模型与表高度一致,但粒度更粗——只有读写两档。
| 操作 | 需要的权限 |
|---|---|
读取内容(loread / lo_get) | SELECT |
写入、修改内容(lowrite / lo_put / lo_truncate) | UPDATE |
删除对象(lo_unlink) | 属主或超级用户 |
修改属主(ALTER LARGE OBJECT ... OWNER TO) | 属主或超级用户 |
添加注释(COMMENT ON LARGE OBJECT) | 属主或超级用户 |
授予权限(GRANT ON LARGE OBJECT) | 属主或超级用户 |
一个容易被忽略的细节:权限检查发生在 lo_open 打开大对象的那一刻。这是 PostgreSQL 11 的行为变更——在此之前,检查推迟到第一次实际读写调用。这个变化意味着,即使用户打开后什么都没做,权限不足也会在 lo_open 阶段直接失败。
-- 以无权限的用户打开一个大对象
SELECT lo_open(16385, 262144); -- INV_READERROR: permission denied for large object 16385lo_compat_privileges 参数
为了兼容 PostgreSQL 9.0 之前"大对象无权限控制"的旧行为,系统提供了一个 lo_compat_privileges 参数:
SHOW lo_compat_privileges; lo_compat_privileges
----------------------
off
(1 row)它默认为 off,即启用 9.0 引入的权限检查。只有为了兼容老应用时才考虑打开,且只有超级用户和拥有 SET 权限的角色才能修改。生产环境不应开启——打开它等于让所有大对象对所有用户可读可写。
事务语义:必须在事务内操作
大对象操作有一个硬性要求:必须在事务块内执行。这不是建议,而是设计约束。
原因在于"大对象描述符"的生命周期:lo_open 返回的描述符只在当前事务内有效,事务结束(提交或回滚)时自动关闭。这样设计的目的是保证一致性——大对象的读写与普通表数据共享同一个事务视图。
-- 正确:包在事务里
BEGIN;
SELECT lo_open(16385, 131072);
SELECT loread(0, 100);
SELECT lo_close(0);
COMMIT;-- 错误:事务外直接操作
SELECT lo_create(0);ERROR: invalid large-object descriptor: 0更严格的场景是只读事务:如果一个事务声明为只读(SET TRANSACTION READ ONLY 或 BEGIN READ ONLY),那么大对象写入会被拒绝。
BEGIN READ ONLY;
SELECT lo_open(16385, 262144); -- INV_READ,只读打开,允许
SELECT lowrite(0, '\x00'); -- 写入,禁止ERROR: cannot write to large object 16385 in a read-only transactionINV_READ 与 INV_WRITE:两个模式常量
打开大对象时需要指定访问模式,由两个位常量组合而成,定义在客户端头文件 libpq/libpq-fs.h 中:
| 常量 | 值(十六进制) | 含义 |
|---|---|---|
INV_READ | 0x00040000 | 允许读取 |
INV_WRITE | 0x00020000 | 允许写入 |
只读打开用 INV_READ,读写打开用 INV_READ | INV_WRITE。这两个值是位标志(bitmask),可以按位或组合。
这两个模式对读取行为的影响还有一个微妙之处:以 INV_READ 打开时,读取的可见性遵循更严格的快照语义,类比 REPEATABLE READ——同一个事务内多次读取看到一致的快照;而带 INV_WRITE 打开时,类似于 READ COMMITTED,可能看到事务期间其他已提交事务的修改。这个差异在实际中不常被注意到,但在并发写同一个大对象的场景下会有影响。
服务端函数与客户端函数的分工
初学大对象最容易踩的坑,就是把服务端函数与客户端函数混为一谈。它们名字相似,但运行位置和调用方式完全不同:
| 层次 | 函数名风格 | 调用方式 | 代表函数 |
|---|---|---|---|
| 服务端(SQL 层) | lo_ 开头 + 无下划线动词 | SQL 语句 | loread、lowrite、lo_lseek |
| 客户端(libpq) | lo_ 开头 + 有下划线动词 | C 函数调用 | lo_read、lo_write、lo_lseek |
注意 lo_lseek 和 lo_close、lo_open 在两侧同名,而读写的动词一侧无下划线(loread/lowrite)、一侧有下划线(lo_read/lo_write)。写代码时务必确认自己在哪一层。
服务端(SQL) 客户端(libpq C API)
loread() ←→ lo_read()
lowrite() ←→ lo_write()
lo_lseek() ←→ lo_lseek()
lo_open() ←→ lo_open()
lo_close() ←→ lo_close()文件导入导出与 pg_read_server_files
lo_import / lo_export 这一对函数比较特殊:它们在服务端执行,读写的是数据库服务器所在主机的文件系统,而不是客户端主机的文件系统。
-- 把服务器上的 /etc/motd 导入成一个大对象
SELECT lo_import('/etc/motd'); lo_import
-----------
16388
(1 row)-- 把大对象 16388 导出到服务器的 /tmp/motd
SELECT lo_export(16388, '/tmp/motd');正因为这意味着"能读写服务器文件系统",默认只有超级用户能调用它们。如果希望普通用户也能用,需要通过预定义角色授权:
| 预定义角色 | 能做什么 |
|---|---|
pg_read_server_files | 读取服务器上的任意文件(含 lo_import) |
pg_write_server_files | 写入服务器上的任意文件(含 lo_export) |
pg_execute_server_program | 执行服务器上的程序(COPY ... PROGRAM) |
GRANT pg_read_server_files TO app_importer;
GRANT pg_write_server_files TO app_exporter;授予这三个角色要极其慎重——它们绕过了操作系统的权限体系,等价于把服务器文件系统的读写能力交给了该角色。
大对象与 bytea 的互转
PostgreSQL 提供了两个函数,让大对象和 bytea 之间可以互相转换,这在数据迁移时非常有用:
-- bytea → 大对象:把一段字节写入大对象(loid 传 0 表示新建)
SELECT lo_from_bytea(0, '\xdeadbeef');
-- 大对象 → bytea:读取整个大对象
SELECT lo_get(16389); lo_from_bytea
---------------
16389
(1 row)
lo_get
--------
\xdeadbeef
(1 row)lo_get 还能只读一部分,这对于"只取文件头部做格式识别"之类的需求很实用:
-- 只读前 3 个字节
SELECT lo_get(16389, 0, 3); lo_get
--------
\xdead
(1 row)自动清理:lo 扩展与 lo_manage 触发器
如前所述,大对象最大的实践痛点是孤儿对象:业务行删了,大对象还在。手动调用 lo_unlink 容易遗漏,因此 lo 扩展提供了一个触发器函数 lo_manage 来自动化这件事。
这个扩展提供两样东西:
- 一个名为
lo的数据类型(本质是oid的 domain),让业务列在语义上更明确; - 一个触发器函数
lo_manage,在行更新或删除时自动清理旧的大对象。
典型用法:
CREATE EXTENSION lo;
CREATE TABLE image (
title text,
raster lo -- 用 lo 类型代替 oid
);
CREATE TRIGGER t_raster
BEFORE UPDATE OR DELETE ON image
FOR EACH ROW EXECUTE FUNCTION lo_manage(raster);挂上触发器后,删除 image 中的一行,raster 指向的大对象会被自动 lo_unlink;更新 raster 为另一个 OID 时,旧的也会被清理。
但它并不能覆盖所有情况。官方文档明确指出:lo_manage 触发器是 BEFORE UPDATE OR DELETE 级别的,无法感知 DROP TABLE 和 TRUNCATE——这两类操作直接丢弃整表内容,不会触发逐行的 delete 触发器,仍然会留下孤儿对象。所以即使使用了 lo 扩展,仍然建议定期运行 vacuumlo 做兜底扫描。
孤儿对象清理:vacuumlo
vacuumlo 是 PostgreSQL 自带的一个命令行工具,专门用来扫描并删除孤儿大对象。它的工作原理是:遍历所有类型为 oid 或 lo 的列,收集其中出现过的所有 OID,然后把 pg_largeobject_metadata 里没有被任何列引用的大对象删掉。
# 用 dry-run 模式先看看会删掉什么(不会真的删)
vacuumlo -n -v postgresql://app@localhost/mydbConnected to database "mydb"
Would remove large object 16385
...确认无误后去掉 -n 正式执行:
vacuumlo -v postgresql://app@localhost/mydb常用选项:
| 选项 | 含义 |
|---|---|
-l limit | 每个事务最多处理多少个大对象,默认 1000 |
-n | dry-run,只报告不删除 |
-v | 输出详细过程 |
-V | 打印版本 |
-? | 显示帮助 |
需要注意两点:一是它只识别类型为 oid 或 lo 的列,如果大对象 OID 被存在 text 或 bigint 列里,vacuumlo 会误判为孤儿并删除;二是它不做域名展开,用 domain 包装过的列也不会被识别。
版本差异一览
大对象机制经历过几次重要演进,了解这些有助于理解为什么文档里有些描述看起来"自相矛盾":
| 版本 | 变化 |
|---|---|
| 8.1 | lo_creat 的 mode 参数被忽略 |
| 8.3 | libpq 新增 lo_truncate |
| 8.4 | libpq 新增 lo_import_with_oid |
| 9.0 | 引入属主与 ACL,lomacl 迁入 pg_largeobject_metadata;pg_largeobject 限制为超级用户可读;bytea 的 hex 输出格式引入 |
| 9.3 | libpq 新增 lo_lseek64 / lo_tell64 / lo_truncate64,支持超过 2 GB 的偏移 |
| 11 | 权限检查从首次读写调用移至 lo_open |
其中最关键的是 9.0 的变化:在此之前大对象完全无权限控制,如果你在网上看到"任何用户都能读大对象"的说法,那是 9.0 之前的旧行为。
适用与不适用场景
大对象不是"更高级的 bytea",它有明确的适用边界:
适合用大对象的场景:
- 单个对象超过 1 GB,
bytea存不下; - 需要随机读取或写入局部内容,例如只读文件头、只在某位置打补丁;
- 需要流式处理,不希望把几十 GB 内容一次性读进内存;
- 数据本身就是"文件",与业务行是松散的引用关系。
不适合用大对象的场景:
- 对象较小(几百 KB 以内),
bytea更简单且能跟着行一起事务管理; - 需要逻辑复制——大对象不被逻辑复制,官方明确说明没有变通方案;
- 需要
pg_dump按 schema 精确导出——指定-n时大对象不会被导出; - 希望删除业务行时自动连带删除数据(
bytea天然如此,大对象需要触发器或vacuumlo)。
实战示例
完整流程:导入、挂载、读取、清理
下面走一遍完整的生命周期,用 psql 演示。
-- 1) 建表,用 oid 列存引用
CREATE TABLE documents (
id serial PRIMARY KEY,
name text NOT NULL,
content oid
);
-- 2) 从服务器导入一个文件
SELECT lo_import('/tmp/report.pdf') AS loid; loid
--------
16390
(1 row)-- 3) 把 OID 挂到业务行上
INSERT INTO documents (name, content)
VALUES ('季度报告.pdf', 16390);INSERT 0 1-- 4) 读取大对象的前 16 个字节(例如判断文件类型)
SELECT lo_get(d.content, 0, 16) AS header
FROM documents d
WHERE d.name = '季度报告.pdf'; header
--------------------------------------
\x255044462d312e370a...
(1 row)25 50 44 46 是 %PDF 的十六进制,说明确实是一个 PDF 文件。
-- 5) 删除业务行,大对象不会自动消失
DELETE FROM documents WHERE name = '季度报告.pdf';
-- 6) 确认大对象仍然存在
SELECT count(*) FROM pg_largeobject_metadata WHERE oid = 16390; count
-------
1
(1 row)-- 7) 显式清理
SELECT lo_unlink(16390); lo_unlink
-----------
1
(1 row)这个例子把"孤儿对象"的成因演示得很清楚:第 5 步删除业务行后,第 6 步仍能查到大对象。这也是为什么推荐用 lo 扩展的 lo_manage 触发器或定期跑 vacuumlo。
用 lo 扩展自动化清理
把上面的流程改造成"自动清理"版本:
CREATE EXTENSION lo;
CREATE TABLE documents (
id serial PRIMARY KEY,
name text NOT NULL,
content lo
);
CREATE TRIGGER t_content
BEFORE UPDATE OR DELETE ON documents
FOR EACH ROW EXECUTE FUNCTION lo_manage(content);
INSERT INTO documents (name, content)
VALUES ('季度报告.pdf', lo_import('/tmp/report.pdf'));
-- 删除行时,触发器自动调用 lo_unlink
DELETE FROM documents WHERE name = '季度报告.pdf';DELETE 0 1此后 pg_largeobject_metadata 中对应的记录会自动消失。
查询所有大对象及其大小
pg_largeobject 的每一行是一页,把某个 loid 的所有页长度相加,就是大对象的近似大小:
SELECT loid,
count(*) AS pages,
sum(length(data)) AS total_bytes,
pg_size_pretty(sum(length(data))) AS size
FROM pg_largeobject
GROUP BY loid
ORDER BY total_bytes DESC
LIMIT 10; loid | pages | total_bytes | size
-------+-------+-------------+--------
16391 | 5120 | 10485760 | 10 MB
16390 | 512 | 1048576 | 1024 kB
(2 rows)注意这里统计的是实际写入的字节数。因为稀疏存储,如果大对象中间有大段空洞,sum(length(data)) 会小于 (max(pageno) + 1) * LOBLKSIZE,即逻辑大小与物理占用不同。
-- 逻辑大小(按最大页号推算)
SELECT loid,
(max(pageno) + 1) * 2048 AS logical_bytes
FROM pg_largeobject
GROUP BY loid;查看权限并授予只读
-- 查看大对象属主与 ACL
SELECT oid, lomowner::regrole AS owner, lomacl
FROM pg_largeobject_metadata
WHERE oid = 16391; oid | owner | lomacl
-------+-----------+---------
16391 | app_owner |
(1 row)-- 授予只读权限
GRANT SELECT ON LARGE OBJECT 16391 TO report_reader;
-- 再查一次,ACL 已经变化
SELECT oid, lomacl FROM pg_largeobject_metadata WHERE oid = 16391; oid | lomacl
-------+-------------------------------
16391 | {report_reader=r/app_owner}
(1 row)report_reader=r/app_owner 表示:report_reader 拥有 r(SELECT,即读)权限,由 app_owner 授予。
定位与分片读取
大对象的核心优势是分片读取。下面演示如何只读出一个大对象中间的一段:
BEGIN;
-- 打开(只读模式),描述符为 0
SELECT lo_open(16391, 262144); -- INV_READ
-- 定位到偏移 1024
SELECT lo_lseek(0, 1024, 0);
-- 读取 32 字节
SELECT loread(0, 32);
-- 查看当前偏移
SELECT lo_tell(0);
SELECT lo_close(0);
COMMIT; lo_tell
---------
1056
(1 row)读完 32 字节后,偏移从 1024 推进到 1056,说明 loread 会移动内部指针。
注意事项
大对象不是自动垃圾回收的
删除业务行不会删除大对象,DROP TABLE 和 TRUNCATE 也不会触发 lo_manage。定期 vacuumlo 是必要的运维动作。这一点与 bytea 的体验差距最大。
逻辑复制不复制大对象
如果需要逻辑复制,不要用大对象存业务数据。官方文档明确写道:大对象不被复制,且没有变通方案,只能把数据存进普通表。这是选型时的硬性约束。
逻辑复制包含:表数据、序列(PG 10+)、DDL(部分)
逻辑复制不含:大对象lo_creat 是旧别名
写代码时统一用 lo_create。lo_creat 的 mode 参数自 8.1 起被忽略,容易误导读者以为它还能控制访问模式。
服务端与客户端函数名容易混淆
loread / lowrite(服务端)与 lo_read / lo_write(libpq 客户端)差一个下划线。看文档或报错时先确认是哪一层。
lo_import / lo_export 动的是服务器文件系统
它们不读客户端本地的文件。要用它们处理客户端文件,得先把文件放到服务器上(或用 psql 的 \lo_import,它是在客户端发起、把文件内容通过协议送到服务器)。
不要开启 lo_compat_privileges
打开它等于关闭大对象权限检查,任何用户都能读写任意大对象。除非在做 9.0 之前老应用的迁移兼容,否则保持 off。
vacuumlo 会误删非标准存储的引用
如果 OID 被存在 text、bigint 或某个 domain 列里,vacuumlo 认不出来,会把这些大对象当孤儿删掉。用 -n 先 dry-run 是必要的。
事务边界要正确
大对象描述符只在当前事务内有效,事务一结束就失效。在连接池里尤其要小心:如果事务被意外提交,后续的大对象操作会直接报 invalid large-object descriptor。
权限检查在 lo_open 阶段
PostgreSQL 11 之后,权限不足会在打开时立即报错,而不是等到第一次读写。写错误处理逻辑时要按这个时机来。
稀疏空洞读出来是零
不要指望能区分"写入的零"与"未写入的零",读操作都返回零字节。依赖长度判断的地方要自己记录实际数据长度。
最佳实践
先用
bytea,确认不够用再上大对象。对象小于 1 GB 且不需要随机访问时,bytea更简单,且天然参与事务与逻辑复制。业务表用
oid列存引用,并建索引。引用列是查询入口,加上索引避免全表扫描:sqlCREATE INDEX idx_documents_content ON documents (content);给大对象引用列加
NOT NULL与外键式约束。虽然oid无法建真正的外键,但可以在应用层或触发器里保证引用非空且有效。用
lo扩展的lo_manage触发器处理常规增删改,减少手工lo_unlink遗漏:sqlCREATE TRIGGER t_content BEFORE UPDATE OR DELETE ON documents FOR EACH ROW EXECUTE FUNCTION lo_manage(content);把
vacuumlo -n加入例行巡检,先 dry-run 观察,再确认后正式执行。频率建议与VACUUM巡检一致。大对象的读写务必包在显式事务里,并使用
BEGIN ... COMMIT而不是依赖自动提交:sqlBEGIN; -- lo_open / loread / lowrite / lo_close COMMIT;权限授予遵循最小权限原则,只给需要的角色
SELECT或UPDATE,不要图省事给ALL。严格控制
pg_read_server_files/pg_write_server_files的授予,这两个角色等价于服务器文件系统的读写权限。大文件上传下载走客户端接口(libpq / JDBC / PHP pgsql),不要用服务端
lo_import/lo_export,前者不依赖服务器文件系统,权限面更小。监控
pg_largeobject的体积与孤儿数量,把它纳入容量规划:sqlSELECT pg_size_pretty(pg_total_relation_size('pg_largeobject')) AS lo_table_size, count(*) AS metadata_rows FROM pg_largeobject_metadata;备份策略要显式确认大对象被包含。
pg_dump默认包含大对象,但指定-n(只导某个 schema)时会排除;如果需要排除可用-B,需要包含可用-b。避免把大对象用于需要逻辑复制或逻辑订阅的数据。这条是选型红线,一旦用了就没有官方推荐的变通方案。
参考链接
- PostgreSQL 官方文档:Large Objects
- PostgreSQL 官方文档:Introduction to Large Objects
- PostgreSQL 官方文档:Implementation Features of Large Objects
- PostgreSQL 官方文档:Server-Side Functions for Large Objects
- PostgreSQL 官方文档:pg_largeobject
- PostgreSQL 官方文档:pg_largeobject_metadata
- PostgreSQL 官方文档:lo module
- PostgreSQL 官方文档:vacuumlo
- PostgreSQL 官方文档:GRANT
- PostgreSQL 官方文档:TOAST