Interview Handbook by Sayed Elmahdy العربية

Topic 10 · Data & databases

SQL & databases

Joins, GROUP BY, window functions, indexes, normalization, ACID, NoSQL, practice queries.

24 questionsJunior · 8Mid · 10Senior · 612 quiz

Concepts

What you will learn: Use to describe data and relationships, then write queries that return the intended rows without accidental omissions or duplicates. The topic moves from keys and constraints through joins, aggregates, indexes and execution plans, connecting the question shape to and execution.

Before you start: A table stores rows, each representing a record; a column is a property such as price. A primary key identifies a row, and a foreign key relates it to a row in another table. If this is new, start with the first card and what happens to the rows.

Select a term with a dotted underline for its meaning, then continue reading from the same place.

#Keys, constraints and data types

validation cannot protect data from scripts or racing requests on its own. Database constraints are the final defense for relationships and uniqueness.

-- SQL Server / T-SQL unless a block explicitly says PostgreSQL.
CREATE TABLE Customers (
    Id int IDENTITY PRIMARY KEY,                         -- (1)
    Name nvarchar(100) NOT NULL,
    Email nvarchar(256) NOT NULL UNIQUE
);
CREATE TABLE Orders (
    Id int IDENTITY PRIMARY KEY,
    CustomerId int NOT NULL REFERENCES Customers(Id),    -- (2)
    CreatedAt datetime2 NOT NULL DEFAULT SYSUTCDATETIME(),
    Status varchar(20) NOT NULL,
    Total decimal(18,2) NOT NULL CHECK (Total >= 0)
);
CREATE TABLE OrderLines (
    OrderId int NOT NULL REFERENCES Orders(Id),
    LineNo int NOT NULL,
    ProductId int NOT NULL,
    Quantity int NOT NULL CHECK (Quantity > 0),
    PRIMARY KEY (OrderId, LineNo)                         -- (3)
);
🧾 Code walkthrough
  1. (1) A table has one primary key, possibly composite, unique and non-null. A candidate key is a minimal identifying set; unchosen candidates are alternate keys. A can contain extra columns.
  2. (2) A foreign key references a candidate key enforced by PRIMARY KEY or UNIQUE. Server does not automatically index the referencing column. DEFAULT supplies an omitted value; CHECK does not replace NOT .
  3. (3) A composite key contains multiple columns. Surrogate keys such as Id are stable; natural keys such as email carry business meaning and may change.

A SQL Server single-column unique constraint usually permits one NULL; PostgreSQL permits multiple NULLs by default and supports NULLS NOT DISTINCT. CHAR is fixed-length; VARCHAR is variable-length and uses collation-dependent encoding, including Unicode with -8 collations. NVARCHAR uses UTF-16, so a character does not always occupy two bytes. Use decimal for money with explicit precision and rounding rules.

#Every join on one sample dataset

A join combines rows by a relationship and can repeat a left row for multiple matches. This is an unconstrained teaching staging dataset: order 13 has a missing customer, which the earlier foreign key would prevent.

CREATE TABLE #Customers (Id int, Name varchar(20));
CREATE TABLE #Orders (Id int, CustomerId int, Total decimal(10,2));
INSERT INTO #Customers VALUES (1,'Ali'),(2,'Mona'),(3,'Omar');
INSERT INTO #Orders VALUES (10,1,500),(11,1,200),(12,2,300),(13,99,50);

-- (1) Matched rows only.
SELECT c.Name, o.Id FROM #Customers c
INNER JOIN #Orders o ON o.CustomerId = c.Id;
-- Ali 10; Ali 11; Mona 12

-- (2) Preserve the left side.
SELECT c.Name, o.Id FROM #Customers c
LEFT JOIN #Orders o ON o.CustomerId = c.Id;
-- Ali 10; Ali 11; Mona 12; Omar NULL

-- (3) Preserve the right side.
SELECT c.Name, o.Id FROM #Customers c
RIGHT JOIN #Orders o ON o.CustomerId = c.Id;
-- Ali 10; Ali 11; Mona 12; NULL 13

-- (4) Preserve both sides.
SELECT c.Name, o.Id FROM #Customers c
FULL OUTER JOIN #Orders o ON o.CustomerId = c.Id;
-- Ali 10; Ali 11; Mona 12; Omar NULL; NULL 13

-- (5) Every combination: 3 customers x 4 orders = 12 rows.
SELECT c.Id AS CustomerId, o.Id AS OrderId
FROM #Customers c CROSS JOIN #Orders o;

-- (6) Self join: unordered pairs of orders belonging to the same customer.
SELECT a.Id, b.Id FROM #Orders a JOIN #Orders b
ON a.CustomerId = b.CustomerId AND a.Id < b.Id;
-- 10 11

