Writing Portable Stored Procedures: PL/pgSQL, T-SQL, PL/SQL
AI generated
SELECT
JOIN
SQL · Stored Procedures · PL/pgSQL · T-SQL · PL/SQL
Writing Portable Stored Procedures
PL/pgSQL, T-SQL, and PL/SQL compared

Stored procedures are among the least portable building blocks of SQL, because every major database system brings its own, incompatible procedural language. This article compares PL/pgSQL, T-SQL, and PL/SQL and shows concrete strategies to keep business logic in stored procedures migratable regardless.

19 min read PL/pgSQL · T-SQL · PL/SQL · SQL/PSM PostgreSQL · SQL Server · Oracle

1. Why stored procedures are barely portable

While declarative SQL such as SELECT and JOIN is at least partly standardized, that barely holds for stored procedures. The ANSI SQL standard does define a procedural extension with SQL/PSM (Persistent Stored Modules), but none of the major commercial systems adheres to it strictly. PostgreSQL uses PL/pgSQL, Microsoft SQL Server uses Transact-SQL (T-SQL), and Oracle uses PL/SQL, three languages that differ substantially in syntax, error handling, and language scope, even though they pursue similar concepts.

This lack of standardization in stored procedures is no accident, but a direct consequence of history: each system developed its procedural language at a time when no practical standard existed, and has since had to maintain backward compatibility with millions of lines of production code. Anyone writing business logic in stored procedures who later needs to switch databases faces one of the most expensive migration tasks in the entire database world. This article shows exactly where the three languages diverge and how to structure stored procedures so that a later migration remains realistic anyway.

2. Basic structure: CREATE PROCEDURE compared

Even the basic structure of a stored procedure differs noticeably between the three systems. PostgreSQL defines a function or procedure with CREATE FUNCTION or CREATE PROCEDURE, followed by a $$ quoted block containing the PL/pgSQL code and closed with LANGUAGE plpgsql. SQL Server uses CREATE PROCEDURE without dollar quoting, instead with the keyword AS before the code block and BEGIN...END as the block delimiter. Oracle structures stored procedures with CREATE OR REPLACE PROCEDURE using an explicit IS or AS section before declaring local variables and BEGIN...END for the execution block.

These structural differences in stored procedures seem cosmetic at first glance, but add up to considerable manual effort during a migration, since practically every single line needs syntax adjustments. An automated search and replace almost never works reliably here, because the three languages maintain different comment syntax, different termination characters, and different case conventions for keywords.


-- PostgreSQL: PL/pgSQL, dollar-quoted body
CREATE OR REPLACE PROCEDURE update_stock(p_product_id INT, p_quantity INT)
LANGUAGE plpgsql
AS $$
BEGIN
    UPDATE products
    SET stock = stock - p_quantity
    WHERE id = p_product_id;
END;
$$;

-- SQL Server: T-SQL
CREATE PROCEDURE UpdateStock
    @ProductId INT,
    @Quantity INT
AS
BEGIN
    UPDATE Products
    SET Stock = Stock - @Quantity
    WHERE Id = @ProductId;
END;

-- Oracle: PL/SQL
CREATE OR REPLACE PROCEDURE update_stock(
    p_product_id IN NUMBER,
    p_quantity IN NUMBER
) IS
BEGIN
    UPDATE products
    SET stock = stock - p_quantity
    WHERE id = p_product_id;
END update_stock;
/

3. Variable declaration and assignment

The three procedural languages also go their own way with variables. PL/pgSQL declares variables in a separate DECLARE section before the BEGIN block, with the type after the variable name, similar to standard SQL column definitions. T-SQL uses the @ prefix for all variable names and declares them directly in the code with DECLARE @variable INT, without forcing a separate declaration block. PL/SQL follows a similar structure to PL/pgSQL with a declaration section before BEGIN, but consistently uses the assignment operator :=, while T-SQL allows both SET @var = value and SELECT @var = value.

These differences in stored procedures seem trivial but are a common source of bugs in manual porting, because developers habitually carry over the source language's syntax into the target language. A PL/pgSQL developer trying to write T-SQL easily forgets the @ prefix, while a T-SQL developer writing PL/pgSQL often forgets the assignment operator := instead of =, which produces a subtle, hard to find syntax error in PostgreSQL.


-- PostgreSQL: PL/pgSQL variable declaration
CREATE OR REPLACE FUNCTION calculate_discount(p_total NUMERIC)
RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
DECLARE
    v_discount_rate NUMERIC := 0.1;
    v_discount_amount NUMERIC;
BEGIN
    v_discount_amount := p_total * v_discount_rate;
    RETURN v_discount_amount;
END;
$$;

-- SQL Server: T-SQL, @ prefix, no separate declaration block
CREATE FUNCTION CalculateDiscount(@Total DECIMAL(10,2))
RETURNS DECIMAL(10,2)
AS
BEGIN
    DECLARE @DiscountRate DECIMAL(4,2) = 0.1;
    DECLARE @DiscountAmount DECIMAL(10,2);
    SET @DiscountAmount = @Total * @DiscountRate;
    RETURN @DiscountAmount;
