[{"data":1,"prerenderedAt":1446},["ShallowReactive",2],{"blog-\u002Fblog\u002Fsql-joins-vs-subqueries-vs-ctes":3},{"id":4,"title":5,"body":6,"description":1433,"difficulty":1434,"extension":1435,"framework":1436,"frameworkSlug":196,"meta":1437,"navigation":832,"order":307,"path":1438,"qaPath":430,"seo":1439,"stem":1440,"subtopic":1441,"topic":1442,"topicSlug":1443,"updated":1444,"__hash__":1445},"blog\u002Fblog\u002Fsql-joins-vs-subqueries-vs-ctes.md","SQL JOINs vs Subqueries vs CTEs — Performance and When to Use Each",{"type":7,"value":8,"toc":1415},"minimark",[9,14,23,27,181,185,191,311,322,412,421,432,436,442,447,453,508,512,522,608,614,748,758,770,871,883,891,898,909,1115,1122,1288,1292,1295,1332,1343,1348,1356,1360,1368,1372,1375,1383,1387,1411],[10,11,13],"h2",{"id":12},"three-ways-to-combine-data-one-right-answer-per-situation","Three ways to combine data, one right answer per situation",[15,16,17,18,22],"p",{},"Every intermediate SQL writer reaches a point where they can express the same query as a\n",[19,20,21],"code",{},"JOIN",", a subquery, or a CTE — and aren't sure which to pick. The choice matters: a\ncorrelated subquery can turn a millisecond query into a minutes-long scan; a CTE with the\nwrong database can silently perform worse than a join. This guide explains how the query\nplanner treats each form, when performance diverges, and the judgment that makes the\nright choice obvious.",[10,24,26],{"id":25},"quick-reference-comparison","Quick-reference comparison",[28,29,30,53],"table",{},[31,32,33],"thead",{},[34,35,36,39,43,46],"tr",{},[37,38],"th",{},[37,40,41],{},[19,42,21],{},[37,44,45],{},"Subquery",[37,47,48,49,52],{},"CTE (",[19,50,51],{},"WITH",")",[54,55,56,74,90,106,129,144,161],"tbody",{},[34,57,58,65,68,71],{},[59,60,61],"td",{},[62,63,64],"strong",{},"What it does",[59,66,67],{},"Combines rows from two tables",[59,69,70],{},"Returns a result set used by the outer query",[59,72,73],{},"Names a temporary result set, referenced above the main query",[34,75,76,81,84,87],{},[59,77,78],{},[62,79,80],{},"Execution",[59,82,83],{},"Planner optimises join order, indexes",[59,85,86],{},"Inline subqueries often merged with outer; correlated ones run per row",[59,88,89],{},"Usually materialised once, or inlined by planner (database-dependent)",[34,91,92,97,100,103],{},[59,93,94],{},[62,95,96],{},"Performance",[59,98,99],{},"Generally fastest — planners are tuned for joins",[59,101,102],{},"Inline: similar to join. Correlated: O(n) per outer row",[59,104,105],{},"Depends on dialect — Postgres often materialises, MySQL\u002FSQL Server may inline",[34,107,108,113,116,126],{},[59,109,110],{},[62,111,112],{},"Readability",[59,114,115],{},"Concise for simple cases; nests poorly",[59,117,118,119,122,123],{},"Can be hard to read inside ",[19,120,121],{},"WHERE"," or ",[19,124,125],{},"FROM",[59,127,128],{},"Best readability for multi-step logic",[34,130,131,136,139,141],{},[59,132,133],{},[62,134,135],{},"Reusable?",[59,137,138],{},"No",[59,140,138],{},[59,142,143],{},"Yes — reference the same CTE multiple times",[34,145,146,151,153,155],{},[59,147,148],{},[62,149,150],{},"Recursive?",[59,152,138],{},[59,154,138],{},[59,156,157,158,52],{},"Yes (",[19,159,160],{},"WITH RECURSIVE",[34,162,163,168,171,178],{},[59,164,165],{},[62,166,167],{},"Best for",[59,169,170],{},"Combining tables, filtering by relation",[59,172,173,174,177],{},"Scalar lookups, ",[19,175,176],{},"EXISTS"," checks",[59,179,180],{},"Multi-step transforms, recursive queries, readability",[10,182,184],{"id":183},"joins-the-planners-home-territory","JOINs — the planner's home territory",[15,186,187,188,190],{},"A ",[19,189,21],{}," is the primary way relational databases combine data. The query planner has\ndecades of optimisation for join operations: it picks hash joins, merge joins, or nested\nloop joins based on table size and index availability, and it can reorder joins freely to\nminimise intermediate row counts.",[192,193,198],"pre",{"className":194,"code":195,"language":196,"meta":197,"style":197},"language-sql shiki shiki-themes github-light github-dark","-- Simple inner join: orders with their customer names\nSELECT o.id, o.total, c.name\nFROM   orders  o\nJOIN   customers c ON c.id = o.customer_id\nWHERE  o.total > 100;\n-- Planner can use index on customer_id and o.total simultaneously\n","sql","",[19,199,200,209,248,256,284,305],{"__ignoreMap":197},[201,202,205],"span",{"class":203,"line":204},"line",1,[201,206,208],{"class":207},"sJ8bj","-- Simple inner join: orders with their customer names\n",[201,210,212,216,220,224,227,230,233,235,238,240,243,245],{"class":203,"line":211},2,[201,213,215],{"class":214},"szBVR","SELECT",[201,217,219],{"class":218},"sj4cs"," o",[201,221,223],{"class":222},"sVt8B",".",[201,225,226],{"class":218},"id",[201,228,229],{"class":222},", ",[201,231,232],{"class":218},"o",[201,234,223],{"class":222},[201,236,237],{"class":218},"total",[201,239,229],{"class":222},[201,241,242],{"class":218},"c",[201,244,223],{"class":222},[201,246,247],{"class":218},"name\n",[201,249,251,253],{"class":203,"line":250},3,[201,252,125],{"class":214},[201,254,255],{"class":222},"   orders  o\n",[201,257,259,261,264,267,270,272,274,277,279,281],{"class":203,"line":258},4,[201,260,21],{"class":214},[201,262,263],{"class":222},"   customers c ",[201,265,266],{"class":214},"ON",[201,268,269],{"class":218}," c",[201,271,223],{"class":222},[201,273,226],{"class":218},[201,275,276],{"class":214}," =",[201,278,219],{"class":218},[201,280,223],{"class":222},[201,282,283],{"class":218},"customer_id\n",[201,285,287,289,292,294,296,299,302],{"class":203,"line":286},5,[201,288,121],{"class":214},[201,290,291],{"class":218},"  o",[201,293,223],{"class":222},[201,295,237],{"class":218},[201,297,298],{"class":214}," >",[201,300,301],{"class":218}," 100",[201,303,304],{"class":222},";\n",[201,306,308],{"class":203,"line":307},6,[201,309,310],{"class":207},"-- Planner can use index on customer_id and o.total simultaneously\n",[15,312,313,314,317,318,321],{},"Joins shine for ",[62,315,316],{},"combining tables on a key"," and ",[62,319,320],{},"filtering by relationship",". They're\nthe right tool when both sides of the relationship are needed in the result.",[192,323,325],{"className":194,"code":324,"language":196,"meta":197,"style":197},"-- LEFT JOIN: all customers, even those with no orders\nSELECT c.name, COUNT(o.id) AS order_count\nFROM   customers c\nLEFT JOIN orders o ON o.customer_id = c.id\nGROUP BY c.name;\n",[19,326,327,332,366,373,399],{"__ignoreMap":197},[201,328,329],{"class":203,"line":204},[201,330,331],{"class":207},"-- LEFT JOIN: all customers, even those with no orders\n",[201,333,334,336,338,340,343,345,348,351,353,355,357,360,363],{"class":203,"line":211},[201,335,215],{"class":214},[201,337,269],{"class":218},[201,339,223],{"class":222},[201,341,342],{"class":218},"name",[201,344,229],{"class":222},[201,346,347],{"class":218},"COUNT",[201,349,350],{"class":222},"(",[201,352,232],{"class":218},[201,354,223],{"class":222},[201,356,226],{"class":218},[201,358,359],{"class":222},") ",[201,361,362],{"class":214},"AS",[201,364,365],{"class":222}," order_count\n",[201,367,368,370],{"class":203,"line":250},[201,369,125],{"class":214},[201,371,372],{"class":222},"   customers c\n",[201,374,375,378,381,383,385,387,390,392,394,396],{"class":203,"line":258},[201,376,377],{"class":214},"LEFT JOIN",[201,379,380],{"class":222}," orders o ",[201,382,266],{"class":214},[201,384,219],{"class":218},[201,386,223],{"class":222},[201,388,389],{"class":218},"customer_id",[201,391,276],{"class":214},[201,393,269],{"class":218},[201,395,223],{"class":222},[201,397,398],{"class":218},"id\n",[201,400,401,404,406,408,410],{"class":203,"line":286},[201,402,403],{"class":214},"GROUP BY",[201,405,269],{"class":218},[201,407,223],{"class":222},[201,409,342],{"class":218},[201,411,304],{"class":222},[15,413,414,417,418,420],{},[62,415,416],{},"Rule of thumb:"," default to a ",[19,419,21],{}," for combining tables. If you find yourself writing\na subquery where a join would work just as well, the join is almost always faster and\nclearer.",[422,423,424],"blockquote",{},[15,425,426,427],{},"Deep dive: ",[428,429,431],"a",{"href":430},"\u002Fsql\u002Fbasics\u002Fjoins","Joins interview questions",[10,433,435],{"id":434},"subqueries-scalar-lookups-and-existence-checks","Subqueries — scalar lookups and existence checks",[15,437,438,439,441],{},"A subquery is a ",[19,440,215],{}," nested inside another statement. There are two fundamentally\ndifferent kinds, and their performance characteristics are opposite.",[443,444,446],"h3",{"id":445},"non-correlated-subqueries-fast","Non-correlated subqueries (fast)",[15,448,187,449,452],{},[62,450,451],{},"non-correlated subquery"," runs once and hands its result to the outer query. The\nplanner usually optimises it identically to a join.",[192,454,456],{"className":194,"code":455,"language":196,"meta":197,"style":197},"-- Non-correlated: inner query runs ONCE, result reused\nSELECT name\nFROM   products\nWHERE  price > (SELECT AVG(price) FROM products);\n--              ↑ computed once, compared against every row\n",[19,457,458,463,470,477,503],{"__ignoreMap":197},[201,459,460],{"class":203,"line":204},[201,461,462],{"class":207},"-- Non-correlated: inner query runs ONCE, result reused\n",[201,464,465,467],{"class":203,"line":211},[201,466,215],{"class":214},[201,468,469],{"class":214}," name\n",[201,471,472,474],{"class":203,"line":250},[201,473,125],{"class":214},[201,475,476],{"class":222},"   products\n",[201,478,479,481,484,487,490,492,495,498,500],{"class":203,"line":258},[201,480,121],{"class":214},[201,482,483],{"class":222},"  price ",[201,485,486],{"class":214},">",[201,488,489],{"class":222}," (",[201,491,215],{"class":214},[201,493,494],{"class":218}," AVG",[201,496,497],{"class":222},"(price) ",[201,499,125],{"class":214},[201,501,502],{"class":222}," products);\n",[201,504,505],{"class":203,"line":286},[201,506,507],{"class":207},"--              ↑ computed once, compared against every row\n",[443,509,511],{"id":510},"correlated-subqueries-can-be-slow","Correlated subqueries (can be slow)",[15,513,187,514,517,518,521],{},[62,515,516],{},"correlated subquery"," references a column from the outer query, forcing it to\n",[62,519,520],{},"re-execute for every row"," the outer query processes. On a million-row table, that's a\nmillion subquery executions.",[192,523,525],{"className":194,"code":524,"language":196,"meta":197,"style":197},"-- Correlated: re-runs the subquery for EVERY employee row — O(n²) risk\nSELECT name, salary\nFROM   employees e\nWHERE  salary > (\n    SELECT AVG(salary)\n    FROM   employees\n    WHERE  department = e.department  -- ← references outer row\n);\n",[19,526,527,532,542,549,561,571,579,602],{"__ignoreMap":197},[201,528,529],{"class":203,"line":204},[201,530,531],{"class":207},"-- Correlated: re-runs the subquery for EVERY employee row — O(n²) risk\n",[201,533,534,536,539],{"class":203,"line":211},[201,535,215],{"class":214},[201,537,538],{"class":214}," name",[201,540,541],{"class":222},", salary\n",[201,543,544,546],{"class":203,"line":250},[201,545,125],{"class":214},[201,547,548],{"class":222},"   employees e\n",[201,550,551,553,556,558],{"class":203,"line":258},[201,552,121],{"class":214},[201,554,555],{"class":222},"  salary ",[201,557,486],{"class":214},[201,559,560],{"class":222}," (\n",[201,562,563,566,568],{"class":203,"line":286},[201,564,565],{"class":214},"    SELECT",[201,567,494],{"class":218},[201,569,570],{"class":222},"(salary)\n",[201,572,573,576],{"class":203,"line":307},[201,574,575],{"class":214},"    FROM",[201,577,578],{"class":222},"   employees\n",[201,580,582,585,588,591,594,596,599],{"class":203,"line":581},7,[201,583,584],{"class":214},"    WHERE",[201,586,587],{"class":222},"  department ",[201,589,590],{"class":214},"=",[201,592,593],{"class":218}," e",[201,595,223],{"class":222},[201,597,598],{"class":218},"department",[201,600,601],{"class":207},"  -- ← references outer row\n",[201,603,605],{"class":203,"line":604},8,[201,606,607],{"class":222},");\n",[15,609,610,611,613],{},"The fix is almost always to push the subquery into a ",[19,612,21],{}," or CTE so it runs once:",[192,615,617],{"className":194,"code":616,"language":196,"meta":197,"style":197},"-- Better: aggregate once, then join\nWITH dept_avg AS (\n    SELECT department, AVG(salary) AS avg_sal\n    FROM   employees\n    GROUP BY department\n)\nSELECT e.name, e.salary\nFROM   employees e\nJOIN   dept_avg d ON d.department = e.department\nWHERE  e.salary > d.avg_sal;\n",[19,618,619,624,635,653,659,667,672,692,698,724],{"__ignoreMap":197},[201,620,621],{"class":203,"line":204},[201,622,623],{"class":207},"-- Better: aggregate once, then join\n",[201,625,626,628,631,633],{"class":203,"line":211},[201,627,51],{"class":214},[201,629,630],{"class":222}," dept_avg ",[201,632,362],{"class":214},[201,634,560],{"class":222},[201,636,637,639,642,645,648,650],{"class":203,"line":250},[201,638,565],{"class":214},[201,640,641],{"class":222}," department, ",[201,643,644],{"class":218},"AVG",[201,646,647],{"class":222},"(salary) ",[201,649,362],{"class":214},[201,651,652],{"class":222}," avg_sal\n",[201,654,655,657],{"class":203,"line":258},[201,656,575],{"class":214},[201,658,578],{"class":222},[201,660,661,664],{"class":203,"line":286},[201,662,663],{"class":214},"    GROUP BY",[201,665,666],{"class":222}," department\n",[201,668,669],{"class":203,"line":307},[201,670,671],{"class":222},")\n",[201,673,674,676,678,680,682,684,687,689],{"class":203,"line":581},[201,675,215],{"class":214},[201,677,593],{"class":218},[201,679,223],{"class":222},[201,681,342],{"class":218},[201,683,229],{"class":222},[201,685,686],{"class":218},"e",[201,688,223],{"class":222},[201,690,691],{"class":218},"salary\n",[201,693,694,696],{"class":203,"line":604},[201,695,125],{"class":214},[201,697,548],{"class":222},[201,699,701,703,706,708,711,713,715,717,719,721],{"class":203,"line":700},9,[201,702,21],{"class":214},[201,704,705],{"class":222},"   dept_avg d ",[201,707,266],{"class":214},[201,709,710],{"class":218}," d",[201,712,223],{"class":222},[201,714,598],{"class":218},[201,716,276],{"class":214},[201,718,593],{"class":218},[201,720,223],{"class":222},[201,722,723],{"class":218},"department\n",[201,725,727,729,732,734,737,739,741,743,746],{"class":203,"line":726},10,[201,728,121],{"class":214},[201,730,731],{"class":218},"  e",[201,733,223],{"class":222},[201,735,736],{"class":218},"salary",[201,738,298],{"class":214},[201,740,710],{"class":218},[201,742,223],{"class":222},[201,744,745],{"class":218},"avg_sal",[201,747,304],{"class":222},[443,749,751,753,754,757],{"id":750},"exists-vs-in-for-membership-checks",[19,752,176],{}," vs ",[19,755,756],{},"IN"," for membership checks",[15,759,760,761,763,764,766,767,769],{},"For checking whether a related row exists, ",[19,762,176],{}," is usually faster than ",[19,765,756],{}," on large\nsets — it short-circuits as soon as it finds one match, while ",[19,768,756],{}," builds the full set.",[192,771,773],{"className":194,"code":772,"language":196,"meta":197,"style":197},"-- EXISTS: stops scanning as soon as one match is found\nSELECT name FROM customers c\nWHERE  EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);\n\n-- IN: materialises the full set of customer_ids first\nSELECT name FROM customers\nWHERE  id IN (SELECT customer_id FROM orders);\n",[19,774,775,780,792,828,834,839,850],{"__ignoreMap":197},[201,776,777],{"class":203,"line":204},[201,778,779],{"class":207},"-- EXISTS: stops scanning as soon as one match is found\n",[201,781,782,784,786,789],{"class":203,"line":211},[201,783,215],{"class":214},[201,785,538],{"class":214},[201,787,788],{"class":214}," FROM",[201,790,791],{"class":222}," customers c\n",[201,793,794,796,799,801,803,806,808,810,812,814,816,818,820,822,824,826],{"class":203,"line":250},[201,795,121],{"class":214},[201,797,798],{"class":214},"  EXISTS",[201,800,489],{"class":222},[201,802,215],{"class":214},[201,804,805],{"class":218}," 1",[201,807,788],{"class":214},[201,809,380],{"class":222},[201,811,121],{"class":214},[201,813,219],{"class":218},[201,815,223],{"class":222},[201,817,389],{"class":218},[201,819,276],{"class":214},[201,821,269],{"class":218},[201,823,223],{"class":222},[201,825,226],{"class":218},[201,827,607],{"class":222},[201,829,830],{"class":203,"line":258},[201,831,833],{"emptyLinePlaceholder":832},true,"\n",[201,835,836],{"class":203,"line":286},[201,837,838],{"class":207},"-- IN: materialises the full set of customer_ids first\n",[201,840,841,843,845,847],{"class":203,"line":307},[201,842,215],{"class":214},[201,844,538],{"class":214},[201,846,788],{"class":214},[201,848,849],{"class":222}," customers\n",[201,851,852,854,857,859,861,863,866,868],{"class":203,"line":581},[201,853,121],{"class":214},[201,855,856],{"class":222},"  id ",[201,858,756],{"class":214},[201,860,489],{"class":222},[201,862,215],{"class":214},[201,864,865],{"class":222}," customer_id ",[201,867,125],{"class":214},[201,869,870],{"class":222}," orders);\n",[15,872,873,875,876,879,880,882],{},[62,874,416],{}," use a subquery for ",[62,877,878],{},"scalar lookups"," (single value comparisons) and\n",[19,881,176],{}," checks. Never use a correlated subquery where a join or CTE with a GROUP BY\nwould work — you'll turn a fast query into a table scan.",[422,884,885],{},[15,886,426,887],{},[428,888,890],{"href":889},"\u002Fsql\u002Fsubqueries\u002Fsubqueries","Subqueries interview questions",[10,892,894,895,897],{"id":893},"ctes-with-readability-and-multi-step-logic","CTEs (",[19,896,51],{},") — readability and multi-step logic",[15,899,187,900,903,904,908],{},[62,901,902],{},"Common Table Expression"," names a temporary result set at the top of a query. The\nmain query can reference it like a table, and the same CTE can be referenced multiple\ntimes. CTEs don't change what is ",[905,906,907],"em",{},"possible"," — anything a CTE expresses could also be a\nsubquery — but they dramatically improve readability for multi-step logic.",[192,910,912],{"className":194,"code":911,"language":196,"meta":197,"style":197},"-- Without CTE: deeply nested, hard to read\nSELECT * FROM (\n    SELECT department, AVG(salary) AS avg_sal\n    FROM (\n        SELECT * FROM employees WHERE active = true\n    ) active_emps\n    GROUP BY department\n) dept_avg\nWHERE avg_sal > 80000;\n\n-- With CTE: reads top to bottom like a story\nWITH active_employees AS (\n    SELECT * FROM employees WHERE active = true\n),\ndept_avg AS (\n    SELECT department, AVG(salary) AS avg_sal\n    FROM   active_employees\n    GROUP BY department\n)\nSELECT * FROM dept_avg WHERE avg_sal > 80000;\n",[19,913,914,919,930,944,950,972,977,983,988,1002,1006,1012,1024,1043,1049,1059,1074,1082,1089,1094],{"__ignoreMap":197},[201,915,916],{"class":203,"line":204},[201,917,918],{"class":207},"-- Without CTE: deeply nested, hard to read\n",[201,920,921,923,926,928],{"class":203,"line":211},[201,922,215],{"class":214},[201,924,925],{"class":214}," *",[201,927,788],{"class":214},[201,929,560],{"class":222},[201,931,932,934,936,938,940,942],{"class":203,"line":250},[201,933,565],{"class":214},[201,935,641],{"class":222},[201,937,644],{"class":218},[201,939,647],{"class":222},[201,941,362],{"class":214},[201,943,652],{"class":222},[201,945,946,948],{"class":203,"line":258},[201,947,575],{"class":214},[201,949,560],{"class":222},[201,951,952,955,957,959,962,964,967,969],{"class":203,"line":286},[201,953,954],{"class":214},"        SELECT",[201,956,925],{"class":214},[201,958,788],{"class":214},[201,960,961],{"class":222}," employees ",[201,963,121],{"class":214},[201,965,966],{"class":222}," active ",[201,968,590],{"class":214},[201,970,971],{"class":222}," true\n",[201,973,974],{"class":203,"line":307},[201,975,976],{"class":222},"    ) active_emps\n",[201,978,979,981],{"class":203,"line":581},[201,980,663],{"class":214},[201,982,666],{"class":222},[201,984,985],{"class":203,"line":604},[201,986,987],{"class":222},") dept_avg\n",[201,989,990,992,995,997,1000],{"class":203,"line":700},[201,991,121],{"class":214},[201,993,994],{"class":222}," avg_sal ",[201,996,486],{"class":214},[201,998,999],{"class":218}," 80000",[201,1001,304],{"class":222},[201,1003,1004],{"class":203,"line":726},[201,1005,833],{"emptyLinePlaceholder":832},[201,1007,1009],{"class":203,"line":1008},11,[201,1010,1011],{"class":207},"-- With CTE: reads top to bottom like a story\n",[201,1013,1015,1017,1020,1022],{"class":203,"line":1014},12,[201,1016,51],{"class":214},[201,1018,1019],{"class":222}," active_employees ",[201,1021,362],{"class":214},[201,1023,560],{"class":222},[201,1025,1027,1029,1031,1033,1035,1037,1039,1041],{"class":203,"line":1026},13,[201,1028,565],{"class":214},[201,1030,925],{"class":214},[201,1032,788],{"class":214},[201,1034,961],{"class":222},[201,1036,121],{"class":214},[201,1038,966],{"class":222},[201,1040,590],{"class":214},[201,1042,971],{"class":222},[201,1044,1046],{"class":203,"line":1045},14,[201,1047,1048],{"class":222},"),\n",[201,1050,1052,1055,1057],{"class":203,"line":1051},15,[201,1053,1054],{"class":222},"dept_avg ",[201,1056,362],{"class":214},[201,1058,560],{"class":222},[201,1060,1062,1064,1066,1068,1070,1072],{"class":203,"line":1061},16,[201,1063,565],{"class":214},[201,1065,641],{"class":222},[201,1067,644],{"class":218},[201,1069,647],{"class":222},[201,1071,362],{"class":214},[201,1073,652],{"class":222},[201,1075,1077,1079],{"class":203,"line":1076},17,[201,1078,575],{"class":214},[201,1080,1081],{"class":222},"   active_employees\n",[201,1083,1085,1087],{"class":203,"line":1084},18,[201,1086,663],{"class":214},[201,1088,666],{"class":222},[201,1090,1092],{"class":203,"line":1091},19,[201,1093,671],{"class":222},[201,1095,1097,1099,1101,1103,1105,1107,1109,1111,1113],{"class":203,"line":1096},20,[201,1098,215],{"class":214},[201,1100,925],{"class":214},[201,1102,788],{"class":214},[201,1104,630],{"class":222},[201,1106,121],{"class":214},[201,1108,994],{"class":222},[201,1110,486],{"class":214},[201,1112,999],{"class":218},[201,1114,304],{"class":222},[15,1116,1117,1118,1121],{},"CTEs are also the only way to write ",[62,1119,1120],{},"recursive queries"," — traversing trees, graphs, or\nhierarchies — without procedural code:",[192,1123,1125],{"className":194,"code":1124,"language":196,"meta":197,"style":197},"-- Recursive CTE: walk an org chart\nWITH RECURSIVE org AS (\n    SELECT id, name, manager_id, 0 AS depth\n    FROM   employees WHERE manager_id IS NULL   -- root\n    UNION ALL\n    SELECT e.id, e.name, e.manager_id, o.depth + 1\n    FROM   employees e\n    JOIN   org o ON o.id = e.manager_id          -- recurse\n)\nSELECT * FROM org ORDER BY depth;\n",[19,1126,1127,1132,1146,1167,1188,1193,1235,1241,1268,1272],{"__ignoreMap":197},[201,1128,1129],{"class":203,"line":204},[201,1130,1131],{"class":207},"-- Recursive CTE: walk an org chart\n",[201,1133,1134,1136,1139,1142,1144],{"class":203,"line":211},[201,1135,51],{"class":214},[201,1137,1138],{"class":214}," RECURSIVE",[201,1140,1141],{"class":222}," org ",[201,1143,362],{"class":214},[201,1145,560],{"class":222},[201,1147,1148,1150,1153,1155,1158,1161,1164],{"class":203,"line":250},[201,1149,565],{"class":214},[201,1151,1152],{"class":222}," id, ",[201,1154,342],{"class":214},[201,1156,1157],{"class":222},", manager_id, ",[201,1159,1160],{"class":218},"0",[201,1162,1163],{"class":214}," AS",[201,1165,1166],{"class":222}," depth\n",[201,1168,1169,1171,1174,1176,1179,1182,1185],{"class":203,"line":258},[201,1170,575],{"class":214},[201,1172,1173],{"class":222},"   employees ",[201,1175,121],{"class":214},[201,1177,1178],{"class":222}," manager_id ",[201,1180,1181],{"class":214},"IS",[201,1183,1184],{"class":214}," NULL",[201,1186,1187],{"class":207},"   -- root\n",[201,1189,1190],{"class":203,"line":286},[201,1191,1192],{"class":214},"    UNION ALL\n",[201,1194,1195,1197,1199,1201,1203,1205,1207,1209,1211,1213,1215,1217,1220,1222,1224,1226,1229,1232],{"class":203,"line":307},[201,1196,565],{"class":214},[201,1198,593],{"class":218},[201,1200,223],{"class":222},[201,1202,226],{"class":218},[201,1204,229],{"class":222},[201,1206,686],{"class":218},[201,1208,223],{"class":222},[201,1210,342],{"class":218},[201,1212,229],{"class":222},[201,1214,686],{"class":218},[201,1216,223],{"class":222},[201,1218,1219],{"class":218},"manager_id",[201,1221,229],{"class":222},[201,1223,232],{"class":218},[201,1225,223],{"class":222},[201,1227,1228],{"class":218},"depth",[201,1230,1231],{"class":214}," +",[201,1233,1234],{"class":218}," 1\n",[201,1236,1237,1239],{"class":203,"line":581},[201,1238,575],{"class":214},[201,1240,548],{"class":222},[201,1242,1243,1246,1249,1251,1253,1255,1257,1259,1261,1263,1265],{"class":203,"line":604},[201,1244,1245],{"class":214},"    JOIN",[201,1247,1248],{"class":222},"   org o ",[201,1250,266],{"class":214},[201,1252,219],{"class":218},[201,1254,223],{"class":222},[201,1256,226],{"class":218},[201,1258,276],{"class":214},[201,1260,593],{"class":218},[201,1262,223],{"class":222},[201,1264,1219],{"class":218},[201,1266,1267],{"class":207},"          -- recurse\n",[201,1269,1270],{"class":203,"line":700},[201,1271,671],{"class":222},[201,1273,1274,1276,1278,1280,1282,1285],{"class":203,"line":726},[201,1275,215],{"class":214},[201,1277,925],{"class":214},[201,1279,788],{"class":214},[201,1281,1141],{"class":222},[201,1283,1284],{"class":214},"ORDER BY",[201,1286,1287],{"class":222}," depth;\n",[443,1289,1291],{"id":1290},"cte-materialisation-the-performance-caveat","CTE materialisation — the performance caveat",[15,1293,1294],{},"Whether a CTE is a performance help or hindrance depends on the database:",[1296,1297,1298,1309,1326],"ul",{},[1299,1300,1301,1304,1305,1308],"li",{},[62,1302,1303],{},"PostgreSQL"," (pre-14): CTEs are always ",[62,1306,1307],{},"materialised"," (computed once, result stored).\nThis can be faster (when referenced many times) or slower (when the optimiser could push\na filter inside).",[1299,1310,1311,1314,1315,1314,1318,1321,1322,1325],{},[62,1312,1313],{},"PostgreSQL 14+"," \u002F ",[62,1316,1317],{},"SQL Server",[62,1319,1320],{},"MySQL 8+",": the planner may ",[62,1323,1324],{},"inline"," a CTE,\ntreating it like a subquery and optimising freely.",[1299,1327,1328,1331],{},[62,1329,1330],{},"Oracle",": similar to Postgres 14+ — may inline or materialise based on cost.",[15,1333,1334,1335,1338,1339,1342],{},"When you need Postgres to ",[905,1336,1337],{},"not"," materialise a CTE, use ",[19,1340,1341],{},"WITH ... AS NOT MATERIALIZED"," (14+).",[15,1344,1345,1347],{},[62,1346,416],{}," use a CTE whenever a query has more than two logical steps or when you\nneed to reference the same intermediate result more than once. Prefer a join over a CTE\nwhen performance is critical and the planner version is uncertain.",[422,1349,1350],{},[15,1351,426,1352],{},[428,1353,1355],{"href":1354},"\u002Fsql\u002Fsubqueries\u002Fctes","CTEs interview questions",[10,1357,1359],{"id":1358},"the-decision-flowchart","The decision flowchart",[192,1361,1366],{"className":1362,"code":1364,"language":1365},[1363],"language-text","Do I need to combine columns from two tables?\n└── Yes → JOIN  (default choice; planner is optimised for it)\n\nDo I need a single value from another table?\n└── Yes → scalar subquery  (SELECT MAX(...) etc.)\n\nDo I just need to check whether a related row exists?\n└── Yes → EXISTS subquery  (short-circuits, usually faster than IN)\n\nDoes my logic have multiple steps, or do I reference the same\nintermediate result more than once?\n└── Yes → CTE  (readability + reuse; watch materialisation in old Postgres)\n\nDoes the query need to walk a tree or graph?\n└── Yes → recursive CTE  (no other clean SQL option)\n","text",[19,1367,1364],{"__ignoreMap":197},[10,1369,1371],{"id":1370},"interview-tip-explain-the-trade-offs","Interview tip — explain the trade-offs",[15,1373,1374],{},"Interviewers asking \"use a join or a subquery?\" are testing whether you understand the\nperformance implications. The safe answer:",[422,1376,1377],{},[15,1378,1379,1380,1382],{},"\"I'd default to a join because planners have the most optimisation for that form. I'd\nuse a non-correlated subquery for scalar comparisons, ",[19,1381,176],{}," for membership checks,\nand a CTE to break complex multi-step logic into readable pieces. I'd avoid correlated\nsubqueries except in rare cases where the database rewrites them to a join anyway.\"",[10,1384,1386],{"id":1385},"recap","Recap",[15,1388,1389,1392,1393,1396,1397,1400,1401,1403,1404,1406,1407,1410],{},[62,1390,1391],{},"JOINs"," are the planner's home territory — use them to combine tables and let the\noptimiser choose the best execution strategy. ",[62,1394,1395],{},"Non-correlated subqueries"," run once and\nare often rewritten to joins internally; ",[62,1398,1399],{},"correlated subqueries"," re-run per outer row\nand can silently cause O(n²) performance — replace them with a ",[19,1402,21],{}," + ",[19,1405,403],{}," or a\nCTE. ",[62,1408,1409],{},"CTEs"," trade raw performance for readability and reusability, and are the only way\nto write recursive queries; their materialisation behaviour varies by database version.\nDefault to joins, escalate to CTEs for clarity, and avoid correlated subqueries on large\ntables.",[1412,1413,1414],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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);}",{"title":197,"searchDepth":211,"depth":211,"links":1416},[1417,1418,1419,1420,1426,1430,1431,1432],{"id":12,"depth":211,"text":13},{"id":25,"depth":211,"text":26},{"id":183,"depth":211,"text":184},{"id":434,"depth":211,"text":435,"children":1421},[1422,1423,1424],{"id":445,"depth":250,"text":446},{"id":510,"depth":250,"text":511},{"id":750,"depth":250,"text":1425},"EXISTS vs IN for membership checks",{"id":893,"depth":211,"text":1427,"children":1428},"CTEs (WITH) — readability and multi-step logic",[1429],{"id":1290,"depth":250,"text":1291},{"id":1358,"depth":211,"text":1359},{"id":1370,"depth":211,"text":1371},{"id":1385,"depth":211,"text":1386},"SQL JOINs vs subqueries vs CTEs — how the query planner treats each, when correlated subqueries kill performance, and the decision rule for writing readable, fast SQL in interviews and production.","medium","md","SQL",{},"\u002Fblog\u002Fsql-joins-vs-subqueries-vs-ctes",{"title":5,"description":1433},"blog\u002Fsql-joins-vs-subqueries-vs-ctes","JOINs vs Subqueries vs CTEs","Subqueries & CTEs","subqueries","2026-06-21","GSfj-1EDCab2AtOmFvHcvAffaJBfXB_S-YPnAuantuk",1782244088095]