-- (7) Anti-join: customers without any order.
SELECT c.Name FROM #Customers c LEFT JOIN #Orders o
ON o.CustomerId = c.Id WHERE o.Id IS NULL;
-- Omar
🧾 Code walkthrough
  1. (1)–(4) The distinction is which unmatched rows survive, with NULLs on the missing side. Results illustrate sets, not guaranteed ordering without ORDER BY.
  2. (5) CROSS has no relationship predicate. It generates useful combinations but can grow rapidly.
  3. (6) A self join uses aliases for the same table. The Id comparison excludes self-pairs and reversed duplicates.
  4. (7) Testing a non-nullable right key for detects a missing match; NOT EXISTS is often clearer.
After a LEFT JOIN, WHERE o.Total > 100 removes unmatched customers because comparison with NULL is . Put the right-side filter in ON to retain every customer.

#Logical query order, WHERE and HAVING

Logical processing explains why a SELECT alias is unavailable in WHERE. This is semantic order, not physical execution order; the optimizer can rearrange work while preserving results.

FROM / JOIN / ON -> WHERE -> GROUP BY -> HAVING
-> SELECT -> DISTINCT -> ORDER BY -> paging / limiting
SELECT CustomerId, COUNT(*) AS OrderCount, SUM(Total) AS Revenue
FROM Orders
WHERE Status = 'Paid' AND CreatedAt >= '20260101'         -- (1)
  AND CreatedAt < '20270101'
GROUP BY CustomerId
HAVING SUM(Total) > 1000                                -- (2)
ORDER BY Revenue DESC, CustomerId;                      -- (3)
🧾 Code walkthrough
  1. (1) WHERE filters rows before aggregation. A half-open time range avoids end-of-day boundaries.
  2. (2) HAVING filters groups after aggregates are calculated. Server requires selected non- columns to be grouped, or expressions built from grouped columns.
  3. (3) SELECT has defined Revenue, so ORDER BY can reference it. A tie-breaker stabilizes ordering.

COUNT(*) counts rows, COUNT(column) counts non-null values, and COUNT(DISTINCT column) counts distinct non-null values. SUM ignores and returns NULL when no values exist; use COALESCE when the business meaning requires zero.

#Subqueries, EXISTS and recursive CTEs

A subquery expresses a query depending on another result. A names an intermediate step for readability; it is not automatically a materialized temporary table.

SELECT Id, Total FROM Orders
WHERE Total > (SELECT AVG(Total) FROM Orders);           -- (1)

SELECT c.Id FROM Customers c
WHERE NOT EXISTS (
    SELECT 1 FROM Orders o WHERE o.CustomerId = c.Id
);                                                     -- (2)

-- Categories(Id, ParentId, Name); roots have ParentId NULL.
WITH Tree AS (
    SELECT Id, ParentId, Name, 0 AS Depth
    FROM Categories WHERE ParentId IS NULL              -- (3)
    UNION ALL
    SELECT c.Id, c.ParentId, c.Name, t.Depth + 1
    FROM Categories c JOIN Tree t ON c.ParentId = t.Id   -- (4)
)
SELECT Id, Name, Depth FROM Tree OPTION (MAXRECURSION 100);
🧾 Code walkthrough
  1. (1) A scalar subquery must return one value; here it computes an average.
  2. (2) The correlated predicate references the current customer. EXISTS tests existence; IN tests membership. Both may optimize to the same semi-join, so EXISTS is not universally faster.
  3. (3) The recursive CTE anchor starts the tree; the CTE belongs to the following statement only.
  4. (4) The recursive member follows children. A recursion limit does not detect cycles; prevent cycles or track paths instead of removing limits casually.
If NOT IN's subquery contains , otherwise nonmatching comparisons become and disappear. Prefer NOT EXISTS or deliberate NULL exclusion. A correlated query need not physically execute once per row; inspect its plan.

#Window functions without collapsing rows

Latest orders and running totals often require keeping individual rows. GROUP BY collapses them; window functions compute across related rows while retaining each row.

SELECT Id, CustomerId, Total,
    ROW_NUMBER() OVER (
        PARTITION BY CustomerId ORDER BY CreatedAt, Id) AS rn, -- (1)
    RANK() OVER (ORDER BY Total DESC) AS rnk,
    DENSE_RANK() OVER (ORDER BY Total DESC) AS dense_rnk,  -- (2)
    SUM(Total) OVER (
        PARTITION BY CustomerId ORDER BY CreatedAt, Id
        ROWS UNBOUNDED PRECEDING) AS RunningTotal,        -- (3)
    LAG(Total) OVER (
        PARTITION BY CustomerId ORDER BY CreatedAt, Id) AS PreviousTotal,
    LEAD(Total) OVER (
        PARTITION BY CustomerId ORDER BY CreatedAt, Id) AS NextTotal,
    100.0 * Total / NULLIF(SUM(Total) OVER (), 0) AS PercentOfTotal
FROM Orders;
🧾 Code walkthrough
  1. (1) Numbers rows per customer. ORDER BY inside OVER does not order the final output.
  2. (2) For 500, 400, 400, 300, RANK produces 1, 2, 2, 4; DENSE_RANK produces 1, 2, 2, 3. Adding Id here would remove value ties.
  3. (3) ROWS gives row-by-row accumulation with deterministic ordering. An implicit RANGE frame may include equal-valued peers together.