END;

4. Control flow: conditions and loops

Conditional branches look similar at first glance in all three languages but differ in detail. PL/pgSQL uses IF ... THEN ... ELSIF ... ELSE ... END IF, PL/SQL follows almost identical syntax with ELSIF instead of ELSE IF, while T-SQL uses the plainer IF ... BEGIN ... END ELSE BEGIN ... END without a dedicated keyword for ELSIF, which quickly becomes hard to read with many nested conditions.

Loops show the greatest variety: PL/pgSQL offers LOOP, WHILE, and FOR loops with EXIT WHEN for breaking out. PL/SQL has very similar constructs, supplemented by FORALL for high performance bulk operations, a feature neither PL/pgSQL nor T-SQL know in this form. T-SQL traditionally limits itself to WHILE loops, since classic FOR loops are missing, which encourages set based thinking but leads to more cumbersome code than in the other two languages for row by row processing.


-- PostgreSQL: PL/pgSQL FOR loop over a query result
CREATE OR REPLACE PROCEDURE archive_old_orders()
LANGUAGE plpgsql
AS $$
DECLARE
    r RECORD;
BEGIN
    FOR r IN SELECT id FROM orders WHERE created_at < NOW() - INTERVAL '1 year'
    LOOP
        INSERT INTO orders_archive SELECT * FROM orders WHERE id = r.id;
        DELETE FROM orders WHERE id = r.id;
    END LOOP;
END;
$$;

-- SQL Server: T-SQL WHILE loop with a cursor-based approach
CREATE PROCEDURE ArchiveOldOrders
AS
BEGIN
    DECLARE @OrderId INT;
    DECLARE order_cursor CURSOR FOR
        SELECT Id FROM Orders WHERE CreatedAt < DATEADD(year, -1, GETDATE());

    OPEN order_cursor;
    FETCH NEXT FROM order_cursor INTO @OrderId;

    WHILE @@FETCH_STATUS = 0
    BEGIN
        INSERT INTO OrdersArchive SELECT * FROM Orders WHERE Id = @OrderId;
        DELETE FROM Orders WHERE Id = @OrderId;
        FETCH NEXT FROM order_cursor INTO @OrderId;
    END;

    CLOSE order_cursor;
    DEALLOCATE order_cursor;
END;

5. Error handling: EXCEPTION, TRY/CATCH, EXCEPTION block

Error handling is one of the areas where stored procedures diverge the most in philosophy between systems. PL/pgSQL uses an EXCEPTION block inside the BEGIN...END construct, which can catch specific error conditions such as WHEN unique_violation. T-SQL has followed a TRY...CATCH model since SQL Server 2005, structurally reminiscent of programming languages like C#, working with ERROR_MESSAGE(), ERROR_NUMBER(), and similar functions inside the CATCH block.

PL/SQL also uses an EXCEPTION block, but with predefined names such as NO_DATA_FOUND or TOO_MANY_ROWS for common error cases, supplemented by custom exceptions via RAISE_APPLICATION_ERROR. These three different error handling models cannot be mechanically translated into each other, because the available error codes, the granularity of error handling, and even the concept of nested transactions on error differ between systems.


-- PostgreSQL: EXCEPTION block within PL/pgSQL
CREATE OR REPLACE PROCEDURE safe_insert_customer(p_email TEXT)
LANGUAGE plpgsql
AS $$
BEGIN
    INSERT INTO customers (email) VALUES (p_email);
EXCEPTION
    WHEN unique_violation THEN
        RAISE NOTICE 'Customer with email % already exists', p_email;
END;
$$;

-- SQL Server: TRY/CATCH block in T-SQL
CREATE PROCEDURE SafeInsertCustomer @Email VARCHAR(255)
AS
BEGIN
    BEGIN TRY
        INSERT INTO Customers (Email) VALUES (@Email);
    END TRY
    BEGIN CATCH
        PRINT 'Error: ' + ERROR_MESSAGE();
    END CATCH
END;

6. Processing cursors and result sets

All three languages support cursors for row by row processing of result sets, but the explicit handling differs. PL/pgSQL mostly encapsulates cursor logic implicitly via FOR record IN query LOOP, which makes the explicit OPEN/FETCH/CLOSE cycle unnecessary for most use cases. T-SQL traditionally requires the explicit cycle with DECLARE CURSOR, OPEN, FETCH NEXT inside a WHILE loop, and a final CLOSE and DEALLOCATE, producing considerably more boilerplate.

PL/SQL offers both variants: implicit cursors via FOR record IN (SELECT ...) LOOP, similar to PL/pgSQL, and explicit cursors with CURSOR cursor_name IS SELECT ... for cases where more control is needed, such as parameterized cursors or bulk fetch operations with BULK COLLECT. For migrations that means: T-SQL cursor code is usually the most expensive part of a port, because it can almost always be simplified to a much more compact implicit loop in PL/pgSQL, but that simplification requires manual analysis rather than automatic translation.

7. Return values and OUT parameters

