[{"data":1,"prerenderedAt":2154},["ShallowReactive",2],{"content:\u002F10-databases\u002F04-postgresql-architecture-mvcc":3},{"title":4,"description":5,"path":6,"body":7},"PostgreSQL: архитектура и MVCC","PostgreSQL кажется простым: приложение отправляет SQL, база возвращает строки. Но под этим интерфейсом живёт много механизмов: отдельный backend process на соединение, общая память, WAL, чекпоинты, фоновые процессы, MVCC, autovacuum, статистика и планировщик.","\u002F10-databases\u002F04-postgresql-architecture-mvcc",{"type":8,"value":9,"toc":2120},"minimark",[10,14,17,37,47,50,53,58,63,70,76,79,102,113,115,119,126,132,135,173,179,182,207,209,213,216,222,225,239,245,248,279,281,285,288,294,297,347,360,363,365,369,373,376,382,389,436,448,450,454,458,464,470,485,488,502,505,511,513,517,520,530,539,545,548,597,604,606,610,618,624,627,644,651,653,657,662,686,700,708,711,792,795,859,866,868,872,875,878,881,895,897,901,904,1087,1090,1092,1096,1099,1102,1108,1116,1118,1122,1128,1174,1177,1191,1194,1212,1240,1245,1247,1251,1263,1266,1277,1283,1286,1306,1384,1386,1390,1403,1441,1444,1527,1530,1536,1539,1542,1548,1551,1553,1557,1561,1611,1614,1618,1658,1661,1665,1713,1716,1720,1796,1802,1804,1808,1865,1867,1871,1898,1900,1904,1939,2057,2059,2063,2116],[11,12,4],"h1",{"id":13},"postgresql-архитектура-и-mvcc",[15,16,5],"p",{},[15,18,19,20,24,25,28,29,32,33,36],{},"Для Go backend-разработчика это не академическая деталь. От понимания внутренней модели зависит, почему ",[21,22,23],"code",{},"UPDATE"," раздувает таблицу, почему длинная транзакция ломает vacuum, почему ",[21,26,27],{},"SELECT"," может видеть старую версию строки, почему ",[21,30,31],{},"EXPLAIN"," показывает ",[21,34,35],{},"Heap Fetches",", и почему база может быть медленной даже без очевидных lock'ов.",[38,39,45],"pre",{"className":40,"code":42,"language":43,"meta":44},[41],"language-text","Go service\n   │ SQL over TCP \u002F Unix socket\n   ▼\nPostgreSQL backend process\n   │\n   ├─ shared buffers\n   ├─ WAL buffers -> WAL files\n   ├─ locks \u002F snapshots \u002F transaction state\n   └─ heap tables + indexes + TOAST\n","text","",[21,46,42],{"__ignoreMap":44},[15,48,49],{},"PostgreSQL 18 - текущая major-версия документации на момент написания. Большинство идей ниже стабильны много лет, но детали EXPLAIN, vacuum и настройки могут развиваться.",[51,52],"hr",{},[54,55,57],"h2",{"id":56},"архитектура-postgresql","Архитектура PostgreSQL",[59,60,62],"h3",{"id":61},"process-model","Process model",[15,64,65,66,69],{},"PostgreSQL использует process-based architecture. Главный процесс ",[21,67,68],{},"postmaster"," принимает соединения и создаёт отдельный server process для клиента. Часто его называют backend process.",[38,71,74],{"className":72,"code":73,"language":43,"meta":44},[41],"postmaster\n  ├─ backend for connection #1\n  ├─ backend for connection #2\n  ├─ checkpointer\n  ├─ background writer\n  ├─ walwriter\n  ├─ autovacuum launcher\n  ├─ autovacuum workers\n  ├─ stats collector \u002F cumulative stats\n  └─ optional workers: logical replication, parallel query, extensions\n",[21,75,73],{"__ignoreMap":44},[15,77,78],{},"Следствия для приложения:",[80,81,82,86,92,99],"ul",{},[83,84,85],"li",{},"каждое соединение не бесплатно;",[83,87,88,91],{},[21,89,90],{},"max_connections=1000"," обычно хуже, чем pooler + разумное число backend'ов;",[83,93,94,95,98],{},"память вроде ",[21,96,97],{},"work_mem"," выделяется не \"на сервер\", а на операции внутри запросов и может умножаться на число активных соединений;",[83,100,101],{},"долгий запрос занимает отдельный backend process, держит snapshot, locks и ресурсы.",[15,103,104,105,108,109,112],{},"В Go это означает: ",[21,106,107],{},"database\u002Fsql"," или ",[21,110,111],{},"pgxpool"," должны иметь лимиты. Если каждый pod держит по 100 соединений, а pod'ов 30, PostgreSQL увидит 3000 потенциальных backend'ов. Для production часто используют PgBouncer или другой connection pooler перед PostgreSQL.",[51,114],{},[59,116,118],{"id":117},"shared-memory-и-caches","Shared memory и caches",[15,120,121,122,125],{},"PostgreSQL хранит общие структуры в shared memory. Самая известная - ",[21,123,124],{},"shared_buffers",": кэш страниц таблиц и индексов внутри PostgreSQL.",[38,127,130],{"className":128,"code":129,"language":43,"meta":44},[41],"query\n  │\n  ├─ ищет страницу в shared_buffers\n  │    ├─ hit  -> читает из памяти PostgreSQL\n  │    └─ miss -> читает с диска \u002F OS cache -> кладёт в shared_buffers\n  │\n  └─ меняет страницу -> page becomes dirty\n",[21,131,129],{"__ignoreMap":44},[15,133,134],{},"Важно не путать:",[80,136,137,142,145,150,167],{},[83,138,139,141],{},[21,140,124],{}," - кэш страниц внутри PostgreSQL;",[83,143,144],{},"page cache операционной системы - кэш файлов на уровне OS;",[83,146,147,149],{},[21,148,97],{}," - память на sort\u002Fhash\u002Fmaterialize операции внутри запроса;",[83,151,152,155,156,159,160,159,163,166],{},[21,153,154],{},"maintenance_work_mem"," - память для ",[21,157,158],{},"VACUUM",", ",[21,161,162],{},"CREATE INDEX",[21,164,165],{},"ALTER TABLE ADD FOREIGN KEY",";",[83,168,169,172],{},[21,170,171],{},"temp_buffers"," - session-local buffers для temporary tables.",[15,174,175,176,178],{},"Официальная документация даёт practical starting point для выделенного DB-сервера: ",[21,177,124],{}," часто начинают примерно с 25% RAM, но редко имеет смысл уходить сильно выше 40%, потому что PostgreSQL всё равно полагается на OS cache.",[15,180,181],{},"Для разработчика важнее не запомнить магическое число, а понимать trade-off:",[80,183,184,190,196,202],{},[83,185,186,187,189],{},"маленький ",[21,188,124],{}," -> больше чтений через OS\u002Fdisk;",[83,191,192,193,195],{},"слишком большой ",[21,194,124],{}," -> больше давление на memory и checkpoint\u002FWAL поведение;",[83,197,198,199,201],{},"большой ",[21,200,97],{}," при сотнях соединений -> риск OOM;",[83,203,186,204,206],{},[21,205,97],{}," -> sort\u002Fhash spill на диск.",[51,208],{},[59,210,212],{"id":211},"wal-журнал-перед-данными","WAL: журнал перед данными",[15,214,215],{},"WAL - Write-Ahead Log. Идея: перед тем как изменённая data page считается durable, PostgreSQL записывает описание изменения в WAL.",[38,217,220],{"className":218,"code":219,"language":43,"meta":44},[41],"UPDATE users SET name = 'Ann' WHERE id = 10;\n\n1. изменить страницу в shared buffers\n2. записать WAL record\n3. на COMMIT гарантировать WAL до нужного LSN\n4. позже dirty page попадёт в data file\n",[21,221,219],{"__ignoreMap":44},[15,223,224],{},"WAL нужен для:",[80,226,227,230,233,236],{},[83,228,229],{},"crash recovery: после падения PostgreSQL проигрывает WAL и доводит data files до согласованного состояния;",[83,231,232],{},"replication: standby получает WAL и применяет изменения;",[83,234,235],{},"PITR: base backup + archived WAL позволяют восстановиться на момент времени;",[83,237,238],{},"logical decoding: изменения можно декодировать в логические события.",[15,240,241,244],{},[21,242,243],{},"COMMIT"," обычно ждёт fsync WAL, а не немедленную запись всех изменённых table pages. Поэтому PostgreSQL может быть быстрым: он пишет последовательный журнал, а грязные страницы сбрасывает позже.",[15,246,247],{},"Что полезно знать в backend:",[80,249,250,253,260,266,276],{},[83,251,252],{},"много мелких commits создают много WAL и fsync pressure;",[83,254,255,256,259],{},"bulk load иногда ускоряют через batching, ",[21,257,258],{},"COPY",", временные таблицы;",[83,261,262,265],{},[21,263,264],{},"synchronous_commit=off"," уменьшает latency, но допускает потерю последних committed transactions при crash;",[83,267,268,269,271,272,275],{},"большие ",[21,270,23],{},"\u002F",[21,273,274],{},"DELETE"," пишут много WAL и создают dead tuples;",[83,277,278],{},"replication lag часто означает, что standby не успевает получить или применить WAL.",[51,280],{},[59,282,284],{"id":283},"checkpoints-и-background-writer","Checkpoints и background writer",[15,286,287],{},"Если WAL растёт бесконечно, recovery после crash станет долгим. Поэтому PostgreSQL периодически делает checkpoint: точку, до которой все dirty pages должны быть записаны в data files.",[38,289,292],{"className":290,"code":291,"language":43,"meta":44},[41],"WAL stream:  ... changes ... | checkpoint | ... new changes ...\n                             ^\n                             recovery can start from here\n",[21,293,291],{"__ignoreMap":44},[15,295,296],{},"Важные процессы:",[298,299,300,313],"table",{},[301,302,303],"thead",{},[304,305,306,310],"tr",{},[307,308,309],"th",{},"Процесс",[307,311,312],{},"Роль",[314,315,316,327,337],"tbody",{},[304,317,318,324],{},[319,320,321],"td",{},[21,322,323],{},"checkpointer",[319,325,326],{},"Организует checkpoint и fsync dirty pages.",[304,328,329,334],{},[319,330,331],{},[21,332,333],{},"background writer",[319,335,336],{},"Плавно сбрасывает dirty pages, чтобы backend'и реже сами ждали запись.",[304,338,339,344],{},[319,340,341],{},[21,342,343],{},"walwriter",[319,345,346],{},"Пишет WAL buffers в WAL files.",[15,348,349,350,159,353,159,356,359],{},"Плохой признак - checkpoint spikes: latency резко растёт, потому что система вынуждена быстро записать много грязных страниц. На это влияют ",[21,351,352],{},"checkpoint_timeout",[21,354,355],{},"max_wal_size",[21,357,358],{},"checkpoint_completion_target",", скорость диска и объём write workload.",[15,361,362],{},"Для Go-сервиса это проявляется как \"иногда запросы к базе внезапно становятся медленными\". Причина может быть не в конкретном SQL, а в I\u002FO фоне: checkpoint, autovacuum, backup, WAL archiving.",[51,364],{},[54,366,368],{"id":367},"storage-layout","Storage layout",[59,370,372],{"id":371},"heap-pages-и-tuple","Heap, pages и tuple",[15,374,375],{},"Обычная таблица в PostgreSQL хранится как heap: набор страниц, обычно по 8 KB. Строка внутри страницы называется tuple.",[38,377,380],{"className":378,"code":379,"language":43,"meta":44},[41],"table heap\n  page 0\n    tuple: id=1, xmin=100, xmax=0\n    tuple: id=2, xmin=101, xmax=0\n  page 1\n    tuple: id=3, xmin=102, xmax=0\n",[21,381,379],{"__ignoreMap":44},[15,383,384,385,388],{},"Индекс не хранит всю строку. Обычно он хранит key + pointer на heap tuple. Этот pointer называется TID\u002Fctid: ",[21,386,387],{},"(block number, item offset)",".",[38,390,394],{"className":391,"code":392,"language":393,"meta":44,"style":44},"language-sql shiki shiki-themes github-dark","SELECT ctid, xmin, xmax, id, email\nFROM users\nWHERE id = 42;\n","sql",[21,395,396,408,417],{"__ignoreMap":44},[397,398,401,404],"span",{"class":399,"line":400},"line",1,[397,402,27],{"class":403},"snl16",[397,405,407],{"class":406},"s95oV"," ctid, xmin, xmax, id, email\n",[397,409,411,414],{"class":399,"line":410},2,[397,412,413],{"class":403},"FROM",[397,415,416],{"class":406}," users\n",[397,418,420,423,426,429,433],{"class":399,"line":419},3,[397,421,422],{"class":403},"WHERE",[397,424,425],{"class":406}," id ",[397,427,428],{"class":403},"=",[397,430,432],{"class":431},"sDLfK"," 42",[397,434,435],{"class":406},";\n",[15,437,438,159,441,159,444,447],{},[21,439,440],{},"ctid",[21,442,443],{},"xmin",[21,445,446],{},"xmax"," - системные колонки. Их не используют как бизнес-API, но для понимания MVCC они полезны.",[51,449],{},[54,451,453],{"id":452},"mvcc-и-vacuum","MVCC и vacuum",[59,455,457],{"id":456},"mvcc-несколько-версий-одной-строки","MVCC: несколько версий одной строки",[15,459,460,461,463],{},"MVCC - Multi-Version Concurrency Control. PostgreSQL не перезаписывает строку \"на месте\" в логическом смысле. ",[21,462,23],{}," создаёт новую версию tuple, а старая остаётся в heap, пока может быть видна старым transactions.",[38,465,468],{"className":466,"code":467,"language":43,"meta":44},[41],"T1: UPDATE accounts SET balance = 90 WHERE id = 1;\n\nold tuple: id=1, balance=100, xmin=10, xmax=20\nnew tuple: id=1, balance=90,  xmin=20, xmax=0\n",[21,469,467],{"__ignoreMap":44},[15,471,472,474,475,478,480,481,484],{},[21,473,443],{}," - transaction id, который создал tuple.",[476,477],"br",{},[21,479,446],{}," - transaction id, который удалил или заменил tuple. Если ",[21,482,483],{},"xmax=0",", версия не удалена.",[15,486,487],{},"Видимость версии зависит от snapshot:",[80,489,490,493,496,499],{},[83,491,492],{},"transaction видит tuple, если создавшая transaction committed и не находится \"в будущем\" относительно snapshot;",[83,494,495],{},"transaction не видит tuple, если tuple удалён committed transaction, которая видима в snapshot;",[83,497,498],{},"in-progress transactions обычно не видны другим transactions;",[83,500,501],{},"свои изменения transaction видит сама.",[15,503,504],{},"Это позволяет readers не блокировать writers, а writers не блокировать обычных readers:",[38,506,509],{"className":507,"code":508,"language":43,"meta":44},[41],"T1: BEGIN;\nT1: SELECT balance FROM accounts WHERE id = 1; -- видит 100\n\nT2: UPDATE accounts SET balance = 90 WHERE id = 1;\nT2: COMMIT;\n\nT1: SELECT balance FROM accounts WHERE id = 1;\n-- Read Committed: новый statement увидит 90\n-- Repeatable Read: transaction продолжит видеть 100\n",[21,510,508],{"__ignoreMap":44},[51,512],{},[59,514,516],{"id":515},"snapshots-и-isolation-levels","Snapshots и isolation levels",[15,518,519],{},"Snapshot - это представление о том, какие transaction id уже committed, какие ещё active, и где граница \"будущего\".",[15,521,522,523,526,527,529],{},"В ",[21,524,525],{},"READ COMMITTED"," PostgreSQL создаёт новый snapshot на каждый statement. Поэтому два ",[21,528,27],{}," внутри одной transaction могут увидеть разные данные.",[15,531,522,532,535,536,538],{},[21,533,534],{},"REPEATABLE READ"," snapshot фиксируется на начало transaction. Повторный ",[21,537,27],{}," видит ту же картину, но возможны serialization errors при конфликтующих writes.",[15,540,522,541,544],{},[21,542,543],{},"SERIALIZABLE"," PostgreSQL использует Serializable Snapshot Isolation и может отменить transaction с ошибкой сериализации, если параллельное исполнение нельзя представить как последовательное. Приложение обязано retry'ить такие transaction.",[15,546,547],{},"Для Go-кода:",[38,549,554],{"className":550,"code":551,"language":552,"meta":553,"style":44},"language-go shiki shiki-themes github-dark","tx, err := db.BeginTx(ctx, &sql.TxOptions{\n    Isolation: sql.LevelSerializable,\n})\n","go","no-run",[21,555,556,587,592],{"__ignoreMap":44},[397,557,558,561,564,567,571,574,577,579,581,584],{"class":399,"line":400},[397,559,560],{"class":406},"tx, err ",[397,562,563],{"class":403},":=",[397,565,566],{"class":406}," db.",[397,568,570],{"class":569},"svObZ","BeginTx",[397,572,573],{"class":406},"(ctx, ",[397,575,576],{"class":403},"&",[397,578,393],{"class":569},[397,580,388],{"class":406},[397,582,583],{"class":569},"TxOptions",[397,585,586],{"class":406},"{\n",[397,588,589],{"class":399,"line":410},[397,590,591],{"class":406},"    Isolation: sql.LevelSerializable,\n",[397,593,594],{"class":399,"line":419},[397,595,596],{"class":406},"})\n",[15,598,599,600,603],{},"Если вы выбираете высокий уровень изоляции, обработайте ",[21,601,602],{},"serialization_failure"," и повторите бизнес-операцию целиком. Нельзя просто повторить последний SQL: вся логика читала старый snapshot.",[51,605],{},[59,607,609],{"id":608},"dead-tuples-и-bloat","Dead tuples и bloat",[15,611,612,613,108,615,617],{},"MVCC даёт concurrency, но создаёт мусор. После ",[21,614,23],{},[21,616,274],{}," старая версия строки остаётся в таблице. Когда никакая active transaction уже не может её видеть, версия становится dead tuple.",[38,619,622],{"className":620,"code":621,"language":43,"meta":44},[41],"UPDATE x 100000 times\n  -> 100000 old tuple versions\n  -> heap grows\n  -> indexes may grow\n  -> queries read more pages\n",[21,623,621],{"__ignoreMap":44},[15,625,626],{},"Bloat - лишнее место в таблицах и индексах, занятое старыми версиями, пустыми страницами и фрагментацией. Он опасен не только размером на диске:",[80,628,629,632,635,638,641],{},[83,630,631],{},"больше страниц надо читать;",[83,633,634],{},"хуже cache hit ratio;",[83,636,637],{},"autovacuum делает больше работы;",[83,639,640],{},"index scan может ходить по мёртвым index entries;",[83,642,643],{},"backup и replication получают больше данных.",[15,645,646,647,650],{},"Типичная причина bloat в приложении - частые updates одних и тех же строк: counters, ",[21,648,649],{},"updated_at",", статусные поля, JSONB-документы.",[51,652],{},[59,654,656],{"id":655},"vacuum-и-autovacuum","VACUUM и autovacuum",[15,658,659,661],{},[21,660,158],{}," делает несколько важных вещей:",[80,663,664,667,670,673,676,679],{},[83,665,666],{},"удаляет dead tuple versions, которые больше никому не видны;",[83,668,669],{},"помечает место как доступное для reuse;",[83,671,672],{},"чистит связанные index entries;",[83,674,675],{},"обновляет visibility map;",[83,677,678],{},"помогает защититься от transaction ID wraparound;",[83,680,681,682,685],{},"вместе с ",[21,683,684],{},"ANALYZE"," обновляет статистику для планировщика.",[15,687,688,689,691,692,695,696,699],{},"Обычный ",[21,690,158],{}," обычно не возвращает место операционной системе. Он делает место переиспользуемым внутри таблицы. ",[21,693,694],{},"VACUUM FULL"," переписывает таблицу, возвращает место OS, но требует ",[21,697,698],{},"ACCESS EXCLUSIVE"," lock и может быть очень тяжёлым.",[15,701,702,703,271,705,707],{},"Autovacuum состоит из launcher и workers. Он автоматически запускает ",[21,704,158],{},[21,706,684],{},", когда таблица достаточно изменилась. В production autovacuum почти никогда нельзя просто выключать: если он не справляется, его настраивают.",[15,709,710],{},"Симптомы проблем:",[38,712,714],{"className":391,"code":713,"language":393,"meta":44,"style":44},"SELECT\n  relname,\n  n_live_tup,\n  n_dead_tup,\n  last_vacuum,\n  last_autovacuum,\n  last_analyze,\n  last_autoanalyze\nFROM pg_stat_user_tables\nORDER BY n_dead_tup DESC\nLIMIT 20;\n",[21,715,716,721,726,731,737,743,749,755,761,769,781],{"__ignoreMap":44},[397,717,718],{"class":399,"line":400},[397,719,720],{"class":403},"SELECT\n",[397,722,723],{"class":399,"line":410},[397,724,725],{"class":406},"  relname,\n",[397,727,728],{"class":399,"line":419},[397,729,730],{"class":406},"  n_live_tup,\n",[397,732,734],{"class":399,"line":733},4,[397,735,736],{"class":406},"  n_dead_tup,\n",[397,738,740],{"class":399,"line":739},5,[397,741,742],{"class":406},"  last_vacuum,\n",[397,744,746],{"class":399,"line":745},6,[397,747,748],{"class":406},"  last_autovacuum,\n",[397,750,752],{"class":399,"line":751},7,[397,753,754],{"class":406},"  last_analyze,\n",[397,756,758],{"class":399,"line":757},8,[397,759,760],{"class":406},"  last_autoanalyze\n",[397,762,764,766],{"class":399,"line":763},9,[397,765,413],{"class":403},[397,767,768],{"class":406}," pg_stat_user_tables\n",[397,770,772,775,778],{"class":399,"line":771},10,[397,773,774],{"class":403},"ORDER BY",[397,776,777],{"class":406}," n_dead_tup ",[397,779,780],{"class":403},"DESC\n",[397,782,784,787,790],{"class":399,"line":783},11,[397,785,786],{"class":403},"LIMIT",[397,788,789],{"class":431}," 20",[397,791,435],{"class":406},[15,793,794],{},"Особенно опасны long-running transactions:",[38,796,798],{"className":391,"code":797,"language":393,"meta":44,"style":44},"SELECT pid, now() - xact_start AS age, state, query\nFROM pg_stat_activity\nWHERE xact_start IS NOT NULL\nORDER BY age DESC;\n",[21,799,800,831,838,847],{"__ignoreMap":44},[397,801,802,804,807,810,813,816,819,822,825,828],{"class":399,"line":400},[397,803,27],{"class":403},[397,805,806],{"class":406}," pid, ",[397,808,809],{"class":403},"now",[397,811,812],{"class":406},"() ",[397,814,815],{"class":403},"-",[397,817,818],{"class":406}," xact_start ",[397,820,821],{"class":403},"AS",[397,823,824],{"class":406}," age, ",[397,826,827],{"class":403},"state",[397,829,830],{"class":406},", query\n",[397,832,833,835],{"class":399,"line":410},[397,834,413],{"class":403},[397,836,837],{"class":406}," pg_stat_activity\n",[397,839,840,842,844],{"class":399,"line":419},[397,841,422],{"class":403},[397,843,818],{"class":406},[397,845,846],{"class":403},"IS NOT NULL\n",[397,848,849,851,854,857],{"class":399,"line":733},[397,850,774],{"class":403},[397,852,853],{"class":406}," age ",[397,855,856],{"class":403},"DESC",[397,858,435],{"class":406},[15,860,861,862,865],{},"Если transaction держит старый snapshot, vacuum не может убрать tuple versions, которые потенциально ей видны. В Go это часто выглядит как забытый ",[21,863,864],{},"tx.Rollback()",", долгий cursor, открытая transaction вокруг внешнего HTTP-вызова или миграция, которая держит lock дольше ожидаемого.",[51,867],{},[59,869,871],{"id":870},"freeze-и-transaction-id-wraparound","Freeze и transaction ID wraparound",[15,873,874],{},"Transaction ID в PostgreSQL конечен. Чтобы старые row versions не стали \"будущими\" после wraparound, PostgreSQL замораживает очень старые tuple versions. Frozen rows считаются видимыми для всех будущих transactions.",[15,876,877],{},"Этим занимается vacuum. Если таблицы долго не vacuum'ятся, PostgreSQL вынужден запускать aggressive vacuum, а в худшем случае может начать защищать кластер от wraparound очень неприятными способами.",[15,879,880],{},"Практический вывод:",[80,882,883,886,889,892],{},[83,884,885],{},"не отключать autovacuum;",[83,887,888],{},"следить за возрастом database\u002Ftable XID;",[83,890,891],{},"не держать долгие transactions без необходимости;",[83,893,894],{},"помнить, что append-only таблицам тоже нужен vacuum для freeze, даже если dead tuples мало.",[51,896],{},[54,898,900],{"id":899},"incident-symptoms-mvcc-vacuum-wal-bloat","Incident symptoms: MVCC, vacuum, WAL, bloat",[15,902,903],{},"В production PostgreSQL редко сообщает \"у вас проблема с MVCC\". Симптомы приходят как рост latency, диска, replication lag или странные планы. Полезно связывать наблюдение с механизмом:",[298,905,906,923],{},[301,907,908],{},[304,909,910,914,917,920],{},[307,911,913],{"align":912},"left","Симптом",[307,915,916],{"align":912},"Возможная причина",[307,918,919],{"align":912},"Где смотреть",[307,921,922],{"align":912},"Первый безопасный шаг",[314,924,925,944,972,991,1008,1028,1048,1067],{},[304,926,927,932,935,941],{},[319,928,929,930],{"align":912},"Таблица быстро растёт после частых ",[21,931,23],{},[319,933,934],{"align":912},"Dead tuples и heap\u002Findex bloat",[319,936,937,940],{"align":912},[21,938,939],{},"pg_stat_user_tables.n_dead_tup",", размер relation\u002Findex",[319,942,943],{"align":912},"Найти hot update pattern, проверить autovacuum, убрать лишние индексы",[304,945,946,951,954,962],{},[319,947,948,950],{"align":912},[21,949,158],{}," идёт, но место на диске не возвращается",[319,952,953],{"align":912},"Обычный vacuum переиспользует место внутри relation",[319,955,956,159,959],{"align":912},[21,957,958],{},"pg_relation_size",[21,960,961],{},"pg_total_relation_size",[319,963,964,965,967,968,971],{"align":912},"Не запускать сразу ",[21,966,694],{},"; оценить ",[21,969,970],{},"pg_repack","\u002Fперепаковку в окно",[304,973,974,977,980,988],{},[319,975,976],{"align":912},"Autovacuum не чистит старые версии",[319,978,979],{"align":912},"Long-running transaction держит старый snapshot",[319,981,982,159,985],{"align":912},[21,983,984],{},"pg_stat_activity.xact_start",[21,986,987],{},"backend_xmin",[319,989,990],{"align":912},"Завершить\u002Fпочинить долгую transaction, потом дать vacuum догнать",[304,992,993,996,999,1005],{},[319,994,995],{"align":912},"Внезапный рост replication lag",[319,997,998],{"align":912},"Standby не успевает получить или применить WAL",[319,1000,1001,1004],{"align":912},[21,1002,1003],{},"pg_stat_replication",", WAL archive, I\u002FO metrics",[319,1006,1007],{"align":912},"Снизить write burst, проверить disk\u002Fnetwork, не удалять WAL вручную",[304,1009,1010,1016,1019,1025],{},[319,1011,1012,1013],{"align":912},"Диск быстро заполняется ",[21,1014,1015],{},"pg_wal",[319,1017,1018],{"align":912},"Checkpoint\u002Farchive\u002Freplica не успевают",[319,1020,1021,1024],{"align":912},[21,1022,1023],{},"pg_stat_bgwriter",", archive status, replication slots",[319,1026,1027],{"align":912},"Проверить slots\u002Farchive, освободить место безопасно, устранить источник WAL",[304,1029,1030,1033,1036,1042],{},[319,1031,1032],{"align":912},"Периодические latency spikes",[319,1034,1035],{"align":912},"Checkpoint пишет много dirty pages",[319,1037,1038,1041],{"align":912},[21,1039,1040],{},"pg_stat_bgwriter.checkpoints_req",", I\u002FO latency",[319,1043,1044,1045,1047],{"align":912},"Настроить checkpoint pacing, ",[21,1046,355],{},", workload batching",[304,1049,1050,1055,1058,1064],{},[319,1051,1052,1053],{"align":912},"Index-only scan делает много ",[21,1054,35],{},[319,1056,1057],{"align":912},"Visibility map не all-visible из-за writes\u002Fvacuum lag",[319,1059,1060,1063],{"align":912},[21,1061,1062],{},"EXPLAIN (ANALYZE, BUFFERS)",", vacuum stats",[319,1065,1066],{"align":912},"Проверить write churn и autovacuum, не обещать index-only как гарантию",[304,1068,1069,1072,1075,1082],{},[319,1070,1071],{"align":912},"Запросы внезапно выбрали seq scan",[319,1073,1074],{"align":912},"Статистика устарела после bulk changes",[319,1076,1077,159,1079],{"align":912},[21,1078,31],{},[21,1080,1081],{},"pg_stat_user_tables.last_analyze",[319,1083,1084,1086],{"align":912},[21,1085,684],{},", затем разбираться с data skew и stats target",[15,1088,1089],{},"Senior review heuristic: перед \"добавим индекс\" или \"поднимем timeout\" сначала спросите, что происходит с версиями строк, WAL и vacuum. Многие DB-инциденты выглядят как slow query, но начинаются с долгой transaction, write amplification или фонового I\u002FO.",[51,1091],{},[59,1093,1095],{"id":1094},"visibility-map-и-index-only-scan","Visibility map и index-only scan",[15,1097,1098],{},"PostgreSQL indexes обычно не знают, видима ли tuple текущей transaction. Поэтому обычный index scan идёт в index, потом в heap, чтобы проверить tuple visibility.",[15,1100,1101],{},"Visibility map отмечает heap pages, где все tuples видимы всем transactions. Если page all-visible, index-only scan может не читать heap page.",[38,1103,1106],{"className":1104,"code":1105,"language":43,"meta":44},[41],"Index Only Scan\n  ├─ index содержит нужные columns\n  └─ visibility map говорит: heap page all-visible\n",[21,1107,1105],{"__ignoreMap":44},[15,1109,522,1110,1112,1113,1115],{},[21,1111,1062],{}," у index-only scan смотрите ",[21,1114,35],{},". Если их много, запрос формально index-only, но всё равно часто ходит в heap. Причина может быть в активных updates, слабом vacuum или недавно изменённых страницах.",[51,1117],{},[59,1119,1121],{"id":1120},"hot-updates","HOT updates",[15,1123,1124,1125,1127],{},"HOT - Heap-Only Tuple optimization. Если ",[21,1126,23],{}," не меняет indexed columns и на той же heap page есть место для новой версии, PostgreSQL может не создавать новые index entries.",[38,1129,1131],{"className":391,"code":1130,"language":393,"meta":44,"style":44},"-- index on email\nUPDATE users SET last_seen_at = now() WHERE id = 42;\n-- если last_seen_at не indexed, есть шанс на HOT update\n",[21,1132,1133,1139,1169],{"__ignoreMap":44},[397,1134,1135],{"class":399,"line":400},[397,1136,1138],{"class":1137},"sAwPA","-- index on email\n",[397,1140,1141,1143,1146,1149,1152,1154,1157,1159,1161,1163,1165,1167],{"class":399,"line":410},[397,1142,23],{"class":403},[397,1144,1145],{"class":406}," users ",[397,1147,1148],{"class":403},"SET",[397,1150,1151],{"class":406}," last_seen_at ",[397,1153,428],{"class":403},[397,1155,1156],{"class":403}," now",[397,1158,812],{"class":406},[397,1160,422],{"class":403},[397,1162,425],{"class":406},[397,1164,428],{"class":403},[397,1166,432],{"class":431},[397,1168,435],{"class":406},[397,1170,1171],{"class":399,"line":419},[397,1172,1173],{"class":1137},"-- если last_seen_at не indexed, есть шанс на HOT update\n",[15,1175,1176],{},"Почему это важно:",[80,1178,1179,1182,1185,1188],{},[83,1180,1181],{},"меньше index bloat;",[83,1183,1184],{},"дешевле update;",[83,1186,1187],{},"меньше WAL;",[83,1189,1190],{},"быстрее vacuum.",[15,1192,1193],{},"Как помочь HOT:",[80,1195,1196,1199,1202,1209],{},[83,1197,1198],{},"не индексировать каждую колонку \"на всякий случай\";",[83,1200,1201],{},"держать часто меняющиеся поля вне лишних индексов;",[83,1203,1204,1205,1208],{},"иногда использовать ",[21,1206,1207],{},"fillfactor"," ниже 100, чтобы оставить место на странице;",[83,1210,1211],{},"не хранить большие mutable JSONB-документы в одной горячей строке без причины.",[38,1213,1215],{"className":391,"code":1214,"language":393,"meta":44,"style":44},"ALTER TABLE users SET (fillfactor = 90);\n",[21,1216,1217],{"__ignoreMap":44},[397,1218,1219,1222,1225,1227,1229,1232,1234,1237],{"class":399,"line":400},[397,1220,1221],{"class":403},"ALTER",[397,1223,1224],{"class":403}," TABLE",[397,1226,1145],{"class":406},[397,1228,1148],{"class":403},[397,1230,1231],{"class":406}," (fillfactor ",[397,1233,428],{"class":403},[397,1235,1236],{"class":431}," 90",[397,1238,1239],{"class":406},");\n",[15,1241,1242,1244],{},[21,1243,1207],{}," не чинит всё. Он обменивает плотность хранения на шанс обновляться внутри той же страницы.",[51,1246],{},[59,1248,1250],{"id":1249},"toast-большие-значения-отдельно","TOAST: большие значения отдельно",[15,1252,1253,1254,159,1256,159,1259,1262],{},"PostgreSQL page обычно 8 KB, но строка может содержать большой ",[21,1255,43],{},[21,1257,1258],{},"bytea",[21,1260,1261],{},"jsonb",". Для этого есть TOAST - The Oversized-Attribute Storage Technique.",[15,1264,1265],{},"Если значение слишком большое, PostgreSQL может:",[80,1267,1268,1271,1274],{},[83,1269,1270],{},"сжать его;",[83,1272,1273],{},"вынести кусками в отдельную TOAST-таблицу;",[83,1275,1276],{},"оставить в основной строке pointer.",[38,1278,1281],{"className":1279,"code":1280,"language":43,"meta":44},[41],"orders heap tuple\n  id\n  status\n  payload pointer -> pg_toast.pg_toast_...\n",[21,1282,1280],{"__ignoreMap":44},[15,1284,1285],{},"Практические последствия:",[80,1287,1288,1297,1300,1303],{},[83,1289,1290,1293,1294,1296],{},[21,1291,1292],{},"SELECT *"," по таблице с большим ",[21,1295,1261],{}," может случайно тащить огромные TOAST-значения;",[83,1298,1299],{},"обновление большого поля создаёт новые версии и может сильно раздувать TOAST;",[83,1301,1302],{},"индексировать весь JSONB через GIN удобно, но дорого по write amplification;",[83,1304,1305],{},"для API лучше выбирать только нужные columns.",[38,1307,1309],{"className":391,"code":1308,"language":393,"meta":44,"style":44},"-- лучше\nSELECT id, status, created_at FROM orders WHERE user_id = $1;\n\n-- осторожно\nSELECT * FROM orders WHERE user_id = $1;\n",[21,1310,1311,1316,1349,1355,1360],{"__ignoreMap":44},[397,1312,1313],{"class":399,"line":400},[397,1314,1315],{"class":1137},"-- лучше\n",[397,1317,1318,1320,1323,1326,1329,1331,1334,1336,1339,1341,1344,1347],{"class":399,"line":410},[397,1319,27],{"class":403},[397,1321,1322],{"class":406}," id, ",[397,1324,1325],{"class":403},"status",[397,1327,1328],{"class":406},", created_at ",[397,1330,413],{"class":403},[397,1332,1333],{"class":406}," orders ",[397,1335,422],{"class":403},[397,1337,1338],{"class":406}," user_id ",[397,1340,428],{"class":403},[397,1342,1343],{"class":406}," $",[397,1345,1346],{"class":431},"1",[397,1348,435],{"class":406},[397,1350,1351],{"class":399,"line":419},[397,1352,1354],{"emptyLinePlaceholder":1353},true,"\n",[397,1356,1357],{"class":399,"line":733},[397,1358,1359],{"class":1137},"-- осторожно\n",[397,1361,1362,1364,1367,1370,1372,1374,1376,1378,1380,1382],{"class":399,"line":739},[397,1363,27],{"class":403},[397,1365,1366],{"class":403}," *",[397,1368,1369],{"class":403}," FROM",[397,1371,1333],{"class":406},[397,1373,422],{"class":403},[397,1375,1338],{"class":406},[397,1377,428],{"class":403},[397,1379,1343],{"class":406},[397,1381,1346],{"class":431},[397,1383,435],{"class":406},[51,1385],{},[54,1387,1389],{"id":1388},"как-читать-explain-с-учётом-mvcc","Как читать EXPLAIN с учётом MVCC",[15,1391,1392,1394,1395,1398,1399,1402],{},[21,1393,31],{}," показывает план. ",[21,1396,1397],{},"EXPLAIN ANALYZE"," выполняет запрос и показывает фактические timings\u002Frows. ",[21,1400,1401],{},"BUFFERS"," добавляет чтение страниц.",[38,1404,1406],{"className":391,"code":1405,"language":393,"meta":44,"style":44},"EXPLAIN (ANALYZE, BUFFERS)\nSELECT id, email\nFROM users\nWHERE email = 'a@example.com';\n",[21,1407,1408,1413,1420,1426],{"__ignoreMap":44},[397,1409,1410],{"class":399,"line":400},[397,1411,1412],{"class":406},"EXPLAIN (ANALYZE, BUFFERS)\n",[397,1414,1415,1417],{"class":399,"line":410},[397,1416,27],{"class":403},[397,1418,1419],{"class":406}," id, email\n",[397,1421,1422,1424],{"class":399,"line":419},[397,1423,413],{"class":403},[397,1425,416],{"class":406},[397,1427,1428,1430,1433,1435,1439],{"class":399,"line":733},[397,1429,422],{"class":403},[397,1431,1432],{"class":406}," email ",[397,1434,428],{"class":403},[397,1436,1438],{"class":1437},"sU2Wk"," 'a@example.com'",[397,1440,435],{"class":406},[15,1442,1443],{},"Минимальный checklist:",[298,1445,1446,1456],{},[301,1447,1448],{},[304,1449,1450,1453],{},[307,1451,1452],{},"Поле",[307,1454,1455],{},"Что спросить",[314,1457,1458,1468,1478,1488,1498,1508,1517],{},[304,1459,1460,1465],{},[319,1461,1462],{},[21,1463,1464],{},"cost",[319,1466,1467],{},"Что планировщик ожидал?",[304,1469,1470,1475],{},[319,1471,1472],{},[21,1473,1474],{},"rows",[319,1476,1477],{},"Насколько estimate отличается от actual?",[304,1479,1480,1485],{},[319,1481,1482],{},[21,1483,1484],{},"actual time",[319,1486,1487],{},"Где реально ушло время?",[304,1489,1490,1495],{},[319,1491,1492],{},[21,1493,1494],{},"loops",[319,1496,1497],{},"Узел выполнялся один раз или много?",[304,1499,1500,1505],{},[319,1501,1502],{},[21,1503,1504],{},"Buffers: shared hit\u002Fread",[319,1506,1507],{},"Данные были в cache или читались с диска?",[304,1509,1510,1514],{},[319,1511,1512],{},[21,1513,35],{},[319,1515,1516],{},"Index-only scan действительно обошёл heap?",[304,1518,1519,1524],{},[319,1520,1521],{},[21,1522,1523],{},"Rows Removed by Filter",[319,1525,1526],{},"Не читаем ли слишком много лишнего?",[15,1528,1529],{},"Пример плохого сигнала:",[38,1531,1534],{"className":1532,"code":1533,"language":43,"meta":44},[41],"Index Only Scan using users_email_idx on users\n  Heap Fetches: 500000\n",[21,1535,1533],{"__ignoreMap":44},[15,1537,1538],{},"Индекс есть, но heap всё равно читается. Причина может быть в visibility map: страницы не all-visible из-за недавних writes или неуспевающего vacuum.",[15,1540,1541],{},"Другой пример:",[38,1543,1546],{"className":1544,"code":1545,"language":43,"meta":44},[41],"Seq Scan on events\n  Rows Removed by Filter: 9999000\n",[21,1547,1545],{"__ignoreMap":44},[15,1549,1550],{},"Это не всегда плохо: если фильтр выбирает половину таблицы, seq scan нормален. Но если фактически нужно 100 строк из 10 млн, нужно смотреть индекс, статистику, тип predicate и selectivity.",[51,1552],{},[54,1554,1556],{"id":1555},"типичные-production-ошибки","Типичные production-ошибки",[59,1558,1560],{"id":1559},"transaction-вокруг-внешнего-мира","Transaction вокруг внешнего мира",[38,1562,1564],{"className":550,"code":1563,"language":552,"meta":553,"style":44},"tx, _ := db.BeginTx(ctx, nil)\n\u002F\u002F SELECT ...\n\u002F\u002F HTTP request to another service\n\u002F\u002F UPDATE ...\ntx.Commit()\n",[21,1565,1566,1585,1590,1595,1600],{"__ignoreMap":44},[397,1567,1568,1571,1573,1575,1577,1579,1582],{"class":399,"line":400},[397,1569,1570],{"class":406},"tx, _ ",[397,1572,563],{"class":403},[397,1574,566],{"class":406},[397,1576,570],{"class":569},[397,1578,573],{"class":406},[397,1580,1581],{"class":431},"nil",[397,1583,1584],{"class":406},")\n",[397,1586,1587],{"class":399,"line":410},[397,1588,1589],{"class":1137},"\u002F\u002F SELECT ...\n",[397,1591,1592],{"class":399,"line":419},[397,1593,1594],{"class":1137},"\u002F\u002F HTTP request to another service\n",[397,1596,1597],{"class":399,"line":733},[397,1598,1599],{"class":1137},"\u002F\u002F UPDATE ...\n",[397,1601,1602,1605,1608],{"class":399,"line":739},[397,1603,1604],{"class":406},"tx.",[397,1606,1607],{"class":569},"Commit",[397,1609,1610],{"class":406},"()\n",[15,1612,1613],{},"Пока Go ждёт внешний сервис, PostgreSQL держит transaction, snapshot и, возможно, locks. Лучше сначала собрать внешние данные, затем открыть короткую transaction и быстро зафиксировать изменения.",[59,1615,1617],{"id":1616},"частый-update-счётчиков","Частый UPDATE счётчиков",[38,1619,1621],{"className":391,"code":1620,"language":393,"meta":44,"style":44},"UPDATE posts SET views = views + 1 WHERE id = $1;\n",[21,1622,1623],{"__ignoreMap":44},[397,1624,1625,1627,1630,1632,1635,1637,1639,1642,1645,1648,1650,1652,1654,1656],{"class":399,"line":400},[397,1626,23],{"class":403},[397,1628,1629],{"class":406}," posts ",[397,1631,1148],{"class":403},[397,1633,1634],{"class":406}," views ",[397,1636,428],{"class":403},[397,1638,1634],{"class":406},[397,1640,1641],{"class":403},"+",[397,1643,1644],{"class":431}," 1",[397,1646,1647],{"class":403}," WHERE",[397,1649,425],{"class":406},[397,1651,428],{"class":403},[397,1653,1343],{"class":406},[397,1655,1346],{"class":431},[397,1657,435],{"class":406},[15,1659,1660],{},"Для горячего поста это создаёт постоянные row versions и lock contention. Иногда лучше buffered aggregation, Redis counter с периодическим flush, append-only events или отдельная sharded counter table.",[59,1662,1664],{"id":1663},"большой-jsonb-как-изменяемая-сущность","Большой JSONB как изменяемая сущность",[38,1666,1668],{"className":391,"code":1667,"language":393,"meta":44,"style":44},"UPDATE documents\nSET payload = jsonb_set(payload, '{status}', '\"done\"')\nWHERE id = $1;\n",[21,1669,1670,1677,1699],{"__ignoreMap":44},[397,1671,1672,1674],{"class":399,"line":400},[397,1673,23],{"class":403},[397,1675,1676],{"class":406}," documents\n",[397,1678,1679,1681,1684,1686,1689,1692,1694,1697],{"class":399,"line":410},[397,1680,1148],{"class":403},[397,1682,1683],{"class":406}," payload ",[397,1685,428],{"class":403},[397,1687,1688],{"class":406}," jsonb_set(payload, ",[397,1690,1691],{"class":1437},"'{status}'",[397,1693,159],{"class":406},[397,1695,1696],{"class":1437},"'\"done\"'",[397,1698,1584],{"class":406},[397,1700,1701,1703,1705,1707,1709,1711],{"class":399,"line":419},[397,1702,422],{"class":403},[397,1704,425],{"class":406},[397,1706,428],{"class":403},[397,1708,1343],{"class":406},[397,1710,1346],{"class":431},[397,1712,435],{"class":406},[15,1714,1715],{},"Логически меняется одно поле, физически может переписываться большая строка\u002FTOAST-значение и GIN index. Для горячих полей часто лучше вынести columns отдельно.",[59,1717,1719],{"id":1718},"забытый-rollback","Забытый rollback",[38,1721,1723],{"className":550,"code":1722,"language":552,"meta":553,"style":44},"tx, err := db.BeginTx(ctx, nil)\nif err != nil { return err }\ndefer tx.Rollback()\n\n\u002F\u002F ...\nreturn tx.Commit()\n",[21,1724,1725,1741,1764,1777,1781,1786],{"__ignoreMap":44},[397,1726,1727,1729,1731,1733,1735,1737,1739],{"class":399,"line":400},[397,1728,560],{"class":406},[397,1730,563],{"class":403},[397,1732,566],{"class":406},[397,1734,570],{"class":569},[397,1736,573],{"class":406},[397,1738,1581],{"class":431},[397,1740,1584],{"class":406},[397,1742,1743,1746,1749,1752,1755,1758,1761],{"class":399,"line":410},[397,1744,1745],{"class":403},"if",[397,1747,1748],{"class":406}," err ",[397,1750,1751],{"class":403},"!=",[397,1753,1754],{"class":431}," nil",[397,1756,1757],{"class":406}," { ",[397,1759,1760],{"class":403},"return",[397,1762,1763],{"class":406}," err }\n",[397,1765,1766,1769,1772,1775],{"class":399,"line":419},[397,1767,1768],{"class":403},"defer",[397,1770,1771],{"class":406}," tx.",[397,1773,1774],{"class":569},"Rollback",[397,1776,1610],{"class":406},[397,1778,1779],{"class":399,"line":733},[397,1780,1354],{"emptyLinePlaceholder":1353},[397,1782,1783],{"class":399,"line":739},[397,1784,1785],{"class":1137},"\u002F\u002F ...\n",[397,1787,1788,1790,1792,1794],{"class":399,"line":745},[397,1789,1760],{"class":403},[397,1791,1771],{"class":406},[397,1793,1607],{"class":569},[397,1795,1610],{"class":406},[15,1797,1798,1801],{},[21,1799,1800],{},"defer tx.Rollback()"," после успешного commit вернёт ошибку, которую обычно игнорируют, зато в ошибочных ветках transaction будет закрыта.",[51,1803],{},[54,1805,1807],{"id":1806},"что-помнить-на-собеседовании","Что помнить на собеседовании",[80,1809,1810,1813,1818,1826,1829,1837,1840,1848,1851,1854,1857,1860],{},[83,1811,1812],{},"PostgreSQL readers и writers не блокируют друг друга в обычном чтении благодаря MVCC.",[83,1814,1815,1817],{},[21,1816,23],{}," создаёт новую версию строки, старая версия очищается позже vacuum'ом.",[83,1819,1820,1822,1823,1825],{},[21,1821,443],{}," и ",[21,1824,446],{}," - transaction ids создания и удаления\u002Fзамены tuple version.",[83,1827,1828],{},"Snapshot определяет, какие tuple versions видимы transaction.",[83,1830,1831,1833,1834,1836],{},[21,1832,525],{}," делает snapshot на statement, ",[21,1835,534],{}," - на transaction.",[83,1838,1839],{},"Длинные transactions мешают vacuum и могут вызвать bloat.",[83,1841,1842,1844,1845,1847],{},[21,1843,158],{}," обычно переиспользует место, но не возвращает его OS; ",[21,1846,694],{}," переписывает таблицу и требует тяжёлый lock.",[83,1849,1850],{},"WAL обеспечивает crash recovery, replication и PITR.",[83,1852,1853],{},"Checkpoint сбрасывает dirty pages и ограничивает объём WAL, который нужен для recovery.",[83,1855,1856],{},"HOT update возможен, когда не меняются indexed columns и есть место на heap page.",[83,1858,1859],{},"TOAST выносит большие значения из основной строки, но не делает их бесплатными.",[83,1861,1862,1864],{},[21,1863,1062],{}," надо читать вместе с actual rows, loops, buffers и visibility effects.",[51,1866],{},[54,1868,1870],{"id":1869},"практика","Практика",[1872,1873,1874,1883,1886,1895],"ol",{},[83,1875,1876,1877,1879,1880,388],{},"Выполните несколько ",[21,1878,23],{}," одной строки и посмотрите, как растут dead tuples в ",[21,1881,1882],{},"pg_stat_user_tables",[83,1884,1885],{},"Откройте длинную transaction и проверьте, как она мешает vacuum очищать старые версии.",[83,1887,1888,1889,1891,1892,388],{},"Сравните ",[21,1890,1062],{}," до и после ",[21,1893,1894],{},"VACUUM ANALYZE",[83,1896,1897],{},"Найдите таблицу, где частые updates indexed columns мешают HOT updates.",[51,1899],{},[54,1901,1903],{"id":1902},"интерактивная-практика","Интерактивная практика",[1905,1906,1909,1915,1934],"quiz",{"answer":1346,"id":1907,"xp":1908},"db-mvcc-q1","10",[15,1910,1911,1912,1914],{},"Что чаще всего происходит при ",[21,1913,23],{}," строки в PostgreSQL?",[1916,1917,1918],"template",{"v-slot:options":44},[80,1919,1920,1923,1926,1931],{},[83,1921,1922],{},"Создаётся новая версия строки, а старая позже очищается vacuum",[83,1924,1925],{},"Старая строка всегда переписывается на месте без следов",[83,1927,1928,1929],{},"Все readers блокируются до ",[21,1930,243],{},[83,1932,1933],{},"WAL не используется, если включён autovacuum",[1916,1935,1936],{"v-slot:explanation":44},[15,1937,1938],{},"MVCC хранит версии строк. Это помогает concurrency, но создаёт dead tuples и bloat, если vacuum не успевает.",[1940,1941,1945,1948,2047],"predict",{"answer":1942,"id":1943,"xp":1944},"statement\\ntransaction","db-mvcc-p1","15",[15,1946,1947],{},"Что вернёт запрос?",[1916,1949,1950],{"v-slot:code":44},[38,1951,1953],{"className":391,"code":1952,"language":393,"meta":44,"style":44},"WITH isolation(level_name) AS (\n    VALUES ('read committed'), ('repeatable read')\n)\nSELECT CASE\n    WHEN level_name = 'repeatable read' THEN 'transaction'\n    ELSE 'statement'\nEND AS snapshot_scope\nFROM isolation;\n",[21,1954,1955,1971,1990,1994,2001,2020,2028,2039],{"__ignoreMap":44},[397,1956,1957,1960,1963,1966,1968],{"class":399,"line":400},[397,1958,1959],{"class":403},"WITH",[397,1961,1962],{"class":403}," isolation",[397,1964,1965],{"class":406},"(level_name) ",[397,1967,821],{"class":403},[397,1969,1970],{"class":406}," (\n",[397,1972,1973,1976,1979,1982,1985,1988],{"class":399,"line":410},[397,1974,1975],{"class":403},"    VALUES",[397,1977,1978],{"class":406}," (",[397,1980,1981],{"class":1437},"'read committed'",[397,1983,1984],{"class":406},"), (",[397,1986,1987],{"class":1437},"'repeatable read'",[397,1989,1584],{"class":406},[397,1991,1992],{"class":399,"line":419},[397,1993,1584],{"class":406},[397,1995,1996,1998],{"class":399,"line":733},[397,1997,27],{"class":403},[397,1999,2000],{"class":403}," CASE\n",[397,2002,2003,2006,2009,2011,2014,2017],{"class":399,"line":739},[397,2004,2005],{"class":403},"    WHEN",[397,2007,2008],{"class":406}," level_name ",[397,2010,428],{"class":403},[397,2012,2013],{"class":1437}," 'repeatable read'",[397,2015,2016],{"class":403}," THEN",[397,2018,2019],{"class":1437}," 'transaction'\n",[397,2021,2022,2025],{"class":399,"line":745},[397,2023,2024],{"class":403},"    ELSE",[397,2026,2027],{"class":1437}," 'statement'\n",[397,2029,2030,2033,2036],{"class":399,"line":751},[397,2031,2032],{"class":403},"END",[397,2034,2035],{"class":403}," AS",[397,2037,2038],{"class":406}," snapshot_scope\n",[397,2040,2041,2043,2045],{"class":399,"line":757},[397,2042,413],{"class":403},[397,2044,1962],{"class":403},[397,2046,435],{"class":406},[1916,2048,2049],{"v-slot:hint":44},[15,2050,2051,2052,2054,2055,1836],{},"В PostgreSQL ",[21,2053,525],{}," берёт snapshot на statement, ",[21,2056,534],{},[51,2058],{},[54,2060,2062],{"id":2061},"полезные-официальные-разделы","Полезные официальные разделы",[80,2064,2065,2074,2081,2088,2095,2102,2109],{},[83,2066,2067],{},[2068,2069,2073],"a",{"href":2070,"rel":2071},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fmvcc.html",[2072],"nofollow","PostgreSQL Documentation: Concurrency Control",[83,2075,2076],{},[2068,2077,2080],{"href":2078,"rel":2079},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Froutine-vacuuming.html",[2072],"PostgreSQL Documentation: Routine Vacuuming",[83,2082,2083],{},[2068,2084,2087],{"href":2085,"rel":2086},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fwal-intro.html",[2072],"PostgreSQL Documentation: Write-Ahead Logging",[83,2089,2090],{},[2068,2091,2094],{"href":2092,"rel":2093},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fruntime-config-resource.html",[2072],"PostgreSQL Documentation: Resource Consumption",[83,2096,2097],{},[2068,2098,2101],{"href":2099,"rel":2100},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fstorage-hot.html",[2072],"PostgreSQL Documentation: Heap-Only Tuples",[83,2103,2104],{},[2068,2105,2108],{"href":2106,"rel":2107},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fstorage-toast.html",[2072],"PostgreSQL Documentation: TOAST",[83,2110,2111],{},[2068,2112,2115],{"href":2113,"rel":2114},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fusing-explain.html",[2072],"PostgreSQL Documentation: Using EXPLAIN",[2117,2118,2119],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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 pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":44,"searchDepth":410,"depth":410,"links":2121},[2122,2128,2131,2138,2143,2144,2150,2151,2152,2153],{"id":56,"depth":410,"text":57,"children":2123},[2124,2125,2126,2127],{"id":61,"depth":419,"text":62},{"id":117,"depth":419,"text":118},{"id":211,"depth":419,"text":212},{"id":283,"depth":419,"text":284},{"id":367,"depth":410,"text":368,"children":2129},[2130],{"id":371,"depth":419,"text":372},{"id":452,"depth":410,"text":453,"children":2132},[2133,2134,2135,2136,2137],{"id":456,"depth":419,"text":457},{"id":515,"depth":419,"text":516},{"id":608,"depth":419,"text":609},{"id":655,"depth":419,"text":656},{"id":870,"depth":419,"text":871},{"id":899,"depth":410,"text":900,"children":2139},[2140,2141,2142],{"id":1094,"depth":419,"text":1095},{"id":1120,"depth":419,"text":1121},{"id":1249,"depth":419,"text":1250},{"id":1388,"depth":410,"text":1389},{"id":1555,"depth":410,"text":1556,"children":2145},[2146,2147,2148,2149],{"id":1559,"depth":419,"text":1560},{"id":1616,"depth":419,"text":1617},{"id":1663,"depth":419,"text":1664},{"id":1718,"depth":419,"text":1719},{"id":1806,"depth":410,"text":1807},{"id":1869,"depth":410,"text":1870},{"id":1902,"depth":410,"text":1903},{"id":2061,"depth":410,"text":2062},1781022065469]