Microsoft SQL Server remains the transactional backbone of global enterprise applications, financial institutions, and cloud-scale data platforms. Beyond writing basic SELECT and JOIN queries, senior database developers and Database Administrators (DBAs) are evaluated on their fundamental understanding of the Storage Engine and Relational Query Optimizer.
Whether you are interviewing for a junior SQL developer position or defending high-availability disaster recovery architectures as a lead database engineer, interviewers look for deep operational intuition: how the B-Tree leaf pages are traversed, how SARGability eliminates table scans, how Isolation Levels impact concurrency, and how to recover from production outages like 100% CPU spikes, blocking chains, and log file exhaustion (Error 9002).
Who Should Use This Guide?
- College Freshers & Junior Developers (0–2 Years): Master core T-SQL syntax—DDL vs DML vs TCL, Primary vs Unique Keys, Foreign Key cascades, Join types, WHERE vs HAVING, and NULL handling.
- Mid-Level Database Engineers (3–5 Years): Master Stored Procedures vs Functions, iTVFs vs mTVFs performance traps, Window Functions (ROW_NUMBER, DENSE_RANK), CTEs, Temp Tables vs Table Variables, and Triggers.
- Senior Query Tuners (6–10 Years): Dive deep into Clustered vs Non-Clustered Indexes, Covering Indexes, Index Seeks vs Scans, SARGability, Parameter Sniffing, Statistics histograms, and Columnstore.
- Database Administrators & Architects (10+ Years): Review ACID internals, Transaction Isolation Levels, RCSI, Deadlock graphs, TempDB allocation latch contention, Always On Availability Groups, and Point-in-Time Disaster Recovery.
- 24-Hour Final Interview Revision: Rapidly review high-frequency troubleshooting scenarios, error recovery playbooks, and critical T-SQL snippets.
SQL Server Transaction Isolation Levels Cheat Sheet
Keep this quick reference matrix in mind during technical interview discussions on locking, concurrency, and anomalies:
| Isolation Level | Dirty Read | Non-Repeatable Read | Phantom Read | Locking & Versioning Mechanism |
|---|---|---|---|---|
| Read Uncommitted (NOLOCK) | Allowed | Allowed | Allowed | No Shared (S) locks acquired; ignores exclusive locks held by other sessions. |
| Read Committed (Default) | Prevented | Allowed | Allowed | Acquires Shared locks, but releases them immediately after the statement completes. |
| Read Committed Snapshot (RCSI) | Prevented | Allowed | Allowed | No Shared locks; readers read pre-update row versions from TempDB Version Store. |
| Repeatable Read | Prevented | Prevented | Allowed | Acquires Shared locks and holds them until the entire transaction ends (COMMIT/ROLLBACK). |
| Snapshot Isolation | Prevented | Prevented | Prevented | Optimistic; readers see data as of transaction start. Detects write conflicts (Error 3960). |
| Serializable | Prevented | Prevented | Prevented | Acquires Key-Range locks, preventing insertions of new rows into queried ranges. |
Filter Questions by Experience Level & Topic:
Select a progression group below, or type keywords in the instant search box to filter questions dynamically (e.g. sargable, deadlock, tempdb, index seek, stored procedure, backup, rcsi).
No matching questions found.
Try searching for a different keyword or click “All Questions (50)”.
What is the difference between DDL, DML, DCL, and TCL commands in SQL Server?
In Microsoft SQL Server, Transact-SQL (T-SQL) commands are organized into four primary functional categories:
| Category | Full Form | Key Commands | Primary Purpose |
|---|---|---|---|
| DDL | Data Definition Language | CREATE, ALTER, DROP, TRUNCATE, RENAME | Defines, alters, or destroys schema structures (tables, indexes, views, schemas). |
| DML | Data Manipulation Language | SELECT, INSERT, UPDATE, DELETE, MERGE | Retrieves, inserts, modifies, or deletes actual data rows inside tables. |
| DCL | Data Control Language | GRANT, REVOKE, DENY | Administers user security privileges, logins, roles, and schema permissions. |
| TCL | Transaction Control Language | BEGIN TRAN, COMMIT, ROLLBACK, SAVE TRAN | Controls the ACID boundaries and state changes of transactional batches. |
Note: Many developers mistakenly classify TRUNCATE as DML because it deletes data rows. In SQL Server, TRUNCATE is officially a DDL statement because it deallocates entire 8 KB data pages rather than logging row deletions.
DDL commands update system metadata and lock schema pages (Schema-Modification [Sch-M] lock). DML commands acquire row/page shared or exclusive locks.-- 1. DDL: Create the schema structure
CREATE TABLE dbo.Customers (
CustomerID INT IDENTITY(1,1) PRIMARY KEY,
CustomerName NVARCHAR(100) NOT NULL,
CreatedDate DATETIME2 DEFAULT SYSDATETIME()
);
-- 2. TCL & DML: Safe data insertion within a transaction
BEGIN TRANSACTION;
BEGIN TRY
-- DML: Insert data rows
INSERT INTO dbo.Customers (CustomerName)
VALUES ('Acme Corp'), ('Global Logistics');
-- DML: Update existing record
UPDATE dbo.Customers
SET CustomerName = 'Acme Corporation'
WHERE CustomerID = 1;
-- TCL: Commit the changes
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
-- TCL: Roll back on any failure
ROLLBACK TRANSACTION;
THROW;
END CATCH;What is the difference between a Primary Key and a Unique Key constraint in SQL Server?
Both constraints enforce uniqueness across a column or composite set of columns, but they have distinct structural differences:
| Feature | Primary Key | Unique Key |
|---|---|---|
| NULL Values | Strictly forbids NULL (Column must be NOT NULL) | Allows one NULL value (Standard SQL Server behavior) |
| Limit per Table | Exactly one Primary Key per table | Multiple Unique Keys allowed per table |
| Default Index Type | Creates a Clustered Index (unless specified otherwise) | Creates a Non-Clustered Index by default |
| Foreign Key Target | Can be referenced by Foreign Keys | Can also be referenced by Foreign Keys |
| Purpose | Defines the entity’s core row identity | Enforces unique alternate business keys (e.g. Email, SSN) |
Pro Tip on Multiple NULLs: If you want a Unique Key in SQL Server that allows multiple NULLs (like standard PostgreSQL or Oracle), create a Filtered Unique Index: CREATE UNIQUE NONCLUSTERED INDEX IX_Email ON dbo.Users(Email) WHERE Email IS NOT NULL;.
A Primary Key clustered index dictates the physical sorting order of leaf pages in the table B-Tree. Unique non-clustered indexes add a secondary B-Tree lookup structure.CREATE TABLE dbo.Users (
-- Primary Key: Clustered by default, exactly 1 allowed
UserID INT IDENTITY(1,1) CONSTRAINT PK_Users PRIMARY KEY CLUSTERED,
-- Unique Key: Non-clustered by default, multiple allowed
Email NVARCHAR(255) NOT NULL CONSTRAINT UQ_Users_Email UNIQUE NONCLUSTERED,
-- Unique Key allowing exactly 1 NULL:
PhoneNumber VARCHAR(20) NULL CONSTRAINT UQ_Users_Phone UNIQUE,
CreatedAt DATETIME2 DEFAULT SYSDATETIME()
);
-- Advanced: Unique index allowing multiple NULLs using a Filtered Index
CREATE UNIQUE NONCLUSTERED INDEX UQ_Users_NationalID
ON dbo.Users(NationalID)
WHERE NationalID IS NOT NULL;What is a Foreign Key constraint, and what are the CASCADE options (ON DELETE / ON UPDATE)?
Foreign Keys prevent “orphan records” in relational databases. SQL Server supports four actions for ON DELETE and ON UPDATE:
NO ACTION(Default): If you try to delete or update a parent row referenced by child rows, SQL Server raises an error (Error 547) and rolls back the statement.CASCADE: If you delete or update a parent row, SQL Server automatically deletes or updates all matching child rows in the child table.SET NULL: If you delete or update a parent row, SQL Server sets the referencing foreign key column in all child rows toNULL(child column must be nullable).SET DEFAULT: Sets the foreign key column in all child rows to its defined default value.
Warning: Multiple Cascade Paths: SQL Server prevents creating cycles or multiple cascade paths where deleting one record triggers cascades through multiple relationships (Error 1785), preventing infinite recursion.
Foreign Keys check the parent table during child inserts. Always index foreign key columns in child tables to prevent expensive Table Scans during parent deletes or joins.-- Parent Table
CREATE TABLE dbo.Departments (
DepartmentID INT PRIMARY KEY,
DepartmentName NVARCHAR(50) NOT NULL
);
-- Child Table with Cascading Referential Integrity
CREATE TABLE dbo.Employees (
EmployeeID INT PRIMARY KEY,
EmployeeName NVARCHAR(100) NOT NULL,
DepartmentID INT NOT NULL,
CONSTRAINT FK_Employees_Departments FOREIGN KEY (DepartmentID)
REFERENCES dbo.Departments (DepartmentID)
ON DELETE CASCADE -- Deleting a department automatically deletes its employees!
ON UPDATE CASCADE -- Changing DepartmentID cascades to child rows
);
-- Best Practice: ALWAYS index Foreign Key columns in child tables!
CREATE NONCLUSTERED INDEX IX_Employees_DepartmentID
ON dbo.Employees (DepartmentID);Explain all types of SQL Joins: INNER, LEFT, RIGHT, FULL OUTER, CROSS, and SELF JOIN.
Joins combine columns from one or more tables based on a related logical predicate:
INNER JOIN: Evaluates the join predicate and returns only rows that have matching records in both tables. Unmatched rows are discarded.LEFT OUTER JOIN: Preserves all records from the left table. If no match exists on the right table, right-side columns returnNULL.RIGHT OUTER JOIN: Preserves all records from the right table. (In practice, developers almost always rewrite right joins as left joins for readability).FULL OUTER JOIN: Preserves all rows from both tables. Where matches exist, they combine; where matches are missing on either side,NULLis filled.CROSS JOIN: Multiplies each row of the first table by every row of the second table (Cartesian product: $M imes N$ rows). Useful for generating dates, numbers, or matrix permutations.SELF JOIN: Joining a table to itself using aliases, commonly used for hierarchical data (e.g. Employee → Manager).
Under the hood, SQL Server query optimizer executes joins using one of three physical operators: Nested Loops (small/indexed data), Merge Join (both inputs sorted), or Hash Match (large unsorted inputs).-- 1. INNER JOIN: Customers with Orders
SELECT c.CustomerName, o.OrderID, o.OrderDate
FROM dbo.Customers c
INNER JOIN dbo.Orders o ON c.CustomerID = o.CustomerID;
-- 2. LEFT JOIN: Find Customers who have NEVER placed an order
SELECT c.CustomerID, c.CustomerName
FROM dbo.Customers c
LEFT JOIN dbo.Orders o ON c.CustomerID = o.CustomerID
WHERE o.OrderID IS NULL; -- Anti-semi join pattern
-- 3. CROSS JOIN: Generate a calendar matrix for reporting
SELECT e.EmployeeName, d.DateValue
FROM dbo.Employees e
CROSS JOIN dbo.CalendarDays d; -- N employees * 30 days = 30N rows
-- 4. SELF JOIN: Employee to Manager lookup
SELECT
emp.EmployeeName AS StaffMember,
ISNULL(mgr.EmployeeName, 'Top Executive') AS ManagerName
FROM dbo.Employees emp
LEFT JOIN dbo.Employees mgr ON emp.ManagerID = mgr.EmployeeID;What is the difference between WHERE and HAVING clauses, and what is the logical query processing order?
Understanding the difference requires knowing the Logical Query Processing Order in SQL Server:
FROM&JOIN(Identifies and joins source tables)WHERE(Filters individual rows before grouping)GROUP BY(Aggregates rows into groups)HAVING(Filters grouped summary rows)SELECT(Evaluates expressions and column projections)DISTINCT(Deduplicates rows)ORDER BY(Sorts output rows)TOP/OFFSET-FETCH(Limits returned row count)
Because WHERE executes at Step 2 and SELECT executes at Step 5, you cannot use column aliases created in SELECT inside the WHERE clause!
Filtering in WHERE reduces the number of rows fed into the hash/stream aggregate operator, significantly lowering memory consumption and CPU.-- Efficient: Filter raw rows with WHERE first, then filter groups with HAVING
SELECT
o.CustomerID,
COUNT(o.OrderID) AS TotalOrders,
SUM(o.TotalAmount) AS TotalSpent
FROM dbo.Orders o
WHERE o.OrderDate >= '2026-01-01' -- Step 2: Filters rows BEFORE grouping (uses index!)
GROUP BY o.CustomerID -- Step 3: Groups remaining rows
HAVING COUNT(o.OrderID) >= 5 -- Step 4: Filters groups AFTER aggregation
AND SUM(o.TotalAmount) > 10000; -- Aggregation filter
-- COMMON MISTAKE: Putting non-aggregate filters in HAVING
-- BAD: HAVING o.OrderDate >= '2026-01-01' forces SQL Server to group EVERYTHING first!
-- GOOD: Always put row-level filters in WHERE to reduce data volume before GROUP BY!How does NULL work in SQL Server, what is Three-Valued Logic, and how do ISNULL() and COALESCE() differ?
In relational databases, NULL is not equal to zero, empty string, or even another NULL:
- Three-Valued Logic: Expressions evaluate to
TRUE,FALSE, orUNKNOWN. Becausecol = NULLyieldsUNKNOWN, queries usingWHERE col = NULLreturn zero rows! You must useIS NULLorIS NOT NULL. ISNULL(check_expr, replacement_val):- T-SQL specific function. Takes exactly 2 arguments.
- Returns the data type of the first argument (can cause truncation if replacement is longer!).
- Evaluates arguments only once.
COALESCE(val1, val2, ..., valN):- ANSI SQL standard. Takes unlimited arguments and returns the first non-null value.
- Determines return type using data type precedence rules (safer against truncation).
- Syntactic sugar for a
CASE WHEN val1 IS NOT NULL THEN val1...expression.
Wrapping indexed columns in ISNULL(col, 0) inside a WHERE clause breaks query sargability, causing an Index Scan instead of an Index Seek.-- 1. Three-valued logic trap
SELECT * FROM dbo.Employees WHERE MiddleName = NULL; -- Returns 0 rows! WRONG!
SELECT * FROM dbo.Employees WHERE MiddleName IS NULL; -- Correct!
-- 2. Truncation Trap with ISNULL vs COALESCE
DECLARE @shortVar VARCHAR(5) = NULL;
-- ISNULL truncates because @shortVar is VARCHAR(5):
SELECT ISNULL(@shortVar, 'Antigravity') AS IsNullResult;
-- Output: 'Antig' (TRUNCATED TO 5 CHARACTERS!)
-- COALESCE uses highest data type precedence (no truncation):
SELECT COALESCE(@shortVar, 'Antigravity') AS CoalesceResult;
-- Output: 'Antigravity' (Full string preserved!)
-- 3. ANSI_NULLS setting behavior
SET ANSI_NULLS ON; -- Standard: col = NULL evaluates to UNKNOWN
SET ANSI_NULLS OFF; -- Legacy (Deprecated): col = NULL evaluates to TRUE/FALSEWhat is the difference between CHAR, VARCHAR, NCHAR, and NVARCHAR in SQL Server?
Choosing the right string data type directly impacts database storage, page density, and buffer pool RAM consumption:
| Data Type | Length Behavior | Character Set | Storage per Character | Best Used For |
|---|---|---|---|---|
CHAR(n) | Fixed length (pads with spaces) | Non-Unicode (ASCII) | 1 byte | Fixed codes (e.g. Country Code CHAR(2), ISO codes) |
VARCHAR(n) | Variable length (up to 8,000 chars) | Non-Unicode (ASCII) | 1 byte per char + 2 bytes overhead | Standard English text with varying lengths |
NCHAR(n) | Fixed length (pads with spaces) | Unicode (UTF-16) | 2 bytes | Fixed multi-language codes (e.g. Japanese postal codes) |
NVARCHAR(n) | Variable length (up to 4,000 chars) | Unicode (UTF-16) | 2 bytes per char + 2 bytes overhead | International names, multilingual descriptions |
VARCHAR(MAX) / NVARCHAR(MAX) | Variable length (up to 2 GB) | ASCII / UTF-16 | Inline up to 8 KB, spills out-of-row to LOB pages | Large documents, JSON payloads, unstructured logs |
Modern SQL Server Feature: Starting with SQL Server 2019, you can enable UTF-8 collations (e.g. Latin1_General_100_CI_AI_SC_UTF8), allowing VARCHAR to store multilingual Unicode using 1 to 4 bytes per character, saving up to 50% storage.
Using NVARCHAR doubles the memory required in the SQL Server Buffer Pool cache for string columns. Use VARCHAR when data is guaranteed to be ASCII.-- Demonstrating space padding and byte storage
DECLARE @Fixed CHAR(10) = 'RTS';
DECLARE @Variable VARCHAR(10) = 'RTS';
DECLARE @Unicode NVARCHAR(10) = N'RTS';
SELECT
DATALENGTH(@Fixed) AS [CHAR_Bytes], -- 10 bytes (Padded with 7 trailing spaces!)
DATALENGTH(@Variable) AS [VARCHAR_Bytes], -- 3 bytes (Only stores 3 characters)
DATALENGTH(@Unicode) AS [NVARCHAR_Bytes]; -- 6 bytes (3 characters * 2 bytes UTF-16)
-- Explicit 'N' prefix is MANDATORY for Unicode literals!
INSERT INTO dbo.Users (FullName) VALUES (N'José González'); -- Without N', 'é' and 'á' become '?'!What is the difference between UNION and UNION ALL, and which one is faster?
Both operators combine rows from two or more queries into a single output, requiring identical column counts and compatible data types:
UNION: To eliminate duplicate rows across both result sets, SQL Server must sort the combined data or build an in-memory hash table (Distinct SortorHash Match (Aggregate)operator in execution plan). On large tables, this causes high CPU usage and can spill toTempDBif memory grants are exceeded.UNION ALL: Simply concatenates the inputs together using a lightweightConcatenationoperator in the execution plan. It requires zero sorting, zero deduplication, and minimal memory grants.
Rule of Thumb: Default to UNION ALL unless you have a strict business requirement to eliminate duplicate rows!
UNION requires sorting the entire combined dataset. On 1,000,000 rows, UNION ALL takes ~150ms while UNION can take 3,500ms and consume 50 MB of query memory grant.-- 1. UNION: Removes duplicates (High CPU Sort / Hash Match)
SELECT City FROM dbo.Customers -- 10,000 rows
UNION
SELECT City FROM dbo.Suppliers; -- 5,000 rows
-- Execution Plan: Concatenates 15,000 rows, then performs DISTINCT SORT!
-- 2. UNION ALL: Preserves duplicates (Fast Concatenation, Zero Sort)
SELECT City FROM dbo.Customers
UNION ALL
SELECT City FROM dbo.Suppliers;
-- Execution Plan: Straight pass-through stream of 15,000 rows with zero CPU sort overhead!
-- 3. Combining disjoint sets (Guaranteed no duplicates)
-- ALWAYS use UNION ALL when sets are naturally disjoint:
SELECT OrderID, 'Online' AS OrderType FROM dbo.OnlineOrders
UNION ALL
SELECT OrderID, 'Retail' AS OrderType FROM dbo.RetailOrders;What is the difference between DELETE, TRUNCATE, and DROP in SQL Server?
These three commands clear data at different levels of the SQL Server storage engine:
| Feature | DELETE | TRUNCATE | DROP |
|---|---|---|---|
| Command Type | DML | DDL | DDL |
| WHERE Clause | Supported (delete specific rows) | Not supported (removes all rows) | Not supported (deletes entire table) |
| Transaction Logging | Fully logged row-by-row in LDF | Minimally logged (page deallocations) | Logs table object drops |
| Performance | Slow for large tables | Near-instantaneous (page pointers) | Instantaneous |
| Triggers | Fires ON DELETE triggers | Does NOT fire delete triggers | Does not fire delete triggers |
| IDENTITY Column | Does NOT reset identity seed | Resets identity to initial seed | Object destroyed |
| Foreign Key Block | Allowed if no child rows exist | Blocked if referenced by ANY Foreign Key | Blocked if referenced by Foreign Key |
| Can be Rolled Back? | Yes (inside a transaction) | Yes (inside a transaction!) | Yes (inside a transaction!) |
Deleting 10 million rows via DELETE writes gigabytes to the LDF transaction log and holds row/page locks. TRUNCATE simply unlinks 8 KB pages in the IAM (Index Allocation Map).-- PROOF: TRUNCATE CAN be rolled back in SQL Server!
CREATE TABLE dbo.TestLog (ID INT IDENTITY(1,1), Val VARCHAR(20));
INSERT INTO dbo.TestLog VALUES ('A'), ('B'), ('C');
BEGIN TRANSACTION;
TRUNCATE TABLE dbo.TestLog; -- Table is now empty!
SELECT COUNT(*) AS [CountAfterTruncate] FROM dbo.TestLog; -- 0 rows
ROLLBACK TRANSACTION;
-- Table data is fully restored!
SELECT COUNT(*) AS [CountAfterRollback] FROM dbo.TestLog; -- 3 rows!
-- DELETE with WHERE clause
DELETE FROM dbo.Orders WHERE OrderDate < '2020-01-01';What is an IDENTITY column, and how do @@IDENTITY, SCOPE_IDENTITY(), and IDENT_CURRENT() differ?
Retrieving the newly generated ID of an inserted record is a frequent task in application development:
SCOPE_IDENTITY()(Best Practice): Returns the last identity generated in the current connection session AND within the current executing batch/stored procedure scope. If your table has a trigger that inserts into an audit table,SCOPE_IDENTITY()still returns your original table’s ID!@@IDENTITY(Dangerous): Returns the last identity generated in the current session across all scopes. If anAFTER INSERTtrigger fires and inserts into anAuditLogtable,@@IDENTITYwill return the AuditLog ID, silently corrupting your application logic!IDENT_CURRENT('TableName'): Returns the last identity value generated for a specific table, regardless of which user or session created it. Subject to race conditions if multiple users insert simultaneously.
SCOPE_IDENTITY() reads directly from execution context memory with zero database I/O.-- 1. Create Order Table and Audit Table
CREATE TABLE dbo.Orders (OrderID INT IDENTITY(100, 1) PRIMARY KEY, Total DECIMAL(10,2));
CREATE TABLE dbo.AuditLog (AuditID INT IDENTITY(5000, 1) PRIMARY KEY, Note VARCHAR(50));
-- 2. Create Trigger that logs audits
CREATE OR ALTER TRIGGER trg_Orders_Audit ON dbo.Orders AFTER INSERT AS
BEGIN
INSERT INTO dbo.AuditLog (Note) VALUES ('New Order Logged');
END;
-- 3. Insert and compare identity functions:
INSERT INTO dbo.Orders (Total) VALUES (250.00);
SELECT
SCOPE_IDENTITY() AS [SCOPE_IDENTITY_Correct], -- Returns 100 (Safe & Correct!)
@@IDENTITY AS [AtAtIdentity_Corrupted], -- Returns 5000 (Corrupted by trigger!)
IDENT_CURRENT('dbo.Orders') AS [TableIdentity]; -- Returns 100 (Across all users)
-- MODERN BEST PRACTICE: Use OUTPUT clause for multi-row inserts
INSERT INTO dbo.Orders (Total)
OUTPUT inserted.OrderID, inserted.Total
VALUES (99.00), (149.00);What is the difference between a Stored Procedure and a User-Defined Function (UDF)?
Stored Procedures and Functions serve completely different purposes in database architecture:
| Feature | Stored Procedure | User-Defined Function (UDF) |
|---|---|---|
| Return Values | Returns 0 or more result sets + integer status code | Must return exactly one scalar value or one table |
| Usage in Queries | Cannot be used in SELECT, WHERE, or JOIN | Can be embedded directly in SELECT, WHERE, JOIN |
| State Modification | Can perform INSERT, UPDATE, DELETE, DDL | Read-only: Cannot modify database state |
| Transactions | Can manage explicit transactions (BEGIN TRAN, COMMIT) | Cannot use transactions or handle try/catch blocks |
| Parameters | Accepts INPUT and OUTPUT parameters | Accepts input parameters only |
| Calling Method | Executed via EXEC procedure_name | Called inline like SELECT dbo.fn_CalculateTax(Price) |
Scalar UDFs historically forced single-threaded RBAR (Row-By-Agonizing-Row) execution on every row, devastating performance. SQL Server 2019+ introduced Scalar UDF Inlining to mitigate this.-- 1. User-Defined Function: Computes value, used inside SELECT
CREATE OR ALTER FUNCTION dbo.fn_CalculateDiscount
(
@TotalAmount DECIMAL(18,2),
@CustomerType VARCHAR(20)
)
RETURNS DECIMAL(18,2)
AS
BEGIN
DECLARE @Discount DECIMAL(18,2) = 0.00;
IF @CustomerType = 'VIP' AND @TotalAmount > 1000
SET @Discount = @TotalAmount * 0.15;
ELSE
SET @Discount = @TotalAmount * 0.05;
RETURN @Discount;
END;
GO
-- Calling Function inline in SELECT:
SELECT OrderID, TotalAmount, dbo.fn_CalculateDiscount(TotalAmount, 'VIP') AS Discount
FROM dbo.Orders;
-- 2. Stored Procedure: Modifies state, executes transactions
CREATE OR ALTER PROCEDURE dbo.usp_ProcessOrderRefund
@OrderID INT,
@RefundAmount DECIMAL(18,2),
@Success BIT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRANSACTION;
BEGIN TRY
UPDATE dbo.Orders SET RefundedAmount = @RefundAmount WHERE OrderID = @OrderID;
INSERT INTO dbo.AuditRefunds (OrderID, Amount, RefundDate) VALUES (@OrderID, @RefundAmount, SYSDATETIME());
COMMIT TRANSACTION;
SET @Success = 1;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
SET @Success = 0;
THROW;
END CATCH;
END;What is the difference between an Inline Table-Valued Function (iTVF) and a Multi-Statement Table-Valued Function (mTVF)?
This is one of the most frequent performance questions in senior SQL Server interviews:
- Inline Table-Valued Function (iTVF): Has no
BEGIN/ENDblock and no table variable declaration. The query optimizer inlines the function directly into the outer query plan. It generates accurate cost-based statistics, supports index seeks, and can run with parallel execution plans. - Multi-Statement Table-Valued Function (mTVF): Declares a return table variable (e.g.
RETURNS @Table TABLE (...)) and populates it using multiple procedural statements.- In SQL Server 2012/2014, the optimizer assumed a static cardinality of 1 row! (SQL Server 2017+ assumes 100 rows).
- If the mTVF actually returns 500,000 rows, this drastic cardinality estimation mismatch causes catastrophic Nested Loop joins instead of Hash joins, memory spills to TempDB, and multi-minute query freezes.
iTVFs have zero overhead compared to raw SQL queries. mTVFs allocate an in-memory table variable in TempDB, generate zero column statistics, and force bad join plans.-- 1. FAST: Inline Table-Valued Function (Inlined into execution plan)
CREATE OR ALTER FUNCTION dbo.fn_GetCustomerOrders_Inline (@CustomerID INT)
RETURNS TABLE
AS
RETURN (
SELECT OrderID, OrderDate, TotalAmount
FROM dbo.Orders
WHERE CustomerID = @CustomerID
);
GO
-- 2. SLOW / DANGEROUS: Multi-Statement TVF (Procedural, bad cardinality)
CREATE OR ALTER FUNCTION dbo.fn_GetCustomerOrders_MultiStatement (@CustomerID INT)
RETURNS @Result TABLE (OrderID INT, OrderDate DATETIME2, TotalAmount DECIMAL(18,2))
AS
BEGIN
INSERT INTO @Result
SELECT OrderID, OrderDate, TotalAmount
FROM dbo.Orders
WHERE CustomerID = @CustomerID;
-- Procedural logic...
RETURN;
END;
GO
-- Test: Using with CROSS APPLY
-- The inline version produces an optimal Index Seek; the multi-statement causes table variable scans!
SELECT c.CustomerName, o.OrderID, o.TotalAmount
FROM dbo.Customers c
CROSS APPLY dbo.fn_GetCustomerOrders_Inline(c.CustomerID) o;Explain Window Functions in SQL Server: ROW_NUMBER(), RANK(), DENSE_RANK(), and NTILE().
Window functions perform calculations across a set of table rows related to the current row without collapsing rows into a single summary like GROUP BY does:
| Score Value | ROW_NUMBER() | RANK() | DENSE_RANK() | NTILE(2) |
|---|---|---|---|---|
| 100 | 1 | 1 | 1 | Bucket 1 |
| 90 | 2 | 2 | 2 | Bucket 1 |
| 90 (Tie) | 3 | 2 (Tied) | 2 (Tied) | Bucket 1 |
| 80 | 4 | 4 (Gap skipped!) | 3 (No gaps!) | Bucket 2 |
| 70 | 5 | 5 | 4 | Bucket 2 |
Clauses Used:
PARTITION BY: Divides query results into partitions (like a local GROUP BY per category).ORDER BY: Defines the logical order of rows inside each partition.
Window functions process data using a Segment and Sequence Project operator in the execution plan. An index matching the `(PARTITION BY, ORDER BY)` columns avoids an expensive Sort.-- Find the Top 2 highest-paid employees in EACH department
WITH RankedSalaries AS (
SELECT
DepartmentID,
EmployeeName,
Salary,
ROW_NUMBER() OVER(PARTITION BY DepartmentID ORDER BY Salary DESC) AS RowNum,
RANK() OVER(PARTITION BY DepartmentID ORDER BY Salary DESC) AS RankNum,
DENSE_RANK() OVER(PARTITION BY DepartmentID ORDER BY Salary DESC) AS DenseRankNum
FROM dbo.Employees
)
SELECT DepartmentID, EmployeeName, Salary, DenseRankNum
FROM RankedSalaries
WHERE DenseRankNum <= 2; -- Returns top 2 salary tiers per department, handling ties cleanly!
-- Using NTILE to divide customers into 4 quartiles based on annual spend:
SELECT
CustomerID,
TotalSpent,
NTILE(4) OVER(ORDER BY TotalSpent DESC) AS SpendingQuartile
FROM dbo.CustomerStats;What is a Common Table Expression (CTE), and how does a Recursive CTE work?
A CTE improves query modularity, readability, and enables hierarchical traversal:
Anatomy of a Recursive CTE:
- Anchor Member: The base query that retrieves the root or starting records (e.g. Top-level CEO where
ManagerID IS NULL). UNION ALL: Combines the anchor with recursive iterations.- Recursive Member: References the CTE itself, joining child records to the parent records from the previous iteration.
- Termination Condition: Recursion stops automatically when the recursive member returns zero new rows.
Safety Precaution: By default, SQL Server restricts recursion to 100 levels (Error 530) to prevent infinite loops. You can adjust this using OPTION (MAXRECURSION 0) for unlimited recursion.
A standard CTE does NOT materialize data on disk; it is syntactic sugar that is substituted directly into the query tree. A Recursive CTE builds an internal worktable in TempDB.-- Organizational Hierarchy Recursive Query
WITH OrgChart_CTE AS (
-- 1. Anchor Member: Find the CEO / Top Executive
SELECT
EmployeeID,
EmployeeName,
ManagerID,
0 AS HierarchyLevel,
CAST(EmployeeName AS VARCHAR(MAX)) AS ReportingPath
FROM dbo.Employees
WHERE ManagerID IS NULL
UNION ALL
-- 2. Recursive Member: Join employees to their managers from previous level
SELECT
e.EmployeeID,
e.EmployeeName,
e.ManagerID,
o.HierarchyLevel + 1,
CAST(o.ReportingPath + ' -> ' + e.EmployeeName AS VARCHAR(MAX))
FROM dbo.Employees e
INNER JOIN OrgChart_CTE o ON e.ManagerID = o.EmployeeID
)
SELECT EmployeeID, EmployeeName, HierarchyLevel, ReportingPath
FROM OrgChart_CTE
ORDER BY HierarchyLevel, EmployeeID
OPTION (MAXRECURSION 50); -- Guard against infinite loopsCompare Temporary Tables (#Temp), Global Temporary Tables (##Temp), and Table Variables (@Table).
Choosing the correct temporary structure is a major tuning skill in SQL Server:
| Feature | Local Temp Table (#Table) | Table Variable (@Table) | Global Temp Table (##Table) |
|---|---|---|---|
| Scope | Current session / SP scope | Current batch / SP scope only | All sessions on instance |
| Physical Storage | TempDB database | TempDB (in-memory buffer + TempDB backing) | TempDB database |
| Statistics | Yes (Full column distribution stats) | No (Assumes 1 row, causing bad plans) | Yes (Full statistics) |
| Custom Indexes | Yes (Clustered & Non-Clustered) | Only inline PRIMARY KEY / UNIQUE constraints | Yes (Full index support) |
| Transactions & Rollback | Fully rolled back on ROLLBACK | NOT rolled back! Retains modifications | Fully rolled back on rollback |
| Parallelism | Supports parallel execution plans | Historically forced serial execution | Supports parallelism |
| Best Used For | >100 rows, complex joins, data pipelines | <100 rows, small lookup sets | Sharing temp data across sessions |
Use Table Variables only for small sets (<100 rows). On large datasets (e.g. 50,000 rows), missing statistics cause the optimizer to choose Nested Loops, causing catastrophic CPU spikes.-- PROOF: Table Variables do NOT participate in ROLLBACK!
BEGIN TRANSACTION;
-- 1. Temp Table
CREATE TABLE #TempData (ID INT);
INSERT INTO #TempData VALUES (1);
-- 2. Table Variable
DECLARE @VarData TABLE (ID INT);
INSERT INTO @VarData VALUES (1);
ROLLBACK TRANSACTION;
-- Temp table row was rolled back and is now gone:
SELECT COUNT(*) AS [TempTableCount] FROM #TempData; -- 0 rows!
-- Table variable row PERSISTS despite the ROLLBACK!
SELECT COUNT(*) AS [TableVarCount] FROM @VarData; -- 1 row!
DROP TABLE #TempData;What are Views in SQL Server, and what is an Indexed (Materialized) View?
Views provide security encapsulation, abstraction, and query simplicity:
- Standard Views: When queried, the SQL Server query optimizer merges the view's definition with the outer query. It reads data from the underlying base tables every time.
- Indexed Views (Materialized Views):
- Physically materializes the aggregated data on disk. When underlying tables update, SQL Server automatically maintains the view data.
- In Enterprise Edition, the optimizer can match queries to the indexed view even if the query does not explicitly name the view! In Standard Edition, you must use the
WITH (NOEXPAND)hint. - Strict Requirements: Must be created with
WITH SCHEMABINDING, underlying tables must use two-part names (dbo.Table), cannot useCOUNT(*)(must useCOUNT_BIG(*)), and cannot containUNION,DISTINCT, or subqueries.
Indexed views provide instantaneous O(1) lookups for heavy aggregations, but slow down base table INSERT/UPDATE/DELETE operations because the view index must be updated synchronously.-- 1. Create View with SCHEMABINDING (Binds view to underlying table schema)
CREATE OR ALTER VIEW dbo.vw_MonthlySalesSummary
WITH SCHEMABINDING
AS
SELECT
o.CustomerID,
SUM(o.TotalAmount) AS TotalRevenue,
COUNT_BIG(*) AS OrderCount -- COUNT_BIG(*) is MANDATORY for indexed views with aggregation!
FROM dbo.Orders o
GROUP BY o.CustomerID;
GO
-- 2. Materialize the View by creating a Unique Clustered Index on it:
CREATE UNIQUE CLUSTERED INDEX CIX_vw_MonthlySalesSummary_CustomerID
ON dbo.vw_MonthlySalesSummary (CustomerID);
GO
-- 3. Querying the Indexed View:
-- In Standard Edition, WITH (NOEXPAND) forces SQL Server to read directly from the view's index!
SELECT CustomerID, TotalRevenue, OrderCount
FROM dbo.vw_MonthlySalesSummary WITH (NOEXPAND)
WHERE CustomerID = 105;What are Triggers in SQL Server? Compare AFTER/FOR vs INSTEAD OF Triggers.
Triggers run within the same transaction scope as the triggering statement and provide access to two virtual memory-resident tables:
insertedTable: Holds copies of the new rows being inserted or the updated state of rows.deletedTable: Holds copies of rows being deleted or the original pre-update state of rows.- For Updates:
deletedcontains the old values;insertedcontains the new values.
| Feature | AFTER / FOR Trigger | INSTEAD OF Trigger |
|---|---|---|
| Execution Timing | Fires after constraints are checked and data is written | Fires before constraints; bypasses standard action |
| Limit per Table | Multiple AFTER triggers per action allowed | Only one INSTEAD OF trigger per table/view |
| Target Objects | Tables only | Both Tables and Views (enables updating complex views!) |
| Constraint Verification | If Foreign Key or CHECK fails, trigger NEVER fires | Executes even if base table constraints would fail |
Triggers run synchronously inside the calling transaction. A slow trigger holds locks and drastically inflates transaction duration.CREATE OR ALTER TRIGGER trg_Employee_SalaryAudit
ON dbo.Employees
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
-- Only log if the Salary column was actually updated:
IF UPDATE(Salary)
BEGIN
-- CRITICAL: Always write triggers to handle MULTI-ROW updates!
-- Joining 'inserted' and 'deleted' tables:
INSERT INTO dbo.SalaryAuditLog (EmployeeID, OldSalary, NewSalary, ChangedAt, ChangedBy)
SELECT
i.EmployeeID,
d.Salary AS OldSalary,
i.Salary AS NewSalary,
SYSDATETIME(),
SYSTEM_USER
FROM inserted i
INNER JOIN deleted d ON i.EmployeeID = d.EmployeeID
WHERE i.Salary <> d.Salary; -- Only log actual changes
END
END;How does EXISTS differ from IN in subqueries, and why is NOT IN dangerous with NULLs?
Understanding this distinction is vital for writing bug-free data queries:
EXISTS: Checks for the existence of rows. It doesn't care what columns are in theSELECTlist (e.g.SELECT 1orSELECT *). As soon as the storage engine finds a single matching row, it returnsTRUEimmediately.- The
NOT INwith NULL Disaster:If you write
WHERE CustomerID NOT IN (SELECT CustomerID FROM dbo.Orders), and the subquery returns[101, 102, NULL], SQL Server evaluates:CustomerID <> 101 AND CustomerID <> 102 AND CustomerID <> NULLBecause
CustomerID <> NULLevaluates toUNKNOWN, the entireANDcondition evaluates toUNKNOWN. The query returns 0 rows! NOT EXISTS(Safe Alternative): Uses two-valued existence logic. It returns the correct results regardless of whether the child table contains nulls.
The query optimizer often transforms both IN and EXISTS into the same Semi-Join operator. However, NOT EXISTS is immune to NULL evaluation bugs and generates superior execution plans.-- DANGEROUS: If Orders contains a single NULL CustomerID, returns ZERO rows!
SELECT CustomerID, CustomerName
FROM dbo.Customers
WHERE CustomerID NOT IN (
SELECT CustomerID FROM dbo.Orders -- What if CustomerID is nullable here?
);
-- SAFE & EFFICIENT: NOT EXISTS handles NULLs correctly (Anti-Semi Join)
SELECT c.CustomerID, c.CustomerName
FROM dbo.Customers c
WHERE NOT EXISTS (
SELECT 1
FROM dbo.Orders o
WHERE o.CustomerID = c.CustomerID
);
-- The optimizer turns NOT EXISTS into a Left Anti-Semi Join, matching indexed keys efficiently!How do you find and remove duplicate rows from a table in SQL Server?
Removing duplicate records without a primary key is a classic live coding interview problem:
The Canonical 3-Step Strategy:
- Define a CTE that partitions rows by the business columns that define a duplicate (e.g.
EmailorFirstName, LastName). - Order the partition by a deterministic column (e.g.
CreatedDate ASCto keep the oldest original record, orDESCto keep the newest). - Execute a
DELETEdirectly against the CTE whereRowNum > 1. In SQL Server, deleting from a CTE directly deletes the underlying physical rows in the base table!
This method scans the table once and avoids creating temporary tables or slow nested cursor loops. For millions of rows, batch the deletion using `DELETE TOP (50000)` in a loop.-- Sample Table with Duplicates
CREATE TABLE #Contacts (
ContactID INT IDENTITY(1,1),
Email NVARCHAR(100),
FullName NVARCHAR(100),
CreatedAt DATETIME2
);
INSERT INTO #Contacts VALUES
('alice@test.com', 'Alice Smith', '2026-01-01'),
('alice@test.com', 'Alice Smith', '2026-01-05'), -- Duplicate!
('bob@test.com', 'Bob Jones', '2026-01-02');
-- DEDUPLICATION QUERY:
WITH DuplicateRecords_CTE AS (
SELECT
ContactID,
Email,
FullName,
-- Partition by duplicate key, order by oldest record to keep
ROW_NUMBER() OVER(
PARTITION BY Email
ORDER BY CreatedAt ASC, ContactID ASC
) AS RowNumber
FROM #Contacts
)
-- Deletes the duplicate rows directly from the physical table:
DELETE FROM DuplicateRecords_CTE
WHERE RowNumber > 1;
-- Verification:
SELECT * FROM #Contacts; -- Only original distinct rows remain!
DROP TABLE #Contacts;What is the MERGE statement in SQL Server, and what are its concurrency pitfalls?
MERGE (Upsert) synchronizes target tables with incoming source data:
WHEN MATCHED THEN UPDATE...(Updates existing records)WHEN NOT MATCHED BY TARGET THEN INSERT...(Inserts new records)WHEN NOT MATCHED BY SOURCE THEN DELETE...(Deletes obsolete records)
Concurrency Pitfalls & Bugs:
Despite being a single T-SQL statement, MERGE does not prevent concurrent race conditions by default! Two concurrent sessions executing MERGE on the same key can both reach the WHEN NOT MATCHED branch simultaneously, resulting in Primary Key violation errors (Error 2627) or deadlocks.
Remedy: Always specify the WITH (HOLDLOCK) table hint on the target table to ensure serializable range locking.
MERGE acquires exclusive locks on both matched and unmatched rows. Under heavy OLTP traffic, separate UPDATE and INSERT IF NOT EXISTS statements often scale better than MERGE.-- Target Table: Product Catalog
-- Source Table: Incoming Supplier Staging Feed
MERGE INTO dbo.Products WITH (HOLDLOCK) AS target -- HOLDLOCK is MANDATORY to prevent race conditions!
USING dbo.ProductStaging AS source
ON target.ProductSKU = source.ProductSKU
-- 1. Update existing products if price or stock changed
WHEN MATCHED AND (target.Price <> source.Price OR target.StockQty <> source.StockQty)
THEN UPDATE SET
target.Price = source.Price,
target.StockQty = source.StockQty,
target.LastUpdated = SYSDATETIME()
-- 2. Insert new products
WHEN NOT MATCHED BY TARGET
THEN INSERT (ProductSKU, ProductName, Price, StockQty)
VALUES (source.ProductSKU, source.ProductName, source.Price, source.StockQty)
-- 3. Output auditing changes directly:
OUTPUT $action AS ActionTaken, inserted.ProductSKU, deleted.Price AS OldPrice, inserted.Price AS NewPrice;What is the difference between a Clustered Index and a Non-Clustered Index in SQL Server?
Indexes in SQL Server are organized as balanced B-Tree (Balanced Tree) structures:
| Feature | Clustered Index | Non-Clustered Index |
|---|---|---|
| Leaf Level Contents | The actual table data pages (all columns exist here) | Index keys + Row Locator (pointer to data) |
| Limit per Table | Exactly one (The table IS the index) | Up to 999 per table |
| Physical Order | Physically dictates the storage order on disk | Separate secondary structure; does not reorder table |
| Row Locator | N/A (Leaf node IS the data row) | Points to Clustered Key (if clustered) or RID (if Heap) |
| Ideal Columns | Narrow, unique, sequential, non-updating (e.g. IDENTITY) | Columns used frequently in WHERE, JOIN, ORDER BY |
What is a Heap? A table with no clustered index is called a Heap. Its rows are stored with no particular order, and non-clustered indexes reference rows via an 8-byte RID (Row Identifier) consisting of FileID:PageID:SlotNumber.
Clustered indexes prevent page fragmentation if keys are sequential (IDENTITY/Sequential GUID). Random keys (e.g. standard NEWID()) cause severe page splits and high I/O.-- 1. Table with Clustered Index on Identity column (Sequential B-Tree)
CREATE TABLE dbo.Invoices (
InvoiceID INT IDENTITY(1,1),
CustomerID INT NOT NULL,
InvoiceDate DATETIME2 NOT NULL,
TotalAmount DECIMAL(12,2) NOT NULL,
Status VARCHAR(20) NOT NULL,
CONSTRAINT PK_Invoices PRIMARY KEY CLUSTERED (InvoiceID)
);
-- 2. Non-Clustered Index on frequently searched columns
CREATE NONCLUSTERED INDEX IX_Invoices_CustomerID_Date
ON dbo.Invoices (CustomerID, InvoiceDate)
INCLUDE (TotalAmount, Status); -- Covering index (prevents Key Lookups)What is a Covering Index, and how does the INCLUDE clause eliminate Key Lookups?
When a query uses a non-clustered index to find matching rows, but needs additional columns not present in that index, SQL Server must execute a Key Lookup (Bookmark Lookup):
- SQL Server seeks through the non-clustered index to find matching keys in
O(log N)time. - For every single matching row, it must jump back to the clustered index root and navigate down to the data page to fetch the missing columns.
- If the query matches 50,000 rows, that means 50,000 separate random I/O Key Lookups! At a certain tipping point, SQL Server abandons the index entirely and performs a full Table Scan.
The Solution: The INCLUDE Clause:
Adding columns to INCLUDE (...) stores them only at the leaf level of the B-Tree. They do not participate in sorting and are not stored in root/intermediate navigation pages, keeping the index narrow and fast while eliminating Key Lookups.
Eliminating Key Lookups converts thousands of random I/O page reads into a single sequential index range scan, frequently speeding up queries by 50x-100x.-- The Query:
SELECT CustomerID, OrderDate, TotalAmount, Status
FROM dbo.Orders
WHERE CustomerID = 1042 AND OrderDate >= '2026-01-01';
-- UNOPTIMIZED INDEX:
CREATE NONCLUSTERED INDEX IX_Orders_Bad
ON dbo.Orders (CustomerID, OrderDate);
-- Problem: TotalAmount and Status are NOT in the index!
-- Execution Plan: Index Seek + Key Lookup (Clustered) for every matching order!
-- COVERING INDEX (OPTIMIZED):
CREATE NONCLUSTERED INDEX IX_Orders_Covering
ON dbo.Orders (CustomerID, OrderDate)
INCLUDE (TotalAmount, Status);
-- Execution Plan: 100% Index Seek! ZERO Key Lookups!
-- The query is completely satisfied from the narrow index pages in memory!Explain Table Scan, Index Scan, Index Seek, and Key Lookup in SQL Server Execution Plans.
When analyzing Graphical Execution Plans in SSMS, these four data retrieval operators represent the spectrum of performance:
| Operator | Visual Symbol | Mechanism | Performance Rating |
|---|---|---|---|
| Index Seek | Seek icon with arrow pointing to key | Navigates root → intermediate → specific leaf page using a search predicate. Reads only qualifying rows. | ⚡ Fastest (Optimal) |
| Index Scan | Scan icon over index pages | Scans through every single page in the index B-Tree from beginning to end. | ⚠️ Moderate to Slow |
| Table Scan | Table icon | Occurs on a Heap (table with no clustered index). Reads every single 8 KB data page in the table. | ❌ Slowest on large tables |
| Key Lookup | Clustered index with magnifying glass | Secondary lookup from non-clustered index into clustered index to retrieve non-indexed columns. | ⚠️ Expensive at scale |
An Index Seek typically performs 3 to 4 logical page reads. An Index Scan on a 10 GB table reads all 1,300,000 pages into buffer RAM, causing disk bottlenecking.-- 1. INDEX SEEK: Optimal filter on indexed leading column
SELECT CustomerID, OrderDate
FROM dbo.Orders
WHERE CustomerID = 500; -- Uses B-Tree navigation to jump directly to row!
-- 2. INDEX SCAN: Caused by scanning entire index (e.g. non-leading column or function)
SELECT CustomerID, OrderDate
FROM dbo.Orders
WHERE YEAR(OrderDate) = 2026; -- Function kills Seek! Scans all 10,000,000 rows!
-- 3. KEY LOOKUP: Index matches filter, but SELECT needs extra columns
SELECT CustomerID, OrderDate, ShippingAddress -- ShippingAddress not in index!
FROM dbo.Orders
WHERE CustomerID = 500;What is Query SARGability (Search Argument Able), and how do functions on columns destroy index seeks?
The query optimizer can only traverse an index B-Tree if the search key is compared directly against a literal constant without transformation:
Non-SARGable Anti-Patterns vs SARGable Solutions:
- Anti-Pattern 1 (Date Functions):
WHERE YEAR(CreatedDate) = 2026
→ SARGable Fix:WHERE CreatedDate >= '2026-01-01' AND CreatedDate < '2027-01-01' - Anti-Pattern 2 (Math on Column):
WHERE Price * 1.10 > 100
→ SARGable Fix:WHERE Price > 100 / 1.10(Move math to the constant side!) - Anti-Pattern 3 (String Concatenation):
WHERE FirstName + ' ' + LastName = 'John Doe'
→ SARGable Fix:WHERE FirstName = 'John' AND LastName = 'Doe' - Anti-Pattern 4 (Leading Wildcards):
WHERE Email LIKE '%@gmail.com'
→ Cannot seek because index is sorted by beginning of string! (Must use Full-Text Search).
A non-SARGable query on a 50-million-row table converts a 2-millisecond Index Seek into a 30-second Index Scan consuming 100% CPU.-- 1. DATE TRUNCATION:
-- NON-SARGABLE (Scans 5,000,000 rows):
SELECT OrderID FROM dbo.Orders WHERE CONVERT(VARCHAR(10), OrderDate, 120) = '2026-09-26';
-- SARGABLE REWRITE (Seeks directly in 2ms):
SELECT OrderID FROM dbo.Orders
WHERE OrderDate >= '2026-09-26 00:00:00'
AND OrderDate < '2026-09-27 00:00:00';
-- 2. NULL COALESCE IN WHERE:
-- NON-SARGABLE (Scans entire index):
SELECT UserID FROM dbo.Users WHERE ISNULL(Status, 'Active') = 'Active';
-- SARGABLE REWRITE (Seeks index):
SELECT UserID FROM dbo.Users WHERE Status = 'Active' OR Status IS NULL;What is Parameter Sniffing in SQL Server, why does it cause sudden slowness, and how do you fix it?
Parameter sniffing is designed as an optimization to tailor execution plans to real data, but causes severe performance volatility:
- Initial Run: User calls
EXEC usp_GetOrders @Status = 'CANCELLED'. Since only 5 orders are cancelled, SQL Server compiles a plan with an Index Seek + Key Lookup and caches it in the Plan Cache. - Subsequent Run: Another user calls
EXEC usp_GetOrders @Status = 'COMPLETED'. 95% of orders (1,000,000 rows) are completed! - The Disaster: SQL Server reuses the cached plan! It attempts to execute 1,000,000 Key Lookups, pegging CPU at 100% and timing out. The optimal plan should have been a clustered index scan or hash join.
Proven Solutions:
OPTIMIZE FOR (@param = 'typical_value'): Tells optimizer to compile for a specific value.OPTIMIZE FOR UNKNOWN: Directs optimizer to use average distribution vector statistics instead of sniffing.WITH RECOMPILE(Statement or SP level): Forces plan compilation on every execution (good for complex reporting queries that run infrequently).- Local Variable Copy Trick: Copying parameters to local variables inside the procedure hides them from the sniffer.
Parameter sniffing causes sudden unexplained latency spikes on previously fast stored procedures after an index rebuild, server restart, or plan cache eviction.-- 1. SOLUTION A: OPTIMIZE FOR UNKNOWN (Best all-rounder)
CREATE OR ALTER PROCEDURE dbo.usp_GetOrdersByStatus
@Status VARCHAR(20)
AS
BEGIN
SELECT OrderID, CustomerID, TotalAmount
FROM dbo.Orders
WHERE Status = @Status
OPTION (OPTIMIZE FOR UNKNOWN); -- Ignores sniffed values, uses statistical average!
END;
GO
-- 2. SOLUTION B: Statement-level RECOMPILE (Best for high-variance reporting queries)
CREATE OR ALTER PROCEDURE dbo.usp_SearchOrders
@CustomerID INT = NULL,
@DateFrom DATETIME2 = NULL
AS
BEGIN
SELECT OrderID, TotalAmount
FROM dbo.Orders
WHERE (@CustomerID IS NULL OR CustomerID = @CustomerID)
AND (@DateFrom IS NULL OR OrderDate >= @DateFrom)
OPTION (RECOMPILE); -- Compiles a custom optimal plan specifically for each call!
END;How do SQL Server Statistics work, and why do outdated statistics lead to bad execution plans?
Statistics are the brains of the Cost-Based Query Optimizer:
- Histogram: Samples data and divides it into up to 200 steps (buckets). Each step records the boundary key value (
RANGE_HI_KEY), exact equal rows (EQ_ROWS), distinct rows within range, and average duplicate frequency. - Density Vector: Measures uniqueness:
Density = 1 / Distinct Values. Used for cross-column correlation and unknown predicates. - Auto-Update Statistics: By default, SQL Server automatically flags statistics as stale when a modification threshold is reached:
- In older SQL Server: 500 rows + 20% of the table modified.
- In SQL Server 2016+: Uses dynamic decreasing threshold (
SQRT(500 * TableRows)) for large tables.
If statistics estimate 1 row when 1,000,000 actually exist, the optimizer allocates a tiny memory grant (1 MB) and chooses Nested Loops, causing huge spills to TempDB and disk thrashing.-- 1. Inspect Histogram steps for an index/column
DBCC SHOW_STATISTICS ('dbo.Orders', 'IX_Orders_CustomerID');
-- 2. Check when statistics were last updated and modification counter
SELECT
obj.name AS TableName,
stat.name AS StatName,
sp.last_updated AS LastUpdated,
sp.rows AS TotalRows,
sp.rows_sampled AS RowsSampled,
sp.modification_counter AS RowsModifiedSinceLastUpdate
FROM sys.stats stat
CROSS APPLY sys.dm_db_stats_properties(stat.object_id, stat.stats_id) sp
JOIN sys.objects obj ON stat.object_id = obj.object_id
WHERE obj.is_ms_shipped = 0
ORDER BY sp.modification_counter DESC;
-- 3. Updating Statistics with Full Scan (Most accurate)
UPDATE STATISTICS dbo.Orders IX_Orders_CustomerID WITH FULLSCAN;What causes Index Fragmentation, and when should you REORGANIZE vs REBUILD an index?
Fragmentation degrades sequential I/O scanning performance across storage pages:
- Internal Fragmentation (Page Density): Pages have excessive empty white space (e.g. pages only 50% full). The engine must read twice as many 8 KB pages into memory to fetch the same data.
- External Fragmentation (Logical Out-of-Order): The logical order of pages in the B-Tree no longer matches the physical order on disk, turning sequential reads into random reads.
ALTER INDEX ... REORGANIZE:- Lightweight, defragments the leaf level by re-ordering existing pages in-place.
- Always an online operation (does not hold long-term exclusive table locks).
- Does NOT update statistics!
ALTER INDEX ... REBUILD:- Drops and reconstructs the index from scratch with brand new pages.
- Automatically updates statistics with equivalent of
WITH FULLSCAN. - Can be run
ONLINE = ONin Enterprise Edition.
Rebuilding indexes on SSD/NVMe drives is less critical for external fragmentation than old spinning disks, but rebuilding remains vital for restoring page density and updating statistics.-- 1. Query Index Fragmentation via DMV
SELECT
tbl.name AS TableName,
idx.name AS IndexName,
dmv.index_type_desc,
dmv.avg_fragmentation_in_percent,
dmv.page_count
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') dmv
JOIN sys.tables tbl ON dmv.object_id = tbl.object_id
JOIN sys.indexes idx ON dmv.object_id = idx.object_id AND dmv.index_id = idx.index_id
WHERE dmv.page_count > 1000 -- Ignore small tables under 1,000 pages
ORDER BY dmv.avg_fragmentation_in_percent DESC;
-- 2. Maintenance Decision Rules:
-- 5% to 30%: REORGANIZE (Online, fast)
ALTER INDEX IX_Orders_CustomerID ON dbo.Orders REORGANIZE;
-- > 30%: REBUILD (Recreates index, updates stats)
ALTER INDEX IX_Orders_CustomerID ON dbo.Orders
REBUILD WITH (ONLINE = ON, FILLFACTOR = 90);What are Filtered Indexes in SQL Server, and what are their advantages and limitations?
Filtered indexes are ideal when a column contains high data skew (e.g. 98% of rows are "Completed" and only 2% are "Pending"):
Key Advantages:
- Reduced Storage & RAM: Instead of indexing 10,000,000 rows, a filtered index on
WHERE Status = 'Pending'only indexes 200,000 rows, fitting entirely into the buffer pool. - Lower DML Maintenance Overhead:
INSERTorUPDATEoperations on "Completed" orders never touch the filtered index, saving write I/O. - Filtered Unique Indexes: Solves the ANSI NULL unique problem by allowing multiple NULLs:
WHERE NationalID IS NOT NULL.
Limitations:
- Cannot be used on parametrized queries inside stored procedures if the parameter prevents compile-time predicate matching (unless
OPTION (RECOMPILE)is used). - Requires specific
SEToptions (e.g.QUOTED_IDENTIFIER ON,ANSI_NULLS ON).
A filtered index on a 2% slice of a 50 GB table takes only 1 GB of disk space and stays permanently hot in RAM, eliminating disk I/O.-- 1. Create Filtered Index on Unprocessed Queue Records:
CREATE NONCLUSTERED INDEX IX_OrderQueue_Pending
ON dbo.OrderQueue (Priority, CreatedAt)
INCLUDE (Payload)
WHERE ProcessingStatus = 'Pending'; -- Only indexes active queue items!
-- 2. Query utilizing the Filtered Index:
-- The WHERE clause in the query MUST match or be a subset of the filtered index predicate!
SELECT Priority, CreatedAt, Payload
FROM dbo.OrderQueue
WHERE ProcessingStatus = 'Pending' AND Priority >= 3;
-- Execution Plan: 100% Index Seek on IX_OrderQueue_Pending!
-- 3. Filtered Unique Constraint allowing multiple NULLs:
CREATE UNIQUE NONCLUSTERED INDEX UQ_Users_SSN
ON dbo.Users (SocialSecurityNumber)
WHERE SocialSecurityNumber IS NOT NULL;What is a Columnstore Index, and why is it 10x-100x faster for analytical and reporting queries?
Columnstore indexes are the architectural foundation for modern Data Warehouses and HTAP (Hybrid Transactional/Analytical Processing):
- Columnar Storage: If a table has 100 columns and a report runs
SELECT SUM(Revenue) FROM Sales, a rowstore engine must read all 100 columns off disk. A columnstore engine reads only the 1 single Revenue column, eliminating 99% of disk I/O! - Column Compression (VertiPaq Engine): Similar data values in the same column compress by up to 10x using dictionary encoding, run-length encoding (RLE), and bit-packing.
- Batch Mode Execution: Processes chunks of up to 900 rows at once using modern CPU vector registers, rather than row-by-row (Row Mode), dramatically slashing CPU cycle consumption.
Clustered Columnstore tables compress storage footprint from 100 GB to ~10 GB and execute multi-million row aggregations in sub-second times using Batch Mode.-- 1. Create Analytical Fact Table with Clustered Columnstore Index
CREATE TABLE dbo.FactSales (
SalesKey BIGINT IDENTITY(1,1),
DateKey INT NOT NULL,
CustomerKey INT NOT NULL,
ProductKey INT NOT NULL,
Quantity INT NOT NULL,
SalesAmount DECIMAL(18,2) NOT NULL,
DiscountAmount DECIMAL(18,2) NOT NULL,
CONSTRAINT PK_FactSales PRIMARY KEY NONCLUSTERED (SalesKey)
);
-- Convert the table into high-performance columnar storage:
CREATE CLUSTERED COLUMNSTORE INDEX CCI_FactSales
ON dbo.FactSales;
-- 2. Analytical Aggregation Query (Runs in Batch Mode with 10x-100x speedup):
SELECT
DateKey,
COUNT(*) AS TotalTransactions,
SUM(SalesAmount) AS TotalRevenue,
AVG(SalesAmount) AS AverageTicket
FROM dbo.FactSales
WHERE DateKey >= 20260101
GROUP BY DateKey
ORDER BY DateKey;How do you read a SQL Server Execution Plan? What do Fat Pipes, Warning Icons, and Spills to TempDB mean?
Interpreting Graphical Execution Plans in SSMS is the #1 query diagnostic skill:
- Flow Direction: Operations begin at the far right (scans, seeks) and flow through joins and aggregations to the
SELECTnode at the top-left. - Fat Pipes (Arrow Thickness): The width of an arrow represents the number of rows passing between operators. If an arrow is thin (1 row estimated) but turns into a massive fat pipe (1,000,000 rows actual), you have identified a severe Cardinality Estimation Error.
- Yellow Warning Icons (⚠️):
- Sort / Hash Spill to TempDB: The query was given too small a memory grant and was forced to dump intermediate sort/hash worktables to physical disk in
TempDB. - Type Conversion in Expression: Signals an implicit conversion that prevented an index seek.
- Columns With No Statistics: Optimizer guessed row counts without distribution data.
- Sort / Hash Spill to TempDB: The query was given too small a memory grant and was forced to dump intermediate sort/hash worktables to physical disk in
- Missing Index Warning: SSMS displays green text at the top of the plan suggesting an index with an estimated improvement percentage.
In `SET STATISTICS IO`, look at 'logical reads' (pages read from RAM buffer). 1 logical read = 8 KB. 100,000 logical reads = 800 MB of data read.-- Enable detailed statistics output in Messages tab:
SET STATISTICS IO ON; -- Displays Logical Reads, Physical Reads, TempDB spills
SET STATISTICS TIME ON; -- Displays CPU Time vs Elapsed Time
-- Enable Actual Execution Plan XML output:
-- (In SSMS: Press Ctrl + M or click 'Include Actual Execution Plan')
SELECT o.CustomerID, c.CustomerName, SUM(o.TotalAmount) AS TotalRevenue
FROM dbo.Customers c
INNER JOIN dbo.Orders o ON c.CustomerID = o.CustomerID
WHERE o.OrderDate >= '2026-01-01'
GROUP BY o.CustomerID, c.CustomerName
ORDER BY TotalRevenue DESC;
SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;Explain the ACID Properties in SQL Server and how the storage engine enforces them.
Understanding how SQL Server's storage engine enforces ACID under the hood:
- Atomicity: All modifications within a transaction either succeed completely or are rolled back. Enforced using the Transaction Log (.ldf) and the Undo phase of crash recovery.
- Consistency: The database transitions from one valid state to another. Enforced by constraints (Primary Keys, Foreign Keys, CHECK constraints, Unique constraints, and Data Types).
- Isolation: Controls how changes made by one transaction are visible to concurrent transactions. Enforced by the Lock Manager (shared, exclusive, update locks) or Row Versioning in the TempDB Version Store.
- Durability: Once a transaction commits, its modifications are permanently recorded and will not be lost even during sudden power failure. Enforced by Write-Ahead Logging (WAL): log records are flushed to disk synchronously before the commit acknowledgment is sent to the client. Dirty data pages can be written to disk asynchronously later during Checkpoints.
Write-Ahead Logging ensures that data pages in memory (buffer pool) do not need to be written to disk synchronously on commit, enabling SQL Server to achieve high transaction throughput.-- Best Practice for Production Transactions:
SET XACT_ABORT ON; -- Automatically rolls back entire transaction on any fatal error!
BEGIN TRANSACTION;
BEGIN TRY
-- 1. Deduct funds from Account A
UPDATE dbo.Accounts
SET Balance = Balance - 500.00
WHERE AccountID = 101;
-- 2. Add funds to Account B
UPDATE dbo.Accounts
SET Balance = Balance + 500.00
WHERE AccountID = 202;
-- Commit both operations atomically:
COMMIT TRANSACTION;
PRINT 'Transfer completed successfully.';
END TRY
BEGIN CATCH
-- On any constraint violation or error, roll back atomically:
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
PRINT 'Error encountered: ' + ERROR_MESSAGE();
THROW; -- Rethrow exception for caller
END CATCH;Compare SQL Server Transaction Isolation Levels: Read Uncommitted, Read Committed, Repeatable Read, and Serializable.
Isolation levels define the trade-off between concurrency and data consistency:
| Isolation Level | Dirty Reads? | Non-Repeatable Reads? | Phantom Reads? | Locking Mechanism |
|---|---|---|---|---|
| Read Uncommitted (NOLOCK) | Allowed | Allowed | Allowed | Acquires no Shared (S) locks; ignores Exclusive (X) locks |
| Read Committed (Default) | Prevented | Allowed | Allowed | Acquires Shared (S) locks, releases immediately after statement |
| Repeatable Read | Prevented | Prevented | Allowed | Acquires Shared (S) locks, holds until transaction ends (COMMIT) |
| Serializable | Prevented | Prevented | Prevented | Acquires Key-Range locks, holds until transaction ends |
The Danger of WITH (NOLOCK): Many developers litter production queries with WITH (NOLOCK) to prevent blocking. This can cause queries to read uncommitted dirty data that is later rolled back, read rows twice due to page splits, or crash with Error 601 (data page unreadable).
Serializable isolation guarantees complete consistency but has the lowest concurrency and highest risk of deadlocks and blocking. Use optimistic concurrency (RCSI) instead.-- 1. Setting session isolation level
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION;
-- Shared locks acquired on matching rows are HELD until COMMIT!
SELECT ProductID, StockQty FROM dbo.Inventory WHERE ProductID = 50;
-- Another session trying to UPDATE Product 50 will BLOCK until this commits!
WAITFOR DELAY '00:00:05';
COMMIT TRANSACTION;
-- 2. Setting Serializable isolation (Guarantees absolute strict consistency)
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
-- Key-Range locks prevent other sessions from INSERTING new rows between 100 and 200!
SELECT * FROM dbo.Orders WHERE OrderID BETWEEN 100 AND 200;
COMMIT TRANSACTION;What is Read Committed Snapshot Isolation (RCSI), and how does it eliminate reader-writer blocking?
In standard pessimistic READ COMMITTED, a writer acquiring an Exclusive (X) lock blocks all readers acquiring Shared (S) locks until the transaction completes, causing massive concurrency bottlenecks.
How RCSI Solves Reader-Writer Blocking:
- When an
UPDATEorDELETEoccurs, the storage engine copies the previous committed version of the row into the Version Store inside TempDB. - A 14-byte pointer is added to the data page row header pointing to the version in TempDB.
- When a concurrent
SELECTquery runs, it reads the row version as it existed at the start of the statement without acquiring shared locks! - Zero Blocking: Writers continue updating while readers continue reading without blocking each other.
Snapshot Isolation vs RCSI:
- RCSI (Database level): Readers see committed data as of the start of the statement. Requires no application code changes.
- Snapshot Isolation (Session level): Readers see data as of the start of the entire transaction. Detects write conflicts (Error 3960).
RCSI increases TempDB storage and I/O due to version generation. Make sure TempDB is hosted on ultra-fast NVMe storage before enabling RCSI.-- 1. Enable RCSI on Database (Must be done when no other active connections exist)
ALTER DATABASE SalesDB
SET READ_COMMITTED_SNAPSHOT ON
WITH ROLLBACK IMMEDIATE;
-- 2. Verify that RCSI is enabled:
SELECT name, is_read_committed_snapshot_on, snapshot_isolation_state_desc
FROM sys.databases
WHERE name = 'SalesDB';
-- 3. Monitoring the Version Store in TempDB:
SELECT
transaction_sequence_num,
commit_sequence_num,
elapsed_time_seconds
FROM sys.dm_tran_active_snapshot_database_transactions;What is a Deadlock in SQL Server, how does the engine resolve it, and how do you capture a Deadlock Graph?
A classic circular deadlock scenario:
- Transaction A: Holds lock on Table 1, requests lock on Table 2.
- Transaction B: Holds lock on Table 2, requests lock on Table 1.
- Neither transaction can proceed; both are frozen waiting on each other indefinitely.
Deadlock Resolution & Priorities:
SQL Server runs a background thread every 5 seconds. It builds a dependency wait-for graph, detects the cycle, and chooses the transaction with the least CPU/log rollback cost as the Deadlock Victim. You can override victim priority using SET DEADLOCK_PRIORITY LOW | NORMAL | HIGH.
Capturing Deadlock Graphs:
- Extended Events (Best): The default
system_healthextended events session automatically captures deadlock graphs with zero configuration! - Trace Flags: Enabling Trace Flag 1222:
DBCC TRACEON (1222, -1);writes readable XML deadlock graphs to the SQL Server Error Log.
To prevent deadlocks: 1) Always access tables in the exact same alphabetical or logical order across all stored procedures. 2) Keep transactions short. 3) Enable RCSI.-- Querying the default system_health session for Deadlock Graphs:
SELECT
XEventData.XEvent.value('@timestamp', 'datetime2') AS [DeadlockTime],
XEventData.XEvent.query('(data[@name="xml_report"]/value/deadlock)[1]') AS [DeadlockGraphXML]
FROM (
SELECT CAST(target_data AS XML) AS TargetData
FROM sys.dm_xe_session_targets st
JOIN sys.dm_xe_sessions s ON s.address = st.event_session_address
WHERE s.name = 'system_health' AND st.target_name = 'ring_buffer'
) AS Data
CROSS APPLY TargetData.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(XEvent)
ORDER BY DeadlockTime DESC;
-- Setting Deadlock Priority on batch processing workers:
SET DEADLOCK_PRIORITY LOW; -- If deadlocked with interactive web user, sacrifice batch worker!What is Lock Escalation in SQL Server, when does it occur, and how do you prevent it?
Every lock in SQL Server consumes memory (~96 to 128 bytes in the lock manager). If a transaction modifies 100,000 rows, tracking 100,000 individual row locks would consume tens of megabytes of memory.
The Escalation Threshold:
When a single T-SQL statement reaches approximately 5,000 locks on a single table index, SQL Server attempts to escalate the fine-grained locks to a full Table Lock (X or S).
The Problem with Lock Escalation:
An Exclusive Table Lock (TABLOCKX) blocks all other users from reading or writing to the entire table for the duration of the transaction, paralyzing high-traffic OLTP applications!
How to Control Lock Escalation:
- Batching Modifications: Break mass updates or deletes into chunks of 4,000 rows.
ALTER TABLE ... SET (LOCK_ESCALATION = AUTO): For partitioned tables, escalates to the partition level instead of the whole table.ALTER TABLE ... SET (LOCK_ESCALATION = DISABLE): Completely disables lock escalation (use with caution to prevent lock memory exhaustion).
Chunking updates below 5,000 rows keeps locks at the row level, prevents transaction log blowout, and allows concurrent queries to execute unimpeded.-- ANTI-PATTERN: Updates 500,000 rows in one statement!
-- Triggers Lock Escalation -> Table Lock -> System-wide Blocking!
-- UPDATE dbo.Orders SET IsArchived = 1 WHERE OrderDate < '2020-01-01';
-- SAFE ARCHITECTURE: Chunked Batching (Prevents Lock Escalation)
DECLARE @BatchSize INT = 4000; -- Kept below the 5,000 lock threshold!
DECLARE @RowsAffected INT = 1;
WHILE @RowsAffected > 0
BEGIN
BEGIN TRANSACTION;
UPDATE TOP (@BatchSize) dbo.Orders
SET IsArchived = 1
WHERE OrderDate < '2020-01-01' AND IsArchived = 0;
SET @RowsAffected = @@ROWCOUNT;
COMMIT TRANSACTION;
-- Brief pause to allow concurrent OLTP transactions to execute:
WAITFOR DELAY '00:00:00.050';
END;Explain the differences between Dirty Reads, Non-Repeatable Reads, and Phantom Reads.
These three concurrency phenomena represent increasing levels of isolation violations:
- Dirty Read (Read Uncommitted): Transaction 1 updates a customer's balance to $0. Transaction 2 reads balance = $0. Transaction 1 fails and rolls back. Transaction 2 acted on phantom data that never legally committed.
- Non-Repeatable Read (Fuzzy Read): Transaction 1 reads row A (Salary = $50,000). Transaction 2 updates row A (Salary = $60,000) and commits. Transaction 1 reads row A again and sees Salary = $60,000. The same row returned two different values within the same transaction! Prevented by
REPEATABLE READ. - Phantom Read: Transaction 1 executes
SELECT * FROM Employees WHERE DeptID = 10(returns 5 rows). Transaction 2 inserts a 6th employee into Dept 10 and commits. Transaction 1 re-runs the exact same query and now sees 6 rows! The new row appeared like a "phantom". Prevented bySERIALIZABLErange locking.
Eliminating phantom reads with SERIALIZABLE requires Key-Range locks (`RangeS-S`), which lock nonexistent gaps between keys, severely reducing insert concurrency.-- Session 1: Repeatable Read prevents Non-Repeatable Read, but ALLOWS Phantom Reads!
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION;
-- Query 1: Returns count of orders over $1000 (e.g. 10 rows)
SELECT COUNT(*) AS [InitialCount] FROM dbo.Orders WHERE TotalAmount > 1000;
-- While Session 1 waits, Session 2 INSERTS a new order with TotalAmount = 1500!
WAITFOR DELAY '00:00:05';
-- Query 2: Re-reading the range now returns 11 rows! (A PHANTOM ROW APPEARED!)
SELECT COUNT(*) AS [PhantomCount] FROM dbo.Orders WHERE TotalAmount > 1000;
COMMIT TRANSACTION;
-- To prevent Phantom Reads, Session 1 MUST use SERIALIZABLE isolation:
-- SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- Places RangeS-S locks on key ranges!What causes TempDB Allocation Contention (PFS, GAM, SGAM pages), and how do you resolve it?
TempDB is a globally shared instance-wide resource used by all databases for temp tables, table variables, spills, and row versioning:
- PFS (Page Free Space): Tracks byte allocation and free space for every 8,000 pages (~64 MB).
- GAM (Global Allocation Map): Tracks allocated extents (blocks of 8 pages = 64 KB).
- SGAM (Shared Global Allocation Map): Tracks mixed extents.
The Contention Bottleneck:
When hundreds of connections execute stored procedures creating #temp tables simultaneously, all threads compete to modify page 2:1:1 (PFS) or 2:1:3 (SGAM), resulting in heavy PAGELATCH_UP wait states.
Microsoft Best Practices to Resolve:
- Multiple Equally-Sized Data Files: Create 1 data file per logical CPU core (up to 8 files). SQL Server uses a round-robin allocation algorithm across files, distributing allocation requests across 8 separate PFS/GAM pages.
- Trace Flag 1118 (Full Extents): Forces SQL Server to allocate dedicated extents immediately, completely bypassing SGAM pages (made default behavior in SQL Server 2016+).
- Memory-Optimized TempDB Metadata (SQL Server 2019+): Moves system tables tracking TempDB objects into in-memory non-blocking tables.
Configuring 8 equally-sized TempDB data files with uniform autogrowth eliminates allocation latch contention and maximizes disk I/O parallelism.-- Check for PAGELATCH waits on TempDB Allocation Pages (Database ID = 2)
SELECT
session_id,
wait_type,
wait_duration_ms,
resource_description,
-- Decode page type: Page 1 = PFS, Page 2 = GAM, Page 3 = SGAM
CASE
WHEN resource_description LIKE '2:%:1' THEN 'PFS Allocation Contention'
WHEN resource_description LIKE '2:%:2' THEN 'GAM Allocation Contention'
WHEN resource_description LIKE '2:%:3' THEN 'SGAM Allocation Contention'
ELSE 'Data Page Latch'
END AS ContentionType
FROM sys.dm_os_waiting_tasks
WHERE wait_type LIKE 'PAGELATCH_%' AND resource_description LIKE '2:%';
-- Enable Memory-Optimized TempDB Metadata (SQL Server 2019+)
ALTER SERVER CONFIGURATION SET MEMORY_OPTIMIZED TEMPDB_METADATA = ON;
-- (Requires SQL Server instance restart to take effect)What are Wait Statistics (sys.dm_os_wait_stats), and what do CXPACKET, PAGEIOLATCH_SH, and LCK_M_* waits indicate?
Whenever a SQL Server thread cannot continue execution, it enters a Wait State and increments the engine's cumulative wait statistics counters in sys.dm_os_wait_stats:
CXPACKET/CXCONSUMER(Parallelism): Occurs when a query executes across multiple CPU threads. The coordinator thread waits for parallel worker threads to finish.- Action: If paired with high CPU, tune Cost Threshold for Parallelism (increase from default 5 to 50) and adjust MAXDOP (Maximum Degree of Parallelism).
PAGEIOLATCH_SH(Storage / Buffer Pool Pressure): A query needs an 8 KB data page that is NOT currently in the buffer pool RAM and must wait for physical disk to read it into memory.- Action: Look for missing indexes (causing table scans), outdated statistics, or server-wide memory starvation.
LCK_M_*(Lock Contention): A query is blocked waiting for an exclusive or shared lock held by another uncommitted transaction.- Action: Identify head blockers, reduce transaction duration, or enable RCSI.
ASYNC_NETWORK_IO(Client Bottleneck): SQL Server has retrieved query data, but the client application is processing rows too slowly before acknowledging the stream.
Signal wait time measures how long a thread waited on the runnable queue after its resource was available. If Signal Waits > 20% of total wait time, the server has CPU pressure.-- Top Cumulative Waits on SQL Server Instance
WITH Waits AS (
SELECT
wait_type,
wait_time_ms / 1000.0 AS WaitTime_Sec,
(wait_time_ms - signal_wait_time_ms) / 1000.0 AS ResourceWait_Sec,
signal_wait_time_ms / 1000.0 AS SignalWait_Sec, -- CPU wait queue time
waiting_tasks_count,
100.0 * wait_time_ms / SUM(wait_time_ms) OVER() AS Percentage
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN (
-- Filter out harmless background system idle waits:
'CLR_AUTO_EVENT', 'DISPATCHER_QUEUE_SEMAPHORE', 'FT_IFTS_SCHEDULER_IDLE_WAIT',
'LAZYWRITER_SLEEP', 'LOGMGR_QUEUE', 'REQUEST_FOR_DEADLOCK_SEARCH',
'SLEEP_TASK', 'SLEEP_SYSTEMTASK', 'SQLTRACE_BUFFER_FLUSH', 'WAITFOR',
'HADR_FILESTREAM_IOMASTER_IOCOMPLETION', 'CHECKPOINT_QUEUE', 'XE_TIMER_EVENT'
)
)
SELECT TOP 10
wait_type,
CAST(WaitTime_Sec AS DECIMAL(12,2)) AS WaitTime_Seconds,
CAST(Percentage AS DECIMAL(5,2)) AS WaitPercentage
FROM Waits
ORDER BY Percentage DESC;What is Query Store in SQL Server, and how do you use it to detect and force good execution plans?
Prior to Query Store (SQL Server 2016+), troubleshooting plan regressions required capturing volatile DMV snapshots before the plan cache was cleared.
How Query Store Solves Plan Regressions:
- Captures a full historical log of every query text, its compiled execution plans, and its actual runtime metrics (CPU time, duration, logical reads, memory grants).
- Persists data in internal tables inside the user database, surviving server restarts and failovers.
- Forcing Plans: If a query suddenly regresses because the optimizer chose Plan 2 (bad parameter sniff) instead of Plan 1 (good index seek), you can force Plan 1 permanently using
sp_query_store_force_plan. - Automatic Plan Correction: In SQL Server 2017+ Enterprise, SQL Server detects plan regressions automatically and forces the last good plan without human intervention.
Query Store introduces minimal overhead (~1-2% CPU), but provides complete auditability of performance regressions caused by server upgrades or index changes.-- 1. Enable Query Store on the database:
ALTER DATABASE SalesDB SET QUERY_STORE = ON (
OPERATION_MODE = READ_WRITE,
CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 30),
DATA_FLUSH_INTERVAL_SECONDS = 900,
MAX_STORAGE_SIZE_MB = 2048
);
-- 2. Identify Top Regressed Queries using Query Store DMVs:
SELECT TOP 5
q.query_id,
qt.query_sql_text,
p.plan_id,
rs.avg_duration / 1000.0 AS AvgDuration_ms,
rs.avg_cpu_time / 1000.0 AS AvgCPU_ms,
rs.count_executions
FROM sys.query_store_query q
JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id
JOIN sys.query_store_plan p ON q.query_id = p.query_id
JOIN sys.query_store_runtime_stats rs ON p.plan_id = rs.plan_id
ORDER BY rs.avg_duration DESC;
-- 3. Force a known good execution plan (e.g. Query 45, Force Plan 102):
EXEC sp_query_store_force_plan @query_id = 45, @plan_id = 102;
-- To unforce:
-- EXEC sp_query_store_unforce_plan @query_id = 45, @plan_id = 102;How do you optimize High-Volume Bulk Inserts in SQL Server using Minimal Logging and TABLOCK?
Inserting 100 million rows using standard INSERT INTO ... VALUES creates catastrophic transaction log growth, locks the database, and takes hours.
Prerequisites for Minimal Logging:
- Database Recovery Model must be Simple or Bulk-Logged.
- The target table must use the
WITH (TABLOCK)hint (or BCP with-h TABLOCK). This signals to the storage engine that it can bypass row locks and log page/extent allocations directly. - If the target table has a Clustered Index, it must be empty, or inserts must be strictly ordered to match the clustering key.
- Target table must not have active non-clustered indexes during ingestion (drop non-clustered indexes before bulk insert, re-create them after).
Minimally logged bulk inserts run 5x to 10x faster and reduce transaction log generation by up to 90%, preventing LDF drive space exhaustion.-- 1. Using BULK INSERT with TABLOCK and optimal batch sizing
BULK INSERT dbo.StagingTelemetry
FROM 'D:\DataFeeds\telemetry_2026_09.csv'
WITH (
DATAFILETYPE = 'char',
FIELDTERMINATOR = ',',
ROWTERMINATOR = '
',
FIRSTROW = 2,
BATCHSIZE = 100000, -- Commit every 100,000 rows (prevents massive single transaction)
TABLOCK -- Critical: Enables Minimal Logging!
);
-- 2. Fast SELECT INTO (Minimally logged by default):
SELECT CustomerID, AccountNumber, Balance
INTO dbo.AccountsBackup -- Creates and populates new table with minimal logging
FROM dbo.Accounts
WHERE IsActive = 1;Explain SQL Server Recovery Models (Full, Simple, Bulk-Logged) and why the Transaction Log grows endlessly.
The database recovery model dictates transaction log retention and disaster recovery capabilities:
| Recovery Model | Log Truncation Trigger | Point-in-Time Restore? | Workload Suitability |
|---|---|---|---|
| Simple | Automatically truncated during CHECKPOINT | No (Can only restore to last Full/Diff backup) | Development, test environments, read-only data warehouses |
| Full | Only truncated during a TRANSACTION LOG BACKUP | Yes (Restore to exact second or LSN) | Mission-critical production enterprise databases |
| Bulk-Logged | Only truncated during a log backup (Minimally logs bulk operations) | Yes (Except if log backup covers bulk operations) | Large periodic data ingestion pipelines |
Why the Transaction Log Grows to 100% Disk Space:
In FULL recovery, committed transactions remain in the log file (.ldf) until a dedicated BACKUP LOG command runs. If an admin creates a database in Full recovery but only takes daily Full backups, the transaction log will expand continuously until the physical drive runs out of disk space!
Virtual Log Files (VLFs): If a log file grows in hundreds of small increments, it creates thousands of tiny VLFs, severely degrading database startup, backup, and restore times.-- 1. Check why the transaction log cannot truncate:
SELECT
name AS DatabaseName,
recovery_model_desc AS RecoveryModel,
log_reuse_wait_desc AS LogReuseWaitReason
FROM sys.databases
WHERE name = 'SalesDB';
-- If log_reuse_wait_desc = 'LOG_BACKUP', you MUST take a transaction log backup!
-- If log_reuse_wait_desc = 'ACTIVE_TRANSACTION', an open uncommitted transaction is holding the log!
-- 2. Check physical log space utilization:
DBCC SQLPERF(LOGSPACE);
-- 3. The correct fix for Full Recovery: Schedule regular log backups (e.g. every 15 min)
BACKUP LOG SalesDB
TO DISK = 'D:\Backups\SalesDB_Log.trn'
WITH COMPRESSION;Design a Production Backup Strategy and walk through a Point-in-Time Recovery.
Disaster Recovery depends on understanding RPO (Recovery Point Objective - acceptable data loss) and RTO (Recovery Time Objective - acceptable downtime):
Scenario: A developer accidentally runs an unconstrained DELETE FROM Customers at 2026-09-26 14:32:15. How do you restore the database to 14:32:00 (15 seconds before the disaster)?
- Take a Tail-Log Backup with
NORECOVERYimmediately to capture transactions up to the present moment without allowing new writes. - Restore the last Full Backup using
WITH NORECOVERY. - Restore the latest Differential Backup taken prior to 14:32:00 using
WITH NORECOVERY. - Restore subsequent Transaction Log Backups in sequence using
WITH NORECOVERY. - Restore the final log backup using
STOPAT = '2026-09-26 14:32:00'andWITH RECOVERY.
Differential backups reduce RTO restore times because you only restore one differential file instead of dozens of transaction logs that accumulated since the full backup.-- 1. Step 1: Capture Tail-Log Backup (Prevents data loss of transactions up to now)
BACKUP LOG SalesDB
TO DISK = 'D:\Backups\SalesDB_TailLog.trn'
WITH NORECOVERY; -- Puts database in restoring state, locks out users!
-- 2. Step 2: Restore Last Full Backup
RESTORE DATABASE SalesDB
FROM DISK = 'D:\Backups\SalesDB_Full_20260920.bak'
WITH NORECOVERY, REPLACE;
-- 3. Step 3: Restore Last Differential Backup (Taken midnight prior to disaster)
RESTORE DATABASE SalesDB
FROM DISK = 'D:\Backups\SalesDB_Diff_20260926_0000.bak'
WITH NORECOVERY;
-- 4. Step 4: Restore Intermediate Log Backups
RESTORE LOG SalesDB
FROM DISK = 'D:\Backups\SalesDB_Log_20260926_1400.trn'
WITH NORECOVERY;
-- 5. Step 5: Restore Final Log Backup to the exact second BEFORE the accidental delete!
RESTORE LOG SalesDB
FROM DISK = 'D:\Backups\SalesDB_TailLog.trn'
WITH STOPAT = '2026-09-26 14:32:00', RECOVERY; -- Restores database to full online access!Compare Always On Availability Groups, Failover Cluster Instances (FCI), and Log Shipping.
Modern high availability and disaster recovery architectures in enterprise SQL Server:
| Feature | Always On Availability Groups | Failover Cluster Instances (FCI) | Log Shipping |
|---|---|---|---|
| Protection Scope | Database level (Selected DBs) | Instance level (Entire SQL Server instance) | Database level |
| Shared Storage Required? | No (Independent local disks on each node) | Yes (Shared SAN, CSV, or SMB storage) | No (Independent disks) |
| Secondary Node Readable? | Yes (Can offload read-only queries & backups!) | No (Passive node is completely offline) | Yes (Read-Only / Standby mode) |
| Failover Time | Near-instantaneous (<5–10 seconds) | 30–90 seconds (Service restart time) | Manual / Several minutes |
| Data Loss Risk (RPO) | Zero (Synchronous Commit) or near-zero (Async) | Zero (Shared disk data is identical) | Minutes (Lag of last log copy) |
| Network Requirement | Low latency for Synchronous; works over WAN for Async | High-speed local cluster network (LAN) | High or low latency WAN |
Offloading reporting and backups to readable secondary replicas frees up 100% of buffer pool RAM and CPU on the primary replica for active OLTP writes.-- Configure Read-Only Routing so reporting queries automatically route to Secondary Replicas!
-- Run on Primary Replica:
ALTER AVAILABILITY GROUP [AG_Sales]
MODIFY REPLICA ON N'SQL-NODE-01' WITH
(
SECONDARY_ROLE (ALLOW_CONNECTIONS = READ_ONLY),
READ_ONLY_ROUTING_URL = N'TCP://SQL-NODE-01.corp.internal:1433'
);
ALTER AVAILABILITY GROUP [AG_Sales]
MODIFY REPLICA ON N'SQL-NODE-02' WITH
(
SECONDARY_ROLE (ALLOW_CONNECTIONS = READ_ONLY),
READ_ONLY_ROUTING_URL = N'TCP://SQL-NODE-02.corp.internal:1433'
);
-- Define Priority Routing List:
ALTER AVAILABILITY GROUP [AG_Sales]
MODIFY REPLICA ON N'SQL-NODE-01' WITH
(PRIMARY_ROLE (READ_ONLY_ROUTING_LIST = (N'SQL-NODE-02', N'SQL-NODE-01')));
-- In Client Connection String, add:
-- "Server=tcp:AGListener,1433;Database=SalesDB;ApplicationIntent=ReadOnly;"
-- SQL Server automatically directs read queries to SQL-NODE-02!Scenario: The CPU is pegged at 100% on a production SQL Server. What are your immediate diagnostic steps?
When production CPU hits 100%, you must act systematically within 60 seconds without restarting the server:
Step-by-Step Incident Response Protocol:
- Isolate the Process: Check Windows Task Manager or
sys.dm_os_ring_buffersto confirmsqlservr.exeis responsible rather than anti-virus or backup software. - Find Active Requests: Query
sys.dm_exec_requestscross-applied withsys.dm_exec_sql_textto see which queries are burning CPU cycles right now. - Inspect Wait Types:
SOS_SCHEDULER_YIELD: Query is burning raw CPU in a loop (massive scans, scalar UDFs, non-sargable functions).CXPACKETwith high CPU: Parallel query skew where one thread is stuck processing unbalanced data.RESOURCE_SEMAPHORE: High memory grants are causing queries to queue, spinning CPU.
- Extract Execution Plans: Pass the
plan_handleintosys.dm_exec_query_planto see if a missing index or parameter sniff caused a table scan.
High CPU is rarely solved by adding more CPU cores; in 90% of production cases, high CPU is caused by a missing index that forces millions of rows into an in-memory hash scan.-- EMERGENCY SCRIPT: Identify High-CPU Queries Running Right Now
SELECT
r.session_id,
r.status,
r.cpu_time AS [CPUTime_ms],
r.total_elapsed_time AS [Duration_ms],
r.logical_reads AS [LogicalReads],
r.wait_type,
r.wait_time,
SUBSTRING(t.text, (r.statement_start_offset/2) + 1,
(((CASE r.statement_end_offset
WHEN -1 THEN DATALENGTH(t.text)
ELSE r.statement_end_offset END) - r.statement_start_offset)/2) + 1) AS [CurrentExecutingQuery],
p.query_plan AS [ExecutionPlan]
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) p
WHERE r.session_id > 50 -- Exclude system background sessions
ORDER BY r.cpu_time DESC;
-- Emergency Kill of Rogue Session (e.g. Session 85 running unbounded cross join):
-- KILL 85;Scenario: A query that normally runs in 200ms suddenly takes 45 seconds today. How do you diagnose it?
When an existing query suddenly degrades with zero application code changes, investigate four root causes:
- Parameter Sniffing / Plan Regression (Most Common): An index rebuild, statistics update, or server reboot purged the plan cache. The query recompiled with an abnormal parameter value, generating a bad plan (e.g. Key Lookups instead of Clustered Index Scan).
- Check: Query Store to compare the historical fast plan vs the new slow plan.
- Blocking & Lock Waits: The query is fast, but it spent 44.8 seconds waiting in a
LCK_M_*wait state behind an open uncommitted transaction.- Check: Compare
CPU timevsElapsed time. If CPU time is 100ms but Elapsed time is 45 seconds, the query was blocked!
- Check: Compare
- Stale Statistics: High data churn exceeded the modification threshold without statistics being refreshed.
- Server-Level Resource Starvation: Heavy parallel batch job or backup running simultaneously.
Always compare CPU time with Elapsed time. If Elapsed Time >> CPU Time, the query is blocked or waiting on disk I/O. If CPU Time == Elapsed Time, the query plan is inefficient.-- 1. Compare CPU Time vs Elapsed Time in Query History:
SELECT TOP 1
qs.execution_count,
qs.total_elapsed_time / qs.execution_count / 1000.0 AS AvgDuration_ms,
qs.total_worker_time / qs.execution_count / 1000.0 AS AvgCPUTime_ms,
(qs.total_elapsed_time - qs.total_worker_time) / qs.execution_count / 1000.0 AS AvgWaitTime_ms,
t.text AS QueryText
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) t
WHERE t.text LIKE '%dbo.SearchCatalog%'
ORDER BY qs.last_execution_time DESC;
-- DIAGNOSIS RULE:
-- If AvgWaitTime_ms is 99% of total time -> IT IS A BLOCKING OR DISK I/O ISSUE!
-- If AvgCPUTime_ms is 99% of total time -> IT IS PARAMETER SNIFFING OR TABLE SCAN!Scenario: Users report application timeouts due to blocking. How do you find the head blocker and resolve it?
In a major blocking cascade, 50 queries might be blocked, but 49 of them are victims waiting in line. You must locate the Head Blocker (Root Blocker) at the very top of the chain:
- A user in SSMS ran
BEGIN TRAN; UPDATE Customers ...and left their desk for lunch without typingCOMMIT. - Their open transaction holds an Exclusive lock on the
Customerstable. - Dozens of incoming web application queries queue up waiting for the lock, eventually timing out.
- How to Identify: The Head Blocker has
blocking_session_id = 0(it is not blocked by anyone), but appears as theblocking_session_idfor multiple other sessions.
Enabling Read Committed Snapshot Isolation (RCSI) eliminates 90% of reader-writer blocking cascades in production OLTP databases.-- FINDING THE HEAD BLOCKER:
SELECT
r.session_id AS [BlockedSessionID],
r.blocking_session_id AS [BlockingSessionID],
r.wait_type AS [WaitType],
r.wait_time / 1000.0 AS [WaitTimeSeconds],
t.text AS [BlockedQueryText]
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.blocking_session_id <> 0;
-- FIND THE EXACT ROOT BLOCKER QUERY:
SELECT
s.session_id AS [HeadBlockerSessionID],
s.login_name,
s.host_name,
s.program_name,
s.status,
t.text AS [HeadBlockerLastSQL]
FROM sys.dm_exec_sessions s
LEFT JOIN sys.dm_exec_connections c ON s.session_id = c.session_id
CROSS APPLY sys.dm_exec_sql_text(c.most_recent_sql_handle) t
WHERE s.session_id IN (
-- Sessions that are blocking someone, but NOT blocked by anyone themselves!
SELECT blocking_session_id
FROM sys.dm_exec_requests
WHERE blocking_session_id <> 0
)
AND s.session_id NOT IN (
SELECT session_id
FROM sys.dm_exec_requests
WHERE blocking_session_id <> 0
);What is Table Partitioning in SQL Server, and how does Partition Switching achieve near-instant data archiving?
Partitioning manages tables containing hundreds of millions of rows (e.g. historical audit or telemetry data):
- Partition Function: Defines boundary values (e.g. monthly date boundaries:
LEFTvsRIGHT). - Partition Scheme: Maps the partitions created by the function to physical database Filegroups.
- Partition Elimination: When a query filters by the partition key (e.g.
WHERE OrderDate >= '2026-09-01'), SQL Server scans only that single partition, skipping millions of historical rows.
The Magic of Partition Switching:
Archiving 50,000,000 old records using DELETE takes hours and locks the database. With Partition Switching, you call ALTER TABLE Sales SWITCH PARTITION 1 TO Sales_Archive_2020. The engine simply updates internal metadata pointers in system tables. 50 million rows are archived in 50 milliseconds!
Partition switching is a pure DDL metadata operation. It generates virtually zero transaction log growth and does not move physical bytes on disk.-- 1. Create Partition Function (Monthly boundaries)
CREATE PARTITION FUNCTION pf_OrderDateRange (DATETIME2)
AS RANGE RIGHT FOR VALUES ('2026-01-01', '2026-02-01', '2026-03-01');
-- 2. Create Partition Scheme mapping to Primary filegroup
CREATE PARTITION SCHEME ps_OrderDateScheme
AS PARTITION pf_OrderDateRange ALL TO ([PRIMARY]);
-- 3. Create Partitioned Table
CREATE TABLE dbo.PartitionedOrders (
OrderID INT IDENTITY(1,1),
OrderDate DATETIME2 NOT NULL,
TotalAmount DECIMAL(12,2),
CONSTRAINT PK_PartitionedOrders PRIMARY KEY (OrderID, OrderDate) -- Partition key MUST be part of PK!
) ON ps_OrderDateScheme (OrderDate);
-- 4. PARTITION SWITCHING (Sub-second metadata swap):
-- Target staging table must have identical schema, same filegroup, and be empty:
ALTER TABLE dbo.PartitionedOrders
SWITCH PARTITION 1 TO dbo.Orders_Archived_Jan2026;
-- 50 million rows moved in 10 milliseconds with ZERO data copy!Compare Transparent Data Encryption (TDE), Always Encrypted, and Dynamic Data Masking.
Modern compliance (PCI-DSS, HIPAA, GDPR) requires layered database security:
| Feature | Where Encryption Happens | Who Can Read Plaintext? | Protects Against |
|---|---|---|---|
| Transparent Data Encryption (TDE) | Storage Engine (Physical disk .mdf / .ldf / .bak) | Applications, DBAs, sysadmins (Decrypted in RAM) | Stolen backup tapes, compromised hard drives |
| Always Encrypted | Client Application Driver (Before hitting network) | Client application only (Keys stored in Azure Key Vault / Windows Cert Store) | Rogue DBAs, cloud infrastructure operators, memory dump scraping |
| Dynamic Data Masking (DDM) | Query Layer (On-the-fly masking) | Privileged users; non-privileged see XXXX-XXXX-XXXX-1234 | Unauthorized front-desk staff or support agents viewing PII |
TDE adds ~3-5% CPU overhead during disk reads/writes. Modern Intel/AMD CPUs feature hardware AES-NI instructions that make encryption virtually seamless.-- 1. Transparent Data Encryption (TDE) Setup:
-- (In Master DB): Create Database Master Key and Server Certificate
USE master;
CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'StrongMasterKeyPassword#2026';
CREATE CERTIFICATE TDE_Cert WITH SUBJECT = 'TDE Database Encryption Certificate';
-- (In User DB): Create Database Encryption Key and Enable TDE
USE SalesDB;
CREATE DATABASE ENCRYPTION KEY
WITH ALGORITHM = AES_256
ENCRYPTION BY SERVER CERTIFICATE TDE_Cert;
ALTER DATABASE SalesDB SET ENCRYPTION ON; -- Storage pages on disk are now encrypted!
-- 2. Dynamic Data Masking (DDM) for PII Compliance:
ALTER TABLE dbo.Customers
ALTER COLUMN CreditCardNumber ADD MASKED WITH (FUNCTION = 'partial(0, "XXXX-XXXX-XXXX-", 4)');
ALTER TABLE dbo.Customers
ALTER COLUMN Email ADD MASKED WITH (FUNCTION = 'email()');
-- Standard users see masked data; DBAs with UNMASK permission see real data.How do you configure Database Mail, SQL Server Agent, and Automated Failover Alerts for DBAs?
A resilient database architecture requires automated alerting before outages impact users:
- Database Mail: Uses an external background process (
DatabaseMail.exe) to queue and dispatch emails via SMTP without stalling database worker threads. - SQL Server Agent: The task scheduling engine for automated backups, index maintenance, integrity checks (
DBCC CHECKDB), and alert triggers. - Critical DBA Alerts to Configure Immediately:
- Severity 17: Insufficient resources (memory, disk space).
- Severity 19: Fatal error in resource.
- Severity 20–25: System fatal errors, hardware faults, database corruption.
- Error 823 / 824 / 825: Physical storage subsystem corruption and I/O read retry warnings!
Database Mail runs outside the sqlservr.exe process space, ensuring that sending emails does not consume buffer pool memory or block client queries.-- 1. Create DBA Team Operator
EXEC msdb.dbo.sp_add_operator
@name = N'DBA_OnCall',
@enabled = 1,
@email_address = N'dba-alerts@rtsall.com';
-- 2. Create Alert for Error 825 (I/O Read Retry - EARLY WARNING OF DISK FAILURE!)
EXEC msdb.dbo.sp_add_alert
@name = N'Alert_Error_825_Disk_IO_Retry',
@message_id = 825,
@severity = 0,
@enabled = 1,
@delay_between_responses = 900, -- 15 minute throttle
@include_event_description_in = 1;
-- Link Alert to Operator:
EXEC msdb.dbo.sp_add_notification
@alert_name = N'Alert_Error_825_Disk_IO_Retry',
@operator_name = N'DBA_OnCall',
@notification_method = 1; -- Email
-- 3. Sending an email via Database Mail:
EXEC msdb.dbo.sp_send_dbmail
@profile_name = 'PrimaryAlertProfile',
@recipients = 'dba-alerts@rtsall.com',
@subject = 'URGENT: Production Server Alert',
@body = 'Database SalesDB experienced an unexpected failover.';Scenario: The Transaction Log disk is 99% full, queries are failing with Error 9002. How do you recover safely without breaking the LSN backup chain?
Error 9002: "The transaction log for database 'X' is full due to 'LOG_BACKUP'." All write queries in the entire application freeze.
Emergency Incident Recovery Plan:
- DO NOT SHRINK YET: Calling
DBCC SHRINKFILEwill do nothing because the log is full of active, unbacked-up records. - Inspect the Block Reason: Run
SELECT log_reuse_wait_desc FROM sys.databases WHERE name = 'SalesDB'.- If
LOG_BACKUP: Run an emergencyBACKUP LOG SalesDB TO DISK = 'E:\EmergencyBackups\log.trn' WITH COMPRESSION. Backing up the log marks inactive VLFs as reusable! - If
ACTIVE_TRANSACTION: An uncommitted transaction is holding the MinLSN. Find it withDBCC OPENTRANand kill the rogue session. - If
AVAILABILITY_GROUP: An AG secondary replica is disconnected or fallen behind. Resolve network connectivity to the replica.
- If
- Drive Completely Frozen (0 bytes free): If you cannot even take a backup because the disk has 0 bytes, add a temporary secondary log file on another drive:
ALTER DATABASE SalesDB ADD LOG FILE (...). The database unfreezes immediately, allowing you to back up and shrink.
Never set Autogrowth to 10% or 1 MB. Configure fixed MB autogrowth (e.g. 512 MB or 1024 MB) to prevent thousands of tiny Virtual Log Files (VLFs).-- 1. Check Root Cause of Log Full:
SELECT name, log_reuse_wait_desc FROM sys.databases WHERE name = 'SalesDB';
-- 2. Find Oldest Active Transaction preventing truncation:
DBCC OPENTRAN('SalesDB');
-- 3. Emergency Log Backup to alternate network share or secondary drive:
BACKUP LOG SalesDB
TO DISK = '\\BackupServer\Storage\SalesDB_EmergencyLog.trn'
WITH COMPRESSION;
-- 4. EMERGENCY ESCAPE VALVE: Add temporary log file on separate drive if disk has 0 bytes!
ALTER DATABASE SalesDB ADD LOG FILE (
NAME = 'SalesDB_TempLog',
FILENAME = 'E:\TempStorage\SalesDB_TempLog.ldf',
SIZE = 2048MB,
FILEGROWTH = 512MB
);
-- 5. Once log backup succeeds, cleanly shrink the bloated original log file:
USE SalesDB;
DBCC SHRINKFILE (SalesDB_Log, 4096); -- Shrink to healthy 4 GB sizeTop 6 Mistakes Candidates Make in SQL Server Interviews
Technical interviewers evaluate your understanding of set-based logic, transaction isolation, and query plan operations. Avoid these 6 common traps:
1. Wrapping Indexed Columns in Functions
Writing WHERE YEAR(OrderDate) = 2026 breaks SARGability, destroying index seeks and converting them into 100% full table scans across millions of pages.
2. Littering Queries with WITH (NOLOCK)
Using NOLOCK does not make queries 'fast'—it causes dirty reads of rolled-back data, reads duplicated rows during page splits, or causes Error 601 crashes.
3. Believing TRUNCATE Cannot Be Rolled Back
TRUNCATE is a logged DDL operation (page deallocations) that fully supports ROLLBACK TRANSACTION in SQL Server. Stating it cannot be rolled back is a red flag.
4. Forgetting the Truncation Trap of ISNULL
ISNULL(col, replacement) truncates the replacement string if it exceeds the data type length of the first argument! Use COALESCE() for type safety.
5. Using NOT IN with Nullable Subqueries
If the inner subquery returns a single NULL, NOT IN evaluates to UNKNOWN and returns zero rows! Always use NOT EXISTS instead.
6. Shrinking Production Databases and Logs
Running DBCC SHRINKDATABASE creates massive 99% index fragmentation and burns CPU. Shrinking the transaction log without taking log backups solves nothing.
The 4-Step SQL Server Interview Problem-Solving Framework
Use this structured method during live coding and scenario interviews to communicate like a senior database architect:
- Step 1: Clarify Cardinality & Volume (2–3 min): Ask about table sizes (1,000 rows vs 100 million rows), write frequency vs read frequency, and whether nullable columns exist.
- Step 2: Design Set-Based Solutions (Avoid RBAR): Always reject procedural cursors and while-loops in favor of set-based declarative SQL (Window functions, CTEs, CASE statements).
- Step 3: Analyze Execution Plan Operators (10–15 min): Explain your index strategy: Clustered key choice, non-clustered covering index with
INCLUDE, and eliminating Key Lookups and Sort spills. - Step 4: Address Concurrency & Locking (5 min): State the appropriate transaction boundary, isolation level (RCSI vs Serializable), and how to avoid deadlocks (consistent table access order).
24-Hour Final Revision Checklist for SQL Server Rounds
Quickly review these vital checkpoints the evening before your interview:
- [ ] Memorize the Logical Query Processing Order: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY.
- [ ] Explain the difference between Clustered Index (data rows at leaf level) and Non-Clustered (row locators).
- [ ] Know how to eliminate Key Lookups using
INCLUDEcolumns. - [ ] Understand why
WHERE NOT INfails when the subquery contains aNULL. - [ ] Know the 4 Transaction Isolation Levels and how RCSI eliminates reader-writer blocking.
- [ ] Be prepared to write a deduplication query using
ROW_NUMBER()and a CTE. - [ ] Explain how to troubleshoot 100% CPU using
sys.dm_exec_requestsandsys.dm_exec_query_plan. - [ ] Know the steps for Point-in-Time recovery using Tail-Log, Full, Differential, and Log backups.
Explore Related Technical Guides & Tools on RTSALL
Continue your interview preparation with RTSALL's curated technical guides, roadmaps, and developer utilities:
Frequently Asked Questions: SQL Server Technical Interviews
What is the most important skill for a SQL Server developer vs DBA interview?
Developers are primarily evaluated on writing set-based T-SQL queries, window functions, understanding SARGability, eliminating Key Lookups, and avoiding transaction deadlocks. DBAs are evaluated on disaster recovery (RPO/RTO), Point-in-Time recovery, Always On Availability Groups, TempDB contention tuning, index maintenance, wait statistics, and managing high CPU or storage incidents.
Is knowing basic T-SQL enough, or do I need to understand execution plans?
For junior roles (0–2 years), solid T-SQL, joins, grouping, and basic indexing are often sufficient. For mid-level and senior roles (3+ years), understanding graphical execution plans—spotting index scans, key lookups, hash spills to TempDB, and reading SET STATISTICS IO/TIME—is absolutely mandatory to pass technical interview rounds.
How do I talk about performance tuning if I haven't worked with terabyte-scale databases?
Focus on principles rather than raw size. Explain that an unindexed Table Scan on 500,000 rows burns the exact same relative CPU and memory as a scan on 500 million rows. Discuss SARGability, parameter sniffing, covering indexes, and converting multi-statement TVFs to inline TVFs. These architectural patterns demonstrate senior engineering maturity regardless of table size.
Why are set-based queries always preferred over cursors in SQL Server?
Relational databases are mathematically optimized to operate on entire sets of data at once using parallel algorithms, cost-based statistics, and batch processing. Cursors force RBAR (Row-By-Agonizing-Row) procedural execution, requiring individual context switches, row locks, and repeated page reads that are often 100x slower than set-based window functions.
What is the difference between SQL Server on Windows vs Linux vs Azure SQL?
The core SQL Server relational engine (PAL - Platform Abstraction Layer) is virtually identical across Windows and Linux. Azure SQL Database is a managed Platform-as-a-Service (PaaS) that handles automated backups, patching, and high availability natively with RCSI enabled by default, while SQL Server on-premises gives full control over OS storage, filegroups, and SQL Server Agent.
Leave a comment