Skip to content

生成列

生成列(Generated Columns)是 PostgreSQL 中一种特殊的列类型,它的值总是由其他列计算而来。本章将详细介绍生成列的概念、语法、使用场景和最佳实践。

什么是生成列

生成列是一种特殊的列,它总是由其他列计算而来。因此,对于列来说,它就像视图对于表一样。生成列的值不能直接写入,而是根据定义的表达式自动计算。

生成列特点

  • 值始终由其他列计算而来
  • 不能直接写入数据
  • 提供两种类型:存储型和虚拟型
  • 当前 PostgreSQL 仅支持存储型

生成列类型

存储型生成列(STORED)

  • 计算时机:在写入(INSERT 或 UPDATE)时计算
  • 存储方式:像普通列一样占用存储空间
  • 特点:类似于自动更新的物化视图

虚拟型生成列(VIRTUAL)

  • 计算时机:在读取时计算
  • 存储方式:不占用存储空间
  • 特点:类似于视图
  • 支持状态:PostgreSQL 目前尚未实现

基本语法

创建生成列

sql
CREATE TABLE table_name (
    column1 data_type,
    column2 data_type,
    generated_column data_type GENERATED ALWAYS AS (expression) STORED
);

语法要点

  • 必须使用 GENERATED ALWAYS AS 关键字
  • 表达式放在括号中
  • 必须指定 STORED 关键字
  • 生成列的数据类型必须与表达式结果兼容

详细示例

示例 1:基本生成列使用

问题陈述:创建一个人员表,包含身高的厘米和英寸两种单位,英寸值自动从厘米值计算。

解决方案

sql
-- 创建包含生成列的表
CREATE TABLE people (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    height_cm NUMERIC(5,2),
    height_in NUMERIC(5,2) GENERATED ALWAYS AS (height_cm / 2.54) STORED
);

测试数据操作

sql
-- 插入数据(不需要指定生成列的值)
INSERT INTO people (name, height_cm) VALUES 
    ('张三', 175.5),
    ('李四', 168.0),
    ('王五', 182.3);

-- 查询结果
SELECT * FROM people;

输出结果

idnameheight_cmheight_in
1张三175.5069.09
2李四168.0066.14
3王五182.3071.77

分析过程

  • height_in 列自动根据公式 height_cm / 2.54 计算
  • 插入时只需提供 height_cm
  • 生成列的值会自动存储在磁盘上

示例 2:复杂表达式生成列

问题陈述:创建订单表,包含单价、数量和总金额,总金额自动计算,并包含税费计算。

解决方案

sql
-- 创建订单表
CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    product_name VARCHAR(200),
    unit_price DECIMAL(10,2),
    quantity INTEGER,
    subtotal DECIMAL(12,2) GENERATED ALWAYS AS (unit_price * quantity) STORED,
    tax_rate DECIMAL(5,4) DEFAULT 0.0825, -- 8.25% 税率
    total_amount DECIMAL(12,2) GENERATED ALWAYS AS (
        unit_price * quantity * (1 + tax_rate)
    ) STORED
);

测试数据

sql
-- 插入测试数据
INSERT INTO orders (product_name, unit_price, quantity) VALUES 
    ('笔记本电脑', 8999.99, 2),
    ('无线鼠标', 299.50, 5),
    ('机械键盘', 599.00, 3);

-- 查询结果
SELECT 
    order_id,
    product_name,
    unit_price,
    quantity,
    subtotal,
    tax_rate,
    total_amount
FROM orders;

输出结果

order_idproduct_nameunit_pricequantitysubtotaltax_ratetotal_amount
1笔记本电脑8999.99217999.980.082519484.98
2无线鼠标299.5051497.500.08251621.11
3机械键盘599.0031797.000.08251945.40

示例 3:字符串操作生成列

问题陈述:创建用户表,自动生成全名和邮箱地址。

解决方案

sql
-- 创建用户表
CREATE TABLE users (
    user_id SERIAL PRIMARY KEY,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    username VARCHAR(50) NOT NULL,
    domain VARCHAR(50) DEFAULT 'company.com',
    full_name VARCHAR(101) GENERATED ALWAYS AS (
        first_name || ' ' || last_name
    ) STORED,
    email VARCHAR(150) GENERATED ALWAYS AS (
        lower(username || '@' || domain)
    ) STORED
);

