Procedures vs. functions in PostgreSQL

PostgreSQL's CREATE PROCEDURE syntax closely mirrors CREATE FUNCTION, but the two constructs behave differently in one important respect: transaction control. A procedure can commit or roll back transactions inside its body; a function cannot.

The basic command structure is:

PgSQL

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

blog=# h CREATE PROCEDURE

Command: CREATE PROCEDURE

Description: define a new procedure

Syntax:

CREATE [ OR REPLACE ] PROCEDURE

    name ( [ [ argmode ] [ argname ] argtype [ { DEFAULT | = } default_expr ] [, ...] ] )

  { LANGUAGE lang_name

    | TRANSFORM { FOR TYPE type_name } [, ... ]

    | [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER

    | SET configuration_parameter { TO value | = value | FROM CURRENT }

    | AS 'definition'

    | AS 'obj_file', 'link_symbol'

  } ...

URL: https://www.postgresql.org/docs/15/sql-createprocedure.html

A simple procedure that inserts data shows the main difference at invocation time. Procedures are run with CALL, not embedded in a SELECT statement:

PgSQL

1

2

3

4

5

6

7

8

9

CREATE TABLE IF NOT EXISTS t_demo (id int);

CREATE OR REPLACE PROCEDURE sample_1(x int)

LANGUAGE SQL

AS $

INSERT INTO t_demo VALUES (x);

$;

CALL sample_1(1000);

You can write procedure bodies in PL/pgSQL just as you would for a function. The key distinction is visible here — a COMMIT appears in the middle of the code:

PgSQL

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

CREATE OR REPLACE PROCEDURE sample_2()

LANGUAGE plpgsql

AS $

DECLARE

  v_sum int8;

BEGIN

  INSERT INTO t_demo VALUES (1);

  INSERT INTO t_demo VALUES (2);

  COMMIT;

  INSERT INTO t_demo VALUES (3);

  INSERT INTO t_demo VALUES (4);

  SELECT sum(id) FROM t_demo WHERE id < 5 INTO v_sum;

  RAISE NOTICE 'debug info: %', v_sum;

END;

$;

Transaction boundaries

In a normal server-side function, all code runs inside the parent transaction. The function is part of the calling statement, so it cannot control when that transaction ends. You cannot, for example, commit after processing the first million rows and then continue:

PgSQL

1

SELECT func(x) FROM tab;

A procedure is never called inside a SELECT; it is invoked with CALL, which allows it to manage its own transactions:

PgSQL

1

2

3

4

5

6

7

8

9

10

11

12

demo=# CALL sample_2();

NOTICE: debug info: 10

CALL

demo=# SELECT * FROM t_demo;

id

------

1000

1

2

3

4

(5 rows)

That means rows written before a COMMIT are in a different transaction than rows written after it. The hidden xmin column, which records the transaction ID that wrote each row, confirms this:

PgSQL

1

2

3

4

5

6

7

8

9

demo=# SELECT xmin, * FROM t_demo;

xmin    | id

--------+------

3060775 | 1000

3060780 | 1

3060780 | 2

3060781 | 3

3060781 | 4

(5 rows)

After a COMMIT inside a procedure, a new transaction starts automatically — there is no explicit BEGIN or START TRANSACTION required. The behavior is analogous to COMMIT AND CHAIN.

Because procedures can span multiple transactions, some restrictions apply. You cannot run a procedure that commits internally inside an explicit transaction block:

PgSQL

1

2

3

4

5

6

7

8

9

demo=# BEGIN;

BEGIN

demo=*# CALL sample_2();

ERROR: invalid transaction termination

CONTEXT: PL/pgSQL function sample_2() line 7 at COMMIT

demo=!# ROLLBACK;

ROLLBACK

If the procedure does not manage transactions itself, however, you can call it from within a transaction block:

PgSQL

1

2

3

4

5

6

7

8

9

10

11

12

CREATE OR REPLACE PROCEDURE sample_3()

LANGUAGE plpgsql

AS $

DECLARE

  v_sum int8;

BEGIN

  INSERT INTO t_demo VALUES (1);

  INSERT INTO t_demo VALUES (2);

  INSERT INTO t_demo VALUES (3);

  INSERT INTO t_demo VALUES (4);

END;

$;

That allows the following to work:

PgSQL

1

2

3

4

5

6

7

8

demo=# BEGIN;

BEGIN

demo=*# CALL sample_3();

CALL

demo=*# COMMIT;

COMMIT

Exception blocks also impose limits. A transaction cannot be ended inside a block that has exception handlers. In a function, an exception block is an internal subtransaction, which is why this restriction makes sense.

Altering procedures

To change a procedure's settings, use ALTER PROCEDURE. The syntax is:

PgSQL

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

blog=# h ALTER PROCEDURE

Command: ALTER PROCEDURE

Description: change the definition of a procedure

Syntax:

ALTER PROCEDURE name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ]

    action [ ... ] [ RESTRICT ]

ALTER PROCEDURE name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ]

    RENAME TO new_name

ALTER PROCEDURE name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ]

    OWNER TO { new_owner | CURRENT_USER | SESSION_USER }

ALTER PROCEDURE name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ]

    SET SCHEMA new_schema

ALTER PROCEDURE name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ]

    DEPENDS ON EXTENSION extension_name

where action is one of:

    [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER

    SET configuration_parameter { TO | = } { value | DEFAULT }

    SET configuration_parameter FROM CURRENT

    RESET configuration_parameter

    RESET ALL

URL: https://www.postgresql.org/docs/15/sql-alterprocedure.html

For example, you can force a procedure to run with UTC as its timezone:

PgSQL

1

2

3

demo=# ALTER PROCEDURE sample_2()

SET timezone TO 'UTC';

ALTER PROCEDURE

When server-side logic makes sense

Some architects dismiss stored procedures entirely, but there are cases where moving logic to the server is clearly the right call. If you are processing billions of rows, shipping the whole dataset to a client, transforming it there, and sending it back is wasteful. Procedures are a good fit when:

  • A large volume of data is processed, so keeping calculations close to the data reduces network overhead
  • Transactional integrity is a requirement
  • The calculation is inherently data-centric

For more on related topics, see Laurenz Albe's post on subtransactions and performance in PostgreSQL.