简体中文 繁體中文 English 日本語 Deutsch 한국 사람 بالعربية TÜRKÇE português คนไทย Français

站内搜索

搜索

活动公告

11-02 12:46
10-23 09:32
通知:本站资源由网友上传分享,如有违规等问题请到版务模块进行投诉,将及时处理!
10-23 09:31
10-23 09:28
通知:签到时间调整为每日4:00(东八区)
10-23 09:26

掌握PostgreSQL函数与视图创建方法 提升数据库开发效率的关键技巧 从入门到精通的实用教程 包含实例演示和最佳实践分享

3万

主题

423

科技点

3万

积分

大区版主

木柜子打湿

积分
31916

三倍冰淇淋无人之境【一阶】财Doro小樱(小丑装)立华奏以外的星空【二阶】⑨的冰沙

发表于 2025-9-23 18:40:01 | 显示全部楼层 |阅读模式 [标记阅至此楼]

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
引言

PostgreSQL作为世界上最强大的开源关系型数据库系统之一,提供了丰富的功能来支持复杂的数据操作和业务逻辑实现。其中,函数和视图是两个极为重要的特性,它们不仅能提高数据库开发效率,还能增强代码的可重用性和可维护性。本教程将带您从基础概念到高级应用,全面掌握PostgreSQL函数与视图的创建方法,并通过实例演示和最佳实践分享,帮助您提升数据库开发技能。

一、PostgreSQL函数基础

1.1 函数概述

在PostgreSQL中,函数是一段可重用的代码,可以接受输入参数,执行特定操作,并返回结果。函数可以帮助我们封装业务逻辑,简化复杂查询,提高代码的可维护性和重用性。

PostgreSQL支持多种函数语言,包括:

• SQL(默认)
• PL/pgSQL(PostgreSQL的过程语言)
• PL/Perl
• PL/Python
• PL/Tcl
• 等其他语言

1.2 创建基本SQL函数

SQL函数是最简单的函数类型,使用SQL语句定义。下面是一个基本的SQL函数示例:
  1. CREATE OR REPLACE FUNCTION add_numbers(a INTEGER, b INTEGER)
  2. RETURNS INTEGER AS $$
  3.     SELECT a + b;
  4. $$ LANGUAGE SQL;
复制代码

使用这个函数:
  1. SELECT add_numbers(5, 3);  -- 返回 8
复制代码

1.3 PL/pgSQL函数

PL/pgSQL是PostgreSQL的专有过程语言,比纯SQL函数更强大,支持变量、条件语句、循环等复杂逻辑。
  1. CREATE OR REPLACE FUNCTION function_name(parameter_list)
  2. RETURNS return_type AS $$
  3. DECLARE
  4.     -- 变量声明
  5.     variable_name variable_type;
  6. BEGIN
  7.     -- 函数体
  8.     -- 逻辑处理
  9.     RETURN return_value;
  10. END;
  11. $$ LANGUAGE plpgsql;
复制代码
  1. CREATE OR REPLACE FUNCTION get_employee_salary(emp_id INTEGER)
  2. RETURNS NUMERIC AS $$
  3. DECLARE
  4.     emp_salary NUMERIC;
  5. BEGIN
  6.     SELECT salary INTO emp_salary
  7.     FROM employees
  8.     WHERE employee_id = emp_id;
  9.    
  10.     IF emp_salary IS NULL THEN
  11.         RAISE EXCEPTION 'Employee not found';
  12.     END IF;
  13.    
  14.     RETURN emp_salary;
  15. END;
  16. $$ LANGUAGE plpgsql;
复制代码

1.4 函数参数模式

PostgreSQL函数支持三种参数模式:

• IN:输入参数(默认)
• OUT:输出参数
• INOUT:输入输出参数
  1. CREATE OR REPLACE FUNCTION get_employee_details(emp_id INTEGER,
  2.                                                OUT name TEXT,
  3.                                                OUT salary NUMERIC)
  4. AS $$
  5. BEGIN
  6.     SELECT employee_name, salary INTO name, salary
  7.     FROM employees
  8.     WHERE employee_id = emp_id;
  9.    
  10.     IF name IS NULL THEN
  11.         RAISE EXCEPTION 'Employee not found';
  12.     END IF;
  13. END;
  14. $$ LANGUAGE plpgsql;
复制代码

使用这个函数:
  1. SELECT * FROM get_employee_details(101);
复制代码
  1. CREATE OR REPLACE FUNCTION increment_value(INOUT value INTEGER, increment INTEGER DEFAULT 1)
  2. AS $$
  3. BEGIN
  4.     value := value + increment;
  5. END;
  6. $$ LANGUAGE plpgsql;
复制代码

使用这个函数:
  1. SELECT increment_value(10, 5);  -- 返回 15
复制代码

1.5 返回表数据的函数

PostgreSQL函数可以返回表数据,这在生成报表或封装复杂查询时非常有用。
  1. CREATE OR REPLACE FUNCTION get_high_earningEmployees(min_salary NUMERIC)
  2. RETURNS TABLE(employee_id INTEGER, employee_name TEXT, department TEXT) AS $$
  3. BEGIN
  4.     RETURN QUERY
  5.     SELECT e.employee_id, e.employee_name, d.department_name
  6.     FROM employees e
  7.     JOIN departments d ON e.department_id = d.department_id
  8.     WHERE e.salary >= min_salary
  9.     ORDER BY e.salary DESC;
  10. END;
  11. $$ LANGUAGE plpgsql;
复制代码

使用这个函数:
  1. SELECT * FROM get_high_earning_employees(50000);
复制代码
  1. CREATE OR REPLACE FUNCTION get_employees_by_department(dept_id INTEGER)
  2. RETURNS SETOF employees AS $$
  3. BEGIN
  4.     RETURN QUERY
  5.     SELECT * FROM employees
  6.     WHERE department_id = dept_id;
  7. END;
  8. $$ LANGUAGE plpgsql;
复制代码

使用这个函数:
  1. SELECT * FROM get_employees_by_department(2);
复制代码

1.6 函数高级特性

PostgreSQL允许创建同名但参数不同的函数:
  1. -- 第一个版本
  2. CREATE OR REPLACE FUNCTION calculate_tax(amount NUMERIC)
  3. RETURNS NUMERIC AS $$
  4. BEGIN
  5.     RETURN amount * 0.1;  -- 10% tax
  6. END;
  7. $$ LANGUAGE plpgsql;
  8. -- 第二个版本(重载)
  9. CREATE OR REPLACE FUNCTION calculate_tax(amount NUMERIC, rate NUMERIC)
  10. RETURNS NUMERIC AS $$
  11. BEGIN
  12.     RETURN amount * rate;
  13. END;
  14. $$ LANGUAGE plpgsql;
复制代码

使用这两个函数:
  1. SELECT calculate_tax(1000);          -- 使用第一个版本
  2. SELECT calculate_tax(1000, 0.15);    -- 使用第二个版本
复制代码
  1. CREATE OR REPLACE FUNCTION get_employees(limit_count INTEGER DEFAULT 10,
  2.                                         offset_count INTEGER DEFAULT 0)
  3. RETURNS TABLE(employee_id INTEGER, employee_name TEXT) AS $$
  4. BEGIN
  5.     RETURN QUERY
  6.     SELECT employee_id, employee_name
  7.     FROM employees
  8.     ORDER BY employee_name
  9.     LIMIT limit_count OFFSET offset_count;
  10. END;
  11. $$ LANGUAGE plpgsql;
复制代码

