<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged inner-join</title>
<link>https://coderlegion.com/tag/inner-join</link>
<description>Powered by Question2Answer</description>
<item>
<title>INNER JOIN vs Correlated Subquery in SQL: What Is Actually Faster?</title>
<link>https://coderlegion.com/21160/inner-join-vs-correlated-subquery-in-sql-what-is-actually-faster</link>
<description>&lt;p&gt;Most SQL developers hear the same rule early: always prefer &lt;code&gt;INNER JOIN&lt;/code&gt; over correlated subqueries, because subqueries create an N+1 performance problem.&lt;/p&gt;
&lt;p&gt;That rule is useful, but it is not always true.&lt;/p&gt;
&lt;p&gt;I tested this on four engines (MySQL, Oracle, PostgreSQL, SQLite) and got mixed results. In some cases, the correlated subquery was faster than the join query.&lt;/p&gt;
&lt;p&gt;If you want to validate the same patterns yourself, you can run these examples in live sandboxes and practice similar SQL tasks on &lt;a rel=&quot;nofollow&quot; href=&quot;https://sqltest.online&quot;&gt;sqltest.online&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;what-you-will-learn&quot;&gt;What You Will Learn&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;what &lt;code&gt;INNER JOIN&lt;/code&gt; does best;&lt;/li&gt;
&lt;li&gt;when a correlated subquery can be competitive;&lt;/li&gt;
&lt;li&gt;why execution plans matter more than &quot;golden rules&quot;;&lt;/li&gt;
&lt;li&gt;how to benchmark both approaches correctly.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-is-inner-join-in-sql-&quot;&gt;What Is INNER JOIN in SQL?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;INNER JOIN&lt;/code&gt; returns only rows that match in both tables.&lt;/p&gt;
&lt;p&gt;Why it matters: it is usually the cleanest set-based way to combine data, and often gives the optimizer strong options.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    c.customer_id,
    c.name,
    o.order_id
FROM customers c
INNER JOIN orders o
    ON c.customer_id = o.customer_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In many workloads, this is exactly what you want. But performance still depends on table sizes, indexes, and optimizer decisions.&lt;/p&gt;
&lt;h3 id=&quot;benchmark-scenario&quot;&gt;Benchmark Scenario&lt;/h3&gt;
&lt;p&gt;I used a small outer table and a larger inner table:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;customers&lt;/code&gt;: 25 rows&lt;/li&gt;
&lt;li&gt;&lt;code&gt;orders&lt;/code&gt;: 1000 rows&lt;/li&gt;
&lt;li&gt;index on &lt;code&gt;orders(customer_id)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Task: return each customer with the number of their orders.&lt;/p&gt;
&lt;h3 id=&quot;two-query-patterns&quot;&gt;Two Query Patterns&lt;/h3&gt;
&lt;h4 id=&quot;pattern-a-join-group-by&quot;&gt;Pattern A: JOIN + GROUP BY&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    c.customer_id,
    COUNT(o.order_id) AS orders_count
FROM customers c
LEFT JOIN orders o
    ON c.customer_id = o.customer_id
GROUP BY c.customer_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: I used &lt;code&gt;LEFT JOIN&lt;/code&gt; here to keep customers with zero orders. If you only need matched rows, use &lt;code&gt;INNER JOIN&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;pattern-b-correlated-subquery&quot;&gt;Pattern B: Correlated Subquery&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    c.customer_id,
    (
        SELECT COUNT(o.order_id)
        FROM orders o
        WHERE o.customer_id = c.customer_id
    ) AS orders_count
FROM customers c;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second form looks like classic N+1, but an index can make each lookup very cheap.&lt;/p&gt;
&lt;h3 id=&quot;how-to-inspect-the-real-cost&quot;&gt;How to Inspect the Real Cost&lt;/h3&gt;
&lt;p&gt;Use execution plans, not assumptions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MySQL / PostgreSQL: &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;SQLite: &lt;code&gt;EXPLAIN QUERY PLAN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Oracle: &lt;code&gt;EXPLAIN PLAN FOR ...&lt;/code&gt; + &lt;code&gt;DBMS_XPLAN.DISPLAY&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;results-summary-across-engines&quot;&gt;Results Summary Across Engines&lt;/h3&gt;
&lt;h4 id=&quot;mysql-8-and-oracle-23c&quot;&gt;MySQL 8 and Oracle 23c&lt;/h4&gt;
&lt;p&gt;In my tests, the correlated subquery often won.&lt;/p&gt;
&lt;p&gt;Why: with a small outer table (25 customers), the optimizer could do very fast indexed lookups on &lt;code&gt;orders(customer_id)&lt;/code&gt; per customer. That was cheaper than building larger join/group intermediate work.&lt;/p&gt;
&lt;h4 id=&quot;postgresql-16&quot;&gt;PostgreSQL 16&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;JOIN + GROUP BY&lt;/code&gt; won in my test.&lt;/p&gt;
&lt;p&gt;Why: PostgreSQL produced an efficient set-based plan and handled the join/aggregate path very well.&lt;/p&gt;
&lt;h4 id=&quot;sqlite&quot;&gt;SQLite&lt;/h4&gt;
&lt;p&gt;Practical tie.&lt;/p&gt;
&lt;p&gt;Both variants ended with very similar plans and near-identical timing on this dataset.&lt;/p&gt;
&lt;h3 id=&quot;why-the-inner-join-rule-sometimes-fails&quot;&gt;Why the INNER JOIN Rule Sometimes Fails&lt;/h3&gt;
&lt;p&gt;The &quot;always use INNER JOIN&quot; advice fails when it ignores three variables:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Database engine optimizer behavior&lt;/li&gt;
&lt;li&gt;Data distribution and table size ratio&lt;/li&gt;
&lt;li&gt;Index availability and quality&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;SQL performance is not about syntax preference. It is about the physical plan your engine chooses.&lt;/p&gt;
&lt;h3 id=&quot;common-mistakes&quot;&gt;Common Mistakes&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;assuming &lt;code&gt;INNER JOIN&lt;/code&gt; is always faster without checking plans;&lt;/li&gt;
&lt;li&gt;forgetting that &lt;code&gt;INNER JOIN&lt;/code&gt; removes non-matching rows;&lt;/li&gt;
&lt;li&gt;testing once without warm-up runs;&lt;/li&gt;
&lt;li&gt;benchmarking without a supporting index;&lt;/li&gt;
&lt;li&gt;comparing query text instead of comparing actual execution plans.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;practical-checklist&quot;&gt;Practical Checklist&lt;/h3&gt;
&lt;p&gt;Before choosing &lt;code&gt;INNER JOIN&lt;/code&gt; or correlated subquery:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add the right index (here: &lt;code&gt;orders(customer_id)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Run both query variants.&lt;/li&gt;
&lt;li&gt;Capture execution plans.&lt;/li&gt;
&lt;li&gt;Compare timing over several runs.&lt;/li&gt;
&lt;li&gt;Pick the query that is both correct and fast for your engine.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;final-takeaway&quot;&gt;Final Takeaway&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;INNER JOIN&lt;/code&gt; is a strong default and often the right choice, but it is not a universal performance guarantee.&lt;/p&gt;
&lt;p&gt;The best query is the one that produces the best execution plan on your actual database, with your actual data.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21160/inner-join-vs-correlated-subquery-in-sql-what-is-actually-faster</guid>
<pubDate>Mon, 22 Jun 2026 20:57:21 +0000</pubDate>
</item>
</channel>
</rss>