Showing posts with label Postgres. Show all posts
Showing posts with label Postgres. Show all posts

Friday, April 17, 2026

What is an OS collation often referred to when dealing with PostgreSQL servers?

We have seen the following error in on of our postgres databases:
WARNING: database "testdb" has a collation version mismatch DETAIL: 
The database was created using collation version 2.17, but the operating system provides version 2.34. 
HINT: Rebuild all objects in this database that use the default collation and run ALTER DATABASE testdb REFRESH COLLATION VERSION, or build PostgreSQL with the right library version.
So what exactly is a "Collation"?

An OS collation is the set of language‑dependent rules supplied by the operating system that define how strings are compared and sorted (alphabetical order, case rules, accents, etc.). On Linux, these rules come from glibc locales.

What the error means

PostgreSQL databases rely on the operating system’s locale / collation implementation (typically glibc on Linux) for:
  • string ordering (ORDER BY)
  • comparisons (<, >, =)
  • indexes on text, varchar, char using the default collation The database testdb was created when the OS provided collation version 2.17, but the server is now running glibc 2.34.

  • In other words:

    • Database collation version: 2.17 
    • OS collation version: 2.34

    This usually happens after:

    • an OS upgrade
    • moving a data directory to a newer system
    • restoring a dump created on an older system
    Collation rules can change between glibc versions.

    Implications for the database

    • The sort order of strings may differ
    • Existing indexes may no longer match the current collation rules
    • Query results involving ordering or comparisons could be wrong
    • Index corruption does not occur, but index semantics may be invalid


    PostgreSQL cannot automatically fix this, so it warns you.

    Only objects that use the default collation, typically:
    • btree indexes on text / varchar
    • constraints using string comparison
    • materialized views
    Objects using COLLATE "C" / POSIX or ICU collations are not affected.

    Solution:

    For each database, and every schema:

    Rebuild all indexes and other dependet objects that rely on the default collation

    Then refresh the collation version:
    ALTER DATABASE prod REFRESH COLLATION VERSION;
    
    The alternative to this is stated in the messages itself:
    or build PostgreSQL with the right library version
    
    which means downgrade glibc or run PostgreSQL on a system with glibc 2.17 — usually not desirable.

    For references, how does this work on the oracle platform?

    For Oracle
    • Collation (NLS sorting/comparison rules) is implemented inside the database engine
    • Controlled via NLS_SORT, NLS_COMP, character set, etc.
    • Independent of the OS
    • OS upgrades do not change string ordering semantics
    For PostgreSQL (default setup)
    • Delegates collation behavior to the operating system locale
    • Uses glibc on Linux for ORDER BY, comparisons, and btree indexes on text
    • If glibc changes, collation rules may change → PostgreSQL detects and warns
    So PostgreSQL is not missing a feature; it is designed to rely on the OS by default. It is a design choice: PostgreSQL usually delegates collation to the OS; Oracle does not

    Wednesday, March 11, 2026

    How to dump the entire schema's DDL into a file

    pg_dump -d mydb \
      --schema-only \
      --no-owner \
      --no-privileges \
      -n myschema \
      -f create_schema.sql
    
    The flag "--no-owner" tells pg_dump not to include OWNER TO ... statements in the dump. When you restore the file in another database, objects will automatically be owned by the user running psql, not by the owner of the the schema in the mydb database.

    The flag "--no-privileges" tells pg_dump not to include GRANT/REVOKE statements. This avoids restoring production permissions into test and lets you manage privileges separately.

    Just paste it into your terminal as the user owning the postgres software, and the file "create_schema.sql" will be created in your current directory.

    Some examples of how to use the function pg_partition_tree

    From PostgreSQL 11, the fuction pg_partition_tree has been available

    Usage, in its simplest form:
    SELECT *
    FROM pg_partition_tree('ldksf.entitet');
    
    select * from pg_partition_tree('ldksf.entitet');
           relid        | parentrelid | isleaf | level
    --------------------+-------------+--------+-------
     entitet            |             | f      |     0
     entitet_default    | entitet     | t      |     1
     entitet_p0         | entitet     | t      |     1
     entitet_p120000000 | entitet     | t      |     1
     entitet_p150000000 | entitet     | t      |     1
    
    Make it a bit more informativ, together with other tables in the data dictionary. Put the following into a file called pg_tree_info.sql:
    \echo myschema = :myschema
    \echo mytable  = :mytable
    
    SELECT
        s.schemaname,
        s.relname AS table_name,
        s.n_live_tup,
        s.last_analyze,
        s.last_autoanalyze
    FROM pg_partition_tree(format('%I.%I', :'myschema', :'mytable')::regclass) pt
    JOIN pg_class c
      ON c.oid = pt.relid
    JOIN pg_namespace n
      ON n.oid = c.relnamespace
    JOIN pg_stat_all_tables s
      ON s.schemaname = n.nspname
     AND s.relname = c.relname
    ORDER BY s.n_live_tup DESC, s.last_analyze;
    
    SELECT
        pt.level,
        pt.isleaf,
        n.nspname,
        c.relname
    FROM pg_partition_tree(
            format('%I.%I', :'myschema', :'mytable')::regclass
         ) pt
    JOIN pg_class c ON c.oid = pt.relid
    JOIN pg_namespace n ON n.oid = c.relnamespace
    ORDER BY pt.level, c.relname;
    
    Example output:
    myschema = scott
    mytable = entitet
     schemaname |     table_name     | n_live_tup |         last_analyze          |       last_autoanalyze
    ------------+--------------------+------------+-------------------------------+-------------------------------
     ldksf      | entitet_p30000000  |   14706380 | 2026-03-10 22:15:50.390363+01 | 2026-03-10 16:29:36.398134+01
     ldksf      | entitet_p0         |   12193064 | 2026-03-10 22:15:50.749426+01 | 2026-03-10 16:27:35.272815+01
     ldksf      | entitet_p60000000  |    5481387 | 2026-03-10 22:15:51.069335+01 | 2026-03-10 16:31:35.842357+01
     ldksf      | entitet_default    |          0 | 2026-03-10 22:15:53.688216+01 |
     ldksf      | entitet_p180000000 |          0 | 2026-03-10 22:15:53.68893+01  |
     
     
     level | isleaf | nspname  |      relname
    -------+--------+----------+--------------------
         0 | f      |   ldksf  | entitet
         1 | t      |   ldksf  | entitet_default
         1 | t      |   ldksf  | entitet_p0
         1 | t      |   ldksf  | entitet_p120000000
         1 | t      |   ldksf  | entitet_p150000000
    
    Excute it like this:
    psql -h prod1.pgsql01.oric.no -d mydb -U scott -v myschema=ldksf -v mytable=entitet -f pg_tree_info.sql
    

    Find active queries in PostgreSQL

    SELECT pid, state, now() - query_start AS duration, left(query, 80) AS query
    FROM pg_stat_activity
    WHERE state != 'idle' AND usename = 'myuser'
    ORDER BY query_start;
    
    Example output:
       pid   | state  |    duration     |                                    query
    ---------+--------+-----------------+------------------------------------------------------------------------------
     1648516 | active | 01:29:28.979693 |                                                                             +
             |        |                 | UPDATE myschema.mytable1 eeu                                   +
             |        |                 | SET aggregated_value = (SELECT e.aggregated_value FROM
    
    This is the query I am looking for, started in another session. The full query text is
    UPDATE myschema.mytable1 eeu
    SET aggregated_value = (SELECT e.aggregated_value FROM myschema.mytable2 e WHERE e.systemid = eeu.entitet AND e.instansid = eeu.instansid);
    

    Thursday, March 5, 2026

    Identity columns vs free-standing sequences in PostgreSQL

    Are the columns typically used in primary key columns, defined as IDENTITY columns, or as standalone columns with a default value generated from a free-standing sequence?
    SELECT table_name, column_name, is_identity, column_default
    FROM information_schema.columns
    WHERE table_schema = 'myschema'
    AND column_name IN ('sekvnr', 'id')
    AND table_name IN ('table1','table2','table3','table4',
                       'table5','table6',
                       'table7','table8')
    ORDER BY table_name, column_name;
    
    Result:
            table_name          |  column_name  | is_identity | column_default
    ----------------------------+---------------+-------------+----------------
     table1                     | id            | YES         |
     table2                     | id            | YES         |
     table3                     | sekvnr        | NO          |
     table4                     | id            | YES         |
     table5                     | id            | YES         |
     table6                     | id            | YES         |
     table7                     | id            | YES         |
     table8                     | id            | YES         |
    (8 rows)
    
    It's clear from the query that the only table that is still supported by a free-standing sequence to generate its primary key values, is table3!

    How to find the current value for a PostgreSQL sequence

    SELECT last_value FROM myschema.mysequence;
    
    or
    SELECT nextval('myschema.mysequence');
    
    Note: nextval() advances the sequence — use last_value if you just want to inspect without side effects.

    Friday, February 20, 2026

    How to find triggers in a PostgreSQL schema

    Execute the SQL below as a user with ownership to the schema. To verify that you have schema privileges, execute:
    SELECT has_schema_privilege('myuser', 'myschema', 'USAGE');
    
    If the reply is
    has_schema_privilege
    ----------------------
     t
    
    You can login
    psql -h localhost -d mydb -U myuser
    
    and execute
    \x
    Expanded display is on.
    
    SELECT
        'TRIGGER' AS object_type,
        t.tgname AS trigger_name,
        n.nspname AS schema_name,
        c.relname AS table_name,
    
        -- Decode tgenabled letters
        CASE t.tgenabled
            WHEN 'O' THEN 'ENABLE'
            WHEN 'A' THEN 'ENABLE ALWAYS'
            WHEN 'R' THEN 'ENABLE REPLICA'
            WHEN 'D' THEN 'DISABLED'
            ELSE t.tgenabled
        END AS enabled_state,
    
        -- Trigger timing from tgtype
        CASE
            WHEN (t.tgtype & 2)  <> 0 THEN 'BEFORE'
            WHEN (t.tgtype & 64) <> 0 THEN 'INSTEAD OF'
            ELSE 'AFTER'
        END AS timing,
    
        -- Events
        (SELECT string_agg(event, ', ')
         FROM (
                SELECT unnest(
                    ARRAY[
                        CASE WHEN (t.tgtype & 4)  <> 0 THEN 'INSERT'   END,
                        CASE WHEN (t.tgtype & 8)  <> 0 THEN 'DELETE'   END,
                        CASE WHEN (t.tgtype & 16) <> 0 THEN 'UPDATE'   END,
                        CASE WHEN (t.tgtype & 32) <> 0 THEN 'TRUNCATE' END
                    ]
                ) AS event
            ) ev
        ) AS events,
    
        pron.nspname || '.' || p.proname AS trigger_function
    
    FROM pg_trigger t
    JOIN pg_class c        ON c.oid = t.tgrelid
    JOIN pg_namespace n    ON n.oid = c.relnamespace
    JOIN pg_proc p         ON p.oid = t.tgfoid
    JOIN pg_namespace pron ON pron.oid = p.pronamespace
    
    WHERE n.nspname = current_schema()
      AND NOT t.tgisinternal
      AND t.tgconstraint = 0
      AND c.relkind <> 'p'
    ORDER BY trigger_name;
    
    Which will list your triggers:
    -[ RECORD 1 ]----+------------------------------------------------------------------
    object_type      | TRIGGER
    trigger_name     | mytrg1
    schema_name      | myschema
    table_name       | mytable1
    enabled_state    | E
    timing           | BEFORE
    events           | UPDATE
    trigger_function | myschema.trigger_fct_upd_mytable1
    -[ RECORD 2 ]----+------------------------------------------------------------------
    object_type      | TRIGGER
    trigger_name     | mytrg2
    schema_name      | myschema
    table_name       | mytable2
    enabled_state    | E
    timing           | AFTER
    events           | DELETE
    trigger_function | myschema.trg_close_fct
    

    Thursday, January 29, 2026

    Compression for Postgres tables

    In PostgreSQL you don’t create a “compressed table” in the Oracle sense. Rather, compression is achieved per-column compression via TOAST, plus (if desirable) a few extension/filesystem tricks.

    Postgres compresses large variable-length columns using TOAST:
    text, varchar, bytea, jsonb, xml, numeric, etc.
    
    TOAST will be applied when a row is too big to fit in an 8kB page; large values are stored in a separate TOAST table, optionally compressed.

    There is no built-in heap/row compression for normal fixed-width columns (e.g. integer, bigint) in vanilla Postgres.

    From PostgreSQL 14 onward you can choose compression algorithm per column: pglz (classic) or lz4 (faster, generally preferred).

    Here is how I did it in my test environment:

    First, ensure your PostgreSQL server was compiled with LZ4 support (use pg_config --configure and look for --with-lz4)
    pg_config --configure | grep lz4
    
    It will show you a long list of options that was used when PostgreSQL was built. Look for '--with-lz4'

    Set compression globally:
    show default_toast_compression;
     default_toast_compression
    ---------------------------
     pglz
    (1 row)
    
    postgres=# ALTER SYSTEM SET default_toast_compression = 'lz4';
    ALTER SYSTEM
    postgres=# SELECT pg_reload_conf();
     pg_reload_conf
    ----------------
     t
    (1 row)
    
    postgres=# show default_toast_compression;
     default_toast_compression
    ---------------------------
     lz4
    
    Optional: default to LZ4 for this session
      SET default_toast_compression = 'lz4';
    
    Yet another option is to set LZ4 for a specific database:
     ALTER DATABASE mydb SET default_toast_compression = 'lz4';
    
    Create the table:
    CREATE TABLE app_logs (
        log_id      bigserial PRIMARY KEY,
        log_time    timestamptz NOT NULL,
        level       text        NOT NULL,
        message     text        COMPRESSION lz4,
        details     jsonb       COMPRESSION lz4
    );
    
    Note:
  • COMPRESSION lz4 / COMPRESSION pglz is a column option.
  • Only matters for TOAST-able types; it won’t change anything for integer, date, etc.
  • Compression only happens when the row gets large enough for TOAST to kick in (roughly when row > ~2kB).

    You can switch existing columns to LZ4 (or back to pglz):
     ALTER TABLE app_logs
        ALTER COLUMN message SET COMPRESSION lz4,
        ALTER COLUMN details SET COMPRESSION lz4;
    
     
    Note that an ALTER TABLE only changes the future TOAST entries. To actually recompress existing rows you need to cause a rewrite. Common options:
    -- 1) Table rewrite (heavy, but clean)
    ALTER TABLE app_logs SET (toast_tuple_target = 2040);  -- optional tweak
    VACUUM FULL app_logs;
    
    -- or 2) Cluster on some index (also rewrites)
    CLUSTER app_logs USING app_logs_pkey;
    ANALYZE app_logs;
    
    Any bulk rewrite (incl. CREATE TABLE AS ..., INSERT INTO new SELECT ... FROM old) will store new TOAST values using the new compression method.


  • Check that the table is using column compression for TOAST values:

    -- Main table vs TOAST table sizes
    SELECT
        relname,
        pg_size_pretty(pg_relation_size(oid))          AS heap_size,
        pg_size_pretty(pg_total_relation_size(oid))    AS total_with_indexes_toast
    FROM pg_class
    WHERE relname IN ('app_logs2','app_logs3','app_logs4');
    
    -- Look at TOAST table directly
    SELECT
        c1.relname       AS main_table,
        c2.relname       AS toast_table,
        pg_size_pretty(pg_total_relation_size(c2.oid)) AS toast_total
    FROM pg_class c1
    JOIN pg_class c2 ON c1.reltoastrelid = c2.oid
    WHERE c1.relname IN ('app_logs2','app_logs3','app_logs4');
    
    In a simple test, I created three tables with three different compression directives and created one long value that would make sure it was TOASTED:
    CREATE TABLE app_logs2 (
        log_id      bigserial PRIMARY KEY,
        log_time    timestamptz NOT NULL,
        level       text        NOT NULL,
        message     text,
        details     jsonb
    );
    
    CREATE TABLE app_logs3 (
        log_id      bigserial PRIMARY KEY,
        log_time    timestamptz NOT NULL,
        level       text        NOT NULL,
        message     text        COMPRESSION lz4,
        details     jsonb       COMPRESSION lz4
    );
    
    CREATE TABLE app_logs4 (
        log_id      bigserial PRIMARY KEY,
        log_time    timestamptz NOT NULL,
        level       text        NOT NULL,
        message     text        COMPRESSION pglz,
        details     jsonb       COMPRESSION pglz
    );
    
    INSERT INTO app_logs2 (log_time, level, message, details)
    VALUES (
        now(),
        'INFO',
        repeat('x', 100000),                -- make it large enough to be TOASTed
        jsonb_build_object('k', repeat('y', 100000))
    );
    
    INSERT INTO app_logs3 (log_time, level, message, details)
    VALUES (
        now(),
        'INFO',
        repeat('x', 100000),                -- make it large enough to be TOASTed
        jsonb_build_object('k', repeat('y', 100000))
    );
    
    INSERT INTO app_logs4 (log_time, level, message, details)
    VALUES (
        now(),
        'INFO',
        repeat('x', 100000),                -- make it large enough to be TOASTed
        jsonb_build_object('k', repeat('y', 100000))
    );
    
    
    As expected, the app_logs2 defaulted to lz4 (set globally):
    SELECT
        relname,
        pg_size_pretty(pg_relation_size(oid))          AS heap_size,
        pg_size_pretty(pg_total_relation_size(oid))    AS total_with_indexes_toast
    FROM pg_class
    WHERE relname IN ('app_logs2','app_logs3','app_logs4');
    
    -- Look at TOAST table directly
    SELECT
        c1.relname       AS main_table,
        c2.relname       AS toast_table,
        pg_size_pretty(pg_total_relation_size(c2.oid)) AS toast_total
    FROM pg_class c1
    JOIN pg_class c2 ON c1.reltoastrelid = c2.oid
    WHERE c1.relname IN ('app_logs2','app_logs3','app_logs4');
    
     relname  | heap_size  | total_with_indexes_toast
    -----------+------------+--------------------------
     app_logs2 | 8192 bytes | 32 kB
     app_logs3 | 8192 bytes | 32 kB
     app_logs4 | 8192 bytes | 48 kB
    (3 rows)
    
     main_table |   toast_table    | toast_total
    ------------+------------------+-------------
     app_logs2  | pg_toast_2510179 | 8192 bytes
     app_logs3  | pg_toast_2510188 | 8192 bytes
     app_logs4  | pg_toast_2510197 | 24 kB
    (3 rows)
    
    Remember, per-column compression via default_toast_compression doesn not show up in \d+ unless it was explicitly set in the column definition.

    So even if table app_logs2 uses compression for TOASTable columns, it does not reveal this fact when being described:
    CREATE TABLE app_logs2 (
        log_id      bigserial PRIMARY KEY,
        log_time    timestamptz NOT NULL,
        level       text        NOT NULL,
        message     text,
        details     jsonb
    );
    
     \d+ app_logs2
                                                                     Table "myschema.app_logs2"
      Column  |           Type           | Collation | Nullable |                  Default                  | Storage  | Compression | Stats target | Description
    ----------+--------------------------+-----------+----------+-------------------------------------------+----------+-------------+--------------+-------------
     log_id   | bigint                   |           | not null | nextval('app_logs2_log_id_seq'::regclass) | plain    |             |              |
     log_time | timestamp with time zone |           | not null |                                           | plain    |             |              |
     level    | text                     |           | not null |                                           | extended |             |              |
     message  | text                     |           |          |                                           | extended |             |              |
     details  | jsonb                    |           |          |                                           | extended |             |              |
     

    Thursday, November 20, 2025

    Find total size of all databases in PostgreSQL cluster

    SELECT database, size FROM (
      SELECT datname AS database,
             pg_size_pretty(pg_database_size(datname)) AS size,
             0 AS sort_order
      FROM pg_database
      UNION ALL
      SELECT 'TOTAL',
             pg_size_pretty(SUM(pg_database_size(datname))),
             1
      FROM pg_database
    ) AS sub
    ORDER BY sort_order, size DESC;
    
    Example output:
                 database             |  size
    ----------------------------------+---------
     mydb01                           | 7819 kB
     mydb02                           | 7795 kB
     postgres                         | 7739 kB
     template0                        | 7731 kB
     template1                        | 7715 kB
     proddb01                         | 76 GB
     proddb02                         | 2971 GB
     proddb03                         | 22 GB
     warehouse01                      | 11 TB
     testdb01                         | 106 MB
     TOTAL                            | 14 TB
    (11 rows)
    

    Monday, November 17, 2025

    Monday, November 10, 2025

    Generate truncate table statements in PostgreSQL

    To generate a script in postgreSQL, equivalent to the Oracle-style shown below:
    select 'truncate table ' || table_name || ' cascade;' from dba_tables where owner='MYSCHEMA';
    
    , put this in a file called gen_truncate.sql
    /*
    | Setting                  | Effect                                                    |
    | ------------------------ | --------------------------------------------------------- |
    | `\o /path/to/file`       | Redirects all query output to the file                    |
    | `\pset format unaligned` | Produces plain text output (no table formatting)          |
    | `\pset tuples_only on`   | Suppresses headers and row counts                         |
    | `\pset footer off`       | Removes `x rows` footer                                   |
    | `\pset border 0`         | Removes any border formatting (mostly for aligned format) |
    */
    
    \o truncate_tables.sql
    \pset format unaligned
    \pset tuples_only on
    \pset footer off
    \pset border 0
    SELECT 'TRUNCATE TABLE ' || schemaname || '.' || tablename || ' CASCADE;' AS stmt
    FROM pg_tables
    WHERE schemaname = 'myschema';
    \o
    
    Execute it:
    psql mydb -f gen_truncate.sql
    
    or login to the desired database directly as the owner of the table:
    psql -h localhost -d mydb -U myuser -f gen_truncate.sql
    

    Do I need special privileges to create temporary tables in a database in PostgreSQL?

    To verify if your user has the privilege to create temporary segments in a postgreSQL database, use this check:
    psql -h localhost -d mydb -U myuser
    
     SELECT has_database_privilege(current_user, current_database(), 'TEMP');
    
    If the value returned shows:
    has_database_privilege
    ------------------------
     f
    
    To be able to create a temporary table in this database, my user needs privileges to do so:
    psql
    postgres=# grant temp on database mydb to myser;
    GRANT
    
    The same test will now yield true instead of false:
    SELECT has_database_privilege(current_user, current_database(), 'TEMP');
     has_database_privilege
    ------------------------
     t
    (1 row)
    
    I can now create a temporary table:
    CREATE TEMP TABLE table_counts (table_name text, row_count bigint);
    
    Before the grant, the access privileges for my database was
    mydb=> \l+ mydb
    
      |      Access privileges       |
      +------------------------------+
      | postgres=CTc/postgres       +|
      | myuser=c/postgres           +|
     
    
    After the grant:
      |      Access privileges       |
      +------------------------------+
      | postgres=CTc/postgres       +|
      | myuser=Tc/postgres          +|
    
    Notice how the privileges for "myuser" has a "T" ammended to it. This indicate the permission to create temporary objects.

    Friday, October 31, 2025

    PostgreSQL query that mimics Oracle's DBA_OBJECTS aggregation

    with base_objects as (
      select 
        case 
          when c.relkind = 'r' then 'TABLE'
          when c.relkind = 'v' then 'VIEW'
          when c.relkind = 'm' then 'MATERIALIZED VIEW'
          when c.relkind = 'i' then 'INDEX'
          when c.relkind = 'S' then 'SEQUENCE'
          when c.relkind = 'f' then 'FOREIGN TABLE'
          when c.relkind = 'p' then 'PARTITIONED TABLE'
          when c.relkind = 'I' then 'PARTITIONED INDEX'
          else 'OTHER'
        end as object_type
      from pg_class c
      join pg_namespace n on n.oid = c.relnamespace
      where n.nspname = 'myschema'
    
      union all
    
      select 
        case p.prokind
          when 'f' then 'FUNCTION'
          when 'p' then 'PROCEDURE'
          when 'a' then 'AGGREGATE'
          when 'w' then 'WINDOW'
          else 'OTHER'
        end as object_type
      from pg_proc p
      join pg_namespace n on n.oid = p.pronamespace
      where n.nspname = 'myschema'
    
      union all
      select 'TYPE'
      from pg_type t
      join pg_namespace n on n.oid = t.typnamespace
      where n.nspname = 'myschema'
        and t.typtype in ('c', 'e', 'd') -- composite, enum, domain
    
      union all
    
      select 'CONSTRAINT'
      from pg_constraint c
      join pg_namespace n on n.oid = c.connamespace
      where n.nspname = 'myschema'
    )
    
    select object_type, count(*)
    from base_objects
    group by object_type
    order by object_type;
    

    Monday, September 29, 2025

    How to pass parameters to a psql script

    To use variables in your sql script, use the -v option to set variables on the command line.

    Consider this example:
    export PGPASSWORD=mysecretpassword
    psql -h myserver.oric.no -d db01 -U scott -v schema=scott -v table=mytable -f myquery.sql
    
    The query in myquery.sql can then referance the "schema" and "table" variables, like this:
    SELECT
        att.attname AS column_name
    FROM
        pg_attribute att
    JOIN
        pg_class cls ON cls.oid = att.attrelid
    JOIN
        pg_namespace ns ON ns.oid = cls.relnamespace
    WHERE
        ns.nspname = :'schema'
        AND cls.relname = :'table'
        AND att.attnotnull
        AND att.attnum > 0
        AND NOT att.attisdropped;
    

    Thursday, September 25, 2025

    Find out if a constraint in PostgreSQL is defined as deferrable

    Logged into the relevant database, find out which FK are deferrable:
    SELECT conname, condeferrable, condeferred
    FROM pg_constraint
    WHERE conrelid in ( 'mytable1'::regclass,'mytable2'::regclass) 
    AND contype='f';
               conname            | condeferrable | condeferred
    ------------------------------+---------------+-------------
     mytable1_id_fkey             | f             | f
     mytable1_id_fkey             | f             | f
     mytable2_id_fkey             | f             | f
    
  • If condeferrable = t (true), the constraint is created initially deferrable
  • If condeferrable = f (false), like in my case, it is not.
  • Wednesday, September 24, 2025

    Terminate PostgresSQL sessions

    -- Terminate all connections from user "sales"
    SELECT pg_terminate_backend(pid)
    FROM pg_stat_activity
    WHERE usename = 'sales';
    
    In my case the result was:
     pg_terminate_backend
    ----------------------
     t
     t
    (2 rows)
    

    Check active sessions in PostgreSQL

    The closest equivalent to this oracle query:
    SELECT USERNAME, COUNT(*) 
    FROM v$session 
    WHERE type <> 'BACKGROUND' 
    GROUP BY username;
    
    Would be
    SELECT datname, usename AS username, COUNT(*)
    FROM pg_stat_activity
    WHERE backend_type = 'client backend'
    GROUP BY datname, usename
    ORDER BY datname, usename;
    
    Example output:
     datname  |     username      | count
    ----------+-------------------+-------
     postgres | postgres          |     1
     postgres | postgres_exporter |     1
     mydb     | myser             |     2
    (3 rows)
    

    Tuesday, September 9, 2025

    How to set echo in a SQL script executed in psql

    Use one of two options 1. in the sql file, add the following directive
    \set ECHO queries
    
    2. execute the script using the -e flag
    psql -e -f myfile.sql
    

    Monday, September 8, 2025

    Syntax for drop/recreate of constraints in PostgreSQL

    1. Find the constraints belonging to a specific table:
    SELECT conname
    FROM pg_constraint
    WHERE conrelid = 'mytable'::regclass;
        conname
    ----------------
     sys_c002733781
     sys_c002733782
    (2 rows)
    
    2. Drop them:
    ALTER TABLE mytable DROP CONSTRAINT sys_c002733781;
    ALTER TABLE
    ALTER TABLE mytable DROP CONSTRAINT sys_c002733782;
    ALTER TABLE
    
    3. Load your data using your preferred method

    4. Recreate the FK constraints but skip validating existing data (only new inserts/updates are checked):
    alter table mytable add constraint sys_c002733781 foreign key(job_id) references jobs(id) on delete cascade not valid;
    ALTER TABLE
    
    alter table mytable add constraint sys_c002733782 foreign key(task_id) references tasks(id) on delete cascade not valid;
    ALTER TABLE
    
    5. Validate constraints:
    alter table mytable validate constraint sys_c002733781;
    ALTER TABLE
    alter table mytable validate constraint sys_c002733782;
    ALTER TABLE
    

    Friday, September 5, 2025

    Query equivalent to SELECT * FROM USER_OBJECTS in PostgreSQL

    SELECT schema_name, object_name, object_type
    FROM (
        -- Tables, Views, Indexes, etc.
        SELECT
            n.nspname AS schema_name,
            c.relname AS object_name,
            CASE c.relkind
                WHEN 'r' THEN 'TABLE'
                WHEN 'v' THEN 'VIEW'
                WHEN 'm' THEN 'MATERIALIZED VIEW'
                WHEN 'i' THEN 'INDEX'
                WHEN 'S' THEN 'SEQUENCE'
                WHEN 'f' THEN 'FOREIGN TABLE'
                WHEN 'p' THEN 'PARTITIONED TABLE'
                WHEN 'I' THEN 'PARTITIONED INDEX'
                ELSE c.relkind::text
            END AS object_type
        FROM pg_class c
        JOIN pg_namespace n ON n.oid = c.relnamespace
        WHERE n.nspname = current_schema()
    
        UNION ALL
    
        -- Functions
        SELECT
            n.nspname AS schema_name,
            p.proname AS object_name,
            'FUNCTION' AS object_type
        FROM pg_proc p
        JOIN pg_namespace n ON n.oid = p.pronamespace
        WHERE n.nspname = current_schema()
    ) AS objects
    ORDER BY object_type, object_name;
    
    It only shows objects in the current schema (like Oracle USER_OBJECTS).

    If you want all objects the user owns (across schemas), replace
    WHERE n.nspname = current_schema()
    
    with
    WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')