Transact-SQL
Transact-SQL (T-SQL) is Microsoft's proprietary extension to the Structured Query Language (SQL), serving as the primary programming language for the SQL Server Database Engine and related products such as Azure SQL Database.[1] It builds on standard SQL by incorporating procedural elements, including variables for storing values, conditional execution statements, parameter passing, and mechanisms for controlling program flow.[2] This enables developers to write complex scripts, stored procedures, functions, and triggers for querying, manipulating, and managing data in relational databases.[3]
T-SQL supports core SQL categories, such as Data Definition Language (DDL) statements for creating, altering, or dropping database structures like tables and indexes; Data Manipulation Language (DML) statements for selecting, inserting, updating, and deleting data; and transaction control statements to ensure atomicity, consistency, isolation, and durability (ACID) properties in database operations.[4] [5] Key features include a wide range of built-in data types for handling integers, characters, dates, binary data, and more; scalar and table-valued functions for computations; and error-handling constructs like TRY-CATCH blocks to manage exceptions during execution.[6]
As the foundational language for SQL Server since its inception, T-SQL continues to evolve with modern enhancements, such as vector data types and functions for AI workloads, regular expression support for pattern matching, and native JSON data type handling (as of SQL Server 2025).[7] While largely compatible with ANSI SQL standards, T-SQL includes Microsoft-specific extensions that distinguish it from other SQL dialects, making it essential for enterprise database administration, business intelligence, and application development on the Microsoft ecosystem.[8]
Overview and History
Definition and Purpose
Transact-SQL (T-SQL) is Microsoft's proprietary version of the Structured Query Language (SQL).[9] It serves as the primary query language for Microsoft SQL Server and related products such as Azure SQL Database[10] and Azure Synapse Analytics.[11] As an extension to standard SQL, T-SQL integrates declarative SQL commands for data querying and manipulation with procedural programming elements, including variables, conditional logic, looping constructs, and error-handling mechanisms, enabling developers to write more sophisticated database scripts and programs.[1]
The primary purpose of T-SQL is to facilitate communication between applications, tools, and the SQL Server database engine through a unified set of commands that support both simple ad-hoc queries and complex, multi-step operations.[1] It powers key database features such as stored procedures for encapsulating reusable logic, triggers for enforcing data integrity rules automatically upon events like inserts or updates, user-defined functions for custom computations, and batch processing for executing sequences of statements as a single unit of work.[1] This combination allows T-SQL to handle advanced data processing directly within the database environment, reducing the need for external application code and improving performance in enterprise settings.[1]
What distinguishes T-SQL from standard SQL is its support for imperative programming paradigms, which extend beyond declarative queries to include flow control and state management, all tightly integrated with SQL Server's ecosystem for scalable data management and transaction processing.[1] Common use cases include automating routine database administration tasks, such as data backups or index maintenance; developing modular code components like stored procedures to standardize business operations; and embedding application logic at the database tier to ensure consistency and security in handling sensitive data. By enabling these capabilities, T-SQL supports efficient enterprise database programming while maintaining compatibility with core SQL standards where applicable.[9]
Development and Evolution
Transact-SQL originated in the late 1980s through a partnership between Microsoft and Sybase to develop a relational database management system for the OS/2 operating system. In 1989, this collaboration resulted in the release of SQL Server 1.0, which utilized Transact-SQL as its query language, extending standard SQL with procedural programming capabilities tailored for enterprise data processing. Microsoft acquired the rights to further develop and adapt the technology independently after the partnership ended in 1994, porting it to Windows platforms and establishing T-SQL as a core component of SQL Server.[12]
Key milestones in T-SQL's evolution reflect Microsoft's focus on enhancing data integration and developer productivity. SQL Server 2000 introduced native XML support, allowing T-SQL queries to generate, query, and update XML data directly within the database. SQL Server 2005 brought significant advancements, including Common Table Expressions (CTEs) for simplifying complex queries, TRY...CATCH blocks for structured error handling, and window functions such as ROW_NUMBER() and RANK() to align with emerging ANSI SQL standards for analytical processing. Later releases expanded these capabilities: SQL Server 2016 added JSON functions like FOR JSON and OPENJSON for seamless handling of semi-structured data, while SQL Server 2022 introduced ledger tables, which incorporate blockchain-inspired cryptographic verification to ensure data immutability and integrity in append-only scenarios.[13][14][15][16][17]
Under Microsoft's stewardship, T-SQL has consistently extended ANSI SQL standards while introducing proprietary features to meet practical database administration and application development needs. This approach includes periodic alignments, such as the adoption of window functions in SQL Server 2005 to support SQL:2003 specifications for advanced analytics without fully diverging from core SQL semantics. T-SQL's design prioritizes compatibility with relational paradigms, enabling developers to leverage both standard SQL constructs and Microsoft-specific extensions like stored procedures and triggers.
As of 2025, T-SQL remains integral to Azure SQL Database, where it supports fully managed cloud deployments with automatic scaling and high availability. Ongoing enhancements emphasize cloud-native optimizations and AI integration, including native vector data types and semantic search capabilities in SQL Server 2025 (released November 19, 2025), allowing T-SQL queries to incorporate machine learning models directly for intelligent data retrieval and analysis.[18][7][19] These developments ensure T-SQL's relevance in hybrid and AI-driven environments, bridging on-premises SQL Server instances with Azure services.
Core Syntax and Elements
Data Types and Declarations
Transact-SQL (T-SQL) supports a variety of built-in scalar data types categorized into exact numerics, approximate numerics, date and time, character strings, Unicode character strings, binary strings, and other specialized types.[6] These types define the storage and behavior of data in columns, variables, and parameters, ensuring data integrity and efficient querying.[6]
Among exact numeric types, INT stores integer values ranging from -2,147,483,648 to 2,147,483,647, occupying 4 bytes of storage.[20] The BIT type represents Boolean values as 0 (false), 1 (true), or NULL, using minimal storage (1 bit per value, packed into bytes).[6] For precise decimal calculations, DECIMAL(p,s) (synonymous with NUMERIC) allows a precision p up to 38 digits and scale s up to the value of p, where p specifies total digits and s the number of decimal places.[21] Character strings include VARCHAR(n) for variable-length non-Unicode data up to 8,000 characters (or VARCHAR(MAX) for up to 2^31-1 bytes).[22] Date and time types feature DATETIME2, which provides a range from 0001-01-01 to 9999-12-31 with fractional seconds precision up to 7 digits, offering greater accuracy than the legacy DATETIME type.[6]
Other specialized types include the JSON data type, introduced in SQL Server 2025, which stores JSON documents in a native binary format for efficient parsing, modification, and querying using built-in functions like JSON_OBJECTAGG and JSON_ARRAYAGG.[23] Additionally, the VECTOR data type, also new in SQL Server 2025, supports storage of vector embeddings for AI and machine learning applications, using single-precision (4-byte) or half-precision (2-byte) floating-point values optimized for similarity searches, exposed as JSON arrays.[24]
User-defined types extend built-in types for consistency and reusability. Alias types are created using CREATE TYPE to base a new type on an existing system type, such as a custom string: CREATE TYPE EmailAddress FROM VARCHAR(255) NOT NULL;, enforcing domain-specific rules like nullability.[25] Table-valued types, also defined with CREATE TYPE, allow declaration of complex structures resembling tables for passing multiple rows and columns as parameters: CREATE TYPE ProductList AS TABLE (ProductID INT PRIMARY KEY, Quantity INT);.[25]
Declarations in T-SQL specify types for local variables or parameters using the DECLARE statement, which supports system, user-defined, or CLR types but not deprecated types like TEXT.[26] The syntax is DECLARE @variable_name [AS] data_type [NOT NULL] [= initial_value];, where NOT NULL prevents null assignments and requires an initial value if specified.[26] For example, DECLARE @Counter [INT](/page/INT) NOT NULL = 0; initializes a non-null integer variable.[26] These declarations are used in batches, stored procedures, or functions to hold temporary data during execution.[26]
Type conversions ensure compatibility between mismatched types in expressions or assignments. The CAST function follows ISO standards: CAST(expression AS data_type), such as CAST(123.45 AS INT) yielding 123.[27] CONVERT, a T-SQL-specific extension, adds a style parameter for formatting: CONVERT(VARCHAR(10), GETDATE(), 101), which formats the current date as MM/DD/YYYY (e.g., '11/10/2025').[27] CONVERT supports additional styles for dates, numbers, and currencies, making it preferable for output formatting over CAST.[27]
Variables and Parameters
In Transact-SQL (T-SQL), variables are temporary storage locations used to hold data during script execution, declared with a specific syntax to specify name, data type, and optional initial value. The declaration uses the DECLARE statement, followed by the variable name prefixed with @, the data type, and an equals sign for initialization if desired. For example:
sql
DECLARE @Counter INT = 0;
DECLARE @Name NVARCHAR(50) = 'Example';
DECLARE @Counter INT = 0;
DECLARE @Name NVARCHAR(50) = 'Example';
This syntax supports various data types, such as INT, VARCHAR, or user-defined types, allowing variables to store scalars or even table structures in advanced cases.
Assignment to variables can occur via the SET statement for simple values or the SELECT statement for results from queries, with SET being preferred for single assignments due to its clarity and performance in certain contexts. The SET syntax is SET @variable = expression;, while SELECT allows batch assignments like SELECT @var1 = col1, @var2 = col2 FROM table;. For instance:
sql
SET @Counter = @Counter + 1;
SELECT @Total = SUM(Amount) FROM Orders;
SET @Counter = @Counter + 1;
SELECT @Total = SUM(Amount) FROM Orders;
SELECT is useful when assigning from query results but should be used judiciously to avoid unexpected multiple-row behaviors, where only the last row's value is retained unless aggregated.
Parameters extend variable usage in stored procedures, functions, and dynamic SQL, categorized as input, output, or table-valued. Input parameters are declared with @param datatype [= default_value], allowing optional defaults for flexibility in calls. Output parameters use the OUTPUT keyword to return values, as in CREATE PROCEDURE GetCount @ID INT, @Count INT OUTPUT AS SELECT @Count = COUNT(*) FROM Table WHERE ID = @ID;. Table-valued parameters, introduced in SQL Server 2008, enable passing structured data via user-defined table types, declared as CREATE TYPE MyTableType AS TABLE (ID INT); and used like @tvp MyTableType READONLY. These parameters promote modular code by encapsulating inputs and outputs without global state.
Scoping rules confine local variables and parameters to their declaring batch, stored procedure, function, or trigger, ensuring isolation across executions. For example, a variable declared in a batch is inaccessible in subsequent batches separated by GO. System global variables, prefixed with @@ like @@ERROR (which captures the last T-SQL statement's error code) or @@ROWCOUNT (returning affected rows), provide environment-wide status but are read-only and reset per statement.
Best practices emphasize declaring variables with precise data types to optimize performance and avoid implicit conversions, initializing them to prevent undefined behavior, and favoring local over global variables for thread safety in concurrent, multi-user SQL Server environments. Avoid overusing SELECT for assignments in loops due to potential overhead, and document parameter defaults clearly in procedure definitions.
Expressions and Control Flow
Operators and Expressions
Transact-SQL (T-SQL) supports a variety of operators that enable the construction of expressions for performing computations, comparisons, and manipulations within queries and statements. Expressions in T-SQL are combinations of constants, variables, columns, scalar functions, and operators, which can be simple (a single element) or complex (joined by operators). These expressions evaluate to a single value and are fundamental to clauses like SELECT, WHERE, and control-of-flow structures.[28][29]
Arithmetic operators in T-SQL perform mathematical operations on numeric expressions, including addition (+), subtraction (-), multiplication (*), division (/), and modulo (%). These operators apply to numeric data types and, for + and -, also to datetime types. Any arithmetic operation involving NULL yields NULL, preserving the three-valued logic of SQL where unknown values propagate unknowns. For example, NULL + 5 evaluates to NULL.[30][28]
Comparison operators facilitate relational tests between expressions, returning a Boolean result under three-valued logic (TRUE, FALSE, or UNKNOWN). The standard operators are equality (=), inequality (<> or !=), greater than (>), less than (<), greater than or equal (>=), and less than or equal (<=). For handling NULL, dedicated operators IS NULL and IS NOT NULL are required, as standard comparisons with NULL yield UNKNOWN (not TRUE or FALSE). This behavior aligns with ANSI SQL standards and applies when SET ANSI_NULLS ON (the default).[31]
Logical operators combine Boolean expressions to form compound conditions: AND returns TRUE only if both operands are TRUE (FALSE or UNKNOWN otherwise), OR returns TRUE if at least one operand is TRUE (FALSE only if both are FALSE), and NOT negates a Boolean value (leaving UNKNOWN unchanged). These operators adhere to three-valued logic, as shown in the truth tables below. They are commonly used in WHERE clauses to filter rows based on multiple criteria.[32][33][34]
AND Truth Table:
| Operand 1 | Operand 2 | Result |
|---|
| TRUE | TRUE | TRUE |
| TRUE | FALSE | FALSE |
| TRUE | UNKNOWN | UNKNOWN |
| FALSE | TRUE | FALSE |
| FALSE | FALSE | FALSE |
| FALSE | UNKNOWN | FALSE |
| UNKNOWN | TRUE | UNKNOWN |
| UNKNOWN | FALSE | FALSE |
| UNKNOWN | UNKNOWN | UNKNOWN |
OR Truth Table:
| Operand 1 | Operand 2 | Result |
|---|
| TRUE | TRUE | TRUE |
| TRUE | FALSE | TRUE |
| TRUE | UNKNOWN | TRUE |
| FALSE | TRUE | TRUE |
| FALSE | FALSE | FALSE |
| FALSE | UNKNOWN | UNKNOWN |
| UNKNOWN | TRUE | TRUE |
| UNKNOWN | FALSE | UNKNOWN |
| UNKNOWN | UNKNOWN | UNKNOWN |
NOT Truth Table:
| Input | NOT Output |
|---|
| TRUE | FALSE |
| FALSE | TRUE |
| UNKNOWN | UNKNOWN |
String operators in T-SQL handle text manipulation within expressions. The + operator concatenates character or binary strings, columns, or a combination thereof; for instance, 'Hello' + ' World' yields 'Hello World'. As of SQL Server 2025, the || operator provides an additional standard-conforming way to concatenate strings, such as 'Hello' || ' World'.[35][36] The LIKE operator performs pattern matching using wildcards: % matches zero or more characters, and _ matches exactly one character (e.g., 'ABC%' LIKE 'A_C' is FALSE, but 'ABC' LIKE 'A_C' is TRUE). Starting with SQL Server 2025, regular expression functions such as REGEXP_LIKE, REGEXP_REPLACE, REGEXP_SUBSTR, and others enable advanced pattern matching beyond LIKE, supporting complex regex patterns for searching and manipulating strings. Scalar functions such as SUBSTRING integrate seamlessly into string expressions, allowing extraction and combination, like SUBSTRING('Microsoft', 1, 4) + ' SQL' resulting in 'Micro SQL'; in SQL Server 2025, the length parameter in SUBSTRING is optional and defaults to the remaining length of the expression.[35][37][38]
Operator precedence in T-SQL determines evaluation order in expressions, with levels from highest (1) to lowest (8). Unary operators like ~ (bitwise NOT) have the highest precedence, followed by multiplicative operators (*, /, %) at level 2, additive and bitwise (+, -, &, ^, |) at level 3, comparisons at level 4, NOT at level 5, AND at level 6, and OR at level 7. Operators at the same level evaluate left-to-right. Parentheses override precedence, ensuring explicit grouping; for example, SELECT 2 + 3 * 4 yields 14 (multiplication first), but SELECT (2 + 3) * 4 yields 20.[39]
Conditional and Looping Statements
Transact-SQL provides imperative control-of-flow constructs that enable procedural programming within SQL scripts, stored procedures, and functions, allowing developers to implement decision-making and iteration logic.[40] These mechanisms extend beyond the declarative nature of standard SQL, incorporating elements like conditional branching and loops to handle complex business rules and data processing tasks.[40]
IF...ELSE Statements
The IF...ELSE statement in Transact-SQL evaluates a Boolean expression and executes a specified statement or block of statements if the condition returns TRUE; an optional ELSE clause executes if the condition is FALSE or NULL.[41] The syntax is:
IF boolean_expression
{ sql_statement | statement_block }
[ ELSE
{ sql_statement | statement_block } ]
IF boolean_expression
{ sql_statement | statement_block }
[ ELSE
{ sql_statement | statement_block } ]
Here, boolean_expression is any valid Transact-SQL expression that evaluates to TRUE or FALSE, and statement_block uses BEGIN...END to group multiple statements for execution as a unit.[41] For example, to check if the current day is a weekend:
IF DATENAME(weekday, GETDATE()) IN (N'Saturday', N'Sunday')
SELECT 'Weekend';
ELSE
SELECT 'Weekday';
IF DATENAME(weekday, GETDATE()) IN (N'Saturday', N'Sunday')
SELECT 'Weekend';
ELSE
SELECT 'Weekday';
This construct is commonly used in stored procedures to test for parameter conditions or object existence before proceeding.[41] Nesting IF statements is supported, with the depth limited only by available memory, enabling hierarchical decision trees.[41]
CASE Expressions
The CASE expression provides multi-way conditional logic, returning a value based on evaluated conditions, and can be used in statements like SET or within larger control structures to assign values dynamically.[42] It comes in two forms: simple CASE for equality comparisons and searched CASE for arbitrary Boolean conditions.
The simple CASE syntax compares an input expression against multiple WHEN values:
CASE input_expression
WHEN when_expression THEN result_expression
[ ...n ]
[ ELSE else_result_expression ]
END
CASE input_expression
WHEN when_expression THEN result_expression
[ ...n ]
[ ELSE else_result_expression ]
END
The searched CASE evaluates sequential Boolean conditions:
CASE
WHEN Boolean_expression THEN result_expression
[ ...n ]
[ ELSE else_result_expression ]
END
CASE
WHEN Boolean_expression THEN result_expression
[ ...n ]
[ ELSE else_result_expression ]
END
Evaluation stops at the first TRUE condition, with an optional ELSE handling unmatched cases; the return type is determined by the highest-precedence data type among the result expressions.[42] For instance, to set a variable based on a value:
SET @status = CASE @score
WHEN 90 THEN 'A'
WHEN 80 THEN 'B'
ELSE 'C'
END;
SET @status = CASE @score
WHEN 90 THEN 'A'
WHEN 80 THEN 'B'
ELSE 'C'
END;
CASE supports up to 10 levels of nesting and cannot directly control execution flow like IF, but it integrates seamlessly into procedural blocks for value selection.[42]
WHILE Loops
The WHILE loop repeatedly executes a statement or block while a Boolean condition remains TRUE, providing iteration capabilities for tasks like batch processing or recursive calculations.[43] Its syntax is:
WHILE boolean_expression
{ sql_statement | statement_block | BREAK | CONTINUE }
WHILE boolean_expression
{ sql_statement | statement_block | BREAK | CONTINUE }
Inside the loop, BREAK exits the innermost WHILE entirely, transferring control to the statement following END, while CONTINUE skips the remainder of the current iteration and re-evaluates the condition.[43] An example that doubles product prices until exceeding a threshold, using both keywords:
WHILE (SELECT MAX(ListPrice) FROM Production.Product) < 500
BEGIN
UPDATE Production.Product
SET ListPrice = ListPrice * 2;
IF (SELECT MAX(ListPrice) FROM Production.Product) > 500
BREAK;
CONTINUE;
END
WHILE (SELECT MAX(ListPrice) FROM Production.Product) < 500
BEGIN
UPDATE Production.Product
SET ListPrice = ListPrice * 2;
IF (SELECT MAX(ListPrice) FROM Production.Product) > 500
BREAK;
CONTINUE;
END
Developers must ensure the condition eventually becomes FALSE, often using counters or state variables, to prevent infinite loops that could consume resources indefinitely.[43]
Nested Control Structures
Transact-SQL allows nesting of IF, CASE, and WHILE within one another or in combination, such as embedding IF statements inside a WHILE loop for conditional iteration or using CASE within an IF to determine branch outcomes.[40] For example, a WHILE loop might increment a counter while an inner IF checks boundaries to avoid overflow. BREAK and CONTINUE apply only to the innermost loop in nested WHILE structures, ensuring precise control without affecting outer iterations.[43] This nesting supports complex algorithms, like simulating recursion in procedural code.
Unlike standard SQL, which focuses on declarative queries without built-in procedural flow, Transact-SQL's imperative style—derived from its integration with SQL Server's programming model—enables these blocks for server-side scripting, distinguishing it from ANSI SQL's set-based paradigm.[40]
Data Manipulation
Querying with SELECT
The SELECT statement in Transact-SQL is the primary mechanism for retrieving data from tables in SQL Server databases, allowing users to specify columns, filter rows, sort results, and perform complex operations like joining multiple tables or applying functions.[44] Its basic syntax is SELECT [ALL | DISTINCT] select_list FROM table_source [WHERE search_condition] [ORDER BY order_expression [ASC | DESC]], where the select_list defines the columns or expressions to return, table_source identifies the data source, WHERE filters rows based on conditions, and ORDER BY sorts the output.[44] For example, to retrieve employee names sorted alphabetically, one might use SELECT FirstName, LastName FROM HumanResources.Employee ORDER BY LastName;.[45] The DISTINCT keyword eliminates duplicate rows, as in SELECT DISTINCT Title FROM HumanResources.Employee;, ensuring unique values are returned.[44]
Joins extend the SELECT statement by combining data from multiple tables based on related columns, using the FROM clause with JOIN keywords and an ON condition.[46] An INNER JOIN returns only matching rows from both tables, such as SELECT p.Name, sod.SalesOrderID FROM Production.Product AS p INNER JOIN Sales.SalesOrderDetail AS sod ON p.ProductID = sod.ProductID;, which lists products involved in sales orders.[46] LEFT OUTER JOIN includes all rows from the left table and matching rows from the right, filling non-matches with NULL, for instance: SELECT p.Name, sod.SalesOrderID FROM Production.Product AS p LEFT OUTER JOIN Sales.SalesOrderDetail AS sod ON p.ProductID = sod.ProductID;.[46] RIGHT OUTER JOIN mirrors this but prioritizes the right table, while FULL OUTER JOIN returns all rows from both with NULLs for non-matches.[46] CROSS JOIN produces a Cartesian product without an ON clause, generating all combinations, as in SELECT e.BusinessEntityID, d.Name FROM HumanResources.Employee AS e CROSS JOIN HumanResources.Department AS d;.[46]
Aggregate functions summarize data within SELECT queries, often paired with GROUP BY to process groups of rows.[47] Common functions include COUNT() to tally all rows, SUM(column) for totals of numeric values, AVG for averages, and MIN/MAX for extrema, all ignoring NULLs except COUNT().[47] For example, SELECT OrderDateKey, SUM(SalesAmount) AS TotalSales FROM FactInternetSales GROUP BY OrderDateKey; computes daily sales totals.[48] The GROUP BY clause divides results into groups based on specified columns, requiring non-aggregated SELECT columns to be included, while HAVING filters those groups post-aggregation, such as HAVING SUM(SalesAmount) > 10000.[48] This contrasts with WHERE, which filters before grouping.[49]
Subqueries nest a SELECT inside another, enabling multi-step data retrieval without temporary tables.[50] Scalar subqueries return a single value for use in comparisons, like SELECT Name FROM Production.Product WHERE ListPrice > (SELECT AVG(ListPrice) FROM Production.Product);, identifying above-average priced items.[50] Table subqueries return multiple rows, often in WHERE with IN or EXISTS, such as SELECT Name FROM Production.Product WHERE ProductSubcategoryID IN (SELECT ProductSubcategoryID FROM Production.ProductSubcategory WHERE Name = 'Wheels');.[50] They can appear in FROM as derived tables or in SELECT lists. Correlated subqueries reference the outer query, executing per row and potentially slower, for example: SELECT DISTINCT c.LastName, c.FirstName FROM Person.Person AS c JOIN HumanResources.Employee AS e ON e.BusinessEntityID = c.BusinessEntityID WHERE 5000.00 IN (SELECT Bonus FROM Sales.SalesPerson AS sp WHERE e.BusinessEntityID = sp.BusinessEntityID);.[50] Non-correlated subqueries run independently once.[50] Up to 32 levels of nesting are supported.[50]
Introduced in SQL Server 2005, window functions perform calculations across row sets defined by the OVER clause, avoiding the need for self-joins or subqueries in many analytic scenarios.[51] The OVER clause specifies PARTITION BY to divide rows into groups and ORDER BY for sequencing within partitions, as in ROW_NUMBER() OVER (PARTITION BY PostalCode ORDER BY SalesYTD DESC).[51] Ranking functions like ROW_NUMBER() assign unique sequential numbers to rows, restarting per partition, for example: SELECT BusinessEntityID, ROW_NUMBER() OVER (ORDER BY BusinessEntityID) AS RowNumber FROM Sales.SalesPerson;, numbering salespeople sequentially.[52] RANK() assigns ranks with gaps for ties, such as RANK() OVER (ORDER BY PostalCode), where tied postal codes share rank 1 but the next is 3.[52] These functions apply after GROUP BY but before the final ORDER BY in query processing.[51]
Modifying Data with INSERT, UPDATE, DELETE
Transact-SQL provides data manipulation language (DML) statements for modifying data in SQL Server tables, including INSERT for adding new rows, UPDATE for altering existing rows, and DELETE for removing rows. These statements conform to ANSI SQL standards but include T-SQL extensions such as the OUTPUT clause for capturing affected rows and the TOP clause for limiting operations on a subset of rows, enhancing control and auditing capabilities.[53][54]
The INSERT statement adds one or more rows to a table, using either explicit VALUES or a subquery based on SELECT to source data. The basic syntax is INSERT INTO table_name [ (column_list) ] VALUES (value_list);, where the column list is optional if all columns are specified in order, and multiple rows can be inserted in a single statement by separating value lists with commas. For example, to insert a single row into a Products table:
sql
INSERT INTO [Production](/page/Production).Product (Name, ProductNumber, SafetyStockLevel)
VALUES ('Adjustable Race', 'AR-5381', 1000);
INSERT INTO [Production](/page/Production).Product (Name, ProductNumber, SafetyStockLevel)
VALUES ('Adjustable Race', 'AR-5381', 1000);
This inserts the specified values into the corresponding columns. Alternatively, INSERT can draw from a SELECT query for bulk insertion without using BULK INSERT: INSERT INTO table_name SELECT ... FROM source_table;, which efficiently transfers rows while respecting data types and constraints.[55]
The UPDATE statement modifies existing data in one or more columns of a table, with the syntax UPDATE table_name SET column1 = value1, column2 = value2, ... [ FROM joined_tables ] [ WHERE condition ];. The SET clause assigns new values, which can include expressions or subqueries, and the optional WHERE clause filters affected rows to prevent unintended updates across the entire table. T-SQL supports multi-table updates via the FROM clause with JOINs, allowing updates based on related data; for instance:
sql
[UPDATE](/page/Update) p
SET p.ListPrice = p.ListPrice * 1.1
FROM Production.Product p
INNER JOIN Production.ProductSubcategory s ON p.ProductSubcategoryID = s.ProductSubcategoryID
WHERE s.Name = 'Wheels';
[UPDATE](/page/Update) p
SET p.ListPrice = p.ListPrice * 1.1
FROM Production.Product p
INNER JOIN Production.ProductSubcategory s ON p.ProductSubcategoryID = s.ProductSubcategoryID
WHERE s.Name = 'Wheels';
This increases prices for products in the 'Wheels' subcategory. A key T-SQL enhancement is the TOP clause, which limits the update to a specified number of rows, such as UPDATE [TOP](/page/Top) (10) table_name SET ..., applied to a random selection unless ordered by an ORDER BY clause. The OUTPUT clause can capture pre- and post-update values for auditing, like OUTPUT inserted.column, deleted.column.[56][54][53]
The DELETE statement removes rows from a table, using the syntax DELETE FROM table_name [ FROM joined_tables ] [ WHERE condition ];. Without a WHERE clause, it deletes all rows, but typically a condition specifies targeted rows, such as DELETE FROM Production.Product WHERE DiscontinuedDate IS NOT NULL;. Similar to UPDATE, T-SQL allows the FROM clause for joins to delete based on related tables, enabling complex criteria like DELETE FROM p FROM Production.Product p INNER JOIN Production.ProductModel m ON p.ProductModelID = m.ProductModelID WHERE m.Name LIKE 'Road%';. The TOP clause limits deletions, e.g., DELETE TOP (5) FROM table_name WHERE ..., again on a random subset unless ordered. The OUTPUT clause supports capturing deleted rows, as in DELETE FROM table_name OUTPUT deleted.* INTO @audit_table;, useful for logging changes without additional queries.[57][54][53]
Introduced in SQL Server 2008, the MERGE statement performs upsert operations by combining INSERT, UPDATE, and DELETE logic in a single statement based on a join between source and target tables. Its syntax is MERGE target_table AS TARGET USING source_table AS SOURCE ON join_condition WHEN MATCHED THEN [UPDATE](/page/Update) SET ... [ WHEN NOT MATCHED BY TARGET THEN INSERT ... ] [ WHEN NOT MATCHED BY SOURCE THEN DELETE ];, allowing conditional actions: update matched rows, insert unmatched source rows, and optionally delete unmatched target rows. For example:
sql
MERGE INTO Sales.Orders AS target
USING (SELECT OrderID, CustomerID, OrderDate FROM NewOrders) AS source
ON target.OrderID = source.OrderID
WHEN MATCHED THEN
[UPDATE](/page/Update) SET target.ShipDate = GETDATE()
WHEN NOT MATCHED BY TARGET THEN
INSERT (OrderID, CustomerID, OrderDate)
VALUES (source.OrderID, source.CustomerID, source.OrderDate);
MERGE INTO Sales.Orders AS target
USING (SELECT OrderID, CustomerID, OrderDate FROM NewOrders) AS source
ON target.OrderID = source.OrderID
WHEN MATCHED THEN
[UPDATE](/page/Update) SET target.ShipDate = GETDATE()
WHEN NOT MATCHED BY TARGET THEN
INSERT (OrderID, CustomerID, OrderDate)
VALUES (source.OrderID, source.CustomerID, source.OrderDate);
This updates shipping dates for existing orders and inserts new ones, with the TOP clause applicable to limit joined rows and OUTPUT for tracking actions. MERGE requires a unique index on the join column and cannot target views without INSTEAD OF triggers.[58]
Bulk Data Operations
Transact-SQL provides mechanisms for efficiently loading large volumes of data into SQL Server tables, primarily through the BULK INSERT statement, which imports data directly from files such as CSV or text formats. This operation is designed for high-performance scenarios, bypassing the overhead of individual row insertions to handle datasets comprising millions of rows in ETL (Extract, Transform, Load) processes. Unlike standard INSERT statements, which process data row by row and are better suited for smaller, ad-hoc modifications, BULK INSERT minimizes logging and locking to achieve significantly faster throughput for bulk imports.[59][60]
The basic syntax of BULK INSERT is as follows:
BULK INSERT [database_name].[schema_name].[table_or_view_name]
FROM 'data_file_path'
WITH (
FIELDTERMINATOR = ',',
ROWTERMINATOR = '\n'
);
BULK INSERT [database_name].[schema_name].[table_or_view_name]
FROM 'data_file_path'
WITH (
FIELDTERMINATOR = ',',
ROWTERMINATOR = '\n'
);
Here, the statement specifies the target table and the source file path, with options defining the file's structure; for instance, FIELDTERMINATOR delimits columns (defaulting to tab), and ROWTERMINATOR separates rows (defaulting to carriage return-line feed). Additional options enhance flexibility: FORMATFILE specifies a non-XML format file to map complex data structures to table columns, BATCHSIZE controls the number of rows committed per transaction to manage memory and rollback risks (defaulting to the entire file), and ERRORFILE directs rejected rows to a separate log file for error analysis. These features support diverse file formats and improve reliability during imports.[60][61]
For ad-hoc bulk loads without direct file access by the SQL Server service, alternatives include the OPENROWSET function with the BULK option, used in an INSERT...SELECT statement like INSERT INTO table SELECT * FROM OPENROWSET(BULK 'file_path', FORMAT = 'CSV', FIELDTERMINATOR = ',', ROWTERMINATOR = '\n') AS DataSource;, which treats the file as a rowset data source and supports table hints such as TABLOCK for locking optimization. Another method involves the bcp (bulk copy program) utility, a command-line tool for importing data, which can be invoked from T-SQL via the deprecated xp_cmdshell extended stored procedure, though this approach is discouraged due to security vulnerabilities and is disabled by default.[62][63][64]
Performance benefits of these bulk operations are pronounced for large-scale data ingestion; for example, BULK INSERT can process millions of rows in seconds by using minimal logging modes and reducing contention, making it ideal for ETL pipelines where row-by-row INSERT would be impractically slow due to per-row transaction costs. To maximize efficiency, options like TABLOCK should be applied to acquire exclusive locks early, and batch sizes tuned based on available resources.[59][60]
Security is a critical aspect of bulk operations, as BULK INSERT requires the executing user to hold INSERT permissions on the target table and the ADMINISTER BULK OPERATIONS server-level permission (or ADMINISTER DATABASE BULK OPERATIONS in Azure SQL Database), replacing the legacy BULKADMIN role. Risks arise from external file access: the SQL Server service account must read the source file, potentially exposing the system to malicious data or unauthorized paths, especially with UNC shares or Azure Blob Storage, where shared access signatures or managed identities are needed for authentication. Best practices include validating file sources and restricting permissions to trusted locations.[60][62]
Error Handling and Transactions
Exception Handling with TRY...CATCH
Transact-SQL's TRY...CATCH construct provides a structured mechanism for handling runtime errors, introduced in SQL Server 2005 to enhance exception management in database programming.[65] This feature allows developers to enclose potentially error-prone code in a TRY block, with error-handling logic in the immediately following CATCH block, enabling graceful recovery or logging without terminating the entire batch.[15] It captures all execution errors with a severity greater than 10 that do not sever the database connection, such as division by zero or constraint violations, while ignoring lower-severity warnings.[15] Unlike ad-hoc error checking, TRY...CATCH supports detailed error introspection and propagation, making it a cornerstone for robust T-SQL applications.
The basic syntax consists of a TRY block containing the statements to monitor, followed directly by a CATCH block for response actions, without intervening statements:
BEGIN TRY
-- Statements that may generate an error
END TRY
BEGIN CATCH
-- Error handling statements
END CATCH
BEGIN TRY
-- Statements that may generate an error
END TRY
BEGIN CATCH
-- Error handling statements
END CATCH
[15] Within the CATCH block, six system functions retrieve specifics about the caught error: ERROR_NUMBER() returns the unique error code; ERROR_SEVERITY() the severity level (11-19 for caught errors); ERROR_STATE() the error state number for context; ERROR_PROCEDURE() the name of the stored procedure or trigger where the error occurred; ERROR_LINE() the line number of the error in the batch or procedure; and ERROR_MESSAGE() the descriptive text of the error.[66][67][68][69][70] These functions ensure precise diagnostics, often used to construct log entries or custom responses. For instance, a representative example might divide two values and catch the result:
BEGIN TRY
DECLARE @Result INT = 10 / 0; -- This raises error 8134 (divide by zero)
END TRY
BEGIN CATCH
SELECT
ERROR_NUMBER() AS ErrorNumber,
ERROR_MESSAGE() AS ErrorMessage;
END CATCH
BEGIN TRY
DECLARE @Result INT = 10 / 0; -- This raises error 8134 (divide by zero)
END TRY
BEGIN CATCH
SELECT
ERROR_NUMBER() AS ErrorNumber,
ERROR_MESSAGE() AS ErrorMessage;
END CATCH
This outputs the error number 8134 and the message "Divide by zero error encountered."[15]
When errors occur within transactions, the CATCH block can invoke XACT_STATE() to assess the transaction's viability before deciding on rollback. This function returns -1 if an active, uncommittable transaction exists (due to an error rendering it doomed); 1 if committable; or 0 if no transaction is active.[71] Developers typically check IF XACT_STATE() = -1 and issue ROLLBACK TRANSACTION to avoid partial commits, preserving data consistency—especially useful when SET XACT_ABORT ON escalates certain errors.[15] Errors caught by TRY...CATCH may leave transactions in a state requiring explicit rollback for integrity.
TRY...CATCH blocks support nesting, where an inner block's error can be handled locally or allowed to propagate outward for broader management.[15] To re-throw an error and propagate it—preserving original details like number, state, and message—use the THROW statement (introduced in SQL Server 2012) without parameters at the end of the CATCH block; alternatively, RAISERROR with the error functions achieves similar results but requires manual reconstruction.[72][73] Best practices for logging involve capturing all error function outputs in the innermost CATCH, inserting them into an error table (e.g., via INSERT INTO ErrorLog (ErrorNumber, ErrorMessage, ...)), and then optionally re-throwing for caller awareness, minimizing silent failures and aiding troubleshooting.[15]
This construct supersedes the legacy @@ERROR global variable, which holds the last error number but resets after every successful statement, demanding immediate checks and risking oversight in complex scripts.[74] TRY...CATCH offers centralized, reliable capture across multiple statements, reducing error-prone code and improving maintainability in T-SQL routines.[15]
Transaction Control Statements
Transaction control statements in Transact-SQL (T-SQL) provide mechanisms to manage database transactions, ensuring the ACID (Atomicity, Consistency, Isolation, Durability) properties that maintain data integrity during multi-statement operations.[5] These statements allow developers to explicitly define transaction boundaries, handle nested operations, and perform partial rollbacks, which is essential for complex data modifications in SQL Server environments.[5] By grouping data manipulation language (DML) statements into logical units, T-SQL transactions prevent partial updates in case of failures, supporting reliable application logic.[5]
The BEGIN [TRANSACTION](/page/Transaction) statement (abbreviated as BEGIN TRAN) initiates an explicit, local transaction, marking the start of a unit of work that can be committed or rolled back.[75] Its syntax is:
BEGIN { TRAN | [TRANSACTION](/page/Transaction) } [ transaction_name | @tran_name_variable ]
BEGIN { TRAN | [TRANSACTION](/page/Transaction) } [ transaction_name | @tran_name_variable ]
When executed, it increments the @@TRANCOUNT system function by 1, indicating an active transaction.[75] Transactions can be nested up to 32 levels deep, where each inner BEGIN [TRANSACTION](/page/Transaction) further increments @@TRANCOUNT, but only the outermost transaction controls the final commitment of changes.[75] Named transactions, specified via transaction_name, aid in tracking but do not alter the engine's behavior beyond the outermost level.[75] For example:
BEGIN TRANSACTION OuterTran;
-- DML statements here
BEGIN TRANSACTION InnerTran;
-- More DML statements
COMMIT TRANSACTION InnerTran; -- Decrements @@TRANCOUNT but keeps OuterTran active
COMMIT TRANSACTION OuterTran; -- Commits all changes when @@TRANCOUNT reaches 0
BEGIN TRANSACTION OuterTran;
-- DML statements here
BEGIN TRANSACTION InnerTran;
-- More DML statements
COMMIT TRANSACTION InnerTran; -- Decrements @@TRANCOUNT but keeps OuterTran active
COMMIT TRANSACTION OuterTran; -- Commits all changes when @@TRANCOUNT reaches 0
This nesting supports modular code structures, such as in stored procedures, without prematurely finalizing data changes.[75]
The COMMIT TRANSACTION statement (or COMMIT TRAN) finalizes a transaction, making all modifications permanent and releasing associated locks.[76] Its syntax is:
COMMIT [ TRAN | TRANSACTION ] [ transaction_name | @tran_name_variable ]
COMMIT [ TRAN | TRANSACTION ] [ transaction_name | @tran_name_variable ]
It decrements @@TRANCOUNT by 1; if @@TRANCOUNT reaches 0 after the decrement, the changes are durably written to the database log.[76] In nested scenarios, inner commits only reduce the count without committing data until the outermost transaction completes.[76] Transaction names are optional and primarily for developer reference, as the engine ignores them for commitment logic.[76] An error occurs if COMMIT is issued when @@TRANCOUNT is already 0.[76] For instance, in a three-level nested transaction, three COMMIT statements are required to fully persist changes, with only the final one invoking durability.[76]
Conversely, the ROLLBACK TRANSACTION statement (or ROLLBACK TRAN) undoes all uncommitted changes since the transaction began or a specified savepoint, restoring the database to its pre-transaction state.[77] Syntax:
ROLLBACK { TRAN | TRANSACTION } [ transaction_name | @tran_name_variable | savepoint_name | @savepoint_variable ]
ROLLBACK { TRAN | TRANSACTION } [ transaction_name | @tran_name_variable | savepoint_name | @savepoint_variable ]
Without a name or savepoint, it rolls back to the outermost transaction, setting @@TRANCOUNT to 0 and freeing resources.[77] In nested contexts, a full rollback affects all levels, but partial rollbacks to inner points are possible via savepoints (detailed below).[77] It releases locks acquired after the rollback point, except for any escalated or converted locks.[77] An example demonstrates rolling back a named transaction after an insert, leaving subsequent operations intact only if outside the transaction.[77]
The SAVE TRANSACTION statement establishes a savepoint within an active transaction, enabling partial rollbacks without aborting the entire unit of work.[78] Syntax:
SAVE { TRAN | TRANSACTION } { savepoint_name | @savepoint_variable }
SAVE { TRAN | TRANSACTION } { savepoint_name | @savepoint_variable }
Savepoint names are case-sensitive and limited to 32 characters; variables can hold them for dynamic use.[78] Multiple savepoints with the same name are permitted, with rollbacks targeting the most recent instance.[78] This feature does not decrement @@TRANCOUNT and is unavailable in distributed transactions.[78] A common pattern in stored procedures involves setting a savepoint before risky operations and rolling back to it on failure, preserving the outer transaction.[78] For example:
BEGIN [TRANSACTION](/page/Transaction);
[SAVE](/page/Save) [TRANSACTION](/page/Transaction) SavePoint1;
-- Operations that might fail
IF @@[ERROR](/page/Error) <> 0
[ROLLBACK](/page/Rollback) [TRANSACTION](/page/Transaction) SavePoint1; -- Undoes only post-savepoint changes
COMMIT [TRANSACTION](/page/Transaction);
BEGIN [TRANSACTION](/page/Transaction);
[SAVE](/page/Save) [TRANSACTION](/page/Transaction) SavePoint1;
-- Operations that might fail
IF @@[ERROR](/page/Error) <> 0
[ROLLBACK](/page/Rollback) [TRANSACTION](/page/Transaction) SavePoint1; -- Undoes only post-savepoint changes
COMMIT [TRANSACTION](/page/Transaction);
This allows fine-grained control, enhancing error recovery within transactions.[78]
T-SQL also supports implicit transactions via the SET IMPLICIT_TRANSACTIONS statement, which alters connection behavior to automatically start transactions for certain statements when none is active.[79] Syntax:
SET IMPLICIT_TRANSACTIONS { ON | OFF }
SET IMPLICIT_TRANSACTIONS { ON | OFF }
When set to ON, DML statements like INSERT, UPDATE, DELETE, MERGE, and others (but not simple SELECT queries without tables) begin a transaction if @@TRANCOUNT is 0, requiring an explicit COMMIT or ROLLBACK to end it.[79] This mode aligns with ANSI standards but defaults to OFF for most connections, where each statement autocommits independently.[79] With ON, subsequent statements within the implicit transaction increment @@TRANCOUNT only if an explicit BEGIN TRANSACTION is used.[79] For example, enabling it before inserts ensures changes are not committed until a COMMIT is issued, preventing unintended partial updates.[79] This setting applies at the session level and resets on connection close.[79]
Advanced Features
Stored Procedures and Functions
Stored procedures in Transact-SQL are precompiled collections of one or more Transact-SQL statements stored under a name and processed as a single unit, allowing for modular code execution in SQL Server.[80] They accept input parameters, return multiple values in the form of result sets, and can include control-of-flow language, making them suitable for complex operations like data validation and business logic implementation.[81] Stored procedures enhance performance by caching execution plans, reduce network traffic through batching, and improve security by limiting direct table access.[80]
There are several types of stored procedures in SQL Server. User-defined stored procedures are created by database developers using Transact-SQL or Common Language Runtime (CLR) integration and reside in user databases.[80] System stored procedures, prefixed with sp_, are predefined in the Resource database for administrative tasks such as database maintenance and configuration queries.[82] Extended stored procedures, prefixed with xp_, interface with external operating system components via dynamic-link libraries (DLLs) but are deprecated in favor of CLR procedures due to security risks.[80] Temporary stored procedures, marked with # for local or ## for global scope, are stored in tempdb and automatically removed at session end or explicitly.[80]
To create a user-defined stored procedure, the CREATE [PROCEDURE](/page/Procedure) statement is used, specifying the procedure name, optional parameters with data types, and the body enclosed in BEGIN...END. For example:
sql
CREATE [PROCEDURE](/page/Procedure) GetEmployeeDetails
@EmployeeID [int](/page/INT)
AS
BEGIN
SELECT * FROM Employees WHERE EmployeeID = @EmployeeID;
END;
CREATE [PROCEDURE](/page/Procedure) GetEmployeeDetails
@EmployeeID [int](/page/INT)
AS
BEGIN
SELECT * FROM Employees WHERE EmployeeID = @EmployeeID;
END;
[81]
Stored procedures are executed using the EXEC or EXECUTE statement, optionally passing parameter values. For instance, EXEC GetEmployeeDetails @EmployeeID = 5; invokes the procedure and returns the result set.[83] Parameters can be input, output, or input/output, enabling data passing without dynamic SQL construction, which helps mitigate SQL injection risks.[81]
User-defined functions (UDFs) in Transact-SQL are routines that accept parameters, perform computations, and return a value or table, promoting code reusability within queries.[84] Unlike ad-hoc code, UDFs cache execution plans similar to stored procedures, reducing compilation overhead.[84] They are invoked directly in SELECT statements, WHERE clauses, or other query contexts, enhancing composability.
UDFs are categorized into scalar and table-valued types. Scalar UDFs return a single value of any data type except text, ntext, image, cursor, or timestamp, and can be inline (single RETURN statement) or multi-statement.[85] Table-valued UDFs return a table data type; inline versions use a single SELECT statement for the return, while multi-statement versions define a table variable and populate it with multiple statements.[84]
Creation of a UDF employs the CREATE FUNCTION statement, including the function name, parameters, return type, and body. An example of a scalar UDF is:
sql
CREATE FUNCTION CalculateBonus (@Salary [decimal](/page/Decimal)(10,2))
RETURNS [decimal](/page/Decimal)(10,2)
AS
BEGIN
RETURN @Salary * 0.10;
END;
CREATE FUNCTION CalculateBonus (@Salary [decimal](/page/Decimal)(10,2))
RETURNS [decimal](/page/Decimal)(10,2)
AS
BEGIN
RETURN @Salary * 0.10;
END;
[85]
Scalar UDFs are called like built-in functions, e.g., SELECT dbo.CalculateBonus(50000);, while table-valued UDFs are used in the FROM clause, such as SELECT * FROM dbo.GetEmployeeProjects(@EmployeeID);.[86] UDFs support up to 1,024 input parameters but no output parameters.[84]
Stored procedures and user-defined functions differ in scope and capabilities. Procedures can return multiple result sets, use transactions to modify database state, and support output parameters, but they cannot be composed within queries.[80] In contrast, functions are designed for read-only operations without side effects, must be deterministic when schema-bound, and are seamlessly integrable into SELECT statements for expressions or joins.[84] Functions cannot execute stored procedures or use non-deterministic elements like GETDATE() unless marked appropriately, ensuring query optimization compatibility.[87]
Security features enhance both constructs. For stored procedures, the EXECUTE AS clause allows impersonation of a specified user or SELF during execution, enabling controlled privilege escalation without granting broad permissions.[81] Procedures can also be encrypted to hide source code. For functions, SCHEMABINDING binds the function to the schema of referenced objects, preventing schema changes that could invalidate it and requiring REFERENCES permission on those objects.[85] This promotes data integrity in indexed views or computed columns.[88]
Cursors, Triggers, and Dynamic SQL
Transact-SQL provides mechanisms for handling row-by-row operations, automated responses to data changes, and the execution of SQL statements constructed at runtime, enabling more flexible database programming. Cursors facilitate sequential processing of query results, triggers automate actions in response to data modification events, and dynamic SQL allows for the generation and execution of queries based on variables or conditions. These features, while powerful, require careful use to maintain performance and security in SQL Server environments.[89][90][91]
Cursors in Transact-SQL enable the processing of result sets one row at a time, which is useful when set-based operations are insufficient for complex logic. A cursor is declared using the DECLARE CURSOR statement, specifying a SELECT query to define the result set, such as DECLARE employee_cursor CURSOR FOR SELECT EmployeeID, LastName FROM HumanResources.Employee;. The cursor is then opened with OPEN cursor_name, allowing rows to be fetched sequentially using FETCH NEXT FROM cursor_name INTO @variable, often within a loop that checks @@FETCH_STATUS = 0 to continue until no more rows are available. After processing, the cursor is closed with CLOSE cursor_name and deallocated with DEALLOCATE cursor_name to release resources. Cursors can be read-only or updatable, with options like FOR UPDATE OF column_list for modifications, but they are typically implemented within stored procedures for encapsulation.[89][92][93]
Triggers are special stored procedures that automatically execute in response to data manipulation language (DML) events on tables or views, enforcing business rules or maintaining data integrity without explicit calls. They are created using CREATE TRIGGER trigger_name ON table_name AFTER {INSERT, [UPDATE](/page/Update), DELETE} AS BEGIN ... END;, where the trigger body contains Transact-SQL statements to respond to the event. For instance, an AFTER INSERT trigger might log new records to an audit table. Special temporary tables, inserted and deleted, provide access to the affected rows: inserted holds new values for INSERT and [UPDATE](/page/Update) operations, while deleted holds old values for [UPDATE](/page/Update) and DELETE. INSTEAD OF triggers, which replace the triggering action, are particularly useful for views to simulate updatable behavior, though only one can exist per action on a table or view. Triggers fire once per qualifying statement, regardless of the number of rows affected, which simplifies logic but can impact performance on bulk operations.[90][94][95]
Dynamic SQL in Transact-SQL allows the construction and execution of SQL statements at runtime, accommodating scenarios where query structure depends on user input or variables. The recommended approach uses the system stored procedure sp_executesql, invoked as EXEC sp_executesql @stmt = N'SELECT * FROM table WHERE id = @id', N'@id [INT](/page/INT)', @id = @parameter_value;, where @stmt is the Unicode string of the dynamic query, the second parameter defines parameter types, and subsequent arguments supply values. This parameterization reuses execution plans across calls, enhancing performance, and mitigates SQL injection risks by separating code from data, unlike direct string concatenation with EXEC (@sql). Output parameters can be specified with OUTPUT to capture results, such as row counts or computed values. Dynamic SQL is often embedded in stored procedures to build queries conditionally, but it introduces overhead from plan compilation if not parameterized properly.[91][96]
Best practices for these features emphasize performance and security. Cursors should be avoided in favor of set-based operations whenever possible, as they process data sequentially and incur higher resource costs; use them sparingly for tasks like complex hierarchical updates. For triggers, employ INSTEAD OF variants on views for custom logic, set NOCOUNT ON to suppress row count messages, and test for recursion risks with sys.triggers. In dynamic SQL, always parameterize with sp_executesql to prevent injection and leverage plan caching; fully qualify object names to avoid resolution issues. Limitations include cursors' inability to parallelize effectively, making them unsuitable for large datasets, and triggers' per-statement firing, which may lead to unexpected behavior in multi-row operations without explicit row checks via @@ROWCOUNT. These elements can be integrated into stored procedures for modular code, but their use demands balancing flexibility against SQL Server's optimization strengths.[89][94][91]