[{"data":1,"prerenderedAt":6537},["ShallowReactive",2],{"page-\u002Fsql\u002F29-common-pitfalls":3},{"id":4,"title":5,"body":6,"description":6530,"extension":6531,"meta":6532,"navigation":105,"path":6533,"seo":6534,"stem":6535,"__hash__":6536},"content\u002Fsql\u002F29-common-pitfalls.md","29 — Common Pitfalls & Idiomatic Fixes",{"type":7,"value":8,"toc":6436},"minimark",[9,13,26,31,41,135,143,280,288,366,378,564,572,657,665,762,766,920,928,1052,1056,1066,1235,1239,1357,1361,1540,1548,1650,1654,1726,1730,1738,1821,1829,1936,1944,2070,2080,2191,2195,2367,2371,2448,2452,2594,2598,2602,2756,2764,2964,2968,3046,3054,3157,3161,3165,3325,3329,3453,3457,3615,3619,3784,3788,3792,4035,4039,4216,4220,4407,4416,4520,4524,4528,4594,4598,4676,4680,4791,4795,4805,4867,4871,4969,4973,5038,5042,5046,5140,5151,5260,5264,5272,5477,5485,5573,5577,5581,5723,5733,5945,5949,5957,6090,6094,6097,6193,6432],[10,11,5],"h1",{"id":12},"_29-common-pitfalls-idiomatic-fixes",[14,15,16,17,21,22,25],"p",{},"40+ traps that catch SQL developers, organized by category. Each trap shows the ",[18,19,20],"strong",{},"wrong code",", explains the mechanism, and gives the ",[18,23,24],{},"idiomatic fix",".",[27,28,30],"h2",{"id":29},"null-traps","NULL Traps",[32,33,35,36,40],"h3",{"id":34},"_1-where-col-null-returns-nothing","1. ",[37,38,39],"code",{},"WHERE col = NULL"," Returns Nothing",[42,43,45],"code-wrapper",{"language":44},"sql",[46,47,51],"pre",{"className":48,"code":49,"language":44,"meta":50,"style":50},"language-sql shiki shiki-themes github-light github-dark","-- ❌ WRONG: NULL = NULL evaluates to UNKNOWN (not TRUE), so no rows match\nSELECT * FROM customers WHERE city = NULL;\n-- The = operator with NULL always yields UNKNOWN, which is filtered out\n\n-- ✅ RIGHT: IS NULL is the only NULL-safe comparison\nSELECT * FROM customers WHERE city IS NULL;\n","",[37,52,53,62,94,100,107,113],{"__ignoreMap":50},[54,55,58],"span",{"class":56,"line":57},"line",1,[54,59,61],{"class":60},"sdCPZ","-- ❌ WRONG: NULL = NULL evaluates to UNKNOWN (not TRUE), so no rows match\n",[54,63,65,69,72,75,79,82,85,88,91],{"class":56,"line":64},2,[54,66,68],{"class":67},"svdQ7","SELECT",[54,70,71],{"class":67}," *",[54,73,74],{"class":67}," FROM",[54,76,78],{"class":77},"ssxIu"," customers ",[54,80,81],{"class":67},"WHERE",[54,83,84],{"class":77}," city ",[54,86,87],{"class":67},"=",[54,89,90],{"class":67}," NULL",[54,92,93],{"class":77},";\n",[54,95,97],{"class":56,"line":96},3,[54,98,99],{"class":60},"-- The = operator with NULL always yields UNKNOWN, which is filtered out\n",[54,101,103],{"class":56,"line":102},4,[54,104,106],{"emptyLinePlaceholder":105},true,"\n",[54,108,110],{"class":56,"line":109},5,[54,111,112],{"class":60},"-- ✅ RIGHT: IS NULL is the only NULL-safe comparison\n",[54,114,116,118,120,122,124,126,128,131,133],{"class":56,"line":115},6,[54,117,68],{"class":67},[54,119,71],{"class":67},[54,121,74],{"class":67},[54,123,78],{"class":77},[54,125,81],{"class":67},[54,127,84],{"class":77},[54,129,130],{"class":67},"IS",[54,132,90],{"class":67},[54,134,93],{"class":77},[32,136,138,139,142],{"id":137},"_2-not-in-with-nulls-returns-zero-rows","2. ",[37,140,141],{},"NOT IN"," with NULLs Returns Zero Rows",[42,144,145],{"language":44},[46,146,148],{"className":48,"code":147,"language":44,"meta":50,"style":50},"-- ❌ WRONG: if the subquery returns ANY NULL, NOT IN returns zero rows\n-- Mechanism: NOT IN (a, b, NULL) = NOT (x = a OR x = b OR x = NULL)\n--   x = NULL → UNKNOWN, OR propagates UNKNOWN, NOT(UNKNOWN) → UNKNOWN → filtered\nSELECT * FROM customers c\nWHERE c.id NOT IN (SELECT customer_id FROM orders);\n-- If orders.customer_id has one NULL (a guest checkout?), the entire result is empty\n\n-- ✅ RIGHT: NOT EXISTS is NULL-safe — it uses a correlated predicate, not a value list\nSELECT * FROM customers c\nWHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);\n",[37,149,150,155,160,165,176,209,214,219,225,236],{"__ignoreMap":50},[54,151,152],{"class":56,"line":57},[54,153,154],{"class":60},"-- ❌ WRONG: if the subquery returns ANY NULL, NOT IN returns zero rows\n",[54,156,157],{"class":56,"line":64},[54,158,159],{"class":60},"-- Mechanism: NOT IN (a, b, NULL) = NOT (x = a OR x = b OR x = NULL)\n",[54,161,162],{"class":56,"line":96},[54,163,164],{"class":60},"--   x = NULL → UNKNOWN, OR propagates UNKNOWN, NOT(UNKNOWN) → UNKNOWN → filtered\n",[54,166,167,169,171,173],{"class":56,"line":102},[54,168,68],{"class":67},[54,170,71],{"class":67},[54,172,74],{"class":67},[54,174,175],{"class":77}," customers c\n",[54,177,178,180,184,186,189,192,195,198,200,203,206],{"class":56,"line":109},[54,179,81],{"class":67},[54,181,183],{"class":182},"snvgF"," c",[54,185,25],{"class":77},[54,187,188],{"class":182},"id",[54,190,191],{"class":67}," NOT",[54,193,194],{"class":67}," IN",[54,196,197],{"class":77}," (",[54,199,68],{"class":67},[54,201,202],{"class":77}," customer_id ",[54,204,205],{"class":67},"FROM",[54,207,208],{"class":77}," orders);\n",[54,210,211],{"class":56,"line":115},[54,212,213],{"class":60},"-- If orders.customer_id has one NULL (a guest checkout?), the entire result is empty\n",[54,215,217],{"class":56,"line":216},7,[54,218,106],{"emptyLinePlaceholder":105},[54,220,222],{"class":56,"line":221},8,[54,223,224],{"class":60},"-- ✅ RIGHT: NOT EXISTS is NULL-safe — it uses a correlated predicate, not a value list\n",[54,226,228,230,232,234],{"class":56,"line":227},9,[54,229,68],{"class":67},[54,231,71],{"class":67},[54,233,74],{"class":67},[54,235,175],{"class":77},[54,237,239,241,243,246,248,250,253,255,258,260,263,265,268,271,273,275,277],{"class":56,"line":238},10,[54,240,81],{"class":67},[54,242,191],{"class":67},[54,244,245],{"class":67}," EXISTS",[54,247,197],{"class":77},[54,249,68],{"class":67},[54,251,252],{"class":182}," 1",[54,254,74],{"class":67},[54,256,257],{"class":77}," orders o ",[54,259,81],{"class":67},[54,261,262],{"class":182}," o",[54,264,25],{"class":77},[54,266,267],{"class":182},"customer_id",[54,269,270],{"class":67}," =",[54,272,183],{"class":182},[54,274,25],{"class":77},[54,276,188],{"class":182},[54,278,279],{"class":77},");\n",[32,281,283,284,287],{"id":282},"_3-silently-drops-nulls","3. ",[37,285,286],{},"!="," Silently Drops NULLs",[42,289,290],{"language":44},[46,291,293],{"className":48,"code":292,"language":44,"meta":50,"style":50},"-- ❌ WRONG: city != 'NYC' excludes NULL-city rows (NULL != 'NYC' → UNKNOWN → dropped)\nSELECT * FROM customers WHERE city != 'NYC';\n-- You lose all customers with NULL city — they're not 'NYC', but they're also not != 'NYC'\n\n-- ✅ RIGHT: IS DISTINCT FROM treats NULL as a comparable distinct value\nSELECT * FROM customers WHERE city IS DISTINCT FROM 'NYC';\n-- NULL IS DISTINCT FROM 'NYC' → TRUE → included\n",[37,294,295,300,322,327,331,336,361],{"__ignoreMap":50},[54,296,297],{"class":56,"line":57},[54,298,299],{"class":60},"-- ❌ WRONG: city != 'NYC' excludes NULL-city rows (NULL != 'NYC' → UNKNOWN → dropped)\n",[54,301,302,304,306,308,310,312,314,316,320],{"class":56,"line":64},[54,303,68],{"class":67},[54,305,71],{"class":67},[54,307,74],{"class":67},[54,309,78],{"class":77},[54,311,81],{"class":67},[54,313,84],{"class":77},[54,315,286],{"class":67},[54,317,319],{"class":318},"sJ6F3"," 'NYC'",[54,321,93],{"class":77},[54,323,324],{"class":56,"line":96},[54,325,326],{"class":60},"-- You lose all customers with NULL city — they're not 'NYC', but they're also not != 'NYC'\n",[54,328,329],{"class":56,"line":102},[54,330,106],{"emptyLinePlaceholder":105},[54,332,333],{"class":56,"line":109},[54,334,335],{"class":60},"-- ✅ RIGHT: IS DISTINCT FROM treats NULL as a comparable distinct value\n",[54,337,338,340,342,344,346,348,350,352,355,357,359],{"class":56,"line":115},[54,339,68],{"class":67},[54,341,71],{"class":67},[54,343,74],{"class":67},[54,345,78],{"class":77},[54,347,81],{"class":67},[54,349,84],{"class":77},[54,351,130],{"class":67},[54,353,354],{"class":67}," DISTINCT",[54,356,74],{"class":67},[54,358,319],{"class":318},[54,360,93],{"class":77},[54,362,363],{"class":56,"line":216},[54,364,365],{"class":60},"-- NULL IS DISTINCT FROM 'NYC' → TRUE → included\n",[32,367,369,370,373,374,377],{"id":368},"_4-count-vs-countcol-in-a-left-join","4. ",[37,371,372],{},"COUNT(*)"," vs ",[37,375,376],{},"COUNT(col)"," in a LEFT JOIN",[42,379,380],{"language":44},[46,381,383],{"className":48,"code":382,"language":44,"meta":50,"style":50},"-- ❌ WRONG: COUNT(*) counts the NULL-extended row for unmatched LEFT JOIN rows as 1\n-- A customer with 0 orders: LEFT JOIN produces 1 row (customer + NULLs)\n-- COUNT(*) counts that row → reports 1 order, should be 0\nSELECT c.name, COUNT(*) AS order_count\nFROM customers c LEFT JOIN orders o ON c.id = o.customer_id\nGROUP BY c.name;\n-- A customer with zero orders shows order_count = 1 (the NULL row)\n\n-- ✅ RIGHT: COUNT(o.id) skips NULLs — counts only matched rows\nSELECT c.name, COUNT(o.id) AS order_count\nFROM customers c LEFT JOIN orders o ON c.id = o.customer_id\nGROUP BY c.name;\n-- A customer with zero orders: o.id is NULL → COUNT skips it → 0\n",[37,384,385,390,395,400,432,462,475,480,484,489,518,545,558],{"__ignoreMap":50},[54,386,387],{"class":56,"line":57},[54,388,389],{"class":60},"-- ❌ WRONG: COUNT(*) counts the NULL-extended row for unmatched LEFT JOIN rows as 1\n",[54,391,392],{"class":56,"line":64},[54,393,394],{"class":60},"-- A customer with 0 orders: LEFT JOIN produces 1 row (customer + NULLs)\n",[54,396,397],{"class":56,"line":96},[54,398,399],{"class":60},"-- COUNT(*) counts that row → reports 1 order, should be 0\n",[54,401,402,404,406,408,411,414,417,420,423,426,429],{"class":56,"line":102},[54,403,68],{"class":67},[54,405,183],{"class":182},[54,407,25],{"class":77},[54,409,410],{"class":182},"name",[54,412,413],{"class":77},", ",[54,415,416],{"class":182},"COUNT",[54,418,419],{"class":77},"(",[54,421,422],{"class":67},"*",[54,424,425],{"class":77},") ",[54,427,428],{"class":67},"AS",[54,430,431],{"class":77}," order_count\n",[54,433,434,436,439,442,444,447,449,451,453,455,457,459],{"class":56,"line":109},[54,435,205],{"class":67},[54,437,438],{"class":77}," customers c ",[54,440,441],{"class":67},"LEFT JOIN",[54,443,257],{"class":77},[54,445,446],{"class":67},"ON",[54,448,183],{"class":182},[54,450,25],{"class":77},[54,452,188],{"class":182},[54,454,270],{"class":67},[54,456,262],{"class":182},[54,458,25],{"class":77},[54,460,461],{"class":182},"customer_id\n",[54,463,464,467,469,471,473],{"class":56,"line":115},[54,465,466],{"class":67},"GROUP BY",[54,468,183],{"class":182},[54,470,25],{"class":77},[54,472,410],{"class":182},[54,474,93],{"class":77},[54,476,477],{"class":56,"line":216},[54,478,479],{"class":60},"-- A customer with zero orders shows order_count = 1 (the NULL row)\n",[54,481,482],{"class":56,"line":221},[54,483,106],{"emptyLinePlaceholder":105},[54,485,486],{"class":56,"line":227},[54,487,488],{"class":60},"-- ✅ RIGHT: COUNT(o.id) skips NULLs — counts only matched rows\n",[54,490,491,493,495,497,499,501,503,505,508,510,512,514,516],{"class":56,"line":238},[54,492,68],{"class":67},[54,494,183],{"class":182},[54,496,25],{"class":77},[54,498,410],{"class":182},[54,500,413],{"class":77},[54,502,416],{"class":182},[54,504,419],{"class":77},[54,506,507],{"class":182},"o",[54,509,25],{"class":77},[54,511,188],{"class":182},[54,513,425],{"class":77},[54,515,428],{"class":67},[54,517,431],{"class":77},[54,519,521,523,525,527,529,531,533,535,537,539,541,543],{"class":56,"line":520},11,[54,522,205],{"class":67},[54,524,438],{"class":77},[54,526,441],{"class":67},[54,528,257],{"class":77},[54,530,446],{"class":67},[54,532,183],{"class":182},[54,534,25],{"class":77},[54,536,188],{"class":182},[54,538,270],{"class":67},[54,540,262],{"class":182},[54,542,25],{"class":77},[54,544,461],{"class":182},[54,546,548,550,552,554,556],{"class":56,"line":547},12,[54,549,466],{"class":67},[54,551,183],{"class":182},[54,553,25],{"class":77},[54,555,410],{"class":182},[54,557,93],{"class":77},[54,559,561],{"class":56,"line":560},13,[54,562,563],{"class":60},"-- A customer with zero orders: o.id is NULL → COUNT skips it → 0\n",[32,565,567,568,571],{"id":566},"_5-avg-ignores-nulls-doesnt-average-them-as-0","5. ",[37,569,570],{},"AVG"," Ignores NULLs, Doesn't Average Them as 0",[42,573,574],{"language":44},[46,575,577],{"className":48,"code":576,"language":44,"meta":50,"style":50},"-- ❌ WRONG: expecting AVG([10, NULL, 20]) = 10 (treating NULL as 0)\n-- AVG skips NULLs → AVG(10, 20) = 15 (divides by 2, not 3)\nSELECT AVG(amount) FROM orders;\n-- If 30% of rows have NULL amount, AVG is biased high (only non-NULL rows counted)\n\n-- ✅ RIGHT: if NULL means 0, COALESCE before averaging\nSELECT AVG(COALESCE(amount, 0)) FROM orders;\n-- AVG(10, 0, 20) = 10 — NULLs treated as 0, denominator includes all rows\n-- ⚠️ But ask: does NULL semantically mean 0, or \"unknown\"? If \"unknown\",\n-- skipping it (default AVG behavior) is correct — don't COALESCE.\n",[37,578,579,584,589,604,609,613,618,642,647,652],{"__ignoreMap":50},[54,580,581],{"class":56,"line":57},[54,582,583],{"class":60},"-- ❌ WRONG: expecting AVG([10, NULL, 20]) = 10 (treating NULL as 0)\n",[54,585,586],{"class":56,"line":64},[54,587,588],{"class":60},"-- AVG skips NULLs → AVG(10, 20) = 15 (divides by 2, not 3)\n",[54,590,591,593,596,599,601],{"class":56,"line":96},[54,592,68],{"class":67},[54,594,595],{"class":182}," AVG",[54,597,598],{"class":77},"(amount) ",[54,600,205],{"class":67},[54,602,603],{"class":77}," orders;\n",[54,605,606],{"class":56,"line":102},[54,607,608],{"class":60},"-- If 30% of rows have NULL amount, AVG is biased high (only non-NULL rows counted)\n",[54,610,611],{"class":56,"line":109},[54,612,106],{"emptyLinePlaceholder":105},[54,614,615],{"class":56,"line":115},[54,616,617],{"class":60},"-- ✅ RIGHT: if NULL means 0, COALESCE before averaging\n",[54,619,620,622,624,626,629,632,635,638,640],{"class":56,"line":216},[54,621,68],{"class":67},[54,623,595],{"class":182},[54,625,419],{"class":77},[54,627,628],{"class":182},"COALESCE",[54,630,631],{"class":77},"(amount, ",[54,633,634],{"class":182},"0",[54,636,637],{"class":77},")) ",[54,639,205],{"class":67},[54,641,603],{"class":77},[54,643,644],{"class":56,"line":221},[54,645,646],{"class":60},"-- AVG(10, 0, 20) = 10 — NULLs treated as 0, denominator includes all rows\n",[54,648,649],{"class":56,"line":227},[54,650,651],{"class":60},"-- ⚠️ But ask: does NULL semantically mean 0, or \"unknown\"? If \"unknown\",\n",[54,653,654],{"class":56,"line":238},[54,655,656],{"class":60},"-- skipping it (default AVG behavior) is correct — don't COALESCE.\n",[32,658,660,661,664],{"id":659},"_6-sum-of-no-rows-is-null-not-0","6. ",[37,662,663],{},"SUM"," of No Rows is NULL, Not 0",[42,666,667],{"language":44},[46,668,670],{"className":48,"code":669,"language":44,"meta":50,"style":50},"-- ❌ WRONG: expecting SUM to return 0 for an empty group\nSELECT SUM(amount) AS total FROM orders WHERE customer_id = 99999;\n-- Returns NULL, not 0 — SUM of an empty set is NULL\n\n-- ✅ RIGHT: COALESCE to 0 for downstream code that expects a number\nSELECT COALESCE(SUM(amount), 0) AS total FROM orders WHERE customer_id = 99999;\n-- Returns 0 — downstream code won't crash on NULL arithmetic\n",[37,671,672,677,707,712,716,721,757],{"__ignoreMap":50},[54,673,674],{"class":56,"line":57},[54,675,676],{"class":60},"-- ❌ WRONG: expecting SUM to return 0 for an empty group\n",[54,678,679,681,684,686,688,691,693,696,698,700,702,705],{"class":56,"line":64},[54,680,68],{"class":67},[54,682,683],{"class":182}," SUM",[54,685,598],{"class":77},[54,687,428],{"class":67},[54,689,690],{"class":77}," total ",[54,692,205],{"class":67},[54,694,695],{"class":77}," orders ",[54,697,81],{"class":67},[54,699,202],{"class":77},[54,701,87],{"class":67},[54,703,704],{"class":182}," 99999",[54,706,93],{"class":77},[54,708,709],{"class":56,"line":96},[54,710,711],{"class":60},"-- Returns NULL, not 0 — SUM of an empty set is NULL\n",[54,713,714],{"class":56,"line":102},[54,715,106],{"emptyLinePlaceholder":105},[54,717,718],{"class":56,"line":109},[54,719,720],{"class":60},"-- ✅ RIGHT: COALESCE to 0 for downstream code that expects a number\n",[54,722,723,725,728,730,732,735,737,739,741,743,745,747,749,751,753,755],{"class":56,"line":115},[54,724,68],{"class":67},[54,726,727],{"class":182}," COALESCE",[54,729,419],{"class":77},[54,731,663],{"class":182},[54,733,734],{"class":77},"(amount), ",[54,736,634],{"class":182},[54,738,425],{"class":77},[54,740,428],{"class":67},[54,742,690],{"class":77},[54,744,205],{"class":67},[54,746,695],{"class":77},[54,748,81],{"class":67},[54,750,202],{"class":77},[54,752,87],{"class":67},[54,754,704],{"class":182},[54,756,93],{"class":77},[54,758,759],{"class":56,"line":216},[54,760,761],{"class":60},"-- Returns 0 — downstream code won't crash on NULL arithmetic\n",[32,763,765],{"id":764},"_7-string-concatenation-with-null-yields-null","7. String Concatenation with NULL Yields NULL",[42,767,768],{"language":44},[46,769,771],{"className":48,"code":770,"language":44,"meta":50,"style":50},"-- ❌ WRONG: 'hello' || NULL = NULL — the entire string is destroyed\nSELECT first_name || ' ' || middle_name || ' ' || last_name AS full_name\nFROM customers;\n-- If middle_name is NULL, full_name is NULL for every customer without a middle name\n\n-- ✅ RIGHT: CONCAT ignores NULLs, or COALESCE each column\nSELECT CONCAT(first_name, ' ', middle_name, ' ', last_name) AS full_name\nFROM customers;\n-- CONCAT('John', ' ', NULL, ' ', 'Doe') = 'John  Doe' (NULL skipped, spaces preserved)\n-- For tighter control: COALESCE each part\nSELECT first_name || COALESCE(' ' || middle_name, '') || ' ' || last_name AS full_name\nFROM customers;\n-- 'John' || '' || ' Doe' = 'John Doe' (no double space when middle_name is NULL)\n",[37,772,773,778,811,818,823,827,832,857,863,868,873,909,915],{"__ignoreMap":50},[54,774,775],{"class":56,"line":57},[54,776,777],{"class":60},"-- ❌ WRONG: 'hello' || NULL = NULL — the entire string is destroyed\n",[54,779,780,782,785,788,791,794,797,799,801,803,806,808],{"class":56,"line":64},[54,781,68],{"class":67},[54,783,784],{"class":77}," first_name ",[54,786,787],{"class":67},"||",[54,789,790],{"class":318}," ' '",[54,792,793],{"class":67}," ||",[54,795,796],{"class":77}," middle_name ",[54,798,787],{"class":67},[54,800,790],{"class":318},[54,802,793],{"class":67},[54,804,805],{"class":77}," last_name ",[54,807,428],{"class":67},[54,809,810],{"class":77}," full_name\n",[54,812,813,815],{"class":56,"line":96},[54,814,205],{"class":67},[54,816,817],{"class":77}," customers;\n",[54,819,820],{"class":56,"line":102},[54,821,822],{"class":60},"-- If middle_name is NULL, full_name is NULL for every customer without a middle name\n",[54,824,825],{"class":56,"line":109},[54,826,106],{"emptyLinePlaceholder":105},[54,828,829],{"class":56,"line":115},[54,830,831],{"class":60},"-- ✅ RIGHT: CONCAT ignores NULLs, or COALESCE each column\n",[54,833,834,836,839,842,845,848,850,853,855],{"class":56,"line":216},[54,835,68],{"class":67},[54,837,838],{"class":182}," CONCAT",[54,840,841],{"class":77},"(first_name, ",[54,843,844],{"class":318},"' '",[54,846,847],{"class":77},", middle_name, ",[54,849,844],{"class":318},[54,851,852],{"class":77},", last_name) ",[54,854,428],{"class":67},[54,856,810],{"class":77},[54,858,859,861],{"class":56,"line":221},[54,860,205],{"class":67},[54,862,817],{"class":77},[54,864,865],{"class":56,"line":227},[54,866,867],{"class":60},"-- CONCAT('John', ' ', NULL, ' ', 'Doe') = 'John  Doe' (NULL skipped, spaces preserved)\n",[54,869,870],{"class":56,"line":238},[54,871,872],{"class":60},"-- For tighter control: COALESCE each part\n",[54,874,875,877,879,881,883,885,887,889,892,895,897,899,901,903,905,907],{"class":56,"line":520},[54,876,68],{"class":67},[54,878,784],{"class":77},[54,880,787],{"class":67},[54,882,727],{"class":182},[54,884,419],{"class":77},[54,886,844],{"class":318},[54,888,793],{"class":67},[54,890,891],{"class":77}," middle_name, ",[54,893,894],{"class":318},"''",[54,896,425],{"class":77},[54,898,787],{"class":67},[54,900,790],{"class":318},[54,902,793],{"class":67},[54,904,805],{"class":77},[54,906,428],{"class":67},[54,908,810],{"class":77},[54,910,911,913],{"class":56,"line":547},[54,912,205],{"class":67},[54,914,817],{"class":77},[54,916,917],{"class":56,"line":560},[54,918,919],{"class":60},"-- 'John' || '' || ' Doe' = 'John Doe' (no double space when middle_name is NULL)\n",[32,921,923,924,927],{"id":922},"_8-check-constraint-allows-nulls","8. ",[37,925,926],{},"CHECK"," Constraint Allows NULLs",[42,929,930],{"language":44},[46,931,933],{"className":48,"code":932,"language":44,"meta":50,"style":50},"-- ❌ WRONG: CHECK (price > 0) passes for price = NULL\n-- CHECK evaluates to UNKNOWN for NULL → UNKNOWN passes (only FALSE fails)\nCREATE TABLE products (price NUMERIC CHECK (price > 0));\nINSERT INTO products VALUES (NULL);  -- succeeds! NULL > 0 → UNKNOWN → passes\n\n-- ✅ RIGHT: add NOT NULL constraint separately, or combine in CHECK\nCREATE TABLE products (\n  price NUMERIC NOT NULL CHECK (price > 0)  -- NOT NULL + CHECK\n);\n-- Or: CHECK (price IS NOT NULL AND price > 0) — but NOT NULL column constraint is cleaner\n",[37,934,935,940,945,977,999,1003,1008,1019,1043,1047],{"__ignoreMap":50},[54,936,937],{"class":56,"line":57},[54,938,939],{"class":60},"-- ❌ WRONG: CHECK (price > 0) passes for price = NULL\n",[54,941,942],{"class":56,"line":64},[54,943,944],{"class":60},"-- CHECK evaluates to UNKNOWN for NULL → UNKNOWN passes (only FALSE fails)\n",[54,946,947,950,953,957,960,963,966,968,971,974],{"class":56,"line":96},[54,948,949],{"class":67},"CREATE",[54,951,952],{"class":67}," TABLE",[54,954,956],{"class":955},"sIsaT"," products",[54,958,959],{"class":77}," (price ",[54,961,962],{"class":67},"NUMERIC",[54,964,965],{"class":67}," CHECK",[54,967,959],{"class":77},[54,969,970],{"class":67},">",[54,972,973],{"class":182}," 0",[54,975,976],{"class":77},"));\n",[54,978,979,982,985,988,990,993,996],{"class":56,"line":102},[54,980,981],{"class":67},"INSERT INTO",[54,983,984],{"class":77}," products ",[54,986,987],{"class":67},"VALUES",[54,989,197],{"class":77},[54,991,992],{"class":67},"NULL",[54,994,995],{"class":77},");  ",[54,997,998],{"class":60},"-- succeeds! NULL > 0 → UNKNOWN → passes\n",[54,1000,1001],{"class":56,"line":109},[54,1002,106],{"emptyLinePlaceholder":105},[54,1004,1005],{"class":56,"line":115},[54,1006,1007],{"class":60},"-- ✅ RIGHT: add NOT NULL constraint separately, or combine in CHECK\n",[54,1009,1010,1012,1014,1016],{"class":56,"line":216},[54,1011,949],{"class":67},[54,1013,952],{"class":67},[54,1015,956],{"class":955},[54,1017,1018],{"class":77}," (\n",[54,1020,1021,1024,1026,1029,1031,1033,1035,1037,1040],{"class":56,"line":221},[54,1022,1023],{"class":77},"  price ",[54,1025,962],{"class":67},[54,1027,1028],{"class":67}," NOT NULL",[54,1030,965],{"class":67},[54,1032,959],{"class":77},[54,1034,970],{"class":67},[54,1036,973],{"class":182},[54,1038,1039],{"class":77},")  ",[54,1041,1042],{"class":60},"-- NOT NULL + CHECK\n",[54,1044,1045],{"class":56,"line":227},[54,1046,279],{"class":77},[54,1048,1049],{"class":56,"line":238},[54,1050,1051],{"class":60},"-- Or: CHECK (price IS NOT NULL AND price > 0) — but NOT NULL column constraint is cleaner\n",[27,1053,1055],{"id":1054},"join-traps","JOIN Traps",[32,1057,1059,1060,1062,1063,1065],{"id":1058},"_9-left-join-where-on-the-right-table-demotes-to-inner-join","9. ",[37,1061,441],{}," + ",[37,1064,81],{}," on the Right Table Demotes to INNER JOIN",[42,1067,1068],{"language":44},[46,1069,1071],{"className":48,"code":1070,"language":44,"meta":50,"style":50},"-- ❌ WRONG: WHERE o.amount > 100 drops customers with no orders\n-- Unmatched LEFT JOIN rows have o.* = NULL; NULL > 100 → UNKNOWN → filtered\nSELECT c.name, o.amount\nFROM customers c\nLEFT JOIN orders o ON c.id = o.customer_id\nWHERE o.amount > 100;\n-- This is effectively an INNER JOIN — customers with 0 orders or orders ≤ 100 are gone\n\n-- ✅ RIGHT: put right-table filters in ON (not WHERE)\nSELECT c.name, o.amount\nFROM customers c\nLEFT JOIN orders o ON c.id = o.customer_id AND o.amount > 100;\n-- ON is evaluated during the join; WHERE is evaluated after\n-- Customers with no qualifying orders still appear (with NULL amount)\n",[37,1072,1073,1078,1083,1102,1108,1130,1149,1154,1158,1163,1181,1187,1224,1229],{"__ignoreMap":50},[54,1074,1075],{"class":56,"line":57},[54,1076,1077],{"class":60},"-- ❌ WRONG: WHERE o.amount > 100 drops customers with no orders\n",[54,1079,1080],{"class":56,"line":64},[54,1081,1082],{"class":60},"-- Unmatched LEFT JOIN rows have o.* = NULL; NULL > 100 → UNKNOWN → filtered\n",[54,1084,1085,1087,1089,1091,1093,1095,1097,1099],{"class":56,"line":96},[54,1086,68],{"class":67},[54,1088,183],{"class":182},[54,1090,25],{"class":77},[54,1092,410],{"class":182},[54,1094,413],{"class":77},[54,1096,507],{"class":182},[54,1098,25],{"class":77},[54,1100,1101],{"class":182},"amount\n",[54,1103,1104,1106],{"class":56,"line":102},[54,1105,205],{"class":67},[54,1107,175],{"class":77},[54,1109,1110,1112,1114,1116,1118,1120,1122,1124,1126,1128],{"class":56,"line":109},[54,1111,441],{"class":67},[54,1113,257],{"class":77},[54,1115,446],{"class":67},[54,1117,183],{"class":182},[54,1119,25],{"class":77},[54,1121,188],{"class":182},[54,1123,270],{"class":67},[54,1125,262],{"class":182},[54,1127,25],{"class":77},[54,1129,461],{"class":182},[54,1131,1132,1134,1136,1138,1141,1144,1147],{"class":56,"line":115},[54,1133,81],{"class":67},[54,1135,262],{"class":182},[54,1137,25],{"class":77},[54,1139,1140],{"class":182},"amount",[54,1142,1143],{"class":67}," >",[54,1145,1146],{"class":182}," 100",[54,1148,93],{"class":77},[54,1150,1151],{"class":56,"line":216},[54,1152,1153],{"class":60},"-- This is effectively an INNER JOIN — customers with 0 orders or orders ≤ 100 are gone\n",[54,1155,1156],{"class":56,"line":221},[54,1157,106],{"emptyLinePlaceholder":105},[54,1159,1160],{"class":56,"line":227},[54,1161,1162],{"class":60},"-- ✅ RIGHT: put right-table filters in ON (not WHERE)\n",[54,1164,1165,1167,1169,1171,1173,1175,1177,1179],{"class":56,"line":238},[54,1166,68],{"class":67},[54,1168,183],{"class":182},[54,1170,25],{"class":77},[54,1172,410],{"class":182},[54,1174,413],{"class":77},[54,1176,507],{"class":182},[54,1178,25],{"class":77},[54,1180,1101],{"class":182},[54,1182,1183,1185],{"class":56,"line":520},[54,1184,205],{"class":67},[54,1186,175],{"class":77},[54,1188,1189,1191,1193,1195,1197,1199,1201,1203,1205,1207,1209,1212,1214,1216,1218,1220,1222],{"class":56,"line":547},[54,1190,441],{"class":67},[54,1192,257],{"class":77},[54,1194,446],{"class":67},[54,1196,183],{"class":182},[54,1198,25],{"class":77},[54,1200,188],{"class":182},[54,1202,270],{"class":67},[54,1204,262],{"class":182},[54,1206,25],{"class":77},[54,1208,267],{"class":182},[54,1210,1211],{"class":67}," AND",[54,1213,262],{"class":182},[54,1215,25],{"class":77},[54,1217,1140],{"class":182},[54,1219,1143],{"class":67},[54,1221,1146],{"class":182},[54,1223,93],{"class":77},[54,1225,1226],{"class":56,"line":560},[54,1227,1228],{"class":60},"-- ON is evaluated during the join; WHERE is evaluated after\n",[54,1230,1232],{"class":56,"line":1231},14,[54,1233,1234],{"class":60},"-- Customers with no qualifying orders still appear (with NULL amount)\n",[32,1236,1238],{"id":1237},"_10-joining-on-null-doesnt-match","10. Joining on NULL Doesn't Match",[42,1240,1241],{"language":44},[46,1242,1244],{"className":48,"code":1243,"language":44,"meta":50,"style":50},"-- ❌ WRONG: NULL = NULL → UNKNOWN → not a match\nSELECT * FROM a JOIN b ON a.col = b.col;\n-- Rows where both a.col and b.col are NULL don't join — NULL doesn't equal NULL\n\n-- ✅ RIGHT: IS NOT DISTINCT FROM (NULL-safe equality)\nSELECT * FROM a JOIN b ON a.col IS NOT DISTINCT FROM b.col;\n-- NULL IS NOT DISTINCT FROM NULL → TRUE → matches\n-- ⚠️ But: this can't use a regular B-tree index → full scan\n-- Better: fix the schema — avoid NULL join keys, use a sentinel value or NOT NULL\n",[37,1245,1246,1251,1289,1294,1298,1303,1342,1347,1352],{"__ignoreMap":50},[54,1247,1248],{"class":56,"line":57},[54,1249,1250],{"class":60},"-- ❌ WRONG: NULL = NULL → UNKNOWN → not a match\n",[54,1252,1253,1255,1257,1259,1262,1265,1268,1270,1273,1275,1278,1280,1283,1285,1287],{"class":56,"line":64},[54,1254,68],{"class":67},[54,1256,71],{"class":67},[54,1258,74],{"class":67},[54,1260,1261],{"class":77}," a ",[54,1263,1264],{"class":67},"JOIN",[54,1266,1267],{"class":77}," b ",[54,1269,446],{"class":67},[54,1271,1272],{"class":182}," a",[54,1274,25],{"class":77},[54,1276,1277],{"class":182},"col",[54,1279,270],{"class":67},[54,1281,1282],{"class":182}," b",[54,1284,25],{"class":77},[54,1286,1277],{"class":182},[54,1288,93],{"class":77},[54,1290,1291],{"class":56,"line":96},[54,1292,1293],{"class":60},"-- Rows where both a.col and b.col are NULL don't join — NULL doesn't equal NULL\n",[54,1295,1296],{"class":56,"line":102},[54,1297,106],{"emptyLinePlaceholder":105},[54,1299,1300],{"class":56,"line":109},[54,1301,1302],{"class":60},"-- ✅ RIGHT: IS NOT DISTINCT FROM (NULL-safe equality)\n",[54,1304,1305,1307,1309,1311,1313,1315,1317,1319,1321,1323,1325,1328,1330,1332,1334,1336,1338,1340],{"class":56,"line":115},[54,1306,68],{"class":67},[54,1308,71],{"class":67},[54,1310,74],{"class":67},[54,1312,1261],{"class":77},[54,1314,1264],{"class":67},[54,1316,1267],{"class":77},[54,1318,446],{"class":67},[54,1320,1272],{"class":182},[54,1322,25],{"class":77},[54,1324,1277],{"class":182},[54,1326,1327],{"class":67}," IS",[54,1329,191],{"class":67},[54,1331,354],{"class":67},[54,1333,74],{"class":67},[54,1335,1282],{"class":182},[54,1337,25],{"class":77},[54,1339,1277],{"class":182},[54,1341,93],{"class":77},[54,1343,1344],{"class":56,"line":216},[54,1345,1346],{"class":60},"-- NULL IS NOT DISTINCT FROM NULL → TRUE → matches\n",[54,1348,1349],{"class":56,"line":221},[54,1350,1351],{"class":60},"-- ⚠️ But: this can't use a regular B-tree index → full scan\n",[54,1353,1354],{"class":56,"line":227},[54,1355,1356],{"class":60},"-- Better: fix the schema — avoid NULL join keys, use a sentinel value or NOT NULL\n",[32,1358,1360],{"id":1359},"_11-row-multiplication-from-a-many-join","11. Row Multiplication from a \"Many\" Join",[42,1362,1363],{"language":44},[46,1364,1366],{"className":48,"code":1365,"language":44,"meta":50,"style":50},"-- ❌ WRONG: joining customers to orders returns one row per order, not per customer\nSELECT c.name FROM customers c JOIN orders o ON c.id = o.customer_id;\n-- A customer with 100 orders appears 100 times in the result\n\n-- ✅ RIGHT: use DISTINCT or aggregate if you want one row per customer\nSELECT DISTINCT c.name FROM customers c JOIN orders o ON c.id = o.customer_id;\n-- Or aggregate:\nSELECT c.name, COUNT(o.id) AS order_count\nFROM customers c LEFT JOIN orders o ON c.id = o.customer_id\nGROUP BY c.id, c.name;\n",[37,1367,1368,1373,1409,1414,1418,1423,1460,1465,1493,1519],{"__ignoreMap":50},[54,1369,1370],{"class":56,"line":57},[54,1371,1372],{"class":60},"-- ❌ WRONG: joining customers to orders returns one row per order, not per customer\n",[54,1374,1375,1377,1379,1381,1383,1385,1387,1389,1391,1393,1395,1397,1399,1401,1403,1405,1407],{"class":56,"line":64},[54,1376,68],{"class":67},[54,1378,183],{"class":182},[54,1380,25],{"class":77},[54,1382,410],{"class":182},[54,1384,74],{"class":67},[54,1386,438],{"class":77},[54,1388,1264],{"class":67},[54,1390,257],{"class":77},[54,1392,446],{"class":67},[54,1394,183],{"class":182},[54,1396,25],{"class":77},[54,1398,188],{"class":182},[54,1400,270],{"class":67},[54,1402,262],{"class":182},[54,1404,25],{"class":77},[54,1406,267],{"class":182},[54,1408,93],{"class":77},[54,1410,1411],{"class":56,"line":96},[54,1412,1413],{"class":60},"-- A customer with 100 orders appears 100 times in the result\n",[54,1415,1416],{"class":56,"line":102},[54,1417,106],{"emptyLinePlaceholder":105},[54,1419,1420],{"class":56,"line":109},[54,1421,1422],{"class":60},"-- ✅ RIGHT: use DISTINCT or aggregate if you want one row per customer\n",[54,1424,1425,1428,1430,1432,1434,1436,1438,1440,1442,1444,1446,1448,1450,1452,1454,1456,1458],{"class":56,"line":115},[54,1426,1427],{"class":67},"SELECT DISTINCT",[54,1429,183],{"class":182},[54,1431,25],{"class":77},[54,1433,410],{"class":182},[54,1435,74],{"class":67},[54,1437,438],{"class":77},[54,1439,1264],{"class":67},[54,1441,257],{"class":77},[54,1443,446],{"class":67},[54,1445,183],{"class":182},[54,1447,25],{"class":77},[54,1449,188],{"class":182},[54,1451,270],{"class":67},[54,1453,262],{"class":182},[54,1455,25],{"class":77},[54,1457,267],{"class":182},[54,1459,93],{"class":77},[54,1461,1462],{"class":56,"line":216},[54,1463,1464],{"class":60},"-- Or aggregate:\n",[54,1466,1467,1469,1471,1473,1475,1477,1479,1481,1483,1485,1487,1489,1491],{"class":56,"line":221},[54,1468,68],{"class":67},[54,1470,183],{"class":182},[54,1472,25],{"class":77},[54,1474,410],{"class":182},[54,1476,413],{"class":77},[54,1478,416],{"class":182},[54,1480,419],{"class":77},[54,1482,507],{"class":182},[54,1484,25],{"class":77},[54,1486,188],{"class":182},[54,1488,425],{"class":77},[54,1490,428],{"class":67},[54,1492,431],{"class":77},[54,1494,1495,1497,1499,1501,1503,1505,1507,1509,1511,1513,1515,1517],{"class":56,"line":227},[54,1496,205],{"class":67},[54,1498,438],{"class":77},[54,1500,441],{"class":67},[54,1502,257],{"class":77},[54,1504,446],{"class":67},[54,1506,183],{"class":182},[54,1508,25],{"class":77},[54,1510,188],{"class":182},[54,1512,270],{"class":67},[54,1514,262],{"class":182},[54,1516,25],{"class":77},[54,1518,461],{"class":182},[54,1520,1521,1523,1525,1527,1529,1531,1534,1536,1538],{"class":56,"line":238},[54,1522,466],{"class":67},[54,1524,183],{"class":182},[54,1526,25],{"class":77},[54,1528,188],{"class":182},[54,1530,413],{"class":77},[54,1532,1533],{"class":182},"c",[54,1535,25],{"class":77},[54,1537,410],{"class":182},[54,1539,93],{"class":77},[32,1541,1543,1544,1547],{"id":1542},"_12-natural-join-joins-on-all-same-named-columns","12. ",[37,1545,1546],{},"NATURAL JOIN"," Joins on All Same-Named Columns",[42,1549,1550],{"language":44},[46,1551,1553],{"className":48,"code":1552,"language":44,"meta":50,"style":50},"-- ❌ WRONG: NATURAL JOIN silently joins on ALL same-named columns\nSELECT * FROM customers NATURAL JOIN orders;\n-- If both tables have `id`, it joins on `id` — but customer.id ≠ orders.id!\n-- Adding a new same-named column changes the join behavior silently\n\n-- ✅ RIGHT: never use NATURAL JOIN. Use explicit ON or USING\nSELECT * FROM customers c JOIN orders o ON c.id = o.customer_id;\n-- Or USING (when the column is the same name and same meaning):\nSELECT * FROM customers JOIN orders USING (customer_id);\n",[37,1554,1555,1560,1574,1579,1584,1588,1593,1625,1630],{"__ignoreMap":50},[54,1556,1557],{"class":56,"line":57},[54,1558,1559],{"class":60},"-- ❌ WRONG: NATURAL JOIN silently joins on ALL same-named columns\n",[54,1561,1562,1564,1566,1568,1570,1572],{"class":56,"line":64},[54,1563,68],{"class":67},[54,1565,71],{"class":67},[54,1567,74],{"class":67},[54,1569,78],{"class":77},[54,1571,1546],{"class":67},[54,1573,603],{"class":77},[54,1575,1576],{"class":56,"line":96},[54,1577,1578],{"class":60},"-- If both tables have `id`, it joins on `id` — but customer.id ≠ orders.id!\n",[54,1580,1581],{"class":56,"line":102},[54,1582,1583],{"class":60},"-- Adding a new same-named column changes the join behavior silently\n",[54,1585,1586],{"class":56,"line":109},[54,1587,106],{"emptyLinePlaceholder":105},[54,1589,1590],{"class":56,"line":115},[54,1591,1592],{"class":60},"-- ✅ RIGHT: never use NATURAL JOIN. Use explicit ON or USING\n",[54,1594,1595,1597,1599,1601,1603,1605,1607,1609,1611,1613,1615,1617,1619,1621,1623],{"class":56,"line":216},[54,1596,68],{"class":67},[54,1598,71],{"class":67},[54,1600,74],{"class":67},[54,1602,438],{"class":77},[54,1604,1264],{"class":67},[54,1606,257],{"class":77},[54,1608,446],{"class":67},[54,1610,183],{"class":182},[54,1612,25],{"class":77},[54,1614,188],{"class":182},[54,1616,270],{"class":67},[54,1618,262],{"class":182},[54,1620,25],{"class":77},[54,1622,267],{"class":182},[54,1624,93],{"class":77},[54,1626,1627],{"class":56,"line":221},[54,1628,1629],{"class":60},"-- Or USING (when the column is the same name and same meaning):\n",[54,1631,1632,1634,1636,1638,1640,1642,1644,1647],{"class":56,"line":227},[54,1633,68],{"class":67},[54,1635,71],{"class":67},[54,1637,74],{"class":67},[54,1639,78],{"class":77},[54,1641,1264],{"class":67},[54,1643,695],{"class":77},[54,1645,1646],{"class":67},"USING",[54,1648,1649],{"class":77}," (customer_id);\n",[32,1651,1653],{"id":1652},"_13-accidental-cross-join","13. Accidental CROSS JOIN",[42,1655,1656],{"language":44},[46,1657,1659],{"className":48,"code":1658,"language":44,"meta":50,"style":50},"-- ❌ WRONG: missing ON clause creates a Cartesian product\nSELECT * FROM customers, orders;  -- N × M rows (every customer × every order)\n-- If customers has 10K rows and orders has 1M → 10 billion rows\n\n-- ✅ RIGHT: always specify the join condition\nSELECT * FROM customers c JOIN orders o ON c.id = o.customer_id;\n",[37,1660,1661,1666,1680,1685,1689,1694],{"__ignoreMap":50},[54,1662,1663],{"class":56,"line":57},[54,1664,1665],{"class":60},"-- ❌ WRONG: missing ON clause creates a Cartesian product\n",[54,1667,1668,1670,1672,1674,1677],{"class":56,"line":64},[54,1669,68],{"class":67},[54,1671,71],{"class":67},[54,1673,74],{"class":67},[54,1675,1676],{"class":77}," customers, orders;  ",[54,1678,1679],{"class":60},"-- N × M rows (every customer × every order)\n",[54,1681,1682],{"class":56,"line":96},[54,1683,1684],{"class":60},"-- If customers has 10K rows and orders has 1M → 10 billion rows\n",[54,1686,1687],{"class":56,"line":102},[54,1688,106],{"emptyLinePlaceholder":105},[54,1690,1691],{"class":56,"line":109},[54,1692,1693],{"class":60},"-- ✅ RIGHT: always specify the join condition\n",[54,1695,1696,1698,1700,1702,1704,1706,1708,1710,1712,1714,1716,1718,1720,1722,1724],{"class":56,"line":115},[54,1697,68],{"class":67},[54,1699,71],{"class":67},[54,1701,74],{"class":67},[54,1703,438],{"class":77},[54,1705,1264],{"class":67},[54,1707,257],{"class":77},[54,1709,446],{"class":67},[54,1711,183],{"class":182},[54,1713,25],{"class":77},[54,1715,188],{"class":182},[54,1717,270],{"class":67},[54,1719,262],{"class":182},[54,1721,25],{"class":77},[54,1723,267],{"class":182},[54,1725,93],{"class":77},[27,1727,1729],{"id":1728},"performance-traps","Performance Traps",[32,1731,1733,1734,1737],{"id":1732},"_14-select-in-production","14. ",[37,1735,1736],{},"SELECT *"," in Production",[42,1739,1740],{"language":44},[46,1741,1743],{"className":48,"code":1742,"language":44,"meta":50,"style":50},"-- ❌ WRONG: fetches all columns, prevents Index Only Scan, wastes bandwidth\nSELECT * FROM orders WHERE customer_id = 42;\n-- If orders has 50 columns including large JSONB, you transfer all of them\n-- No covering index can satisfy SELECT * (unless it includes all columns)\n\n-- ✅ RIGHT: select only needed columns — enables Index Only Scan\nSELECT id, amount FROM orders WHERE customer_id = 42;\n-- With CREATE INDEX ON orders(customer_id) INCLUDE (amount), this is an\n-- Index Only Scan — zero heap fetches\n",[37,1744,1745,1750,1771,1776,1781,1785,1790,1811,1816],{"__ignoreMap":50},[54,1746,1747],{"class":56,"line":57},[54,1748,1749],{"class":60},"-- ❌ WRONG: fetches all columns, prevents Index Only Scan, wastes bandwidth\n",[54,1751,1752,1754,1756,1758,1760,1762,1764,1766,1769],{"class":56,"line":64},[54,1753,68],{"class":67},[54,1755,71],{"class":67},[54,1757,74],{"class":67},[54,1759,695],{"class":77},[54,1761,81],{"class":67},[54,1763,202],{"class":77},[54,1765,87],{"class":67},[54,1767,1768],{"class":182}," 42",[54,1770,93],{"class":77},[54,1772,1773],{"class":56,"line":96},[54,1774,1775],{"class":60},"-- If orders has 50 columns including large JSONB, you transfer all of them\n",[54,1777,1778],{"class":56,"line":102},[54,1779,1780],{"class":60},"-- No covering index can satisfy SELECT * (unless it includes all columns)\n",[54,1782,1783],{"class":56,"line":109},[54,1784,106],{"emptyLinePlaceholder":105},[54,1786,1787],{"class":56,"line":115},[54,1788,1789],{"class":60},"-- ✅ RIGHT: select only needed columns — enables Index Only Scan\n",[54,1791,1792,1794,1797,1799,1801,1803,1805,1807,1809],{"class":56,"line":216},[54,1793,68],{"class":67},[54,1795,1796],{"class":77}," id, amount ",[54,1798,205],{"class":67},[54,1800,695],{"class":77},[54,1802,81],{"class":67},[54,1804,202],{"class":77},[54,1806,87],{"class":67},[54,1808,1768],{"class":182},[54,1810,93],{"class":77},[54,1812,1813],{"class":56,"line":221},[54,1814,1815],{"class":60},"-- With CREATE INDEX ON orders(customer_id) INCLUDE (amount), this is an\n",[54,1817,1818],{"class":56,"line":227},[54,1819,1820],{"class":60},"-- Index Only Scan — zero heap fetches\n",[32,1822,1824,1825,1828],{"id":1823},"_15-leading-wildcard-like-defeats-the-index","15. Leading Wildcard ",[37,1826,1827],{},"LIKE"," Defeats the Index",[42,1830,1831],{"language":44},[46,1832,1834],{"className":48,"code":1833,"language":44,"meta":50,"style":50},"-- ❌ WRONG: LIKE '%john%' can't use a B-tree index (leading wildcard)\n-- B-tree indexes sort prefix-first; a leading % means scan every leaf node\nSELECT * FROM customers WHERE name LIKE '%john%';\n\n-- ✅ RIGHT: use pg_trgm GIN\u002FGiST index for substring search\nCREATE EXTENSION pg_trgm;\nCREATE INDEX ON customers USING gin (name gin_trgm_ops);\nSELECT * FROM customers WHERE name LIKE '%john%';  -- now uses the GIN index\n-- Or: for word-level search, use full-text search with tsvector + GIN\n",[37,1835,1836,1841,1846,1869,1873,1878,1885,1907,1931],{"__ignoreMap":50},[54,1837,1838],{"class":56,"line":57},[54,1839,1840],{"class":60},"-- ❌ WRONG: LIKE '%john%' can't use a B-tree index (leading wildcard)\n",[54,1842,1843],{"class":56,"line":64},[54,1844,1845],{"class":60},"-- B-tree indexes sort prefix-first; a leading % means scan every leaf node\n",[54,1847,1848,1850,1852,1854,1856,1858,1861,1864,1867],{"class":56,"line":96},[54,1849,68],{"class":67},[54,1851,71],{"class":67},[54,1853,74],{"class":67},[54,1855,78],{"class":77},[54,1857,81],{"class":67},[54,1859,1860],{"class":67}," name",[54,1862,1863],{"class":67}," LIKE",[54,1865,1866],{"class":318}," '%john%'",[54,1868,93],{"class":77},[54,1870,1871],{"class":56,"line":102},[54,1872,106],{"emptyLinePlaceholder":105},[54,1874,1875],{"class":56,"line":109},[54,1876,1877],{"class":60},"-- ✅ RIGHT: use pg_trgm GIN\u002FGiST index for substring search\n",[54,1879,1880,1882],{"class":56,"line":115},[54,1881,949],{"class":67},[54,1883,1884],{"class":77}," EXTENSION pg_trgm;\n",[54,1886,1887,1889,1892,1895,1897,1899,1902,1904],{"class":56,"line":216},[54,1888,949],{"class":67},[54,1890,1891],{"class":67}," INDEX",[54,1893,1894],{"class":955}," ON",[54,1896,78],{"class":77},[54,1898,1646],{"class":67},[54,1900,1901],{"class":77}," gin (",[54,1903,410],{"class":67},[54,1905,1906],{"class":77}," gin_trgm_ops);\n",[54,1908,1909,1911,1913,1915,1917,1919,1921,1923,1925,1928],{"class":56,"line":221},[54,1910,68],{"class":67},[54,1912,71],{"class":67},[54,1914,74],{"class":67},[54,1916,78],{"class":77},[54,1918,81],{"class":67},[54,1920,1860],{"class":67},[54,1922,1863],{"class":67},[54,1924,1866],{"class":318},[54,1926,1927],{"class":77},";  ",[54,1929,1930],{"class":60},"-- now uses the GIN index\n",[54,1932,1933],{"class":56,"line":227},[54,1934,1935],{"class":60},"-- Or: for word-level search, use full-text search with tsvector + GIN\n",[32,1937,1939,1940,1943],{"id":1938},"_16-offset-pagination-at-scale","16. ",[37,1941,1942],{},"OFFSET"," Pagination at Scale",[42,1945,1946],{"language":44},[46,1947,1949],{"className":48,"code":1948,"language":44,"meta":50,"style":50},"-- ❌ WRONG: OFFSET 100000 scans and discards 100000 rows\nSELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 100000;\n-- The database reads 100020 rows, discards 100000, returns 20\n-- Page 5000 is as slow as the first page of a 100K-row scan\n\n-- ✅ RIGHT: keyset pagination — use WHERE to skip directly to the offset\nSELECT * FROM orders\nWHERE (ordered_on, id) \u003C ('2024-06-15', 12345)  -- last row from previous page\nORDER BY ordered_on DESC, id DESC\nLIMIT 20;\n-- Uses the index to find the start point — O(log N), not O(N)\n",[37,1950,1951,1956,1986,1991,1996,2000,2005,2016,2041,2057,2065],{"__ignoreMap":50},[54,1952,1953],{"class":56,"line":57},[54,1954,1955],{"class":60},"-- ❌ WRONG: OFFSET 100000 scans and discards 100000 rows\n",[54,1957,1958,1960,1962,1964,1966,1969,1972,1975,1978,1981,1984],{"class":56,"line":64},[54,1959,68],{"class":67},[54,1961,71],{"class":67},[54,1963,74],{"class":67},[54,1965,695],{"class":77},[54,1967,1968],{"class":67},"ORDER BY",[54,1970,1971],{"class":77}," id ",[54,1973,1974],{"class":67},"LIMIT",[54,1976,1977],{"class":182}," 20",[54,1979,1980],{"class":77}," OFFSET ",[54,1982,1983],{"class":182},"100000",[54,1985,93],{"class":77},[54,1987,1988],{"class":56,"line":96},[54,1989,1990],{"class":60},"-- The database reads 100020 rows, discards 100000, returns 20\n",[54,1992,1993],{"class":56,"line":102},[54,1994,1995],{"class":60},"-- Page 5000 is as slow as the first page of a 100K-row scan\n",[54,1997,1998],{"class":56,"line":109},[54,1999,106],{"emptyLinePlaceholder":105},[54,2001,2002],{"class":56,"line":115},[54,2003,2004],{"class":60},"-- ✅ RIGHT: keyset pagination — use WHERE to skip directly to the offset\n",[54,2006,2007,2009,2011,2013],{"class":56,"line":216},[54,2008,68],{"class":67},[54,2010,71],{"class":67},[54,2012,74],{"class":67},[54,2014,2015],{"class":77}," orders\n",[54,2017,2018,2020,2023,2026,2028,2031,2033,2036,2038],{"class":56,"line":221},[54,2019,81],{"class":67},[54,2021,2022],{"class":77}," (ordered_on, id) ",[54,2024,2025],{"class":67},"\u003C",[54,2027,197],{"class":77},[54,2029,2030],{"class":318},"'2024-06-15'",[54,2032,413],{"class":77},[54,2034,2035],{"class":182},"12345",[54,2037,1039],{"class":77},[54,2039,2040],{"class":60},"-- last row from previous page\n",[54,2042,2043,2045,2048,2051,2054],{"class":56,"line":227},[54,2044,1968],{"class":67},[54,2046,2047],{"class":77}," ordered_on ",[54,2049,2050],{"class":67},"DESC",[54,2052,2053],{"class":77},", id ",[54,2055,2056],{"class":67},"DESC\n",[54,2058,2059,2061,2063],{"class":56,"line":238},[54,2060,1974],{"class":67},[54,2062,1977],{"class":182},[54,2064,93],{"class":77},[54,2066,2067],{"class":56,"line":520},[54,2068,2069],{"class":60},"-- Uses the index to find the start point — O(log N), not O(N)\n",[32,2071,2073,2074,373,2076,2079],{"id":2072},"_17-not-in-vs-not-exists-performance","17. ",[37,2075,141],{},[37,2077,2078],{},"NOT EXISTS"," Performance",[42,2081,2082],{"language":44},[46,2083,2085],{"className":48,"code":2084,"language":44,"meta":50,"style":50},"-- ❌ WRONG: NOT IN can be slow and is NULL-unsafe (see trap #2)\nSELECT * FROM customers c\nWHERE c.id NOT IN (SELECT customer_id FROM orders);\n\n-- ✅ RIGHT: NOT EXISTS is NULL-safe and often faster (anti-join optimization)\nSELECT * FROM customers c\nWHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);\n-- The planner can implement NOT EXISTS as an Anti Join — stop scanning\n-- orders as soon as one match is found per customer\n",[37,2086,2087,2092,2102,2126,2130,2135,2145,2181,2186],{"__ignoreMap":50},[54,2088,2089],{"class":56,"line":57},[54,2090,2091],{"class":60},"-- ❌ WRONG: NOT IN can be slow and is NULL-unsafe (see trap #2)\n",[54,2093,2094,2096,2098,2100],{"class":56,"line":64},[54,2095,68],{"class":67},[54,2097,71],{"class":67},[54,2099,74],{"class":67},[54,2101,175],{"class":77},[54,2103,2104,2106,2108,2110,2112,2114,2116,2118,2120,2122,2124],{"class":56,"line":96},[54,2105,81],{"class":67},[54,2107,183],{"class":182},[54,2109,25],{"class":77},[54,2111,188],{"class":182},[54,2113,191],{"class":67},[54,2115,194],{"class":67},[54,2117,197],{"class":77},[54,2119,68],{"class":67},[54,2121,202],{"class":77},[54,2123,205],{"class":67},[54,2125,208],{"class":77},[54,2127,2128],{"class":56,"line":102},[54,2129,106],{"emptyLinePlaceholder":105},[54,2131,2132],{"class":56,"line":109},[54,2133,2134],{"class":60},"-- ✅ RIGHT: NOT EXISTS is NULL-safe and often faster (anti-join optimization)\n",[54,2136,2137,2139,2141,2143],{"class":56,"line":115},[54,2138,68],{"class":67},[54,2140,71],{"class":67},[54,2142,74],{"class":67},[54,2144,175],{"class":77},[54,2146,2147,2149,2151,2153,2155,2157,2159,2161,2163,2165,2167,2169,2171,2173,2175,2177,2179],{"class":56,"line":216},[54,2148,81],{"class":67},[54,2150,191],{"class":67},[54,2152,245],{"class":67},[54,2154,197],{"class":77},[54,2156,68],{"class":67},[54,2158,252],{"class":182},[54,2160,74],{"class":67},[54,2162,257],{"class":77},[54,2164,81],{"class":67},[54,2166,262],{"class":182},[54,2168,25],{"class":77},[54,2170,267],{"class":182},[54,2172,270],{"class":67},[54,2174,183],{"class":182},[54,2176,25],{"class":77},[54,2178,188],{"class":182},[54,2180,279],{"class":77},[54,2182,2183],{"class":56,"line":221},[54,2184,2185],{"class":60},"-- The planner can implement NOT EXISTS as an Anti Join — stop scanning\n",[54,2187,2188],{"class":56,"line":227},[54,2189,2190],{"class":60},"-- orders as soon as one match is found per customer\n",[32,2192,2194],{"id":2193},"_18-correlated-subquery-re-execution","18. Correlated Subquery Re-Execution",[42,2196,2197],{"language":44},[46,2198,2200],{"className":48,"code":2199,"language":44,"meta":50,"style":50},"-- ❌ WRONG: correlated subquery executes once per outer row\nSELECT c.name,\n  (SELECT SUM(amount) FROM orders o WHERE o.customer_id = c.id) AS total\nFROM customers c;\n-- 10K customers → 10K subquery executions\n\n-- ✅ RIGHT: use a JOIN + GROUP BY (single pass over orders)\nSELECT c.name, COALESCE(SUM(o.amount), 0) AS total\nFROM customers c LEFT JOIN orders o ON c.id = o.customer_id\nGROUP BY c.id, c.name;\n-- Or LATERAL if you need complex per-row logic\n",[37,2201,2202,2207,2220,2258,2265,2270,2274,2279,2316,2342,2362],{"__ignoreMap":50},[54,2203,2204],{"class":56,"line":57},[54,2205,2206],{"class":60},"-- ❌ WRONG: correlated subquery executes once per outer row\n",[54,2208,2209,2211,2213,2215,2217],{"class":56,"line":64},[54,2210,68],{"class":67},[54,2212,183],{"class":182},[54,2214,25],{"class":77},[54,2216,410],{"class":182},[54,2218,2219],{"class":77},",\n",[54,2221,2222,2225,2227,2229,2231,2233,2235,2237,2239,2241,2243,2245,2247,2249,2251,2253,2255],{"class":56,"line":96},[54,2223,2224],{"class":77},"  (",[54,2226,68],{"class":67},[54,2228,683],{"class":182},[54,2230,598],{"class":77},[54,2232,205],{"class":67},[54,2234,257],{"class":77},[54,2236,81],{"class":67},[54,2238,262],{"class":182},[54,2240,25],{"class":77},[54,2242,267],{"class":182},[54,2244,270],{"class":67},[54,2246,183],{"class":182},[54,2248,25],{"class":77},[54,2250,188],{"class":182},[54,2252,425],{"class":77},[54,2254,428],{"class":67},[54,2256,2257],{"class":77}," total\n",[54,2259,2260,2262],{"class":56,"line":102},[54,2261,205],{"class":67},[54,2263,2264],{"class":77}," customers c;\n",[54,2266,2267],{"class":56,"line":109},[54,2268,2269],{"class":60},"-- 10K customers → 10K subquery executions\n",[54,2271,2272],{"class":56,"line":115},[54,2273,106],{"emptyLinePlaceholder":105},[54,2275,2276],{"class":56,"line":216},[54,2277,2278],{"class":60},"-- ✅ RIGHT: use a JOIN + GROUP BY (single pass over orders)\n",[54,2280,2281,2283,2285,2287,2289,2291,2293,2295,2297,2299,2301,2303,2305,2308,2310,2312,2314],{"class":56,"line":221},[54,2282,68],{"class":67},[54,2284,183],{"class":182},[54,2286,25],{"class":77},[54,2288,410],{"class":182},[54,2290,413],{"class":77},[54,2292,628],{"class":182},[54,2294,419],{"class":77},[54,2296,663],{"class":182},[54,2298,419],{"class":77},[54,2300,507],{"class":182},[54,2302,25],{"class":77},[54,2304,1140],{"class":182},[54,2306,2307],{"class":77},"), ",[54,2309,634],{"class":182},[54,2311,425],{"class":77},[54,2313,428],{"class":67},[54,2315,2257],{"class":77},[54,2317,2318,2320,2322,2324,2326,2328,2330,2332,2334,2336,2338,2340],{"class":56,"line":227},[54,2319,205],{"class":67},[54,2321,438],{"class":77},[54,2323,441],{"class":67},[54,2325,257],{"class":77},[54,2327,446],{"class":67},[54,2329,183],{"class":182},[54,2331,25],{"class":77},[54,2333,188],{"class":182},[54,2335,270],{"class":67},[54,2337,262],{"class":182},[54,2339,25],{"class":77},[54,2341,461],{"class":182},[54,2343,2344,2346,2348,2350,2352,2354,2356,2358,2360],{"class":56,"line":238},[54,2345,466],{"class":67},[54,2347,183],{"class":182},[54,2349,25],{"class":77},[54,2351,188],{"class":182},[54,2353,413],{"class":77},[54,2355,1533],{"class":182},[54,2357,25],{"class":77},[54,2359,410],{"class":182},[54,2361,93],{"class":77},[54,2363,2364],{"class":56,"line":520},[54,2365,2366],{"class":60},"-- Or LATERAL if you need complex per-row logic\n",[32,2368,2370],{"id":2369},"_19-missing-index-on-foreign-key","19. Missing Index on Foreign Key",[42,2372,2373],{"language":44},[46,2374,2376],{"className":48,"code":2375,"language":44,"meta":50,"style":50},"-- ❌ WRONG: FK without an index on the child column\nALTER TABLE orders ADD CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES customers(id);\n-- No index on orders.customer_id → ON DELETE CASCADE does a full scan of orders\n-- JOINs on customer_id also do full scans\n\n-- ✅ RIGHT: always index FK columns\nCREATE INDEX ON orders(customer_id);\n-- Now CASCADE deletes and JOINs use an index lookup\n",[37,2377,2378,2383,2413,2418,2423,2427,2432,2443],{"__ignoreMap":50},[54,2379,2380],{"class":56,"line":57},[54,2381,2382],{"class":60},"-- ❌ WRONG: FK without an index on the child column\n",[54,2384,2385,2388,2390,2392,2395,2398,2401,2404,2407,2410],{"class":56,"line":64},[54,2386,2387],{"class":67},"ALTER",[54,2389,952],{"class":67},[54,2391,695],{"class":77},[54,2393,2394],{"class":67},"ADD",[54,2396,2397],{"class":67}," CONSTRAINT",[54,2399,2400],{"class":77}," fk_customer ",[54,2402,2403],{"class":67},"FOREIGN KEY",[54,2405,2406],{"class":77}," (customer_id) ",[54,2408,2409],{"class":67},"REFERENCES",[54,2411,2412],{"class":77}," customers(id);\n",[54,2414,2415],{"class":56,"line":96},[54,2416,2417],{"class":60},"-- No index on orders.customer_id → ON DELETE CASCADE does a full scan of orders\n",[54,2419,2420],{"class":56,"line":102},[54,2421,2422],{"class":60},"-- JOINs on customer_id also do full scans\n",[54,2424,2425],{"class":56,"line":109},[54,2426,106],{"emptyLinePlaceholder":105},[54,2428,2429],{"class":56,"line":115},[54,2430,2431],{"class":60},"-- ✅ RIGHT: always index FK columns\n",[54,2433,2434,2436,2438,2440],{"class":56,"line":216},[54,2435,949],{"class":67},[54,2437,1891],{"class":67},[54,2439,1894],{"class":955},[54,2441,2442],{"class":77}," orders(customer_id);\n",[54,2444,2445],{"class":56,"line":221},[54,2446,2447],{"class":60},"-- Now CASCADE deletes and JOINs use an index lookup\n",[32,2449,2451],{"id":2450},"_20-function-wrapping-prevents-index-use","20. Function Wrapping Prevents Index Use",[42,2453,2454],{"language":44},[46,2455,2457],{"className":48,"code":2456,"language":44,"meta":50,"style":50},"-- ❌ WRONG: lower(email) prevents using an index on email\nSELECT * FROM users WHERE lower(email) = 'john@example.com';\n-- The function transforms the column — the index on email is on the raw value\n\n-- ✅ RIGHT: create an expression index matching the function\nCREATE INDEX ON users (lower(email));\nSELECT * FROM users WHERE lower(email) = 'john@example.com';  -- uses expression index\n-- Or: use a case-insensitive collation (no function needed)\nCREATE INDEX ON users (email COLLATE \"und-x-icu\");\nSELECT * FROM users WHERE email = 'john@example.com';  -- case-insensitive match\n",[37,2458,2459,2464,2490,2495,2499,2504,2521,2546,2551,2570],{"__ignoreMap":50},[54,2460,2461],{"class":56,"line":57},[54,2462,2463],{"class":60},"-- ❌ WRONG: lower(email) prevents using an index on email\n",[54,2465,2466,2468,2470,2472,2475,2477,2480,2483,2485,2488],{"class":56,"line":64},[54,2467,68],{"class":67},[54,2469,71],{"class":67},[54,2471,74],{"class":67},[54,2473,2474],{"class":77}," users ",[54,2476,81],{"class":67},[54,2478,2479],{"class":182}," lower",[54,2481,2482],{"class":77},"(email) ",[54,2484,87],{"class":67},[54,2486,2487],{"class":318}," 'john@example.com'",[54,2489,93],{"class":77},[54,2491,2492],{"class":56,"line":96},[54,2493,2494],{"class":60},"-- The function transforms the column — the index on email is on the raw value\n",[54,2496,2497],{"class":56,"line":102},[54,2498,106],{"emptyLinePlaceholder":105},[54,2500,2501],{"class":56,"line":109},[54,2502,2503],{"class":60},"-- ✅ RIGHT: create an expression index matching the function\n",[54,2505,2506,2508,2510,2512,2515,2518],{"class":56,"line":115},[54,2507,949],{"class":67},[54,2509,1891],{"class":67},[54,2511,1894],{"class":955},[54,2513,2514],{"class":77}," users (",[54,2516,2517],{"class":182},"lower",[54,2519,2520],{"class":77},"(email));\n",[54,2522,2523,2525,2527,2529,2531,2533,2535,2537,2539,2541,2543],{"class":56,"line":216},[54,2524,68],{"class":67},[54,2526,71],{"class":67},[54,2528,74],{"class":67},[54,2530,2474],{"class":77},[54,2532,81],{"class":67},[54,2534,2479],{"class":182},[54,2536,2482],{"class":77},[54,2538,87],{"class":67},[54,2540,2487],{"class":318},[54,2542,1927],{"class":77},[54,2544,2545],{"class":60},"-- uses expression index\n",[54,2547,2548],{"class":56,"line":221},[54,2549,2550],{"class":60},"-- Or: use a case-insensitive collation (no function needed)\n",[54,2552,2553,2555,2557,2559,2562,2565,2568],{"class":56,"line":227},[54,2554,949],{"class":67},[54,2556,1891],{"class":67},[54,2558,1894],{"class":955},[54,2560,2561],{"class":77}," users (email ",[54,2563,2564],{"class":67},"COLLATE",[54,2566,2567],{"class":318}," \"und-x-icu\"",[54,2569,279],{"class":77},[54,2571,2572,2574,2576,2578,2580,2582,2585,2587,2589,2591],{"class":56,"line":238},[54,2573,68],{"class":67},[54,2575,71],{"class":67},[54,2577,74],{"class":67},[54,2579,2474],{"class":77},[54,2581,81],{"class":67},[54,2583,2584],{"class":77}," email ",[54,2586,87],{"class":67},[54,2588,2487],{"class":318},[54,2590,1927],{"class":77},[54,2592,2593],{"class":60},"-- case-insensitive match\n",[27,2595,2597],{"id":2596},"type-traps","Type Traps",[32,2599,2601],{"id":2600},"_21-floating-point-for-money","21. Floating-Point for Money",[42,2603,2604],{"language":44},[46,2605,2607],{"className":48,"code":2606,"language":44,"meta":50,"style":50},"-- ❌ WRONG: float is binary — 0.1 + 0.2 ≠ 0.3\nCREATE TABLE orders (amount float);\nINSERT INTO orders VALUES (0.1), (0.2);\nSELECT SUM(amount) FROM orders;  -- 0.30000000000000004\n\n-- ✅ RIGHT: NUMERIC\u002FDECIMAL for exact decimal arithmetic\nCREATE TABLE orders (amount NUMERIC(10,2));\nINSERT INTO orders VALUES (0.10), (0.20);\nSELECT SUM(amount) FROM orders;  -- 0.30 exactly\n-- NUMERIC stores decimal digits exactly, at the cost of slower arithmetic\n",[37,2608,2609,2614,2631,2660,2676,2680,2685,2709,2736,2751],{"__ignoreMap":50},[54,2610,2611],{"class":56,"line":57},[54,2612,2613],{"class":60},"-- ❌ WRONG: float is binary — 0.1 + 0.2 ≠ 0.3\n",[54,2615,2616,2618,2620,2623,2626,2629],{"class":56,"line":64},[54,2617,949],{"class":67},[54,2619,952],{"class":67},[54,2621,2622],{"class":955}," orders",[54,2624,2625],{"class":77}," (amount ",[54,2627,2628],{"class":67},"float",[54,2630,279],{"class":77},[54,2632,2633,2635,2637,2639,2641,2643,2645,2648,2651,2653,2655,2658],{"class":56,"line":96},[54,2634,981],{"class":67},[54,2636,695],{"class":77},[54,2638,987],{"class":67},[54,2640,197],{"class":77},[54,2642,634],{"class":182},[54,2644,25],{"class":77},[54,2646,2647],{"class":182},"1",[54,2649,2650],{"class":77},"), (",[54,2652,634],{"class":182},[54,2654,25],{"class":77},[54,2656,2657],{"class":182},"2",[54,2659,279],{"class":77},[54,2661,2662,2664,2666,2668,2670,2673],{"class":56,"line":102},[54,2663,68],{"class":67},[54,2665,683],{"class":182},[54,2667,598],{"class":77},[54,2669,205],{"class":67},[54,2671,2672],{"class":77}," orders;  ",[54,2674,2675],{"class":60},"-- 0.30000000000000004\n",[54,2677,2678],{"class":56,"line":109},[54,2679,106],{"emptyLinePlaceholder":105},[54,2681,2682],{"class":56,"line":115},[54,2683,2684],{"class":60},"-- ✅ RIGHT: NUMERIC\u002FDECIMAL for exact decimal arithmetic\n",[54,2686,2687,2689,2691,2693,2695,2697,2699,2702,2705,2707],{"class":56,"line":216},[54,2688,949],{"class":67},[54,2690,952],{"class":67},[54,2692,2622],{"class":955},[54,2694,2625],{"class":77},[54,2696,962],{"class":67},[54,2698,419],{"class":77},[54,2700,2701],{"class":182},"10",[54,2703,2704],{"class":77},",",[54,2706,2657],{"class":182},[54,2708,976],{"class":77},[54,2710,2711,2713,2715,2717,2719,2721,2723,2725,2727,2729,2731,2734],{"class":56,"line":221},[54,2712,981],{"class":67},[54,2714,695],{"class":77},[54,2716,987],{"class":67},[54,2718,197],{"class":77},[54,2720,634],{"class":182},[54,2722,25],{"class":77},[54,2724,2701],{"class":182},[54,2726,2650],{"class":77},[54,2728,634],{"class":182},[54,2730,25],{"class":77},[54,2732,2733],{"class":182},"20",[54,2735,279],{"class":77},[54,2737,2738,2740,2742,2744,2746,2748],{"class":56,"line":227},[54,2739,68],{"class":67},[54,2741,683],{"class":182},[54,2743,598],{"class":77},[54,2745,205],{"class":67},[54,2747,2672],{"class":77},[54,2749,2750],{"class":60},"-- 0.30 exactly\n",[54,2752,2753],{"class":56,"line":238},[54,2754,2755],{"class":60},"-- NUMERIC stores decimal digits exactly, at the cost of slower arithmetic\n",[32,2757,2759,2760,2763],{"id":2758},"_22-charn-padding-and-comparison-surprises","22. ",[37,2761,2762],{},"char(n)"," Padding and Comparison Surprises",[42,2765,2766],{"language":44},[46,2767,2769],{"className":48,"code":2768,"language":44,"meta":50,"style":50},"-- ❌ WRONG: char(n) pads with spaces — comparisons are surprising\nCREATE TABLE t (code char(3));\nINSERT INTO t VALUES ('NY');  -- stored as 'NY ' (padded to 3)\nSELECT * FROM t WHERE code = 'NY';   -- may match (trailing spaces ignored in char)\nSELECT * FROM t WHERE code = 'NY ';  -- also matches\n-- Length checks: char_length(code) = 3, not 2\n\n-- ✅ RIGHT: use text or varchar(n) — no padding, predictable behavior\nCREATE TABLE t (code varchar(3));\nINSERT INTO t VALUES ('NY');  -- stored as 'NY' (no padding)\nSELECT * FROM t WHERE code = 'NY';  -- matches\nSELECT * FROM t WHERE code = 'NY '; -- does NOT match (different string)\n",[37,2770,2771,2776,2798,2817,2843,2867,2872,2876,2881,2900,2917,2940],{"__ignoreMap":50},[54,2772,2773],{"class":56,"line":57},[54,2774,2775],{"class":60},"-- ❌ WRONG: char(n) pads with spaces — comparisons are surprising\n",[54,2777,2778,2780,2782,2785,2788,2791,2793,2796],{"class":56,"line":64},[54,2779,949],{"class":67},[54,2781,952],{"class":67},[54,2783,2784],{"class":955}," t",[54,2786,2787],{"class":77}," (code ",[54,2789,2790],{"class":67},"char",[54,2792,419],{"class":77},[54,2794,2795],{"class":182},"3",[54,2797,976],{"class":77},[54,2799,2800,2802,2805,2807,2809,2812,2814],{"class":56,"line":96},[54,2801,981],{"class":67},[54,2803,2804],{"class":77}," t ",[54,2806,987],{"class":67},[54,2808,197],{"class":77},[54,2810,2811],{"class":318},"'NY'",[54,2813,995],{"class":77},[54,2815,2816],{"class":60},"-- stored as 'NY ' (padded to 3)\n",[54,2818,2819,2821,2823,2825,2827,2829,2832,2834,2837,2840],{"class":56,"line":102},[54,2820,68],{"class":67},[54,2822,71],{"class":67},[54,2824,74],{"class":67},[54,2826,2804],{"class":77},[54,2828,81],{"class":67},[54,2830,2831],{"class":77}," code ",[54,2833,87],{"class":67},[54,2835,2836],{"class":318}," 'NY'",[54,2838,2839],{"class":77},";   ",[54,2841,2842],{"class":60},"-- may match (trailing spaces ignored in char)\n",[54,2844,2845,2847,2849,2851,2853,2855,2857,2859,2862,2864],{"class":56,"line":109},[54,2846,68],{"class":67},[54,2848,71],{"class":67},[54,2850,74],{"class":67},[54,2852,2804],{"class":77},[54,2854,81],{"class":67},[54,2856,2831],{"class":77},[54,2858,87],{"class":67},[54,2860,2861],{"class":318}," 'NY '",[54,2863,1927],{"class":77},[54,2865,2866],{"class":60},"-- also matches\n",[54,2868,2869],{"class":56,"line":115},[54,2870,2871],{"class":60},"-- Length checks: char_length(code) = 3, not 2\n",[54,2873,2874],{"class":56,"line":216},[54,2875,106],{"emptyLinePlaceholder":105},[54,2877,2878],{"class":56,"line":221},[54,2879,2880],{"class":60},"-- ✅ RIGHT: use text or varchar(n) — no padding, predictable behavior\n",[54,2882,2883,2885,2887,2889,2891,2894,2896,2898],{"class":56,"line":227},[54,2884,949],{"class":67},[54,2886,952],{"class":67},[54,2888,2784],{"class":955},[54,2890,2787],{"class":77},[54,2892,2893],{"class":67},"varchar",[54,2895,419],{"class":77},[54,2897,2795],{"class":182},[54,2899,976],{"class":77},[54,2901,2902,2904,2906,2908,2910,2912,2914],{"class":56,"line":238},[54,2903,981],{"class":67},[54,2905,2804],{"class":77},[54,2907,987],{"class":67},[54,2909,197],{"class":77},[54,2911,2811],{"class":318},[54,2913,995],{"class":77},[54,2915,2916],{"class":60},"-- stored as 'NY' (no padding)\n",[54,2918,2919,2921,2923,2925,2927,2929,2931,2933,2935,2937],{"class":56,"line":520},[54,2920,68],{"class":67},[54,2922,71],{"class":67},[54,2924,74],{"class":67},[54,2926,2804],{"class":77},[54,2928,81],{"class":67},[54,2930,2831],{"class":77},[54,2932,87],{"class":67},[54,2934,2836],{"class":318},[54,2936,1927],{"class":77},[54,2938,2939],{"class":60},"-- matches\n",[54,2941,2942,2944,2946,2948,2950,2952,2954,2956,2958,2961],{"class":56,"line":547},[54,2943,68],{"class":67},[54,2945,71],{"class":67},[54,2947,74],{"class":67},[54,2949,2804],{"class":77},[54,2951,81],{"class":67},[54,2953,2831],{"class":77},[54,2955,87],{"class":67},[54,2957,2861],{"class":318},[54,2959,2960],{"class":77},"; ",[54,2962,2963],{"class":60},"-- does NOT match (different string)\n",[32,2965,2967],{"id":2966},"_23-implicit-type-coercion","23. Implicit Type Coercion",[42,2969,2970],{"language":44},[46,2971,2973],{"className":48,"code":2972,"language":44,"meta":50,"style":50},"-- ❌ WRONG: comparing string to integer triggers implicit cast — defeats index\nSELECT * FROM orders WHERE id = '42';  -- id is integer, '42' is text\n-- PG casts '42' to integer (not the other way) — but some engines cast the column\n-- to string, which prevents index use on the integer column\n\n-- ✅ RIGHT: match the column type exactly\nSELECT * FROM orders WHERE id = 42;  -- integer = integer, index used\n",[37,2974,2975,2980,3004,3009,3014,3018,3023],{"__ignoreMap":50},[54,2976,2977],{"class":56,"line":57},[54,2978,2979],{"class":60},"-- ❌ WRONG: comparing string to integer triggers implicit cast — defeats index\n",[54,2981,2982,2984,2986,2988,2990,2992,2994,2996,2999,3001],{"class":56,"line":64},[54,2983,68],{"class":67},[54,2985,71],{"class":67},[54,2987,74],{"class":67},[54,2989,695],{"class":77},[54,2991,81],{"class":67},[54,2993,1971],{"class":77},[54,2995,87],{"class":67},[54,2997,2998],{"class":318}," '42'",[54,3000,1927],{"class":77},[54,3002,3003],{"class":60},"-- id is integer, '42' is text\n",[54,3005,3006],{"class":56,"line":96},[54,3007,3008],{"class":60},"-- PG casts '42' to integer (not the other way) — but some engines cast the column\n",[54,3010,3011],{"class":56,"line":102},[54,3012,3013],{"class":60},"-- to string, which prevents index use on the integer column\n",[54,3015,3016],{"class":56,"line":109},[54,3017,106],{"emptyLinePlaceholder":105},[54,3019,3020],{"class":56,"line":115},[54,3021,3022],{"class":60},"-- ✅ RIGHT: match the column type exactly\n",[54,3024,3025,3027,3029,3031,3033,3035,3037,3039,3041,3043],{"class":56,"line":216},[54,3026,68],{"class":67},[54,3028,71],{"class":67},[54,3030,74],{"class":67},[54,3032,695],{"class":77},[54,3034,81],{"class":67},[54,3036,1971],{"class":77},[54,3038,87],{"class":67},[54,3040,1768],{"class":182},[54,3042,1927],{"class":77},[54,3044,3045],{"class":60},"-- integer = integer, index used\n",[32,3047,3049,3050,3053],{"id":3048},"_24-timestamp-without-time-zone","24. ",[37,3051,3052],{},"TIMESTAMP"," Without Time Zone",[42,3055,3056],{"language":44},[46,3057,3059],{"className":48,"code":3058,"language":44,"meta":50,"style":50},"-- ❌ WRONG: TIMESTAMP (without time zone) is ambiguous\n-- It stores a wall-clock time with NO zone — interpretation depends on the session\nCREATE TABLE events (created_at TIMESTAMP);\nINSERT INTO events VALUES ('2024-06-15 14:30:00');\n-- Is this UTC? Local time? Nobody knows — the zone is lost\n\n-- ✅ RIGHT: use TIMESTAMPTZ for event times\nCREATE TABLE events (created_at TIMESTAMPTZ);\nINSERT INTO events VALUES ('2024-06-15 14:30:00+00');\n-- Stored as UTC, displayed in the session's time zone\n-- No ambiguity — the instant is fixed\n",[37,3060,3061,3066,3071,3087,3103,3108,3112,3117,3132,3147,3152],{"__ignoreMap":50},[54,3062,3063],{"class":56,"line":57},[54,3064,3065],{"class":60},"-- ❌ WRONG: TIMESTAMP (without time zone) is ambiguous\n",[54,3067,3068],{"class":56,"line":64},[54,3069,3070],{"class":60},"-- It stores a wall-clock time with NO zone — interpretation depends on the session\n",[54,3072,3073,3075,3077,3080,3083,3085],{"class":56,"line":96},[54,3074,949],{"class":67},[54,3076,952],{"class":67},[54,3078,3079],{"class":955}," events",[54,3081,3082],{"class":77}," (created_at ",[54,3084,3052],{"class":67},[54,3086,279],{"class":77},[54,3088,3089,3091,3094,3096,3098,3101],{"class":56,"line":102},[54,3090,981],{"class":67},[54,3092,3093],{"class":77}," events ",[54,3095,987],{"class":67},[54,3097,197],{"class":77},[54,3099,3100],{"class":318},"'2024-06-15 14:30:00'",[54,3102,279],{"class":77},[54,3104,3105],{"class":56,"line":109},[54,3106,3107],{"class":60},"-- Is this UTC? Local time? Nobody knows — the zone is lost\n",[54,3109,3110],{"class":56,"line":115},[54,3111,106],{"emptyLinePlaceholder":105},[54,3113,3114],{"class":56,"line":216},[54,3115,3116],{"class":60},"-- ✅ RIGHT: use TIMESTAMPTZ for event times\n",[54,3118,3119,3121,3123,3125,3127,3130],{"class":56,"line":221},[54,3120,949],{"class":67},[54,3122,952],{"class":67},[54,3124,3079],{"class":955},[54,3126,3082],{"class":77},[54,3128,3129],{"class":67},"TIMESTAMPTZ",[54,3131,279],{"class":77},[54,3133,3134,3136,3138,3140,3142,3145],{"class":56,"line":227},[54,3135,981],{"class":67},[54,3137,3093],{"class":77},[54,3139,987],{"class":67},[54,3141,197],{"class":77},[54,3143,3144],{"class":318},"'2024-06-15 14:30:00+00'",[54,3146,279],{"class":77},[54,3148,3149],{"class":56,"line":238},[54,3150,3151],{"class":60},"-- Stored as UTC, displayed in the session's time zone\n",[54,3153,3154],{"class":56,"line":520},[54,3155,3156],{"class":60},"-- No ambiguity — the instant is fixed\n",[27,3158,3160],{"id":3159},"transaction-traps","Transaction Traps",[32,3162,3164],{"id":3163},"_25-lost-updates-at-read-committed","25. Lost Updates at READ COMMITTED",[42,3166,3167],{"language":44},[46,3168,3170],{"className":48,"code":3169,"language":44,"meta":50,"style":50},"-- ❌ WRONG: read-then-write — another transaction's update is overwritten\n-- T1: SELECT balance FROM accounts WHERE id = 1;  -- 100\n-- T2: SELECT balance FROM accounts WHERE id = 1;  -- 100\n-- T1: UPDATE accounts SET balance = 100 - 10 WHERE id = 1;  -- 90\n-- T2: UPDATE accounts SET balance = 100 - 20 WHERE id = 1;  -- 80 (T1's update lost!)\n\n-- ✅ RIGHT: atomic update (reads and writes in one statement)\nUPDATE accounts SET balance = balance - 10 WHERE id = 1;\n-- The row is locked for the duration; balance is read and written atomically\n-- Or: SELECT ... FOR UPDATE to lock the row\nBEGIN;\nSELECT balance FROM accounts WHERE id = 1 FOR UPDATE;  -- locks the row\n-- application checks balance\nUPDATE accounts SET balance = 90 WHERE id = 1;\nCOMMIT;\n",[37,3171,3172,3177,3182,3187,3192,3197,3201,3206,3241,3246,3251,3258,3287,3292,3317],{"__ignoreMap":50},[54,3173,3174],{"class":56,"line":57},[54,3175,3176],{"class":60},"-- ❌ WRONG: read-then-write — another transaction's update is overwritten\n",[54,3178,3179],{"class":56,"line":64},[54,3180,3181],{"class":60},"-- T1: SELECT balance FROM accounts WHERE id = 1;  -- 100\n",[54,3183,3184],{"class":56,"line":96},[54,3185,3186],{"class":60},"-- T2: SELECT balance FROM accounts WHERE id = 1;  -- 100\n",[54,3188,3189],{"class":56,"line":102},[54,3190,3191],{"class":60},"-- T1: UPDATE accounts SET balance = 100 - 10 WHERE id = 1;  -- 90\n",[54,3193,3194],{"class":56,"line":109},[54,3195,3196],{"class":60},"-- T2: UPDATE accounts SET balance = 100 - 20 WHERE id = 1;  -- 80 (T1's update lost!)\n",[54,3198,3199],{"class":56,"line":115},[54,3200,106],{"emptyLinePlaceholder":105},[54,3202,3203],{"class":56,"line":216},[54,3204,3205],{"class":60},"-- ✅ RIGHT: atomic update (reads and writes in one statement)\n",[54,3207,3208,3211,3214,3217,3220,3222,3224,3227,3230,3233,3235,3237,3239],{"class":56,"line":221},[54,3209,3210],{"class":67},"UPDATE",[54,3212,3213],{"class":77}," accounts ",[54,3215,3216],{"class":67},"SET",[54,3218,3219],{"class":77}," balance ",[54,3221,87],{"class":67},[54,3223,3219],{"class":77},[54,3225,3226],{"class":67},"-",[54,3228,3229],{"class":182}," 10",[54,3231,3232],{"class":67}," WHERE",[54,3234,1971],{"class":77},[54,3236,87],{"class":67},[54,3238,252],{"class":182},[54,3240,93],{"class":77},[54,3242,3243],{"class":56,"line":227},[54,3244,3245],{"class":60},"-- The row is locked for the duration; balance is read and written atomically\n",[54,3247,3248],{"class":56,"line":238},[54,3249,3250],{"class":60},"-- Or: SELECT ... FOR UPDATE to lock the row\n",[54,3252,3253,3256],{"class":56,"line":520},[54,3254,3255],{"class":67},"BEGIN",[54,3257,93],{"class":77},[54,3259,3260,3262,3264,3266,3268,3270,3272,3274,3276,3279,3282,3284],{"class":56,"line":547},[54,3261,68],{"class":67},[54,3263,3219],{"class":77},[54,3265,205],{"class":67},[54,3267,3213],{"class":77},[54,3269,81],{"class":67},[54,3271,1971],{"class":77},[54,3273,87],{"class":67},[54,3275,252],{"class":182},[54,3277,3278],{"class":67}," FOR",[54,3280,3281],{"class":67}," UPDATE",[54,3283,1927],{"class":77},[54,3285,3286],{"class":60},"-- locks the row\n",[54,3288,3289],{"class":56,"line":560},[54,3290,3291],{"class":60},"-- application checks balance\n",[54,3293,3294,3296,3298,3300,3302,3304,3307,3309,3311,3313,3315],{"class":56,"line":1231},[54,3295,3210],{"class":67},[54,3297,3213],{"class":77},[54,3299,3216],{"class":67},[54,3301,3219],{"class":77},[54,3303,87],{"class":67},[54,3305,3306],{"class":182}," 90",[54,3308,3232],{"class":67},[54,3310,1971],{"class":77},[54,3312,87],{"class":67},[54,3314,252],{"class":182},[54,3316,93],{"class":77},[54,3318,3320,3323],{"class":56,"line":3319},15,[54,3321,3322],{"class":67},"COMMIT",[54,3324,93],{"class":77},[32,3326,3328],{"id":3327},"_26-deadlocks-from-inconsistent-lock-ordering","26. Deadlocks from Inconsistent Lock Ordering",[42,3330,3331],{"language":44},[46,3332,3334],{"className":48,"code":3333,"language":44,"meta":50,"style":50},"-- ❌ WRONG: T1 locks A then B; T2 locks B then A → deadlock\n-- T1: BEGIN; UPDATE accounts SET balance = balance - 10 WHERE id = 1; -- locks id=1\n-- T2: BEGIN; UPDATE accounts SET balance = balance - 10 WHERE id = 2; -- locks id=2\n-- T1: UPDATE accounts SET balance = balance + 10 WHERE id = 2; -- waits for T2's lock\n-- T2: UPDATE accounts SET balance = balance + 10 WHERE id = 1; -- waits for T1's lock\n-- → DEADLOCK — one transaction is aborted with 40P01\n\n-- ✅ RIGHT: always lock in a consistent order (e.g., ascending id)\n-- Both transactions lock id=1 first, then id=2:\nBEGIN;\nUPDATE accounts SET balance = balance - 10 WHERE id = 1;  -- lock smaller id first\nUPDATE accounts SET balance = balance + 10 WHERE id = 2;\nCOMMIT;\n",[37,3335,3336,3341,3346,3351,3356,3361,3366,3370,3375,3380,3386,3417,3447],{"__ignoreMap":50},[54,3337,3338],{"class":56,"line":57},[54,3339,3340],{"class":60},"-- ❌ WRONG: T1 locks A then B; T2 locks B then A → deadlock\n",[54,3342,3343],{"class":56,"line":64},[54,3344,3345],{"class":60},"-- T1: BEGIN; UPDATE accounts SET balance = balance - 10 WHERE id = 1; -- locks id=1\n",[54,3347,3348],{"class":56,"line":96},[54,3349,3350],{"class":60},"-- T2: BEGIN; UPDATE accounts SET balance = balance - 10 WHERE id = 2; -- locks id=2\n",[54,3352,3353],{"class":56,"line":102},[54,3354,3355],{"class":60},"-- T1: UPDATE accounts SET balance = balance + 10 WHERE id = 2; -- waits for T2's lock\n",[54,3357,3358],{"class":56,"line":109},[54,3359,3360],{"class":60},"-- T2: UPDATE accounts SET balance = balance + 10 WHERE id = 1; -- waits for T1's lock\n",[54,3362,3363],{"class":56,"line":115},[54,3364,3365],{"class":60},"-- → DEADLOCK — one transaction is aborted with 40P01\n",[54,3367,3368],{"class":56,"line":216},[54,3369,106],{"emptyLinePlaceholder":105},[54,3371,3372],{"class":56,"line":221},[54,3373,3374],{"class":60},"-- ✅ RIGHT: always lock in a consistent order (e.g., ascending id)\n",[54,3376,3377],{"class":56,"line":227},[54,3378,3379],{"class":60},"-- Both transactions lock id=1 first, then id=2:\n",[54,3381,3382,3384],{"class":56,"line":238},[54,3383,3255],{"class":67},[54,3385,93],{"class":77},[54,3387,3388,3390,3392,3394,3396,3398,3400,3402,3404,3406,3408,3410,3412,3414],{"class":56,"line":520},[54,3389,3210],{"class":67},[54,3391,3213],{"class":77},[54,3393,3216],{"class":67},[54,3395,3219],{"class":77},[54,3397,87],{"class":67},[54,3399,3219],{"class":77},[54,3401,3226],{"class":67},[54,3403,3229],{"class":182},[54,3405,3232],{"class":67},[54,3407,1971],{"class":77},[54,3409,87],{"class":67},[54,3411,252],{"class":182},[54,3413,1927],{"class":77},[54,3415,3416],{"class":60},"-- lock smaller id first\n",[54,3418,3419,3421,3423,3425,3427,3429,3431,3434,3436,3438,3440,3442,3445],{"class":56,"line":547},[54,3420,3210],{"class":67},[54,3422,3213],{"class":77},[54,3424,3216],{"class":67},[54,3426,3219],{"class":77},[54,3428,87],{"class":67},[54,3430,3219],{"class":77},[54,3432,3433],{"class":67},"+",[54,3435,3229],{"class":182},[54,3437,3232],{"class":67},[54,3439,1971],{"class":77},[54,3441,87],{"class":67},[54,3443,3444],{"class":182}," 2",[54,3446,93],{"class":77},[54,3448,3449,3451],{"class":56,"line":560},[54,3450,3322],{"class":67},[54,3452,93],{"class":77},[32,3454,3456],{"id":3455},"_27-long-transactions-block-vacuum","27. Long Transactions Block VACUUM",[42,3458,3459],{"language":44},[46,3460,3462],{"className":48,"code":3461,"language":44,"meta":50,"style":50},"-- ❌ WRONG: transaction spans an HTTP call — holds locks and pins dead tuples\nBEGIN;\nSELECT * FROM orders WHERE id = 1 FOR UPDATE;\n-- ... application makes an HTTP call to a payment gateway (5 seconds) ...\n-- During those 5 seconds: VACUUM can't reclaim dead tuples that are newer\n-- than this transaction's snapshot → bloat accumulates\nUPDATE orders SET status = 'paid' WHERE id = 1;\nCOMMIT;\n\n-- ✅ RIGHT: keep transactions short — never span external calls\n-- Do the HTTP call OUTSIDE the transaction:\nresult = call_payment_gateway(order_id)  -- no transaction open\nBEGIN;\nUPDATE orders SET status = 'paid' WHERE id = 1;\nCOMMIT;\n-- Or: set idle_in_transaction_session_timeout = '10s' to kill stuck sessions\n",[37,3463,3464,3469,3475,3499,3504,3509,3514,3540,3546,3550,3555,3560,3573,3579,3603,3609],{"__ignoreMap":50},[54,3465,3466],{"class":56,"line":57},[54,3467,3468],{"class":60},"-- ❌ WRONG: transaction spans an HTTP call — holds locks and pins dead tuples\n",[54,3470,3471,3473],{"class":56,"line":64},[54,3472,3255],{"class":67},[54,3474,93],{"class":77},[54,3476,3477,3479,3481,3483,3485,3487,3489,3491,3493,3495,3497],{"class":56,"line":96},[54,3478,68],{"class":67},[54,3480,71],{"class":67},[54,3482,74],{"class":67},[54,3484,695],{"class":77},[54,3486,81],{"class":67},[54,3488,1971],{"class":77},[54,3490,87],{"class":67},[54,3492,252],{"class":182},[54,3494,3278],{"class":67},[54,3496,3281],{"class":67},[54,3498,93],{"class":77},[54,3500,3501],{"class":56,"line":102},[54,3502,3503],{"class":60},"-- ... application makes an HTTP call to a payment gateway (5 seconds) ...\n",[54,3505,3506],{"class":56,"line":109},[54,3507,3508],{"class":60},"-- During those 5 seconds: VACUUM can't reclaim dead tuples that are newer\n",[54,3510,3511],{"class":56,"line":115},[54,3512,3513],{"class":60},"-- than this transaction's snapshot → bloat accumulates\n",[54,3515,3516,3518,3520,3522,3525,3527,3530,3532,3534,3536,3538],{"class":56,"line":216},[54,3517,3210],{"class":67},[54,3519,695],{"class":77},[54,3521,3216],{"class":67},[54,3523,3524],{"class":67}," status",[54,3526,270],{"class":67},[54,3528,3529],{"class":318}," 'paid'",[54,3531,3232],{"class":67},[54,3533,1971],{"class":77},[54,3535,87],{"class":67},[54,3537,252],{"class":182},[54,3539,93],{"class":77},[54,3541,3542,3544],{"class":56,"line":221},[54,3543,3322],{"class":67},[54,3545,93],{"class":77},[54,3547,3548],{"class":56,"line":227},[54,3549,106],{"emptyLinePlaceholder":105},[54,3551,3552],{"class":56,"line":238},[54,3553,3554],{"class":60},"-- ✅ RIGHT: keep transactions short — never span external calls\n",[54,3556,3557],{"class":56,"line":520},[54,3558,3559],{"class":60},"-- Do the HTTP call OUTSIDE the transaction:\n",[54,3561,3562,3565,3567,3570],{"class":56,"line":547},[54,3563,3564],{"class":77},"result ",[54,3566,87],{"class":67},[54,3568,3569],{"class":77}," call_payment_gateway(order_id)  ",[54,3571,3572],{"class":60},"-- no transaction open\n",[54,3574,3575,3577],{"class":56,"line":560},[54,3576,3255],{"class":67},[54,3578,93],{"class":77},[54,3580,3581,3583,3585,3587,3589,3591,3593,3595,3597,3599,3601],{"class":56,"line":1231},[54,3582,3210],{"class":67},[54,3584,695],{"class":77},[54,3586,3216],{"class":67},[54,3588,3524],{"class":67},[54,3590,270],{"class":67},[54,3592,3529],{"class":318},[54,3594,3232],{"class":67},[54,3596,1971],{"class":77},[54,3598,87],{"class":67},[54,3600,252],{"class":182},[54,3602,93],{"class":77},[54,3604,3605,3607],{"class":56,"line":3319},[54,3606,3322],{"class":67},[54,3608,93],{"class":77},[54,3610,3612],{"class":56,"line":3611},16,[54,3613,3614],{"class":60},"-- Or: set idle_in_transaction_session_timeout = '10s' to kill stuck sessions\n",[32,3616,3618],{"id":3617},"_28-serializable-without-retry","28. SERIALIZABLE Without Retry",[42,3620,3621],{"language":44},[46,3622,3624],{"className":48,"code":3623,"language":44,"meta":50,"style":50},"-- ❌ WRONG: using SERIALIZABLE but not retrying on serialization failure\nSET transaction_isolation = 'serializable';\nBEGIN;\nSELECT balance FROM accounts WHERE id = 1;\n-- ... concurrent transaction modifies the same row ...\nUPDATE accounts SET balance = balance - 10 WHERE id = 1;\nCOMMIT;\n-- → ERROR: could not serialize access due to concurrent update (40001)\n-- Application crashes instead of retrying\n\n-- ✅ RIGHT: retry the entire transaction on 40001\n-- In application code (Python example):\n-- for attempt in range(3):\n--     try:\n--         conn.execute(\"BEGIN ISOLATION LEVEL SERIALIZABLE\")\n--         ... transaction body ...\n--         conn.execute(\"COMMIT\")\n--         break\n--     except SerializationFailure:\n--         conn.execute(\"ROLLBACK\")\n--         continue\n",[37,3625,3626,3631,3645,3651,3671,3676,3704,3710,3715,3720,3724,3729,3734,3739,3744,3749,3754,3760,3766,3772,3778],{"__ignoreMap":50},[54,3627,3628],{"class":56,"line":57},[54,3629,3630],{"class":60},"-- ❌ WRONG: using SERIALIZABLE but not retrying on serialization failure\n",[54,3632,3633,3635,3638,3640,3643],{"class":56,"line":64},[54,3634,3216],{"class":67},[54,3636,3637],{"class":77}," transaction_isolation ",[54,3639,87],{"class":67},[54,3641,3642],{"class":318}," 'serializable'",[54,3644,93],{"class":77},[54,3646,3647,3649],{"class":56,"line":96},[54,3648,3255],{"class":67},[54,3650,93],{"class":77},[54,3652,3653,3655,3657,3659,3661,3663,3665,3667,3669],{"class":56,"line":102},[54,3654,68],{"class":67},[54,3656,3219],{"class":77},[54,3658,205],{"class":67},[54,3660,3213],{"class":77},[54,3662,81],{"class":67},[54,3664,1971],{"class":77},[54,3666,87],{"class":67},[54,3668,252],{"class":182},[54,3670,93],{"class":77},[54,3672,3673],{"class":56,"line":109},[54,3674,3675],{"class":60},"-- ... concurrent transaction modifies the same row ...\n",[54,3677,3678,3680,3682,3684,3686,3688,3690,3692,3694,3696,3698,3700,3702],{"class":56,"line":115},[54,3679,3210],{"class":67},[54,3681,3213],{"class":77},[54,3683,3216],{"class":67},[54,3685,3219],{"class":77},[54,3687,87],{"class":67},[54,3689,3219],{"class":77},[54,3691,3226],{"class":67},[54,3693,3229],{"class":182},[54,3695,3232],{"class":67},[54,3697,1971],{"class":77},[54,3699,87],{"class":67},[54,3701,252],{"class":182},[54,3703,93],{"class":77},[54,3705,3706,3708],{"class":56,"line":216},[54,3707,3322],{"class":67},[54,3709,93],{"class":77},[54,3711,3712],{"class":56,"line":221},[54,3713,3714],{"class":60},"-- → ERROR: could not serialize access due to concurrent update (40001)\n",[54,3716,3717],{"class":56,"line":227},[54,3718,3719],{"class":60},"-- Application crashes instead of retrying\n",[54,3721,3722],{"class":56,"line":238},[54,3723,106],{"emptyLinePlaceholder":105},[54,3725,3726],{"class":56,"line":520},[54,3727,3728],{"class":60},"-- ✅ RIGHT: retry the entire transaction on 40001\n",[54,3730,3731],{"class":56,"line":547},[54,3732,3733],{"class":60},"-- In application code (Python example):\n",[54,3735,3736],{"class":56,"line":560},[54,3737,3738],{"class":60},"-- for attempt in range(3):\n",[54,3740,3741],{"class":56,"line":1231},[54,3742,3743],{"class":60},"--     try:\n",[54,3745,3746],{"class":56,"line":3319},[54,3747,3748],{"class":60},"--         conn.execute(\"BEGIN ISOLATION LEVEL SERIALIZABLE\")\n",[54,3750,3751],{"class":56,"line":3611},[54,3752,3753],{"class":60},"--         ... transaction body ...\n",[54,3755,3757],{"class":56,"line":3756},17,[54,3758,3759],{"class":60},"--         conn.execute(\"COMMIT\")\n",[54,3761,3763],{"class":56,"line":3762},18,[54,3764,3765],{"class":60},"--         break\n",[54,3767,3769],{"class":56,"line":3768},19,[54,3770,3771],{"class":60},"--     except SerializationFailure:\n",[54,3773,3775],{"class":56,"line":3774},20,[54,3776,3777],{"class":60},"--         conn.execute(\"ROLLBACK\")\n",[54,3779,3781],{"class":56,"line":3780},21,[54,3782,3783],{"class":60},"--         continue\n",[27,3785,3787],{"id":3786},"schema-traps","Schema Traps",[32,3789,3791],{"id":3790},"_29-missing-constraints","29. Missing Constraints",[42,3793,3794],{"language":44},[46,3795,3797],{"className":48,"code":3796,"language":44,"meta":50,"style":50},"-- ❌ WRONG: no constraints — bad data enters silently\nCREATE TABLE orders (\n  id SERIAL PRIMARY KEY,\n  customer_id INTEGER,\n  amount NUMERIC,\n  status TEXT,\n  created_at TIMESTAMP\n);\n-- customer_id can reference a nonexistent customer\n-- amount can be negative, NULL, or 0\n-- status can be any string ('pinding', 'PAID!', null)\n\n-- ✅ RIGHT: constrain everything\nCREATE TABLE orders (\n  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n  customer_id BIGINT NOT NULL REFERENCES customers(id) ON DELETE CASCADE,\n  amount NUMERIC(10,2) NOT NULL CHECK (amount > 0),\n  status TEXT NOT NULL CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled')),\n  created_at TIMESTAMPTZ NOT NULL DEFAULT now()\n);\n",[37,3798,3799,3804,3814,3827,3837,3846,3856,3864,3868,3873,3878,3883,3887,3892,3902,3925,3944,3974,4014,4031],{"__ignoreMap":50},[54,3800,3801],{"class":56,"line":57},[54,3802,3803],{"class":60},"-- ❌ WRONG: no constraints — bad data enters silently\n",[54,3805,3806,3808,3810,3812],{"class":56,"line":64},[54,3807,949],{"class":67},[54,3809,952],{"class":67},[54,3811,2622],{"class":955},[54,3813,1018],{"class":77},[54,3815,3816,3819,3822,3825],{"class":56,"line":96},[54,3817,3818],{"class":77},"  id ",[54,3820,3821],{"class":67},"SERIAL",[54,3823,3824],{"class":67}," PRIMARY KEY",[54,3826,2219],{"class":77},[54,3828,3829,3832,3835],{"class":56,"line":102},[54,3830,3831],{"class":77},"  customer_id ",[54,3833,3834],{"class":67},"INTEGER",[54,3836,2219],{"class":77},[54,3838,3839,3842,3844],{"class":56,"line":109},[54,3840,3841],{"class":77},"  amount ",[54,3843,962],{"class":67},[54,3845,2219],{"class":77},[54,3847,3848,3851,3854],{"class":56,"line":115},[54,3849,3850],{"class":67},"  status",[54,3852,3853],{"class":67}," TEXT",[54,3855,2219],{"class":77},[54,3857,3858,3861],{"class":56,"line":216},[54,3859,3860],{"class":77},"  created_at ",[54,3862,3863],{"class":67},"TIMESTAMP\n",[54,3865,3866],{"class":56,"line":221},[54,3867,279],{"class":77},[54,3869,3870],{"class":56,"line":227},[54,3871,3872],{"class":60},"-- customer_id can reference a nonexistent customer\n",[54,3874,3875],{"class":56,"line":238},[54,3876,3877],{"class":60},"-- amount can be negative, NULL, or 0\n",[54,3879,3880],{"class":56,"line":520},[54,3881,3882],{"class":60},"-- status can be any string ('pinding', 'PAID!', null)\n",[54,3884,3885],{"class":56,"line":547},[54,3886,106],{"emptyLinePlaceholder":105},[54,3888,3889],{"class":56,"line":560},[54,3890,3891],{"class":60},"-- ✅ RIGHT: constrain everything\n",[54,3893,3894,3896,3898,3900],{"class":56,"line":1231},[54,3895,949],{"class":67},[54,3897,952],{"class":67},[54,3899,2622],{"class":955},[54,3901,1018],{"class":77},[54,3903,3904,3906,3909,3912,3915,3918,3921,3923],{"class":56,"line":3319},[54,3905,3818],{"class":77},[54,3907,3908],{"class":67},"BIGINT",[54,3910,3911],{"class":67}," GENERATED",[54,3913,3914],{"class":67}," ALWAYS",[54,3916,3917],{"class":67}," AS",[54,3919,3920],{"class":67}," IDENTITY",[54,3922,3824],{"class":67},[54,3924,2219],{"class":77},[54,3926,3927,3929,3931,3933,3936,3939,3942],{"class":56,"line":3611},[54,3928,3831],{"class":77},[54,3930,3908],{"class":67},[54,3932,1028],{"class":67},[54,3934,3935],{"class":67}," REFERENCES",[54,3937,3938],{"class":77}," customers(id) ",[54,3940,3941],{"class":67},"ON DELETE CASCADE",[54,3943,2219],{"class":77},[54,3945,3946,3948,3950,3952,3954,3956,3958,3960,3963,3965,3967,3969,3971],{"class":56,"line":3756},[54,3947,3841],{"class":77},[54,3949,962],{"class":67},[54,3951,419],{"class":77},[54,3953,2701],{"class":182},[54,3955,2704],{"class":77},[54,3957,2657],{"class":182},[54,3959,425],{"class":77},[54,3961,3962],{"class":67},"NOT NULL",[54,3964,965],{"class":67},[54,3966,2625],{"class":77},[54,3968,970],{"class":67},[54,3970,973],{"class":182},[54,3972,3973],{"class":77},"),\n",[54,3975,3976,3978,3980,3982,3984,3986,3989,3991,3993,3996,3998,4001,4003,4006,4008,4011],{"class":56,"line":3762},[54,3977,3850],{"class":67},[54,3979,3853],{"class":67},[54,3981,1028],{"class":67},[54,3983,965],{"class":67},[54,3985,197],{"class":77},[54,3987,3988],{"class":67},"status",[54,3990,194],{"class":67},[54,3992,197],{"class":77},[54,3994,3995],{"class":318},"'pending'",[54,3997,413],{"class":77},[54,3999,4000],{"class":318},"'paid'",[54,4002,413],{"class":77},[54,4004,4005],{"class":318},"'shipped'",[54,4007,413],{"class":77},[54,4009,4010],{"class":318},"'cancelled'",[54,4012,4013],{"class":77},")),\n",[54,4015,4016,4018,4020,4022,4025,4028],{"class":56,"line":3768},[54,4017,3860],{"class":77},[54,4019,3129],{"class":67},[54,4021,1028],{"class":67},[54,4023,4024],{"class":67}," DEFAULT",[54,4026,4027],{"class":67}," now",[54,4029,4030],{"class":77},"()\n",[54,4032,4033],{"class":56,"line":3774},[54,4034,279],{"class":77},[32,4036,4038],{"id":4037},"_30-over-indexing","30. Over-Indexing",[42,4040,4041],{"language":44},[46,4042,4044],{"className":48,"code":4043,"language":44,"meta":50,"style":50},"-- ❌ WRONG: index every column \"just in case\"\nCREATE INDEX ON orders(customer_id);\nCREATE INDEX ON orders(amount);\nCREATE INDEX ON orders(status);\nCREATE INDEX ON orders(ordered_on);\nCREATE INDEX ON orders(customer_id, amount);\nCREATE INDEX ON orders(customer_id, status);\nCREATE INDEX ON orders(customer_id, ordered_on);\n-- Each index slows INSERT\u002FUPDATE\u002FDELETE (maintain every index on every write)\n-- Each index consumes disk + cache space (fewer pages for actual data in shared_buffers)\n\n-- ✅ RIGHT: measure with EXPLAIN, index only what queries need\nEXPLAIN (ANALYZE) SELECT * FROM orders WHERE customer_id = 42 AND status = 'pending';\n-- If a single composite index covers it:\nCREATE INDEX ON orders(customer_id, status) INCLUDE (amount);\n-- One index serves this query AND related queries (customer_id + status prefix)\n",[37,4045,4046,4051,4061,4072,4087,4098,4109,4124,4135,4140,4145,4149,4154,4186,4191,4211],{"__ignoreMap":50},[54,4047,4048],{"class":56,"line":57},[54,4049,4050],{"class":60},"-- ❌ WRONG: index every column \"just in case\"\n",[54,4052,4053,4055,4057,4059],{"class":56,"line":64},[54,4054,949],{"class":67},[54,4056,1891],{"class":67},[54,4058,1894],{"class":955},[54,4060,2442],{"class":77},[54,4062,4063,4065,4067,4069],{"class":56,"line":96},[54,4064,949],{"class":67},[54,4066,1891],{"class":67},[54,4068,1894],{"class":955},[54,4070,4071],{"class":77}," orders(amount);\n",[54,4073,4074,4076,4078,4080,4083,4085],{"class":56,"line":102},[54,4075,949],{"class":67},[54,4077,1891],{"class":67},[54,4079,1894],{"class":955},[54,4081,4082],{"class":77}," orders(",[54,4084,3988],{"class":67},[54,4086,279],{"class":77},[54,4088,4089,4091,4093,4095],{"class":56,"line":109},[54,4090,949],{"class":67},[54,4092,1891],{"class":67},[54,4094,1894],{"class":955},[54,4096,4097],{"class":77}," orders(ordered_on);\n",[54,4099,4100,4102,4104,4106],{"class":56,"line":115},[54,4101,949],{"class":67},[54,4103,1891],{"class":67},[54,4105,1894],{"class":955},[54,4107,4108],{"class":77}," orders(customer_id, amount);\n",[54,4110,4111,4113,4115,4117,4120,4122],{"class":56,"line":216},[54,4112,949],{"class":67},[54,4114,1891],{"class":67},[54,4116,1894],{"class":955},[54,4118,4119],{"class":77}," orders(customer_id, ",[54,4121,3988],{"class":67},[54,4123,279],{"class":77},[54,4125,4126,4128,4130,4132],{"class":56,"line":221},[54,4127,949],{"class":67},[54,4129,1891],{"class":67},[54,4131,1894],{"class":955},[54,4133,4134],{"class":77}," orders(customer_id, ordered_on);\n",[54,4136,4137],{"class":56,"line":227},[54,4138,4139],{"class":60},"-- Each index slows INSERT\u002FUPDATE\u002FDELETE (maintain every index on every write)\n",[54,4141,4142],{"class":56,"line":238},[54,4143,4144],{"class":60},"-- Each index consumes disk + cache space (fewer pages for actual data in shared_buffers)\n",[54,4146,4147],{"class":56,"line":520},[54,4148,106],{"emptyLinePlaceholder":105},[54,4150,4151],{"class":56,"line":547},[54,4152,4153],{"class":60},"-- ✅ RIGHT: measure with EXPLAIN, index only what queries need\n",[54,4155,4156,4159,4161,4163,4165,4167,4169,4171,4173,4175,4177,4179,4181,4184],{"class":56,"line":560},[54,4157,4158],{"class":77},"EXPLAIN (ANALYZE) ",[54,4160,68],{"class":67},[54,4162,71],{"class":67},[54,4164,74],{"class":67},[54,4166,695],{"class":77},[54,4168,81],{"class":67},[54,4170,202],{"class":77},[54,4172,87],{"class":67},[54,4174,1768],{"class":182},[54,4176,1211],{"class":67},[54,4178,3524],{"class":67},[54,4180,270],{"class":67},[54,4182,4183],{"class":318}," 'pending'",[54,4185,93],{"class":77},[54,4187,4188],{"class":56,"line":1231},[54,4189,4190],{"class":60},"-- If a single composite index covers it:\n",[54,4192,4193,4195,4197,4199,4201,4203,4205,4208],{"class":56,"line":3319},[54,4194,949],{"class":67},[54,4196,1891],{"class":67},[54,4198,1894],{"class":955},[54,4200,4119],{"class":77},[54,4202,3988],{"class":67},[54,4204,425],{"class":77},[54,4206,4207],{"class":67},"INCLUDE",[54,4209,4210],{"class":77}," (amount);\n",[54,4212,4213],{"class":56,"line":3611},[54,4214,4215],{"class":60},"-- One index serves this query AND related queries (customer_id + status prefix)\n",[32,4217,4219],{"id":4218},"_31-premature-denormalization","31. Premature Denormalization",[42,4221,4222],{"language":44},[46,4223,4225],{"className":48,"code":4224,"language":44,"meta":50,"style":50},"-- ❌ WRONG: denormalizing before you have a measured performance problem\nCREATE TABLE orders (\n  id BIGINT PRIMARY KEY,\n  customer_id BIGINT,\n  customer_name TEXT,      -- denormalized — what if customer changes name?\n  customer_email TEXT,     -- denormalized — goes stale\n  amount NUMERIC\n);\n-- Now you must UPDATE every order when a customer's name\u002Femail changes\n-- And the data can be inconsistent (some orders have old name, some new)\n\n-- ✅ RIGHT: normalize first, denormalize only when measured to be needed\nCREATE TABLE orders (\n  id BIGINT PRIMARY KEY,\n  customer_id BIGINT REFERENCES customers(id),\n  amount NUMERIC\n);\nCREATE TABLE customers (\n  id BIGINT PRIMARY KEY,\n  name TEXT,\n  email TEXT\n);\n-- If JOINs are slow, add a covering index or materialized view — don't duplicate data\n",[37,4226,4227,4232,4242,4252,4260,4274,4287,4294,4298,4303,4308,4312,4317,4327,4337,4348,4354,4358,4369,4379,4388,4396,4401],{"__ignoreMap":50},[54,4228,4229],{"class":56,"line":57},[54,4230,4231],{"class":60},"-- ❌ WRONG: denormalizing before you have a measured performance problem\n",[54,4233,4234,4236,4238,4240],{"class":56,"line":64},[54,4235,949],{"class":67},[54,4237,952],{"class":67},[54,4239,2622],{"class":955},[54,4241,1018],{"class":77},[54,4243,4244,4246,4248,4250],{"class":56,"line":96},[54,4245,3818],{"class":77},[54,4247,3908],{"class":67},[54,4249,3824],{"class":67},[54,4251,2219],{"class":77},[54,4253,4254,4256,4258],{"class":56,"line":102},[54,4255,3831],{"class":77},[54,4257,3908],{"class":67},[54,4259,2219],{"class":77},[54,4261,4262,4265,4268,4271],{"class":56,"line":109},[54,4263,4264],{"class":77},"  customer_name ",[54,4266,4267],{"class":67},"TEXT",[54,4269,4270],{"class":77},",      ",[54,4272,4273],{"class":60},"-- denormalized — what if customer changes name?\n",[54,4275,4276,4279,4281,4284],{"class":56,"line":115},[54,4277,4278],{"class":77},"  customer_email ",[54,4280,4267],{"class":67},[54,4282,4283],{"class":77},",     ",[54,4285,4286],{"class":60},"-- denormalized — goes stale\n",[54,4288,4289,4291],{"class":56,"line":216},[54,4290,3841],{"class":77},[54,4292,4293],{"class":67},"NUMERIC\n",[54,4295,4296],{"class":56,"line":221},[54,4297,279],{"class":77},[54,4299,4300],{"class":56,"line":227},[54,4301,4302],{"class":60},"-- Now you must UPDATE every order when a customer's name\u002Femail changes\n",[54,4304,4305],{"class":56,"line":238},[54,4306,4307],{"class":60},"-- And the data can be inconsistent (some orders have old name, some new)\n",[54,4309,4310],{"class":56,"line":520},[54,4311,106],{"emptyLinePlaceholder":105},[54,4313,4314],{"class":56,"line":547},[54,4315,4316],{"class":60},"-- ✅ RIGHT: normalize first, denormalize only when measured to be needed\n",[54,4318,4319,4321,4323,4325],{"class":56,"line":560},[54,4320,949],{"class":67},[54,4322,952],{"class":67},[54,4324,2622],{"class":955},[54,4326,1018],{"class":77},[54,4328,4329,4331,4333,4335],{"class":56,"line":1231},[54,4330,3818],{"class":77},[54,4332,3908],{"class":67},[54,4334,3824],{"class":67},[54,4336,2219],{"class":77},[54,4338,4339,4341,4343,4345],{"class":56,"line":3319},[54,4340,3831],{"class":77},[54,4342,3908],{"class":67},[54,4344,3935],{"class":67},[54,4346,4347],{"class":77}," customers(id),\n",[54,4349,4350,4352],{"class":56,"line":3611},[54,4351,3841],{"class":77},[54,4353,4293],{"class":67},[54,4355,4356],{"class":56,"line":3756},[54,4357,279],{"class":77},[54,4359,4360,4362,4364,4367],{"class":56,"line":3762},[54,4361,949],{"class":67},[54,4363,952],{"class":67},[54,4365,4366],{"class":955}," customers",[54,4368,1018],{"class":77},[54,4370,4371,4373,4375,4377],{"class":56,"line":3768},[54,4372,3818],{"class":77},[54,4374,3908],{"class":67},[54,4376,3824],{"class":67},[54,4378,2219],{"class":77},[54,4380,4381,4384,4386],{"class":56,"line":3774},[54,4382,4383],{"class":67},"  name",[54,4385,3853],{"class":67},[54,4387,2219],{"class":77},[54,4389,4390,4393],{"class":56,"line":3780},[54,4391,4392],{"class":77},"  email ",[54,4394,4395],{"class":67},"TEXT\n",[54,4397,4399],{"class":56,"line":4398},22,[54,4400,279],{"class":77},[54,4402,4404],{"class":56,"line":4403},23,[54,4405,4406],{"class":60},"-- If JOINs are slow, add a covering index or materialized view — don't duplicate data\n",[32,4408,4410,4411,373,4413],{"id":4409},"_32-serial-vs-identity","32. ",[37,4412,3821],{},[37,4414,4415],{},"IDENTITY",[42,4417,4418],{"language":44},[46,4419,4421],{"className":48,"code":4420,"language":44,"meta":50,"style":50},"-- ❌ WRONG: SERIAL creates an implicit sequence that's hard to manage\nCREATE TABLE orders (id SERIAL PRIMARY KEY);\n-- The sequence is named orders_id_seq — renaming the table doesn't rename it\n-- pg_dump may not restore the sequence correctly across schema changes\n-- Can't easily override the value in INSERT (must use OVERRIDING SYSTEM VALUE)\n\n-- ✅ RIGHT: IDENTITY columns (SQL standard, PG 10+)\nCREATE TABLE orders (\n  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY\n);\n-- Sequence is managed internally, tied to the column\n-- GENERATED ALWAYS prevents accidental INSERT of explicit values\n-- For controlled overrides: INSERT INTO orders (id, ...) OVERRIDING SYSTEM VALUE VALUES (100, ...)\n-- IDENTITY is SQL standard, more portable, and cleaner for schema management\n",[37,4422,4423,4428,4445,4450,4455,4460,4464,4469,4479,4496,4500,4505,4510,4515],{"__ignoreMap":50},[54,4424,4425],{"class":56,"line":57},[54,4426,4427],{"class":60},"-- ❌ WRONG: SERIAL creates an implicit sequence that's hard to manage\n",[54,4429,4430,4432,4434,4436,4439,4441,4443],{"class":56,"line":64},[54,4431,949],{"class":67},[54,4433,952],{"class":67},[54,4435,2622],{"class":955},[54,4437,4438],{"class":77}," (id ",[54,4440,3821],{"class":67},[54,4442,3824],{"class":67},[54,4444,279],{"class":77},[54,4446,4447],{"class":56,"line":96},[54,4448,4449],{"class":60},"-- The sequence is named orders_id_seq — renaming the table doesn't rename it\n",[54,4451,4452],{"class":56,"line":102},[54,4453,4454],{"class":60},"-- pg_dump may not restore the sequence correctly across schema changes\n",[54,4456,4457],{"class":56,"line":109},[54,4458,4459],{"class":60},"-- Can't easily override the value in INSERT (must use OVERRIDING SYSTEM VALUE)\n",[54,4461,4462],{"class":56,"line":115},[54,4463,106],{"emptyLinePlaceholder":105},[54,4465,4466],{"class":56,"line":216},[54,4467,4468],{"class":60},"-- ✅ RIGHT: IDENTITY columns (SQL standard, PG 10+)\n",[54,4470,4471,4473,4475,4477],{"class":56,"line":221},[54,4472,949],{"class":67},[54,4474,952],{"class":67},[54,4476,2622],{"class":955},[54,4478,1018],{"class":77},[54,4480,4481,4483,4485,4487,4489,4491,4493],{"class":56,"line":227},[54,4482,3818],{"class":77},[54,4484,3908],{"class":67},[54,4486,3911],{"class":67},[54,4488,3914],{"class":67},[54,4490,3917],{"class":67},[54,4492,3920],{"class":67},[54,4494,4495],{"class":67}," PRIMARY KEY\n",[54,4497,4498],{"class":56,"line":238},[54,4499,279],{"class":77},[54,4501,4502],{"class":56,"line":520},[54,4503,4504],{"class":60},"-- Sequence is managed internally, tied to the column\n",[54,4506,4507],{"class":56,"line":547},[54,4508,4509],{"class":60},"-- GENERATED ALWAYS prevents accidental INSERT of explicit values\n",[54,4511,4512],{"class":56,"line":560},[54,4513,4514],{"class":60},"-- For controlled overrides: INSERT INTO orders (id, ...) OVERRIDING SYSTEM VALUE VALUES (100, ...)\n",[54,4516,4517],{"class":56,"line":1231},[54,4518,4519],{"class":60},"-- IDENTITY is SQL standard, more portable, and cleaner for schema management\n",[27,4521,4523],{"id":4522},"group-by-traps","GROUP BY Traps",[32,4525,4527],{"id":4526},"_33-column-in-select-but-not-in-group-by-or-aggregated","33. Column in SELECT but Not in GROUP BY or Aggregated",[42,4529,4530],{"language":44},[46,4531,4533],{"className":48,"code":4532,"language":44,"meta":50,"style":50},"-- ❌ WRONG: amount is neither in GROUP BY nor aggregated\nSELECT customer_id, amount FROM orders GROUP BY customer_id;\n-- ERROR: column \"orders.amount\" must appear in GROUP BY or be used in an aggregate\n\n-- ✅ RIGHT: aggregate it or add to GROUP BY\nSELECT customer_id, SUM(amount) FROM orders GROUP BY customer_id;\n-- Or: SELECT customer_id, amount FROM orders GROUP BY customer_id, amount;\n",[37,4534,4535,4540,4556,4561,4565,4570,4589],{"__ignoreMap":50},[54,4536,4537],{"class":56,"line":57},[54,4538,4539],{"class":60},"-- ❌ WRONG: amount is neither in GROUP BY nor aggregated\n",[54,4541,4542,4544,4547,4549,4551,4553],{"class":56,"line":64},[54,4543,68],{"class":67},[54,4545,4546],{"class":77}," customer_id, amount ",[54,4548,205],{"class":67},[54,4550,695],{"class":77},[54,4552,466],{"class":67},[54,4554,4555],{"class":77}," customer_id;\n",[54,4557,4558],{"class":56,"line":96},[54,4559,4560],{"class":60},"-- ERROR: column \"orders.amount\" must appear in GROUP BY or be used in an aggregate\n",[54,4562,4563],{"class":56,"line":102},[54,4564,106],{"emptyLinePlaceholder":105},[54,4566,4567],{"class":56,"line":109},[54,4568,4569],{"class":60},"-- ✅ RIGHT: aggregate it or add to GROUP BY\n",[54,4571,4572,4574,4577,4579,4581,4583,4585,4587],{"class":56,"line":115},[54,4573,68],{"class":67},[54,4575,4576],{"class":77}," customer_id, ",[54,4578,663],{"class":182},[54,4580,598],{"class":77},[54,4582,205],{"class":67},[54,4584,695],{"class":77},[54,4586,466],{"class":67},[54,4588,4555],{"class":77},[54,4590,4591],{"class":56,"line":216},[54,4592,4593],{"class":60},"-- Or: SELECT customer_id, amount FROM orders GROUP BY customer_id, amount;\n",[32,4595,4597],{"id":4596},"_34-aggregate-in-where","34. Aggregate in WHERE",[42,4599,4600],{"language":44},[46,4601,4603],{"className":48,"code":4602,"language":44,"meta":50,"style":50},"-- ❌ WRONG: WHERE is evaluated per-row, aggregates work on groups\nSELECT customer_id FROM orders WHERE SUM(amount) > 100 GROUP BY customer_id;\n-- ERROR: aggregate functions are not allowed in WHERE\n\n-- ✅ RIGHT: use HAVING (evaluated after GROUP BY, on aggregates)\nSELECT customer_id FROM orders GROUP BY customer_id HAVING SUM(amount) > 100;\n",[37,4604,4605,4610,4635,4640,4644,4649],{"__ignoreMap":50},[54,4606,4607],{"class":56,"line":57},[54,4608,4609],{"class":60},"-- ❌ WRONG: WHERE is evaluated per-row, aggregates work on groups\n",[54,4611,4612,4614,4616,4618,4620,4622,4624,4626,4628,4630,4633],{"class":56,"line":64},[54,4613,68],{"class":67},[54,4615,202],{"class":77},[54,4617,205],{"class":67},[54,4619,695],{"class":77},[54,4621,81],{"class":67},[54,4623,683],{"class":182},[54,4625,598],{"class":77},[54,4627,970],{"class":67},[54,4629,1146],{"class":182},[54,4631,4632],{"class":67}," GROUP BY",[54,4634,4555],{"class":77},[54,4636,4637],{"class":56,"line":96},[54,4638,4639],{"class":60},"-- ERROR: aggregate functions are not allowed in WHERE\n",[54,4641,4642],{"class":56,"line":102},[54,4643,106],{"emptyLinePlaceholder":105},[54,4645,4646],{"class":56,"line":109},[54,4647,4648],{"class":60},"-- ✅ RIGHT: use HAVING (evaluated after GROUP BY, on aggregates)\n",[54,4650,4651,4653,4655,4657,4659,4661,4663,4666,4668,4670,4672,4674],{"class":56,"line":115},[54,4652,68],{"class":67},[54,4654,202],{"class":77},[54,4656,205],{"class":67},[54,4658,695],{"class":77},[54,4660,466],{"class":67},[54,4662,202],{"class":77},[54,4664,4665],{"class":67},"HAVING",[54,4667,683],{"class":182},[54,4669,598],{"class":77},[54,4671,970],{"class":67},[54,4673,1146],{"class":182},[54,4675,93],{"class":77},[32,4677,4679],{"id":4678},"_35-alias-in-where","35. Alias in WHERE",[42,4681,4682],{"language":44},[46,4683,4685],{"className":48,"code":4684,"language":44,"meta":50,"style":50},"-- ❌ WRONG: alias is created in SELECT, which runs after WHERE\nSELECT amount * 1.08 AS taxed FROM orders WHERE taxed > 100;\n-- ERROR: column \"taxed\" does not exist\n-- SQL execution order: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT\n\n-- ✅ RIGHT: repeat the expression or use an outer query\nSELECT amount * 1.08 AS taxed FROM orders WHERE amount * 1.08 > 100;\n-- Or: SELECT * FROM (SELECT amount * 1.08 AS taxed FROM orders) t WHERE taxed > 100;\n",[37,4686,4687,4692,4727,4732,4737,4741,4746,4786],{"__ignoreMap":50},[54,4688,4689],{"class":56,"line":57},[54,4690,4691],{"class":60},"-- ❌ WRONG: alias is created in SELECT, which runs after WHERE\n",[54,4693,4694,4696,4699,4701,4703,4705,4708,4710,4713,4715,4717,4719,4721,4723,4725],{"class":56,"line":64},[54,4695,68],{"class":67},[54,4697,4698],{"class":77}," amount ",[54,4700,422],{"class":67},[54,4702,252],{"class":182},[54,4704,25],{"class":77},[54,4706,4707],{"class":182},"08",[54,4709,3917],{"class":67},[54,4711,4712],{"class":77}," taxed ",[54,4714,205],{"class":67},[54,4716,695],{"class":77},[54,4718,81],{"class":67},[54,4720,4712],{"class":77},[54,4722,970],{"class":67},[54,4724,1146],{"class":182},[54,4726,93],{"class":77},[54,4728,4729],{"class":56,"line":96},[54,4730,4731],{"class":60},"-- ERROR: column \"taxed\" does not exist\n",[54,4733,4734],{"class":56,"line":102},[54,4735,4736],{"class":60},"-- SQL execution order: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT\n",[54,4738,4739],{"class":56,"line":109},[54,4740,106],{"emptyLinePlaceholder":105},[54,4742,4743],{"class":56,"line":115},[54,4744,4745],{"class":60},"-- ✅ RIGHT: repeat the expression or use an outer query\n",[54,4747,4748,4750,4752,4754,4756,4758,4760,4762,4764,4766,4768,4770,4772,4774,4776,4778,4780,4782,4784],{"class":56,"line":216},[54,4749,68],{"class":67},[54,4751,4698],{"class":77},[54,4753,422],{"class":67},[54,4755,252],{"class":182},[54,4757,25],{"class":77},[54,4759,4707],{"class":182},[54,4761,3917],{"class":67},[54,4763,4712],{"class":77},[54,4765,205],{"class":67},[54,4767,695],{"class":77},[54,4769,81],{"class":67},[54,4771,4698],{"class":77},[54,4773,422],{"class":67},[54,4775,252],{"class":182},[54,4777,25],{"class":77},[54,4779,4707],{"class":182},[54,4781,1143],{"class":67},[54,4783,1146],{"class":182},[54,4785,93],{"class":77},[54,4787,4788],{"class":56,"line":221},[54,4789,4790],{"class":60},"-- Or: SELECT * FROM (SELECT amount * 1.08 AS taxed FROM orders) t WHERE taxed > 100;\n",[27,4792,4794],{"id":4793},"ordering-and-pagination-traps","Ordering and Pagination Traps",[32,4796,4798,4799,4801,4802,4804],{"id":4797},"_36-limit-without-order-by-is-non-deterministic","36. ",[37,4800,1974],{}," Without ",[37,4803,1968],{}," Is Non-Deterministic",[42,4806,4807],{"language":44},[46,4808,4810],{"className":48,"code":4809,"language":44,"meta":50,"style":50},"-- ❌ WRONG: no ORDER BY → arbitrary rows (depends on physical layout)\nSELECT * FROM orders LIMIT 10;\n-- Returns whatever 10 rows the scan happens to hit first — may differ between runs\n\n-- ✅ RIGHT: always pair LIMIT with ORDER BY on a unique key\nSELECT * FROM orders ORDER BY id LIMIT 10;\n",[37,4811,4812,4817,4833,4838,4842,4847],{"__ignoreMap":50},[54,4813,4814],{"class":56,"line":57},[54,4815,4816],{"class":60},"-- ❌ WRONG: no ORDER BY → arbitrary rows (depends on physical layout)\n",[54,4818,4819,4821,4823,4825,4827,4829,4831],{"class":56,"line":64},[54,4820,68],{"class":67},[54,4822,71],{"class":67},[54,4824,74],{"class":67},[54,4826,695],{"class":77},[54,4828,1974],{"class":67},[54,4830,3229],{"class":182},[54,4832,93],{"class":77},[54,4834,4835],{"class":56,"line":96},[54,4836,4837],{"class":60},"-- Returns whatever 10 rows the scan happens to hit first — may differ between runs\n",[54,4839,4840],{"class":56,"line":102},[54,4841,106],{"emptyLinePlaceholder":105},[54,4843,4844],{"class":56,"line":109},[54,4845,4846],{"class":60},"-- ✅ RIGHT: always pair LIMIT with ORDER BY on a unique key\n",[54,4848,4849,4851,4853,4855,4857,4859,4861,4863,4865],{"class":56,"line":115},[54,4850,68],{"class":67},[54,4852,71],{"class":67},[54,4854,74],{"class":67},[54,4856,695],{"class":77},[54,4858,1968],{"class":67},[54,4860,1971],{"class":77},[54,4862,1974],{"class":67},[54,4864,3229],{"class":182},[54,4866,93],{"class":77},[32,4868,4870],{"id":4869},"_37-pagination-without-a-unique-tie-breaker","37. Pagination Without a Unique Tie-Breaker",[42,4872,4873],{"language":44},[46,4874,4876],{"className":48,"code":4875,"language":44,"meta":50,"style":50},"-- ❌ WRONG: ORDER BY ordered_on isn't unique — tied rows drift between pages\nSELECT * FROM orders ORDER BY ordered_on DESC LIMIT 10 OFFSET 10;\n-- On page 1 you get rows with ordered_on = '2024-06-15'\n-- On page 2, if the physical order changed (VACUUM, new index), different\n-- rows with the same ordered_on appear → duplicate or skipped rows\n\n-- ✅ RIGHT: add a unique tie-breaker\nSELECT * FROM orders ORDER BY ordered_on DESC, id DESC LIMIT 10 OFFSET 10;\n-- id is unique → the sort is total → no ambiguity → no drift\n",[37,4877,4878,4883,4910,4915,4920,4925,4929,4934,4964],{"__ignoreMap":50},[54,4879,4880],{"class":56,"line":57},[54,4881,4882],{"class":60},"-- ❌ WRONG: ORDER BY ordered_on isn't unique — tied rows drift between pages\n",[54,4884,4885,4887,4889,4891,4893,4895,4897,4899,4902,4904,4906,4908],{"class":56,"line":64},[54,4886,68],{"class":67},[54,4888,71],{"class":67},[54,4890,74],{"class":67},[54,4892,695],{"class":77},[54,4894,1968],{"class":67},[54,4896,2047],{"class":77},[54,4898,2050],{"class":67},[54,4900,4901],{"class":67}," LIMIT",[54,4903,3229],{"class":182},[54,4905,1980],{"class":77},[54,4907,2701],{"class":182},[54,4909,93],{"class":77},[54,4911,4912],{"class":56,"line":96},[54,4913,4914],{"class":60},"-- On page 1 you get rows with ordered_on = '2024-06-15'\n",[54,4916,4917],{"class":56,"line":102},[54,4918,4919],{"class":60},"-- On page 2, if the physical order changed (VACUUM, new index), different\n",[54,4921,4922],{"class":56,"line":109},[54,4923,4924],{"class":60},"-- rows with the same ordered_on appear → duplicate or skipped rows\n",[54,4926,4927],{"class":56,"line":115},[54,4928,106],{"emptyLinePlaceholder":105},[54,4930,4931],{"class":56,"line":216},[54,4932,4933],{"class":60},"-- ✅ RIGHT: add a unique tie-breaker\n",[54,4935,4936,4938,4940,4942,4944,4946,4948,4950,4952,4954,4956,4958,4960,4962],{"class":56,"line":221},[54,4937,68],{"class":67},[54,4939,71],{"class":67},[54,4941,74],{"class":67},[54,4943,695],{"class":77},[54,4945,1968],{"class":67},[54,4947,2047],{"class":77},[54,4949,2050],{"class":67},[54,4951,2053],{"class":77},[54,4953,2050],{"class":67},[54,4955,4901],{"class":67},[54,4957,3229],{"class":182},[54,4959,1980],{"class":77},[54,4961,2701],{"class":182},[54,4963,93],{"class":77},[54,4965,4966],{"class":56,"line":227},[54,4967,4968],{"class":60},"-- id is unique → the sort is total → no ambiguity → no drift\n",[32,4970,4972],{"id":4971},"_38-null-sort-order-is-engine-specific","38. NULL Sort Order Is Engine-Specific",[42,4974,4975],{"language":44},[46,4976,4978],{"className":48,"code":4977,"language":44,"meta":50,"style":50},"-- ❌ WRONG: assuming NULLs sort first (or last) across all engines\n-- PostgreSQL: NULLs sort LAST by default in ASC, FIRST in DESC\n-- MySQL: NULLs sort FIRST in ASC, LAST in DESC\n-- SQLite: NULLs sort FIRST in ASC\n\n-- ✅ RIGHT: specify explicitly\nSELECT * FROM orders ORDER BY amount DESC NULLS LAST;\n-- Or portable: ORDER BY (amount IS NULL), amount DESC\n",[37,4979,4980,4985,4990,4995,5000,5004,5009,5033],{"__ignoreMap":50},[54,4981,4982],{"class":56,"line":57},[54,4983,4984],{"class":60},"-- ❌ WRONG: assuming NULLs sort first (or last) across all engines\n",[54,4986,4987],{"class":56,"line":64},[54,4988,4989],{"class":60},"-- PostgreSQL: NULLs sort LAST by default in ASC, FIRST in DESC\n",[54,4991,4992],{"class":56,"line":96},[54,4993,4994],{"class":60},"-- MySQL: NULLs sort FIRST in ASC, LAST in DESC\n",[54,4996,4997],{"class":56,"line":102},[54,4998,4999],{"class":60},"-- SQLite: NULLs sort FIRST in ASC\n",[54,5001,5002],{"class":56,"line":109},[54,5003,106],{"emptyLinePlaceholder":105},[54,5005,5006],{"class":56,"line":115},[54,5007,5008],{"class":60},"-- ✅ RIGHT: specify explicitly\n",[54,5010,5011,5013,5015,5017,5019,5021,5023,5025,5028,5031],{"class":56,"line":216},[54,5012,68],{"class":67},[54,5014,71],{"class":67},[54,5016,74],{"class":67},[54,5018,695],{"class":77},[54,5020,1968],{"class":67},[54,5022,4698],{"class":77},[54,5024,2050],{"class":67},[54,5026,5027],{"class":67}," NULLS",[54,5029,5030],{"class":67}," LAST",[54,5032,93],{"class":77},[54,5034,5035],{"class":56,"line":221},[54,5036,5037],{"class":60},"-- Or portable: ORDER BY (amount IS NULL), amount DESC\n",[27,5039,5041],{"id":5040},"index-traps","Index Traps",[32,5043,5045],{"id":5044},"_39-composite-index-leftmost-prefix-rule","39. Composite Index Leftmost-Prefix Rule",[42,5047,5048],{"language":44},[46,5049,5051],{"className":48,"code":5050,"language":44,"meta":50,"style":50},"-- ❌ WRONG: expecting (customer_id, ordered_on) to serve WHERE ordered_on = ...\nCREATE INDEX ON orders(customer_id, ordered_on);\nSELECT * FROM orders WHERE ordered_on >= '2024-01-01';\n-- Can't use the index — ordered_on is the second column, not the prefix\n-- B-tree indexes are sorted by customer_id first, then ordered_on within each\n-- customer_id. Without a customer_id filter, the index can't narrow the search.\n\n-- ✅ RIGHT: add a separate index for the non-prefix column\nCREATE INDEX ON orders(ordered_on);\n-- Or: if ordered_on-only queries dominate, reorder the composite:\nCREATE INDEX ON orders(ordered_on, customer_id);\n",[37,5052,5053,5058,5068,5090,5095,5100,5105,5109,5114,5124,5129],{"__ignoreMap":50},[54,5054,5055],{"class":56,"line":57},[54,5056,5057],{"class":60},"-- ❌ WRONG: expecting (customer_id, ordered_on) to serve WHERE ordered_on = ...\n",[54,5059,5060,5062,5064,5066],{"class":56,"line":64},[54,5061,949],{"class":67},[54,5063,1891],{"class":67},[54,5065,1894],{"class":955},[54,5067,4134],{"class":77},[54,5069,5070,5072,5074,5076,5078,5080,5082,5085,5088],{"class":56,"line":96},[54,5071,68],{"class":67},[54,5073,71],{"class":67},[54,5075,74],{"class":67},[54,5077,695],{"class":77},[54,5079,81],{"class":67},[54,5081,2047],{"class":77},[54,5083,5084],{"class":67},">=",[54,5086,5087],{"class":318}," '2024-01-01'",[54,5089,93],{"class":77},[54,5091,5092],{"class":56,"line":102},[54,5093,5094],{"class":60},"-- Can't use the index — ordered_on is the second column, not the prefix\n",[54,5096,5097],{"class":56,"line":109},[54,5098,5099],{"class":60},"-- B-tree indexes are sorted by customer_id first, then ordered_on within each\n",[54,5101,5102],{"class":56,"line":115},[54,5103,5104],{"class":60},"-- customer_id. Without a customer_id filter, the index can't narrow the search.\n",[54,5106,5107],{"class":56,"line":216},[54,5108,106],{"emptyLinePlaceholder":105},[54,5110,5111],{"class":56,"line":221},[54,5112,5113],{"class":60},"-- ✅ RIGHT: add a separate index for the non-prefix column\n",[54,5115,5116,5118,5120,5122],{"class":56,"line":227},[54,5117,949],{"class":67},[54,5119,1891],{"class":67},[54,5121,1894],{"class":955},[54,5123,4097],{"class":77},[54,5125,5126],{"class":56,"line":238},[54,5127,5128],{"class":60},"-- Or: if ordered_on-only queries dominate, reorder the composite:\n",[54,5130,5131,5133,5135,5137],{"class":56,"line":520},[54,5132,949],{"class":67},[54,5134,1891],{"class":67},[54,5136,1894],{"class":955},[54,5138,5139],{"class":77}," orders(ordered_on, customer_id);\n",[32,5141,5143,5144,5146,5147,5150],{"id":5142},"_40-rarely-uses-indexes","40. ",[37,5145,286],{}," \u002F ",[37,5148,5149],{},"\u003C>"," Rarely Uses Indexes",[42,5152,5153],{"language":44},[46,5154,5156],{"className":48,"code":5155,"language":44,"meta":50,"style":50},"-- ❌ WRONG: != matches most rows → index is more expensive than seq scan\nSELECT * FROM orders WHERE status != 'cancelled';\n-- If 99% of rows are not cancelled, the index would fetch 99% of the table\n-- via random I\u002FO — a seq scan is cheaper (sequential is cheaper than random)\n\n-- ✅ RIGHT: use a positive form (indexable) or a partial index\nSELECT * FROM orders WHERE status IN ('pending', 'paid', 'shipped');\n-- Or: partial index excluding the common case\nCREATE INDEX ON orders(id) WHERE status != 'cancelled';\n",[37,5157,5158,5163,5185,5190,5195,5199,5204,5234,5239],{"__ignoreMap":50},[54,5159,5160],{"class":56,"line":57},[54,5161,5162],{"class":60},"-- ❌ WRONG: != matches most rows → index is more expensive than seq scan\n",[54,5164,5165,5167,5169,5171,5173,5175,5177,5180,5183],{"class":56,"line":64},[54,5166,68],{"class":67},[54,5168,71],{"class":67},[54,5170,74],{"class":67},[54,5172,695],{"class":77},[54,5174,81],{"class":67},[54,5176,3524],{"class":67},[54,5178,5179],{"class":67}," !=",[54,5181,5182],{"class":318}," 'cancelled'",[54,5184,93],{"class":77},[54,5186,5187],{"class":56,"line":96},[54,5188,5189],{"class":60},"-- If 99% of rows are not cancelled, the index would fetch 99% of the table\n",[54,5191,5192],{"class":56,"line":102},[54,5193,5194],{"class":60},"-- via random I\u002FO — a seq scan is cheaper (sequential is cheaper than random)\n",[54,5196,5197],{"class":56,"line":109},[54,5198,106],{"emptyLinePlaceholder":105},[54,5200,5201],{"class":56,"line":115},[54,5202,5203],{"class":60},"-- ✅ RIGHT: use a positive form (indexable) or a partial index\n",[54,5205,5206,5208,5210,5212,5214,5216,5218,5220,5222,5224,5226,5228,5230,5232],{"class":56,"line":216},[54,5207,68],{"class":67},[54,5209,71],{"class":67},[54,5211,74],{"class":67},[54,5213,695],{"class":77},[54,5215,81],{"class":67},[54,5217,3524],{"class":67},[54,5219,194],{"class":67},[54,5221,197],{"class":77},[54,5223,3995],{"class":318},[54,5225,413],{"class":77},[54,5227,4000],{"class":318},[54,5229,413],{"class":77},[54,5231,4005],{"class":318},[54,5233,279],{"class":77},[54,5235,5236],{"class":56,"line":221},[54,5237,5238],{"class":60},"-- Or: partial index excluding the common case\n",[54,5240,5241,5243,5245,5247,5250,5252,5254,5256,5258],{"class":56,"line":227},[54,5242,949],{"class":67},[54,5244,1891],{"class":67},[54,5246,1894],{"class":955},[54,5248,5249],{"class":77}," orders(id) ",[54,5251,81],{"class":67},[54,5253,3524],{"class":67},[54,5255,5179],{"class":67},[54,5257,5182],{"class":318},[54,5259,93],{"class":77},[27,5261,5263],{"id":5262},"ddl-traps","DDL Traps",[32,5265,5267,5268,5271],{"id":5266},"_41-alter-column-type-rewrites-the-table","41. ",[37,5269,5270],{},"ALTER COLUMN ... TYPE"," Rewrites the Table",[42,5273,5274],{"language":44},[46,5275,5277],{"className":48,"code":5276,"language":44,"meta":50,"style":50},"-- ❌ WRONG: changing column type rewrites the entire table\nALTER TABLE orders ALTER COLUMN amount TYPE NUMERIC(12,2);\n-- Takes ACCESS EXCLUSIVE lock — no reads or writes until complete\n-- On a billion-row table, this is hours of downtime\n\n-- ✅ RIGHT: add new column, backfill in batches, swap, drop old\nALTER TABLE orders ADD COLUMN amount_new NUMERIC(12,2);\n-- Backfill in batches (no long lock):\nUPDATE orders SET amount_new = amount WHERE id BETWEEN 1 AND 10000;\nUPDATE orders SET amount_new = amount WHERE id BETWEEN 10001 AND 20000;\n-- ... etc\nALTER TABLE orders RENAME COLUMN amount TO amount_old;\nALTER TABLE orders RENAME COLUMN amount_new TO amount;\nALTER TABLE orders DROP COLUMN amount_old;\n-- Each step is brief; the batched updates don't hold a long lock\n",[37,5278,5279,5284,5314,5319,5324,5328,5333,5358,5363,5394,5424,5429,5444,5458,5472],{"__ignoreMap":50},[54,5280,5281],{"class":56,"line":57},[54,5282,5283],{"class":60},"-- ❌ WRONG: changing column type rewrites the entire table\n",[54,5285,5286,5288,5290,5292,5294,5297,5300,5303,5305,5308,5310,5312],{"class":56,"line":64},[54,5287,2387],{"class":67},[54,5289,952],{"class":67},[54,5291,695],{"class":77},[54,5293,2387],{"class":67},[54,5295,5296],{"class":77}," COLUMN amount ",[54,5298,5299],{"class":67},"TYPE",[54,5301,5302],{"class":67}," NUMERIC",[54,5304,419],{"class":77},[54,5306,5307],{"class":182},"12",[54,5309,2704],{"class":77},[54,5311,2657],{"class":182},[54,5313,279],{"class":77},[54,5315,5316],{"class":56,"line":96},[54,5317,5318],{"class":60},"-- Takes ACCESS EXCLUSIVE lock — no reads or writes until complete\n",[54,5320,5321],{"class":56,"line":102},[54,5322,5323],{"class":60},"-- On a billion-row table, this is hours of downtime\n",[54,5325,5326],{"class":56,"line":109},[54,5327,106],{"emptyLinePlaceholder":105},[54,5329,5330],{"class":56,"line":115},[54,5331,5332],{"class":60},"-- ✅ RIGHT: add new column, backfill in batches, swap, drop old\n",[54,5334,5335,5337,5339,5341,5343,5346,5348,5350,5352,5354,5356],{"class":56,"line":216},[54,5336,2387],{"class":67},[54,5338,952],{"class":67},[54,5340,695],{"class":77},[54,5342,2394],{"class":67},[54,5344,5345],{"class":77}," COLUMN amount_new ",[54,5347,962],{"class":67},[54,5349,419],{"class":77},[54,5351,5307],{"class":182},[54,5353,2704],{"class":77},[54,5355,2657],{"class":182},[54,5357,279],{"class":77},[54,5359,5360],{"class":56,"line":221},[54,5361,5362],{"class":60},"-- Backfill in batches (no long lock):\n",[54,5364,5365,5367,5369,5371,5374,5376,5378,5380,5382,5385,5387,5389,5392],{"class":56,"line":227},[54,5366,3210],{"class":67},[54,5368,695],{"class":77},[54,5370,3216],{"class":67},[54,5372,5373],{"class":77}," amount_new ",[54,5375,87],{"class":67},[54,5377,4698],{"class":77},[54,5379,81],{"class":67},[54,5381,1971],{"class":77},[54,5383,5384],{"class":67},"BETWEEN",[54,5386,252],{"class":182},[54,5388,1211],{"class":67},[54,5390,5391],{"class":182}," 10000",[54,5393,93],{"class":77},[54,5395,5396,5398,5400,5402,5404,5406,5408,5410,5412,5414,5417,5419,5422],{"class":56,"line":238},[54,5397,3210],{"class":67},[54,5399,695],{"class":77},[54,5401,3216],{"class":67},[54,5403,5373],{"class":77},[54,5405,87],{"class":67},[54,5407,4698],{"class":77},[54,5409,81],{"class":67},[54,5411,1971],{"class":77},[54,5413,5384],{"class":67},[54,5415,5416],{"class":182}," 10001",[54,5418,1211],{"class":67},[54,5420,5421],{"class":182}," 20000",[54,5423,93],{"class":77},[54,5425,5426],{"class":56,"line":520},[54,5427,5428],{"class":60},"-- ... etc\n",[54,5430,5431,5433,5435,5438,5441],{"class":56,"line":547},[54,5432,2387],{"class":67},[54,5434,952],{"class":67},[54,5436,5437],{"class":77}," orders RENAME COLUMN amount ",[54,5439,5440],{"class":67},"TO",[54,5442,5443],{"class":77}," amount_old;\n",[54,5445,5446,5448,5450,5453,5455],{"class":56,"line":560},[54,5447,2387],{"class":67},[54,5449,952],{"class":67},[54,5451,5452],{"class":77}," orders RENAME COLUMN amount_new ",[54,5454,5440],{"class":67},[54,5456,5457],{"class":77}," amount;\n",[54,5459,5460,5462,5464,5466,5469],{"class":56,"line":1231},[54,5461,2387],{"class":67},[54,5463,952],{"class":67},[54,5465,695],{"class":77},[54,5467,5468],{"class":67},"DROP",[54,5470,5471],{"class":77}," COLUMN amount_old;\n",[54,5473,5474],{"class":56,"line":3319},[54,5475,5476],{"class":60},"-- Each step is brief; the batched updates don't hold a long lock\n",[32,5478,5480,5481,5484],{"id":5479},"_42-truncate-doesnt-reset-sequences","42. ",[37,5482,5483],{},"TRUNCATE"," Doesn't Reset Sequences",[42,5486,5487],{"language":44},[46,5488,5490],{"className":48,"code":5489,"language":44,"meta":50,"style":50},"-- ❌ WRONG: TRUNCATE clears data but leaves the sequence at its old position\nTRUNCATE orders;\nINSERT INTO orders (customer_id, amount) VALUES (1, 100);\n-- id = 1000001 (continuing from where the sequence was before TRUNCATE)\n\n-- ✅ RIGHT: use RESTART IDENTITY to reset sequences\nTRUNCATE orders RESTART IDENTITY;\nINSERT INTO orders (customer_id, amount) VALUES (1, 100);\n-- id = 1 (sequence restarted)\n",[37,5491,5492,5497,5503,5523,5528,5532,5537,5550,5568],{"__ignoreMap":50},[54,5493,5494],{"class":56,"line":57},[54,5495,5496],{"class":60},"-- ❌ WRONG: TRUNCATE clears data but leaves the sequence at its old position\n",[54,5498,5499,5501],{"class":56,"line":64},[54,5500,5483],{"class":67},[54,5502,603],{"class":77},[54,5504,5505,5507,5510,5512,5514,5516,5518,5521],{"class":56,"line":96},[54,5506,981],{"class":67},[54,5508,5509],{"class":77}," orders (customer_id, amount) ",[54,5511,987],{"class":67},[54,5513,197],{"class":77},[54,5515,2647],{"class":182},[54,5517,413],{"class":77},[54,5519,5520],{"class":182},"100",[54,5522,279],{"class":77},[54,5524,5525],{"class":56,"line":102},[54,5526,5527],{"class":60},"-- id = 1000001 (continuing from where the sequence was before TRUNCATE)\n",[54,5529,5530],{"class":56,"line":109},[54,5531,106],{"emptyLinePlaceholder":105},[54,5533,5534],{"class":56,"line":115},[54,5535,5536],{"class":60},"-- ✅ RIGHT: use RESTART IDENTITY to reset sequences\n",[54,5538,5539,5541,5543,5546,5548],{"class":56,"line":216},[54,5540,5483],{"class":67},[54,5542,695],{"class":77},[54,5544,5545],{"class":67},"RESTART",[54,5547,3920],{"class":67},[54,5549,93],{"class":77},[54,5551,5552,5554,5556,5558,5560,5562,5564,5566],{"class":56,"line":221},[54,5553,981],{"class":67},[54,5555,5509],{"class":77},[54,5557,987],{"class":67},[54,5559,197],{"class":77},[54,5561,2647],{"class":182},[54,5563,413],{"class":77},[54,5565,5520],{"class":182},[54,5567,279],{"class":77},[54,5569,5570],{"class":56,"line":227},[54,5571,5572],{"class":60},"-- id = 1 (sequence restarted)\n",[27,5574,5576],{"id":5575},"security-traps","Security Traps",[32,5578,5580],{"id":5579},"_43-table-owner-bypasses-rls","43. Table Owner Bypasses RLS",[42,5582,5583],{"language":44},[46,5584,5586],{"className":48,"code":5585,"language":44,"meta":50,"style":50},"-- ❌ WRONG: app connects as the table owner — RLS doesn't apply\n-- By default, the table owner bypasses Row Level Security\nCREATE TABLE orders (id INT, tenant_id INT, amount NUMERIC);\nALTER TABLE orders ENABLE ROW LEVEL SECURITY;\nCREATE POLICY tenant_isolation ON orders USING (tenant_id = current_setting('app.tenant')::int);\n-- But if the app connects as the owner (postgres), RLS is bypassed!\n\n-- ✅ RIGHT: FORCE RLS, or connect as a non-owner role\nALTER TABLE orders FORCE ROW LEVEL SECURITY;\n-- Now even the owner is subject to RLS policies\n-- Better: use a dedicated app role (not the owner) for the application\n",[37,5587,5588,5593,5598,5623,5645,5680,5685,5689,5694,5713,5718],{"__ignoreMap":50},[54,5589,5590],{"class":56,"line":57},[54,5591,5592],{"class":60},"-- ❌ WRONG: app connects as the table owner — RLS doesn't apply\n",[54,5594,5595],{"class":56,"line":64},[54,5596,5597],{"class":60},"-- By default, the table owner bypasses Row Level Security\n",[54,5599,5600,5602,5604,5606,5608,5611,5614,5616,5619,5621],{"class":56,"line":96},[54,5601,949],{"class":67},[54,5603,952],{"class":67},[54,5605,2622],{"class":955},[54,5607,4438],{"class":77},[54,5609,5610],{"class":67},"INT",[54,5612,5613],{"class":77},", tenant_id ",[54,5615,5610],{"class":67},[54,5617,5618],{"class":77},", amount ",[54,5620,962],{"class":67},[54,5622,279],{"class":77},[54,5624,5625,5627,5629,5631,5634,5637,5640,5643],{"class":56,"line":102},[54,5626,2387],{"class":67},[54,5628,952],{"class":67},[54,5630,695],{"class":77},[54,5632,5633],{"class":67},"ENABLE",[54,5635,5636],{"class":67}," ROW",[54,5638,5639],{"class":67}," LEVEL",[54,5641,5642],{"class":67}," SECURITY",[54,5644,93],{"class":77},[54,5646,5647,5649,5652,5655,5657,5659,5661,5664,5666,5669,5672,5675,5678],{"class":56,"line":109},[54,5648,949],{"class":67},[54,5650,5651],{"class":67}," POLICY",[54,5653,5654],{"class":77}," tenant_isolation ",[54,5656,446],{"class":67},[54,5658,695],{"class":77},[54,5660,1646],{"class":67},[54,5662,5663],{"class":77}," (tenant_id ",[54,5665,87],{"class":67},[54,5667,5668],{"class":77}," current_setting(",[54,5670,5671],{"class":318},"'app.tenant'",[54,5673,5674],{"class":77},")::",[54,5676,5677],{"class":67},"int",[54,5679,279],{"class":77},[54,5681,5682],{"class":56,"line":115},[54,5683,5684],{"class":60},"-- But if the app connects as the owner (postgres), RLS is bypassed!\n",[54,5686,5687],{"class":56,"line":216},[54,5688,106],{"emptyLinePlaceholder":105},[54,5690,5691],{"class":56,"line":221},[54,5692,5693],{"class":60},"-- ✅ RIGHT: FORCE RLS, or connect as a non-owner role\n",[54,5695,5696,5698,5700,5702,5705,5707,5709,5711],{"class":56,"line":227},[54,5697,2387],{"class":67},[54,5699,952],{"class":67},[54,5701,695],{"class":77},[54,5703,5704],{"class":67},"FORCE",[54,5706,5636],{"class":67},[54,5708,5639],{"class":67},[54,5710,5642],{"class":67},[54,5712,93],{"class":77},[54,5714,5715],{"class":56,"line":238},[54,5716,5717],{"class":60},"-- Now even the owner is subject to RLS policies\n",[54,5719,5720],{"class":56,"line":520},[54,5721,5722],{"class":60},"-- Better: use a dedicated app role (not the owner) for the application\n",[32,5724,5726,5727,4801,5730],{"id":5725},"_44-security-definer-without-search_path","44. ",[37,5728,5729],{},"SECURITY DEFINER",[37,5731,5732],{},"search_path",[42,5734,5735],{"language":44},[46,5736,5738],{"className":48,"code":5737,"language":44,"meta":50,"style":50},"-- ❌ WRONG: definer function with default search_path — vulnerable to hijacking\nCREATE FUNCTION get_balance(p_id INT) RETURNS NUMERIC\nLANGUAGE plpgsql SECURITY DEFINER AS $$\nBEGIN\n  RETURN (SELECT balance FROM accounts WHERE id = p_id);  -- unqualified table name\nEND;\n$$;\n-- If an attacker can create a table named `accounts` in a schema that's\n-- earlier in search_path, the function reads from the attacker's table\n\n-- ✅ RIGHT: set search_path explicitly in the function\nCREATE FUNCTION get_balance(p_id INT) RETURNS NUMERIC\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path = public, pg_temp  -- pg_temp LAST — prevents temp table hijacking\nAS $$\nBEGIN\n  RETURN (SELECT balance FROM public.accounts WHERE id = p_id);  -- schema-qualified\nEND;\n$$;\n",[37,5739,5740,5745,5768,5787,5792,5819,5826,5831,5836,5841,5845,5850,5868,5879,5894,5900,5904,5935,5941],{"__ignoreMap":50},[54,5741,5742],{"class":56,"line":57},[54,5743,5744],{"class":60},"-- ❌ WRONG: definer function with default search_path — vulnerable to hijacking\n",[54,5746,5747,5749,5752,5755,5758,5760,5762,5765],{"class":56,"line":64},[54,5748,949],{"class":67},[54,5750,5751],{"class":67}," FUNCTION",[54,5753,5754],{"class":955}," get_balance",[54,5756,5757],{"class":77},"(p_id ",[54,5759,5610],{"class":67},[54,5761,425],{"class":77},[54,5763,5764],{"class":67},"RETURNS",[54,5766,5767],{"class":67}," NUMERIC\n",[54,5769,5770,5773,5776,5779,5782,5784],{"class":56,"line":96},[54,5771,5772],{"class":67},"LANGUAGE",[54,5774,5775],{"class":77}," plpgsql ",[54,5777,5778],{"class":67},"SECURITY",[54,5780,5781],{"class":77}," DEFINER ",[54,5783,428],{"class":67},[54,5785,5786],{"class":77}," $$\n",[54,5788,5789],{"class":56,"line":102},[54,5790,5791],{"class":67},"BEGIN\n",[54,5793,5794,5797,5799,5801,5803,5805,5807,5809,5811,5813,5816],{"class":56,"line":109},[54,5795,5796],{"class":67},"  RETURN",[54,5798,197],{"class":77},[54,5800,68],{"class":67},[54,5802,3219],{"class":77},[54,5804,205],{"class":67},[54,5806,3213],{"class":77},[54,5808,81],{"class":67},[54,5810,1971],{"class":77},[54,5812,87],{"class":67},[54,5814,5815],{"class":77}," p_id);  ",[54,5817,5818],{"class":60},"-- unqualified table name\n",[54,5820,5821,5824],{"class":56,"line":115},[54,5822,5823],{"class":67},"END",[54,5825,93],{"class":77},[54,5827,5828],{"class":56,"line":216},[54,5829,5830],{"class":77},"$$;\n",[54,5832,5833],{"class":56,"line":221},[54,5834,5835],{"class":60},"-- If an attacker can create a table named `accounts` in a schema that's\n",[54,5837,5838],{"class":56,"line":227},[54,5839,5840],{"class":60},"-- earlier in search_path, the function reads from the attacker's table\n",[54,5842,5843],{"class":56,"line":238},[54,5844,106],{"emptyLinePlaceholder":105},[54,5846,5847],{"class":56,"line":520},[54,5848,5849],{"class":60},"-- ✅ RIGHT: set search_path explicitly in the function\n",[54,5851,5852,5854,5856,5858,5860,5862,5864,5866],{"class":56,"line":547},[54,5853,949],{"class":67},[54,5855,5751],{"class":67},[54,5857,5754],{"class":955},[54,5859,5757],{"class":77},[54,5861,5610],{"class":67},[54,5863,425],{"class":77},[54,5865,5764],{"class":67},[54,5867,5767],{"class":67},[54,5869,5870,5872,5874,5876],{"class":56,"line":560},[54,5871,5772],{"class":67},[54,5873,5775],{"class":77},[54,5875,5778],{"class":67},[54,5877,5878],{"class":77}," DEFINER\n",[54,5880,5881,5883,5886,5888,5891],{"class":56,"line":1231},[54,5882,3216],{"class":67},[54,5884,5885],{"class":77}," search_path ",[54,5887,87],{"class":67},[54,5889,5890],{"class":77}," public, pg_temp  ",[54,5892,5893],{"class":60},"-- pg_temp LAST — prevents temp table hijacking\n",[54,5895,5896,5898],{"class":56,"line":3319},[54,5897,428],{"class":67},[54,5899,5786],{"class":77},[54,5901,5902],{"class":56,"line":3611},[54,5903,5791],{"class":67},[54,5905,5906,5908,5910,5912,5914,5916,5919,5921,5924,5926,5928,5930,5932],{"class":56,"line":3756},[54,5907,5796],{"class":67},[54,5909,197],{"class":77},[54,5911,68],{"class":67},[54,5913,3219],{"class":77},[54,5915,205],{"class":67},[54,5917,5918],{"class":182}," public",[54,5920,25],{"class":77},[54,5922,5923],{"class":182},"accounts",[54,5925,3232],{"class":67},[54,5927,1971],{"class":77},[54,5929,87],{"class":67},[54,5931,5815],{"class":77},[54,5933,5934],{"class":60},"-- schema-qualified\n",[54,5936,5937,5939],{"class":56,"line":3762},[54,5938,5823],{"class":67},[54,5940,93],{"class":77},[54,5942,5943],{"class":56,"line":3768},[54,5944,5830],{"class":77},[27,5946,5948],{"id":5947},"query-plan-traps","Query Plan Traps",[32,5950,5952,5953,5956],{"id":5951},"_45-materialized-cte-prevents-predicate-pushdown","45. ",[37,5954,5955],{},"MATERIALIZED"," CTE Prevents Predicate Pushdown",[42,5958,5959],{"language":44},[46,5960,5962],{"className":48,"code":5961,"language":44,"meta":50,"style":50},"-- ❌ WRONG: MATERIALIZED forces the full CTE to be computed before filtering\nWITH x AS MATERIALIZED (\n  SELECT * FROM orders  -- computes ALL orders (50M rows)\n)\nSELECT * FROM x WHERE customer_id = 42;\n-- The WHERE is applied AFTER the CTE materializes — scans all 50M rows\n\n-- ✅ RIGHT: drop MATERIALIZED (let the planner inline and push predicates)\nWITH x AS (\n  SELECT * FROM orders\n)\nSELECT * FROM x WHERE customer_id = 42;\n-- PG 12+ defaults to NOT MATERIALIZED for non-recursive CTEs\n-- The planner inlines → pushes WHERE into the CTE → uses index on customer_id\n",[37,5963,5964,5969,5982,5997,6002,6022,6027,6031,6036,6046,6056,6060,6080,6085],{"__ignoreMap":50},[54,5965,5966],{"class":56,"line":57},[54,5967,5968],{"class":60},"-- ❌ WRONG: MATERIALIZED forces the full CTE to be computed before filtering\n",[54,5970,5971,5974,5977,5979],{"class":56,"line":64},[54,5972,5973],{"class":67},"WITH",[54,5975,5976],{"class":77}," x ",[54,5978,428],{"class":67},[54,5980,5981],{"class":77}," MATERIALIZED (\n",[54,5983,5984,5987,5989,5991,5994],{"class":56,"line":96},[54,5985,5986],{"class":67},"  SELECT",[54,5988,71],{"class":67},[54,5990,74],{"class":67},[54,5992,5993],{"class":77}," orders  ",[54,5995,5996],{"class":60},"-- computes ALL orders (50M rows)\n",[54,5998,5999],{"class":56,"line":102},[54,6000,6001],{"class":77},")\n",[54,6003,6004,6006,6008,6010,6012,6014,6016,6018,6020],{"class":56,"line":109},[54,6005,68],{"class":67},[54,6007,71],{"class":67},[54,6009,74],{"class":67},[54,6011,5976],{"class":77},[54,6013,81],{"class":67},[54,6015,202],{"class":77},[54,6017,87],{"class":67},[54,6019,1768],{"class":182},[54,6021,93],{"class":77},[54,6023,6024],{"class":56,"line":115},[54,6025,6026],{"class":60},"-- The WHERE is applied AFTER the CTE materializes — scans all 50M rows\n",[54,6028,6029],{"class":56,"line":216},[54,6030,106],{"emptyLinePlaceholder":105},[54,6032,6033],{"class":56,"line":221},[54,6034,6035],{"class":60},"-- ✅ RIGHT: drop MATERIALIZED (let the planner inline and push predicates)\n",[54,6037,6038,6040,6042,6044],{"class":56,"line":227},[54,6039,5973],{"class":67},[54,6041,5976],{"class":77},[54,6043,428],{"class":67},[54,6045,1018],{"class":77},[54,6047,6048,6050,6052,6054],{"class":56,"line":238},[54,6049,5986],{"class":67},[54,6051,71],{"class":67},[54,6053,74],{"class":67},[54,6055,2015],{"class":77},[54,6057,6058],{"class":56,"line":520},[54,6059,6001],{"class":77},[54,6061,6062,6064,6066,6068,6070,6072,6074,6076,6078],{"class":56,"line":547},[54,6063,68],{"class":67},[54,6065,71],{"class":67},[54,6067,74],{"class":67},[54,6069,5976],{"class":77},[54,6071,81],{"class":67},[54,6073,202],{"class":77},[54,6075,87],{"class":67},[54,6077,1768],{"class":182},[54,6079,93],{"class":77},[54,6081,6082],{"class":56,"line":560},[54,6083,6084],{"class":60},"-- PG 12+ defaults to NOT MATERIALIZED for non-recursive CTEs\n",[54,6086,6087],{"class":56,"line":1231},[54,6088,6089],{"class":60},"-- The planner inlines → pushes WHERE into the CTE → uses index on customer_id\n",[27,6091,6093],{"id":6092},"spot-the-bug-comprehensive","🧠 Spot the Bug (Comprehensive)",[14,6095,6096],{},"A developer writes a report query and gets wrong results — NULL customers excluded, averages off, and pagination skips rows. Find all the issues:",[42,6098,6099],{"language":44},[46,6100,6102],{"className":48,"code":6101,"language":44,"meta":50,"style":50},"SELECT c.name, AVG(o.amount) AS avg_order\nFROM customers c\nLEFT JOIN orders o ON c.id = o.customer_id\nWHERE o.amount > 50\nORDER BY avg_order DESC\nLIMIT 10;\n",[37,6103,6104,6133,6139,6161,6176,6185],{"__ignoreMap":50},[54,6105,6106,6108,6110,6112,6114,6116,6118,6120,6122,6124,6126,6128,6130],{"class":56,"line":57},[54,6107,68],{"class":67},[54,6109,183],{"class":182},[54,6111,25],{"class":77},[54,6113,410],{"class":182},[54,6115,413],{"class":77},[54,6117,570],{"class":182},[54,6119,419],{"class":77},[54,6121,507],{"class":182},[54,6123,25],{"class":77},[54,6125,1140],{"class":182},[54,6127,425],{"class":77},[54,6129,428],{"class":67},[54,6131,6132],{"class":77}," avg_order\n",[54,6134,6135,6137],{"class":56,"line":64},[54,6136,205],{"class":67},[54,6138,175],{"class":77},[54,6140,6141,6143,6145,6147,6149,6151,6153,6155,6157,6159],{"class":56,"line":96},[54,6142,441],{"class":67},[54,6144,257],{"class":77},[54,6146,446],{"class":67},[54,6148,183],{"class":182},[54,6150,25],{"class":77},[54,6152,188],{"class":182},[54,6154,270],{"class":67},[54,6156,262],{"class":182},[54,6158,25],{"class":77},[54,6160,461],{"class":182},[54,6162,6163,6165,6167,6169,6171,6173],{"class":56,"line":102},[54,6164,81],{"class":67},[54,6166,262],{"class":182},[54,6168,25],{"class":77},[54,6170,1140],{"class":182},[54,6172,1143],{"class":67},[54,6174,6175],{"class":182}," 50\n",[54,6177,6178,6180,6183],{"class":56,"line":109},[54,6179,1968],{"class":67},[54,6181,6182],{"class":77}," avg_order ",[54,6184,2056],{"class":67},[54,6186,6187,6189,6191],{"class":56,"line":115},[54,6188,1974],{"class":67},[54,6190,3229],{"class":182},[54,6192,93],{"class":77},[6194,6195,6196,6200,6203,6282,6285,6412],"details",{},[6197,6198,6199],"summary",{},"Answer",[14,6201,6202],{},"Three issues:",[6204,6205,6206,6243,6265],"ol",{},[6207,6208,6209,6214,6215,6218,6219,6221,6222,6225,6226,6229,6230,6232,6233,6236,6237,6239,6240,25],"li",{},[18,6210,6211,6213],{},[37,6212,441],{}," demoted to INNER JOIN"," (trap #9): ",[37,6216,6217],{},"WHERE o.amount > 50"," filters on the right table in ",[37,6220,81],{},", dropping customers with no orders (the NULL-extended rows have ",[37,6223,6224],{},"o.amount = NULL",", and ",[37,6227,6228],{},"NULL > 50"," is UNKNOWN, not TRUE). The ",[37,6231,441],{}," is pointless — it behaves as an ",[37,6234,6235],{},"INNER JOIN",". Fix: move the filter to ",[37,6238,446],{},": ",[37,6241,6242],{},"LEFT JOIN orders o ON c.id = o.customer_id AND o.amount > 50",[6207,6244,6245,6250,6251,6254,6255,6258,6259,6261,6262,6264],{},[18,6246,6247,6249],{},[37,6248,570],{}," ignores NULLs"," (trap #5): for a customer with no orders > 50, ",[37,6252,6253],{},"AVG(o.amount)"," is NULL (no non-NULL values). If the intent was \"customers with no qualifying orders show 0,\" wrap: ",[37,6256,6257],{},"COALESCE(AVG(o.amount), 0)",". But if the intent is \"only show customers with qualifying orders,\" the ",[37,6260,6235],{}," (original behavior) is correct — just remove the ",[37,6263,441],{}," to make the intent explicit.",[6207,6266,6267,6270,6271,6274,6275,6278,6279,25],{},[18,6268,6269],{},"Pagination without a tie-breaker"," (trap #37): ",[37,6272,6273],{},"ORDER BY avg_order DESC LIMIT 10"," — ",[37,6276,6277],{},"avg_order"," isn't unique; tied customers can drift between pages. Fix: add a tie-breaker: ",[37,6280,6281],{},"ORDER BY avg_order DESC, c.id",[14,6283,6284],{},"The corrected query (assuming \"all customers, with their average order > 50, or 0 if none\"):",[42,6286,6287],{"language":44},[46,6288,6290],{"className":48,"code":6289,"language":44,"meta":50,"style":50},"SELECT c.name, COALESCE(AVG(o.amount), 0) AS avg_order\nFROM customers c\nLEFT JOIN orders o ON c.id = o.customer_id AND o.amount > 50\nGROUP BY c.id, c.name\nORDER BY avg_order DESC, c.id\nLIMIT 10;\n",[37,6291,6292,6328,6334,6368,6387,6404],{"__ignoreMap":50},[54,6293,6294,6296,6298,6300,6302,6304,6306,6308,6310,6312,6314,6316,6318,6320,6322,6324,6326],{"class":56,"line":57},[54,6295,68],{"class":67},[54,6297,183],{"class":182},[54,6299,25],{"class":77},[54,6301,410],{"class":182},[54,6303,413],{"class":77},[54,6305,628],{"class":182},[54,6307,419],{"class":77},[54,6309,570],{"class":182},[54,6311,419],{"class":77},[54,6313,507],{"class":182},[54,6315,25],{"class":77},[54,6317,1140],{"class":182},[54,6319,2307],{"class":77},[54,6321,634],{"class":182},[54,6323,425],{"class":77},[54,6325,428],{"class":67},[54,6327,6132],{"class":77},[54,6329,6330,6332],{"class":56,"line":64},[54,6331,205],{"class":67},[54,6333,175],{"class":77},[54,6335,6336,6338,6340,6342,6344,6346,6348,6350,6352,6354,6356,6358,6360,6362,6364,6366],{"class":56,"line":96},[54,6337,441],{"class":67},[54,6339,257],{"class":77},[54,6341,446],{"class":67},[54,6343,183],{"class":182},[54,6345,25],{"class":77},[54,6347,188],{"class":182},[54,6349,270],{"class":67},[54,6351,262],{"class":182},[54,6353,25],{"class":77},[54,6355,267],{"class":182},[54,6357,1211],{"class":67},[54,6359,262],{"class":182},[54,6361,25],{"class":77},[54,6363,1140],{"class":182},[54,6365,1143],{"class":67},[54,6367,6175],{"class":182},[54,6369,6370,6372,6374,6376,6378,6380,6382,6384],{"class":56,"line":102},[54,6371,466],{"class":67},[54,6373,183],{"class":182},[54,6375,25],{"class":77},[54,6377,188],{"class":182},[54,6379,413],{"class":77},[54,6381,1533],{"class":182},[54,6383,25],{"class":77},[54,6385,6386],{"class":182},"name\n",[54,6388,6389,6391,6393,6395,6397,6399,6401],{"class":56,"line":109},[54,6390,1968],{"class":67},[54,6392,6182],{"class":77},[54,6394,2050],{"class":67},[54,6396,413],{"class":77},[54,6398,1533],{"class":182},[54,6400,25],{"class":77},[54,6402,6403],{"class":182},"id\n",[54,6405,6406,6408,6410],{"class":56,"line":115},[54,6407,1974],{"class":67},[54,6409,3229],{"class":182},[54,6411,93],{"class":77},[14,6413,6414,6417,6418,6420,6421,2960,6423,6425,6426,6428,6429,6431],{},[18,6415,6416],{},"The lessons",": right-table filters go in ",[37,6419,446],{}," for a ",[37,6422,441],{},[37,6424,570],{}," of NULLs is NULL (use ",[37,6427,628],{}," if you want 0); ",[37,6430,1968],{}," for pagination needs a unique tie-breaker.",[6433,6434,6435],"style",{},"html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}",{"title":50,"searchDepth":64,"depth":64,"links":6437},[6438,6455,6464,6477,6485,6491,6498,6503,6509,6514,6520,6525,6529],{"id":29,"depth":64,"text":30,"children":6439},[6440,6442,6444,6446,6448,6450,6452,6453],{"id":34,"depth":96,"text":6441},"1. WHERE col = NULL Returns Nothing",{"id":137,"depth":96,"text":6443},"2. NOT IN with NULLs Returns Zero Rows",{"id":282,"depth":96,"text":6445},"3. != Silently Drops NULLs",{"id":368,"depth":96,"text":6447},"4. COUNT(*) vs COUNT(col) in a LEFT JOIN",{"id":566,"depth":96,"text":6449},"5. AVG Ignores NULLs, Doesn't Average Them as 0",{"id":659,"depth":96,"text":6451},"6. SUM of No Rows is NULL, Not 0",{"id":764,"depth":96,"text":765},{"id":922,"depth":96,"text":6454},"8. CHECK Constraint Allows NULLs",{"id":1054,"depth":64,"text":1055,"children":6456},[6457,6459,6460,6461,6463],{"id":1058,"depth":96,"text":6458},"9. LEFT JOIN + WHERE on the Right Table Demotes to INNER JOIN",{"id":1237,"depth":96,"text":1238},{"id":1359,"depth":96,"text":1360},{"id":1542,"depth":96,"text":6462},"12. NATURAL JOIN Joins on All Same-Named Columns",{"id":1652,"depth":96,"text":1653},{"id":1728,"depth":64,"text":1729,"children":6465},[6466,6468,6470,6472,6474,6475,6476],{"id":1732,"depth":96,"text":6467},"14. SELECT * in Production",{"id":1823,"depth":96,"text":6469},"15. Leading Wildcard LIKE Defeats the Index",{"id":1938,"depth":96,"text":6471},"16. OFFSET Pagination at Scale",{"id":2072,"depth":96,"text":6473},"17. NOT IN vs NOT EXISTS Performance",{"id":2193,"depth":96,"text":2194},{"id":2369,"depth":96,"text":2370},{"id":2450,"depth":96,"text":2451},{"id":2596,"depth":64,"text":2597,"children":6478},[6479,6480,6482,6483],{"id":2600,"depth":96,"text":2601},{"id":2758,"depth":96,"text":6481},"22. char(n) Padding and Comparison Surprises",{"id":2966,"depth":96,"text":2967},{"id":3048,"depth":96,"text":6484},"24. TIMESTAMP Without Time Zone",{"id":3159,"depth":64,"text":3160,"children":6486},[6487,6488,6489,6490],{"id":3163,"depth":96,"text":3164},{"id":3327,"depth":96,"text":3328},{"id":3455,"depth":96,"text":3456},{"id":3617,"depth":96,"text":3618},{"id":3786,"depth":64,"text":3787,"children":6492},[6493,6494,6495,6496],{"id":3790,"depth":96,"text":3791},{"id":4037,"depth":96,"text":4038},{"id":4218,"depth":96,"text":4219},{"id":4409,"depth":96,"text":6497},"32. SERIAL vs IDENTITY",{"id":4522,"depth":64,"text":4523,"children":6499},[6500,6501,6502],{"id":4526,"depth":96,"text":4527},{"id":4596,"depth":96,"text":4597},{"id":4678,"depth":96,"text":4679},{"id":4793,"depth":64,"text":4794,"children":6504},[6505,6507,6508],{"id":4797,"depth":96,"text":6506},"36. LIMIT Without ORDER BY Is Non-Deterministic",{"id":4869,"depth":96,"text":4870},{"id":4971,"depth":96,"text":4972},{"id":5040,"depth":64,"text":5041,"children":6510},[6511,6512],{"id":5044,"depth":96,"text":5045},{"id":5142,"depth":96,"text":6513},"40. != \u002F \u003C> Rarely Uses Indexes",{"id":5262,"depth":64,"text":5263,"children":6515},[6516,6518],{"id":5266,"depth":96,"text":6517},"41. ALTER COLUMN ... TYPE Rewrites the Table",{"id":5479,"depth":96,"text":6519},"42. TRUNCATE Doesn't Reset Sequences",{"id":5575,"depth":64,"text":5576,"children":6521},[6522,6523],{"id":5579,"depth":96,"text":5580},{"id":5725,"depth":96,"text":6524},"44. SECURITY DEFINER Without search_path",{"id":5947,"depth":64,"text":5948,"children":6526},[6527],{"id":5951,"depth":96,"text":6528},"45. MATERIALIZED CTE Prevents Predicate Pushdown",{"id":6092,"depth":64,"text":6093},"40+ traps that catch SQL developers, organized by category. Each trap shows the wrong code, explains the mechanism, and gives the idiomatic fix.","md",{},"\u002Fsql\u002F29-common-pitfalls",{"title":5,"description":6530},"sql\u002F29-common-pitfalls","Ic3WGcqJuhPpsyCdVRBMO2ZeodZ_HHG5Ad6D42xnvRY",1789924655198]