Skip to content

Tablespace(表空间) ​

PostgreSQL 的「表空间」常被误解成 Oracle 那种「逻辑存储单元」。实际上它做的事情非常单一:把某张表、某个索引的物理文件,从默认目录挪到另一个磁盘路径下。页格式、SQL 语义、MVCC 全部不变,变的只是文件落在哪块盘上。本节讲解表空间的原理、创建与使用、默认表空间的真实含义,以及它在「把热数据放到 SSD」这类场景中的正确用法。

前置知识

阅读本节前,建议先了解:WAL Files(WAL 文件组织)

基础概念 ​

表空间(Tablespace) 是 PostgreSQL 中「一个目录位置的别名」。你定义它、给它一个路径、给它命名,之后就可以在建表时指定「这张表放在哪个表空间里」,PostgreSQL 就会把该表的数据文件创建在那个路径下。

理解表空间需要先统一以下术语:

术语英文说明
表空间Tablespace目录位置的命名别名,记录在 pg_tablespace
默认表空间pg_default未显式指定时使用的表空间,指向 PGDATA/base
全局表空间pg_global系统目录专用,指向 PGDATA/global,不可用于用户对象
符号链接Symbolic Linkpg_tblspc/<oid> 是指向真实路径的软链
表空间 OIDTablespace OID表空间的唯一标识,也是 pg_tblspc/ 下的目录名
相对路径Relative Path文件路径表达中的第二段,如 1663/16384/16385 中的 16384
LOCATIONLOCATION 子句建库时指定默认表空间,如 CREATE DATABASE ... TABLESPACE

表空间的物理形态 ​

创建一个表空间,PostgreSQL 会做三件事:

text
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 里建表时,数据文件的完整路径是:

text
/data/pg/tblspc_hot/PG_18_202506071/16384/16385
  └──────── 真实挂载点 ────────┘ └──┬──┘ └──┬──┘
                                    │       └── 表的 relfilenode
                                    └── 数据库 OID(即 pg_database.oid)

而默认表空间里的表,路径则少了一层:

text
$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 的表空间只是「换个地方放文件」,它不提供任何额外的隔离、配额或性能保证——性能差异完全来自那块盘本身。

核心机制 ​

为什么需要表空间 ​

表空间解决的从来不是「逻辑建模」问题,而是三个物理运维问题:

text
① 磁盘容量不够了
   PGDATA 所在分区写满 → 无法扩容该分区
   → 把大表、大索引迁到另一块盘 ✔

② 热点与冷数据混在一盘上
   users 表被高频读写,logs 表几乎只读
   → 把 users 放到 SSD 表空间,logs 留在 HDD ✔

③ 需要把备份、临时文件、WAL 分开到不同盘
   → temp_tablespaces 指向专门的临时表空间 ✔

反过来,如果一个实例只有一块盘、容量充裕、读写均匀,那么建表空间没有任何收益,只会增加运维复杂度。

关键机制:表空间是「目录级」而非「表级」的重定向 ​

指定表空间发生在 对象创建时:

sql
-- 建表时指定表空间
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 锁。索引需要单独迁移,不会自动跟随表:

text
ALTER TABLE only SET TABLESPACE 的行为:

  users 表文件   → 复制到新表空间 ✔
  users 的索引   → 留在原表空间 ✗(必须单独 ALTER INDEX)
  TOAST 表       → 跟随主表迁移 ✔
  TOAST 表索引   → 跟随迁移 ✔

索引表空间与表表空间分离的意义 ​

把索引放到另一个表空间不是为了「省空间」,而是为了分散 I/O:

text
磁盘 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 可以指定整个库的默认表空间,这样库里不显式指定表空间的对象都会落在那里:

sql
CREATE DATABASE demo TABLESPACE ts_cold;

这之后 demo 库里的表,路径会变成 pg_tblspc/<ts_oid>/PG_18_*/<db_oid>/<relfilenode>。这解决了「一个模板库复制出多个库、每个库想放不同盘」的问题——因为数据库 OID 不同,文件天然不冲突。

但要注意:template0、template1、postgres 三个库不能随便改表空间(template0 也不允许指定非默认表空间),它们是新建库的模板,必须保持可访问。

删除表空间的严格条件 ​

sql
DROP TABLESPACE ts_cold;

这条语句只有在表空间完全空时才能成功。任何「还留着对象」的情况都会报错:

text
ERROR:  tablespace "ts_cold" is not empty