测试数据

sql
-- 插入数据
INSERT INTO users (first_name, last_name, username) VALUES 
    ('张', '三', 'zhangsan'),
    ('李', '四', 'lisi'),
    ('王', '五', 'wangwu');

-- 查询结果
SELECT user_id, first_name, last_name, full_name, email FROM users;

输出结果

user_idfirst_namelast_namefull_nameemail
1张 三[email protected]
2李 四[email protected]
3王 五[email protected]

生成列的操作

插入数据

sql
-- 正确的插入方式(不指定生成列)
INSERT INTO people (name, height_cm) VALUES ('新用户', 170.0);

-- 使用 DEFAULT 关键字(可选)
INSERT INTO people (name, height_cm, height_in) VALUES ('新用户2', 165.0, DEFAULT);

注意事项

sql
-- 错误:不能直接为生成列赋值
INSERT INTO people (name, height_cm, height_in) VALUES ('错误示例', 170.0, 67.0);
-- 这将产生错误:ERROR: cannot insert into column "height_in"

更新数据

sql
-- 更新基础列,生成列会自动重新计算
UPDATE people SET height_cm = 180.0 WHERE id = 1;

-- 查看更新后的结果
SELECT id, name, height_cm, height_in FROM people WHERE id = 1;

输出结果

idnameheight_cmheight_in
1张三180.0070.87

生成列的限制

表达式限制

  • 只能使用不可变函数
  • 不能使用子查询
  • 不能引用其他生成列
  • 不能引用系统列
  • 不能引用当前行外内容

示例:限制演示

sql
-- 正确:使用不可变函数
CREATE TABLE test_constraints (
    id SERIAL,
    value1 NUMERIC,
    value2 NUMERIC,
    sum_value NUMERIC GENERATED ALWAYS AS (value1 + value2) STORED,
    abs_value NUMERIC GENERATED ALWAYS AS (ABS(value1)) STORED
);

-- 错误示例:

禁止的操作

sql
-- 错误:使用易变函数
CREATE TABLE bad_example1 (
    id SERIAL,
    created_at TIMESTAMP GENERATED ALWAYS AS (NOW()) STORED  -- 错误
);

-- 错误:引用其他生成列
CREATE TABLE bad_example2 (
    val1 NUMERIC,
    gen1 NUMERIC GENERATED ALWAYS AS (val1 * 2) STORED,
    gen2 NUMERIC GENERATED ALWAYS AS (gen1 * 3) STORED  -- 错误
);

-- 错误:使用子查询
CREATE TABLE bad_example3 (
    id SERIAL,
    category_id INTEGER,
    category_name TEXT GENERATED ALWAYS AS (
        (SELECT name FROM categories WHERE id = category_id)  -- 错误
    ) STORED
);

继承和分区中的生成列

继承表中的生成列

sql
-- 父表
CREATE TABLE vehicles (
    id SERIAL PRIMARY KEY,
    brand VARCHAR(50),
    model VARCHAR(50),
    full_name VARCHAR(101) GENERATED ALWAYS AS (brand || ' ' || model) STORED
);

-- 子表:继承父表的生成列定义
CREATE TABLE cars (
    doors INTEGER,
    engine_type VARCHAR(20)
) INHERITS (vehicles);

-- 子表:覆盖生成列表达式
CREATE TABLE motorcycles (
    cc INTEGER,
    full_name VARCHAR(101) GENERATED ALWAYS AS (
        brand || ' ' || model || ' (' || cc || 'cc)'
    ) STORED
) INHERITS (vehicles);

分区表中的生成列

sql
-- 创建分区表
CREATE TABLE sales (
    id SERIAL,
    sale_date DATE,
    amount DECIMAL(10,2),
    tax DECIMAL(10,2) GENERATED ALWAYS AS (amount * 0.1) STORED,
    year INTEGER GENERATED ALWAYS AS (EXTRACT(YEAR FROM sale_date)) STORED
) PARTITION BY RANGE (sale_date);

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

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

生成列的高级应用

示例 4:JSON 数据提取

问题陈述:存储 JSON 格式的用户配置,并自动提取常用字段便于查询。

