第八章:视图与函数

最后更新: 2024-01-01 作者: PostgreSQL Team
页面目录

第八章:视图与函数

8.1 视图 (View)

视图是基于 SQL 查询的虚拟表,不存储实际数据。

8.1.1 创建视图

-- 创建简单视图
CREATE VIEW active_users AS
SELECT id, username, email, created_at
FROM users
WHERE status = 'active';

-- 查询视图
SELECT * FROM active_users WHERE username LIKE 'A%';

8.1.2 视图类型

-- 可更新视图
CREATE VIEW user_orders AS
SELECT 
    u.id,
    u.username,
    COUNT(o.id) AS order_count,
    COALESCE(SUM(o.total), 0) AS total_spent
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.username;

-- 插入数据到视图(通过规则)
CREATE OR REPLACE RULE "user_orders_insert" AS
    ON INSERT TO user_orders
    DO INSTEAD 
    INSERT INTO users (username) VALUES (NEW.username);

-- 创建只读视图
CREATE VIEW read_only_users AS
SELECT * FROM users
WITH READ ONLY;

8.1.3 物化视图

物化视图存储实际数据,支持索引,适合大数据集。

-- 创建物化视图
CREATE MATERIALIZED VIEW monthly_sales AS
SELECT 
    DATE_TRUNC('month', created_at) AS month,
    COUNT(*) AS order_count,
    SUM(total) AS revenue
FROM orders
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY month;

-- 在物化视图上创建索引
CREATE INDEX ON monthly_sales (month);

-- 刷新物化视图
REFRESH MATERIALIZED VIEW monthly_sales;

-- 并行刷新(不阻塞查询)
REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_sales;

-- 查看物化视图信息
SELECT * FROM pg_matviews WHERE matviewname = 'monthly_sales';

8.1.4 视图管理

-- 修改视图
CREATE OR REPLACE VIEW active_users AS
SELECT id, username, email, phone, created_at
FROM users
WHERE status = 'active';

-- 删除视图
DROP VIEW IF EXISTS old_view;

-- 重命名视图
ALTER VIEW old_name RENAME TO new_name;

-- 查看视图定义
SELECT pg_get_viewdef('view_name', true);

8.2 函数基础

8.2.1 函数结构

CREATE OR REPLACE FUNCTION function_name(params)
RETURNS return_type AS $$
DECLARE
    -- 变量声明
BEGIN
    -- 函数逻辑
    RETURN result;
END;
$$ LANGUAGE plpgsql;

8.2.2 标量函数

-- 返回用户订单数量
CREATE OR REPLACE FUNCTION get_user_order_count(user_id INTEGER)
RETURNS INTEGER AS $$
DECLARE
    order_cnt INTEGER;
BEGIN
    SELECT COUNT(*) INTO order_cnt
    FROM orders
    WHERE user_id = get_user_order_count.user_id;
    
    RETURN order_cnt;
END;
$$ LANGUAGE plpgsql;

-- 调用函数
SELECT get_user_order_count(1);
SELECT username, get_user_order_count(id) AS order_count FROM users;

8.2.3 复合类型函数

-- 返回表行类型
CREATE OR REPLACE FUNCTION get_user_info(user_id INTEGER)
RETURNS users AS $$
DECLARE
    user_record users;
BEGIN
    SELECT * INTO user_record
    FROM users
    WHERE id = get_user_info.user_id;
    
    RETURN user_record;
END;
$$ LANGUAGE plpgsql;

-- 调用
SELECT (get_user_info(1)).username;
SELECT (get_user_info(1)).*;

8.2.4 表函数

-- 返回表
CREATE OR REPLACE FUNCTION get_orders_by_status(order_status VARCHAR)
RETURNS TABLE (
    order_id INTEGER,
    user_id INTEGER,
    total DECIMAL(10, 2),
    status VARCHAR
) AS $$
BEGIN
    RETURN QUERY
    SELECT o.id, o.user_id, o.total, o.status
    FROM orders o
    WHERE o.status = get_orders_by_status.order_status;
END;
$$ LANGUAGE plpgsql;

-- 调用
SELECT * FROM get_orders_by_status('completed');

8.3 PL/pgSQL 进阶

8.3.1 变量与数据类型