Filter rn in an outer query or , not WHERE at the same level. Sorting and memory grants can be expensive; a suitable index may reduce the cost.

#UNION, DDL and database programmability

The wrong command can remove a instead of rows or add unnecessary deduplication.

Tool Server behavior
UNION / UNION ALLCombine compatible result sets; UNION removes duplicate rows, ALL retains them
DELETEOptional WHERE; logs row deletions, fires triggers and retains identity state
TRUNCATEUsually deallocates pages for all rows; no WHERE or DELETE triggers, resets identity; commonly restricted by referencing FKs, with self-reference exceptions
DROPRemoves object definition, data and dependent indexes
ViewSaved query, normally not materialized; an indexed view stores results with restrictions and write costs
ProcedureStatements, DML, , output parameters and result sets; invoked with EXEC
FunctionScalar or table result usable in queries; SQL Server UDFs cannot change database state
TriggerAutomatic AFTER or INSTEAD OF logic; inserted and deleted can contain many rows, requiring set-based code
CREATE OR ALTER VIEW dbo.PaidOrderTotals AS             -- (1)
SELECT CustomerId, SUM(Total) AS Revenue
FROM dbo.Orders WHERE Status = 'Paid'
GROUP BY CustomerId;
-- Run as a separate batch:
SELECT CustomerId, Revenue FROM dbo.PaidOrderTotals;

BEGIN TRANSACTION;
TRUNCATE TABLE dbo.ImportStaging;                       -- (2)
ROLLBACK TRANSACTION;
🧾 Code walkthrough
  1. (1) A view hides query details but is not a cache. Simple views can be updatable under restrictions; this aggregated view is not ordinarily directly updatable.
  2. (2) Assumes an existing staging table eligible for truncation. SQL Server logs enough to roll TRUNCATE back inside a transaction; it is not magically unlogged.

Procedures can reduce round trips and expose narrow permissions but need versioning and tests. Plan caching also applies to parameterized SQL. Per-row scalar UDFs can cost heavily, although supported inlining changes the outcome; measure. Triggers can support auditing but add hidden behavior and lock duration.

#Normalization from 1NF to BCNF

Repeating a customer's current address across orders creates update anomalies. Normalization organizes facts by dependencies, not simply by table count.

Raw: Orders(OrderId, CustomerId, CustomerCity, Products="Mug,Pen")
1NF: Orders + OrderLines(OrderId, ProductId, ProductName, Qty)
2NF: Products(ProductId, ProductName) + OrderLines without ProductName
3NF: Customers(CustomerId, CurrentCity) + Orders without CurrentCity
Snapshot: OrderLines(UnitPriceAtSale) and Orders(ShippingAddressAtPurchase)
  1. : single values appropriate to the domain, without repeating groups or a list of products in one field.
  2. : 1NF with no non-prime attribute dependent on a proper subset of any candidate key. With an (OrderId, ProductId) line key when each product appears once, ProductName depends only on ProductId.
  3. : for every nontrivial dependency X → A, X is a or A is prime. CustomerId → CurrentCity belongs in Customers.
  4. : every nontrivial determinant must be a superkey. This removes 3NF's exception for prime attributes.

For BCNF, consider ProductSupplier(ProductId, SupplierId, AgentId), with (ProductId, SupplierId) → AgentId and AgentId → SupplierId. Candidate keys are (ProductId, SupplierId) and (ProductId, AgentId). AgentId is not a superkey, although SupplierId is prime, so the relation can satisfy 3NF while violating BCNF. Decomposition into AgentSupplier and ProductAgent is lossless, but preserving all dependencies may need extra enforcement.

Denormalization, such as a summary table, improves reads at a synchronization cost. Sale price and shipping address at purchase are historical facts distinct from current values. Do not add joins simply to maximize normal form without understanding constraints and workload.

#ACID, isolation and transactions in .NET

Orders and inventory may need to change together without violating stock . Atomicity is all-or-nothing; consistency preserves constraints and business invariants; controls interference; durability preserves committed results under the storage and configuration guarantees.

using Microsoft.Data.SqlClient;
using System.Data;

await using var conn = new SqlConnection(connectionString);
await conn.OpenAsync(ct);
await using var tx = (SqlTransaction)await conn.BeginTransactionAsync(ct); // (1)
await using var cmd = conn.CreateCommand();
cmd.Transaction = tx;
cmd.CommandText = "UPDATE Products SET Available = Available - @qty " +
    "WHERE Id = @id AND Available >= @qty";
cmd.Parameters.Add("@id", SqlDbType.Int).Value = productId;
cmd.Parameters.Add("@qty", SqlDbType.Int).Value = quantity;  // (2)
if (quantity <= 0) throw new ArgumentOutOfRangeException(nameof(quantity));
if (await cmd.ExecuteNonQueryAsync(ct) != 1)
    throw new InvalidOperationException("Reservation failed");
