Tag: Multiple Tables

  • UNION vs UNION ALL in SQL Server, Plus Your First Subquery

    UNION vs UNION ALL in SQL Server, Plus Your First Subquery

    JOINs combine tables side by side, adding columns. UNION combines result sets top to bottom, stacking rows — a fundamentally different kind of combination, useful whenever two separate queries produce compatible-shaped rows you want as one result. Subqueries, the second half of this lesson, let a query’s WHERE clause be driven by the result of an entirely separate query.

    UNION stacks rows; subqueries nest(a different combo than JOIN)Query AAmir KhanPriya SharmaQuery BPriya Sharma*Zara Alistacked together, top to bottom ↓UNION ALLkeeps every rowAmir KhanPriya SharmaPriya Sharma (again)Zara AliUNIONdedups to 3 rowsAmir KhanPriya SharmaZara Ali✕ duplicate Priya removedOuter Query2SELECT full_name FROM DriverWHERE driver_id IN ( … )Inner Subquery1SELECT driver_id FROM TripWHERE distance_km > 20runs firstfeeds IDs inGotcha: NOT IN silently returns ZERO rows if the subquery’s column has any NULL.NOT EXISTS doesn’t have this trap — prefer it for “not in” logic.

    UNION Combines and Deduplicates

    SELECT full_name, 'Austin driver' AS note FROM dbo.Driver WHERE city = 'Austin'
    UNION ALL
    SELECT full_name, 'High earner' AS note FROM dbo.Driver
    WHERE driver_id IN (SELECT driver_id FROM dbo.Trip WHERE fare_usd > 30);

    Every SELECT in a UNION must return the same number of columns, in compatible types, in the same order — the column names in the final result come from the first SELECT only. This is worth testing directly:

    -- FAILS: mismatched column counts
    SELECT full_name FROM dbo.Driver
    UNION ALL
    SELECT full_name, city FROM dbo.Driver;
    -- Msg 205: All queries combined using a UNION, INTERSECT or EXCEPT operator must have
    -- an equal number of expressions in their target lists.

    UNION vs UNION ALL

    UNION Removes duplicate rows Extra work — slower UNION ALL Keeps every row Faster, no dedup pass

    UNION runs an implicit dedup step (conceptually similar to SELECT DISTINCT applied to the combined result) — real work that costs real time on large result sets. If you know there’s no overlap between the two queries (as in the example above, since a driver can’t simultaneously fail and pass the same filter), or duplicates are genuinely fine for your use case, UNION ALL is the better default. Reach for plain UNION only when you specifically need duplicates removed.

    Two More Set Operators, Briefly

    -- INTERSECT: only rows present in BOTH result sets
    SELECT city FROM dbo.Driver INTERSECT SELECT city FROM dbo.Driver WHERE driver_id > 2;
    
    -- EXCEPT: rows in the first result set but NOT the second
    SELECT city FROM dbo.Driver EXCEPT SELECT city FROM dbo.Driver WHERE driver_id > 2;

    Same column-matching rules as UNION apply. These are less common day-to-day than UNION ALL, but genuinely useful for comparison/reconciliation queries — “what’s in this dataset that isn’t in that one.”

    Your First Subquery

    SELECT full_name
    FROM dbo.Driver
    WHERE driver_id IN (
        SELECT driver_id FROM dbo.Trip WHERE distance_km > 20
    );

    The inner SELECT driver_id FROM dbo.Trip WHERE distance_km > 20 runs first (conceptually), producing a list of IDs the outer query then filters against. This pattern — nesting a query inside another’s WHERE clause — is one you’ll use constantly, and it comes in a few distinct shapes:

    -- Scalar subquery: returns exactly one value, usable anywhere a single value fits
    SELECT full_name FROM dbo.Driver
    WHERE driver_id = (SELECT TOP 1 driver_id FROM dbo.Trip ORDER BY fare_usd DESC);
    
    -- Correlated subquery: references the OUTER query's row, re-evaluated per row
    SELECT full_name FROM dbo.Driver d
    WHERE EXISTS (SELECT 1 FROM dbo.Trip t WHERE t.driver_id = d.driver_id AND t.fare_usd > 25);

    That last one — a correlated subquery using EXISTS — is worth flagging early even though it looks more advanced: it’s generally the safer, often faster alternative to IN for “does at least one matching row exist” checks, and unlike NOT IN (Chapter 3), NOT EXISTS handles NULLs correctly with no surprise gotcha.

    Practice tip: Rewrite the very first example in this lesson (drivers with any trip over 20km) using EXISTS instead of IN, and confirm you get the same result. Getting comfortable moving between the two forms pays off enormously once query performance becomes a topic in the advanced course.

    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server Fundamentals, coming soon on this site.

  • SQL Server Temp Tables: A First Look Before You Need the Full Picture

    SQL Server Temp Tables: A First Look Before You Need the Full Picture

    Sometimes a problem is genuinely easier to solve in two steps than one giant nested query. Temp tables are SQL Server’s answer to “I need somewhere to put an intermediate result while I keep working.”

    Temp Tables: Your Own Scratch Space(session-scoped, not permanent)tempdb (system database)##GlobalScratch — every session can see thisSession A (Window 1)#Scratchvisible only to Session ASession B (Window 2)#Scratchdifferent table — zero conflict!session ends → auto-dropped (or DROP TABLE)Gotcha: a temp table computes its result ONCE and can be reused for free.A subquery/CTE re-runs every time — use a temp table to reuse an expensive result.

    A Local Temp Table in Action

    CREATE TABLE #HighValueTrips (
        trip_id INT,
        fare_usd DECIMAL(8,2)
    );
    
    INSERT INTO #HighValueTrips
    SELECT trip_id, fare_usd FROM dbo.Trip WHERE fare_usd > 20;
    
    SELECT * FROM #HighValueTrips;
    
    DROP TABLE #HighValueTrips;

    Notice this is genuinely a real table — it has its own CREATE TABLE, accepts INSERT, and can be queried, filtered, and even joined to other tables exactly like a permanent one, for as long as your session lasts.

    What Makes It “Temporary”

    #HighValueTrips Visible only to your session Auto-dropped when your session ends

    A local temp table (prefixed with #) physically lives in the special system database tempdb, not your regular database — but is visible only to the session that created it, and is automatically cleaned up when that session ends, or you can drop it explicitly as shown above. Two sessions can both create a table named #Scratch at the same time without any conflict; SQL Server keeps them completely separate internally.

    A Realistic Two-Step Use Case

    -- Step 1: capture an expensive-to-compute intermediate result once
    SELECT driver_id, COUNT(*) AS trip_count, SUM(fare_usd) AS total_earned
    INTO #DriverSummary
    FROM dbo.Trip
    GROUP BY driver_id;
    
    -- Step 2: reuse it multiple times without recomputing the aggregation
    SELECT * FROM #DriverSummary WHERE trip_count > 5;
    SELECT d.full_name, s.total_earned
    FROM dbo.Driver d JOIN #DriverSummary s ON d.driver_id = s.driver_id
    ORDER BY s.total_earned DESC;

    SELECT ... INTO creates the temp table and populates it in one statement, inferring column types automatically — a common shortcut once you’re comfortable with the explicit CREATE TABLE form shown earlier.

    Why Not Just Use a Bigger Subquery?

    You often could. The tradeoff: a temp table computes its result once and lets you reuse and re-query it as many times as needed; a subquery or CTE re-runs its logic each time it’s referenced (with some caveats the advanced course covers). For a genuinely expensive intermediate calculation you need to reuse several times in a longer script, a temp table can be both clearer to read and faster to run.

    Just the Beginning

    This is a preview — the full comparison of local temp tables, table variables, and global temp tables (prefixed ##, visible across all sessions), including exactly when each one is the right tool for a given job and their real performance differences, is a dedicated chapter (Chapter 3) in SQL Server for Developers & DBAs. For now, know that temp tables exist and behave like session-scoped scratch space you can CREATE, INSERT into, query, and DROP just like any other table.

    Practice tip: Rebuild the two-step example above from memory, then try querying #DriverSummary from a brand-new query window/tab in the same tool — you’ll get an “invalid object name” error, since a fresh window is a fresh session. That’s the session-scoping rule made concrete.

    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server Fundamentals, coming soon on this site.

  • SQL Server JOIN Types Explained: INNER, LEFT, RIGHT, FULL, CROSS, SELF

    SQL Server JOIN Types Explained: INNER, LEFT, RIGHT, FULL, CROSS, SELF

    Joining tables is where SQL starts feeling genuinely powerful — and where a subtle mistake silently produces wrong results with zero errors. This lesson covers all six JOIN types with real output differences, then spends real time on the single bug that catches nearly everyone at least once.

    Six Ways to JOIN Two Tables(same two tables, six different results)INNER JOINonly the matchesLEFT JOINall left + matchesRIGHT JOINall right + matchesFULL OUTEReverything, both sidesCROSS JOINevery combinationSELF JOINDrivertable joined to itselfGotcha: a WHERE filter on the right table’s columnsilently turns LEFT JOIN back into INNER JOIN — filter in ON instead.

    The Core Two

    -- INNER JOIN: only rows that match in both tables
    SELECT d.full_name, t.fare_usd
    FROM dbo.Driver d
    INNER JOIN dbo.Trip t ON d.driver_id = t.driver_id;
    
    -- LEFT JOIN: all rows from the left table, matched rows from the right (NULL if no match)
    SELECT d.full_name, t.fare_usd
    FROM dbo.Driver d
    LEFT JOIN dbo.Trip t ON d.driver_id = t.driver_id;

    INNER JOIN LEFT JOIN

    Concretely, if a driver named ‘Amir Khan’ exists but has never had a trip logged: INNER JOIN omits him from the result entirely; LEFT JOIN still shows one row for him, with fare_usd as NULL. That NULL is the entire reason LEFT JOIN exists — it’s how you answer “show me every driver, including ones with zero trips.”

    The Rest of the Set

    -- RIGHT JOIN: mirror of LEFT — all rows from the right table instead
    SELECT d.full_name, t.fare_usd FROM dbo.Trip t RIGHT JOIN dbo.Driver d ON d.driver_id = t.driver_id;
    
    -- FULL OUTER JOIN: everything from both sides, matched where possible
    SELECT d.full_name, t.trip_id FROM dbo.Driver d FULL OUTER JOIN dbo.Trip t ON d.driver_id = t.driver_id;
    
    -- CROSS JOIN: every combination (Cartesian product) — rarely intentional by accident
    SELECT d.full_name, x.label FROM dbo.Driver d CROSS JOIN (VALUES ('Gold'),('Silver')) AS x(label);
    
    -- SELF JOIN: a table joined to itself — e.g. drivers sharing a city
    SELECT d1.full_name, d2.full_name, d1.city
    FROM dbo.Driver d1 INNER JOIN dbo.Driver d2 ON d1.city = d2.city AND d1.driver_id < d2.driver_id;

    In practice, RIGHT JOIN is rarely used on purpose — anything expressible with RIGHT JOIN can be rewritten as a LEFT JOIN by swapping which table comes first, and most style guides prefer that for consistency. FULL OUTER JOIN is genuinely useful for reconciliation tasks ("what's in table A but not B, and vice versa, in one query"). CROSS JOIN's real, non-accidental use case is generating combinations — like every product paired with every size, before either exists in a real order.

    The SELF JOIN's d1.driver_id < d2.driver_id condition deserves its own note: without it, every pair of same-city drivers would appear twice (Amir/Priya and Priya/Amir), plus every driver paired with themselves. The inequality keeps exactly one direction of each unique pair.

    The Bug That Gets Everyone at Least Once

    Using LEFT JOIN correctly, then adding a WHERE filter on the right-hand table's column, silently turns it back into an INNER JOIN:

    -- BUG: this discards the NULL rows LEFT JOIN was specifically trying to preserve
    SELECT d.full_name, t.fare_usd
    FROM dbo.Driver d LEFT JOIN dbo.Trip t ON d.driver_id = t.driver_id
    WHERE t.fare_usd > 20;

    NULL fails the > 20 comparison (from Chapter 3's three-valued-logic rule), so unmatched drivers with no trips disappear from the result — exactly what LEFT JOIN was meant to prevent. The query runs without error and looks completely reasonable; you only notice something's wrong when a driver you know exists is mysteriously missing from a report.

    -- The fix: move the condition into the ON clause instead of WHERE
    SELECT d.full_name, t.fare_usd
    FROM dbo.Driver d LEFT JOIN dbo.Trip t ON d.driver_id = t.driver_id AND t.fare_usd > 20;
    -- Now unmatched drivers still appear (fare_usd NULL); only trips are filtered before the join completes
    The rule to memorize: a WHERE condition on the "preserved" side's columns filters the final result, potentially undoing your LEFT JOIN. The same condition inside the ON clause filters before the join decides what counts as a match, which is almost always what you actually want when the goal is "keep all drivers, but only join in their high-value trips."
    Practice tip: Run the buggy version and the fixed version side by side and count the rows returned by each. Seeing the actual row-count difference with your own data makes this rule permanent in a way that reading about it doesn't.

    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it's published, and share it with a developer who'd find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server Fundamentals, coming soon on this site.

  • Primary Keys and Foreign Keys in SQL Server: How Tables Actually Connect

    Primary Keys and Foreign Keys in SQL Server: How Tables Actually Connect

    Everything up to this chapter queried one table at a time. Real schemas are almost never one table — they’re a web of tables connected by keys, and understanding exactly how that connection is enforced (not just “it points to the other table”) is what makes JOINs in the next lesson click instead of feeling like memorized syntax.

    Primary & Foreign Keys, connected(how two tables actually link up)Driverdriver_id (PK)full_namecityone row per driverTriptrip_id (PK)driver_id (FK)distance_kmmany rows per driverFK points to a PKINSERT Trip driver_id=999driver_id 999 isn’t in DriverSQL Server rejects itGotcha: deleting aDriver with existingTrips fails by default —no auto-cascade!

    Setup — a driver/trip example to follow along:

    CREATE TABLE dbo.Driver (
        driver_id   INT IDENTITY(1,1) PRIMARY KEY,
        full_name   NVARCHAR(100) NOT NULL,
        city        NVARCHAR(50)  NOT NULL
    );
    CREATE TABLE dbo.Trip (
        trip_id       INT IDENTITY(1,1) PRIMARY KEY,
        driver_id     INT NOT NULL REFERENCES dbo.Driver(driver_id),
        distance_km   DECIMAL(6,2) NOT NULL,
        fare_usd      DECIMAL(8,2) NOT NULL
    );

    The Relationship, Visualized

    Driver driver_id (PK) full_name city Trip trip_id (PK) driver_id (FK) distance_km 1-to-many

    A primary key (PK) uniquely identifies each row (driver_id in Driver) — SQL Server automatically creates a unique index behind every primary key, which is why lookups by PK are fast by default, before you’ve even thought about indexing. A foreign key (FK) is a column in one table that points to a primary key in another (Trip.driver_idDriver.driver_id).

    Watching the Constraint Actually Enforce Something

    -- This fails — driver_id 999 doesn't exist in Driver:
    INSERT INTO dbo.Trip (driver_id, distance_km, fare_usd) VALUES (999, 5.2, 12.50);
    -- Msg 547: The INSERT statement conflicted with the FOREIGN KEY constraint ...
    
    -- This fails too — you can't delete a driver who still has trips referencing them:
    INSERT INTO dbo.Driver (full_name, city) VALUES ('Amir Khan', 'Austin');
    INSERT INTO dbo.Trip (driver_id, distance_km, fare_usd) VALUES (1, 8.0, 18.00);
    DELETE FROM dbo.Driver WHERE driver_id = 1;
    -- Msg 547: The DELETE statement conflicted with the REFERENCE constraint ...

    This is the entire point of a foreign key: SQL Server enforces that you can’t log a trip for a driver who doesn’t exist, and can’t delete a driver out from under existing trips — it rejects the operation outright, rather than silently leaving a trip pointing at nothing (an “orphaned row”). Without this constraint, that kind of data corruption is entirely possible and often goes unnoticed until a report breaks months later.

    Three Relationship Shapes

    Shape Example How it’s modeled
    One-to-many (1:N) One driver, many trips A foreign key on the “many” side, as shown above
    Many-to-many (N:N) Many students enroll in many courses A junction table in between, holding two foreign keys — e.g. Student ↔ Course via Enrollment
    One-to-one (1:1) Employee ↔ EmployeeConfidentialDetails A foreign key with a UNIQUE constraint added — rare, often used to split sensitive columns into a separately-secured table

    A Many-to-Many Example, Concretely

    CREATE TABLE dbo.Student (student_id INT IDENTITY PRIMARY KEY, name NVARCHAR(100) NOT NULL);
    CREATE TABLE dbo.Course (course_id INT IDENTITY PRIMARY KEY, title NVARCHAR(100) NOT NULL);
    
    -- The junction table: one row per student-course PAIR, with a composite primary key
    CREATE TABLE dbo.Enrollment (
        student_id INT NOT NULL REFERENCES dbo.Student(student_id),
        course_id  INT NOT NULL REFERENCES dbo.Course(course_id),
        enrolled_on DATE NOT NULL DEFAULT GETDATE(),
        PRIMARY KEY (student_id, course_id)
    );

    Neither Student nor Course has a foreign key pointing directly at the other — they can’t, since either side could relate to many rows on the other. The junction table’s composite primary key (both columns together) also does double duty: it prevents the same student from enrolling in the same course twice.

    Practice tip: Sketch the junction table for a “many books can have many authors, many authors can write many books” relationship before the Chapter 7 capstone — you’ll build exactly this schema there for real.

    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server Fundamentals, coming soon on this site.