因为表空间可能被别的库使用,PostgreSQL 无法自动判断该删什么。所以删除前必须逐库检查:

sql
-- 在每一个数据库里都要执行,才能确认该表空间是否真的空了
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」成为可能:

ini
# postgresql.conf
# 默认表空间(未在 SQL 中指定表空间对象时使用)
default_tablespace = ''

# 临时文件优先放到专用盘,缓解 PGDATA 盘压力
temp_tablespaces = 'ts_temp'
sql
-- 建一个带磁盘代价参数的表空间(PG15+)
CREATE TABLESPACE ts_slow
  LOCATION '/data/pg/tblspc_slow'
  WITH (random_page_cost = 2.0, effective_io_concurrency = 4);

详细说明 ​

表空间信息一览 ​

sql
-- 查看所有表空间:名字、属主、位置、大小
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;
text
    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。

对象与表空间的对应关系 ​

sql
-- 列出当前库中所有"显式指定了非默认表空间"的对象
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;
text
 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 只能查出「显式指定」的对象。要用一种写法同时覆盖「显式指定」与「继承默认表空间」的情况:

sql
-- 查询某张表到底落在哪个表空间(含继承默认表空间的情形)
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');
text
 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> 软链指向的真实路径

磁盘层面的验证 ​

bash
# 1) 查看 pg_tblspc 下的软链指向
ls -l $PGDATA/pg_tblspc
text
total 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
bash
# 2) 查看表空间真实目录结构(注意版本子目录)
ls -l /data/pg/tblspc_hot/
ls -l /data/pg/tblspc_hot/PG_18_202506071/
text
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
bash
# 3) 与 SQL 查出的 relative_path 对照,确认文件确实在那里
ls -lh /data/pg/tblspc_hot/PG_18_202506071/16384/16385
text
-rw------- 1 postgres postgres 1.0G Sep 20 12:20 /data/pg/tblspc_hot/PG_18_202506071/16384/16385
bash
# 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:

bash
# 默认行为:表空间内容会被原样打包(可能与主数据一起放在同一 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 所在分区快满了,把最大的表迁到新挂载的盘。

准备阶段:

bash
# 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_cold
text
drwx------ 2 postgres postgres 4096 Sep 20 13:00 /data/pg/tblspc_cold

创建表空间并评估现状:

sql
-- 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;
text
    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

执行迁移(注意锁的影响):

sql
-- 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;
text
    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 数据量下可能耗时数分钟,务必在维护窗口执行。

清理旧空间:

bash
# 10) 迁移完成后确认 PGDATA 所在分区释放了空间
df -h $PGDATA
du -sh $PGDATA/base/16384
text
Filesystem      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 盘压力:

sql
-- 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;
text
                       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
sql
-- 5) 在操作系统层面确认临时文件确实在 ts_temp,而不是 PGDATA
bash
ls -lh /data/pg/tblspc_temp/PG_18_202506071/pgsql_tmp/ | head -3
text
-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:安全下线一个表空间 ​

下线表空间是「删库跑路」级别的高危操作,正确流程是先确认空、再删记录、最后清目录:

sql
-- 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');
text
                       ddl
--------------------------------------------------
 ALTER TABLE public.legacy_log SET TABLESPACE pg_default;
 ALTER INDEX public.legacy_log_pkey SET TABLESPACE pg_default;
sql
-- 3) 确认某库确实用完了,才能删表空间
DROP TABLESPACE ts_old;
bash
# 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 运行用户,权限过宽会被拒绝启动。
bash
# 错误!权限过宽会导致 PostgreSQL 拒绝使用该表空间
# chmod 755 /data/pg/tblspc_hot   # 错误!PostgreSQL 要求 700
  • 表空间路径一旦创建就尽量不要改。虽然可以通过停库、改软链、重启来「换盘」,但任何一步出错都会导致实例无法启动。
  • 不要手工创建或删除 pg_tblspc 下的软链,这属于系统目录的一部分。
bash
# 错误!手工改软链会导致 pg_tablespace 与磁盘不一致,实例可能无法启动
# rm -f $PGDATA/pg_tblspc/16405
# ln -s /new/path $PGDATA/pg_tblspc/16405
sql
-- 错误!ALTER DATABASE ... SET TABLESPACE 不能给 template0 指定非默认表空间
-- ALTER DATABASE template0 SET TABLESPACE ts_cold;   -- 错误!template0 只允许 pg_default
  • ALTER 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 概述

参考链接 ​