T-SQL
Complete T-SQL Guide with Detailed Explanations
1. T-SQL Fundamentals
Section titled “1. T-SQL Fundamentals”What is T-SQL?
Section titled “What is T-SQL?”T-SQL (Transact-SQL) is Microsoft’s proprietary extension to SQL used in SQL Server and Azure SQL Database. It adds programming constructs, local variables, and control-flow features to standard SQL.
Key Differences from PL/SQL:
Section titled “Key Differences from PL/SQL:”- Vendor: Microsoft SQL Server vs Oracle
- Syntax: Different syntax for similar concepts
- Error Handling: TRY…CATCH blocks vs EXCEPTION sections
- Variables: @ prefix for variables
- Procedural Extensions: Different built-in functions and system procedures
Basic T-SQL Batch Structure
Section titled “Basic T-SQL Batch Structure”-- T-SQL uses batches separated by GOUSE DatabaseName; -- Switch database contextGO
-- Variable declarationsDECLARE @variable_name data_type;
-- Executable statementsSELECT @variable_name = column_name FROM table_name;
-- Control flow statementsIF @variable_name > 100 BEGIN PRINT 'Value is greater than 100'; ENDGOSetting Up Your Environment
Section titled “Setting Up Your Environment”-- Switch to a specific databaseUSE AdventureWorks2019;GO
-- Check if database exists and create if neededIF NOT EXISTS(SELECT name FROM sys.databases WHERE name = 'TrainingDB')BEGIN CREATE DATABASE TrainingDB;ENDGO
USE TrainingDB;GO
-- Create a sample table for demonstrationsIF OBJECT_ID('Employees', 'U') IS NOT NULL DROP TABLE Employees;GO
CREATE TABLE Employees ( EmployeeID INT IDENTITY(1,1) PRIMARY KEY, FirstName NVARCHAR(50) NOT NULL, LastName NVARCHAR(50) NOT NULL, Email NVARCHAR(100), Salary DECIMAL(10,2), HireDate DATE DEFAULT GETDATE(), DepartmentID INT, IsActive BIT DEFAULT 1);GO
-- Insert sample dataINSERT INTO Employees (FirstName, LastName, Email, Salary, DepartmentID)VALUES ('John', 'Doe', 'john.doe@company.com', 50000.00, 1), ('Jane', 'Smith', 'jane.smith@company.com', 65000.00, 1), ('Bob', 'Johnson', 'bob.johnson@company.com', 45000.00, 2), ('Alice', 'Brown', 'alice.brown@company.com', 75000.00, 2), ('Charlie', 'Wilson', 'charlie.wilson@company.com', 55000.00, 3);GO2. Variables and Data Types
Section titled “2. Variables and Data Types”Understanding Variables in T-SQL
Section titled “Understanding Variables in T-SQL”Variables in T-SQL are prefixed with @ and must be declared before use. They are scoped to the batch, stored procedure, or function.
Scalar Data Types - Detailed Explanation
Section titled “Scalar Data Types - Detailed Explanation”-- Demonstrating T-SQL variables and data typesDECLARE -- Numeric types @EmployeeID INT = 100, @Salary DECIMAL(10,2) = 50000.50, @Percentage FLOAT = 0.75, @SmallNumber SMALLINT = 32000,
-- Character types @FirstName NVARCHAR(50) = N'John Doe', -- NVARCHAR for Unicode @Code CHAR(3) = 'USA', -- CHAR for fixed length @Description VARCHAR(MAX) = 'Long text description', -- VARCHAR(MAX) for large text
-- Date/Time types @HireDate DATE = GETDATE(), @BirthDate DATETIME = '1990-01-15', @Timestamp DATETIME2 = SYSDATETIME(), -- Higher precision @TimeOnly TIME = '14:30:00',
-- Binary types @BinaryData VARBINARY(MAX),
-- Other types @IsActive BIT = 1, -- Boolean equivalent (0 or 1) @UniqueID UNIQUEIDENTIFIER = NEWID(); -- GUID
-- Display variable valuesPRINT 'Employee: ' + @FirstName;PRINT 'Salary: ' + CAST(@Salary AS NVARCHAR(20));PRINT 'Hire Date: ' + CONVERT(NVARCHAR(20), @HireDate, 101);PRINT 'Is Active: ' + CAST(@IsActive AS NVARCHAR(1));PRINT 'GUID: ' + CAST(@UniqueID AS NVARCHAR(50));GOTable Variables and Temporary Tables
Section titled “Table Variables and Temporary Tables”-- TABLE VARIABLE (in-memory, scope limited to batch)DECLARE @EmployeeTable TABLE ( EmpID INT, FullName NVARCHAR(100), Salary DECIMAL(10,2), HireDate DATE);
-- Insert into table variableINSERT INTO @EmployeeTable (EmpID, FullName, Salary, HireDate)SELECT EmployeeID, FirstName + ' ' + LastName, Salary, HireDateFROM EmployeesWHERE Salary > 50000;
-- Query table variableSELECT * FROM @EmployeeTable;GO
-- TEMPORARY TABLES (stored in tempdb, broader scope)-- Local temporary table (prefix #, visible to current session)CREATE TABLE #TempEmployees ( EmpID INT, EmpName NVARCHAR(100), Department NVARCHAR(50));
-- Global temporary table (prefix ##, visible to all sessions)CREATE TABLE ##GlobalTemp ( ID INT, Data NVARCHAR(100));
-- Insert into temporary tableINSERT INTO #TempEmployees (EmpID, EmpName, Department)SELECT EmployeeID, FirstName + ' ' + LastName, 'Department ' + CAST(DepartmentID AS NVARCHAR(10))FROM Employees;
-- Query temporary tableSELECT * FROM #TempEmployees;GO
-- Temporary tables are automatically dropped when session ends-- Or you can explicitly drop themDROP TABLE #TempEmployees;DROP TABLE ##GlobalTemp;GOSystem Functions and Variable Assignment
Section titled “System Functions and Variable Assignment”-- Different ways to assign values to variablesDECLARE @EmpCount INT, @TotalSalary DECIMAL(10,2), @AvgSalary DECIMAL(10,2), @MaxSalary DECIMAL(10,2), @CurrentDate DATETIME = GETDATE();
-- Method 1: SET (single variable assignment)SET @EmpCount = (SELECT COUNT(*) FROM Employees);PRINT 'Employee count: ' + CAST(@EmpCount AS NVARCHAR(10));
-- Method 2: SELECT (multiple variable assignment in single statement)SELECT @TotalSalary = SUM(Salary), @AvgSalary = AVG(Salary), @MaxSalary = MAX(Salary)FROM Employees;
PRINT 'Total Salary: ' + CAST(@TotalSalary AS NVARCHAR(20));PRINT 'Average Salary: ' + CAST(@AvgSalary AS NVARCHAR(20));PRINT 'Max Salary: ' + CAST(@MaxSalary AS NVARCHAR(20));
-- Method 3: Using system functionsDECLARE @ServerName NVARCHAR(100) = @@SERVERNAME, @Version NVARCHAR(100) = @@VERSION, @RowCount INT = @@ROWCOUNT;
PRINT 'Server: ' + @ServerName;PRINT 'Rows affected: ' + CAST(@RowCount AS NVARCHAR(10));
-- Method 4: Using SCOPE_IDENTITY() for identity columnsINSERT INTO Employees (FirstName, LastName, Email, Salary, DepartmentID)VALUES ('New', 'Employee', 'new.employee@company.com', 48000.00, 1);
DECLARE @NewEmployeeID INT = SCOPE_IDENTITY();PRINT 'New Employee ID: ' + CAST(@NewEmployeeID AS NVARCHAR(10));GO3. Control Structures
Section titled “3. Control Structures”Conditional Statements - Making Decisions
Section titled “Conditional Statements - Making Decisions”IF…ELSE Statements:
- Purpose: Execute different code paths based on conditions
- Syntax: Must use BEGIN…END for multiple statements
- Use Case: Business rules, validation, workflow decisions
DECLARE @Salary DECIMAL(10,2) = 75000, @Grade NVARCHAR(10), @Bonus DECIMAL(10,2), @PerformanceRating INT = 4; -- Scale 1-5
PRINT '=== CONDITIONAL STATEMENTS DEMO ===';
-- SIMPLE IF STATEMENTIF @Salary > 50000 PRINT 'Salary is above average';
-- IF-ELSE STATEMENTIF @Salary > 100000BEGIN SET @Grade = 'A'; SET @Bonus = @Salary * 0.20;ENDELSEBEGIN SET @Grade = 'B'; SET @Bonus = @Salary * 0.10;END
PRINT 'Grade: ' + @Grade + ', Bonus: $' + CAST(@Bonus AS NVARCHAR(20));
-- IF-ELSE IF-ELSE STATEMENT (Multiple conditions)IF @PerformanceRating = 5BEGIN SET @Bonus = @Salary * 0.25; SET @Grade = 'Excellent';ENDELSE IF @PerformanceRating = 4BEGIN SET @Bonus = @Salary * 0.15; SET @Grade = 'Good';ENDELSE IF @PerformanceRating = 3BEGIN SET @Bonus = @Salary * 0.10; SET @Grade = 'Average';ENDELSEBEGIN SET @Bonus = 0; SET @Grade = 'Needs Improvement';END
PRINT 'Performance: ' + @Grade + ', Bonus: $' + CAST(@Bonus AS NVARCHAR(20));
-- CASE STATEMENT (Alternative to multiple IF-ELSE)DECLARE @PerformanceText NVARCHAR(50);
SET @PerformanceText = CASE @PerformanceRating WHEN 5 THEN 'Outstanding' WHEN 4 THEN 'Exceeds Expectations' WHEN 3 THEN 'Meets Expectations' ELSE 'Below Expectations' END;
PRINT 'Performance Text: ' + @PerformanceText;
-- SEARCHED CASE (more flexible)DECLARE @SalaryCategory NVARCHAR(20);
SET @SalaryCategory = CASE WHEN @Salary > 100000 THEN 'Executive' WHEN @Salary BETWEEN 75000 AND 100000 THEN 'Senior' WHEN @Salary BETWEEN 50000 AND 74999 THEN 'Mid-Level' ELSE 'Junior' END;
PRINT 'Salary Category: ' + @SalaryCategory;GOLoops - Repeating Execution
Section titled “Loops - Repeating Execution”WHILE Loop:
- Purpose: Execute code repeatedly while condition is true
- Use Case: Iterate until condition met, process datasets in chunks
PRINT '=== LOOPING CONSTRUCTS DEMO ===';
-- BASIC WHILE LOOPDECLARE @Counter INT = 1, @Total INT = 0;
PRINT '=== BASIC WHILE LOOP ===';WHILE @Counter <= 5BEGIN SET @Total = @Total + @Counter; PRINT 'Counter: ' + CAST(@Counter AS NVARCHAR(10)) + ', Total: ' + CAST(@Total AS NVARCHAR(10)); SET @Counter = @Counter + 1;END
PRINT 'Final Total: ' + CAST(@Total AS NVARCHAR(10));
-- WHILE LOOP with BREAK and CONTINUEPRINT '=== WHILE LOOP WITH BREAK/CONTINUE ===';DECLARE @i INT = 1;
WHILE @i <= 10BEGIN IF @i = 3 BEGIN SET @i = @i + 1; CONTINUE; -- Skip iteration when i=3 END
IF @i = 8 BREAK; -- Exit loop when i=8
PRINT 'Current value: ' + CAST(@i AS NVARCHAR(10)); SET @i = @i + 1;END
-- Practical example: Process employees in batchesPRINT '=== BATCH PROCESSING EXAMPLE ===';DECLARE @BatchSize INT = 2, @Offset INT = 0, @Processed INT = 0;
WHILE 1 = 1BEGIN -- Process employees in batches UPDATE TOP (@BatchSize) Employees SET Salary = Salary * 1.05 WHERE EmployeeID > @Offset AND EmployeeID <= @Offset + @BatchSize;
SET @Processed = @Processed + @@ROWCOUNT; SET @Offset = @Offset + @BatchSize;
IF @@ROWCOUNT = 0 BREAK;
PRINT 'Processed batch, total so far: ' + CAST(@Processed AS NVARCHAR(10));END
PRINT 'Total employees processed: ' + CAST(@Processed AS NVARCHAR(10));GODynamic SQL - Building Queries at Runtime
Section titled “Dynamic SQL - Building Queries at Runtime”PRINT '=== DYNAMIC SQL DEMO ===';
DECLARE @TableName NVARCHAR(100) = N'Employees', @ColumnName NVARCHAR(100) = N'Salary', @MinValue DECIMAL(10,2) = 50000, @SQLQuery NVARCHAR(MAX);
-- Build dynamic SQL querySET @SQLQuery = N'SELECT EmployeeID, FirstName, LastName, ' + @ColumnName + 'FROM ' + @TableName + 'WHERE ' + @ColumnName + ' >= @MinValueORDER BY ' + @ColumnName + ' DESC';
PRINT 'Dynamic Query:';PRINT @SQLQuery;
-- Execute dynamic SQL with parametersDECLARE @ParamDefinition NVARCHAR(500) = N'@MinValue DECIMAL(10,2)';
EXEC sp_executesql @SQLQuery, @ParamDefinition, @MinValue = @MinValue;GO4. Cursors - Row-by-Row Processing
Section titled “4. Cursors - Row-by-Row Processing”What are Cursors?
Section titled “What are Cursors?”Cursors in T-SQL allow row-by-row processing of result sets. They provide a way to retrieve and manipulate rows sequentially.
Why Use Cursors?
Section titled “Why Use Cursors?”- Row Processing: Handle complex logic for each row
- Sequential Access: Process data in specific order
- Complex Calculations: Perform calculations that require row context
- Data Migration: Move data between tables with transformations
Types of Cursors:
Section titled “Types of Cursors:”1. Basic Cursor Operations
Section titled “1. Basic Cursor Operations”PRINT '=== BASIC CURSOR OPERATIONS ===';
DECLARE @EmployeeID INT, @FirstName NVARCHAR(50), @LastName NVARCHAR(50), @Salary DECIMAL(10,2), @TotalSalary DECIMAL(10,2) = 0, @EmployeeCount INT = 0;
-- Step 1: DECLARE cursorDECLARE employee_cursor CURSOR FOR SELECT EmployeeID, FirstName, LastName, Salary FROM Employees WHERE DepartmentID = 1 ORDER BY Salary DESC;
-- Step 2: OPEN cursorOPEN employee_cursor;
-- Step 3: FETCH first rowFETCH NEXT FROM employee_cursor INTO @EmployeeID, @FirstName, @LastName, @Salary;
-- Step 4: Process rows in loopWHILE @@FETCH_STATUS = 0BEGIN SET @EmployeeCount = @EmployeeCount + 1; SET @TotalSalary = @TotalSalary + @Salary;
PRINT 'Employee: ' + @FirstName + ' ' + @LastName + ' - Salary: $' + CAST(@Salary AS NVARCHAR(20));
-- Fetch next row FETCH NEXT FROM employee_cursor INTO @EmployeeID, @FirstName, @LastName, @Salary;END
-- Step 5: CLOSE cursorCLOSE employee_cursor;
-- Step 6: DEALLOCATE cursorDEALLOCATE employee_cursor;
PRINT '=== CURSOR SUMMARY ===';PRINT 'Total Employees: ' + CAST(@EmployeeCount AS NVARCHAR(10));PRINT 'Total Salary: $' + CAST(@TotalSalary AS NVARCHAR(20));PRINT 'Average Salary: $' + CAST(@TotalSalary / NULLIF(@EmployeeCount, 0) AS NVARCHAR(20));GO2. Cursor Types and Options
Section titled “2. Cursor Types and Options”PRINT '=== CURSOR TYPES AND OPTIONS ===';
-- Different cursor types with their characteristicsDECLARE @EmpID INT, @EmpName NVARCHAR(100), @EmpSalary DECIMAL(10,2);
-- FORWARD_ONLY cursor (default) - fastest, only move forwardPRINT '=== FORWARD_ONLY CURSOR ===';DECLARE fast_cursor CURSOR FORWARD_ONLY FOR SELECT EmployeeID, FirstName + ' ' + LastName, Salary FROM Employees;
OPEN fast_cursor;FETCH NEXT FROM fast_cursor INTO @EmpID, @EmpName, @EmpSalary;
WHILE @@FETCH_STATUS = 0BEGIN PRINT 'Fast - ID: ' + CAST(@EmpID AS NVARCHAR(10)) + ', Name: ' + @EmpName; FETCH NEXT FROM fast_cursor INTO @EmpID, @EmpName, @EmpSalary;END
CLOSE fast_cursor;DEALLOCATE fast_cursor;
-- SCROLL cursor - can move in any directionPRINT '=== SCROLL CURSOR ===';DECLARE scroll_cursor CURSOR SCROLL FOR SELECT EmployeeID, FirstName + ' ' + LastName, Salary FROM Employees ORDER BY Salary DESC;
OPEN scroll_cursor;
-- Fetch first rowFETCH FIRST FROM scroll_cursor INTO @EmpID, @EmpName, @EmpSalary;PRINT 'First: ' + @EmpName + ' - $' + CAST(@EmpSalary AS NVARCHAR(20));
-- Fetch last rowFETCH LAST FROM scroll_cursor INTO @EmpID, @EmpName, @EmpSalary;PRINT 'Last: ' + @EmpName + ' - $' + CAST(@EmpSalary AS NVARCHAR(20));
-- Fetch previous rowFETCH PRIOR FROM scroll_cursor INTO @EmpID, @EmpName, @EmpSalary;PRINT 'Previous: ' + @EmpName + ' - $' + CAST(@EmpSalary AS NVARCHAR(20));
-- Fetch absolute positionFETCH ABSOLUTE 2 FROM scroll_cursor INTO @EmpID, @EmpName, @EmpSalary;PRINT 'Second: ' + @EmpName + ' - $' + CAST(@EmpSalary AS NVARCHAR(20));
CLOSE scroll_cursor;DEALLOCATE scroll_cursor;
-- STATIC cursor - snapshot of data at cursor open timePRINT '=== STATIC CURSOR ===';DECLARE static_cursor CURSOR STATIC FOR SELECT EmployeeID, FirstName, Salary FROM Employees;
OPEN static_cursor;
-- Changes to data won't affect static cursorUPDATE Employees SET FirstName = 'Updated' WHERE EmployeeID = 1;
FETCH NEXT FROM static_cursor INTO @EmpID, @EmpName, @EmpSalary;WHILE @@FETCH_STATUS = 0BEGIN PRINT 'Static - ID: ' + CAST(@EmpID AS NVARCHAR(10)) + ', Name: ' + @EmpName; FETCH NEXT FROM static_cursor INTO @EmpID, @EmpName, @EmpSalary;END
CLOSE static_cursor;DEALLOCATE static_cursor;GO3. Cursor with Updates and Error Handling
Section titled “3. Cursor with Updates and Error Handling”PRINT '=== CURSOR WITH UPDATES AND ERROR HANDLING ===';
-- Create a log table for cursor operationsIF OBJECT_ID('CursorLog', 'U') IS NOT NULL DROP TABLE CursorLog;
CREATE TABLE CursorLog ( LogID INT IDENTITY PRIMARY KEY, EmployeeID INT, Operation NVARCHAR(50), OldSalary DECIMAL(10,2), NewSalary DECIMAL(10,2), LogDate DATETIME DEFAULT GETDATE());
DECLARE @EmpID INT, @CurrentSalary DECIMAL(10,2), @NewSalary DECIMAL(10,2), @ErrorCount INT = 0;
BEGIN TRY -- Declare cursor for update DECLARE update_cursor CURSOR FOR SELECT EmployeeID, Salary FROM Employees WHERE DepartmentID = 1 FOR UPDATE OF Salary; -- Specify which column can be updated
OPEN update_cursor; FETCH NEXT FROM update_cursor INTO @EmpID, @CurrentSalary;
WHILE @@FETCH_STATUS = 0 BEGIN BEGIN TRY -- Calculate new salary (10% raise) SET @NewSalary = @CurrentSalary * 1.10;
-- Update employee salary UPDATE Employees SET Salary = @NewSalary WHERE CURRENT OF update_cursor;
-- Log the operation INSERT INTO CursorLog (EmployeeID, Operation, OldSalary, NewSalary) VALUES (@EmpID, 'SALARY_UPDATE', @CurrentSalary, @NewSalary);
PRINT 'Updated Employee ' + CAST(@EmpID AS NVARCHAR(10)) + ': $' + CAST(@CurrentSalary AS NVARCHAR(20)) + ' -> $' + CAST(@NewSalary AS NVARCHAR(20));
END TRY BEGIN CATCH SET @ErrorCount = @ErrorCount + 1; PRINT 'Error updating employee ' + CAST(@EmpID AS NVARCHAR(10)) + ': ' + ERROR_MESSAGE();
-- Log error INSERT INTO CursorLog (EmployeeID, Operation, OldSalary, NewSalary) VALUES (@EmpID, 'UPDATE_ERROR', @CurrentSalary, @NewSalary); END CATCH
FETCH NEXT FROM update_cursor INTO @EmpID, @CurrentSalary; END
CLOSE update_cursor; DEALLOCATE update_cursor;
PRINT 'Cursor update completed. Errors: ' + CAST(@ErrorCount AS NVARCHAR(10));
END TRYBEGIN CATCH PRINT 'Critical error in cursor processing: ' + ERROR_MESSAGE();
IF CURSOR_STATUS('local', 'update_cursor') >= 0 BEGIN CLOSE update_cursor; DEALLOCATE update_cursor; ENDEND CATCH
-- Display log resultsSELECT * FROM CursorLog;GOWhen to Use Cursors vs Set-Based Operations
Section titled “When to Use Cursors vs Set-Based Operations”PRINT '=== CURSOR VS SET-BASED OPERATIONS ===';
-- Example where CURSOR might be necessaryPRINT 'SCENARIO: Complex row-by-row calculation';
-- Using CURSOR (when necessary)DECLARE @EmployeeID INT, @Salary DECIMAL(10,2), @PreviousSalary DECIMAL(10,2) = 0, @SalaryIncrease DECIMAL(10,2);
PRINT '=== CURSOR APPROACH (Complex Logic) ===';DECLARE complex_cursor CURSOR FOR SELECT EmployeeID, Salary FROM Employees ORDER BY Salary;
OPEN complex_cursor;FETCH NEXT FROM complex_cursor INTO @EmployeeID, @Salary;
WHILE @@FETCH_STATUS = 0BEGIN -- Complex calculation that depends on previous row IF @PreviousSalary > 0 SET @SalaryIncrease = @Salary - @PreviousSalary; ELSE SET @SalaryIncrease = 0;
PRINT 'Employee ' + CAST(@EmployeeID AS NVARCHAR(10)) + ': Salary $' + CAST(@Salary AS NVARCHAR(20)) + ', Increase from previous: $' + CAST(@SalaryIncrease AS NVARCHAR(20));
SET @PreviousSalary = @Salary; FETCH NEXT FROM complex_cursor INTO @EmployeeID, @Salary;END
CLOSE complex_cursor;DEALLOCATE complex_cursor;
-- Example where SET-BASED is betterPRINT '=== SET-BASED APPROACH (Better Performance) ===';
-- Same operation using set-based approach (usually faster)UPDATE EmployeesSET Salary = Salary * 1.10WHERE DepartmentID = 1;
PRINT 'Set-based update completed for all employees in department 1';PRINT 'Rows affected: ' + CAST(@@ROWCOUNT AS NVARCHAR(10));
-- Demonstrate why set-based is usually betterPRINT '=== PERFORMANCE COMPARISON ===';PRINT 'CURSORS:';PRINT ' - Process rows one by one';PRINT ' - Higher overhead (lock, fetch, process)';PRINT ' - Slower for large datasets';PRINT ' - Use only when necessary for complex row-specific logic';PRINT '';PRINT 'SET-BASED:';PRINT ' - Process all rows at once';PRINT ' - Lower overhead';PRINT ' - Much faster for large datasets';PRINT ' - Preferred approach in most cases';GO5. Error Handling
Section titled “5. Error Handling”What is Error Handling in T-SQL?
Section titled “What is Error Handling in T-SQL?”Error handling in T-SQL is primarily done using TRY…CATCH blocks, which provide structured exception handling similar to modern programming languages.
Why Handle Errors?
Section titled “Why Handle Errors?”- Prevent Data Corruption: Ensure data integrity
- Improve User Experience: Provide meaningful error messages
- Maintain Application Stability: Graceful error recovery
- Audit and Logging: Track errors for debugging
TRY…CATCH Blocks
Section titled “TRY…CATCH Blocks”PRINT '=== BASIC TRY...CATCH DEMO ===';
-- Example 1: Handling division by zeroBEGIN TRY DECLARE @Result DECIMAL(10,2); SET @Result = 100 / 0; -- This will cause an error PRINT 'Result: ' + CAST(@Result AS NVARCHAR(20));END TRYBEGIN CATCH PRINT 'Error occurred: ' + ERROR_MESSAGE(); PRINT 'Error Number: ' + CAST(ERROR_NUMBER() AS NVARCHAR(10)); PRINT 'Error Severity: ' + CAST(ERROR_SEVERITY() AS NVARCHAR(10)); PRINT 'Error State: ' + CAST(ERROR_STATE() AS NVARCHAR(10));END CATCHGO
-- Example 2: Handling database errorsBEGIN TRY -- This will fail if table doesn't exist SELECT * FROM NonExistentTable;END TRYBEGIN CATCH PRINT 'Database error: ' + ERROR_MESSAGE(); PRINT 'Procedure: ' + ISNULL(ERROR_PROCEDURE(), 'Not in procedure'); PRINT 'Line: ' + CAST(ERROR_LINE() AS NVARCHAR(10));END CATCHGOAdvanced Error Handling Techniques
Section titled “Advanced Error Handling Techniques”PRINT '=== ADVANCED ERROR HANDLING ===';
-- Create error log tableIF OBJECT_ID('ErrorLog', 'U') IS NOT NULL DROP TABLE ErrorLog;
CREATE TABLE ErrorLog ( ErrorID INT IDENTITY PRIMARY KEY, ErrorNumber INT, ErrorSeverity INT, ErrorState INT, ErrorProcedure NVARCHAR(200), ErrorLine INT, ErrorMessage NVARCHAR(4000), ErrorDateTime DATETIME DEFAULT GETDATE(), UserName NVARCHAR(100) DEFAULT SYSTEM_USER);
-- Comprehensive error handling procedureCREATE OR REPLACE PROCEDURE LogErrorASBEGIN INSERT INTO ErrorLog ( ErrorNumber, ErrorSeverity, ErrorState, ErrorProcedure, ErrorLine, ErrorMessage ) VALUES ( ERROR_NUMBER(), ERROR_SEVERITY(), ERROR_STATE(), ERROR_PROCEDURE(), ERROR_LINE(), ERROR_MESSAGE() );
-- Return the error log ID SELECT SCOPE_IDENTITY() AS ErrorLogID;END;GO
-- Demonstration of comprehensive error handlingCREATE OR REPLACE PROCEDURE ProcessEmployeeSalary @EmployeeID INT, @SalaryIncrease DECIMAL(5,2)ASBEGIN SET NOCOUNT ON;
DECLARE @OldSalary DECIMAL(10,2), @NewSalary DECIMAL(10,2), @ErrorLogID INT;
BEGIN TRY PRINT 'Starting salary processing for employee: ' + CAST(@EmployeeID AS NVARCHAR(10));
-- Validate input parameters IF @EmployeeID IS NULL OR @SalaryIncrease IS NULL BEGIN RAISERROR('EmployeeID and SalaryIncrease cannot be NULL', 16, 1); END
IF @SalaryIncrease <= 0 OR @SalaryIncrease > 1.0 -- 100% max increase BEGIN RAISERROR('Salary increase must be between 0 and 1.0 (0%% to 100%%)', 16, 1); END
-- Get current salary SELECT @OldSalary = Salary FROM Employees WHERE EmployeeID = @EmployeeID;
IF @OldSalary IS NULL BEGIN RAISERROR('Employee not found with ID: %d', 16, 1, @EmployeeID); END
-- Calculate new salary SET @NewSalary = @OldSalary * (1 + @SalaryIncrease);
-- Business rule: Salary cannot exceed $200,000 IF @NewSalary > 200000 BEGIN RAISERROR('New salary $%0.2f exceeds maximum allowed ($200,000)', 16, 1, @NewSalary); END
-- Update salary UPDATE Employees SET Salary = @NewSalary WHERE EmployeeID = @EmployeeID;
PRINT 'Salary updated successfully: $' + CAST(@OldSalary AS NVARCHAR(20)) + ' -> $' + CAST(@NewSalary AS NVARCHAR(20));
END TRY BEGIN CATCH PRINT 'Error in ProcessEmployeeSalary: ' + ERROR_MESSAGE();
-- Log the error EXEC LogError;
-- Re-raise the error if it's a business logic error IF ERROR_NUMBER() >= 50000 THROW; -- Re-raise custom errors
-- For system errors, you might want to handle differently PRINT 'System error handled gracefully'; END CATCHEND;GO
-- Test the error handlingPRINT '=== TESTING ERROR HANDLING ===';
-- Test 1: Valid caseEXEC ProcessEmployeeSalary @EmployeeID = 1, @SalaryIncrease = 0.10;
-- Test 2: Invalid employeeEXEC ProcessEmployeeSalary @EmployeeID = 999, @SalaryIncrease = 0.10;
-- Test 3: Invalid salary increaseEXEC ProcessEmployeeSalary @EmployeeID = 1, @SalaryIncrease = 1.50;
-- Test 4: NULL parametersEXEC ProcessEmployeeSalary @EmployeeID = NULL, @SalaryIncrease = 0.10;
-- Check error logSELECT * FROM ErrorLog;GORAISERROR and THROW Statements
Section titled “RAISERROR and THROW Statements”PRINT '=== RAISERROR vs THROW ===';
-- RAISERROR (older method, more control)BEGIN TRY RAISERROR('This is a custom error message with RAISERROR', 16, 1);END TRYBEGIN CATCH PRINT 'Caught RAISERROR: ' + ERROR_MESSAGE();END CATCH
-- THROW (newer method, simpler syntax)BEGIN TRY THROW 50000, 'This is a custom error message with THROW', 1;END TRYBEGIN CATCH PRINT 'Caught THROW: ' + ERROR_MESSAGE(); PRINT 'Error Number: ' + CAST(ERROR_NUMBER() AS NVARCHAR(10));END CATCH
-- Using THROW to re-throw caught exceptionsBEGIN TRY BEGIN TRY SELECT 1/0; -- Cause division by zero END TRY BEGIN CATCH PRINT 'Inner catch: ' + ERROR_MESSAGE(); THROW; -- Re-throw the original error END CATCHEND TRYBEGIN CATCH PRINT 'Outer catch: ' + ERROR_MESSAGE();END CATCHGOTransaction Management with Error Handling
Section titled “Transaction Management with Error Handling”PRINT '=== TRANSACTIONS WITH ERROR HANDLING ===';
-- Create audit tableIF OBJECT_ID('SalaryAudit', 'U') IS NOT NULL DROP TABLE SalaryAudit;
CREATE TABLE SalaryAudit ( AuditID INT IDENTITY PRIMARY KEY, EmployeeID INT, OldSalary DECIMAL(10,2), NewSalary DECIMAL(10,2), ChangeDate DATETIME DEFAULT GETDATE(), ChangedBy NVARCHAR(100) DEFAULT SYSTEM_USER);
-- Procedure with transaction and error handlingCREATE OR REPLACE PROCEDURE UpdateSalaryWithTransaction @EmployeeID INT, @NewSalary DECIMAL(10,2)ASBEGIN SET NOCOUNT ON; SET XACT_ABORT ON; -- Automatically rollback on error
DECLARE @OldSalary DECIMAL(10,2);
BEGIN TRY BEGIN TRANSACTION;
-- Get current salary SELECT @OldSalary = Salary FROM Employees WHERE EmployeeID = @EmployeeID;
IF @OldSalary IS NULL BEGIN RAISERROR('Employee not found: %d', 16, 1, @EmployeeID); END
-- Validate new salary IF @NewSalary <= 0 BEGIN RAISERROR('Salary must be positive: %0.2f', 16, 1, @NewSalary); END
-- Update salary UPDATE Employees SET Salary = @NewSalary WHERE EmployeeID = @EmployeeID;
-- Log to audit table INSERT INTO SalaryAudit (EmployeeID, OldSalary, NewSalary) VALUES (@EmployeeID, @OldSalary, @NewSalary);
-- Commit transaction COMMIT TRANSACTION;
PRINT 'Salary updated successfully for employee ' + CAST(@EmployeeID AS NVARCHAR(10)); PRINT 'Old: $' + CAST(@OldSalary AS NVARCHAR(20)) + ', New: $' + CAST(@NewSalary AS NVARCHAR(20));
END TRY BEGIN CATCH -- Rollback transaction if it's still active IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
-- Log error DECLARE @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE(); DECLARE @ErrorSeverity INT = ERROR_SEVERITY(); DECLARE @ErrorState INT = ERROR_STATE();
PRINT 'Transaction rolled back due to error: ' + @ErrorMessage;
-- Re-raise error THROW; END CATCHEND;GO
-- Test transaction handlingPRINT '=== TESTING TRANSACTION HANDLING ===';
-- Test 1: Successful transactionEXEC UpdateSalaryWithTransaction @EmployeeID = 1, @NewSalary = 60000.00;
-- Test 2: Failed transaction (invalid salary)BEGIN TRY EXEC UpdateSalaryWithTransaction @EmployeeID = 1, @NewSalary = -1000.00;END TRYBEGIN CATCH PRINT 'Expected error: ' + ERROR_MESSAGE();END CATCH
-- Verify data integrityPRINT '=== VERIFYING DATA INTEGRITY ===';SELECT e.EmployeeID, e.FirstName, e.Salary as CurrentSalary, sa.OldSalary, sa.NewSalary as PreviousNewSalary, sa.ChangeDateFROM Employees eLEFT JOIN SalaryAudit sa ON e.EmployeeID = sa.EmployeeIDWHERE e.EmployeeID = 1;GO6. Stored Procedures and Functions
Section titled “6. Stored Procedures and Functions”Stored Procedures - Reusable Code Blocks
Section titled “Stored Procedures - Reusable Code Blocks”-- Create stored procedureCREATE PROCEDURE GetEmployeeDetails @EmployeeID INT = NULL, @DepartmentID INT = NULLASBEGIN SET NOCOUNT ON;
SELECT EmployeeID, FirstName + ' ' + LastName AS FullName, Salary, HireDate, DepartmentID FROM Employees WHERE (@EmployeeID IS NULL OR EmployeeID = @EmployeeID) AND (@DepartmentID IS NULL OR DepartmentID = @DepartmentID) ORDER BY Salary DESC;END;GO
-- Execute procedureEXEC GetEmployeeDetails @DepartmentID = 1;EXEC GetEmployeeDetails @EmployeeID = 1;Procedures with Output Parameters
Section titled “Procedures with Output Parameters”CREATE PROCEDURE GetEmployeeStats @DepartmentID INT, @EmployeeCount INT OUTPUT, @TotalSalary DECIMAL(10,2) OUTPUT, @AvgSalary DECIMAL(10,2) OUTPUTASBEGIN SELECT @EmployeeCount = COUNT(*), @TotalSalary = SUM(Salary), @AvgSalary = AVG(Salary) FROM Employees WHERE DepartmentID = @DepartmentID;END;GO
-- Use output parametersDECLARE @Count INT, @Total DECIMAL(10,2), @Avg DECIMAL(10,2);
EXEC GetEmployeeStats @DepartmentID = 1, @EmployeeCount = @Count OUTPUT, @TotalSalary = @Total OUTPUT, @AvgSalary = @Avg OUTPUT;
SELECT @Count AS EmployeeCount, @Total AS TotalSalary, @Avg AS AverageSalary;Functions - Return Single Value or Table
Section titled “Functions - Return Single Value or Table”-- Scalar function (returns single value)CREATE FUNCTION CalculateBonus( @Salary DECIMAL(10,2), @PerformanceRating INT)RETURNS DECIMAL(10,2)ASBEGIN DECLARE @Bonus DECIMAL(10,2);
SET @Bonus = @Salary * CASE @PerformanceRating WHEN 5 THEN 0.20 WHEN 4 THEN 0.15 WHEN 3 THEN 0.10 ELSE 0.05 END;
RETURN @Bonus;END;GO
-- Use scalar functionSELECT FirstName, Salary, dbo.CalculateBonus(Salary, 4) AS BonusFROM Employees;Table-Valued Functions
Section titled “Table-Valued Functions”-- Inline table-valued functionCREATE FUNCTION GetEmployeesBySalaryRange( @MinSalary DECIMAL(10,2), @MaxSalary DECIMAL(10,2))RETURNS TABLEASRETURN SELECT EmployeeID, FirstName + ' ' + LastName AS FullName, Salary FROM Employees WHERE Salary BETWEEN @MinSalary AND @MaxSalary;GO
-- Use table-valued functionSELECT * FROM dbo.GetEmployeesBySalaryRange(40000, 70000);7. Triggers
Section titled “7. Triggers”DML Triggers - Automatic Actions
Section titled “DML Triggers - Automatic Actions”-- Create audit tableCREATE TABLE EmployeeAudit ( AuditID INT IDENTITY PRIMARY KEY, EmployeeID INT, ActionType VARCHAR(10), OldSalary DECIMAL(10,2), NewSalary DECIMAL(10,2), ChangeDate DATETIME DEFAULT GETDATE());
-- AFTER trigger for auditingCREATE TRIGGER trg_EmployeeSalaryAuditON EmployeesAFTER UPDATEASBEGIN SET NOCOUNT ON;
IF UPDATE(Salary) BEGIN INSERT INTO EmployeeAudit (EmployeeID, ActionType, OldSalary, NewSalary) SELECT d.EmployeeID, 'UPDATE', d.Salary, i.Salary FROM deleted d INNER JOIN inserted i ON d.EmployeeID = i.EmployeeID WHERE d.Salary <> i.Salary; ENDEND;GO
-- Test triggerUPDATE Employees SET Salary = Salary * 1.1 WHERE EmployeeID = 1;SELECT * FROM EmployeeAudit;INSTEAD OF Triggers
Section titled “INSTEAD OF Triggers”-- INSTEAD OF trigger for complex validationCREATE TRIGGER trg_PreventSalaryDecreaseON EmployeesINSTEAD OF UPDATEASBEGIN SET NOCOUNT ON;
IF EXISTS( SELECT 1 FROM inserted i JOIN deleted d ON i.EmployeeID = d.EmployeeID WHERE i.Salary < d.Salary ) BEGIN RAISERROR('Salary cannot be decreased', 16, 1); RETURN; END
-- Perform the actual update UPDATE e SET FirstName = i.FirstName, LastName = i.LastName, Salary = i.Salary, DepartmentID = i.DepartmentID FROM Employees e INNER JOIN inserted i ON e.EmployeeID = i.EmployeeID;END;GO8. Advanced Features
Section titled “8. Advanced Features”Common Table Expressions (CTEs)
Section titled “Common Table Expressions (CTEs)”-- Recursive CTE for hierarchyWITH EmployeeCTE AS ( -- Anchor member SELECT EmployeeID, FirstName, LastName, ManagerID, 0 AS Level FROM Employees WHERE ManagerID IS NULL
UNION ALL
-- Recursive member SELECT e.EmployeeID, e.FirstName, e.LastName, e.ManagerID, Level + 1 FROM Employees e INNER JOIN EmployeeCTE cte ON e.ManagerID = cte.EmployeeID)SELECT * FROM EmployeeCTE ORDER BY Level, EmployeeID;Window Functions
Section titled “Window Functions”-- Advanced analytics with window functionsSELECT EmployeeID, FirstName + ' ' + LastName AS FullName, Salary, DepartmentID, -- Ranking ROW_NUMBER() OVER (PARTITION BY DepartmentID ORDER BY Salary DESC) AS DeptRank, -- Running total SUM(Salary) OVER (PARTITION BY DepartmentID ORDER BY Salary DESC) AS RunningTotal, -- Department averages AVG(Salary) OVER (PARTITION BY DepartmentID) AS DeptAvgSalary, -- Compare to department average Salary - AVG(Salary) OVER (PARTITION BY DepartmentID) AS DiffFromAvgFROM EmployeesORDER BY DepartmentID, Salary DESC;MERGE Statement - Upsert Operations
Section titled “MERGE Statement - Upsert Operations”-- MERGE for insert/update operationsMERGE Employees AS targetUSING (VALUES (1, 'John', 'Doe', 55000, 1), (6, 'New', 'Employee', 48000, 2)) AS source (EmployeeID, FirstName, LastName, Salary, DepartmentID)ON target.EmployeeID = source.EmployeeID
-- Update if existsWHEN MATCHED THEN UPDATE SET Salary = source.Salary, FirstName = source.FirstName
-- Insert if doesn't existWHEN NOT MATCHED THEN INSERT (FirstName, LastName, Salary, DepartmentID) VALUES (source.FirstName, source.LastName, source.Salary, source.DepartmentID);Dynamic SQL
Section titled “Dynamic SQL”-- Dynamic SQL for flexible queriesCREATE PROCEDURE DynamicEmployeeSearch @ColumnName NVARCHAR(50), @SearchValue NVARCHAR(100)ASBEGIN DECLARE @SQL NVARCHAR(MAX);
SET @SQL = ' SELECT EmployeeID, FirstName, LastName, Salary, DepartmentID FROM Employees WHERE ' + @ColumnName + ' = @Value';
EXEC sp_executesql @SQL, N'@Value NVARCHAR(100)', @SearchValue;END;GO
EXEC DynamicEmployeeSearch 'DepartmentID', '1';Error Handling with THROW
Section titled “Error Handling with THROW”-- Modern error handlingCREATE PROCEDURE SafeDataUpdate @EmployeeID INT, @NewSalary DECIMAL(10,2)ASBEGIN BEGIN TRY BEGIN TRANSACTION;
-- Validate input IF @NewSalary <= 0 THROW 50001, 'Salary must be positive', 1;
-- Check employee exists IF NOT EXISTS(SELECT 1 FROM Employees WHERE EmployeeID = @EmployeeID) THROW 50002, 'Employee not found', 1;
-- Perform update UPDATE Employees SET Salary = @NewSalary WHERE EmployeeID = @EmployeeID;
COMMIT TRANSACTION; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
THROW; -- Re-throw the error END CATCHEND;GOTemporal Tables (SQL Server 2016+)
Section titled “Temporal Tables (SQL Server 2016+)”-- System-versioned temporal tablesCREATE TABLE EmployeeHistory ( EmployeeID INT PRIMARY KEY, FirstName NVARCHAR(50), LastName NVARCHAR(50), Salary DECIMAL(10,2), ValidFrom DATETIME2 GENERATED ALWAYS AS ROW START, ValidTo DATETIME2 GENERATED ALWAYS AS ROW END, PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo))WITH (SYSTEM_VERSIONING = ON);Key T-SQL Best Practices
Section titled “Key T-SQL Best Practices”Performance Tips
Section titled “Performance Tips”-- 1. Use SET NOCOUNT ON-- 2. Avoid cursors when possible-- 3. Use proper indexing-- 4. Use EXISTS instead of COUNT(*)-- 5. Parameterize queries to avoid SQL injection
-- Good: Uses EXISTSIF EXISTS(SELECT 1 FROM Employees WHERE DepartmentID = 1) PRINT 'Department has employees';
-- Bad: Uses COUNTIF (SELECT COUNT(*) FROM Employees WHERE DepartmentID = 1) > 0 PRINT 'Department has employees';Security Best Practices
Section titled “Security Best Practices”-- Use parameterized queriesCREATE PROCEDURE GetEmployee @EmployeeID INTASBEGIN SET NOCOUNT ON; SELECT * FROM Employees WHERE EmployeeID = @EmployeeID;END;
-- Grant minimal permissionsGRANT EXECUTE ON GetEmployee TO UserRole;This covers the essential T-SQL concepts. The key differences from PL/SQL are:
- Syntax: @ prefix for variables, different control structures
- Error Handling: TRY…CATCH instead of EXCEPTION blocks
- Procedural Code: Batches separated by GO
- Functions: Different types (scalar, table-valued)
- Temporal Features: Built-in system versioning