[{"data":1,"prerenderedAt":2388},["ShallowReactive",2],{"page-\u002Fsql\u002F04-joins":3},{"id":4,"title":5,"body":6,"description":16,"extension":2382,"meta":2383,"navigation":697,"path":2384,"seo":2385,"stem":2386,"__hash__":2387},"content\u002Fsql\u002F04-joins.md","04 — Joins",{"type":7,"value":8,"toc":2355},"minimark",[9,13,17,22,30,46,54,58,65,175,178,182,189,265,274,279,365,384,388,397,473,477,483,554,571,575,581,634,638,644,780,787,825,832,869,873,1205,1209,1212,1295,1298,1302,1309,1382,1475,1479,1716,1720,1778,1782,1868,1872,2029,2033,2040,2130,2312,2315,2351],[10,11,5],"h1",{"id":12},"_04-joins",[14,15,16],"p",{},"Joins combine rows from two (or more) tables based on a related column. They are the defining feature of relational databases — the \"relational\" in \"relational\" is realized through joins.",[18,19,21],"h2",{"id":20},"the-mental-model-cartesian-product-filter","The Mental Model: Cartesian Product + Filter",[14,23,24,25,29],{},"A join produces a ",[26,27,28],"strong",{},"combined result set"," by matching rows from the left and right inputs. Think of it in two steps:",[31,32,33,40],"ol",{},[34,35,36,39],"li",{},[26,37,38],{},"Cartesian product"," — every row of the left table paired with every row of the right table.",[34,41,42,45],{},[26,43,44],{},"Filter"," — keep only the pairs that satisfy the join condition.",[14,47,48,49,53],{},"Different join types vary step 2's filter and which un-matched rows are kept. The optimizer doesn't actually compute the full Cartesian product (it uses nested loops, hashes, or merges), but the ",[50,51,52],"em",{},"result"," is as if it did.",[18,55,57],{"id":56},"inner-join","INNER JOIN",[14,59,60,61,64],{},"Keeps only rows that match in ",[26,62,63],{},"both"," tables.",[66,67,69],"code-wrapper",{"language":68},"sql",[70,71,75],"pre",{"className":72,"code":73,"language":68,"meta":74,"style":74},"language-sql shiki shiki-themes github-light github-dark","-- Customers and their orders (only customers who have orders)\n-- INNER JOIN = Cartesian product filtered by ON condition\nSELECT c.name, o.id AS order_id, o.amount\nFROM customers c\nINNER JOIN orders o ON c.id = o.customer_id;\n","",[76,77,78,87,93,135,144],"code",{"__ignoreMap":74},[79,80,83],"span",{"class":81,"line":82},"line",1,[79,84,86],{"class":85},"sdCPZ","-- Customers and their orders (only customers who have orders)\n",[79,88,90],{"class":81,"line":89},2,[79,91,92],{"class":85},"-- INNER JOIN = Cartesian product filtered by ON condition\n",[79,94,96,100,104,108,111,114,117,119,122,125,128,130,132],{"class":81,"line":95},3,[79,97,99],{"class":98},"svdQ7","SELECT",[79,101,103],{"class":102},"snvgF"," c",[79,105,107],{"class":106},"ssxIu",".",[79,109,110],{"class":102},"name",[79,112,113],{"class":106},", ",[79,115,116],{"class":102},"o",[79,118,107],{"class":106},[79,120,121],{"class":102},"id",[79,123,124],{"class":98}," AS",[79,126,127],{"class":106}," order_id, ",[79,129,116],{"class":102},[79,131,107],{"class":106},[79,133,134],{"class":102},"amount\n",[79,136,138,141],{"class":81,"line":137},4,[79,139,140],{"class":98},"FROM",[79,142,143],{"class":106}," customers c\n",[79,145,147,149,152,155,157,159,161,164,167,169,172],{"class":81,"line":146},5,[79,148,57],{"class":98},[79,150,151],{"class":106}," orders o ",[79,153,154],{"class":98},"ON",[79,156,103],{"class":102},[79,158,107],{"class":106},[79,160,121],{"class":102},[79,162,163],{"class":98}," =",[79,165,166],{"class":102}," o",[79,168,107],{"class":106},[79,170,171],{"class":102},"customer_id",[79,173,174],{"class":106},";\n",[14,176,177],{},"Unmatched rows (customers with no orders, orders with no customer) are dropped.",[18,179,181],{"id":180},"left-outer-join","LEFT (OUTER) JOIN",[14,183,184,185,188],{},"Keeps ",[26,186,187],{},"all rows from the left"," table, with NULLs for unmatched right rows.",[66,190,191],{"language":68},[70,192,194],{"className":72,"code":193,"language":68,"meta":74,"style":74},"-- All customers, with their orders if any (NULLs for customers with no orders)\n-- Left rows preserved; right columns are NULL when no match found\nSELECT c.name, o.id AS order_id, o.amount\nFROM customers c\nLEFT JOIN orders o ON c.id = o.customer_id;\n",[76,195,196,201,206,234,240],{"__ignoreMap":74},[79,197,198],{"class":81,"line":82},[79,199,200],{"class":85},"-- All customers, with their orders if any (NULLs for customers with no orders)\n",[79,202,203],{"class":81,"line":89},[79,204,205],{"class":85},"-- Left rows preserved; right columns are NULL when no match found\n",[79,207,208,210,212,214,216,218,220,222,224,226,228,230,232],{"class":81,"line":95},[79,209,99],{"class":98},[79,211,103],{"class":102},[79,213,107],{"class":106},[79,215,110],{"class":102},[79,217,113],{"class":106},[79,219,116],{"class":102},[79,221,107],{"class":106},[79,223,121],{"class":102},[79,225,124],{"class":98},[79,227,127],{"class":106},[79,229,116],{"class":102},[79,231,107],{"class":106},[79,233,134],{"class":102},[79,235,236,238],{"class":81,"line":137},[79,237,140],{"class":98},[79,239,143],{"class":106},[79,241,242,245,247,249,251,253,255,257,259,261,263],{"class":81,"line":146},[79,243,244],{"class":98},"LEFT JOIN",[79,246,151],{"class":106},[79,248,154],{"class":98},[79,250,103],{"class":102},[79,252,107],{"class":106},[79,254,121],{"class":102},[79,256,163],{"class":98},[79,258,166],{"class":102},[79,260,107],{"class":106},[79,262,171],{"class":102},[79,264,174],{"class":106},[14,266,267,268,113,271,107],{},"Customers without orders appear once with ",[76,269,270],{},"order_id = NULL",[76,272,273],{},"amount = NULL",[275,276,278],"h3",{"id":277},"left-join-anti-join-pattern","LEFT JOIN Anti-Join Pattern",[66,280,281],{"language":68},[70,282,284],{"className":72,"code":283,"language":68,"meta":74,"style":74},"-- Customers who have never placed an order — the idiomatic \"anti-join\"\n-- LEFT JOIN produces NULL-extended rows for unmatched customers,\n-- then WHERE o.id IS NULL filters to only those unmatched rows.\nSELECT c.*\nFROM customers c\nLEFT JOIN orders o ON c.id = o.customer_id\nWHERE o.id IS NULL;                            -- test a NON-NULLABLE column (PK), not o.amount\n",[76,285,286,291,296,301,311,317,341],{"__ignoreMap":74},[79,287,288],{"class":81,"line":82},[79,289,290],{"class":85},"-- Customers who have never placed an order — the idiomatic \"anti-join\"\n",[79,292,293],{"class":81,"line":89},[79,294,295],{"class":85},"-- LEFT JOIN produces NULL-extended rows for unmatched customers,\n",[79,297,298],{"class":81,"line":95},[79,299,300],{"class":85},"-- then WHERE o.id IS NULL filters to only those unmatched rows.\n",[79,302,303,305,308],{"class":81,"line":137},[79,304,99],{"class":98},[79,306,307],{"class":106}," c.",[79,309,310],{"class":98},"*\n",[79,312,313,315],{"class":81,"line":146},[79,314,140],{"class":98},[79,316,143],{"class":106},[79,318,320,322,324,326,328,330,332,334,336,338],{"class":81,"line":319},6,[79,321,244],{"class":98},[79,323,151],{"class":106},[79,325,154],{"class":98},[79,327,103],{"class":102},[79,329,107],{"class":106},[79,331,121],{"class":102},[79,333,163],{"class":98},[79,335,166],{"class":102},[79,337,107],{"class":106},[79,339,340],{"class":102},"customer_id\n",[79,342,344,347,349,351,353,356,359,362],{"class":81,"line":343},7,[79,345,346],{"class":98},"WHERE",[79,348,166],{"class":102},[79,350,107],{"class":106},[79,352,121],{"class":102},[79,354,355],{"class":98}," IS",[79,357,358],{"class":98}," NULL",[79,360,361],{"class":106},";                            ",[79,363,364],{"class":85},"-- test a NON-NULLABLE column (PK), not o.amount\n",[14,366,367,368,371,372,375,376,379,380,383],{},"Test a ",[26,369,370],{},"non-nullable"," column (like ",[76,373,374],{},"o.id",", the primary key) — testing ",[76,377,378],{},"o.amount IS NULL"," would also exclude rows where the order exists but ",[76,381,382],{},"amount"," happens to be NULL.",[18,385,387],{"id":386},"right-outer-join","RIGHT (OUTER) JOIN",[14,389,184,390,393,394,396],{},[26,391,392],{},"all rows from the right"," table. It's a ",[76,395,244],{}," with the tables swapped.",[66,398,399],{"language":68},[70,400,402],{"className":72,"code":401,"language":68,"meta":74,"style":74},"-- All orders, with their customer if any\n-- Most teams ban RIGHT JOIN for readability — swap tables and use LEFT JOIN instead\nSELECT c.name, o.id AS order_id, o.amount\nFROM customers c\nRIGHT JOIN orders o ON c.id = o.customer_id;\n",[76,403,404,409,414,442,448],{"__ignoreMap":74},[79,405,406],{"class":81,"line":82},[79,407,408],{"class":85},"-- All orders, with their customer if any\n",[79,410,411],{"class":81,"line":89},[79,412,413],{"class":85},"-- Most teams ban RIGHT JOIN for readability — swap tables and use LEFT JOIN instead\n",[79,415,416,418,420,422,424,426,428,430,432,434,436,438,440],{"class":81,"line":95},[79,417,99],{"class":98},[79,419,103],{"class":102},[79,421,107],{"class":106},[79,423,110],{"class":102},[79,425,113],{"class":106},[79,427,116],{"class":102},[79,429,107],{"class":106},[79,431,121],{"class":102},[79,433,124],{"class":98},[79,435,127],{"class":106},[79,437,116],{"class":102},[79,439,107],{"class":106},[79,441,134],{"class":102},[79,443,444,446],{"class":81,"line":137},[79,445,140],{"class":98},[79,447,143],{"class":106},[79,449,450,453,455,457,459,461,463,465,467,469,471],{"class":81,"line":146},[79,451,452],{"class":98},"RIGHT JOIN",[79,454,151],{"class":106},[79,456,154],{"class":98},[79,458,103],{"class":102},[79,460,107],{"class":106},[79,462,121],{"class":102},[79,464,163],{"class":98},[79,466,166],{"class":102},[79,468,107],{"class":106},[79,470,171],{"class":102},[79,472,174],{"class":106},[18,474,476],{"id":475},"full-outer-join","FULL (OUTER) JOIN",[14,478,184,479,482],{},[26,480,481],{},"all rows from both"," tables, with NULLs on whichever side has no match.",[66,484,485],{"language":68},[70,486,488],{"className":72,"code":487,"language":68,"meta":74,"style":74},"-- All customers and all orders, matched where possible\nSELECT c.name, o.id AS order_id, o.amount\nFROM customers c\nFULL JOIN orders o ON c.id = o.customer_id;\n",[76,489,490,495,523,529],{"__ignoreMap":74},[79,491,492],{"class":81,"line":82},[79,493,494],{"class":85},"-- All customers and all orders, matched where possible\n",[79,496,497,499,501,503,505,507,509,511,513,515,517,519,521],{"class":81,"line":89},[79,498,99],{"class":98},[79,500,103],{"class":102},[79,502,107],{"class":106},[79,504,110],{"class":102},[79,506,113],{"class":106},[79,508,116],{"class":102},[79,510,107],{"class":106},[79,512,121],{"class":102},[79,514,124],{"class":98},[79,516,127],{"class":106},[79,518,116],{"class":102},[79,520,107],{"class":106},[79,522,134],{"class":102},[79,524,525,527],{"class":81,"line":95},[79,526,140],{"class":98},[79,528,143],{"class":106},[79,530,531,534,536,538,540,542,544,546,548,550,552],{"class":81,"line":137},[79,532,533],{"class":98},"FULL JOIN",[79,535,151],{"class":106},[79,537,154],{"class":98},[79,539,103],{"class":102},[79,541,107],{"class":106},[79,543,121],{"class":102},[79,545,163],{"class":98},[79,547,166],{"class":102},[79,549,107],{"class":106},[79,551,171],{"class":102},[79,553,174],{"class":106},[14,555,556,557,559,560,563,564,566,567,570],{},"PostgreSQL and SQL Server support ",[76,558,533],{},". ",[26,561,562],{},"MySQL and SQLite do not"," — emulate with ",[76,565,244],{}," + ",[76,568,569],{},"UNION"," + anti-join.",[18,572,574],{"id":573},"cross-join","CROSS JOIN",[14,576,577,578,580],{},"The Cartesian product — every left row paired with every right row, no filter. No ",[76,579,154],{}," clause.",[66,582,583],{"language":68},[70,584,586],{"className":72,"code":585,"language":68,"meta":74,"style":74},"-- All (customer, product) pairs — useful for generating combinations or sparse matrices\n-- If customers has 3 rows and products has 5, result has 15 rows\nSELECT c.name, p.name AS product\nFROM customers c\nCROSS JOIN products p;\n",[76,587,588,593,598,621,627],{"__ignoreMap":74},[79,589,590],{"class":81,"line":82},[79,591,592],{"class":85},"-- All (customer, product) pairs — useful for generating combinations or sparse matrices\n",[79,594,595],{"class":81,"line":89},[79,596,597],{"class":85},"-- If customers has 3 rows and products has 5, result has 15 rows\n",[79,599,600,602,604,606,608,610,612,614,616,618],{"class":81,"line":95},[79,601,99],{"class":98},[79,603,103],{"class":102},[79,605,107],{"class":106},[79,607,110],{"class":102},[79,609,113],{"class":106},[79,611,14],{"class":102},[79,613,107],{"class":106},[79,615,110],{"class":102},[79,617,124],{"class":98},[79,619,620],{"class":106}," product\n",[79,622,623,625],{"class":81,"line":137},[79,624,140],{"class":98},[79,626,143],{"class":106},[79,628,629,631],{"class":81,"line":146},[79,630,574],{"class":98},[79,632,633],{"class":106}," products p;\n",[18,635,637],{"id":636},"join-syntax-on-using-natural","JOIN Syntax: ON, USING, NATURAL",[275,639,641,643],{"id":640},"on-explicit-condition",[76,642,154],{}," — explicit condition",[66,645,646],{"language":68},[70,647,649],{"className":72,"code":648,"language":68,"meta":74,"style":74},"-- ON can include any boolean expression, not just equality\nSELECT * FROM customers c\nJOIN orders o ON c.id = o.customer_id;\n\n-- Non-equi join: match orders that exceed the customer's credit limit\nSELECT c.name, o.id, o.amount\nFROM customers c\nJOIN orders o ON o.customer_id = c.id AND o.amount > c.credit_limit;\n",[76,650,651,656,668,693,699,704,730,736],{"__ignoreMap":74},[79,652,653],{"class":81,"line":82},[79,654,655],{"class":85},"-- ON can include any boolean expression, not just equality\n",[79,657,658,660,663,666],{"class":81,"line":89},[79,659,99],{"class":98},[79,661,662],{"class":98}," *",[79,664,665],{"class":98}," FROM",[79,667,143],{"class":106},[79,669,670,673,675,677,679,681,683,685,687,689,691],{"class":81,"line":95},[79,671,672],{"class":98},"JOIN",[79,674,151],{"class":106},[79,676,154],{"class":98},[79,678,103],{"class":102},[79,680,107],{"class":106},[79,682,121],{"class":102},[79,684,163],{"class":98},[79,686,166],{"class":102},[79,688,107],{"class":106},[79,690,171],{"class":102},[79,692,174],{"class":106},[79,694,695],{"class":81,"line":137},[79,696,698],{"emptyLinePlaceholder":697},true,"\n",[79,700,701],{"class":81,"line":146},[79,702,703],{"class":85},"-- Non-equi join: match orders that exceed the customer's credit limit\n",[79,705,706,708,710,712,714,716,718,720,722,724,726,728],{"class":81,"line":319},[79,707,99],{"class":98},[79,709,103],{"class":102},[79,711,107],{"class":106},[79,713,110],{"class":102},[79,715,113],{"class":106},[79,717,116],{"class":102},[79,719,107],{"class":106},[79,721,121],{"class":102},[79,723,113],{"class":106},[79,725,116],{"class":102},[79,727,107],{"class":106},[79,729,134],{"class":102},[79,731,732,734],{"class":81,"line":343},[79,733,140],{"class":98},[79,735,143],{"class":106},[79,737,739,741,743,745,747,749,751,753,755,757,759,762,764,766,768,771,773,775,778],{"class":81,"line":738},8,[79,740,672],{"class":98},[79,742,151],{"class":106},[79,744,154],{"class":98},[79,746,166],{"class":102},[79,748,107],{"class":106},[79,750,171],{"class":102},[79,752,163],{"class":98},[79,754,103],{"class":102},[79,756,107],{"class":106},[79,758,121],{"class":102},[79,760,761],{"class":98}," AND",[79,763,166],{"class":102},[79,765,107],{"class":106},[79,767,382],{"class":102},[79,769,770],{"class":98}," >",[79,772,103],{"class":102},[79,774,107],{"class":106},[79,776,777],{"class":102},"credit_limit",[79,779,174],{"class":106},[275,781,783,786],{"id":782},"using-shorthand-for-same-named-columns",[76,784,785],{},"USING"," — shorthand for same-named columns",[66,788,789],{"language":68},[70,790,792],{"className":72,"code":791,"language":68,"meta":74,"style":74},"-- When both tables have a column named \"customer_id\"\n-- USING returns the joined column ONCE in the output (ON returns both c.customer_id and o.customer_id)\nSELECT * FROM customers JOIN orders USING (customer_id);\n",[76,793,794,799,804],{"__ignoreMap":74},[79,795,796],{"class":81,"line":82},[79,797,798],{"class":85},"-- When both tables have a column named \"customer_id\"\n",[79,800,801],{"class":81,"line":89},[79,802,803],{"class":85},"-- USING returns the joined column ONCE in the output (ON returns both c.customer_id and o.customer_id)\n",[79,805,806,808,810,812,815,817,820,822],{"class":81,"line":95},[79,807,99],{"class":98},[79,809,662],{"class":98},[79,811,665],{"class":98},[79,813,814],{"class":106}," customers ",[79,816,672],{"class":98},[79,818,819],{"class":106}," orders ",[79,821,785],{"class":98},[79,823,824],{"class":106}," (customer_id);\n",[275,826,828,831],{"id":827},"natural-join-auto-join-on-all-same-named-columns-avoid",[76,829,830],{},"NATURAL JOIN"," — auto-join on all same-named columns (avoid)",[66,833,834],{"language":68},[70,835,837],{"className":72,"code":836,"language":68,"meta":74,"style":74},"-- DANGEROUS: joins on EVERY column with the same name in both tables.\n-- If a future schema change adds a same-named column (e.g., created_at),\n-- the join silently changes behavior. Never use in production.\nSELECT * FROM customers NATURAL JOIN orders;\n",[76,838,839,844,849,854],{"__ignoreMap":74},[79,840,841],{"class":81,"line":82},[79,842,843],{"class":85},"-- DANGEROUS: joins on EVERY column with the same name in both tables.\n",[79,845,846],{"class":81,"line":89},[79,847,848],{"class":85},"-- If a future schema change adds a same-named column (e.g., created_at),\n",[79,850,851],{"class":81,"line":95},[79,852,853],{"class":85},"-- the join silently changes behavior. Never use in production.\n",[79,855,856,858,860,862,864,866],{"class":81,"line":137},[79,857,99],{"class":98},[79,859,662],{"class":98},[79,861,665],{"class":98},[79,863,814],{"class":106},[79,865,830],{"class":98},[79,867,868],{"class":106}," orders;\n",[18,870,872],{"id":871},"complex-implementation-4-table-production-join-chain","Complex Implementation: 4-Table Production Join Chain",[66,874,875],{"language":68},[70,876,878],{"className":72,"code":877,"language":68,"meta":74,"style":74},"-- A production join chain: customers → orders → order_items → products\n-- Filtering at each step to minimize intermediate result size.\nSELECT\n  c.name AS customer,\n  o.id AS order_id,\n  o.placed_at,\n  p.name AS product,\n  oi.quantity,\n  oi.unit_price,\n  (oi.quantity * oi.unit_price) AS line_total    -- computed per line item\nFROM customers c\nJOIN orders o        ON c.id = o.customer_id      -- INNER: only customers with orders\nJOIN order_items oi  ON o.id = oi.order_id        -- INNER: only orders with items\nJOIN products p      ON oi.product_id = p.id      -- INNER: only items with valid products\nWHERE c.city = 'NYC'                              -- filter early — reduces rows before joins\n  AND o.status IN ('paid', 'shipped')             -- only fulfilled orders\n  AND o.placed_at >= '2024-01-01'                 -- date range filter\nORDER BY o.placed_at DESC, o.id, p.name;\n",[76,879,880,885,890,895,909,923,935,949,961,973,1007,1014,1041,1069,1098,1119,1152,1172],{"__ignoreMap":74},[79,881,882],{"class":81,"line":82},[79,883,884],{"class":85},"-- A production join chain: customers → orders → order_items → products\n",[79,886,887],{"class":81,"line":89},[79,888,889],{"class":85},"-- Filtering at each step to minimize intermediate result size.\n",[79,891,892],{"class":81,"line":95},[79,893,894],{"class":98},"SELECT\n",[79,896,897,900,902,904,906],{"class":81,"line":137},[79,898,899],{"class":102},"  c",[79,901,107],{"class":106},[79,903,110],{"class":102},[79,905,124],{"class":98},[79,907,908],{"class":106}," customer,\n",[79,910,911,914,916,918,920],{"class":81,"line":146},[79,912,913],{"class":102},"  o",[79,915,107],{"class":106},[79,917,121],{"class":102},[79,919,124],{"class":98},[79,921,922],{"class":106}," order_id,\n",[79,924,925,927,929,932],{"class":81,"line":319},[79,926,913],{"class":102},[79,928,107],{"class":106},[79,930,931],{"class":102},"placed_at",[79,933,934],{"class":106},",\n",[79,936,937,940,942,944,946],{"class":81,"line":343},[79,938,939],{"class":102},"  p",[79,941,107],{"class":106},[79,943,110],{"class":102},[79,945,124],{"class":98},[79,947,948],{"class":106}," product,\n",[79,950,951,954,956,959],{"class":81,"line":738},[79,952,953],{"class":102},"  oi",[79,955,107],{"class":106},[79,957,958],{"class":102},"quantity",[79,960,934],{"class":106},[79,962,964,966,968,971],{"class":81,"line":963},9,[79,965,953],{"class":102},[79,967,107],{"class":106},[79,969,970],{"class":102},"unit_price",[79,972,934],{"class":106},[79,974,976,979,982,984,986,988,991,993,995,998,1001,1004],{"class":81,"line":975},10,[79,977,978],{"class":106},"  (",[79,980,981],{"class":102},"oi",[79,983,107],{"class":106},[79,985,958],{"class":102},[79,987,662],{"class":98},[79,989,990],{"class":102}," oi",[79,992,107],{"class":106},[79,994,970],{"class":102},[79,996,997],{"class":106},") ",[79,999,1000],{"class":98},"AS",[79,1002,1003],{"class":106}," line_total    ",[79,1005,1006],{"class":85},"-- computed per line item\n",[79,1008,1010,1012],{"class":81,"line":1009},11,[79,1011,140],{"class":98},[79,1013,143],{"class":106},[79,1015,1017,1019,1022,1024,1026,1028,1030,1032,1034,1036,1038],{"class":81,"line":1016},12,[79,1018,672],{"class":98},[79,1020,1021],{"class":106}," orders o        ",[79,1023,154],{"class":98},[79,1025,103],{"class":102},[79,1027,107],{"class":106},[79,1029,121],{"class":102},[79,1031,163],{"class":98},[79,1033,166],{"class":102},[79,1035,107],{"class":106},[79,1037,171],{"class":102},[79,1039,1040],{"class":85},"      -- INNER: only customers with orders\n",[79,1042,1044,1046,1049,1051,1053,1055,1057,1059,1061,1063,1066],{"class":81,"line":1043},13,[79,1045,672],{"class":98},[79,1047,1048],{"class":106}," order_items oi  ",[79,1050,154],{"class":98},[79,1052,166],{"class":102},[79,1054,107],{"class":106},[79,1056,121],{"class":102},[79,1058,163],{"class":98},[79,1060,990],{"class":102},[79,1062,107],{"class":106},[79,1064,1065],{"class":102},"order_id",[79,1067,1068],{"class":85},"        -- INNER: only orders with items\n",[79,1070,1072,1074,1077,1079,1081,1083,1086,1088,1091,1093,1095],{"class":81,"line":1071},14,[79,1073,672],{"class":98},[79,1075,1076],{"class":106}," products p      ",[79,1078,154],{"class":98},[79,1080,990],{"class":102},[79,1082,107],{"class":106},[79,1084,1085],{"class":102},"product_id",[79,1087,163],{"class":98},[79,1089,1090],{"class":102}," p",[79,1092,107],{"class":106},[79,1094,121],{"class":102},[79,1096,1097],{"class":85},"      -- INNER: only items with valid products\n",[79,1099,1101,1103,1105,1107,1110,1112,1116],{"class":81,"line":1100},15,[79,1102,346],{"class":98},[79,1104,103],{"class":102},[79,1106,107],{"class":106},[79,1108,1109],{"class":102},"city",[79,1111,163],{"class":98},[79,1113,1115],{"class":1114},"sJ6F3"," 'NYC'",[79,1117,1118],{"class":85},"                              -- filter early — reduces rows before joins\n",[79,1120,1122,1125,1127,1129,1132,1135,1138,1141,1143,1146,1149],{"class":81,"line":1121},16,[79,1123,1124],{"class":98},"  AND",[79,1126,166],{"class":102},[79,1128,107],{"class":106},[79,1130,1131],{"class":102},"status",[79,1133,1134],{"class":98}," IN",[79,1136,1137],{"class":106}," (",[79,1139,1140],{"class":1114},"'paid'",[79,1142,113],{"class":106},[79,1144,1145],{"class":1114},"'shipped'",[79,1147,1148],{"class":106},")             ",[79,1150,1151],{"class":85},"-- only fulfilled orders\n",[79,1153,1155,1157,1159,1161,1163,1166,1169],{"class":81,"line":1154},17,[79,1156,1124],{"class":98},[79,1158,166],{"class":102},[79,1160,107],{"class":106},[79,1162,931],{"class":102},[79,1164,1165],{"class":98}," >=",[79,1167,1168],{"class":1114}," '2024-01-01'",[79,1170,1171],{"class":85},"                 -- date range filter\n",[79,1173,1175,1178,1180,1182,1184,1187,1189,1191,1193,1195,1197,1199,1201,1203],{"class":81,"line":1174},18,[79,1176,1177],{"class":98},"ORDER BY",[79,1179,166],{"class":102},[79,1181,107],{"class":106},[79,1183,931],{"class":102},[79,1185,1186],{"class":98}," DESC",[79,1188,113],{"class":106},[79,1190,116],{"class":102},[79,1192,107],{"class":106},[79,1194,121],{"class":102},[79,1196,113],{"class":106},[79,1198,14],{"class":102},[79,1200,107],{"class":106},[79,1202,110],{"class":102},[79,1204,174],{"class":106},[18,1206,1208],{"id":1207},"self-joins","Self-Joins",[14,1210,1211],{},"A table can join to itself — use aliases to distinguish the two \"copies.\"",[66,1213,1214],{"language":68},[70,1215,1217],{"className":72,"code":1216,"language":68,"meta":74,"style":74},"-- Employees and their managers (manager_id references employees.id)\n-- e = employee alias, m = manager alias — same table, two logical roles\nSELECT e.name AS employee, m.name AS manager\nFROM employees e\nLEFT JOIN employees m ON e.manager_id = m.id;    -- LEFT JOIN: top-level managers (manager_id IS NULL) still appear\n",[76,1218,1219,1224,1229,1257,1264],{"__ignoreMap":74},[79,1220,1221],{"class":81,"line":82},[79,1222,1223],{"class":85},"-- Employees and their managers (manager_id references employees.id)\n",[79,1225,1226],{"class":81,"line":89},[79,1227,1228],{"class":85},"-- e = employee alias, m = manager alias — same table, two logical roles\n",[79,1230,1231,1233,1236,1238,1240,1242,1245,1248,1250,1252,1254],{"class":81,"line":95},[79,1232,99],{"class":98},[79,1234,1235],{"class":102}," e",[79,1237,107],{"class":106},[79,1239,110],{"class":102},[79,1241,124],{"class":98},[79,1243,1244],{"class":106}," employee, ",[79,1246,1247],{"class":102},"m",[79,1249,107],{"class":106},[79,1251,110],{"class":102},[79,1253,124],{"class":98},[79,1255,1256],{"class":106}," manager\n",[79,1258,1259,1261],{"class":81,"line":137},[79,1260,140],{"class":98},[79,1262,1263],{"class":106}," employees e\n",[79,1265,1266,1268,1271,1273,1275,1277,1280,1282,1285,1287,1289,1292],{"class":81,"line":146},[79,1267,244],{"class":98},[79,1269,1270],{"class":106}," employees m ",[79,1272,154],{"class":98},[79,1274,1235],{"class":102},[79,1276,107],{"class":106},[79,1278,1279],{"class":102},"manager_id",[79,1281,163],{"class":98},[79,1283,1284],{"class":102}," m",[79,1286,107],{"class":106},[79,1288,121],{"class":102},[79,1290,1291],{"class":106},";    ",[79,1293,1294],{"class":85},"-- LEFT JOIN: top-level managers (manager_id IS NULL) still appear\n",[14,1296,1297],{},"Self-joins are the classic way to walk a hierarchy (employees→managers). For deep hierarchies, use recursive CTEs (Chapter 19).",[18,1299,1301],{"id":1300},"join-algorithms-what-the-planner-chooses","Join Algorithms (What the Planner Chooses)",[14,1303,1304,1305,1308],{},"The optimizer picks one of these per join — visible in ",[76,1306,1307],{},"EXPLAIN",":",[1310,1311,1312,1331],"table",{},[1313,1314,1315],"thead",{},[1316,1317,1318,1322,1325,1328],"tr",{},[1319,1320,1321],"th",{},"Algorithm",[1319,1323,1324],{},"Best when",[1319,1326,1327],{},"Cost",[1319,1329,1330],{},"Memory",[1332,1333,1334,1351,1367],"tbody",{},[1316,1335,1336,1342,1345,1348],{},[1337,1338,1339],"td",{},[26,1340,1341],{},"Nested Loop",[1337,1343,1344],{},"One side small, or indexed lookup on inner.",[1337,1346,1347],{},"O(N × M) worst, O(N × log M) with index",[1337,1349,1350],{},"O(1)",[1316,1352,1353,1358,1361,1364],{},[1337,1354,1355],{},[26,1356,1357],{},"Hash Join",[1337,1359,1360],{},"Both sides large, no usable index, equality condition.",[1337,1362,1363],{},"O(N + M) — builds hash table on smaller side",[1337,1365,1366],{},"O(min(N,M))",[1316,1368,1369,1374,1377,1380],{},[1337,1370,1371],{},[26,1372,1373],{},"Merge Join",[1337,1375,1376],{},"Both sides pre-sorted on join key (e.g., via indexes).",[1337,1378,1379],{},"O(N + M) — both inputs must be sorted",[1337,1381,1350],{},[66,1383,1384],{"language":68},[70,1385,1387],{"className":72,"code":1386,"language":68,"meta":74,"style":74},"-- See which algorithm the planner chose:\nEXPLAIN SELECT c.name, o.id\nFROM customers c\nJOIN orders o ON c.id = o.customer_id\nWHERE o.amount > 100;\n\n-- If you see \"Hash Join\" but work_mem is too low, the hash spills to disk → slow.\n-- Fix: SET work_mem = '256MB';  (per-session, per-node — beware multi-node plans)\n",[76,1388,1389,1394,1416,1422,1444,1461,1465,1470],{"__ignoreMap":74},[79,1390,1391],{"class":81,"line":82},[79,1392,1393],{"class":85},"-- See which algorithm the planner chose:\n",[79,1395,1396,1399,1401,1403,1405,1407,1409,1411,1413],{"class":81,"line":89},[79,1397,1398],{"class":106},"EXPLAIN ",[79,1400,99],{"class":98},[79,1402,103],{"class":102},[79,1404,107],{"class":106},[79,1406,110],{"class":102},[79,1408,113],{"class":106},[79,1410,116],{"class":102},[79,1412,107],{"class":106},[79,1414,1415],{"class":102},"id\n",[79,1417,1418,1420],{"class":81,"line":95},[79,1419,140],{"class":98},[79,1421,143],{"class":106},[79,1423,1424,1426,1428,1430,1432,1434,1436,1438,1440,1442],{"class":81,"line":137},[79,1425,672],{"class":98},[79,1427,151],{"class":106},[79,1429,154],{"class":98},[79,1431,103],{"class":102},[79,1433,107],{"class":106},[79,1435,121],{"class":102},[79,1437,163],{"class":98},[79,1439,166],{"class":102},[79,1441,107],{"class":106},[79,1443,340],{"class":102},[79,1445,1446,1448,1450,1452,1454,1456,1459],{"class":81,"line":146},[79,1447,346],{"class":98},[79,1449,166],{"class":102},[79,1451,107],{"class":106},[79,1453,382],{"class":102},[79,1455,770],{"class":98},[79,1457,1458],{"class":102}," 100",[79,1460,174],{"class":106},[79,1462,1463],{"class":81,"line":319},[79,1464,698],{"emptyLinePlaceholder":697},[79,1466,1467],{"class":81,"line":343},[79,1468,1469],{"class":85},"-- If you see \"Hash Join\" but work_mem is too low, the hash spills to disk → slow.\n",[79,1471,1472],{"class":81,"line":738},[79,1473,1474],{"class":85},"-- Fix: SET work_mem = '256MB';  (per-session, per-node — beware multi-node plans)\n",[18,1476,1478],{"id":1477},"anti-pattern-left-join-where-on-right-table-demoted-to-inner-join","Anti-Pattern: LEFT JOIN + WHERE on Right Table → Demoted to INNER JOIN",[66,1480,1481],{"language":68},[70,1482,1484],{"className":72,"code":1483,"language":68,"meta":74,"style":74},"-- ❌ WRONG: WHERE on the right table demotes LEFT JOIN to INNER JOIN\n-- The WHERE filters out NULL-extended rows for unmatched left rows\n-- (NULL >= '2024-01-01' is UNKNOWN, not TRUE → row excluded)\nSELECT c.name, SUM(o.amount) AS total\nFROM customers c\nLEFT JOIN orders o ON c.id = o.customer_id\nWHERE o.ordered_on >= '2024-01-01'    -- this condition filters the FINAL result, not the join\nGROUP BY c.name;\n-- Result: customers with no 2024 orders DISAPPEAR — LEFT JOIN defeated\n\n-- ✅ RIGHT: move the right-table filter into ON\n-- ON filters WHICH right rows match, without dropping left rows that have no match\nSELECT c.name, COALESCE(SUM(o.amount), 0) AS total\nFROM customers c\nLEFT JOIN orders o\n  ON c.id = o.customer_id\n AND o.ordered_on >= '2024-01-01'     -- ON condition: only match 2024 orders, but keep all customers\nGROUP BY c.name;\n-- Result: all customers appear; those with no 2024 orders show total = 0 (via COALESCE)\n",[76,1485,1486,1491,1496,1501,1532,1538,1560,1578,1591,1596,1600,1605,1610,1649,1655,1662,1681,1698,1710],{"__ignoreMap":74},[79,1487,1488],{"class":81,"line":82},[79,1489,1490],{"class":85},"-- ❌ WRONG: WHERE on the right table demotes LEFT JOIN to INNER JOIN\n",[79,1492,1493],{"class":81,"line":89},[79,1494,1495],{"class":85},"-- The WHERE filters out NULL-extended rows for unmatched left rows\n",[79,1497,1498],{"class":81,"line":95},[79,1499,1500],{"class":85},"-- (NULL >= '2024-01-01' is UNKNOWN, not TRUE → row excluded)\n",[79,1502,1503,1505,1507,1509,1511,1513,1516,1519,1521,1523,1525,1527,1529],{"class":81,"line":137},[79,1504,99],{"class":98},[79,1506,103],{"class":102},[79,1508,107],{"class":106},[79,1510,110],{"class":102},[79,1512,113],{"class":106},[79,1514,1515],{"class":102},"SUM",[79,1517,1518],{"class":106},"(",[79,1520,116],{"class":102},[79,1522,107],{"class":106},[79,1524,382],{"class":102},[79,1526,997],{"class":106},[79,1528,1000],{"class":98},[79,1530,1531],{"class":106}," total\n",[79,1533,1534,1536],{"class":81,"line":146},[79,1535,140],{"class":98},[79,1537,143],{"class":106},[79,1539,1540,1542,1544,1546,1548,1550,1552,1554,1556,1558],{"class":81,"line":319},[79,1541,244],{"class":98},[79,1543,151],{"class":106},[79,1545,154],{"class":98},[79,1547,103],{"class":102},[79,1549,107],{"class":106},[79,1551,121],{"class":102},[79,1553,163],{"class":98},[79,1555,166],{"class":102},[79,1557,107],{"class":106},[79,1559,340],{"class":102},[79,1561,1562,1564,1566,1568,1571,1573,1575],{"class":81,"line":343},[79,1563,346],{"class":98},[79,1565,166],{"class":102},[79,1567,107],{"class":106},[79,1569,1570],{"class":102},"ordered_on",[79,1572,1165],{"class":98},[79,1574,1168],{"class":1114},[79,1576,1577],{"class":85},"    -- this condition filters the FINAL result, not the join\n",[79,1579,1580,1583,1585,1587,1589],{"class":81,"line":738},[79,1581,1582],{"class":98},"GROUP BY",[79,1584,103],{"class":102},[79,1586,107],{"class":106},[79,1588,110],{"class":102},[79,1590,174],{"class":106},[79,1592,1593],{"class":81,"line":963},[79,1594,1595],{"class":85},"-- Result: customers with no 2024 orders DISAPPEAR — LEFT JOIN defeated\n",[79,1597,1598],{"class":81,"line":975},[79,1599,698],{"emptyLinePlaceholder":697},[79,1601,1602],{"class":81,"line":1009},[79,1603,1604],{"class":85},"-- ✅ RIGHT: move the right-table filter into ON\n",[79,1606,1607],{"class":81,"line":1016},[79,1608,1609],{"class":85},"-- ON filters WHICH right rows match, without dropping left rows that have no match\n",[79,1611,1612,1614,1616,1618,1620,1622,1625,1627,1629,1631,1633,1635,1637,1640,1643,1645,1647],{"class":81,"line":1043},[79,1613,99],{"class":98},[79,1615,103],{"class":102},[79,1617,107],{"class":106},[79,1619,110],{"class":102},[79,1621,113],{"class":106},[79,1623,1624],{"class":102},"COALESCE",[79,1626,1518],{"class":106},[79,1628,1515],{"class":102},[79,1630,1518],{"class":106},[79,1632,116],{"class":102},[79,1634,107],{"class":106},[79,1636,382],{"class":102},[79,1638,1639],{"class":106},"), ",[79,1641,1642],{"class":102},"0",[79,1644,997],{"class":106},[79,1646,1000],{"class":98},[79,1648,1531],{"class":106},[79,1650,1651,1653],{"class":81,"line":1071},[79,1652,140],{"class":98},[79,1654,143],{"class":106},[79,1656,1657,1659],{"class":81,"line":1100},[79,1658,244],{"class":98},[79,1660,1661],{"class":106}," orders o\n",[79,1663,1664,1667,1669,1671,1673,1675,1677,1679],{"class":81,"line":1121},[79,1665,1666],{"class":98},"  ON",[79,1668,103],{"class":102},[79,1670,107],{"class":106},[79,1672,121],{"class":102},[79,1674,163],{"class":98},[79,1676,166],{"class":102},[79,1678,107],{"class":106},[79,1680,340],{"class":102},[79,1682,1683,1685,1687,1689,1691,1693,1695],{"class":81,"line":1154},[79,1684,761],{"class":98},[79,1686,166],{"class":102},[79,1688,107],{"class":106},[79,1690,1570],{"class":102},[79,1692,1165],{"class":98},[79,1694,1168],{"class":1114},[79,1696,1697],{"class":85},"     -- ON condition: only match 2024 orders, but keep all customers\n",[79,1699,1700,1702,1704,1706,1708],{"class":81,"line":1174},[79,1701,1582],{"class":98},[79,1703,103],{"class":102},[79,1705,107],{"class":106},[79,1707,110],{"class":102},[79,1709,174],{"class":106},[79,1711,1713],{"class":81,"line":1712},19,[79,1714,1715],{"class":85},"-- Result: all customers appear; those with no 2024 orders show total = 0 (via COALESCE)\n",[18,1717,1719],{"id":1718},"anti-pattern-accidental-cross-join","Anti-Pattern: Accidental CROSS JOIN",[66,1721,1722],{"language":68},[70,1723,1725],{"className":72,"code":1724,"language":68,"meta":74,"style":74},"-- ❌ WRONG: forgetting ON in a JOIN → CROSS JOIN (cartesian product)\nSELECT * FROM customers c JOIN orders o;   -- 3 customers × 4 orders = 12 rows (not 4!)\n\n-- ✅ RIGHT: always include ON for INNER\u002FOUTER joins, or use explicit CROSS JOIN\nSELECT * FROM customers c CROSS JOIN orders o;  -- explicit: \"yes, I want the cartesian product\"\n",[76,1726,1727,1732,1751,1755,1760],{"__ignoreMap":74},[79,1728,1729],{"class":81,"line":82},[79,1730,1731],{"class":85},"-- ❌ WRONG: forgetting ON in a JOIN → CROSS JOIN (cartesian product)\n",[79,1733,1734,1736,1738,1740,1743,1745,1748],{"class":81,"line":89},[79,1735,99],{"class":98},[79,1737,662],{"class":98},[79,1739,665],{"class":98},[79,1741,1742],{"class":106}," customers c ",[79,1744,672],{"class":98},[79,1746,1747],{"class":106}," orders o;   ",[79,1749,1750],{"class":85},"-- 3 customers × 4 orders = 12 rows (not 4!)\n",[79,1752,1753],{"class":81,"line":95},[79,1754,698],{"emptyLinePlaceholder":697},[79,1756,1757],{"class":81,"line":137},[79,1758,1759],{"class":85},"-- ✅ RIGHT: always include ON for INNER\u002FOUTER joins, or use explicit CROSS JOIN\n",[79,1761,1762,1764,1766,1768,1770,1772,1775],{"class":81,"line":146},[79,1763,99],{"class":98},[79,1765,662],{"class":98},[79,1767,665],{"class":98},[79,1769,1742],{"class":106},[79,1771,574],{"class":98},[79,1773,1774],{"class":106}," orders o;  ",[79,1776,1777],{"class":85},"-- explicit: \"yes, I want the cartesian product\"\n",[18,1779,1781],{"id":1780},"tips-tricks","💡 Tips & Tricks",[1783,1784,1785,1803,1820,1841],"ul",{},[34,1786,1787,1790,1791,1794,1795,1798,1799,1802],{},[26,1788,1789],{},"Idiom",": use ",[76,1792,1793],{},"LEFT JOIN ... WHERE right.id IS NULL"," (anti-join) instead of ",[76,1796,1797],{},"NOT IN (subquery)"," — the anti-join is immune to the NULL-poisoning trap that breaks ",[76,1800,1801],{},"NOT IN",", and it's usually faster because the planner can use a hash anti-join.",[34,1804,1805,1808,1809,1812,1813,1815,1816,1819],{},[26,1806,1807],{},"Performance",": a hash join needs memory to build the hash table (the smaller input). If ",[76,1810,1811],{},"work_mem"," is too low, PostgreSQL spills the hash to disk and performance collapses. Increase ",[76,1814,1811],{}," for big join queries per-session: ",[76,1817,1818],{},"SET work_mem = '256MB';"," — but beware it's per-node, not per-query, so a 10-node plan with 256MB each can consume 2.5GB.",[34,1821,1822,1824,1825,1828,1829,1832,1833,1836,1837,1840],{},[26,1823,1789],{},": prefer ",[76,1826,1827],{},"USING (col)"," over ",[76,1830,1831],{},"ON a.col = b.col"," when the column names match — it's shorter, returns the joined column once (no need to pick ",[76,1834,1835],{},"a.col"," vs ",[76,1838,1839],{},"b.col","), and signals intent (\"these are the same key\").",[34,1842,1843,1846,1847,1850,1851,1853,1854,1856,1857,1860,1861,1864,1865,1867],{},[26,1844,1845],{},"Debug",": when a join returns ",[50,1848,1849],{},"more"," rows than expected, look for a non-unique join key on the \"many\" side — a ",[76,1852,244],{}," from customers to orders on ",[76,1855,171],{}," produces one row per ",[50,1858,1859],{},"order",", not per customer. Use ",[76,1862,1863],{},"DISTINCT"," or aggregate (",[76,1866,1582],{},") if you want one row per customer.",[18,1869,1871],{"id":1870},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[1783,1873,1874,1901,1915,1934,1948,1971,1999,2015],{},[34,1875,1876,1883,1884,1887,1888,1890,1891,1894,1895,1897,1898,107],{},[26,1877,1878,566,1880,1882],{},[76,1879,244],{},[76,1881,346],{}," on right table → INNER JOIN demotion",": ",[76,1885,1886],{},"SELECT ... FROM c LEFT JOIN o ON c.id=o.customer_id WHERE o.amount > 100"," drops customers with no orders (the ",[76,1889,346],{}," filters out the NULL-extended rows). To filter right-table rows ",[50,1892,1893],{},"without"," losing left-only rows, put the condition in the ",[76,1896,154],{}," clause: ",[76,1899,1900],{},"LEFT JOIN o ON c.id=o.customer_id AND o.amount > 100",[34,1902,1903,1906,1907,1910,1911,1914],{},[26,1904,1905],{},"Row multiplication",": joining to a \"many\" side multiplies rows. ",[76,1908,1909],{},"LEFT JOIN orders"," on a customer with 5 orders returns 5 rows for that customer — a ",[76,1912,1913],{},"SELECT c.*"," \"to list customers\" unexpectedly has duplicates.",[34,1916,1917,1883,1926,1929,1930,1933],{},[26,1918,1919,1922,1923,1925],{},[76,1920,1921],{},"COUNT(*)"," on a ",[76,1924,244],{}," lies",[76,1927,1928],{},"SELECT c.name, COUNT(*) FROM customers c LEFT JOIN orders o ON ... GROUP BY c.name"," counts 1 even for customers with no orders (the NULL-extended row still counts). Use ",[76,1931,1932],{},"COUNT(o.id)"," — counting a non-nullable right-column — to count only matched rows (NULLs don't count).",[34,1935,1936,1883,1941,1944,1945,1947],{},[26,1937,1938,1940],{},[76,1939,785],{}," and quoted columns",[76,1942,1943],{},"USING (\"CustomerID\")"," works but is case-sensitive and fragile; prefer snake_case column names to keep ",[76,1946,785],{}," ergonomic.",[34,1949,1950,1883,1953,1956,1957,1959,1960,1963,1964,1967,1968,1970],{},[26,1951,1952],{},"Joining on NULL",[76,1954,1955],{},"NULL = NULL"," is UNKNOWN, so a join ",[76,1958,1831],{}," does ",[26,1961,1962],{},"not"," match rows where both sides are NULL. If NULL-keys should match, use ",[76,1965,1966],{},"IS NOT DISTINCT FROM"," in the ",[76,1969,154],{}," (slower — no index).",[34,1972,1973,1978,1979,1982,1983,1986,1987,1990,1991,1986,1993,113,1996,1998],{},[26,1974,1975,1977],{},[76,1976,830],{}," surprises",": it joins on ",[50,1980,1981],{},"all"," common columns. If ",[76,1984,1985],{},"customers"," and ",[76,1988,1989],{},"orders"," both have ",[76,1992,121],{},[76,1994,1995],{},"created_at",[76,1997,830],{}," requires all four to match — almost certainly not what you want.",[34,2000,2001,1883,2004,2007,2008,2010,2011,2014],{},[26,2002,2003],{},"Three-way joins and column ambiguity",[76,2005,2006],{},"SELECT id FROM customers JOIN orders ON ..."," fails — ",[76,2009,121],{}," exists in both tables. Always qualify (",[76,2012,2013],{},"SELECT c.id",") in multi-table queries.",[34,2016,2017,2022,2023,2025,2026,2028],{},[26,2018,2019,2021],{},[76,2020,452],{}," readability",": most teams ban ",[76,2024,452],{}," in code style and require rewriting as ",[76,2027,244],{}," — it's easier to read left-to-right.",[18,2030,2032],{"id":2031},"spot-the-bug","🧠 Spot the Bug",[14,2034,2035,2036,2039],{},"A developer wants a report of ",[26,2037,2038],{},"all customers and the total of their 2024 orders",", including customers who haven't ordered in 2024 (showing $0). This query is wrong. Why?",[66,2041,2042],{"language":68},[70,2043,2045],{"className":72,"code":2044,"language":68,"meta":74,"style":74},"SELECT c.name, SUM(o.amount) AS total\nFROM customers c\nLEFT JOIN orders o ON c.id = o.customer_id\nWHERE o.ordered_on >= '2024-01-01'\nGROUP BY c.name;\n",[76,2046,2047,2075,2081,2103,2118],{"__ignoreMap":74},[79,2048,2049,2051,2053,2055,2057,2059,2061,2063,2065,2067,2069,2071,2073],{"class":81,"line":82},[79,2050,99],{"class":98},[79,2052,103],{"class":102},[79,2054,107],{"class":106},[79,2056,110],{"class":102},[79,2058,113],{"class":106},[79,2060,1515],{"class":102},[79,2062,1518],{"class":106},[79,2064,116],{"class":102},[79,2066,107],{"class":106},[79,2068,382],{"class":102},[79,2070,997],{"class":106},[79,2072,1000],{"class":98},[79,2074,1531],{"class":106},[79,2076,2077,2079],{"class":81,"line":89},[79,2078,140],{"class":98},[79,2080,143],{"class":106},[79,2082,2083,2085,2087,2089,2091,2093,2095,2097,2099,2101],{"class":81,"line":95},[79,2084,244],{"class":98},[79,2086,151],{"class":106},[79,2088,154],{"class":98},[79,2090,103],{"class":102},[79,2092,107],{"class":106},[79,2094,121],{"class":102},[79,2096,163],{"class":98},[79,2098,166],{"class":102},[79,2100,107],{"class":106},[79,2102,340],{"class":102},[79,2104,2105,2107,2109,2111,2113,2115],{"class":81,"line":137},[79,2106,346],{"class":98},[79,2108,166],{"class":102},[79,2110,107],{"class":106},[79,2112,1570],{"class":102},[79,2114,1165],{"class":98},[79,2116,2117],{"class":1114}," '2024-01-01'\n",[79,2119,2120,2122,2124,2126,2128],{"class":81,"line":146},[79,2121,1582],{"class":98},[79,2123,103],{"class":102},[79,2125,107],{"class":106},[79,2127,110],{"class":102},[79,2129,174],{"class":106},[2131,2132,2133,2137,2140,2167,2177,2279,2288],"details",{},[2134,2135,2136],"summary",{},"Answer",[14,2138,2139],{},"Two bugs:",[31,2141,2142,2161],{},[34,2143,2144,2150,2151,2154,2155,2158,2159,107],{},[26,2145,2146,2147,2149],{},"The ",[76,2148,346],{}," converts the LEFT JOIN to an INNER JOIN."," ",[76,2152,2153],{},"WHERE o.ordered_on >= '2024-01-01'"," filters out the NULL-extended rows for customers with no orders (because ",[76,2156,2157],{},"NULL >= '2024-01-01'"," is UNKNOWN, not TRUE). Customers who haven't ordered disappear from the result — defeating the purpose of the ",[76,2160,244],{},[34,2162,2163,2166],{},[26,2164,2165],{},"Even for customers with orders, only 2024 orders are summed"," — which may or may not be the intent, but combined with bug #1, the \"include customers with no 2024 orders\" requirement is violated.",[14,2168,2169,2170,2172,2173,2176],{},"The fix: move the right-table filter into the ",[76,2171,154],{}," clause so it filters ",[50,2174,2175],{},"which orders match"," without dropping the customer row:",[66,2178,2179],{"language":68},[70,2180,2182],{"className":72,"code":2181,"language":68,"meta":74,"style":74},"SELECT c.name, COALESCE(SUM(o.amount), 0) AS total\nFROM customers c\nLEFT JOIN orders o\n  ON c.id = o.customer_id\n AND o.ordered_on >= '2024-01-01'    -- ON: filters which orders match, preserves all customers\nGROUP BY c.name;\n",[76,2183,2184,2220,2226,2232,2250,2267],{"__ignoreMap":74},[79,2185,2186,2188,2190,2192,2194,2196,2198,2200,2202,2204,2206,2208,2210,2212,2214,2216,2218],{"class":81,"line":82},[79,2187,99],{"class":98},[79,2189,103],{"class":102},[79,2191,107],{"class":106},[79,2193,110],{"class":102},[79,2195,113],{"class":106},[79,2197,1624],{"class":102},[79,2199,1518],{"class":106},[79,2201,1515],{"class":102},[79,2203,1518],{"class":106},[79,2205,116],{"class":102},[79,2207,107],{"class":106},[79,2209,382],{"class":102},[79,2211,1639],{"class":106},[79,2213,1642],{"class":102},[79,2215,997],{"class":106},[79,2217,1000],{"class":98},[79,2219,1531],{"class":106},[79,2221,2222,2224],{"class":81,"line":89},[79,2223,140],{"class":98},[79,2225,143],{"class":106},[79,2227,2228,2230],{"class":81,"line":95},[79,2229,244],{"class":98},[79,2231,1661],{"class":106},[79,2233,2234,2236,2238,2240,2242,2244,2246,2248],{"class":81,"line":137},[79,2235,1666],{"class":98},[79,2237,103],{"class":102},[79,2239,107],{"class":106},[79,2241,121],{"class":102},[79,2243,163],{"class":98},[79,2245,166],{"class":102},[79,2247,107],{"class":106},[79,2249,340],{"class":102},[79,2251,2252,2254,2256,2258,2260,2262,2264],{"class":81,"line":146},[79,2253,761],{"class":98},[79,2255,166],{"class":102},[79,2257,107],{"class":106},[79,2259,1570],{"class":102},[79,2261,1165],{"class":98},[79,2263,1168],{"class":1114},[79,2265,2266],{"class":85},"    -- ON: filters which orders match, preserves all customers\n",[79,2268,2269,2271,2273,2275,2277],{"class":81,"line":319},[79,2270,1582],{"class":98},[79,2272,103],{"class":102},[79,2274,107],{"class":106},[79,2276,110],{"class":102},[79,2278,174],{"class":106},[14,2280,2281,2282,2284,2285,2287],{},"Now customers with no (matching) orders still appear, with ",[76,2283,1515],{}," of zero rows = NULL, which ",[76,2286,1624],{}," turns into 0.",[14,2289,2290,2293,2294,2296,2297,2299,2300,2302,2303,2306,2307,2309,2310,107],{},[26,2291,2292],{},"The lesson",": conditions on the right table go in ",[76,2295,154],{}," for a ",[76,2298,244],{}," that should preserve all left rows; conditions in ",[76,2301,346],{}," filter the ",[50,2304,2305],{},"final"," rows and silently demote ",[76,2308,244],{}," to ",[76,2311,57],{},[18,2313,2314],{"id":2134},"Summary",[14,2316,2317,2318,2321,2322,2321,2325,2321,2328,2321,2331,2334,2335,2321,2337,2339,2340,2342,2343,2345,2346,1986,2348,107],{},"You can now combine tables with ",[76,2319,2320],{},"INNER","\u002F",[76,2323,2324],{},"LEFT",[76,2326,2327],{},"RIGHT",[76,2329,2330],{},"FULL",[76,2332,2333],{},"CROSS"," joins, choose between ",[76,2336,154],{},[76,2338,785],{},", write self-joins, and avoid the ",[76,2341,244],{},"+",[76,2344,346],{}," trap that demotes outer joins to inner joins. Next: aggregating rows with ",[76,2347,1582],{},[76,2349,2350],{},"HAVING",[2352,2353,2354],"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 .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}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 .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}",{"title":74,"searchDepth":89,"depth":89,"links":2356},[2357,2358,2359,2362,2363,2364,2365,2373,2374,2375,2376,2377,2378,2379,2380,2381],{"id":20,"depth":89,"text":21},{"id":56,"depth":89,"text":57},{"id":180,"depth":89,"text":181,"children":2360},[2361],{"id":277,"depth":95,"text":278},{"id":386,"depth":89,"text":387},{"id":475,"depth":89,"text":476},{"id":573,"depth":89,"text":574},{"id":636,"depth":89,"text":637,"children":2366},[2367,2369,2371],{"id":640,"depth":95,"text":2368},"ON — explicit condition",{"id":782,"depth":95,"text":2370},"USING — shorthand for same-named columns",{"id":827,"depth":95,"text":2372},"NATURAL JOIN — auto-join on all same-named columns (avoid)",{"id":871,"depth":89,"text":872},{"id":1207,"depth":89,"text":1208},{"id":1300,"depth":89,"text":1301},{"id":1477,"depth":89,"text":1478},{"id":1718,"depth":89,"text":1719},{"id":1780,"depth":89,"text":1781},{"id":1870,"depth":89,"text":1871},{"id":2031,"depth":89,"text":2032},{"id":2134,"depth":89,"text":2314},"md",{},"\u002Fsql\u002F04-joins",{"title":5,"description":16},"sql\u002F04-joins","bXTSAK2-MJJUqFDr3t72jSPDHxf7Pl0IzEjmcaFltjI",1789924654299]