Files
playbook/antigravity-awesome-skills/skills/postgresql-cli/references/tips-advanced.md
T
2026-07-01 16:02:41 +00:00

290 lines
8.0 KiB
Markdown

# 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)
```