CREATE OR REPLACE FUNCTION calculate_discount(price NUMERIC, discount_rate NUMERIC)
RETURNS NUMERIC AS $$
DECLARE
    final_price NUMERIC;
    discount_amount NUMERIC;
BEGIN
    -- 类型转换
    discount_amount := price * discount_rate::NUMERIC;
    final_price := price - discount_amount;
    
    -- 使用 %TYPE
    DECLARE
        user_email users.email%TYPE;
        user_record users%ROWTYPE;
    BEGIN
        SELECT * INTO user_record FROM users WHERE id = 1;
        user_email := user_record.email;
    END;
    
    RETURN final_price;
END;
$$ LANGUAGE plpgsql;

8.3.2 条件逻辑

CREATE OR REPLACE FUNCTION get_user_tier(total_spent NUMERIC)
RETURNS TEXT AS $$
BEGIN
    IF total_spent >= 10000 THEN
        RETURN 'VIP';
    ELSIF total_spent >= 5000 THEN
        RETURN 'Gold';
    ELSIF total_spent >= 1000 THEN
        RETURN 'Silver';
    ELSE
        RETURN 'Bronze';
    END IF;
END;
$$ LANGUAGE plpgsql;

-- CASE 表达式
CREATE OR REPLACE FUNCTION get_month_name(month_num INTEGER)
RETURNS TEXT AS $$
BEGIN
    RETURN CASE month_num
        WHEN 1 THEN 'January'
        WHEN 2 THEN 'February'
        WHEN 3 THEN 'March'
        WHEN 4 THEN 'April'
        WHEN 5 THEN 'May'
        WHEN 6 THEN 'June'
        WHEN 7 THEN 'July'
        WHEN 8 THEN 'August'
        WHEN 9 THEN 'September'
        WHEN 10 THEN 'October'
        WHEN 11 THEN 'November'
        WHEN 12 THEN 'December'
        ELSE 'Unknown'
    END CASE;
END;
$$ LANGUAGE plpgsql;

8.3.3 循环

-- FOR 循环(整数范围)
CREATE OR REPLACE FUNCTION generate_series(start_num INTEGER, end_num INTEGER)
RETURNS TABLE(n INTEGER) AS $$
BEGIN
    FOR n IN start_num..end_num LOOP
        RETURN NEXT;
    END LOOP;
    RETURN;
END;
$$ LANGUAGE plpgsql;

-- WHILE 循环
CREATE OR REPLACE FUNCTION factorial(n INTEGER)
RETURNS BIGINT AS $$
DECLARE
    result BIGINT := 1;
    i INTEGER := 1;
BEGIN
    WHILE i <= n LOOP
        result := result * i;
        i := i + 1;
    END LOOP;
    RETURN result;
END;
$$ LANGUAGE plpgsql;

-- FOR 循环(查询结果)
CREATE OR REPLACE FUNCTION get_user_summary()
RETURNS TABLE(username TEXT, order_count BIGINT) AS $$
BEGIN
    FOR username, order_count IN 
        SELECT u.username, COUNT(o.id)
        FROM users u
        LEFT JOIN orders o ON u.id = o.user_id
        GROUP BY u.username
    LOOP
        RETURN NEXT;
    END LOOP;
    RETURN;
END;
$$ LANGUAGE plpgsql;

8.4 事务与异常处理

8.4.1 事务控制

CREATE OR REPLACE FUNCTION transfer_funds(
    from_user_id INTEGER,
    to_user_id INTEGER,
    amount NUMERIC
)
RETURNS BOOLEAN AS $$
BEGIN
    -- 开始事务
    IF amount <= 0 THEN
        RAISE EXCEPTION '金额必须大于 0';
    END IF;
    
    -- 扣除转出账户
    UPDATE accounts 
    SET balance = balance - amount 
    WHERE user_id = from_user_id;
    
    IF NOT FOUND THEN
        RAISE EXCEPTION '转出账户不存在';
    END IF;
    
    -- 增加转入账户
    UPDATE accounts 
    SET balance = balance + amount 
    WHERE user_id = to_user_id;
    
    IF NOT FOUND THEN
        -- 回滚(隐式)
        RAISE EXCEPTION '转入账户不存在';
    END IF;
    
    RETURN TRUE;
EXCEPTION
    WHEN OTHERS THEN
        RAISE NOTICE '交易失败: %', SQLERRM;
        RETURN FALSE;