使用这个函数:
  1. SELECT * FROM get_employees();                    -- 使用默认参数
  2. SELECT * FROM get_employees(5);                   -- 只指定limit
  3. SELECT * FROM get_employees(5, 10);               -- 指定两个参数
  4. SELECT * FROM get_employees(limit_count := 5);    -- 命名参数
复制代码
  1. CREATE OR REPLACE FUNCTION concatenate_strings(VARIADIC text_array TEXT[])
  2. RETURNS TEXT AS $$
  3. DECLARE
  4.     result TEXT := '';
  5. BEGIN
  6.     FOR i IN 1..array_length(text_array, 1) LOOP
  7.         result := result || text_array[i];
  8.     END LOOP;
  9.    
  10.     RETURN result;
  11. END;
  12. $$ LANGUAGE plpgsql;
复制代码

使用这个函数:
  1. SELECT concatenate_strings('Hello', ' ', 'World', '!');  -- 返回 "Hello World!"
复制代码

二、PostgreSQL视图基础

2.1 视图概述

视图是基于SQL语句的结果集的可视化表。视图包含行和列,就像一个真实的表。视图中的字段是来自数据库中一个或多个真实表的字段。

视图的主要优点:

• 简化复杂查询
• 安全性(可以限制用户访问特定数据)
• 逻辑数据独立性
• 重用SQL逻辑

2.2 创建基本视图
  1. CREATE VIEW employee_department_view AS
  2. SELECT
  3.     e.employee_id,
  4.     e.employee_name,
  5.     e.salary,
  6.     d.department_name
  7. FROM
  8.     employees e
  9. JOIN
  10.     departments d ON e.department_id = d.department_id;
复制代码

使用视图:
  1. SELECT * FROM employee_department_view;
复制代码

2.3 修改和删除视图
  1. CREATE OR REPLACE VIEW employee_department_view AS
  2. SELECT
  3.     e.employee_id,
  4.     e.employee_name,
  5.     e.salary,
  6.     e.hire_date,
  7.     d.department_name
  8. FROM
  9.     employees e
  10. JOIN
  11.     departments d ON e.department_id = d.department_id;
复制代码
  1. DROP VIEW employee_department_view;
复制代码

2.4 视图的类型

基于单个表且不包含函数或分组数据的视图。
  1. CREATE VIEW employee_basic_view AS
  2. SELECT employee_id, employee_name, email
  3. FROM employees;
复制代码

基于多个表或包含函数、分组数据的视图。
  1. CREATE VIEW department_summary_view AS
  2. SELECT
  3.     d.department_id,
  4.     d.department_name,
  5.     COUNT(e.employee_id) AS employee_count,
  6.     AVG(e.salary) AS average_salary,
  7.     MAX(e.salary) AS max_salary,
  8.     MIN(e.salary) AS min_salary
  9. FROM
  10.     departments d
  11. LEFT JOIN
  12.     employees e ON d.department_id = e.department_id
  13. GROUP BY
  14.     d.department_id, d.department_name;
复制代码

2.5 物化视图

物化视图是物理存储的视图,可以定期刷新以提高查询性能。PostgreSQL 9.3及以上版本支持物化视图。
  1. CREATE MATERIALIZED VIEW sales_summary AS
  2. SELECT
  3.     p.product_id,
  4.     p.product_name,
  5.     SUM(s.quantity) AS total_quantity,
  6.     SUM(s.quantity * s.unit_price) AS total_revenue
  7. FROM
  8.     products p
  9. JOIN
  10.     sales s ON p.product_id = s.product_id
  11. GROUP BY
  12.     p.product_id, p.product_name
  13. WITH DATA;
复制代码
  1. -- 完全刷新
  2. REFRESH MATERIALIZED VIEW sales_summary;
  3. -- 并发刷新(PostgreSQL 9.4+)
  4. REFRESH MATERIALIZED VIEW CONCURRENTLY sales_summary;
复制代码
  1. DROP MATERIALIZED VIEW sales_summary;
复制代码

2.6 可更新视图

某些视图是可更新的,意味着可以通过视图更新底层表的数据。视图要可更新,必须满足以下条件:

• 基于单个表
• 不包含GROUP BY、HAVING、DISTINCT或聚合函数
• 不包含窗口函数
• 不包含集合操作(UNION、INTERSECT、EXCEPT)
  1. CREATE VIEW employee_contact_view AS
  2. SELECT employee_id, employee_name, email, phone
  3. FROM employees;
复制代码
  1. -- 更新数据
  2. UPDATE employee_contact_view
  3. SET email = 'new.email@example.com'
  4. WHERE employee_id = 101;
  5. -- 插入数据
  6. INSERT INTO employee_contact_view (employee_id, employee_name, email, phone)
  7. VALUES (999, 'New Employee', 'new.employee@example.com', '555-1234');
  8. -- 删除数据
  9. DELETE FROM employee_contact_view
  10. WHERE employee_id = 999;
复制代码

2.7 视图的高级特性

WITH CHECK OPTION确保通过视图插入或更新的数据在视图中仍然可见。
  1. CREATE VIEW high_salary_employees AS
  2. SELECT employee_id, employee_name, salary, department_id
  3. FROM employees
  4. WHERE salary > 50000
  5. WITH CHECK OPTION;
复制代码

现在,如果尝试通过视图更新工资低于50000的记录,将会失败:
  1. -- 这将失败,因为更新后salary不再满足视图条件
  2. UPDATE high_salary_employees
  3. SET salary = 45000
  4. WHERE employee_id = 101;
复制代码

递归视图使用WITH RECURSIVE子句定义,适用于处理层次结构数据。
  1. CREATE RECURSIVE VIEW employee_hierarchy (employee_id, employee_name, manager_id, level) AS
  2. SELECT
  3.     employee_id,
  4.     employee_name,
  5.     manager_id,
  6.     1
  7. FROM
  8.     employees
  9. WHERE
  10.     manager_id IS NULL
  11. UNION ALL
  12. SELECT
  13.     e.employee_id,
  14.     e.employee_name,
  15.     e.manager_id,
  16.     eh.level + 1
  17. FROM
  18.     employees e
  19. JOIN
  20.     employee_hierarchy eh ON e.manager_id = eh.employee_id;
复制代码

使用递归视图:
  1. SELECT * FROM employee_hierarchy ORDER BY level, employee_id;
复制代码

三、PostgreSQL函数高级应用

3.1 触发器函数

触发器函数是特殊的函数,用于在特定事件(INSERT、UPDATE、DELETE)发生时自动执行。
  1. CREATE OR REPLACE FUNCTION audit_employee_changes()
  2. RETURNS TRIGGER AS $$
  3. BEGIN
  4.     IF TG_OP = 'INSERT' THEN
  5.         INSERT INTO employee_audit (employee_id, operation, operation_time, new_values)
  6.         VALUES (NEW.employee_id, 'I', NOW(), row_to_json(NEW));
  7.         RETURN NEW;
  8.     ELSIF TG_OP = 'UPDATE' THEN
  9.         INSERT INTO employee_audit (employee_id, operation, operation_time, old_values, new_values)
  10.         VALUES (NEW.employee_id, 'U', NOW(), row_to_json(OLD), row_to_json(NEW));
  11.         RETURN NEW;
  12.     ELSIF TG_OP = 'DELETE' THEN
  13.         INSERT INTO employee_audit (employee_id, operation, operation_time, old_values)
  14.         VALUES (OLD.employee_id, 'D', NOW(), row_to_json(OLD));
  15.         RETURN OLD;
  16.     END IF;
  17.     RETURN NULL;
  18. END;
  19. $$ LANGUAGE plpgsql;
