Chapter 08

存储过程、触发器与扩展

PL/pgSQL 过程语言、数据库级自动化逻辑,以及 PostgreSQL 强大的扩展生态

8.1 PL/pgSQL 函数基础

PL/pgSQL 是 PostgreSQL 的过程语言扩展,允许编写包含条件判断、循环、异常处理的复杂逻辑,直接在数据库端执行。

SQL-- 基础函数:计算订单总金额
CREATE OR REPLACE FUNCTION get_order_total(p_order_id BIGINT)
RETURNS NUMERIC AS $$
DECLARE
  v_total NUMERIC;
BEGIN
  SELECT SUM(quantity * unit_price)
  INTO v_total
  FROM order_items
  WHERE order_id = p_order_id;

  RETURN COALESCE(v_total, 0);
END;
$$ LANGUAGE plpgsql STABLE;  -- STABLE: 相同输入相同输出,不修改数据

-- 调用函数
SELECT get_order_total(1001);

-- 返回表的函数(RETURNS TABLE / RETURNS SETOF)
CREATE OR REPLACE FUNCTION get_user_orders(p_user_id BIGINT, p_limit INTEGER DEFAULT 10)
RETURNS TABLE(
  order_id    BIGINT,
  status      TEXT,
  total       NUMERIC,
  item_count  BIGINT,
  created_at  TIMESTAMPTZ
) AS $$
BEGIN
  RETURN QUERY
  SELECT
    o.id,
    o.status,
    o.total,
    COUNT(oi.id),
    o.created_at
  FROM orders o
  LEFT JOIN order_items oi ON o.id = oi.order_id
  WHERE o.user_id = p_user_id
  GROUP BY o.id, o.status, o.total, o.created_at
  ORDER BY o.created_at DESC
  LIMIT p_limit;
END;
$$ LANGUAGE plpgsql STABLE;

-- 使用:像表一样查询
SELECT * FROM get_user_orders(42, 5);

8.2 PL/pgSQL 控制流与异常处理

SQL-- 带异常处理的转账函数
CREATE OR REPLACE FUNCTION transfer_funds(
  p_from_id  BIGINT,
  p_to_id    BIGINT,
  p_amount   NUMERIC
) RETURNS VOID AS $$
DECLARE
  v_from_balance NUMERIC;
BEGIN
  -- 参数校验
  IF p_amount <= 0 THEN
    RAISE EXCEPTION '转账金额必须大于 0,收到:%', p_amount
      USING ERRCODE = '22003';  -- numeric_value_out_of_range
  END IF;

  -- 锁定源账户并检查余额
  SELECT balance INTO STRICT v_from_balance
  FROM accounts
  WHERE id = p_from_id
  FOR UPDATE;

  IF v_from_balance < p_amount THEN
    RAISE EXCEPTION '余额不足:当前 %,需要 %', v_from_balance, p_amount
      USING ERRCODE = 'P0001';  -- raise_exception
  END IF;

  -- 执行转账
  UPDATE accounts SET balance = balance - p_amount WHERE id = p_from_id;
  UPDATE accounts SET balance = balance + p_amount WHERE id = p_to_id;

  RAISE NOTICE '转账成功:从账户 % 转入账户 % 金额 %', p_from_id, p_to_id, p_amount;

EXCEPTION
  WHEN NO_DATA_FOUND THEN
    RAISE EXCEPTION '账户 % 不存在', p_from_id;
  WHEN OTHERS THEN
    RAISE EXCEPTION '转账失败:% %', SQLERRM, SQLSTATE;
END;
$$ LANGUAGE plpgsql;

-- 调用
SELECT transfer_funds(1, 2, 500.00);

8.3 存储过程(Procedure)

PostgreSQL 11+ 引入了独立的 CREATE PROCEDURE(与函数的区别:过程可以提交/回滚事务,函数不能)。

SQL-- 存储过程可以在内部 COMMIT/ROLLBACK
CREATE OR REPLACE PROCEDURE batch_update_prices(p_category_id INTEGER, p_discount NUMERIC)
LANGUAGE plpgsql AS $$
DECLARE
  v_count INTEGER := 0;
  v_row products%ROWTYPE;
BEGIN
  FOR v_row IN
    SELECT * FROM products WHERE category_id = p_category_id
  LOOP
    UPDATE products
    SET price = price * (1 - p_discount / 100), updated_at = NOW()
    WHERE id = v_row.id;

    v_count := v_count + 1;

    -- 每 100 条提交一次(减少锁持有时间)
    IF v_count % 100 = 0 THEN
      COMMIT;
    END IF;
  END LOOP;

  COMMIT;
  RAISE NOTICE '已更新 % 条商品价格', v_count;
END;
$$;

-- 调用过程(用 CALL,不是 SELECT)
CALL batch_update_prices(1, 10);  -- 分类1的商品打9折

8.4 触发器

触发器在表的 INSERT/UPDATE/DELETE 操作时自动执行函数。PostgreSQL 的触发器函数必须返回 TRIGGER 类型。

SQL-- 最常用的触发器:自动更新 updated_at
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();  -- NEW 是即将写入的新行
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- 为 orders 表绑定触发器
CREATE TRIGGER orders_updated_at
  BEFORE UPDATE ON orders   -- BEFORE UPDATE:在实际更新之前执行
  FOR EACH ROW               -- ROW-level:每修改一行触发一次
  EXECUTE FUNCTION update_updated_at();

