0% found this document useful (0 votes)
5 views29 pages

SQL Server Complete Study Guide

The document is a comprehensive study guide for SQL Server, covering 149 topics across 10 parts, including database fundamentals, querying data, joins, stored procedures, and advanced features. It provides detailed explanations, examples, and best practices for each topic, making it suitable for both beginners and advanced users. Key areas include database management, T-SQL syntax, data manipulation, and performance optimization techniques.
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
5 views29 pages

SQL Server Complete Study Guide

The document is a comprehensive study guide for SQL Server, covering 149 topics across 10 parts, including database fundamentals, querying data, joins, stored procedures, and advanced features. It provides detailed explanations, examples, and best practices for each topic, making it suitable for both beginners and advanced users. Key areas include database management, T-SQL syntax, data manipulation, and performance optimization techniques.
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

SQL SERVER

Complete Study Guide


From Fundamentals to Advanced Features
149 Topics | 10 Parts | T-SQL Reference & Best Practices
Table of Contents

Part Title Key Topics

1 Database & Table SSMS, CREATE/ALTER/DROP DB, Tables, Constraints, Identity, Keys
Fundamentals

2 Querying Data SELECT, DISTINCT, WHERE, GROUP BY, HAVING, ORDER BY, NULL handling

3 Joins INNER, LEFT, RIGHT, FULL, CROSS, SELF, Advanced Joins, Join Performance

4 Stored Procedures & Functions SP basics, Output Params, Scalar/Inline/Multi-Statement UDFs

5 Indexes & Views Clustered, Non-Clustered, Unique, Filtered, Views, Indexed Views

6 Triggers DML AFTER/INSTEAD OF, DDL Triggers, Logon Triggers, EVENTDATA()

7 CTEs, Subqueries & Set Ops Subqueries, CTEs, Recursive CTEs, UNION, PIVOT, APPLY, SELECT INTO

8 Transactions & Error Handling ACID, Isolation Levels, TRY/CATCH, Deadlocks, Blocking Queries

9 Window Functions ROW_NUMBER, RANK, DENSE_RANK, NTILE, LEAD/LAG,


FIRST/LAST_VALUE, ROWS vs RANGE

10 Advanced Topics Dynamic SQL, Temp Tables, Normalization, Sequences, GUIDs, MERGE,
Cursors, TVP, Grouping Sets
PART 1

Database & Table Fundamentals

Chapter 1: Connecting & Managing Databases


SQL Server Management Studio (SSMS) is the primary GUI tool. It connects to Database Engine, SSAS, SSRS, or
SSIS. SSMS is the client -- SQL Server (Database Engine) is the server, usually on a dedicated machine.

1.1 Connecting via SSMS


• Server Type: Database Engine (most common)
• Server Name: (local) | . (period) | [Link] | ComputerName | IP Address
• Authentication: Windows Authentication (no password needed) or SQL Server Authentication (mixed mode)

1.2 Create, Alter, and Drop Databases


-- Create
CREATE DATABASE SampleDB

-- Rename
ALTER DATABASE SampleDB MODIFY NAME = NewSampleDB
EXECUTE sp_renameDB 'OldName', 'NewName' -- alternative

-- Drop
DROP DATABASE NewSampleDB

-- Force drop: disconnect all users first


ALTER DATABASE NewSampleDB SET SINGLE_USER WITH ROLLBACK IMMEDIATE
DROP DATABASE NewSampleDB
-- ROLLBACK IMMEDIATE: rolls back open transactions and closes connections

NOTE

Creating a database generates two files:

.MDF -- Master Data File: stores actual table and index data

.LDF -- Log Data File: transaction log used for recovery

System databases (master, model, msdb, tempdb) CANNOT be dropped.


Chapter 2: Tables & Constraints
2.1 Creating and Altering Tables
CREATE TABLE tblGender (
ID INT NOT NULL PRIMARY KEY,
Gender NVARCHAR(10) NOT NULL
)

CREATE TABLE tblPerson (


ID INT NOT NULL PRIMARY KEY,
Name NVARCHAR(50) NOT NULL,
Email NVARCHAR(50) NOT NULL,
GenderID INT FOREIGN KEY REFERENCES tblGender(ID)
)

-- Add a column
ALTER TABLE tblPerson ADD City NVARCHAR(50)

-- Drop a column
ALTER TABLE tblPerson DROP COLUMN City

-- Modify column type


ALTER TABLE tblPerson ALTER COLUMN Salary BIGINT

2.2 Constraint Types


Constraint Purpose Example

PRIMARY KEY Uniquely identifies each row; no ID INT NOT NULL PRIMARY KEY
NULLs allowed

FOREIGN KEY Enforces referential integrity to FOREIGN KEY (GenderID) REFERENCES tblGender(ID)
another table

DEFAULT Provides value when none is supplied ALTER TABLE t ADD CONSTRAINT DF_Name DEFAULT
on INSERT 'London' FOR City

CHECK Validates data against a boolean ALTER TABLE t ADD CONSTRAINT CK_Age CHECK
condition (Age >= 0 AND Age <= 150)

UNIQUE Enforces uniqueness; allows one NULL ALTER TABLE t ADD CONSTRAINT UQ_Email UNIQUE
(Email)

NOT NULL Prevents NULL values in a column Name NVARCHAR(50) NOT NULL

2.3 Cascading Referential Integrity


Option Behavior When Parent Row Is Deleted or Updated

NO ACTION (default) Error raised; child rows prevent the parent from being deleted/updated

CASCADE Child rows automatically deleted or updated along with parent


SET NULL Foreign key column in child rows set to NULL

SET DEFAULT Foreign key column in child rows set to its default value

2.4 Identity Column


-- IDENTITY(seed, increment) auto-generates sequential integers
CREATE TABLE tblPerson (
PersonID INT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(50)
)

-- Three ways to get the last generated identity value:


SELECT SCOPE_IDENTITY() -- last identity in CURRENT SCOPE (safest)
SELECT @@IDENTITY -- last identity in current CONNECTION (trigger-
affected)
SELECT IDENT_CURRENT('tblPerson') -- last identity for TABLE regardless of session

-- Allow explicit identity value (override auto-generation)


SET IDENTITY_INSERT tblPerson ON
INSERT INTO tblPerson (PersonID, Name) VALUES (100, John)
SET IDENTITY_INSERT tblPerson OFF

-- Re-seed (reset the identity counter)


DBCC CHECKIDENT(tblPerson, RESEED, 0)

INTERVIEW Q&A

Q: Difference between SCOPE_IDENTITY(), @@IDENTITY, IDENT_CURRENT()?

SCOPE_IDENTITY() -- last identity in CURRENT scope; unaffected by triggers

@@IDENTITY -- last identity in current session across any scope (trigger-affected)

IDENT_CURRENT(tbl) -- last identity for that TABLE regardless of session or scope

BEST PRACTICE: Always use SCOPE_IDENTITY() in stored procedures.

If a trigger fires an INSERT on another identity table, @@IDENTITY returns THAT value,

while SCOPE_IDENTITY() returns the value from your original INSERT.


PART 2

Querying Data

Chapter 3: SELECT Statements


3.1 Core SELECT Patterns
SELECT Name, Salary, Gender FROM tblEmployee

-- Column aliases
SELECT Name AS EmployeeName, Salary AS Annual FROM tblEmployee

-- DISTINCT: eliminate duplicate rows


SELECT DISTINCT City FROM tblEmployee

-- TOP: limit rows


SELECT TOP 5 Name, Salary FROM tblEmployee ORDER BY Salary DESC
SELECT TOP 10 PERCENT Name FROM tblEmployee ORDER BY Salary DESC

-- WHERE filtering
SELECT * FROM tblEmployee WHERE Gender = 'Male' AND Salary > 5000
SELECT * FROM tblEmployee WHERE City IN ('London', 'Paris', 'Berlin')
SELECT * FROM tblEmployee WHERE Salary BETWEEN 3000 AND 8000

-- LIKE pattern matching


SELECT * FROM tblEmployee WHERE Name LIKE 'J%' -- starts with J
SELECT * FROM tblEmployee WHERE Name LIKE '%son' -- ends with son
SELECT * FROM tblEmployee WHERE Name LIKE '%ar%' -- contains ar
SELECT * FROM tblEmployee WHERE Name LIKE '_ohn' -- any char + ohn

