SQL Server / BI / ETL Developer — Interview Questions
In short: DELETE (DML) removes rows one by one, supports WHERE, fires triggers, logs each row and keeps the identity counter.
- TRUNCATE (DDL) removes all rows by deallocating pages, is minimally logged and much faster, resets IDENTITY, doesn't fire triggers and isn't allowed on tables referenced by a foreign key.
- DROP removes the whole table definition, data, indexes and permissions.
- In SQL Server all three can be rolled back inside an explicit transaction.
In short: INNER JOIN (matching rows in both), LEFT/RIGHT OUTER JOIN (all rows from one side + matches, NULLs otherwise), FULL OUTER JOIN (all rows from both), CROSS JOIN (Cartesian product), SELF JOIN (table joined to itself, e.g. employee–manager).
- T-SQL also has CROSS APPLY/OUTER APPLY for correlated table expressions.
- Physical algorithms: nested loops, merge, hash.
In short: WHERE filters individual rows before grouping and can't use aggregates.
- HAVING filters groups after GROUP BY and can use aggregates (
HAVING COUNT(*) > 5). - Put non-aggregate conditions in WHERE for performance — fewer rows get grouped.
In short: Primary key: uniquely identifies each row, NOT NULL, one per table, creates a clustered index by default.
- Unique key: enforces uniqueness on other columns, allows one NULL in SQL Server, many per table, creates a non-clustered index by default.
- Foreign key: a column referencing another table's primary/unique key to enforce referential integrity (with cascade options).
In short: Organising tables to reduce redundancy and update anomalies.
- 1NF: atomic columns and no repeating groups.
- 2NF: 1NF and every non-key column depends on the whole primary key (no partial dependency — relevant for composite keys).
- 3NF: 2NF and no transitive dependency (non-key columns depend only on the key, e.g. move DeptName out of Employees into Departments).
- Data warehouses deliberately denormalise for read speed.
In short: A clustered index sorts and stores the table's data rows in key order — the leaf level is the data; only one per table.
- A non-clustered index is a separate structure with key columns and a pointer (clustered key or RID) back to the row; up to 999 per table.
- Clustered is ideal for range queries on the key; non-clustered for selective lookups on other columns, ideally covering with INCLUDE columns.
-- Handles ties correctly; change 2 to N
SELECT DISTINCT Salary FROM (
SELECT Salary, DENSE_RANK() OVER (ORDER BY Salary DESC) AS rnk FROM dbo.Employees
) t WHERE rnk = 2;
-- Alternative
SELECT MAX(Salary) FROM dbo.Employees WHERE Salary < (SELECT MAX(Salary) FROM dbo.Employees);
-- OFFSET/FETCH
SELECT DISTINCT Salary FROM dbo.Employees ORDER BY Salary DESC OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY;-- find
SELECT Email, COUNT(*) AS Cnt FROM dbo.Customers GROUP BY Email HAVING COUNT(*) > 1;
-- delete all but one (keep lowest Id)
WITH d AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY Email ORDER BY CustomerId) AS rn FROM dbo.Customers
)
DELETE FROM d WHERE rn > 1;Then add a UNIQUE constraint so it can't happen again.
In short: Atomicity — all statements in a transaction succeed or none do (rollback on failure).
- Consistency — a transaction moves the database from one valid state to another, respecting constraints, keys and rules.
- Isolation — concurrent transactions don't see each other's uncommitted work, controlled by isolation levels and locks/row versions.
- Durability — once committed, changes survive crashes because the log is hardened to disk (write-ahead logging).
- Example: a money transfer debits one account and credits another inside one transaction so it can never half-complete.
In short: All are window ranking functions.
- For salaries 100, 90, 90, 80: ROW_NUMBER gives 1,2,3,4 (unique, arbitrary order among ties), RANK gives 1,2,2,4 (ties share a rank, gaps follow), DENSE_RANK gives 1,2,2,3 (ties share, no gaps).
- Use ROW_NUMBER for deduplication and paging, DENSE_RANK for "Nth highest" with ties.
In short: A Common Table Expression (WITH name AS (...)) is a named temporary result set for a single statement — improves readability, can be referenced multiple times in that statement, and supports recursion.
- A recursive CTE has an anchor member UNION ALL a recursive member referencing the CTE;
- used for hierarchies (org charts, category trees, bill of materials) and generating sequences.
- Default max recursion 100 (
OPTION (MAXRECURSION n)).
In short: Procedures can return multiple result sets, output parameters and an integer return code;
- can modify data, use transactions and TRY/CATCH;
- are called with EXEC and can't be used inside SELECT.
- Functions return a scalar or a table, must be side-effect free (no INSERT/UPDATE to tables), can't use TRY/CATCH or transactions, and can be used in SELECT/WHERE/JOIN/APPLY.
- Prefer inline TVFs for reusable query logic.
In short: READ UNCOMMITTED (dirty reads possible;
NOLOCK), READ COMMITTED (default;- no dirty reads), REPEATABLE READ (no non-repeatable reads), SERIALIZABLE (no phantoms;
- range locks), SNAPSHOT (row versioning, consistent view as of transaction start, readers don't block writers), and READ COMMITTED SNAPSHOT (database setting making read committed use versioning per statement;
- default in Azure SQL Database).
- Higher isolation = more consistency, more blocking.
In short: Two (or more) transactions each hold locks the other needs, so neither can proceed.
- SQL Server's lock monitor detects it and kills the cheapest transaction (error 1205).
- Investigate with the
system_healthExtended Events session (deadlock graphs). - Prevent: access tables in a consistent order, keep transactions short, add indexes so fewer rows are locked, avoid user interaction inside transactions, use RCSI/SNAPSHOT for readers;
- and retry 1205 in the application.
In short: Temp tables (#t) live in tempdb for the session/proc, have statistics, support any indexes, and are rolled back with transactions — good for larger intermediate data.
- Table variables (
@t) are scoped to the batch, have no column statistics (estimates improved by 2019 deferred compilation), aren't affected by rollback, best for small sets. - CTEs aren't stored at all — they're inlined into the single statement.
In short: Using functions, calculations or conversions on an indexed column in WHERE/JOIN prevents index seeks: WHERE YEAR(OrderDate) = 2026, WHERE ISNULL(Col, 0) = 5, WHERE Col + 1 = 10, LIKE '%abc', implicit conversions (nvarchar parameter vs varchar column).
- Rewrite as ranges or move the calculation to the parameter side:
OrderDate >= '2026-01-01' AND OrderDate < '2027-01-01'.
In short: When a parameterised query/proc compiles, the optimizer "sniffs" the first parameter values to estimate row counts and caches that plan.
- If data is skewed, later calls with different values reuse a plan that's bad for them (e.g. a nested-loop plan built for a customer with 5 orders reused for one with 5 million).
- Fixes:
OPTION (RECOMPILE)for infrequent queries,OPTIMIZE FOR UNKNOWN/specific value, local variables (generic estimate), separate procedures for different data shapes, Query Store plan forcing, and SQL Server 2022's Parameter Sensitive Plan optimisation.
In short: 1) Reproduce and measure (SET STATISTICS IO, TIME ON, actual execution plan, Query Store history).
- 2) Read the plan: scans on large tables, key lookups, bad estimates, spills, implicit conversions, parallelism issues.
- 3) Fix the query: SARGable predicates, remove
SELECT *, avoid scalar UDFs/cursors (set-based), rewrite correlated subqueries, correct data types. - 4) Fix indexes: covering/composite indexes in the right column order, filtered indexes, remove unused.
- 5) Update statistics; handle parameter sniffing.
- 6) Check blocking/waits (
sys.dm_exec_requests,sp_WhoIsActive). - 7) Validate the improvement and watch write overhead.
In short: Something changed in data or plans.
- Check Query Store for a plan regression (different plan since date X) and force the old plan as a stopgap;
- check statistics staleness after big data loads (update them);
- parameter sniffing (different first-call parameters);
- data growth crossing a threshold;
- new blocking (
sp_WhoIsActive, wait stats); - index changes or dropped indexes;
- server pressure (memory grants, tempdb spills, CPU).
- Then fix permanently with indexes/query rewrite.
WITH ranked AS (
SELECT e.*, DENSE_RANK() OVER (PARTITION BY DeptId ORDER BY Salary DESC) AS rnk
FROM dbo.Employees e
)
SELECT d.DeptName, r.FullName, r.Salary, r.rnk
FROM ranked r JOIN dbo.Departments d ON d.DeptId = r.DeptId
WHERE r.rnk <= 3
ORDER BY d.DeptName, r.rnk;Use ROW_NUMBER for exactly 3 rows per department regardless of ties; DENSE_RANK to include ties.
In short: Likely blocking: the finance job holds long locks (large UPDATEs, SERIALIZABLE, or lock escalation) while the screen's SELECTs wait.
- Investigate with
sp_WhoIsActive/sys.dm_exec_requests(blocking_session_id, wait types like LCK_M_S) during the window and the job's plans. - Fixes: enable READ COMMITTED SNAPSHOT so readers don't block on writers, batch the job's updates into small transactions, add indexes so it locks fewer rows, schedule heavy parts off-hours, or read reports from a readable secondary/replica.
In short: A clustered index defines the physical order of the table: its B+ tree leaf level contains the actual data rows, so there can be only one.
- A nonclustered index is a separate B+ tree whose leaf rows contain the index key plus a pointer (the clustering key or RID) to the data row; there can be many.
- Queries needing columns not in a nonclustered index do key lookups into the clustered index.
In short: A page is the basic 8 KB unit of storage and I/O, holding rows (≈8,060 bytes max in-row data) with a header and a row offset array.
- An extent is 8 contiguous pages (64 KB); SQL Server allocates space in extents.
- Special pages (GAM, SGAM, PFS, IAM) track which extents and pages are free or belong to which object.
In short: READ UNCOMMITTED (dirty reads possible), READ COMMITTED (default;
- no dirty reads), REPEATABLE READ (rows you read can't change), SERIALIZABLE (no phantoms — range locks) plus row-versioning options READ COMMITTED SNAPSHOT (statement-level consistent versions, readers don't block writers) and SNAPSHOT (transaction-level consistent snapshot with update-conflict detection).
In short: The step-by-step strategy the optimizer chose to run a query (operators like seeks, scans, joins, sorts).
- Read it right to left following the data flow, check the most expensive operators, compare estimated vs actual row counts, and look for scans on large tables, key lookups repeated many times, sorts/hash spills, implicit conversion and missing-index warnings.
- Use
SET STATISTICS IOto measure logical reads before and after a change.
Connection and TDS packets → parser (syntax) → algebrizer (binding names and types) → plan cache lookup → cost-based optimizer using statistics to choose a plan (cached) → executor runs the operators → storage engine reads pages via the buffer pool (from disk if not cached), taking locks or reading row versions per isolation level → rows streamed back to the client.
In short: Objects with a histogram (up to 200 steps) and density information about column value distributions, used by the optimizer to estimate how many rows each operation returns.
- Good estimates → good plans (right join type, memory grant, index choice).
- Stale or missing statistics lead to bad plans; they're auto-created/updated, and can be updated manually (`UPDATE STATISTICS ...
- WITH FULLSCAN`) after large data changes.
In short: SQL Server compiles a parameterised query or procedure using the parameter values of the first execution and reuses that plan for all later values.
- If data is skewed, the plan can be terrible for other values.
- Fixes: a covering index that suits all values,
OPTION (RECOMPILE),OPTIMIZE FOR, splitting logic, forcing a good plan with Query Store, or SQL Server 2022's Parameter Sensitive Plan optimisation.
In short: Every modification is written to the transaction log (with an LSN) before the data page changes reach disk, and a commit waits until its log records are hardened.
- After a crash, recovery runs analysis (find dirty pages and open transactions), redo (replay logged changes since the last checkpoint) and undo (roll back uncommitted transactions), returning the database to a consistent state.
In short: A session holding locks (usually a long or open transaction, or one scanning many rows because of missing indexes) blocks others needing incompatible locks.
- Find it with
sys.dm_exec_requests(blocking_session_id),sp_WhoIsActive, and the head blocker's query/plan; - fix by shortening transactions, adding indexes, committing promptly from the app, and enabling RCSI so readers don't block writers.
In short: READ UNCOMMITTED doesn't take shared locks, so a query can read uncommitted data that later rolls back, read the same row twice or miss rows when pages split during an allocation-order scan, and still be blocked by schema locks.
- It's "fast but wrong".
- Use RCSI or SNAPSHOT for non-blocking consistent reads instead.
In short: Check Query Store for a plan change (regressed query) — often due to a statistics update, parameter sniffing with a different first value, or an index change.
- Compare old and new plans (estimated vs actual rows, seek vs scan, join types).
- Quick fix: force the good plan in Query Store; real fix: update statistics, add/adjust a covering index, or rewrite non-SARGable predicates.
- Also rule out blocking (
LCK_M_*waits) and resource pressure (waits, memory, I/O).
In short: Capture the deadlock graph from the system_health Extended Events session, identify the statements, objects and lock types.
- Typical fixes: access tables in a consistent order, keep transactions short, add indexes so fewer rows are locked (especially on foreign keys and WHERE columns), use
UPDLOCKfor read-then-write, enable RCSI to remove reader/writer conflicts — and add retry logic for error 1205 in the application.
In short: Azure SQL Database is a fully managed single database (or elastic pool) — best for new cloud apps, with database-scoped features.
- Managed Instance is a fully managed SQL Server instance in your VNet with near-complete compatibility (SQL Agent, cross-database queries, CLR, linked servers) — best for lift-and-shift migrations.
- SQL Server on an Azure VM is IaaS with full control and 100 % compatibility but you manage patching, backups and HA.
In short: DTU bundles CPU, memory and I/O into one measure with fixed tiers (Basic, Standard, Premium) — simple but less flexible. vCore lets you choose cores, memory and storage independently, pick hardware generations and tiers (General Purpose, Business Critical, Hyperscale), use serverless compute, and save with Azure Hybrid Benefit and reserved capacity.
- Most new deployments use vCore.
In short: Enable a managed identity on the App Service, set an Entra admin on the SQL server, create a contained database user FROM EXTERNAL PROVIDER for the identity and grant roles, and use a connection string with Authentication=Active Directory Default (or Managed Identity).
- Locally the same code uses your developer login through DefaultAzureCredential.
In short: Active geo-replication replicates a single database to up to four readable secondaries in other regions, and failover is per database with connection strings pointing to specific servers.
- Failover groups manage a group of databases (or a whole managed instance) together and provide stable listener endpoints for read-write and read-only traffic that follow the primary after failover, so applications don't need connection-string changes.
- Failover can be automatic or manual.
In short: Backups are automatic: weekly full, differential every 12–24 hours and log backups every 5–10 minutes, stored in geo-redundant (or zone/locally redundant) storage.
- You can restore to any point in time within the retention period (1–35 days) as a new database, keep long-term retention backups up to 10 years, and geo-restore to another region if the primary region is unavailable.
In short: The property used to distribute items across logical and physical partitions; all items with the same value form a logical partition (max 20 GB).
- Choose a key with high cardinality and evenly spread reads/writes that is present in most queries (e.g. customerId, tenantId, deviceId) — so queries hit one partition and throughput isn't limited by a hot partition.
- It can't be changed later without migrating data.
In short: RUs are the normalised cost of operations (CPU, IO, memory): a 1 KB point read ≈ 1 RU, writes and queries cost more;
- you provision RU/s (manual/autoscale) or pay per RU in serverless.
- Consistency levels trade latency/availability for freshness: Strong, Bounded staleness, Session (default, read-your-writes), Consistent prefix and Eventual.