// Insert the order using this same connection and transaction.
await tx.CommitAsync(ct);                                 // (3)
🧾 Code walkthrough
  1. (1) One connection owns the ; every command must join it. Dapper accepts a transaction argument per command. Disposal before commit rolls back uncommitted changes.
  2. (2) Typed parameters and an atomic predicate prevent overselling. This Product assumes an Available column.
  3. (3) Commit covers , not a gateway or email. SaveChanges is transactional; multiple operations with BeginTransactionAsync, inside an execution strategy when retries apply. TransactionScope provides ambient transactions with async flow, subject to provider, platform and distributed-escalation support.
SQL Server isolationGuarantee and cost
Read UncommittedAllows dirty reads; NOLOCK is not a correctness solution
Read CommittedPrevents dirty reads; repeated reads may differ
Repeatable ReadProtects read rows; phantoms remain possible
SerializableEquivalent to serial execution; contention and retries remain possible
Statement-level versioning for Read Committed
SnapshotTransaction-level snapshot; write conflicts and write skew remain possible

A is a wait cycle; SQL Server chooses a victim with error 1205. Use consistent resource order, short transactions, deadlock graphs and bounded retries of safe units. Row versioning reduces reader-writer blocking but not all writer locks or deadlocks. PostgreSQL isolation implementations differ; topic 12 covers the deeper details.

#Clustered, covering and filtered indexes

A worked example
  1. You need one user's orders ordered by creation time. Without a suitable index, the database may inspect many rows and then sort them.
  2. An index starting with UserId and CreatedAt may help locate that user's ordered range. Selected output columns determine whether extra table lookups are needed.
  3. An index is an additional access structure with storage and write-maintenance costs. Verify a representative execution plan; not every column needs its own index.

An index resembles a book index: faster lookup, with maintenance on writes. In Server rowstore, clustered leaves contain data rows. Their logical key order does not promise contiguous disk sectors or ordered SELECT output.

-- Assumes Orders has a clustered Id key and a StoreId column.
CREATE INDEX IX_Orders_Store_Created
ON Orders (StoreId, CreatedAt DESC, Id DESC)
INCLUDE (Total, Status);                                 -- (1)

SELECT Id, CreatedAt, Total, Status FROM Orders
WHERE StoreId = @store AND CreatedAt >= @from
ORDER BY CreatedAt DESC, Id DESC;

CREATE INDEX IX_Orders_Pending
ON Orders (CreatedAt) WHERE Status = 'Pending';          -- (2)
🧾 Code walkthrough
  1. (1) Equality prefix followed by range and ordering suits this query. INCLUDE adds leaf payload and can avoid key lookups; it is not a reason to include every column.
  2. (2) A filtered index covers a known subset. The optimizer must prove the query fits its predicate, which parameterization can complicate.

A table has one clustered index or is a heap; nonclustered indexes use the clustered key or a heap RID as row locator. A primary key need not be clustered. Covering describes an index relative to a query, not a universal index type. A (StoreId, CreatedAt) index supports its leading prefix; date-only predicates usually lack an efficient direct seek on it. Consider write amplification, storage and statistics.

#Sargability, plans and parameter sensitivity

An existing index is not enough. Predicates, data distribution and estimates determine whether it is useful.

❌ Function around the column
SELECT Id FROM Orders WHERE YEAR(CreatedAt) = 2026;
✅ Range on the original column
SELECT Id FROM Orders
WHERE CreatedAt >= '20260101' AND CreatedAt < '20270101';

Sargable predicates permit useful index access; they do not force a seek. Watch implicit conversions, leading wildcards such as LIKE '%pen', and collation. Computed or expression indexes can support specific expressions, so functions do not universally prevent every index.

  1. Find the query through Query Store or and record duration, logical reads, rows and waits.
  2. Inspect the actual plan for estimate errors, spills, memory grants, repeated lookups and blocking.
  3. Reduce columns and rows, fix , rewrite predicates, tune indexes and inspect statistics.
  4. Test varied parameters and production-like volumes. A scan can be right for many rows; a seek can still read extensively.
  5. Compare before and after, including write costs and plan stability; use caching or summaries when measurements justify them.

Parameter sniffing: compilation uses current parameter values to improve estimates, but a plan for a small store may perform poorly for a huge one. Compare compiled and runtime values. OPTION(RECOMPILE) costs compilation ; OPTIMIZE FOR may produce a poor compromise. Query Store forcing or query variants require measurement. Modern Server offers Parameter Sensitive Plan optimization for eligible queries under suitable versions and compatibility levels, not every workload.

#SQL Server, PostgreSQL and tenant isolationBonus

Optional but valuable: not asked in every interview, but knowing it deepens your understanding and sets you apart.

Choosing a relational engine involves operations, expertise, licensing and workload rather than one feature.

