调用存储过程和函数

本章将详细介绍如何在PostgreSQL中调用存储过程和函数,包括不同的调用方式、参数传递和结果处理。

调用函数

基本调用语法

函数使用SELECT语句调用:

-- 调用无参数函数
SELECT function_name();

-- 调用带参数函数
SELECT function_name(param1, param2, param3);

-- 调用带命名参数的函数
SELECT function_name(param1 => value1, param2 => value2);

-- 在表达式中调用函数
SELECT column1, function_name(column2) FROM table_name;

-- 在WHERE子句中调用函数
SELECT * FROM table_name WHERE function_name(column1) = 'value';

调用简单函数

-- 假设已创建以下函数
CREATE OR REPLACE FUNCTION add_numbers(a INTEGER, b INTEGER)
RETURNS INTEGER AS $$
BEGIN
    RETURN a + b;
END;
$$ LANGUAGE plpgsql;

-- 调用函数
SELECT add_numbers(5, 3); -- 返回 8

-- 在查询中使用函数
SELECT 
    id,
    username,
    add_numbers(score1, score2) AS total_score
FROM student_scores;

调用返回表的函数

-- 假设已创建以下函数
CREATE OR REPLACE FUNCTION get_active_users()
RETURNS TABLE(
    user_id INTEGER,
    username VARCHAR(50),
    email VARCHAR(255)
) AS $$
BEGIN
    RETURN QUERY
    SELECT id, username, email
    FROM users
    WHERE is_active = true;
END;
$$ LANGUAGE plpgsql;

-- 调用返回表的函数
SELECT * FROM get_active_users();

-- 在FROM子句中使用函数
SELECT 
    user_id,
    username,
    email
FROM get_active_users()
WHERE username LIKE 'j%';

调用返回集合的函数

-- 假设已创建以下函数
CREATE OR REPLACE FUNCTION get_user_orders(user_id INTEGER)
RETURNS SETOF orders AS $$
BEGIN
    RETURN QUERY
    SELECT * FROM orders WHERE orders.user_id = get_user_orders.user_id;
END;
$$ LANGUAGE plpgsql;

-- 调用返回集合的函数
SELECT * FROM get_user_orders(1);

-- 与JOIN一起使用
SELECT 
    u.username,
    o.order_date,
    o.total_amount
FROM users u
JOIN get_user_orders(u.id) o ON true
WHERE u.id = 1;

调用存储过程

基本调用语法

存储过程使用CALL语句调用(PostgreSQL 11+):

-- 调用无参数存储过程
CALL procedure_name();

-- 调用带参数存储过程
CALL procedure_name(param1, param2, param3);

-- 调用带命名参数的存储过程
CALL procedure_name(param1 => value1, param2 => value2);

调用简单存储过程

-- 假设已创建以下存储过程
CREATE OR REPLACE PROCEDURE create_user(
    p_username VARCHAR(50),
    p_email VARCHAR(255),
    p_first_name VARCHAR(50),
    p_last_name VARCHAR(50)
)
LANGUAGE plpgsql
AS $$
BEGIN
    INSERT INTO users (username, email, first_name, last_name, created_at)
    VALUES (p_username, p_email, p_first_name, p_last_name, CURRENT_TIMESTAMP);
    
    COMMIT;
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        RAISE;
END;
$$;

-- 调用存储过程
CALL create_user('newuser', 'newuser@example.com', 'New', 'User');

调用带INOUT参数的存储过程

-- 假设已创建以下存储过程
CREATE OR REPLACE PROCEDURE update_user_stats(
    p_user_id INTEGER,
    INOUT p_order_count INTEGER DEFAULT 0,
    INOUT p_total_spent NUMERIC(12,2) DEFAULT 0.00
)
LANGUAGE plpgsql
AS $$
BEGIN
    SELECT COUNT(*), COALESCE(SUM(total_amount), 0)
    INTO p_order_count, p_total_spent
    FROM orders
    WHERE user_id = p_user_id;
