mirror of
https://github.com/neondatabase/postgres-skills.git
synced 2026-09-22 00:46:51 +03:00
Fix live-tested PostgreSQL examples
Correct executable SQL, operational commands, version gates, and semantic guidance found through PostgreSQL 14/17 integration testing.
This commit is contained in:
@@ -9,7 +9,7 @@ Guidelines and best practices for working with Postgres, covering schema design,
|
||||
|
||||
## Supported Versions
|
||||
|
||||
This skill covers PostgreSQL 14 through 18. Version-specific features are tagged (e.g., `[PG15+]`, `[PG18+]`). All queries have been validated against PG14-PG18.
|
||||
This skill covers PostgreSQL 14 through 18. Version-specific features are tagged (e.g., `[PG15+]`, `[PG18+]`); environment-dependent examples identify required privileges, extensions, or multi-node setup.
|
||||
|
||||
PostgreSQL provides 5 years of support per major version. Always run the latest minor release.
|
||||
|
||||
|
||||
@@ -88,11 +88,13 @@ pg_restore -d mydb --data-only backup.dump
|
||||
# Clean (drop) objects before recreating
|
||||
pg_restore -d mydb --clean --if-exists backup.dump
|
||||
|
||||
# Continue on errors (useful for partial restores)
|
||||
# Restore atomically and stop on the first error
|
||||
pg_restore -d mydb --single-transaction -j 1 backup.dump
|
||||
# Note: --single-transaction is incompatible with -j > 1
|
||||
```
|
||||
|
||||
Selective restore patterns do not automatically include dependencies. Schema-qualify table patterns when names may exist in multiple schemas, and verify required types, sequences, constraints, and referenced tables separately.
|
||||
|
||||
### Restoring Plain SQL Dumps
|
||||
|
||||
```bash
|
||||
@@ -144,9 +146,8 @@ Without statistics, the planner uses default estimates after restore until `ANAL
|
||||
5. **Drop indexes before restore, recreate after** — faster than incremental index maintenance during bulk inserts
|
||||
6. **Restore schema first, then data, then indexes**:
|
||||
```bash
|
||||
pg_restore -d mydb --schema-only backup.dump
|
||||
pg_restore -d mydb --section=pre-data backup.dump
|
||||
pg_restore -d mydb --data-only -j 4 backup.dump
|
||||
# Then recreate indexes (already created by schema restore, but if you dropped them:)
|
||||
pg_restore -d mydb --section=post-data -j 4 backup.dump
|
||||
```
|
||||
|
||||
@@ -251,7 +252,7 @@ recovery_target_action = 'promote' # 'pause' to inspect before promoting
|
||||
|
||||
### Recovery Target Options
|
||||
|
||||
```sql
|
||||
```text
|
||||
-- Recover to a specific time
|
||||
recovery_target_time = '2024-06-15 14:30:00+00'
|
||||
|
||||
@@ -290,8 +291,11 @@ This gives you an exact target to recover to if the operation goes wrong.
|
||||
# List contents without restoring
|
||||
pg_restore --list backup.dump
|
||||
|
||||
# Verify a physical backup (PG17+: also works with tar format)
|
||||
# Verify a plain-format physical backup (PG13+)
|
||||
pg_verifybackup /backup/base_20240615
|
||||
|
||||
# PG18+: verify a tar-format backup directly
|
||||
pg_verifybackup --no-parse-wal /backup/base_tar_20240615
|
||||
```
|
||||
|
||||
### Test Restores Regularly
|
||||
|
||||
@@ -124,7 +124,7 @@ COPY orders FROM '/path/to/orders.csv' WITH (FORMAT csv, HEADER true);
|
||||
ALTER TABLE orders ENABLE TRIGGER ALL;
|
||||
```
|
||||
|
||||
Requires table owner or superuser privileges.
|
||||
`DISABLE TRIGGER ALL` requires superuser privileges when the table has foreign-key or other internally generated constraint triggers. A table owner can use `DISABLE TRIGGER USER` to disable only user-defined triggers. Disabling constraint triggers can admit invalid data, so validate constraints before re-enabling writes.
|
||||
|
||||
### 3. Increase maintenance_work_mem
|
||||
|
||||
@@ -243,11 +243,13 @@ DROP TABLE orders_old;
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
`LIKE ... INCLUDING ALL` does not copy foreign keys, triggers, rules, grants, row-level security policies, or publication membership. Recreate and verify those objects before swapping, or use a data-only refresh when the original table's identity and dependencies must remain unchanged.
|
||||
|
||||
## Bulk Updates and Deletes
|
||||
|
||||
### Batched Deletes
|
||||
### Chunked Deletes
|
||||
|
||||
Large DELETEs lock rows and generate WAL. Batch them:
|
||||
Large DELETEs lock rows and generate WAL. This loop limits each statement to 10,000 rows:
|
||||
|
||||
```sql
|
||||
-- Delete in batches of 10,000
|
||||
@@ -270,9 +272,11 @@ BEGIN
|
||||
END $$;
|
||||
```
|
||||
|
||||
### Batched Updates
|
||||
The entire `DO` block is still one transaction: locks, WAL, and dead rows accumulate until it finishes. For true transaction-level batching, execute one limited DELETE per client transaction and commit between batches.
|
||||
|
||||
Same pattern for large updates:
|
||||
### Chunked Updates
|
||||
|
||||
The same statement-size pattern works for large updates:
|
||||
|
||||
```sql
|
||||
DO $$
|
||||
@@ -295,6 +299,8 @@ BEGIN
|
||||
END $$;
|
||||
```
|
||||
|
||||
As with the DELETE loop, the `DO` block commits only once. Drive batches from the client or a transaction-controlling procedure invoked outside an explicit transaction when each batch must commit independently.
|
||||
|
||||
### Partition Drop Instead of Delete
|
||||
|
||||
If data is partitioned by time, dropping a partition is instant vs. a slow DELETE:
|
||||
|
||||
@@ -29,13 +29,18 @@ SELECT
|
||||
(SELECT setting::int FROM pg_settings WHERE name = 'superuser_reserved_connections') AS reserved
|
||||
FROM pg_stat_activity;
|
||||
|
||||
-- Memory per backend (approximate)
|
||||
SELECT pg_size_pretty(
|
||||
(SELECT setting::bigint * 1024 FROM pg_settings WHERE name = 'work_mem') +
|
||||
(SELECT setting::bigint * 1024 FROM pg_settings WHERE name = 'temp_buffers')
|
||||
) AS per_backend_estimate;
|
||||
-- Potential per-operation and per-session memory settings
|
||||
SELECT
|
||||
name,
|
||||
current_setting(name) AS configured_value,
|
||||
pg_size_bytes(current_setting(name)) AS configured_bytes
|
||||
FROM pg_settings
|
||||
WHERE name IN ('work_mem', 'temp_buffers')
|
||||
ORDER BY name;
|
||||
```
|
||||
|
||||
These are not baseline allocations per backend. `work_mem` can be consumed by multiple query operations, while `temp_buffers` is allocated lazily when a session uses temporary tables.
|
||||
|
||||
## PgBouncer Configuration
|
||||
|
||||
PgBouncer is the most widely used connection pooler for PostgreSQL.
|
||||
@@ -196,7 +201,7 @@ psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer
|
||||
|
||||
### Key Commands
|
||||
|
||||
```sql
|
||||
```text
|
||||
-- Pool status (most useful)
|
||||
SHOW POOLS;
|
||||
-- Columns: database, user, cl_active, cl_waiting, sv_active, sv_idle, sv_used, pool_mode
|
||||
@@ -216,6 +221,8 @@ SHOW CONFIG;
|
||||
SHOW MEM;
|
||||
```
|
||||
|
||||
These commands use PgBouncer's admin protocol and fail if sent directly to PostgreSQL.
|
||||
|
||||
### What to Watch
|
||||
|
||||
| Metric | Healthy | Problem |
|
||||
|
||||
@@ -94,7 +94,8 @@ FROM pg_stat_replication;
|
||||
```sql
|
||||
SELECT
|
||||
status,
|
||||
received_lsn,
|
||||
written_lsn,
|
||||
flushed_lsn,
|
||||
latest_end_lsn,
|
||||
latest_end_time,
|
||||
slot_name,
|
||||
|
||||
@@ -75,7 +75,10 @@ CREATE INDEX idx_booking_range ON bookings USING gist(during);
|
||||
-- Matches: WHERE during && '[2024-01-01, 2024-02-01)'
|
||||
|
||||
-- Used in exclusion constraints
|
||||
EXCLUDE USING gist (room_id WITH =, during WITH &&)
|
||||
CREATE EXTENSION IF NOT EXISTS btree_gist;
|
||||
ALTER TABLE bookings
|
||||
ADD CONSTRAINT bookings_no_overlap
|
||||
EXCLUDE USING gist (room_id WITH =, during WITH &&);
|
||||
```
|
||||
|
||||
### BRIN (Block Range Index)
|
||||
@@ -159,7 +162,9 @@ CREATE INDEX idx_events_type ON events((payload->>'type'));
|
||||
-- Query: WHERE payload->>'type' = 'click'
|
||||
|
||||
-- Date truncation
|
||||
CREATE INDEX idx_orders_month ON orders(date_trunc('month', created_at));
|
||||
-- timestamptz must be made timezone-independent for an immutable expression
|
||||
CREATE INDEX idx_orders_month
|
||||
ON orders(date_trunc('month', created_at AT TIME ZONE 'UTC'));
|
||||
```
|
||||
|
||||
The query must use the same expression for the planner to match the index.
|
||||
@@ -199,13 +204,20 @@ ALTER TABLE events ADD PRIMARY KEY (id, occurred_at);
|
||||
CREATE INDEX idx_events_q1_status ON events_2024_q1((payload->>'status'));
|
||||
```
|
||||
|
||||
- **CONCURRENTLY on partitioned tables**: `CREATE INDEX CONCURRENTLY` on a partitioned parent creates indexes on each partition one at a time, concurrently. This avoids locking the entire table during index builds on large partitioned tables.
|
||||
- **CONCURRENTLY on partitioned tables**: PostgreSQL does not support `CREATE INDEX CONCURRENTLY` directly on a partitioned parent. Create an invalid parent index with `ON ONLY`, build matching indexes concurrently on each partition, then attach them:
|
||||
|
||||
```sql
|
||||
CREATE INDEX CONCURRENTLY idx_events_customer ON events(customer_id);
|
||||
CREATE INDEX idx_events_customer
|
||||
ON ONLY events ((payload->>'customer_id'));
|
||||
|
||||
CREATE INDEX CONCURRENTLY idx_events_q1_customer
|
||||
ON events_2024_q1 ((payload->>'customer_id'));
|
||||
|
||||
ALTER INDEX idx_events_customer
|
||||
ATTACH PARTITION idx_events_q1_customer;
|
||||
```
|
||||
|
||||
If a concurrent build fails on one partition, the parent index is marked `INVALID`. Fix the failed partition index, then run `ALTER INDEX idx_events_customer ATTACH PARTITION ...` or drop and retry.
|
||||
Repeat the concurrent build and attach steps for every partition. The parent index becomes valid after all partition indexes are attached. If a child build fails, drop or rebuild that invalid child index before attaching it.
|
||||
|
||||
- **Index-only scans**: Work across partitions. The planner prunes irrelevant partitions first, then uses index-only scans on the remaining ones.
|
||||
|
||||
|
||||
@@ -117,15 +117,19 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO repl_user;
|
||||
|
||||
### Decoder Plugins
|
||||
|
||||
Postgres supports two decoder plugins:
|
||||
Postgres supports logical decoding output plugins, including:
|
||||
- **`pgoutput`** (default): built into Postgres, used by native logical replication
|
||||
- **`wal2json`**: converts WAL to JSON format, useful for CDC integrations (Debezium, etc.)
|
||||
- **`wal2json`**: an optional third-party plugin that converts WAL to JSON format for CDC integrations
|
||||
|
||||
The decoder is specified when creating a replication slot manually:
|
||||
|
||||
```sql
|
||||
SELECT pg_create_logical_replication_slot('my_slot', 'pgoutput');
|
||||
-- or
|
||||
```
|
||||
|
||||
After installing `wal2json` on the database server:
|
||||
|
||||
```sql
|
||||
SELECT pg_create_logical_replication_slot('my_slot', 'wal2json');
|
||||
```
|
||||
|
||||
@@ -267,7 +271,7 @@ ALTER SUBSCRIPTION my_sub SET PUBLICATION new_pub;
|
||||
-- Add a publication
|
||||
ALTER SUBSCRIPTION my_sub ADD PUBLICATION extra_pub;
|
||||
|
||||
-- Remove a publication (PG17+)
|
||||
-- Remove a publication
|
||||
ALTER SUBSCRIPTION my_sub DROP PUBLICATION old_pub;
|
||||
```
|
||||
|
||||
@@ -551,10 +555,12 @@ Initial sync speed depends on table size and network. For very large tables, con
|
||||
If the subscriber has conflicting data (e.g., duplicate key):
|
||||
|
||||
```sql
|
||||
-- Check for errors in pg_stat_subscription
|
||||
-- PG15+: cumulative subscription apply-error counters
|
||||
SELECT * FROM pg_stat_subscription_stats;
|
||||
```
|
||||
|
||||
This view reports counters rather than detailed error messages. Check PostgreSQL server logs for the specific conflict; on PG14, logs are the primary source because `pg_stat_subscription_stats` is unavailable.
|
||||
|
||||
**PG16+**: Skip a conflicting transaction:
|
||||
|
||||
```sql
|
||||
|
||||
@@ -21,11 +21,12 @@
|
||||
|
||||
## pg_upgrade (In-Place)
|
||||
|
||||
`pg_upgrade` replaces the old cluster's data files with the new version in place, without dumping and reloading data. It supports two modes:
|
||||
`pg_upgrade` replaces the old cluster's data files with the new version in place, without dumping and reloading data. PG18 supports five transfer modes:
|
||||
|
||||
| Mode | Flag | Speed | Disk usage |
|
||||
|------|------|-------|------------|
|
||||
| Copy | (default) | Moderate | 2x disk during upgrade |
|
||||
| Copy file range | `--copy-file-range` (PG18+) | Fast where supported | 2x disk during upgrade |
|
||||
| Link | `--link` | Fast | Minimal extra disk (hard links) |
|
||||
| Swap | `--swap` (PG18+) | Fastest | No extra disk (swaps data dirs) |
|
||||
| Clone | `--clone` | Fast (if filesystem supports reflinks) | Minimal extra disk |
|
||||
@@ -91,7 +92,7 @@ pg_upgrade \
|
||||
--new-bindir /usr/lib/postgresql/18/bin \
|
||||
--swap
|
||||
|
||||
# Parallel checking (PG18+): speed up pre-checks
|
||||
# Parallel checking: speed up pre-checks
|
||||
pg_upgrade ... --jobs 4
|
||||
```
|
||||
|
||||
@@ -103,19 +104,21 @@ pg_ctl start -D /var/lib/postgresql/17/main
|
||||
|
||||
#### 6. Post-Upgrade Tasks
|
||||
|
||||
pg_upgrade generates helper scripts in the current directory after a successful run:
|
||||
After a successful upgrade, refresh optimizer statistics and remove the old cluster only after verification:
|
||||
|
||||
```bash
|
||||
# Generated by pg_upgrade — analyze all databases (update optimizer statistics)
|
||||
./analyze_new_cluster.sh
|
||||
# Update optimizer statistics in stages
|
||||
vacuumdb --all --analyze-in-stages
|
||||
|
||||
# Generated by pg_upgrade — delete old cluster data (only after confirming the upgrade works)
|
||||
./delete_old_cluster.sh
|
||||
```
|
||||
|
||||
Current `pg_upgrade` releases generate the deletion script but may instruct you to run `vacuumdb` directly instead of generating `analyze_new_cluster.sh`.
|
||||
|
||||
### Optimizer Statistics Preservation (PG18+)
|
||||
|
||||
PG18's `pg_upgrade` preserves optimizer statistics by default, eliminating the post-upgrade performance dip while `ANALYZE` runs:
|
||||
PG18's `pg_upgrade` preserves most optimizer statistics by default, reducing the post-upgrade performance dip:
|
||||
|
||||
```bash
|
||||
# Default in PG18: statistics are preserved
|
||||
@@ -125,7 +128,7 @@ pg_upgrade ...
|
||||
pg_upgrade ... --no-statistics
|
||||
```
|
||||
|
||||
On older versions, always run `ANALYZE` on all databases immediately after upgrading.
|
||||
Some statistics, including cumulative statistics and statistics that cannot be transferred, still need to be rebuilt. On older versions, always run `ANALYZE` on all databases immediately after upgrading.
|
||||
|
||||
## Logical Replication (Minimal Downtime)
|
||||
|
||||
@@ -164,7 +167,7 @@ Converts a physical standby into a logical subscriber, simplifying the setup:
|
||||
# PG18+: --all flag converts all databases at once
|
||||
pg_createsubscriber \
|
||||
--pgdata /var/lib/postgresql/18/main \
|
||||
--publisher-conninfo "host=old_primary dbname=mydb" \
|
||||
--publisher-server "host=old_primary dbname=mydb" \
|
||||
--all
|
||||
```
|
||||
|
||||
@@ -243,14 +246,11 @@ If using streaming replication:
|
||||
### 1. Update Optimizer Statistics
|
||||
|
||||
```bash
|
||||
# Run the generated script (analyzes all databases)
|
||||
./analyze_new_cluster.sh
|
||||
|
||||
# Or manually:
|
||||
vacuumdb --all --analyze-only --jobs 4
|
||||
# Rebuild optimizer statistics in stages
|
||||
vacuumdb --all --analyze-in-stages --jobs 4
|
||||
```
|
||||
|
||||
On PG18+ with `pg_upgrade`, statistics are preserved by default — this step is optional but still recommended after significant schema changes.
|
||||
On PG18+ with `pg_upgrade`, most optimizer statistics are preserved by default, but analyze tables whose statistics were not transferred and tables changed after the upgrade.
|
||||
|
||||
### 2. Check for Invalid Indexes
|
||||
|
||||
@@ -326,6 +326,6 @@ rm -rf /var/lib/postgresql/16/main # be very careful with this
|
||||
|
||||
- **pg_upgrade (copy mode)**: Old cluster is intact — start it back up
|
||||
- **pg_upgrade (link mode)**: Old cluster is unusable — restore from backup
|
||||
- **pg_upgrade (swap mode)**: Run `pg_upgrade --swap` again to reverse the swap
|
||||
- **pg_upgrade (swap mode)**: The file transfer is destructive; restore the old cluster from backup
|
||||
- **Logical replication**: Point applications back to the old cluster
|
||||
- **Always have a tested backup** regardless of method
|
||||
|
||||
@@ -66,7 +66,7 @@ LIMIT 20;
|
||||
|
||||
`dead_pct` > 20% = VACUUM is falling behind. Check autovacuum settings.
|
||||
|
||||
### Table Bloat Estimate
|
||||
### Relation Storage Breakdown
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
@@ -83,7 +83,7 @@ ORDER BY pg_total_relation_size(schemaname || '.' || tablename) DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
For precise bloat estimation, use the `pgstattuple` extension (requires superuser or elevated privileges):
|
||||
This separates heap size from indexes and TOAST; it is not a bloat estimate. For bloat measurements, use the `pgstattuple` extension (requires superuser or elevated privileges):
|
||||
|
||||
```sql
|
||||
CREATE EXTENSION IF NOT EXISTS pgstattuple;
|
||||
@@ -190,10 +190,10 @@ CREATE EXTENSION IF NOT EXISTS pg_buffercache;
|
||||
|
||||
SELECT
|
||||
c.relname,
|
||||
pg_size_pretty(count(*) * 8192) AS buffered,
|
||||
pg_size_pretty(count(*) * current_setting('block_size')::bigint) AS buffered,
|
||||
round(100.0 * count(*) / (SELECT setting::int FROM pg_settings WHERE name = 'shared_buffers'), 1) AS pct_of_cache
|
||||
FROM pg_buffercache b
|
||||
JOIN pg_class c ON c.relfilenode = b.relfilenode
|
||||
JOIN pg_class c ON pg_relation_filenode(c.oid) = b.relfilenode
|
||||
WHERE b.reldatabase = (SELECT oid FROM pg_database WHERE datname = current_database())
|
||||
GROUP BY c.relname
|
||||
ORDER BY count(*) DESC
|
||||
@@ -209,15 +209,22 @@ FK columns without indexes cause slow JOINs and slow CASCADE deletes:
|
||||
```sql
|
||||
SELECT
|
||||
c.conrelid::regclass AS table_name,
|
||||
a.attname AS fk_column,
|
||||
c.conname AS constraint_name
|
||||
c.conname AS constraint_name,
|
||||
pg_get_constraintdef(c.oid) AS constraint_definition
|
||||
FROM pg_constraint c
|
||||
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
|
||||
WHERE c.contype = 'f'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM pg_index i
|
||||
WHERE i.indrelid = c.conrelid
|
||||
AND a.attnum = ANY(i.indkey)
|
||||
AND i.indisvalid
|
||||
AND i.indpred IS NULL
|
||||
AND i.indnkeyatts >= cardinality(c.conkey)
|
||||
AND (
|
||||
SELECT array_agg(key_attnum ORDER BY ordinality)
|
||||
FROM unnest(i.indkey::smallint[]) WITH ORDINALITY
|
||||
AS keys(key_attnum, ordinality)
|
||||
WHERE ordinality <= cardinality(c.conkey)
|
||||
) = c.conkey
|
||||
);
|
||||
```
|
||||
|
||||
@@ -275,13 +282,16 @@ ORDER BY duration DESC;
|
||||
### Cancel or Terminate a Query
|
||||
|
||||
```sql
|
||||
-- Graceful cancel (sends cancel signal)
|
||||
SELECT pg_cancel_backend(pid);
|
||||
-- Replace 12345 with a PID selected from pg_stat_activity.
|
||||
-- Graceful cancel (sends a cancel signal)
|
||||
SELECT pg_cancel_backend(12345);
|
||||
|
||||
-- Force terminate (kills the connection)
|
||||
SELECT pg_terminate_backend(pid);
|
||||
SELECT pg_terminate_backend(12345);
|
||||
```
|
||||
|
||||
Do not target your current session (`pg_backend_pid()`). Prefer cancellation first; terminate only when cancellation does not resolve the problem.
|
||||
|
||||
## Lock Analysis
|
||||
|
||||
### Blocked Queries and What's Blocking Them
|
||||
@@ -325,13 +335,15 @@ WHERE NOT bl.granted AND kl.granted;
|
||||
For application-level coordination without row locking:
|
||||
|
||||
```sql
|
||||
-- Acquire (blocks until available)
|
||||
-- Choose one acquisition method, not both.
|
||||
-- Blocking acquisition:
|
||||
SELECT pg_advisory_lock(hashtext('my_job_name'));
|
||||
|
||||
-- Try (non-blocking, returns boolean)
|
||||
SELECT pg_try_advisory_lock(hashtext('my_job_name'));
|
||||
-- Release once for each successful session-level acquisition:
|
||||
SELECT pg_advisory_unlock(hashtext('my_job_name'));
|
||||
|
||||
-- Release
|
||||
-- Or use a non-blocking acquisition:
|
||||
SELECT pg_try_advisory_lock(hashtext('my_job_name'));
|
||||
SELECT pg_advisory_unlock(hashtext('my_job_name'));
|
||||
```
|
||||
|
||||
|
||||
@@ -266,11 +266,19 @@ Except for `shared_buffers`, these parameters can be overridden in the current s
|
||||
|
||||
```sql
|
||||
SET random_page_cost = 1.1;
|
||||
SET effective_io_concurrency = 200;
|
||||
-- Some platforms without posix_fadvise only support 0.
|
||||
DO $$
|
||||
BEGIN
|
||||
PERFORM set_config('effective_io_concurrency', '200', false);
|
||||
EXCEPTION
|
||||
WHEN invalid_parameter_value THEN
|
||||
PERFORM set_config('effective_io_concurrency', '0', false);
|
||||
END
|
||||
$$;
|
||||
SET effective_cache_size = '24GB'; -- adjust to your server
|
||||
```
|
||||
|
||||
These make the planner prefer index scans (correct for SSDs where random reads are fast).
|
||||
These make the planner more willing to use index scans when measurements show SSD random reads are fast. Some platforms without asynchronous prefetch support cap `effective_io_concurrency` at `0`.
|
||||
|
||||
### Asynchronous I/O (PG18+)
|
||||
|
||||
|
||||
@@ -232,9 +232,11 @@ INSERT INTO users (email, name)
|
||||
VALUES ('a@b.com', 'Alice')
|
||||
ON CONFLICT (email)
|
||||
DO UPDATE SET name = EXCLUDED.name
|
||||
RETURNING id, (xmax = 0) AS inserted; -- true if inserted, false if updated
|
||||
RETURNING id;
|
||||
```
|
||||
|
||||
Do not use `(xmax = 0)` as an inserted-versus-updated contract; it relies on implementation details. On PG18+, use `OLD`/`NEW` in `RETURNING`. On earlier versions, distinguish outcomes in application logic or use separate statements when the distinction matters.
|
||||
|
||||
### OLD/NEW in RETURNING (PG18+)
|
||||
|
||||
All DML commands support `OLD` and `NEW` references in RETURNING, making it easy to see before/after values:
|
||||
@@ -389,7 +391,7 @@ UPDATE events SET payload = jsonb_set(payload, '{status,code}', '"200"');
|
||||
Simpler syntax for JSONB access and assignment:
|
||||
|
||||
```sql
|
||||
-- Read (equivalent to payload->'address'->>'city')
|
||||
-- Read JSONB (equivalent to payload->'address'->'city')
|
||||
SELECT payload['address']['city'] FROM events;
|
||||
|
||||
-- Update (equivalent to jsonb_set)
|
||||
|
||||
@@ -75,10 +75,11 @@ By default, NULLs are considered distinct in unique constraints (multiple NULLs
|
||||
|
||||
```sql
|
||||
-- Standard: allows multiple rows with NULL in email
|
||||
CREATE UNIQUE INDEX idx_email ON users(email);
|
||||
CREATE UNIQUE INDEX idx_email_standard ON users(email);
|
||||
|
||||
-- PG15+: only one NULL allowed
|
||||
CREATE UNIQUE INDEX idx_email ON users(email) NULLS NOT DISTINCT;
|
||||
CREATE UNIQUE INDEX idx_email_nulls_not_distinct
|
||||
ON users(email) NULLS NOT DISTINCT;
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
@@ -191,7 +192,7 @@ CREATE TABLE reservations (
|
||||
);
|
||||
```
|
||||
|
||||
This eliminates the need for `btree_gist` and manual exclusion constraints for non-overlapping range scenarios.
|
||||
This eliminates the need to write the exclusion constraint manually. The temporal key is implemented with GiST; scalar key columns such as `room_id int` still need an appropriate GiST operator class, supplied by `btree_gist` for common scalar types.
|
||||
|
||||
### NOT ENFORCED Constraints (PG18+)
|
||||
|
||||
@@ -284,7 +285,6 @@ ALTER TABLE events DETACH PARTITION events_2024_q1 CONCURRENTLY;
|
||||
- Partition key must be part of the primary key and all unique indexes
|
||||
- Indexes defined on parent are auto-created on child partitions
|
||||
- Foreign keys referencing partitioned tables are supported
|
||||
- Identity columns on partitioned tables require PG17+
|
||||
- Exclusion constraints on partitioned tables require PG17+ (equality on partition key only)
|
||||
|
||||
## Virtual Generated Columns (PG18+)
|
||||
@@ -296,6 +296,7 @@ CREATE TABLE orders (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
quantity int NOT NULL,
|
||||
unit_price numeric(10,2) NOT NULL,
|
||||
description text NOT NULL,
|
||||
-- Virtual: computed on read, no storage cost
|
||||
total numeric GENERATED ALWAYS AS (quantity * unit_price) VIRTUAL,
|
||||
-- Stored: persisted on disk, can be indexed
|
||||
|
||||
@@ -239,11 +239,13 @@ ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tenant_isolation ON orders
|
||||
USING (tenant_id = current_setting('app.current_tenant')::int);
|
||||
|
||||
-- Set per-connection
|
||||
-- Test through a non-owner application role; table owners bypass RLS by default.
|
||||
SET ROLE app_user;
|
||||
SET app.current_tenant = '42';
|
||||
|
||||
-- All queries on orders now automatically filter by tenant_id = 42
|
||||
SELECT * FROM orders; -- only sees tenant 42's orders
|
||||
RESET ROLE;
|
||||
```
|
||||
|
||||
### Policy Types
|
||||
@@ -252,29 +254,29 @@ SELECT * FROM orders; -- only sees tenant 42's orders
|
||||
-- SELECT policy (restrict which rows can be read)
|
||||
CREATE POLICY read_own ON documents
|
||||
FOR SELECT
|
||||
USING (owner_id = current_user_id());
|
||||
USING (owner_id = current_setting('app.current_user_id')::bigint);
|
||||
|
||||
-- INSERT policy (restrict which rows can be inserted)
|
||||
CREATE POLICY insert_own ON documents
|
||||
FOR INSERT
|
||||
WITH CHECK (owner_id = current_user_id());
|
||||
WITH CHECK (owner_id = current_setting('app.current_user_id')::bigint);
|
||||
|
||||
-- UPDATE policy (restrict which rows can be updated, and what values are allowed)
|
||||
CREATE POLICY update_own ON documents
|
||||
FOR UPDATE
|
||||
USING (owner_id = current_user_id()) -- which rows can be selected for update
|
||||
WITH CHECK (owner_id = current_user_id()); -- what the row must look like after update
|
||||
USING (owner_id = current_setting('app.current_user_id')::bigint) -- rows selectable for update
|
||||
WITH CHECK (owner_id = current_setting('app.current_user_id')::bigint); -- required post-update value
|
||||
|
||||
-- DELETE policy
|
||||
CREATE POLICY delete_own ON documents
|
||||
FOR DELETE
|
||||
USING (owner_id = current_user_id());
|
||||
USING (owner_id = current_setting('app.current_user_id')::bigint);
|
||||
|
||||
-- ALL (applies to all commands)
|
||||
CREATE POLICY full_access ON documents
|
||||
FOR ALL
|
||||
USING (owner_id = current_user_id())
|
||||
WITH CHECK (owner_id = current_user_id());
|
||||
USING (owner_id = current_setting('app.current_user_id')::bigint)
|
||||
WITH CHECK (owner_id = current_setting('app.current_user_id')::bigint);
|
||||
```
|
||||
|
||||
### Multiple Policies
|
||||
@@ -287,7 +289,7 @@ CREATE POLICY see_active ON orders
|
||||
USING (status = 'active');
|
||||
|
||||
CREATE POLICY see_own ON orders
|
||||
USING (user_id = current_user_id());
|
||||
USING (user_id = current_setting('app.current_user_id')::bigint);
|
||||
-- User sees rows that are active OR owned by them
|
||||
|
||||
-- Restrictive: combined with AND (with permissive policies)
|
||||
|
||||
@@ -198,7 +198,7 @@ Use SERIALIZABLE to prevent write skew.
|
||||
|
||||
## SERIALIZABLE
|
||||
|
||||
The strongest level. Transactions behave **as if they executed one at a time** (serially). PostgreSQL uses Serializable Snapshot Isolation (SSI) — an optimistic approach that detects conflicts at commit time rather than acquiring heavy locks upfront.
|
||||
The strongest level. Transactions behave **as if they executed one at a time** (serially). PostgreSQL uses Serializable Snapshot Isolation (SSI) — an optimistic approach that detects conflicts during statement execution or at commit rather than acquiring heavy locks upfront.
|
||||
|
||||
### Behavior
|
||||
|
||||
@@ -251,7 +251,7 @@ Keep transactions short regardless of isolation level.
|
||||
|
||||
### Pitfall: Mixing Isolation Levels
|
||||
|
||||
If Transaction A is SERIALIZABLE but Transaction B is READ COMMITTED, you only get SERIALIZABLE guarantees for A's view of the data. B can still see intermediate states. For full serializable behavior, **all participating transactions** must use SERIALIZABLE.
|
||||
If Transaction A is SERIALIZABLE but Transaction B is READ COMMITTED, you only get SERIALIZABLE guarantees for A's view of the data. B never sees uncommitted intermediate states, but it can observe newer committed states between statements and produce non-serializable outcomes. For full serializable behavior, **all participating transactions** must use SERIALIZABLE.
|
||||
|
||||
### Pitfall: DDL and Isolation
|
||||
|
||||
|
||||
Reference in New Issue
Block a user