\set ECHO queries2. execute the script using the -e flag
psql -e -f myfile.sql
Minimalistic Oracle contains a collection of practical examples from my encounters with Oracle technologies. When relevant, I also write about other technologies, like Linux or PostgreSQL. Many of the posts starts with "how to" since they derive directly from my own personal experience. My goal is to provide simple examples, so that they can be easily adapted to other situations.
\set ECHO queries2. execute the script using the -e flag
psql -e -f myfile.sql
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 TABLE3. Load your data using your preferred method
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 TABLE5. Validate constraints:
alter table mytable validate constraint sys_c002733781; ALTER TABLE alter table mytable validate constraint sys_c002733782; ALTER TABLE
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).
WHERE n.nspname = current_schema()with
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
psql -h postgres01.oric.no -U mysuperuser postgres -c "COPY (SELECT name || '=' || setting FROM pg_settings ORDER BY name) TO STDOUT WITH CSV HEADER" > pg_config.csv
SELECT s.schemaname, s.relname AS table_name, s.n_live_tup, s.last_analyze, s.last_autoanalyze FROM pg_stat_all_tables s JOIN pg_class c ON c.relname = s.relname LEFT JOIN pg_inherits i ON i.inhrelid = c.oid OR i.inhparent = c.oid WHERE s.schemaname = 'myschema' AND ( s.relname = 'mytable' OR c.oid IN ( SELECT inhrelid FROM pg_inherits WHERE inhparent = ( SELECT oid FROM pg_class WHERE relname = 'mytable' ) ) ) ORDER BY s.n_live_tup DESC,s.last_analyze;Example output
schemaname | table_name | n_live_tup | last_analyze | last_autoanalyze -----------------+-----------------+------------+-------------------------------+------------------------------ myschema | mytable_2015_02 | 95788115 | 2025-08-06 12:58:22.952592+00 | myschema | mytable_2015_03 | 78505350 | 2025-08-06 12:58:37.147433+00 | myschema | mytable_2015_04 | 71211253 | 2025-08-06 12:58:51.311452+00 | myschema | mytable_2015_01 | 68255510 | 2025-08-06 12:58:08.42708+00 | myschema | mytable_2015_05 | 62075043 | 2025-08-06 12:59:05.434118+00 | myschema | mytable_2015_06 | 60888876 | 2025-08-06 12:59:19.918657+00 | 2025-08-06 11:31:19.551345+00 myschema | mytable_2015_12 | 57485594 | 2025-08-06 13:00:43.112316+00 | 2025-08-27 03:13:11.394606+00 myschema | mytable_2016_01 | 57405307 | 2025-08-06 13:00:55.395113+00 | 2025-08-25 13:35:07.749773+00
SELECT pg_current_wal_lsn(); -- On the primaryIn my case, the result was
pg_current_wal_lsn -------------------- 0/E015420On the standby, run
SELECT pg_last_wal_replay_lsn();In my case, the result was
pg_last_wal_replay_lsn ------------------------ 0/E015420
psql -h hostname -U username -d databaseOptions:
Option Description -h <host> Hostname or IP address of the PostgreSQL server -p <port> Port number (default is 5432) -U <username> Username to connect as -d <dbname> Database name -W Prompt for password (useful if password auth is required) -f <filename> Execute commands from a SQL script file -c "<command>" Execute a single SQL command and exit -v var=value Set psql variable (used inside scripts) --set=var=value Same as -v, more explicit --dbname=<connstr> Full connection string, e.g.: postgresql://user:pass@host:port/dbnameExample
psql -h pg01.oric.no -p 5432 -U myuser -d mydbAlternatively, use an URL, which is more scripting friendly:
psql "postgresql://replicate@pg01.oric.no:5432/mydb"
SELECT column_name FROM information_schema.columns WHERE table_name = 'pg_stat_wal_receiver';Output
column_name ----------------------- pid last_msg_receipt_time latest_end_lsn latest_end_time sender_port receive_start_lsn receive_start_tli written_lsn flushed_lsn received_tli last_msg_send_time status slot_name sender_host conninfo (15 rows)Or use the psql metacommand "\d+":
echo '\d+ pg_stat_wal_receiver' |psqlOutput
View "pg_catalog.pg_stat_wal_receiver" Column | Type | Collation | Nullable | Default | Storage | Description -----------------------+--------------------------+-----------+----------+---------+----------+------------- pid | integer | | | | plain | status | text | | | | extended | receive_start_lsn | pg_lsn | | | | plain | receive_start_tli | integer | | | | plain | written_lsn | pg_lsn | | | | plain | flushed_lsn | pg_lsn | | | | plain | received_tli | integer | | | | plain | last_msg_send_time | timestamp with time zone | | | | plain | last_msg_receipt_time | timestamp with time zone | | | | plain | latest_end_lsn | pg_lsn | | | | plain | latest_end_time | timestamp with time zone | | | | plain | slot_name | text | | | | extended | sender_host | text | | | | extended | sender_port | integer | | | | plain | conninfo | text | | | | extended | View definition: SELECT pid, status, receive_start_lsn, receive_start_tli, written_lsn, flushed_lsn, received_tli, last_msg_send_time, last_msg_receipt_time, latest_end_lsn, latest_end_time, slot_name, sender_host, sender_port, conninfo FROM pg_stat_get_wal_receiver() s(pid, status, receive_start_lsn, receive_start_tli, written_lsn, flushed_lsn, received_tli, last_msg_send_time, last_msg_receipt_time, latest_end_lsn, latest_end_time, slot_name, sender_host, sender_port, conninfo) WHERE pid IS NOT NULL;
ps -fu postgres | grep $(pgrep -f postmaster) postgres 1913 1 0 Jul17 ? 00:02:51 /usr/pgsql-15/bin/postmaster -D /var/lib/pgsql/15/data/ postgres 1982 1913 0 Jul17 ? 00:00:24 postgres: logger postgres 2063 1913 0 Jul17 ? 00:00:18 postgres: checkpointer postgres 2064 1913 0 Jul17 ? 00:00:03 postgres: background writer postgres 2119 1913 0 Jul17 ? 00:00:07 postgres: walwriter postgres 2120 1913 0 Jul17 ? 00:00:14 postgres: autovacuum launcher postgres 2121 1913 0 Jul17 ? 00:00:01 postgres: archiver postgres 2122 1913 0 Jul17 ? 00:00:00 postgres: logical replication launcher postgres 2657 1913 0 Jul17 ? 00:02:05 postgres: postgres_exporter postgres localhost(48674) idleThe command lists all processes by user "postgres" and and greps for the output of the command "pgrep -f postmaster", which returns a list of process IDs
pgrep -c postmaster 9
SELECT schemaname, relname, last_vacuum, last_autovacuum, last_analyze, last_autoanalyze FROM pg_stat_user_tables WHERE relname = 'documents'; schemaname | relname | last_vacuum | last_autovacuum | last_analyze | last_autoanalyze ----------------+----------+-------------+-------------------------------+-------------------------------+------------------------------- scott | documents| | 2025-06-25 02:05:20.692869+02 | 2025-07-12 22:04:05.119051+02 | 2025-06-17 08:41:12.567351+02 (1 row)Explanation of columns:
ANALYZE;Analyze a specific table:
ANALYZE my_table;Analyze specific columns only:
ANALYZE my_table (column1, column2);
postgres=# alter system set log_min_duration_statement='2s'; ALTER SYSTEM postgres=# SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) postgres=# show log_min_duration_statement; log_min_duration_statement ---------------------------- 2s (1 row)I expect every statement that lasts longer than 2 seconds to be logged in the postgreSQL server main log.
postgres=# SELECT pg_sleep(3); pg_sleep ---------- (1 row)From another session, tail the log from today:
tail -F /var/log/postgres/postgresql-2025-07-11_000000.log | grep --line-buffered "duration:.*statement:"Output
2025-07-11 09:45:42 CEST [173349]: [5-1] user=postgres,db=postgres,app=psql,client=[local] LOG: duration: 3003.698 ms statement: SELECT pg_sleep(3);
mydb=> \d mytable Partitioned table "myschema.mytable" Column | Type | Collation | Nullable | Default -------------------+--------------------------+-----------+----------+------------------------------ id | bigint | | not null | generated always as identity ad_updtud_log_id | bigint | | not null | period | integer | | not null | created | timestamp with time zone | | not null | Partition key: RANGE (id) Indexes: "mytable_pkey" PRIMARY KEY, btree (id, periode) "mytable_id_index" btree (im_oppsum_logg_id) "mytable_opprettet_index" btree (opprettet) Foreign-key constraints: "fk_mytable" FOREIGN KEY (ad_updtud_log_id, period) REFERENCES mytable2(id, period) Number of partitions: 180 (Use \d+ to list them.)This is how you can list all partitions belonging to a range-partitioned table in postgreSQL, based on the values:
SELECT child.relname AS partition_name, regexp_replace(pg_get_expr(child.relpartbound, child.oid), '.*FROM \(''(\d+).*', '\1')::bigint AS range_start, pg_get_expr(child.relpartbound, child.oid) AS partition_range FROM pg_inherits JOIN pg_class parent ON pg_inherits.inhparent = parent.oid JOIN pg_class child ON pg_inherits.inhrelid = child.oid JOIN pg_namespace nm ON nm.oid = child.relnamespace WHERE parent.relname = 'mytable' AND nm.nspname = 'myschema' ORDER BY range_start;Result (excerpt only):
partition_name | range_start | partition_range ---------------------------------------+-------------+-------------------------------------------- mytable_0 | 0 | FOR VALUES FROM ('0') TO ('30000000') mytable_30000000 | 30000000 | FOR VALUES FROM ('30000000') TO ('60000000') mytable_60000000 | 60000000 | FOR VALUES FROM ('60000000') TO ('90000000') mytable_90000000 | 90000000 | FOR VALUES FROM ('90000000') TO ('120000000') mytable_120000000 | 120000000 | FOR VALUES FROM ('120000000') TO ('150000000') mytable_150000000 | 150000000 | FOR VALUES FROM ('150000000') TO ('180000000') mytable_180000000 | 180000000 | FOR VALUES FROM ('180000000') TO ('210000000') mytable_210000000 | 210000000 | FOR VALUES FROM ('210000000') TO ('240000000') mytable_240000000 | 240000000 | FOR VALUES FROM ('240000000') TO ('270000000') mytable_270000000 | 270000000 | FOR VALUES FROM ('270000000') TO ('300000000') mytable_300000000 | 300000000 | FOR VALUES FROM ('300000000') TO ('330000000') mytable_330000000 | 330000000 | FOR VALUES FROM ('330000000') TO ('360000000') mytable_360000000 | 360000000 | FOR VALUES FROM ('360000000') TO ('390000000') mytable_390000000 | 390000000 | FOR VALUES FROM ('390000000') TO ('420000000') mytable_420000000 | 420000000 | FOR VALUES FROM ('420000000') TO ('450000000') mytable_450000000 | 450000000 | FOR VALUES FROM ('450000000') TO ('480000000')
alter session set current_schema=<schema_name>
, in PostgreSQL, search_path
is a session-level setting that determines the order in which schemas are searched when you reference database objects without a schema name.
It consists of a list of schema names. When you run a query like
SELECT * FROM mytable;
PostgreSQL checks each schema in the list — in order — to find mytable
.
"$user", publicThis means:
public
schema.search_path
:
SHOW search_path;Set it for the session:
SET search_path TO schema1, public;or
SET search_path TO schema1, schema2, public;The last example is important in cases where you logon to a database with a user with no matching schema. Consider the following example:
psql -h server1.oric.no -U jim -d musicdbI am logging onto the database "musicdb" with a user called "jim". By default, jim will have its own user followed by public, in his search path:
musicdb=> show search_path; search_path ----------------- "$user", public (1 row)I have already given user jim the privilges needed to "see" the objects created by schema "music" which exists in the database "musicdb".
musicdb=> set search_path to 'music','$user','public'; SET musicdb=> show search_path; search_path ------------------------------- music, "$user", public (1 row)The "current_user" and "current_schema" functions will now return the actual user name, and the first match in the search_path, respectivly:
musicdb=> select current_user, current_schema; current_user | current_schema --------------+---------------- jim | music (1 row)Why is it important?
postgres=# alter database db01 connection limit 0; ALTER DATABASEVerify
SELECT datname, datconnlimit FROM pg_database WHERE datname = 'db01'; -[ RECORD 1 ]+------------- datname | db01 datconnlimit | 0Set back to unlimited:
ALTER DATABASE db01 CONNECTION LIMIT -1; ALTER DATABASEVerify:
SELECT datname, datconnlimit FROM pg_database WHERE datname = 'db01'; -[ RECORD 1 ]+------------- datname | db01 datconnlimit | -1To limit the connections for a specific user only:
psql psql (15.13) Type "help" for help. postgres=# alter user read_db01 connection limit 0; ALTER ROLE postgres=# alter user read_db01 connection limit -1; ALTER ROLE postgres=#The current setting can be verified with:
SELECT rolname, rolconnlimit FROM pg_roles WHERE rolname = 'read_db01'; rolname | rolconnlimit -------------------+-------------- read_db01 | -1or, list all users that does have restrictions on the number of connections:
SELECT rolname, rolconnlimit FROM pg_roles WHERE rolconnlimit != -1; rolname | rolconnlimit -------------------+-------------- pganalyze | 5 read_db01 | 0 (2 rows)
psql CREATE ROLE common_users WITH NOLOGIN NOSUPERUSER INHERIT NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS; GRANT pg_read_all_stats TO common_users;Then, create the user ro_user1:
create user ro_user1 password 'mysecretpassword'; grant common_users to ro_user1; grant connect on database db01 to ro_user1;Log into the database db01 and revoke and grant some privileges:
psql \connect db01 revoke all on schema schema1 from ro_user1; grant usage on schema schema1 to ro_user1; grant select on all tables in schema schema1 to ro_user1;Confirm the privileges:
\connect postgres select database_privs('ro_user1');
The amount of memory the database server uses for shared memory buffers.
The postgres documentation suggest starting with allocating 25% of the available memory to the shared database memory pool:Controls the maximum size the Write-Ahead Logging (WAL) files can grow before triggering a checkpoint. Checkponts are relative expensive operations, so we do not want them to occur too often. On the other hands, too infrequent checkpointing may increase recovery times. max_wal_size can be set to balance performance and recovery time by influencing how often checkpoints occur.
work_mem
Sets the base maximum amount of memory to be used by a query operation (such as a sort or hash table) before writing to temporary disk files.
The documentation points out that a complex query might perform several sort and hash operations at the same time, with each operation generally being allowed to use as much memory as this value specifies before it starts to write data into temporary files and that serveral running sessions could be executing such operations at the same time. So even if the 6M specified as its value does not seem like much, it could mean significant memory usage on a busy system.
It is similar to pga_aggregate_target in an oracle database: the amount of memory set for all private global areas on the server. After the introduction of this parameter in Oracle 9i, the private global area parameters used back then, for example sort_area_size and hash_area_size, was not necessary any longer.
Specifies the maximum amount of memory to be used by maintenance operations, such as VACUUM, CREATE INDEX, and ALTER TABLE ADD FOREIGN KEY.
It can be set higher than work_mem:
Since only one of these operations can be executed at a time by a database session, and an installation normally doesn't have many of them running concurrently, it's safe to set this value significantly larger than work_mem. Larger settings might improve performance for vacuuming and for restoring database dumps.
Specifies the maximum amount of memory to be used by each autovacuum worker process. If this value is specified without units, it is taken as kilobytes. It defaults to -1, indicating that the value of maintenance_work_mem should be used instead. The setting has no effect on the behavior of VACUUM when run in other contexts.
Here is a table with my settings for my 16G Memory serverParameter | Value |
---|---|
shared_buffers | 4GB |
max_wal_size | 8GB |
work_mem | 6MB |
maintenance_work_mem | 479MB |
grep Hugepagesize /proc/meminfo Hugepagesize: 2048 kBOn my 16G server, I would like to start with 25% of the total memory as available for shared_buffers:
su - postgres sudo systemctl stop postgresql-15.service postgres --shared-buffers=4096MB -D $PGDATA -C shared_memory_size_in_huge_pages 2102Update /etc/sysctl.conf. Add
vm.nr_hugepages=2102Restart sysctl
sysctl -pNow, tell the postgres server to use huge pages, if they are available.
huge_pages = tryAdd the following lines to /etc/security/limits.conf so that postgres can lock down the memory set aside for huge tables:
postgres soft memlock unlimited postgres hard memlock unlimitedReboot the server and verify that huge pages are being used:
cat /proc/meminfo | grep HugeInterestingly, the parameter huge_page_size should only be used if you wish to override the default huge page size on your system. The default is zero (0). When set to 0, the default huge page size on the system will be used. In my case this is what I want to so the parameter can be ignored.
SELECT con.conname AS constraint_name, CASE con.contype WHEN 'p' THEN 'PRIMARY KEY' WHEN 'u' THEN 'UNIQUE' WHEN 'f' THEN 'FOREIGN KEY' WHEN 'c' THEN 'CHECK' WHEN 'x' THEN 'EXCLUSION' ELSE con.contype END AS constraint_type, rel.relname AS table_name, pg_get_constraintdef(con.oid) AS definition FROM pg_constraint con JOIN pg_class rel ON rel.oid = con.conrelid JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace WHERE nsp.nspname = 'owner' AND rel.relname = 'table_name' ORDER BY constraint_name;
COPY at_locations FROM '/external/data/geolocations_at_fixed.csv' WITH ( FORMAT csv, DELIMITER ';', NULL 'NULL', HEADER false );and execute it like this:
psql -h myserver.oric.no -U vinmonopolet -d vinmonopolet vinmonopolet=> \i cp_at.sqlYou will see the error
ERROR: must be superuser or have privileges of the pg_read_server_files role to COPY from a file HINT: Anyone can COPY to stdout or from stdin. psql's \copy command also works for anyone.This is because the COPY command runs server-side, and PostgreSQL server expects be able to access the file and its entire path on the server.
\copy vinmonopolet.at_locations FROM '/external/data/geolocations_at_fixed.csv' WITH (FORMAT csv, DELIMITER ';', NULL 'NULL', HEADER false)and execute either with the \i
psql -h myserver.oric.no -U vinmonopolet -d vinmonopolet -f cp_at.sql Password for user vinmonopolet: COPY 5This works because the \copy command is interpreted client-side. psql reads the file from your local machine (in my case, the "local machine" was indeed the postgres server, but the point stands), and pipes the content into the database. And it succeeds because no elevated privileges are needed for this method.
\connect mydb \prompt 'Enter value for querid: ' v_quid \echo You entered: :v_quid SELECT query_sql_text FROM query_store.query_texts_view WHERE query_text_id = :'v_quid';Run it:
sql -h pgserver1.oric.no -U dbadmin -d postgres -f find_rest_of_statement.sql Enter value for querid: -7644745457188850826 You entered: -7644745457188850826 query_sql_text ---------------------------------------------------------------------------------------------------------------------- THE QUERY TEXT WILL SHOW UP HERE :-)