SQL Server interviews test your understanding of T-SQL, indexing strategies, query optimisation, transaction isolation, high availability, and administration. This guide covers 50 of the most common questions with concise answers and T-SQL code examples.
Quick reference
| Topic | Most asked questions |
|---|---|
| Architecture | SQL Server engine, memory, buffer pool, execution engine |
| T-SQL | CTEs, window functions, MERGE, TRY/CATCH, PIVOT |
| Indexes | Clustered vs non-clustered, columnstore, covering indexes |
| Transactions | ACID, isolation levels, deadlocks, row versioning (RCSI) |
| Performance | Execution plans, query hints, statistics, tempdb |
| Stored routines | Stored procedures, functions, triggers, CLR |
| High Availability | Always On AG, log shipping, mirroring, replication |
| Security | Logins vs users, schemas, row-level security, TDE |
| Administration | Backup/restore, SQL Server Agent, maintenance |
| Advanced | Temporal tables, in-memory OLTP, JSON, XML |
Architecture
1. Describe the SQL Server engine architecture.
SQL Server consists of two major components:
SQLOS (SQL Server Operating System Layer) — manages threads, memory, I/O, and scheduling. SQL Server uses a cooperative scheduling model (SQLOS schedulers, not OS threads directly).
Relational Engine — includes:
- Query Parser — parses T-SQL into a parse tree
- Algebrizer — resolves names and types, produces a bound tree
- Query Optimiser — generates and picks the cheapest execution plan
- Query Executor — executes the plan using the Storage Engine
Storage Engine — includes:
- Buffer Manager — manages the Buffer Pool (in-memory data and index pages)
- Transaction Manager — handles locking, logging, and ACID
- Access Methods — B-tree traversal, full-table scans
- Log Manager — manages the Write-Ahead Log (WAL) /
.ldffile
Client
↓
Protocol Layer (TDS)
↓
Relational Engine: Parser → Algebrizer → Optimiser → Executor
↓
Storage Engine: Buffer Pool ↔ Data Files (.mdf/.ndf) + Log (.ldf)
2. What is the Buffer Pool and how does it work?
The Buffer Pool is SQL Server's main memory cache for 8 KB data and index pages.
- When a query needs a page, SQL Server first checks the Buffer Pool (logical read). If found, it uses it.
- If not found, it reads from disk (physical read) and loads the page into the Buffer Pool.
- A background process called Lazy Writer evicts old pages when memory pressure occurs (LRU-like algorithm called clock sweep).
- Buffer Pool Extensions (BPE) — on Standard edition you can extend the buffer pool to SSD.
-- Check buffer pool hit ratio
SELECT
object_name,
counter_name,
cntr_value
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Buffer cache hit ratio';
A healthy ratio is > 95 %.
3. What is the difference between a data file and a log file?
| Feature | Data File (.mdf / .ndf) | Transaction Log (.ldf) |
|---|---|---|
| Purpose | Stores tables, indexes, data | Records every transaction (WAL) |
| Structure | 8 KB pages organised in extents | Virtual Log Files (VLFs) |
| Growth | Can be auto-grown | Sequential, circular reuse after backup/checkpoint |
| Recovery | Used during RESTORE | Used to roll forward / roll back |
| Shrink | Possible but not recommended | DBCC SHRINKFILE (avoid in prod) |
T-SQL
4. What are CTEs and when should you use them?
A Common Table Expression (CTE) is a named temporary result set scoped to a single statement.
-- Simple CTE
WITH TopSales AS (
SELECT
SalesPersonID,
SUM(TotalDue) AS TotalSales,
RANK() OVER (ORDER BY SUM(TotalDue) DESC) AS rnk
FROM Sales.SalesOrderHeader
GROUP BY SalesPersonID
)
SELECT * FROM TopSales WHERE rnk <= 5;
Recursive CTE — for hierarchical data:
WITH OrgChart AS (
-- Anchor: top-level employees
SELECT EmployeeID, ManagerID, Name, 0 AS Level
FROM Employees
WHERE ManagerID IS NULL
UNION ALL
-- Recursive: employees reporting to the above
SELECT e.EmployeeID, e.ManagerID, e.Name, oc.Level + 1
FROM Employees e
INNER JOIN OrgChart oc ON e.ManagerID = oc.EmployeeID
)
SELECT * FROM OrgChart OPTION (MAXRECURSION 100);
Use CTEs for: readability, recursive queries, avoiding repeated subqueries.
5. Explain window functions with examples.
Window functions perform calculations across a set of rows related to the current row without collapsing the result set.
SELECT
OrderID,
CustomerID,
OrderDate,
TotalDue,
-- Running total per customer
SUM(TotalDue) OVER (
PARTITION BY CustomerID
ORDER BY OrderDate
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS RunningTotal,
-- Rank within customer
RANK() OVER (
PARTITION BY CustomerID
ORDER BY TotalDue DESC
) AS SalesRank,
-- Previous order amount
LAG(TotalDue, 1, 0) OVER (
PARTITION BY CustomerID
ORDER BY OrderDate
) AS PrevOrderAmt
FROM Sales.SalesOrderHeader;
| Function | Description |
|---|---|
ROW_NUMBER() |
Unique sequential number, no ties |
RANK() |
Ties get same rank, next rank skips |
DENSE_RANK() |
Ties get same rank, no gaps |
NTILE(n) |
Divides rows into n equal buckets |
LAG(col, n) |
Value n rows before current |
LEAD(col, n) |
Value n rows after current |
FIRST_VALUE(col) |
First value in the window |
LAST_VALUE(col) |
Last value in the window |
SUM / AVG / COUNT |
Aggregates over the window |
6. What is the MERGE statement?
MERGE performs INSERT, UPDATE, and DELETE in a single statement based on matching rows between a source and a target.
MERGE INTO Products AS target
USING ProductUpdates AS source
ON target.ProductID = source.ProductID
WHEN MATCHED AND target.Price <> source.Price THEN
UPDATE SET target.Price = source.Price,
target.UpdatedAt = GETUTCDATE()
WHEN NOT MATCHED BY TARGET THEN
INSERT (ProductID, Name, Price)
VALUES (source.ProductID, source.Name, source.Price)
WHEN NOT MATCHED BY SOURCE THEN
DELETE
OUTPUT $action, deleted.ProductID, inserted.ProductID;
Caution:
MERGEhas historically had several bugs in SQL Server. For simple upserts, prefer explicitIF EXISTS ... UPDATE ... ELSE INSERTpatterns orINSERT ... ON CONFLICTequivalent workarounds.
7. How does TRY/CATCH work in T-SQL?
BEGIN TRY
BEGIN TRANSACTION;
INSERT INTO Orders (CustomerID, OrderDate) VALUES (1, GETUTCDATE());
INSERT INTO OrderItems (OrderID, ProductID, Qty)
VALUES (SCOPE_IDENTITY(), 999, 1); -- might fail (FK)
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
SELECT
ERROR_NUMBER() AS ErrorNumber,
ERROR_SEVERITY() AS Severity,
ERROR_STATE() AS State,
ERROR_PROCEDURE() AS Procedure,
ERROR_LINE() AS Line,
ERROR_MESSAGE() AS Message;
END CATCH;
Key error functions: ERROR_NUMBER(), ERROR_MESSAGE(), ERROR_SEVERITY(), ERROR_STATE(), ERROR_LINE(), ERROR_PROCEDURE().
THROW (SQL Server 2012+) re-raises errors:
BEGIN CATCH
ROLLBACK;
THROW; -- re-throws the original error
END CATCH;
8. What is the difference between ISNULL and COALESCE?
| Feature | ISNULL(a, b) |
COALESCE(a, b, c, ...) |
|---|---|---|
| Standard | T-SQL only | ANSI SQL standard |
| Arguments | Exactly 2 | 2 or more |
| Return type | Type of first argument | Type of highest precedence arg |
| NULL handling | Returns b if a is NULL | Returns first non-NULL value |
| Performance | Slightly faster (no subquery) | May be slightly slower |
-- ISNULL
SELECT ISNULL(NULL, 'default'); -- 'default'
-- COALESCE
SELECT COALESCE(NULL, NULL, 'third', 'fourth'); -- 'third'
9. Explain the PIVOT and UNPIVOT operators.
PIVOT rotates rows into columns:
SELECT *
FROM (
SELECT Year, Quarter, Revenue
FROM SalesData
) AS src
PIVOT (
SUM(Revenue)
FOR Quarter IN ([Q1], [Q2], [Q3], [Q4])
) AS pvt;
UNPIVOT rotates columns into rows:
SELECT Year, Quarter, Revenue
FROM SalesPivoted
UNPIVOT (
Revenue FOR Quarter IN (Q1, Q2, Q3, Q4)
) AS unpvt;
10. What is the difference between DELETE, TRUNCATE, and DROP?
| Feature | DELETE |
TRUNCATE |
DROP |
|---|---|---|---|
| Removes rows | Yes (filtered) | Yes (all) | Yes (table gone) |
| WHERE clause | Yes | No | No |
| Logged | Fully (row-by-row) | Minimally (page deallocations) | Yes |
| Rollback | Yes | Yes (in transaction) | Yes |
| Resets identity | No | Yes | N/A |
| Triggers | Fires DELETE triggers | Does NOT fire triggers | No |
| Foreign keys | Respects FKs | Fails if FKs reference table | Fails if FKs reference table |
| Speed | Slower (large tables) | Much faster | Instant |
Indexes
11. What is the difference between a clustered and a non-clustered index?
| Feature | Clustered Index | Non-Clustered Index |
|---|---|---|
| Physical order | Rows are physically sorted by key | Separate structure; row pointers |
| Per table | Maximum 1 | Up to 999 |
| Leaf level | Contains actual data rows | Contains key + row locator (RID or clustered key) |
| Lookup | Direct — no extra lookup | May require Key Lookup for non-covered columns |
| Default | Created with PRIMARY KEY | Created with UNIQUE, CREATE INDEX |
| Size | Entire table | Smaller (key + locator) |
-- Create a clustered index explicitly
CREATE CLUSTERED INDEX CIX_Orders_OrderDate
ON Orders(OrderDate);
-- Create a non-clustered index
CREATE NONCLUSTERED INDEX NIX_Orders_CustomerID
ON Orders(CustomerID)
INCLUDE (OrderDate, TotalDue); -- covering
12. What is a covering index?
A covering index includes all columns needed by a query, so SQL Server can satisfy the query entirely from the index without performing a Key Lookup back to the clustered index.
-- Query
SELECT CustomerID, OrderDate, TotalDue
FROM Orders
WHERE CustomerID = 42;
-- Covering non-clustered index
CREATE NONCLUSTERED INDEX NIX_Orders_Customer_Covering
ON Orders(CustomerID)
INCLUDE (OrderDate, TotalDue);
Without the INCLUDE columns, SQL Server would use the index to find matching rows but then perform an expensive Key Lookup for each row to get OrderDate and TotalDue.
13. What is a Columnstore index and when should you use it?
A Columnstore index stores data column-by-column rather than row-by-row, achieving massive compression and enabling batch mode execution for analytical queries.
| Feature | Row Store (B-tree) | Columnstore |
|---|---|---|
| Storage layout | Row-by-row | Column-by-column |
| Compression | Page compression | ~10× compression |
| Best for | OLTP (point lookups) | OLAP (aggregations on millions of rows) |
| Execution mode | Row mode | Batch mode (512+ rows at a time) |
| Update overhead | Low | Higher (delta store) |
-- Clustered columnstore (replaces heap/row store — for DW tables)
CREATE CLUSTERED COLUMNSTORE INDEX CCI_FactSales
ON FactSales;
-- Non-clustered columnstore (analytical queries on OLTP table)
CREATE NONCLUSTERED COLUMNSTORE INDEX NCCI_Orders_Analytics
ON Orders(CustomerID, OrderDate, TotalDue, Quantity);
14. What is index fragmentation and how do you fix it?
Fragmentation occurs when index pages are out of logical order due to INSERT/UPDATE/DELETE operations, causing extra I/O.
-- Check fragmentation
SELECT
OBJECT_NAME(ips.object_id) AS TableName,
i.name AS IndexName,
ips.avg_fragmentation_in_percent,
ips.page_count
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') ips
JOIN sys.indexes i ON ips.object_id = i.object_id
AND ips.index_id = i.index_id
WHERE ips.avg_fragmentation_in_percent > 5
ORDER BY ips.avg_fragmentation_in_percent DESC;
Remediation strategy:
| Fragmentation | Action |
|---|---|
| < 5 % | No action needed |
| 5–30 % | ALTER INDEX ... REORGANIZE (online, minimal locking) |
| > 30 % | ALTER INDEX ... REBUILD (can be ONLINE in Enterprise) |
-- Reorganise (online, low impact)
ALTER INDEX NIX_Orders_CustomerID ON Orders REORGANIZE;
-- Rebuild (more thorough)
ALTER INDEX ALL ON Orders REBUILD WITH (ONLINE = ON);
15. What are statistics and why do they matter?
Statistics are histograms that describe the distribution of data in columns. The Query Optimiser uses statistics to estimate the number of rows that will be returned by predicates, influencing plan choice.
-- View statistics
DBCC SHOW_STATISTICS ('Sales.Orders', 'NIX_Orders_CustomerID');
-- Manually update statistics
UPDATE STATISTICS Sales.Orders WITH FULLSCAN;
-- Auto-update threshold (default: 20 % + 500 rows changed)
-- Enable Trace Flag 2371 / SQL 2016+ dynamic threshold for large tables
Outdated statistics → poor row estimates → bad plans → slow queries.
Transactions and Locking
16. What are the SQL Server transaction isolation levels?
| Isolation Level | Dirty Read | Non-Repeatable Read | Phantom Read | Implementation |
|---|---|---|---|---|
| READ UNCOMMITTED | ✓ possible | ✓ possible | ✓ possible | No shared locks |
| READ COMMITTED (default) | ✗ | ✓ possible | ✓ possible | Shared lock released after read |
| READ COMMITTED SNAPSHOT (RCSI) | ✗ | ✓ possible | ✓ possible | Row versioning (tempdb) |
| REPEATABLE READ | ✗ | ✗ | ✓ possible | Shared locks held until commit |
| SNAPSHOT | ✗ | ✗ | ✗ | Row versioning (tempdb) |
| SERIALIZABLE | ✗ | ✗ | ✗ | Range locks |
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRANSACTION;
-- ... your queries ...
COMMIT;
17. What is RCSI (Read Committed Snapshot Isolation) and why enable it?
RCSI (enabled at database level) changes READ COMMITTED to use row versioning from tempdb instead of shared locks.
- Readers never block writers, writers never block readers.
- Readers see the last committed version of each row.
- Eliminates most blocking without changing application code.
-- Enable RCSI
ALTER DATABASE AdventureWorks SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE;
-- Check RCSI status
SELECT name, is_read_committed_snapshot_on
FROM sys.databases
WHERE name = 'AdventureWorks';
Trade-off: increased tempdb activity and version store size.
18. What causes deadlocks and how do you resolve them?
A deadlock occurs when two (or more) sessions each hold a lock that the other needs.
Session 1: locks Table A, waits for Table B
Session 2: locks Table B, waits for Table A
→ Deadlock! SQL Server kills one session (the "deadlock victim")
Common causes:
| Cause | Fix |
|---|---|
| Inconsistent lock order | Access tables in the same order in all code paths |
| Long transactions | Keep transactions short; commit ASAP |
| Missing indexes | Add indexes to reduce lock duration and scope |
| Row escalation | ALTER TABLE t SET (LOCK_ESCALATION = DISABLE) if needed |
| READ UNCOMMITTED readers | Enable RCSI instead of using NOLOCK hints |
Capture deadlocks:
-- Extended Events — system_health session captures deadlocks automatically
SELECT CAST(target_data AS XML)
FROM sys.dm_xe_session_targets t
JOIN sys.dm_xe_sessions s ON s.address = t.event_session_address
WHERE s.name = 'system_health'
AND t.target_name = 'ring_buffer';
19. What is the WITH (NOLOCK) hint and what are its risks?
WITH (NOLOCK) is equivalent to READ UNCOMMITTED — it reads data without acquiring shared locks.
SELECT COUNT(*) FROM Orders WITH (NOLOCK);
Risks:
- Dirty reads — reads uncommitted data that may be rolled back
- Non-repeatable reads — data changes between reads in the same transaction
- Ghost reads / phantom data — reads rows that are being moved during a page split
- Incorrect counts — can count the same row twice or miss rows during page splits
Recommendation: Use RCSI instead. It provides non-blocking reads without the data correctness risks.
20. Explain optimistic vs pessimistic locking in SQL Server.
| Feature | Pessimistic Locking | Optimistic Locking |
|---|---|---|
| Mechanism | Acquires lock before reading/writing | No locks; checks for conflict at commit |
| Implementation | Shared/exclusive/update locks | rowversion / timestamp column |
| Concurrency | Lower (blocked readers/writers) | Higher (no blocking) |
| Conflict cost | Prevents conflict | Detects and rolls back on conflict |
| SQL Server default | Pessimistic (READ COMMITTED) | Snapshot Isolation / application-level |
-- Optimistic locking with rowversion
ALTER TABLE Products ADD RowVer rowversion NOT NULL;
-- At read time: store RowVer
-- At update time: check RowVer hasn't changed
UPDATE Products
SET Price = @NewPrice
WHERE ProductID = @ID AND RowVer = @StoredRowVer;
IF @@ROWCOUNT = 0
THROW 50001, 'Concurrency conflict — row was modified.', 1;
Performance Tuning
21. How do you read and interpret an execution plan?
An execution plan shows how SQL Server physically executes a query.
-- Text plan
SET SHOWPLAN_TEXT ON;
SELECT * FROM Orders WHERE CustomerID = 1;
SET SHOWPLAN_TEXT OFF;
-- Actual plan (captures runtime stats)
SET STATISTICS PROFILE ON;
SELECT * FROM Orders WHERE CustomerID = 1;
SET STATISTICS PROFILE OFF;
Key operators to recognise:
| Operator | Good/Bad | Meaning |
|---|---|---|
| Index Seek | Good | Uses index efficiently (range scan) |
| Index Scan | Caution | Full index scan; OK for small tables |
| Table Scan / Clustered Index Scan | Bad (large tables) | No usable index |
| Key Lookup | Bad (many rows) | Extra lookup to fetch non-covered columns |
| Hash Match | Caution | Build/probe hash table; memory grant needed |
| Nested Loops | Good (small outer set) | Row-by-row join |
| Merge Join | Good | Requires sorted inputs |
| Sort | Caution | May cause tempdb spill if memory insufficient |
| Parallelism (Gather Streams) | OK | Query went parallel |
Warning signs: thick arrows (large row estimates), yellow exclamation marks, missing index hints.
22. How do you identify slow queries?
-- Top 10 queries by total CPU time
SELECT TOP 10
qs.total_worker_time / qs.execution_count AS avg_cpu_us,
qs.total_elapsed_time / qs.execution_count AS avg_elapsed_us,
qs.execution_count,
qs.total_logical_reads / qs.execution_count AS avg_logical_reads,
SUBSTRING(qt.text, (qs.statement_start_offset/2)+1,
((CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(qt.text)
ELSE qs.statement_end_offset END
- qs.statement_start_offset)/2)+1) AS query_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
ORDER BY avg_cpu_us DESC;
-- Enable slow query log equivalent: Query Store (SQL Server 2016+)
ALTER DATABASE AdventureWorks SET QUERY_STORE = ON;
ALTER DATABASE AdventureWorks SET QUERY_STORE (
OPERATION_MODE = READ_WRITE,
QUERY_CAPTURE_MODE = AUTO,
MAX_STORAGE_SIZE_MB = 500
);
23. What is Parameter Sniffing and how do you handle it?
Parameter sniffing occurs when SQL Server compiles a stored procedure plan based on the first parameter values passed, then reuses that plan for all subsequent calls — even when different parameters would benefit from a different plan.
-- Problematic proc
CREATE PROCEDURE GetOrders @CustomerID INT AS
SELECT * FROM Orders WHERE CustomerID = @CustomerID;
-- If first call was @CustomerID = 1 (1 order), plan uses Index Seek
-- Later call with @CustomerID = 9999 (10k orders) → wrong plan
-- Solutions:
-- 1. OPTION (RECOMPILE) — recompile every execution
SELECT * FROM Orders WHERE CustomerID = @CustomerID
OPTION (RECOMPILE);
-- 2. Local variable workaround (breaks sniffing)
CREATE PROCEDURE GetOrders @CustomerID INT AS
DECLARE @LocalCust INT = @CustomerID;
SELECT * FROM Orders WHERE CustomerID = @LocalCust;
-- 3. OPTIMIZE FOR UNKNOWN
SELECT * FROM Orders WHERE CustomerID = @CustomerID
OPTION (OPTIMIZE FOR (@CustomerID UNKNOWN));
-- 4. Multiple plans (Query Store → Force Plan)
24. What is the role of tempdb and why can it become a bottleneck?
tempdb is a shared, system database reset on every SQL Server restart. It stores:
| Usage | Example |
|---|---|
Temporary tables (#temp, ##global) |
Intermediate results |
Table variables (@table) |
In some cases |
| Row versioning (RCSI / Snapshot Isolation) | Version store |
| Sort/hash spills | When memory grants are insufficient |
| Work tables for cursors | Spool operators |
| DBCC operations | Internal work |
Bottleneck causes: allocation page contention (PFS, GAM, SGAM).
Best practices:
-- Pre-create tempdb data files (1 per core, up to 8)
-- Set equal size and autogrowth for all files
-- Place on fast SSD
-- SQL Server 2016+: Trace Flag 1118 and 1117 are enabled by default
-- Earlier: add TF 1118, 1117 to startup parameters
25. What query hints are commonly used, and when?
| Hint | Purpose | When to use |
|---|---|---|
NOLOCK / READ UNCOMMITTED |
Skip shared locks | Avoid if possible; use RCSI |
RECOMPILE |
Recompile per execution | Parameter sniffing; highly variable data |
OPTIMIZE FOR (@p = value) |
Compile for specific value | Known skewed distribution |
OPTIMIZE FOR UNKNOWN |
Ignore sniffed value | Use average statistics |
FORCE ORDER |
Preserve FROM join order | Optimiser picks wrong order |
HASH JOIN / LOOP JOIN / MERGE JOIN |
Force join algorithm | Specific join strategy needed |
MAXDOP n |
Limit parallelism | Reduce contention; report queries |
INDEX (index_name) |
Force specific index | Optimiser picks wrong index |
FAST n |
Optimise for first n rows | Paging / streaming scenarios |
SELECT o.OrderID, c.Name
FROM Orders o
INNER HASH JOIN Customers c ON o.CustomerID = c.CustomerID
WHERE o.OrderDate > '2025-01-01'
OPTION (MAXDOP 4, RECOMPILE);
Hints bypass the optimiser — use as a last resort after fixing statistics and indexes.
Stored Procedures, Functions, and Triggers
26. What is the difference between a stored procedure and a function?
| Feature | Stored Procedure | User-Defined Function |
|---|---|---|
| Returns | 0 or more result sets, output params | Scalar value or table |
| DML allowed | Yes (INSERT/UPDATE/DELETE) | Only in table-valued functions |
| Non-deterministic funcs | Yes (GETDATE(), RAND()) |
No (scalar) / limited (TVF) |
| Use in SELECT/WHERE | No (scalar UDF: technically yes but slow) | Yes (TVF in FROM, scalar in expression) |
| Transaction control | Yes | No |
| Error handling | TRY/CATCH | Limited |
| Called with | EXEC proc |
Inline in query |
-- Scalar UDF (avoid in hot queries — row-by-row execution until SQL 2019 UDF inlining)
CREATE FUNCTION dbo.GetFullName (@First NVARCHAR(50), @Last NVARCHAR(50))
RETURNS NVARCHAR(100) AS
BEGIN
RETURN @First + ' ' + @Last;
END;
-- Inline table-valued function (ITVF) — best performance
CREATE FUNCTION dbo.GetCustomerOrders (@CustomerID INT)
RETURNS TABLE AS
RETURN (
SELECT OrderID, OrderDate, TotalDue
FROM Orders
WHERE CustomerID = @CustomerID
);
27. What are the types of triggers in SQL Server?
| Trigger Type | When | Use Case |
|---|---|---|
AFTER INSERT |
After INSERT completes | Audit trail, cascade |
AFTER UPDATE |
After UPDATE completes | Audit log changes |
AFTER DELETE |
After DELETE completes | Soft delete enforcement |
INSTEAD OF INSERT/UPDATE/DELETE |
Instead of the DML | Views, custom logic |
| DDL Trigger | After DDL (CREATE/ALTER/DROP) | Prevent schema changes |
| Logon Trigger | At login | Restrict connections |
-- AFTER UPDATE audit trigger
CREATE TRIGGER trg_Orders_Audit
ON Orders
AFTER UPDATE AS
BEGIN
SET NOCOUNT ON;
INSERT INTO OrdersAudit (OrderID, OldStatus, NewStatus, ChangedAt, ChangedBy)
SELECT
d.OrderID,
d.Status AS OldStatus,
i.Status AS NewStatus,
GETUTCDATE(),
SYSTEM_USER
FROM deleted d
JOIN inserted i ON d.OrderID = i.OrderID
WHERE d.Status <> i.Status;
END;
Note:
insertedanddeletedpseudo-tables are always available inside DML triggers.
28. What is the difference between a view and a materialized view (indexed view)?
| Feature | Regular View | Indexed View |
|---|---|---|
| Storage | No — query is re-run each time | Yes — result set is stored |
| Performance | Same as underlying query | Faster for aggregations |
| Requirements | None | Schema-bound; deterministic; no outer joins/subqueries |
| Index support | No clustered index | Must have unique clustered index first |
| Maintenance | None | Maintained automatically on DML |
| SQL Server editions | All | Standard limited; full in Enterprise |
-- Create an indexed view
CREATE VIEW dbo.SalesByProduct WITH SCHEMABINDING AS
SELECT ProductID, SUM(Quantity) AS TotalQty, COUNT_BIG(*) AS OrderCount
FROM dbo.OrderItems
GROUP BY ProductID;
GO
CREATE UNIQUE CLUSTERED INDEX UIX_SalesByProduct
ON dbo.SalesByProduct(ProductID);
High Availability and Disaster Recovery
29. What is Always On Availability Groups (AG)?
Always On Availability Groups (SQL Server 2012+) provide high availability and disaster recovery by replicating a group of databases to one or more secondary replicas.
Primary Replica Secondary Replicas
[AG Primary] → Synchronous → [AG Secondary 1] (HA, auto-failover)
→ Asynchronous → [AG Secondary 2] (DR, manual failover)
| Feature | Always On AG |
|---|---|
| Failover | Automatic (sync) / Manual (async) |
| Data loss (sync) | Zero (RPO = 0) |
| Readable secondaries | Yes (for reporting/backups) |
| Max replicas | 8 secondaries (SQL 2022) |
| Requires | Windows Server Failover Cluster (WSFC) or Domain-Independent AG |
| Listener | Virtual name/IP for transparent client failover |
30. Compare Always On AG, Log Shipping, and Database Mirroring.
| Feature | Always On AG | Log Shipping | Database Mirroring |
|---|---|---|---|
| SQL version | 2012+ | 2000+ | 2005–2016 (deprecated) |
| Multiple secondaries | Yes (up to 8) | Yes | No (1 mirror) |
| Readable secondary | Yes | Yes (STANDBY) | No |
| Auto-failover | Yes (sync) | No | Yes (with witness) |
| Data loss (sync) | Zero | Configurable minutes | Zero (sync mode) |
| Setup complexity | High | Low | Medium |
| Status | Current | Current | Deprecated |
Security
31. What is the difference between a Login and a User in SQL Server?
| Concept | Login | User |
|---|---|---|
| Scope | Server level | Database level |
| Authenticates | Yes — to the SQL Server instance | No — mapped from login |
| Types | SQL Login, Windows Login, Certificate, AAD | Database user, contained user |
| Location | master database |
Individual database |
-- Create a SQL Server login
CREATE LOGIN AppLogin WITH PASSWORD = 'Str0ng!Pass';
-- Create a database user mapped to the login
USE AdventureWorks;
CREATE USER AppUser FOR LOGIN AppLogin;
-- Grant permissions
GRANT SELECT, INSERT ON dbo.Orders TO AppUser;
DENY DELETE ON dbo.Orders TO AppUser;
32. What is Row-Level Security (RLS)?
RLS restricts which rows a user can see/modify based on a security predicate (a filter function).
-- 1. Create the filter function
CREATE FUNCTION dbo.fn_SecurityPredicate (@SalesRegion NVARCHAR(50))
RETURNS TABLE WITH SCHEMABINDING AS
RETURN (
SELECT 1 AS fn_Result
WHERE @SalesRegion = CAST(SESSION_CONTEXT(N'SalesRegion') AS NVARCHAR(50))
OR IS_MEMBER('db_owner') = 1
);
-- 2. Create the security policy
CREATE SECURITY POLICY SalesFilter
ADD FILTER PREDICATE dbo.fn_SecurityPredicate(SalesRegion)
ON dbo.Orders
WITH (STATE = ON);
-- 3. Set context for each session
EXEC sp_set_session_context N'SalesRegion', N'EMEA';
-- Now queries on Orders only see EMEA rows
33. What is Transparent Data Encryption (TDE)?
TDE encrypts the physical data files (.mdf, .ndf, .ldf) and backups at rest using a Database Encryption Key (DEK) protected by the server certificate.
-- 1. Create master key
CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'StrongP@ssw0rd!';
-- 2. Create certificate
CREATE CERTIFICATE TDE_Cert WITH SUBJECT = 'TDE Certificate';
-- 3. Create database encryption key
USE AdventureWorks;
CREATE DATABASE ENCRYPTION KEY
WITH ALGORITHM = AES_256
ENCRYPTION BY SERVER CERTIFICATE TDE_Cert;
-- 4. Enable TDE
ALTER DATABASE AdventureWorks SET ENCRYPTION ON;
-- Check status
SELECT name, encryption_state, encryption_state_desc
FROM sys.dm_database_encryption_keys;
Critical: Back up the TDE certificate and private key immediately. If lost, the database is unrecoverable.
Administration
34. What are the SQL Server backup types?
| Backup Type | What it captures | Restore dependency | Size |
|---|---|---|---|
| Full | Entire database | None | Largest |
| Differential | Changes since last Full | Full backup | Smaller |
| Transaction Log | Log records since last log backup | Full + all subsequent log backups | Smallest |
| Copy-Only Full | Full backup, does not reset differential base | None | Same as Full |
| File/Filegroup | Individual files | Complex restore chain | Variable |
Typical backup strategy:
Weekly: Full backup
Daily: Differential backup
Hourly: Transaction Log backup (for point-in-time recovery)
-- Full backup
BACKUP DATABASE AdventureWorks
TO DISK = 'D:\Backups\AdventureWorks_Full.bak'
WITH COMPRESSION, STATS = 10, CHECKSUM;
-- Transaction log backup
BACKUP LOG AdventureWorks
TO DISK = 'D:\Backups\AdventureWorks_Log.trn'
WITH COMPRESSION, STATS = 10;
-- Restore with NORECOVERY (more log backups to apply)
RESTORE DATABASE AdventureWorks
FROM DISK = 'D:\Backups\AdventureWorks_Full.bak'
WITH NORECOVERY, REPLACE, STATS = 10;
-- Final restore — bring database online
RESTORE DATABASE AdventureWorks WITH RECOVERY;
35. What are the SQL Server recovery models?
| Recovery Model | Log Space | Point-in-time Restore | Log Backup Required | Use Case |
|---|---|---|---|---|
| SIMPLE | Auto-truncated on checkpoint | No | No | Dev/test, no need for RPO |
| FULL | Kept until log backup | Yes | Yes | Production — mission critical |
| BULK_LOGGED | Minimal for bulk ops | Limited | Yes | Large ETL loads in production |
-- Check current model
SELECT name, recovery_model_desc FROM sys.databases;
-- Change recovery model
ALTER DATABASE AdventureWorks SET RECOVERY FULL;
36. What is SQL Server Agent and what is it used for?
SQL Server Agent is a Windows service that runs scheduled jobs, monitors alerts, and handles operator notifications.
| Component | Description |
|---|---|
| Jobs | Sequences of steps (T-SQL, SSIS, PowerShell, etc.) |
| Job Steps | Individual tasks within a job |
| Schedules | When jobs run (daily, weekly, on SQL startup, etc.) |
| Alerts | Respond to performance conditions or error events |
| Operators | Who receives notifications (email, pager, net send) |
-- Create a simple Agent job via T-SQL
USE msdb;
EXEC sp_add_job @job_name = N'Nightly_Index_Rebuild';
EXEC sp_add_jobstep
@job_name = N'Nightly_Index_Rebuild',
@step_name = N'Rebuild Indexes',
@command = N'ALTER INDEX ALL ON dbo.Orders REBUILD;',
@database_name = N'AdventureWorks';
EXEC sp_add_schedule
@schedule_name = N'Nightly_2AM',
@freq_type = 4, -- daily
@active_start_time = 020000; -- 02:00:00
EXEC sp_attach_schedule
@job_name = N'Nightly_Index_Rebuild',
@schedule_name = N'Nightly_2AM';
EXEC sp_add_jobserver @job_name = N'Nightly_Index_Rebuild';
Advanced Topics
37. What are Temporal Tables?
Temporal tables (SQL Server 2016+) automatically track the full history of row changes, enabling time-travel queries.
-- Create temporal table
CREATE TABLE Products (
ProductID INT NOT NULL PRIMARY KEY,
Name NVARCHAR(100) NOT NULL,
Price DECIMAL(10,2) NOT NULL,
ValidFrom DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL,
ValidTo DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL,
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.ProductsHistory));
-- Query as of a point in time
SELECT * FROM Products
FOR SYSTEM_TIME AS OF '2025-01-01T00:00:00';
-- Query a range
SELECT * FROM Products
FOR SYSTEM_TIME BETWEEN '2025-01-01' AND '2025-06-01';
38. What is In-Memory OLTP (Hekaton)?
In-Memory OLTP (SQL Server 2014+) provides lock-free, latch-free data structures stored entirely in RAM, dramatically improving OLTP throughput.
-- Create memory-optimized filegroup
ALTER DATABASE AdventureWorks
ADD FILEGROUP MemoryOptimized CONTAINS MEMORY_OPTIMIZED_DATA;
ALTER DATABASE AdventureWorks
ADD FILE (NAME = 'MemOptFile', FILENAME = 'D:\Data\MemOpt')
TO FILEGROUP MemoryOptimized;
-- Create in-memory table
CREATE TABLE dbo.HotQueue (
QueueID INT NOT NULL PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 100000),
Payload NVARCHAR(1000) NOT NULL,
CreatedAt DATETIME2 NOT NULL
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);
-- Natively compiled stored procedure
CREATE PROCEDURE dbo.EnqueueItem
@Payload NVARCHAR(1000)
WITH NATIVE_COMPILATION, SCHEMABINDING AS
BEGIN ATOMIC WITH (TRANSACTION ISOLATION LEVEL = SNAPSHOT, LANGUAGE = N'English')
INSERT INTO dbo.HotQueue (QueueID, Payload, CreatedAt)
VALUES (NEXT VALUE FOR dbo.QueueSeq, @Payload, SYSUTCDATETIME());
END;
39. How does SQL Server handle JSON?
SQL Server 2016+ includes JSON support for parsing and generating JSON.
-- Parse JSON
SELECT *
FROM OPENJSON('{"orders": [{"id":1,"amt":100},{"id":2,"amt":200}]}', '$.orders')
WITH (
OrderID INT '$.id',
Amount DECIMAL '$.amt'
);
-- Query JSON column
SELECT
CustomerID,
JSON_VALUE(Preferences, '$.Theme') AS Theme,
JSON_QUERY(Preferences, '$.Settings') AS SettingsJson
FROM Customers
WHERE JSON_VALUE(Preferences, '$.Newsletter') = 'true';
-- Build JSON output
SELECT OrderID, TotalDue, Status
FROM Orders
WHERE CustomerID = 42
FOR JSON PATH, ROOT('orders');
40. What are computed columns and when should you index them?
A computed column is a virtual column whose value is derived from an expression.
-- Computed column
ALTER TABLE Orders
ADD TotalWithTax AS (TotalDue * 1.21); -- virtual (not stored)
ALTER TABLE Orders
ADD TotalWithTax AS (TotalDue * 1.21) PERSISTED; -- stored on disk
-- Index on a computed column (must be PERSISTED and deterministic)
CREATE INDEX NIX_Orders_TotalWithTax ON Orders(TotalWithTax);
Practical Scenarios
41. How do you find duplicate rows and remove them?
-- Find duplicates
SELECT Email, COUNT(*) AS Cnt
FROM Customers
GROUP BY Email
HAVING COUNT(*) > 1;
-- Delete duplicates, keeping the lowest CustomerID
WITH Dupes AS (
SELECT CustomerID,
ROW_NUMBER() OVER (PARTITION BY Email ORDER BY CustomerID) AS rn
FROM Customers
)
DELETE FROM Dupes WHERE rn > 1;
42. How do you implement pagination efficiently?
Avoid OFFSET/FETCH for large offsets — it scans all skipped rows.
-- OFFSET/FETCH (simple but slow at large offsets)
SELECT OrderID, OrderDate, TotalDue
FROM Orders
ORDER BY OrderID
OFFSET 10000 ROWS FETCH NEXT 20 ROWS ONLY;
-- Keyset (cursor) pagination — much faster
-- First page
SELECT TOP 20 OrderID, OrderDate, TotalDue
FROM Orders
ORDER BY OrderID;
-- Next page: pass last OrderID from previous result
SELECT TOP 20 OrderID, OrderDate, TotalDue
FROM Orders
WHERE OrderID > @LastOrderID
ORDER BY OrderID;
43. How do you identify blocking sessions?
-- Current blocking chains
SELECT
blocking.session_id AS blocking_spid,
blocked.session_id AS blocked_spid,
blocked.wait_time / 1000.0 AS wait_seconds,
blocked.wait_type,
blocked_sql.text AS blocked_query,
blocking_sql.text AS blocking_query
FROM sys.dm_exec_requests blocked
JOIN sys.dm_exec_sessions blocking ON blocked.blocking_session_id = blocking.session_id
CROSS APPLY sys.dm_exec_sql_text(blocked.sql_handle) AS blocked_sql
CROSS APPLY sys.dm_exec_sql_text(blocking.most_recent_sql_handle) AS blocking_sql
WHERE blocked.blocking_session_id > 0;
-- Kill a blocking session (use with caution)
KILL 55; -- replace with actual SPID
44. How do you perform a schema change without downtime?
Use the expand-contract (parallel change) pattern:
- Expand — add new column as nullable; update application to write to both old and new column.
- Migrate — backfill data in batches (avoid large locks):
-- Batched backfill
DECLARE @BatchSize INT = 1000, @LastID INT = 0;
WHILE 1 = 1 BEGIN
UPDATE TOP (@BatchSize) Orders
SET NewStatusColumn = OldStatusColumn
WHERE NewStatusColumn IS NULL AND OrderID > @LastID;
IF @@ROWCOUNT = 0 BREAK;
SET @LastID = @LastID + @BatchSize;
WAITFOR DELAY '00:00:00.1';
END;
- Contract — once migration is complete and verified, drop old column.
45. How do you monitor SQL Server health?
-- CPU usage by query
SELECT TOP 10
total_worker_time / execution_count AS avg_cpu_us,
execution_count,
SUBSTRING(st.text, (qs.statement_start_offset/2)+1, 200) AS query_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
ORDER BY avg_cpu_us DESC;
-- Memory pressure
SELECT
physical_memory_in_use_kb / 1024.0 AS memory_used_mb,
page_fault_count
FROM sys.dm_os_process_memory;
-- Wait statistics (overall bottlenecks)
SELECT TOP 20
wait_type,
waiting_tasks_count,
wait_time_ms / 1000.0 AS total_wait_sec,
max_wait_time_ms / 1000.0 AS max_wait_sec
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN (
'SLEEP_TASK','BROKER_TO_FLUSH','HADR_WORK_QUEUE',
'CLR_AUTO_EVENT','DISPATCHER_QUEUE_SEMAPHORE'
)
ORDER BY wait_time_ms DESC;
SQL Server vs Other Databases
46. SQL Server vs MySQL vs PostgreSQL
| Feature | SQL Server | MySQL | PostgreSQL |
|---|---|---|---|
| Vendor | Microsoft | Oracle | Community (open source) |
| License | Commercial (free Developer ed.) | GPL / Commercial | PostgreSQL License (free) |
| OS | Windows / Linux | Windows / Linux / macOS | Windows / Linux / macOS |
| Default isolation | READ COMMITTED | REPEATABLE READ | READ COMMITTED |
| JSON support | 2016+ (good) | 5.7+ (good) | 9.2+ (excellent, JSONB) |
| Full-text search | Built-in FTS | Basic FTS | Built-in, extensible |
| Window functions | Full | 8.0+ | Full |
| CTEs recursive | Yes | 8.0+ | Yes |
| Columnstore index | Yes (Enterprise) | NDB only | No (native) |
| In-memory tables | In-Memory OLTP | NDB Cluster | No (native) |
| Replication / HA | Always On AG | Group Replication | Streaming Replication |
| T-SQL / Dialect | T-SQL | MySQL SQL | PL/pgSQL |
| GUI tool | SSMS (free) | MySQL Workbench | pgAdmin |
47. What are T-SQL-specific features not in standard SQL?
| T-SQL Feature | Description |
|---|---|
TOP (n) |
Limit rows (vs LIMIT in MySQL/PG) |
ISNULL() |
2-arg NULL replacement (vs COALESCE) |
GETDATE() / GETUTCDATE() |
Current timestamp (vs NOW()) |
IDENTITY(1,1) |
Auto-increment column |
PRINT |
Debug output |
RAISERROR / THROW |
Raise errors |
EXEC / sp_executesql |
Dynamic SQL |
GOTO |
Jump to label |
WAITFOR DELAY |
Sleep |
CHARINDEX() / PATINDEX() |
String search (vs LOCATE(), STRPOS()) |
STUFF() |
Replace substring |
FORMAT() |
Locale-aware formatting |
IIF() |
Inline if-then-else |
CHOOSE() |
Pick from list by index |
NEWID() |
Generate GUID |
@@ROWCOUNT, @@IDENTITY |
System variables |
Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
| Scalar UDFs in WHERE | Row-by-row execution — kills performance | Use inline TVFs or rewrite logic inline |
SELECT * in production |
Unnecessary I/O, plan instability | Explicit column list |
| Missing indexes on FK columns | Full scans on joins | Index every FK |
NOLOCK everywhere |
Dirty/phantom reads, incorrect counts | Enable RCSI instead |
| Implicit data type conversion | Index scans instead of seeks | Match data types; cast on param side |
| Cursors for row-by-row | Very slow vs set-based operations | Rewrite as set-based |
TRUNCATE on replicated table |
Breaks replication | Use DELETE with batching |
SHRINKFILE frequently |
Fragmentation, I/O overhead | Avoid; size files correctly upfront |
SQL Server vs related tools
| Tool | Purpose |
|---|---|
| SSMS | SQL Server Management Studio — primary GUI |
| Azure Data Studio | Cross-platform GUI with notebook support |
| SSIS | SQL Server Integration Services — ETL |
| SSRS | SQL Server Reporting Services — reporting |
| SSAS | SQL Server Analysis Services — OLAP cubes, tabular models |
| SQL Server Profiler | Trace queries (deprecated; use Extended Events) |
| Extended Events (XE) | Lightweight performance monitoring |
| Query Store | Track query plan changes over time |
| Database Tuning Advisor | Index and statistics recommendations |
| Brent Ozar's sp_Blitz | Community DBA health check scripts |
FAQ
Q: When should I use a stored procedure vs ad-hoc SQL? Stored procedures offer plan caching, reduced network traffic, permission management (grant EXEC without table access), and easier maintenance. Use ad-hoc SQL for simple one-off queries and ORM-generated queries; use stored procedures for complex, frequently executed business logic.
Q: What is the difference between @@IDENTITY, SCOPE_IDENTITY(), and IDENT_CURRENT()?
@@IDENTITY returns the last identity value inserted in the current session (any scope, any table — can be wrong if a trigger inserts to another table). SCOPE_IDENTITY() returns the last identity in the current scope (correct, use this). IDENT_CURRENT('tableName') returns the last identity for a specific table, any session — use for diagnostic purposes only.
Q: How does SQL Server handle NULL in indexes?
SQL Server stores NULL values in non-clustered indexes. A regular index can be used for IS NULL queries. A filtered index on WHERE col IS NULL or WHERE col IS NOT NULL is more efficient for selective NULL queries:
CREATE INDEX NIX_Orders_NullStatus
ON Orders(CustomerID)
WHERE Status IS NULL;
Q: What is the difference between UNION and UNION ALL?
UNION removes duplicate rows (performs a DISTINCT sort — expensive). UNION ALL returns all rows including duplicates (no sort — much faster). Always use UNION ALL unless you explicitly need duplicate elimination.
Q: How do you prevent SQL injection in SQL Server? Always use parameterised queries or stored procedures with parameters. Never concatenate user input into SQL strings.
-- WRONG: SQL injection risk
EXEC('SELECT * FROM Users WHERE Name = ''' + @UserInput + '''');
-- CORRECT: parameterised
EXEC sp_executesql
N'SELECT * FROM Users WHERE Name = @Name',
N'@Name NVARCHAR(100)',
@Name = @UserInput;
Q: What is the Query Store and when should you enable it? Query Store (SQL Server 2016+) persists query plans, execution statistics, and runtime stats in the user database. Use it to: detect plan regressions, force a known-good plan, compare query performance before/after an index change or upgrade. Enable on all production databases — overhead is minimal (< 1–2 % CPU).
ALTER DATABASE AdventureWorks SET QUERY_STORE = ON;
ALTER DATABASE AdventureWorks SET QUERY_STORE (
OPERATION_MODE = READ_WRITE,
MAX_STORAGE_SIZE_MB = 1000,
QUERY_CAPTURE_MODE = AUTO,
SIZE_BASED_CLEANUP_MODE = AUTO
);