复制代码
  1. CREATE TRIGGER tr_employee_audit
  2. AFTER INSERT OR UPDATE OR DELETE ON employees
  3. FOR EACH ROW EXECUTE FUNCTION audit_employee_changes();
复制代码

3.2 聚合函数

PostgreSQL允许创建自定义聚合函数。
  1. -- 创建状态类型
  2. CREATE TYPE median_state AS (
  3.     count INTEGER,
  4.     values NUMERIC[]
  5. );
  6. -- 创建状态转换函数
  7. CREATE OR REPLACE FUNCTION median_transfn(state median_state, value NUMERIC)
  8. RETURNS median_state AS $$
  9. BEGIN
  10.     state.count := state.count + 1;
  11.     state.values := array_append(state.values, value);
  12.     RETURN state;
  13. END;
  14. $$ LANGUAGE plpgsql;
  15. -- 创建最终函数
  16. CREATE OR REPLACE FUNCTION median_finalfn(state median_state)
  17. RETURNS NUMERIC AS $$
  18. DECLARE
  19.     sorted_values NUMERIC[];
  20.     middle_index INTEGER;
  21. BEGIN
  22.     sorted_values := array(SELECT unnest(state.values) ORDER BY 1);
  23.    
  24.     IF state.count % 2 = 1 THEN
  25.         -- 奇数个值,返回中间值
  26.         middle_index := (state.count + 1) / 2;
  27.         RETURN sorted_values[middle_index];
  28.     ELSE
  29.         -- 偶数个值,返回中间两个值的平均
  30.         RETURN (sorted_values[state.count / 2] + sorted_values[(state.count / 2) + 1]) / 2;
  31.     END IF;
  32. END;
  33. $$ LANGUAGE plpgsql;
  34. -- 创建聚合函数
  35. CREATE AGGREGATE median(NUMERIC) (
  36.     SFUNC = median_transfn,
  37.     STYPE = median_state,
  38.     FINALFUNC = median_finalfn,
  39.     INITCOND = '(0, {})'
  40. );
复制代码

使用自定义聚合函数:
  1. SELECT department_id, median(salary) as median_salary
  2. FROM employees
  3. GROUP BY department_id;
复制代码

3.3 窗口函数

虽然PostgreSQL提供了许多内置窗口函数,但您也可以创建自定义窗口函数。
  1. -- 创建状态类型
  2. CREATE TYPE running_product_state AS (
  3.     current_product NUMERIC,
  4.     row_count INTEGER
  5. );
  6. -- 创建状态转换函数
  7. CREATE OR REPLACE FUNCTION running_product_transfn(state running_product_state, value NUMERIC, order_by_clause NUMERIC)
  8. RETURNS running_product_state AS $$
  9. BEGIN
  10.     IF state.row_count = 0 THEN
  11.         state.current_product := value;
  12.     ELSE
  13.         state.current_product := state.current_product * value;
  14.     END IF;
  15.    
  16.     state.row_count := state.row_count + 1;
  17.     RETURN state;
  18. END;
  19. $$ LANGUAGE plpgsql;
  20. -- 创建最终函数
  21. CREATE OR REPLACE FUNCTION running_product_finalfn(state running_product_state)
  22. RETURNS NUMERIC AS $$
  23. BEGIN
  24.     RETURN state.current_product;
  25. END;
  26. $$ LANGUAGE plpgsql;
  27. -- 创建窗口函数
  28. CREATE WINDOW FUNCTION running_product(NUMERIC) RETURNS NUMERIC
  29. IMMUTABLE
  30. AS 'SELECT running_product_finalfn(running_product_transfn($1, $2, $3))'
  31. LANGUAGE SQL;
复制代码

使用自定义窗口函数:
  1. SELECT
  2.     product_id,
  3.     quantity,
  4.     unit_price,
  5.     running_product(quantity * unit_price) OVER (ORDER BY product_id) AS cumulative_product
  6. FROM sales_items;
复制代码

3.4 表函数

表函数是返回表的函数,可以像表一样在查询中使用。
  1. CREATE OR REPLACE FUNCTION generate_series_dates(start_date DATE, end_date DATE)
  2. RETURNS TABLE(date_value DATE) AS $$
  3. BEGIN
  4.     RETURN QUERY
  5.     SELECT start_date + (n || ' day')::INTERVAL
  6.     FROM generate_series(0, (end_date - start_date)) AS n;
  7. END;
  8. $$ LANGUAGE plpgsql;
复制代码

使用表函数:
  1. SELECT date_value, extract(DOW FROM date_value) AS day_of_week
  2. FROM generate_series_dates('2023-01-01', '2023-01-31');
复制代码

3.5 安全定义者函数

安全定义者函数(SECURITY DEFINER)以函数所有者的权限执行,而不是调用者的权限。这对于创建需要特殊权限的函数非常有用。
  1. CREATE OR REPLACE FUNCTION reset_user_password(username TEXT, new_password TEXT)
  2. RETURNS VOID AS $$
  3. BEGIN
  4.     -- 这个函数需要更新users表,普通用户可能没有这个权限
  5.     UPDATE users
  6.     SET password_hash = crypt(new_password, gen_salt('bf'))
  7.     WHERE user_name = username;
  8.    
  9.     IF NOT FOUND THEN
  10.         RAISE EXCEPTION 'User % not found', username;
  11.     END IF;
  12. END;
  13. $$ LANGUAGE plpgsql SECURITY DEFINER;
复制代码

四、PostgreSQL视图高级应用

4.1 视图与权限管理

视图可以用于实现行级安全性,限制用户只能看到他们有权访问的数据。
  1. -- 创建只显示特定部门员工信息的视图
  2. CREATE VIEW department_employees_view AS
  3. SELECT e.employee_id, e.employee_name, e.email, e.salary
  4. FROM employees e
  5. JOIN departments d ON e.department_id = d.department_id
  6. JOIN user_departments ud ON d.department_id = ud.department_id
  7. WHERE ud.user_name = current_user;
复制代码

PostgreSQL 9.5及以上版本支持行级安全性,可以更精细地控制数据访问。
  1. -- 启用行级安全性
  2. ALTER TABLE employees ENABLE ROW LEVEL SECURITY;
  3. -- 创建策略,只允许用户查看自己部门的信息
  4. CREATE POLICY department_policy ON employees
  5. FOR SELECT
  6. USING (department_id IN (
  7.     SELECT department_id
  8.     FROM user_departments
  9.     WHERE user_name = current_user
  10. ));
复制代码

4.2 视图性能优化

视图可能会影响查询性能,特别是复杂视图。以下是优化视图性能的一些技巧。

确保视图底层表有适当的索引:
  1. -- 为employees表的department_id创建索引
  2. CREATE INDEX idx_employees_department_id ON employees(department_id);
  3. -- 为经常用于连接的列创建索引
  4. CREATE INDEX idx_employees_employee_name ON employees(employee_name);
复制代码

对于频繁执行但数据不常变化的复杂查询,使用物化视图:
  1. CREATE MATERIALIZED VIEW monthly_sales_summary AS
  2. SELECT
  3.     DATE_TRUNC('month', sale_date) AS month,
  4.     product_id,
  5.     SUM(quantity) AS total_quantity,
  6.     SUM(quantity * unit_price) AS total_revenue
  7. FROM sales
  8. GROUP BY DATE_TRUNC('month', sale_date), product_id;
  9. -- 创建索引
  10. CREATE INDEX idx_monthly_sales_summary_month ON monthly_sales_summary(month);
  11. CREATE INDEX idx_monthly_sales_summary_product ON monthly_sales_summary(product_id);
