[{"data":1,"prerenderedAt":2456},["ShallowReactive",2],{"page-\u002Fsql\u002F08-set-operations":3},{"id":4,"title":5,"body":6,"description":2449,"extension":2450,"meta":2451,"navigation":187,"path":2452,"seo":2453,"stem":2454,"__hash__":2455},"content\u002Fsql\u002F08-set-operations.md","08 — Set Operations",{"type":7,"value":8,"toc":2431},"minimark",[9,13,22,27,120,313,317,333,455,467,471,474,501,613,617,631,752,760,839,843,854,873,888,932,936,1256,1260,1480,1484,1489,1585,1589,1668,1672,1686,1880,1884,2004,2008,2146,2150,2153,2208,2406,2409,2427],[10,11,5],"h1",{"id":12},"_08-set-operations",[14,15,16,17,21],"p",{},"Set operations combine the ",[18,19,20],"strong",{},"rows"," of two result sets vertically (stacking), as opposed to joins, which combine them horizontally. Both inputs must have compatible column structure.",[23,24,26],"h2",{"id":25},"the-four-operations","The Four Operations",[28,29,30,46],"table",{},[31,32,33],"thead",{},[34,35,36,40,43],"tr",{},[37,38,39],"th",{},"Operation",[37,41,42],{},"Behavior",[37,44,45],{},"Dedup?",[47,48,49,67,82,99],"tbody",{},[34,50,51,58,64],{},[52,53,54],"td",{},[55,56,57],"code",{},"UNION",[52,59,60,61],{},"All rows from both inputs, ",[18,62,63],{},"duplicates removed",[52,65,66],{},"✅ (sort\u002Fhash)",[34,68,69,74,79],{},[52,70,71],{},[55,72,73],{},"UNION ALL",[52,75,60,76],{},[18,77,78],{},"duplicates kept",[52,80,81],{},"❌ (concatenate)",[34,83,84,89,96],{},[52,85,86],{},[55,87,88],{},"INTERSECT",[52,90,91,92,95],{},"Rows present in ",[18,93,94],{},"both"," inputs, duplicates removed",[52,97,98],{},"✅",[34,100,101,111,118],{},[52,102,103,106,107,110],{},[55,104,105],{},"EXCEPT"," (a.k.a. ",[55,108,109],{},"MINUS"," in Oracle)",[52,112,113,114,117],{},"Rows in the first input ",[18,115,116],{},"not in"," the second, duplicates removed",[52,119,98],{},[121,122,124],"code-wrapper",{"language":123},"sql",[125,126,130],"pre",{"className":127,"code":128,"language":123,"meta":129,"style":129},"language-sql shiki shiki-themes github-light github-dark","-- All cities that appear in either customers or suppliers (deduplicated).\n-- UNION sorts\u002Fhashes the combined result to remove duplicates: O(N log N) or O(N) memory.\nSELECT city FROM customers\nUNION\nSELECT city FROM suppliers;\n\n-- All cities including duplicates. UNION ALL just concatenates: O(1) per row.\n-- Use when inputs are known-disjoint or duplicates are desired.\nSELECT city FROM customers\nUNION ALL\nSELECT city FROM suppliers;\n\n-- Cities that have BOTH customers and suppliers (intersection).\nSELECT city FROM customers\nINTERSECT\nSELECT city FROM suppliers;\n\n-- Cities with customers but NO suppliers (set difference \u002F anti-join).\n-- EXCEPT is NULL-safe: (1, NULL) in both inputs → treated as equal → removed.\nSELECT city FROM customers\nEXCEPT\nSELECT city FROM suppliers;\n","",[55,131,132,141,147,164,170,182,189,195,201,212,218,229,234,240,251,257,268,273,279,285,296,302],{"__ignoreMap":129},[133,134,137],"span",{"class":135,"line":136},"line",1,[133,138,140],{"class":139},"sdCPZ","-- All cities that appear in either customers or suppliers (deduplicated).\n",[133,142,144],{"class":135,"line":143},2,[133,145,146],{"class":139},"-- UNION sorts\u002Fhashes the combined result to remove duplicates: O(N log N) or O(N) memory.\n",[133,148,150,154,158,161],{"class":135,"line":149},3,[133,151,153],{"class":152},"svdQ7","SELECT",[133,155,157],{"class":156},"ssxIu"," city ",[133,159,160],{"class":152},"FROM",[133,162,163],{"class":156}," customers\n",[133,165,167],{"class":135,"line":166},4,[133,168,169],{"class":152},"UNION\n",[133,171,173,175,177,179],{"class":135,"line":172},5,[133,174,153],{"class":152},[133,176,157],{"class":156},[133,178,160],{"class":152},[133,180,181],{"class":156}," suppliers;\n",[133,183,185],{"class":135,"line":184},6,[133,186,188],{"emptyLinePlaceholder":187},true,"\n",[133,190,192],{"class":135,"line":191},7,[133,193,194],{"class":139},"-- All cities including duplicates. UNION ALL just concatenates: O(1) per row.\n",[133,196,198],{"class":135,"line":197},8,[133,199,200],{"class":139},"-- Use when inputs are known-disjoint or duplicates are desired.\n",[133,202,204,206,208,210],{"class":135,"line":203},9,[133,205,153],{"class":152},[133,207,157],{"class":156},[133,209,160],{"class":152},[133,211,163],{"class":156},[133,213,215],{"class":135,"line":214},10,[133,216,217],{"class":152},"UNION ALL\n",[133,219,221,223,225,227],{"class":135,"line":220},11,[133,222,153],{"class":152},[133,224,157],{"class":156},[133,226,160],{"class":152},[133,228,181],{"class":156},[133,230,232],{"class":135,"line":231},12,[133,233,188],{"emptyLinePlaceholder":187},[133,235,237],{"class":135,"line":236},13,[133,238,239],{"class":139},"-- Cities that have BOTH customers and suppliers (intersection).\n",[133,241,243,245,247,249],{"class":135,"line":242},14,[133,244,153],{"class":152},[133,246,157],{"class":156},[133,248,160],{"class":152},[133,250,163],{"class":156},[133,252,254],{"class":135,"line":253},15,[133,255,256],{"class":152},"INTERSECT\n",[133,258,260,262,264,266],{"class":135,"line":259},16,[133,261,153],{"class":152},[133,263,157],{"class":156},[133,265,160],{"class":152},[133,267,181],{"class":156},[133,269,271],{"class":135,"line":270},17,[133,272,188],{"emptyLinePlaceholder":187},[133,274,276],{"class":135,"line":275},18,[133,277,278],{"class":139},"-- Cities with customers but NO suppliers (set difference \u002F anti-join).\n",[133,280,282],{"class":135,"line":281},19,[133,283,284],{"class":139},"-- EXCEPT is NULL-safe: (1, NULL) in both inputs → treated as equal → removed.\n",[133,286,288,290,292,294],{"class":135,"line":287},20,[133,289,153],{"class":152},[133,291,157],{"class":156},[133,293,160],{"class":152},[133,295,163],{"class":156},[133,297,299],{"class":135,"line":298},21,[133,300,301],{"class":152},"EXCEPT\n",[133,303,305,307,309,311],{"class":135,"line":304},22,[133,306,153],{"class":152},[133,308,157],{"class":156},[133,310,160],{"class":152},[133,312,181],{"class":156},[23,314,316],{"id":315},"union-vs-union-all-the-hidden-dedup-cost","UNION vs UNION ALL — The Hidden Dedup Cost",[14,318,319,321,322,325,326,329,330,332],{},[55,320,57],{}," (without ",[55,323,324],{},"ALL",") removes duplicates, which requires a ",[18,327,328],{},"sort or hash"," across the entire combined result — O(N log N) time or O(N) memory. ",[55,331,73],{}," just concatenates — O(1).",[121,334,335],{"language":123},[125,336,338],{"className":127,"code":337,"language":123,"meta":129,"style":129},"-- ❌ Reflexive UNION: pays dedup cost even though the two queries can never overlap\n-- (different 'kind' values). The sort\u002Fhash is wasted work.\nSELECT id, name, 'customer' AS kind FROM customers\nUNION\nSELECT id, name, 'supplier' AS kind FROM suppliers;\n\n-- ✅ UNION ALL: no dedup, O(1) concatenation. Correct when inputs are disjoint.\nSELECT id, name, 'customer' AS kind FROM customers\nUNION ALL\nSELECT id, name, 'supplier' AS kind FROM suppliers;\n",[55,339,340,345,350,377,381,402,406,411,431,435],{"__ignoreMap":129},[133,341,342],{"class":135,"line":136},[133,343,344],{"class":139},"-- ❌ Reflexive UNION: pays dedup cost even though the two queries can never overlap\n",[133,346,347],{"class":135,"line":143},[133,348,349],{"class":139},"-- (different 'kind' values). The sort\u002Fhash is wasted work.\n",[133,351,352,354,357,360,363,367,370,373,375],{"class":135,"line":149},[133,353,153],{"class":152},[133,355,356],{"class":156}," id, ",[133,358,359],{"class":152},"name",[133,361,362],{"class":156},", ",[133,364,366],{"class":365},"sJ6F3","'customer'",[133,368,369],{"class":152}," AS",[133,371,372],{"class":156}," kind ",[133,374,160],{"class":152},[133,376,163],{"class":156},[133,378,379],{"class":135,"line":166},[133,380,169],{"class":152},[133,382,383,385,387,389,391,394,396,398,400],{"class":135,"line":172},[133,384,153],{"class":152},[133,386,356],{"class":156},[133,388,359],{"class":152},[133,390,362],{"class":156},[133,392,393],{"class":365},"'supplier'",[133,395,369],{"class":152},[133,397,372],{"class":156},[133,399,160],{"class":152},[133,401,181],{"class":156},[133,403,404],{"class":135,"line":184},[133,405,188],{"emptyLinePlaceholder":187},[133,407,408],{"class":135,"line":191},[133,409,410],{"class":139},"-- ✅ UNION ALL: no dedup, O(1) concatenation. Correct when inputs are disjoint.\n",[133,412,413,415,417,419,421,423,425,427,429],{"class":135,"line":197},[133,414,153],{"class":152},[133,416,356],{"class":156},[133,418,359],{"class":152},[133,420,362],{"class":156},[133,422,366],{"class":365},[133,424,369],{"class":152},[133,426,372],{"class":156},[133,428,160],{"class":152},[133,430,163],{"class":156},[133,432,433],{"class":135,"line":203},[133,434,217],{"class":152},[133,436,437,439,441,443,445,447,449,451,453],{"class":135,"line":214},[133,438,153],{"class":152},[133,440,356],{"class":156},[133,442,359],{"class":152},[133,444,362],{"class":156},[133,446,393],{"class":365},[133,448,369],{"class":152},[133,450,372],{"class":156},[133,452,160],{"class":152},[133,454,181],{"class":156},[14,456,457,458,460,461,463,464,466],{},"If you know the inputs are disjoint (or you want duplicates), always use ",[55,459,73],{},". A common mistake is reflexively writing ",[55,462,57],{}," when ",[55,465,73],{}," is correct, paying a hidden dedup cost.",[23,468,470],{"id":469},"column-compatibility-rules","Column Compatibility Rules",[14,472,473],{},"For a set operation to be valid:",[475,476,477,484,490],"ol",{},[478,479,480,483],"li",{},[18,481,482],{},"Same number of columns"," in both inputs.",[478,485,486,489],{},[18,487,488],{},"Compatible types"," in each column position (the database coerces where possible).",[478,491,492,493,496,497,500],{},"Column ",[18,494,495],{},"names"," come from the ",[18,498,499],{},"first"," input — the second input's column names are ignored.",[121,502,503],{"language":123},[125,504,506],{"className":127,"code":505,"language":123,"meta":129,"style":129},"-- ✅ OK: same column count, compatible types. Result column is named \"name\" (from first SELECT).\nSELECT id, name FROM customers\nUNION ALL\nSELECT id, company_name FROM suppliers;   -- 'company_name' is ignored; result column = 'name'\n\n-- ❌ ERROR: each SELECT must have the same number of columns.\nSELECT id, name FROM customers\nUNION ALL\nSELECT id FROM suppliers;\n\n-- Type coercion can surprise: integer + text may coerce or error depending on engine.\nSELECT 1 UNION SELECT 'a';   -- PostgreSQL: errors; MySQL: coerces to text\n",[55,507,508,513,526,530,545,549,554,566,570,581,585,590],{"__ignoreMap":129},[133,509,510],{"class":135,"line":136},[133,511,512],{"class":139},"-- ✅ OK: same column count, compatible types. Result column is named \"name\" (from first SELECT).\n",[133,514,515,517,519,521,524],{"class":135,"line":143},[133,516,153],{"class":152},[133,518,356],{"class":156},[133,520,359],{"class":152},[133,522,523],{"class":152}," FROM",[133,525,163],{"class":156},[133,527,528],{"class":135,"line":149},[133,529,217],{"class":152},[133,531,532,534,537,539,542],{"class":135,"line":166},[133,533,153],{"class":152},[133,535,536],{"class":156}," id, company_name ",[133,538,160],{"class":152},[133,540,541],{"class":156}," suppliers;   ",[133,543,544],{"class":139},"-- 'company_name' is ignored; result column = 'name'\n",[133,546,547],{"class":135,"line":172},[133,548,188],{"emptyLinePlaceholder":187},[133,550,551],{"class":135,"line":184},[133,552,553],{"class":139},"-- ❌ ERROR: each SELECT must have the same number of columns.\n",[133,555,556,558,560,562,564],{"class":135,"line":191},[133,557,153],{"class":152},[133,559,356],{"class":156},[133,561,359],{"class":152},[133,563,523],{"class":152},[133,565,163],{"class":156},[133,567,568],{"class":135,"line":197},[133,569,217],{"class":152},[133,571,572,574,577,579],{"class":135,"line":203},[133,573,153],{"class":152},[133,575,576],{"class":156}," id ",[133,578,160],{"class":152},[133,580,181],{"class":156},[133,582,583],{"class":135,"line":214},[133,584,188],{"emptyLinePlaceholder":187},[133,586,587],{"class":135,"line":220},[133,588,589],{"class":139},"-- Type coercion can surprise: integer + text may coerce or error depending on engine.\n",[133,591,592,594,598,601,604,607,610],{"class":135,"line":231},[133,593,153],{"class":152},[133,595,597],{"class":596},"snvgF"," 1",[133,599,600],{"class":152}," UNION",[133,602,603],{"class":152}," SELECT",[133,605,606],{"class":365}," 'a'",[133,608,609],{"class":156},";   ",[133,611,612],{"class":139},"-- PostgreSQL: errors; MySQL: coerces to text\n",[23,614,616],{"id":615},"order-by-and-limit-with-set-operations","ORDER BY and LIMIT with Set Operations",[14,618,619,622,623,626,627,630],{},[55,620,621],{},"ORDER BY","\u002F",[55,624,625],{},"LIMIT"," apply to the ",[18,628,629],{},"whole"," combined result and must come at the end. You can't order one input separately:",[121,632,633],{"language":123},[125,634,636],{"className":127,"code":635,"language":123,"meta":129,"style":129},"-- ORDER BY applies to the ENTIRE combined result, not individual SELECTs.\n-- Column names in ORDER BY must match the FIRST input's column names.\nSELECT name, 'customer' AS kind FROM customers\nUNION ALL\nSELECT name, 'supplier' AS kind FROM suppliers\nORDER BY name;                             -- sorts the combined 2-row-per-entity result\n\n-- LIMIT also applies to the whole combined result.\nSELECT name FROM customers\nUNION ALL\nSELECT name FROM suppliers\nORDER BY name\nLIMIT 10;\n",[55,637,638,643,648,667,671,690,702,706,711,721,725,735,742],{"__ignoreMap":129},[133,639,640],{"class":135,"line":136},[133,641,642],{"class":139},"-- ORDER BY applies to the ENTIRE combined result, not individual SELECTs.\n",[133,644,645],{"class":135,"line":143},[133,646,647],{"class":139},"-- Column names in ORDER BY must match the FIRST input's column names.\n",[133,649,650,652,655,657,659,661,663,665],{"class":135,"line":149},[133,651,153],{"class":152},[133,653,654],{"class":152}," name",[133,656,362],{"class":156},[133,658,366],{"class":365},[133,660,369],{"class":152},[133,662,372],{"class":156},[133,664,160],{"class":152},[133,666,163],{"class":156},[133,668,669],{"class":135,"line":166},[133,670,217],{"class":152},[133,672,673,675,677,679,681,683,685,687],{"class":135,"line":172},[133,674,153],{"class":152},[133,676,654],{"class":152},[133,678,362],{"class":156},[133,680,393],{"class":365},[133,682,369],{"class":152},[133,684,372],{"class":156},[133,686,160],{"class":152},[133,688,689],{"class":156}," suppliers\n",[133,691,692,694,696,699],{"class":135,"line":184},[133,693,621],{"class":152},[133,695,654],{"class":152},[133,697,698],{"class":156},";                             ",[133,700,701],{"class":139},"-- sorts the combined 2-row-per-entity result\n",[133,703,704],{"class":135,"line":191},[133,705,188],{"emptyLinePlaceholder":187},[133,707,708],{"class":135,"line":197},[133,709,710],{"class":139},"-- LIMIT also applies to the whole combined result.\n",[133,712,713,715,717,719],{"class":135,"line":203},[133,714,153],{"class":152},[133,716,654],{"class":152},[133,718,523],{"class":152},[133,720,163],{"class":156},[133,722,723],{"class":135,"line":214},[133,724,217],{"class":152},[133,726,727,729,731,733],{"class":135,"line":220},[133,728,153],{"class":152},[133,730,654],{"class":152},[133,732,523],{"class":152},[133,734,689],{"class":156},[133,736,737,739],{"class":135,"line":231},[133,738,621],{"class":152},[133,740,741],{"class":152}," name\n",[133,743,744,746,749],{"class":135,"line":236},[133,745,625],{"class":152},[133,747,748],{"class":596}," 10",[133,750,751],{"class":156},";\n",[14,753,754,755,759],{},"To limit ",[756,757,758],"em",{},"each"," input before combining, use parenthesized subqueries:",[121,761,762],{"language":123},[125,763,765],{"className":127,"code":764,"language":123,"meta":129,"style":129},"-- Limit each branch to 5 rows, then combine and sort the 10-row result.\n-- PostgreSQL allows parenthesized set operation branches; standard SQL and other engines vary.\n(SELECT name FROM customers ORDER BY name LIMIT 5)\nUNION ALL\n(SELECT name FROM suppliers ORDER BY name LIMIT 5)\nORDER BY name;\n",[55,766,767,772,777,804,808,831],{"__ignoreMap":129},[133,768,769],{"class":135,"line":136},[133,770,771],{"class":139},"-- Limit each branch to 5 rows, then combine and sort the 10-row result.\n",[133,773,774],{"class":135,"line":143},[133,775,776],{"class":139},"-- PostgreSQL allows parenthesized set operation branches; standard SQL and other engines vary.\n",[133,778,779,782,784,786,788,791,793,795,798,801],{"class":135,"line":149},[133,780,781],{"class":156},"(",[133,783,153],{"class":152},[133,785,654],{"class":152},[133,787,523],{"class":152},[133,789,790],{"class":156}," customers ",[133,792,621],{"class":152},[133,794,654],{"class":152},[133,796,797],{"class":152}," LIMIT",[133,799,800],{"class":596}," 5",[133,802,803],{"class":156},")\n",[133,805,806],{"class":135,"line":166},[133,807,217],{"class":152},[133,809,810,812,814,816,818,821,823,825,827,829],{"class":135,"line":172},[133,811,781],{"class":156},[133,813,153],{"class":152},[133,815,654],{"class":152},[133,817,523],{"class":152},[133,819,820],{"class":156}," suppliers ",[133,822,621],{"class":152},[133,824,654],{"class":152},[133,826,797],{"class":152},[133,828,800],{"class":596},[133,830,803],{"class":156},[133,832,833,835,837],{"class":135,"line":184},[133,834,621],{"class":152},[133,836,654],{"class":152},[133,838,751],{"class":156},[23,840,842],{"id":841},"intersect-all-and-except-all-postgresql","INTERSECT ALL and EXCEPT ALL (PostgreSQL)",[14,844,845,846,849,850,853],{},"PostgreSQL supports ",[55,847,848],{},"INTERSECT ALL"," and ",[55,851,852],{},"EXCEPT ALL",", which keep duplicates based on row multiplicity:",[855,856,857,866],"ul",{},[478,858,859,861,862,865],{},[55,860,848],{},": if a row appears N times in the left and M times in the right, it appears ",[55,863,864],{},"min(N, M)"," times.",[478,867,868,861,870,865],{},[55,869,852],{},[55,871,872],{},"max(0, N - M)",[14,874,875,876,622,878,321,880,882,883,622,885,887],{},"Standard ",[55,877,88],{},[55,879,105],{},[55,881,324],{},") dedup to one copy. MySQL and SQLite don't support ",[55,884,848],{},[55,886,852],{},".",[121,889,890],{"language":123},[125,891,893],{"className":127,"code":892,"language":123,"meta":129,"style":129},"-- EXCEPT ALL: preserves multiplicity. If 'NYC' appears 3x in customers and 1x in suppliers,\n-- the result has 'NYC' 2x (3 - 1 = 2). EXCEPT (without ALL) would show 'NYC' 0x (deduped to 1, then removed).\nSELECT city FROM customers\nEXCEPT ALL\nSELECT city FROM suppliers;\n",[55,894,895,900,905,915,922],{"__ignoreMap":129},[133,896,897],{"class":135,"line":136},[133,898,899],{"class":139},"-- EXCEPT ALL: preserves multiplicity. If 'NYC' appears 3x in customers and 1x in suppliers,\n",[133,901,902],{"class":135,"line":143},[133,903,904],{"class":139},"-- the result has 'NYC' 2x (3 - 1 = 2). EXCEPT (without ALL) would show 'NYC' 0x (deduped to 1, then removed).\n",[133,906,907,909,911,913],{"class":135,"line":149},[133,908,153],{"class":152},[133,910,157],{"class":156},[133,912,160],{"class":152},[133,914,163],{"class":156},[133,916,917,919],{"class":135,"line":166},[133,918,105],{"class":152},[133,920,921],{"class":156}," ALL\n",[133,923,924,926,928,930],{"class":135,"line":172},[133,925,153],{"class":152},[133,927,157],{"class":156},[133,929,160],{"class":152},[133,931,181],{"class":156},[23,933,935],{"id":934},"complex-implementation-unified-customer-view-from-multiple-sources","Complex Implementation: Unified Customer View from Multiple Sources",[121,937,938],{"language":123},[125,939,941],{"className":127,"code":940,"language":123,"meta":129,"style":129},"-- Build a unified customer view from three sources: CRM, billing, and support.\n-- UNION ALL because the sources are disjoint (different ID ranges, tagged by source).\n-- A provenance column ('source') tracks origin for debugging and dedup audits.\nSELECT\n  id,\n  email,\n  name,\n  company,\n  'crm' AS source,                           -- provenance: which system this row came from\n  created_at\nFROM crm.customers\nWHERE email IS NOT NULL                       -- drop rows without email (can't dedup on NULL)\n\nUNION ALL\n\nSELECT\n  id + 1000000 AS id,                         -- offset to avoid ID collision with CRM\n  email,\n  billing_contact AS name,\n  company_name AS company,\n  'billing' AS source,\n  registered_at AS created_at\nFROM billing.accounts\nWHERE email IS NOT NULL\n\nUNION ALL\n\nSELECT\n  id + 2000000 AS id,                         -- offset to avoid ID collision with CRM and billing\n  email,\n  display_name AS name,\n  organization AS company,\n  'support' AS source,\n  first_seen AS created_at\nFROM support.users\nWHERE email IS NOT NULL\n\nORDER BY email, source;                       -- group duplicates together for inspection\n",[55,942,943,948,953,958,963,968,973,981,986,999,1004,1016,1030,1034,1038,1042,1046,1065,1069,1081,1091,1101,1111,1124,1134,1139,1144,1149,1154,1171,1176,1188,1198,1208,1218,1231,1240,1245],{"__ignoreMap":129},[133,944,945],{"class":135,"line":136},[133,946,947],{"class":139},"-- Build a unified customer view from three sources: CRM, billing, and support.\n",[133,949,950],{"class":135,"line":143},[133,951,952],{"class":139},"-- UNION ALL because the sources are disjoint (different ID ranges, tagged by source).\n",[133,954,955],{"class":135,"line":149},[133,956,957],{"class":139},"-- A provenance column ('source') tracks origin for debugging and dedup audits.\n",[133,959,960],{"class":135,"line":166},[133,961,962],{"class":152},"SELECT\n",[133,964,965],{"class":135,"line":172},[133,966,967],{"class":156},"  id,\n",[133,969,970],{"class":135,"line":184},[133,971,972],{"class":156},"  email,\n",[133,974,975,978],{"class":135,"line":191},[133,976,977],{"class":152},"  name",[133,979,980],{"class":156},",\n",[133,982,983],{"class":135,"line":197},[133,984,985],{"class":156},"  company,\n",[133,987,988,991,993,996],{"class":135,"line":203},[133,989,990],{"class":365},"  'crm'",[133,992,369],{"class":152},[133,994,995],{"class":156}," source,                           ",[133,997,998],{"class":139},"-- provenance: which system this row came from\n",[133,1000,1001],{"class":135,"line":214},[133,1002,1003],{"class":156},"  created_at\n",[133,1005,1006,1008,1011,1013],{"class":135,"line":220},[133,1007,160],{"class":152},[133,1009,1010],{"class":596}," crm",[133,1012,887],{"class":156},[133,1014,1015],{"class":596},"customers\n",[133,1017,1018,1021,1024,1027],{"class":135,"line":231},[133,1019,1020],{"class":152},"WHERE",[133,1022,1023],{"class":156}," email ",[133,1025,1026],{"class":152},"IS NOT NULL",[133,1028,1029],{"class":139},"                       -- drop rows without email (can't dedup on NULL)\n",[133,1031,1032],{"class":135,"line":236},[133,1033,188],{"emptyLinePlaceholder":187},[133,1035,1036],{"class":135,"line":242},[133,1037,217],{"class":152},[133,1039,1040],{"class":135,"line":253},[133,1041,188],{"emptyLinePlaceholder":187},[133,1043,1044],{"class":135,"line":259},[133,1045,962],{"class":152},[133,1047,1048,1051,1054,1057,1059,1062],{"class":135,"line":270},[133,1049,1050],{"class":156},"  id ",[133,1052,1053],{"class":152},"+",[133,1055,1056],{"class":596}," 1000000",[133,1058,369],{"class":152},[133,1060,1061],{"class":156}," id,                         ",[133,1063,1064],{"class":139},"-- offset to avoid ID collision with CRM\n",[133,1066,1067],{"class":135,"line":275},[133,1068,972],{"class":156},[133,1070,1071,1074,1077,1079],{"class":135,"line":281},[133,1072,1073],{"class":156},"  billing_contact ",[133,1075,1076],{"class":152},"AS",[133,1078,654],{"class":152},[133,1080,980],{"class":156},[133,1082,1083,1086,1088],{"class":135,"line":287},[133,1084,1085],{"class":156},"  company_name ",[133,1087,1076],{"class":152},[133,1089,1090],{"class":156}," company,\n",[133,1092,1093,1096,1098],{"class":135,"line":298},[133,1094,1095],{"class":365},"  'billing'",[133,1097,369],{"class":152},[133,1099,1100],{"class":156}," source,\n",[133,1102,1103,1106,1108],{"class":135,"line":304},[133,1104,1105],{"class":156},"  registered_at ",[133,1107,1076],{"class":152},[133,1109,1110],{"class":156}," created_at\n",[133,1112,1114,1116,1119,1121],{"class":135,"line":1113},23,[133,1115,160],{"class":152},[133,1117,1118],{"class":596}," billing",[133,1120,887],{"class":156},[133,1122,1123],{"class":596},"accounts\n",[133,1125,1127,1129,1131],{"class":135,"line":1126},24,[133,1128,1020],{"class":152},[133,1130,1023],{"class":156},[133,1132,1133],{"class":152},"IS NOT NULL\n",[133,1135,1137],{"class":135,"line":1136},25,[133,1138,188],{"emptyLinePlaceholder":187},[133,1140,1142],{"class":135,"line":1141},26,[133,1143,217],{"class":152},[133,1145,1147],{"class":135,"line":1146},27,[133,1148,188],{"emptyLinePlaceholder":187},[133,1150,1152],{"class":135,"line":1151},28,[133,1153,962],{"class":152},[133,1155,1157,1159,1161,1164,1166,1168],{"class":135,"line":1156},29,[133,1158,1050],{"class":156},[133,1160,1053],{"class":152},[133,1162,1163],{"class":596}," 2000000",[133,1165,369],{"class":152},[133,1167,1061],{"class":156},[133,1169,1170],{"class":139},"-- offset to avoid ID collision with CRM and billing\n",[133,1172,1174],{"class":135,"line":1173},30,[133,1175,972],{"class":156},[133,1177,1179,1182,1184,1186],{"class":135,"line":1178},31,[133,1180,1181],{"class":156},"  display_name ",[133,1183,1076],{"class":152},[133,1185,654],{"class":152},[133,1187,980],{"class":156},[133,1189,1191,1194,1196],{"class":135,"line":1190},32,[133,1192,1193],{"class":156},"  organization ",[133,1195,1076],{"class":152},[133,1197,1090],{"class":156},[133,1199,1201,1204,1206],{"class":135,"line":1200},33,[133,1202,1203],{"class":365},"  'support'",[133,1205,369],{"class":152},[133,1207,1100],{"class":156},[133,1209,1211,1214,1216],{"class":135,"line":1210},34,[133,1212,1213],{"class":156},"  first_seen ",[133,1215,1076],{"class":152},[133,1217,1110],{"class":156},[133,1219,1221,1223,1226,1228],{"class":135,"line":1220},35,[133,1222,160],{"class":152},[133,1224,1225],{"class":596}," support",[133,1227,887],{"class":156},[133,1229,1230],{"class":596},"users\n",[133,1232,1234,1236,1238],{"class":135,"line":1233},36,[133,1235,1020],{"class":152},[133,1237,1023],{"class":156},[133,1239,1133],{"class":152},[133,1241,1243],{"class":135,"line":1242},37,[133,1244,188],{"emptyLinePlaceholder":187},[133,1246,1248,1250,1253],{"class":135,"line":1247},38,[133,1249,621],{"class":152},[133,1251,1252],{"class":156}," email, source;                       ",[133,1254,1255],{"class":139},"-- group duplicates together for inspection\n",[23,1257,1259],{"id":1258},"complex-implementation-except-based-missing-data-query","Complex Implementation: EXCEPT-Based Missing Data Query",[121,1261,1262],{"language":123},[125,1263,1265],{"className":127,"code":1264,"language":123,"meta":129,"style":129},"-- Find products that exist in the catalog but have NEVER been ordered.\n-- EXCEPT is a NULL-safe anti-join: (product_id, NULL) in both sides → treated as equal → removed.\n-- Unlike NOT IN, EXCEPT doesn't poison on NULL.\nSELECT product_id, product_name\nFROM (\n  SELECT id AS product_id, name AS product_name FROM products\n) catalog\nEXCEPT\nSELECT product_id, NULL AS product_name        -- column count must match; NULL placeholder\nFROM (\n  SELECT DISTINCT product_id FROM order_items\n) ordered\n\nORDER BY product_id;\n\n-- Symmetric difference: rows in exactly one of two tables (useful for prod vs staging diffs).\n-- (A EXCEPT B) UNION (B EXCEPT A) = empty iff A = B (as sets).\n(SELECT * FROM prod.users\n EXCEPT\n SELECT * FROM staging.users)\nUNION\n(SELECT * FROM staging.users\n EXCEPT\n SELECT * FROM prod.users);\n",[55,1266,1267,1272,1277,1282,1289,1296,1320,1328,1332,1349,1355,1368,1373,1377,1384,1388,1393,1398,1416,1421,1439,1443,1459,1463],{"__ignoreMap":129},[133,1268,1269],{"class":135,"line":136},[133,1270,1271],{"class":139},"-- Find products that exist in the catalog but have NEVER been ordered.\n",[133,1273,1274],{"class":135,"line":143},[133,1275,1276],{"class":139},"-- EXCEPT is a NULL-safe anti-join: (product_id, NULL) in both sides → treated as equal → removed.\n",[133,1278,1279],{"class":135,"line":149},[133,1280,1281],{"class":139},"-- Unlike NOT IN, EXCEPT doesn't poison on NULL.\n",[133,1283,1284,1286],{"class":135,"line":166},[133,1285,153],{"class":152},[133,1287,1288],{"class":156}," product_id, product_name\n",[133,1290,1291,1293],{"class":135,"line":172},[133,1292,160],{"class":152},[133,1294,1295],{"class":156}," (\n",[133,1297,1298,1301,1303,1305,1308,1310,1312,1315,1317],{"class":135,"line":184},[133,1299,1300],{"class":152},"  SELECT",[133,1302,576],{"class":156},[133,1304,1076],{"class":152},[133,1306,1307],{"class":156}," product_id, ",[133,1309,359],{"class":152},[133,1311,369],{"class":152},[133,1313,1314],{"class":156}," product_name ",[133,1316,160],{"class":152},[133,1318,1319],{"class":156}," products\n",[133,1321,1322,1325],{"class":135,"line":191},[133,1323,1324],{"class":156},") ",[133,1326,1327],{"class":152},"catalog\n",[133,1329,1330],{"class":135,"line":197},[133,1331,301],{"class":152},[133,1333,1334,1336,1338,1341,1343,1346],{"class":135,"line":203},[133,1335,153],{"class":152},[133,1337,1307],{"class":156},[133,1339,1340],{"class":152},"NULL",[133,1342,369],{"class":152},[133,1344,1345],{"class":156}," product_name        ",[133,1347,1348],{"class":139},"-- column count must match; NULL placeholder\n",[133,1350,1351,1353],{"class":135,"line":214},[133,1352,160],{"class":152},[133,1354,1295],{"class":156},[133,1356,1357,1360,1363,1365],{"class":135,"line":220},[133,1358,1359],{"class":152},"  SELECT DISTINCT",[133,1361,1362],{"class":156}," product_id ",[133,1364,160],{"class":152},[133,1366,1367],{"class":156}," order_items\n",[133,1369,1370],{"class":135,"line":231},[133,1371,1372],{"class":156},") ordered\n",[133,1374,1375],{"class":135,"line":236},[133,1376,188],{"emptyLinePlaceholder":187},[133,1378,1379,1381],{"class":135,"line":242},[133,1380,621],{"class":152},[133,1382,1383],{"class":156}," product_id;\n",[133,1385,1386],{"class":135,"line":253},[133,1387,188],{"emptyLinePlaceholder":187},[133,1389,1390],{"class":135,"line":259},[133,1391,1392],{"class":139},"-- Symmetric difference: rows in exactly one of two tables (useful for prod vs staging diffs).\n",[133,1394,1395],{"class":135,"line":270},[133,1396,1397],{"class":139},"-- (A EXCEPT B) UNION (B EXCEPT A) = empty iff A = B (as sets).\n",[133,1399,1400,1402,1404,1407,1409,1412,1414],{"class":135,"line":275},[133,1401,781],{"class":156},[133,1403,153],{"class":152},[133,1405,1406],{"class":152}," *",[133,1408,523],{"class":152},[133,1410,1411],{"class":596}," prod",[133,1413,887],{"class":156},[133,1415,1230],{"class":596},[133,1417,1418],{"class":135,"line":281},[133,1419,1420],{"class":152}," EXCEPT\n",[133,1422,1423,1425,1427,1429,1432,1434,1437],{"class":135,"line":287},[133,1424,603],{"class":152},[133,1426,1406],{"class":152},[133,1428,523],{"class":152},[133,1430,1431],{"class":596}," staging",[133,1433,887],{"class":156},[133,1435,1436],{"class":596},"users",[133,1438,803],{"class":156},[133,1440,1441],{"class":135,"line":298},[133,1442,169],{"class":152},[133,1444,1445,1447,1449,1451,1453,1455,1457],{"class":135,"line":304},[133,1446,781],{"class":156},[133,1448,153],{"class":152},[133,1450,1406],{"class":152},[133,1452,523],{"class":152},[133,1454,1431],{"class":596},[133,1456,887],{"class":156},[133,1458,1230],{"class":596},[133,1460,1461],{"class":135,"line":1113},[133,1462,1420],{"class":152},[133,1464,1465,1467,1469,1471,1473,1475,1477],{"class":135,"line":1126},[133,1466,603],{"class":152},[133,1468,1406],{"class":152},[133,1470,523],{"class":152},[133,1472,1411],{"class":596},[133,1474,887],{"class":156},[133,1476,1436],{"class":596},[133,1478,1479],{"class":156},");\n",[23,1481,1483],{"id":1482},"anti-pattern-reflexive-union-when-union-all-is-correct","Anti-Pattern: Reflexive UNION When UNION ALL Is Correct",[1485,1486,1488],"h3",{"id":1487},"wrong-way","❌ Wrong Way",[121,1490,1491],{"language":123},[125,1492,1494],{"className":127,"code":1493,"language":123,"meta":129,"style":129},"-- Intent: combine active and inactive users into one report.\n-- Bug: using UNION forces a sort\u002Fhash dedup across the entire result.\n-- But active and inactive users are DISJOINT (status = 'active' vs 'inactive') —\n-- there can be no duplicates between them. The dedup is wasted work.\nSELECT id, name, 'active' AS status FROM users WHERE status = 'active'\nUNION\nSELECT id, name, 'inactive' AS status FROM users WHERE status = 'inactive';\n",[55,1495,1496,1501,1506,1511,1516,1549,1553],{"__ignoreMap":129},[133,1497,1498],{"class":135,"line":136},[133,1499,1500],{"class":139},"-- Intent: combine active and inactive users into one report.\n",[133,1502,1503],{"class":135,"line":143},[133,1504,1505],{"class":139},"-- Bug: using UNION forces a sort\u002Fhash dedup across the entire result.\n",[133,1507,1508],{"class":135,"line":149},[133,1509,1510],{"class":139},"-- But active and inactive users are DISJOINT (status = 'active' vs 'inactive') —\n",[133,1512,1513],{"class":135,"line":166},[133,1514,1515],{"class":139},"-- there can be no duplicates between them. The dedup is wasted work.\n",[133,1517,1518,1520,1522,1524,1526,1529,1531,1534,1536,1539,1541,1543,1546],{"class":135,"line":172},[133,1519,153],{"class":152},[133,1521,356],{"class":156},[133,1523,359],{"class":152},[133,1525,362],{"class":156},[133,1527,1528],{"class":365},"'active'",[133,1530,369],{"class":152},[133,1532,1533],{"class":152}," status",[133,1535,523],{"class":152},[133,1537,1538],{"class":156}," users ",[133,1540,1020],{"class":152},[133,1542,1533],{"class":152},[133,1544,1545],{"class":152}," =",[133,1547,1548],{"class":365}," 'active'\n",[133,1550,1551],{"class":135,"line":184},[133,1552,169],{"class":152},[133,1554,1555,1557,1559,1561,1563,1566,1568,1570,1572,1574,1576,1578,1580,1583],{"class":135,"line":191},[133,1556,153],{"class":152},[133,1558,356],{"class":156},[133,1560,359],{"class":152},[133,1562,362],{"class":156},[133,1564,1565],{"class":365},"'inactive'",[133,1567,369],{"class":152},[133,1569,1533],{"class":152},[133,1571,523],{"class":152},[133,1573,1538],{"class":156},[133,1575,1020],{"class":152},[133,1577,1533],{"class":152},[133,1579,1545],{"class":152},[133,1581,1582],{"class":365}," 'inactive'",[133,1584,751],{"class":156},[1485,1586,1588],{"id":1587},"right-way","✅ Right Way",[121,1590,1591],{"language":123},[125,1592,1594],{"className":127,"code":1593,"language":123,"meta":129,"style":129},"-- UNION ALL: no dedup, O(1) concatenation. Correct because the WHERE clauses\n-- partition the table into disjoint sets — no row can satisfy both conditions.\nSELECT id, name, 'active' AS status FROM users WHERE status = 'active'\nUNION ALL\nSELECT id, name, 'inactive' AS status FROM users WHERE status = 'inactive';\n",[55,1595,1596,1601,1606,1634,1638],{"__ignoreMap":129},[133,1597,1598],{"class":135,"line":136},[133,1599,1600],{"class":139},"-- UNION ALL: no dedup, O(1) concatenation. Correct because the WHERE clauses\n",[133,1602,1603],{"class":135,"line":143},[133,1604,1605],{"class":139},"-- partition the table into disjoint sets — no row can satisfy both conditions.\n",[133,1607,1608,1610,1612,1614,1616,1618,1620,1622,1624,1626,1628,1630,1632],{"class":135,"line":149},[133,1609,153],{"class":152},[133,1611,356],{"class":156},[133,1613,359],{"class":152},[133,1615,362],{"class":156},[133,1617,1528],{"class":365},[133,1619,369],{"class":152},[133,1621,1533],{"class":152},[133,1623,523],{"class":152},[133,1625,1538],{"class":156},[133,1627,1020],{"class":152},[133,1629,1533],{"class":152},[133,1631,1545],{"class":152},[133,1633,1548],{"class":365},[133,1635,1636],{"class":135,"line":166},[133,1637,217],{"class":152},[133,1639,1640,1642,1644,1646,1648,1650,1652,1654,1656,1658,1660,1662,1664,1666],{"class":135,"line":172},[133,1641,153],{"class":152},[133,1643,356],{"class":156},[133,1645,359],{"class":152},[133,1647,362],{"class":156},[133,1649,1565],{"class":365},[133,1651,369],{"class":152},[133,1653,1533],{"class":152},[133,1655,523],{"class":152},[133,1657,1538],{"class":156},[133,1659,1020],{"class":152},[133,1661,1533],{"class":152},[133,1663,1545],{"class":152},[133,1665,1582],{"class":365},[133,1667,751],{"class":156},[23,1669,1671],{"id":1670},"emulating-full-outer-join-with-union","Emulating FULL OUTER JOIN with UNION",[14,1673,1674,1675,1678,1679,1681,1682,1685],{},"MySQL and SQLite lack ",[55,1676,1677],{},"FULL OUTER JOIN",". Emulate it with ",[55,1680,57],{}," of a ",[55,1683,1684],{},"LEFT JOIN"," and an anti-join:",[121,1687,1688],{"language":123},[125,1689,1691],{"className":127,"code":1690,"language":123,"meta":129,"style":129},"-- Full outer join of customers and orders on customer_id.\n-- Branch 1: all customers with their matching orders (LEFT JOIN).\n-- Branch 2: orders with no matching customer (RIGHT JOIN anti-join: WHERE c.id IS NULL).\n-- UNION dedups the overlap (customers with orders appear in both branches).\nSELECT c.id AS customer_id, c.name, o.id AS order_id, o.amount\nFROM customers c\nLEFT JOIN orders o ON c.id = o.customer_id\nUNION\nSELECT c.id AS customer_id, c.name, o.id AS order_id, o.amount\nFROM customers c\nRIGHT JOIN orders o ON c.id = o.customer_id\nWHERE c.id IS NULL;\n",[55,1692,1693,1698,1703,1708,1713,1758,1765,1791,1795,1833,1839,1862],{"__ignoreMap":129},[133,1694,1695],{"class":135,"line":136},[133,1696,1697],{"class":139},"-- Full outer join of customers and orders on customer_id.\n",[133,1699,1700],{"class":135,"line":143},[133,1701,1702],{"class":139},"-- Branch 1: all customers with their matching orders (LEFT JOIN).\n",[133,1704,1705],{"class":135,"line":149},[133,1706,1707],{"class":139},"-- Branch 2: orders with no matching customer (RIGHT JOIN anti-join: WHERE c.id IS NULL).\n",[133,1709,1710],{"class":135,"line":166},[133,1711,1712],{"class":139},"-- UNION dedups the overlap (customers with orders appear in both branches).\n",[133,1714,1715,1717,1720,1722,1725,1727,1730,1733,1735,1737,1739,1742,1744,1746,1748,1751,1753,1755],{"class":135,"line":172},[133,1716,153],{"class":152},[133,1718,1719],{"class":596}," c",[133,1721,887],{"class":156},[133,1723,1724],{"class":596},"id",[133,1726,369],{"class":152},[133,1728,1729],{"class":156}," customer_id, ",[133,1731,1732],{"class":596},"c",[133,1734,887],{"class":156},[133,1736,359],{"class":596},[133,1738,362],{"class":156},[133,1740,1741],{"class":596},"o",[133,1743,887],{"class":156},[133,1745,1724],{"class":596},[133,1747,369],{"class":152},[133,1749,1750],{"class":156}," order_id, ",[133,1752,1741],{"class":596},[133,1754,887],{"class":156},[133,1756,1757],{"class":596},"amount\n",[133,1759,1760,1762],{"class":135,"line":184},[133,1761,160],{"class":152},[133,1763,1764],{"class":156}," customers c\n",[133,1766,1767,1769,1772,1775,1777,1779,1781,1783,1786,1788],{"class":135,"line":191},[133,1768,1684],{"class":152},[133,1770,1771],{"class":156}," orders o ",[133,1773,1774],{"class":152},"ON",[133,1776,1719],{"class":596},[133,1778,887],{"class":156},[133,1780,1724],{"class":596},[133,1782,1545],{"class":152},[133,1784,1785],{"class":596}," o",[133,1787,887],{"class":156},[133,1789,1790],{"class":596},"customer_id\n",[133,1792,1793],{"class":135,"line":197},[133,1794,169],{"class":152},[133,1796,1797,1799,1801,1803,1805,1807,1809,1811,1813,1815,1817,1819,1821,1823,1825,1827,1829,1831],{"class":135,"line":203},[133,1798,153],{"class":152},[133,1800,1719],{"class":596},[133,1802,887],{"class":156},[133,1804,1724],{"class":596},[133,1806,369],{"class":152},[133,1808,1729],{"class":156},[133,1810,1732],{"class":596},[133,1812,887],{"class":156},[133,1814,359],{"class":596},[133,1816,362],{"class":156},[133,1818,1741],{"class":596},[133,1820,887],{"class":156},[133,1822,1724],{"class":596},[133,1824,369],{"class":152},[133,1826,1750],{"class":156},[133,1828,1741],{"class":596},[133,1830,887],{"class":156},[133,1832,1757],{"class":596},[133,1834,1835,1837],{"class":135,"line":214},[133,1836,160],{"class":152},[133,1838,1764],{"class":156},[133,1840,1841,1844,1846,1848,1850,1852,1854,1856,1858,1860],{"class":135,"line":220},[133,1842,1843],{"class":152},"RIGHT JOIN",[133,1845,1771],{"class":156},[133,1847,1774],{"class":152},[133,1849,1719],{"class":596},[133,1851,887],{"class":156},[133,1853,1724],{"class":596},[133,1855,1545],{"class":152},[133,1857,1785],{"class":596},[133,1859,887],{"class":156},[133,1861,1790],{"class":596},[133,1863,1864,1866,1868,1870,1872,1875,1878],{"class":135,"line":231},[133,1865,1020],{"class":152},[133,1867,1719],{"class":596},[133,1869,887],{"class":156},[133,1871,1724],{"class":596},[133,1873,1874],{"class":152}," IS",[133,1876,1877],{"class":152}," NULL",[133,1879,751],{"class":156},[23,1881,1883],{"id":1882},"tips-tricks","💡 Tips & Tricks",[855,1885,1886,1904,1922,1941,1960,1976],{},[478,1887,1888,1891,1892,1894,1895,1897,1898,1900,1901,1903],{},[18,1889,1890],{},"Performance"," — default to ",[55,1893,73],{},". Use ",[55,1896,57],{}," only when you specifically need deduplication. The dedup cost (sort or hash) is often the dominant cost of a ",[55,1899,57],{}," query, and most real-world ",[55,1902,57],{},"s are over disjoint inputs where dedup is wasted work.",[478,1905,1906,1909,1910,1913,1914,1917,1918,1921],{},[18,1907,1908],{},"Idiom"," — add a ",[18,1911,1912],{},"provenance column"," (",[55,1915,1916],{},"SELECT ..., 'left' AS src"," vs ",[55,1919,1920],{},"SELECT ..., 'right' AS src",") to track which input each result row came from — invaluable for debugging \"why did this row appear\u002Fdisappear\" and for building audit-style diffs.",[478,1923,1924,1926,1927,1929,1930,1933,1934,1937,1938,1940],{},[18,1925,1908],{}," — use ",[55,1928,105],{}," as a NULL-safe anti-join. ",[55,1931,1932],{},"NOT IN"," poisons on NULL; ",[55,1935,1936],{},"LEFT JOIN ... IS NULL"," requires careful NULL handling. ",[55,1939,105],{}," treats NULLs as equal for dedup, making it the cleanest \"in A but not in B\" operation.",[478,1942,1943,1945,1946,1948,1949,1952,1953,1956,1957,1959],{},[18,1944,1908],{}," — when comparing two tables for equality, use ",[55,1947,105],{}," in both directions rather than ",[55,1950,1951],{},"count(*)",". Counts can match while row contents differ. ",[55,1954,1955],{},"(A EXCEPT B) UNION ALL (B EXCEPT A)"," is empty iff A = B (as sets; use ",[55,1958,852],{}," for multiset equality).",[478,1961,1962,1965,1966,622,1968,622,1970,1972,1973,1975],{},[18,1963,1964],{},"Debug"," — if a set operation returns fewer rows than expected, suspect an unexpected duplicate collapse. ",[55,1967,57],{},[55,1969,88],{},[55,1971,105],{}," dedup silently. Switch to ",[55,1974,73],{}," temporarily to see the raw combined rows, or add a unique row identifier.",[478,1977,1978,1981,1982,1984,1985,1987,1988,622,1990,1992,1993,1996,1997,849,2000,2003],{},[18,1979,1980],{},"Portability"," — ",[55,1983,105],{}," is standard SQL but Oracle calls it ",[55,1986,109],{},". ",[55,1989,848],{},[55,1991,852],{}," are PostgreSQL-specific. For portable multiplicity-preserving set ops, use ",[55,1994,1995],{},"JOIN"," with ",[55,1998,1999],{},"GROUP BY",[55,2001,2002],{},"count()"," comparisons.",[23,2005,2007],{"id":2006},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[855,2009,2010,2019,2028,2041,2049,2073,2097,2109,2132],{},[478,2011,2012,1981,2015,2018],{},[18,2013,2014],{},"Column count mismatch",[55,2016,2017],{},"SELECT id, name FROM t1 UNION SELECT id FROM t2"," is a syntax error. Both inputs must have the same number of columns. No implicit NULL padding.",[478,2020,2021,1981,2024,2027],{},[18,2022,2023],{},"Type coercion in set ops",[55,2025,2026],{},"SELECT 1 UNION SELECT '1'"," may coerce to text or integer depending on the engine. PostgreSQL is strict (may error); MySQL is lenient. Keep types consistent across branches.",[478,2029,2030,1981,2035,2037,2038,2040],{},[18,2031,2032,2034],{},[55,2033,621],{}," applies to the entire result",[55,2036,621],{}," after a set operation sorts the combined output, not individual SELECTs. It must reference the first input's column names. You cannot ",[55,2039,621],{}," inside an individual branch without parentheses.",[478,2042,2043,2048],{},[18,2044,2045,2047],{},[55,2046,621],{}," column names must match the first input"," — if the second input named the column differently, you can't sort by that name. Alias in the first SELECT.",[478,2050,2051,2054,2055,2058,2059,2062,2063,2065,2066,2069,2070,2072],{},[18,2052,2053],{},"NULL handling in set operations"," — set ops treat NULLs as ",[18,2056,2057],{},"equal"," for dedup. ",[55,2060,2061],{},"(1, NULL)"," in both inputs is considered a duplicate and ",[55,2064,57],{}," keeps one. This differs from ",[55,2067,2068],{},"NULL = NULL"," being UNKNOWN in ",[55,2071,1020],{}," — set ops use \"is not distinct from\" semantics internally.",[478,2074,2075,1981,2082,2084,2085,622,2087,1987,2089,2092,2093,2096],{},[18,2076,2077,622,2079,2081],{},[55,2078,88],{},[55,2080,105],{}," precedence",[55,2083,88],{}," binds tighter than ",[55,2086,57],{},[55,2088,105],{},[55,2090,2091],{},"A UNION B INTERSECT C"," is ",[55,2094,2095],{},"A UNION (B INTERSECT C)",". Use parentheses to make intent explicit.",[478,2098,2099,2102,2103,2105,2106,2108],{},[18,2100,2101],{},"Set ops don't preserve order"," — the inputs' ",[55,2104,621],{}," (if any inside subqueries) is not preserved. The only ",[55,2107,621],{}," that matters is the one at the top of the whole set operation.",[478,2110,2111,2117,2118,2121,2122,2124,2125,2127,2128,2131],{},[18,2112,2113,2114,2116],{},"Large ",[55,2115,57],{}," can spill"," — the dedup sort\u002Fhash needs ",[55,2119,2120],{},"work_mem","; if it exceeds that, it spills to disk. For huge ",[55,2123,57],{},"s, increase ",[55,2126,2120],{}," or pre-dedup each branch with ",[55,2129,2130],{},"DISTINCT"," (which may let the planner use an index).",[478,2133,2134,1981,2140,2142,2143,2145],{},[18,2135,2136,1917,2138],{},[55,2137,105],{},[55,2139,852],{},[55,2141,105],{}," dedups (a row in both sides is removed entirely). ",[55,2144,852],{}," preserves multiplicity (if a row appears 3x left and 1x right, result has 2x). Pick based on whether duplicates matter.",[23,2147,2149],{"id":2148},"spot-the-bug","🧠 Spot the Bug",[14,2151,2152],{},"A developer wants to count how many distinct cities have either a customer or a supplier. This returns a much larger number than expected. What's wrong?",[121,2154,2155],{"language":123},[125,2156,2158],{"className":127,"code":2157,"language":123,"meta":129,"style":129},"SELECT COUNT(*) FROM (\n  SELECT city FROM customers\n  UNION ALL\n  SELECT city FROM suppliers\n) all_cities;\n",[55,2159,2160,2178,2188,2193,2203],{"__ignoreMap":129},[133,2161,2162,2164,2167,2169,2172,2174,2176],{"class":135,"line":136},[133,2163,153],{"class":152},[133,2165,2166],{"class":596}," COUNT",[133,2168,781],{"class":156},[133,2170,2171],{"class":152},"*",[133,2173,1324],{"class":156},[133,2175,160],{"class":152},[133,2177,1295],{"class":156},[133,2179,2180,2182,2184,2186],{"class":135,"line":143},[133,2181,1300],{"class":152},[133,2183,157],{"class":156},[133,2185,160],{"class":152},[133,2187,163],{"class":156},[133,2189,2190],{"class":135,"line":149},[133,2191,2192],{"class":152},"  UNION ALL\n",[133,2194,2195,2197,2199,2201],{"class":135,"line":166},[133,2196,1300],{"class":152},[133,2198,157],{"class":156},[133,2200,160],{"class":152},[133,2202,689],{"class":156},[133,2204,2205],{"class":135,"line":172},[133,2206,2207],{"class":156},") all_cities;\n",[2209,2210,2211,2215,2240,2250,2361,2386],"details",{},[2212,2213,2214],"summary",{},"Answer",[14,2216,2217,2219,2220,2223,2224,2227,2228,2231,2232,2235,2236,2239],{},[55,2218,73],{}," does ",[18,2221,2222],{},"not"," remove duplicates — it concatenates with multiplicity. So ",[55,2225,2226],{},"'NYC'"," appearing 50 times in ",[55,2229,2230],{},"customers"," and 20 times in ",[55,2233,2234],{},"suppliers"," contributes 70 rows, not 1. ",[55,2237,2238],{},"COUNT(*)"," counts all 70, inflating the \"distinct cities\" count dramatically.",[14,2241,2242,2243,2245,2246,2249],{},"The fix: use ",[55,2244,57],{}," (which dedups), or ",[55,2247,2248],{},"COUNT(DISTINCT city)",":",[121,2251,2252],{"language":123},[125,2253,2255],{"className":127,"code":2254,"language":123,"meta":129,"style":129},"-- Option 1: UNION dedups, then COUNT(*) counts the deduped rows.\nSELECT COUNT(*) FROM (\n  SELECT city FROM customers\n  UNION\n  SELECT city FROM suppliers\n) all_cities;\n\n-- Option 2: COUNT DISTINCT over UNION ALL (works, but pays both concat AND a distinct hash).\nSELECT COUNT(DISTINCT city) FROM (\n  SELECT city FROM customers\n  UNION ALL\n  SELECT city FROM suppliers\n) all_cities;\n",[55,2256,2257,2262,2278,2288,2293,2303,2307,2311,2316,2333,2343,2347,2357],{"__ignoreMap":129},[133,2258,2259],{"class":135,"line":136},[133,2260,2261],{"class":139},"-- Option 1: UNION dedups, then COUNT(*) counts the deduped rows.\n",[133,2263,2264,2266,2268,2270,2272,2274,2276],{"class":135,"line":143},[133,2265,153],{"class":152},[133,2267,2166],{"class":596},[133,2269,781],{"class":156},[133,2271,2171],{"class":152},[133,2273,1324],{"class":156},[133,2275,160],{"class":152},[133,2277,1295],{"class":156},[133,2279,2280,2282,2284,2286],{"class":135,"line":149},[133,2281,1300],{"class":152},[133,2283,157],{"class":156},[133,2285,160],{"class":152},[133,2287,163],{"class":156},[133,2289,2290],{"class":135,"line":166},[133,2291,2292],{"class":152},"  UNION\n",[133,2294,2295,2297,2299,2301],{"class":135,"line":172},[133,2296,1300],{"class":152},[133,2298,157],{"class":156},[133,2300,160],{"class":152},[133,2302,689],{"class":156},[133,2304,2305],{"class":135,"line":184},[133,2306,2207],{"class":156},[133,2308,2309],{"class":135,"line":191},[133,2310,188],{"emptyLinePlaceholder":187},[133,2312,2313],{"class":135,"line":197},[133,2314,2315],{"class":139},"-- Option 2: COUNT DISTINCT over UNION ALL (works, but pays both concat AND a distinct hash).\n",[133,2317,2318,2320,2322,2324,2326,2329,2331],{"class":135,"line":203},[133,2319,153],{"class":152},[133,2321,2166],{"class":596},[133,2323,781],{"class":156},[133,2325,2130],{"class":152},[133,2327,2328],{"class":156}," city) ",[133,2330,160],{"class":152},[133,2332,1295],{"class":156},[133,2334,2335,2337,2339,2341],{"class":135,"line":214},[133,2336,1300],{"class":152},[133,2338,157],{"class":156},[133,2340,160],{"class":152},[133,2342,163],{"class":156},[133,2344,2345],{"class":135,"line":220},[133,2346,2192],{"class":152},[133,2348,2349,2351,2353,2355],{"class":135,"line":231},[133,2350,1300],{"class":152},[133,2352,157],{"class":156},[133,2354,160],{"class":152},[133,2356,689],{"class":156},[133,2358,2359],{"class":135,"line":236},[133,2360,2207],{"class":156},[14,2362,2363,2365,2366,2368,2369,2372,2373,2375,2376,2378,2379,2382,2383,2385],{},[55,2364,57],{}," is cleaner here — it dedups once during the combine, and ",[55,2367,2238],{}," then counts the deduped rows. ",[55,2370,2371],{},"COUNT(DISTINCT)"," over ",[55,2374,73],{}," works but pays both the ",[55,2377,73],{}," concatenation ",[756,2380,2381],{},"and"," a separate hash for ",[55,2384,2130],{}," — usually slower.",[14,2387,2388,2391,2392,2394,2395,2397,2398,2400,2401,2403,2404,887],{},[18,2389,2390],{},"The lesson",": ",[55,2393,73],{}," keeps duplicates; ",[55,2396,57],{}," removes them. When you want \"distinct values across inputs,\" use ",[55,2399,57],{},", not ",[55,2402,73],{}," + ",[55,2405,2238],{},[23,2407,2408],{"id":2212},"Summary",[14,2410,2411,2412,622,2414,622,2416,622,2418,2420,2421,2423,2424,2426],{},"You can now combine result sets with ",[55,2413,57],{},[55,2415,73],{},[55,2417,88],{},[55,2419,105],{},", understand the dedup cost and the ",[55,2422,73],{}," performance advantage, handle column compatibility, use set operations for NULL-safe anti-joins, and emulate ",[55,2425,1677],{}," on engines that lack it. Next: window functions — the most powerful analytical feature in SQL.",[2428,2429,2430],"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 .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}",{"title":129,"searchDepth":143,"depth":143,"links":2432},[2433,2434,2435,2436,2437,2438,2439,2440,2444,2445,2446,2447,2448],{"id":25,"depth":143,"text":26},{"id":315,"depth":143,"text":316},{"id":469,"depth":143,"text":470},{"id":615,"depth":143,"text":616},{"id":841,"depth":143,"text":842},{"id":934,"depth":143,"text":935},{"id":1258,"depth":143,"text":1259},{"id":1482,"depth":143,"text":1483,"children":2441},[2442,2443],{"id":1487,"depth":149,"text":1488},{"id":1587,"depth":149,"text":1588},{"id":1670,"depth":143,"text":1671},{"id":1882,"depth":143,"text":1883},{"id":2006,"depth":143,"text":2007},{"id":2148,"depth":143,"text":2149},{"id":2212,"depth":143,"text":2408},"Set operations combine the rows of two result sets vertically (stacking), as opposed to joins, which combine them horizontally. Both inputs must have compatible column structure.","md",{},"\u002Fsql\u002F08-set-operations",{"title":5,"description":2449},"sql\u002F08-set-operations","RL41B9p8T7Z4MS-YY_gf6hZ0Ghz7Wl4v4IOk_cmizGI",1789924654380]