-- 同样为 products 绑定(一个函数可以被多个触发器使用)
CREATE TRIGGER products_updated_at
  BEFORE UPDATE ON products
  FOR EACH ROW
  EXECUTE FUNCTION update_updated_at();
SQL-- AFTER 触发器:审计日志
CREATE TABLE audit_log (
  id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  table_name TEXT NOT NULL,
  operation  TEXT NOT NULL,
  old_data   JSONB,
  new_data   JSONB,
  changed_by TEXT NOT NULL DEFAULT current_user,
  changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE OR REPLACE FUNCTION audit_changes()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO audit_log (table_name, operation, old_data, new_data)
  VALUES (
    TG_TABLE_NAME,    -- 内置变量:触发器所在表名
    TG_OP,            -- 内置变量:操作类型 INSERT/UPDATE/DELETE
    CASE WHEN TG_OP = 'INSERT' THEN NULL ELSE row_to_json(OLD)::JSONB END,
    CASE WHEN TG_OP = 'DELETE' THEN NULL ELSE row_to_json(NEW)::JSONB END
  );
  RETURN NULL;  -- AFTER 触发器返回值被忽略,返回 NULL 即可
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER orders_audit
  AFTER INSERT OR UPDATE OR DELETE ON orders
  FOR EACH ROW
  EXECUTE FUNCTION audit_changes();

8.5 扩展系统

PostgreSQL 的扩展系统(CREATE EXTENSION)是其最大优势之一,可以无缝添加新的数据类型、函数、操作符、索引方法。

SQL-- 查看已安装的扩展
SELECT name, default_version, installed_version, comment
FROM pg_available_extensions
WHERE installed_version IS NOT NULL
ORDER BY name;

-- 查看可安装(未安装)的扩展
SELECT name, default_version, comment
FROM pg_available_extensions
WHERE installed_version IS NULL
ORDER BY name;

常用扩展速览

SQL-- 1. uuid-ossp:UUID 生成函数(PG 13+ 用内置 gen_random_uuid 即可)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
SELECT uuid_generate_v4();    -- v4: 随机 UUID
SELECT uuid_generate_v1();    -- v1: 基于时间+MAC 地址

-- 2. pgcrypto:加密函数
CREATE EXTENSION IF NOT EXISTS pgcrypto;
SELECT crypt('mypassword', gen_salt('bf'));   -- bcrypt 哈希
SELECT crypt('input', stored_hash) = stored_hash; -- 验证密码

-- 3. pg_trgm:三元组相似度搜索(模糊搜索)
CREATE EXTENSION IF NOT EXISTS pg_trgm;
SELECT similarity('PostgreSQL', 'PostgresQL');    -- 相似度 0~1

-- 为相似度搜索建 GiST 索引
CREATE INDEX idx_products_name_trgm ON products
  USING GIN (name gin_trgm_ops);

-- 模糊搜索(即使拼写有错误也能找到)
SELECT name, similarity(name, 'Makcbook') AS sim
FROM products
WHERE name % 'Makcbook'   -- % 操作符:相似度 > 0.3(阈值可配置)
ORDER BY sim DESC;

-- 4. PostGIS:地理空间扩展
CREATE EXTENSION IF NOT EXISTS postgis;
ALTER TABLE stores ADD COLUMN location GEOMETRY(Point, 4326);

-- 插入地理坐标(经度, 纬度)
UPDATE stores
SET location = ST_SetSRID(ST_MakePoint(116.4074, 39.9042), 4326)
WHERE name = '北京旗舰店';

-- 查找 5 公里内的门店
SELECT name,
  ST_Distance(location::geography, ST_MakePoint(116.40, 39.90)::geography) / 1000 AS km
FROM stores
WHERE ST_DWithin(location::geography, ST_MakePoint(116.40, 39.90)::geography, 5000)
ORDER BY km;

-- 5. pg_stat_statements(性能监控,需 shared_preload_libraries)
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- 6. timescaledb:时序数据库(需单独安装)
-- 将普通表转换为超表(Hypertable),自动按时间分区
CREATE EXTENSION IF NOT EXISTS timescaledb;

CREATE TABLE metrics (
  time        TIMESTAMPTZ NOT NULL,
  device_id   TEXT NOT NULL,
  temperature NUMERIC,
  humidity    NUMERIC
);

-- 转换为超表,按时间自动分区(每 7 天一个 chunk)
SELECT create_hypertable('metrics', 'time', chunk_time_interval => INTERVAL '7 days');

-- 时序聚合查询
SELECT
  time_bucket('1 hour', time) AS bucket,
  device_id,
  AVG(temperature) AS avg_temp
FROM metrics
GROUP BY bucket, device_id
ORDER BY bucket DESC;
📌

本章小结PL/pgSQL 用 $$ 语法定义函数体,DECLARE 声明变量,RETURN QUERY 返回结果集,异常处理用 EXCEPTION WHEN。触发器函数返回 TRIGGER 类型,BEFORE 触发器可修改 NEW 行,AFTER 触发器返回 NULL。常用扩展:pg_trgm 模糊搜索、pgcrypto 加密、PostGIS 地理空间、timescaledb 时序数据。