📘 Comprehensive T-SQL Knowledge Base
Beginner & Intermediate Levels — Detailed Answers with Examples
🔰 Beginner Level
Basic Queries & Syntax
1. SELECT * vs SELECT specific columns? When to avoid SELECT *?SELECT * retrieves all columns from a table. Specific columns only fetch the columns you name.- Performance: * reads unnecessary data, increasing I/O and memory usage.
- Maintainability: If table schema changes (new/dropped columns), * breaks applications expecting fixed column order.
- Best Practice: Always list columns explicitly in production code.
-- ❌ Avoid
SELECT * FROM Sales.Orders;
-- ✅ Recommended
SELECT OrderID, CustomerID, OrderDate FROM Sales.Orders;
2. Retrieve distinct values from a columnSELECT DISTINCT City FROM Customers;
City
-------
London
Berlin
Madrid3. Difference between WHERE and HAVINGWHERE filters rows before aggregation. HAVING filters groups after aggregation with GROUP BY.-- WHERE filters individual orders
SELECT CustomerID, COUNT(*) AS OrderCount
FROM Orders
WHERE OrderDate > '2023-01-01'
GROUP BY CustomerID
HAVING COUNT(*) > 2; -- HAVING filters aggregated groups
4. Aliases (AS) for columns and tables-- Column alias
SELECT FirstName + ' ' + LastName AS FullName FROM Employees;
-- Table alias (useful for joins)
SELECT o.OrderID, c.CustomerName
FROM Orders AS o
INNER JOIN Customers AS c ON o.CustomerID = c.CustomerID;
- Performance: * reads unnecessary data, increasing I/O and memory usage.
- Maintainability: If table schema changes (new/dropped columns), * breaks applications expecting fixed column order.
- Best Practice: Always list columns explicitly in production code.
-- ❌ Avoid
SELECT * FROM Sales.Orders;
-- ✅ Recommended
SELECT OrderID, CustomerID, OrderDate FROM Sales.Orders;SELECT DISTINCT City FROM Customers;-- WHERE filters individual orders
SELECT CustomerID, COUNT(*) AS OrderCount
FROM Orders
WHERE OrderDate > '2023-01-01'
GROUP BY CustomerID
HAVING COUNT(*) > 2; -- HAVING filters aggregated groups-- Column alias
SELECT FirstName + ' ' + LastName AS FullName FROM Employees;
-- Table alias (useful for joins)
SELECT o.OrderID, c.CustomerName
FROM Orders AS o
INNER JOIN Customers AS c ON o.CustomerID = c.CustomerID;Rules: AS is optional but recommended. Use square brackets [Full Name] or double quotes for spaces.
SELECT TOP 5 CustomerID, COUNT(*) AS Cnt -- 5. SELECT
FROM Orders -- 1. FROM
WHERE OrderDate > '2023-01-01' -- 2. WHERE
GROUP BY CustomerID -- 3. GROUP BY
HAVING COUNT(*) > 1 -- 4. HAVING
ORDER BY Cnt DESC; -- 6. ORDER BY-- Using + (NULL propagation risk)
SELECT 'Hello' + NULL; -- Result: NULL
-- Using CONCAT() (ignores NULLs)
SELECT CONCAT('Hello', NULL, 'World'); -- Result: HelloWorld+ operator returns NULL if any operand is NULL. CONCAT treats NULL as empty string.
-- TOP (legacy, no easy pagination)
SELECT TOP 10 * FROM Products ORDER BY Price DESC;
-- OFFSET-FETCH (standard, page 2 with 10 rows)
SELECT * FROM Products ORDER BY Price DESC
OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;SELECT ProductName, Price FROM Products ORDER BY Price ASC; -- ascending default
SELECT ProductName, Price FROM Products ORDER BY Price DESC; -- descending-- Single line comment
/*
Multi-line comment
block
*/-- IN
SELECT * FROM Products WHERE CategoryID IN (1, 2, 3);
-- BETWEEN (inclusive)
SELECT * FROM Orders WHERE OrderDate BETWEEN '2023-01-01' AND '2023-01-31';
-- LIKE (wildcards % and _)
SELECT * FROM Customers WHERE LastName LIKE 'Sm%'; -- starts with Sm
SELECT * FROM Customers WHERE Phone LIKE '555-___'; -- single charData Types
11. CHAR vs VARCHAR, NCHAR vs NVARCHAR
N prefix stores Unicode, essential for global applications.
-- DECIMAL (exact numeric, precise)
DECLARE @d DECIMAL(10,2) = 12345678.12;
-- FLOAT (approximate, floating point)
DECLARE @f FLOAT = 1.123456789012345;Use DECIMAL for money, quantities. FLOAT for scientific calculations.
-- DATETIME: 1753-9999, accuracy 3.33ms, 8 bytes
-- DATETIME2: 0001-9999, 100ns precision, 6-8 bytes
-- DATE: 0001-9999, date only, 3 bytes
SELECT CAST(GETDATE() AS DATE); -- 2023-10-05
SELECT CAST(GETDATE() AS DATETIME2); -- 2023-10-05 14:30:45.12345672^31-1 bytes (2 GB). Used for large text storage.
-- NULL arithmetic yields NULL
SELECT 10 + NULL; -- NULL
-- Use ISNULL/COALESCE
SELECT ISNULL(Price, 0) + Tax FROM Products;
-- NULL comparison uses IS NULL, not =
SELECT * FROM Customers WHERE Region IS NULL;SELECT CAST(123.45 AS INT); -- 123
SELECT CONVERT(VARCHAR, GETDATE(), 101); -- '01/15/2023' (style format)CONVERT has a style parameter for dates, CAST is ANSI standard.
Basic Functions
18. COUNT(*), COUNT(column), COUNT(DISTINCT column)SELECT COUNT(*) FROM Employees; -- counts all rows
SELECT COUNT(Region) FROM Employees; -- counts non-NULL regions
SELECT COUNT(DISTINCT City) FROM Employees; -- unique cities
19. LEFT, RIGHT, SUBSTRING, LENSELECT LEFT('Hello World', 5); -- 'Hello'
SELECT RIGHT('Hello World', 5); -- 'World'
SELECT SUBSTRING('Database', 2, 4); -- 'atab'
SELECT LEN('Test '); -- 4 (excludes trailing spaces)
20. ISNULL() vs COALESCE()-- ISNULL takes 2 args, returns first non-null, uses first arg data type
SELECT ISNULL(NULL, 'Fallback');
-- COALESCE takes multiple args, returns first non-null, data type precedence
SELECT COALESCE(NULL, NULL, 'Third', 'Fourth');
21. GETDATE(), DATEADD(), DATEDIFF()SELECT GETDATE(); -- current datetime
SELECT DATEADD(DAY, 7, '2023-01-01'); -- add 7 days
SELECT DATEDIFF(DAY, '2023-01-01', GETDATE()); -- difference in days
22. UPPER(), LOWER(), TRIM(), LTRIM(), RTRIM()SELECT UPPER('sql'); -- 'SQL'
SELECT LOWER('SERVER'); -- 'server'
SELECT TRIM(' text '); -- 'text' (removes both sides, SQL Server 2017+)
SELECT LTRIM(' left'); -- 'left'
SELECT RTRIM('right '); -- 'right'
SELECT COUNT(*) FROM Employees; -- counts all rows
SELECT COUNT(Region) FROM Employees; -- counts non-NULL regions
SELECT COUNT(DISTINCT City) FROM Employees; -- unique citiesSELECT LEFT('Hello World', 5); -- 'Hello'
SELECT RIGHT('Hello World', 5); -- 'World'
SELECT SUBSTRING('Database', 2, 4); -- 'atab'
SELECT LEN('Test '); -- 4 (excludes trailing spaces)-- ISNULL takes 2 args, returns first non-null, uses first arg data type
SELECT ISNULL(NULL, 'Fallback');
-- COALESCE takes multiple args, returns first non-null, data type precedence
SELECT COALESCE(NULL, NULL, 'Third', 'Fourth');SELECT GETDATE(); -- current datetime
SELECT DATEADD(DAY, 7, '2023-01-01'); -- add 7 days
SELECT DATEDIFF(DAY, '2023-01-01', GETDATE()); -- difference in daysSELECT UPPER('sql'); -- 'SQL'
SELECT LOWER('SERVER'); -- 'server'
SELECT TRIM(' text '); -- 'text' (removes both sides, SQL Server 2017+)
SELECT LTRIM(' left'); -- 'left'
SELECT RTRIM('right '); -- 'right'🟡 Intermediate Level
JOINs
23. All types of JOINs-- INNER JOIN: matching rows only
SELECT o.OrderID, c.CustomerName
FROM Orders o INNER JOIN Customers c ON o.CustomerID = c.CustomerID;
-- LEFT JOIN: all from left, matching from right
SELECT c.CustomerName, o.OrderID
FROM Customers c LEFT JOIN Orders o ON c.CustomerID = o.CustomerID;
-- RIGHT JOIN: all from right
-- FULL OUTER JOIN: all rows from both
-- CROSS JOIN: Cartesian product
24. Self-join-- Find employees and their managers
SELECT e1.FirstName AS Employee, e2.FirstName AS Manager
FROM Employees e1
LEFT JOIN Employees e2 ON e1.ManagerID = e2.EmployeeID;
25. JOIN syntax vs old comma syntaxModern JOIN ... ON separates relation conditions from filtering WHERE. Old comma style mixes both, risking unintended cross joins.26. ON vs WHERE in JOINs-- ON defines how tables relate
SELECT * FROM Orders o
LEFT JOIN Customers c ON o.CustomerID = c.CustomerID AND c.Active = 1;
-- WHERE filters result after join
27. Join using multiple conditionsSELECT * FROM TableA a
INNER JOIN TableB b ON a.ID = b.A_ID AND a.Status = b.Status;
28. Performance impact of joining large tables
-- INNER JOIN: matching rows only
SELECT o.OrderID, c.CustomerName
FROM Orders o INNER JOIN Customers c ON o.CustomerID = c.CustomerID;
-- LEFT JOIN: all from left, matching from right
SELECT c.CustomerName, o.OrderID
FROM Customers c LEFT JOIN Orders o ON c.CustomerID = o.CustomerID;
-- RIGHT JOIN: all from right
-- FULL OUTER JOIN: all rows from both
-- CROSS JOIN: Cartesian product-- Find employees and their managers
SELECT e1.FirstName AS Employee, e2.FirstName AS Manager
FROM Employees e1
LEFT JOIN Employees e2 ON e1.ManagerID = e2.EmployeeID;-- ON defines how tables relate
SELECT * FROM Orders o
LEFT JOIN Customers c ON o.CustomerID = c.CustomerID AND c.Active = 1;
-- WHERE filters result after joinSELECT * FROM TableA a
INNER JOIN TableB b ON a.ID = b.A_ID AND a.Status = b.Status;Ensure indexes on join columns. Avoid functions on join columns. Consider hash vs merge join hints only if necessary.
Subqueries
29. Subquery in SELECT, FROM, WHERE-- SELECT subquery (must return single value)
SELECT ProductName, (SELECT AVG(Price) FROM Products) AS AvgPrice FROM Products;
-- FROM subquery (derived table)
SELECT * FROM (SELECT CustomerID, COUNT(*) cnt FROM Orders GROUP BY CustomerID) AS Sub;
-- WHERE subquery
SELECT * FROM Products WHERE Price > (SELECT AVG(Price) FROM Products);
30. Correlated subquery-- Re-executed for each outer row
SELECT ProductName, Price FROM Products p
WHERE Price > (SELECT AVG(Price) FROM Products WHERE CategoryID = p.CategoryID);
31. EXISTS and NOT EXISTSSELECT CustomerName FROM Customers c
WHERE EXISTS (SELECT 1 FROM Orders o WHERE o.CustomerID = c.CustomerID);
32. IN vs EXISTS vs JOIN performance
-- SELECT subquery (must return single value)
SELECT ProductName, (SELECT AVG(Price) FROM Products) AS AvgPrice FROM Products;
-- FROM subquery (derived table)
SELECT * FROM (SELECT CustomerID, COUNT(*) cnt FROM Orders GROUP BY CustomerID) AS Sub;
-- WHERE subquery
SELECT * FROM Products WHERE Price > (SELECT AVG(Price) FROM Products);-- Re-executed for each outer row
SELECT ProductName, Price FROM Products p
WHERE Price > (SELECT AVG(Price) FROM Products WHERE CategoryID = p.CategoryID);SELECT CustomerName FROM Customers c
WHERE EXISTS (SELECT 1 FROM Orders o WHERE o.CustomerID = c.CustomerID);EXISTS often faster for large lists; it stops after first match. JOIN can be efficient with proper indexes.
-- Use derived table or APPLY
SELECT * FROM Orders o
CROSS APPLY (SELECT TOP 1 ProductName, Price FROM Products WHERE ProductID = o.ProductID) p;GROUP BY & Aggregation
34. GROUP BY with SUM, AVG, MIN, MAXSELECT CategoryID, SUM(Price) AS Total, AVG(Price) AS AvgPrice
FROM Products GROUP BY CategoryID;
35. WHERE, GROUP BY, HAVING filtering
SELECT CategoryID, SUM(Price) AS Total, AVG(Price) AS AvgPrice
FROM Products GROUP BY CategoryID;WHERE filters rows, GROUP BY creates groups, HAVING filters groups.
SELECT CategoryID, SupplierID, COUNT(*) FROM Products
GROUP BY CategoryID, SupplierID;SELECT CategoryID, SupplierID, SUM(Price)
FROM Products
GROUP BY GROUPING SETS ((CategoryID), (SupplierID), ()); -- subtotals-- Post 2012 window function
SELECT OrderDate, Amount,
SUM(Amount) OVER (ORDER BY OrderDate) AS RunningTotal
FROM Transactions;
-- Pre 2012: self-join or quirky updateNULLs are grouped together as one group.
Common Table Expressions (CTEs)
40. What is a CTE?WITH SalesCTE AS (
SELECT CustomerID, SUM(Amount) Total FROM Orders GROUP BY CustomerID
)
SELECT * FROM SalesCTE WHERE Total > 1000;
41. CTE vs temp table vs table variable
WITH SalesCTE AS (
SELECT CustomerID, SUM(Amount) Total FROM Orders GROUP BY CustomerID
)
SELECT * FROM SalesCTE WHERE Total > 1000;CTE: inline, not persisted. Temp table: physical, indexed. Table variable: memory-optimized, limited scope.
WITH OrgChart AS (
SELECT EmployeeID, ManagerID, 0 AS Level FROM Employees WHERE ManagerID IS NULL
UNION ALL
SELECT e.EmployeeID, e.ManagerID, Level + 1
FROM Employees e INNER JOIN OrgChart o ON e.ManagerID = o.EmployeeID
)
SELECT * FROM OrgChart;Single-use scope, cannot have indexes, only referenced immediately after definition.
WITH CTE1 AS (...), CTE2 AS (...)
SELECT * FROM CTE1 JOIN CTE2 ON ...Window Functions
45. ROW_NUMBER() vs RANK() vs DENSE_RANK() vs NTILE()SELECT ProductName, Price,
ROW_NUMBER() OVER (ORDER BY Price) AS RowNum,
RANK() OVER (ORDER BY Price) AS RankVal,
DENSE_RANK() OVER (ORDER BY Price) AS DenseVal,
NTILE(4) OVER (ORDER BY Price) AS Quartile
FROM Products;
46. PARTITION BYSELECT CategoryID, ProductName, Price,
AVG(Price) OVER (PARTITION BY CategoryID) AS AvgByCat
FROM Products;
47. ROWS vs RANGE in window frames
SELECT ProductName, Price,
ROW_NUMBER() OVER (ORDER BY Price) AS RowNum,
RANK() OVER (ORDER BY Price) AS RankVal,
DENSE_RANK() OVER (ORDER BY Price) AS DenseVal,
NTILE(4) OVER (ORDER BY Price) AS Quartile
FROM Products;SELECT CategoryID, ProductName, Price,
AVG(Price) OVER (PARTITION BY CategoryID) AS AvgByCat
FROM Products;ROWS counts physical rows, RANGE considers value duplicates.
SELECT OrderDate, Amount,
LAG(Amount) OVER (ORDER BY OrderDate) AS PrevAmount,
LEAD(Amount) OVER (ORDER BY OrderDate) AS NextAmount
FROM Orders;SELECT ProductName, Price,
FIRST_VALUE(Price) OVER (ORDER BY Price) AS Cheapest,
LAST_VALUE(Price) OVER (ORDER BY Price ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS MostExpensive
FROM Products;RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW (affects LAST_VALUE behavior).