CodeGym /Courses /SQL SELF /Optimizing Procedures with Transactions: Performance Anal...

Optimizing Procedures with Transactions: Performance Analysis and Rollbacks

SQL SELF
Level 54 , Lesson 3
Available

When you write procedures, they often become the "heart" of your database, running a bunch of operations. But these same procedures can turn into a "bottleneck," especially if:

  1. They do unnecessary stuff (like hitting the same data over and over).
  2. They don't use indexes efficiently.
  3. They do way too many operations inside a single transaction.

Like one wise developer said: "Trying to speed up badly written code is like asking your lazy friend to run faster." So optimizing procedures isn't just about making things faster—it's about making the whole foundation better!

Minimizing the Number of Operations Inside a Transaction

Every transaction in PostgreSQL adds some overhead to manage its operations. The bigger the transaction, the longer it holds locks, and the more likely it is to block other users. To keep these effects in check:

  1. Don't cram too many operations into one transaction.
  2. Use EXCEPTION END to locally limit changes. This is handy if only part of the work needs to be rolled back.
  3. Break up big transactions into smaller ones (if your app's logic allows it).

Example: splitting a bulk data insert into "batches":

-- Example: Procedure for batch loading with step-by-step commits
CREATE PROCEDURE batch_load()
LANGUAGE plpgsql
AS $$
DECLARE
    r RECORD;
    batch_cnt INT := 0;
BEGIN
    FOR r IN SELECT * FROM staging_table LOOP
        BEGIN
            INSERT INTO target_table (col1, col2) VALUES (r.col1, r.col2);
            batch_cnt := batch_cnt + 1;
        EXCEPTION
            WHEN OTHERS THEN
                -- Logging the error; changes for this item will be rolled back
                INSERT INTO load_errors(msg) VALUES (SQLERRM);
        END;
        IF batch_cnt >= 1000 THEN
            COMMIT; -- commit every 1000 operations
            batch_cnt := 0;
        END IF;
    END LOOP;
    COMMIT; -- final commit
END;
$$;

Tip: don't forget that every COMMIT saves changes, so make sure splitting the transaction won't mess up your data integrity.

Using Indexes to Speed Up Queries

Let's say you have a table called orders with a million rows, and you often query by customer_id. Without an index, the query will scan every row:

CREATE INDEX idx_customer_id ON orders(customer_id);

Now queries like this:

SELECT * FROM orders WHERE customer_id = 42;

will run way faster, skipping the full table scan.

Important: When writing procedures, make sure the fields you use are indexed, especially in filter conditions, sorts, and joins.

Performance Analysis with EXPLAIN ANALYZE

EXPLAIN shows the query execution plan (how PostgreSQL plans to run it), and ANALYZE adds real execution stats (like how long it took). Here's a typical example:

EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 42;

How do you use it inside a procedure?

You can "break out" complex queries from your procedure and run them separately with EXPLAIN ANALYZE:

DO $$
BEGIN
    RAISE NOTICE 'Query Plan: %',
    (
        SELECT query_plan
        FROM pg_stat_statements
        WHERE query = 'SELECT * FROM orders WHERE customer_id = 42'
    );
END $$;

Example of Analysis and Improvement

Original procedure (slow):

CREATE OR REPLACE FUNCTION update_total_sales()
RETURNS VOID AS $$
BEGIN
    UPDATE sales
    SET total = (
        SELECT SUM(amount)
        FROM orders
        WHERE orders.sales_id = sales.id
    );
END $$ LANGUAGE plpgsql;

What's happening? For every row in the sales table, it runs a subquery SUM(amount), which means a ton of operations. That's slow.

Better version:

CREATE OR REPLACE FUNCTION update_total_sales()
RETURNS VOID AS $$
BEGIN
    UPDATE sales as s
    SET total = o.total_amount
    FROM (
        SELECT sales_id, SUM(amount) as total_amount
        FROM orders
        GROUP BY sales_id
    ) o
    WHERE o.sales_id = s.id;
END $$ LANGUAGE plpgsql;

Now the subquery with SUM runs just once, and all the data gets updated at once.

Rolling Back Data on Errors

If something goes wrong inside a procedure, you can roll back just part of the transaction. For example:

BEGIN
    -- Insert data
    INSERT INTO inventory(product_id, quantity) VALUES (1, -5);
EXCEPTION
    WHEN OTHERS THEN
        -- This block is like rolling back to an internal savepoint!
        RAISE WARNING 'Error updating data: %', SQLERRM;
END;

Practice: Building a Robust Order Processing Procedure

Let's say your task is to process an order. If something goes wrong (like not enough product in stock), the order is canceled and the error is logged.

CREATE OR REPLACE PROCEDURE process_order(p_order_id INT)
LANGUAGE plpgsql
AS $$
DECLARE
    v_in_stock INT;
BEGIN
    -- Check stock
    SELECT stock INTO v_in_stock FROM products WHERE id = p_order_id;

    BEGIN
        IF v_in_stock < 1 THEN
            RAISE EXCEPTION 'No product in stock';
        END IF;
        UPDATE products SET stock = stock - 1 WHERE id = p_order_id;
        -- ... other operations
    EXCEPTION
        WHEN OTHERS THEN
            -- All changes in this block are rolled back!
            INSERT INTO order_logs(order_id, log_message)
                VALUES (p_order_id, 'Order processing error: ' || SQLERRM);
            RAISE NOTICE 'Order processing error: %', SQLERRM;
    END;

    -- The rest of the code continues if there were no errors
    -- You can log: order processed successfully
END;
$$;
  • Even if there's an error, the order won't be processed, and a log will show up in the order_logs table.
  • If there's an error, the internal savepoint kicks in, so you don't lose all your context.

Key Rules for Optimizing and Making Procedures Robust

  1. Use indexes for queries inside procedures.
  2. Break big operations into small batches, process step by step.
  3. Be able to log errors—make a separate table for logging errors from bulk operations.
  4. For "partial" rollbacks, only use nested blocks with EXCEPTION.
  5. Don't use ROLLBACK TO SAVEPOINT inside PL/pgSQL—it'll throw a syntax error.
  6. In procedures, use COMMIT/SAVEPOINT only when called in autocommit mode!
  7. Analyze the execution plan of heavy queries (EXPLAIN ANALYZE) outside of procedures, before integrating them.
2
Task
SQL SELF, level 54, lesson 3
Locked
Splitting a Large Transaction into Batches
Splitting a Large Transaction into Batches
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION