Skip to content

PostgreSQL 表继承详解

概述

PostgreSQL 的表继承功能允许一个表从另一个表继承结构和数据,这为数据库设计提供了强大的建模能力。继承机制类似于面向对象编程中的类继承概念,子表自动获得父表的所有列和约束。

什么是表继承?

表继承是 PostgreSQL 特有的功能,它允许你创建一个表(子表)来"继承"另一个表(父表)的列结构。子表会自动包含父表的所有列,同时可以添加自己特有的列。

基本语法与概念

继承表的创建语法

sql
-- 创建父表
CREATE TABLE parent_table (
    column1 datatype,
    column2 datatype,
    ...
);

-- 创建继承表
CREATE TABLE child_table (
    additional_column datatype
) INHERITS (parent_table);
sql
-- 从多个父表继承
CREATE TABLE child_table (
    additional_column datatype
) INHERITS (parent_table1, parent_table2);

继承关系图示

实战示例:城市数据模型

让我们通过一个完整的例子来理解表继承的工作原理。

问题场景

我们需要设计一个城市数据库,包含普通城市和州首府。州首府除了具有普通城市的属性外,还需要记录所属的州信息。

解决方案实现

步骤 1:创建父表

sql
-- 创建城市基础表
CREATE TABLE cities (
    name            text,           -- 城市名称
    population      float,          -- 人口数量
    elevation       int             -- 海拔高度(英尺)
);

解释

  • cities 表定义了所有城市的通用属性
  • 包含名称、人口和海拔三个基本字段

步骤 2:创建继承表

sql
-- 创建州首府表(继承自cities)
CREATE TABLE capitals (
    state           char(2)         -- 所属州的缩写
) INHERITS (cities);

解释

  • capitals 表自动继承 cities 的所有列
  • 额外添加了 state 列来标识所属州
  • 最终 capitals 表包含:name, population, elevation, state

步骤 3:插入测试数据

sql
-- 向父表插入数据
INSERT INTO cities (name, population, elevation) VALUES
    ('Las Vegas', 651319, 2174),
    ('Mariposa', 2173, 1953),
    ('San Francisco', 883305, 52);

-- 向子表插入数据
INSERT INTO capitals (name, population, elevation, state) VALUES
    ('Madison', 259680, 845, 'WI'),
    ('Albany', 97856, 150, 'NY'),
    ('Sacramento', 524943, 30, 'CA');

数据结构展示

表名namepopulationelevationstate
citiesLas Vegas6513192174-
citiesMariposa21731953-
citiesSan Francisco88330552-
capitalsMadison259680845WI
capitalsAlbany97856150NY
capitalsSacramento52494330CA

查询操作详解

默认继承查询

sql
-- 查询所有城市(包括州首府)
SELECT name, elevation
FROM cities
WHERE elevation > 500;

输出结果

   name    | elevation
-----------+-----------
 Las Vegas |      2174
 Mariposa  |      1953
 Madison   |       845

分析过程

  1. 查询 cities 表时,PostgreSQL 默认包含所有继承的子表数据
  2. 结果同时包含 cities 表和 capitals 表中满足条件的记录
  3. 这种行为使得对"所有城市"的查询变得简单

使用 ONLY 关键字

sql
-- 仅查询父表,不包含子表数据
SELECT name, elevation
FROM ONLY cities
WHERE elevation > 500;

输出结果

   name    | elevation
-----------+-----------
 Las Vegas |      2174
 Mariposa  |      1953

分析过程

  1. ONLY 关键字限制查询范围仅为指定的表
  2. 不包含任何继承的子表数据
  3. 适用于需要区分表数据来源的场景

明确指定包含继承

sql
-- 使用 * 明确指定包含继承(可选)
SELECT name, elevation
FROM cities*
WHERE elevation > 500;

语法提示

cities* 中的 * 符号是可选的,因为包含继承是默认行为。但在某些旧版本兼容性场景中可能需要明确指定。

系统列与表识别

使用 tableoid 识别数据来源

sql
-- 查询数据的来源表
SELECT c.tableoid, c.name, c.elevation
FROM cities c
WHERE c.elevation > 500;

输出结果

 tableoid |   name    | elevation
----------+-----------+-----------
   139793 | Las Vegas |      2174
   139793 | Mariposa  |      1953
   139798 | Madison   |       845

获取表名称

有两种方法可以获取实际的表名:

sql
SELECT p.relname, c.name, c.elevation
FROM cities c, pg_class p
WHERE c.elevation > 500 AND c.tableoid = p.oid;
sql
SELECT c.tableoid::regclass, c.name, c.elevation
FROM cities c
WHERE c.elevation > 500;

输出结果

 relname  |   name    | elevation
----------+-----------+-----------
 cities   | Las Vegas |      2174
 cities   | Mariposa  |      1953
 capitals | Madison   |       845

分析过程

  • tableoid 是每个表都有的系统列,存储表的 OID
  • 通过 JOIN pg_class 或转换为 regclass 类型可以获取表名
  • 这对于区分数据来源非常有用

数据操作限制

INSERT 操作的限制