Area ServerPostgreSQL
Language and toolsT-SQL, Query Store and Microsoft toolingSQL, PL/pgSQL, EXPLAIN ANALYZE and extensions
Index storageRowstore clustered or heap with nonclustered indexesHeap and separate indexes; CLUSTER reorders once rather than maintaining clustering
JSON functions; native json in SQL Server 2025 subject to edition/service supportjsonb and indexes such as GIN, chosen by query pattern
Locking Read Committed or depending on settings with statement snapshots under Read Committed
OperationsCommercial editions and use-dependent licensingOpen-source license; hosting and support still cost money
-- PostgreSQL; app connects as a non-owner role without BYPASSRLS.
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY orders_tenant ON orders
USING (tenant_id = current_setting('app.tenant_id')::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid); -- (1)

BEGIN;
SET LOCAL app.tenant_id = '00000000-0000-0000-0000-000000000001'; -- (2)
SELECT id, total FROM orders;
COMMIT;
🧾 Code walkthrough
  1. (1) adds read and write protection, not authentication. Owners, superusers and BYPASSRLS roles can bypass it; test with the actual application role.
  2. (2) Trusted server code sets tenant context after authorization. SET LOCAL inside a avoids leaking context across pooled connections. Never trust an unchecked client tenant identifier.

SQL Server has a different RLS mechanism. Topic 11 covers families, partitioning and consistency in depth. Relational databases can also store JSON and scale through several approaches.

Twelve classic SQL exercises using store data

#Practice queries: prices, rankings and reports

These exercises assume Products(Id, CategoryId, ParentProductId, Price), Customers(Id, Email), Orders(Id, CustomerId, StoreId, CreatedAt, Status, Total), OrderLines(OrderId, ProductId, Quantity), and AccountEntries(Id, CustomerId, CreatedAt, Amount). Duplicate-email exercises use import staging before uniqueness is enforced. Familiar salary patterns are expressed as product-price questions to keep one domain.

1 and 2: second and Nth distinct highest prices

-- (1) Second DISTINCT highest price; NULL if none exists.
SELECT MAX(Price) AS SecondPrice FROM Products
WHERE Price < (SELECT MAX(Price) FROM Products);

-- (2) Nth distinct price, including all tied products if needed.
WITH Ranked AS (
    SELECT Id, Price, DENSE_RANK() OVER (ORDER BY Price DESC) AS rnk
    FROM Products WHERE Price IS NOT NULL
)
SELECT DISTINCT Price FROM Ranked WHERE rnk = @N;
🧾 Code walkthrough
  1. (1) Exclude the maximum then take MAX again; ties do not consume another rank.
  2. (2) DENSE_RANK counts distinct values; validate positive @N. Select Id and Price without DISTINCT to return tied products. A missing rank yields no rows.

3: highest-priced products in each category

WITH Ranked AS (
    SELECT Id, CategoryId, Price,
        RANK() OVER (PARTITION BY CategoryId ORDER BY Price DESC) AS rnk
    FROM Products WHERE Price IS NOT NULL
)
SELECT Id, CategoryId, Price FROM Ranked WHERE rnk = 1;   -- (3)
🧾 Code walkthrough
  1. (3) Ranking restarts per category and retains all first-place ties. Use ROW_NUMBER with an Id tie-breaker if exactly one product is required.

4 and 5: find and remove duplicates

SELECT Email, COUNT(*) AS Copies FROM CustomerImport
WHERE Email IS NOT NULL GROUP BY Email HAVING COUNT(*) > 1; -- (4)

WITH Duplicates AS (
    SELECT Id, Email,
        ROW_NUMBER() OVER (PARTITION BY Email ORDER BY Id) AS rn
    FROM CustomerImport WHERE Email IS NOT NULL
)
DELETE FROM Duplicates WHERE rn > 1;                    -- (5)
🧾 Code walkthrough
  1. (4) Grouping follows email collation and normalization policy; is deliberately excluded.
  2. (5) This T- updatable retains the smallest Id. Preview with SELECT first and review references and merge rules; never delete a live customer with orders merely for sharing an email. Add appropriate uniqueness afterwards.

6 and 7: missing relationships and self joins

SELECT c.Id FROM Customers c WHERE NOT EXISTS (
    SELECT 1 FROM Orders o WHERE o.CustomerId = c.Id
);                                                     -- (6)

SELECT variant.Id, variant.Price, parent.Price AS ParentPrice
FROM Products variant JOIN Products parent
ON variant.ParentProductId = parent.Id
WHERE variant.Price > parent.Price;                     -- (7)
🧾 Code walkthrough
  1. (6) Returns customers who never ordered. Adding a status predicate changes the question to absence of that kind of order.
  2. (7) Compares a row with its parent, adapting the classic direct-report pattern to product variants. Products without parents are excluded.

8: monthly revenue and month-over-month change

WITH Monthly AS (
    SELECT DATEFROMPARTS(YEAR(CreatedAt), MONTH(CreatedAt), 1) AS MonthStart,
        SUM(Total) AS Revenue
    FROM Orders WHERE Status = 'Paid'
    GROUP BY DATEFROMPARTS(YEAR(CreatedAt), MONTH(CreatedAt), 1)
)
SELECT MonthStart, Revenue,
    Revenue - LAG(Revenue) OVER (ORDER BY MonthStart) AS Change
