# psql Tips — Advanced Debugging, Performance & Safety Part of the psql tips reference. See also: tips-workflows.md Advanced techniques for debugging, performance tuning, and safe psql usage. ## Table of Contents - [Performance Tips](#performance-tips) - [Debugging and Introspection](#debugging-and-introspection) - [Safety and Best Practices](#safety-and-best-practices) - [Gotchas and Common Mistakes](#gotchas-and-common-mistakes) --- ## Performance Tips ### Large Result Sets ```sql -- Don't load entire result into memory \set FETCH_COUNT 1000 SELECT * FROM billion_row_table; -- Use \copy instead of COPY for client-side operations \copy huge_table TO '/data/export.csv' WITH (FORMAT csv) ``` ### Pipeline Mode for Batch Operations Pipeline mode batches multiple queries into single network round trips: ```sql \startpipeline INSERT INTO logs (msg) VALUES ($1) \bind 'entry 1' \sendpipeline INSERT INTO logs (msg) VALUES ($1) \bind 'entry 2' \sendpipeline INSERT INTO logs (msg) VALUES ($1) \bind 'entry 3' \sendpipeline \getresults \endpipeline ``` This sends all three INSERTs in one network round trip instead of three. ### Script Execution ```bash # Run in a single transaction (faster, and all-or-nothing) psql -1 -f migration.sql mydb # Multiple files sequentially psql -1 -f 001.sql -f 002.sql -f 003.sql mydb ``` --- ## Debugging and Introspection ### See What psql Sends to the Server ```sql -- Echo all SQL commands \set ECHO queries -- See the SQL behind \d commands (incredibly useful for learning) \set ECHO_HIDDEN on -- Or in noexec mode (show but don't execute) \set ECHO_HIDDEN noexec -- Now run any \d command to see its SQL \dt \d users ``` ### Check Query Plans ```sql -- Basic plan EXPLAIN SELECT * FROM users WHERE email = 'test@example.com'; -- With actual execution time EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com'; -- Detailed with buffer info EXPLAIN (ANALYZE, BUFFERS, VERBOSE) SELECT ...; -- JSON format for tooling EXPLAIN (FORMAT JSON) SELECT ...; ``` ### Lock Analysis ```sql -- Current locks waiting to be granted SELECT * FROM pg_locks WHERE NOT granted; -- Blocked sessions with their blockers (PostgreSQL 9.6+) SELECT blocked.pid, blocked.query, pg_blocking_pids(blocked.pid) AS blocked_by_pids FROM pg_stat_activity blocked WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0; ``` --- ## Safety and Best Practices ### Always Set ON_ERROR_STOP in Scripts Without `ON_ERROR_STOP`, a script continues even after errors, potentially leaving the database in an inconsistent state: ```sql -- Top of every script \set ON_ERROR_STOP on ``` ### Use Single-Transaction Mode for Migrations ```bash # -1 wraps everything in BEGIN...COMMIT # On error, the entire migration rolls back psql -1 -f migration.sql mydb ``` ### Never Use PGPASSWORD in Scripts ```bash # BAD: Password visible in process list, env vars PGPASSWORD=secret psql -c "SELECT 1" mydb# GOOD: Use ~/.pgpass (manually edit to avoid shell history) touch ~/.pgpass && chmod 600 ~/.pgpass # Then edit ~/.pgpass and your add: # hostname:port:database:username:password # Example: localhost:5432:mydb:myuser:mysecret psql -c "SELECT 1" mydb ``` ### Preview Before Executing ```sql -- Dry-run a script to see what commands will execute (shows SQL, does NOT execute) \set ECHO all BEGIN; -- Paste or review migration SQL here, then ROLLBACK instead of COMMIT \i migration.sql ROLLBACK; -- Or use \gdesc to check result columns without executing SELECT * FROM complex_view \gdesc ``` ### Use \copy Over COPY `\copy` uses client permissions and filesystem. SQL `COPY` runs on the server and requires superuser or `pg_read_server_files`/`pg_write_server_files` roles. `\copy` transfers all data through the client/server connection, which is less efficient than SQL `COPY` for very large datasets. For bulk data transfer, prefer SQL `COPY` when server-side file access is available. ```sql -- BAD (requires server-side file access) COPY users TO '/tmp/users.csv' WITH CSV HEADER; -- GOOD (uses client-side file access) \copy users TO '/tmp/users.csv' WITH CSV HEADER ``` ### search_path Safety for Untrusted Users If untrusted users have access to the database, remove publicly-writable schemas from `search_path` at session start: ```sql SELECT pg_catalog.set_config('search_path', '', false); ``` ### Automatic LISTEN/NOTIFY Polling Whenever a command is executed, psql automatically polls for asynchronous notification events generated by `LISTEN`/`NOTIFY`. This happens without any special configuration. --- ## Gotchas and Common Mistakes ### Semicolons in \copy `\copy` does NOT end with a semicolon. It's a meta-command: ```sql -- CORRECT \copy users TO '/tmp/users.csv' WITH CSV HEADER -- WRONG (psql interprets the semicolon oddly) \copy users TO '/tmp/users.csv' WITH CSV HEADER; ``` ### Variable Substitution and SQL Injection psql variables are simple text substitution. They are NOT parameterized queries: ```sql -- The :'varname' form (single-quoted) escapes embedded single quotes, making it -- safe against SQL injection for STRING VALUES in WHERE clauses: \set name "Robert'); DROP TABLE students;--" SELECT * FROM users WHERE name = :'name'; -- Expands to: WHERE name = 'Robert''); DROP TABLE students;--' -- The '' is an escaped quote, so the entire value is a single string literal — NOT injected. -- However, :'varname' is NOT safe for identifiers or unquoted contexts. -- For identifiers (table/column names), use :"varname" (double-quoted form). -- For the safest parameterized queries, use \bind: SELECT * FROM users WHERE name = $1; \bind 'Robert' \g ``` ### Transaction State After Error After an error in a transaction block, all subsequent commands fail until ROLLBACK: ```sql BEGIN; INSERT INTO users (id) VALUES (1); INSERT INTO users (id) VALUES ('bad'); -- ERROR INSERT INTO users (id) VALUES (2); -- Also fails! COMMIT; -- Also fails! ``` Use `ON_ERROR_ROLLBACK` to auto-savepoint: ```sql \set ON_ERROR_ROLLBACK on BEGIN; INSERT INTO users (id) VALUES (1); INSERT INTO users (id) VALUES ('bad'); -- ERROR, auto-rollback to savepoint INSERT INTO users (id) VALUES (2); -- This works now COMMIT; ``` ### Pattern Matching Uses Regex The `*` and `?` in \d commands are converted to regex (`.*` and `.`). Advanced regex like `[0-9]` works. Important differences from standard regex: - `.` is a schema/object separator, not any-char (use `?` for any single char) - `$` is matched literally, not as an anchor (pattern must match whole name anyway) - Within double quotes, all special characters (`*`, `?`, regex chars) are literal ```sql -- These work as expected \dt user* -- matches users, user_accounts, etc. \dt user? -- matches users, user1, etc. \dt user[0-9]* -- matches user1, user2, user123 -- If your table name contains special chars, use double quotes \dt "table.with.dots" -- matches literally, dots not treated as separator ``` ### Connection String vs CLI Arguments ```bash # These are equivalent psql -h localhost -p 5432 -U admin -d mydb psql "postgresql://admin@localhost:5432/mydb" # But you can't mix freely — URI overrides individual flags psql -h otherhost "postgresql://admin@localhost:5432/mydb" -- uses localhost, not otherhost ``` ### \i vs \ir - `\i filename` — resolves relative to the **current working directory** (where psql was started) - `\ir filename` — resolves relative to the **currently executing script's directory** For script portability, prefer `\ir`: ```sql -- In /scripts/migrations/run_all.sql: \ir 001_schema.sql -- resolves to /scripts/migrations/001_schema.sql \ir 002_data.sql -- resolves to /scripts/migrations/002_data.sql ``` ### \o and Query Output `\o` redirects query output, not meta-command output: ```sql \o /tmp/output.txt SELECT * FROM users; -- goes to file \d users -- also goes to file \echo 'hello' -- goes to STDOUT (not affected by \o) \qecho 'hello' -- goes to /tmp/output.txt (affected by \o) ```