sql
-- ❌ 这个操作会失败
INSERT INTO cities (name, population, elevation, state)
VALUES ('Albany', NULL, NULL, 'NY');

错误信息

ERROR: column "state" of relation "cities" does not exist

原因分析

  1. INSERT 操作不会自动路由到子表
  2. cities 表不包含 state
  3. 必须直接向目标表插入数据

正确做法

sql
-- ✅ 直接向子表插入
INSERT INTO capitals (name, population, elevation, state)
VALUES ('Albany', NULL, NULL, 'NY');

数据修改操作

继承对于数据修改操作的影响:

sql
-- UPDATE 和 DELETE 默认作用于整个继承层次
UPDATE cities SET population = population * 1.1;  -- 影响所有子表
DELETE FROM cities WHERE population < 1000;       -- 从所有表删除

-- 使用 ONLY 限制到单表
UPDATE ONLY cities SET population = population * 1.1;  -- 仅影响父表
DELETE FROM ONLY cities WHERE population < 1000;       -- 仅从父表删除

约束与继承

自动继承的约束

sql
-- 在父表上定义约束
CREATE TABLE cities_with_constraints (
    name            text NOT NULL,              -- 非空约束
    population      float CHECK (population > 0), -- 检查约束
    elevation       int
);

-- 子表自动继承约束
CREATE TABLE capitals_with_constraints (
    state           char(2) NOT NULL
) INHERITS (cities_with_constraints);

约束继承规则

约束类型是否继承说明
NOT NULL自动继承
CHECK自动继承,可用 NO INHERIT 排除
UNIQUE不继承
PRIMARY KEY不继承
FOREIGN KEY不继承

约束合并示例

sql
-- 多重继承时的约束合并
CREATE TABLE base1 (
    id int NOT NULL,
    value text CHECK (length(value) > 0)
);

CREATE TABLE base2 (
    id int NOT NULL,      -- 相同列名和类型
    category text
);

-- 子表继承并合并约束
CREATE TABLE combined () INHERITS (base1, base2);
-- 结果:id 列合并为 NOT NULL,继承 value 的 CHECK 约束

动态继承管理

事后添加继承关系

sql
-- 创建兼容的表结构
CREATE TABLE existing_table (
    name            text,
    population      float,
    elevation       int,
    region          text    -- 额外列
);

-- 添加继承关系
ALTER TABLE existing_table INHERIT cities;

使用 LIKE 创建兼容表

sql
-- 复制表结构
CREATE TABLE new_cities (
    LIKE cities INCLUDING CONSTRAINTS,  -- 包含约束
    region text
);

-- 建立继承关系
ALTER TABLE new_cities INHERIT cities;

移除继承关系

sql
-- 移除继承关系
ALTER TABLE capitals NO INHERIT cities;

继承的高级特性

多重继承

sql
-- 创建多个父表
CREATE TABLE geographical_entities (
    latitude    decimal(10,8),
    longitude   decimal(11,8)
);

CREATE TABLE administrative_units (
    admin_code  varchar(10),
    admin_level int
);

-- 多重继承
CREATE TABLE major_cities (
    tourism_rating int
) INHERITS (cities, geographical_entities, administrative_units);

继承关系图

权限与安全

sql
-- 在父表上授权
GRANT SELECT ON cities TO public;
GRANT UPDATE ON cities TO city_admin;

-- 查询继承数据时的权限检查
SELECT * FROM cities;  -- 检查 cities 表权限,可访问所有继承数据

-- 直接访问子表需要额外权限
SELECT * FROM capitals;  -- 需要 capitals 表的权限

权限继承规则

权限说明

  • 通过父表查询时,只检查父表权限
  • 直接访问子表时,需要子表权限
  • 行级安全策略遵循相同规则

重要限制与注意事项

1. 索引和唯一约束限制

sql
-- ❌ 唯一约束不能跨继承层次工作
CREATE TABLE cities_unique (
    name text UNIQUE,    -- 仅在单表内唯一
    population float,
    elevation int
);

CREATE TABLE capitals_unique (
    state char(2)
) INHERITS (cities_unique);

-- 可能存在重复数据的情况
INSERT INTO cities_unique VALUES ('New York', 8400000, 33);
INSERT INTO capitals_unique VALUES ('New York', 8400000, 33, 'NY');
-- 两条记录都会被接受!

2. 外键约束限制

sql
-- ❌ 外键不能引用继承层次
CREATE TABLE countries (
    code char(2) PRIMARY KEY,
    name text
);

-- 这个外键只能引用 capitals 表,不包括其他继承表
ALTER TABLE capitals
ADD CONSTRAINT fk_state
FOREIGN KEY (state) REFERENCES countries(code);

3. 命令支持情况

命令类型继承支持ONLY 支持说明
SELECT默认包含继承,支持 ONLY
UPDATE默认影响继承层次
DELETE默认影响继承层次
INSERTN/A不自动路由到子表
VACUUMN/A仅作用于单表
REINDEXN/A仅作用于单表

实际应用场景

场景 1:产品分类系统

sql
-- 基础产品表
CREATE TABLE products (
    id              serial PRIMARY KEY,
    name            text NOT NULL,
    price           decimal(10,2),
    description     text,
    created_at      timestamp DEFAULT NOW()
);