END;
$$ LANGUAGE plpgsql;

8.4.2 异常处理

CREATE OR REPLACE FUNCTION safe_divide(a NUMERIC, b NUMERIC)
RETURNS NUMERIC AS $$
BEGIN
    IF b = 0 THEN
        RAISE EXCEPTION '除数不能为零';
    END IF;
    
    RETURN a / b;
EXCEPTION
    WHEN raise_exception THEN
        RAISE NOTICE '发生错误: %', SQLERRM;
        RETURN NULL;
    WHEN OTHERS THEN
        RAISE NOTICE '未知错误: %', SQLERRM;
        RETURN NULL;
END;
$$ LANGUAGE plpgsql;

8.5 触发器

8.5.1 创建触发器函数

-- 审计日志触发器
CREATE OR REPLACE FUNCTION audit_trigger_function()
RETURNS TRIGGER AS $$
BEGIN
    IF TG_OP = 'INSERT' THEN
        INSERT INTO audit_log (table_name, operation, old_data, new_data, user_name)
        VALUES (TG_TABLE_NAME, TG_OP, NULL, row_to_json(NEW), current_user);
        RETURN NEW;
        
    ELSIF TG_OP = 'UPDATE' THEN
        INSERT INTO audit_log (table_name, operation, old_data, new_data, user_name)
        VALUES (TG_TABLE_NAME, TG_OP, row_to_json(OLD), row_to_json(NEW), current_user);
        RETURN NEW;
        
    ELSIF TG_OP = 'DELETE' THEN
        INSERT INTO audit_log (table_name, operation, old_data, new_data, user_name)
        VALUES (TG_TABLE_NAME, TG_OP, row_to_json(OLD), NULL, current_user);
        RETURN OLD;
    END IF;
    
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

8.5.2 创建触发器

-- 在表上创建触发器
CREATE TRIGGER users_audit_trigger
AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW EXECUTE FUNCTION audit_trigger_function();

-- 行级触发器(BEFORE)
CREATE OR REPLACE FUNCTION before_user_update()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at := CURRENT_TIMESTAMP;
    NEW.updated_by := current_user;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER users_before_update
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION before_user_update();

-- 条件触发器
CREATE TRIGGER users_salary_audit
AFTER UPDATE OF salary ON employees
FOR EACH ROW EXECUTE FUNCTION audit_trigger_function();

8.5.3 触发器管理

-- 查看触发器
\d users

-- 禁用触发器
ALTER TABLE users DISABLE TRIGGER users_audit_trigger;

-- 启用触发器
ALTER TABLE users ENABLE TRIGGER users_audit_trigger;

-- 删除触发器
DROP TRIGGER IF EXISTS users_audit_trigger ON users;

8.6 内置函数扩展

8.6.1 自定义聚合函数

-- 创建自定义聚合:字符串连接
CREATE AGGREGATE string_agg_delim (TEXT, TEXT) (
    SFUNC = CONCAT_WS,
    STYPE = TEXT,
    INITCOND = ''
);

-- 使用
SELECT string_agg_delim(DISTINCT email, ', ') FROM users;

8.6.2 自定义类型

-- 创建复合类型
CREATE TYPE inventory_item AS (
    product_name TEXT,
    supplier_name TEXT,
    quantity INTEGER
);

-- 在函数中使用
CREATE OR REPLACE FUNCTION get_inventory(product_id INTEGER)
RETURNS inventory_item AS $$
DECLARE
    item inventory_item;
BEGIN
    SELECT p.name, s.name, inv.quantity
    INTO item
    FROM products p
    JOIN inventory inv ON p.id = inv.product_id
    JOIN suppliers s ON inv.supplier_id = s.id
    WHERE p.id = product_id;
    
    RETURN item;
END;
$$ LANGUAGE plpgsql;

-- 调用
SELECT (get_inventory(1)).product_name;

8.7 本章小结

本章介绍了 PostgreSQL 的视图和函数:

主题 关键功能
视图 普通视图、物化视图、可更新视图
函数 标量函数、表函数、复合类型函数
PL/pgSQL 变量、条件、循环、异常处理
触发器 AFTER/BEFORE、行级/语句级触发器

📌 下一章预告

下一章将介绍 PostgreSQL 的事务机制和并发控制原理。