复制代码

4.3 视图与数据分区

视图可以用于简化分区表的访问,使分区对应用程序透明。
  1. -- 创建主表
  2. CREATE TABLE sales (
  3.     sale_id SERIAL,
  4.     sale_date DATE NOT NULL,
  5.     product_id INTEGER NOT NULL,
  6.     quantity INTEGER NOT NULL,
  7.     unit_price NUMERIC(10,2) NOT NULL
  8. ) PARTITION BY RANGE (sale_date);
  9. -- 创建分区
  10. CREATE TABLE sales_2023_q1 PARTITION OF sales
  11.     FOR VALUES FROM ('2023-01-01') TO ('2023-04-01');
  12. CREATE TABLE sales_2023_q2 PARTITION OF sales
  13.     FOR VALUES FROM ('2023-04-01') TO ('2023-07-01');
  14. CREATE TABLE sales_2023_q3 PARTITION OF sales
  15.     FOR VALUES FROM ('2023-07-01') TO ('2023-10-01');
  16. CREATE TABLE sales_2023_q4 PARTITION OF sales
  17.     FOR VALUES FROM ('2023-10-01') TO ('2024-01-01');
复制代码
  1. CREATE VIEW current_year_sales AS
  2. SELECT * FROM sales
  3. WHERE sale_date >= DATE_TRUNC('year', CURRENT_DATE)
  4.   AND sale_date < DATE_TRUNC('year', CURRENT_DATE) + INTERVAL '1 year';
复制代码

4.4 视图与应用集成

视图可以用于简化应用程序代码,将复杂逻辑移到数据库层。
  1. CREATE VIEW customer_order_summary AS
  2. SELECT
  3.     c.customer_id,
  4.     c.customer_name,
  5.     c.email,
  6.     COUNT(o.order_id) AS total_orders,
  7.     COALESCE(SUM(o.total_amount), 0) AS total_spent,
  8.     MAX(o.order_date) AS last_order_date
  9. FROM
  10.     customers c
  11. LEFT JOIN
  12.     orders o ON c.customer_id = o.customer_id
  13. GROUP BY
  14.     c.customer_id, c.customer_name, c.email;
复制代码

应用程序可以简单地查询这个视图,而不是执行复杂的连接和聚合:
  1. -- 应用程序查询
  2. SELECT * FROM customer_order_summary WHERE customer_id = 123;
复制代码

五、综合实例:构建报表系统

让我们通过一个综合实例,展示如何结合函数和视图构建一个报表系统。

5.1 场景描述

假设我们有一个销售数据库,包含产品、客户、销售订单等表。我们需要创建一个报表系统,提供以下功能:

1. 按时间段、产品类别和地区生成销售报表
2. 计算销售趋势和增长率
3. 识别顶级客户和产品
4. 提供销售预测

5.2 数据库表结构
  1. -- 产品表
  2. CREATE TABLE products (
  3.     product_id SERIAL PRIMARY KEY,
  4.     product_name VARCHAR(100) NOT NULL,
  5.     category_id INTEGER NOT NULL,
  6.     unit_price NUMERIC(10,2) NOT NULL,
  7.     cost NUMERIC(10,2) NOT NULL,
  8.     stock_quantity INTEGER NOT NULL,
  9.     is_active BOOLEAN DEFAULT true
  10. );
  11. -- 产品类别表
  12. CREATE TABLE categories (
  13.     category_id SERIAL PRIMARY KEY,
  14.     category_name VARCHAR(50) NOT NULL,
  15.     parent_category_id INTEGER REFERENCES categories(category_id)
  16. );
  17. -- 客户表
  18. CREATE TABLE customers (
  19.     customer_id SERIAL PRIMARY KEY,
  20.     customer_name VARCHAR(100) NOT NULL,
  21.     email VARCHAR(100),
  22.     phone VARCHAR(20),
  23.     address TEXT,
  24.     city VARCHAR(50),
  25.     state VARCHAR(50),
  26.     country VARCHAR(50),
  27.     postal_code VARCHAR(20),
  28.     registration_date DATE NOT NULL
  29. );
  30. -- 销售订单表
  31. CREATE TABLE orders (
  32.     order_id SERIAL PRIMARY KEY,
  33.     customer_id INTEGER NOT NULL REFERENCES customers(customer_id),
  34.     order_date TIMESTAMP NOT NULL,
  35.     total_amount NUMERIC(12,2) NOT NULL,
  36.     status VARCHAR(20) NOT NULL
  37. );
  38. -- 订单明细表
  39. CREATE TABLE order_items (
  40.     order_item_id SERIAL PRIMARY KEY,
  41.     order_id INTEGER NOT NULL REFERENCES orders(order_id),
  42.     product_id INTEGER NOT NULL REFERENCES products(product_id),
  43.     quantity INTEGER NOT NULL,
  44.     unit_price NUMERIC(10,2) NOT NULL,
  45.     discount NUMERIC(5,2) DEFAULT 0
  46. );
复制代码

5.3 创建辅助函数
  1. CREATE OR REPLACE FUNCTION calculate_order_profit(order_id INTEGER)
  2. RETURNS NUMERIC AS $$
  3. DECLARE
  4.     total_cost NUMERIC := 0;
  5.     total_revenue NUMERIC := 0;
  6.     profit NUMERIC := 0;
  7. BEGIN
  8.     -- 计算总成本
  9.     SELECT SUM(oi.quantity * p.cost) INTO total_cost
  10.     FROM order_items oi
  11.     JOIN products p ON oi.product_id = p.product_id
  12.     WHERE oi.order_id = order_id;
  13.    
  14.     -- 计算总收入
  15.     SELECT SUM(oi.quantity * (oi.unit_price - oi.discount)) INTO total_revenue
  16.     FROM order_items oi
  17.     WHERE oi.order_id = order_id;
  18.    
  19.     -- 计算利润
  20.     profit := total_revenue - total_cost;
  21.    
  22.     RETURN profit;
  23. END;
  24. $$ LANGUAGE plpgsql;
复制代码
  1. CREATE OR REPLACE FUNCTION calculate_growth_rate(current_period_value NUMERIC, previous_period_value NUMERIC)
  2. RETURNS NUMERIC AS $$
  3. BEGIN
  4.     IF previous_period_value = 0 THEN
  5.         RETURN NULL;  -- 无法计算增长率
  6.     END IF;
  7.    
  8.     RETURN ((current_period_value - previous_period_value) / previous_period_value) * 100;
  9. END;
  10. $$ LANGUAGE plpgsql;
复制代码
  1. CREATE OR REPLACE FUNCTION generate_date_range(start_date DATE, end_date DATE, interval_type TEXT DEFAULT 'day')
  2. RETURNS TABLE(date_value DATE) AS $$
  3. BEGIN
  4.     IF interval_type = 'day' THEN
  5.         RETURN QUERY
  6.         SELECT start_date + (n || ' day')::INTERVAL
  7.         FROM generate_series(0, (end_date - start_date)) AS n;
  8.     ELSIF interval_type = 'week' THEN
  9.         RETURN QUERY
  10.         SELECT start_date + (n || ' week')::INTERVAL
  11.         FROM generate_series(0, (end_date - start_date) / 7) AS n;
  12.     ELSIF interval_type = 'month' THEN
  13.         RETURN QUERY
  14.         SELECT (start_date + (n || ' month')::INTERVAL)::DATE
  15.         FROM generate_series(0, (EXTRACT(YEAR FROM end_date) - EXTRACT(YEAR FROM start_date)) * 12 +
  16.                                   (EXTRACT(MONTH FROM end_date) - EXTRACT(MONTH FROM start_date))) AS n;
  17.     ELSE
  18.         RAISE EXCEPTION 'Invalid interval type: %', interval_type;
  19.     END IF;
  20. END;
  21. $$ LANGUAGE plpgsql;