SELECT Name, Salary FROM tblEmployee ORDER BY Salary DESC, Name ASC

3.2 GROUP BY and Aggregates


SELECT
Gender,
COUNT(*) AS TotalCount,
SUM(Salary) AS TotalSalary,
AVG(Salary) AS AvgSalary,
MIN(Salary) AS MinSalary,
MAX(Salary) AS MaxSalary
FROM tblEmployee
GROUP BY Gender

-- HAVING: filter AFTER grouping


SELECT DeptID, COUNT(*) AS EmpCount
FROM tblEmployee
GROUP BY DeptID
HAVING COUNT(*) > 2

-- WHERE filters rows BEFORE grouping; HAVING filters groups AFTER aggregation
-- WHERE cannot use aggregate functions; HAVING can

3.3 NULL Handling


-- IS NULL / IS NOT NULL
SELECT * FROM tblEmployee WHERE ManagerID IS NULL

-- ISNULL(check, replacement) -- SQL Server specific


SELECT ISNULL(MiddleName, 'N/A') AS MiddleName FROM tblEmployee

-- NULLIF(expr1, expr2) -- returns NULL if both equal


SELECT NULLIF(10, 10) -- returns NULL
SELECT NULLIF(10, 20) -- returns 10

-- COALESCE(v1, v2, ...) -- returns first non-NULL; ANSI standard


SELECT COALESCE(HomePhone, MobilePhone, WorkPhone, 'No Phone') AS Phone
FROM tblEmployee
-- COALESCE accepts N params; ISNULL accepts only 2
PART 3

Joins

Chapter 4: SQL Server Joins


Joins retrieve data from two or more related tables. The ON clause specifies the join condition, typically
matching primary and foreign keys.

4.1 Join Types


Join Type Returns NULL behavior

INNER JOIN Only rows with matching values in BOTH tables No NULLs from join itself

LEFT JOIN (LEFT OUTER) ALL rows from LEFT table + matched rows from Right-side columns are NULL
RIGHT where no match

RIGHT JOIN (RIGHT OUTER) ALL rows from RIGHT table + matched rows from Left-side columns are NULL
LEFT where no match

FULL JOIN (FULL OUTER) ALL rows from BOTH tables NULLs on either side where no
match found

CROSS JOIN Cartesian product: every row paired with every N/A
row (no ON clause)

SELF JOIN A table joined to itself using two different aliases Depends on join type used

4.2 Join Examples


-- INNER JOIN: only matching rows
SELECT [Link], [Link]
FROM tblEmployee E
INNER JOIN tblDepartment D ON [Link] = [Link]

-- LEFT JOIN: all employees (even without a department)


SELECT [Link], [Link]
FROM tblEmployee E
LEFT JOIN tblDepartment D ON [Link] = [Link]

-- RIGHT JOIN: all departments (even without employees)


SELECT [Link], [Link]
FROM tblEmployee E
RIGHT JOIN tblDepartment D ON [Link] = [Link]

-- FULL OUTER JOIN


SELECT [Link], [Link]
FROM tblEmployee E
FULL OUTER JOIN tblDepartment D ON [Link] = [Link]

-- CROSS JOIN: all combinations


SELECT [Link], [Link]
FROM tblEmployee E CROSS JOIN tblDepartment D

-- SELF JOIN: employee with their manager (both in same table)


SELECT [Link] AS Employee, [Link] AS Manager
FROM tblEmployee E
LEFT JOIN tblEmployee M ON [Link] = [Link]

4.3 Advanced Joins: Non-Matching Rows


-- Employees with NO department (LEFT table non-matches)
SELECT [Link] FROM tblEmployee E
LEFT JOIN tblDepartment D ON [Link] = [Link]
WHERE [Link] IS NULL

-- Departments with NO employees (RIGHT table non-matches)


SELECT [Link] FROM tblEmployee E
RIGHT JOIN tblDepartment D ON [Link] = [Link]
WHERE [Link] IS NULL

-- Rows in ONLY ONE table (non-matches from BOTH sides)


SELECT [Link], [Link]
FROM tblEmployee E
FULL OUTER JOIN tblDepartment D ON [Link] = [Link]
WHERE [Link] IS NULL OR [Link] IS NULL

INTERVIEW Q&A

Q: INNER JOIN vs LEFT JOIN?

INNER JOIN: returns rows only where both tables have a match.

LEFT JOIN: returns ALL rows from left + matched rows from right (NULLs where no match).

Q: What is a SELF JOIN and when is it used?

A SELF JOIN joins a table to itself using two different aliases.

Classic use: Employee/Manager hierarchy where both are rows in the same table.

Use LEFT JOIN (not INNER) to include root records that have no manager.

Q: What is the difference between WHERE [Link] IS NULL vs WHERE [Link] IS NOT NULL?

WHERE [Link] IS NULL after LEFT JOIN: returns only NON-matching rows (orphaned left rows).

WHERE [Link] IS NOT NULL after LEFT JOIN: same as INNER JOIN result.
PART 4

Stored Procedures & Functions

Chapter 5: Stored Procedures


A stored procedure is a precompiled, named group of T-SQL statements. The execution plan is compiled once,
cached, and reused -- reducing overhead compared to ad-hoc queries.

5.1 Basic Stored Procedures


-- Simple SP (no parameters)
CREATE PROCEDURE spGetAllEmployees
AS BEGIN
SELECT Name, Gender FROM tblEmployee
END

EXEC spGetAllEmployees

-- SP with INPUT parameter


CREATE PROCEDURE spGetByGender @Gender NVARCHAR(20)
AS BEGIN
SELECT * FROM tblEmployee WHERE Gender = @Gender
END

EXEC spGetByGender 'Male'


EXEC spGetByGender @Gender = 'Female' -- named parameter

-- SP with OUTPUT parameter


CREATE PROCEDURE spCountByGender
@Gender NVARCHAR(20),
@EmployeeCount INT OUTPUT
AS BEGIN
SELECT @EmployeeCount = COUNT(ID)
FROM tblEmployee WHERE Gender = @Gender
END

DECLARE @Total INT


EXEC spCountByGender @Gender='Male', @EmployeeCount=@Total OUTPUT
SELECT @Total AS MaleCount

-- Optional parameters using defaults


CREATE PROCEDURE spSearch
@Name NVARCHAR(50) = NULL,
@Gender NVARCHAR(10) = NULL
AS BEGIN
SELECT * FROM tblEmployee
WHERE (@Name IS NULL OR Name LIKE @Name)
AND (@Gender IS NULL OR Gender = @Gender)
END

-- Modify SP
ALTER PROCEDURE spGetAllEmployees AS BEGIN ...

-- Drop SP
DROP PROCEDURE spGetAllEmployees

5.2 Output Parameters vs Return Values


Feature Output Parameters RETURN Values

Data type Any SQL data type INTEGER only

Multiple values Yes -- can have multiple OUTPUT params No -- single RETURN value only

NULL capable Yes Yes

Typical purpose Return computed result data to caller Signal success (0) or error code (non-
zero)

5.3 Advantages of Stored Procedures


• Execution Plan Reuse: Plans compiled once and cached; no recompilation on each call
• Reduces Network Traffic: Only the procedure name travels over the network, not full SQL
• Security: Grant EXECUTE without giving direct table SELECT/INSERT/UPDATE/DELETE access
• Prevents SQL Injection: Parameterized SPs treat inputs as data, not executable code
• Centralized Logic: Business rules in one place -- change SP once and all callers benefit

Chapter 6: User-Defined Functions (UDF)


6.1 Scalar Functions
Returns one value. Can be used in SELECT, WHERE, ORDER BY, DEFAULT constraints. Must be called with
schema: dbo.fn_Name().
CREATE FUNCTION dbo.fn_AgeFromDOB (@DOB DATE)
RETURNS INT
AS
BEGIN
DECLARE @Age INT
SET @Age = DATEDIFF(YEAR, @DOB, GETDATE())
-- Adjust if birthday has not occurred yet this year
IF (MONTH(@DOB) > MONTH(GETDATE())) OR
(MONTH(@DOB) = MONTH(GETDATE()) AND DAY(@DOB) > DAY(GETDATE()))
SET @Age = @Age - 1
RETURN @Age
END
-- Usage
SELECT Name, dbo.fn_AgeFromDOB(DateOfBirth) AS Age FROM tblEmployee
SELECT * FROM tblEmployee WHERE dbo.fn_AgeFromDOB(DateOfBirth) > 30