解决方案

sql
-- 创建用户配置表
CREATE TABLE user_preferences (
    user_id SERIAL PRIMARY KEY,
    username VARCHAR(50),
    preferences JSONB,
    theme VARCHAR(20) GENERATED ALWAYS AS (preferences->>'theme') STORED,
    language VARCHAR(10) GENERATED ALWAYS AS (preferences->>'language') STORED,
    notifications_enabled BOOLEAN GENERATED ALWAYS AS (
        (preferences->>'notifications')::BOOLEAN
    ) STORED
);

测试数据

sql
-- 插入 JSON 配置数据
INSERT INTO user_preferences (username, preferences) VALUES 
    ('user1', '{"theme": "dark", "language": "zh", "notifications": true}'),
    ('user2', '{"theme": "light", "language": "en", "notifications": false}'),
    ('user3', '{"theme": "auto", "language": "zh", "notifications": true}');

-- 查询提取的字段
SELECT username, theme, language, notifications_enabled FROM user_preferences;

输出结果

usernamethemelanguagenotifications_enabled
user1darkzht
user2lightenf
user3autozht

示例 5:时间计算生成列

问题陈述:员工表需要根据生日自动计算年龄和工龄。

解决方案

sql
-- 创建员工表
CREATE TABLE employees (
    emp_id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    birth_date DATE,
    hire_date DATE,
    age INTEGER GENERATED ALWAYS AS (
        EXTRACT(YEAR FROM AGE(CURRENT_DATE, birth_date))
    ) STORED,
    years_of_service NUMERIC(4,1) GENERATED ALWAYS AS (
        ROUND(EXTRACT(EPOCH FROM AGE(CURRENT_DATE, hire_date)) / (365.25 * 24 * 3600), 1)
    ) STORED
);

注意

上述示例中使用了 CURRENT_DATE,这在实际应用中可能不适合生成列,因为它是易变的。在实际应用中,应该考虑使用触发器或在查询时计算。

正确的年龄计算方案

sql
-- 修正版本:创建视图来处理动态计算
CREATE TABLE employees_base (
    emp_id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    birth_date DATE,
    hire_date DATE
);

-- 创建视图计算年龄
CREATE VIEW employees AS
SELECT 
    emp_id,
    name,
    birth_date,
    hire_date,
    EXTRACT(YEAR FROM AGE(CURRENT_DATE, birth_date)) AS age,
    ROUND(EXTRACT(EPOCH FROM AGE(CURRENT_DATE, hire_date)) / (365.25 * 24 * 3600), 1) AS years_of_service
FROM employees_base;

最佳实践

1. 适用场景

推荐使用场景

  • 数据转换:单位转换、格式转换
  • 计算字段:价格计算、统计值
  • 数据标准化:全名组合、邮箱生成
  • 索引优化:复杂表达式的预计算

2. 设计原则

3. 代码示例:最佳实践

sql
-- 良好的生成列设计示例
CREATE TABLE product_catalog (
    product_id SERIAL PRIMARY KEY,
    name VARCHAR(200),
    category VARCHAR(50),
    price DECIMAL(10,2),
    discount_rate DECIMAL(4,3) DEFAULT 0.000,
    
    -- 简单、有用的生成列
    sale_price DECIMAL(10,2) GENERATED ALWAYS AS (
        price * (1 - discount_rate)
    ) STORED,
    
    -- 便于搜索的标准化字段
    search_name TEXT GENERATED ALWAYS AS (
        lower(regexp_replace(name, '[^a-zA-Z0-9\s]', '', 'g'))
    ) STORED,
    
    -- 分类标签
    price_category VARCHAR(20) GENERATED ALWAYS AS (
        CASE 
            WHEN price < 100 THEN 'budget'
            WHEN price < 500 THEN 'mid-range'
            ELSE 'premium'
        END
    ) STORED
);

-- 在生成列上创建有用的索引
CREATE INDEX idx_product_sale_price ON product_catalog(sale_price);
CREATE INDEX idx_product_price_category ON product_catalog(price_category);
CREATE INDEX idx_product_search_name ON product_catalog USING gin(to_tsvector('simple', search_name));

生成列与其他特性的对比

生成列 vs 默认值