复制代码

5.4 创建基础视图
  1. CREATE VIEW sales_detail_view AS
  2. SELECT
  3.     o.order_id,
  4.     o.customer_id,
  5.     c.customer_name,
  6.     o.order_date,
  7.     oi.order_item_id,
  8.     oi.product_id,
  9.     p.product_name,
  10.     cat.category_name,
  11.     oi.quantity,
  12.     oi.unit_price,
  13.     oi.discount,
  14.     (oi.quantity * oi.unit_price) AS gross_amount,
  15.     (oi.quantity * oi.discount) AS discount_amount,
  16.     (oi.quantity * (oi.unit_price - oi.discount)) AS net_amount,
  17.     (oi.quantity * p.cost) AS cost_amount,
  18.     (oi.quantity * (oi.unit_price - oi.discount - p.cost)) AS profit_amount
  19. FROM
  20.     orders o
  21. JOIN
  22.     customers c ON o.customer_id = c.customer_id
  23. JOIN
  24.     order_items oi ON o.order_id = oi.order_id
  25. JOIN
  26.     products p ON oi.product_id = p.product_id
  27. JOIN
  28.     categories cat ON p.category_id = cat.category_id;
复制代码
  1. CREATE VIEW product_sales_summary_view AS
  2. SELECT
  3.     p.product_id,
  4.     p.product_name,
  5.     cat.category_name,
  6.     COUNT(DISTINCT o.order_id) AS order_count,
  7.     SUM(oi.quantity) AS total_quantity,
  8.     SUM(oi.quantity * oi.unit_price) AS gross_revenue,
  9.     SUM(oi.quantity * oi.discount) AS total_discount,
  10.     SUM(oi.quantity * (oi.unit_price - oi.discount)) AS net_revenue,
  11.     SUM(oi.quantity * p.cost) AS total_cost,
  12.     SUM(oi.quantity * (oi.unit_price - oi.discount - p.cost)) AS total_profit
  13. FROM
  14.     products p
  15. JOIN
  16.     order_items oi ON p.product_id = oi.product_id
  17. JOIN
  18.     orders o ON oi.order_id = o.order_id
  19. JOIN
  20.     categories cat ON p.category_id = cat.category_id
  21. GROUP BY
  22.     p.product_id, p.product_name, cat.category_name;
复制代码
  1. CREATE VIEW customer_sales_summary_view AS
  2. SELECT
  3.     c.customer_id,
  4.     c.customer_name,
  5.     c.city,
  6.     c.state,
  7.     c.country,
  8.     COUNT(DISTINCT o.order_id) AS order_count,
  9.     MIN(o.order_date) AS first_order_date,
  10.     MAX(o.order_date) AS last_order_date,
  11.     SUM(o.total_amount) AS total_spent,
  12.     AVG(o.total_amount) AS average_order_value
  13. FROM
  14.     customers c
  15. JOIN
  16.     orders o ON c.customer_id = o.customer_id
  17. GROUP BY
  18.     c.customer_id, c.customer_name, c.city, c.state, c.country;
复制代码

5.5 创建高级报表视图
  1. CREATE VIEW time_series_sales_view AS
  2. SELECT
  3.     dr.date_value,
  4.     COUNT(DISTINCT o.order_id) AS order_count,
  5.     SUM(o.total_amount) AS total_revenue,
  6.     calculate_order_profit(o.order_id) AS total_profit
  7. FROM
  8.     generate_date_range(
  9.         (SELECT MIN(order_date)::DATE FROM orders),
  10.         (SELECT MAX(order_date)::DATE FROM orders),
  11.         'day'
  12.     ) dr
  13. LEFT JOIN
  14.     orders o ON dr.date_value = o.order_date::DATE
  15. GROUP BY
  16.     dr.date_value
  17. ORDER BY
  18.     dr.date_value;
复制代码
  1. CREATE VIEW sales_trend_view AS
  2. WITH monthly_sales AS (
  3.     SELECT
  4.         DATE_TRUNC('month', order_date)::DATE AS month,
  5.         COUNT(DISTINCT order_id) AS order_count,
  6.         SUM(total_amount) AS total_revenue
  7.     FROM
  8.         orders
  9.     GROUP BY
  10.         DATE_TRUNC('month', order_date)::DATE
  11. )
  12. SELECT
  13.     ms.month,
  14.     ms.order_count,
  15.     ms.total_revenue,
  16.     LAG(ms.order_count) OVER (ORDER BY ms.month) AS prev_month_order_count,
  17.     LAG(ms.total_revenue) OVER (ORDER BY ms.month) AS prev_month_revenue,
  18.     calculate_growth_rate(ms.order_count, LAG(ms.order_count) OVER (ORDER BY ms.month)) AS order_count_growth,
  19.     calculate_growth_rate(ms.total_revenue, LAG(ms.total_revenue) OVER (ORDER BY ms.month)) AS revenue_growth
  20. FROM
  21.     monthly_sales ms
  22. ORDER BY
  23.     ms.month;
复制代码
  1. CREATE VIEW category_sales_view AS
  2. SELECT
  3.     cat.category_id,
  4.     cat.category_name,
  5.     COUNT(DISTINCT o.order_id) AS order_count,
  6.     SUM(oi.quantity) AS total_quantity,
  7.     SUM(oi.quantity * oi.unit_price) AS gross_revenue,
  8.     SUM(oi.quantity * oi.discount) AS total_discount,
  9.     SUM(oi.quantity * (oi.unit_price - oi.discount)) AS net_revenue,
  10.     SUM(oi.quantity * p.cost) AS total_cost,
  11.     SUM(oi.quantity * (oi.unit_price - oi.discount - p.cost)) AS total_profit,
  12.     CASE
  13.         WHEN SUM(oi.quantity * p.cost) = 0 THEN NULL
  14.         ELSE (SUM(oi.quantity * (oi.unit_price - oi.discount - p.cost)) / SUM(oi.quantity * p.cost)) * 100
  15.     END AS profit_margin_percentage
  16. FROM
  17.     categories cat
  18. LEFT JOIN
  19.     products p ON cat.category_id = p.category_id
  20. LEFT JOIN
  21.     order_items oi ON p.product_id = oi.product_id
  22. LEFT JOIN
  23.     orders o ON oi.order_id = o.order_id
  24. GROUP BY
  25.     cat.category_id, cat.category_name
  26. ORDER BY
  27.     total_revenue DESC;
复制代码

