[{"data":1,"prerenderedAt":3425},["ShallowReactive",2],{"content:\u002F10-databases\u002F05-postgresql-indexes-query-planner":3},{"title":4,"description":5,"path":6,"body":7},"PostgreSQL: индексы и планировщик запросов","Индекс - не кнопка \"сделать быстро\". Это отдельная структура данных, которую PostgreSQL должен поддерживать при INSERT, UPDATE, DELETE, читать при запросах, vacuum'ить и хранить на диске. Хороший индекс ускоряет частый и селективный access pattern. Плохой индекс замедляет writes, раздувает базу и всё равно не используется.","\u002F10-databases\u002F05-postgresql-indexes-query-planner",{"type":8,"value":9,"toc":3380},"minimark",[10,14,30,33,43,46,49,54,57,63,66,82,85,94,186,194,196,200,205,208,211,260,320,323,325,329,332,362,365,379,382,384,388,391,394,411,528,531,533,537,540,543,557,560,562,566,569,571,592,646,649,709,712,714,718,721,724,741,766,769,771,775,779,782,813,816,892,895,935,938,981,993,1022,1025,1027,1031,1034,1068,1071,1085,1088,1128,1131,1171,1174,1176,1180,1183,1245,1256,1259,1270,1272,1276,1281,1368,1371,1374,1388,1390,1394,1397,1404,1431,1438,1445,1454,1457,1477,1480,1506,1509,1549,1552,1589,1592,1594,1598,1601,1604,1621,1624,1641,1644,1650,1653,1656,1689,1692,1694,1698,1701,1705,1711,1714,1717,1723,1726,1730,1733,1739,1743,1746,1749,1763,1765,1769,1772,1794,1803,1846,1849,1937,1944,1946,1950,1957,1993,1996,2000,2003,2031,2034,2087,2090,2094,2097,2116,2123,2193,2196,2200,2240,2243,2287,2291,2294,2314,2317,2376,2382,2384,2388,2395,2426,2432,2435,2463,2469,2492,2495,2529,2532,2554,2558,2561,2564,2603,2614,2618,2621,2627,2633,2636,2658,2665,2667,2671,2674,2786,2789,2920,2923,2997,3003,3006,3079,3088,3090,3094,3146,3148,3152,3178,3180,3184,3217,3324,3326,3330,3376],[11,12,4],"h1",{"id":13},"postgresql-индексы-и-планировщик-запросов",[15,16,17,18,22,23,22,26,29],"p",{},"Индекс - не кнопка \"сделать быстро\". Это отдельная структура данных, которую PostgreSQL должен поддерживать при ",[19,20,21],"code",{},"INSERT",", ",[19,24,25],{},"UPDATE",[19,27,28],{},"DELETE",", читать при запросах, vacuum'ить и хранить на диске. Хороший индекс ускоряет частый и селективный access pattern. Плохой индекс замедляет writes, раздувает базу и всё равно не используется.",[15,31,32],{},"Планировщик PostgreSQL выбирает план по статистике и cost model. Он не \"знает\", что запрос важный; он оценивает cardinality, selectivity, стоимость чтения страниц, сортировки, joins и параллелизма.",[34,35,41],"pre",{"className":36,"code":38,"language":39,"meta":40},[37],"language-text","SQL\n  -> parser\n  -> rewriter\n  -> planner \u002F optimizer\n  -> executor\n  -> rows\n","text","",[19,42,38],{"__ignoreMap":40},[15,44,45],{},"Задача backend-разработчика - писать запросы и схемы так, чтобы планировщик мог выбрать хороший план.",[47,48],"hr",{},[50,51,53],"h2",{"id":52},"что-хранит-индекс","Что хранит индекс",[15,55,56],{},"Упрощённо индекс хранит ключ и pointer на heap tuple.",[34,58,61],{"className":59,"code":60,"language":39,"meta":40},[37],"users_email_idx\n  \"a@example.com\" -> (block=10, offset=4)\n  \"b@example.com\" -> (block=11, offset=2)\n",[19,62,60],{"__ignoreMap":40},[15,64,65],{},"Обычный index scan:",[67,68,69,73,76,79],"ol",{},[70,71,72],"li",{},"найти ключи в индексе;",[70,74,75],{},"сходить в heap за строками;",[70,77,78],{},"проверить visibility по MVCC;",[70,80,81],{},"вернуть подходящие rows.",[15,83,84],{},"Index-only scan возможен, если:",[86,87,88,91],"ul",{},[70,89,90],{},"все нужные columns есть в индексе;",[70,92,93],{},"heap page помечена all-visible в visibility map.",[34,95,99],{"className":96,"code":97,"language":98,"meta":40,"style":40},"language-sql shiki shiki-themes github-dark","CREATE INDEX users_email_include_name_idx\nON users (email) INCLUDE (name);\n\nEXPLAIN (ANALYZE, BUFFERS)\nSELECT name FROM users WHERE email = $1;\n","sql",[19,100,101,117,139,146,152],{"__ignoreMap":40},[102,103,106,110,113],"span",{"class":104,"line":105},"line",1,[102,107,109],{"class":108},"snl16","CREATE",[102,111,112],{"class":108}," INDEX",[102,114,116],{"class":115},"svObZ"," users_email_include_name_idx\n",[102,118,120,123,127,130,133,136],{"class":104,"line":119},2,[102,121,122],{"class":108},"ON",[102,124,126],{"class":125},"s95oV"," users (email) ",[102,128,129],{"class":108},"INCLUDE",[102,131,132],{"class":125}," (",[102,134,135],{"class":108},"name",[102,137,138],{"class":125},");\n",[102,140,142],{"class":104,"line":141},3,[102,143,145],{"emptyLinePlaceholder":144},true,"\n",[102,147,149],{"class":104,"line":148},4,[102,150,151],{"class":125},"EXPLAIN (ANALYZE, BUFFERS)\n",[102,153,155,158,161,164,167,170,173,176,179,183],{"class":104,"line":154},5,[102,156,157],{"class":108},"SELECT",[102,159,160],{"class":108}," name",[102,162,163],{"class":108}," FROM",[102,165,166],{"class":125}," users ",[102,168,169],{"class":108},"WHERE",[102,171,172],{"class":125}," email ",[102,174,175],{"class":108},"=",[102,177,178],{"class":125}," $",[102,180,182],{"class":181},"sDLfK","1",[102,184,185],{"class":125},";\n",[15,187,188,190,191,193],{},[19,189,129],{}," columns не участвуют в поиске и сортировке как key columns, но позволяют покрыть ",[19,192,157],{},".",[47,195],{},[50,197,199],{"id":198},"типы-индексов","Типы индексов",[201,202,204],"h3",{"id":203},"b-tree","B-tree",[15,206,207],{},"B-tree - индекс по умолчанию и главный рабочий инструмент PostgreSQL.",[15,209,210],{},"Подходит для:",[86,212,213,218,236,241,250,257],{},[70,214,215,217],{},[19,216,175],{},";",[70,219,220,221,22,224,22,227,22,230,22,233,217],{},"range predicates: ",[19,222,223],{},"\u003C",[19,225,226],{},"\u003C=",[19,228,229],{},">",[19,231,232],{},">=",[19,234,235],{},"BETWEEN",[70,237,238,217],{},[19,239,240],{},"ORDER BY",[70,242,243,246,247,217],{},[19,244,245],{},"MIN","\u002F",[19,248,249],{},"MAX",[70,251,252,253,256],{},"prefix search через ",[19,254,255],{},"LIKE 'abc%'"," при подходящей operator class\u002Fcollation;",[70,258,259],{},"uniqueness и primary keys.",[34,261,263],{"className":96,"code":262,"language":98,"meta":40,"style":40},"CREATE INDEX users_email_idx ON users (email);\nCREATE UNIQUE INDEX users_lower_email_uidx ON users (lower(email));\nCREATE INDEX orders_created_at_idx ON orders (created_at DESC);\n",[19,264,265,280,301],{"__ignoreMap":40},[102,266,267,269,271,274,277],{"class":104,"line":105},[102,268,109],{"class":108},[102,270,112],{"class":108},[102,272,273],{"class":115}," users_email_idx",[102,275,276],{"class":108}," ON",[102,278,279],{"class":125}," users (email);\n",[102,281,282,284,287,290,292,295,298],{"class":104,"line":119},[102,283,109],{"class":108},[102,285,286],{"class":108}," UNIQUE INDEX",[102,288,289],{"class":115}," users_lower_email_uidx",[102,291,276],{"class":108},[102,293,294],{"class":125}," users (",[102,296,297],{"class":181},"lower",[102,299,300],{"class":125},"(email));\n",[102,302,303,305,307,310,312,315,318],{"class":104,"line":141},[102,304,109],{"class":108},[102,306,112],{"class":108},[102,308,309],{"class":115}," orders_created_at_idx",[102,311,276],{"class":108},[102,313,314],{"class":125}," orders (created_at ",[102,316,317],{"class":108},"DESC",[102,319,138],{"class":125},[15,321,322],{},"B-tree хорошо работает, когда условие селективное: возвращает малую долю таблицы. Если запрос выбирает 40% строк, seq scan может быть дешевле, потому что random heap reads дороже последовательного чтения.",[47,324],{},[201,326,328],{"id":327},"hash","Hash",[15,330,331],{},"Hash index поддерживает equality lookup.",[34,333,335],{"className":96,"code":334,"language":98,"meta":40,"style":40},"CREATE INDEX sessions_token_hash_idx\nON sessions USING hash (token);\n",[19,336,337,346],{"__ignoreMap":40},[102,338,339,341,343],{"class":104,"line":105},[102,340,109],{"class":108},[102,342,112],{"class":108},[102,344,345],{"class":115}," sessions_token_hash_idx\n",[102,347,348,350,353,356,359],{"class":104,"line":119},[102,349,122],{"class":108},[102,351,352],{"class":108}," sessions",[102,354,355],{"class":108}," USING",[102,357,358],{"class":108}," hash",[102,360,361],{"class":125}," (token);\n",[15,363,364],{},"В современных версиях PostgreSQL hash indexes WAL-logged и пригодны к использованию, но B-tree всё равно часто выбирают по умолчанию:",[86,366,367,370,373,376],{},[70,368,369],{},"B-tree тоже хорошо работает для equality;",[70,371,372],{},"B-tree поддерживает range и ordering;",[70,374,375],{},"unique constraint использует B-tree;",[70,377,378],{},"большинство команд лучше знакомы с B-tree behavior.",[15,380,381],{},"Hash index имеет смысл рассматривать для специфичных equality-only workloads после измерений.",[47,383],{},[201,385,387],{"id":386},"gist","GiST",[15,389,390],{},"GiST - Generalized Search Tree. Это framework для индексов над данными, где \"близость\", пересечение и containment важнее обычного порядка.",[15,392,393],{},"Типичные сценарии:",[86,395,396,399,402,405,408],{},[70,397,398],{},"геометрия;",[70,400,401],{},"ranges;",[70,403,404],{},"full-text search в некоторых вариантах;",[70,406,407],{},"exclusion constraints;",[70,409,410],{},"PostGIS.",[34,412,414],{"className":96,"code":413,"language":98,"meta":40,"style":40},"CREATE EXTENSION IF NOT EXISTS btree_gist;\n\nCREATE TABLE room_bookings (\n  room_id bigint NOT NULL,\n  period tstzrange NOT NULL,\n  EXCLUDE USING gist (\n    room_id WITH =,\n    period WITH &&\n  )\n);\n",[19,415,416,435,439,452,466,479,491,505,517,523],{"__ignoreMap":40},[102,417,418,420,423,426,429,432],{"class":104,"line":105},[102,419,109],{"class":108},[102,421,422],{"class":125}," EXTENSION ",[102,424,425],{"class":108},"IF",[102,427,428],{"class":108}," NOT",[102,430,431],{"class":108}," EXISTS",[102,433,434],{"class":125}," btree_gist;\n",[102,436,437],{"class":104,"line":119},[102,438,145],{"emptyLinePlaceholder":144},[102,440,441,443,446,449],{"class":104,"line":141},[102,442,109],{"class":108},[102,444,445],{"class":108}," TABLE",[102,447,448],{"class":115}," room_bookings",[102,450,451],{"class":125}," (\n",[102,453,454,457,460,463],{"class":104,"line":148},[102,455,456],{"class":125},"  room_id ",[102,458,459],{"class":108},"bigint",[102,461,462],{"class":108}," NOT NULL",[102,464,465],{"class":125},",\n",[102,467,468,471,474,477],{"class":104,"line":154},[102,469,470],{"class":108},"  period",[102,472,473],{"class":125}," tstzrange ",[102,475,476],{"class":108},"NOT NULL",[102,478,465],{"class":125},[102,480,482,485,488],{"class":104,"line":481},6,[102,483,484],{"class":125},"  EXCLUDE ",[102,486,487],{"class":108},"USING",[102,489,490],{"class":125}," gist (\n",[102,492,494,497,500,503],{"class":104,"line":493},7,[102,495,496],{"class":125},"    room_id ",[102,498,499],{"class":108},"WITH",[102,501,502],{"class":108}," =",[102,504,465],{"class":125},[102,506,508,511,514],{"class":104,"line":507},8,[102,509,510],{"class":108},"    period",[102,512,513],{"class":108}," WITH",[102,515,516],{"class":125}," &&\n",[102,518,520],{"class":104,"line":519},9,[102,521,522],{"class":125},"  )\n",[102,524,526],{"class":104,"line":525},10,[102,527,138],{"class":125},[15,529,530],{},"Такой constraint запрещает пересекающиеся бронирования одной комнаты. Это пример, где база выражает инвариант лучше, чем application-level check.",[47,532],{},[201,534,536],{"id":535},"sp-gist","SP-GiST",[15,538,539],{},"SP-GiST - Space-Partitioned GiST. Он полезен для структур, которые естественно разбиваются на непересекающиеся области: tries, quadtrees, kd-trees.",[15,541,542],{},"Примеры:",[86,544,545,548,551,554],{},[70,546,547],{},"prefix-like структуры;",[70,549,550],{},"точки;",[70,552,553],{},"некоторые network address\u002Fsearch задачи;",[70,555,556],{},"геометрические типы.",[15,558,559],{},"SP-GiST встречается реже, чем B-tree\u002FGIN\u002FGiST, но на собеседовании полезно сказать: это не \"ещё один B-tree\", а индекс для partitioned search spaces.",[47,561],{},[201,563,565],{"id":564},"gin","GIN",[15,567,568],{},"GIN - Generalized Inverted Index. Он индексирует составные значения: \"элемент -> строки, где элемент встречается\".",[15,570,210],{},[86,572,573,578,581,584,589],{},[70,574,575,217],{},[19,576,577],{},"jsonb",[70,579,580],{},"arrays;",[70,582,583],{},"full-text search;",[70,585,586,217],{},[19,587,588],{},"tsvector",[70,590,591],{},"containment queries.",[34,593,595],{"className":96,"code":594,"language":98,"meta":40,"style":40},"CREATE INDEX posts_tags_gin_idx\nON posts USING gin (tags);\n\nSELECT * FROM posts WHERE tags @> ARRAY['go', 'postgres'];\n",[19,596,597,606,618,622],{"__ignoreMap":40},[102,598,599,601,603],{"class":104,"line":105},[102,600,109],{"class":108},[102,602,112],{"class":108},[102,604,605],{"class":115}," posts_tags_gin_idx\n",[102,607,608,610,613,615],{"class":104,"line":119},[102,609,122],{"class":108},[102,611,612],{"class":125}," posts ",[102,614,487],{"class":108},[102,616,617],{"class":125}," gin (tags);\n",[102,619,620],{"class":104,"line":141},[102,621,145],{"emptyLinePlaceholder":144},[102,623,624,626,629,631,633,635,638,640,643],{"class":104,"line":148},[102,625,157],{"class":108},[102,627,628],{"class":108}," *",[102,630,163],{"class":108},[102,632,612],{"class":125},[102,634,169],{"class":108},[102,636,637],{"class":125}," tags @",[102,639,229],{"class":108},[102,641,642],{"class":108}," ARRAY",[102,644,645],{"class":125},"['go', 'postgres'];\n",[15,647,648],{},"Для JSONB:",[34,650,652],{"className":96,"code":651,"language":98,"meta":40,"style":40},"CREATE INDEX events_payload_gin_idx\nON events USING gin (payload jsonb_path_ops);\n\nSELECT *\nFROM events\nWHERE payload @> '{\"type\": \"payment_succeeded\"}';\n",[19,653,654,663,675,679,686,694],{"__ignoreMap":40},[102,655,656,658,660],{"class":104,"line":105},[102,657,109],{"class":108},[102,659,112],{"class":108},[102,661,662],{"class":115}," events_payload_gin_idx\n",[102,664,665,667,670,672],{"class":104,"line":119},[102,666,122],{"class":108},[102,668,669],{"class":125}," events ",[102,671,487],{"class":108},[102,673,674],{"class":125}," gin (payload jsonb_path_ops);\n",[102,676,677],{"class":104,"line":141},[102,678,145],{"emptyLinePlaceholder":144},[102,680,681,683],{"class":104,"line":148},[102,682,157],{"class":108},[102,684,685],{"class":108}," *\n",[102,687,688,691],{"class":104,"line":154},[102,689,690],{"class":108},"FROM",[102,692,693],{"class":125}," events\n",[102,695,696,698,701,703,707],{"class":104,"line":481},[102,697,169],{"class":108},[102,699,700],{"class":125}," payload @",[102,702,229],{"class":108},[102,704,706],{"class":705},"sU2Wk"," '{\"type\": \"payment_succeeded\"}'",[102,708,185],{"class":125},[15,710,711],{},"GIN может быть очень полезен, но у него высокая цена записи. Если вы индексируете большой mutable JSONB и часто его обновляете, получите write amplification, WAL и bloat.",[47,713],{},[201,715,717],{"id":716},"brin","BRIN",[15,719,720],{},"BRIN - Block Range Index. Он хранит summary по диапазонам heap pages, а не pointer на каждую строку.",[15,722,723],{},"Подходит для огромных таблиц, где физический порядок коррелирует с колонкой:",[86,725,726,732,735,738],{},[70,727,728,729,217],{},"append-only events by ",[19,730,731],{},"created_at",[70,733,734],{},"logs;",[70,736,737],{},"time-series;",[70,739,740],{},"monotonic ids.",[34,742,744],{"className":96,"code":743,"language":98,"meta":40,"style":40},"CREATE INDEX events_created_at_brin_idx\nON events USING brin (created_at);\n",[19,745,746,755],{"__ignoreMap":40},[102,747,748,750,752],{"class":104,"line":105},[102,749,109],{"class":108},[102,751,112],{"class":108},[102,753,754],{"class":115}," events_created_at_brin_idx\n",[102,756,757,759,761,763],{"class":104,"line":119},[102,758,122],{"class":108},[102,760,669],{"class":125},[102,762,487],{"class":108},[102,764,765],{"class":125}," brin (created_at);\n",[15,767,768],{},"BRIN маленький и дешёвый, но неточный: он быстро отбрасывает ranges, где данных точно нет, а затем executor проверяет строки. Если данные перемешаны случайно, BRIN почти бесполезен.",[47,770],{},[50,772,774],{"id":773},"проектирование-индекса","Проектирование индекса",[201,776,778],{"id":777},"composite-index-order","Composite index order",[15,780,781],{},"Порядок колонок в составном B-tree индексе критичен.",[34,783,785],{"className":96,"code":784,"language":98,"meta":40,"style":40},"CREATE INDEX orders_user_status_created_idx\nON orders (user_id, status, created_at DESC);\n",[19,786,787,796],{"__ignoreMap":40},[102,788,789,791,793],{"class":104,"line":105},[102,790,109],{"class":108},[102,792,112],{"class":108},[102,794,795],{"class":115}," orders_user_status_created_idx\n",[102,797,798,800,803,806,809,811],{"class":104,"line":119},[102,799,122],{"class":108},[102,801,802],{"class":125}," orders (user_id, ",[102,804,805],{"class":108},"status",[102,807,808],{"class":125},", created_at ",[102,810,317],{"class":108},[102,812,138],{"class":125},[15,814,815],{},"Такой индекс хорошо подходит для:",[34,817,819],{"className":96,"code":818,"language":98,"meta":40,"style":40},"WHERE user_id = $1\nWHERE user_id = $1 AND status = $2\nWHERE user_id = $1 AND status = $2 ORDER BY created_at DESC\n",[19,820,821,835,860],{"__ignoreMap":40},[102,822,823,825,828,830,832],{"class":104,"line":105},[102,824,169],{"class":108},[102,826,827],{"class":125}," user_id ",[102,829,175],{"class":108},[102,831,178],{"class":125},[102,833,834],{"class":181},"1\n",[102,836,837,839,841,843,845,847,850,853,855,857],{"class":104,"line":119},[102,838,169],{"class":108},[102,840,827],{"class":125},[102,842,175],{"class":108},[102,844,178],{"class":125},[102,846,182],{"class":181},[102,848,849],{"class":108}," AND",[102,851,852],{"class":108}," status",[102,854,502],{"class":108},[102,856,178],{"class":125},[102,858,859],{"class":181},"2\n",[102,861,862,864,866,868,870,872,874,876,878,880,883,886,889],{"class":104,"line":141},[102,863,169],{"class":108},[102,865,827],{"class":125},[102,867,175],{"class":108},[102,869,178],{"class":125},[102,871,182],{"class":181},[102,873,849],{"class":108},[102,875,852],{"class":108},[102,877,502],{"class":108},[102,879,178],{"class":125},[102,881,882],{"class":181},"2",[102,884,885],{"class":108}," ORDER BY",[102,887,888],{"class":125}," created_at ",[102,890,891],{"class":108},"DESC\n",[15,893,894],{},"Но хуже подходит для:",[34,896,898],{"className":96,"code":897,"language":98,"meta":40,"style":40},"WHERE status = $1\nWHERE created_at > now() - interval '1 day'\n",[19,899,900,912],{"__ignoreMap":40},[102,901,902,904,906,908,910],{"class":104,"line":105},[102,903,169],{"class":108},[102,905,852],{"class":108},[102,907,502],{"class":108},[102,909,178],{"class":125},[102,911,834],{"class":181},[102,913,914,916,918,920,923,926,929,932],{"class":104,"line":119},[102,915,169],{"class":108},[102,917,888],{"class":125},[102,919,229],{"class":108},[102,921,922],{"class":108}," now",[102,924,925],{"class":125},"() ",[102,927,928],{"class":108},"-",[102,930,931],{"class":125}," interval ",[102,933,934],{"class":705},"'1 day'\n",[15,936,937],{},"Правило левого префикса: B-tree эффективно использует начальные columns, особенно с equality predicates. Range predicate обычно \"останавливает\" дальнейшее сужение по порядку.",[34,939,941],{"className":96,"code":940,"language":98,"meta":40,"style":40},"-- index (tenant_id, created_at, status)\nWHERE tenant_id = $1 AND created_at >= $2 AND status = 'paid'\n",[19,942,943,949],{"__ignoreMap":40},[102,944,945],{"class":104,"line":105},[102,946,948],{"class":947},"sAwPA","-- index (tenant_id, created_at, status)\n",[102,950,951,953,956,958,960,962,964,966,968,970,972,974,976,978],{"class":104,"line":119},[102,952,169],{"class":108},[102,954,955],{"class":125}," tenant_id ",[102,957,175],{"class":108},[102,959,178],{"class":125},[102,961,182],{"class":181},[102,963,849],{"class":108},[102,965,888],{"class":125},[102,967,232],{"class":108},[102,969,178],{"class":125},[102,971,882],{"class":181},[102,973,849],{"class":108},[102,975,852],{"class":108},[102,977,502],{"class":108},[102,979,980],{"class":705}," 'paid'\n",[15,982,983,986,987,989,990,992],{},[19,984,985],{},"tenant_id"," и ",[19,988,731],{}," помогут найти range. ",[19,991,805],{}," после range может проверяться как filter, но не всегда эффективно сужает index scan. Если частый запрос именно такой, возможно лучше:",[34,994,996],{"className":96,"code":995,"language":98,"meta":40,"style":40},"CREATE INDEX orders_tenant_status_created_idx\nON orders (tenant_id, status, created_at DESC);\n",[19,997,998,1007],{"__ignoreMap":40},[102,999,1000,1002,1004],{"class":104,"line":105},[102,1001,109],{"class":108},[102,1003,112],{"class":108},[102,1005,1006],{"class":115}," orders_tenant_status_created_idx\n",[102,1008,1009,1011,1014,1016,1018,1020],{"class":104,"line":119},[102,1010,122],{"class":108},[102,1012,1013],{"class":125}," orders (tenant_id, ",[102,1015,805],{"class":108},[102,1017,808],{"class":125},[102,1019,317],{"class":108},[102,1021,138],{"class":125},[15,1023,1024],{},"В PostgreSQL 18 появился более широкий support skip scan для multicolumn B-tree, но это не отменяет проектирование индексов под реальные predicates.",[47,1026],{},[201,1028,1030],{"id":1029},"partial-indexes","Partial indexes",[15,1032,1033],{},"Partial index индексирует только часть строк.",[34,1035,1037],{"className":96,"code":1036,"language":98,"meta":40,"style":40},"CREATE INDEX orders_unpaid_due_idx\nON orders (due_at)\nWHERE status = 'unpaid';\n",[19,1038,1039,1048,1055],{"__ignoreMap":40},[102,1040,1041,1043,1045],{"class":104,"line":105},[102,1042,109],{"class":108},[102,1044,112],{"class":108},[102,1046,1047],{"class":115}," orders_unpaid_due_idx\n",[102,1049,1050,1052],{"class":104,"line":119},[102,1051,122],{"class":108},[102,1053,1054],{"class":125}," orders (due_at)\n",[102,1056,1057,1059,1061,1063,1066],{"class":104,"line":141},[102,1058,169],{"class":108},[102,1060,852],{"class":108},[102,1062,502],{"class":108},[102,1064,1065],{"class":705}," 'unpaid'",[102,1067,185],{"class":125},[15,1069,1070],{},"Хорошо, если:",[86,1072,1073,1076,1079,1082],{},[70,1074,1075],{},"запросы часто фильтруют по условию;",[70,1077,1078],{},"subset маленький;",[70,1080,1081],{},"условие стабильно;",[70,1083,1084],{},"planner может доказать, что query predicate соответствует index predicate.",[15,1086,1087],{},"Пример:",[34,1089,1091],{"className":96,"code":1090,"language":98,"meta":40,"style":40},"SELECT *\nFROM orders\nWHERE status = 'unpaid' AND due_at \u003C now();\n",[19,1092,1093,1099,1106],{"__ignoreMap":40},[102,1094,1095,1097],{"class":104,"line":105},[102,1096,157],{"class":108},[102,1098,685],{"class":108},[102,1100,1101,1103],{"class":104,"line":119},[102,1102,690],{"class":108},[102,1104,1105],{"class":125}," orders\n",[102,1107,1108,1110,1112,1114,1116,1118,1121,1123,1125],{"class":104,"line":141},[102,1109,169],{"class":108},[102,1111,852],{"class":108},[102,1113,502],{"class":108},[102,1115,1065],{"class":705},[102,1117,849],{"class":108},[102,1119,1120],{"class":125}," due_at ",[102,1122,223],{"class":108},[102,1124,922],{"class":108},[102,1126,1127],{"class":125},"();\n",[15,1129,1130],{},"Partial unique index - частый production-паттерн:",[34,1132,1134],{"className":96,"code":1133,"language":98,"meta":40,"style":40},"CREATE UNIQUE INDEX users_active_email_uidx\nON users (lower(email))\nWHERE deleted_at IS NULL;\n",[19,1135,1136,1145,1156],{"__ignoreMap":40},[102,1137,1138,1140,1142],{"class":104,"line":105},[102,1139,109],{"class":108},[102,1141,286],{"class":108},[102,1143,1144],{"class":115}," users_active_email_uidx\n",[102,1146,1147,1149,1151,1153],{"class":104,"line":119},[102,1148,122],{"class":108},[102,1150,294],{"class":125},[102,1152,297],{"class":181},[102,1154,1155],{"class":125},"(email))\n",[102,1157,1158,1160,1163,1166,1169],{"class":104,"line":141},[102,1159,169],{"class":108},[102,1161,1162],{"class":125}," deleted_at ",[102,1164,1165],{"class":108},"IS",[102,1167,1168],{"class":108}," NULL",[102,1170,185],{"class":125},[15,1172,1173],{},"Так можно разрешить повторное использование email у soft-deleted пользователей и сохранить уникальность среди активных.",[47,1175],{},[201,1177,1179],{"id":1178},"expression-indexes","Expression indexes",[15,1181,1182],{},"Expression index хранит результат выражения.",[34,1184,1186],{"className":96,"code":1185,"language":98,"meta":40,"style":40},"CREATE INDEX users_lower_email_idx\nON users (lower(email));\n\nSELECT *\nFROM users\nWHERE lower(email) = lower($1);\n",[19,1187,1188,1197,1207,1211,1217,1224],{"__ignoreMap":40},[102,1189,1190,1192,1194],{"class":104,"line":105},[102,1191,109],{"class":108},[102,1193,112],{"class":108},[102,1195,1196],{"class":115}," users_lower_email_idx\n",[102,1198,1199,1201,1203,1205],{"class":104,"line":119},[102,1200,122],{"class":108},[102,1202,294],{"class":125},[102,1204,297],{"class":181},[102,1206,300],{"class":125},[102,1208,1209],{"class":104,"line":141},[102,1210,145],{"emptyLinePlaceholder":144},[102,1212,1213,1215],{"class":104,"line":148},[102,1214,157],{"class":108},[102,1216,685],{"class":108},[102,1218,1219,1221],{"class":104,"line":154},[102,1220,690],{"class":108},[102,1222,1223],{"class":125}," users\n",[102,1225,1226,1228,1231,1234,1236,1238,1241,1243],{"class":104,"line":481},[102,1227,169],{"class":108},[102,1229,1230],{"class":181}," lower",[102,1232,1233],{"class":125},"(email) ",[102,1235,175],{"class":108},[102,1237,1230],{"class":181},[102,1239,1240],{"class":125},"($",[102,1242,182],{"class":181},[102,1244,138],{"class":125},[15,1246,1247,1248,1251,1252,1255],{},"Без такого индекса ",[19,1249,1250],{},"lower(email)"," мешает использовать обычный index on ",[19,1253,1254],{},"email",", потому что индекс хранит исходное значение, а запрос ищет результат функции.",[15,1257,1258],{},"Важно:",[86,1260,1261,1264,1267],{},[70,1262,1263],{},"expression должна совпадать с запросом по смыслу и форме;",[70,1265,1266],{},"функции должны быть immutable или достаточно стабильны для index semantics;",[70,1268,1269],{},"expression index замедляет writes, потому что выражение надо пересчитывать.",[47,1271],{},[201,1273,1275],{"id":1274},"covering-indexes-и-include","Covering indexes и INCLUDE",[15,1277,1278,1279,193],{},"Covering index содержит всё, что нужно запросу. В PostgreSQL это часто делают через ",[19,1280,129],{},[34,1282,1284],{"className":96,"code":1283,"language":98,"meta":40,"style":40},"CREATE INDEX orders_user_created_cover_idx\nON orders (user_id, created_at DESC)\nINCLUDE (id, total_amount, status);\n\nSELECT id, total_amount, status\nFROM orders\nWHERE user_id = $1\nORDER BY created_at DESC\nLIMIT 20;\n",[19,1285,1286,1295,1307,1318,1322,1332,1338,1350,1358],{"__ignoreMap":40},[102,1287,1288,1290,1292],{"class":104,"line":105},[102,1289,109],{"class":108},[102,1291,112],{"class":108},[102,1293,1294],{"class":115}," orders_user_created_cover_idx\n",[102,1296,1297,1299,1302,1304],{"class":104,"line":119},[102,1298,122],{"class":108},[102,1300,1301],{"class":125}," orders (user_id, created_at ",[102,1303,317],{"class":108},[102,1305,1306],{"class":125},")\n",[102,1308,1309,1311,1314,1316],{"class":104,"line":141},[102,1310,129],{"class":108},[102,1312,1313],{"class":125}," (id, total_amount, ",[102,1315,805],{"class":108},[102,1317,138],{"class":125},[102,1319,1320],{"class":104,"line":148},[102,1321,145],{"emptyLinePlaceholder":144},[102,1323,1324,1326,1329],{"class":104,"line":154},[102,1325,157],{"class":108},[102,1327,1328],{"class":125}," id, total_amount, ",[102,1330,1331],{"class":108},"status\n",[102,1333,1334,1336],{"class":104,"line":481},[102,1335,690],{"class":108},[102,1337,1105],{"class":125},[102,1339,1340,1342,1344,1346,1348],{"class":104,"line":493},[102,1341,169],{"class":108},[102,1343,827],{"class":125},[102,1345,175],{"class":108},[102,1347,178],{"class":125},[102,1349,834],{"class":181},[102,1351,1352,1354,1356],{"class":104,"line":507},[102,1353,240],{"class":108},[102,1355,888],{"class":125},[102,1357,891],{"class":108},[102,1359,1360,1363,1366],{"class":104,"line":519},[102,1361,1362],{"class":108},"LIMIT",[102,1364,1365],{"class":181}," 20",[102,1367,185],{"class":125},[15,1369,1370],{},"Key columns участвуют в поиске и ordering. Included columns лежат в leaf pages и помогают избежать heap access.",[15,1372,1373],{},"Цена:",[86,1375,1376,1379,1382,1385],{},[70,1377,1378],{},"индекс больше;",[70,1380,1381],{},"writes дороже;",[70,1383,1384],{},"included columns не бесплатны;",[70,1386,1387],{},"index-only scan всё равно зависит от visibility map.",[47,1389],{},[50,1391,1393],{"id":1392},"query-planner-статистика-и-планы","Query planner: статистика и планы",[15,1395,1396],{},"Планировщик выбирает план по оценкам.",[15,1398,1399,1400,1403],{},"Cardinality - сколько строк ожидается.",[1401,1402],"br",{},"\nSelectivity - какая доля строк проходит predicate.",[34,1405,1407],{"className":96,"code":1406,"language":98,"meta":40,"style":40},"SELECT * FROM users WHERE country = 'DE';\n",[19,1408,1409],{"__ignoreMap":40},[102,1410,1411,1413,1415,1417,1419,1421,1424,1426,1429],{"class":104,"line":105},[102,1412,157],{"class":108},[102,1414,628],{"class":108},[102,1416,163],{"class":108},[102,1418,166],{"class":125},[102,1420,169],{"class":108},[102,1422,1423],{"class":125}," country ",[102,1425,175],{"class":108},[102,1427,1428],{"class":705}," 'DE'",[102,1430,185],{"class":125},[15,1432,1433,1434,1437],{},"Если ",[19,1435,1436],{},"country='DE'"," у 1% пользователей, index scan вероятен. Если у 60%, seq scan может быть дешевле.",[15,1439,1440,1441,1444],{},"Статистика собирается ",[19,1442,1443],{},"ANALYZE",":",[34,1446,1448],{"className":96,"code":1447,"language":98,"meta":40,"style":40},"ANALYZE users;\n",[19,1449,1450],{"__ignoreMap":40},[102,1451,1452],{"class":104,"line":105},[102,1453,1447],{"class":125},[15,1455,1456],{},"PostgreSQL хранит:",[86,1458,1459,1462,1465,1468,1471,1474],{},[70,1460,1461],{},"примерное число rows\u002Fpages;",[70,1463,1464],{},"most common values;",[70,1466,1467],{},"histogram;",[70,1469,1470],{},"null fraction;",[70,1472,1473],{},"correlation;",[70,1475,1476],{},"distinct estimates.",[15,1478,1479],{},"Проблема: обычная статистика по колонкам не всегда видит зависимость между колонками.",[34,1481,1483],{"className":96,"code":1482,"language":98,"meta":40,"style":40},"WHERE country = 'US' AND state = 'CA'\n",[19,1484,1485],{"__ignoreMap":40},[102,1486,1487,1489,1491,1493,1496,1498,1501,1503],{"class":104,"line":105},[102,1488,169],{"class":108},[102,1490,1423],{"class":125},[102,1492,175],{"class":108},[102,1494,1495],{"class":705}," 'US'",[102,1497,849],{"class":108},[102,1499,1500],{"class":108}," state",[102,1502,502],{"class":108},[102,1504,1505],{"class":705}," 'CA'\n",[15,1507,1508],{},"Если planner считает conditions независимыми, он может сильно ошибиться. Для таких случаев есть extended statistics:",[34,1510,1512],{"className":96,"code":1511,"language":98,"meta":40,"style":40},"CREATE STATISTICS users_country_state_stats\nON country, state\nFROM users;\n\nANALYZE users;\n",[19,1513,1514,1524,1534,1541,1545],{"__ignoreMap":40},[102,1515,1516,1518,1521],{"class":104,"line":105},[102,1517,109],{"class":108},[102,1519,1520],{"class":108}," STATISTICS",[102,1522,1523],{"class":125}," users_country_state_stats\n",[102,1525,1526,1528,1531],{"class":104,"line":119},[102,1527,122],{"class":108},[102,1529,1530],{"class":125}," country, ",[102,1532,1533],{"class":108},"state\n",[102,1535,1536,1538],{"class":104,"line":141},[102,1537,690],{"class":108},[102,1539,1540],{"class":125}," users;\n",[102,1542,1543],{"class":104,"line":148},[102,1544,145],{"emptyLinePlaceholder":144},[102,1546,1547],{"class":104,"line":154},[102,1548,1447],{"class":125},[15,1550,1551],{},"Ещё один инструмент:",[34,1553,1555],{"className":96,"code":1554,"language":98,"meta":40,"style":40},"ALTER TABLE events ALTER COLUMN type SET STATISTICS 1000;\nANALYZE events;\n",[19,1556,1557,1584],{"__ignoreMap":40},[102,1558,1559,1562,1564,1566,1568,1571,1574,1577,1579,1582],{"class":104,"line":105},[102,1560,1561],{"class":108},"ALTER",[102,1563,445],{"class":108},[102,1565,669],{"class":125},[102,1567,1561],{"class":108},[102,1569,1570],{"class":125}," COLUMN ",[102,1572,1573],{"class":108},"type",[102,1575,1576],{"class":108}," SET",[102,1578,1520],{"class":108},[102,1580,1581],{"class":181}," 1000",[102,1583,185],{"class":125},[102,1585,1586],{"class":104,"line":119},[102,1587,1588],{"class":125},"ANALYZE events;\n",[15,1590,1591],{},"Высокий statistics target полезен для skewed columns, но увеличивает время analyze и размер статистики.",[47,1593],{},[201,1595,1597],{"id":1596},"seq-scan-vs-index-scan","Seq scan vs index scan",[15,1599,1600],{},"Seq scan читает таблицу последовательно.",[15,1602,1603],{},"Это нормально, если:",[86,1605,1606,1609,1612,1615,1618],{},[70,1607,1608],{},"таблица маленькая;",[70,1610,1611],{},"нужна большая доля строк;",[70,1613,1614],{},"predicate не селективен;",[70,1616,1617],{},"index lookup привёл бы к множеству random heap reads;",[70,1619,1620],{},"данные уже в cache и full scan дешевле.",[15,1622,1623],{},"Index scan хорош, если:",[86,1625,1626,1629,1635,1638],{},[70,1627,1628],{},"predicate селективен;",[70,1630,1631,1632,217],{},"нужен ",[19,1633,1634],{},"ORDER BY ... LIMIT",[70,1636,1637],{},"индекс покрывает запрос;",[70,1639,1640],{},"чтение heap ограничено.",[15,1642,1643],{},"Bitmap scan - компромисс.",[34,1645,1648],{"className":1646,"code":1647,"language":39,"meta":40},[37],"Bitmap Index Scan -> строит bitmap подходящих tuple locations\nBitmap Heap Scan  -> читает heap pages пачками\n",[19,1649,1647],{"__ignoreMap":40},[15,1651,1652],{},"Bitmap scan часто появляется, когда строк больше, чем удобно читать random index scan'ом, но меньше, чем читать всю таблицу.",[15,1654,1655],{},"BitmapAnd\u002FBitmapOr могут объединять несколько индексов:",[34,1657,1659],{"className":96,"code":1658,"language":98,"meta":40,"style":40},"WHERE status = 'paid' AND created_at >= now() - interval '7 days'\n",[19,1660,1661],{"__ignoreMap":40},[102,1662,1663,1665,1667,1669,1672,1674,1676,1678,1680,1682,1684,1686],{"class":104,"line":105},[102,1664,169],{"class":108},[102,1666,852],{"class":108},[102,1668,502],{"class":108},[102,1670,1671],{"class":705}," 'paid'",[102,1673,849],{"class":108},[102,1675,888],{"class":125},[102,1677,232],{"class":108},[102,1679,922],{"class":108},[102,1681,925],{"class":125},[102,1683,928],{"class":108},[102,1685,931],{"class":125},[102,1687,1688],{"class":705},"'7 days'\n",[15,1690,1691],{},"Но \"PostgreSQL объединит индексы\" не должно быть основным дизайном. Хороший composite index часто лучше.",[47,1693],{},[201,1695,1697],{"id":1696},"joins","Joins",[15,1699,1700],{},"PostgreSQL обычно выбирает один из трёх join algorithms.",[201,1702,1704],{"id":1703},"nested-loop","Nested Loop",[34,1706,1709],{"className":1707,"code":1708,"language":39,"meta":40},[37],"for each row in outer:\n  find matching rows in inner\n",[19,1710,1708],{"__ignoreMap":40},[15,1712,1713],{},"Хорош для маленького outer и indexed lookup во inner.",[15,1715,1716],{},"Плохой симптом:",[34,1718,1721],{"className":1719,"code":1720,"language":39,"meta":40},[37],"Nested Loop\n  -> rows=100000\n  -> Index Scan ... loops=100000\n",[19,1722,1720],{"__ignoreMap":40},[15,1724,1725],{},"Если inner scan дорогой, loops убьют latency.",[201,1727,1729],{"id":1728},"hash-join","Hash Join",[15,1731,1732],{},"PostgreSQL строит hash table по одной стороне и пробегает другую.",[15,1734,1735,1736,193],{},"Хорош для equality joins и средних\u002Fбольших наборов. Может spill на диск, если не хватает ",[19,1737,1738],{},"work_mem",[201,1740,1742],{"id":1741},"merge-join","Merge Join",[15,1744,1745],{},"Обе стороны отсортированы по join key, затем идут последовательно.",[15,1747,1748],{},"Хорош, если:",[86,1750,1751,1754,1757,1760],{},[70,1752,1753],{},"данные уже отсортированы индексами;",[70,1755,1756],{},"нужен ordering;",[70,1758,1759],{},"большие наборы;",[70,1761,1762],{},"equality\u002Frange compatible conditions.",[47,1764],{},[50,1766,1768],{"id":1767},"explain-analyze-buffers","EXPLAIN, ANALYZE, BUFFERS",[15,1770,1771],{},"Главная команда для tuning:",[34,1773,1775],{"className":96,"code":1774,"language":98,"meta":40,"style":40},"EXPLAIN (ANALYZE, BUFFERS, VERBOSE)\nSELECT ...\n",[19,1776,1777,1787],{"__ignoreMap":40},[102,1778,1779,1782,1785],{"class":104,"line":105},[102,1780,1781],{"class":125},"EXPLAIN (ANALYZE, BUFFERS, ",[102,1783,1784],{"class":108},"VERBOSE",[102,1786,1306],{"class":125},[102,1788,1789,1791],{"class":104,"line":119},[102,1790,157],{"class":108},[102,1792,1793],{"class":125}," ...\n",[15,1795,1796,1798,1799,1802],{},[19,1797,1443],{}," реально выполняет запрос. Для ",[19,1800,1801],{},"INSERT\u002FUPDATE\u002FDELETE"," используйте transaction и rollback, если экспериментируете:",[34,1804,1806],{"className":96,"code":1805,"language":98,"meta":40,"style":40},"BEGIN;\nEXPLAIN (ANALYZE, BUFFERS)\nDELETE FROM sessions WHERE expires_at \u003C now();\nROLLBACK;\n",[19,1807,1808,1815,1819,1839],{"__ignoreMap":40},[102,1809,1810,1813],{"class":104,"line":105},[102,1811,1812],{"class":108},"BEGIN",[102,1814,185],{"class":125},[102,1816,1817],{"class":104,"line":119},[102,1818,151],{"class":125},[102,1820,1821,1823,1825,1827,1830,1833,1835,1837],{"class":104,"line":141},[102,1822,28],{"class":108},[102,1824,163],{"class":108},[102,1826,352],{"class":108},[102,1828,1829],{"class":108}," WHERE",[102,1831,1832],{"class":125}," expires_at ",[102,1834,223],{"class":108},[102,1836,922],{"class":108},[102,1838,1127],{"class":125},[102,1840,1841,1844],{"class":104,"line":148},[102,1842,1843],{"class":108},"ROLLBACK",[102,1845,185],{"class":125},[15,1847,1848],{},"Что смотреть:",[1850,1851,1852,1865],"table",{},[1853,1854,1855],"thead",{},[1856,1857,1858,1862],"tr",{},[1859,1860,1861],"th",{},"Признак",[1859,1863,1864],{},"Возможный вывод",[1866,1867,1868,1877,1885,1896,1906,1917,1927],"tbody",{},[1856,1869,1870,1874],{},[1871,1872,1873],"td",{},"estimated rows сильно меньше actual",[1871,1875,1876],{},"неактуальная или недостаточная статистика",[1856,1878,1879,1882],{},[1871,1880,1881],{},"estimated rows сильно больше actual",[1871,1883,1884],{},"planner может выбрать seq\u002Fhash там, где нужен index\u002Fnested",[1856,1886,1887,1893],{},[1871,1888,1889,1892],{},[19,1890,1891],{},"shared read"," много",[1871,1894,1895],{},"данные читаются с диска, cache не помогает",[1856,1897,1898,1903],{},[1871,1899,1900],{},[19,1901,1902],{},"temp read\u002Fwrite",[1871,1904,1905],{},"sort\u002Fhash spilled на диск",[1856,1907,1908,1914],{},[1871,1909,1910,1913],{},[19,1911,1912],{},"loops"," большое",[1871,1915,1916],{},"внутренний узел исполняется много раз",[1856,1918,1919,1924],{},[1871,1920,1921,1892],{},[19,1922,1923],{},"Rows Removed by Filter",[1871,1925,1926],{},"индекс не сужает достаточно или predicate не indexable",[1856,1928,1929,1934],{},[1871,1930,1931,1892],{},[19,1932,1933],{},"Heap Fetches",[1871,1935,1936],{},"index-only scan упирается в visibility map",[15,1938,1939,1940,1943],{},"Не оптимизируйте только по ",[19,1941,1942],{},"cost",". Cost - абстрактная модель для сравнения планов внутри одной базы, а не milliseconds.",[47,1945],{},[201,1947,1949],{"id":1948},"common-query-tuning","Common query tuning",[201,1951,1953,1954],{"id":1952},"уберите-select","Уберите ",[19,1955,1956],{},"SELECT *",[34,1958,1960],{"className":96,"code":1959,"language":98,"meta":40,"style":40},"SELECT id, email, name\nFROM users\nWHERE id = $1;\n",[19,1961,1962,1972,1978],{"__ignoreMap":40},[102,1963,1964,1966,1969],{"class":104,"line":105},[102,1965,157],{"class":108},[102,1967,1968],{"class":125}," id, email, ",[102,1970,1971],{"class":108},"name\n",[102,1973,1974,1976],{"class":104,"line":119},[102,1975,690],{"class":108},[102,1977,1223],{"class":125},[102,1979,1980,1982,1985,1987,1989,1991],{"class":104,"line":141},[102,1981,169],{"class":108},[102,1983,1984],{"class":125}," id ",[102,1986,175],{"class":108},[102,1988,178],{"class":125},[102,1990,182],{"class":181},[102,1992,185],{"class":125},[15,1994,1995],{},"Это снижает I\u002FO, TOAST access, network payload и шанс сломать covering index.",[201,1997,1999],{"id":1998},"делайте-predicates-index-friendly","Делайте predicates index-friendly",[15,2001,2002],{},"Плохо:",[34,2004,2006],{"className":96,"code":2005,"language":98,"meta":40,"style":40},"WHERE date(created_at) = date(now())\n",[19,2007,2008],{"__ignoreMap":40},[102,2009,2010,2012,2015,2018,2020,2022,2025,2028],{"class":104,"line":105},[102,2011,169],{"class":108},[102,2013,2014],{"class":108}," date",[102,2016,2017],{"class":125},"(created_at) ",[102,2019,175],{"class":108},[102,2021,2014],{"class":108},[102,2023,2024],{"class":125},"(",[102,2026,2027],{"class":108},"now",[102,2029,2030],{"class":125},"())\n",[15,2032,2033],{},"Лучше:",[34,2035,2037],{"className":96,"code":2036,"language":98,"meta":40,"style":40},"WHERE created_at >= date_trunc('day', now())\n  AND created_at \u003C  date_trunc('day', now()) + interval '1 day'\n",[19,2038,2039,2059],{"__ignoreMap":40},[102,2040,2041,2043,2045,2047,2050,2053,2055,2057],{"class":104,"line":105},[102,2042,169],{"class":108},[102,2044,888],{"class":125},[102,2046,232],{"class":108},[102,2048,2049],{"class":125}," date_trunc(",[102,2051,2052],{"class":705},"'day'",[102,2054,22],{"class":125},[102,2056,2027],{"class":108},[102,2058,2030],{"class":125},[102,2060,2061,2064,2066,2068,2071,2073,2075,2077,2080,2083,2085],{"class":104,"line":119},[102,2062,2063],{"class":108},"  AND",[102,2065,888],{"class":125},[102,2067,223],{"class":108},[102,2069,2070],{"class":125},"  date_trunc(",[102,2072,2052],{"class":705},[102,2074,22],{"class":125},[102,2076,2027],{"class":108},[102,2078,2079],{"class":125},"()) ",[102,2081,2082],{"class":108},"+",[102,2084,931],{"class":125},[102,2086,934],{"class":705},[15,2088,2089],{},"Либо expression index, если именно expression - доменный access pattern.",[201,2091,2093],{"id":2092},"индексируйте-foreign-keys","Индексируйте foreign keys",[15,2095,2096],{},"PostgreSQL автоматически создаёт индекс для primary key\u002Funique на referenced side, но не всегда создаёт индекс на referencing foreign key column. Для joins и deletes parent rows индекс на child FK часто обязателен.",[34,2098,2100],{"className":96,"code":2099,"language":98,"meta":40,"style":40},"CREATE INDEX order_items_order_id_idx ON order_items (order_id);\n",[19,2101,2102],{"__ignoreMap":40},[102,2103,2104,2106,2108,2111,2113],{"class":104,"line":105},[102,2105,109],{"class":108},[102,2107,112],{"class":108},[102,2109,2110],{"class":115}," order_items_order_id_idx",[102,2112,276],{"class":108},[102,2114,2115],{"class":125}," order_items (order_id);\n",[201,2117,2119,2120,2122],{"id":2118},"используйте-order-by-limit-вместе-с-индексом","Используйте ",[19,2121,1634],{}," вместе с индексом",[34,2124,2126],{"className":96,"code":2125,"language":98,"meta":40,"style":40},"CREATE INDEX events_user_created_idx\nON events (user_id, created_at DESC);\n\nSELECT *\nFROM events\nWHERE user_id = $1\nORDER BY created_at DESC\nLIMIT 50;\n",[19,2127,2128,2137,2148,2152,2158,2164,2176,2184],{"__ignoreMap":40},[102,2129,2130,2132,2134],{"class":104,"line":105},[102,2131,109],{"class":108},[102,2133,112],{"class":108},[102,2135,2136],{"class":115}," events_user_created_idx\n",[102,2138,2139,2141,2144,2146],{"class":104,"line":119},[102,2140,122],{"class":108},[102,2142,2143],{"class":125}," events (user_id, created_at ",[102,2145,317],{"class":108},[102,2147,138],{"class":125},[102,2149,2150],{"class":104,"line":141},[102,2151,145],{"emptyLinePlaceholder":144},[102,2153,2154,2156],{"class":104,"line":148},[102,2155,157],{"class":108},[102,2157,685],{"class":108},[102,2159,2160,2162],{"class":104,"line":154},[102,2161,690],{"class":108},[102,2163,693],{"class":125},[102,2165,2166,2168,2170,2172,2174],{"class":104,"line":481},[102,2167,169],{"class":108},[102,2169,827],{"class":125},[102,2171,175],{"class":108},[102,2173,178],{"class":125},[102,2175,834],{"class":181},[102,2177,2178,2180,2182],{"class":104,"line":493},[102,2179,240],{"class":108},[102,2181,888],{"class":125},[102,2183,891],{"class":108},[102,2185,2186,2188,2191],{"class":104,"line":507},[102,2187,1362],{"class":108},[102,2189,2190],{"class":181}," 50",[102,2192,185],{"class":125},[15,2194,2195],{},"Без подходящего индекса база может найти все rows пользователя, отсортировать и только потом взять 50.",[201,2197,2199],{"id":2198},"осторожно-с-offset-pagination","Осторожно с offset pagination",[34,2201,2203],{"className":96,"code":2202,"language":98,"meta":40,"style":40},"SELECT *\nFROM events\nORDER BY created_at DESC\nOFFSET 100000 LIMIT 50;\n",[19,2204,2205,2211,2217,2225],{"__ignoreMap":40},[102,2206,2207,2209],{"class":104,"line":105},[102,2208,157],{"class":108},[102,2210,685],{"class":108},[102,2212,2213,2215],{"class":104,"line":119},[102,2214,690],{"class":108},[102,2216,693],{"class":125},[102,2218,2219,2221,2223],{"class":104,"line":141},[102,2220,240],{"class":108},[102,2222,888],{"class":125},[102,2224,891],{"class":108},[102,2226,2227,2230,2233,2236,2238],{"class":104,"line":148},[102,2228,2229],{"class":125},"OFFSET ",[102,2231,2232],{"class":181},"100000",[102,2234,2235],{"class":108}," LIMIT",[102,2237,2190],{"class":181},[102,2239,185],{"class":125},[15,2241,2242],{},"PostgreSQL всё равно должен пройти первые 100000 строк. Keyset pagination обычно лучше:",[34,2244,2246],{"className":96,"code":2245,"language":98,"meta":40,"style":40},"SELECT *\nFROM events\nWHERE created_at \u003C $last_seen_created_at\nORDER BY created_at DESC\nLIMIT 50;\n",[19,2247,2248,2254,2260,2271,2279],{"__ignoreMap":40},[102,2249,2250,2252],{"class":104,"line":105},[102,2251,157],{"class":108},[102,2253,685],{"class":108},[102,2255,2256,2258],{"class":104,"line":119},[102,2257,690],{"class":108},[102,2259,693],{"class":125},[102,2261,2262,2264,2266,2268],{"class":104,"line":141},[102,2263,169],{"class":108},[102,2265,888],{"class":125},[102,2267,223],{"class":108},[102,2269,2270],{"class":125}," $last_seen_created_at\n",[102,2272,2273,2275,2277],{"class":104,"line":148},[102,2274,240],{"class":108},[102,2276,888],{"class":125},[102,2278,891],{"class":108},[102,2280,2281,2283,2285],{"class":104,"line":154},[102,2282,1362],{"class":108},[102,2284,2190],{"class":181},[102,2286,185],{"class":125},[201,2288,2290],{"id":2289},"не-плодите-индексы","Не плодите индексы",[15,2292,2293],{},"Каждый индекс:",[86,2295,2296,2299,2302,2305,2308,2311],{},[70,2297,2298],{},"занимает disk;",[70,2300,2301],{},"обновляется при writes;",[70,2303,2304],{},"создаёт WAL;",[70,2306,2307],{},"vacuum'ится;",[70,2309,2310],{},"может мешать HOT updates;",[70,2312,2313],{},"усложняет выбор planner.",[15,2315,2316],{},"Проверяйте usage:",[34,2318,2320],{"className":96,"code":2319,"language":98,"meta":40,"style":40},"SELECT\n  schemaname,\n  relname,\n  indexrelname,\n  idx_scan,\n  idx_tup_read,\n  idx_tup_fetch\nFROM pg_stat_user_indexes\nORDER BY idx_scan ASC;\n",[19,2321,2322,2327,2332,2337,2342,2347,2352,2357,2364],{"__ignoreMap":40},[102,2323,2324],{"class":104,"line":105},[102,2325,2326],{"class":108},"SELECT\n",[102,2328,2329],{"class":104,"line":119},[102,2330,2331],{"class":125},"  schemaname,\n",[102,2333,2334],{"class":104,"line":141},[102,2335,2336],{"class":125},"  relname,\n",[102,2338,2339],{"class":104,"line":148},[102,2340,2341],{"class":125},"  indexrelname,\n",[102,2343,2344],{"class":104,"line":154},[102,2345,2346],{"class":125},"  idx_scan,\n",[102,2348,2349],{"class":104,"line":481},[102,2350,2351],{"class":125},"  idx_tup_read,\n",[102,2353,2354],{"class":104,"line":493},[102,2355,2356],{"class":125},"  idx_tup_fetch\n",[102,2358,2359,2361],{"class":104,"line":507},[102,2360,690],{"class":108},[102,2362,2363],{"class":125}," pg_stat_user_indexes\n",[102,2365,2366,2368,2371,2374],{"class":104,"line":519},[102,2367,240],{"class":108},[102,2369,2370],{"class":125}," idx_scan ",[102,2372,2373],{"class":108},"ASC",[102,2375,185],{"class":125},[15,2377,2378,2381],{},[19,2379,2380],{},"idx_scan=0"," не всегда значит \"удалить\": индекс мог быть новым, нужен для constraint, rare incident query или monthly job. Но это повод спросить.",[47,2383],{},[50,2385,2387],{"id":2386},"безопасный-rollout-индексов","Безопасный rollout индексов",[15,2389,2390,2391,2394],{},"Индекс в production - это миграция с I\u002FO, WAL, lock'ами и планировщиком, а не только ",[19,2392,2393],{},"CREATE INDEX",". Безопасный rollout начинается до SQL:",[67,2396,2397,2403,2406,2411,2414,2420,2423],{},[70,2398,2399,2400,193],{},"Зафиксируйте конкретный query pattern: SQL, параметры, частоту, latency SLO, ",[19,2401,2402],{},"EXPLAIN (ANALYZE, BUFFERS)",[70,2404,2405],{},"Оцените размер: таблица, существующие индексы, write rate, свободное место на диске, WAL\u002Freplication lag.",[70,2407,2408,2409,193],{},"Проверьте, не решается ли проблема статистикой, переписыванием predicate, pagination или меньшим ",[19,2410,157],{},[70,2412,2413],{},"Создавайте индекс в окно меньшей нагрузки и наблюдайте I\u002FO, locks, WAL, lag.",[70,2415,2416,2417,2419],{},"После создания выполните ",[19,2418,1443],{},", если статистика могла быть неактуальна.",[70,2421,2422],{},"Проверьте фактический план на production-like параметрах, включая skewed tenants\u002Fstatuses.",[70,2424,2425],{},"Держите drop plan: какой старый индекс можно удалить, когда и по каким метрикам.",[201,2427,2429],{"id":2428},"create-index-concurrently",[19,2430,2431],{},"CREATE INDEX CONCURRENTLY",[15,2433,2434],{},"Для большой таблицы обычно используют:",[34,2436,2438],{"className":96,"code":2437,"language":98,"meta":40,"style":40},"CREATE INDEX CONCURRENTLY orders_tenant_created_idx\nON orders (tenant_id, created_at DESC);\n",[19,2439,2440,2452],{"__ignoreMap":40},[102,2441,2442,2444,2446,2449],{"class":104,"line":105},[102,2443,109],{"class":108},[102,2445,112],{"class":108},[102,2447,2448],{"class":115}," CONCURRENTLY",[102,2450,2451],{"class":125}," orders_tenant_created_idx\n",[102,2453,2454,2456,2459,2461],{"class":104,"line":119},[102,2455,122],{"class":108},[102,2457,2458],{"class":125}," orders (tenant_id, created_at ",[102,2460,317],{"class":108},[102,2462,138],{"class":125},[15,2464,2465,2466,2468],{},"Он не блокирует обычные reads\u002Fwrites так же жёстко, как обычный ",[19,2467,2393],{},", но у него есть caveats:",[86,2470,2471,2474,2477,2480,2483,2486,2489],{},[70,2472,2473],{},"нельзя запускать внутри transaction block;",[70,2475,2476],{},"проходит таблицу больше одного раза, поэтому дольше и дороже по I\u002FO;",[70,2478,2479],{},"всё равно берёт locks, которые могут конфликтовать с некоторыми DDL;",[70,2481,2482],{},"при ошибке может оставить invalid index, который нужно явно удалить;",[70,2484,2485],{},"unique concurrent index сначала строится, а потом валидируется, поэтому возможны конфликты на данных;",[70,2487,2488],{},"на busy table создаёт WAL и может увеличить replication lag;",[70,2490,2491],{},"несколько concurrent builds на одной таблице легко перегружают диск.",[15,2493,2494],{},"Если build упал, проверьте:",[34,2496,2498],{"className":96,"code":2497,"language":98,"meta":40,"style":40},"SELECT indexrelid::regclass, indisvalid, indisready\nFROM pg_index\nWHERE indrelid = 'orders'::regclass;\n",[19,2499,2500,2507,2514],{"__ignoreMap":40},[102,2501,2502,2504],{"class":104,"line":105},[102,2503,157],{"class":108},[102,2505,2506],{"class":125}," indexrelid::regclass, indisvalid, indisready\n",[102,2508,2509,2511],{"class":104,"line":119},[102,2510,690],{"class":108},[102,2512,2513],{"class":125}," pg_index\n",[102,2515,2516,2518,2521,2523,2526],{"class":104,"line":141},[102,2517,169],{"class":108},[102,2519,2520],{"class":125}," indrelid ",[102,2522,175],{"class":108},[102,2524,2525],{"class":705}," 'orders'",[102,2527,2528],{"class":125},"::regclass;\n",[15,2530,2531],{},"Invalid index не помогает запросам, но может занимать место и мешать writes. Обычно его удаляют отдельно:",[34,2533,2535],{"className":96,"code":2534,"language":98,"meta":40,"style":40},"DROP INDEX CONCURRENTLY IF EXISTS orders_tenant_created_idx;\n",[19,2536,2537],{"__ignoreMap":40},[102,2538,2539,2542,2544,2547,2549,2551],{"class":104,"line":105},[102,2540,2541],{"class":108},"DROP",[102,2543,112],{"class":108},[102,2545,2546],{"class":125}," CONCURRENTLY ",[102,2548,425],{"class":108},[102,2550,431],{"class":108},[102,2552,2553],{"class":125}," orders_tenant_created_idx;\n",[201,2555,2557],{"id":2556},"статистика-и-реалистичные-проверки","Статистика и реалистичные проверки",[15,2559,2560],{},"План на маленькой dev-базе почти ничего не доказывает. Planner чувствителен к размеру таблицы, skew, correlation, visibility map и cache state.",[15,2562,2563],{},"Проверяйте:",[86,2565,2566,2569,2572,2575,2583,2586,2595],{},[70,2567,2568],{},"частый tenant и редкий tenant;",[70,2570,2571],{},"status, который встречается у 1% и у 70% строк;",[70,2573,2574],{},"свежие строки и старые строки;",[70,2576,2577,2578,22,2580,2582],{},"запрос с реальными ",[19,2579,1362],{},[19,2581,240],{},", selected columns;",[70,2584,2585],{},"prepared statement поведение, если приложение использует bind-параметры;",[70,2587,2588,2591,2592,217],{},[19,2589,2590],{},"actual rows"," против ",[19,2593,2594],{},"estimated rows",[70,2596,2597,22,2599,22,2601,193],{},[19,2598,1891],{},[19,2600,1902],{},[19,2602,1933],{},[15,2604,2605,2606,2609,2610,2613],{},"Если partial index зависит от literal predicate, запрос вида ",[19,2607,2608],{},"status = $1"," может хуже доказываться, чем ",[19,2611,2612],{},"status = 'unpaid'",". Это не повод хардкодить всё в SQL бездумно, но повод проверить план именно так, как запрос отправляет приложение.",[201,2615,2617],{"id":2616},"цена-индекса-и-drop-plan","Цена индекса и drop plan",[15,2619,2620],{},"Каждый новый индекс увеличивает write amplification:",[34,2622,2625],{"className":2623,"code":2624,"language":39,"meta":40},[37],"INSERT row\n  -> write heap\n  -> update every affected index\n  -> write WAL for heap and indexes\n  -> later vacuum heap and indexes\n",[19,2626,2624],{"__ignoreMap":40},[15,2628,2629,2630,2632],{},"Для ",[19,2631,25],{}," особенно важно, меняются ли indexed columns. Если меняются, PostgreSQL должен поддерживать index entries и чаще теряет шанс на HOT update. Поэтому индекс на \"удобную для фильтра\" колонку может замедлить самый горячий write path.",[15,2634,2635],{},"Перед merge senior reviewer ожидает ответы:",[86,2637,2638,2641,2644,2647,2652,2655],{},[70,2639,2640],{},"сколько места займёт индекс и хватит ли disk с учётом WAL\u002Freplication;",[70,2642,2643],{},"какой write path станет дороже;",[70,2645,2646],{},"какие queries реально ускорятся и на каких параметрах это проверено;",[70,2648,2649,2650,217],{},"нужен ли ",[19,2651,2431],{},[70,2653,2654],{},"когда индекс будет удалён, если planner его не использует;",[70,2656,2657],{},"какие метрики покажут регресс: write latency, WAL rate, bloat, autovacuum time, replication lag.",[15,2659,2660,2661,2664],{},"Удаление тоже делают осторожно. Сначала убедитесь, что индекс не поддерживает constraint, не нужен редкому batch job и не является аварийным access path. Затем наблюдайте ",[19,2662,2663],{},"pg_stat_user_indexes"," достаточно долго, потому что статистика сбрасывается при restart\u002Freset и не видит будущие месячные задачи.",[47,2666],{},[50,2668,2670],{"id":2669},"практический-пример-проектирования","Практический пример проектирования",[15,2672,2673],{},"Есть таблица:",[34,2675,2677],{"className":96,"code":2676,"language":98,"meta":40,"style":40},"CREATE TABLE orders (\n  id bigserial PRIMARY KEY,\n  tenant_id bigint NOT NULL,\n  user_id bigint NOT NULL,\n  status text NOT NULL,\n  created_at timestamptz NOT NULL,\n  paid_at timestamptz,\n  total_amount numeric(12,2) NOT NULL\n);\n",[19,2678,2679,2690,2703,2714,2725,2737,2749,2758,2782],{"__ignoreMap":40},[102,2680,2681,2683,2685,2688],{"class":104,"line":105},[102,2682,109],{"class":108},[102,2684,445],{"class":108},[102,2686,2687],{"class":115}," orders",[102,2689,451],{"class":125},[102,2691,2692,2695,2698,2701],{"class":104,"line":119},[102,2693,2694],{"class":125},"  id ",[102,2696,2697],{"class":108},"bigserial",[102,2699,2700],{"class":108}," PRIMARY KEY",[102,2702,465],{"class":125},[102,2704,2705,2708,2710,2712],{"class":104,"line":141},[102,2706,2707],{"class":125},"  tenant_id ",[102,2709,459],{"class":108},[102,2711,462],{"class":108},[102,2713,465],{"class":125},[102,2715,2716,2719,2721,2723],{"class":104,"line":148},[102,2717,2718],{"class":125},"  user_id ",[102,2720,459],{"class":108},[102,2722,462],{"class":108},[102,2724,465],{"class":125},[102,2726,2727,2730,2733,2735],{"class":104,"line":154},[102,2728,2729],{"class":108},"  status",[102,2731,2732],{"class":108}," text",[102,2734,462],{"class":108},[102,2736,465],{"class":125},[102,2738,2739,2742,2745,2747],{"class":104,"line":481},[102,2740,2741],{"class":125},"  created_at ",[102,2743,2744],{"class":108},"timestamptz",[102,2746,462],{"class":108},[102,2748,465],{"class":125},[102,2750,2751,2754,2756],{"class":104,"line":493},[102,2752,2753],{"class":125},"  paid_at ",[102,2755,2744],{"class":108},[102,2757,465],{"class":125},[102,2759,2760,2763,2766,2768,2771,2774,2776,2779],{"class":104,"line":507},[102,2761,2762],{"class":125},"  total_amount ",[102,2764,2765],{"class":108},"numeric",[102,2767,2024],{"class":125},[102,2769,2770],{"class":181},"12",[102,2772,2773],{"class":125},",",[102,2775,882],{"class":181},[102,2777,2778],{"class":125},") ",[102,2780,2781],{"class":108},"NOT NULL\n",[102,2783,2784],{"class":104,"line":519},[102,2785,138],{"class":125},[15,2787,2788],{},"Запросы:",[34,2790,2792],{"className":96,"code":2791,"language":98,"meta":40,"style":40},"-- последние заказы tenant\nSELECT id, status, total_amount\nFROM orders\nWHERE tenant_id = $1\nORDER BY created_at DESC\nLIMIT 50;\n\n-- неоплаченные overdue\nSELECT id\nFROM orders\nWHERE tenant_id = $1\n  AND status = 'unpaid'\n  AND paid_at IS NULL\n  AND created_at \u003C $2;\n",[19,2793,2794,2799,2811,2817,2829,2837,2845,2849,2854,2861,2867,2880,2892,2905],{"__ignoreMap":40},[102,2795,2796],{"class":104,"line":105},[102,2797,2798],{"class":947},"-- последние заказы tenant\n",[102,2800,2801,2803,2806,2808],{"class":104,"line":119},[102,2802,157],{"class":108},[102,2804,2805],{"class":125}," id, ",[102,2807,805],{"class":108},[102,2809,2810],{"class":125},", total_amount\n",[102,2812,2813,2815],{"class":104,"line":141},[102,2814,690],{"class":108},[102,2816,1105],{"class":125},[102,2818,2819,2821,2823,2825,2827],{"class":104,"line":148},[102,2820,169],{"class":108},[102,2822,955],{"class":125},[102,2824,175],{"class":108},[102,2826,178],{"class":125},[102,2828,834],{"class":181},[102,2830,2831,2833,2835],{"class":104,"line":154},[102,2832,240],{"class":108},[102,2834,888],{"class":125},[102,2836,891],{"class":108},[102,2838,2839,2841,2843],{"class":104,"line":481},[102,2840,1362],{"class":108},[102,2842,2190],{"class":181},[102,2844,185],{"class":125},[102,2846,2847],{"class":104,"line":493},[102,2848,145],{"emptyLinePlaceholder":144},[102,2850,2851],{"class":104,"line":507},[102,2852,2853],{"class":947},"-- неоплаченные overdue\n",[102,2855,2856,2858],{"class":104,"line":519},[102,2857,157],{"class":108},[102,2859,2860],{"class":125}," id\n",[102,2862,2863,2865],{"class":104,"line":525},[102,2864,690],{"class":108},[102,2866,1105],{"class":125},[102,2868,2870,2872,2874,2876,2878],{"class":104,"line":2869},11,[102,2871,169],{"class":108},[102,2873,955],{"class":125},[102,2875,175],{"class":108},[102,2877,178],{"class":125},[102,2879,834],{"class":181},[102,2881,2883,2885,2887,2889],{"class":104,"line":2882},12,[102,2884,2063],{"class":108},[102,2886,852],{"class":108},[102,2888,502],{"class":108},[102,2890,2891],{"class":705}," 'unpaid'\n",[102,2893,2895,2897,2900,2902],{"class":104,"line":2894},13,[102,2896,2063],{"class":108},[102,2898,2899],{"class":125}," paid_at ",[102,2901,1165],{"class":108},[102,2903,2904],{"class":108}," NULL\n",[102,2906,2908,2910,2912,2914,2916,2918],{"class":104,"line":2907},14,[102,2909,2063],{"class":108},[102,2911,888],{"class":125},[102,2913,223],{"class":108},[102,2915,178],{"class":125},[102,2917,882],{"class":181},[102,2919,185],{"class":125},[15,2921,2922],{},"Индексы:",[34,2924,2926],{"className":96,"code":2925,"language":98,"meta":40,"style":40},"CREATE INDEX orders_tenant_created_idx\nON orders (tenant_id, created_at DESC)\nINCLUDE (status, total_amount);\n\nCREATE INDEX orders_unpaid_overdue_idx\nON orders (tenant_id, created_at)\nWHERE status = 'unpaid' AND paid_at IS NULL;\n",[19,2927,2928,2936,2946,2957,2961,2970,2977],{"__ignoreMap":40},[102,2929,2930,2932,2934],{"class":104,"line":105},[102,2931,109],{"class":108},[102,2933,112],{"class":108},[102,2935,2451],{"class":115},[102,2937,2938,2940,2942,2944],{"class":104,"line":119},[102,2939,122],{"class":108},[102,2941,2458],{"class":125},[102,2943,317],{"class":108},[102,2945,1306],{"class":125},[102,2947,2948,2950,2952,2954],{"class":104,"line":141},[102,2949,129],{"class":108},[102,2951,132],{"class":125},[102,2953,805],{"class":108},[102,2955,2956],{"class":125},", total_amount);\n",[102,2958,2959],{"class":104,"line":148},[102,2960,145],{"emptyLinePlaceholder":144},[102,2962,2963,2965,2967],{"class":104,"line":154},[102,2964,109],{"class":108},[102,2966,112],{"class":108},[102,2968,2969],{"class":115}," orders_unpaid_overdue_idx\n",[102,2971,2972,2974],{"class":104,"line":481},[102,2973,122],{"class":108},[102,2975,2976],{"class":125}," orders (tenant_id, created_at)\n",[102,2978,2979,2981,2983,2985,2987,2989,2991,2993,2995],{"class":104,"line":493},[102,2980,169],{"class":108},[102,2982,852],{"class":108},[102,2984,502],{"class":108},[102,2986,1065],{"class":705},[102,2988,849],{"class":108},[102,2990,2899],{"class":125},[102,2992,1165],{"class":108},[102,2994,1168],{"class":108},[102,2996,185],{"class":125},[15,2998,2999,3000,3002],{},"Первый индекс поддерживает ",[19,3001,1634],{}," и может стать index-only. Второй не тратит место на оплаченные заказы.",[15,3004,3005],{},"Проверка:",[34,3007,3009],{"className":96,"code":3008,"language":98,"meta":40,"style":40},"EXPLAIN (ANALYZE, BUFFERS)\nSELECT id\nFROM orders\nWHERE tenant_id = 10\n  AND status = 'unpaid'\n  AND paid_at IS NULL\n  AND created_at \u003C now() - interval '3 days';\n",[19,3010,3011,3015,3021,3027,3038,3048,3058],{"__ignoreMap":40},[102,3012,3013],{"class":104,"line":105},[102,3014,151],{"class":125},[102,3016,3017,3019],{"class":104,"line":119},[102,3018,157],{"class":108},[102,3020,2860],{"class":125},[102,3022,3023,3025],{"class":104,"line":141},[102,3024,690],{"class":108},[102,3026,1105],{"class":125},[102,3028,3029,3031,3033,3035],{"class":104,"line":148},[102,3030,169],{"class":108},[102,3032,955],{"class":125},[102,3034,175],{"class":108},[102,3036,3037],{"class":181}," 10\n",[102,3039,3040,3042,3044,3046],{"class":104,"line":154},[102,3041,2063],{"class":108},[102,3043,852],{"class":108},[102,3045,502],{"class":108},[102,3047,2891],{"class":705},[102,3049,3050,3052,3054,3056],{"class":104,"line":481},[102,3051,2063],{"class":108},[102,3053,2899],{"class":125},[102,3055,1165],{"class":108},[102,3057,2904],{"class":108},[102,3059,3060,3062,3064,3066,3068,3070,3072,3074,3077],{"class":104,"line":493},[102,3061,2063],{"class":108},[102,3063,888],{"class":125},[102,3065,223],{"class":108},[102,3067,922],{"class":108},[102,3069,925],{"class":125},[102,3071,928],{"class":108},[102,3073,931],{"class":125},[102,3075,3076],{"class":705},"'3 days'",[102,3078,185],{"class":125},[15,3080,3081,3082,3084,3085,3087],{},"Если planner не использует partial index, проверьте, совпадает ли predicate. Например ",[19,3083,2608],{}," с prepared statement иногда хуже доказывается как ",[19,3086,2612],{}," на этапе planning.",[47,3089],{},[50,3091,3093],{"id":3092},"что-помнить-на-собеседовании","Что помнить на собеседовании",[86,3095,3096,3099,3102,3105,3108,3111,3114,3117,3123,3126,3129,3135,3143],{},[70,3097,3098],{},"B-tree - основной индекс: equality, range, ordering, uniqueness.",[70,3100,3101],{},"GIN - inverted index для arrays\u002Fjsonb\u002Ffull-text, часто дорогой на writes.",[70,3103,3104],{},"GiST - generalized tree для ranges, geometry, exclusion constraints.",[70,3106,3107],{},"BRIN - маленький summary index для больших физически упорядоченных таблиц.",[70,3109,3110],{},"Составной индекс зависит от порядка колонок и левого префикса.",[70,3112,3113],{},"Partial index полезен для маленького важного subset.",[70,3115,3116],{},"Expression index нужен, если запрос фильтрует по выражению.",[70,3118,3119,3120,3122],{},"Covering index через ",[19,3121,129],{}," может дать index-only scan, но visibility map всё ещё важна.",[70,3124,3125],{},"Seq scan не всегда плохой; index scan не всегда хороший.",[70,3127,3128],{},"Bitmap scan читает index matches и затем heap pages пачками.",[70,3130,3131,3132,3134],{},"Planner выбирает планы по статистике; плохие estimates часто лечатся ",[19,3133,1443],{},", higher statistics target или extended statistics.",[70,3136,3137,3139,3140,193],{},[19,3138,2402],{}," важнее голого ",[19,3141,3142],{},"EXPLAIN",[70,3144,3145],{},"Индексы ускоряют reads ценой writes, WAL, disk и vacuum.",[47,3147],{},[50,3149,3151],{"id":3150},"практика","Практика",[67,3153,3154,3161,3168,3171],{},[70,3155,3156,3157,3160],{},"Для таблицы ",[19,3158,3159],{},"orders"," придумайте два реальных запроса и только потом спроектируйте индексы.",[70,3162,3163,3164,986,3166,193],{},"Сравните план с ",[19,3165,3142],{},[19,3167,2402],{},[70,3169,3170],{},"Сделайте partial index для активных\u002Fнеоплаченных записей и проверьте совпадение predicate.",[70,3172,3173,3174,3177],{},"Перепишите запрос с ",[19,3175,3176],{},"OFFSET"," на keyset pagination и добавьте составной индекс.",[47,3179],{},[50,3181,3183],{"id":3182},"интерактивная-практика","Интерактивная практика",[3185,3186,3190,3193,3212],"quiz",{"answer":3187,"id":3188,"xp":3189},"3","db-indexes-q1","10",[15,3191,3192],{},"Когда partial index обычно полезнее полного индекса?",[3194,3195,3196],"template",{"v-slot:options":40},[86,3197,3198,3201,3206,3209],{},[70,3199,3200],{},"Когда нужно индексировать каждую строку без исключений",[70,3202,3203,3204],{},"Когда запрос никогда не содержит ",[19,3205,169],{},[70,3207,3208],{},"Когда важный запрос обращается к маленькому подмножеству строк",[70,3210,3211],{},"Когда таблица маленькая и редко меняется",[3194,3213,3214],{"v-slot:explanation":40},[15,3215,3216],{},"Partial index экономит место и write cost, если predicate стабильно совпадает с важным subset, например активные или неоплаченные записи.",[3218,3219,3223,3226,3319],"predict",{"answer":3220,"id":3221,"xp":3222},"btree\\ngin","db-indexes-p1","15",[15,3224,3225],{},"Что вернёт запрос?",[3194,3227,3228],{"v-slot:code":40},[34,3229,3231],{"className":96,"code":3230,"language":98,"meta":40,"style":40},"WITH access_patterns(kind) AS (\n    VALUES ('tenant-created'), ('jsonb-contains')\n)\nSELECT CASE\n    WHEN kind = 'jsonb-contains' THEN 'gin'\n    ELSE 'btree'\nEND AS index_family\nFROM access_patterns;\n",[19,3232,3233,3245,3263,3267,3274,3293,3301,3312],{"__ignoreMap":40},[102,3234,3235,3237,3240,3243],{"class":104,"line":105},[102,3236,499],{"class":108},[102,3238,3239],{"class":125}," access_patterns(kind) ",[102,3241,3242],{"class":108},"AS",[102,3244,451],{"class":125},[102,3246,3247,3250,3252,3255,3258,3261],{"class":104,"line":119},[102,3248,3249],{"class":108},"    VALUES",[102,3251,132],{"class":125},[102,3253,3254],{"class":705},"'tenant-created'",[102,3256,3257],{"class":125},"), (",[102,3259,3260],{"class":705},"'jsonb-contains'",[102,3262,1306],{"class":125},[102,3264,3265],{"class":104,"line":141},[102,3266,1306],{"class":125},[102,3268,3269,3271],{"class":104,"line":148},[102,3270,157],{"class":108},[102,3272,3273],{"class":108}," CASE\n",[102,3275,3276,3279,3282,3284,3287,3290],{"class":104,"line":154},[102,3277,3278],{"class":108},"    WHEN",[102,3280,3281],{"class":125}," kind ",[102,3283,175],{"class":108},[102,3285,3286],{"class":705}," 'jsonb-contains'",[102,3288,3289],{"class":108}," THEN",[102,3291,3292],{"class":705}," 'gin'\n",[102,3294,3295,3298],{"class":104,"line":481},[102,3296,3297],{"class":108},"    ELSE",[102,3299,3300],{"class":705}," 'btree'\n",[102,3302,3303,3306,3309],{"class":104,"line":493},[102,3304,3305],{"class":108},"END",[102,3307,3308],{"class":108}," AS",[102,3310,3311],{"class":125}," index_family\n",[102,3313,3314,3316],{"class":104,"line":507},[102,3315,690],{"class":108},[102,3317,3318],{"class":125}," access_patterns;\n",[3194,3320,3321],{"v-slot:hint":40},[15,3322,3323],{},"B-tree - базовый выбор для equality\u002Frange\u002Forder, GIN часто нужен для containment по JSONB\u002Farrays\u002Ffull-text.",[47,3325],{},[50,3327,3329],{"id":3328},"полезные-официальные-разделы","Полезные официальные разделы",[86,3331,3332,3341,3348,3355,3362,3369],{},[70,3333,3334],{},[3335,3336,3340],"a",{"href":3337,"rel":3338},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Findexes.html",[3339],"nofollow","PostgreSQL Documentation: Indexes",[70,3342,3343],{},[3335,3344,3347],{"href":3345,"rel":3346},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fusing-explain.html",[3339],"PostgreSQL Documentation: Using EXPLAIN",[70,3349,3350],{},[3335,3351,3354],{"href":3352,"rel":3353},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fplanner-stats.html",[3339],"PostgreSQL Documentation: Statistics Used by the Planner",[70,3356,3357],{},[3335,3358,3361],{"href":3359,"rel":3360},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Findexes-index-only-scans.html",[3339],"PostgreSQL Documentation: Index-Only Scans",[70,3363,3364],{},[3335,3365,3368],{"href":3366,"rel":3367},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Findexes-partial.html",[3339],"PostgreSQL Documentation: Partial Indexes",[70,3370,3371],{},[3335,3372,3375],{"href":3373,"rel":3374},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Findexes-multicolumn.html",[3339],"PostgreSQL Documentation: Multicolumn Indexes",[3377,3378,3379],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}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 .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":40,"searchDepth":119,"depth":119,"links":3381},[3382,3383,3391,3397,3404,3415,3420,3421,3422,3423,3424],{"id":52,"depth":119,"text":53},{"id":198,"depth":119,"text":199,"children":3384},[3385,3386,3387,3388,3389,3390],{"id":203,"depth":141,"text":204},{"id":327,"depth":141,"text":328},{"id":386,"depth":141,"text":387},{"id":535,"depth":141,"text":536},{"id":564,"depth":141,"text":565},{"id":716,"depth":141,"text":717},{"id":773,"depth":119,"text":774,"children":3392},[3393,3394,3395,3396],{"id":777,"depth":141,"text":778},{"id":1029,"depth":141,"text":1030},{"id":1178,"depth":141,"text":1179},{"id":1274,"depth":141,"text":1275},{"id":1392,"depth":119,"text":1393,"children":3398},[3399,3400,3401,3402,3403],{"id":1596,"depth":141,"text":1597},{"id":1696,"depth":141,"text":1697},{"id":1703,"depth":141,"text":1704},{"id":1728,"depth":141,"text":1729},{"id":1741,"depth":141,"text":1742},{"id":1767,"depth":119,"text":1768,"children":3405},[3406,3407,3409,3410,3411,3413,3414],{"id":1948,"depth":141,"text":1949},{"id":1952,"depth":141,"text":3408},"Уберите SELECT *",{"id":1998,"depth":141,"text":1999},{"id":2092,"depth":141,"text":2093},{"id":2118,"depth":141,"text":3412},"Используйте ORDER BY ... LIMIT вместе с индексом",{"id":2198,"depth":141,"text":2199},{"id":2289,"depth":141,"text":2290},{"id":2386,"depth":119,"text":2387,"children":3416},[3417,3418,3419],{"id":2428,"depth":141,"text":2431},{"id":2556,"depth":141,"text":2557},{"id":2616,"depth":141,"text":2617},{"id":2669,"depth":119,"text":2670},{"id":3092,"depth":119,"text":3093},{"id":3150,"depth":119,"text":3151},{"id":3182,"depth":119,"text":3183},{"id":3328,"depth":119,"text":3329},1781022067103]