6.2 Inline Table-Valued Functions (ITVF)


Returns a TABLE from a single SELECT. Faster than Multi-statement TVF -- optimizer inlines them like views.
CREATE FUNCTION dbo.fn_GetByGender (@Gender NVARCHAR(20))
RETURNS TABLE
AS
RETURN (
SELECT ID, Name, Gender, Salary
FROM tblEmployee WHERE Gender = @Gender
)

SELECT * FROM dbo.fn_GetByGender('Male')

-- Can JOIN with other tables


SELECT [Link], [Link]
FROM dbo.fn_GetByGender('Female') F
JOIN tblDepartment D ON [Link] = [Link]

6.3 Multi-Statement TVF (MSTVF)


Returns a table built by multiple statements. Slower than ITVF but supports complex procedural logic.
CREATE FUNCTION dbo.fn_EmpSummary (@ManagerId INT)
RETURNS @Result TABLE (
EmployeeName NVARCHAR(50),
ManagerName NVARCHAR(50),
Salary INT
)
AS
BEGIN
INSERT INTO @Result
SELECT [Link], [Link], [Link]
FROM tblEmployee E
LEFT JOIN tblEmployee M ON [Link] = [Link]
WHERE [Link] = @ManagerId
RETURN
END

SELECT * FROM dbo.fn_EmpSummary(5)

6.4 Stored Procedure vs Function


Feature Stored Procedure Function

Return value Optional; uses OUTPUT params or Must return a value (scalar or table)
RETURN n

Use in SELECT/WHERE Cannot be used inline Scalar/TVF can be used inline

DML inside INSERT, UPDATE, DELETE allowed NOT allowed in functions


TRY/CATCH Supported NOT supported

Transactions Supported NOT supported

Deterministic Not required Deterministic UDFs can be indexed


PART 5

Indexes & Views

Chapter 7: Indexes
Indexes allow queries to find rows quickly without full table scans. Without an index, SQL Server does a Table
Scan -- checking every row. The right index can cut query time from minutes to milliseconds.

7.1 Index Types


Type Key Characteristics

Clustered Determines physical row order on disk. Only ONE per table. AUTO-created by PRIMARY
KEY.

Non-Clustered Separate B-tree structure with row pointers. Up to 999 per table.

Unique Enforces uniqueness of index keys. Can be clustered or non-clustered.

Filtered Index on a subset of rows (uses a WHERE clause). Smaller and more efficient.

Columnstore Column-based storage optimal for analytics, DW, and large aggregation queries.

Index with INCLUDE Non-clustered with extra columns added to leaf level -- eliminates Key Lookup.

7.2 Creating and Managing Indexes


-- View existing indexes
EXECUTE sp_helpindex tblEmployee

-- Clustered Index (only one per table)


CREATE CLUSTERED INDEX IX_Salary ON tblEmployee (Salary)

-- Non-Clustered Index
CREATE NONCLUSTERED INDEX IX_Name ON tblEmployee (Name)

-- Covering index: INCLUDE avoids Key Lookup


CREATE NONCLUSTERED INDEX IX_Gender
ON tblEmployee (Gender)
INCLUDE (Name, Salary)

-- Unique Index
CREATE UNIQUE INDEX IX_Email ON tblEmployee (Email)

-- Filtered Index (only active employees)


CREATE INDEX IX_Active ON tblEmployee (Name) WHERE IsActive = 1

-- Drop
DROP INDEX tblEmployee.IX_Name

KEY CONCEPT

CLUSTERED INDEX: Leaf nodes ARE the actual data pages.

Data is physically ordered by the clustered key.

Table with no clustered index = HEAP (unordered storage).

NON-CLUSTERED INDEX: Separate B-tree with row locators.

Leaf nodes contain: index key + clustered key (or RID for heaps).

KEY LOOKUP: When a non-clustered index is used but additional columns are needed,

SQL Server must look up the row in the clustered index -- expensive for many rows.

Fix: use INCLUDE to add needed columns to the non-clustered index leaf level.

PRO TIP

Index columns used in WHERE, JOIN ON, and ORDER BY clauses.

Use INCLUDE columns to create covering indexes that eliminate Key Lookups.

Avoid over-indexing OLTP tables: each index adds overhead to INSERT/UPDATE/DELETE.

Use sys.dm_db_index_usage_stats to identify unused indexes consuming write resources.

Chapter 8: Views
A View is a saved SQL query -- a virtual table. By default no data is stored (non-indexed view executes the
underlying query each time). Views simplify queries, implement security, and provide backward compatibility.

8.1 Basic Views


-- Create
CREATE VIEW vwEmployeeDetails
AS
SELECT [Link], [Link], [Link], [Link] AS Department
FROM tblEmployee E
INNER JOIN tblDepartment D ON [Link] = [Link]

-- Use like a table


SELECT * FROM vwEmployeeDetails
SELECT * FROM vwEmployeeDetails WHERE Gender = 'Female'

-- DML through updateable views (single base table, no aggregates)


UPDATE vwEmployeeDetails SET Name = 'John' WHERE ID = 1

-- WITH ENCRYPTION: hides view source from sp_helptext


CREATE VIEW vwSecure WITH ENCRYPTION AS ...

-- WITH SCHEMABINDING: prevents base tables from being modified/dropped


-- Must use 2-part names ([Link])
CREATE VIEW vwBound WITH SCHEMABINDING AS
SELECT [Link], [Link]
FROM [Link] E JOIN [Link] D ON [Link] = [Link]

8.2 Indexed (Materialized) Views


Indexed Views physically store the result set. SQL Server auto-maintains them on DML. Best for complex
aggregations queried frequently.
-- Requirements:
-- 1. WITH SCHEMABINDING
-- 2. First index must be UNIQUE CLUSTERED
-- 3. COUNT_BIG(*) required if GROUP BY used
-- 4. ANSI_NULLS and QUOTED_IDENTIFIER ON (session settings)

SET ANSI_NULLS ON
SET QUOTED_IDENTIFIER ON

CREATE VIEW vwDeptSummary WITH SCHEMABINDING AS


SELECT DepartmentId, COUNT_BIG(*) AS EmpCount,
SUM(CONVERT(BIGINT, Salary)) AS TotalSalary
FROM [Link]
GROUP BY DepartmentId

-- Materialize the view


CREATE UNIQUE CLUSTERED INDEX UIX_DeptSummary ON vwDeptSummary (DepartmentId)

8.3 View Limitations


• Cannot pass parameters to views -- use Inline Table-Valued Functions instead
• Cannot use ORDER BY unless combined with TOP or FOR XML
• Cannot reference temporary tables or table variables
• DML through multi-table views is restricted
• WITH ENCRYPTION prevents sp_helptext from showing source (cannot be decrypted)
PART 6

Triggers

Chapter 9: DML Triggers


Triggers are special stored procedures that fire automatically when DML or DDL events occur. They cannot be
called directly -- they execute in response to INSERT, UPDATE, DELETE, or DDL events.

9.1 Trigger Types


Type When It Fires Typical Use

AFTER INSERT After INSERT completes Audit logging, cascading data changes

AFTER UPDATE After UPDATE completes Track changes, maintain audit trail

AFTER DELETE After DELETE completes Archive records before deletion

INSTEAD OF INSERT Replaces the INSERT Make non-updateable views insertable

INSTEAD OF UPDATE Replaces the UPDATE Complex updates through views

INSTEAD OF DELETE Replaces the DELETE Soft delete, cascade through views

DDL (DATABASE) On CREATE/ALTER/DROP in Prevent schema changes, audit DDL


current database

DDL (SERVER) On server-level DDL events Cross-database auditing, governance

Logon Trigger On user LOGON event Restrict connections, audit logins

KEY CONCEPT

INSERTED and DELETED -- the two magic virtual tables in triggers:

INSERTED: Contains NEW row data (new values after INSERT or UPDATE)

DELETED: Contains OLD row data (old values before DELETE or UPDATE)

For INSERT triggers: only INSERTED is populated

For DELETE triggers: only DELETED is populated


For UPDATE triggers: DELETED = old values, INSERTED = new values