Stored procedures also differ fundamentally in return values. PostgreSQL conceptually separates functions, which always return a value via RETURN, from procedures, which instead use OUT or INOUT parameters for return values, since PostgreSQL 11 also with a genuine CALL invocation instead of SELECT. T-SQL has OUTPUT parameters directly in the procedure signature and additionally an implicit integer return value via RETURN, traditionally used for status codes, not business data.

PL/SQL distinguishes between functions (always with a RETURN value) and procedures (with IN, OUT, and IN OUT parameters), similar to PostgreSQL, but additionally allows package based organization of multiple related procedures and functions in a named namespace, a concept neither PostgreSQL nor SQL Server know in this form. This package structure is one of the reasons large PL/SQL codebases are particularly expensive to break apart during a migration, since package boundaries rarely align cleanly with modular boundaries in other systems.

8. Strategies for migratable stored procedures

Given the substantial differences between PL/pgSQL, T-SQL, and PL/SQL, a deliberate architecture decision pays off before business logic is written into stored procedures. The most important principle: stored procedures should limit themselves to operations that genuinely benefit from proximity to the data, such as bulk updates, complex set based aggregations, or operations that save many round trips to the application. Pure business rules that would also work in the application layer are better kept there, since application code is generally considerably more portable between database systems than procedural SQL.

Where stored procedures are unavoidable, a consistent separation between simple, mechanically translatable constructs (variable declaration, simple conditions, standard SELECT/INSERT/UPDATE) and complex, language specific constructs (cursors, exception handling, package structures) helps. Anyone who deliberately keeps the complex parts small and well documented reduces migration effort considerably, even though full portability between PL/pgSQL, T-SQL, and PL/SQL will realistically never be achievable.

9. PL/pgSQL, T-SQL, and PL/SQL side by side

The following table summarizes the key structural differences between the three procedural languages.

Aspect PL/pgSQL T-SQL PL/SQL
Variable prefix No prefix @variable No prefix
Error handling EXCEPTION WHEN TRY...CATCH EXCEPTION WHEN
Implicit cursors Yes, via FOR loop No, always explicit Yes, via FOR loop
Namespace organization Schemas Schemas Packages (unique)

This comparison shows why stored procedures almost always need to be rewritten rather than translated, even for a similar business task. Migration effort depends heavily on how many language specific features like package structures, FORALL bulk operations, or explicit cursors were actually used.

Mironsoft

Stored procedure migration and procedural SQL consulting

Migrating PL/SQL, T-SQL, or PL/pgSQL?

We analyze existing stored procedures, identify language specific risk areas such as cursors and package structures, and develop a realistic migration strategy between PL/pgSQL, T-SQL, and PL/SQL.

Code Analysis

Inventory of all stored procedures and their language specific constructs

Porting

Manual translation of complex logic between PL/pgSQL, T-SQL, and PL/SQL

Refactoring Consulting

Moving suitable business logic into the more portable application layer

10. Summary

Stored procedures are one of the least portable layers of SQL, because PL/pgSQL, T-SQL, and PL/SQL differ substantially from each other in syntax, error handling, cursor processing, and namespace organization despite similar underlying concepts. The SQL/PSM standard exists on paper, but is not strictly implemented by any of the three systems, so any migration between these languages remains at its core a manual rewrite, not a mechanical translation.

Anyone who deliberately limits stored procedures to operations that genuinely benefit from database proximity, and avoids complex, language specific constructs such as package structures or explicit cursors where possible, reduces migration effort considerably. The most pragmatic strategy remains to keep business logic primarily in the application layer and use stored procedures deliberately for performance critical, data proximate operations, with clear documentation of which language features were used in each case.

Writing Portable Stored Procedures: The Key Takeaways

No real standardization

SQL/PSM exists, but is not strictly implemented by PostgreSQL, SQL Server, or Oracle.

Biggest differences

Error handling, cursor handling, and package structures are the least compatible.

Migration strategy

Translate simple constructs mechanically, rewrite complex parts manually.

Pragmatic advice

Keep business logic primarily in the application layer, use stored procedures only for data proximate operations.

11. FAQ: Writing Portable Stored Procedures

1Can PL/pgSQL be automatically translated to T-SQL?
Partly, but cursors and exception handling almost always require manual rework.
2What is SQL/PSM?
The ANSI standard for procedural extensions, not strictly implemented by any major system.
3Biggest difference T-SQL vs. PL/pgSQL?
Explicit vs. implicit cursors, and the @ prefix for variables in T-SQL.
4What are PL/SQL packages?
Namespaces for procedures and functions, PostgreSQL has no direct equivalent.
5Stored procedures or application logic?
Usually the application, stored procedures only for genuine performance benefits.
6How does error handling differ?
EXCEPTION block in PL/pgSQL and PL/SQL, TRY/CATCH in T-SQL, similar concepts, not directly transferable.
7What is FORALL in PL/SQL?
Bulk DML in one context switch, no direct equivalent in PL/pgSQL or T-SQL.
8How expensive is a typical migration?
From days for simple procedures to months for extensive package structures.
9Are there migration tools?
Yes, like ora2pg, but rarely deliver fully runnable code without follow up.
10Are stored procedures worth it for new projects?
Yes for performance critical operations, usually not for general logic.