FROM Monthly ORDER BY MonthStart;                       -- (8)
🧾 Code walkthrough
  1. (8) by year and month, then compare with the preceding row. Missing months are absent; use a calendar table and zero-fill for true calendar-month comparisons. The first change is NULL.

9: top three products by units sold per store

WITH Sales AS (
    SELECT o.StoreId, l.ProductId, SUM(l.Quantity) AS Units
    FROM Orders o JOIN OrderLines l ON l.OrderId = o.Id
    WHERE o.Status = 'Paid'
    GROUP BY o.StoreId, l.ProductId
), Ranked AS (
    SELECT StoreId, ProductId, Units,
        ROW_NUMBER() OVER (
            PARTITION BY StoreId ORDER BY Units DESC, ProductId) AS rn
    FROM Sales
)
SELECT StoreId, ProductId, Units FROM Ranked WHERE rn <= 3; -- (9)
🧾 Code walkthrough
  1. (9) Aggregate before ranking. ROW_NUMBER returns at most three products; DENSE_RANK by Units includes all ties for three distinct totals. Returns require an explicit sales definition.

10 and 11: running balances and conditional aggregation

SELECT CustomerId, Id, Amount,
    SUM(Amount) OVER (
        PARTITION BY CustomerId ORDER BY CreatedAt, Id
        ROWS UNBOUNDED PRECEDING) AS Balance
FROM AccountEntries;                                   -- (10)

SELECT COUNT(CASE WHEN Status = 'Paid' THEN 1 END) AS Paid,
    COUNT(CASE WHEN Status = 'Pending' THEN 1 END) AS Pending,
    COUNT(CASE WHEN Status = 'Cancelled' THEN 1 END) AS Cancelled
FROM Orders;                                           -- (11)
🧾 Code walkthrough
  1. (10) Amount is signed and each customer has a separate balance. Filtering dates before the window requires an opening balance.
  2. (11) COUNT ignores the NULL produced by nonmatching CASE branches, returning zero even for an empty table. This pivots a fixed status set without dynamic SQL.

12: consecutive order days — gaps and islands

WITH Days AS (
    SELECT DISTINCT CAST(CreatedAt AS date) AS OrderDay FROM Orders
), Islands AS (
    SELECT OrderDay,
        DATEADD(day, -CAST(ROW_NUMBER() OVER (ORDER BY OrderDay) AS int),
            OrderDay) AS IslandKey
    FROM Days
)
SELECT MIN(OrderDay) AS StartDay, MAX(OrderDay) AS EndDay, COUNT(*) AS DayCount
FROM Islands GROUP BY IslandKey ORDER BY StartDay;       -- (12)
🧾 Code walkthrough
  1. (12) Deduplicate dates, then subtract sequence numbers so consecutive days share a key. For per-customer islands, carry CustomerId through DISTINCT, PARTITION BY and GROUP BY. Resolve business timezone before converting timestamps to dates.

References: SQL Server index design, Parameter Sensitive Plan optimization, TRUNCATE restrictions, and PostgreSQL row security. Blocks state their dialect and assumptions. Run modification exercises in a training database and inspect results before applying patterns to real data.

Interview questions

Check your reasoning

Why does this choice fit? When would it fail? What changes under higher load or a dependency failure?

10.1How do primary, candidate, unique and foreign keys differ?Junior
Model answer

I distinguish a candidate key, which minimally identifies a row, from the chosen primary key. Other candidate keys can be enforced with unique constraints. A primary key cannot be null and can contain several columns. A foreign key enforces a reference to an eligible unique key, not necessarily the primary key. I use stable identifiers while preserving business uniqueness separately, and I check provider-specific behavior and indexing on the referencing columns.

10.2Explain inner and outer joins using customers and orders.Junior
Model answer

I use an inner join when I need only matching customers and orders. A left join retains all customers and fills order columns with when none match. A right join preserves all orders, while a full outer join retains unmatched rows from both sides. Multiple matching orders repeat the customer columns, which is expected. I am careful with right-side filters after a left join, because a WHERE predicate can remove the unmatched rows.

10.3What are cross joins, self joins and anti-joins?Junior
Model answer

I use a cross join to generate every combination, such as all product sizes and colors, while watching the resulting row count. A self join uses two aliases of the same table to compare related rows, such as a product variant and its parent. An anti-join asks which rows have no match. I usually express that with NOT EXISTS because it clearly states the question and avoids the pitfalls of NOT IN.

10.4What is the difference between WHERE and HAVING?Junior
Model answer

I use WHERE to filter individual rows before grouping and HAVING to filter groups. For example, WHERE selects paid orders in a date range, and HAVING keeps customers whose total spending exceeds a threshold. I put conditions that do not need aggregates in WHERE when that matches the intended meaning. Logical processing also explains why a SELECT alias is normally unavailable in WHERE but can be used in ORDER BY.

10.5How do COUNT(*) and COUNT(column) handle NULL?Junior
Model answer

