As someone who's spent years architecting SQL Server solutions and reviewing code in enterprise environments, I can tell you that writing clean and maintainable T-SQL code separates intermediate developers from senior architects. While beginners focus on making queries work, intermediate developers must master code quality, performance optimization, and architectural patterns that scale across large teams and complex databases.
In this advanced guide, I'll share the T-SQL best practices I've learned through production incidents, performance tuning sessions, and code reviews at enterprise scale. We'll explore sophisticated techniques using the WideWorldImporters sample database, covering everything from error handling patterns to query optimization strategies that will elevate your SQL development skills.
Let me walk you through the professional-grade practices that will make your T-SQL code production-ready and maintainable.
Advanced Naming Conventions and Code Organization
Schema-Qualified Object References
Always use fully-qualified object names (schema.object) in production code. This eliminates ambiguity and improves query plan caching.
Problematic Approach:
SELECT CustomerName, PhoneNumber
FROM Customers -- Which schema? Sales? dbo?
WHERE CustomerID = 1001;Professional Standard:
SELECT
cust.CustomerName,
cust.PhoneNumber,
cust.DeliveryAddressLine1,
city.CityName,
sp.StateProvinceName
FROM Sales.Customers AS cust
INNER JOIN Application.Cities AS city
ON cust.DeliveryCityID = city.CityID
INNER JOIN Application.StateProvinces AS sp
ON city.StateProvinceID = sp.StateProvinceID
WHERE cust.CustomerID = 1001;Standardized Variable Naming with Hungarian Notation
Use prefixes to make data types immediately obvious in complex stored procedures.
CREATE PROCEDURE Sales.usp_GetCustomerOrderSummary
@piCustomerID INT,
@pdStartDate DATE,
@pdEndDate DATE,
@piMinOrderValue DECIMAL(18, 2) = 0,
@pbIncludeBackorders BIT = 0
AS
BEGIN
SET NOCOUNT ON;
DECLARE @dtProcessingDate DATETIME2 = SYSDATETIME();
DECLARE @nvCustomerName NVARCHAR(100);
DECLARE @iOrderCount INT;
-- Procedure logic here
END;Prefix Convention:
@pi= Parameter, Integer@pd= Parameter, Date@pb= Parameter, Bit/Boolean@nv= Variable, NVARCHAR@dt= Variable, DATETIME/DATETIME2@i= Variable, Integer@dc= Variable, Decimal
Robust Error Handling and Transaction Management
Implementing TRY-CATCH with Proper Transaction Control
Professional T-SQL code must handle errors gracefully and maintain data integrity through proper transaction management.
CREATE PROCEDURE Purchasing.usp_CreatePurchaseOrder
@piSupplierID INT,
@piContactPersonID INT,
@piDeliveryMethodID INT,
@tvOrderLines Purchasing.PurchaseOrderLineType READONLY
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON; -- Critical for proper error handling
DECLARE @iPurchaseOrderID INT;
DECLARE @dtOrderDate DATE = CAST(SYSDATETIME() AS DATE);
DECLARE @nvErrorMessage NVARCHAR(4000);
DECLARE @iErrorSeverity INT;
DECLARE @iErrorState INT;
BEGIN TRY
BEGIN TRANSACTION;
-- Validate supplier exists and is active
IF NOT EXISTS (
SELECT 1
FROM Purchasing.Suppliers
WHERE SupplierID = @piSupplierID
)
BEGIN
THROW 50001, 'Invalid SupplierID provided', 1;
END;
-- Insert purchase order header
INSERT INTO Purchasing.PurchaseOrders (
SupplierID,
OrderDate,
DeliveryMethodID,
ContactPersonID,
ExpectedDeliveryDate,
LastEditedBy
)
VALUES (
@piSupplierID,
@dtOrderDate,
@piDeliveryMethodID,
@piContactPersonID,
DATEADD(DAY, 14, @dtOrderDate),
1 -- System user
);
SET @iPurchaseOrderID = SCOPE_IDENTITY();
-- Insert order lines from table-valued parameter
INSERT INTO Purchasing.PurchaseOrderLines (
PurchaseOrderID,
StockItemID,
OrderedOuters,
ExpectedUnitPricePerOuter,
LastEditedBy
)
SELECT
@iPurchaseOrderID,
StockItemID,
OrderedOuters,
ExpectedUnitPricePerOuter,
1
FROM @tvOrderLines;
-- Verify at least one line was inserted
IF @@ROWCOUNT = 0
BEGIN
THROW 50002, 'Purchase order must contain at least one line item', 1;
END;
COMMIT TRANSACTION;
-- Return the new PurchaseOrderID
SELECT @iPurchaseOrderID AS PurchaseOrderID;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION;
END;
-- Capture error details
SELECT
@nvErrorMessage = ERROR_MESSAGE(),
@iErrorSeverity = ERROR_SEVERITY(),
@iErrorState = ERROR_STATE();
-- Log error to audit table (assuming one exists)
-- INSERT INTO dbo.ErrorLog (...)
-- Re-throw the error to the calling application
RAISERROR(@nvErrorMessage, @iErrorSeverity, @iErrorState);
RETURN -1;
END CATCH;
END;
GOKey Error Handling Principles:
- Always use
SET XACT_ABORT ONin procedures with transactions - Capture error details before re-throwing
- Roll back transactions in CATCH blocks
- Log errors for troubleshooting
- Return meaningful error codes
- Use THROW for modern error handling (SQL Server 2012+)
Performance-Optimized Query Patterns
Using Window Functions for Complex Analytics
Window functions eliminate self-joins and provide cleaner, more efficient code for analytical queries.
Calculate Running Totals and Rankings:
WITH CustomerOrderAnalytics AS (
SELECT
cust.CustomerID,
cust.CustomerName,
ord.OrderID,
ord.OrderDate,
SUM(line.Quantity * line.UnitPrice) AS OrderTotal,
-- Running total of sales per customer
SUM(SUM(line.Quantity * line.UnitPrice))
OVER (
PARTITION BY cust.CustomerID
ORDER BY ord.OrderDate
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS RunningTotal,
-- Rank orders by value within each customer
ROW_NUMBER()
OVER (
PARTITION BY cust.CustomerID
ORDER BY SUM(line.Quantity * line.UnitPrice) DESC
) AS OrderValueRank,
-- Calculate days since last order
DATEDIFF(
DAY,
LAG(ord.OrderDate) OVER (
PARTITION BY cust.CustomerID
ORDER BY ord.OrderDate
),
ord.OrderDate
) AS DaysSinceLastOrder
FROM Sales.Customers AS cust
INNER JOIN Sales.Orders AS ord
ON cust.CustomerID = ord.CustomerID
INNER JOIN Sales.OrderLines AS line
ON ord.OrderID = line.OrderID
WHERE ord.OrderDate >= '2016-01-01'
GROUP BY
cust.CustomerID,
cust.CustomerName,
ord.OrderID,
ord.OrderDate
)
SELECT
CustomerID,
CustomerName,
OrderID,
OrderDate,
OrderTotal,
RunningTotal,
OrderValueRank,
ISNULL(DaysSinceLastOrder, 0) AS DaysSinceLastOrder
FROM CustomerOrderAnalytics
WHERE OrderValueRank <= 5 -- Top 5 orders per customer
ORDER BY CustomerID, OrderDate;Optimizing with Proper Indexing Hints and Query Patterns
-- Complex query with explicit index hints for performance-critical scenarios
SELECT
item.StockItemID,
item.StockItemName,
item.UnitPrice,
hold.QuantityOnHand,
supplier.SupplierName,
-- Calculate months of inventory on hand
CASE
WHEN item.TypicalWeightPerUnit > 0 THEN
CAST(
hold.QuantityOnHand /
NULLIF(
(
SELECT AVG(line.Quantity)
FROM Sales.OrderLines AS line WITH (NOLOCK) -- Read uncommitted for reporting
WHERE line.StockItemID = item.StockItemID
AND line.OrderID IN (
SELECT OrderID
FROM Sales.Orders
WHERE OrderDate >= DATEADD(MONTH, -3, GETDATE())
)
),
0
) * 3 -- 3 months average
AS DECIMAL(10, 2)
)
ELSE NULL
END AS MonthsOfInventory
FROM Warehouse.StockItems AS item
INNER JOIN Warehouse.StockItemHoldings AS hold
ON item.StockItemID = hold.StockItemID
LEFT JOIN Purchasing.Suppliers AS supplier
ON item.SupplierID = supplier.SupplierID
WHERE item.IsChillerStock = 0
AND hold.QuantityOnHand > 0
ORDER BY MonthsOfInventory ASC;Advanced Code Modularity Techniques
Creating Reusable Table-Valued Functions
Table-valued functions promote code reuse and encapsulation of complex logic.
CREATE FUNCTION Sales.fn_GetCustomerOrderMetrics
(
@pdStartDate DATE,
@pdEndDate DATE,
@piMinimumOrders INT = 1
)
RETURNS TABLE
AS
RETURN
(
SELECT
cust.CustomerID,
cust.CustomerName,
cust.CustomerCategoryID,
COUNT(DISTINCT ord.OrderID) AS TotalOrders,
SUM(line.Quantity * line.UnitPrice) AS TotalRevenue,
AVG(line.Quantity * line.UnitPrice) AS AvgLineValue,
MIN(ord.OrderDate) AS FirstOrderDate,
MAX(ord.OrderDate) AS LastOrderDate,
DATEDIFF(DAY, MIN(ord.OrderDate), MAX(ord.OrderDate)) AS CustomerLifespanDays,
COUNT(DISTINCT line.StockItemID) AS UniqueProductsPurchased
FROM Sales.Customers AS cust
INNER JOIN Sales.Orders AS ord
ON cust.CustomerID = ord.CustomerID
INNER JOIN Sales.OrderLines AS line
ON ord.OrderID = line.OrderID
WHERE ord.OrderDate BETWEEN @pdStartDate AND @pdEndDate
GROUP BY
cust.CustomerID,
cust.CustomerName,
cust.CustomerCategoryID
HAVING COUNT(DISTINCT ord.OrderID) >= @piMinimumOrders
);
GO
-- Using the function in queries
SELECT
metrics.CustomerName,
metrics.TotalOrders,
metrics.TotalRevenue,
metrics.AvgLineValue,
metrics.UniqueProductsPurchased,
cat.CustomerCategoryName
FROM Sales.fn_GetCustomerOrderMetrics('2016-01-01', '2016-12-31', 5) AS metrics
INNER JOIN Sales.CustomerCategories AS cat
ON metrics.CustomerCategoryID = cat.CustomerCategoryID
WHERE metrics.TotalRevenue > 50000
ORDER BY metrics.TotalRevenue DESC;Implementing Dynamic SQL Safely
When dynamic SQL is necessary, use sp_executesql with parameters to prevent SQL injection.
CREATE PROCEDURE Sales.usp_GetOrdersByDynamicCriteria
@pnvOrderByColumn NVARCHAR(50),
@pnvSortDirection NVARCHAR(4) = 'ASC',
@pdStartDate DATE,
@pdEndDate DATE
AS
BEGIN
SET NOCOUNT ON;
-- Whitelist allowed column names to prevent SQL injection
IF @pnvOrderByColumn NOT IN ('CustomerName', 'OrderDate', 'OrderTotal', 'OrderID')
BEGIN
RAISERROR('Invalid sort column specified', 16, 1);
RETURN;
END;
IF @pnvSortDirection NOT IN ('ASC', 'DESC')
BEGIN
RAISERROR('Invalid sort direction specified', 16, 1);
RETURN;
END;
DECLARE @nvSQL NVARCHAR(MAX);
DECLARE @nvParams NVARCHAR(500);
SET @nvSQL = N'
SELECT
ord.OrderID,
cust.CustomerName,
ord.OrderDate,
SUM(line.Quantity * line.UnitPrice) AS OrderTotal,
COUNT(line.OrderLineID) AS LineItemCount
FROM Sales.Orders AS ord
INNER JOIN Sales.Customers AS cust
ON ord.CustomerID = cust.CustomerID
INNER JOIN Sales.OrderLines AS line
ON ord.OrderID = line.OrderID
WHERE ord.OrderDate BETWEEN @pdStartDateParam AND @pdEndDateParam
GROUP BY
ord.OrderID,
cust.CustomerName,
ord.OrderDate
ORDER BY ' + QUOTENAME(@pnvOrderByColumn) + ' ' + @pnvSortDirection + ';';
SET @nvParams = N'@pdStartDateParam DATE, @pdEndDateParam DATE';
EXEC sp_executesql
@nvSQL,
@nvParams,
@pdStartDateParam = @pdStartDate,
@pdEndDateParam = @pdEndDate;
END;
GOCode Documentation and Maintenance Strategies
Comprehensive Header Comments
Every stored procedure and function should have standardized header documentation.
/*******************************************************************************
* Procedure Name: Sales.usp_GenerateMonthlyRevenueReport
*
* Purpose: Generates aggregated revenue metrics by customer category and
* sales territory for monthly executive reporting
*
* Author: [Your Name]
* Created: 2024-12-14
* Modified: 2024-12-14
*
* Parameters:
* @piReportYear INT - The year for the report (Required)
* @piReportMonth INT - The month for the report (1-12) (Required)
* @pbIncludeBackorders BIT - Include backordered items in calculations (Default: 0)
* @pnvOutputFormat NVARCHAR(20) - 'SUMMARY' or 'DETAILED' (Default: 'SUMMARY')
*
* Returns: Result set with revenue metrics
*
* Error Handling: Uses TRY-CATCH with transaction rollback
*
* Performance: Optimized with CTE and proper indexing
* - Expected execution time: < 2 seconds for 1 year of data
* - Uses indexes: IX_Orders_OrderDate, IX_OrderLines_OrderID
*
* Dependencies:
* - Sales.Orders
* - Sales.OrderLines
* - Sales.Customers
* - Sales.CustomerCategories
*
* Example Usage:
* EXEC Sales.usp_GenerateMonthlyRevenueReport
* @piReportYear = 2016,
* @piReportMonth = 5,
* @pbIncludeBackorders = 0,
* @pnvOutputFormat = 'SUMMARY';
*
* Change History:
* Date Author Description
* ---------- -------------- ------------------------------------------------
* 2024-12-14 Developer Name Initial creation
*
*******************************************************************************/
CREATE PROCEDURE Sales.usp_GenerateMonthlyRevenueReport
@piReportYear INT,
@piReportMonth INT,
@pbIncludeBackorders BIT = 0,
@pnvOutputFormat NVARCHAR(20) = 'SUMMARY'
AS
BEGIN
-- Procedure implementation
END;
GOCommon Intermediate-Level Mistakes to Avoid
Mistake 1: Ignoring Query Execution Plans
Always analyze execution plans for queries that will run frequently or against large datasets.
-- Enable actual execution plan before running
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
-- Complex query with potential performance issues
SELECT
cust.CustomerName,
COUNT(DISTINCT ord.OrderID) AS OrderCount,
SUM(line.Quantity * line.UnitPrice) AS TotalRevenue
FROM Sales.Customers AS cust
LEFT JOIN Sales.Orders AS ord
ON cust.CustomerID = ord.CustomerID
LEFT JOIN Sales.OrderLines AS line
ON ord.OrderID = line.OrderID
WHERE ord.OrderDate >= '2016-01-01'
OR ord.OrderDate IS NULL -- This OR condition can hurt performance
GROUP BY cust.CustomerName
OPTION (RECOMPILE); -- Force recompilation to see current statistics
SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;Review the execution plan for:
- Index scans vs. index seeks
- Missing index recommendations
- High-cost operations (sorts, hash matches)
- Implicit conversions
- Parallelism issues
Mistake 2: Not Using Parameter Sniffing Awareness
Parameter sniffing can cause performance issues when the same procedure performs differently based on parameters.
-- Problem: First execution parameters affect all subsequent plans
CREATE PROCEDURE Sales.usp_GetOrdersByDateRange
@pdStartDate DATE,
@pdEndDate DATE
AS
BEGIN
SET NOCOUNT ON;
-- Without OPTION(RECOMPILE) or OPTION(OPTIMIZE FOR),
-- this query may have inconsistent performance
SELECT
ord.OrderID,
ord.OrderDate,
cust.CustomerName,
SUM(line.Quantity * line.UnitPrice) AS OrderTotal
FROM Sales.Orders AS ord
INNER JOIN Sales.Customers AS cust
ON ord.CustomerID = cust.CustomerID
INNER JOIN Sales.OrderLines AS line
ON ord.OrderID = line.OrderID
WHERE ord.OrderDate BETWEEN @pdStartDate AND @pdEndDate
GROUP BY
ord.OrderID,
ord.OrderDate,
cust.CustomerName
OPTION (RECOMPILE); -- Force new plan each execution for varying data ranges
END;
GOMistake 3: Improper Temp Table vs Table Variable Usage
Understanding when to use each temporary storage option is critical for performance.
CREATE PROCEDURE Warehouse.usp_AnalyzeInventoryTurnover
AS
BEGIN
SET NOCOUNT ON;
-- Use temp table for large result sets (better statistics)
CREATE TABLE #StockMovement (
StockItemID INT NOT NULL,
StockItemName NVARCHAR(100),
TotalSold INT,
AvgQuantityPerOrder DECIMAL(10, 2),
LastSaleDate DATE,
PRIMARY KEY CLUSTERED (StockItemID)
);
INSERT INTO #StockMovement
SELECT
item.StockItemID,
item.StockItemName,
SUM(line.Quantity) AS TotalSold,
AVG(line.Quantity) AS AvgQuantityPerOrder,
MAX(ord.OrderDate) AS LastSaleDate
FROM Warehouse.StockItems AS item
INNER JOIN Sales.OrderLines AS line
ON item.StockItemID = line.StockItemID
INNER JOIN Sales.Orders AS ord
ON line.OrderID = ord.OrderID
WHERE ord.OrderDate >= DATEADD(YEAR, -1, GETDATE())
GROUP BY
item.StockItemID,
item.StockItemName;
-- Now use the temp table in subsequent queries
SELECT
sm.StockItemName,
sm.TotalSold,
sm.AvgQuantityPerOrder,
sm.LastSaleDate,
hold.QuantityOnHand,
CASE
WHEN sm.AvgQuantityPerOrder > 0 THEN
CAST(hold.QuantityOnHand / sm.AvgQuantityPerOrder AS DECIMAL(10, 1))
ELSE NULL
END AS MonthsOfInventory
FROM #StockMovement AS sm
INNER JOIN Warehouse.StockItemHoldings AS hold
ON sm.StockItemID = hold.StockItemID
ORDER BY MonthsOfInventory DESC;
-- Cleanup
DROP TABLE #StockMovement;
END;
GOGuidelines:
- Use temp tables (#TempTable) for:
- Large result sets (> 100 rows)
- When you need statistics and indexes
- Multiple operations on the same data
- Use table variables (@TableVar) for:
- Small result sets (< 100 rows)
- Simple operations
- When transaction logging overhead matters
Advanced Testing and Validation Patterns
Unit Testing Your T-SQL Code
Implement validation checks within your procedures for data quality assurance.
CREATE PROCEDURE Sales.usp_ValidateAndProcessOrder
@piOrderID INT
AS
BEGIN
SET NOCOUNT ON;
DECLARE @iErrorCount INT = 0;
DECLARE @nvErrorMessages NVARCHAR(MAX) = '';
-- Validation 1: Order exists
IF NOT EXISTS (
SELECT 1
FROM Sales.Orders
WHERE OrderID = @piOrderID
)
BEGIN
SET @iErrorCount += 1;
SET @nvErrorMessages += 'Order does not exist. ';
END;
-- Validation 2: Order has line items
IF NOT EXISTS (
SELECT 1
FROM Sales.OrderLines
WHERE OrderID = @piOrderID
)
BEGIN
SET @iErrorCount += 1;
SET @nvErrorMessages += 'Order has no line items. ';
END;
-- Validation 3: All items are in stock
IF EXISTS (
SELECT 1
FROM Sales.OrderLines AS line
INNER JOIN Warehouse.StockItemHoldings AS hold
ON line.StockItemID = hold.StockItemID
WHERE line.OrderID = @piOrderID
AND hold.QuantityOnHand < line.Quantity
)
BEGIN
SET @iErrorCount += 1;
SET @nvErrorMessages += 'Insufficient stock for one or more items. ';
END;
-- If validation fails, return errors
IF @iErrorCount > 0
BEGIN
SELECT
@piOrderID AS OrderID,
'VALIDATION_FAILED' AS Status,
@iErrorCount AS ErrorCount,
@nvErrorMessages AS ErrorMessages;
RETURN;
END;
-- Process order (actual business logic here)
SELECT
@piOrderID AS OrderID,
'SUCCESS' AS Status,
0 AS ErrorCount,
'Order validated and processed successfully.' AS Message;
END;
GOSummary: Enterprise-Grade T-SQL Development
Mastering clean, maintainable T-SQL code at the intermediate level requires discipline, architectural thinking, and attention to production realities. Let's recap the essential practices:
✅ Always use schema-qualified names and consistent naming conventions
✅ Implement comprehensive error handling with TRY-CATCH and transaction control
✅ Leverage window functions for efficient analytical queries
✅ Create reusable functions and modular code components
✅ Use dynamic SQL safely with sp_executesql and parameter validation
✅ Document thoroughly with standardized header comments
✅ Understand execution plans and optimize query performance
✅ Be aware of parameter sniffing and plan caching issues
✅ Choose appropriate temp storage based on data volume and operations
✅ Validate data quality within your procedures
✅ Test rigorously before deploying to production
Remember: clean code isn't just about aesthetics—it's about creating reliable, performant, maintainable database solutions that scale with your organization. The patterns and practices demonstrated with WideWorldImporters provide a foundation you can apply immediately to real-world projects.
As you continue developing your skills, focus on understanding why these practices matter in production environments, not just how to implement them. Performance, maintainability, and reliability are the hallmarks of professional SQL development.
Frequently Asked Questions
Q: When should I use stored procedures versus inline SQL in my application?
A: Use stored procedures for complex business logic, operations requiring multiple statements, or frequently-executed queries that benefit from plan caching. Use inline SQL for simple, dynamic queries where the flexibility of building SQL in application code outweighs the benefits of encapsulation. In enterprise environments, stored procedures provide better security through permission management and easier performance tuning.
Q: How do I handle deadlocks in production T-SQL code?
A: Implement retry logic in your application layer for deadlock victims (error 1205). In T-SQL, minimize deadlock potential by accessing tables in the same order across procedures, keeping transactions short, using appropriate isolation levels (consider READ COMMITTED SNAPSHOT), and ensuring proper indexing. Use Extended Events or SQL Profiler to identify deadlock patterns, then analyze the deadlock graph to understand which resources are causing conflicts.
Q: What's the best way to manage database migrations and schema changes in version-controlled environments?
A: Use a migration-based approach with tools like SQL Server Data Tools (SSDT), Flyway, or custom migration scripts. Maintain scripts in version control with timestamps or version numbers, include both UP (apply) and DOWN (rollback) scripts, test migrations in development and staging environments first, and always backup production before deploying. For complex changes, use techniques like expand-contract pattern: add new columns/tables first, migrate data, then remove old structures.