Tag: Queries

  • SQL Server Pagination: OFFSET-FETCH and Multi-Column Sorting Explained

    SQL Server Pagination: OFFSET-FETCH and Multi-Column Sorting Explained

    Real apps rarely show every row at once. Here’s how to sort by more than one column, split results into pages the correct way, and why a well-meaning shortcut (paginating without ORDER BY) causes bugs that only show up in production under load.

    Paging Through Results(skip, take, repeat — the right way)1. ORDER BYsort first — city, rating DESC2. OFFSETskip the rows already shown3. FETCH NEXTgrab just this page’s rowsthe trap: duplicate values split unpredictably across pagesPROBLEMNonna’s Table · 4.7Sakura Grill · 4.7→ page split is arbitraryFIXORDER BY rating DESC,restaurant_id ASCstable across every pageOFFSET-FETCH needs ORDER BY —T-SQL throws a syntax error withoutit. No accidental unsorted paging.

    Multi-Column Sort

    SELECT name, city, rating FROM dbo.Restaurant
    ORDER BY city ASC, rating DESC;

    City sorts alphabetically first; within each city, restaurants sort by rating, highest first. Each column can independently be ASC (default) or DESC — read it left to right as “sort by this, and within ties, sort by this next.”

    -- Sorting by an expression, not just a raw column, works too:
    SELECT name, rating, price_range FROM dbo.Restaurant
    ORDER BY rating / price_range DESC; -- crude "value for money" ranking

    You can also sort by column position (ORDER BY 3 DESC) — it works, but avoid it in real code; a column reordering elsewhere in the query silently changes what you sort by, with no warning.

    OFFSET-FETCH: The Standard Way to Paginate

    SELECT name, rating FROM dbo.Restaurant
    ORDER BY rating DESC
    OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY;    -- page 1 (rows 1-3)
    
    SELECT name, rating FROM dbo.Restaurant
    ORDER BY rating DESC
    OFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY;    -- page 2 (rows 4-6)

    The general formula for “page N with a page size of S” is OFFSET (N-1) * S ROWS FETCH NEXT S ROWS ONLY — this is exactly the calculation a web application’s backend does every time you click “next page” on a paginated table.

    Why ORDER BY Is Non-Negotiable Here

    OFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY; — no ORDER BY = undefined which rows ORDER BY rating DESC OFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY; — predictable

    Without a defined sort order, SQL Server makes no guarantee about row order — “skip 3, take 3” is meaningless if the underlying order can silently shift between calls. OFFSET-FETCH is actually a syntax extension of ORDER BY itself in T-SQL — you cannot write it without an ORDER BY clause at all; SQL Server will raise a syntax error, which is the engine protecting you from this exact bug.

    A Subtler Pagination Bug: Ties

    -- Two restaurants both rated 4.7 could land on either side of a page boundary
    -- unpredictably, if rating is the ONLY sort column and duplicates exist.
    SELECT name, rating FROM dbo.Restaurant
    ORDER BY rating DESC
    OFFSET 0 ROWS FETCH NEXT 2 ROWS ONLY;
    
    -- Fix: add a tie-breaker column that's guaranteed unique, like the primary key
    SELECT name, rating FROM dbo.Restaurant
    ORDER BY rating DESC, restaurant_id ASC
    OFFSET 0 ROWS FETCH NEXT 2 ROWS ONLY;
    Practice tip: Any time you paginate on a column that might have duplicate values (a rating, a status, a category), add the primary key as a final tie-breaker in ORDER BY. Without it, the exact same row can appear on two different pages, or vanish between pages, as data changes underneath a multi-page scroll — a real bug that’s genuinely hard to reproduce without knowing to look for it.

    Key Takeaways

    • List multiple ORDER BY columns to sort within a sort — first column is primary, rest break ties
    • OFFSET-FETCH is the standard SQL Server pagination pattern: skip N rows, take the next M — and T-SQL enforces that it can’t be used without ORDER BY
    • Add a unique tie-breaker column (usually the primary key) to ORDER BY whenever the sort column can have duplicate values, to keep pagination stable

    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.

  • SELECT, WHERE, ORDER BY: Writing Your First Real SQL Server Queries

    SELECT, WHERE, ORDER BY: Writing Your First Real SQL Server Queries

    This is the query you’ll write more than any other, in some form, for the rest of your career. Getting a genuinely solid mental model here — not just “it works” but why it works in the order it does — pays off in every later chapter.

    Anatomy of a SELECT(you type it one way, SQL runs it another)FROMgrab the tableWHEREkeep matching rowsSELECTpick the columnsORDER BYsort what’s leftpredictablebut you TYPED select first — SQL actually runs FROM firstSELECT * is fine forexploring — never inreal app code. It breakssilently when columns change.TOP N without ORDER BY = arbitrary rows —never “the first N you inserted.”

    Setup — run this once to follow along:

    CREATE TABLE dbo.Restaurant (
        restaurant_id INT IDENTITY(1,1) PRIMARY KEY,
        name          NVARCHAR(100) NOT NULL,
        cuisine       NVARCHAR(50)  NOT NULL,
        city          NVARCHAR(50)  NOT NULL,
        rating        DECIMAL(2,1)  NOT NULL,
        price_range   TINYINT       NOT NULL
    );
    INSERT INTO dbo.Restaurant (name, cuisine, city, rating, price_range) VALUES
    ('Spice Route', 'Indian', 'Austin', 4.5, 2),
    ('Nonna''s Table', 'Italian', 'Austin', 4.7, 3),
    ('Sakura Grill', 'Japanese', 'Austin', 4.8, 3),
    ('Taco Libre', 'Mexican', 'Dallas', 4.3, 1);

    SELECT: Choose Your Columns

    SELECT name, cuisine, rating FROM dbo.Restaurant;
    
    -- SELECT * grabs every column — fine for exploring, avoid it in real application code
    SELECT * FROM dbo.Restaurant;
    
    -- Aliasing a column for a cleaner result header
    SELECT name AS restaurant_name, rating AS star_rating FROM dbo.Restaurant;
    Why avoid SELECT * in real code: it silently breaks if someone adds or reorders columns later, pulls more data over the network than you need, and (once you reach indexing in the advanced course) can prevent the optimizer from using an efficient covering index. Naming columns explicitly costs nothing and avoids all three problems.

    WHERE: Filter the Rows

    SELECT name, rating FROM dbo.Restaurant WHERE city = 'Austin';

    WHERE is evaluated once per row, against the raw table data — it runs before SELECT decides which columns to keep, which is why you can filter on a column you don’t even include in the output.

    ORDER BY, DISTINCT, and TOP

    SELECT name, rating FROM dbo.Restaurant ORDER BY rating DESC;
    SELECT DISTINCT cuisine FROM dbo.Restaurant;
    SELECT TOP 3 name, rating FROM dbo.Restaurant ORDER BY rating DESC;
    SELECT TOP 25 PERCENT name FROM dbo.Restaurant ORDER BY rating DESC;

    TOP without an ORDER BY gives you an arbitrary N rows in whatever order the engine happens to read them — not “the first N you inserted,” and not stable across runs. Always pair TOP with ORDER BY when you mean “the highest/lowest N,” which is almost always what you actually want.

    The Order SQL Server Actually Processes Your Query

    FROM WHERE SELECT ORDER BY

    Even though you type SELECT first, SQL Server processes FROM → WHERE → SELECT → ORDER BY (this is called logical query processing order, and it’s the single most useful mental model for debugging “why doesn’t this work” moments for the rest of this course). That’s why a column alias defined in SELECT can’t be reused in that same query’s WHERE clause — WHERE runs before SELECT even exists:

    -- This fails:
    SELECT rating * 2 AS double_rating FROM dbo.Restaurant WHERE double_rating > 8;
    -- Msg 207: Invalid column name 'double_rating'
    
    -- Because WHERE runs before the alias exists, repeat the expression instead:
    SELECT rating * 2 AS double_rating FROM dbo.Restaurant WHERE rating * 2 > 8;

    ORDER BY, on the other hand, runs last — after SELECT — which is exactly why it’s the one clause that can reference a column alias.

    Comments and Readability

    -- single-line comment
    /* multi-line
       comment block */
    SELECT name, rating -- inline comment on the same line
    FROM dbo.Restaurant
    WHERE city = 'Austin'; -- Austin locations only

    Key Takeaways

    • SELECT picks columns; WHERE filters rows; ORDER BY sorts the result
    • DISTINCT removes duplicate rows; TOP limits row count — always pair TOP with ORDER BY, or “top” is meaningless
    • Logical execution order (FROM → WHERE → SELECT → ORDER BY) explains several “why doesn’t this work” surprises, including why WHERE can’t see a SELECT alias but ORDER BY can
    • Avoid SELECT * outside of quick exploration — name your columns explicitly
    Practice tip: Add one more restaurant row of your own, then write three queries against this table from memory before moving to the next lesson: one filtered by cuisine, one sorted by price_range ascending, one using TOP to get the single highest-rated restaurant.

    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.

  • LIKE, IN, BETWEEN: Advanced Filtering Patterns in SQL Server

    LIKE, IN, BETWEEN: Advanced Filtering Patterns in SQL Server

    Basic equality filters only get you so far. Here’s the rest of the filtering toolkit — wildcard patterns, membership tests, ranges, and combining them all with AND/OR/NOT correctly — plus a NULL-comparison bug that costs beginners hours of confused debugging because it never throws an error.

    The Filtering Toolkit(three ways into the same WHERE)LIKE ‘%pat%’wildcard pattern matchIN (a, b, c)shorthand for many ORsBETWEEN x AND yinclusive on both endsWHEREAND binds tighter than OR — always parenthesize when you mix them.x = NULL is alwaysfalse — zero rows, noerror. Use IS NULL.

    Comparison and Logical Operators

    SELECT name FROM dbo.Restaurant WHERE rating >= 4.5;
    SELECT name FROM dbo.Restaurant WHERE city = 'Austin' AND rating > 4.0;
    SELECT name FROM dbo.Restaurant WHERE cuisine = 'Indian' OR cuisine = 'Japanese';
    SELECT name FROM dbo.Restaurant WHERE NOT (city = 'Dallas');

    Operator Precedence: A Real Beginner Trap

    -- Looks like "Austin restaurants, plus anything highly rated" — but AND binds tighter than OR:
    SELECT name FROM dbo.Restaurant
    WHERE city = 'Austin' OR cuisine = 'Italian' AND rating > 4.5;
    -- Actually means: city = 'Austin' OR (cuisine = 'Italian' AND rating > 4.5)
    
    -- Use explicit parentheses to say what you actually mean:
    SELECT name FROM dbo.Restaurant
    WHERE (city = 'Austin' OR cuisine = 'Italian') AND rating > 4.5;
    Common mistake: Mixing AND and OR without parentheses. SQL follows standard operator precedence (AND binds tighter than OR, same as multiplication binds tighter than addition in arithmetic) — when in doubt, add parentheses. They cost nothing and remove all ambiguity, for you and for anyone reading your query later.

    IN: A Cleaner Way to Write Multiple ORs

    SELECT name FROM dbo.Restaurant
    WHERE cuisine IN ('Indian', 'Japanese', 'Italian');
    
    -- NOT IN excludes a list — but see the NULL warning below before relying on this
    SELECT name FROM dbo.Restaurant
    WHERE cuisine NOT IN ('Mexican');

    BETWEEN: Inclusive on Both Ends

    SELECT name, rating FROM dbo.Restaurant
    WHERE rating BETWEEN 4.0 AND 4.5;
    -- Equivalent to: rating >= 4.0 AND rating <= 4.5 — both endpoints are included

    LIKE: Pattern Matching

    Wildcard Matches
    % Any sequence of characters (zero or more)
    _ Exactly one character
    [abc] Any single character in the set
    [^abc] Any single character NOT in the set
    SELECT name FROM dbo.Restaurant WHERE name LIKE 'T%';        -- starts with T
    SELECT name FROM dbo.Restaurant WHERE name LIKE '%Grill%';    -- contains "Grill" anywhere
    SELECT name FROM dbo.Restaurant WHERE name LIKE '_a%';        -- 2nd letter is 'a'
    SELECT name FROM dbo.Restaurant WHERE name LIKE '[ST]%';      -- starts with S or T
    Performance preview: A pattern starting with % (like '%Grill%' or '%Grill') can't use a standard index efficiently — SQL Server has to scan every row, because it can't know in advance where a match might start. A pattern anchored at the start ('Grill%') can. You'll see exactly why once you reach indexing in the advanced course — for now, just know that "starts with" is cheap and "contains" is comparatively expensive at scale.

    The NULL Trap

    WHERE price_range = NULL  →  always returns ZERO rows, no error WHERE price_range IS NULL  →  correct

    NULL means "unknown," and SQL uses three-valued logic — unknown = unknown evaluates to unknown, not true. WHERE column = NULL silently returns zero rows every single time, with no error to tip you off. Always use IS NULL / IS NOT NULL instead.

    NULL's Sequel: The NOT IN Trap

    -- If ANY value in the list (or subquery result) is NULL, the ENTIRE NOT IN silently
    -- matches zero rows — not an error, just wrong results:
    SELECT name FROM dbo.Restaurant
    WHERE cuisine NOT IN (SELECT cuisine FROM dbo.Restaurant WHERE cuisine IS NULL OR city = 'Dallas');
    -- If that subquery returns even one NULL alongside real values, this returns nothing at all.
    
    -- Safer: explicitly exclude NULLs from the list first, or use NOT EXISTS instead (Chapter 5)
    SELECT name FROM dbo.Restaurant r
    WHERE NOT EXISTS (
        SELECT 1 FROM dbo.Restaurant r2 WHERE r2.city = 'Dallas' AND r2.cuisine = r.cuisine
    );
    Practice tip: This NOT IN + NULL interaction is a genuinely famous SQL gotcha — it has caused real production bugs at companies with otherwise experienced teams. You'll meet NOT EXISTS properly once subqueries are covered in Chapter 5; for now, just remember that NOT IN and NULL don't mix safely.

    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.