-- 电子产品表
CREATE TABLE electronics (
    warranty_period int,        -- 保修期(月)
    brand           text,
    model           text
) INHERITS (products);

-- 图书表
CREATE TABLE books (
    isbn            varchar(13),
    author          text,
    publisher       text,
    pages           int
) INHERITS (products);

-- 查询所有产品
SELECT name, price FROM products ORDER BY price DESC;

-- 查询特定类型产品
SELECT name, brand, warranty_period
FROM electronics
WHERE warranty_period > 12;

场景 2:日志系统

sql
-- 基础日志表
CREATE TABLE base_logs (
    id              bigserial PRIMARY KEY,
    timestamp       timestamp DEFAULT NOW(),
    level           varchar(10),
    message         text,
    source          text
);

-- 应用日志
CREATE TABLE app_logs (
    user_id         int,
    session_id      varchar(32),
    action          varchar(50)
) INHERITS (base_logs);

-- 系统日志
CREATE TABLE system_logs (
    server_name     varchar(50),
    process_id      int,
    cpu_usage       decimal(5,2),
    memory_usage    decimal(5,2)
) INHERITS (base_logs);

-- 按时间查询所有日志
SELECT timestamp, level, message, source
FROM base_logs
WHERE timestamp > NOW() - INTERVAL '1 hour'
ORDER BY timestamp DESC;

场景 3:地理数据模型

sql
-- 地理实体基表
CREATE TABLE geo_entities (
    id              serial PRIMARY KEY,
    name            text NOT NULL,
    area_km2        decimal(10,2),
    population      bigint
);

-- 国家表
CREATE TABLE countries (
    iso_code        char(2) UNIQUE,
    continent       varchar(20),
    capital_city    text
) INHERITS (geo_entities);

-- 省/州表
CREATE TABLE states (
    country_code    char(2),
    state_code      varchar(10),
    governor        text
) INHERITS (geo_entities);

-- 城市表
CREATE TABLE cities_geo (
    state_code      varchar(10),
    mayor           text,
    is_capital      boolean DEFAULT FALSE
) INHERITS (geo_entities);

性能优化建议

1. 分区表 vs 继承

现代替代方案

对于数据分区需求,建议使用声明式分区而不是继承:

sql
-- 使用声明式分区(推荐)
CREATE TABLE sales (
    sale_date date,
    amount decimal
) PARTITION BY RANGE (sale_date);

CREATE TABLE sales_2023 PARTITION OF sales
FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');

2. 查询优化

sql
-- 使用约束排除优化查询
ALTER TABLE capitals ADD CONSTRAINT check_state
CHECK (state IS NOT NULL);

-- PostgreSQL 可以在适当时候排除不相关的子表
SELECT * FROM cities WHERE state = 'CA';  -- 可能只扫描 capitals 表

3. 索引策略

sql
-- 在每个相关表上创建相同的索引
CREATE INDEX idx_cities_name ON cities (name);
CREATE INDEX idx_capitals_name ON capitals (name);
CREATE INDEX idx_capitals_state ON capitals (state);

-- 组合索引用于常见查询模式
CREATE INDEX idx_capitals_state_pop ON capitals (state, population);

监控和调试

查看继承关系

sql
-- 查询表的继承关系
SELECT
    schemaname,
    tablename,
    attname,
    inherited
FROM pg_stats
WHERE tablename IN ('cities', 'capitals');

-- 查看表的继承层次
SELECT
    c.relname as child_table,
    p.relname as parent_table
FROM pg_inherits i
JOIN pg_class c ON i.inhrelid = c.oid
JOIN pg_class p ON i.inhparent = p.oid;

分析查询计划

sql
-- 查看查询是否正确使用继承
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM cities WHERE elevation > 1000;

-- 对比 ONLY 查询的执行计划
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM ONLY cities WHERE elevation > 1000;

最佳实践总结

✅ 适合使用继承的场景

  1. 共享属性的相关实体:如产品分类、用户类型等
  2. 历史数据归档:保持相同结构的时间分片
  3. 多态查询需求:需要同时查询多种相关类型的数据

❌ 不适合使用继承的场景

  1. 需要严格唯一约束:跨表的唯一性无法保证
  2. 复杂外键关系:外键约束不支持继承层次
  3. 性能敏感的大表:考虑使用分区表替代

🔧 开发建议

  1. 谨慎设计:继承关系一旦建立,修改成本较高
  2. 文档化:清楚记录继承关系和业务逻辑
  3. 测试完整性:特别注意约束和权限的行为
  4. 监控性能:定期检查查询计划和性能指标

重要提醒

PostgreSQL 的继承功能虽然强大,但有其局限性。在现代应用中,对于数据分区需求,建议优先考虑声明式分区功能。继承更适合于真正的"is-a"关系建模。

通过本文的学习,你应该能够:

  • 理解 PostgreSQL 继承的基本概念和语法
  • 掌握继承表的创建、查询和管理
  • 了解继承的限制和注意事项
  • 在适合的场景中正确应用继承功能