Tablespace(表空间)
PostgreSQL 的「表空间」常被误解成 Oracle 那种「逻辑存储单元」。实际上它做的事情非常单一:把某张表、某个索引的物理文件,从默认目录挪到另一个磁盘路径下。页格式、SQL 语义、MVCC 全部不变,变的只是文件落在哪块盘上。本节讲解表空间的原理、创建与使用、默认表空间的真实含义,以及它在「把热数据放到 SSD」这类场景中的正确用法。
前置知识
阅读本节前,建议先了解:WAL Files(WAL 文件组织)
基础概念
表空间(Tablespace) 是 PostgreSQL 中「一个目录位置的别名」。你定义它、给它一个路径、给它命名,之后就可以在建表时指定「这张表放在哪个表空间里」,PostgreSQL 就会把该表的数据文件创建在那个路径下。
理解表空间需要先统一以下术语:
| 术语 | 英文 | 说明 |
|---|---|---|
| 表空间 | Tablespace | 目录位置的命名别名,记录在 pg_tablespace |
| 默认表空间 | pg_default | 未显式指定时使用的表空间,指向 PGDATA/base |
| 全局表空间 | pg_global | 系统目录专用,指向 PGDATA/global,不可用于用户对象 |
| 符号链接 | Symbolic Link | pg_tblspc/<oid> 是指向真实路径的软链 |
| 表空间 OID | Tablespace OID | 表空间的唯一标识,也是 pg_tblspc/ 下的目录名 |
| 相对路径 | Relative Path | 文件路径表达中的第二段,如 1663/16384/16385 中的 16384 |
| LOCATION | LOCATION 子句 | 建库时指定默认表空间,如 CREATE DATABASE ... TABLESPACE |
表空间的物理形态
创建一个表空间,PostgreSQL 会做三件事:
CREATE TABLESPACE ts_hot LOCATION '/data/pg/tblspc_hot';
① 在 $PGDATA/pg_tblspc/ 下创建一个软链接
$PGDATA/pg_tblspc/16405 -> /data/pg/tblspc_hot
(16405 是新表空间的 OID)
② 在目标路径下创建版本子目录(版本号保证跨大版本升级不混用)
/data/pg/tblspc_hot/PG_18_202506071/ ← 实际放文件的目录
③ 在系统目录里登记一条记录
pg_tablespace: oid=16405, spcname='ts_hot',
spcowner=10, spclocation 由 pg_tblspc 推导之后在 ts_hot 里建表时,数据文件的完整路径是:
/data/pg/tblspc_hot/PG_18_202506071/16384/16385
└──────── 真实挂载点 ────────┘ └──┬──┘ └──┬──┘
│ └── 表的 relfilenode
└── 数据库 OID(即 pg_database.oid)而默认表空间里的表,路径则少了一层:
$PGDATA/base/16384/16385
└─┬─┘ └──┬──┘ └──┬──┘
│ │ └── 表的 relfilenode
│ └── 数据库 OID
└── pg_default 的固定位置pg_relation_filepath 能直接给出答案
不要手工拼路径,用 pg_relation_filepath('表名') 就能拿到权威的相对路径。它是排查表空间问题的第一工具——因为老式文档里的 spclocation 列早已移除,真实路径要从 pg_tblspc 软链和版本目录推导。
与 Oracle 表空间的本质区别
这是最容易带错直觉的地方:
| 维度 | PostgreSQL 表空间 | Oracle 表空间 |
|---|---|---|
| 本质 | 一个目录别名 | 一组数据文件的逻辑容器 |
| 物理单位 | 目录(含版本子目录) | 数据文件(Datafile) |
| 与表的关系 | 表内所有分段(表、索引、TOAST)可各自指定 | 段分配到表空间 |
| 存储上限 | 无独立配额概念,受文件系统限制 | 可设配额、可自动扩展 |
| 是否必须 | 可选,默认只用 pg_default | 必须,且需规划 |
| 数量代价 | 需要额外的软链与目录,Open file 无影响 | 影响管理复杂度 |
最重要的差异是第一行:PostgreSQL 的表空间只是「换个地方放文件」,它不提供任何额外的隔离、配额或性能保证——性能差异完全来自那块盘本身。
核心机制
为什么需要表空间
表空间解决的从来不是「逻辑建模」问题,而是三个物理运维问题:
① 磁盘容量不够了
PGDATA 所在分区写满 → 无法扩容该分区
→ 把大表、大索引迁到另一块盘 ✔
② 热点与冷数据混在一盘上
users 表被高频读写,logs 表几乎只读
→ 把 users 放到 SSD 表空间,logs 留在 HDD ✔
③ 需要把备份、临时文件、WAL 分开到不同盘
→ temp_tablespaces 指向专门的临时表空间 ✔反过来,如果一个实例只有一块盘、容量充裕、读写均匀,那么建表空间没有任何收益,只会增加运维复杂度。
关键机制:表空间是「目录级」而非「表级」的重定向
指定表空间发生在 对象创建时:
-- 建表时指定表空间
CREATE TABLE users (...) TABLESPACE ts_hot;
-- 建索引时指定(可以和表不同)
CREATE INDEX idx_users_email ON users (email) TABLESPACE ts_hot_idx;
-- 已存在的对象迁移表空间
ALTER TABLE users SET TABLESPACE ts_cold;注意第三条:ALTER TABLE ... SET TABLESPACE 是重写操作,它把整张表的文件从旧路径复制到新路径,期间持有 ACCESS EXCLUSIVE 锁。索引需要单独迁移,不会自动跟随表:
ALTER TABLE only SET TABLESPACE 的行为:
users 表文件 → 复制到新表空间 ✔
users 的索引 → 留在原表空间 ✗(必须单独 ALTER INDEX)
TOAST 表 → 跟随主表迁移 ✔
TOAST 表索引 → 跟随迁移 ✔索引表空间与表表空间分离的意义
把索引放到另一个表空间不是为了「省空间」,而是为了分散 I/O:
磁盘 A(pg_default) 磁盘 B(ts_hot_idx)
├── users 主表文件 ├── idx_users_email
├── users 的 TOAST ├── idx_users_phone
└── orders 主表文件 └── idx_orders_user_id
一次索引扫描 → 在磁盘 B 上读索引 → 拿着 ctid 到磁盘 A 读堆表
两盘并行,随机 I/O 被摊开在机械盘时代这个收益很明显;在 SSD/NVMe 上收益基本消失,反而增加了管理成本。
数据库级别的默认表空间
CREATE DATABASE 可以指定整个库的默认表空间,这样库里不显式指定表空间的对象都会落在那里:
CREATE DATABASE demo TABLESPACE ts_cold;这之后 demo 库里的表,路径会变成 pg_tblspc/<ts_oid>/PG_18_*/<db_oid>/<relfilenode>。这解决了「一个模板库复制出多个库、每个库想放不同盘」的问题——因为数据库 OID 不同,文件天然不冲突。
但要注意:template0、template1、postgres 三个库不能随便改表空间(template0 也不允许指定非默认表空间),它们是新建库的模板,必须保持可访问。
删除表空间的严格条件
DROP TABLESPACE ts_cold;这条语句只有在表空间完全空时才能成功。任何「还留着对象」的情况都会报错:
ERROR: tablespace "ts_cold" is not empty因为表空间可能被别的库使用,PostgreSQL 无法自动判断该删什么。所以删除前必须逐库检查:
-- 在每一个数据库里都要执行,才能确认该表空间是否真的空了
SELECT n.nspname AS schema_name,
c.relname AS object_name,
c.relkind
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_tablespace t ON t.oid = c.reltablespace
WHERE t.spcname = 'ts_cold'
ORDER BY 1, 2;另外,DROP TABLESPACE 只删除 pg_tblspc 里的软链,不会删除目标目录——真正的目录需要你手工清理,这是防止误删数据的保护设计。
相关参数
| 参数/命令 | 默认值 | 说明 |
|---|---|---|
default_tablespace | 空字符串 | 未指定表空间时使用的默认表空间;空表示 pg_default |
temp_tablespaces | 空字符串 | 临时表与排序溢出文件的表空间,逗号分隔可多个 |
CREATE TABLESPACE ... WITH (random_page_cost = ...) | 继承 | 表空间级参数(PG15+ 支持 random_page_cost、seq_page_cost、effective_io_concurrency) |
pg_tblspc/ | —— | PGDATA 下的软链目录,一个表空间一条软链 |
PG_18_YYYYMMDD | —— | 表空间目录下的版本子目录,随大版本变化 |
\db(psql) | —— | 列出所有表空间 |
ALTER DATABASE ... SET TABLESPACE | —— | 改库的默认表空间(需要连接到其他库执行) |
PG15 起表空间可以携带磁盘代价参数,这让「把慢盘上的表空间调高 random_page_cost」成为可能:
# postgresql.conf
# 默认表空间(未在 SQL 中指定表空间对象时使用)
default_tablespace = ''
# 临时文件优先放到专用盘,缓解 PGDATA 盘压力
temp_tablespaces = 'ts_temp'-- 建一个带磁盘代价参数的表空间(PG15+)
CREATE TABLESPACE ts_slow
LOCATION '/data/pg/tblspc_slow'
WITH (random_page_cost = 2.0, effective_io_concurrency = 4);详细说明
表空间信息一览
-- 查看所有表空间:名字、属主、位置、大小
SELECT spcname,
pg_get_userbyid(spcowner) AS owner,
pg_tablespace_location(oid) AS location,
pg_size_pretty(pg_tablespace_size(oid)) AS size
FROM pg_tablespace
ORDER BY spcname; spcname | owner | location | size
---------------+----------+---------------------------+---------
pg_default | postgres | | 246 MB
pg_global | postgres | | 8129 kB
ts_cold | postgres | /data/pg/tblspc_cold | 14 GB
ts_hot | postgres | /data/pg/tblspc_hot | 1283 MB
ts_temp | postgres | /data/pg/tblspc_temp | 96 MB注意 pg_default 与 pg_global 的 location 是空的——它们没有独立的挂载路径,就是 PGDATA/base 与 PGDATA/global。
对象与表空间的对应关系
-- 列出当前库中所有"显式指定了非默认表空间"的对象
SELECT n.nspname AS schema_name,
c.relname AS object_name,
CASE c.relkind
WHEN 'r' THEN '普通表'
WHEN 'i' THEN '索引'
WHEN 'm' THEN '物化视图'
WHEN 't' THEN 'TOAST 表'
ELSE c.relkind::text
END AS object_type,
t.spcname AS tablespace,
pg_size_pretty(pg_relation_size(c.oid)) AS size
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_tablespace t ON t.oid = c.reltablespace
WHERE c.relkind IN ('r', 'i', 'm', 't')
ORDER BY pg_relation_size(c.oid) DESC
LIMIT 10; schema_name | object_name | object_type | tablespace | size
-------------+----------------------+-------------+------------+---------
public | orders | 普通表 | ts_cold | 8192 MB
public | idx_orders_created | 索引 | ts_cold | 2048 MB
public | users | 普通表 | ts_hot | 1024 MB
public | users_pkey | 索引 | ts_hot | 256 MB
public | pg_toast_16385 | TOAST 表 | ts_hot | 128 MB这张表恰好证明了前文的结论:users 的 TOAST 表跟随主表进了 ts_hot,但每个索引都单独记了一条——索引不会自动跟随。
默认表空间里的对象怎么查
上面的 JOIN pg_tablespace 只能查出「显式指定」的对象。要用一种写法同时覆盖「显式指定」与「继承默认表空间」的情况:
-- 查询某张表到底落在哪个表空间(含继承默认表空间的情形)
SELECT c.relname,
t.spcname AS tablespace,
pg_relation_filepath(c.oid) AS relative_path,
pg_tablespace_location(t.oid) AS location
FROM pg_class c
LEFT JOIN pg_tablespace t ON t.oid = c.reltablespace
WHERE c.relname IN ('users', 'orders', 'products'); relname | tablespace | relative_path | location
----------+------------+----------------------+--------------------------
users | ts_hot | pg_tblspc/16405/PG_18_202506071/16384/16385 | /data/pg/tblspc_hot
orders | ts_cold | pg_tblspc/16406/PG_18_202506071/16384/16390 | /data/pg/tblspc_cold
products | pg_default | base/16384/16392 |relative_path 就是 pg_relation_filepath() 的返回结果,它的第一段直接告诉你文件落在哪:
relative_path 首段 | 含义 | 实际磁盘位置 |
|---|---|---|
base/ | 默认表空间 | $PGDATA/base/... |
global/ | 全局表空间 | $PGDATA/global/... |
pg_tblspc/<oid>/ | 自定义表空间 | pg_tblspc/<oid> 软链指向的真实路径 |
磁盘层面的验证
# 1) 查看 pg_tblspc 下的软链指向
ls -l $PGDATA/pg_tblspctotal 0
lrwxrwxrwx 1 postgres postgres 22 Sep 20 09:00 16405 -> /data/pg/tblspc_hot
lrwxrwxrwx 1 postgres postgres 23 Sep 20 09:00 16406 -> /data/pg/tblspc_cold
lrwxrwxrwx 1 postgres postgres 24 Sep 20 09:00 16408 -> /data/pg/tblspc_temp# 2) 查看表空间真实目录结构(注意版本子目录)
ls -l /data/pg/tblspc_hot/
ls -l /data/pg/tblspc_hot/PG_18_202506071/total 4
drwx------ 3 postgres postgres 4096 Sep 20 09:00 PG_18_202506071
total 8
drwx------ 2 postgres postgres 4096 Sep 20 09:05 16384# 3) 与 SQL 查出的 relative_path 对照,确认文件确实在那里
ls -lh /data/pg/tblspc_hot/PG_18_202506071/16384/16385-rw------- 1 postgres postgres 1.0G Sep 20 12:20 /data/pg/tblspc_hot/PG_18_202506071/16384/16385# 4) 关键:确认 PostgreSQL 进程对该路径确实可读写
sudo -u postgres test -r /data/pg/tblspc_hot/PG_18_202506071 && echo "read OK"
sudo -u postgres test -w /data/pg/tblspc_hot/PG_18_202506071 && echo "write OK"权限是表空间最常见的坑:目录必须由运行 PostgreSQL 的 OS 用户拥有,权限必须是 700。否则会报 could not open directory 或 permission denied。
表空间与 pg_basebackup
pg_basebackup 是否备份表空间,取决于是否开启 --tablespace-mapping:
# 默认行为:表空间内容会被原样打包(可能与主数据一起放在同一 tar/目录)
pg_basebackup -D /backup/base -Ft -z
# 恢复时若表空间原路径不可用,可以重映射
pg_basebackup -D /backup/base -Ft -z \
--tablespace-mapping=/data/pg/tblspc_hot=/backup/ts_hot关键点:基础备份一定会包含表空间数据(它们也是数据的一部分),但备份工具需要知道往哪写。用 tar 格式时,表空间会生成独立的 tar 文件;用 plain 格式时,如果表空间路径不在 -D 目录内,pg_basebackup 会额外创建软链。
实战示例
示例 1:把大表迁到新磁盘(完整序列)
这是表空间最典型的运维场景——PGDATA 所在分区快满了,把最大的表迁到新挂载的盘。
准备阶段:
# 1) 新盘挂载并建目录(假设新盘挂在 /data)
sudo mkdir -p /data/pg/tblspc_cold
sudo chown postgres:postgres /data/pg/tblspc_cold
sudo chmod 700 /data/pg/tblspc_cold
# 2) 确认权限正确(必须 700 且属主是 postgres)
ls -ld /data/pg/tblspc_colddrwx------ 2 postgres postgres 4096 Sep 20 13:00 /data/pg/tblspc_cold创建表空间并评估现状:
-- 3) 创建表空间
CREATE TABLESPACE ts_cold LOCATION '/data/pg/tblspc_cold';
-- 4) 确认创建成功
SELECT spcname, pg_tablespace_location(oid) AS location
FROM pg_tablespace WHERE spcname = 'ts_cold';
-- 5) 先看清要迁什么、有多大(迁移前必做的评估)
SELECT c.relname AS object_name,
CASE c.relkind WHEN 'r' THEN '表' WHEN 'i' THEN '索引' END AS kind,
pg_size_pretty(pg_relation_size(c.oid)) AS size,
pg_relation_filepath(c.oid) AS path
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public'
AND c.relkind IN ('r', 'i')
AND c.relname IN ('orders', 'orders_pkey', 'idx_orders_created')
ORDER BY pg_relation_size(c.oid) DESC; object_name | kind | size | path
---------------------+------+----------+--------------------
orders | 表 | 8192 MB | base/16384/16390
idx_orders_created | 索引 | 2048 MB | base/16384/16391
orders_pkey | 索引 | 512 MB | base/16384/16392执行迁移(注意锁的影响):
-- 6) 记录迁移前的时间与锁等待情况(生产环境务必在低峰期)
SHOW lock_timeout;
SELECT clock_timestamp() AS migrate_start;
-- 7) 迁移索引(先迁小的,验证流程无误)
ALTER INDEX orders_pkey SET TABLESPACE ts_cold;
ALTER INDEX idx_orders_created SET TABLESPACE ts_cold;
-- 8) 迁移表(这一步会重写 8GB 数据,持 ACCESS EXCLUSIVE 锁)
ALTER TABLE orders SET TABLESPACE ts_cold;
-- 9) 确认迁移结果
SELECT c.relname,
COALESCE(t.spcname, 'pg_default') AS tablespace,
pg_size_pretty(pg_relation_size(c.oid)) AS size,
pg_relation_filepath(c.oid) AS path
FROM pg_class c
LEFT JOIN pg_tablespace t ON t.oid = c.reltablespace
WHERE c.relname IN ('orders', 'orders_pkey', 'idx_orders_created')
ORDER BY pg_relation_size(c.oid) DESC; relname | tablespace | size | path
--------------------+------------+----------+-------------------------------------------------
orders | ts_cold | 8192 MB | pg_tblspc/16410/PG_18_202506071/16384/16415
idx_orders_created | ts_cold | 2048 MB | pg_tblspc/16410/PG_18_202506071/16384/16416
orders_pkey | ts_cold | 512 MB | pg_tblspc/16410/PG_18_202506071/16384/16417结果解读:
- 三个对象的
path首段都变成了pg_tblspc/16410/,16410就是ts_cold的 OID。 - relfilenode 变了(
16390→16415):SET TABLESPACE是「新建文件 + 拷贝 + 删除旧文件」,所以会分配新的 relfilenode。这一点在建了物化视图或依赖 relfilenode 的监控脚本时要注意。 orders表迁移后,其 TOAST 表也自动跟到了ts_cold,但索引必须手工迁(本示例就是手工迁的)。- 迁移期间
orders无法读写,8GB 数据量下可能耗时数分钟,务必在维护窗口执行。
清理旧空间:
# 10) 迁移完成后确认 PGDATA 所在分区释放了空间
df -h $PGDATA
du -sh $PGDATA/base/16384Filesystem Size Used Avail Use% Mounted on
/dev/sda1 50G 28G 20G 56% /var/lib/pgsql
/data 500G 11G 470G 3% /data示例 2:把临时文件引导到专用表空间
排序、哈希、CREATE INDEX 都会产生临时文件,默认落在 PGDATA/base/pgsql_tmp。把临时表空间指到专用盘能显著缓解 PGDATA 盘压力:
-- 1) 建立临时表空间(PG15+ 可带磁盘代价参数)
CREATE TABLESPACE ts_temp
LOCATION '/data/pg/tblspc_temp'
WITH (random_page_cost = 1.1, effective_io_concurrency = 200);
-- 2) 在库级或会话级指定临时表空间
ALTER DATABASE demo SET temp_tablespaces = 'ts_temp';
-- 3) 确认生效(重连会话后检查)
SHOW temp_tablespaces;
-- 4) 触发一次大排序,观察临时文件落到哪里
SET work_mem = '64kB'; -- 故意调小以强制落盘
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
SELECT o.user_id, o.amount
FROM orders o
ORDER BY o.amount DESC
LIMIT 10; QUERY PLAN
---------------------------------------------------------
Limit (actual time=142.331..142.335 rows=10 loops=1)
Buffers: shared hit=1024, temp read=8192 written=8192 ← 产生了临时文件
-> Sort (actual time=142.318..142.322 rows=10 loops=1)
Sort Key: amount DESC
Sort Method: external merge Disk: 65536kB ← 65536kB 落到了 ts_temp
Buffers: shared hit=1024, temp read=8192 written=8192
-> Seq Scan on orders (actual time=0.011..58.204 rows=500000 loops=1)
Buffers: shared hit=1024
Planning Time: 0.198 ms
Execution Time: 142.512 ms-- 5) 在操作系统层面确认临时文件确实在 ts_temp,而不是 PGDATAls -lh /data/pg/tblspc_temp/PG_18_202506071/pgsql_tmp/ | head -3-rw------- 1 postgres postgres 67108864 Sep 20 13:12 pgsql_tmp12345.0结果解读:
Sort Method: external merge Disk: 65536kB说明排序溢出到了磁盘,这部分正是临时表空间的典型负载。- 临时文件在
ts_temp/PG_18_202506071/pgsql_tmp/下,文件名形如pgsql_tmp<PID>.<序号>。 - 临时文件在事务/查询结束后会被自动删除,但崩溃后残留的文件需要重启时清理,因此临时表空间所在的盘必须有可靠的清理路径。
- 把
temp_tablespaces指向 SSD 能显著加速大排序与哈希连接;指向机械盘则可能适得其反。
示例 3:安全下线一个表空间
下线表空间是「删库跑路」级别的高危操作,正确流程是先确认空、再删记录、最后清目录:
-- 1) 确认表空间下还有没有对象(必须在每个库都跑一遍)
SELECT current_database() AS db, count(*) AS object_count
FROM pg_class c
JOIN pg_tablespace t ON t.oid = c.reltablespace
WHERE t.spcname = 'ts_old'
GROUP BY current_database();
-- 2) 若上面返回非 0,先迁走这些对象
-- (表与索引要分别处理,可用以下 SQL 生成 ALTER 语句)
SELECT format('ALTER %s %I.%I SET TABLESPACE pg_default;',
CASE c.relkind WHEN 'i' THEN 'INDEX' WHEN 'm' THEN 'MATERIALIZED VIEW' ELSE 'TABLE' END,
n.nspname, c.relname) AS ddl
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_tablespace t ON t.oid = c.reltablespace
WHERE t.spcname = 'ts_old'
AND c.relkind IN ('r', 'i', 'm'); ddl
--------------------------------------------------
ALTER TABLE public.legacy_log SET TABLESPACE pg_default;
ALTER INDEX public.legacy_log_pkey SET TABLESPACE pg_default;-- 3) 确认某库确实用完了,才能删表空间
DROP TABLESPACE ts_old;# 4) DROP 只删软链,目标目录必须自己清理
ls -l $PGDATA/pg_tblspc/ # 确认软链已消失
ls -ld /data/pg/tblspc_old # 目录还在,需手工删除
sudo rm -rf /data/pg/tblspc_old结果解读:
- 第 1 步是关键,必须对每个数据库执行,因为
pg_class只能看到当前库的对象。 - 第 2 步生成的 DDL 可以直接复制执行,比手工拼 SQL 可靠。
DROP TABLESPACE报is not empty时不要怀疑有 bug,一定是某个库里还留着对象(包括 TOAST 表与索引)。- 目录不自动删除是保护设计:万一是误删,目录还在就还有救。
注意事项
- 表空间目录权限必须是
700且属主为 PostgreSQL 运行用户,权限过宽会被拒绝启动。
# 错误!权限过宽会导致 PostgreSQL 拒绝使用该表空间
# chmod 755 /data/pg/tblspc_hot # 错误!PostgreSQL 要求 700- 表空间路径一旦创建就尽量不要改。虽然可以通过停库、改软链、重启来「换盘」,但任何一步出错都会导致实例无法启动。
- 不要手工创建或删除
pg_tblspc下的软链,这属于系统目录的一部分。
# 错误!手工改软链会导致 pg_tablespace 与磁盘不一致,实例可能无法启动
# rm -f $PGDATA/pg_tblspc/16405
# ln -s /new/path $PGDATA/pg_tblspc/16405-- 错误!ALTER DATABASE ... SET TABLESPACE 不能给 template0 指定非默认表空间
-- ALTER DATABASE template0 SET TABLESPACE ts_cold; -- 错误!template0 只允许 pg_defaultALTER TABLE ... SET TABLESPACE会持排他锁并全量重写数据,大表上等同于一次全表重建,务必在维护窗口执行。ALTER TABLE ... SET TABLESPACE不会迁移索引,索引要单独ALTER INDEX。- 表空间不会自动做负载均衡。指定在哪个表空间,文件就永远在那里,除非你手动迁移。
pg_tablespace.spclocation列在 PG 9.2 后已移除,老文档里的写法会报错,改用pg_tablespace_location(oid)。- 临时表空间里的文件不是持久的,不要把它当作归档区;重启会清理,但异常关机后可能残留需清扫。
DROP TABLESPACE需要超级用户权限,且不能有活跃连接正在使用该表空间下的对象。
最佳实践
- 先问「为什么」,再建表空间:没有明确的物理盘差异需求,就不要引入表空间。
- 用表空间解决容量问题,用表空间解决 I/O 分布问题,不要用它解决业务建模问题。
- 表空间命名加上用途前缀(
ts_hot/ts_cold/ts_idx/ts_temp),让运维一眼看懂。 - 把
temp_tablespaces指向最快的盘,大排序与CREATE INDEX的收益立竿见影。 - 迁移大表前先评估大小与锁影响,并提前检查
pg_stat_activity是否有长事务会阻塞ACCESS EXCLUSIVE锁的获取。 - 迁移完成后立刻
ANALYZE目标表,让统计信息与物理分布重新匹配。 - 把
pg_tblspc的软链指向与挂载点写入运维文档,换机器、做容灾时这是必须逐个核对的信息。 - 基础备份方案必须覆盖表空间路径:
pg_basebackup会带上表空间,但基于文件系统的冷备需要额外同步表空间目录。 - PG15+ 善用表空间级磁盘参数:把慢盘的
random_page_cost调高、effective_io_concurrency调低,让优化器正确感知盘的特性。 - 不为每个表建独立表空间:表空间数量应按「物理盘/存储介质」来规划,通常 3~5 个足够。
下一节
继续学习:MVCC 概述