{"id":1270,"date":"2026-07-11T23:56:40","date_gmt":"2026-07-11T18:26:40","guid":{"rendered":"https:\/\/lastroundai.com\/blog\/?post_type=iq&#038;p=1270"},"modified":"2026-07-19T09:23:29","modified_gmt":"2026-07-19T03:53:29","slug":"sql-server","status":"publish","type":"iq","link":"https:\/\/lastroundai.com\/interview-questions\/sql-server","title":{"rendered":"SQL Server Interview Questions (2026): T-SQL &#038; Performance Q&#038;A"},"content":{"rendered":"<p>Microsoft SQL Server sits behind roughly 30.9% of the databases professional developers reported using in the <a href=\"https:\/\/survey.stackoverflow.co\/2025\/technology\" target=\"_blank\" rel=\"noopener noreferrer\">2025 Stack Overflow Developer Survey<\/a>, still the fourth most-used database in that list behind PostgreSQL, MySQL, and SQLite. That number undersells how many interviews actually touch it, because a huge share of .NET, enterprise, and internal-tooling roles run on SQL Server even when the job posting just says &#8220;SQL.&#8221; Candidates walk in ready to talk about joins and get asked why a stored procedure with a table variable is ten times slower than the same query written with a temp table, and that&#8217;s a different conversation entirely.<\/p>\n<p>This page is scoped to T-SQL and SQL Server internals specifically, not generic ANSI SQL. If you&#8217;re prepping for a PostgreSQL or MySQL role, our <a href=\"\/interview-questions\/sql\/\">general SQL interview questions<\/a> guide covers joins, subqueries, and window functions in database-agnostic syntax instead. Everything here assumes you&#8217;re sitting down in front of SSMS, Azure Data Studio, or a connection string pointed at an actual SQL Server or Azure SQL instance.<\/p>\n<p>My honest opinion: prep guides spend too much time on CREATE PROCEDURE syntax and not enough on execution plans. You can learn stored procedure syntax in twenty minutes from the docs. Reading a plan well enough to explain why the optimizer picked a table scan over a seek takes real practice, and it&#8217;s the actual skill that separates someone who can debug a slow production query from someone who can only recite definitions.<\/p>\n<p>These SQL Server interview questions cover twenty-seven items across ten areas: T-SQL basics and how they diverge from ANSI SQL, clustered versus non-clustered indexes and covering indexes, reading execution plans and sargability, stored procedures versus functions, CTEs and window functions, transactions and isolation levels, locking and deadlocks, temp tables versus table variables versus CTEs, DELETE versus TRUNCATE, and the performance problems that actually show up in production. Every code sample is T-SQL, tested against the syntax SQL Server 2019 and later actually accepts.<\/p>\n<div class=\"iq-stats not-prose\"><div class=\"iq-stat\"><span class=\"iq-stat__value\">55<\/span><span class=\"iq-stat__label\">Questions<\/span><\/div><div class=\"iq-stat\"><span class=\"iq-stat__value\">Indexes &amp; Execution Plans<\/span><span class=\"iq-stat__label\">Core Topic<\/span><\/div><div class=\"iq-stat\"><span class=\"iq-stat__value\">T-SQL &amp; DMV Queries<\/span><span class=\"iq-stat__label\">Format<\/span><\/div><div class=\"iq-stat\"><span class=\"iq-stat__value\">Read Committed<\/span><span class=\"iq-stat__label\">Default Isolation<\/span><\/div><\/div>\n<h2>T-SQL basics: what actually diverges from ANSI SQL<\/h2>\n<p>Nobody fails an interview on this section alone, but a shaky answer here signals you&#8217;ve only ever worked against MySQL or Postgres and copy-pasted your way through a SQL Server task once.<\/p>\n<div class=\"iq-dsec iq-dsec--easy\"><div class=\"iq-dsec__row\"><h2 class=\"iq-dsec__h\" id=\"easy\"><span class=\"iq-dsec__dot\" aria-hidden=\"true\"><\/span>Easy questions<\/h2><span class=\"iq-dsec__n\">17<\/span><\/div><div class=\"iq-dsec__bar\" aria-hidden=\"true\"><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What makes T-SQL different from standard ANSI SQL?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">T-SQL<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>T-SQL is Microsoft&#8217;s procedural extension of SQL, and the differences that trip people up are mostly syntactic. Variables are declared with a leading @ (DECLARE @count INT), TOP N limits row count instead of LIMIT, IDENTITY handles auto-incrementing columns instead of AUTO_INCREMENT or SERIAL, and T-SQL adds real procedural constructs: IF\/ELSE, WHILE loops, TRY\/CATCH error handling, and cursors. GO isn&#8217;t actually part of T-SQL at all, it&#8217;s a batch separator that SSMS and sqlcmd recognize client-side; the server never sees it.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">ISNULL versus COALESCE, does it actually matter which one you use?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">T-SQL<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>It does, more than the two-second answer suggests. ISNULL is T-SQL-proprietary, takes exactly two arguments, and returns a data type based on the first argument, which can silently truncate a longer string if you&#8217;re not careful. COALESCE is ANSI-standard, takes any number of arguments, and returns the highest-precedence type among all of them. I default to COALESCE unless I have a specific reason not to, mostly because the type behavior is more predictable and it isn&#8217;t proprietary if the code ever needs to move.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What are the main join types in SQL Server, and what does each one actually return?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Joins<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>INNER JOIN returns only rows that match in both tables. LEFT OUTER JOIN returns every row from the left table plus matching rows from the right, filling in NULL where there&#8217;s no match. RIGHT OUTER JOIN is the mirror image, and in practice almost nobody writes it, since you can always flip the table order and use LEFT instead. FULL OUTER JOIN returns every row from both sides, matched where possible and NULL-padded where not. CROSS JOIN returns the full Cartesian product, every row from the left paired with every row from the right, which is rarely what you want unless you&#8217;re deliberately generating combinations.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s a self join, and can you give an example where you&#039;d actually need one?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Joins<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A self join joins a table to itself, using two aliases so SQL Server can tell the two &#8220;copies&#8221; apart. The classic example is an employees table with a manager_id column that points back to another row in the same table: to list each employee next to their manager&#8217;s name, you join employees to employees a second time on manager_id = employee_id.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\nSELECT e.name AS employee, m.name AS manager\n\nFROM employees e\n\nLEFT JOIN employees m ON e.manager_id = m.employee_id;\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Why can&#039;t you filter on an aggregate like COUNT(*) in a WHERE clause?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Aggregates<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>WHERE filters individual rows before grouping happens, and an aggregate value doesn&#8217;t exist yet at that stage, there&#8217;s nothing to compare it against. HAVING runs after GROUP BY has produced its groups, so it&#8217;s the clause that actually has an aggregate value available to filter on. Logically, SQL Server evaluates a query in the order FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, which is also why you can reference a column alias in ORDER BY but not in WHERE.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\nSELECT department_id, COUNT(*) AS headcount\n\nFROM employees\n\nWHERE status = \u2018active\u2019\n\nGROUP BY department_id\n\nHAVING COUNT(*) &gt; 10;\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the difference between a clustered and a non-clustered index?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Indexes<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A clustered index determines the physical storage order of the table&#8217;s data. It isn&#8217;t a separate structure sitting next to the table, it <em>is<\/em> the table, organized as a B-tree where the leaf level holds the actual rows. A table can have exactly one clustered index, and a table with none is called a heap. A non-clustered index is a separate structure: its leaf level holds the index key plus a row locator, either the clustering key if the table has one, or a physical row ID if it&#8217;s a heap. You can have up to 999 non-clustered indexes on a table, though I&#8217;ve never seen a real schema get anywhere close to that number without something else being wrong. See Microsoft&#8217;s <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/indexes\/clustered-and-nonclustered-indexes-described\" target=\"_blank\" rel=\"noopener noreferrer\">clustered and non-clustered indexes documentation<\/a> for the full structural breakdown.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the difference between an estimated and an actual execution plan?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Execution Plans<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>An estimated plan is built from statistics without running the query at all, so its row counts are the optimizer&#8217;s compile-time guesses. An actual plan runs the query and reports what really happened: true row counts per operator, actual execution counts, and (since SQL Server 2016) live query statistics baked directly into the plan output. The single most useful thing to check first in any plan is where estimated and actual row counts diverge sharply on one operator. That gap is usually where the real problem is hiding.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What can a stored procedure do that a function can&#039;t?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Stored Procedures<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A stored procedure can run DML with side effects (INSERT, UPDATE, DELETE against real tables), use TRY\/CATCH for error handling, return multiple result sets, use output parameters, and can&#8217;t be called from inside a SELECT statement. A function has to be effectively side-effect free (it can only modify local table variables, never real tables), can be used inline in a SELECT, WHERE, or JOIN, and historically got invoked once per row, sometimes called RBAR, row by agonizing row, which was a genuine performance liability before SQL Server 2019 shipped scalar UDF inlining.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What is a CTE, and does SQL Server materialize it once or run it every time it&#039;s referenced?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">CTEs &amp; Window Functions<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A CTE, defined with WITH, is a named result set scoped to the single statement right after it. It isn&#8217;t materialized or cached: if you reference the same CTE twice in one query, SQL Server can execute the underlying logic twice, which is the opposite of how a temp table behaves. Recursive CTEs (an anchor member, a recursive member, joined by UNION ALL) are the standard way to walk an org chart or a bill-of-materials tree.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\nWITH OrgChart AS (\n\n    SELECT employee_id, manager_id, name, 0 AS depth\n\n    FROM employees WHERE manager_id IS NULL\n\n    UNION ALL\n\n    SELECT e.employee_id, e.manager_id, e.name, oc.depth + 1\n\n    FROM employees e\n\n    JOIN OrgChart oc ON e.manager_id = oc.employee_id\n\n)\n\nSELECT * FROM OrgChart ORDER BY depth;\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What isolation levels does SQL Server support, and which one is the default?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Isolation Levels<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>The ANSI standard defines four: READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, and SERIALIZABLE. SQL Server defaults to READ COMMITTED out of the box, which blocks dirty reads (you never see another transaction&#8217;s uncommitted changes) but still allows non-repeatable reads and phantom reads within the same transaction. SQL Server also adds two row-versioning-based levels beyond the standard four: SNAPSHOT and READ COMMITTED SNAPSHOT, which behave differently enough that they deserve separate treatment. Full definitions are in Microsoft&#8217;s <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/statements\/set-transaction-isolation-level-transact-sql\" target=\"_blank\" rel=\"noopener noreferrer\">SET TRANSACTION ISOLATION LEVEL documentation<\/a>.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What does ACID actually stand for, and which piece does the SQL Server transaction log protect?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Transactions<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Atomicity, Consistency, Isolation, Durability. Atomicity means a transaction&#8217;s statements either all commit or all roll back together, nothing partial. Consistency means a transaction moves the database from one valid state to another, respecting constraints and triggers along the way. Isolation is what the four isolation levels above actually control, how much of one in-flight transaction another transaction can see. Durability is what the transaction log guarantees specifically: once SQL Server reports a COMMIT as successful, that change is written to the log on disk and will survive a crash or power loss, even if the data file itself hasn&#8217;t been flushed yet.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the actual difference between blocking and a deadlock?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Locking<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Blocking is normal: one transaction holds a lock a second transaction needs, so the second one waits until the first commits, rolls back, or the wait times out. A deadlock is two or more transactions each holding a lock the other one needs, a circular wait where neither can ever proceed on its own. SQL Server&#8217;s lock monitor checks for that condition roughly every five seconds by default, more often once it starts finding them, and kills one transaction (the &#8220;deadlock victim,&#8221; normally the one with the lowest estimated rollback cost) with error 1205.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Why doesn&#039;t a CTE behave like a temp table when you reference it more than once in the same query?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Temp Tables<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A CTE isn&#8217;t materialized once and reused, it&#8217;s expanded inline everywhere it&#8217;s referenced, closer to a macro than a stored result. Reference the same CTE three times in one statement and SQL Server can run the underlying query three separate times. A temp table, by contrast, computes once, stores the result physically in tempdb, and every later reference just reads that stored data. A CTE wrapped around an expensive query and then self-joined twice is a real, easy-to-miss performance trap.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the actual difference between DELETE and TRUNCATE in SQL Server?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">DELETE vs TRUNCATE<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>DELETE is DML: it accepts a WHERE clause, fires DML triggers, and is fully logged row by row, so a big DELETE against a huge table generates a huge log. TRUNCATE behaves closer to a DDL operation: no WHERE clause allowed, it doesn&#8217;t fire DML triggers, it deallocates whole data pages instead of logging individual row deletions (so it&#8217;s minimally logged and much faster on large tables), it resets an IDENTITY column back to its seed value, and it requires ALTER permission on the table rather than DELETE permission.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">AFTER trigger versus INSTEAD OF trigger, what&#039;s actually different?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Triggers<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>An AFTER trigger fires once the triggering INSERT, UPDATE, or DELETE has already completed against the table, so the data is already there when the trigger&#8217;s logic runs. AFTER triggers only work on tables, not views. An INSTEAD OF trigger fires in place of the triggering statement, meaning the original DML never actually happens unless the trigger&#8217;s own code does it, which makes INSTEAD OF the standard way to make an otherwise non-updatable view (one built on a join, for instance) accept INSERT or UPDATE statements.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What do 1NF, 2NF, and 3NF actually require, in plain terms?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Normalization<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>First normal form requires atomic values, no repeating groups or comma-separated lists stuffed into a single column. Second normal form requires that every non-key column depends on the entire primary key, not just part of a composite key, which only matters once your primary key has more than one column. Third normal form requires that non-key columns depend only on the key, not on each other, so a customers table storing both zip_code and city violates 3NF if city can be derived from zip_code, because city now depends on another non-key column instead of directly on the customer&#8217;s primary key.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What are the three recovery models, and how do they change what backups you actually need to take?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Backup &amp; Restore<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>SIMPLE recovery model truncates the transaction log automatically after each checkpoint, which keeps log management effectively hands-off but means you can only restore to the point of your last full or differential backup, no point-in-time recovery. FULL recovery model keeps every logged transaction until a log backup runs, which is what makes point-in-time restore possible, at the cost of needing regular log backups so the log file itself doesn&#8217;t grow without bound. BULK_LOGGED is a narrow middle ground that minimally logs certain bulk operations (index rebuilds, bulk inserts) for speed, while still supporting log backups, though it gives up point-in-time recovery for the specific window a minimally-logged operation ran in. Production databases that actually matter almost always run FULL, specifically so a log backup taken minutes before an incident isn&#8217;t the most recent recovery point available.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-dsec iq-dsec--medium\"><div class=\"iq-dsec__row\"><h2 class=\"iq-dsec__h\" id=\"medium\"><span class=\"iq-dsec__dot\" aria-hidden=\"true\"><\/span>Medium questions<\/h2><span class=\"iq-dsec__n\">27<\/span><\/div><div class=\"iq-dsec__bar\" aria-hidden=\"true\"><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">TOP versus OFFSET-FETCH for paging results, which do you reach for and why?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">T-SQL<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>TOP N grabs the first N rows after sorting and works fine for &#8220;give me the 10 highest,&#8221; but it doesn&#8217;t do true paging on its own. OFFSET-FETCH, added in SQL Server 2012, skips a specified number of rows and then fetches the next batch, which is what you actually need for page 2, page 3, and so on.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\n\/* Page 3 of results, 20 rows per page, newest first *\/\n\nSELECT order_id, customer_id, order_date\n\nFROM orders\n\nORDER BY order_date DESC\n\nOFFSET 40 ROWS\n\nFETCH NEXT 20 ROWS ONLY;\n<\/code><\/pre><\/div><\/p>\n<p>One gotcha that catches people in interviews: TOP without an ORDER BY has no guaranteed row order at all. SQL Server will happily return whatever 10 rows the storage engine hands back first, and that can change between runs.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">You put a filter on the right table in the WHERE clause instead of the ON clause with a LEFT JOIN. Why does that change the result?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Joins<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Putting the filter in ON happens during the join itself, before NULL-padding kicks in, so unmatched left rows still show up with NULL in the filtered column. Putting the same filter in WHERE happens after the join has already produced its NULL-padded rows, and WHERE has no special NULL handling, so any row where that column is NULL gets thrown out entirely. That silently turns your LEFT JOIN back into something that behaves like an INNER JOIN. I&#8217;ve seen this exact bug ship to production more than once: a report that was supposed to show customers with zero orders quietly stopped showing them the day someone added AND o.status = &#8216;completed&#8217; to the WHERE clause instead of the ON clause.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\n\/* Wrong: silently drops customers with no orders *\/\n\nSELECT c.customer_id, o.order_id\n\nFROM customers c\n\nLEFT JOIN orders o ON c.customer_id = o.customer_id\n\nWHERE o.status = \u2018completed\u2019;\n\/* Right: keeps every customer, filters only matched orders *\/\n\nSELECT c.customer_id, o.order_id\n\nFROM customers c\n\nLEFT JOIN orders o ON c.customer_id = o.customer_id\n\n    AND o.status = \u2018completed\u2019;\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Does the optimizer treat a correlated subquery and an equivalent JOIN the same way?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Joins<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Often, yes. SQL Server&#8217;s optimizer frequently rewrites a correlated subquery into a join-like plan internally, especially for EXISTS, so the &#8220;subqueries run once per outer row&#8221; mental model people bring in from procedural languages usually isn&#8217;t what actually happens at execution time. That said, &#8220;often&#8221; isn&#8217;t &#8220;always,&#8221; and I wouldn&#8217;t bet a production query&#8217;s performance on the optimizer always making that rewrite. If you&#8217;re staring at a plan and a subquery genuinely is running once per row (look for a Nested Loops operator with a suspiciously high execution count on the inner side), that&#8217;s your evidence it didn&#8217;t get rewritten this time, and it&#8217;s worth trying the join form directly to see if the plan changes.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you find duplicate rows in a table?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Aggregates<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>GROUP BY the columns that define a duplicate, then HAVING COUNT(*) > 1. That&#8217;s the whole trick, and it&#8217;s asked constantly because it&#8217;s a genuinely useful pattern outside interviews too, cleaning up a staging table before it gets loaded into one with a unique constraint is a routine task on real projects.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\nSELECT email, COUNT(*) AS occurrences\n\nFROM customers\n\nGROUP BY email\n\nHAVING COUNT(*) &gt; 1;\n<\/code><\/pre><\/div><\/p>\n<p>If you also need the actual duplicate row IDs so you can delete all but one, ROW_NUMBER() partitioned by the duplicate-defining columns, then deleting everything where the row number is greater than 1, does that in a single statement without a self join.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What do ROLLUP and CUBE add on top of a plain GROUP BY?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Aggregates<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Plain GROUP BY gives you one row per unique combination of grouping columns. ROLLUP adds subtotal rows going from most detailed up to a single grand total, following the hierarchy of the columns you listed, so GROUP BY ROLLUP (region, product) gives you region-and-product rows, region-only subtotals, and one grand total row. CUBE goes further and produces subtotals for every possible combination of the grouping columns, not just the hierarchical path ROLLUP follows. The GROUPING() function tells you whether a NULL in the result came from an actual NULL value in the data or from a ROLLUP\/CUBE subtotal row, which matters because both look identical otherwise.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s a covering index, and how do included columns fit in?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Indexes<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A covering index is a non-clustered index that contains every column a specific query needs, either in the key itself or added via INCLUDE. When a query is fully covered, SQL Server answers it entirely from the non-clustered index and never touches the underlying table or clustered index. INCLUDE columns sit at the leaf level only, aren&#8217;t sorted, and don&#8217;t count toward the 16-column and 900-byte limits on index key size, which makes them the cheap way to widen a covering index without bloating the B-tree&#8217;s upper levels.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\nCREATE NONCLUSTERED INDEX IX_Orders_CustomerId\n\nON dbo.Orders (customer_id)\n\nINCLUDE (order_date, total_amount, status);\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What makes a WHERE clause non-sargable, and why do interviewers keep asking about it?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Execution Plans<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>SARGable means &#8220;Search ARGument-able,&#8221; meaning the predicate is written in a form the optimizer can use to seek an index directly. Wrap an indexed column in a function, like WHERE YEAR(order_date) = 2026, and the optimizer can&#8217;t seek on it anymore, because it would have to evaluate that function against every row first. Same problem with implicit type conversions on the column side. Interviewers ask this constantly because it&#8217;s one of the most common ways a perfectly indexed column still ends up doing a full scan, and rewriting it is almost free once you know the pattern.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\n\/* Non-sargable: index on order_date can\u2019t be seeked *\/\n\nSELECT order_id FROM orders WHERE YEAR(order_date) = 2026;\n\/* Sargable: rewritten as a range, seekable on the index *\/\n\nSELECT order_id FROM orders\n\nWHERE order_date &gt;= \u20182026-01-01\u2019 AND order_date &lt; \u20182027-01-01\u2019;\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What exactly are statistics in SQL Server, and what makes them go stale enough to trigger an automatic update?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Execution Plans<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A statistics object is a histogram plus density information describing how values are distributed across a column or index key, and it&#8217;s what the optimizer actually uses to estimate how many rows a predicate will match before it picks a plan. By default, SQL Server auto-updates statistics once roughly 20% of a table&#8217;s rows have changed since the last update (small tables use a different, more aggressive threshold), which is also why a bulk load followed immediately by a query can hit a genuinely bad plan: the insert already happened, but the auto-update hasn&#8217;t kicked in yet. AUTO_UPDATE_STATISTICS_ASYNC changes the trade-off, letting the current query run against slightly stale statistics while the update happens in the background for the next one, rather than making every query wait for a fresh sample first.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Scalar function versus inline table-valued function, why does the difference matter for performance?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Stored Procedures<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A multi-statement scalar or table-valued function is opaque to the optimizer. It gets treated as a black box, executed separately per row, with a flat cardinality estimate that has no relationship to what the function actually returns. An inline table-valued function is different: SQL Server expands it into the calling query the way it would a parameterized view, so the optimizer can push predicates through it and pick indexes based on the real logic inside. My rule of thumb: reach for an inline TVF over a multi-statement one whenever the logic is simple enough to express as a single SELECT, which covers more cases than people assume going in.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">ROW_NUMBER, RANK, and DENSE_RANK, when two rows tie, what actually happens differently?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">CTEs &amp; Window Functions<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>ROW_NUMBER assigns strictly increasing unique numbers no matter what, so tied rows get an arbitrary tiebreak based purely on the ORDER BY you gave it (add a real tiebreaker column if you need that to be deterministic). RANK gives tied rows the same rank and then skips ahead by the number of ties, so two rows tied for rank 1 push the next row to rank 3. DENSE_RANK gives tied rows the same rank without skipping, so the next distinct value gets rank 2.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\nSELECT employee_id, department_id, salary,\n\n    ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rn,\n\n    RANK()       OVER (PARTITION BY department_id ORDER BY salary DESC) AS rnk,\n\n    DENSE_RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS drnk\n\nFROM employees;\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What problem do LAG and LEAD solve that used to require a self join?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">CTEs &amp; Window Functions<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>LAG and LEAD look at a row before or after the current one within a defined ORDER BY, without a self join or a correlated subquery. Before these existed (SQL Server 2012), comparing a row&#8217;s value to the previous row&#8217;s value meant joining the table to itself on some kind of row-offset condition, which was slow and awkward to write correctly. Now it&#8217;s one window function call.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\nSELECT order_date, revenue,\n\n    LAG(revenue) OVER (ORDER BY order_date) AS prev_day_revenue,\n\n    revenue \u2013 LAG(revenue) OVER (ORDER BY order_date) AS day_over_day_change\n\nFROM daily_sales;\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you calculate a running total in T-SQL?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">CTEs &amp; Window Functions<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>SUM() as a window function with an ORDER BY inside OVER gives you a running total by default, no explicit frame clause required, because SQL Server&#8217;s default frame for an ordered window is RANGE UNBOUNDED PRECEDING to CURRENT ROW. PARTITION BY resets the running total separately per group, if you need per-customer or per-region totals instead of one total across the whole table.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\nSELECT customer_id, order_date, amount,\n\n    SUM(amount) OVER (\n\n        PARTITION BY customer_id\n\n        ORDER BY order_date\n\n        ROWS UNBOUNDED PRECEDING\n\n    ) AS running_total\n\nFROM orders;\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you find the Nth highest salary when N isn&#039;t fixed at 2?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">CTEs &amp; Window Functions<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Same DENSE_RANK pattern as finding the second-highest salary above, just parameterize the filter instead of hardcoding it to 2. Wrap the ranked CTE in a stored procedure or pass N in as a variable, and the WHERE clause in the outer query filters on that variable instead of a literal.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\nDECLARE @n INT = 4;\nWITH RankedSalaries AS (\n\n    SELECT employee_id, salary,\n\n        DENSE_RANK() OVER (ORDER BY salary DESC) AS salary_rank\n\n    FROM employees\n\n)\n\nSELECT employee_id, salary\n\nFROM RankedSalaries\n\nWHERE salary_rank = @n;\n<\/code><\/pre><\/div><\/p>\n<p>OFFSET-FETCH is a reasonable alternative if you specifically want the Nth distinct row by position rather than by rank, but it doesn&#8217;t handle tied salaries the same way DENSE_RANK does, so know which behavior the interviewer actually wants before picking one.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the actual difference between READ COMMITTED and READ COMMITTED SNAPSHOT (RCSI)?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Isolation Levels<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Default READ COMMITTED uses shared locks under the hood, meaning a reader can block a writer and a writer can block a reader while data is in flight. RCSI, a database-level setting, swaps that locking behavior for row versioning: readers get a transactionally consistent snapshot pulled from tempdb&#8217;s version store instead of taking locks, so readers stop blocking writers and vice versa. The cost isn&#8217;t free, it pushes more load onto tempdb, and reads are now consistent &#8220;as of the start of the statement&#8221; rather than reflecting whatever committed a millisecond ago mid-scan.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\nALTER DATABASE MyDatabase SET READ_COMMITTED_SNAPSHOT ON;\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s a SAVEPOINT, and when do you actually reach for one?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Transactions<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>SAVE TRANSACTION marks a point inside a larger transaction that you can roll back to without undoing the whole thing. It&#8217;s useful in a multi-step batch where step 3 out of 5 might fail in a recoverable way, you want to unwind just that step and try an alternate path, but you don&#8217;t want to lose the work steps 1 and 2 already did. In practice I don&#8217;t reach for these often, most application code is better served by keeping transactions short and handling retry logic outside the transaction entirely, but they&#8217;re the right tool for a genuinely multi-stage batch process.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\nBEGIN TRANSACTION;\n\nINSERT INTO orders (customer_id, total) VALUES (101, 249.00);\n\nSAVE TRANSACTION AfterOrderInsert;\nINSERT INTO order_items (order_id, sku, qty) VALUES (SCOPE_IDENTITY(), \u2018SKU-1\u2019, 2);\n\nIF @@ERROR &lt;&gt; 0\n\n    ROLLBACK TRANSACTION AfterOrderInsert;\nCOMMIT TRANSACTION;\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What does SET XACT_ABORT ON do, and why does it show up at the top of so many stored procedures?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Transactions<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>By default, a runtime error inside a T-SQL transaction doesn&#8217;t automatically roll the whole transaction back, some errors just abort the current statement and let execution continue to the next one, which can leave a transaction half-applied if nobody checks @@ERROR carefully after every single statement. SET XACT_ABORT ON changes that behavior: any runtime error rolls back the entire transaction automatically and stops execution of the batch. People put it at the top of every procedure that opens a transaction because it turns an easy-to-miss bug (forgetting an error check after one INSERT out of five) into something that just can&#8217;t happen.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you actually diagnose a deadlock after it&#039;s already happened?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Locking<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>The system_health Extended Events session captures deadlock graphs automatically, no setup required, and it&#8217;s usually the first place to look. Trace flag 1222 is the older approach, dumping the deadlock graph as XML straight into the SQL Server error log. Either way, the graph shows both transactions&#8217; statements, exactly which resources (locks on which pages or objects) each side held and was waiting on, and which one got picked as the victim.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\nSELECT xed.value(\u2018(event\/data\/value)[1]\u2019, \u2018varchar(max)\u2019) AS deadlock_graph\n\nFROM (\n\n    SELECT XEventData = CAST(target_data AS XML)\n\n    FROM sys.dm_xe_session_targets st\n\n    JOIN sys.dm_xe_sessions s ON s.address = st.event_session_address\n\n    WHERE s.name = \u2018system_health\u2019 AND st.target_name = \u2018ring_buffer\u2019\n\n) AS Data\n\nCROSS APPLY XEventData.nodes(\u2018RingBufferTarget\/event[@name=\u201dxml_deadlock_report\u201d]\u2019) AS XEventData(xed);\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Temp table or table variable, when do you actually pick one over the other?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Temp Tables<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Both live in tempdb under the hood, so the &#8220;table variables are in memory&#8221; claim floating around isn&#8217;t accurate. The real differences are statistics and scope. A #temp table gets real column statistics, so the optimizer can make a reasonable cardinality estimate against it, and you can add your own indexes to it explicitly. A @table variable had no column statistics at all until SQL Server 2019&#8217;s table variable deferred compilation, which delays plan compilation until the table variable&#8217;s first actual execution so the optimizer can see real row counts instead of always assuming one row. My rule: anything beyond a few hundred rows, or anything that needs an index the optimizer will genuinely seek against, gets a #temp table. Small lookup sets scoped to one batch are fine as table variables.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What does tempdb actually get used for besides #temp tables, and why does contention show up there specifically?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Temp Tables<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Tempdb is shared by the entire instance, and it backs a lot more than the #temp tables developers create on purpose: the row-versioning store behind RCSI and SNAPSHOT isolation lives there, sort and hash operations that spill to disk during a big query land there, and internal work tables SQL Server builds for things like a hash join or a big GROUP BY use it too. Because every database on the instance shares one tempdb, and every session hitting any of those features competes for the same allocation pages, contention on the shared allocation structures (GAM, SGAM, PFS pages) used to be a common bottleneck under heavy concurrent load. The standard fix is configuring multiple tempdb data files (Microsoft&#8217;s current guidance is one file per logical CPU core up to eight, then evaluate further based on observed contention), which spreads that allocation work across files instead of funneling everything through one.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">True or false: TRUNCATE can&#039;t be rolled back. Defend your answer.<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">DELETE vs TRUNCATE<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>False, and it&#8217;s one of the most confidently wrong answers I hear in interview prep. Run TRUNCATE TABLE inside an explicit transaction and it absolutely can be rolled back, because the page deallocations are still logged enough to reverse, just not as a row-by-row log entry the way DELETE is.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\nBEGIN TRANSACTION;\n\nTRUNCATE TABLE staging.temp_import;\n\n\/* verify something looks wrong before committing *\/\n\nROLLBACK TRANSACTION;\n<\/code><\/pre><\/div><\/p>\n<p>The &#8220;TRUNCATE can&#8217;t be rolled back&#8221; reputation comes from running it in autocommit mode with no explicit transaction open, which is a completely different problem than what TRUNCATE itself is capable of. Getting this one right, and explaining why, tends to land better than reciting &#8220;TRUNCATE is DDL, DELETE is DML&#8221; from a flashcard.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What are the inserted and deleted tables inside a trigger, and how do you use them for an UPDATE?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Triggers<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Both are special in-memory tables SQL Server populates automatically for the duration of the trigger, mirroring the structure of the table the trigger fired on. For an INSERT, only inserted has rows. For a DELETE, only deleted has rows. For an UPDATE, both are populated at once, deleted holds the pre-update values and inserted holds the post-update values, which is exactly what you need to build an audit trigger that logs old value next to new value for every changed column.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\nCREATE TRIGGER trg_Employees_AuditSalary\n\nON employees\n\nAFTER UPDATE\n\nAS\n\nBEGIN\n\n    IF UPDATE(salary)\n\n    INSERT INTO salary_audit (employee_id, old_salary, new_salary, changed_at)\n\n    SELECT i.employee_id, d.salary, i.salary, SYSUTCDATETIME()\n\n    FROM inserted i\n\n    JOIN deleted d ON i.employee_id = d.employee_id\n\n    WHERE i.salary &lt;&gt; d.salary;\n\nEND;\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">When would you deliberately denormalize a schema that&#039;s already in 3NF?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Normalization<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Reporting and analytics workloads are the usual case: a star schema with a wide, deliberately denormalized fact table avoids the multi-table joins a fully normalized OLTP schema would need for every single report query, and read-heavy dashboards care a lot more about query speed than about avoiding update anomalies. A cached, precomputed summary column (an order&#8217;s total stored directly on the order row instead of recalculated from order_items every time) is a smaller-scale version of the same trade. The honest cost is that denormalized data can drift out of sync with its source if the update path isn&#8217;t airtight, so I only reach for it once I&#8217;ve actually measured that the join cost is a real problem, not because normalized schemas are inherently slow.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Surrogate key versus natural key, which do you default to and why?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Normalization<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A surrogate key is a system-generated value with no business meaning (an IDENTITY int or a GUID), while a natural key is a real-world attribute that happens to be unique (an email address, a national ID number, an SKU). I default to surrogate keys almost every time, mostly because business meaning tends to change (an email gets updated, a SKU gets renumbered by a vendor) and a key that has to change value is expensive to change everywhere it&#8217;s referenced as a foreign key. The one place I don&#8217;t reflexively reach for a surrogate key is a small, genuinely immutable code table, like a two-letter country code or a status enum, where the natural key is also the most readable thing in every query that joins to it.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What is parameter sniffing, and why does the exact same stored procedure sometimes run fast and sometimes crawl?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Performance<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>SQL Server compiles and caches a plan the first time a stored procedure runs, built around whatever parameter values were passed in at that moment. If the first call filters to 3 rows, the cached plan gets optimized for a tiny result set, typically nested loops and index seeks. Call the same cached plan later with a parameter matching 3 million rows and SQL Server reuses that same tiny-result-set plan instead of recompiling, dragging a nested-loop plan across millions of rows it was never shaped for. Fixes include OPTION (RECOMPILE) to force a fresh plan every call (real CPU cost each time), OPTIMIZE FOR UNKNOWN to compile against average statistical distribution instead of the first call&#8217;s specific value, or splitting the logic into separate procedures for known skewed cases.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Give a real example of an implicit conversion silently killing index performance.<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Performance<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A column is defined nvarchar, and the application (or an ORM) sends a plain varchar parameter, or the reverse. SQL Server has to convert one side before it can compare them, and if the conversion has to happen on the column side rather than the literal, the index on that column becomes unseekable, forcing a full scan even though the column is indexed and the query looks perfectly reasonable. The fix is matching parameter and application data types to the actual column types, or explicitly casting the constant instead of leaving the conversion to chance. This one is genuinely hard to spot by reading the query text alone. It only shows up once you actually look at the execution plan and see an unexpected CONVERT_IMPLICIT operator.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Full, differential, and transaction log backups, how do they actually fit together during a restore?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Backup &amp; Restore<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A full backup captures the entire database at a point in time and is always the base every restore chain starts from. A differential backup captures everything that changed since the last full backup (not since the last differential), so restoring a full plus one differential only ever needs those two files regardless of how many differentials happened in between. A transaction log backup captures the log records since the last log backup and is what enables restoring forward to any specific point in time, not just to whenever the last full or differential happened to run. The restore order is always full, then the most recent differential (if any), then every log backup taken after that differential, in order, ending with WITH RECOVERY on the final one to actually bring the database online.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\nRESTORE DATABASE Sales FROM DISK = \u2018D:\\Backups\\Sales_Full.bak\u2019 WITH NORECOVERY;\n\nRESTORE DATABASE Sales FROM DISK = \u2018D:\\Backups\\Sales_Diff.bak\u2019 WITH NORECOVERY;\n\nRESTORE LOG Sales FROM DISK = \u2018D:\\Backups\\Sales_Log1.trn\u2019 WITH NORECOVERY;\n\nRESTORE LOG Sales FROM DISK = \u2018D:\\Backups\\Sales_Log2.trn\u2019 WITH RECOVERY;\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What is an Always On Availability Group, and how is it actually different from database mirroring?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">High Availability<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>An Availability Group replicates one or more databases to up to eight secondary replicas, keeping them in sync through the same log-shipping mechanism under the hood, and can fail over automatically to a secondary if the primary goes down. Database mirroring, the older feature it effectively replaced, only ever supported a single database per mirroring session and a single secondary, no readable secondaries, and Microsoft deprecated it years ago in favor of Availability Groups. An AG&#8217;s readable secondary replicas are the practical difference people care about day to day: you can point reporting workloads at a secondary and take load off the primary entirely, which mirroring never offered. See Microsoft&#8217;s <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/database-engine\/availability-groups\/windows\/overview-of-always-on-availability-groups-sql-server\" target=\"_blank\" rel=\"noopener noreferrer\">Always On Availability Groups overview<\/a> for the full architecture.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-dsec iq-dsec--hard\"><div class=\"iq-dsec__row\"><h2 class=\"iq-dsec__h\" id=\"hard\"><span class=\"iq-dsec__dot\" aria-hidden=\"true\"><\/span>Hard questions<\/h2><span class=\"iq-dsec__n\">11<\/span><\/div><div class=\"iq-dsec__bar\" aria-hidden=\"true\"><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Why can NOT IN silently return zero rows when NOT EXISTS returns the correct result?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Joins<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>If the subquery in a NOT IN returns even a single NULL, the entire NOT IN comparison evaluates to UNKNOWN for every outer row, not just the row that would have matched the NULL, and SQL Server discards UNKNOWN the same way it discards FALSE. The practical result is a query that looks correct, runs without error, and quietly returns an empty result set the day someone&#8217;s foreign key column allows NULL and one row actually has one. NOT EXISTS doesn&#8217;t have this problem, because it&#8217;s checking for the existence of a matching row rather than comparing values against a list that might contain NULL. My default now is NOT EXISTS whenever there&#8217;s any chance the subquery&#8217;s column is nullable, which in practice is most of the time unless I wrote the schema myself.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\n\/* Dangerous if orders.customer_id allows NULL *\/\n\nSELECT customer_id FROM customers\n\nWHERE customer_id NOT IN (SELECT customer_id FROM orders);\n\/* Safe regardless of NULLs *\/\n\nSELECT c.customer_id FROM customers c\n\nWHERE NOT EXISTS (\n\n    SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id\n\n);\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">A query filters on an indexed column but the plan still shows a Key Lookup for every row. Why, and how do you fix it?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Indexes<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A Key Lookup happens when the non-clustered index used for the seek doesn&#8217;t contain every column the query needs. SQL Server seeks the non-clustered index to find matching rows, then goes back to the clustered index (or heap) once per matching row to fetch the missing columns. That per-row round trip is cheap for a handful of rows and brutal for a few hundred thousand, at which point the optimizer usually gives up on the seek entirely and scans instead. The fix is almost always to widen the non-clustered index with INCLUDE so the lookup disappears and the plan becomes a plain index seek. Sometimes the honest answer is that the lookup is fine, if the query only ever touches a small number of rows, chasing a covering index isn&#8217;t worth the extra write overhead on every insert and update.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">You see a huge gap between estimated and actual rows on one plan operator. What&#039;s your first guess, and what do you check?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Execution Plans<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Outdated or missing statistics is the first guess, and it&#8217;s right often enough to check it before anything else, either by looking at STATS_DATE() on the relevant index or running sys.dm_db_stats_properties. It could also be a multi-statement table-valued function feeding the query, since those get a flat, almost-always-wrong row estimate regardless of the actual data. Or it could be parameter sniffing, where the cached plan was compiled for a very different data distribution than the one running now. I don&#8217;t have one universal fix that works every time here. Sometimes it&#8217;s UPDATE STATISTICS, sometimes it&#8217;s OPTION (RECOMPILE), and figuring out which one applies to a specific query is honestly a big part of the job.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you find missing index suggestions, and how much should you actually trust them?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Execution Plans<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>sys.dm_db_missing_index_details, along with the related group and group stats DMVs, tracks columns the optimizer wanted an index on but couldn&#8217;t find one for, going all the way back to the last service restart. Not very much, is my honest answer to the trust question. These DMVs don&#8217;t account for indexes you already have that are close but not quite covering, don&#8217;t know anything about write overhead on the table, and will happily suggest three overlapping indexes that do almost the same job if three similar queries ran. I use them as a starting point for investigation, never as a create-index script to run unmodified. The actual decision needs a look at the query workload as a whole, not one query&#8217;s missing-index hint in isolation.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\nSELECT d.statement AS table_name, d.equality_columns,\n\n    d.inequality_columns, d.included_columns,\n\n    s.avg_total_user_cost * s.avg_user_impact * (s.user_seeks + s.user_scans) AS estimated_benefit\n\nFROM sys.dm_db_missing_index_details d\n\nJOIN sys.dm_db_missing_index_groups g ON d.index_handle = g.index_handle\n\nJOIN sys.dm_db_missing_index_group_stats s ON g.index_group_handle = s.group_handle\n\nORDER BY estimated_benefit DESC;\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How would you find the second-highest salary per department using a window function?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">CTEs &amp; Window Functions<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Wrap DENSE_RANK() in a CTE, partitioned by department and ordered by salary descending, then filter the outer query for rank = 2. You can&#8217;t filter a window function&#8217;s result directly in the same SELECT&#8217;s WHERE clause, since WHERE evaluates before window functions do, so it has to go in a CTE or subquery. DENSE_RANK matters over ROW_NUMBER here specifically because of ties: if two employees in a department share the highest salary, ROW_NUMBER just hands you another top earner as &#8220;rank 2,&#8221; while DENSE_RANK correctly skips to the actual second distinct salary value.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\nWITH RankedSalaries AS (\n\n    SELECT department_id, employee_id, salary,\n\n        DENSE_RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS salary_rank\n\n    FROM employees\n\n)\n\nSELECT department_id, employee_id, salary\n\nFROM RankedSalaries\n\nWHERE salary_rank = 2;\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Why would a team turn on full SNAPSHOT isolation instead of just using RCSI?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Isolation Levels<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>SNAPSHOT isolation is a separate, opt-in level (SET TRANSACTION ISOLATION LEVEL SNAPSHOT) that gives the entire transaction one consistent view from the moment it started, not per-statement the way RCSI works. That matters for a multi-statement reporting transaction that genuinely needs every read across several queries to reflect the exact same point in time. The trade-off is real: write-write conflicts under SNAPSHOT isolation throw error 3960 and abort the transaction outright, so your application needs retry logic it might not otherwise have needed. I&#8217;d reach for RCSI first almost every time I&#8217;ve had the choice. Full SNAPSHOT isolation earns its complexity less often than people assume walking into this question.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">A batch that used to run fine suddenly causes lock escalation to a full table lock. What triggers that, and how do you fix it?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Locking<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>SQL Server escalates row and page locks up to a single table-level lock once a statement holds roughly 5,000 locks on one table or index in a single go, mostly to keep lock-manager memory from ballooning. Once that escalation happens, any other session needing a lock on that table blocks, even for rows nowhere near the ones the original statement actually touched. The usual fix is batching large UPDATE or DELETE operations into smaller chunks so no single statement ever gets close to that threshold. ALTER TABLE &#8230; SET (LOCK_ESCALATION = DISABLE) is available as a targeted override, but it&#8217;s not a free fix, it trades broad blocking for higher sustained lock-manager memory use, and I&#8217;d only reach for it after batching the statement first didn&#8217;t solve the actual problem. See Microsoft&#8217;s <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/sql-server-transaction-locking-and-row-versioning-guide\" target=\"_blank\" rel=\"noopener noreferrer\">transaction locking and row versioning guide<\/a> for the full escalation rules.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Why do most teams avoid stacking multiple triggers or letting triggers call other triggers?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Triggers<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Every trigger on a table fires for every qualifying statement, silently, with no line of code at the call site hinting that extra logic is about to run. Stack three AFTER triggers on the same table and a single UPDATE can cascade into a chain of side effects that&#8217;s genuinely hard to reason about from reading the original statement alone, and debugging usually means stepping through each trigger by hand to find where something unexpected happened. Nested triggers (one trigger&#8217;s DML firing another table&#8217;s trigger) make this worse and can hit SQL Server&#8217;s nesting-level limit or, if RECURSIVE_TRIGGERS is on, actually loop. My rule of thumb: one trigger per table per operation, keep the logic inside it boring and obvious, and if the logic needs to branch into several different concerns, that&#8217;s usually a sign the work belongs in application code or a scheduled job instead.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">A stored procedure with a row-by-row cursor takes 40 minutes. Rewritten as a set-based UPDATE it takes 12 seconds. Why is the gap that big?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Performance<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A cursor processes one row per iteration, and each FETCH is its own round trip through the query engine, with per-row overhead (lock acquisition, log activity, sometimes tempdb work) multiplied by however many rows exist. A set-based UPDATE lets the optimizer plan the whole operation as one batch, taking full advantage of index seeks and sequential I\/O, with one coordinated set of locks and log writes covering the entire operation instead of thousands of tiny separate ones. There are legitimate uses for cursors, admin scripts and a few genuine ETL edge cases come to mind, but defaulting to row-by-row processing for ordinary business logic is close to the single most common performance mistake I see in T-SQL written by people coming from a procedural-first background.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you restore a database to a specific point in time, say two minutes before a bad DELETE ran?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Backup &amp; Restore<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>This only works at all if the database is in FULL (or BULK_LOGGED, with caveats) recovery model and log backups have actually been running, so it&#8217;s worth stating that precondition before diving into syntax. Restore the last full backup WITH NORECOVERY, then the most recent differential if one exists, then each log backup in order, and on the log backup that covers the moment the bad DELETE happened, use STOPAT with a timestamp a moment before the DELETE instead of restoring the whole log file. The last statement needs WITH RECOVERY to actually bring the database online afterward, since every prior step leaves it in a restoring state on purpose.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">sql<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-sql\">\n\nRESTORE DATABASE Sales FROM DISK = \u2018D:\\Backups\\Sales_Full.bak\u2019 WITH NORECOVERY;\n\nRESTORE LOG Sales FROM DISK = \u2018D:\\Backups\\Sales_Log_Incident.trn\u2019\n\n    WITH STOPAT = \u20182026-07-18T14:32:00\u2019, RECOVERY;\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Synchronous versus asynchronous commit in an Availability Group, what&#039;s the actual trade-off?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">High Availability<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Synchronous commit waits for the secondary replica to confirm the transaction log record was hardened to disk before the primary reports COMMIT back to the application, guaranteeing zero data loss on failover at the cost of added commit latency, since every write now waits on a network round trip to the secondary. Asynchronous commit doesn&#8217;t wait, the primary commits as soon as its own log write finishes, so latency looks like a standalone instance, but a failover can lose whatever transactions hadn&#8217;t made it to the secondary yet. I&#8217;ve only seen synchronous commit make sense within a single datacenter or availability zone where the network round trip is a couple of milliseconds. Cross-region replicas, where the round trip is 30 to 80 milliseconds depending on distance, are asynchronous in practically every setup I&#8217;ve come across, because synchronous commit at that latency would make every write noticeably slower for zero acceptable business reason.<\/p>\n<p><\/div><\/div><\/div>\n<h2>How to prepare for a SQL Server interview in 2026<\/h2>\n<p>Install SQL Server Developer Edition or spin up the mssql-server Docker image, both free, and pull down Microsoft&#8217;s WideWorldImporters sample database instead of practicing on toy tables with five rows. Turn on &#8220;Include Actual Execution Plan&#8221; in SSMS (Ctrl+M) before running anything, so reading a plan becomes a reflex instead of something you only do during a live interview. Then break things on purpose: open two query windows, start a transaction in one without committing it, run a conflicting update in the other, and watch it block in sys.dm_exec_requests. Force a deadlock between two sessions and pull the graph out of system_health afterward. None of that takes more than an evening, and it teaches you more about locking than another week of reading definitions would.<\/p>\n<p>Across mock interviews run through LastRoundAI tagged SQL Server or .NET backend, the isolation-level and locking questions trip up more candidates than the indexing questions, even though most prep time skews the other way. My guess is that indexing has a clean, memorizable mental model (B-tree, seek versus scan), while isolation levels require actually reasoning through what two concurrent transactions each see, and that doesn&#8217;t compress into a flashcard the same way. We don&#8217;t have a clean percentage to attach to that observation, only that it comes up often enough in review to be worth flagging here.<\/p>\n<h2>Defend your T-SQL answers before an interviewer changes the parameters on you<\/h2>\n<p>Most SQL Server interview questions are easy to answer from a script and hard to defend once someone changes the scenario mid-question, doubles the row count, drops the index you were relying on, or asks what happens under RCSI instead of default READ COMMITTED. <a href=\"\/products\/mock-interviews\">LastRoundAI&#8217;s mock interview mode<\/a> runs backend and database-focused rounds with follow-ups that adapt to what you actually said, not a fixed script, and the free plan includes 15 credits a month that reset monthly. Starter is $19\/mo if fifteen sessions isn&#8217;t enough runway to feel ready.<\/p>\n<p>If the harder problem right now is finding enough SQL Server or backend roles worth interviewing for in the first place, <a href=\"\/products\/auto-apply\">Auto-Apply<\/a> queues tailored applications for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and nothing goes out until you approve it.<\/p>\n<p>Questions about either product go to contact@lastroundai.com. That&#8217;s the only inbox we check.<\/p>\n<div class=\"iq-sources not-prose\"><div class=\"iq-sources__title\">Sources &amp; further reading<\/div><ul class=\"iq-sources__list\"><li><a href=\"https:\/\/survey.stackoverflow.co\/2025\/technology\" target=\"_blank\" rel=\"nofollow noopener\">Stack Overflow Developer Survey 2025: Technology<\/a><\/li><li><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/indexes\/clustered-and-nonclustered-indexes-described\" target=\"_blank\" rel=\"nofollow noopener\">Microsoft Learn: Clustered and Nonclustered Indexes Described<\/a><\/li><li><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/statements\/set-transaction-isolation-level-transact-sql\" target=\"_blank\" rel=\"nofollow noopener\">Microsoft Learn: SET TRANSACTION ISOLATION LEVEL (Transact-SQL)<\/a><\/li><li><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/sql-server-transaction-locking-and-row-versioning-guide\" target=\"_blank\" rel=\"nofollow noopener\">Microsoft Learn: SQL Server Transaction Locking and Row Versioning Guide<\/a><\/li><\/ul><\/div>\n","protected":false},"excerpt":{"rendered":"<p>Microsoft SQL Server sits behind roughly 30.9% of the databases professional developers reported using in the 2025 Stack Overflow Developer Survey, still the fourth most-used database in that list behind PostgreSQL, MySQL, and SQLite. That number undersells how many interviews actually touch it, because a huge share of .NET, enterprise, and internal-tooling roles run on&#8230;<\/p>\n","protected":false},"author":5,"featured_media":1709,"comment_status":"open","ping_status":"closed","template":"","meta":{"_kad_post_transparent":"","_kad_post_title":"","_kad_post_layout":"","_kad_post_sidebar_id":"","_kad_post_content_style":"","_kad_post_vertical_padding":"","_kad_post_feature":"","_kad_post_feature_position":"","_kad_post_header":false,"_kad_post_footer":false,"_kad_post_classname":"","footnotes":""},"tags":[],"class_list":["post-1270","iq","type-iq","status-publish","has-post-thumbnail","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>SQL Server Interview Questions (2026) | LastRoundAI<\/title>\n<meta name=\"description\" content=\"Real SQL Server interview questions for 2026: T-SQL basics, indexes, execution plans, isolation levels, and the performance bugs that trip up candidates.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/lastroundai.com\/interview-questions\/sql-server\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"SQL Server Interview Questions (2026) | LastRoundAI\" \/>\n<meta property=\"og:description\" content=\"Real SQL Server interview questions for 2026: T-SQL basics, indexes, execution plans, isolation levels, and the performance bugs that trip up candidates.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/lastroundai.com\/interview-questions\/sql-server\" \/>\n<meta property=\"og:site_name\" content=\"LastRound AI\" \/>\n<meta property=\"article:modified_time\" content=\"2026-07-19T03:53:29+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/07\/iq-sql-server-og.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"20 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/sql-server\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/sql-server\",\"name\":\"SQL Server Interview Questions (2026) | LastRoundAI\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/sql-server#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/sql-server#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/iq-sql-server-og.png\",\"datePublished\":\"2026-07-11T18:26:40+00:00\",\"dateModified\":\"2026-07-19T03:53:29+00:00\",\"description\":\"Real SQL Server interview questions for 2026: T-SQL basics, indexes, execution plans, isolation levels, and the performance bugs that trip up candidates.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/sql-server#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/sql-server\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/sql-server#primaryimage\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/iq-sql-server-og.png\",\"contentUrl\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/iq-sql-server-og.png\",\"width\":1200,\"height\":630,\"caption\":\"SQL Server interview questions \u2014 LastRoundAI\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/sql-server#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/lastroundai.com\\\/blog\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Interview Questions\",\"item\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"SQL Server Interview Questions (2026): T-SQL &#038; Performance Q&#038;A\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/\",\"name\":\"LastRound AI\",\"description\":\"Interview Assistant prep, tech careers and AI tools\",\"publisher\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#organization\",\"name\":\"LastRound AI\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/lastroundai-transprant-logo-optimized-BxEo2Wtq.png\",\"contentUrl\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/lastroundai-transprant-logo-optimized-BxEo2Wtq.png\",\"width\":400,\"height\":400,\"caption\":\"LastRound AI\"},\"image\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"SQL Server Interview Questions (2026) | LastRoundAI","description":"Real SQL Server interview questions for 2026: T-SQL basics, indexes, execution plans, isolation levels, and the performance bugs that trip up candidates.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/lastroundai.com\/interview-questions\/sql-server","og_locale":"en_US","og_type":"article","og_title":"SQL Server Interview Questions (2026) | LastRoundAI","og_description":"Real SQL Server interview questions for 2026: T-SQL basics, indexes, execution plans, isolation levels, and the performance bugs that trip up candidates.","og_url":"https:\/\/lastroundai.com\/interview-questions\/sql-server","og_site_name":"LastRound AI","article_modified_time":"2026-07-19T03:53:29+00:00","og_image":[{"width":1200,"height":630,"url":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/07\/iq-sql-server-og.png","type":"image\/png"}],"twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"20 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/lastroundai.com\/interview-questions\/sql-server","url":"https:\/\/lastroundai.com\/interview-questions\/sql-server","name":"SQL Server Interview Questions (2026) | LastRoundAI","isPartOf":{"@id":"https:\/\/lastroundai.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/lastroundai.com\/interview-questions\/sql-server#primaryimage"},"image":{"@id":"https:\/\/lastroundai.com\/interview-questions\/sql-server#primaryimage"},"thumbnailUrl":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/07\/iq-sql-server-og.png","datePublished":"2026-07-11T18:26:40+00:00","dateModified":"2026-07-19T03:53:29+00:00","description":"Real SQL Server interview questions for 2026: T-SQL basics, indexes, execution plans, isolation levels, and the performance bugs that trip up candidates.","breadcrumb":{"@id":"https:\/\/lastroundai.com\/interview-questions\/sql-server#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/lastroundai.com\/interview-questions\/sql-server"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/lastroundai.com\/interview-questions\/sql-server#primaryimage","url":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/07\/iq-sql-server-og.png","contentUrl":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/07\/iq-sql-server-og.png","width":1200,"height":630,"caption":"SQL Server interview questions \u2014 LastRoundAI"},{"@type":"BreadcrumbList","@id":"https:\/\/lastroundai.com\/interview-questions\/sql-server#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/lastroundai.com\/blog"},{"@type":"ListItem","position":2,"name":"Interview Questions","item":"https:\/\/lastroundai.com\/interview-questions"},{"@type":"ListItem","position":3,"name":"SQL Server Interview Questions (2026): T-SQL &#038; Performance Q&#038;A"}]},{"@type":"WebSite","@id":"https:\/\/lastroundai.com\/blog\/#website","url":"https:\/\/lastroundai.com\/blog\/","name":"LastRound AI","description":"Interview Assistant prep, tech careers and AI tools","publisher":{"@id":"https:\/\/lastroundai.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/lastroundai.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/lastroundai.com\/blog\/#organization","name":"LastRound AI","url":"https:\/\/lastroundai.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/lastroundai.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/06\/lastroundai-transprant-logo-optimized-BxEo2Wtq.png","contentUrl":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/06\/lastroundai-transprant-logo-optimized-BxEo2Wtq.png","width":400,"height":400,"caption":"LastRound AI"},"image":{"@id":"https:\/\/lastroundai.com\/blog\/#\/schema\/logo\/image\/"}}]}},"_links":{"self":[{"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/iq\/1270","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/iq"}],"about":[{"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/types\/iq"}],"author":[{"embeddable":true,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/users\/5"}],"replies":[{"embeddable":true,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/comments?post=1270"}],"version-history":[{"count":3,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/iq\/1270\/revisions"}],"predecessor-version":[{"id":1523,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/iq\/1270\/revisions\/1523"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/media\/1709"}],"wp:attachment":[{"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/media?parent=1270"}],"wp:term":[{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/tags?post=1270"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}