PostgreSQL CREATE PROCEDURE
Introduction
Procedures (also known as stored procedures) are a powerful feature in PostgreSQL that allow you to encapsulate a series of SQL statements and operations into a single, reusable unit. Unlike functions, procedures don't necessarily return a value but can perform multiple database operations, including transactions.
PostgreSQL introduced the CREATE PROCEDURE
command in version 11, providing better compatibility with the SQL standard and other database systems. Procedures are particularly useful when you need to execute a series of statements that modify data, as they can contain transaction control commands (COMMIT
, ROLLBACK
) within their body.
Basic Syntax
The basic syntax for creating a procedure in PostgreSQL is:
CREATE [OR REPLACE] PROCEDURE procedure_name (parameter_name parameter_type [, ...])
LANGUAGE language_name
AS $$
-- procedure body
$$;
Let's break down the key components:
OR REPLACE
: Optional clause that allows you to update an existing procedure without dropping it firstprocedure_name
: Name of the procedureparameter_name parameter_type
: Input and output parameters for the procedurelanguage_name
: The language used in the procedure body (common options areplpgsql
,sql
)procedure body
: The code that will be executed when the procedure is called
Creating a Simple Procedure
Let's start with a simple example:
CREATE OR REPLACE PROCEDURE create_employee(
employee_name VARCHAR,
employee_salary NUMERIC
)
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO employees (name, salary, created_at)
VALUES (employee_name, employee_salary, NOW());
RAISE NOTICE 'Employee % successfully created', employee_name;
END;
$$;
This procedure inserts a new employee into the employees
table with the given name and salary, and sets the created_at
column to the current timestamp.
Calling a Procedure
You can call a procedure using the CALL
statement:
CALL create_employee('John Doe', 50000);
Output:
NOTICE: Employee John Doe successfully created
CALL
Parameter Modes
PostgreSQL procedures support three parameter modes:
IN
: Input parameters (default)OUT
: Output parametersINOUT
: Parameters that are both input and output
Let's create a procedure that demonstrates these different modes:
CREATE OR REPLACE PROCEDURE calculate_statistics(
IN min_salary NUMERIC,
OUT employee_count INTEGER,
OUT avg_salary NUMERIC,
INOUT max_salary NUMERIC
)
LANGUAGE plpgsql
AS $$
BEGIN
-- Get count of employees with salary >= min_salary
SELECT COUNT(*), AVG(salary), GREATEST(max_salary, MAX(salary))
INTO employee_count, avg_salary, max_salary
FROM employees
WHERE salary >= min_salary;
RAISE NOTICE 'Processed statistics for % employees', employee_count;
END;
$$;
You can call this procedure with:
DO $$
DECLARE
count INTEGER;
avg NUMERIC;
max_value NUMERIC := 0; -- Initial value for INOUT parameter
BEGIN
CALL calculate_statistics(40000, count, avg, max_value);
RAISE NOTICE 'Results: count=%, avg=%, max=%', count, avg, max_value;
END;
$$;
Output:
NOTICE: Processed statistics for 12 employees
NOTICE: Results: count=12, avg=52500.00, max=95000.00
Transactions Within Procedures
One of the main advantages of procedures over functions is the ability to manage transactions within the procedure body. This allows procedures to commit or rollback changes separately from the calling transaction.
Here's an example of a procedure that uses transaction control:
CREATE OR REPLACE PROCEDURE transfer_funds(
sender_id INTEGER,
receiver_id INTEGER,
amount NUMERIC
)
LANGUAGE plpgsql
AS $$
DECLARE
sender_balance NUMERIC;
BEGIN
-- Start a transaction
BEGIN
-- Check sender's balance
SELECT balance INTO sender_balance
FROM accounts
WHERE id = sender_id;
IF sender_balance < amount THEN
RAISE EXCEPTION 'Insufficient funds: balance is %', sender_balance;
END IF;
-- Update sender's account
UPDATE accounts
SET balance = balance - amount
WHERE id = sender_id;
-- Update receiver's account
UPDATE accounts
SET balance = balance + amount
WHERE id = receiver_id;
RAISE NOTICE 'Successfully transferred % from account % to account %',
amount, sender_id, receiver_id;
-- Commit the transaction
COMMIT;
EXCEPTION WHEN OTHERS THEN
-- Rollback in case of any error
ROLLBACK;
RAISE;
END;
END;
$$;
This procedure transfers funds between accounts and uses a transaction to ensure that both updates happen together or not at all.
Procedure vs. Function: Key Differences
Understanding when to use a procedure versus a function is important:
Key differences include:
- Procedures can include transaction control statements (
COMMIT
,ROLLBACK
) - Functions must return a value, procedures don't need to
- Procedures are called with
CALL
, functions are used in expressions - Procedures cannot be used in SQL queries, functions can
Best Practices
When creating procedures in PostgreSQL, keep these best practices in mind:
- Use meaningful names that describe what the procedure does
- Document your procedures with comments explaining their purpose and parameters
- Handle exceptions properly to avoid unexpected behavior
- Validate input parameters to prevent errors or security issues
- Use transactions appropriately to ensure data integrity
- Keep procedures focused on a single responsibility
- Consider security implications when granting execute permissions
Real-World Example: Order Processing System
Here's a more complex real-world example of a procedure for a basic order processing system:
CREATE OR REPLACE PROCEDURE process_order(
p_customer_id INTEGER,
p_items JSONB,
OUT p_order_id INTEGER
)
LANGUAGE plpgsql
AS $$
DECLARE
v_total_amount NUMERIC := 0;
v_item JSONB;
v_product_id INTEGER;
v_quantity INTEGER;
v_price NUMERIC;
v_inventory_count INTEGER;
BEGIN
-- Start transaction
BEGIN
-- Create order record
INSERT INTO orders (customer_id, order_date, status, total_amount)
VALUES (p_customer_id, NOW(), 'PENDING', 0)
RETURNING id INTO p_order_id;
-- Process each item in the order
FOR v_item IN SELECT * FROM jsonb_array_elements(p_items)
LOOP
v_product_id := (v_item->>'product_id')::INTEGER;
v_quantity := (v_item->>'quantity')::INTEGER;
-- Check inventory
SELECT price, inventory_count
INTO v_price, v_inventory_count
FROM products
WHERE id = v_product_id;
IF v_inventory_count < v_quantity THEN
RAISE EXCEPTION 'Insufficient inventory for product ID %', v_product_id;
END IF;
-- Create order item
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (p_order_id, v_product_id, v_quantity, v_price);
-- Update inventory
UPDATE products
SET inventory_count = inventory_count - v_quantity
WHERE id = v_product_id;
-- Add to total
v_total_amount := v_total_amount + (v_price * v_quantity);
END LOOP;
-- Update order total
UPDATE orders
SET total_amount = v_total_amount,
status = 'CONFIRMED'
WHERE id = p_order_id;
RAISE NOTICE 'Order % processed successfully. Total amount: %',
p_order_id, v_total_amount;
COMMIT;
EXCEPTION WHEN OTHERS THEN
ROLLBACK;
RAISE;
END;
END;
$$;
To call this procedure:
DO $$
DECLARE
order_id INTEGER;
items JSONB := '[
{"product_id": 101, "quantity": 2},
{"product_id": 203, "quantity": 1}
]';
BEGIN
CALL process_order(42, items, order_id);
RAISE NOTICE 'Created order with ID: %', order_id;
END;
$$;
Output:
NOTICE: Order 1256 processed successfully. Total amount: 159.97
NOTICE: Created order with ID: 1256
Managing Procedures
Listing All Procedures
You can list all procedures in your database using this query:
SELECT
n.nspname AS schema,
p.proname AS procedure_name,
pg_get_function_arguments(p.oid) AS parameters
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE p.prokind = 'p'
ORDER BY schema, procedure_name;
Dropping a Procedure
To remove a procedure:
DROP PROCEDURE [IF EXISTS] procedure_name [CASCADE | RESTRICT];
Example:
DROP PROCEDURE IF EXISTS create_employee CASCADE;
Summary
PostgreSQL procedures provide a powerful way to encapsulate business logic and perform complex operations on your database. They offer several advantages over functions when you need transaction control or need to perform multiple data modification operations.
Key points to remember:
- Procedures are created with the
CREATE PROCEDURE
command - They are called using the
CALL
statement - Procedures can contain transaction control statements
- They support IN, OUT, and INOUT parameters
- They're ideal for encapsulating data modification operations
Exercises
- Create a procedure to archive old records from a
users
table to ausers_archive
table based on a date parameter. - Develop a procedure that generates a report by aggregating data from multiple tables and stores the results in a report table.
- Create a procedure that implements a simple approval workflow, moving items through different status stages and recording the history of each change.
- Extend the transfer_funds procedure to include logging of all transactions in a separate audit table.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)