I use COUNT(*) to count rows and COUNT(column) to count only rows with a non-null value in that column. COUNT DISTINCT counts distinct non-null values. After a left join I count the right table's non-nullable key when I want zero for a customer without orders; COUNT(*) would count the preserved customer row. I also use IS rather than equality, because comparisons involving NULL usually evaluate to rather than true.

10.6How do UNION and UNION ALL differ?Junior
Model answer

I use both to combine result sets with the same number of columns and compatible types. UNION removes duplicate rows across the complete selected shape, which can require extra sorting or hashing. UNION ALL keeps duplicates and avoids that deduplication work. I choose ALL when duplicates are acceptable or already impossible, rather than using UNION to hide a mistaken join. Neither guarantees output order without a final ORDER BY clause.

10.7How do DELETE, TRUNCATE and DROP differ in SQL Server?Junior
Model answer

I use DELETE to remove selected rows, with row deletion logging and trigger behavior. TRUNCATE removes all rows through operations, resets identity and does not fire DELETE triggers, but has eligibility restrictions such as referencing foreign keys. It is still logged sufficiently for rollback inside a Server . DROP removes the table definition and its data. I review permissions, dependencies and recovery before choosing any destructive command.

10.8Why normalize a relational model?Junior
Model answer

I normalize to reduce update, insert and delete anomalies by storing each current fact with the key it depends on. First normal form removes repeating groups. Second normal form removes partial dependencies of non-prime attributes on candidate keys. Third normal form addresses non-key dependency problems, while requires every nontrivial determinant to be a . I balance the design with real constraints and workloads, and distinguish historical snapshots from accidental duplication.

10.9When is NOT EXISTS safer than NOT IN?Mid
Model answer

I prefer NOT EXISTS when expressing absence of a related row, especially if the subquery can contain . With NOT IN, a NULL can make otherwise nonmatching comparisons and remove rows unexpectedly. EXISTS tests whether a matching row exists without comparing against a NULL list member. I still inspect the execution plan for performance, because the optimizer can transform both constructs into similar joins. There is no universal rule that one spelling always runs faster.

10.10What does a CTE provide, and is it materialized?Mid
Model answer

I use a to name a query step and make a complex statement easier to understand. In Server it is not a promise of materialization or caching; the optimizer chooses the plan, and repeated references may repeat work. Recursive CTEs combine an anchor with a recursive member for hierarchies such as categories. I guard against cycles and unreasonable depth. If I need reuse across statements or intermediate indexes, a temporary table may be more suitable.

10.11How do ROW_NUMBER, RANK and DENSE_RANK differ?Mid
Model answer

I use ROW_NUMBER when I need one sequential position per row, with a unique tie-breaker for deterministic selection. RANK gives equal values the same rank and leaves gaps afterwards. DENSE_RANK also preserves ties but does not leave gaps. For the Nth distinct price I use DENSE_RANK. For exactly three products per category I use ROW_NUMBER, while including all ties needs a different ranking choice and an explicit business definition.

10.12How do you find the latest order per customer?Mid
Model answer

I assign ROW_NUMBER partitioned by CustomerId and ordered by CreatedAt descending plus a unique identifier descending. An outer query filters the number to one. The identifier makes selection deterministic when timestamps tie. If the requirement is to return every order tied for the latest time, I use ranking without that tie-breaker instead. I also consider an index beginning with CustomerId and the ordering columns, then verify the actual plan on representative data.

10.13When would you use a view, procedure, function or trigger?Mid
Model answer

I use a view to expose a reusable query shape or restricted columns, and a procedure for a controlled data operation with statements and logic. A Server function returns a scalar or table usable within queries, subject to side-effect restrictions. A trigger runs automatically on changes, which can help enforce auditing but adds hidden behavior. I keep triggers set-based for multi-row statements and version and test all database code alongside application changes.

10.14What are ACID properties, and do they enforce business rules automatically?Mid
Model answer

I describe atomicity as all-or-nothing changes, consistency as preserving valid states, as controlling interference between , and durability as retaining committed results under the configured guarantees. A transaction does not invent business rules: constraints and correct operations still need to enforce them. For stock, I may use a conditional update or suitable concurrency control. I also keep remote payments outside the assumption that a local database transaction can roll everything back.

10.15How do you use transactions in .NET with EF Core or Dapper?Mid
Model answer

I rely on a single SaveChanges when it covers the operation. For multiple saves or direct statements I create an explicit transaction, inside the execution strategy when retries apply. With Dapper or ADO.NET I open one connection, begin a transaction and pass it to every command. I keep the unit short, select for the and avoid remote calls inside it. Retryable operations also need safe handling of uncertain commit outcomes.

10.16What is the difference between clustered and nonclustered indexes?Mid
Model answer

In Server rowstore, I can organize a table as a clustered index whose leaf level contains the data rows, or as a heap. A nonclustered index is a separate structure with keys and a row locator, using the clustered key or a heap RID. A primary key need not be clustered. I choose index keys for access patterns and consider included columns for coverage, while remembering that output order still requires ORDER BY.