5.6 创建报表函数
  1. CREATE OR REPLACE FUNCTION generate_sales_report(
  2.     start_date DATE DEFAULT NULL,
  3.     end_date DATE DEFAULT NULL,
  4.     category_id INTEGER DEFAULT NULL,
  5.     country VARCHAR(50) DEFAULT NULL
  6. )
  7. RETURNS TABLE (
  8.     report_date DATE,
  9.     order_count INTEGER,
  10.     total_revenue NUMERIC,
  11.     total_profit NUMERIC,
  12.     profit_margin NUMERIC
  13. ) AS $$
  14. BEGIN
  15.     -- 设置默认日期范围
  16.     IF start_date IS NULL THEN
  17.         start_date := (SELECT MIN(order_date)::DATE FROM orders);
  18.     END IF;
  19.    
  20.     IF end_date IS NULL THEN
  21.         end_date := (SELECT MAX(order_date)::DATE FROM orders);
  22.     END IF;
  23.    
  24.     RETURN QUERY
  25.     WITH daily_sales AS (
  26.         SELECT
  27.             o.order_date::DATE AS sale_date,
  28.             COUNT(DISTINCT o.order_id) AS order_count,
  29.             SUM(o.total_amount) AS total_revenue,
  30.             SUM(calculate_order_profit(o.order_id)) AS total_profit
  31.         FROM
  32.             orders o
  33.         JOIN
  34.             order_items oi ON o.order_id = oi.order_id
  35.         JOIN
  36.             products p ON oi.product_id = p.product_id
  37.         JOIN
  38.             customers c ON o.customer_id = c.customer_id
  39.         WHERE
  40.             o.order_date::DATE BETWEEN start_date AND end_date
  41.             AND (category_id IS NULL OR p.category_id = category_id)
  42.             AND (country IS NULL OR c.country = country)
  43.         GROUP BY
  44.             o.order_date::DATE
  45.     )
  46.     SELECT
  47.         ds.sale_date AS report_date,
  48.         ds.order_count,
  49.         ds.total_revenue,
  50.         ds.total_profit,
  51.         CASE
  52.             WHEN ds.total_revenue = 0 THEN 0
  53.             ELSE (ds.total_profit / ds.total_revenue) * 100
  54.         END AS profit_margin
  55.     FROM
  56.         daily_sales ds
  57.     ORDER BY
  58.         ds.sale_date;
  59. END;
  60. $$ LANGUAGE plpgsql;
复制代码

使用这个函数生成报表:
  1. -- 生成所有销售报表
  2. SELECT * FROM generate_sales_report();
  3. -- 生成特定日期范围的销售报表
  4. SELECT * FROM generate_sales_report('2023-01-01', '2023-01-31');
  5. -- 生成特定类别和国家的销售报表
  6. SELECT * FROM generate_sales_report('2023-01-01', '2023-01-31', 5, 'USA');
复制代码
  1. CREATE OR REPLACE FUNCTION generate_top_customers_report(
  2.     limit_count INTEGER DEFAULT 10,
  3.     start_date DATE DEFAULT NULL,
  4.     end_date DATE DEFAULT NULL
  5. )
  6. RETURNS TABLE (
  7.     customer_id INTEGER,
  8.     customer_name VARCHAR(100),
  9.     city VARCHAR(50),
  10.     country VARCHAR(50),
  11.     order_count INTEGER,
  12.     total_spent NUMERIC,
  13.     average_order_value NUMERIC
  14. ) AS $$
  15. BEGIN
  16.     -- 设置默认日期范围
  17.     IF start_date IS NULL THEN
  18.         start_date := (SELECT MIN(order_date)::DATE FROM orders);
  19.     END IF;
  20.    
  21.     IF end_date IS NULL THEN
  22.         end_date := (SELECT MAX(order_date)::DATE FROM orders);
  23.     END IF;
  24.    
  25.     RETURN QUERY
  26.     SELECT
  27.         c.customer_id,
  28.         c.customer_name,
  29.         c.city,
  30.         c.country,
  31.         COUNT(DISTINCT o.order_id) AS order_count,
  32.         SUM(o.total_amount) AS total_spent,
  33.         AVG(o.total_amount) AS average_order_value
  34.     FROM
  35.         customers c
  36.     JOIN
  37.         orders o ON c.customer_id = o.customer_id
  38.     WHERE
  39.         o.order_date::DATE BETWEEN start_date AND end_date
  40.     GROUP BY
  41.         c.customer_id, c.customer_name, c.city, c.country
  42.     ORDER BY
  43.         total_spent DESC
  44.     LIMIT limit_count;
  45. END;
  46. $$ LANGUAGE plpgsql;
复制代码

使用这个函数生成顶级客户报表:
  1. -- 生成前10名客户报表
  2. SELECT * FROM generate_top_customers_report();
  3. -- 生成特定日期范围的前5名客户报表
  4. SELECT * FROM generate_top_customers_report(5, '2023-01-01', '2023-01-31');
复制代码
  1. CREATE OR REPLACE FUNCTION generate_sales_forecast(
  2.     forecast_months INTEGER DEFAULT 3,
  3.     method VARCHAR(10) DEFAULT 'linear'  -- 'linear' 或 'exponential'
  4. )
  5. RETURNS TABLE (
  6.     forecast_month DATE,
  7.     forecast_revenue NUMERIC,
  8.     confidence_lower NUMERIC,
  9.     confidence_upper NUMERIC
  10. ) AS $$
  11. DECLARE
  12.     historical_months INTEGER := 12;  -- 使用过去12个月的数据进行预测
  13.     slope NUMERIC;
  14.     intercept NUMERIC;
  15.     r_squared NUMERIC;
  16.     std_error NUMERIC;
  17.     t_value NUMERIC := 2.576;  -- 99%置信区间的t值
  18.     i INTEGER;
  19. BEGIN
  20.     -- 创建临时表存储历史数据
  21.     CREATE TEMPORARY TABLE temp_historical_sales AS
  22.     SELECT
  23.         DATE_TRUNC('month', order_date)::DATE AS month,
  24.         SUM(total_amount) AS monthly_revenue
  25.     FROM
  26.         orders
  27.     WHERE
  28.         order_date >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 year' * historical_months
  29.     GROUP BY
  30.         DATE_TRUNC('month', order_date)::DATE
  31.     ORDER BY
  32.         month;
  33.    
  34.     -- 计算线性回归参数
  35.     WITH regression_data AS (
  36.         SELECT
  37.             month,
  38.             monthly_revenue,
  39.             ROW_NUMBER() OVER (ORDER BY month) - 1 AS x  -- 0-based index
  40.         FROM
  41.             temp_historical_sales
  42.     ),
  43.     regression_stats AS (
  44.         SELECT
  45.             COUNT(*) AS n,
  46.             AVG(x) AS avg_x,
  47.             AVG(monthly_revenue) AS avg_y,
  48.             SUM((x - avg_x) * (monthly_revenue - avg_y)) AS sum_xy,
  49.             SUM((x - avg_x) * (x - avg_x)) AS sum_xx,
  50.             SUM((monthly_revenue - avg_y) * (monthly_revenue - avg_y)) AS sum_yy
  51.         FROM
  52.             regression_data
  53.     )
  54.     SELECT
  55.         sum_xy / sum_xx AS slope,
  56.         avg_y - (sum_xy / sum_xx) * avg_x AS intercept,
  57.         POWER(sum_xy, 2) / (sum_xx * sum_yy) AS r_squared,
  58.         SQRT((sum_yy - (POWER(sum_xy, 2) / sum_xx)) / (n - 2)) AS std_error
  59.     INTO
  60.         slope, intercept, r_squared, std_error
  61.     FROM
  62.         regression_stats;
  63.    
  64.     -- 生成预测数据
  65.     FOR i IN 1..forecast_months LOOP
  66.         RETURN QUERY
  67.         SELECT
  68.             (DATE_TRUNC('month', CURRENT_DATE) + (i || ' month')::INTERVAL)::DATE AS forecast_month,
  69.             CASE
  70.                 WHEN method = 'linear' THEN intercept + slope * (historical_months + i)
  71.                 WHEN method = 'exponential' THEN (SELECT monthly_revenue FROM temp_historical_sales ORDER BY month DESC LIMIT 1) * POWER(1 + slope/100, i)
  72.                 ELSE NULL
  73.             END AS forecast_revenue,
  74.             CASE
  75.                 WHEN method = 'linear' THEN (intercept + slope * (historical_months + i)) - t_value * std_error
  76.                 WHEN method = 'exponential' THEN (SELECT monthly_revenue FROM temp_historical_sales ORDER BY month DESC LIMIT 1) * POWER(1 + slope/100, i) * (1 - t_value * std_error/100)
  77.                 ELSE NULL
  78.             END AS confidence_lower,
  79.             CASE
  80.                 WHEN method = 'linear' THEN (intercept + slope * (historical_months + i)) + t_value * std_error
  81.                 WHEN method = 'exponential' THEN (SELECT monthly_revenue FROM temp_historical_sales ORDER BY month DESC LIMIT 1) * POWER(1 + slope/100, i) * (1 + t_value * std_error/100)
  82.                 ELSE NULL
  83.             END AS confidence_upper;
  84.     END LOOP;
  85.    
  86.     -- 清理临时表
  87.     DROP TABLE temp_historical_sales;
  88. END;
  89. $$ LANGUAGE plpgsql;