特性生成列默认值
计算时机每次写入时重新计算仅在插入时计算一次
依赖关系可以引用其他列不能引用其他列
更新行为自动更新不会自动更新
函数限制仅不可变函数可以使用易变函数

生成列 vs 触发器

特性生成列触发器
性能较高(内置优化)较低(额外开销)
复杂度简单表达式复杂逻辑
维护性自动维护需要手动维护
灵活性有限高度灵活

生成列 vs 视图

特性生成列视图
存储占用存储空间不占用存储空间
查询性能高(预计算)低(实时计算)
索引支持支持有限支持
更新开销写入时计算无更新开销

故障排除

常见错误及解决方案

1. 语法错误

sql
-- 错误:缺少 STORED 关键字
CREATE TABLE wrong_syntax (
    id SERIAL,
    value1 INTEGER,
    value2 INTEGER GENERATED ALWAYS AS (value1 * 2)  -- 错误
);

-- 正确写法
CREATE TABLE correct_syntax (
    id SERIAL,
    value1 INTEGER,
    value2 INTEGER GENERATED ALWAYS AS (value1 * 2) STORED
);

2. 类型不匹配

sql
-- 错误:类型不匹配
CREATE TABLE type_mismatch (
    id SERIAL,
    name VARCHAR(50),
    length INTEGER GENERATED ALWAYS AS (length(name)) STORED  -- length() 返回 INTEGER,匹配
);

-- 注意:确保表达式返回类型与列类型兼容

3. 循环依赖

sql
-- 错误:循环依赖
CREATE TABLE circular_dependency (
    id SERIAL,
    a INTEGER GENERATED ALWAYS AS (b + 1) STORED,    -- 错误:引用另一个生成列
    b INTEGER GENERATED ALWAYS AS (a + 1) STORED     -- 错误:循环依赖
);

实际应用场景

电商系统价格计算

sql
-- 电商商品表
CREATE TABLE ecommerce_products (
    product_id SERIAL PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    base_price DECIMAL(10,2) NOT NULL,
    discount_percentage DECIMAL(5,2) DEFAULT 0.00,
    tax_rate DECIMAL(5,4) DEFAULT 0.0875, -- 8.75% 税率
    
    -- 自动计算字段
    discount_amount DECIMAL(10,2) GENERATED ALWAYS AS (
        base_price * discount_percentage / 100
    ) STORED,
    
    discounted_price DECIMAL(10,2) GENERATED ALWAYS AS (
        base_price - (base_price * discount_percentage / 100)
    ) STORED,
    
    final_price DECIMAL(10,2) GENERATED ALWAYS AS (
        (base_price - (base_price * discount_percentage / 100)) * (1 + tax_rate)
    ) STORED,
    
    price_tier VARCHAR(20) GENERATED ALWAYS AS (
        CASE 
            WHEN base_price < 50 THEN 'low'
            WHEN base_price < 200 THEN 'medium'
            WHEN base_price < 500 THEN 'high'
            ELSE 'premium'
        END
    ) STORED
);

-- 插入示例数据
INSERT INTO ecommerce_products (name, base_price, discount_percentage) VALUES
    ('无线耳机', 199.99, 15.00),
    ('智能手环', 299.50, 10.00),
    ('机械键盘', 599.00, 5.00);

-- 查看计算结果
SELECT 
    name,
    base_price,
    discount_percentage,
    discount_amount,
    discounted_price,
    final_price,
    price_tier
FROM ecommerce_products;

总结

生成列是 PostgreSQL 中一个强大的特性,它提供了以下优势:

主要优势

  1. 自动维护:值始终与依赖列保持同步
  2. 性能优化:预计算复杂表达式,支持索引
  3. 数据一致性:避免手动维护计算字段的错误
  4. 简化查询:复杂计算逻辑封装在表结构中

使用建议

  • 适用于稳定的计算逻辑
  • 优先考虑简单的数学和字符串操作
  • 合理评估存储空间成本
  • 在生成列上创建索引以提高查询性能
  • 避免在生成列中使用易变函数

通过合理使用生成列,可以显著简化应用程序逻辑,提高数据一致性和查询性能。在设计时应该权衡计算复杂度、存储成本和查询性能等因素,选择最适合的实现方案。