IMPORTANT: Triggers fire ONCE per DML statement, not once per row.

If UPDATE affects 100 rows, INSERTED and DELETED each have 100 rows.

Never assume single-row triggers -- always handle multi-row cases.

9.2 AFTER Triggers


-- AFTER INSERT trigger
CREATE TRIGGER tr_Employee_AfterInsert
ON tblEmployee FOR INSERT
AS
BEGIN
INSERT INTO tblAudit (AuditData, AuditDate)
SELECT 'New Employee: ' + Name, GETDATE()
FROM INSERTED
END

-- AFTER UPDATE trigger (uses both DELETED and INSERTED)


CREATE TRIGGER tr_Employee_AfterUpdate
ON tblEmployee FOR UPDATE
AS
BEGIN
INSERT INTO tblAudit (AuditData, AuditDate)
SELECT [Link] + changed salary to + CAST([Link] AS NVARCHAR), GETDATE()', '
FROM DELETED D INNER JOIN INSERTED I ON [Link] = [Link]', 'END', '', '-- AFTER
DELETE trigger', 'CREATE TRIGGER tr_Employee_AfterDelete', 'ON tblEmployee FOR
DELETE', 'AS', 'BEGIN', " INSERT INTO tblAudit (AuditData, AuditDate)",
" SELECT 'Deleted: ' + Name, GETDATE() FROM DELETED", 'END', ]))
content_parts.append(el()) content_parts.append(h2('9.3 INSTEAD OF Triggers'))
content_parts.append(code_block([ '-- INSTEAD OF INSERT: make a multi-table view
insertable', 'CREATE TRIGGER tr_vwEmpDept_Insert', 'ON vwEmployeeDept',
'INSTEAD OF INSERT', 'AS', 'BEGIN', ' DECLARE @DeptID INT', '
SELECT @DeptID = DeptId FROM tblDepartment', ' WHERE DeptName = (SELECT DeptName
FROM INSERTED)', '', ' IF @DeptID IS NULL', ' BEGIN', '
INSERT INTO tblDepartment (DeptName)', ' SELECT DeptName FROM INSERTED', '
SET @DeptID = SCOPE_IDENTITY()', ' END', '', ' INSERT INTO tblEmployee
(Name, Gender, DepartmentId)', ' SELECT Name, Gender, @DeptID FROM INSERTED',
'END', '', '-- Manage triggers', 'DISABLE TRIGGER tr_Employee_AfterUpdate ON
tblEmployee', 'ENABLE TRIGGER tr_Employee_AfterUpdate ON tblEmployee', 'DROP
TRIGGER tr_Employee_AfterInsert', ])) content_parts.append(el())
content_parts.append(h2('9.4 DDL Triggers')) content_parts.append(code_block([ '--
Database-scoped: prevent table changes', 'CREATE TRIGGER tr_PreventDDL', 'ON
DATABASE FOR CREATE_TABLE, ALTER_TABLE, DROP_TABLE', 'AS', 'BEGIN', '
ROLLBACK', " PRINT 'Table changes are not permitted.'", 'END', '', '--
Server-scoped: audit DDL changes across all databases', 'CREATE TRIGGER tr_AuditDDL',
'ON ALL SERVER FOR CREATE_TABLE, ALTER_TABLE, DROP_TABLE', 'AS', 'BEGIN', "
INSERT INTO [Link] (DatabaseName, ObjectName, EventType, SQLCmd, LogDate)",
" SELECT", " EVENTDATA().value('(/EVENT_INSTANCE/DatabaseName)[1]',
'NVARCHAR(250)'),", " EVENTDATA().value('(/EVENT_INSTANCE/ObjectName)[1]',
'NVARCHAR(250)'),", " EVENTDATA().value('(/EVENT_INSTANCE/EventType)[1]',
'NVARCHAR(250)'),", " EVENTDATA().value('(/EVENT_INSTANCE/TSQLCommand)[1]',
'NVARCHAR(2500)'),", ' GETDATE()', 'END', '', '-- Trigger
execution order', "EXEC sp_settriggerorder @triggername='tr_First', @order='First',
@stmttype='INSERT'", '-- Server-scoped triggers ALWAYS fire before database-scoped
triggers', ])) content_parts.append(el()) content_parts.append(pb()) # ==== PART 7 ====
content_parts.append(part(7, 'CTEs, Subqueries & Set Operations'))
content_parts.append(h1('Chapter 10: Subqueries & CTEs')) content_parts.append(h2('10.1
Subqueries')) content_parts.append(code_block([ '-- Non-correlated: executes once',
'SELECT Name FROM tblEmployee', 'WHERE Salary > (SELECT AVG(Salary) FROM
tblEmployee)', '', '-- Correlated: references outer query; runs once per outer
row', 'SELECT Name, Salary,', ' (SELECT AVG(Salary) FROM tblEmployee E2',
' WHERE [Link] = [Link]) AS DeptAvg', 'FROM tblEmployee E1', '',
'-- EXISTS: true if subquery returns any rows', 'SELECT Name FROM tblDepartment D',
'WHERE EXISTS (SELECT 1 FROM tblEmployee E WHERE [Link] = [Link])', '', '-- NOT
EXISTS: true if no rows returned', 'SELECT Name FROM tblDepartment D', 'WHERE NOT
EXISTS (SELECT 1 FROM tblEmployee E WHERE [Link] = [Link])', '', '-- SubQuery vs
JOIN: in most cases SQL Server produces the same execution plan.', '-- Use EXISTS
instead of IN for nullable columns to handle NULLs correctly.', ]))
content_parts.append(el()) content_parts.append(h2('10.2 Common Table Expressions
(CTEs)')) content_parts.append(p('A CTE is a named temporary result set defined with
WITH. It improves readability, can be referenced multiple times, and supports
UPDATE/DELETE.')) content_parts.append(code_block([ '-- Basic CTE', 'WITH
CTE_HighEarners AS (', ' SELECT ID, Name, Salary, Gender', ' FROM
tblEmployee WHERE Salary > 5000', ')', "SELECT * FROM CTE_HighEarners WHERE
Gender = 'Male'", '', '-- Multiple CTEs', 'WITH', "CTE_Male AS (SELECT
* FROM tblEmployee WHERE Gender = 'Male'),", "CTE_Female AS (SELECT * FROM
tblEmployee WHERE Gender = 'Female')", "SELECT 'Male', COUNT(*) FROM CTE_Male",
'UNION ALL', "SELECT 'Female', COUNT(*) FROM CTE_Female", '', '-- Updatable
CTE (single-table)', 'WITH CTE_Update AS (', ' SELECT TOP 3 Salary FROM
tblEmployee ORDER BY Salary DESC', ')', 'UPDATE CTE_Update SET Salary = Salary *
1.10', ])) content_parts.append(el()) content_parts.append(h2('10.3 Recursive CTE'))
content_parts.append(code_block([ '-- Recursive CTE: anchor member UNION ALL
recursive member', 'WITH CTE_OrgChart AS (', ' -- Anchor: top of hierarchy
(CEO with no manager)', ' SELECT ID, Name, ManagerID, 0 AS Level', ' FROM
tblEmployee WHERE ManagerID IS NULL', '', ' UNION ALL', '', ' --
Recursive: join CTE back to employees', ' SELECT [Link], [Link], [Link],
[Link] + 1', ' FROM tblEmployee E', ' INNER JOIN CTE_OrgChart CTE ON
[Link] = [Link]', ')', 'SELECT ID, Name, Level FROM CTE_OrgChart ORDER BY
Level, Name', 'OPTION (MAXRECURSION 500) -- default=100; 0=unlimited (use with
caution)', ])) content_parts.append(el()) content_parts.append(h2('10.4 UNION,
INTERSECT, EXCEPT'))
content_parts.append(dtable( ['Operator','Returns','Duplicates'],
[ ['UNION','Rows from first OR second (combined)','Removes duplicates (like
DISTINCT)'], ['UNION ALL','Rows from first OR second (combined)','Keeps ALL
duplicates (faster)'], ['INTERSECT','Rows present in BOTH result sets','No
duplicates'], ['EXCEPT','Rows in FIRST set but NOT in second','No duplicates'],
], [1800, 4800, 2760] )) content_parts.append(el()) content_parts.append(h2('10.5
PIVOT and UNPIVOT')) content_parts.append(code_block([ '-- PIVOT: rotate unique row
values into column headers', 'SELECT Country, [2012], [2013], [2014], [2015]',
'FROM SalesSummary', 'PIVOT (', ' SUM(SaleAmount)', ' FOR SaleYear IN
([2012], [2013], [2014], [2015])', ') AS PivotTable', '', '-- UNPIVOT: rotate
columns back into rows', 'SELECT Country, SaleYear, SaleAmount', 'FROM
PivotTable', 'UNPIVOT (', ' SaleAmount FOR SaleYear IN ([2012], [2013],
[2014], [2015])', ') AS UnpivotTable', '-- Note: UNPIVOT cannot reverse
aggregated PIVOT data', ])) content_parts.append(el()) content_parts.append(h2('10.6
CROSS APPLY and OUTER APPLY')) content_parts.append(code_block([ '-- APPLY: invoke a
TVF for each row of a driving table', '-- CROSS APPLY = like INNER JOIN (excludes
rows where TVF returns nothing)', '-- OUTER APPLY = like LEFT JOIN (includes rows
even if TVF returns nothing)', '', '-- Top 3 employees per department using CROSS
APPLY', 'SELECT [Link], [Link], [Link]', 'FROM tblDepartment D',
'CROSS APPLY (', ' SELECT TOP 3 Name, Salary FROM tblEmployee', ' WHERE
DepartmentId = [Link]', ' ORDER BY Salary DESC', ') E', ]))
content_parts.append(el()) content_parts.append(h2('10.7 SELECT INTO'))
content_parts.append(code_block([ '-- Copy data into a NEW table (table is auto-
created)', 'SELECT * INTO tblEmployeeBackup FROM tblEmployee', '', '-- Copy
structure only (no data)', 'SELECT * INTO tblEmptyClone FROM tblEmployee WHERE 1 =
0', '', '-- Note: indexes, constraints, and triggers are NOT copied', '-- You
must recreate them manually on the new table', ])) content_parts.append(el())
content_parts.append(pb()) # ==== PART 8 ==== content_parts.append(part(8, 'Transactions
& Error Handling')) content_parts.append(h1('Chapter 11: Transactions'))
content_parts.append(code_block([ '-- Transaction with TRY/CATCH (recommended
pattern)', 'BEGIN TRY', ' BEGIN TRANSACTION', ' UPDATE tblInventory
SET Qty = Qty - @Qty WHERE ItemId = @ItemId', ' INSERT INTO tblSales VALUES
(@ItemId, @Qty, GETDATE())', ' COMMIT TRANSACTION', 'END TRY', 'BEGIN
CATCH', ' ROLLBACK TRANSACTION', ' SELECT ERROR_NUMBER() AS ErrNum,
ERROR_MESSAGE() AS ErrMsg', 'END CATCH', ])) content_parts.append(el())
content_parts.append(h2('11.1 ACID Properties'))
content_parts.append(dtable( ['Property','Definition','Example'],
[ ['Atomic','All-or-nothing: all statements succeed or all roll back','Bank
transfer: debit and credit both succeed or both fail'], ['Consistent','Data is
left in a logically valid state after every transaction','Inventory cannot go negative
after a sale'], ['Isolated','Concurrent transactions cannot see each other s
uncommitted changes','Two users booking same seat -- only one wins'],
['Durable','Committed changes survive crashes and power failures','Written to disk and
transaction log before COMMIT returns'], ], [1600, 3600, 4160] ))
content_parts.append(el()) content_parts.append(h2('11.2 Isolation Levels & Concurrency
Problems')) content_parts.append(dtable( ['Problem','Description','Prevented By
Isolation Level'], [ ['Dirty Read','Reading uncommitted data that may be
rolled back','Read Committed or higher'], ['Lost Update','Two tx read same value,
both update -- second overwrites first','Repeatable Read or higher'], ['Non-
Repeatable Read','Same row read twice gets different values','Repeatable Read or
higher'], ['Phantom Read','Query gets different row count on re-
execution','Serializable'], ], [2400, 3600, 3360] )) content_parts.append(el())
content_parts.append(code_block([ '-- Set isolation level', 'SET TRANSACTION
ISOLATION LEVEL READ UNCOMMITTED -- dirty reads allowed', 'SET TRANSACTION ISOLATION
LEVEL READ COMMITTED -- default', 'SET TRANSACTION ISOLATION LEVEL REPEATABLE
READ', 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE -- strictest locking',
'SET TRANSACTION ISOLATION LEVEL SNAPSHOT -- row versioning, no shared locks',
'', '-- Enable Read Committed Snapshot Isolation (RCSI) at database level',
'ALTER DATABASE SampleDB SET READ_COMMITTED_SNAPSHOT ON', '-- RCSI: reads use row
versions instead of shared locks', '-- No application code changes needed; resolves
most blocking issues', '', '-- Snapshot vs RCSI:', '-- Snapshot: protects
from UPDATE conflicts; app may need retry logic', '-- RCSI: no update conflict risk;
works transparently with existing apps', ])) content_parts.append(el())
content_parts.append(h1('Chapter 12: Error Handling')) content_parts.append(h2('12.1
TRY/CATCH and Error Functions')) content_parts.append(code_block([ '-- Error
information functions (only valid inside CATCH block)', 'SELECT', '
ERROR_NUMBER() AS ErrorNumber,', ' ERROR_SEVERITY() AS Severity,', '
ERROR_STATE() AS State,', ' ERROR_PROCEDURE() AS Procedure,', '
ERROR_LINE() AS Line,', ' ERROR_MESSAGE() AS Message', '', '--
RAISERROR: raise user-defined error (older syntax)', "RAISERROR ('Custom error: %s',
16, 1, 'detail')", '-- Parameters: message, severity (11-19=user error, 20-25=fatal),
state', '', '-- THROW (SQL Server 2012+): preferred; re-throws original error',
'BEGIN TRY', ' SELECT 1/0', 'END TRY', 'BEGIN CATCH', ' ; THROW
-- semicolon required before THROW', ' -- THROW with no args: re-raises original
error unchanged', 'END CATCH', ])) content_parts.append(el())
content_parts.append(h2('12.2 Deadlocks')) content_parts.append(p('A deadlock occurs
when two transactions each hold a lock the other needs. SQL Server detects deadlocks via
a lock monitor (runs every 5 seconds) and kills one transaction (the deadlock victim) to
break the cycle.')) content_parts.append(code_block([ '-- Log deadlocks to error log
(enable trace flag 1222)', 'DBCC TRACEON (1222, -1) -- -1 means server-wide',
'', '-- Catch deadlock error (error 1205) in T-SQL', 'BEGIN CATCH', ' IF
ERROR_NUMBER() = 1205', " PRINT 'Deadlock victim -- retry the transaction'",
' ELSE', ' ; THROW', 'END CATCH', '', '-- Control which session
is chosen as victim', 'SET DEADLOCK_PRIORITY LOW -- prefer this session to be
killed', 'SET DEADLOCK_PRIORITY HIGH -- prefer other session to be killed', '',
'-- DEADLOCK PREVENTION best practices:', '-- 1. Access tables in the SAME ORDER in
all transactions', '-- 2. Keep transactions SHORT to minimize lock duration', '--
3. Use SNAPSHOT or RCSI isolation to eliminate read locks', '-- 4. Add indexes to
reduce lock scope (fewer rows locked)', '', '-- Find blocking queries',
'SELECT blocking_session_id, session_id, wait_type, wait_time', 'FROM
sys.dm_exec_requests WHERE blocking_session_id <> 0', ])) content_parts.append(el())
content_parts.append(pb()) # ==== PART 9 ==== content_parts.append(part(9, 'Window
Functions')) content_parts.append(h1('Chapter 13: OVER Clause & Window Functions'))
content_parts.append(p('Window functions compute values across a set of rows (the
"window") related to the current row. Unlike GROUP BY, they do not collapse rows -- all
rows are retained with additional computed columns alongside.'))
content_parts.append(el()) content_parts.append(h2('13.1 OVER Clause'))
content_parts.append(code_block([ '-- Syntax: function() OVER ([PARTITION BY cols]
[ORDER BY cols] [ROWS/RANGE ...])', '', '-- Aggregates with OVER: group stats
alongside individual rows', 'SELECT', ' Name, Gender, Salary,', '
COUNT(*) OVER (PARTITION BY Gender) AS GenderCount,', ' AVG(Salary) OVER
(PARTITION BY Gender) AS GenderAvg,', ' SUM(Salary) OVER () AS TotalSalary',
'FROM Employees', '', '-- Running total (ORDER BY inside OVER)', 'SELECT
Name, Salary,', ' SUM(Salary) OVER (ORDER BY Salary ROWS UNBOUNDED PRECEDING) AS
RunningTotal', 'FROM Employees', ])) content_parts.append(el())
content_parts.append(h2('13.2 ROW_NUMBER, RANK, DENSE_RANK, NTILE'))
content_parts.append(dtable( ['Function','Ties Handling','Gaps After Tie','Example (5
rows, 2-way tie at top)'], [ ['ROW_NUMBER()','Unique sequential number for
all rows','No gaps','1, 2, 3, 4, 5'], ['RANK()','Same rank for ties','Gaps after
tie','1, 1, 3, 4, 5'], ['DENSE_RANK()','Same rank for ties','No gaps','1, 1, 2,
3, 4'], ['NTILE(n)','Distributes rows into n groups','N/A','Groups of ~equal
size'], ], [2200, 2200, 1400, 3560] )) content_parts.append(el())
content_parts.append(code_block([ 'SELECT', ' Name, Gender, Salary,', '
ROW_NUMBER() OVER (PARTITION BY Gender ORDER BY Salary DESC) AS RowNum,', ' RANK()
OVER (PARTITION BY Gender ORDER BY Salary DESC) AS Rnk,', ' DENSE_RANK() OVER
(PARTITION BY Gender ORDER BY Salary DESC) AS DRnk,', ' NTILE(4) OVER (ORDER
BY Salary) AS Quartile', 'FROM Employees', '', '-- Pagination with ROW_NUMBER
(SQL 2005-2011)', 'WITH Paged AS (', ' SELECT *, ROW_NUMBER() OVER (ORDER BY
Name) AS RowNum FROM Employees', ')', 'SELECT * FROM Paged WHERE RowNum BETWEEN
11 AND 20 -- page 2', '', '-- OFFSET FETCH (SQL 2012+): cleaner pagination',
'SELECT * FROM Employees', 'ORDER BY Name', 'OFFSET 10 ROWS FETCH NEXT 10 ROWS
ONLY', ])) content_parts.append(el()) content_parts.append(h2('13.3 LEAD, LAG,
FIRST_VALUE, LAST_VALUE')) content_parts.append(code_block([ '-- LAG: access previous
row data', 'SELECT Name, Salary,', ' LAG(Salary, 1, 0) OVER (ORDER BY Salary)
AS PrevSalary,', ' Salary - LAG(Salary, 1, 0) OVER (ORDER BY Salary) AS Increase',
'FROM Employees', '-- LAG(column, offset, default_when_null)', '', '-- LEAD:
access next row data', 'SELECT Name, Salary,', ' LEAD(Salary, 1, 0) OVER
(ORDER BY Salary) AS NextSalary', 'FROM Employees', '', '-- FIRST_VALUE:
first value in the window', 'SELECT Name, Gender, Salary,', '
FIRST_VALUE(Name) OVER (PARTITION BY Gender ORDER BY Salary) AS LowestPaid', 'FROM
Employees', '', '-- LAST_VALUE: REQUIRES explicit ROWS clause for correct
behavior!', 'SELECT Name, Gender, Salary,', ' LAST_VALUE(Name) OVER (', '
PARTITION BY Gender', ' ORDER BY Salary', ' ROWS BETWEEN UNBOUNDED
PRECEDING AND UNBOUNDED FOLLOWING', ' ) AS HighestPaid', 'FROM Employees',
'-- Without ROWS clause: default is RANGE CURRENT ROW, meaning', '-- LAST_VALUE only
considers up to the current row -- NOT the true last!', ])) content_parts.append(el())
content_parts.append(h2('13.4 ROWS vs RANGE')) content_parts.append(box('ARCH',
[ 'ROWS: Physical framing -- counts actual rows in the window.', ' ROWS BETWEEN
UNBOUNDED PRECEDING AND CURRENT ROW = running total', ' ROWS BETWEEN 1 PRECEDING
AND 1 FOLLOWING = 3-row moving average', ' ROWS BETWEEN UNBOUNDED PRECEDING
AND UNBOUNDED FOLLOWING = entire partition', '', 'RANGE: Logical framing --
groups rows with the same ORDER BY value together.', ' Default when ORDER BY is
specified (but no ROWS/RANGE explicit):', ' RANGE BETWEEN UNBOUNDED PRECEDING AND
CURRENT ROW', ' For tied ORDER BY values: all tied rows are in the same "current"
group.', '', 'KEY DIFFERENCE for ties:', ' ROWS: each row counted
independently; running total increments one row at a time.', ' RANGE: all rows with
same ORDER BY value are processed together; total jumps for all.', '',
'RECOMMENDATION: Use ROWS explicitly for running totals to get predictable results.',
'Use ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING for LAST_VALUE.', ]))
content_parts.append(el()) content_parts.append(pb()) # ==== PART 10 ====
content_parts.append(part(10, 'Advanced Topics')) content_parts.append(h1('Chapter 14:
Dynamic SQL')) content_parts.append(p('Dynamic SQL is T-SQL code constructed as a string
at runtime and then executed. Required when object names (tables, columns) or filter
criteria are not known at compile time.')) content_parts.append(el())
content_parts.append(h2('14.1 EXEC vs sp_executesql'))
content_parts.append(dtable( ['Feature','EXEC','sp_executesql'],
[ ['Parameters','No parameterization','Typed input and output parameters'],
['Plan caching','New plan per unique string','Plans reused when parameterized'],
['SQL Injection','Vulnerable with string concat','Safe with parameters'],
['Performance','Worse for repeated calls','Better -- plan reuse'], ['Best used
for','Simple one-off statements','Production code, repeated queries'], ], [2500,
3300, 3560] )) content_parts.append(el()) content_parts.append(code_block([ '-- EXEC:
simple (use QUOTENAME for object names)', 'DECLARE @SQL NVARCHAR(1000)', 'SET
@SQL = N''SELECT * FROM '' + QUOTENAME(@TableName)', 'EXEC (@SQL)', '', '--
sp_executesql: parameterized (safe + plan caching)', 'DECLARE @SQL
NVARCHAR(1000)', 'DECLARE @ParamDef NVARCHAR(500)', "SET @SQL = N'SELECT *
FROM tblEmployee WHERE Gender = @Gender AND Salary > @Min'", "SET @ParamDef =
N'@Gender NVARCHAR(20), @Min INT'", "EXEC sp_executesql @SQL, @ParamDef, @Gender =
'Male', @Min = 5000", '', '-- sp_executesql with OUTPUT parameter', 'DECLARE
@CountSQL NVARCHAR(500)', 'DECLARE @Count INT', "SET @CountSQL = N'SELECT
@TotalCount = COUNT(*) FROM tblEmployee'", "EXEC sp_executesql @CountSQL,
N'@TotalCount INT OUTPUT', @TotalCount = @Count OUTPUT", 'SELECT @Count AS Total',
'', '-- QUOTENAME: wraps object name in [] and escapes embedded brackets', "SET
@SQL = 'SELECT * FROM ' + QUOTENAME(@TableName)", "-- Input: 'Bad]Table' -> Output:
[Bad]]Table]", ])) content_parts.append(el()) content_parts.append(box('WARN',
[ 'NEVER concatenate user input directly into SQL strings -- this enables SQL
Injection.', "Example attack: @Name = '; DROP TABLE tblEmployee; --' destroys your
table.", '', 'RULE 1: For VALUES in WHERE clause -- use sp_executesql
parameters.', 'RULE 2: For OBJECT NAMES (tables, columns) -- use QUOTENAME().',
'', 'Plan caching: sp_executesql caches plans when parameterized.', 'EXEC creates
a brand new plan for every unique string -- wastes resources.', ]))
content_parts.append(el()) content_parts.append(h1('Chapter 15: Temporary Tables & Table
Variables')) content_parts.append(code_block([ '-- Local temp table: # prefix;
visible only to current session', 'CREATE TABLE #TempEmp (ID INT, Name NVARCHAR(50),
Salary INT)', 'INSERT INTO #TempEmp SELECT ID, Name, Salary FROM tblEmployee',
'SELECT * FROM #TempEmp', 'DROP TABLE #TempEmp -- or auto-dropped when session
ends', '', '-- Global temp table: ## prefix; all sessions can see it',
'CREATE TABLE ##GlobalEmp (ID INT, Name NVARCHAR(50))', '-- Dropped when the creating
session ends AND all other references close', '', '-- Table variable: @ prefix;
scoped to batch only', 'DECLARE @T TABLE (ID INT, Name NVARCHAR(50))', "INSERT
INTO @T VALUES (1, 'Alice'), (2, 'Bob')", 'SELECT * FROM @T', '', '-- Dynamic
SQL + temp tables: create BEFORE calling sp_executesql', 'CREATE TABLE #Results (ID
INT, Name NVARCHAR(50))', "EXEC sp_executesql N'INSERT INTO #Results SELECT ID, Name
FROM tblEmployee'", 'SELECT * FROM #Results -- works! Table is in outer scope.',
'-- Temp tables created INSIDE sp_executesql are dropped when it finishes.', ]))
content_parts.append(el()) content_parts.append(dtable( ['Feature','Local Temp
(#)','Global Temp (##)','Table Variable (@)'], [ ['Visible to','Current
session only','All sessions','Current batch only'], ['Lifetime','Session end or
DROP','All references closed','End of batch'], ['Transactions','Participates; can
rollback','Yes','Not rolled back on ROLLBACK'], ['Indexes','CREATE INDEX
supported','Yes','Only via PRIMARY KEY/UNIQUE constraints'],
['Statistics','Maintained by SQL Server','Yes','None -- optimizer uses estimate of 1
row'], ['Best for','Medium result sets; complex SP logic','Shared scratch
space','Small sets; simple lookups'], ], [2600, 2100, 2000, 2660] ))
content_parts.append(el()) content_parts.append(h1('Chapter 16: Normalization'))
content_parts.append(p('Normalization is the process of organizing data to minimize
redundancy and ensure consistency. It proceeds through a series of "normal forms", each
eliminating a specific type of data anomaly.')) content_parts.append(el())
content_parts.append(dtable( ['Normal Form','Requirement','Eliminates'], [
['1NF','Atomic values in each cell; no repeating groups; primary key defined','Multi-
valued columns; repeating groups'], ['2NF','Must be 1NF; every non-key column
depends on the ENTIRE primary key (no partial dependencies)','Partial dependencies in
composite key tables'], ['3NF','Must be 2NF; non-key columns depend ONLY on the
primary key (no transitive dependencies)','Transitive dependencies: non-key column ->
non-key column'], ], [1800, 4000, 3560] )) content_parts.append(el())
content_parts.append(code_block([ '-- 1NF violation: multiple values in one cell',
'-- PersonID | Phone', '-- 1 | 555-1234, 555-5678 <- not atomic!', '--
Fix: separate PhoneNumbers table with PersonID FK', '', '-- 2NF violation:
partial dependency', '-- OrderItem: OrderID, ProductID, ProductName, Qty', '--
ProductName depends only on ProductID (partial, not full composite key)', '-- Fix:
extract Product table; OrderItem keeps only (OrderID, ProductID FK, Qty)', '',
'-- 3NF violation: transitive dependency', '-- Employee: EmpID, ZipCode, City,
State', '-- City and State depend on ZipCode, not directly on EmpID', '-- Fix:
extract ZipCodes table; Employee keeps only (EmpID, ZipCode FK)', ]))
content_parts.append(el()) content_parts.append(h1('Chapter 17: Grouping Sets, ROLLUP &
CUBE')) content_parts.append(code_block([ '-- GROUPING SETS: define multiple grouping
combinations in one query', 'SELECT City, Gender, COUNT(*) AS Count', 'FROM
tblEmployee', 'GROUP BY GROUPING SETS (', ' (City, Gender), -- group by City
AND Gender', ' (City), -- group by City only', ' (Gender),
-- group by Gender only', ' () -- grand total row', ')',
'', '-- ROLLUP: hierarchical subtotals, right-to-left', 'SELECT Country, State,
City, SUM(Sales) AS Total', 'FROM SalesData', 'GROUP BY ROLLUP (Country, State,
City)', '-- Produces: City totals -> State totals -> Country totals -> Grand Total',
'', '-- CUBE: ALL possible combinations of grouping sets', 'SELECT Country,
State, City, SUM(Sales)', 'FROM SalesData', 'GROUP BY CUBE (Country, State,
City)', '-- 3 columns = 2^3 = 8 combinations including grand total', '', '--
GROUPING(col): 1=aggregated summary row, 0=detail row', 'SELECT', " CASE WHEN
GROUPING(City) = 1 THEN 'All Cities' ELSE City END AS City,", ' SUM(Sales) AS
Total', 'FROM SalesData', 'GROUP BY ROLLUP (City)', '', '--
GROUPING_ID(c1,c2,...): binary mask of which cols are aggregated', 'SELECT Country,
Gender, SUM(Salary),', ' GROUPING_ID(Country, Gender) AS Level', 'FROM
tblEmployee', 'GROUP BY CUBE (Country, Gender)', ])) content_parts.append(el())
content_parts.append(h1('Chapter 18: Sequences, GUIDs & MERGE'))
content_parts.append(h2('18.1 Sequence Object (SQL Server 2012+)'))
content_parts.append(code_block([ '-- Create a Sequence', 'CREATE SEQUENCE
[Link]', ' START WITH 1', ' INCREMENT BY 1', ' MINVALUE 1',
' MAXVALUE 99999', ' CYCLE -- restart from MINVALUE after MAXVALUE',
' CACHE 10 -- pre-allocate 10 values for performance', '', '-- Use',
'SELECT NEXT VALUE FOR [Link] AS NextOrderID', '', '-- Get value BEFORE
INSERT (advantage over IDENTITY)', 'DECLARE @NewID INT = NEXT VALUE FOR
[Link]', 'INSERT INTO tblOrder (OrderID) VALUES (@NewID)', 'INSERT INTO
tblOrderDetail (OrderID) VALUES (@NewID)', ])) content_parts.append(el())
content_parts.append(dtable( ['Feature','IDENTITY','SEQUENCE'],
[ ['Scope','Tied to a specific table column','Independent database object'],
['Shared across tables','No','Yes'], ['Get value before INSERT','No','Yes -- NEXT
VALUE FOR'], ['Cycling','DBCC CHECKIDENT to reset','ALTER SEQUENCE; CYCLE option
built-in'], ['Min/Max control','No','Yes -- MINVALUE / MAXVALUE'],
['Performance','N/A','CACHE option pre-allocates values'], ], [2700, 3300,
3360] )) content_parts.append(el()) content_parts.append(h2('18.2 GUIDs
(UNIQUEIDENTIFIER)')) content_parts.append(code_block([ '-- GUID: 16-byte, globally
unique identifier', 'SELECT NEWID() -- random GUID; causes index
fragmentation as PK', 'SELECT NEWSEQUENTIALID() -- sequential GUID; better for
clustered index PKs', '', 'CREATE TABLE tblSession (', ' SessionID
UNIQUEIDENTIFIER DEFAULT NEWSEQUENTIALID() PRIMARY KEY,', ' UserName
NVARCHAR(50)', ')', '', '-- Check if NULL or empty GUID', 'IF @MyGuid IS
NULL PRINT ''GUID is NULL
IF @MyGuid = '00000000-0000-0000-0000-000000000000' PRINT 'GUID is empty'

18.3 MERGE Statement


-- MERGE: perform INSERT, UPDATE, and DELETE in a single statement
MERGE tblProduct AS Target
USING tblUpdates AS Source
ON [Link] = [Link]

WHEN MATCHED AND [Link] <> [Link] THEN


UPDATE SET [Link] = [Link]

WHEN NOT MATCHED BY TARGET THEN


INSERT (ProductID, Name, Price)
VALUES ([Link], [Link], [Link])

WHEN NOT MATCHED BY SOURCE THEN


DELETE; -- semicolon required at end of MERGE statement
Chapter 19: Built-in Functions
19.1 String Functions
Function Description

LEN(str) Length of string (excluding trailing spaces)

UPPER(str) / LOWER(str) Convert case

LTRIM(str) / RTRIM(str) Remove leading / trailing spaces

LEFT(str,n) / RIGHT(str,n) Return leftmost / rightmost n characters

SUBSTRING(str,start,len) Return part of string starting at position

CHARINDEX(find,str) Find position of substring (0 if not found)

REPLACE(str,old,new) Replace all occurrences of old with new

REPLICATE(str,n) Repeat a string n times

PATINDEX(pattern,str) Pattern search (supports wildcards); returns position or 0

STUFF(str,start,len,new) Delete len chars at start, then insert new string

REVERSE(str) Reverse string characters

SPACE(n) Return n spaces

ASCII(char) / CHAR(n) Convert between character and ASCII code

19.2 Date & Time Functions


Function Description

GETDATE() Current server date and time (datetime)

GETUTCDATE() Current UTC date and time

SYSDATETIME() Higher-precision current datetime (datetime2)

DATEADD(part,n,date) Add n of datepart: DATEADD(DAY,7,GETDATE()) = next week

DATEDIFF(part,d1,d2) Difference in datepart units between two dates

DATEPART(part,date) Returns integer: DATEPART(MONTH,GETDATE()) = 1-12

DATENAME(part,date) Returns string: DATENAME(MONTH,GETDATE()) = January

DAY/MONTH/YEAR(date) Shorthand for DATEPART(DAY/MONTH/YEAR,date)

ISDATE(expr) 1 if valid date/time; 0 otherwise (returns 0 for datetime2)

EOMONTH(date) Last day of the month of the given date

DATEFROMPARTS(y,m,d) Build a date from year, month, day integers


FORMAT(val,format) Flexible formatting: FORMAT(GETDATE(),dd/MM/yyyy)

CONVERT(type,val,style) Convert with optional format style (103=DD/MM/YYYY)

TRY_PARSE(str AS type) SQL 2012+: parse string to date/numeric; NULL on failure

TRY_CONVERT(type,val) SQL 2012+: convert with NULL on failure instead of error

CHOOSE(n,v1,v2,...) SQL 2012+: return nth item (1-based index)

IIF(cond,true,false) SQL 2012+: inline if-else (shorthand for CASE WHEN)

19.3 DateTime Type Comparison


Type Date Range Accuracy Size

SmallDateTime 1900-01-01 to 2079-06-06 1 minute 4 bytes

DateTime 1753-01-01 to 9999-12-31 3.33 milliseconds 8 bytes

DateTime2 (preferred) 0001-01-01 to 9999-12-31 100 nanoseconds 6-8 bytes

Date 0001-01-01 to 9999-12-31 Day only 3 bytes

Time N/A (time only) 100 nanoseconds 3-5 bytes

DateTimeOffset 0001-01-01 to 9999-12-31 + tz 100 nanoseconds 8-10 bytes

19.4 Mathematical & Utility


Function Description

ABS(n) Absolute value: ABS(-10) = 10

CEILING(n) Round up to nearest integer: CEILING(1.1) = 2

FLOOR(n) Round down to nearest integer: FLOOR(1.9) = 1

ROUND(n,d) Round to d decimal places: ROUND(3.456,2) = 3.46

SQUARE(n) / SQRT(n) Square and square root

POWER(base,exp) Power: POWER(2,10) = 1024

RAND() Random float between 0 and 1

SCOPE_IDENTITY() Last identity value generated in current scope

IDENT_CURRENT('tbl') Last identity value for specific table, any session

ISNULL(check,rep) Replace NULL with replacement value (2 params)

COALESCE(v1,v2,...) First non-NULL value from list (N params; ANSI standard)

NULLIF(a,b) Returns NULL if a equals b; else returns a

NEWID() Generate a random GUID


NEWSEQUENTIALID() Generate a sequential GUID (use as clustered index PK)
Quick Reference Summary

Topic / Command Key Syntax / Rule

Create Database CREATE DATABASE dbName

Force Drop DB ALTER DATABASE db SET SINGLE_USER WITH ROLLBACK IMMEDIATE then
DROP DATABASE db

Identity Column ColName INT IDENTITY(1,1) PRIMARY KEY

Get Last Identity SCOPE_IDENTITY() -- always use this (not @@IDENTITY)

Default Constraint ALTER TABLE t ADD CONSTRAINT DF_Name DEFAULT 'value' FOR ColName

Check Constraint ALTER TABLE t ADD CONSTRAINT CK_Name CHECK (condition)

INNER JOIN FROM A INNER JOIN B ON [Link] = [Link] -- only matching rows

LEFT JOIN FROM A LEFT JOIN B ON [Link] = [Link] -- all from A + NULLs

SELF JOIN FROM tbl E LEFT JOIN tbl M ON [Link] = [Link]

Non-matching LEFT LEFT JOIN ... WHERE [Link] IS NULL -- orphan rows only

Create SP CREATE PROCEDURE spName @P type AS BEGIN...END

Execute SP EXEC spName @Param = value

SP Output Param @Count INT OUTPUT -- pass with OUTPUT keyword when calling

Scalar UDF CREATE FUNCTION dbo.fn_Name(...) RETURNS type AS BEGIN...RETURN v


END

Inline TVF CREATE FUNCTION fn_Name() RETURNS TABLE AS RETURN (SELECT...)

Clustered Index CREATE CLUSTERED INDEX IX ON tbl(col) -- only 1 per table

Covering Index CREATE INDEX IX ON tbl(col) INCLUDE (col2, col3) -- eliminates Key Lookup

Indexed View CREATE VIEW WITH SCHEMABINDING then CREATE UNIQUE CLUSTERED
INDEX

AFTER Trigger CREATE TRIGGER tr ON tbl FOR INSERT AS BEGIN...END

INSERTED/DELETED INSERTED = new rows after INSERT/UPDATE; DELETED = old rows

INSTEAD OF Trigger CREATE TRIGGER tr ON tbl INSTEAD OF INSERT AS BEGIN...END

Basic CTE WITH CTE AS (SELECT...) SELECT * FROM CTE

Recursive CTE Anchor UNION ALL Recursive; use OPTION (MAXRECURSION n)

PIVOT PIVOT(AGG(col) FOR col IN ([v1],[v2]))

CROSS APPLY FROM tbl CROSS APPLY tvf_Function([Link])

SELECT INTO SELECT * INTO newTable FROM source -- indexes not copied
Transaction BEGIN TRAN...COMMIT TRAN / ROLLBACK TRAN

TRY/CATCH BEGIN TRY...END TRY BEGIN CATCH ROLLBACK; ;THROW END CATCH

Isolation Level SET TRANSACTION ISOLATION LEVEL READ COMMITTED (default)

ROW_NUMBER ROW_NUMBER() OVER (PARTITION BY col ORDER BY col)

RANK vs DENSE_RANK RANK: gaps after ties (1,1,3); DENSE_RANK: no gaps (1,1,2)

LEAD / LAG LEAD/LAG(col, offset, default) OVER (ORDER BY col)

LAST_VALUE Needs: ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED


FOLLOWING

Running Total SUM(col) OVER (ORDER BY col ROWS UNBOUNDED PRECEDING)

Dynamic SQL (safe) sp_executesql @sql, @params, @v = value -- parameters prevent injection

Object Names in SQL QUOTENAME(@TableName) -- brackets and escapes embedded brackets

Local Temp Table CREATE TABLE #Name (...) -- session-scoped, supports indexes

Table Variable DECLARE @t TABLE (...) -- batch-scoped, no rollback, no stats

ROLLUP GROUP BY ROLLUP(c1,c2) -- hierarchical subtotals right-to-left

CUBE GROUP BY CUBE(c1,c2) -- all 2^n grouping combinations

Sequence CREATE SEQUENCE s; SELECT NEXT VALUE FOR s -- table-independent

GUID NEWID() random; NEWSEQUENTIALID() for clustered index PK columns

MERGE MERGE Target USING Source ON ... WHEN MATCHED THEN


UPDATE/DELETE; WHEN NOT MATCHED THEN INSERT;

1NF Atomic values; no repeating groups; primary key defined

2NF 1NF + no partial dependencies on composite primary key

3NF 2NF + no transitive dependencies (non-key depending on non-key)

SQL SERVER -- Complete Study Guide


149 Topics | 10 Parts | T-SQL Reference & Best Practices

You might also like