模式
模式(schema)是数据库内部的命名空间。它把表、视图、函数、类型等对象分组,使不同模式中可以存在同名对象:
sales.orders
archive.orders
模式适合按业务边界、权限边界或生命周期组织对象,但它不是独立数据库:同一数据库中的模式共享连接、事务和许多数据库级配置。
本文以 PostgreSQL 为主。其他数据库可能把 schema 与“数据库”“用户”绑定,迁移设计时不能只按名称类比。
限定名称
完整对象名由模式名和对象名组成:
SELECT *
FROM sales.orders;
这称为模式限定名称(schema-qualified name)。显式限定有几个好处:
- 读者知道对象来自哪里;
- 不依赖会话的名称搜索路径;
- 避免同名对象产生歧义;
- 安全敏感代码更不容易被恶意对象劫持。
创建和删除模式
创建模式:
CREATE SCHEMA sales;
指定所有者:
CREATE SCHEMA AUTHORIZATION sales_owner;
也可以同时指定名称和所有者:
CREATE SCHEMA sales AUTHORIZATION sales_owner;
删除空模式:
DROP SCHEMA sales;
删除模式及其中所有对象:
DROP SCHEMA sales CASCADE;
CASCADE 可能删除表、视图、函数以及其他模式中依赖它们的对象。生产环境应先检查依赖,并优先使用明确的迁移脚本逐项处理。
search_path
未限定的对象名通过 search_path 解析:
SHOW search_path;
常见设置可能包含:
"$user", public
"$user" 表示与当前角色同名且存在的模式。解析 orders 时,PostgreSQL 会按有效搜索路径寻找第一个匹配对象。
设置当前会话的搜索路径:
SET search_path TO sales, public;
只在当前事务内设置:
SET LOCAL search_path TO sales, pg_catalog;
第一个有效模式通常也是未限定创建对象时的目标位置:
CREATE TABLE invoices (...);
因此 search_path 不只影响读取,也可能决定对象被创建在哪里。
当前模式
查看当前默认创建模式:
SELECT current_schema();
查看有效搜索路径:
SELECT current_schemas(TRUE);
参数 TRUE 会包含隐式搜索的系统模式。
pg_catalog 与临时模式
PostgreSQL 的系统表和内置函数位于 pg_catalog。即使未显式列出,它通常也会被搜索;若显式列入,则顺序会影响解析。
临时表位于会话专属的临时模式中,常通过 pg_temp 引用:
CREATE TEMP TABLE work_items (...);
SELECT * FROM pg_temp.work_items;
临时模式在关系名称解析中有较高优先级,但函数和操作符解析有不同规则。安全敏感代码不要依赖对这些细节的模糊记忆,应使用限定名称并固定安全路径。
模式权限
模式主要有两类普通权限:
USAGE:允许解析和访问模式中的对象名称;CREATE:允许在模式中创建对象。
GRANT USAGE ON SCHEMA sales TO app_runtime;
GRANT CREATE ON SCHEMA scratch TO developer_role;
拥有模式 USAGE 不等于拥有其中表的 SELECT 权限;拥有 CREATE 也不等于能读取已有对象。访问一张表通常同时需要模式和表两层权限。
GRANT USAGE ON SCHEMA sales TO app_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA sales TO app_readonly;
未来新表还需要默认权限配置。
public 模式
新数据库通常包含名为 public 的模式。它只是普通模式加上一些初始约定,不等于 PUBLIC 角色:
public:一个模式名称;PUBLIC:代表所有角色的伪角色。
不同 PostgreSQL 版本、安装模板和云服务可能对 public 的默认 CREATE 权限采取不同设置。应查询实际权限,而不是假设任何用户都能或都不能在其中创建对象。
不需要共享创建能力时,可明确收紧:
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
执行前要确认现有扩展、迁移工具和开发流程是否依赖该权限。
search_path 安全
如果一个不受信任角色能在搜索路径前部的模式中创建对象,它可能创建与预期函数、操作符或表同名的对象,从而劫持未限定调用。
高权限函数和迁移脚本应:
- 使用模式限定对象名;
- 把
pg_catalog和受信任业务模式放入固定路径; - 排除任何不受信任角色拥有
CREATE的模式; - 特别审查
SECURITY DEFINER函数。
例如:
CREATE FUNCTION admin.close_order(target_order_id BIGINT)
RETURNS VOID
LANGUAGE SQL
SECURITY DEFINER
SET search_path = pg_catalog, sales
AS $$
UPDATE sales.orders
SET status = 'closed'
WHERE order_id = target_order_id;
$$;
函数体仍应显式限定关键对象,并安全处理动态 SQL。
用模式组织项目
常见方式包括:
按业务领域
account.users
sales.orders
inventory.products
边界清晰,适合模块化单体或共享数据库中的多个服务,但跨模式外键与事务仍会形成耦合。
按数据生命周期
app.events
archive.events
便于区分在线数据与归档数据。归档策略、权限和备份仍需单独设计。
按环境或测试隔离
为短期测试创建独立模式能快速隔离对象,但不能替代数据库级资源隔离。连接数、系统目录、扩展和数据库参数仍然共享。
每租户一个模式
“每个租户一个模式”可以提供清晰命名隔离,并便于单租户导出;但租户数量很大时会产生大量表、索引和迁移对象,增加系统目录膨胀、连接搜索路径管理、部署和监控成本。
常见方案对比:
| 方案 | 优点 | 主要成本 |
|---|---|---|
共享表加 tenant_id | 对象数量少、统一迁移 | 依赖正确过滤、约束和 RLS |
| 每租户一个模式 | 逻辑隔离直观 | 对象数量与迁移复杂度随租户增长 |
| 每租户一个数据库 | 隔离最强、可独立运维 | 连接、升级和资源管理成本更高 |
选择应基于租户数量、法规隔离、定制化程度、备份恢复和运维能力,而不是只看 SQL 查询是否简短。
移动与重命名对象
移动表:
ALTER TABLE public.orders SET SCHEMA sales;
数据库内部依赖通常按对象身份继续关联,但应用中的文本 SQL、动态查询、权限和 search_path 可能失效。移动后要重新审查模式 USAGE 与默认权限。
重命名模式:
ALTER SCHEMA sales RENAME TO commerce;
这同样需要应用兼容和依赖审计。
常见误区
模式等于独立数据库
模式只提供命名和权限组织,不隔离连接、资源、数据库级设置和故障域。
设置 search_path 后永远不必限定名称
搜索路径会随角色、连接和函数上下文变化。安全敏感或长期维护的 SQL 应优先显式限定。
模式 USAGE 会授予表读取权限
不会。模式与对象权限需要分别授予。
public 模式和 PUBLIC 角色是同一个东西
不是。前者是对象容器,后者表示所有角色。
模式检查清单
- 模式边界是业务组织、权限隔离还是生命周期管理;
- 应用是否使用稳定、受控的
search_path; - 高权限代码是否限定对象名并排除不受信任模式;
public的CREATE权限是否符合安全基线;- 模式
USAGE与对象权限是否都正确; - 默认权限是否覆盖未来由正确角色创建的对象;
- 移动或重命名对象时是否检查文本 SQL 和外部工具;
- 多租户方案的对象数量和迁移成本是否可持续。
模式最有价值的地方是提供清晰的命名和授权边界。把 search_path 当成安全配置,而不只是省略前缀的便利功能,能避免很多隐蔽问题。