[{"data":1,"prerenderedAt":181},["ShallowReactive",2],{"topic-sql-basics":3},{"framework":4,"topic":16,"subtopics":24},{"id":5,"description":6,"extension":7,"icon":8,"meta":9,"name":10,"order":11,"slug":12,"stem":13,"tier":14,"__hash__":15},"frameworks\u002Fframeworks\u002Fsql.yml","SQL interview questions on queries, joins and aggregation — essential for every backend, data and analytics interview.","yml","database",{},"SQL",4,"sql","frameworks\u002Fsql",1,"lpzsOj2p9p9W0Tctwc61nP-ulZAA80R5gJiyaZS6ZeI",{"id":17,"description":18,"extension":7,"frameworkSlug":12,"meta":19,"name":20,"order":14,"slug":21,"stem":22,"__hash__":23},"topics\u002Ftopics\u002Fsql-basics.yml","SELECT, WHERE, JOINs and aggregation — the core SQL every interview expects you to know cold.",{},"Query Basics","basics","topics\u002Fsql-basics","HCjWR2CEk8Qa71ZK3hKymDtquizhpXvBFklpn4fxcuM",[25],{"id":26,"title":27,"body":28,"description":32,"difficulty":35,"extension":36,"framework":10,"frameworkSlug":12,"meta":37,"navigation":38,"order":14,"path":39,"questions":40,"related":175,"seo":176,"seoDescription":177,"stem":178,"subtopic":27,"topic":20,"topicSlug":21,"updated":179,"__hash__":180},"qa\u002Fsql\u002Fbasics\u002Fjoins.md","Joins",{"type":29,"value":30,"toc":31},"minimark",[],{"title":32,"searchDepth":33,"depth":33,"links":34},"",2,[],"medium","md",{},true,"\u002Fsql\u002Fbasics\u002Fjoins",[41,46,50,54,59,63,67,71,75,79,83,87,91,95,99,103,107,111,115,119,123,127,131,135,139,143,147,151,155,159,163,167,171],{"id":42,"difficulty":43,"q":44,"a":45},"what-is-join","easy","What is a JOIN?","A JOIN combines rows from **two or more tables** into one result set, matching\nthem on a **related column** (typically a foreign key referencing a primary\nkey). Relational databases store data in *normalized* tables to avoid\nduplication; joins are how you stitch that data back together at query time.\n\n```sql\n-- users(id, name)  and  orders(id, user_id, total)\nSELECT users.name, orders.total\nFROM users\nJOIN orders ON orders.user_id = users.id;\n```\n\nThe `ON` clause is the **join condition** — it decides which rows from the left\ntable pair with which rows from the right. The *type* of join (INNER, LEFT,\netc.) then decides what to do with rows that have **no match**.\n",{"id":47,"difficulty":35,"q":48,"a":49},"inner-vs-outer","What is the difference between INNER JOIN and OUTER JOIN?","The difference is what happens to **unmatched rows**:\n\n- **`INNER JOIN`** returns **only** rows that have a match in *both* tables.\n  Rows with no counterpart are dropped from the result.\n- **`OUTER JOIN`** (LEFT \u002F RIGHT \u002F FULL) **keeps unmatched rows** from one or\n  both sides, filling the missing columns with **`NULL`**.\n\n```sql\n-- only users who have placed at least one order\nSELECT u.name, o.total\nFROM users u\nINNER JOIN orders o ON o.user_id = u.id;\n\n-- every user, with NULLs for those who never ordered\nSELECT u.name, o.total\nFROM users u\nLEFT OUTER JOIN orders o ON o.user_id = u.id;\n```\n\nThink of INNER as the **intersection** and OUTER as \"intersection **plus** the\nleftovers from the chosen side(s).\" (`INNER` and `OUTER` are optional keywords —\n`JOIN` alone means `INNER JOIN`, and `LEFT JOIN` means `LEFT OUTER JOIN`.)\n",{"id":51,"difficulty":35,"q":52,"a":53},"left-vs-right","What is the difference between LEFT and RIGHT JOIN?","Both are outer joins; they differ only in **which side is preserved**:\n\n- **`LEFT JOIN`** keeps **all rows from the left** (first) table, plus matching\n  rows from the right — unmatched right columns become `NULL`.\n- **`RIGHT JOIN`** keeps **all rows from the right** (second) table, plus\n  matches from the left.\n\nThey're **mirror images**: any RIGHT JOIN can be rewritten as a LEFT JOIN by\nswapping the table order, which is why teams often standardize on LEFT JOIN for\nreadability.\n\n```sql\n-- these two return the same rows\nSELECT u.name, o.id FROM users u LEFT  JOIN orders o ON o.user_id = u.id;\nSELECT u.name, o.id FROM orders o RIGHT JOIN users  u ON o.user_id = u.id;\n```\n",{"id":55,"difficulty":56,"q":57,"a":58},"self-join","hard","What is a self join?","A self join is a table **joined to itself**, using **table aliases** to treat\nthe one physical table as two logical ones. It's the standard way to relate\nrows *within* the same table — most classically, **hierarchies** where a row\npoints to another row in the same table.\n\n```sql\n-- employees(id, name, manager_id) where manager_id -> employees.id\nSELECT e.name AS employee, m.name AS manager\nFROM employees e\nLEFT JOIN employees m ON e.manager_id = m.id;\n```\n\nHere `e` is the \"employee\" view of the table and `m` is the \"manager\" view.\nUsing `LEFT JOIN` keeps top-level employees (whose `manager_id` is `NULL`) in\nthe result with a `NULL` manager. Aliases are **mandatory** — without them the\ncolumn references would be ambiguous.\n",{"id":60,"difficulty":35,"q":61,"a":62},"cross-join","What does a CROSS JOIN produce?","A `CROSS JOIN` returns the **Cartesian product**: every row of the first table\npaired with **every** row of the second, with **no `ON` condition**. If the\ntables have *M* and *N* rows, you get *M × N* rows back — so it grows fast.\n\n```sql\n-- generate every size\u002Fcolor combination\nSELECT s.label, c.name\nFROM sizes s\nCROSS JOIN colors c;   -- 4 sizes × 6 colors = 24 rows\n```\n\nUse it deliberately — for generating combinations, building calendars, or\ncreating test data. An **accidental** cross join (forgetting the join\ncondition, the dreaded \"comma join\" `FROM a, b`) is a common cause of\nrunaway result sets and slow queries.\n",{"id":64,"difficulty":56,"q":65,"a":66},"join-nulls","How do you find rows with no match using a join?","Use the **anti-join** pattern: `LEFT JOIN` the second table, then filter where\nits key **`IS NULL`**. Because unmatched rows get `NULL` in the right-hand\ncolumns, \"right key is NULL\" precisely selects the rows that had **no match**.\n\n```sql\n-- users who have never placed an order\nSELECT u.*\nFROM users u\nLEFT JOIN orders o ON o.user_id = u.id\nWHERE o.id IS NULL;\n```\n\nTwo things to get right: filter on a column that's **non-nullable in the source\ntable** (like the right table's primary key `o.id`) so a `NULL` there truly\nmeans \"no row matched,\" and remember you must use **`IS NULL`**, never\n`= NULL` — in SQL, `anything = NULL` evaluates to `unknown`, never `true`.\n`NOT EXISTS` is an equivalent and often clearer alternative.\n",{"id":68,"difficulty":56,"q":69,"a":70},"on-vs-where","What is the difference between the ON and WHERE clauses in a join?","`ON` defines **how rows are matched** (it runs *during* the join); `WHERE` filters\nthe **result** *after* the join. For **INNER** joins they're often\ninterchangeable, but for **OUTER** joins they behave very differently.\n\n```sql\n-- keeps all users; only joins orders with amount > 100\nSELECT u.name, o.amount\nFROM users u\nLEFT JOIN orders o ON o.user_id = u.id AND o.amount > 100;\n\n-- effectively INNER: WHERE drops users whose joined row is NULL\nSELECT u.name, o.amount\nFROM users u\nLEFT JOIN orders o ON o.user_id = u.id\nWHERE o.amount > 100;\n```\n\nPut conditions on the **joined (right) table** in `ON` to preserve outer rows; put\nthem in `WHERE` to filter the final result.\n",{"id":72,"difficulty":56,"q":73,"a":74},"left-join-where-trap","Why does filtering the right table in WHERE turn a LEFT JOIN into an INNER JOIN?","An outer join fills unmatched right-table columns with `NULL`. A `WHERE` condition\non those columns (other than `IS NULL`) evaluates to `unknown` for the unmatched\nrows and **removes them** — silently converting your LEFT JOIN into an INNER JOIN.\n\n```sql\n-- unmatched users have o.status = NULL -> WHERE drops them\nSELECT u.name, o.status\nFROM users u\nLEFT JOIN orders o ON o.user_id = u.id\nWHERE o.status = 'shipped';\n\n-- move the predicate into ON to keep all users\nLEFT JOIN orders o ON o.user_id = u.id AND o.status = 'shipped';\n```\n\nThis is one of the most common SQL bugs. Rule: predicates on the optional side\nbelong in `ON`.\n",{"id":76,"difficulty":35,"q":77,"a":78},"multiple-joins","How do you join three or more tables?","Chain `JOIN` clauses; each `ON` connects the new table to one already in the query.\nThe joins are applied left to right, building a progressively wider result.\n\n```sql\nSELECT u.name, o.id AS order_id, p.name AS product\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```\n\nEach join multiplies\u002Ffilters rows based on its matches, so a user with many orders\nand items appears on many rows. Mind the cardinality — joining several\none-to-many tables can explode row counts (the \"fan trap\").\n",{"id":80,"difficulty":35,"q":81,"a":82},"using-clause","What does the USING clause do?","`USING (col)` is shorthand for an equi-join when the join columns have the **same\nname** in both tables: `USING (user_id)` ≡ `ON a.user_id = b.user_id`. It also\n**merges** the shared column into one in the output.\n\n```sql\nSELECT name, amount\nFROM users\nJOIN orders USING (user_id);   -- one user_id column in the result\n```\n\nCleaner than repeating the column, but only works when names match exactly, and\nthe coalesced column can't be qualified with a table alias. `ON` is more explicit\nand flexible.\n",{"id":84,"difficulty":56,"q":85,"a":86},"natural-join","What is a NATURAL JOIN and why is it risky?","`NATURAL JOIN` automatically joins on **all columns with the same name** in both\ntables — no `ON` needed. It's dangerous because the join condition is **implicit**:\nadding a same-named column later (like `created_at`) silently changes the join.\n\n```sql\nSELECT * FROM users NATURAL JOIN orders;\n-- joins on EVERY shared column name — fragile and surprising\n```\n\nA new `updated_at` column on both tables would suddenly become part of the join\ncondition, breaking results with no error. Most teams **avoid** `NATURAL JOIN` in\nfavor of explicit `ON`\u002F`USING`.\n",{"id":88,"difficulty":35,"q":89,"a":90},"join-groupby","How do you combine a join with aggregation?","Join the tables, then `GROUP BY` the dimension you want to aggregate per, applying\naggregate functions to the joined rows. With outer joins, choose `COUNT(column)`\nvs `COUNT(*)` carefully (next question).\n\n```sql\nSELECT u.name, COUNT(o.id) AS order_count, COALESCE(SUM(o.amount), 0) AS total\nFROM users u\nLEFT JOIN orders o ON o.user_id = u.id\nGROUP BY u.id, u.name;\n```\n\nThe `LEFT JOIN` keeps users with zero orders (counted as 0). Every non-aggregated\nselected column must appear in `GROUP BY` (in standard SQL).\n",{"id":92,"difficulty":35,"q":93,"a":94},"duplicate-rows","Why do joins sometimes produce duplicate rows?","A join produces a row for **every matching pair**. If one side matches **multiple**\nrows on the other (a one-to-many relationship), the single-side values repeat\nacross those matches — not true duplicates, but multiplied rows.\n\n```sql\n-- a user with 3 orders appears on 3 rows\nSELECT u.name, o.id\nFROM users u\nJOIN orders o ON o.user_id = u.id;\n```\n\nTo collapse them, aggregate (`GROUP BY` + `COUNT`\u002F`SUM`), use `DISTINCT`, or\npre-aggregate the many-side in a subquery before joining. Joining two\none-to-many tables to the same parent multiplies rows (the fan trap).\n",{"id":96,"difficulty":56,"q":97,"a":98},"left-join-count-trap","What is the COUNT trap with LEFT JOIN?","`COUNT(*)` counts **rows**, including the NULL-filled row produced for an unmatched\nLEFT JOIN — so users with no orders wrongly count as 1. `COUNT(column)` ignores\n`NULL`s, giving the correct 0.\n\n```sql\nSELECT u.name,\n       COUNT(*)    AS wrong,   -- counts the NULL row -> 1 for zero-order users\n       COUNT(o.id) AS correct  -- ignores NULLs -> 0\nFROM users u\nLEFT JOIN orders o ON o.user_id = u.id\nGROUP BY u.id, u.name;\n```\n\nAlways `COUNT` a **non-nullable column from the joined table** (like its primary\nkey) when counting matches in an outer join.\n",{"id":100,"difficulty":35,"q":101,"a":102},"join-vs-subquery","When should you use a join vs a subquery?","Use a **join** when you need **columns from both tables** in the output. Use a\n**subquery** (especially `EXISTS`\u002F`IN`) when you only need to **filter** by the\nexistence of related rows, not return their columns.\n\n```sql\n-- join: need order data in the result\nSELECT u.name, o.amount FROM users u JOIN orders o ON o.user_id = u.id;\n\n-- subquery: only filter users who have any order\nSELECT name FROM users u WHERE EXISTS (\n  SELECT 1 FROM orders o WHERE o.user_id = u.id\n);\n```\n\nA join can produce duplicate rows when filtering by existence; `EXISTS` won't.\nModern optimizers often plan them similarly, so favor whichever is clearer.\n",{"id":104,"difficulty":56,"q":105,"a":106},"exists-vs-in","What is the difference between EXISTS and IN?","Both test membership, but: `EXISTS` stops at the **first match** (often faster for\nlarge\u002Fcorrelated subqueries) and handles `NULL`s safely. `IN` compares against a\nvalue list and has a **NULL trap** — `NOT IN` with any `NULL` in the list returns\nno rows.\n\n```sql\n-- if any user_id is NULL, NOT IN returns NOTHING\nSELECT * FROM users WHERE id NOT IN (SELECT user_id FROM orders);\n\n-- NOT EXISTS is NULL-safe\nSELECT * FROM users u WHERE NOT EXISTS (\n  SELECT 1 FROM orders o WHERE o.user_id = u.id\n);\n```\n\nPrefer `EXISTS`\u002F`NOT EXISTS` for correlated existence checks, especially when\n`NULL`s are possible.\n",{"id":108,"difficulty":35,"q":109,"a":110},"not-exists-antijoin","How do you write an anti-join?","An anti-join returns rows from the first table that have **no match** in the\nsecond. Two idioms: `NOT EXISTS`, or `LEFT JOIN ... WHERE right.key IS NULL`.\n\n```sql\n-- products never ordered (NOT EXISTS)\nSELECT p.* FROM products p\nWHERE NOT EXISTS (\n  SELECT 1 FROM order_items i WHERE i.product_id = p.id\n);\n\n-- equivalent LEFT JOIN form\nSELECT p.* FROM products p\nLEFT JOIN order_items i ON i.product_id = p.id\nWHERE i.product_id IS NULL;\n```\n\n`NOT EXISTS` is usually clearest and NULL-safe; the LEFT JOIN form can be faster\nwith the right indexes. Avoid `NOT IN` here due to its NULL trap.\n",{"id":112,"difficulty":56,"q":113,"a":114},"semi-join","What is a semi-join?","A semi-join returns rows from the first table that have **at least one match** in\nthe second — but **without** duplicating them per match and without returning the\nsecond table's columns. `EXISTS`\u002F`IN` express it.\n\n```sql\n-- users who have placed at least one order (each user once)\nSELECT u.* FROM users u\nWHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);\n```\n\nContrast with an inner join, which would repeat a user once per order. Semi-join =\n\"filter by existence.\" Anti-join = \"filter by non-existence.\" Databases have\ndedicated semi\u002Fanti-join operators for these.\n",{"id":116,"difficulty":35,"q":117,"a":118},"full-outer-join","What does a FULL OUTER JOIN do?","A `FULL OUTER JOIN` keeps **all rows from both tables**, matching where possible\nand filling the non-matching side with `NULL`s. It's the union of LEFT and RIGHT\nouter joins.\n\n```sql\nSELECT COALESCE(a.id, b.id) AS id, a.val AS left_val, b.val AS right_val\nFROM table_a a\nFULL OUTER JOIN table_b b ON a.id = b.id;\n```\n\nUseful for reconciliation (\"what's in either source\"), like comparing two datasets\nand finding rows present in one but not the other. Supported in PostgreSQL\u002FSQL\nServer\u002FOracle, but **not** in MySQL (which needs an emulation).\n",{"id":120,"difficulty":56,"q":121,"a":122},"emulate-full-outer","How do you emulate a FULL OUTER JOIN in MySQL?","MySQL lacks `FULL OUTER JOIN`, so you **UNION a LEFT JOIN with a RIGHT JOIN** (or a\nLEFT JOIN where the left key is NULL), using `UNION` to dedupe the overlapping\nmatched rows.\n\n```sql\nSELECT a.id, a.val, b.val FROM a LEFT JOIN b ON a.id = b.id\nUNION\nSELECT b.id, a.val, b.val FROM a RIGHT JOIN b ON a.id = b.id;\n```\n\n`UNION` (not `UNION ALL`) removes the duplicate matched rows that appear in both\nhalves. The first half gives all left rows + matches; the second adds the\nunmatched right rows.\n",{"id":124,"difficulty":35,"q":125,"a":126},"equi-non-equi","What is the difference between an equi-join and a non-equi join?","An **equi-join** matches with equality (`a.x = b.x`) — by far the most common. A\n**non-equi join** uses other operators (`\u003C`, `>`, `BETWEEN`, `!=`), useful for\nranges, bands, and comparisons.\n\n```sql\n-- non-equi: match each sale to its price tier by range\nSELECT s.amount, t.label\nFROM sales s\nJOIN tiers t ON s.amount BETWEEN t.min_amount AND t.max_amount;\n```\n\nNon-equi joins can match many rows and are costlier (no simple hash join), but\nthey're powerful for bucketing, gaps-and-islands, and \"find rows within a range\"\nproblems.\n",{"id":128,"difficulty":43,"q":129,"a":130},"join-multiple-columns","How do you join on multiple columns?","Combine the conditions in the `ON` clause with `AND` — all must match. This is\ncommon for **composite keys** or matching on several attributes.\n\n```sql\nSELECT *\nFROM order_items i\nJOIN inventory v\n  ON v.product_id = i.product_id\n AND v.warehouse_id = i.warehouse_id;\n```\n\nEvery `AND` condition tightens the match. If the columns share names across both\ntables, `USING (product_id, warehouse_id)` is a shorthand. Make sure composite-key\ncolumns are indexed together for performance.\n",{"id":132,"difficulty":56,"q":133,"a":134},"self-join-consecutive","How do you use a self join to compare consecutive rows?","Join a table to itself on a key offset by one (e.g. matching `id = id + 1`, or\nusing a date difference) to put each row next to its neighbor — useful for\ncomputing differences between sequential records.\n\n```sql\nSELECT a.day, b.sales - a.sales AS daily_change\nFROM daily a\nJOIN daily b ON b.day = a.day + INTERVAL '1 day';\n```\n\nModern SQL often replaces this with **window functions** (`LAG`\u002F`LEAD`), which are\ncleaner and faster, but the self-join technique is the classic approach and still\nappears in interviews.\n",{"id":136,"difficulty":56,"q":137,"a":138},"fan-trap","What is the fan trap (row multiplication) in joins?","The fan trap occurs when you join one parent to **two different one-to-many**\nchild tables. Their rows multiply against each other (a partial Cartesian\nproduct), inflating aggregates like `SUM`.\n\n```sql\n-- orders × shipments multiply; SUM(amount) is overcounted\nSELECT o.id, SUM(p.amount), SUM(s.weight)\nFROM orders o\nJOIN payments p  ON p.order_id = o.id\nJOIN shipments s ON s.order_id = o.id\nGROUP BY o.id;\n```\n\nFix by **pre-aggregating** each child in its own subquery before joining, so each\ncontributes a single row per parent. Always sanity-check counts when joining\nmultiple one-to-many tables.\n",{"id":140,"difficulty":56,"q":141,"a":142},"pre-aggregate","Why and how do you pre-aggregate before joining?","Pre-aggregating collapses a one-to-many child to **one row per key** in a subquery,\nso the subsequent join doesn't multiply rows or distort aggregates (avoiding the\nfan trap and double counting).\n\n```sql\nSELECT u.name, o.order_count, o.total\nFROM users u\nLEFT JOIN (\n  SELECT user_id, COUNT(*) AS order_count, SUM(amount) AS total\n  FROM orders GROUP BY user_id\n) o ON o.user_id = u.id;\n```\n\nNow each user joins to exactly one aggregated order row. This pattern also lets you\ncombine **multiple** independent aggregates without them multiplying together.\n",{"id":144,"difficulty":56,"q":145,"a":146},"join-indexes","How do indexes affect join performance?","Joins match rows on the `ON` columns, so an **index on the join key** (typically\nthe foreign key) lets the database find matches quickly instead of scanning the\nwhole table. Without it, joins degrade to slow full scans.\n\n```sql\nCREATE INDEX idx_orders_user_id ON orders(user_id); -- speeds up the join\nSELECT * FROM users u JOIN orders o ON o.user_id = u.id;\n```\n\nPrimary keys are indexed automatically, but **foreign keys often aren't** — a\nfrequent cause of slow joins. Index both sides of frequent join conditions, and\ncomposite indexes for multi-column joins.\n",{"id":148,"difficulty":56,"q":149,"a":150},"join-algorithms","What join algorithms do databases use?","The optimizer picks among three physical strategies based on data size and\nindexes:\n\n- **Nested loop join** — for each row in one table, look up matches in the other.\n  Great with an index on the inner table; small inputs.\n- **Hash join** — build a hash table on one input, probe with the other. Best for\n  large, unindexed equi-joins.\n- **Merge join** — sort both inputs on the key, then merge. Efficient when inputs\n  are already sorted\u002Findexed.\n\n```sql\nEXPLAIN SELECT * FROM users u JOIN orders o ON o.user_id = u.id;\n-- shows which join algorithm the planner chose\n```\n\nYou don't pick directly, but understanding them (and reading `EXPLAIN`) explains\nwhy a query is slow.\n",{"id":152,"difficulty":35,"q":153,"a":154},"coalesce-join","How do you handle NULLs from outer joins in the output?","Outer joins produce `NULL`s for unmatched rows. Use **`COALESCE`** to substitute a\ndefault (0, '', 'N\u002FA') so results are clean and aggregates behave.\n\n```sql\nSELECT u.name,\n       COALESCE(o.amount, 0)       AS amount,\n       COALESCE(o.status, 'none')  AS status\nFROM users u\nLEFT JOIN orders o ON o.user_id = u.id;\n```\n\n`COALESCE` returns the first non-NULL argument. It's essential after outer joins\nand aggregates (`COALESCE(SUM(x), 0)`) so missing data shows as a sensible value\nrather than `NULL`.\n",{"id":156,"difficulty":35,"q":157,"a":158},"distinct-dedupe","How do you remove duplicate rows from a join result?","A join that matches one-to-many repeats the single-side rows. Options to dedupe:\n`DISTINCT`, aggregation with `GROUP BY`, or restructuring to a semi-join\n(`EXISTS`) when you don't need the joined columns.\n\n```sql\n-- DISTINCT removes exact duplicate rows\nSELECT DISTINCT u.id, u.name\nFROM users u JOIN orders o ON o.user_id = u.id;\n\n-- better when you only want \"users with orders\":\nSELECT u.id, u.name FROM users u\nWHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);\n```\n\nPrefer `EXISTS` over `DISTINCT` when you're really filtering by existence —\n`DISTINCT` does extra sorting\u002Fwork to remove the duplicates the join created.\n",{"id":160,"difficulty":56,"q":161,"a":162},"lateral-join","What is a LATERAL join (or CROSS APPLY)?","A `LATERAL` join (Postgres) \u002F `CROSS APPLY` (SQL Server) lets a subquery in the\n`FROM` clause **reference columns from preceding tables** in the same `FROM` — so\nit runs per outer row. Ideal for \"top-N per group.\"\n\n```sql\n-- each user's 3 most recent orders\nSELECT u.name, o.id, o.created_at\nFROM users u\nCROSS JOIN LATERAL (\n  SELECT id, created_at FROM orders\n  WHERE user_id = u.id            -- references the outer u\n  ORDER BY created_at DESC LIMIT 3\n) o;\n```\n\nRegular subqueries can't see outer `FROM` columns; `LATERAL` can, making\nper-row\u002Fper-group derived tables possible.\n",{"id":164,"difficulty":56,"q":165,"a":166},"outer-join-chain","What happens when you chain outer joins across three tables?","Join order and type matter. A `LEFT JOIN` followed by an `INNER JOIN` on the\noptional table can **drop** the preserved rows, because the inner join requires a\nmatch the NULL-filled rows don't have.\n\n```sql\n-- the INNER JOIN re-filters out users with no orders\nSELECT u.name, oi.qty\nFROM users u\nLEFT JOIN orders o      ON o.user_id = u.id\nJOIN order_items oi     ON oi.order_id = o.id;  -- inner -> drops NULL o.id\n\n-- keep it LEFT all the way down\nLEFT JOIN order_items oi ON oi.order_id = o.id;\n```\n\nTo preserve outer rows through a chain, every downstream join on the optional path\nmust also be an outer join.\n",{"id":168,"difficulty":43,"q":169,"a":170},"join-vs-union","What is the difference between a JOIN and a UNION?","A **JOIN** combines tables **horizontally** — adding columns by matching rows. A\n**UNION** combines result sets **vertically** — stacking rows from queries that\nhave the **same columns**.\n\n```sql\n-- JOIN: wider rows (user + their order)\nSELECT u.name, o.amount FROM users u JOIN orders o ON o.user_id = u.id;\n\n-- UNION: more rows (current + archived orders)\nSELECT id, amount FROM orders\nUNION ALL\nSELECT id, amount FROM archived_orders;\n```\n\n`UNION` dedupes; `UNION ALL` keeps duplicates (and is faster). Use JOIN to relate\ntables, UNION to append similar datasets.\n",{"id":172,"difficulty":56,"q":173,"a":174},"range-join","How do you join rows within a date or value range?","Use a non-equi join with `BETWEEN` or comparison operators in `ON` — matching each\nrow to all rows of the other table that fall within a range. Common for\ntime-windows, price tiers, and IP-range lookups.\n\n```sql\n-- attribute each event to the active campaign window it falls in\nSELECT e.id, c.name\nFROM events e\nJOIN campaigns c\n  ON e.occurred_at BETWEEN c.start_at AND c.end_at;\n```\n\nRange joins can match multiple rows and don't use simple hash joins, so they're\nheavier — index the range columns, and ensure ranges don't unintentionally overlap\n(which multiplies rows).\n",null,{"description":32},"SQL join interview questions — inner vs outer joins, left vs right, self joins and how NULLs behave, with examples.","sql\u002Fbasics\u002Fjoins","2026-06-17","1D24_T3jGo4Oh0JqSVvR75PpYEfzw3-UWKgF21mDRPw",1781808674083]