PL/pgSQL Constants
Master PL/pgSQL constants — the CONSTANT keyword for compile-time-immutable values. Covers required initialization, error-on-reassign behavior, type matching (TIMESTAMP not TIME for clock_timestamp), and the now()/statement_timestamp()/clock_timestamp() distinction. Worked examples include VAT calculation and elapsed-time measurement.
A constant is a named value declared once at the top of a PL/pgSQL block whose value can never change after declaration. Trying to assign a new value to a constant inside the body produces a compile-time error.
Constants exist for two main reasons:
- Clarity. A magic number like
0.18sprinkled through code is meaningless;vat_rate CONSTANT NUMERIC := 0.18documents what it is. - Safety. Code can't accidentally overwrite a value that's supposed to stay fixed for the entire block. The compiler enforces it.
| Element | Purpose |
|---|---|
constant_name | The identifier you'll use to reference the value. |
CONSTANT | The keyword that marks this as a constant — required for compile-time enforcement. |
data_type | Any PostgreSQL type — INTEGER, NUMERIC, TEXT, BOOLEAN, DATE, TIMESTAMP, etc. |
:= initial_value | Required for constants — unlike variables, you can't declare a constant without giving it a value at the same time. |
:= value part on a constant produces a compile error — the language won't let you have a "blank" constant whose value gets set later, because that would defeat the immutability guarantee.| Feature | Variable | Constant |
|---|---|---|
| Initial value required at declaration | Optional (defaults to NULL) | Required |
| Can be reassigned in the body? | Yes | No — compile error |
| Can be NULL? | Yes (unless NOT NULL) | Whatever you initialized it to (typically not NULL) |
| Use case | Working values that change during the block | Fixed values: rates, limits, configuration |
The classic use of a constant: a tax rate that should never change mid-calculation. The block computes VAT and the gross total for a given net price:
DO $$
DECLARE
vat_rate CONSTANT NUMERIC(4, 2) := 0.18;
net_price NUMERIC(10, 2) := 1000.00;
vat_amount NUMERIC(10, 2);
gross_price NUMERIC(10, 2);
BEGIN
vat_amount := net_price * vat_rate;
gross_price := net_price + vat_amount;
RAISE NOTICE 'Net price: %', net_price;
RAISE NOTICE 'VAT (% percent): %', vat_rate * 100, vat_amount;
RAISE NOTICE 'Gross price: %', gross_price;
END $$;
The vat_rate stays at 0.18 for the entire block — there's no way to accidentally change it from 0.18 to 0.20 partway through the calculation.
Demonstrating the safety guarantee — try to assign a new value to a constant and the compiler refuses:
DO $$
DECLARE
pi CONSTANT NUMERIC := 3.14159;
BEGIN
pi := 3.14; -- This line will not compile
RAISE NOTICE 'pi = %', pi;
END $$;
The error fires at parse time — before any of the body code runs. That's the value of CONSTANT: bugs caught at compile, not at runtime.
A common pattern: capture when a block started using a timestamp constant, then use it later for elapsed-time logging. Note the type — TIMESTAMP, not TIME:
DO $$
DECLARE
started_at CONSTANT TIMESTAMP := clock_timestamp();
finished_at TIMESTAMP;
elapsed_ms NUMERIC;
BEGIN
-- Simulate some work
PERFORM pg_sleep(0.25);
finished_at := clock_timestamp();
elapsed_ms := EXTRACT(EPOCH FROM (finished_at - started_at)) * 1000;
RAISE NOTICE 'Started: %', started_at;
RAISE NOTICE 'Finished: %', finished_at;
RAISE NOTICE 'Elapsed: % ms', round(elapsed_ms, 1);
END $$;
started_at CONSTANT TIME := clock_timestamp() — but TIME stores only hours/minutes/seconds with no date, while clock_timestamp() returns TIMESTAMPTZ. The cast would lose the date entirely, breaking elapsed-time math that crosses midnight. Always pair timestamp-returning functions with TIMESTAMP (or TIMESTAMPTZ) declarations.Postgres exposes several "current time" functions that constants frequently use. Knowing which is which matters:
| Function | Returns | Updates within a transaction? |
|---|---|---|
now() / CURRENT_TIMESTAMP | Time the transaction started | No — same value for the whole transaction |
statement_timestamp() | Time the current statement started | Yes — changes between statements |
clock_timestamp() | Real-time wall clock — now | Yes — changes on every call within a single statement |
For elapsed-time measurement (Example 3), clock_timestamp() is the only correct choice — the others would always show 0 elapsed because they don't advance during the block.
- Use constants for true invariants — tax rates, mathematical constants, configuration limits. Don't make every variable a constant just because its value happens not to change in this block.
- Name them descriptively.
vat_ratebeatsv;max_login_attemptsbeatsn. - Match the type to the value. A timestamp constant needs
TIMESTAMP(notTIME); a percentage needsNUMERICwith appropriate precision (notINTEGER). - Consider session-level alternatives for values shared across many functions: a
SETcustom GUC parameter, a config table, or a function that returns the value. Constants live only inside one block. - Don't fake constants with regular variables.
my_const NUMERIC := 5looks the same in casual reading but offers no protection — the next developer can change it.CONSTANTdocuments intent and enforces it.
- A constant is declared with
name CONSTANT type := initial_value;— initialization is mandatory. - Constants cannot be reassigned in the body; the compiler refuses with
ERROR: "name" is declared CONSTANT. - Use them for fixed values: rates, limits, configuration. They make code self-documenting and safer.
- Match the type carefully —
TIMESTAMPfor timestamps, notTIME(which only stores hours/minutes/seconds without a date). - For shared values across many functions, consider config tables or GUC parameters; constants are scoped to a single block.