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.
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.
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.
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.
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.
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
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.
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.
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.
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 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
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.
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.
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.
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.
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
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.
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 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.
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
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.”
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
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.
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
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.
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
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.
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 Trigger Best Practices: Why Triggers Are Invisible, and When to Avoid Them
Triggers are powerful precisely because they’re automatic — which is also exactly why they need to be used deliberately, not as a default habit. This closing lesson of the chapter is about judgment: having built both DML and DDL/logon triggers, here’s when that power is worth the tradeoff and when it isn’t.
Four Real Pitfalls
Pitfall
Fix
Assuming single-row operation
Always write set-based logic joining to inserted/deleted (the exact bug proven in Lesson 1)
Recursive triggers firing themselves
Know your nested/recursive triggers DB settings; guard with logic to detect and skip re-entry
Hidden performance cost
Document clearly; keep triggers fast; avoid heavy logic on hot-path, high-write tables
Multiple triggers, no guaranteed order
Prefer one trigger per table/event; use sp_settriggerorder if unavoidable
Recursive Triggers, Concretely
-- A trigger on Account that updates Account itself can re-fire the same trigger,
-- if RECURSIVE_TRIGGERS is on for the database (off by default):
ALTER DATABASE CURRENT SET RECURSIVE_TRIGGERS OFF; -- the safe default
-- Even with recursion off, an INDIRECT loop is still possible:
-- trigger on Account updates Order → trigger on Order updates Account → fires the first trigger again
-- RECURSIVE_TRIGGERS OFF only blocks DIRECT self-triggering, not this indirect cycle
Common mistake: Assuming RECURSIVE_TRIGGERS OFF (the default) makes trigger loops impossible. It only prevents a trigger from directly re-firing itself — an indirect cycle through a second table’s trigger is still entirely possible and won’t be caught by this setting.
The Biggest Philosophical Pitfall
A developer reading application code that runs a plain UPDATE has no way to know a trigger will also fire, unless they separately go check the database schema. A stored procedure call, by contrast, is a visible, greppable, explicit decision to invoke specific logic — anyone reading the call site immediately knows exactly what runs. This invisibility is triggers’ single biggest real-world cost, independent of performance.
-- A genuinely good way to discover what triggers exist on a table you've inherited:
SELECT name, is_disabled, OBJECT_DEFINITION(object_id) AS definition
FROM sys.triggers WHERE parent_id = OBJECT_ID('dbo.Account');
When Triggers Are Still the Right Call
Use triggers when you genuinely need guaranteed enforcement regardless of write path — auditing (Lesson 2’s DDL example), cross-table integrity that CHECK constraints can’t express (Chapter 5), or a rule that must apply even to ad-hoc scripts run directly by a DBA, bypassing any application or stored procedure entirely. Avoid them for things a stored procedure or application layer could handle just as reliably, and far more visibly, since “guaranteed no matter what” is the specific property that justifies accepting the invisibility tradeoff — don’t pay that cost for a rule nothing will ever actually bypass.
Practice tip: Run the sys.triggers query above against any table you’ve built triggers on across this chapter, and read back the definitions via OBJECT_DEFINITION. Getting comfortable discovering triggers this way is a genuinely useful skill for working with a database you didn’t design yourself.
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.
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.