10.17How do covering and filtered indexes help?Mid
Model answer

I call an index covering when it contains everything a particular query needs, allowing the engine to avoid extra base-row lookups. Included columns can provide payload without enlarging the search key in the same way. A filtered index stores a subset, such as pending orders, reducing size and maintenance for irrelevant rows. The optimizer must prove the query fits the filter. I evaluate storage, write costs, parameterization and actual usage rather than creating broad indexes speculatively.

10.18What is a SQL deadlock, and how do you handle it?Mid
Model answer

I distinguish blocking, where one operation waits, from a , where a cycle prevents progress. Server chooses a victim and returns error 1205. I inspect the deadlock graph to find resource order and access paths, then reduce risk with consistent ordering, short and appropriate indexes. Row versioning can reduce some reader-writer conflicts but does not eliminate deadlocks. I retry the complete aborted unit with a bounded policy only when its side effects are safe.

10.19How do RCSI and Snapshot isolation differ?Senior
Model answer

In Server I use to give Read Committed statements versioned reads, with each statement seeing its own committed snapshot. Snapshot provides a -level view instead. Both can reduce reader-writer blocking, but writers still take locks and snapshot transactions can encounter update conflicts. A stable snapshot also does not imply serializability; write skew across different rows can remain possible. I choose isolation from the business and account for version-store overhead and retry behavior.

10.20How do you optimize a slow query without guessing?Senior
Model answer

I capture the query, parameters, duration, logical reads and waits, then inspect its actual execution plan. I compare estimated and actual rows, look for spills, expensive lookups, large transfers and blocking, and check statistics. I reduce unnecessary data and round trips, rewrite predicates or adjust indexes according to evidence. I test different parameter sizes and compare before and after, including write impact. A scan is not automatically bad, and a seek is not automatically cheap.

10.21What makes a predicate sargable?Senior
Model answer

I call a predicate sargable when its form permits efficient index search for the available indexing strategy. For example, a half-open date range is usually more useful than applying YEAR to every timestamp. I also check implicit conversions, collation and leading wildcards. This is not a guarantee of an index seek, because selectivity and cost still drive the optimizer. Computed or expression indexes can support particular expressions, so I avoid absolute claims about every function.

10.22What is parameter sniffing, and how do you address harmful sensitivity?Senior
Model answer

I view parameter sniffing as using current parameter values during compilation to improve estimates, which is normally useful. Harmful sensitivity appears when one cached plan performs poorly for very different distributions. I compare compiled and runtime values and test representative parameters. Depending on evidence, I consider recompilation, query variants or Query Store controls, and check eligibility for Parameter Sensitive Plan optimization in modern Server. Every remedy has costs, so I do not disable sniffing globally by default.

10.23When would you denormalize, and how is BCNF stronger than 3NF?Senior
Model answer

I use normalization to reason about dependencies and prevent anomalies. is stricter than because every nontrivial determinant must be a , whereas 3NF permits a prime attribute on the dependent side. I may denormalize a read model or summary when measured query cost justifies synchronization complexity. I define who updates it, its acceptable staleness and how it can be rebuilt. Historical sale prices are separate facts, so I do not normalize them into current catalog prices.

10.24How do you safely remove duplicate customer records?Senior
Model answer

I first define duplicate identity, including email normalization, case rules and policy. I inspect candidate groups and choose a survivor deterministically, often using ROW_NUMBER in staging data. For live customers I merge or reassign related records and preserve audit requirements before deleting anything. I use a controlled or batch strategy, preview affected rows and plan recovery. Finally, I add an appropriate uniqueness constraint so the application cannot immediately recreate the same problem.

Test yourself

Quiz answers and completed-topic marks are saved in this browser; they do not sync across devices.

Score: 0 / 0

LEFT JOIN followed by WHERE right.Total > 100 does what to unmatched rows?

comparisons are ; filter in ON to preserve the left side.

Which clause filters grouped SUM(Total)?

HAVING logically follows grouping.

For values 500, 400, 400, 300, what is the DENSE_RANK of 300 descending?

DENSE_RANK does not leave a gap after ties.

What can NULL inside a NOT IN subquery cause?

WHERE retains only TRUE; NOT EXISTS expresses absence more clearly.

TRUNCATE inside a SQL Server transaction...

changes have sufficient logging for rollback.

A SQL Server index on (StoreId, CreatedAt) best fits...

The leading equality prefix followed by a range fits naturally.

Which date predicate is usually more sargable on CreatedAt?

A range avoids wrapping the column; plan choice still depends on cost.

Which SQL Server option provides statement-level versioned Read Committed reads?

uses statement snapshots; Snapshot is -level.

A CTE in SQL Server guarantees...

The optimizer chooses the physical plan; a promises no cache.

A monthly LAG query omits months without orders. It compares against...

Fill missing months for a true calendar-month comparison.

Which statement about plans is correct?

An operator name alone is not a diagnosis.

What should follow deduplication once business identity is defined?

Cleanup without prevention is temporary.