复制代码

使用这个函数生成销售预测:
  1. -- 生成未来3个月的线性预测
  2. SELECT * FROM generate_sales_forecast();
  3. -- 生成未来6个月的指数预测
  4. SELECT * FROM generate_sales_forecast(6, 'exponential');
复制代码

5.7 创建物化视图提高报表性能

对于复杂的报表,我们可以使用物化视图来提高性能:
  1. CREATE MATERIALIZED VIEW sales_report_materialized_view AS
  2. SELECT
  3.     DATE_TRUNC('month', o.order_date)::DATE AS report_month,
  4.     cat.category_id,
  5.     cat.category_name,
  6.     c.country,
  7.     COUNT(DISTINCT o.order_id) AS order_count,
  8.     SUM(oi.quantity) AS total_quantity,
  9.     SUM(oi.quantity * oi.unit_price) AS gross_revenue,
  10.     SUM(oi.quantity * oi.discount) AS total_discount,
  11.     SUM(oi.quantity * (oi.unit_price - oi.discount)) AS net_revenue,
  12.     SUM(oi.quantity * p.cost) AS total_cost,
  13.     SUM(oi.quantity * (oi.unit_price - oi.discount - p.cost)) AS total_profit
  14. FROM
  15.     orders o
  16. JOIN
  17.     order_items oi ON o.order_id = oi.order_id
  18. JOIN
  19.     products p ON oi.product_id = p.product_id
  20. JOIN
  21.     categories cat ON p.category_id = cat.category_id
  22. JOIN
  23.     customers c ON o.customer_id = c.customer_id
  24. GROUP BY
  25.     DATE_TRUNC('month', o.order_date)::DATE,
  26.     cat.category_id,
  27.     cat.category_name,
  28.     c.country;
  29. -- 创建索引
  30. CREATE INDEX idx_sales_report_month ON sales_report_materialized_view(report_month);
  31. CREATE INDEX idx_sales_report_category ON sales_report_materialized_view(category_id);
  32. CREATE INDEX idx_sales_report_country ON sales_report_materialized_view(country);
复制代码

创建定期刷新物化视图的函数:
  1. CREATE OR REPLACE FUNCTION refresh_sales_report_materialized_view()
  2. RETURNS VOID AS $$
  3. BEGIN
  4.     REFRESH MATERIALIZED VIEW CONCURRENTLY sales_report_materialized_view;
  5. END;
  6. $$ LANGUAGE plpgsql;
复制代码

六、最佳实践与性能优化

6.1 函数最佳实践

• 使用SQL函数进行简单的数据操作和转换
• 使用PL/pgSQL函数处理复杂逻辑、流程控制和错误处理
• 对于特定任务,考虑使用其他过程语言(如PL/Python用于数据分析)

在函数中避免在循环中执行查询,这会导致N+1查询问题。相反,使用批量操作:
  1. -- 不好的做法:在循环中查询
  2. CREATE OR REPLACE FUNCTION get_employee_departments_bad()
  3. RETURNS TEXT AS $$
  4. DECLARE
  5.     emp_rec RECORD;
  6.     dept_name TEXT;
  7.     result TEXT := '';
  8. BEGIN
  9.     FOR emp_rec IN SELECT employee_id, department_id FROM employees LOOP
  10.         SELECT department_name INTO dept_name
  11.         FROM departments
  12.         WHERE department_id = emp_rec.department_id;
  13.         
  14.         result := result || emp_rec.employee_id || ': ' || dept_name || E'\n';
  15.     END LOOP;
  16.    
  17.     RETURN result;
  18. END;
  19. $$ LANGUAGE plpgsql;
  20. -- 好的做法:使用JOIN批量获取数据
  21. CREATE OR REPLACE FUNCTION get_employee_departments_good()
  22. RETURNS TEXT AS $$
  23. DECLARE
  24.     result TEXT := '';
  25. BEGIN
  26.     SELECT INTO result
  27.         string_agg(employee_id || ': ' || department_name, E'\n' ORDER BY employee_id)
  28.     FROM
  29.         employees e
  30.     JOIN
  31.         departments d ON e.department_id = d.department_id;
  32.    
  33.     RETURN result;
  34. END;
  35. $$ LANGUAGE plpgsql;
复制代码
  1. CREATE OR REPLACE FUNCTION transfer_funds(
  2.     from_account INTEGER,
  3.     to_account INTEGER,
  4.     amount NUMERIC
  5. ) RETURNS VOID AS $$
  6. DECLARE
  7.     from_balance NUMERIC;
  8. BEGIN
  9.     -- 检查账户是否存在
  10.     IF NOT EXISTS (SELECT 1 FROM accounts WHERE account_id = from_account) THEN
  11.         RAISE EXCEPTION 'Source account % not found', from_account;
  12.     END IF;
  13.    
  14.     IF NOT EXISTS (SELECT 1 FROM accounts WHERE account_id = to_account) THEN
  15.         RAISE EXCEPTION 'Destination account % not found', to_account;
  16.     END IF;
  17.    
  18.     -- 检查余额是否足够
  19.     SELECT balance INTO from_balance
  20.     FROM accounts
  21.     WHERE account_id = from_account;
  22.    
  23.     IF from_balance < amount THEN
  24.         RAISE EXCEPTION 'Insufficient funds in account %. Balance: %, Required: %',
  25.                          from_account, from_balance, amount;
  26.     END IF;
  27.    
  28.     -- 执行转账
  29.     UPDATE accounts
  30.     SET balance = balance - amount
  31.     WHERE account_id = from_account;
  32.    
  33.     UPDATE accounts
  34.     SET balance = balance + amount
  35.     WHERE account_id = to_account;
  36.    
  37.     -- 记录交易
  38.     INSERT INTO transactions (from_account, to_account, amount, transaction_date)
  39.     VALUES (from_account, to_account, amount, NOW());
  40.    
  41. EXCEPTION
  42.     WHEN OTHERS THEN
  43.         -- 记录错误
  44.         INSERT INTO error_logs (error_time, error_message, error_details)
  45.         VALUES (NOW(), SQLERRM,
  46.                 'Transfer failed from ' || from_account || ' to ' || to_account ||
  47.                 ' for amount ' || amount);
  48.         
  49.         -- 重新抛出异常
  50.         RAISE;
  51. END;
  52. $$ LANGUAGE plpgsql;
复制代码

