[{"data":1,"prerenderedAt":1311},["ShallowReactive",2],{"blog-\u002Fblog\u002Fsql-joins-inner-outer-self-anti":3},{"id":4,"title":5,"body":6,"description":1298,"difficulty":1299,"extension":1300,"framework":1301,"frameworkSlug":72,"meta":1302,"navigation":243,"order":80,"path":1303,"qaPath":1304,"seo":1305,"stem":1306,"subtopic":21,"topic":1307,"topicSlug":1308,"updated":1309,"__hash__":1310},"blog\u002Fblog\u002Fsql-joins-inner-outer-self-anti.md","SQL Joins Explained — INNER, OUTER, SELF & Anti-Joins with Examples",{"type":7,"value":8,"toc":1287},"minimark",[9,14,44,48,67,145,149,177,297,328,332,355,501,513,517,524,592,601,714,721,725,743,797,820,931,938,942,949,967,1088,1124,1128,1149,1218,1244,1248,1283],[10,11,13],"h2",{"id":12},"sql-joins-explained","SQL joins, explained",[15,16,17,18,22,23,27,28,31,32,35,36,39,40,43],"p",{},"Relational databases split data across normalized tables to avoid duplication. ",[19,20,21],"strong",{},"Joins","\nare how you stitch that data back together at query time. Knowing the join types is\ntable stakes; knowing how ",[24,25,26],"code",{},"NULL","s behave, why rows multiply, and where the ",[24,29,30],{},"ON"," vs\n",[24,33,34],{},"WHERE"," distinction bites is what separates correct queries from subtly wrong ones. This\nguide covers all of it with runnable examples (",[24,37,38],{},"users",", ",[24,41,42],{},"orders",").",[10,45,47],{"id":46},"what-a-join-is","What a join is",[15,49,50,51,53,54,57,58,62,63,66],{},"A join combines rows from two or more tables by matching a related column — typically a\nforeign key referencing a primary key. The ",[24,52,30],{}," clause is the ",[19,55,56],{},"join condition","; the\njoin ",[59,60,61],"em",{},"type"," decides what to do with rows that have ",[19,64,65],{},"no match",".",[68,69,74],"pre",{"className":70,"code":71,"language":72,"meta":73,"style":73},"language-sql shiki shiki-themes github-light github-dark","SELECT users.name, orders.total\nFROM users\nJOIN orders ON orders.user_id = users.id;\n","sql","",[24,75,76,104,113],{"__ignoreMap":73},[77,78,81,85,89,92,95,97,99,101],"span",{"class":79,"line":80},"line",1,[77,82,84],{"class":83},"szBVR","SELECT",[77,86,88],{"class":87},"sj4cs"," users",[77,90,66],{"class":91},"sVt8B",[77,93,94],{"class":87},"name",[77,96,39],{"class":91},[77,98,42],{"class":87},[77,100,66],{"class":91},[77,102,103],{"class":87},"total\n",[77,105,107,110],{"class":79,"line":106},2,[77,108,109],{"class":83},"FROM",[77,111,112],{"class":91}," users\n",[77,114,116,119,122,124,127,129,132,135,137,139,142],{"class":79,"line":115},3,[77,117,118],{"class":83},"JOIN",[77,120,121],{"class":91}," orders ",[77,123,30],{"class":83},[77,125,126],{"class":87}," orders",[77,128,66],{"class":91},[77,130,131],{"class":87},"user_id",[77,133,134],{"class":83}," =",[77,136,88],{"class":87},[77,138,66],{"class":91},[77,140,141],{"class":87},"id",[77,143,144],{"class":91},";\n",[10,146,148],{"id":147},"inner-vs-outer","INNER vs OUTER",[150,151,152,165],"ul",{},[153,154,155,160,161,164],"li",{},[19,156,157],{},[24,158,159],{},"INNER JOIN"," returns only rows with a match in ",[19,162,163],{},"both"," tables — the intersection.",[153,166,167,172,173,66],{},[19,168,169],{},[24,170,171],{},"OUTER JOIN"," (LEFT\u002FRIGHT\u002FFULL) keeps unmatched rows from one or both sides, filling\nmissing columns with ",[19,174,175],{},[24,176,26],{},[68,178,180],{"className":70,"code":179,"language":72,"meta":73,"style":73},"-- only users who have ordered\nSELECT u.name, o.total FROM users u INNER JOIN orders o ON o.user_id = u.id;\n\n-- every user, NULL for those who never ordered\nSELECT u.name, o.total FROM users u LEFT JOIN orders o ON o.user_id = u.id;\n",[24,181,182,188,239,245,251],{"__ignoreMap":73},[77,183,184],{"class":79,"line":80},[77,185,187],{"class":186},"sJ8bj","-- only users who have ordered\n",[77,189,190,192,195,197,199,201,204,206,209,212,215,217,220,222,225,227,229,231,233,235,237],{"class":79,"line":106},[77,191,84],{"class":83},[77,193,194],{"class":87}," u",[77,196,66],{"class":91},[77,198,94],{"class":87},[77,200,39],{"class":91},[77,202,203],{"class":87},"o",[77,205,66],{"class":91},[77,207,208],{"class":87},"total",[77,210,211],{"class":83}," FROM",[77,213,214],{"class":91}," users u ",[77,216,159],{"class":83},[77,218,219],{"class":91}," orders o ",[77,221,30],{"class":83},[77,223,224],{"class":87}," o",[77,226,66],{"class":91},[77,228,131],{"class":87},[77,230,134],{"class":83},[77,232,194],{"class":87},[77,234,66],{"class":91},[77,236,141],{"class":87},[77,238,144],{"class":91},[77,240,241],{"class":79,"line":115},[77,242,244],{"emptyLinePlaceholder":243},true,"\n",[77,246,248],{"class":79,"line":247},4,[77,249,250],{"class":186},"-- every user, NULL for those who never ordered\n",[77,252,254,256,258,260,262,264,266,268,270,272,274,277,279,281,283,285,287,289,291,293,295],{"class":79,"line":253},5,[77,255,84],{"class":83},[77,257,194],{"class":87},[77,259,66],{"class":91},[77,261,94],{"class":87},[77,263,39],{"class":91},[77,265,203],{"class":87},[77,267,66],{"class":91},[77,269,208],{"class":87},[77,271,211],{"class":83},[77,273,214],{"class":91},[77,275,276],{"class":83},"LEFT JOIN",[77,278,219],{"class":91},[77,280,30],{"class":83},[77,282,224],{"class":87},[77,284,66],{"class":91},[77,286,131],{"class":87},[77,288,134],{"class":83},[77,290,194],{"class":87},[77,292,66],{"class":91},[77,294,141],{"class":87},[77,296,144],{"class":91},[15,298,299,301,302,304,305,307,308,311,312,315,316,319,320,323,324,327],{},[24,300,118],{}," alone means ",[24,303,159],{},"; ",[24,306,276],{}," means ",[24,309,310],{},"LEFT OUTER JOIN",". ",[19,313,314],{},"LEFT"," keeps all\nrows from the left table; ",[19,317,318],{},"RIGHT"," keeps all from the right — they're mirror images, so\nteams usually standardize on LEFT for readability. ",[19,321,322],{},"FULL OUTER"," keeps everything from\nboth sides (great for reconciliation), though MySQL lacks it and needs a ",[24,325,326],{},"UNION"," of a\nLEFT and RIGHT join to emulate it.",[10,329,331],{"id":330},"on-vs-where-the-trap-that-turns-left-into-inner","ON vs WHERE — the trap that turns LEFT into INNER",[15,333,334,336,337,339,340,343,344,346,347,350,351,354],{},[24,335,30],{}," defines how rows match (during the join); ",[24,338,34],{}," filters the result (after the\njoin). For INNER joins they're often interchangeable. For OUTER joins they are ",[19,341,342],{},"not",": a\n",[24,345,34],{}," condition on the optional table's columns evaluates to ",[24,348,349],{},"unknown"," for the\nNULL-filled rows and ",[19,352,353],{},"removes them",", silently converting your LEFT JOIN into an INNER\nJOIN.",[68,356,358],{"className":70,"code":357,"language":72,"meta":73,"style":73},"-- unmatched users have o.status = NULL -> WHERE drops them\nSELECT u.name, o.status FROM users u\nLEFT JOIN orders o ON o.user_id = u.id\nWHERE o.status = 'shipped';\n\n-- predicate on the optional side belongs in ON\nSELECT u.name, o.status FROM users u\nLEFT JOIN orders o ON o.user_id = u.id AND o.status = 'shipped';\n",[24,359,360,365,389,412,430,434,440,463],{"__ignoreMap":73},[77,361,362],{"class":79,"line":80},[77,363,364],{"class":186},"-- unmatched users have o.status = NULL -> WHERE drops them\n",[77,366,367,369,371,373,375,377,379,381,384,386],{"class":79,"line":106},[77,368,84],{"class":83},[77,370,194],{"class":87},[77,372,66],{"class":91},[77,374,94],{"class":87},[77,376,39],{"class":91},[77,378,203],{"class":87},[77,380,66],{"class":91},[77,382,383],{"class":87},"status",[77,385,211],{"class":83},[77,387,388],{"class":91}," users u\n",[77,390,391,393,395,397,399,401,403,405,407,409],{"class":79,"line":115},[77,392,276],{"class":83},[77,394,219],{"class":91},[77,396,30],{"class":83},[77,398,224],{"class":87},[77,400,66],{"class":91},[77,402,131],{"class":87},[77,404,134],{"class":83},[77,406,194],{"class":87},[77,408,66],{"class":91},[77,410,411],{"class":87},"id\n",[77,413,414,416,418,420,422,424,428],{"class":79,"line":247},[77,415,34],{"class":83},[77,417,224],{"class":87},[77,419,66],{"class":91},[77,421,383],{"class":87},[77,423,134],{"class":83},[77,425,427],{"class":426},"sZZnC"," 'shipped'",[77,429,144],{"class":91},[77,431,432],{"class":79,"line":253},[77,433,244],{"emptyLinePlaceholder":243},[77,435,437],{"class":79,"line":436},6,[77,438,439],{"class":186},"-- predicate on the optional side belongs in ON\n",[77,441,443,445,447,449,451,453,455,457,459,461],{"class":79,"line":442},7,[77,444,84],{"class":83},[77,446,194],{"class":87},[77,448,66],{"class":91},[77,450,94],{"class":87},[77,452,39],{"class":91},[77,454,203],{"class":87},[77,456,66],{"class":91},[77,458,383],{"class":87},[77,460,211],{"class":83},[77,462,388],{"class":91},[77,464,466,468,470,472,474,476,478,480,482,484,486,489,491,493,495,497,499],{"class":79,"line":465},8,[77,467,276],{"class":83},[77,469,219],{"class":91},[77,471,30],{"class":83},[77,473,224],{"class":87},[77,475,66],{"class":91},[77,477,131],{"class":87},[77,479,134],{"class":83},[77,481,194],{"class":87},[77,483,66],{"class":91},[77,485,141],{"class":87},[77,487,488],{"class":83}," AND",[77,490,224],{"class":87},[77,492,66],{"class":91},[77,494,383],{"class":87},[77,496,134],{"class":83},[77,498,427],{"class":426},[77,500,144],{"class":91},[15,502,503,504,507,508,510,511,66],{},"Rule: put conditions on the ",[19,505,506],{},"joined (right) table"," in ",[24,509,30],{}," to preserve outer rows; put\nconditions on the final result in ",[24,512,34],{},[10,514,516],{"id":515},"self-joins-and-multiple-joins","Self joins and multiple joins",[15,518,519,520,523],{},"A ",[19,521,522],{},"self join"," joins a table to itself using aliases — ideal for hierarchies stored in\none table.",[68,525,527],{"className":70,"code":526,"language":72,"meta":73,"style":73},"SELECT e.name AS employee, m.name AS manager\nFROM employees e\nLEFT JOIN employees m ON e.manager_id = m.id;\n",[24,528,529,558,565],{"__ignoreMap":73},[77,530,531,533,536,538,540,543,546,549,551,553,555],{"class":79,"line":80},[77,532,84],{"class":83},[77,534,535],{"class":87}," e",[77,537,66],{"class":91},[77,539,94],{"class":87},[77,541,542],{"class":83}," AS",[77,544,545],{"class":91}," employee, ",[77,547,548],{"class":87},"m",[77,550,66],{"class":91},[77,552,94],{"class":87},[77,554,542],{"class":83},[77,556,557],{"class":91}," manager\n",[77,559,560,562],{"class":79,"line":106},[77,561,109],{"class":83},[77,563,564],{"class":91}," employees e\n",[77,566,567,569,572,574,576,578,581,583,586,588,590],{"class":79,"line":115},[77,568,276],{"class":83},[77,570,571],{"class":91}," employees m ",[77,573,30],{"class":83},[77,575,535],{"class":87},[77,577,66],{"class":91},[77,579,580],{"class":87},"manager_id",[77,582,134],{"class":83},[77,584,585],{"class":87}," m",[77,587,66],{"class":91},[77,589,141],{"class":87},[77,591,144],{"class":91},[15,593,594,595,597,598,600],{},"To join three or more tables, chain ",[24,596,118],{}," clauses; each ",[24,599,30],{}," connects the new table to\none already in the query.",[68,602,604],{"className":70,"code":603,"language":72,"meta":73,"style":73},"SELECT u.name, o.id, p.name\nFROM users u\nJOIN orders o      ON o.user_id = u.id\nJOIN order_items i ON i.order_id = o.id\nJOIN products p    ON p.id = i.product_id;\n",[24,605,606,633,639,662,687],{"__ignoreMap":73},[77,607,608,610,612,614,616,618,620,622,624,626,628,630],{"class":79,"line":80},[77,609,84],{"class":83},[77,611,194],{"class":87},[77,613,66],{"class":91},[77,615,94],{"class":87},[77,617,39],{"class":91},[77,619,203],{"class":87},[77,621,66],{"class":91},[77,623,141],{"class":87},[77,625,39],{"class":91},[77,627,15],{"class":87},[77,629,66],{"class":91},[77,631,632],{"class":87},"name\n",[77,634,635,637],{"class":79,"line":106},[77,636,109],{"class":83},[77,638,388],{"class":91},[77,640,641,643,646,648,650,652,654,656,658,660],{"class":79,"line":115},[77,642,118],{"class":83},[77,644,645],{"class":91}," orders o      ",[77,647,30],{"class":83},[77,649,224],{"class":87},[77,651,66],{"class":91},[77,653,131],{"class":87},[77,655,134],{"class":83},[77,657,194],{"class":87},[77,659,66],{"class":91},[77,661,411],{"class":87},[77,663,664,666,669,671,674,676,679,681,683,685],{"class":79,"line":247},[77,665,118],{"class":83},[77,667,668],{"class":91}," order_items i ",[77,670,30],{"class":83},[77,672,673],{"class":87}," i",[77,675,66],{"class":91},[77,677,678],{"class":87},"order_id",[77,680,134],{"class":83},[77,682,224],{"class":87},[77,684,66],{"class":91},[77,686,411],{"class":87},[77,688,689,691,694,696,699,701,703,705,707,709,712],{"class":79,"line":253},[77,690,118],{"class":83},[77,692,693],{"class":91}," products p    ",[77,695,30],{"class":83},[77,697,698],{"class":87}," p",[77,700,66],{"class":91},[77,702,141],{"class":87},[77,704,134],{"class":83},[77,706,673],{"class":87},[77,708,66],{"class":91},[77,710,711],{"class":87},"product_id",[77,713,144],{"class":91},[15,715,716,717,720],{},"For outer-join chains, every downstream join on the optional path must ",[19,718,719],{},"also"," be an\nouter join, or an inner join will re-filter the preserved rows back out.",[10,722,724],{"id":723},"why-joins-multiply-rows-and-the-count-trap","Why joins multiply rows (and the COUNT trap)",[15,726,727,728,731,732,735,736,739,740,66],{},"A join produces a row for ",[19,729,730],{},"every matching pair",". If one side matches multiple rows on\nthe other (one-to-many), the single-side values repeat. Joining a parent to ",[19,733,734],{},"two","\none-to-many children multiplies their rows together — the ",[19,737,738],{},"fan trap"," — inflating\naggregates like ",[24,741,742],{},"SUM",[68,744,746],{"className":70,"code":745,"language":72,"meta":73,"style":73},"-- a user with 3 orders appears on 3 rows\nSELECT u.name, o.id FROM users u JOIN orders o ON o.user_id = u.id;\n",[24,747,748,753],{"__ignoreMap":73},[77,749,750],{"class":79,"line":80},[77,751,752],{"class":186},"-- a user with 3 orders appears on 3 rows\n",[77,754,755,757,759,761,763,765,767,769,771,773,775,777,779,781,783,785,787,789,791,793,795],{"class":79,"line":106},[77,756,84],{"class":83},[77,758,194],{"class":87},[77,760,66],{"class":91},[77,762,94],{"class":87},[77,764,39],{"class":91},[77,766,203],{"class":87},[77,768,66],{"class":91},[77,770,141],{"class":87},[77,772,211],{"class":83},[77,774,214],{"class":91},[77,776,118],{"class":83},[77,778,219],{"class":91},[77,780,30],{"class":83},[77,782,224],{"class":87},[77,784,66],{"class":91},[77,786,131],{"class":87},[77,788,134],{"class":83},[77,790,194],{"class":87},[77,792,66],{"class":91},[77,794,141],{"class":87},[77,796,144],{"class":91},[15,798,799,800,803,804,807,808,811,812,815,816,819],{},"Two fixes: aggregate with ",[24,801,802],{},"GROUP BY",", or ",[19,805,806],{},"pre-aggregate"," each child in its own subquery\nbefore joining so each contributes one row. And watch the ",[19,809,810],{},"COUNT trap"," with LEFT JOIN:\n",[24,813,814],{},"COUNT(*)"," counts the NULL-filled row (so zero-match rows wrongly count as 1), while\n",[24,817,818],{},"COUNT(column)"," ignores NULLs.",[68,821,823],{"className":70,"code":822,"language":72,"meta":73,"style":73},"SELECT u.name,\n       COUNT(*)    AS wrong,   -- counts the NULL row\n       COUNT(o.id) AS correct  -- ignores NULLs -> 0 for users with no orders\nFROM users u LEFT JOIN orders o ON o.user_id = u.id\nGROUP BY u.id, u.name;\n",[24,824,825,838,861,884,910],{"__ignoreMap":73},[77,826,827,829,831,833,835],{"class":79,"line":80},[77,828,84],{"class":83},[77,830,194],{"class":87},[77,832,66],{"class":91},[77,834,94],{"class":87},[77,836,837],{"class":91},",\n",[77,839,840,843,846,849,852,855,858],{"class":79,"line":106},[77,841,842],{"class":87},"       COUNT",[77,844,845],{"class":91},"(",[77,847,848],{"class":83},"*",[77,850,851],{"class":91},")    ",[77,853,854],{"class":83},"AS",[77,856,857],{"class":91}," wrong,   ",[77,859,860],{"class":186},"-- counts the NULL row\n",[77,862,863,865,867,869,871,873,876,878,881],{"class":79,"line":115},[77,864,842],{"class":87},[77,866,845],{"class":91},[77,868,203],{"class":87},[77,870,66],{"class":91},[77,872,141],{"class":87},[77,874,875],{"class":91},") ",[77,877,854],{"class":83},[77,879,880],{"class":91}," correct  ",[77,882,883],{"class":186},"-- ignores NULLs -> 0 for users with no orders\n",[77,885,886,888,890,892,894,896,898,900,902,904,906,908],{"class":79,"line":247},[77,887,109],{"class":83},[77,889,214],{"class":91},[77,891,276],{"class":83},[77,893,219],{"class":91},[77,895,30],{"class":83},[77,897,224],{"class":87},[77,899,66],{"class":91},[77,901,131],{"class":87},[77,903,134],{"class":83},[77,905,194],{"class":87},[77,907,66],{"class":91},[77,909,411],{"class":87},[77,911,912,914,916,918,920,922,925,927,929],{"class":79,"line":253},[77,913,802],{"class":83},[77,915,194],{"class":87},[77,917,66],{"class":91},[77,919,141],{"class":87},[77,921,39],{"class":91},[77,923,924],{"class":87},"u",[77,926,66],{"class":91},[77,928,94],{"class":87},[77,930,144],{"class":91},[15,932,933,934,937],{},"Always ",[24,935,936],{},"COUNT"," a non-nullable column from the joined table.",[10,939,941],{"id":940},"semi-joins-and-anti-joins","Semi-joins and anti-joins",[15,943,944,945,948],{},"Sometimes you want to filter by ",[19,946,947],{},"existence",", not return the other table's columns.",[150,950,951,957],{},[153,952,953,956],{},[19,954,955],{},"Semi-join"," — rows that have at least one match (each row once, no duplicates).",[153,958,959,962,963,966],{},[19,960,961],{},"Anti-join"," — rows that have ",[19,964,965],{},"no"," match.",[68,968,970],{"className":70,"code":969,"language":72,"meta":73,"style":73},"-- semi-join: users with at least one order\nSELECT u.* FROM users u\nWHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);\n\n-- anti-join: products never ordered\nSELECT p.* FROM products p\nWHERE NOT EXISTS (SELECT 1 FROM order_items i WHERE i.product_id = p.id);\n",[24,971,972,977,990,1028,1032,1037,1051],{"__ignoreMap":73},[77,973,974],{"class":79,"line":80},[77,975,976],{"class":186},"-- semi-join: users with at least one order\n",[77,978,979,981,984,986,988],{"class":79,"line":106},[77,980,84],{"class":83},[77,982,983],{"class":91}," u.",[77,985,848],{"class":83},[77,987,211],{"class":83},[77,989,388],{"class":91},[77,991,992,994,997,1000,1002,1005,1007,1009,1011,1013,1015,1017,1019,1021,1023,1025],{"class":79,"line":115},[77,993,34],{"class":83},[77,995,996],{"class":83}," EXISTS",[77,998,999],{"class":91}," (",[77,1001,84],{"class":83},[77,1003,1004],{"class":87}," 1",[77,1006,211],{"class":83},[77,1008,219],{"class":91},[77,1010,34],{"class":83},[77,1012,224],{"class":87},[77,1014,66],{"class":91},[77,1016,131],{"class":87},[77,1018,134],{"class":83},[77,1020,194],{"class":87},[77,1022,66],{"class":91},[77,1024,141],{"class":87},[77,1026,1027],{"class":91},");\n",[77,1029,1030],{"class":79,"line":247},[77,1031,244],{"emptyLinePlaceholder":243},[77,1033,1034],{"class":79,"line":253},[77,1035,1036],{"class":186},"-- anti-join: products never ordered\n",[77,1038,1039,1041,1044,1046,1048],{"class":79,"line":436},[77,1040,84],{"class":83},[77,1042,1043],{"class":91}," p.",[77,1045,848],{"class":83},[77,1047,211],{"class":83},[77,1049,1050],{"class":91}," products p\n",[77,1052,1053,1055,1058,1060,1062,1064,1066,1068,1070,1072,1074,1076,1078,1080,1082,1084,1086],{"class":79,"line":442},[77,1054,34],{"class":83},[77,1056,1057],{"class":83}," NOT",[77,1059,996],{"class":83},[77,1061,999],{"class":91},[77,1063,84],{"class":83},[77,1065,1004],{"class":87},[77,1067,211],{"class":83},[77,1069,668],{"class":91},[77,1071,34],{"class":83},[77,1073,673],{"class":87},[77,1075,66],{"class":91},[77,1077,711],{"class":87},[77,1079,134],{"class":83},[77,1081,698],{"class":87},[77,1083,66],{"class":91},[77,1085,141],{"class":87},[77,1087,1027],{"class":91},[15,1089,1090,1093,1094,1097,1098,1101,1102,1105,1106,1108,1109,1112,1113,1116,1117,1119,1120,1123],{},[24,1091,1092],{},"EXISTS"," stops at the first match and is ",[19,1095,1096],{},"NULL-safe",", unlike ",[24,1099,1100],{},"NOT IN"," — which returns\n",[19,1103,1104],{},"no rows"," if the value list contains any ",[24,1107,26],{},". The anti-join can also be written as a\n",[24,1110,1111],{},"LEFT JOIN ... WHERE right_key IS NULL",". (Remember: in SQL, ",[24,1114,1115],{},"anything = NULL"," is\n",[24,1118,349],{},", never true — always use ",[24,1121,1122],{},"IS NULL",".)",[10,1125,1127],{"id":1126},"cross-join-non-equi-joins-and-performance","CROSS JOIN, non-equi joins, and performance",[15,1129,519,1130,1133,1134,1137,1138,39,1141,1144,1145,1148],{},[24,1131,1132],{},"CROSS JOIN"," produces the Cartesian product (every row paired with every row) — useful\nfor generating combinations, dangerous by accident. ",[19,1135,1136],{},"Non-equi joins"," use ",[24,1139,1140],{},"\u003C",[24,1142,1143],{},">",", or\n",[24,1146,1147],{},"BETWEEN"," instead of equality — handy for matching values to ranges (price tiers,\ntime windows):",[68,1150,1152],{"className":70,"code":1151,"language":72,"meta":73,"style":73},"SELECT e.id, c.name\nFROM events e\nJOIN campaigns c ON e.occurred_at BETWEEN c.start_at AND c.end_at;\n",[24,1153,1154,1173,1180],{"__ignoreMap":73},[77,1155,1156,1158,1160,1162,1164,1166,1169,1171],{"class":79,"line":80},[77,1157,84],{"class":83},[77,1159,535],{"class":87},[77,1161,66],{"class":91},[77,1163,141],{"class":87},[77,1165,39],{"class":91},[77,1167,1168],{"class":87},"c",[77,1170,66],{"class":91},[77,1172,632],{"class":87},[77,1174,1175,1177],{"class":79,"line":106},[77,1176,109],{"class":83},[77,1178,1179],{"class":91}," events e\n",[77,1181,1182,1184,1187,1189,1191,1193,1196,1199,1202,1204,1207,1209,1211,1213,1216],{"class":79,"line":115},[77,1183,118],{"class":83},[77,1185,1186],{"class":91}," campaigns c ",[77,1188,30],{"class":83},[77,1190,535],{"class":87},[77,1192,66],{"class":91},[77,1194,1195],{"class":87},"occurred_at",[77,1197,1198],{"class":83}," BETWEEN",[77,1200,1201],{"class":87}," c",[77,1203,66],{"class":91},[77,1205,1206],{"class":87},"start_at",[77,1208,488],{"class":83},[77,1210,1201],{"class":87},[77,1212,66],{"class":91},[77,1214,1215],{"class":87},"end_at",[77,1217,144],{"class":91},[15,1219,1220,1221,1224,1225,1227,1228,1231,1232,1235,1236,1239,1240,1243],{},"Performance hinges on ",[19,1222,1223],{},"indexes",": the database matches on the ",[24,1226,30],{}," columns, so an index\non the join key (usually the foreign key, which often isn't indexed automatically) turns\na full scan into a fast lookup. The optimizer then picks a physical strategy — ",[19,1229,1230],{},"nested\nloop"," (small\u002Findexed), ",[19,1233,1234],{},"hash join"," (large unindexed equi-joins), or ",[19,1237,1238],{},"merge join","\n(sorted inputs). Use ",[24,1241,1242],{},"EXPLAIN"," to see which.",[10,1245,1247],{"id":1246},"recap","Recap",[15,1249,1250,1251,1253,1254,1257,1258,1261,1262,1264,1265,1267,1268,1271,1272,1275,1276,1278,1279,1282],{},"Joins combine tables on a related column; the ",[19,1252,61],{}," decides how unmatched rows are\nhandled — ",[19,1255,1256],{},"INNER"," keeps only matches, ",[19,1259,1260],{},"OUTER"," keeps the leftovers with ",[24,1263,26],{},"s. The\nbiggest gotchas are putting optional-table predicates in ",[24,1266,34],{}," (which silently makes a\nLEFT JOIN behave like INNER), ",[19,1269,1270],{},"row multiplication"," from one-to-many joins (fix with\naggregation or pre-aggregation), and the ",[19,1273,1274],{},"LEFT JOIN COUNT"," trap. Use ",[24,1277,1092],{},"\u002F",[24,1280,1281],{},"NOT EXISTS"," for NULL-safe existence checks, and index your join keys. Get those right and\nyour joins return exactly the rows you intend.",[1284,1285,1286],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":73,"searchDepth":106,"depth":106,"links":1288},[1289,1290,1291,1292,1293,1294,1295,1296,1297],{"id":12,"depth":106,"text":13},{"id":46,"depth":106,"text":47},{"id":147,"depth":106,"text":148},{"id":330,"depth":106,"text":331},{"id":515,"depth":106,"text":516},{"id":723,"depth":106,"text":724},{"id":940,"depth":106,"text":941},{"id":1126,"depth":106,"text":1127},{"id":1246,"depth":106,"text":1247},"SQL join interview questions — inner vs outer joins, left vs right, self joins and how NULLs behave, with examples.","medium","md","SQL",{},"\u002Fblog\u002Fsql-joins-inner-outer-self-anti","\u002Fsql\u002Fbasics\u002Fjoins",{"title":5,"description":1298},"blog\u002Fsql-joins-inner-outer-self-anti","Query Basics","basics","2026-06-17","i2ASrHiqybGmmktMUZh4mm-AVj5pVrchSB6HGRtRAL0",1781808673081]