[{"data":1,"prerenderedAt":3887},["ShallowReactive",2],{"page-\u002Fsql\u002F28-database-administration":3},{"id":4,"title":5,"body":6,"description":35,"extension":3881,"meta":3882,"navigation":81,"path":3883,"seo":3884,"stem":3885,"__hash__":3886},"content\u002Fsql\u002F28-database-administration.md","28 — Database Administration",{"type":7,"value":8,"toc":3853},"minimark",[9,13,18,26,369,374,481,485,488,630,634,983,1268,1272,1279,1283,1293,1297,1305,1309,1524,1528,1702,1706,1833,1837,1841,1849,1853,2020,2024,2239,2247,2251,2255,2513,2517,2833,2837,3097,3101,3248,3252,3362,3366,3530,3534,3544,3610,3849],[10,11,5],"h1",{"id":12},"_28-database-administration",[14,15,17],"h2",{"id":16},"backups-logical","Backups — Logical",[19,20,21,25],"p",{},[22,23,24],"code",{},"pg_dump"," produces a SQL script or custom archive that can restore on any PostgreSQL instance. Logical backups are portable across versions and architectures but slow for large databases.",[27,28,30],"code-wrapper",{"language":29},"bash",[31,32,36],"pre",{"className":33,"code":34,"language":29,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark","# Custom format (-Fc): compressed, supports parallel restore, selective table restore\n# This is the recommended format for most use cases\npg_dump -Fc -d mydb -f mydb.dump\n\n# Plain SQL: human-readable, can be piped to psql, but SLOW to restore\n# and can't be parallelized or selectively restored\npg_dump -d mydb -f mydb.sql\n\n# Parallel dump (PG 11+): use multiple jobs to speed up the dump\n# -j 4 uses 4 parallel connections to dump tables concurrently\npg_dump -Fc -j 4 -d mydb -f mydb.dump\n\n# Restore with parallel jobs (much faster than single-threaded)\npg_restore -j 4 -d newdb mydb.dump\n\n# Selective restore: restore only specific tables from a custom-format dump\npg_restore -d newdb -t orders -t customers mydb.dump\n\n# Dump a single table (useful for extracting one table from a large DB)\npg_dump -Fc -d mydb -t orders -f orders.dump\n\n# Dump with schema only (no data) — useful for creating a staging DB\npg_dump -Fc -d mydb --schema-only -f schema.dump\n\n# Dump with data only (schema already exists) — for data refresh\npg_dump -Fc -d mydb --data-only -f data.dump\n\n# ⚠️ pg_dump does NOT dump global objects: roles, tablespaces, database grants\n# Use pg_dumpall for those:\npg_dumpall --globals-only -f globals.sql  -- roles + tablespaces only\npg_dumpall -f all_databases.sql            -- ALL databases + globals\n","",[22,37,38,47,53,76,83,89,95,109,114,120,126,147,152,158,175,180,186,208,213,219,239,244,250,269,274,280,299,304,310,316,345],{"__ignoreMap":35},[39,40,43],"span",{"class":41,"line":42},"line",1,[39,44,46],{"class":45},"sdCPZ","# Custom format (-Fc): compressed, supports parallel restore, selective table restore\n",[39,48,50],{"class":41,"line":49},2,[39,51,52],{"class":45},"# This is the recommended format for most use cases\n",[39,54,56,59,63,66,70,73],{"class":41,"line":55},3,[39,57,24],{"class":58},"sIsaT",[39,60,62],{"class":61},"snvgF"," -Fc",[39,64,65],{"class":61}," -d",[39,67,69],{"class":68},"sJ6F3"," mydb",[39,71,72],{"class":61}," -f",[39,74,75],{"class":68}," mydb.dump\n",[39,77,79],{"class":41,"line":78},4,[39,80,82],{"emptyLinePlaceholder":81},true,"\n",[39,84,86],{"class":41,"line":85},5,[39,87,88],{"class":45},"# Plain SQL: human-readable, can be piped to psql, but SLOW to restore\n",[39,90,92],{"class":41,"line":91},6,[39,93,94],{"class":45},"# and can't be parallelized or selectively restored\n",[39,96,98,100,102,104,106],{"class":41,"line":97},7,[39,99,24],{"class":58},[39,101,65],{"class":61},[39,103,69],{"class":68},[39,105,72],{"class":61},[39,107,108],{"class":68}," mydb.sql\n",[39,110,112],{"class":41,"line":111},8,[39,113,82],{"emptyLinePlaceholder":81},[39,115,117],{"class":41,"line":116},9,[39,118,119],{"class":45},"# Parallel dump (PG 11+): use multiple jobs to speed up the dump\n",[39,121,123],{"class":41,"line":122},10,[39,124,125],{"class":45},"# -j 4 uses 4 parallel connections to dump tables concurrently\n",[39,127,129,131,133,136,139,141,143,145],{"class":41,"line":128},11,[39,130,24],{"class":58},[39,132,62],{"class":61},[39,134,135],{"class":61}," -j",[39,137,138],{"class":61}," 4",[39,140,65],{"class":61},[39,142,69],{"class":68},[39,144,72],{"class":61},[39,146,75],{"class":68},[39,148,150],{"class":41,"line":149},12,[39,151,82],{"emptyLinePlaceholder":81},[39,153,155],{"class":41,"line":154},13,[39,156,157],{"class":45},"# Restore with parallel jobs (much faster than single-threaded)\n",[39,159,161,164,166,168,170,173],{"class":41,"line":160},14,[39,162,163],{"class":58},"pg_restore",[39,165,135],{"class":61},[39,167,138],{"class":61},[39,169,65],{"class":61},[39,171,172],{"class":68}," newdb",[39,174,75],{"class":68},[39,176,178],{"class":41,"line":177},15,[39,179,82],{"emptyLinePlaceholder":81},[39,181,183],{"class":41,"line":182},16,[39,184,185],{"class":45},"# Selective restore: restore only specific tables from a custom-format dump\n",[39,187,189,191,193,195,198,201,203,206],{"class":41,"line":188},17,[39,190,163],{"class":58},[39,192,65],{"class":61},[39,194,172],{"class":68},[39,196,197],{"class":61}," -t",[39,199,200],{"class":68}," orders",[39,202,197],{"class":61},[39,204,205],{"class":68}," customers",[39,207,75],{"class":68},[39,209,211],{"class":41,"line":210},18,[39,212,82],{"emptyLinePlaceholder":81},[39,214,216],{"class":41,"line":215},19,[39,217,218],{"class":45},"# Dump a single table (useful for extracting one table from a large DB)\n",[39,220,222,224,226,228,230,232,234,236],{"class":41,"line":221},20,[39,223,24],{"class":58},[39,225,62],{"class":61},[39,227,65],{"class":61},[39,229,69],{"class":68},[39,231,197],{"class":61},[39,233,200],{"class":68},[39,235,72],{"class":61},[39,237,238],{"class":68}," orders.dump\n",[39,240,242],{"class":41,"line":241},21,[39,243,82],{"emptyLinePlaceholder":81},[39,245,247],{"class":41,"line":246},22,[39,248,249],{"class":45},"# Dump with schema only (no data) — useful for creating a staging DB\n",[39,251,253,255,257,259,261,264,266],{"class":41,"line":252},23,[39,254,24],{"class":58},[39,256,62],{"class":61},[39,258,65],{"class":61},[39,260,69],{"class":68},[39,262,263],{"class":61}," --schema-only",[39,265,72],{"class":61},[39,267,268],{"class":68}," schema.dump\n",[39,270,272],{"class":41,"line":271},24,[39,273,82],{"emptyLinePlaceholder":81},[39,275,277],{"class":41,"line":276},25,[39,278,279],{"class":45},"# Dump with data only (schema already exists) — for data refresh\n",[39,281,283,285,287,289,291,294,296],{"class":41,"line":282},26,[39,284,24],{"class":58},[39,286,62],{"class":61},[39,288,65],{"class":61},[39,290,69],{"class":68},[39,292,293],{"class":61}," --data-only",[39,295,72],{"class":61},[39,297,298],{"class":68}," data.dump\n",[39,300,302],{"class":41,"line":301},27,[39,303,82],{"emptyLinePlaceholder":81},[39,305,307],{"class":41,"line":306},28,[39,308,309],{"class":45},"# ⚠️ pg_dump does NOT dump global objects: roles, tablespaces, database grants\n",[39,311,313],{"class":41,"line":312},29,[39,314,315],{"class":45},"# Use pg_dumpall for those:\n",[39,317,319,322,325,327,330,333,336,339,342],{"class":41,"line":318},30,[39,320,321],{"class":58},"pg_dumpall",[39,323,324],{"class":61}," --globals-only",[39,326,72],{"class":61},[39,328,329],{"class":68}," globals.sql",[39,331,332],{"class":61},"  --",[39,334,335],{"class":68}," roles",[39,337,338],{"class":68}," +",[39,340,341],{"class":68}," tablespaces",[39,343,344],{"class":68}," only\n",[39,346,348,350,352,355,358,361,364,366],{"class":41,"line":347},31,[39,349,321],{"class":58},[39,351,72],{"class":61},[39,353,354],{"class":68}," all_databases.sql",[39,356,357],{"class":61},"            --",[39,359,360],{"class":68}," ALL",[39,362,363],{"class":68}," databases",[39,365,338],{"class":68},[39,367,368],{"class":68}," globals\n",[370,371,373],"h3",{"id":372},"backup-strategy-decision-matrix","Backup Strategy Decision Matrix",[375,376,377,396],"table",{},[378,379,380],"thead",{},[381,382,383,387,390,393],"tr",{},[384,385,386],"th",{},"Scenario",[384,388,389],{},"Tool",[384,391,392],{},"Frequency",[384,394,395],{},"Retention",[397,398,399,416,431,448,467],"tbody",{},[381,400,401,405,410,413],{},[402,403,404],"td",{},"Small DB (\u003C 10GB)",[402,406,407],{},[22,408,409],{},"pg_dump -Fc",[402,411,412],{},"Daily",[402,414,415],{},"30 days",[381,417,418,421,426,428],{},[402,419,420],{},"Medium DB (10-100GB)",[402,422,423],{},[22,424,425],{},"pg_dump -Fc -j 4",[402,427,412],{},[402,429,430],{},"14 days",[381,432,433,436,442,445],{},[402,434,435],{},"Large DB (100GB+)",[402,437,438,441],{},[22,439,440],{},"pg_basebackup"," + WAL",[402,443,444],{},"Continuous WAL + weekly base",[402,446,447],{},"7 days PITR",[381,449,450,453,461,464],{},[402,451,452],{},"Cross-version migration",[402,454,455,457,458,460],{},[22,456,24],{}," from old → ",[22,459,163],{}," to new",[402,462,463],{},"One-time",[402,465,466],{},"N\u002FA",[381,468,469,472,477,479],{},[402,470,471],{},"Global objects (roles)",[402,473,474],{},[22,475,476],{},"pg_dumpall --globals-only",[402,478,412],{},[402,480,415],{},[14,482,484],{"id":483},"backups-physical","Backups — Physical",[19,486,487],{},"Physical backups copy the data files directly. Faster for large databases, supports PITR, and is the foundation of streaming replication.",[27,489,490],{"language":29},[31,491,493],{"className":33,"code":492,"language":29,"meta":35,"style":35},"# pg_basebackup: creates a binary copy of the entire data directory\n# -Ft: tar format, -z: gzip compressed, -P: show progress, -R: write recovery config\npg_basebackup -h primary -D \u002Fbackup\u002Fbase -Ft -z -P -R\n\n# -R creates standby.signal + primary_conninfo in the backup,\n# making it ready to start as a streaming replica immediately\n\n# For PITR (Point-in-Time Recovery): you need the base backup + WAL archives\n# Configure WAL archiving in postgresql.conf:\n#   archive_mode = on\n#   archive_command = 'test ! -f \u002Fbackup\u002Fwal\u002F%f && cp %p \u002Fbackup\u002Fwal\u002F%f'\n#   wal_level = replica\n# The archive_command must be idempotent: the test prevents overwriting\n\n# Restore to a specific point in time:\n# 1. Stop PostgreSQL\n# 2. Replace data directory with base backup\n# 3. Create recovery.signal in data directory\n# 4. Configure recovery in postgresql.auto.conf:\n#    restore_command = 'cp \u002Fbackup\u002Fwal\u002F%f %p'\n#    recovery_target_time = '2024-06-15 14:30:00+00'\n#    recovery_target_action = 'promote'\n# 5. Start PostgreSQL — it replays WAL up to the target time, then promotes\n",[22,494,495,500,505,533,537,542,547,551,556,561,566,571,576,581,585,590,595,600,605,610,615,620,625],{"__ignoreMap":35},[39,496,497],{"class":41,"line":42},[39,498,499],{"class":45},"# pg_basebackup: creates a binary copy of the entire data directory\n",[39,501,502],{"class":41,"line":49},[39,503,504],{"class":45},"# -Ft: tar format, -z: gzip compressed, -P: show progress, -R: write recovery config\n",[39,506,507,509,512,515,518,521,524,527,530],{"class":41,"line":55},[39,508,440],{"class":58},[39,510,511],{"class":61}," -h",[39,513,514],{"class":68}," primary",[39,516,517],{"class":61}," -D",[39,519,520],{"class":68}," \u002Fbackup\u002Fbase",[39,522,523],{"class":61}," -Ft",[39,525,526],{"class":61}," -z",[39,528,529],{"class":61}," -P",[39,531,532],{"class":61}," -R\n",[39,534,535],{"class":41,"line":78},[39,536,82],{"emptyLinePlaceholder":81},[39,538,539],{"class":41,"line":85},[39,540,541],{"class":45},"# -R creates standby.signal + primary_conninfo in the backup,\n",[39,543,544],{"class":41,"line":91},[39,545,546],{"class":45},"# making it ready to start as a streaming replica immediately\n",[39,548,549],{"class":41,"line":97},[39,550,82],{"emptyLinePlaceholder":81},[39,552,553],{"class":41,"line":111},[39,554,555],{"class":45},"# For PITR (Point-in-Time Recovery): you need the base backup + WAL archives\n",[39,557,558],{"class":41,"line":116},[39,559,560],{"class":45},"# Configure WAL archiving in postgresql.conf:\n",[39,562,563],{"class":41,"line":122},[39,564,565],{"class":45},"#   archive_mode = on\n",[39,567,568],{"class":41,"line":128},[39,569,570],{"class":45},"#   archive_command = 'test ! -f \u002Fbackup\u002Fwal\u002F%f && cp %p \u002Fbackup\u002Fwal\u002F%f'\n",[39,572,573],{"class":41,"line":149},[39,574,575],{"class":45},"#   wal_level = replica\n",[39,577,578],{"class":41,"line":154},[39,579,580],{"class":45},"# The archive_command must be idempotent: the test prevents overwriting\n",[39,582,583],{"class":41,"line":160},[39,584,82],{"emptyLinePlaceholder":81},[39,586,587],{"class":41,"line":177},[39,588,589],{"class":45},"# Restore to a specific point in time:\n",[39,591,592],{"class":41,"line":182},[39,593,594],{"class":45},"# 1. Stop PostgreSQL\n",[39,596,597],{"class":41,"line":188},[39,598,599],{"class":45},"# 2. Replace data directory with base backup\n",[39,601,602],{"class":41,"line":210},[39,603,604],{"class":45},"# 3. Create recovery.signal in data directory\n",[39,606,607],{"class":41,"line":215},[39,608,609],{"class":45},"# 4. Configure recovery in postgresql.auto.conf:\n",[39,611,612],{"class":41,"line":221},[39,613,614],{"class":45},"#    restore_command = 'cp \u002Fbackup\u002Fwal\u002F%f %p'\n",[39,616,617],{"class":41,"line":241},[39,618,619],{"class":45},"#    recovery_target_time = '2024-06-15 14:30:00+00'\n",[39,621,622],{"class":41,"line":246},[39,623,624],{"class":45},"#    recovery_target_action = 'promote'\n",[39,626,627],{"class":41,"line":252},[39,628,629],{"class":45},"# 5. Start PostgreSQL — it replays WAL up to the target time, then promotes\n",[14,631,633],{"id":632},"complex-example-production-backup-recovery-strategy","Complex Example — Production Backup + Recovery Strategy",[27,635,636],{"language":29},[31,637,639],{"className":33,"code":638,"language":29,"meta":35,"style":35},"#!\u002Fbin\u002Fbash\n# production_backup.sh — daily backup script with rotation + verification\nset -euo pipefail\n\nBACKUP_DIR=\u002Fbackup\u002Fpostgres\nRETENTION_DAYS=7\nDB_NAME=production\nDATE=$(date +%Y%m%d_%H%M%S)\n\n# ─── 1. Dump global objects (roles, tablespaces) ───\n# Without this, a restore has no roles and all GRANTs fail\npg_dumpall --globals-only -f \"$BACKUP_DIR\u002Fglobals_$DATE.sql\"\n\n# ─── 2. Logical backup (custom format, parallel) ───\n# -Fc: custom compressed format (supports parallel + selective restore)\n# -j 4: 4 parallel jobs for faster dump\npg_dump -Fc -j 4 -d \"$DB_NAME\" -f \"$BACKUP_DIR\u002F${DB_NAME}_$DATE.dump\"\n\n# ─── 3. Verify the backup (critical — an untested backup is no backup) ───\n# Use pg_restore --list to verify the archive is readable\nif ! pg_restore --list \"$BACKUP_DIR\u002F${DB_NAME}_$DATE.dump\" > \u002Fdev\u002Fnull 2>&1; then\n  echo \"ERROR: Backup verification failed for ${DB_NAME}_$DATE.dump\" >&2\n  exit 1\nfi\n\n# ─── 4. Rotate: delete backups older than RETENTION_DAYS ───\nfind \"$BACKUP_DIR\" -name \"*.dump\" -mtime +$RETENTION_DAYS -delete\nfind \"$BACKUP_DIR\" -name \"globals_*.sql\" -mtime +$RETENTION_DAYS -delete\n\necho \"Backup completed: ${DB_NAME}_$DATE.dump\"\n",[22,640,641,646,651,662,666,679,689,699,718,722,727,732,755,759,764,769,774,813,817,822,827,871,890,898,903,907,912,940,963,967],{"__ignoreMap":35},[39,642,643],{"class":41,"line":42},[39,644,645],{"class":45},"#!\u002Fbin\u002Fbash\n",[39,647,648],{"class":41,"line":49},[39,649,650],{"class":45},"# production_backup.sh — daily backup script with rotation + verification\n",[39,652,653,656,659],{"class":41,"line":55},[39,654,655],{"class":61},"set",[39,657,658],{"class":61}," -euo",[39,660,661],{"class":68}," pipefail\n",[39,663,664],{"class":41,"line":78},[39,665,82],{"emptyLinePlaceholder":81},[39,667,668,672,676],{"class":41,"line":85},[39,669,671],{"class":670},"ssxIu","BACKUP_DIR",[39,673,675],{"class":674},"svdQ7","=",[39,677,678],{"class":68},"\u002Fbackup\u002Fpostgres\n",[39,680,681,684,686],{"class":41,"line":91},[39,682,683],{"class":670},"RETENTION_DAYS",[39,685,675],{"class":674},[39,687,688],{"class":68},"7\n",[39,690,691,694,696],{"class":41,"line":97},[39,692,693],{"class":670},"DB_NAME",[39,695,675],{"class":674},[39,697,698],{"class":68},"production\n",[39,700,701,704,706,709,712,715],{"class":41,"line":111},[39,702,703],{"class":670},"DATE",[39,705,675],{"class":674},[39,707,708],{"class":670},"$(",[39,710,711],{"class":58},"date",[39,713,714],{"class":68}," +%Y%m%d_%H%M%S",[39,716,717],{"class":670},")\n",[39,719,720],{"class":41,"line":116},[39,721,82],{"emptyLinePlaceholder":81},[39,723,724],{"class":41,"line":122},[39,725,726],{"class":45},"# ─── 1. Dump global objects (roles, tablespaces) ───\n",[39,728,729],{"class":41,"line":128},[39,730,731],{"class":45},"# Without this, a restore has no roles and all GRANTs fail\n",[39,733,734,736,738,740,743,746,749,752],{"class":41,"line":149},[39,735,321],{"class":58},[39,737,324],{"class":61},[39,739,72],{"class":61},[39,741,742],{"class":68}," \"",[39,744,745],{"class":670},"$BACKUP_DIR",[39,747,748],{"class":68},"\u002Fglobals_",[39,750,751],{"class":670},"$DATE",[39,753,754],{"class":68},".sql\"\n",[39,756,757],{"class":41,"line":154},[39,758,82],{"emptyLinePlaceholder":81},[39,760,761],{"class":41,"line":160},[39,762,763],{"class":45},"# ─── 2. Logical backup (custom format, parallel) ───\n",[39,765,766],{"class":41,"line":177},[39,767,768],{"class":45},"# -Fc: custom compressed format (supports parallel + selective restore)\n",[39,770,771],{"class":41,"line":182},[39,772,773],{"class":45},"# -j 4: 4 parallel jobs for faster dump\n",[39,775,776,778,780,782,784,786,788,791,794,796,798,800,803,805,808,810],{"class":41,"line":188},[39,777,24],{"class":58},[39,779,62],{"class":61},[39,781,135],{"class":61},[39,783,138],{"class":61},[39,785,65],{"class":61},[39,787,742],{"class":68},[39,789,790],{"class":670},"$DB_NAME",[39,792,793],{"class":68},"\"",[39,795,72],{"class":61},[39,797,742],{"class":68},[39,799,745],{"class":670},[39,801,802],{"class":68},"\u002F${",[39,804,693],{"class":670},[39,806,807],{"class":68},"}_",[39,809,751],{"class":670},[39,811,812],{"class":68},".dump\"\n",[39,814,815],{"class":41,"line":210},[39,816,82],{"emptyLinePlaceholder":81},[39,818,819],{"class":41,"line":215},[39,820,821],{"class":45},"# ─── 3. Verify the backup (critical — an untested backup is no backup) ───\n",[39,823,824],{"class":41,"line":221},[39,825,826],{"class":45},"# Use pg_restore --list to verify the archive is readable\n",[39,828,829,832,835,838,841,843,845,847,849,851,853,856,859,862,865,868],{"class":41,"line":241},[39,830,831],{"class":674},"if",[39,833,834],{"class":674}," !",[39,836,837],{"class":58}," pg_restore",[39,839,840],{"class":61}," --list",[39,842,742],{"class":68},[39,844,745],{"class":670},[39,846,802],{"class":68},[39,848,693],{"class":670},[39,850,807],{"class":68},[39,852,751],{"class":670},[39,854,855],{"class":68},".dump\"",[39,857,858],{"class":674}," >",[39,860,861],{"class":68}," \u002Fdev\u002Fnull",[39,863,864],{"class":674}," 2>&1",[39,866,867],{"class":670},"; ",[39,869,870],{"class":674},"then\n",[39,872,873,876,879,881,883,885,887],{"class":41,"line":246},[39,874,875],{"class":61},"  echo",[39,877,878],{"class":68}," \"ERROR: Backup verification failed for ${",[39,880,693],{"class":670},[39,882,807],{"class":68},[39,884,751],{"class":670},[39,886,855],{"class":68},[39,888,889],{"class":674}," >&2\n",[39,891,892,895],{"class":41,"line":252},[39,893,894],{"class":61},"  exit",[39,896,897],{"class":61}," 1\n",[39,899,900],{"class":41,"line":271},[39,901,902],{"class":674},"fi\n",[39,904,905],{"class":41,"line":276},[39,906,82],{"emptyLinePlaceholder":81},[39,908,909],{"class":41,"line":282},[39,910,911],{"class":45},"# ─── 4. Rotate: delete backups older than RETENTION_DAYS ───\n",[39,913,914,917,919,921,923,926,929,932,934,937],{"class":41,"line":301},[39,915,916],{"class":58},"find",[39,918,742],{"class":68},[39,920,745],{"class":670},[39,922,793],{"class":68},[39,924,925],{"class":61}," -name",[39,927,928],{"class":68}," \"*.dump\"",[39,930,931],{"class":61}," -mtime",[39,933,338],{"class":68},[39,935,936],{"class":670},"$RETENTION_DAYS ",[39,938,939],{"class":61},"-delete\n",[39,941,942,944,946,948,950,952,955,957,959,961],{"class":41,"line":306},[39,943,916],{"class":58},[39,945,742],{"class":68},[39,947,745],{"class":670},[39,949,793],{"class":68},[39,951,925],{"class":61},[39,953,954],{"class":68}," \"globals_*.sql\"",[39,956,931],{"class":61},[39,958,338],{"class":68},[39,960,936],{"class":670},[39,962,939],{"class":61},[39,964,965],{"class":41,"line":312},[39,966,82],{"emptyLinePlaceholder":81},[39,968,969,972,975,977,979,981],{"class":41,"line":318},[39,970,971],{"class":61},"echo",[39,973,974],{"class":68}," \"Backup completed: ${",[39,976,693],{"class":670},[39,978,807],{"class":68},[39,980,751],{"class":670},[39,982,812],{"class":68},[27,984,985],{"language":29},[31,986,988],{"className":33,"code":987,"language":29,"meta":35,"style":35},"#!\u002Fbin\u002Fbash\n# pitr_restore.sh — point-in-time recovery from base backup + WAL\nset -euo pipefail\n\nDATA_DIR=\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\nBASE_BACKUP=\u002Fbackup\u002Fbase\u002Flatest.tar\nWAL_DIR=\u002Fbackup\u002Fwal\nTARGET_TIME=\"2024-06-15 14:30:00+00\"\n\n# ─── 1. Stop PostgreSQL ───\npg_ctl -D \"$DATA_DIR\" stop -m fast\n\n# ─── 2. Replace data directory with base backup ───\nrm -rf \"$DATA_DIR\"\nmkdir -p \"$DATA_DIR\"\ntar xzf \"$BASE_BACKUP\" -C \"$DATA_DIR\"\n\n# ─── 3. Create recovery.signal (tells PG to enter recovery mode) ───\ntouch \"$DATA_DIR\u002Frecovery.signal\"\n\n# ─── 4. Configure recovery target ───\ncat >> \"$DATA_DIR\u002Fpostgresql.auto.conf\" \u003C\u003C EOF\nrestore_command = 'cp $WAL_DIR\u002F%f %p'\nrecovery_target_time = '$TARGET_TIME'\nrecovery_target_action = 'promote'\nEOF\n\n# ─── 5. Start PostgreSQL — replays WAL to target time, then promotes ───\npg_ctl -D \"$DATA_DIR\" start\n# PG logs: \"recovery stopping before commit of transaction N at (time)\"\n# Then: \"database system is ready to accept read only connections\" (during)\n# Then: \"archive recovery complete\" and promotion to read-write\n",[22,989,990,994,999,1007,1011,1021,1031,1041,1051,1055,1060,1083,1087,1092,1107,1121,1145,1149,1154,1166,1170,1175,1196,1207,1218,1223,1228,1232,1237,1252,1257,1262],{"__ignoreMap":35},[39,991,992],{"class":41,"line":42},[39,993,645],{"class":45},[39,995,996],{"class":41,"line":49},[39,997,998],{"class":45},"# pitr_restore.sh — point-in-time recovery from base backup + WAL\n",[39,1000,1001,1003,1005],{"class":41,"line":55},[39,1002,655],{"class":61},[39,1004,658],{"class":61},[39,1006,661],{"class":68},[39,1008,1009],{"class":41,"line":78},[39,1010,82],{"emptyLinePlaceholder":81},[39,1012,1013,1016,1018],{"class":41,"line":85},[39,1014,1015],{"class":670},"DATA_DIR",[39,1017,675],{"class":674},[39,1019,1020],{"class":68},"\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n",[39,1022,1023,1026,1028],{"class":41,"line":91},[39,1024,1025],{"class":670},"BASE_BACKUP",[39,1027,675],{"class":674},[39,1029,1030],{"class":68},"\u002Fbackup\u002Fbase\u002Flatest.tar\n",[39,1032,1033,1036,1038],{"class":41,"line":97},[39,1034,1035],{"class":670},"WAL_DIR",[39,1037,675],{"class":674},[39,1039,1040],{"class":68},"\u002Fbackup\u002Fwal\n",[39,1042,1043,1046,1048],{"class":41,"line":111},[39,1044,1045],{"class":670},"TARGET_TIME",[39,1047,675],{"class":674},[39,1049,1050],{"class":68},"\"2024-06-15 14:30:00+00\"\n",[39,1052,1053],{"class":41,"line":116},[39,1054,82],{"emptyLinePlaceholder":81},[39,1056,1057],{"class":41,"line":122},[39,1058,1059],{"class":45},"# ─── 1. Stop PostgreSQL ───\n",[39,1061,1062,1065,1067,1069,1072,1074,1077,1080],{"class":41,"line":128},[39,1063,1064],{"class":58},"pg_ctl",[39,1066,517],{"class":61},[39,1068,742],{"class":68},[39,1070,1071],{"class":670},"$DATA_DIR",[39,1073,793],{"class":68},[39,1075,1076],{"class":68}," stop",[39,1078,1079],{"class":61}," -m",[39,1081,1082],{"class":68}," fast\n",[39,1084,1085],{"class":41,"line":149},[39,1086,82],{"emptyLinePlaceholder":81},[39,1088,1089],{"class":41,"line":154},[39,1090,1091],{"class":45},"# ─── 2. Replace data directory with base backup ───\n",[39,1093,1094,1097,1100,1102,1104],{"class":41,"line":160},[39,1095,1096],{"class":58},"rm",[39,1098,1099],{"class":61}," -rf",[39,1101,742],{"class":68},[39,1103,1071],{"class":670},[39,1105,1106],{"class":68},"\"\n",[39,1108,1109,1112,1115,1117,1119],{"class":41,"line":177},[39,1110,1111],{"class":58},"mkdir",[39,1113,1114],{"class":61}," -p",[39,1116,742],{"class":68},[39,1118,1071],{"class":670},[39,1120,1106],{"class":68},[39,1122,1123,1126,1129,1131,1134,1136,1139,1141,1143],{"class":41,"line":182},[39,1124,1125],{"class":58},"tar",[39,1127,1128],{"class":68}," xzf",[39,1130,742],{"class":68},[39,1132,1133],{"class":670},"$BASE_BACKUP",[39,1135,793],{"class":68},[39,1137,1138],{"class":61}," -C",[39,1140,742],{"class":68},[39,1142,1071],{"class":670},[39,1144,1106],{"class":68},[39,1146,1147],{"class":41,"line":188},[39,1148,82],{"emptyLinePlaceholder":81},[39,1150,1151],{"class":41,"line":210},[39,1152,1153],{"class":45},"# ─── 3. Create recovery.signal (tells PG to enter recovery mode) ───\n",[39,1155,1156,1159,1161,1163],{"class":41,"line":215},[39,1157,1158],{"class":58},"touch",[39,1160,742],{"class":68},[39,1162,1071],{"class":670},[39,1164,1165],{"class":68},"\u002Frecovery.signal\"\n",[39,1167,1168],{"class":41,"line":221},[39,1169,82],{"emptyLinePlaceholder":81},[39,1171,1172],{"class":41,"line":241},[39,1173,1174],{"class":45},"# ─── 4. Configure recovery target ───\n",[39,1176,1177,1180,1183,1185,1187,1190,1193],{"class":41,"line":246},[39,1178,1179],{"class":58},"cat",[39,1181,1182],{"class":674}," >>",[39,1184,742],{"class":68},[39,1186,1071],{"class":670},[39,1188,1189],{"class":68},"\u002Fpostgresql.auto.conf\"",[39,1191,1192],{"class":674}," \u003C\u003C",[39,1194,1195],{"class":68}," EOF\n",[39,1197,1198,1201,1204],{"class":41,"line":252},[39,1199,1200],{"class":68},"restore_command = 'cp ",[39,1202,1203],{"class":670},"$WAL_DIR",[39,1205,1206],{"class":68},"\u002F%f %p'\n",[39,1208,1209,1212,1215],{"class":41,"line":271},[39,1210,1211],{"class":68},"recovery_target_time = '",[39,1213,1214],{"class":670},"$TARGET_TIME",[39,1216,1217],{"class":68},"'\n",[39,1219,1220],{"class":41,"line":276},[39,1221,1222],{"class":68},"recovery_target_action = 'promote'\n",[39,1224,1225],{"class":41,"line":282},[39,1226,1227],{"class":68},"EOF\n",[39,1229,1230],{"class":41,"line":301},[39,1231,82],{"emptyLinePlaceholder":81},[39,1233,1234],{"class":41,"line":306},[39,1235,1236],{"class":45},"# ─── 5. Start PostgreSQL — replays WAL to target time, then promotes ───\n",[39,1238,1239,1241,1243,1245,1247,1249],{"class":41,"line":312},[39,1240,1064],{"class":58},[39,1242,517],{"class":61},[39,1244,742],{"class":68},[39,1246,1071],{"class":670},[39,1248,793],{"class":68},[39,1250,1251],{"class":68}," start\n",[39,1253,1254],{"class":41,"line":318},[39,1255,1256],{"class":45},"# PG logs: \"recovery stopping before commit of transaction N at (time)\"\n",[39,1258,1259],{"class":41,"line":347},[39,1260,1261],{"class":45},"# Then: \"database system is ready to accept read only connections\" (during)\n",[39,1263,1265],{"class":41,"line":1264},32,[39,1266,1267],{"class":45},"# Then: \"archive recovery complete\" and promotion to read-write\n",[14,1269,1271],{"id":1270},"vacuum-and-bloat","VACUUM and Bloat",[19,1273,1274,1275,1278],{},"PostgreSQL's MVCC creates new row versions on UPDATE\u002FDELETE. Old versions (\"dead tuples\") remain in the heap until ",[22,1276,1277],{},"VACUUM"," reclaims them. Without vacuuming, tables and indexes bloat — scans slow, cache efficiency drops.",[370,1280,1282],{"id":1281},"how-mvcc-creates-dead-tuples","How MVCC Creates Dead Tuples",[27,1284,1286],{"language":1285},"text",[31,1287,1291],{"className":1288,"code":1290,"language":1285,"meta":35},[1289],"language-text","-- UPDATE in PostgreSQL is: INSERT new version + mark old version as dead\n-- The old version is NOT immediately removed — other transactions may\n-- still need to see it (snapshot isolation). It becomes a \"dead tuple\".\n\n-- Row at T1: {id=1, amount=100, xmin=T1, xmax=∞}  → visible\n-- UPDATE at T2: {id=1, amount=100, xmin=T1, xmax=T2}  → dead (xmax set)\n--                {id=1, amount=150, xmin=T2, xmax=∞}  → visible\n--\n-- VACUUM: removes the dead tuple (xmax=T2) → free space for reuse\n-- But the heap file doesn't shrink — the space is marked free, not\n-- returned to the OS. Only VACUUM FULL (or pg_repack) shrinks the file.\n",[22,1292,1290],{"__ignoreMap":35},[370,1294,1296],{"id":1295},"autovacuum-configuration","Autovacuum Configuration",[27,1298,1299],{"language":1285},[31,1300,1303],{"className":1301,"code":1302,"language":1285,"meta":35},[1289],"# postgresql.conf — global autovacuum settings\nautovacuum = on                    # enabled by default, leave on\nautovacuum_max_workers = 3         # max concurrent autovacuum workers\nautovacuum_naptime = 1min          # how often to check tables for vacuum need\nautovacuum_vacuum_cost_limit = 200 # I\u002FO budget per autovacuum (higher = faster)\nautovacuum_vacuum_cost_delay = 2ms # throttle between I\u002FO operations\n\n# Default vacuum trigger: dead_tuples > threshold + scale_factor × total_rows\nautovacuum_vacuum_threshold = 50        # minimum dead tuples\nautovacuum_vacuum_scale_factor = 0.2    # + 20% of total rows\n# A table with 1M rows is vacuumed when dead_tuples > 50 + 0.2 × 1M = 200050\n# That's 200K dead tuples before vacuuming — too lenient for hot tables\n",[22,1304,1302],{"__ignoreMap":35},[370,1306,1308],{"id":1307},"per-table-autovacuum-tuning","Per-Table Autovacuum Tuning",[27,1310,1312],{"language":1311},"sql",[31,1313,1316],{"className":1314,"code":1315,"language":1311,"meta":35,"style":35},"language-sql shiki shiki-themes github-light github-dark","-- For high-churn tables (frequent UPDATE\u002FDELETE), vacuum more aggressively\n-- Lower scale_factor → vacuum when fewer dead tuples accumulate\nALTER TABLE events SET (\n  autovacuum_vacuum_scale_factor = 0.05,   -- vacuum at 5% dead (not 20%)\n  autovacuum_analyze_scale_factor = 0.02,  -- analyze at 2% changed (not 10%)\n  autovacuum_vacuum_threshold = 1000        -- minimum 1000 dead tuples\n);\n\n-- For append-only tables (INSERT only, no UPDATE\u002FDELETE), be lenient\n-- They don't produce dead tuples, so aggressive vacuuming wastes resources\nALTER TABLE audit_log SET (\n  autovacuum_vacuum_scale_factor = 0.5,    -- vacuum at 50% (rarely needed)\n  autovacuum_insert_scale_factor = 0.1     -- analyze on inserts at 10%\n);\n\n-- Check current per-table settings:\nSELECT relname, reloptions\nFROM pg_class\nWHERE relname = 'events' AND relkind = 'r';\n",[22,1317,1318,1323,1328,1345,1367,1387,1400,1405,1409,1414,1419,1432,1451,1468,1472,1476,1481,1489,1497],{"__ignoreMap":35},[39,1319,1320],{"class":41,"line":42},[39,1321,1322],{"class":45},"-- For high-churn tables (frequent UPDATE\u002FDELETE), vacuum more aggressively\n",[39,1324,1325],{"class":41,"line":49},[39,1326,1327],{"class":45},"-- Lower scale_factor → vacuum when fewer dead tuples accumulate\n",[39,1329,1330,1333,1336,1339,1342],{"class":41,"line":55},[39,1331,1332],{"class":674},"ALTER",[39,1334,1335],{"class":674}," TABLE",[39,1337,1338],{"class":670}," events ",[39,1340,1341],{"class":674},"SET",[39,1343,1344],{"class":670}," (\n",[39,1346,1347,1350,1352,1355,1358,1361,1364],{"class":41,"line":78},[39,1348,1349],{"class":670},"  autovacuum_vacuum_scale_factor ",[39,1351,675],{"class":674},[39,1353,1354],{"class":61}," 0",[39,1356,1357],{"class":670},".",[39,1359,1360],{"class":61},"05",[39,1362,1363],{"class":670},",   ",[39,1365,1366],{"class":45},"-- vacuum at 5% dead (not 20%)\n",[39,1368,1369,1372,1374,1376,1378,1381,1384],{"class":41,"line":85},[39,1370,1371],{"class":670},"  autovacuum_analyze_scale_factor ",[39,1373,675],{"class":674},[39,1375,1354],{"class":61},[39,1377,1357],{"class":670},[39,1379,1380],{"class":61},"02",[39,1382,1383],{"class":670},",  ",[39,1385,1386],{"class":45},"-- analyze at 2% changed (not 10%)\n",[39,1388,1389,1392,1394,1397],{"class":41,"line":91},[39,1390,1391],{"class":670},"  autovacuum_vacuum_threshold ",[39,1393,675],{"class":674},[39,1395,1396],{"class":61}," 1000",[39,1398,1399],{"class":45},"        -- minimum 1000 dead tuples\n",[39,1401,1402],{"class":41,"line":97},[39,1403,1404],{"class":670},");\n",[39,1406,1407],{"class":41,"line":111},[39,1408,82],{"emptyLinePlaceholder":81},[39,1410,1411],{"class":41,"line":116},[39,1412,1413],{"class":45},"-- For append-only tables (INSERT only, no UPDATE\u002FDELETE), be lenient\n",[39,1415,1416],{"class":41,"line":122},[39,1417,1418],{"class":45},"-- They don't produce dead tuples, so aggressive vacuuming wastes resources\n",[39,1420,1421,1423,1425,1428,1430],{"class":41,"line":128},[39,1422,1332],{"class":674},[39,1424,1335],{"class":674},[39,1426,1427],{"class":670}," audit_log ",[39,1429,1341],{"class":674},[39,1431,1344],{"class":670},[39,1433,1434,1436,1438,1440,1442,1445,1448],{"class":41,"line":149},[39,1435,1349],{"class":670},[39,1437,675],{"class":674},[39,1439,1354],{"class":61},[39,1441,1357],{"class":670},[39,1443,1444],{"class":61},"5",[39,1446,1447],{"class":670},",    ",[39,1449,1450],{"class":45},"-- vacuum at 50% (rarely needed)\n",[39,1452,1453,1456,1458,1460,1462,1465],{"class":41,"line":154},[39,1454,1455],{"class":670},"  autovacuum_insert_scale_factor ",[39,1457,675],{"class":674},[39,1459,1354],{"class":61},[39,1461,1357],{"class":670},[39,1463,1464],{"class":61},"1",[39,1466,1467],{"class":45},"     -- analyze on inserts at 10%\n",[39,1469,1470],{"class":41,"line":160},[39,1471,1404],{"class":670},[39,1473,1474],{"class":41,"line":177},[39,1475,82],{"emptyLinePlaceholder":81},[39,1477,1478],{"class":41,"line":182},[39,1479,1480],{"class":45},"-- Check current per-table settings:\n",[39,1482,1483,1486],{"class":41,"line":188},[39,1484,1485],{"class":674},"SELECT",[39,1487,1488],{"class":670}," relname, reloptions\n",[39,1490,1491,1494],{"class":41,"line":210},[39,1492,1493],{"class":674},"FROM",[39,1495,1496],{"class":670}," pg_class\n",[39,1498,1499,1502,1505,1507,1510,1513,1516,1518,1521],{"class":41,"line":215},[39,1500,1501],{"class":674},"WHERE",[39,1503,1504],{"class":670}," relname ",[39,1506,675],{"class":674},[39,1508,1509],{"class":68}," 'events'",[39,1511,1512],{"class":674}," AND",[39,1514,1515],{"class":670}," relkind ",[39,1517,675],{"class":674},[39,1519,1520],{"class":68}," 'r'",[39,1522,1523],{"class":670},";\n",[370,1525,1527],{"id":1526},"manual-vacuum-operations","Manual VACUUM Operations",[27,1529,1530],{"language":1311},[31,1531,1533],{"className":1314,"code":1532,"language":1311,"meta":35,"style":35},"-- VACUUM (plain): reclaims dead tuples for reuse, does NOT shrink the file\n-- No lock held — concurrent reads and writes proceed normally\nVACUUM orders;\n\n-- VACUUM ANALYZE: reclaim + update planner statistics\nVACUUM ANALYZE orders;\n\n-- VACUUM FULL: rewrites the table, shrinks the file, returns space to OS\n-- ⚠️ Takes ACCESS EXCLUSIVE lock — NO reads or writes during the rewrite\n-- On a large table, this is DOWNTIME. Use pg_repack for zero-lock rewriting.\nVACUUM FULL orders;\n\n-- ANALYZE only: update planner statistics without vacuuming\nANALYZE orders;\n\n-- Check if a table needs vacuuming (dead tuple ratio):\nSELECT relname, n_live_tup, n_dead_tup,\n  round(n_dead_tup::numeric \u002F NULLIF(n_live_tup, 0) * 100, 2) AS dead_pct,\n  last_autovacuum, last_autoanalyze\nFROM pg_stat_user_tables\nWHERE n_live_tup > 0\nORDER BY dead_pct DESC;\n",[22,1534,1535,1540,1545,1550,1554,1559,1564,1568,1573,1578,1583,1588,1592,1597,1602,1606,1611,1618,1664,1669,1676,1689],{"__ignoreMap":35},[39,1536,1537],{"class":41,"line":42},[39,1538,1539],{"class":45},"-- VACUUM (plain): reclaims dead tuples for reuse, does NOT shrink the file\n",[39,1541,1542],{"class":41,"line":49},[39,1543,1544],{"class":45},"-- No lock held — concurrent reads and writes proceed normally\n",[39,1546,1547],{"class":41,"line":55},[39,1548,1549],{"class":670},"VACUUM orders;\n",[39,1551,1552],{"class":41,"line":78},[39,1553,82],{"emptyLinePlaceholder":81},[39,1555,1556],{"class":41,"line":85},[39,1557,1558],{"class":45},"-- VACUUM ANALYZE: reclaim + update planner statistics\n",[39,1560,1561],{"class":41,"line":91},[39,1562,1563],{"class":670},"VACUUM ANALYZE orders;\n",[39,1565,1566],{"class":41,"line":97},[39,1567,82],{"emptyLinePlaceholder":81},[39,1569,1570],{"class":41,"line":111},[39,1571,1572],{"class":45},"-- VACUUM FULL: rewrites the table, shrinks the file, returns space to OS\n",[39,1574,1575],{"class":41,"line":116},[39,1576,1577],{"class":45},"-- ⚠️ Takes ACCESS EXCLUSIVE lock — NO reads or writes during the rewrite\n",[39,1579,1580],{"class":41,"line":122},[39,1581,1582],{"class":45},"-- On a large table, this is DOWNTIME. Use pg_repack for zero-lock rewriting.\n",[39,1584,1585],{"class":41,"line":128},[39,1586,1587],{"class":670},"VACUUM FULL orders;\n",[39,1589,1590],{"class":41,"line":149},[39,1591,82],{"emptyLinePlaceholder":81},[39,1593,1594],{"class":41,"line":154},[39,1595,1596],{"class":45},"-- ANALYZE only: update planner statistics without vacuuming\n",[39,1598,1599],{"class":41,"line":160},[39,1600,1601],{"class":670},"ANALYZE orders;\n",[39,1603,1604],{"class":41,"line":177},[39,1605,82],{"emptyLinePlaceholder":81},[39,1607,1608],{"class":41,"line":182},[39,1609,1610],{"class":45},"-- Check if a table needs vacuuming (dead tuple ratio):\n",[39,1612,1613,1615],{"class":41,"line":188},[39,1614,1485],{"class":674},[39,1616,1617],{"class":670}," relname, n_live_tup, n_dead_tup,\n",[39,1619,1620,1623,1626,1629,1632,1635,1638,1641,1644,1647,1650,1653,1656,1658,1661],{"class":41,"line":210},[39,1621,1622],{"class":61},"  round",[39,1624,1625],{"class":670},"(n_dead_tup::",[39,1627,1628],{"class":674},"numeric",[39,1630,1631],{"class":674}," \u002F",[39,1633,1634],{"class":61}," NULLIF",[39,1636,1637],{"class":670},"(n_live_tup, ",[39,1639,1640],{"class":61},"0",[39,1642,1643],{"class":670},") ",[39,1645,1646],{"class":674},"*",[39,1648,1649],{"class":61}," 100",[39,1651,1652],{"class":670},", ",[39,1654,1655],{"class":61},"2",[39,1657,1643],{"class":670},[39,1659,1660],{"class":674},"AS",[39,1662,1663],{"class":670}," dead_pct,\n",[39,1665,1666],{"class":41,"line":215},[39,1667,1668],{"class":670},"  last_autovacuum, last_autoanalyze\n",[39,1670,1671,1673],{"class":41,"line":221},[39,1672,1493],{"class":674},[39,1674,1675],{"class":670}," pg_stat_user_tables\n",[39,1677,1678,1680,1683,1686],{"class":41,"line":241},[39,1679,1501],{"class":674},[39,1681,1682],{"class":670}," n_live_tup ",[39,1684,1685],{"class":674},">",[39,1687,1688],{"class":61}," 0\n",[39,1690,1691,1694,1697,1700],{"class":41,"line":246},[39,1692,1693],{"class":674},"ORDER BY",[39,1695,1696],{"class":670}," dead_pct ",[39,1698,1699],{"class":674},"DESC",[39,1701,1523],{"class":670},[370,1703,1705],{"id":1704},"bloat-diagnosis-with-pgstattuple","Bloat Diagnosis with pgstattuple",[27,1707,1708],{"language":1311},[31,1709,1711],{"className":1314,"code":1710,"language":1311,"meta":35,"style":35},"-- pgstattuple: exact bloat measurement (scans the table, accurate but slow)\nCREATE EXTENSION pgstattuple;\n\nSELECT\n  table_len,\n  tuple_count,\n  tuple_len,\n  tuple_percent,        -- % of table occupied by live tuples\n  dead_tuple_count,\n  dead_tuple_len,\n  dead_tuple_percent,   -- % wasted by dead tuples\n  free_space,\n  free_percent          -- % free space (reusable by future inserts)\nFROM pgstattuple('orders');\n\n-- pgstatindex: check index bloat\nSELECT * FROM pgstatindex('orders_customer_id_idx');\n-- If leaf_pages is much larger than expected → index is bloated → REINDEX\n",[22,1712,1713,1718,1726,1730,1735,1740,1745,1750,1758,1763,1768,1776,1781,1789,1801,1805,1810,1828],{"__ignoreMap":35},[39,1714,1715],{"class":41,"line":42},[39,1716,1717],{"class":45},"-- pgstattuple: exact bloat measurement (scans the table, accurate but slow)\n",[39,1719,1720,1723],{"class":41,"line":49},[39,1721,1722],{"class":674},"CREATE",[39,1724,1725],{"class":670}," EXTENSION pgstattuple;\n",[39,1727,1728],{"class":41,"line":55},[39,1729,82],{"emptyLinePlaceholder":81},[39,1731,1732],{"class":41,"line":78},[39,1733,1734],{"class":674},"SELECT\n",[39,1736,1737],{"class":41,"line":85},[39,1738,1739],{"class":670},"  table_len,\n",[39,1741,1742],{"class":41,"line":91},[39,1743,1744],{"class":670},"  tuple_count,\n",[39,1746,1747],{"class":41,"line":97},[39,1748,1749],{"class":670},"  tuple_len,\n",[39,1751,1752,1755],{"class":41,"line":111},[39,1753,1754],{"class":670},"  tuple_percent,        ",[39,1756,1757],{"class":45},"-- % of table occupied by live tuples\n",[39,1759,1760],{"class":41,"line":116},[39,1761,1762],{"class":670},"  dead_tuple_count,\n",[39,1764,1765],{"class":41,"line":122},[39,1766,1767],{"class":670},"  dead_tuple_len,\n",[39,1769,1770,1773],{"class":41,"line":128},[39,1771,1772],{"class":670},"  dead_tuple_percent,   ",[39,1774,1775],{"class":45},"-- % wasted by dead tuples\n",[39,1777,1778],{"class":41,"line":149},[39,1779,1780],{"class":670},"  free_space,\n",[39,1782,1783,1786],{"class":41,"line":154},[39,1784,1785],{"class":670},"  free_percent          ",[39,1787,1788],{"class":45},"-- % free space (reusable by future inserts)\n",[39,1790,1791,1793,1796,1799],{"class":41,"line":160},[39,1792,1493],{"class":674},[39,1794,1795],{"class":670}," pgstattuple(",[39,1797,1798],{"class":68},"'orders'",[39,1800,1404],{"class":670},[39,1802,1803],{"class":41,"line":177},[39,1804,82],{"emptyLinePlaceholder":81},[39,1806,1807],{"class":41,"line":182},[39,1808,1809],{"class":45},"-- pgstatindex: check index bloat\n",[39,1811,1812,1814,1817,1820,1823,1826],{"class":41,"line":188},[39,1813,1485],{"class":674},[39,1815,1816],{"class":674}," *",[39,1818,1819],{"class":674}," FROM",[39,1821,1822],{"class":670}," pgstatindex(",[39,1824,1825],{"class":68},"'orders_customer_id_idx'",[39,1827,1404],{"class":670},[39,1829,1830],{"class":41,"line":210},[39,1831,1832],{"class":45},"-- If leaf_pages is much larger than expected → index is bloated → REINDEX\n",[14,1834,1836],{"id":1835},"replication","Replication",[370,1838,1840],{"id":1839},"streaming-replication-physical","Streaming Replication (Physical)",[27,1842,1843],{"language":1285},[31,1844,1847],{"className":1845,"code":1846,"language":1285,"meta":35},[1289],"# PRIMARY (postgresql.conf)\nwal_level = replica              # required for streaming replication\nmax_wal_senders = 10             # max simultaneous standby connections\nwal_keep_size = 1GB              # retain WAL segments for lagging standbys\nhot_standby = on                 # (on standby) allow read-only queries\n\n# STANDBY: create from base backup with -R flag\npg_basebackup -h primary -D \u002Fvar\u002Flib\u002Fpostgresql\u002Fdata -R -P\n# -R writes: standby.signal + primary_conninfo to postgresql.auto.conf\n\n# FAILOVER: promote the standby to primary\npg_ctl -D \u002Fvar\u002Flib\u002Fpostgresql\u002Fdata promote\n# The standby stops replaying WAL and becomes a read-write primary\n# This is irreversible — the standby can no longer rejoin as a replica\n# Use Patroni or repmgr for automated failover with replan\n",[22,1848,1846],{"__ignoreMap":35},[370,1850,1852],{"id":1851},"logical-replication","Logical Replication",[27,1854,1855],{"language":1311},[31,1856,1858],{"className":1314,"code":1857,"language":1311,"meta":35,"style":35},"-- PUBLISHER (source database)\n-- Create a publication for specific tables\nCREATE PUBLICATION my_pub FOR TABLE orders, customers;\n\n-- Or publish ALL tables (use with caution — includes future tables)\nCREATE PUBLICATION all_tables FOR ALL TABLES;\n\n-- SUBSCRIBER (destination database — can be a different PG version)\n-- Create a subscription that pulls changes from the publisher\nCREATE SUBSCRIPTION my_sub\n  CONNECTION 'host=publisher.example.com port=5432 dbname=mydb'\n  PUBLICATION my_pub;\n\n-- Monitor replication progress on the subscriber:\nSELECT subname, pid, relid, received_lsn, last_msg_send_time\nFROM pg_stat_subscription;\n\n-- Monitor on the publisher:\nSELECT pubname, pid, sent_lsn, write_lsn, flush_lsn, replay_lsn\nFROM pg_stat_replication;\n\n-- ⚠️ Limitations of logical replication:\n--   No sequences (sequence values diverge — must sync manually)\n--   No DDL (schema changes must be applied to both sides separately)\n--   No TRUNCATE (unless explicitly added: ALTER PUBLICATION ... ADD TABLE ... TRUNCATE)\n--   No schema-less replication (tables must exist on subscriber with compatible types)\n--   Conflicts: subscriber is writable — if a row exists, INSERT fails\n",[22,1859,1860,1865,1870,1885,1889,1894,1906,1910,1915,1920,1927,1935,1940,1944,1949,1956,1963,1967,1972,1979,1986,1990,1995,2000,2005,2010,2015],{"__ignoreMap":35},[39,1861,1862],{"class":41,"line":42},[39,1863,1864],{"class":45},"-- PUBLISHER (source database)\n",[39,1866,1867],{"class":41,"line":49},[39,1868,1869],{"class":45},"-- Create a publication for specific tables\n",[39,1871,1872,1874,1877,1880,1882],{"class":41,"line":55},[39,1873,1722],{"class":674},[39,1875,1876],{"class":670}," PUBLICATION my_pub ",[39,1878,1879],{"class":674},"FOR",[39,1881,1335],{"class":674},[39,1883,1884],{"class":670}," orders, customers;\n",[39,1886,1887],{"class":41,"line":78},[39,1888,82],{"emptyLinePlaceholder":81},[39,1890,1891],{"class":41,"line":85},[39,1892,1893],{"class":45},"-- Or publish ALL tables (use with caution — includes future tables)\n",[39,1895,1896,1898,1901,1903],{"class":41,"line":91},[39,1897,1722],{"class":674},[39,1899,1900],{"class":670}," PUBLICATION all_tables ",[39,1902,1879],{"class":674},[39,1904,1905],{"class":670}," ALL TABLES;\n",[39,1907,1908],{"class":41,"line":97},[39,1909,82],{"emptyLinePlaceholder":81},[39,1911,1912],{"class":41,"line":111},[39,1913,1914],{"class":45},"-- SUBSCRIBER (destination database — can be a different PG version)\n",[39,1916,1917],{"class":41,"line":116},[39,1918,1919],{"class":45},"-- Create a subscription that pulls changes from the publisher\n",[39,1921,1922,1924],{"class":41,"line":122},[39,1923,1722],{"class":674},[39,1925,1926],{"class":670}," SUBSCRIPTION my_sub\n",[39,1928,1929,1932],{"class":41,"line":128},[39,1930,1931],{"class":674},"  CONNECTION",[39,1933,1934],{"class":68}," 'host=publisher.example.com port=5432 dbname=mydb'\n",[39,1936,1937],{"class":41,"line":149},[39,1938,1939],{"class":670},"  PUBLICATION my_pub;\n",[39,1941,1942],{"class":41,"line":154},[39,1943,82],{"emptyLinePlaceholder":81},[39,1945,1946],{"class":41,"line":160},[39,1947,1948],{"class":45},"-- Monitor replication progress on the subscriber:\n",[39,1950,1951,1953],{"class":41,"line":177},[39,1952,1485],{"class":674},[39,1954,1955],{"class":670}," subname, pid, relid, received_lsn, last_msg_send_time\n",[39,1957,1958,1960],{"class":41,"line":182},[39,1959,1493],{"class":674},[39,1961,1962],{"class":670}," pg_stat_subscription;\n",[39,1964,1965],{"class":41,"line":188},[39,1966,82],{"emptyLinePlaceholder":81},[39,1968,1969],{"class":41,"line":210},[39,1970,1971],{"class":45},"-- Monitor on the publisher:\n",[39,1973,1974,1976],{"class":41,"line":215},[39,1975,1485],{"class":674},[39,1977,1978],{"class":670}," pubname, pid, sent_lsn, write_lsn, flush_lsn, replay_lsn\n",[39,1980,1981,1983],{"class":41,"line":221},[39,1982,1493],{"class":674},[39,1984,1985],{"class":670}," pg_stat_replication;\n",[39,1987,1988],{"class":41,"line":241},[39,1989,82],{"emptyLinePlaceholder":81},[39,1991,1992],{"class":41,"line":246},[39,1993,1994],{"class":45},"-- ⚠️ Limitations of logical replication:\n",[39,1996,1997],{"class":41,"line":252},[39,1998,1999],{"class":45},"--   No sequences (sequence values diverge — must sync manually)\n",[39,2001,2002],{"class":41,"line":271},[39,2003,2004],{"class":45},"--   No DDL (schema changes must be applied to both sides separately)\n",[39,2006,2007],{"class":41,"line":276},[39,2008,2009],{"class":45},"--   No TRUNCATE (unless explicitly added: ALTER PUBLICATION ... ADD TABLE ... TRUNCATE)\n",[39,2011,2012],{"class":41,"line":282},[39,2013,2014],{"class":45},"--   No schema-less replication (tables must exist on subscriber with compatible types)\n",[39,2016,2017],{"class":41,"line":301},[39,2018,2019],{"class":45},"--   Conflicts: subscriber is writable — if a row exists, INSERT fails\n",[14,2021,2023],{"id":2022},"connection-pooling-pgbouncer","Connection Pooling (PgBouncer)",[27,2025,2027],{"language":2026},"ini",[31,2028,2031],{"className":2029,"code":2030,"language":2026,"meta":35,"style":35},"language-ini shiki shiki-themes github-light github-dark","# pgbouncer.ini\n\n[databases]\n; Route connections to the real PostgreSQL\nmydb = host=127.0.0.1 port=5432 dbname=mydb\n\n[pgbouncer]\nlisten_addr = 0.0.0.0\nlisten_port = 6432\n\n; ─── Pool Mode ───\n; transaction: connections returned to pool at COMMIT\u002FROLLBACK\n;   Highest efficiency — thousands of clients share tens of server connections\n;   ⚠️ Breaks: temp tables, SET (session-level), LISTEN\u002FNOTIFY,\n;      prepared statements (unless max_prepared_statements > 0, PgBouncer 1.21+),\n;      advisory locks (session-level)\n; session: connections held for entire client session\n;   Compatible with all features, less efficient (1 client = 1 server connection)\npool_mode = transaction\n\n; ─── Capacity ───\nmax_client_conn = 1000      ; max client connections to PgBouncer\ndefault_pool_size = 25      ; max server connections per database\u002Fuser pair\nreserve_pool_size = 5       ; extra connections when pool is exhausted\nreserve_pool_timeout = 3    ; seconds to wait before using reserve pool\n\n; ─── Timeout ───\nserver_idle_timeout = 600   ; close idle server connections after 10 min\nquery_wait_timeout = 120    ; client waits max 2 min for a server connection\n",[22,2032,2033,2038,2042,2047,2052,2078,2082,2087,2095,2103,2107,2112,2117,2122,2127,2132,2137,2142,2147,2155,2159,2164,2175,2186,2197,2208,2212,2217,2228],{"__ignoreMap":35},[39,2034,2035],{"class":41,"line":42},[39,2036,2037],{"class":45},"# pgbouncer.ini\n",[39,2039,2040],{"class":41,"line":49},[39,2041,82],{"emptyLinePlaceholder":81},[39,2043,2044],{"class":41,"line":55},[39,2045,2046],{"class":58},"[databases]\n",[39,2048,2049],{"class":41,"line":78},[39,2050,2051],{"class":45},"; Route connections to the real PostgreSQL\n",[39,2053,2054,2057,2060,2063,2066,2069,2072,2075],{"class":41,"line":85},[39,2055,2056],{"class":674},"mydb",[39,2058,2059],{"class":670}," = ",[39,2061,2062],{"class":674},"host",[39,2064,2065],{"class":670},"=127.0.0.1 ",[39,2067,2068],{"class":674},"port",[39,2070,2071],{"class":670},"=5432 ",[39,2073,2074],{"class":674},"dbname",[39,2076,2077],{"class":670},"=mydb\n",[39,2079,2080],{"class":41,"line":91},[39,2081,82],{"emptyLinePlaceholder":81},[39,2083,2084],{"class":41,"line":97},[39,2085,2086],{"class":58},"[pgbouncer]\n",[39,2088,2089,2092],{"class":41,"line":111},[39,2090,2091],{"class":674},"listen_addr",[39,2093,2094],{"class":670}," = 0.0.0.0\n",[39,2096,2097,2100],{"class":41,"line":116},[39,2098,2099],{"class":674},"listen_port",[39,2101,2102],{"class":670}," = 6432\n",[39,2104,2105],{"class":41,"line":122},[39,2106,82],{"emptyLinePlaceholder":81},[39,2108,2109],{"class":41,"line":128},[39,2110,2111],{"class":45},"; ─── Pool Mode ───\n",[39,2113,2114],{"class":41,"line":149},[39,2115,2116],{"class":45},"; transaction: connections returned to pool at COMMIT\u002FROLLBACK\n",[39,2118,2119],{"class":41,"line":154},[39,2120,2121],{"class":45},";   Highest efficiency — thousands of clients share tens of server connections\n",[39,2123,2124],{"class":41,"line":160},[39,2125,2126],{"class":45},";   ⚠️ Breaks: temp tables, SET (session-level), LISTEN\u002FNOTIFY,\n",[39,2128,2129],{"class":41,"line":177},[39,2130,2131],{"class":45},";      prepared statements (unless max_prepared_statements > 0, PgBouncer 1.21+),\n",[39,2133,2134],{"class":41,"line":182},[39,2135,2136],{"class":45},";      advisory locks (session-level)\n",[39,2138,2139],{"class":41,"line":188},[39,2140,2141],{"class":45},"; session: connections held for entire client session\n",[39,2143,2144],{"class":41,"line":210},[39,2145,2146],{"class":45},";   Compatible with all features, less efficient (1 client = 1 server connection)\n",[39,2148,2149,2152],{"class":41,"line":215},[39,2150,2151],{"class":674},"pool_mode",[39,2153,2154],{"class":670}," = transaction\n",[39,2156,2157],{"class":41,"line":221},[39,2158,82],{"emptyLinePlaceholder":81},[39,2160,2161],{"class":41,"line":241},[39,2162,2163],{"class":45},"; ─── Capacity ───\n",[39,2165,2166,2169,2172],{"class":41,"line":246},[39,2167,2168],{"class":674},"max_client_conn",[39,2170,2171],{"class":670}," = 1000      ",[39,2173,2174],{"class":45},"; max client connections to PgBouncer\n",[39,2176,2177,2180,2183],{"class":41,"line":252},[39,2178,2179],{"class":674},"default_pool_size",[39,2181,2182],{"class":670}," = 25      ",[39,2184,2185],{"class":45},"; max server connections per database\u002Fuser pair\n",[39,2187,2188,2191,2194],{"class":41,"line":271},[39,2189,2190],{"class":674},"reserve_pool_size",[39,2192,2193],{"class":670}," = 5       ",[39,2195,2196],{"class":45},"; extra connections when pool is exhausted\n",[39,2198,2199,2202,2205],{"class":41,"line":276},[39,2200,2201],{"class":674},"reserve_pool_timeout",[39,2203,2204],{"class":670}," = 3    ",[39,2206,2207],{"class":45},"; seconds to wait before using reserve pool\n",[39,2209,2210],{"class":41,"line":282},[39,2211,82],{"emptyLinePlaceholder":81},[39,2213,2214],{"class":41,"line":301},[39,2215,2216],{"class":45},"; ─── Timeout ───\n",[39,2218,2219,2222,2225],{"class":41,"line":306},[39,2220,2221],{"class":674},"server_idle_timeout",[39,2223,2224],{"class":670}," = 600   ",[39,2226,2227],{"class":45},"; close idle server connections after 10 min\n",[39,2229,2230,2233,2236],{"class":41,"line":312},[39,2231,2232],{"class":674},"query_wait_timeout",[39,2234,2235],{"class":670}," = 120    ",[39,2237,2238],{"class":45},"; client waits max 2 min for a server connection\n",[27,2240,2241],{"language":1285},[31,2242,2245],{"className":2243,"code":2244,"language":1285,"meta":35},[1289],"# When to use which pool mode:\n#\n# Transaction mode (recommended for most apps):\n#   Web apps, serverless\u002Flambda, microservices with short transactions\n#   1000 clients → 25 server connections (40:1 multiplexing)\n#\n# Session mode (when you need session features):\n#   Batch jobs using temp tables, apps using LISTEN\u002FNOTIFY,\n#   session-level advisory locks, SET search_path per session\n#\n# Connection count economics:\n#   Each PostgreSQL connection forks a backend process (~5-10MB RSS)\n#   1000 connections = 5-10GB just for connection overhead\n#   With PgBouncer transaction mode: 25 connections = 125-250MB\n",[22,2246,2244],{"__ignoreMap":35},[14,2248,2250],{"id":2249},"monitoring","Monitoring",[370,2252,2254],{"id":2253},"pg_stat_activity-current-queries","pg_stat_activity — Current Queries",[27,2256,2257],{"language":1311},[31,2258,2260],{"className":1314,"code":2259,"language":1311,"meta":35,"style":35},"-- Find all active queries with duration and wait events\nSELECT\n  pid,\n  usename,\n  application_name,\n  state,                              -- active, idle, idle in transaction\n  wait_event_type,                     -- Lock, IO, LWLock, etc. (NULL = not waiting)\n  wait_event,                          -- specific event name\n  now() - query_start AS duration,     -- how long the query has been running\n  now() - xact_start AS xact_duration, -- how long the TRANSACTION has been open\n  LEFT(query, 100) AS query_preview\nFROM pg_stat_activity\nWHERE state != 'idle'\nORDER BY xact_duration DESC NULLS LAST;\n\n-- Find idle-in-transaction sessions (these block VACUUM and hold locks)\nSELECT pid, usename, now() - xact_start AS xact_duration, query\nFROM pg_stat_activity\nWHERE state = 'idle in transaction'\n  AND now() - xact_start > interval '5 minutes'\nORDER BY xact_duration DESC;\n\n-- ⚠️ idle-in-transaction is dangerous:\n--   It holds locks, prevents VACUUM from reclaiming dead tuples (bloat),\n--   and holds the replication slot's xmin horizon\n--   Set: idle_in_transaction_session_timeout = '5min' in postgresql.conf\n",[22,2261,2262,2267,2271,2276,2281,2286,2297,2305,2313,2335,2354,2372,2379,2392,2409,2413,2418,2439,2445,2457,2479,2489,2493,2498,2503,2508],{"__ignoreMap":35},[39,2263,2264],{"class":41,"line":42},[39,2265,2266],{"class":45},"-- Find all active queries with duration and wait events\n",[39,2268,2269],{"class":41,"line":49},[39,2270,1734],{"class":674},[39,2272,2273],{"class":41,"line":55},[39,2274,2275],{"class":670},"  pid,\n",[39,2277,2278],{"class":41,"line":78},[39,2279,2280],{"class":670},"  usename,\n",[39,2282,2283],{"class":41,"line":85},[39,2284,2285],{"class":670},"  application_name,\n",[39,2287,2288,2291,2294],{"class":41,"line":91},[39,2289,2290],{"class":674},"  state",[39,2292,2293],{"class":670},",                              ",[39,2295,2296],{"class":45},"-- active, idle, idle in transaction\n",[39,2298,2299,2302],{"class":41,"line":97},[39,2300,2301],{"class":670},"  wait_event_type,                     ",[39,2303,2304],{"class":45},"-- Lock, IO, LWLock, etc. (NULL = not waiting)\n",[39,2306,2307,2310],{"class":41,"line":111},[39,2308,2309],{"class":670},"  wait_event,                          ",[39,2311,2312],{"class":45},"-- specific event name\n",[39,2314,2315,2318,2321,2324,2327,2329,2332],{"class":41,"line":116},[39,2316,2317],{"class":674},"  now",[39,2319,2320],{"class":670},"() ",[39,2322,2323],{"class":674},"-",[39,2325,2326],{"class":670}," query_start ",[39,2328,1660],{"class":674},[39,2330,2331],{"class":670}," duration,     ",[39,2333,2334],{"class":45},"-- how long the query has been running\n",[39,2336,2337,2339,2341,2343,2346,2348,2351],{"class":41,"line":122},[39,2338,2317],{"class":674},[39,2340,2320],{"class":670},[39,2342,2323],{"class":674},[39,2344,2345],{"class":670}," xact_start ",[39,2347,1660],{"class":674},[39,2349,2350],{"class":670}," xact_duration, ",[39,2352,2353],{"class":45},"-- how long the TRANSACTION has been open\n",[39,2355,2356,2359,2362,2365,2367,2369],{"class":41,"line":128},[39,2357,2358],{"class":61},"  LEFT",[39,2360,2361],{"class":670},"(query, ",[39,2363,2364],{"class":61},"100",[39,2366,1643],{"class":670},[39,2368,1660],{"class":674},[39,2370,2371],{"class":670}," query_preview\n",[39,2373,2374,2376],{"class":41,"line":149},[39,2375,1493],{"class":674},[39,2377,2378],{"class":670}," pg_stat_activity\n",[39,2380,2381,2383,2386,2389],{"class":41,"line":154},[39,2382,1501],{"class":674},[39,2384,2385],{"class":674}," state",[39,2387,2388],{"class":674}," !=",[39,2390,2391],{"class":68}," 'idle'\n",[39,2393,2394,2396,2399,2401,2404,2407],{"class":41,"line":160},[39,2395,1693],{"class":674},[39,2397,2398],{"class":670}," xact_duration ",[39,2400,1699],{"class":674},[39,2402,2403],{"class":674}," NULLS",[39,2405,2406],{"class":674}," LAST",[39,2408,1523],{"class":670},[39,2410,2411],{"class":41,"line":177},[39,2412,82],{"emptyLinePlaceholder":81},[39,2414,2415],{"class":41,"line":182},[39,2416,2417],{"class":45},"-- Find idle-in-transaction sessions (these block VACUUM and hold locks)\n",[39,2419,2420,2422,2425,2428,2430,2432,2434,2436],{"class":41,"line":188},[39,2421,1485],{"class":674},[39,2423,2424],{"class":670}," pid, usename, ",[39,2426,2427],{"class":674},"now",[39,2429,2320],{"class":670},[39,2431,2323],{"class":674},[39,2433,2345],{"class":670},[39,2435,1660],{"class":674},[39,2437,2438],{"class":670}," xact_duration, query\n",[39,2440,2441,2443],{"class":41,"line":210},[39,2442,1493],{"class":674},[39,2444,2378],{"class":670},[39,2446,2447,2449,2451,2454],{"class":41,"line":215},[39,2448,1501],{"class":674},[39,2450,2385],{"class":674},[39,2452,2453],{"class":674}," =",[39,2455,2456],{"class":68}," 'idle in transaction'\n",[39,2458,2459,2462,2465,2467,2469,2471,2473,2476],{"class":41,"line":221},[39,2460,2461],{"class":674},"  AND",[39,2463,2464],{"class":674}," now",[39,2466,2320],{"class":670},[39,2468,2323],{"class":674},[39,2470,2345],{"class":670},[39,2472,1685],{"class":674},[39,2474,2475],{"class":670}," interval ",[39,2477,2478],{"class":68},"'5 minutes'\n",[39,2480,2481,2483,2485,2487],{"class":41,"line":241},[39,2482,1693],{"class":674},[39,2484,2398],{"class":670},[39,2486,1699],{"class":674},[39,2488,1523],{"class":670},[39,2490,2491],{"class":41,"line":246},[39,2492,82],{"emptyLinePlaceholder":81},[39,2494,2495],{"class":41,"line":252},[39,2496,2497],{"class":45},"-- ⚠️ idle-in-transaction is dangerous:\n",[39,2499,2500],{"class":41,"line":271},[39,2501,2502],{"class":45},"--   It holds locks, prevents VACUUM from reclaiming dead tuples (bloat),\n",[39,2504,2505],{"class":41,"line":276},[39,2506,2507],{"class":45},"--   and holds the replication slot's xmin horizon\n",[39,2509,2510],{"class":41,"line":282},[39,2511,2512],{"class":45},"--   Set: idle_in_transaction_session_timeout = '5min' in postgresql.conf\n",[370,2514,2516],{"id":2515},"pg_stat_statements-slow-query-identification","pg_stat_statements — Slow Query Identification",[27,2518,2519],{"language":1311},[31,2520,2522],{"className":1314,"code":2521,"language":1311,"meta":35,"style":35},"-- Enable (requires restart — it's a shared_preload_libraries entry):\n--   shared_preload_libraries = 'pg_stat_statements' in postgresql.conf\nCREATE EXTENSION IF NOT EXISTS pg_stat_statements;\n\n-- Top 10 queries by total execution time\nSELECT\n  LEFT(query, 120) AS query_preview,\n  calls,                               -- how many times executed\n  mean_exec_time,                      -- avg ms per call\n  max_exec_time,                       -- worst-case ms (outliers!)\n  total_exec_time,                     -- total ms across all calls\n  rows,                                -- total rows returned\n  shared_blks_hit,                     -- cache hits (fast)\n  shared_blks_read,                    -- disk reads (slow)\n  round(100.0 * shared_blks_hit \u002F\n    NULLIF(shared_blks_hit + shared_blks_read, 0), 2) AS hit_pct\nFROM pg_stat_statements\nORDER BY total_exec_time DESC\nLIMIT 10;\n\n-- Find queries with high variance (mean vs max) — sporadic slowness\nSELECT LEFT(query, 80) AS query_preview,\n  calls, mean_exec_time, max_exec_time,\n  max_exec_time \u002F NULLIF(mean_exec_time, 0) AS max_to_mean_ratio\nFROM pg_stat_statements\nWHERE calls > 10\nORDER BY max_to_mean_ratio DESC\nLIMIT 10;\n-- A ratio > 10x means the query is sometimes fast, sometimes very slow\n-- → investigate: stale stats, lock contention, or cold cache\n\n-- Reset stats (after deploying an optimization, to measure fresh)\nSELECT pg_stat_statements_reset();\n",[22,2523,2524,2529,2534,2553,2557,2562,2566,2582,2590,2598,2606,2614,2625,2633,2641,2662,2690,2697,2707,2717,2721,2726,2744,2749,2771,2777,2789,2798,2806,2811,2816,2820,2825],{"__ignoreMap":35},[39,2525,2526],{"class":41,"line":42},[39,2527,2528],{"class":45},"-- Enable (requires restart — it's a shared_preload_libraries entry):\n",[39,2530,2531],{"class":41,"line":49},[39,2532,2533],{"class":45},"--   shared_preload_libraries = 'pg_stat_statements' in postgresql.conf\n",[39,2535,2536,2538,2541,2544,2547,2550],{"class":41,"line":55},[39,2537,1722],{"class":674},[39,2539,2540],{"class":670}," EXTENSION ",[39,2542,2543],{"class":674},"IF",[39,2545,2546],{"class":674}," NOT",[39,2548,2549],{"class":674}," EXISTS",[39,2551,2552],{"class":670}," pg_stat_statements;\n",[39,2554,2555],{"class":41,"line":78},[39,2556,82],{"emptyLinePlaceholder":81},[39,2558,2559],{"class":41,"line":85},[39,2560,2561],{"class":45},"-- Top 10 queries by total execution time\n",[39,2563,2564],{"class":41,"line":91},[39,2565,1734],{"class":674},[39,2567,2568,2570,2572,2575,2577,2579],{"class":41,"line":97},[39,2569,2358],{"class":61},[39,2571,2361],{"class":670},[39,2573,2574],{"class":61},"120",[39,2576,1643],{"class":670},[39,2578,1660],{"class":674},[39,2580,2581],{"class":670}," query_preview,\n",[39,2583,2584,2587],{"class":41,"line":111},[39,2585,2586],{"class":670},"  calls,                               ",[39,2588,2589],{"class":45},"-- how many times executed\n",[39,2591,2592,2595],{"class":41,"line":116},[39,2593,2594],{"class":670},"  mean_exec_time,                      ",[39,2596,2597],{"class":45},"-- avg ms per call\n",[39,2599,2600,2603],{"class":41,"line":122},[39,2601,2602],{"class":670},"  max_exec_time,                       ",[39,2604,2605],{"class":45},"-- worst-case ms (outliers!)\n",[39,2607,2608,2611],{"class":41,"line":128},[39,2609,2610],{"class":670},"  total_exec_time,                     ",[39,2612,2613],{"class":45},"-- total ms across all calls\n",[39,2615,2616,2619,2622],{"class":41,"line":149},[39,2617,2618],{"class":674},"  rows",[39,2620,2621],{"class":670},",                                ",[39,2623,2624],{"class":45},"-- total rows returned\n",[39,2626,2627,2630],{"class":41,"line":154},[39,2628,2629],{"class":670},"  shared_blks_hit,                     ",[39,2631,2632],{"class":45},"-- cache hits (fast)\n",[39,2634,2635,2638],{"class":41,"line":160},[39,2636,2637],{"class":670},"  shared_blks_read,                    ",[39,2639,2640],{"class":45},"-- disk reads (slow)\n",[39,2642,2643,2645,2648,2650,2652,2654,2656,2659],{"class":41,"line":177},[39,2644,1622],{"class":61},[39,2646,2647],{"class":670},"(",[39,2649,2364],{"class":61},[39,2651,1357],{"class":670},[39,2653,1640],{"class":61},[39,2655,1816],{"class":674},[39,2657,2658],{"class":670}," shared_blks_hit ",[39,2660,2661],{"class":674},"\u002F\n",[39,2663,2664,2667,2670,2673,2676,2678,2681,2683,2685,2687],{"class":41,"line":182},[39,2665,2666],{"class":61},"    NULLIF",[39,2668,2669],{"class":670},"(shared_blks_hit ",[39,2671,2672],{"class":674},"+",[39,2674,2675],{"class":670}," shared_blks_read, ",[39,2677,1640],{"class":61},[39,2679,2680],{"class":670},"), ",[39,2682,1655],{"class":61},[39,2684,1643],{"class":670},[39,2686,1660],{"class":674},[39,2688,2689],{"class":670}," hit_pct\n",[39,2691,2692,2694],{"class":41,"line":188},[39,2693,1493],{"class":674},[39,2695,2696],{"class":670}," pg_stat_statements\n",[39,2698,2699,2701,2704],{"class":41,"line":210},[39,2700,1693],{"class":674},[39,2702,2703],{"class":670}," total_exec_time ",[39,2705,2706],{"class":674},"DESC\n",[39,2708,2709,2712,2715],{"class":41,"line":215},[39,2710,2711],{"class":674},"LIMIT",[39,2713,2714],{"class":61}," 10",[39,2716,1523],{"class":670},[39,2718,2719],{"class":41,"line":221},[39,2720,82],{"emptyLinePlaceholder":81},[39,2722,2723],{"class":41,"line":241},[39,2724,2725],{"class":45},"-- Find queries with high variance (mean vs max) — sporadic slowness\n",[39,2727,2728,2730,2733,2735,2738,2740,2742],{"class":41,"line":246},[39,2729,1485],{"class":674},[39,2731,2732],{"class":61}," LEFT",[39,2734,2361],{"class":670},[39,2736,2737],{"class":61},"80",[39,2739,1643],{"class":670},[39,2741,1660],{"class":674},[39,2743,2581],{"class":670},[39,2745,2746],{"class":41,"line":252},[39,2747,2748],{"class":670},"  calls, mean_exec_time, max_exec_time,\n",[39,2750,2751,2754,2757,2759,2762,2764,2766,2768],{"class":41,"line":271},[39,2752,2753],{"class":670},"  max_exec_time ",[39,2755,2756],{"class":674},"\u002F",[39,2758,1634],{"class":61},[39,2760,2761],{"class":670},"(mean_exec_time, ",[39,2763,1640],{"class":61},[39,2765,1643],{"class":670},[39,2767,1660],{"class":674},[39,2769,2770],{"class":670}," max_to_mean_ratio\n",[39,2772,2773,2775],{"class":41,"line":276},[39,2774,1493],{"class":674},[39,2776,2696],{"class":670},[39,2778,2779,2781,2784,2786],{"class":41,"line":282},[39,2780,1501],{"class":674},[39,2782,2783],{"class":670}," calls ",[39,2785,1685],{"class":674},[39,2787,2788],{"class":61}," 10\n",[39,2790,2791,2793,2796],{"class":41,"line":301},[39,2792,1693],{"class":674},[39,2794,2795],{"class":670}," max_to_mean_ratio ",[39,2797,2706],{"class":674},[39,2799,2800,2802,2804],{"class":41,"line":306},[39,2801,2711],{"class":674},[39,2803,2714],{"class":61},[39,2805,1523],{"class":670},[39,2807,2808],{"class":41,"line":312},[39,2809,2810],{"class":45},"-- A ratio > 10x means the query is sometimes fast, sometimes very slow\n",[39,2812,2813],{"class":41,"line":318},[39,2814,2815],{"class":45},"-- → investigate: stale stats, lock contention, or cold cache\n",[39,2817,2818],{"class":41,"line":347},[39,2819,82],{"emptyLinePlaceholder":81},[39,2821,2822],{"class":41,"line":1264},[39,2823,2824],{"class":45},"-- Reset stats (after deploying an optimization, to measure fresh)\n",[39,2826,2828,2830],{"class":41,"line":2827},33,[39,2829,1485],{"class":674},[39,2831,2832],{"class":670}," pg_stat_statements_reset();\n",[370,2834,2836],{"id":2835},"lock-monitoring","Lock Monitoring",[27,2838,2839],{"language":1311},[31,2840,2842],{"className":1314,"code":2841,"language":1311,"meta":35,"style":35},"-- Find blocked queries and what's blocking them\nSELECT\n  blocked.pid     AS blocked_pid,\n  blocked.query   AS blocked_query,\n  blocking.pid    AS blocking_pid,\n  blocking.query  AS blocking_query,\n  now() - blocked.xact_start AS blocked_duration\nFROM pg_stat_activity blocked\nJOIN pg_stat_activity blocking\n  ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))\nWHERE blocked.state = 'active';\n\n-- Find all locks held by a specific PID (for debugging deadlocks)\nSELECT locktype, relation::regclass, mode, granted, pid\nFROM pg_locks\nWHERE pid = 12345\nORDER BY granted, relation::regclass;\n\n-- Find queries waiting on locks\nSELECT pid, wait_event_type, wait_event,\n  now() - query_start AS waited_for\nFROM pg_stat_activity\nWHERE wait_event_type = 'Lock'\nORDER BY waited_for DESC;\n",[22,2843,2844,2849,2853,2869,2884,2899,2913,2935,2942,2950,2977,2995,2999,3004,3011,3018,3030,3037,3041,3046,3053,3068,3074,3086],{"__ignoreMap":35},[39,2845,2846],{"class":41,"line":42},[39,2847,2848],{"class":45},"-- Find blocked queries and what's blocking them\n",[39,2850,2851],{"class":41,"line":49},[39,2852,1734],{"class":674},[39,2854,2855,2858,2860,2863,2866],{"class":41,"line":55},[39,2856,2857],{"class":61},"  blocked",[39,2859,1357],{"class":670},[39,2861,2862],{"class":61},"pid",[39,2864,2865],{"class":674},"     AS",[39,2867,2868],{"class":670}," blocked_pid,\n",[39,2870,2871,2873,2875,2878,2881],{"class":41,"line":78},[39,2872,2857],{"class":61},[39,2874,1357],{"class":670},[39,2876,2877],{"class":61},"query",[39,2879,2880],{"class":674},"   AS",[39,2882,2883],{"class":670}," blocked_query,\n",[39,2885,2886,2889,2891,2893,2896],{"class":41,"line":85},[39,2887,2888],{"class":61},"  blocking",[39,2890,1357],{"class":670},[39,2892,2862],{"class":61},[39,2894,2895],{"class":674},"    AS",[39,2897,2898],{"class":670}," blocking_pid,\n",[39,2900,2901,2903,2905,2907,2910],{"class":41,"line":91},[39,2902,2888],{"class":61},[39,2904,1357],{"class":670},[39,2906,2877],{"class":61},[39,2908,2909],{"class":674},"  AS",[39,2911,2912],{"class":670}," blocking_query,\n",[39,2914,2915,2917,2919,2921,2924,2926,2929,2932],{"class":41,"line":97},[39,2916,2317],{"class":674},[39,2918,2320],{"class":670},[39,2920,2323],{"class":674},[39,2922,2923],{"class":61}," blocked",[39,2925,1357],{"class":670},[39,2927,2928],{"class":61},"xact_start",[39,2930,2931],{"class":674}," AS",[39,2933,2934],{"class":670}," blocked_duration\n",[39,2936,2937,2939],{"class":41,"line":111},[39,2938,1493],{"class":674},[39,2940,2941],{"class":670}," pg_stat_activity blocked\n",[39,2943,2944,2947],{"class":41,"line":116},[39,2945,2946],{"class":674},"JOIN",[39,2948,2949],{"class":670}," pg_stat_activity blocking\n",[39,2951,2952,2955,2958,2960,2962,2964,2967,2970,2972,2974],{"class":41,"line":122},[39,2953,2954],{"class":674},"  ON",[39,2956,2957],{"class":61}," blocking",[39,2959,1357],{"class":670},[39,2961,2862],{"class":61},[39,2963,2453],{"class":674},[39,2965,2966],{"class":670}," ANY(pg_blocking_pids(",[39,2968,2969],{"class":61},"blocked",[39,2971,1357],{"class":670},[39,2973,2862],{"class":61},[39,2975,2976],{"class":670},"))\n",[39,2978,2979,2981,2983,2985,2988,2990,2993],{"class":41,"line":128},[39,2980,1501],{"class":674},[39,2982,2923],{"class":61},[39,2984,1357],{"class":670},[39,2986,2987],{"class":61},"state",[39,2989,2453],{"class":674},[39,2991,2992],{"class":68}," 'active'",[39,2994,1523],{"class":670},[39,2996,2997],{"class":41,"line":149},[39,2998,82],{"emptyLinePlaceholder":81},[39,3000,3001],{"class":41,"line":154},[39,3002,3003],{"class":45},"-- Find all locks held by a specific PID (for debugging deadlocks)\n",[39,3005,3006,3008],{"class":41,"line":160},[39,3007,1485],{"class":674},[39,3009,3010],{"class":670}," locktype, relation::regclass, mode, granted, pid\n",[39,3012,3013,3015],{"class":41,"line":177},[39,3014,1493],{"class":674},[39,3016,3017],{"class":670}," pg_locks\n",[39,3019,3020,3022,3025,3027],{"class":41,"line":182},[39,3021,1501],{"class":674},[39,3023,3024],{"class":670}," pid ",[39,3026,675],{"class":674},[39,3028,3029],{"class":61}," 12345\n",[39,3031,3032,3034],{"class":41,"line":188},[39,3033,1693],{"class":674},[39,3035,3036],{"class":670}," granted, relation::regclass;\n",[39,3038,3039],{"class":41,"line":210},[39,3040,82],{"emptyLinePlaceholder":81},[39,3042,3043],{"class":41,"line":215},[39,3044,3045],{"class":45},"-- Find queries waiting on locks\n",[39,3047,3048,3050],{"class":41,"line":221},[39,3049,1485],{"class":674},[39,3051,3052],{"class":670}," pid, wait_event_type, wait_event,\n",[39,3054,3055,3057,3059,3061,3063,3065],{"class":41,"line":241},[39,3056,2317],{"class":674},[39,3058,2320],{"class":670},[39,3060,2323],{"class":674},[39,3062,2326],{"class":670},[39,3064,1660],{"class":674},[39,3066,3067],{"class":670}," waited_for\n",[39,3069,3070,3072],{"class":41,"line":246},[39,3071,1493],{"class":674},[39,3073,2378],{"class":670},[39,3075,3076,3078,3081,3083],{"class":41,"line":252},[39,3077,1501],{"class":674},[39,3079,3080],{"class":670}," wait_event_type ",[39,3082,675],{"class":674},[39,3084,3085],{"class":68}," 'Lock'\n",[39,3087,3088,3090,3093,3095],{"class":41,"line":271},[39,3089,1693],{"class":674},[39,3091,3092],{"class":670}," waited_for ",[39,3094,1699],{"class":674},[39,3096,1523],{"class":670},[14,3098,3100],{"id":3099},"maintenance-operations","Maintenance Operations",[27,3102,3103],{"language":1311},[31,3104,3106],{"className":1314,"code":3105,"language":1311,"meta":35,"style":35},"-- Reindex without blocking (CONCURRENTLY builds a new index in parallel,\n-- then swaps — but can fail, leaving an INVALID index you must DROP)\nREINDEX INDEX CONCURRENTLY orders_customer_id_idx;\n\n-- If CONCURRENTLY fails, the old index is still valid but a new INVALID index exists:\n-- Check: SELECT indexrelname, indisvalid FROM pg_index WHERE indexrelname LIKE 'orders_%';\n-- Clean up: DROP INDEX orders_customer_id_idx_ccnew;  -- the failed new index\n\n-- Cluster: physically reorder rows by an index (improves cache locality)\n-- ⚠️ Takes ACCESS EXCLUSIVE lock — don't run in production\nCLUSTER orders USING orders_pkey;\n\n-- Analyze: refresh planner statistics (lightweight, no lock)\nANALYZE orders;\n\n-- Vacuum + analyze in one call\nVACUUM (ANALYZE) orders;\n\n-- Check index validity (an INVALID index is ignored by the planner)\nSELECT indexrelname, indisvalid, indisready\nFROM pg_index\nWHERE indrelid = 'orders'::regclass;\n-- indisvalid = false → index exists but planner won't use it (REINDEX CONCURRENTLY failed)\n-- indisready = false → index is still being built\n",[22,3107,3108,3113,3118,3129,3133,3138,3143,3148,3152,3157,3162,3173,3177,3182,3186,3190,3195,3200,3204,3209,3216,3223,3238,3243],{"__ignoreMap":35},[39,3109,3110],{"class":41,"line":42},[39,3111,3112],{"class":45},"-- Reindex without blocking (CONCURRENTLY builds a new index in parallel,\n",[39,3114,3115],{"class":41,"line":49},[39,3116,3117],{"class":45},"-- then swaps — but can fail, leaving an INVALID index you must DROP)\n",[39,3119,3120,3123,3126],{"class":41,"line":55},[39,3121,3122],{"class":670},"REINDEX ",[39,3124,3125],{"class":674},"INDEX",[39,3127,3128],{"class":670}," CONCURRENTLY orders_customer_id_idx;\n",[39,3130,3131],{"class":41,"line":78},[39,3132,82],{"emptyLinePlaceholder":81},[39,3134,3135],{"class":41,"line":85},[39,3136,3137],{"class":45},"-- If CONCURRENTLY fails, the old index is still valid but a new INVALID index exists:\n",[39,3139,3140],{"class":41,"line":91},[39,3141,3142],{"class":45},"-- Check: SELECT indexrelname, indisvalid FROM pg_index WHERE indexrelname LIKE 'orders_%';\n",[39,3144,3145],{"class":41,"line":97},[39,3146,3147],{"class":45},"-- Clean up: DROP INDEX orders_customer_id_idx_ccnew;  -- the failed new index\n",[39,3149,3150],{"class":41,"line":111},[39,3151,82],{"emptyLinePlaceholder":81},[39,3153,3154],{"class":41,"line":116},[39,3155,3156],{"class":45},"-- Cluster: physically reorder rows by an index (improves cache locality)\n",[39,3158,3159],{"class":41,"line":122},[39,3160,3161],{"class":45},"-- ⚠️ Takes ACCESS EXCLUSIVE lock — don't run in production\n",[39,3163,3164,3167,3170],{"class":41,"line":128},[39,3165,3166],{"class":670},"CLUSTER orders ",[39,3168,3169],{"class":674},"USING",[39,3171,3172],{"class":670}," orders_pkey;\n",[39,3174,3175],{"class":41,"line":149},[39,3176,82],{"emptyLinePlaceholder":81},[39,3178,3179],{"class":41,"line":154},[39,3180,3181],{"class":45},"-- Analyze: refresh planner statistics (lightweight, no lock)\n",[39,3183,3184],{"class":41,"line":160},[39,3185,1601],{"class":670},[39,3187,3188],{"class":41,"line":177},[39,3189,82],{"emptyLinePlaceholder":81},[39,3191,3192],{"class":41,"line":182},[39,3193,3194],{"class":45},"-- Vacuum + analyze in one call\n",[39,3196,3197],{"class":41,"line":188},[39,3198,3199],{"class":670},"VACUUM (ANALYZE) orders;\n",[39,3201,3202],{"class":41,"line":210},[39,3203,82],{"emptyLinePlaceholder":81},[39,3205,3206],{"class":41,"line":215},[39,3207,3208],{"class":45},"-- Check index validity (an INVALID index is ignored by the planner)\n",[39,3210,3211,3213],{"class":41,"line":221},[39,3212,1485],{"class":674},[39,3214,3215],{"class":670}," indexrelname, indisvalid, indisready\n",[39,3217,3218,3220],{"class":41,"line":241},[39,3219,1493],{"class":674},[39,3221,3222],{"class":670}," pg_index\n",[39,3224,3225,3227,3230,3232,3235],{"class":41,"line":246},[39,3226,1501],{"class":674},[39,3228,3229],{"class":670}," indrelid ",[39,3231,675],{"class":674},[39,3233,3234],{"class":68}," 'orders'",[39,3236,3237],{"class":670},"::regclass;\n",[39,3239,3240],{"class":41,"line":252},[39,3241,3242],{"class":45},"-- indisvalid = false → index exists but planner won't use it (REINDEX CONCURRENTLY failed)\n",[39,3244,3245],{"class":41,"line":271},[39,3246,3247],{"class":45},"-- indisready = false → index is still being built\n",[14,3249,3251],{"id":3250},"tips-tricks","💡 Tips & Tricks",[3253,3254,3255,3271,3280,3296,3310,3319,3347],"ul",{},[3256,3257,3258,3262,3263,3266,3267,3270],"li",{},[3259,3260,3261],"strong",{},"Idiom"," — enable ",[22,3264,3265],{},"pg_stat_statements"," in ",[22,3268,3269],{},"shared_preload_libraries"," from day one. It's the single most valuable monitoring tool (slowest queries by total time, call counts, I\u002FO, buffer hit ratio). It's cheap (uses a shared memory hash table) and works retroactively — without it, you're guessing at what's slow. Requires a restart to enable.",[3256,3272,3273,3275,3276,3279],{},[3259,3274,3261],{}," — tune autovacuum per-table for high-churn tables. ",[22,3277,3278],{},"ALTER TABLE events SET (autovacuum_vacuum_scale_factor = 0.05)"," vacuums when 5% (not 20%) of rows are dead, keeping up with high-UPDATE tables. The default 20% is too lenient for hot tables — by the time 20% are dead, bloat has already degraded performance.",[3256,3281,3282,3284,3285,3287,3288,3291,3292,3295],{},[3259,3283,3261],{}," — use ",[22,3286,409],{}," (custom format) always. It's compressed, supports parallel restore (",[22,3289,3290],{},"pg_restore -j 4","), and selective restore (",[22,3293,3294],{},"pg_restore -t specific_table","). Plain SQL dumps are slow to restore and can't be parallelized or selectively restored. The only reason for plain SQL is cross-version migration to very old PostgreSQL.",[3256,3297,3298,3300,3301,2756,3304,3307,3308,1357],{},[3259,3299,3261],{}," — use PgBouncer in transaction-pooling mode for workloads with many connections (serverless, microservices). It pools at the transaction boundary, letting thousands of clients share tens of PostgreSQL backends. Reserve session-pooling for apps that need temp tables, ",[22,3302,3303],{},"LISTEN",[22,3305,3306],{},"NOTIFY",", or session-level ",[22,3309,1341],{},[3256,3311,3312,3315,3316,3318],{},[3259,3313,3314],{},"Reliability"," — test your backups by restoring to a test instance regularly. An untested backup is no backup. A ",[22,3317,24],{}," that succeeds but produces a corrupt file (rare but possible — disk errors, interrupted writes) is discovered at restore time, which is too late. Monthly restore drills catch this.",[3256,3320,3321,3324,3325,3328,3329,3332,3333,3336,3337,3340,3341,3343,3344,3346],{},[3259,3322,3323],{},"Performance"," — ",[22,3326,3327],{},"VACUUM FULL"," shrinks a bloated table but takes ",[22,3330,3331],{},"ACCESS EXCLUSIVE"," lock. Use ",[22,3334,3335],{},"pg_repack"," (or ",[22,3338,3339],{},"pg_squeeze",") for zero-lock rewriting in production. Regular ",[22,3342,1277],{}," (autovacuum) prevents bloat from accumulating; ",[22,3345,3327],{}," is for reclaiming already-bloated space, not for routine maintenance.",[3256,3348,3349,3324,3351,3354,3355,3358,3359,3361],{},[3259,3350,3323],{},[22,3352,3353],{},"REINDEX CONCURRENTLY"," rebuilds indexes without blocking writes, but can fail (leaving an invalid index). Check ",[22,3356,3357],{},"pg_index.indisvalid"," after. If it failed, drop the invalid index and retry. For very large indexes, consider ",[22,3360,3335],{}," which rebuilds indexes online.",[14,3363,3365],{"id":3364},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[3253,3367,3368,3385,3409,3423,3441,3454,3463,3473,3494,3515],{},[3256,3369,3370,3375,3376,3378,3379,3381,3382,3384],{},[3259,3371,3372,3374],{},[22,3373,3327],{}," locks the table",": ",[22,3377,3331],{}," — no reads or writes during the rewrite. On a large table, this is downtime. Use ",[22,3380,3335],{}," for online rewriting. Plain ",[22,3383,1277],{}," doesn't shrink but doesn't lock.",[3256,3386,3387,3390,3391,3266,3394,3397,3398,3401,3402,3405,3406,1357],{},[3259,3388,3389],{},"Autovacuum can fall behind",": on very high-churn tables, autovacuum may not keep up with dead tuple creation. Symptoms: growing table size, slowing queries, high ",[22,3392,3393],{},"n_dead_tup",[22,3395,3396],{},"pg_stat_user_tables",". Fix: lower ",[22,3399,3400],{},"autovacuum_vacuum_scale_factor",", increase ",[22,3403,3404],{},"autovacuum_vacuum_cost_limit",", or increase ",[22,3407,3408],{},"autovacuum_max_workers",[3256,3410,3411,3414,3415,3418,3419,3422],{},[3259,3412,3413],{},"Long-running transactions block VACUUM",": dead tuples can't be reclaimed until no transaction could see them — a long-running transaction pins the vacuum horizon, causing bloat across ALL tables (not just the one it's querying). Set ",[22,3416,3417],{},"idle_in_transaction_session_timeout"," and ",[22,3420,3421],{},"statement_timeout"," to prevent runaway transactions.",[3256,3424,3425,3430,3431,3433,3434,3436,3437,3440],{},[3259,3426,3427,3429],{},[22,3428,24],{}," doesn't dump global objects",": roles, tablespaces, and database-level grants need ",[22,3432,476],{},". A ",[22,3435,24],{}," restore without globals leaves you with no roles — all ",[22,3438,3439],{},"GRANT"," statements in the dump fail. Always run both.",[3256,3442,3443,3446,3447,1652,3450,3453],{},[3259,3444,3445],{},"Streaming replication lag",": a standby can lag behind the primary (network, load, slow WAL replay). Reads from the standby may see stale data. Use synchronous replication (",[22,3448,3449],{},"synchronous_commit = on",[22,3451,3452],{},"synchronous_standby_names",") for read-after-write consistency, or route critical reads to the primary.",[3256,3455,3456,3459,3460,1357],{},[3259,3457,3458],{},"Logical replication doesn't replicate sequences or DDL",": a failover to a logical subscriber leaves sequences behind (duplicate key errors on INSERT) and schema differences unhandled. Manually sync sequences after failover: ",[22,3461,3462],{},"SELECT setval(pg_get_serial_sequence('t', 'id'), (SELECT max(id) FROM t))",[3256,3464,3465,3468,3469,3472],{},[3259,3466,3467],{},"PgBouncer transaction pooling and prepared statements",": in transaction mode, prepared statements don't work by default (the server connection changes between transactions, losing the prepared statement). Use ",[22,3470,3471],{},"max_prepared_statements"," (PgBouncer 1.21+) to enable protocol-level prepared statement support, or switch to session pooling for prepared statements.",[3256,3474,3475,3482,3483,3486,3487,3489,3490,3493],{},[3259,3476,3477,3479,3480],{},[22,3478,3265],{}," needs ",[22,3481,3269],{},": it's a shared library loaded at server start — you can't ",[22,3484,3485],{},"CREATE EXTENSION"," it without a restart to add it to ",[22,3488,3269],{},". Plan for a restart when enabling it. The extension also uses ",[22,3491,3492],{},"pg_stat_statements.max"," (default 5000) entries of shared memory — increase for high-query-diversity workloads.",[3256,3495,3496,3375,3499,3502,3503,3506,3507,3510,3511,3514],{},[3259,3497,3498],{},"WAL archive growth",[22,3500,3501],{},"archive_mode = on"," copies every WAL segment to ",[22,3504,3505],{},"archive_command",". If the archive destination fills up, PostgreSQL ",[3259,3508,3509],{},"pauses"," WAL generation (blocking all writes) until archiving succeeds. Monitor archive directory size and set ",[22,3512,3513],{},"archive_timeout"," to force segment switches on low-write databases.",[3256,3516,3517,3522,3523,3526,3527,1357],{},[3259,3518,3519,3521],{},[22,3520,3353],{}," can fail",": if it encounters a lock conflict or unique constraint violation during the build, it aborts, leaving an INVALID index. The old index is still valid and used. Clean up: ",[22,3524,3525],{},"DROP INDEX \u003Cnew_index_name>"," and retry. Check for invalid indexes: ",[22,3528,3529],{},"SELECT indexrelname FROM pg_index WHERE NOT indisvalid",[14,3531,3533],{"id":3532},"spot-the-bug","🧠 Spot the Bug",[19,3535,3536,3537,3539,3540,3543],{},"A team restores their database from a ",[22,3538,24],{}," backup after a failure. The restore completes, but when the application tries to connect, it gets ",[22,3541,3542],{},"FATAL: role \"app_user\" does not exist",". They check the backup — it contains all the tables and data. What went wrong, and what's the fix?",[27,3545,3546],{"language":29},[31,3547,3549],{"className":33,"code":3548,"language":29,"meta":35,"style":35},"# Their backup script:\npg_dump -Fc -d production -f production.dump\n\n# Their restore:\npg_restore -d newdb production.dump\n# All tables and data restored successfully\n\n# Application fails:\n# FATAL: role \"app_user\" does not exist\n",[22,3550,3551,3556,3572,3576,3581,3591,3596,3600,3605],{"__ignoreMap":35},[39,3552,3553],{"class":41,"line":42},[39,3554,3555],{"class":45},"# Their backup script:\n",[39,3557,3558,3560,3562,3564,3567,3569],{"class":41,"line":49},[39,3559,24],{"class":58},[39,3561,62],{"class":61},[39,3563,65],{"class":61},[39,3565,3566],{"class":68}," production",[39,3568,72],{"class":61},[39,3570,3571],{"class":68}," production.dump\n",[39,3573,3574],{"class":41,"line":55},[39,3575,82],{"emptyLinePlaceholder":81},[39,3577,3578],{"class":41,"line":78},[39,3579,3580],{"class":45},"# Their restore:\n",[39,3582,3583,3585,3587,3589],{"class":41,"line":85},[39,3584,163],{"class":58},[39,3586,65],{"class":61},[39,3588,172],{"class":68},[39,3590,3571],{"class":68},[39,3592,3593],{"class":41,"line":91},[39,3594,3595],{"class":45},"# All tables and data restored successfully\n",[39,3597,3598],{"class":41,"line":97},[39,3599,82],{"emptyLinePlaceholder":81},[39,3601,3602],{"class":41,"line":111},[39,3603,3604],{"class":45},"# Application fails:\n",[39,3606,3607],{"class":41,"line":116},[39,3608,3609],{"class":45},"# FATAL: role \"app_user\" does not exist\n",[3611,3612,3613,3617,3644,3650,3732,3735,3835],"details",{},[3614,3615,3616],"summary",{},"Answer",[19,3618,3619,3621,3622,3625,3626,3629,3630,3633,3634,3637,3638,3640,3641,3643],{},[22,3620,24],{}," backs up a ",[3259,3623,3624],{},"single database"," — it does NOT dump global objects (roles, tablespaces, database-level grants). The ",[22,3627,3628],{},"app_user"," role is a global object stored in ",[22,3631,3632],{},"pg_authid",", not in the ",[22,3635,3636],{},"production"," database's catalog. The restore created all tables and data, but no roles — so ",[22,3639,3439],{}," statements in the dump that reference ",[22,3642,3628],{}," failed silently (or were skipped), and the application can't connect because the role doesn't exist.",[19,3645,3646,3647,3649],{},"The fix: always dump globals separately with ",[22,3648,476],{},", and restore them BEFORE the database dump:",[27,3651,3652],{"language":29},[31,3653,3655],{"className":33,"code":3654,"language":29,"meta":35,"style":35},"# Backup — ALWAYS run both:\npg_dumpall --globals-only -f globals.sql\npg_dump -Fc -d production -f production.dump\n\n# Restore — globals FIRST, then the database:\npsql -f globals.sql          # creates roles, tablespaces\ncreatedb production          # create the empty database\npg_restore -d production production.dump  # restore schema + data\n",[22,3656,3657,3662,3673,3687,3691,3696,3708,3718],{"__ignoreMap":35},[39,3658,3659],{"class":41,"line":42},[39,3660,3661],{"class":45},"# Backup — ALWAYS run both:\n",[39,3663,3664,3666,3668,3670],{"class":41,"line":49},[39,3665,321],{"class":58},[39,3667,324],{"class":61},[39,3669,72],{"class":61},[39,3671,3672],{"class":68}," globals.sql\n",[39,3674,3675,3677,3679,3681,3683,3685],{"class":41,"line":55},[39,3676,24],{"class":58},[39,3678,62],{"class":61},[39,3680,65],{"class":61},[39,3682,3566],{"class":68},[39,3684,72],{"class":61},[39,3686,3571],{"class":68},[39,3688,3689],{"class":41,"line":78},[39,3690,82],{"emptyLinePlaceholder":81},[39,3692,3693],{"class":41,"line":85},[39,3694,3695],{"class":45},"# Restore — globals FIRST, then the database:\n",[39,3697,3698,3701,3703,3705],{"class":41,"line":91},[39,3699,3700],{"class":58},"psql",[39,3702,72],{"class":61},[39,3704,329],{"class":68},[39,3706,3707],{"class":45},"          # creates roles, tablespaces\n",[39,3709,3710,3713,3715],{"class":41,"line":97},[39,3711,3712],{"class":58},"createdb",[39,3714,3566],{"class":68},[39,3716,3717],{"class":45},"          # create the empty database\n",[39,3719,3720,3722,3724,3726,3729],{"class":41,"line":111},[39,3721,163],{"class":58},[39,3723,65],{"class":61},[39,3725,3566],{"class":68},[39,3727,3728],{"class":68}," production.dump",[39,3730,3731],{"class":45},"  # restore schema + data\n",[19,3733,3734],{},"The updated backup script should include both:",[27,3736,3737],{"language":29},[31,3738,3740],{"className":33,"code":3739,"language":29,"meta":35,"style":35},"#!\u002Fbin\u002Fbash\nset -euo pipefail\nDATE=$(date +%Y%m%d_%H%M%S)\n\n# Global objects (roles, tablespaces) — REQUIRED\npg_dumpall --globals-only -f \"\u002Fbackup\u002Fglobals_$DATE.sql\"\n\n# Database dump\npg_dump -Fc -d production -f \"\u002Fbackup\u002Fproduction_$DATE.dump\"\n\necho \"Backup complete: globals + production_$DATE.dump\"\n",[22,3741,3742,3746,3754,3768,3772,3777,3792,3796,3801,3820,3824],{"__ignoreMap":35},[39,3743,3744],{"class":41,"line":42},[39,3745,645],{"class":45},[39,3747,3748,3750,3752],{"class":41,"line":49},[39,3749,655],{"class":61},[39,3751,658],{"class":61},[39,3753,661],{"class":68},[39,3755,3756,3758,3760,3762,3764,3766],{"class":41,"line":55},[39,3757,703],{"class":670},[39,3759,675],{"class":674},[39,3761,708],{"class":670},[39,3763,711],{"class":58},[39,3765,714],{"class":68},[39,3767,717],{"class":670},[39,3769,3770],{"class":41,"line":78},[39,3771,82],{"emptyLinePlaceholder":81},[39,3773,3774],{"class":41,"line":85},[39,3775,3776],{"class":45},"# Global objects (roles, tablespaces) — REQUIRED\n",[39,3778,3779,3781,3783,3785,3788,3790],{"class":41,"line":91},[39,3780,321],{"class":58},[39,3782,324],{"class":61},[39,3784,72],{"class":61},[39,3786,3787],{"class":68}," \"\u002Fbackup\u002Fglobals_",[39,3789,751],{"class":670},[39,3791,754],{"class":68},[39,3793,3794],{"class":41,"line":97},[39,3795,82],{"emptyLinePlaceholder":81},[39,3797,3798],{"class":41,"line":111},[39,3799,3800],{"class":45},"# Database dump\n",[39,3802,3803,3805,3807,3809,3811,3813,3816,3818],{"class":41,"line":116},[39,3804,24],{"class":58},[39,3806,62],{"class":61},[39,3808,65],{"class":61},[39,3810,3566],{"class":68},[39,3812,72],{"class":61},[39,3814,3815],{"class":68}," \"\u002Fbackup\u002Fproduction_",[39,3817,751],{"class":670},[39,3819,812],{"class":68},[39,3821,3822],{"class":41,"line":122},[39,3823,82],{"emptyLinePlaceholder":81},[39,3825,3826,3828,3831,3833],{"class":41,"line":128},[39,3827,971],{"class":61},[39,3829,3830],{"class":68}," \"Backup complete: globals + production_",[39,3832,751],{"class":670},[39,3834,812],{"class":68},[19,3836,3837,3375,3840,3842,3843,3845,3846,3848],{},[3259,3838,3839],{},"The lesson",[22,3841,24],{}," dumps one database's schema and data, but NOT roles, tablespaces, or database-level grants (those are cluster-level globals). Always pair ",[22,3844,24],{}," with ",[22,3847,476],{},". An untested restore would have caught this — another reason to test restores regularly.",[3850,3851,3852],"style",{},"html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}",{"title":35,"searchDepth":49,"depth":49,"links":3854},[3855,3858,3859,3860,3867,3871,3872,3877,3878,3879,3880],{"id":16,"depth":49,"text":17,"children":3856},[3857],{"id":372,"depth":55,"text":373},{"id":483,"depth":49,"text":484},{"id":632,"depth":49,"text":633},{"id":1270,"depth":49,"text":1271,"children":3861},[3862,3863,3864,3865,3866],{"id":1281,"depth":55,"text":1282},{"id":1295,"depth":55,"text":1296},{"id":1307,"depth":55,"text":1308},{"id":1526,"depth":55,"text":1527},{"id":1704,"depth":55,"text":1705},{"id":1835,"depth":49,"text":1836,"children":3868},[3869,3870],{"id":1839,"depth":55,"text":1840},{"id":1851,"depth":55,"text":1852},{"id":2022,"depth":49,"text":2023},{"id":2249,"depth":49,"text":2250,"children":3873},[3874,3875,3876],{"id":2253,"depth":55,"text":2254},{"id":2515,"depth":55,"text":2516},{"id":2835,"depth":55,"text":2836},{"id":3099,"depth":49,"text":3100},{"id":3250,"depth":49,"text":3251},{"id":3364,"depth":49,"text":3365},{"id":3532,"depth":49,"text":3533},"md",{},"\u002Fsql\u002F28-database-administration",{"title":5,"description":35},"sql\u002F28-database-administration","gNgfg8-piw_6ERaTqKOFiAw8zynl3prOga_mO0m4aHM",1789924655175]