当函数需要返回多行数据时,使用SETOF或TABLE类型,而不是构建文本字符串:
  1. -- 不好的做法:返回格式化字符串
  2. CREATE OR REPLACE FUNCTION get_employees_by_department_bad(dept_id INTEGER)
  3. RETURNS TEXT AS $$
  4. DECLARE
  5.     result TEXT := '';
  6. BEGIN
  7.     SELECT INTO result
  8.         string_agg(employee_id || ', ' || employee_name || ', ' || email, E'\n' ORDER BY employee_id)
  9.     FROM
  10.         employees
  11.     WHERE
  12.         department_id = dept_id;
  13.    
  14.     RETURN result;
  15. END;
  16. $$ LANGUAGE plpgsql;
  17. -- 好的做法:返回表
  18. CREATE OR REPLACE FUNCTION get_employees_by_department_good(dept_id INTEGER)
  19. RETURNS TABLE(employee_id INTEGER, employee_name TEXT, email TEXT) AS $$
  20. BEGIN
  21.     RETURN QUERY
  22.     SELECT employee_id, employee_name, email
  23.     FROM employees
  24.     WHERE department_id = dept_id
  25.     ORDER BY employee_id;
  26. END;
  27. $$ LANGUAGE plpgsql;
复制代码

6.2 视图最佳实践

只包含应用程序需要的列,而不是使用SELECT *:
  1. -- 不好的做法:使用SELECT *
  2. CREATE VIEW employee_view_bad AS
  3. SELECT * FROM employees;
  4. -- 好的做法:明确指定列
  5. CREATE VIEW employee_view_good AS
  6. SELECT
  7.     employee_id,
  8.     employee_name,
  9.     email,
  10.     phone,
  11.     department_id
  12. FROM employees;
复制代码

避免创建基于其他视图的视图,这会导致性能问题:
  1. -- 不好的做法:创建基于其他视图的视图
  2. CREATE VIEW employee_department_view AS
  3. SELECT e.employee_id, e.employee_name, d.department_name
  4. FROM employees e
  5. JOIN departments d ON e.department_id = d.department_id;
  6. CREATE VIEW employee_project_view AS
  7. SELECT ed.employee_id, ed.employee_name, ed.department_name, p.project_name
  8. FROM employee_department_view ed
  9. JOIN employee_projects ep ON ed.employee_id = ep.employee_id
  10. JOIN projects p ON ep.project_id = p.project_id;
  11. -- 好的做法:直接基于表创建视图
  12. CREATE VIEW employee_project_view_good AS
  13. SELECT
  14.     e.employee_id,
  15.     e.employee_name,
  16.     d.department_name,
  17.     p.project_name
  18. FROM
  19.     employees e
  20. JOIN
  21.     departments d ON e.department_id = d.department_id
  22. JOIN
  23.     employee_projects ep ON e.employee_id = ep.employee_id
  24. JOIN
  25.     projects p ON ep.project_id = p.project_id;
复制代码

对于可更新视图,使用WITH CHECK OPTION确保通过视图插入或更新的数据满足视图条件:
  1. CREATE VIEW active_employees_view AS
  2. SELECT employee_id, employee_name, email, department_id, salary
  3. FROM employees
  4. WHERE status = 'active'
  5. WITH CHECK OPTION;
复制代码

对于复杂且不常变化的查询,使用物化视图:
  1. CREATE MATERIALIZED VIEW sales_summary_materialized_view AS
  2. SELECT
  3.     p.product_id,
  4.     p.product_name,
  5.     c.category_name,
  6.     COUNT(DISTINCT o.order_id) AS order_count,
  7.     SUM(oi.quantity) AS total_quantity,
  8.     SUM(oi.quantity * oi.unit_price) AS total_revenue
  9. FROM
  10.     products p
  11. JOIN
  12.     order_items oi ON p.product_id = oi.product_id
  13. JOIN
  14.     orders o ON oi.order_id = o.order_id
  15. JOIN
  16.     categories c ON p.category_id = c.category_id
  17. GROUP BY
  18.     p.product_id, p.product_name, c.category_name;
  19. -- 创建索引
  20. CREATE INDEX idx_sales_summary_product ON sales_summary_materialized_view(product_id);
  21. CREATE INDEX idx_sales_summary_category ON sales_summary_materialized_view(category_name);
复制代码

6.3 性能优化技巧

确保函数和视图使用的表有适当的索引:
  1. -- 为常用查询条件创建索引
  2. CREATE INDEX idx_employees_department_id ON employees(department_id);
  3. CREATE INDEX idx_orders_customer_id ON orders(customer_id);
  4. CREATE INDEX idx_orders_order_date ON orders(order_date);
  5. -- 为常用连接条件创建索引
  6. CREATE INDEX idx_order_items_product_id ON order_items(product_id);
  7. CREATE INDEX idx_order_items_order_id ON order_items(order_id);
复制代码

使用EXPLAIN ANALYZE分析函数和视图的查询性能:
  1. EXPLAIN ANALYZE SELECT * FROM employee_department_view WHERE department_id = 5;
复制代码

避免在视图中使用排序、限制和其他昂贵的操作,让调用者决定是否需要:
  1. -- 不好的做法:在视图中使用排序和限制
  2. CREATE VIEW top_employees_view AS
  3. SELECT employee_id, employee_name, salary
  4. FROM employees
  5. ORDER BY salary DESC
  6. LIMIT 10;
  7. -- 好的做法:让调用者决定排序和限制
  8. CREATE VIEW employee_salary_view AS
  9. SELECT employee_id, employee_name, salary
  10. FROM employees;
  11. -- 调用者可以按需排序和限制
  12. SELECT * FROM employee_salary_view ORDER BY salary DESC LIMIT 10;
复制代码

虽然PostgreSQL不支持参数化视图,但可以使用函数模拟:
  1. CREATE OR REPLACE FUNCTION get_employees_by_department(dept_id INTEGER)
  2. RETURNS TABLE(employee_id INTEGER, employee_name TEXT, email TEXT) AS $$
  3. BEGIN
  4.     RETURN QUERY
  5.     SELECT employee_id, employee_name, email
  6.     FROM employees
  7.     WHERE department_id = dept_id;
  8. END;
  9. $$ LANGUAGE plpgsql;
复制代码

七、总结

通过本教程,我们全面了解了PostgreSQL函数与视图的创建方法,从基础概念到高级应用,包括:

1. 函数基础:学习了如何创建SQL函数和PL/pgSQL函数,以及如何使用不同类型的参数和返回值。
2. 函数高级应用:探讨了触发器函数、聚合函数、窗口函数、表函数和安全定义者函数等高级主题。
3. 视图基础:掌握了如何创建基本视图、物化视图和可更新视图,以及如何修改和删除视图。
4. 视图高级应用:学习了如何使用视图进行权限管理、性能优化、数据分区和应用集成。
5. 综合实例:通过构建一个完整的报表系统,展示了如何结合函数和视图解决实际问题。
6. 最佳实践与性能优化:分享了函数和视图的最佳实践,以及如何优化性能。

函数和视图是PostgreSQL中非常强大的功能,它们可以帮助我们:

• 封装业务逻辑,提高代码重用性
• 简化复杂查询,提高开发效率
• 增强数据安全性,实现细粒度访问控制
• 优化查询性能,提高系统响应速度

通过掌握这些关键技巧,您可以显著提升PostgreSQL数据库开发效率,构建更强大、更高效的数据库应用程序。

希望本教程对您有所帮助,祝您在PostgreSQL数据库开发的道路上取得更大的成功!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

频道订阅

频道订阅

加入社群

加入社群

联系我们|TG频道|RSS

Powered by Pixtech

© 2025 Pixtech Team.