END;
$$;

-- 调用带INOUT参数的存储过程
CALL update_user_stats(1, 0, 0.00);

-- 使用匿名代码块调用并获取结果
DO $$
DECLARE
    order_count INTEGER := 0;
    total_spent NUMERIC(12,2) := 0.00;
BEGIN
    CALL update_user_stats(1, order_count, total_spent);
    RAISE NOTICE 'User 1 has % orders totaling %', order_count, total_spent;
END;
$$;

参数传递方式

位置参数

-- 按位置传递参数
SELECT calculate_discount(100.00, 20.00);

-- 等同于
CALL create_user('john_doe', 'john@example.com', 'John', 'Doe');

命名参数

-- 使用命名参数传递
SELECT calculate_discount(price => 100.00, discount_percent => 20.00);

-- 混合使用位置参数和命名参数(命名参数必须在位置参数之后)
SELECT calculate_discount(100.00, discount_percent => 20.00);

-- 存储过程的命名参数
CALL create_user(
    p_username => 'jane_doe',
    p_email => 'jane@example.com',
    p_first_name => 'Jane',
    p_last_name => 'Doe'
);

默认参数

-- 假设已创建带默认参数的函数
CREATE OR REPLACE FUNCTION calculate_discount(
    price NUMERIC(10,2), 
    discount_percent NUMERIC(5,2) DEFAULT 10.00
)
RETURNS NUMERIC(10,2) AS $$
BEGIN
    RETURN price * (1 - discount_percent / 100);
END;
$$ LANGUAGE plpgsql;

-- 使用默认参数
SELECT calculate_discount(100.00); -- discount_percent使用默认值10.00

-- 覆盖默认参数
SELECT calculate_discount(100.00, 20.00); -- 指定discount_percent为20.00

结果处理

处理函数返回值

-- 将函数结果存储在变量中(在PL/pgSQL块中)
DO $$
DECLARE
    result INTEGER;
BEGIN
    SELECT add_numbers(5, 3) INTO result;
    RAISE NOTICE 'Result: %', result;
END;
$$;

-- 在INSERT语句中使用函数
INSERT INTO calculation_results (value1, value2, sum_result)
SELECT 5, 3, add_numbers(5, 3);

-- 在UPDATE语句中使用函数
UPDATE products 
SET discounted_price = calculate_discount(price, 15.00)
WHERE category = 'Electronics';

处理存储过程结果

-- 使用匿名代码块处理存储过程结果
DO $$
DECLARE
    order_count INTEGER := 0;
    total_spent NUMERIC(12,2) := 0.00;
BEGIN
    CALL update_user_stats(1, order_count, total_spent);
    
    IF order_count > 10 THEN
        RAISE NOTICE 'VIP user with % orders totaling %', order_count, total_spent;
    ELSE
        RAISE NOTICE 'Regular user with % orders totaling %', order_count, total_spent;
    END IF;
END;
$$;

实际应用场景

批量操作

-- 创建批量处理存储过程
CREATE OR REPLACE PROCEDURE batch_process_orders()
LANGUAGE plpgsql
AS $$
DECLARE
    order_record RECORD;
    processed_count INTEGER := 0;
BEGIN
    -- 处理待处理订单
    FOR order_record IN 
        SELECT id, user_id, total_amount 
        FROM orders 
        WHERE status = 'pending'
    LOOP
        -- 更新订单状态
        UPDATE orders 
        SET status = 'processed', processed_at = CURRENT_TIMESTAMP
        WHERE id = order_record.id;
        
        -- 更新用户统计
        UPDATE user_statistics 
        SET total_orders = total_orders + 1,
            total_spent = total_spent + order_record.total_amount
        WHERE user_id = order_record.user_id;
        
        processed_count := processed_count + 1;
    END LOOP;
    
    COMMIT;
    RAISE NOTICE 'Processed % orders', processed_count;
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        RAISE;
END;
$$;

-- 调用批量处理存储过程
CALL batch_process_orders();

