Skip to content
Reference / SQL Stored Procedures
Reference

SQL Stored Procedures

A stored procedure is a named program kept in the database and run with CALL or EXEC. DuckDB has none; macros, prepared statements, and scripts cover the same ground.

A stored procedure is a named block of SQL statements kept in the database and run on request, with CALL in PostgreSQL and MySQL or EXEC in SQL Server. It takes parameters, can run several statements in order, can change data, and in most engines can use variables, loops, and error handling.

The point is to put a whole operation ("ship this order") behind one name, so every application performs it the same way.

Procedure or function

Most databases have both, and they are easy to confuse. A function computes a value and is used inside a query; a procedure performs an action and is called on its own.

Compared Function Procedure
How you run it Inside a query: select order_total(1) On its own: call ship_order(4)
Returns A value, or rows for a table function Nothing, or output parameters and result sets, depending on the engine
Changes data Usually not, and many engines forbid it Yes, that is its usual job
Commits Runs inside the caller's transaction Can COMMIT or ROLLBACK partway through in PostgreSQL 11 and later, and in SQL Server, MySQL, and Oracle

How other databases write one

A procedure that marks an order shipped and logs the change, in PostgreSQL:

PostgreSQL
create procedure ship_order(p_order_id integer)
language plpgsql
as $$
begin
  insert into order_status_log (order_id, old_status, new_status)
  select order_id, status, 'shipped'
  from orders
  where order_id = p_order_id;

  update orders
  set status = 'shipped'
  where order_id = p_order_id;
end;
$$;

call ship_order(4);

The same in SQL Server, where parameters start with @:

SQL Server
create procedure dbo.ship_order @order_id int
as
begin
  set nocount on;

  insert into order_status_log (order_id, old_status, new_status)
  select order_id, status, 'shipped'
  from orders
  where order_id = @order_id;

  update orders
  set status = 'shipped'
  where order_id = @order_id;
end;

exec dbo.ship_order @order_id = 4;
Engine Create Run
PostgreSQL create procedure, in SQL or PL/pgSQL (version 11 and later; before that, functions did this job) call name(...)
SQL Server create procedure, in T-SQL exec name @param = ...
MySQL create procedure, with delimiter changed in the client so the body's semicolons do not end the statement call name(...)
Oracle create procedure, in PL/SQL, often in a package exec name(...) or an anonymous block
Snowflake, BigQuery create procedure, in Snowflake Scripting, JavaScript, or Python (Snowflake), or GoogleSQL scripting (BigQuery) call name(...)
SQLite, DuckDB None Not applicable

Why teams use them, and why some avoid them

For: one definition of an operation shared by every application; fewer round trips, since several statements run inside the database; and permissions granted on the procedure instead of the tables, so a user can ship an order without being able to update orders freely.

Against: the logic lives outside the application's code, version control, and tests unless you put it there deliberately; procedural SQL dialects are the least portable part of any database; and, like triggers, a procedure can change several tables behind one innocent-looking name.

DuckDB: no procedures

Not in DuckDB
create procedure ship_order(p_order_id integer)
language sql
as $$ update orders set status = 'shipped' where order_id = p_order_id $$

It fails with Parser Error: syntax error at or near "procedure". DuckDB does have CALL, but only for built-in table functions such as call pragma_table_info('orders'); call ship_order(13) fails with Catalog Error: Table Function with name ship_order does not exist! DuckDB is usually embedded in a program or run from a script, and the reusable parts of a procedure map onto three things.

Reusable calculations: macros

In DuckDB, CREATE FUNCTION is another name for CREATE MACRO. A scalar macro returns a value:

A function, in DuckDB
create function order_total(oid) as (
    select sum(quantity * unit_price)
    from order_items
    where order_id = oid
);

select order_id, order_total(order_id) as total
from orders
where customer_id = 1
order by order_id

Customer 1's three orders come to 54, 44, and 33.5. A table macro returns rows, which covers procedures whose only job is to run a query with parameters:

A parameterized query
create function orders_for(cid) as table
select order_id, order_date, status
from orders
where customer_id = cid;

select *
from orders_for(1)
order by order_id

duckdb_functions() lists them with a function_type of macro and table_macro. A macro body is one expression or one query, never an INSERT or UPDATE: create macro ... as table update ... is a Parser Error. See macros for parameters, defaults, and overloads.

A data change with parameters: a prepared statement

PREPARE names a statement with numbered placeholders, and EXECUTE runs it with values. Unlike a macro, it can change data:

Prepare once, run with values
prepare ship_order as
update orders
set status = 'shipped'
where order_id = $1;

execute ship_order(13);

select order_id, status
from orders
where order_id = 13

Order 13 was pending and is now shipped. A prepared statement lasts only as long as the connection that prepared it, and deallocate ship_order removes it sooner. In the SQLShed playground every Run opens a new connection, so prepare and execute in the same Run.

Several statements as one operation: a script

The procedure's real body (log the change, then make it, all or nothing) becomes a script with an explicit transaction. The triggers page builds exactly that for order 4, and the rerunnable scripts lesson shows how to write a script that is safe to run twice. In an application, the script lives in a function in your own code that runs the statements on one connection.

What can go wrong

  • Porting a procedure as a macro. A macro cannot change data, loop, or hold variables. Anything a procedure did beyond a single query has to move into a script or the application.
  • A prepared statement that is gone. Executing one from a different connection, or after deallocate, fails with Binder Error: Prepared statement "ship_order" does not exist.
  • Copying MySQL procedure code with its delimiter. delimiter // is a command for the MySQL client, not SQL. Pasted into another engine it is a syntax error.
  • A procedure name that hides a data change. Wherever the operation lives, name it for what it changes (ship_order, not process_order), and keep its statements where someone reviewing the data can find them.
Dialect note

Some engines blur the line. In PostgreSQL before version 11 there were no procedures, so functions returning void did the job and are still common in older schemas. In SQL Server, procedures can return result sets like a query, which is why reporting tools often call them.