Author: admin

  • SQL Server Incident Report Template: How to Document a Performance Post-Mortem

    SQL Server Incident Report Template: How to Document a Performance Post-Mortem

    A fix without a written record teaches nothing to the next person who hits a similar symptom. This template mirrors the Evidence-First workflow directly, so writing the report is almost free once you’ve actually done the diagnosis.

    A post-mortem is just the workflow, written down(the report is free once you’ve done the work)BASELINEstep 2EVIDENCEstep 4ROOT CAUSEstep 5FIXstep 6VALIDATEstep 71. Summary β€” plain language, written for someone who wasn’t there3. Timeline β€” timestamped, start to finish8. Prevention β€” would monitoring (Module 6) have caught this sooner?A fix with no written record teaches nothing to thenext person who hits a similar symptom. πŸ“Œ

    1. Summary

    One paragraph: what broke, for how long, and who/what was affected. Written for someone who wasn’t in the room.

    2. Baseline

    What did normal look like, with numbers? (duration, CPU, I/O, or whatever metric defines “working” for this system)

    3. Timeline

    Timestamped sequence: when was it first noticed, when did diagnosis start, when was the fix applied, when was it confirmed resolved.

    4. Evidence Captured

    Exactly what was captured and how (Extended Events session definition, DMV queries run, execution plan attached). Paste the actual queries β€” future-you will thank present-you.

    5. Root Cause

    The specific, technical cause β€” not “the database was slow,” but “missing nonclustered index on OrderLog.customer_id causing a clustered index scan on a 400M-row table.”

    6. Fix Applied

    The exact change made, plus why this specific fix (not a bigger rewrite, not a hardware upgrade) was the right scope.

    7. Validation

    Post-fix measurement against the Step 2 baseline, with numbers.

    8. Prevention

    Would monitoring (Module 6) have caught this earlier? Is this a pattern worth a standing alert or a schema review checklist item?

    Why the Structure Matters

    Following this template forces you to separate what happened from what you did about it from whether it actually worked β€” exactly the discipline the Evidence-First workflow teaches, just written down for the next person instead of held in your head.


    Enjoyed this?

    Subscribe to get every new 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 and hands-on labs? Check out SQL Server Performance Tuning, coming soon on this site.

  • Memory-Optimized TempDB Metadata and Azure SQL Performance Specifics

    Memory-Optimized TempDB Metadata and Azure SQL Performance Specifics

    Two focused topics to close out the course: a direct engine-level fix for Module 5’s tempdb contention, and what genuinely changes when your target isn’t on-prem SQL Server anymore.

    Two ways the ground shifts under you(an engine fix, and a platform)TEMPDB METADATAbefore: PAGELATCH_EX pileup evenon the SYSTEM CATALOG pagesMEMORY_OPTIMIZEDTEMPDB_METADATA = ONlock & latch-free β€” even the catalog βœ“+ON AZURE SQL DATABASExProfiler β€” gone, use XEventsxResource Governor β€” not availablextempdb files β€” managed for youEvidence-First workflow β€” identicalNote: Azure SQL Managed Instance sits in between β€”more on-prem surface than Azure SQL Database, stillless than a true on-prem or IaaS VM install. πŸ“Œ

    Memory-Optimized TempDB Metadata: The Modern Fix

    ALTER SERVER CONFIGURATION SET MEMORY_OPTIMIZED TEMPDB_METADATA = ON;
    -- Requires a service restart to take effect

    Rather than just spreading GAM/SGAM/PFS contention across multiple files (Module 5’s fix), this moves tempdb’s system tables and metadata structures themselves into lock/latch-free memory-optimized structures β€” directly eliminating the contention at its source rather than diluting it. Available since SQL Server 2019; genuinely the better long-term answer where supported.

    What Actually Changes on Azure SQL Database

    Concept from this course Azure SQL Database difference
    Buffer pool / memory (Module 1) Sized by your chosen service tier/vCore, not physical server RAM you control directly
    SQL Server Profiler Not available β€” Extended Events (Module 6) is the only option, which is exactly why this course taught XEvents thoroughly
    Resource Governor Not available on single/elastic pool databases β€” workload isolation instead comes from service tier/DTU-vCore choice itself
    tempdb file configuration Managed automatically β€” you don’t configure files directly

    Azure SQL Managed Instance sits in between: it supports far more of the on-prem surface (including SQL Agent, cross-database queries, and more configuration control) than Azure SQL Database, but still abstracts some infrastructure-level tuning compared to a true on-prem or IaaS VM install.

    The Takeaway for This Whole Course

    Every diagnostic principle β€” Evidence-First, SARGability, execution plans, DMVs, Query Store β€” applies identically on Azure. What changes is which infrastructure-level levers (Resource Governor, tempdb files, Profiler) are available to you versus abstracted away by the platform.


    Enjoyed this?

    Subscribe to get every new 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 and hands-on labs? Check out SQL Server Performance Tuning, coming soon on this site.

  • Safely Testing SQL Server Performance Fixes: Capturing and Replaying Production Workloads

    Safely Testing SQL Server Performance Fixes: Capturing and Replaying Production Workloads

    Every fix in this course has been validated by re-running “a” query. In real production incidents, you need to validate against the real, full mixed workload before deploying β€” a synthetic single-query test can miss regressions elsewhere.

    Capture there, replay here β€” never mix them(the whole point is safety)NEVERPRODUCTIONreal data, real trafficnever touch directlycapture via XEventsCAPTURED TRACE.xel workload filepeak-hour windowrestore + replayISOLATED TEST COPY+ candidate fix applied(new index, forced plan…)compare vs baseline replayCompare aggregate duration & CPU against the pre-fixbaseline replay β€” not just one query in isolation.Restore a recent backup to a genuinely isolated copy first.The whole safety guarantee depends on that boundary. πŸ“Œ

    Capturing a Real Workload

    -- Capture via an Extended Events session (modern approach, replaces old SQL Trace .trc capture)
    CREATE EVENT SESSION WorkloadCapture ON SERVER
    ADD EVENT sqlserver.rpc_completed, ADD EVENT sqlserver.sql_batch_completed
    ADD TARGET package0.event_file (SET filename = N'WorkloadCapture')
    WITH (MAX_DISPATCH_LATENCY = 5 SECONDS);
    GO
    ALTER EVENT SESSION WorkloadCapture ON SERVER STATE = START;
    -- Let it run for a representative window (e.g. peak business hours), then STOP

    Replaying It Against an Isolated Copy

    Production Captured Trace Isolated Test Copy(WITH your candidate fix applied)

    Restore a recent production backup to a genuinely isolated environment β€” never replay a captured production workload against anything that could touch real production data (it contains real inserts/updates/deletes). Apply your candidate fix (new index, forced plan, Resource Governor config) there, then replay the captured trace and compare aggregate duration/CPU against the pre-fix baseline replay.

    Why This Beats Testing One Query in Isolation

    A new index that speeds up the one query you were chasing can slow down a dozen other write-heavy statements in the real mixed workload β€” something a single-query test will never reveal. Full workload replay is how you catch that before it reaches production, not after.

    The Safety Rule

    Never test against production. Never replay a real captured workload containing real data changes against anything other than a genuinely isolated copy β€” the whole point is safety, and that guarantee only holds if the target environment can’t affect anything real.


    Enjoyed this?

    Subscribe to get every new 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 and hands-on labs? Check out SQL Server Performance Tuning, coming soon on this site.

  • SQL Server Performance Myths, Debunked: What Actually Deserves a Second Look

    SQL Server Performance Myths, Debunked: What Actually Deserves a Second Look

    Every one of these has been mentioned somewhere in this course already β€” collected here in one place as a final reference.

    Folklore, stamped(seven myths, one root cause)Rebuild ALL indexesweekly, no matter whatBUSTEDMore RAM always fixesa slow queryBUSTEDHigh cost % is alwaysthe real bottleneckBUSTEDAdding an index canonly ever helpBUSTEDPAGELATCH = LCK,same fix appliesBUSTEDNOLOCK on everythingis a free performance winBUSTEDThe root cause, every single time:skipping baseline + evidence, reaching for a remembered rule of thumb instead

    Myth: “Always rebuild all indexes weekly, regardless of fragmentation.”

    Truth:

    Measure fragmentation first (sys.dm_db_index_physical_stats). Below 5%, do nothing. A tiny, rarely-scanned table at 60% fragmentation often doesn’t matter at all. Blind scheduled rebuilds waste CPU/IO on indexes that never needed it.

    Myth: “More RAM always fixes a slow query.”

    Truth:

    More RAM only helps if the bottleneck is actually buffer pool pressure (pages being evicted and re-read from disk). It does nothing for a CPU-bound query, a lock-bound query, or a query with a genuinely bad plan from a missing index.

    Myth: “A high cost % operator in the execution plan is always the real bottleneck.”

    Truth:

    Cost % is computed from row estimates, which can be badly wrong under parameter sniffing or stale statistics. Cross-check against actual row counts β€” the Estimated-vs-Actual gap is often more informative than cost % alone.

    Myth: “Adding an index can only help, never hurt.”

    Truth:

    Every index adds write-maintenance cost to every INSERT/UPDATE/DELETE that touches it. An index that helps a rarely-run report but slows down a high-throughput write path is a net loss β€” always weigh read benefit against write cost.

    Myth: “PAGELATCH waits are the same problem as lock (LCK) waits, and the same fix (shorten transactions) applies.”

    Truth:

    Latches protect physical memory structures; locks protect logical transaction consistency. Applying lock-blocking fixes to a latch contention problem (like tempdb allocation contention) solves nothing β€” the fix is structurally different (e.g. more tempdb files, not shorter transactions).

    Myth: “WITH (NOLOCK) on every query is a safe, free performance win.”

    Truth:

    NOLOCK (READ UNCOMMITTED) permits dirty reads β€” data that may later be rolled back, or in rare cases, skipped/duplicated rows during concurrent page splits. It’s a genuine tool for tolerant reporting scenarios, not a default habit to apply blindly everywhere.

    Myth: “If a query is slow, the database server needs more powerful hardware.”

    Truth:

    The 13-Hour Delete case study earlier in this course was fixed with one targeted index, not a hardware upgrade. Diagnose with evidence first β€” hardware is sometimes genuinely the answer, but it’s usually the most expensive way to mask an unindexed query.

    The One Rule Underlying All of These

    Every single myth above traces back to skipping the Evidence-First workflow’s first two steps β€” baseline and capture evidence β€” and jumping straight to a remembered rule of thumb instead. That’s not a coincidence; it’s the whole thesis of this course, stated one more time on the way out.

    Every myth on this page shares the same root cause: a plausible-sounding rule applied without measurement. The Evidence-First workflow from the start of this course exists specifically to replace folklore with verified cause and effect.


    Enjoyed this?

    Subscribe to get every new 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 and hands-on labs? Check out SQL Server Performance Tuning, coming soon on this site.

  • Top SQL Server Interview Questions and Answers, by Topic

    Top SQL Server Interview Questions and Answers, by Topic

    Not new material β€” every answer below traces back to a specific earlier lesson in this course, cited so you can go re-derive the full reasoning if a follow-up question digs deeper than the one-liner. A reference bank organized the way it actually gets asked in an interview room.

    Interview Q β†’ A, by topic(the reasoning behind the one-liner)Q: DELETE vs TRUNCATEvs DROP?(the classic opener)flip β†’A: logged row deletes,page deallocation,or structure gone entirely β€”each fires (or skips) triggers differentlyevery card is tagged back to its chapter:FundamentalsJoins & SetsFunctions & ProcsPerformanceTransactions & Securityevery answer traces back toa chapter β€” go re-derive itsay WHY, not just WHAT β€”interviewers probe pastthe one-line definition πŸ“Œ

    Fundamentals

    Q: What’s the difference between DELETE, TRUNCATE, and DROP?
    A: DELETE removes rows (optionally filtered with WHERE), is logged row-by-row, fires DELETE triggers, and can be rolled back mid-transaction. TRUNCATE removes all rows, deallocates pages directly, resets IDENTITY, doesn’t fire triggers, and can’t be filtered. DROP removes the entire table structure and data permanently. (Fundamentals Ch.2)
    Q: What’s the difference between WHERE and HAVING?
    A: WHERE filters rows before grouping; HAVING filters groups after GROUP BY β€” HAVING can reference aggregates, WHERE cannot, because at the point WHERE runs, no aggregate has been computed yet. (Fundamentals Ch.4)
    Q: Why does WHERE column = NULL always return zero rows?
    A: SQL uses three-valued logic β€” NULL means “unknown,” and unknown = unknown evaluates to unknown, not true. Use IS NULL instead. (Fundamentals Ch.3)

    Joins & Sets

    Q: When does a LEFT JOIN silently behave like an INNER JOIN?
    A: When a filter on the right table’s column sits in WHERE instead of ON β€” NULL fails most WHERE comparisons, discarding the unmatched left rows LEFT JOIN was meant to preserve. (Fundamentals Ch.5)
    Q: What’s the difference between UNION and UNION ALL?
    A: UNION removes duplicate rows across the combined result (a real cost); UNION ALL keeps every row including duplicates and is faster since it skips the dedup pass. (Fundamentals Ch.5)

    Functions & Procedures

    Q: When would you choose a stored procedure over a function?
    A: When you need to modify data, manage explicit transactions, use TRY/CATCH, or return multiple result sets β€” none of which a function can do, by design (attempting DML inside a function throws “invalid use of a side-effecting operator”). (Ch.2, Ch.4)
    Q: What’s the difference between a temp table and a table variable?
    A: The behavioral difference that actually matters: a table variable’s contents survive a transaction ROLLBACK; a temp table’s contents are rolled back with the transaction. Table variables also historically carry weaker optimizer statistics. (Ch.3)

    Performance

    Q: What’s the difference between a clustered and nonclustered index?
    A: A clustered index’s leaf level IS the actual data, physically ordered by the key β€” at most one per table. A nonclustered index’s leaf holds the key plus a pointer back to the clustered index, requiring a key lookup for any additional columns not covered. (Ch.8)
    Q: How would you diagnose a slow query in production?
    A: Check sys.dm_exec_query_stats for cost, capture the actual execution plan, look for Table Scans/Key Lookups and Estimated-vs-Actual gaps, confirm with STATISTICS IO, then design a targeted (ideally covering) index and re-measure. (Ch.8)
    Q: What is parameter sniffing?
    A: A stored procedure’s execution plan is compiled once and cached based on the first parameter value seen; that plan gets reused for every later call regardless of whether the shape of the data matches, sometimes producing a fast plan for one caller and a terrible one for another. (Ch.4)

    Transactions & Security

    Q: What causes a deadlock, and how do you prevent one?
    A: Two transactions each holding a lock the other needs, in a circular wait. Prevent by always acquiring locks on shared resources in the same order across the entire application β€” a code-level fix, not a database configuration one. (Ch.9)
    Q: What’s the difference between TDE and Always Encrypted?
    A: TDE protects data at rest (stolen files/backups) β€” an authorized query still sees plaintext. Always Encrypted keeps the server from ever seeing plaintext at all; decryption happens client-side, protecting the data even from a DBA with full query access. (Ch.10)
    Q: Why shouldn’t an application’s service account be db_owner?
    A: Least privilege β€” a compromised connection with db_owner can drop every table and read every row; the same compromise with a narrowly-scoped role can only do what that role explicitly permits. (Ch.10)

    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 for Developers & DBAs, coming soon on this site.

  • DML Triggers in SQL Server: AFTER vs INSTEAD OF, and the inserted/deleted Tables

    DML Triggers in SQL Server: AFTER vs INSTEAD OF, and the inserted/deleted Tables

    Chapter 5 asked “where do I enforce a business rule” and pointed to triggers whenever logic needs to reach across rows or tables β€” something CHECK constraints structurally can’t do. This is that tool, in full. A trigger is code that runs automatically in response to an INSERT/UPDATE/DELETE. Getting the two DML trigger types right β€” and understanding their special tables β€” matters a lot in production, because a trigger bug affects every write to the table, silently.

    AFTER vs INSTEAD OF(same event, two very different timings)UPDATE Account SET balance=…AFTER triggerrows are alreadychanged by the timethis code runsextra logic (audit, etc)DELETE FROM Account WHERE…INSTEAD OF triggeroriginal DELETE isREPLACED entirely β€”never happens unlessthe trigger body does itinserteddeletedboth special tables, populated per STATEMENTGotcha: triggers fire ONCE per statement, not once per row β€”always JOIN to inserted/deleted, never grab one scalar row. πŸ“Œ

    AFTER Trigger: Audit Logging

    CREATE TRIGGER trg_Account_AuditBalance
    ON dbo.Account
    AFTER UPDATE
    AS
    BEGIN
        SET NOCOUNT ON;
        IF UPDATE(balance)
        BEGIN
            INSERT INTO dbo.AccountAudit (account_id, old_balance, new_balance)
            SELECT i.account_id, d.balance, i.balance
            FROM inserted i
            INNER JOIN deleted d ON d.account_id = i.account_id
            WHERE i.balance <> d.balance;
        END
    END;

    UPDATE(balance) is a trigger-specific function that returns true only if the balance column was included in the UPDATE’s SET list β€” a cheap early-exit that avoids doing audit work on updates that never touched the column you actually care about.

    The Two Special Tables

    inserted New values (INSERT & UPDATE) deleted Old values (UPDATE & DELETE)

    On an UPDATE, both are populated simultaneously β€” exactly how the audit trigger above compares old vs. new balance by joining them together on the primary key. On a plain INSERT, only inserted has rows; on a plain DELETE, only deleted does.

    The Bug That Bites in Production

    Triggers fire once per statement, operating on the whole batch of affected rows, not once per row. A trigger written assuming only one row was updated (using a scalar variable instead of joining to inserted/deleted) will silently process only one arbitrary row and miss the rest of a multi-row UPDATE β€” with no error, just quietly incomplete auditing.

    -- The exact bug: looks reasonable, is completely wrong for multi-row updates
    CREATE TRIGGER trg_Bad_AuditBalance ON dbo.Account AFTER UPDATE AS
    BEGIN
        DECLARE @id INT, @newBalance DECIMAL(10,2);
        SELECT @id = account_id, @newBalance = balance FROM inserted; -- only grabs ONE row
        INSERT INTO dbo.AccountAudit (account_id, new_balance) VALUES (@id, @newBalance);
    END;
    
    -- Prove the bug: update multiple rows in one statement, check the audit table
    UPDATE dbo.Account SET balance = balance * 1.01 WHERE balance > 0; -- affects many rows
    SELECT COUNT(*) FROM dbo.AccountAudit; -- only 1 row logged, not one per account updated
    Common mistake: Testing a trigger only against single-row UPDATE statements during development, where the scalar-variable bug above produces correct-looking results by coincidence. It fails silently the first time a real batch UPDATE or bulk import touches multiple rows at once β€” always test triggers against multi-row operations before trusting them.

    INSTEAD OF: Replacing the Operation Entirely

    CREATE TRIGGER trg_Account_PreventDeleteIfFunded
    ON dbo.Account
    INSTEAD OF DELETE
    AS
    BEGIN
        SET NOCOUNT ON;
        IF EXISTS (SELECT 1 FROM deleted WHERE balance > 0)
        BEGIN
            RAISERROR('Cannot delete an account with a positive balance.', 16, 1);
            RETURN;
        END
        DELETE FROM dbo.Account WHERE account_id IN (SELECT account_id FROM deleted);
    END;

    INSTEAD OF triggers replace the operation entirely β€” the original INSERT/UPDATE/DELETE never happens unless the trigger body performs it itself. This is the exact mechanism that makes a JOIN-based view (Chapter 6) updatable despite SQL Server’s own restrictions: an INSTEAD OF trigger on the view can manually split the write across the correct base tables, however that logic needs to work.

    Practice tip: Rebuild both example triggers, then deliberately run a multi-row UPDATE against Account and confirm the AFTER trigger logs every changed row correctly. Then try deleting a funded account and confirm the INSTEAD OF trigger blocks it with the custom error message.

    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 for Developers & DBAs, coming soon on this site.

  • DDL and Logon Triggers in SQL Server: Auditing Schema Changes and Login Restrictions

    DDL and Logon Triggers in SQL Server: Auditing Schema Changes and Login Restrictions

    Beyond DML, SQL Server triggers can fire on schema changes (CREATE/ALTER/DROP β€” Fundamentals Chapter 2’s DDL statements) and even login attempts β€” powerful, and in the case of logon triggers, genuinely risky if you get it wrong, in a way DML triggers never are.

    Two More Places Triggers Fire(schema changes, and login attempts)CREATE / ALTER / DROP TABLEDDL triggerEVENTDATA() captureswho + what + whenSchemaChangeLog rowwho dropped what, and whenLOGIN attemptLOGON triggersits BETWEEN you andthe ability to connect β€” a bughere can lock out EVERYONE,admins included ⚠DAC (sqlcmd -A)the one door triggers can’t blockGotcha: test logon triggers in non-prod first, and know yourDAC login path BEFORE ever enabling one in production. πŸ“Œ

    DDL Trigger: Auditing Schema Changes

    CREATE TRIGGER trg_LogSchemaChanges
    ON DATABASE
    FOR CREATE_TABLE, ALTER_TABLE, DROP_TABLE
    AS
    BEGIN
        SET NOCOUNT ON;
        DECLARE @data XML = EVENTDATA();
        INSERT INTO dbo.SchemaChangeLog (event_type, object_name, changed_by)
        VALUES (
            @data.value('(/EVENT_INSTANCE/EventType)[1]', 'NVARCHAR(100)'),
            @data.value('(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(200)'),
            @data.value('(/EVENT_INSTANCE/LoginName)[1]', 'NVARCHAR(100)')
        );
    END;

    EVENTDATA() returns an XML document describing exactly what changed and who changed it β€” a genuinely useful audit trail for compliance-sensitive environments, where “who dropped that table, and when” is a question that needs a real answer, not a guess from backup timestamps.

    -- Trigger it and see the audit row appear
    CREATE TABLE dbo.Scratch_DDLTest (id INT);
    SELECT * FROM dbo.SchemaChangeLog ORDER BY changed_by DESC; -- your CREATE TABLE is logged
    DROP TABLE dbo.Scratch_DDLTest;

    Logon Triggers: Powerful, and Genuinely Dangerous

    A broken logon trigger can lock out EVERY login, including administrators

    A logon trigger fires when a login session is established β€” used for things like restricting logins by time of day or capping concurrent sessions. Unlike every other trigger type in this chapter, a logon trigger sits between you and the ability to connect at all β€” an error in its logic, or a bug that always evaluates to “deny,” locks out every single login attempt, with no normal way back in.

    CREATE TRIGGER trg_RestrictOffHoursLogin ON ALL SERVER WITH EXECUTE AS 'sa' FOR LOGON AS
    BEGIN
        IF DATEPART(HOUR, GETDATE()) NOT BETWEEN 6 AND 22
           AND ORIGINAL_LOGIN() NOT IN ('sa', 'app_admin')
            ROLLBACK; -- rejects the connection
    END;
    Critical safety note: Before deploying any logon trigger, know how to reach the Dedicated Administrator Connection (DAC) β€” a special, separate connection path (sqlcmd -A) that logon triggers cannot block, reserved exactly for this recovery scenario. Test in a non-production environment first, and always keep a DAC-based rollback plan ready before enabling anything that can reject logins.

    Test extremely carefully, typically with that recovery plan via DAC in case something goes wrong β€” this is one of the very few features in this entire course where “just try it and see” is genuinely bad advice.

    Practice tip: Rather than testing a logon trigger live, read through the exact trigger definition above and trace what happens for a login attempt at 3am from a non-admin account, versus one from app_admin at the same hour. Understanding the logic on paper first, before ever enabling it, is the responsible way to work with this specific feature.

    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 for Developers & DBAs, coming soon on this site.

  • How to Read a SQL Server Execution Plan: Seeks, Scans, and Key Lookups

    How to Read a SQL Server Execution Plan: Seeks, Scans, and Key Lookups

    The last two lessons referenced execution plans repeatedly to prove their claims. Here’s how to actually read one systematically β€” the skill that turns “this query feels slow” into “this specific operator is the problem, for this specific reason.”

    Reading an execution plan(right to left, top to bottom)data flows this direction as you read β†’Index Seekcustomer_id = 42β‘  runs first β€” good!Key Lookupjumps back, per row!β‘‘ the expensive partSELECT resultrows you seeβ‘’ you read this lastthe #1 diagnostic signal β€” estimated vs actual rows:Estimated: 12Actual: 45,000huge gap = stale statsor a bad estimate! ⚠cost % is built onthe SAME estimate β€”a 5% operator can bethe real bottleneck πŸ“Œ

    In SSMS, press Ctrl+M (Include Actual Execution Plan) before running a query.

    SET STATISTICS IO ON;
    SET STATISTICS TIME ON;
    
    SELECT customer_id, order_status, order_total
    FROM dbo.OrderLog
    WHERE customer_id = 42;

    The Key Plan Elements

    Plan element What it means
    Index Seek Good — navigated the B-tree directly to matching rows, exactly the root→branch→leaf path from Lesson 1
    Index Scan Read the entire index β€” fine on small tables, a red flag on huge ones for selective queries
    Table Scan No usable index existed at all β€” the engine has no B-tree to navigate
    Key Lookup Jump back to the clustered index per row β€” the exact problem Lesson 2’s covering index (INCLUDE) fixes

    Read a plan right to left, top to bottom β€” the rightmost, deepest operators run first (typically the actual table/index access), feeding data up and left into operators that filter, join, and aggregate it, until the leftmost operator produces the final result.

    The Single Most Useful Diagnostic Signal

    A large gap between Estimated and Actual rows means stale statistics, or a shape defeating good estimation

    The optimizer chooses its plan based on estimated row counts β€” when those estimates are badly wrong, it often picks a suboptimal plan (the wrong join type, an index skipped in favor of a scan, an inappropriate memory grant). STATISTICS IO reports logical reads per table, often a more stable, comparable metric across runs than wall-clock time, since wall-clock time is affected by whatever else the machine happens to be doing at that moment.

    The Cost Percentage Trap

    Common mistake: Treating an operator’s cost percentage (the big bold number SSMS shows under each operator) as a reliable measure of real-world expense. It’s derived from the same potentially-wrong estimates driving the whole plan β€” an operator estimated at 5% of a query’s cost can be the actual bottleneck if its underlying row estimate was badly off. Cross-check cost percentage against actual row counts (visible by hovering over each operator) before trusting it.
    -- A parameter-sniffing-prone query worth trying: run once with a common value,
    -- once with a rare one, and compare estimated vs actual rows on the same plan shape
    SELECT * FROM dbo.OrderLog WHERE order_status = 'completed'; -- common
    SELECT * FROM dbo.OrderLog WHERE order_status = 'cancelled'; -- rare
    -- Different row counts naturally produce different (correct) estimates per query --
    -- this becomes a real problem specifically inside a cached stored procedure plan,
    -- covered in the Performance Tuning course
    Practice tip: Run a query you already know is slow (or deliberately write one against a large table with no useful index), capture its actual execution plan, and walk it right to left identifying every Scan, Seek, and Lookup by name before looking at cost percentages at all. Building that habit β€” identify operators first, judge cost second β€” avoids the trap above.

    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 for Developers & DBAs, coming soon on this site.

  • SQL Server Scenario-Based Interview Questions: Find the 2nd Highest Salary, and More

    SQL Server Scenario-Based Interview Questions: Find the 2nd Highest Salary, and More

    Modern interviews increasingly favor “solve this problem” over “define this term.” Here’s how to actually handle the classics β€” not just the working query, but the reasoning an interviewer is actually listening for.

    The 2nd-highest-salary trap(same data, two different answers)Alex 100kSam 100kJordan 95kPriya 90knaive MAX-WHERE says THIS βœ—DENSE_RANK correctly says THIS βœ“the same tie-handling matters for dedupe:rn=1 β€” Sam, Eng β€” KEEPrn=2 β€” Sam, Eng β€” DELETErn=3 β€” Sam, Eng β€” DELETEties are the whole test β€”ROW_NUMBER breaks them, DENSE_RANK doesn’tadd a 4th row and re-run β€” that’s how youactually prove which version is right πŸ“Œ

    Find the Second-Highest Salary (Correctly, With Ties)

    -- Robust version using DENSE_RANK, correctly handles ties at the top
    WITH Ranked AS (
        SELECT *, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk FROM dbo.Salary
    )
    SELECT * FROM Ranked WHERE rnk = 2;

    The common wrong answer (MAX(salary) WHERE salary < MAX(salary)) often works by luck, but most candidates can’t explain why it breaks down when the top salary is tied across multiple people β€” in that case, the “wrong” version silently returns the third-highest distinct salary, not the second, because two people share first place. DENSE_RANK (Ch.6) makes the tie-handling explicit and correct by definition, not by accident.

    -- Prove the difference yourself: with a tie at the top, these give different answers
    INSERT INTO dbo.Salary (name, salary) VALUES ('Alex', 100000), ('Sam', 100000), ('Priya', 90000);
    SELECT MAX(salary) FROM dbo.Salary WHERE salary < (SELECT MAX(salary) FROM dbo.Salary); -- 90000, correct here by luck
    -- Add a 4th row: ('Jordan', 95000) and re-run β€” now compare against the DENSE_RANK version

    Find Duplicate Rows

    SELECT name, department, COUNT(*) AS occurrences
    FROM dbo.Salary
    GROUP BY name, department
    HAVING COUNT(*) > 1;

    This is Fundamentals Ch.4's GROUP BY + HAVING pattern applied directly β€” "duplicates" is really just "groups with more than one member," the same shape as every other GROUP BY/HAVING question, just with a different threshold.

    Delete Duplicates, Keeping One Copy

    WITH Deduped AS (
        SELECT *, ROW_NUMBER() OVER (PARTITION BY name, department ORDER BY employee_id) AS rn
        FROM dbo.Salary
    )
    DELETE FROM Deduped WHERE rn > 1;

    A genuinely common follow-up to the duplicate-finding question above, and a real test of whether you understand ROW_NUMBER's uniqueness-guarantee well enough to use it for a DELETE, not just a SELECT β€” note this is deleting through the CTE, a pattern worth having ready.

    "How Would You Diagnose a Slow Query?" β€” The Strong Answer Structure

    Confirm where time goes Capture execution plan Check DMVs for waits Test a hypothesis

    Interviewers evaluate the process, not just the final answer β€” narrate your reasoning out loud, in this order (which is precisely the Chapter 8 DMV lesson's toolkit, applied as a live workflow), rather than jumping straight to "add an index." A candidate who says "I'd add an index" with no diagnostic step first reads as guessing; one who walks through sys.dm_exec_query_stats β†’ execution plan β†’ sys.dm_os_wait_stats β†’ a specific, testable fix reads as someone who's actually done this under pressure before.

    Practice tip: Pick any two questions from this lesson and Lesson 1 combined, and answer them out loud, to another person or recorded, within 90 seconds each β€” the real interview constraint isn't knowing the answer, it's producing a clear, well-structured explanation of it under mild time pressure. That's a different skill from recognizing the right answer on a page, and it's worth practicing separately.

    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 for Developers & DBAs, coming soon on this site.

  • Diagnosing Blocking in SQL Server: Finding Who’s Blocking Whom

    Diagnosing Blocking in SQL Server: Finding Who’s Blocking Whom

    A deadlock (previous lesson) is SQL Server actively resolving an impossible situation by killing one transaction. Ordinary blocking is different and far more common: one session simply waiting its turn for a lock another session holds, which resolves on its own once the first session finishes β€” no error, no victim, just a delay. Not every case of one session waiting on another is a bug; moderate, short-lived blocking is normal under real concurrent load. Here’s how to tell when it’s actually a problem.

    Who’s blocking whom?(one session waiting on another’s lock)Session 61BLOCKED β€” waitingwait_type: LCK_M_Ublocking_session_id = 55Session 55HOLDS the lockstill running…the usual root cause β€” a transaction held open too long:BEGIN TRAN…calls a payment API, waits on network…COMMITlocks held this whole time β€”everyone else just queues up 😩short blocking under loadis normal β€” the fix is aSHORTER window, not more RAM πŸ“Œ

    Finding the Blocker

    SELECT
        blocking.session_id AS blocking_session,
        blocked.session_id AS blocked_session,
        blocked.wait_type,
        blocked.wait_time,
        blocked_text.text AS blocked_query
    FROM sys.dm_exec_requests blocked
    JOIN sys.dm_exec_sessions blocking ON blocking.session_id = blocked.blocking_session_id
    CROSS APPLY sys.dm_exec_sql_text(blocked.sql_handle) blocked_text
    WHERE blocked.blocking_session_id <> 0;

    This is the same blocking_session_id column flagged as the single most actionable field in Chapter 8’s DMV lesson β€” this query is that pointer, fully realized into a real diagnostic report.

    The Real Root Cause, Most of the Time

    A transaction held open far longer than necessary (e.g. waiting on user input mid-transaction)

    It becomes a genuine problem when a transaction holds locks far longer than necessary. The fix is almost always “keep transactions as short as possible” β€” not “add more indexes” or “increase timeout,” which just makes users wait longer for a symptom instead of fixing the actual cause.

    -- The specific anti-pattern that causes most real-world blocking incidents:
    BEGIN TRANSACTION;
    UPDATE dbo.Order SET status = 'processing' WHERE order_id = 500;
    -- ...application code here calls an external API, waits on a user click,
    -- or does anything else slow, all while the transaction (and its locks) stays open...
    COMMIT TRANSACTION; -- doesn't happen until that slow thing finishes
    
    -- The fix: do all slow, non-database work BEFORE or AFTER the transaction,
    -- never DURING it. Keep the window between BEGIN and COMMIT as short as possible.
    Common mistake: Opening a transaction, then calling out to an external service (payment gateway, email API, another microservice) before committing. Any latency or hang in that external call directly extends how long your locks are held, potentially blocking every other session that needs the same rows β€” this single anti-pattern is behind a large share of real production blocking incidents.

    Chapter 9, End to End

    These three lessons form one continuous story: ACID (Lesson 1) is the guarantee; isolation levels (Lesson 2) are the tunable dial controlling how strictly “Isolation” is enforced, with deadlocks as the sharp edge of getting concurrent access patterns wrong; and blocking (this lesson) is the everyday, non-error version of the same underlying mechanism β€” locks doing their job, just visible when they hold longer than expected.

    Practice tip: Open a transaction, run an UPDATE, and deliberately leave it open (don’t COMMIT or ROLLBACK yet) in one query window. In a second window, run the blocking-detection query above and confirm you can see your own first session listed as the blocker. This hands-on confirmation is exactly the diagnostic workflow you’d use on a real, unfamiliar production incident.

    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 for Developers & DBAs, coming soon on this site.