数据迁移

-- 创建数据迁移存储过程
CREATE OR REPLACE PROCEDURE migrate_user_data()
LANGUAGE plpgsql
AS $$
DECLARE
    user_record RECORD;
    migrated_count INTEGER := 0;
BEGIN
    -- 迁移用户数据
    FOR user_record IN 
        SELECT id, old_username, old_email, created_date
        FROM old_users 
        WHERE migrated = FALSE
    LOOP
        -- 插入到新表
        INSERT INTO new_users (username, email, created_at, source_system)
        VALUES (
            user_record.old_username,
            user_record.old_email,
            user_record.created_date,
            'legacy_system'
        );
        
        -- 标记为已迁移
        UPDATE old_users 
        SET migrated = TRUE, migrated_at = CURRENT_TIMESTAMP
        WHERE id = user_record.id;
        
        migrated_count := migrated_count + 1;
    END LOOP;
    
    COMMIT;
    RAISE NOTICE 'Migrated % users', migrated_count;
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        RAISE;
END;
$$;

-- 调用数据迁移存储过程
CALL migrate_user_data();

报告生成

-- 创建返回报告数据的函数
CREATE OR REPLACE FUNCTION generate_sales_report(start_date DATE, end_date DATE)
RETURNS TABLE(
    period TEXT,
    total_orders BIGINT,
    total_sales NUMERIC(15,2),
    avg_order_value NUMERIC(10,2),
    top_product VARCHAR(100)
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        CONCAT(start_date, ' to ', end_date) AS period,
        COUNT(*) AS total_orders,
        SUM(total_amount) AS total_sales,
        ROUND(AVG(total_amount), 2) AS avg_order_value,
        (SELECT product_name 
         FROM order_items oi 
         JOIN products p ON oi.product_id = p.id
         WHERE oi.order_id IN (
             SELECT id FROM orders 
             WHERE order_date BETWEEN start_date AND end_date
         )
         GROUP BY product_name 
         ORDER BY SUM(quantity) DESC 
         LIMIT 1) AS top_product
    FROM orders
    WHERE order_date BETWEEN start_date AND end_date;
END;
$$ LANGUAGE plpgsql;

-- 调用报告函数
SELECT * FROM generate_sales_report('2023-10-01', '2023-10-31');

错误处理

处理函数异常

-- 在匿名代码块中处理函数异常
DO $$
DECLARE
    result NUMERIC;
BEGIN
    BEGIN
        SELECT safe_divide(10, 0) INTO result;
        RAISE NOTICE 'Result: %', result;
    EXCEPTION
        WHEN OTHERS THEN
            RAISE NOTICE 'Function call failed: %', SQLERRM;
    END;
END;
$$;

处理存储过程异常

-- 在匿名代码块中处理存储过程异常
DO $$
BEGIN
    BEGIN
        CALL create_user(NULL, NULL, NULL, NULL);
    EXCEPTION
        WHEN OTHERS THEN
            RAISE NOTICE 'Procedure call failed: %', SQLERRM;
    END;
END;
$$;

性能考虑

调用优化

-- 避免在循环中重复调用函数
-- 不好的做法
SELECT 
    id,
    username,
    (SELECT COUNT(*) FROM orders WHERE user_id = users.id) AS order_count
FROM users;

-- 好的做法
SELECT 
    u.id,
    u.username,
    COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.username;

批量调用

-- 使用数组参数进行批量操作
CREATE OR REPLACE FUNCTION process_user_batch(user_ids INTEGER[])
RETURNS INTEGER AS $$
DECLARE
    user_id INTEGER;
    processed_count INTEGER := 0;
BEGIN
    FOREACH user_id IN ARRAY user_ids LOOP
        -- 处理每个用户
        UPDATE users 
        SET last_processed = CURRENT_TIMESTAMP
        WHERE id = user_id;
        
        processed_count := processed_count + 1;
    END LOOP;
    
    RETURN processed_count;
END;
$$ LANGUAGE plpgsql;

-- 调用批量处理函数
SELECT process_user_batch(ARRAY[1, 2, 3, 4, 5]);

综合示例

-- 创建完整的订单处理系统
CREATE OR REPLACE FUNCTION process_order_with_notification(
    p_user_id INTEGER,
    p_products JSONB
)
RETURNS TABLE(
    order_id INTEGER,
    status VARCHAR(50),
    message TEXT
) AS $$
DECLARE
    v_order_id INTEGER;
    v_error_message TEXT;
BEGIN
    -- 调用订单处理函数
    BEGIN
        SELECT process_order(p_user_id, p_products) INTO v_order_id;
        
        RETURN QUERY SELECT 
            v_order_id AS order_id,
            'SUCCESS'::VARCHAR(50) AS status,
            'Order processed successfully'::TEXT AS message;
            
    EXCEPTION
        WHEN OTHERS THEN
            v_error_message := SQLERRM;
            
            RETURN QUERY SELECT 
                NULL::INTEGER AS order_id,
                'ERROR'::VARCHAR(50) AS status,
                ('Order processing failed: ' || v_error_message)::TEXT AS message;
    END;
END;
$$ LANGUAGE plpgsql;

-- 创建通知发送存储过程
CREATE OR REPLACE PROCEDURE send_order_notification(
    p_order_id INTEGER,
    p_user_id INTEGER,
    p_notification_type VARCHAR(20) DEFAULT 'email'
)
LANGUAGE plpgsql
AS $$
DECLARE
    user_email VARCHAR(255);
    order_total NUMERIC(10,2);
BEGIN
    -- 获取用户邮箱和订单总额
    SELECT u.email, o.total_amount 
    INTO user_email, order_total
    FROM users u
    JOIN orders o ON u.id = o.user_id
    WHERE u.id = p_user_id AND o.id = p_order_id;
    
    -- 根据通知类型发送通知
    CASE p_notification_type
        WHEN 'email' THEN
            INSERT INTO email_queue (recipient, subject, body, created_at)
            VALUES (
                user_email,
                'Order Confirmation',
                'Your order #' || p_order_id || ' for $' || order_total || ' has been confirmed.',
                CURRENT_TIMESTAMP
            );
        WHEN 'sms' THEN
            INSERT INTO sms_queue (recipient, message, created_at)
            VALUES (
                user_email,
                'Order #' || p_order_id || ' confirmed. Total: $' || order_total,
                CURRENT_TIMESTAMP
            );
        ELSE
            RAISE NOTICE 'Unknown notification type: %', p_notification_type;
    END CASE;
    
    COMMIT;
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        RAISE;
END;
$$;

-- 完整的订单处理流程
CREATE OR REPLACE PROCEDURE complete_order_process(
    p_user_id INTEGER,
    p_products JSONB
)
LANGUAGE plpgsql
AS $$
DECLARE
    order_result RECORD;
BEGIN
    -- 处理订单
    SELECT * FROM process_order_with_notification(p_user_id, p_products) INTO order_result;
    
    IF order_result.status = 'SUCCESS' THEN
        -- 发送通知
        CALL send_order_notification(order_result.order_id, p_user_id, 'email');
        CALL send_order_notification(order_result.order_id, p_user_id, 'sms');
        
        RAISE NOTICE 'Order % processed successfully and notifications sent', order_result.order_id;
    ELSE
        RAISE EXCEPTION 'Order processing failed: %', order_result.message;
    END IF;
    
EXCEPTION
    WHEN OTHERS THEN
        RAISE;
END;
$$;

-- 调用完整的订单处理流程
CALL complete_order_process(
    1, 
    '[{"product_id": 1, "quantity": 2}, {"product_id": 2, "quantity": 1}]'
);

提示:在调用存储过程和函数时,要注意参数的类型匹配和数量。使用命名参数可以提高代码的可读性和维护性。对于复杂的业务逻辑,建议将功能分解为多个小函数,然后通过存储过程协调调用。