Post

Mastering Server-Side Logic in Microsoft SQL Server

Mastering Server-Side Logic in Microsoft SQL Server

As software engineers, we often debate where business logic should reside. Should it be entirely in the application layer (C#, Java, Python) or within the database itself? While modern trends often favor the application layer, understanding server-side database programming is crucial for performance optimization, data consistency, and security.

In this deep dive, I will synthesize core concepts regarding Microsoft SQL Server architecture, T-SQL programming, and advanced constructs like stored procedures, cursors, and triggers.

1. The Architecture: Files, Schemas, and Services

Before writing code, it is vital to understand the environment. A SQL Server instance isn’t just a black box; it relies on specific services like the SQL Server Agent for automation and Analysis Services for BI tasks.

At the physical level, a database consists of two primary file types:

  • Data files (.mdf): Where the actual table data and objects reside.
  • Transaction logs (.ldf): Critical for ACID compliancehttps://4ykh4ncyb3r.github.io/posts/transactions/, recording all modifications to recovery.

Logically, we organize these objects using Schemas (with dbo being the default). Think of schemas as namespaces in programming; they provide logical separation and boundaries for security access control.


2. Transact-SQL (T-SQL): Beyond Standard SQL

T-SQL is Microsoft’s proprietary extension of SQL. It adds procedural programming features—variables, flow control, and error handling—to standard set-based operations.

Variable Assignment and Flow Control

Unlike standard SQL, T-SQL allows us to declare local variables and control execution flow with loops and conditionals.

Scenario: Let’s say we need to archive old logs, but we want to do it in small batches to avoid locking the table for too long.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- Original Example: Batch Deletion Loop
DECLARE @BatchSize INT = 1000;
DECLARE @RowsAffected INT = 1;

-- Loop until no more rows are deleted
WHILE @RowsAffected > 0
BEGIN
    DELETE TOP (@BatchSize)
    FROM AppLogs
    WHERE LogDate < DATEADD(YEAR, -1, GETDATE());

    -- Capture the count of deleted rows
    SET @RowsAffected = @@ROWCOUNT;
    
    -- Optional: Wait briefly to let other transactions breathe
    WAITFOR DELAY '00:00:01'; 
END

Concept Source: T-SQL extends SQL with variables and cycles.


3. Handling Identity Values: Scope Matters

A common pitfall in SQL development is retrieving the ID of a newly inserted record. The source material highlights three distinct ways to get this value, and using the wrong one can lead to critical data corruption in concurrent environments.

  • @@IDENTITY: Returns the last identity value generated in the current session, regardless of scope (e.g., if your insert triggers another insert elsewhere, you might get that ID).
  • SCOPE_IDENTITY(): The safest bet. Returns the last identity created in the current scope and current session.
  • IDENT_CURRENT('TableName'): Returns the last identity for a specific table, regardless of who (which session) performed the insert.

Best Practice: Always use SCOPE_IDENTITY() immediately after an INSERT to ensure you are linking to the correct parent record.

4. Cursors: The “Foreach” Loop of SQL

Relational databases are optimized for set-based operations (acting on all rows at once). However, sometimes we need row-by-row processing, which is where Cursors come in.

A cursor acts as a pointer that iterates through a result set. The lifecycle defined in the documentation is strict: DECLARE → OPEN → FETCH → CLOSE → DEALLOCATE.

Scenario: Calculating a running total where the logic depends heavily on the previous row’s calculated state (difficult in standard SQL).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
-- Original Example: Cursor for Sequential Processing
DECLARE @CurrentID INT, @CurrentValue DECIMAL(10,2);
DECLARE @RunningTotal DECIMAL(10,2) = 0;

-- 1. Declaration
DECLARE run_total_cursor CURSOR FAST_FORWARD FOR 
SELECT TransactionID, Amount FROM FinancialTransactions ORDER BY TransactionDate;

-- 2. Open
OPEN run_total_cursor;

-- 3. Fetch first row
FETCH NEXT FROM run_total_cursor INTO @CurrentID, @CurrentValue;

-- Check status (0 = success) [cite: 461]
WHILE @@FETCH_STATUS = 0
BEGIN
    SET @RunningTotal = @RunningTotal + @CurrentValue;

    -- Update logic (pseudo-code)
    -- UPDATE FinancialTransactions SET Balance = @RunningTotal WHERE ID = @CurrentID...

    -- Fetch next
    FETCH NEXT FROM run_total_cursor INTO @CurrentID, @CurrentValue;
END

-- 4. Close and Deallocate [cite: 469, 474]
CLOSE run_total_cursor;
DEALLOCATE run_total_cursor;

Note: While powerful, cursors can be performance bottlenecks and should be used sparingly.

5. Stored Procedures: Encapsulation and Transactions

Stored procedures are pre-compiled collections of SQL statements. They offer distinct advantages:

  1. Reduced Network Traffic: You send one command, not a script of hundreds of lines.
  2. Security: You can grant execute permissions on the procedure without exposing the underlying tables.
  3. Maintainability: Fix logic in one place (the DB) without redeploying client apps.

Transaction Management

Stored procedures are the ideal place to manage Transactions. A transaction ensures that a series of operations either all succeed or all fail (ACID).

Scenario: A bank transfer. We must deduct from Alice and add to Bob. If either fails, we must roll back.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
-- Original Example: Atomic Transaction
CREATE OR ALTER PROCEDURE ProcessTransfer
    @FromAccount INT,
    @ToAccount INT,
    @Amount DECIMAL(18,2)
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Start explicit transaction [cite: 192]
    BEGIN TRANSACTION;

    BEGIN TRY
        -- deduct
        UPDATE Accounts SET Balance = Balance - @Amount WHERE ID = @FromAccount;
        
        -- add
        UPDATE Accounts SET Balance = Balance + @Amount WHERE ID = @ToAccount;

        -- If we get here, commit changes
        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        -- If error, roll back everything
        IF @@TRANCOUNT > 0 
            ROLLBACK TRANSACTION;

        -- Re-throw the error to the application [cite: 608]
        THROW; 
    END CATCH
END

This uses structured error handling (TRY...CATCH), which is standard since SQL Server 2005, and THROW, which is the modern replacement for RAISERROR.


6. Triggers: Automated Auditing

Triggers are special stored procedures that fire automatically in response to events like INSERT, UPDATE, or DELETE. They are particularly useful for audit logging or enforcing complex integrity rules that constraints cannot handle.

In triggers, SQL Server exposes two virtual tables:

  • inserted: Contains the new version of the rows (for INSERT/UPDATE).
  • deleted: Contains the old version of the rows (for DELETE/UPDATE).

Scenario: We want to log every time a user changes their email address.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- Original Example: Audit Trigger
CREATE TRIGGER trg_UserEmailAudit
ON Users
AFTER UPDATE
AS
BEGIN
    SET NOCOUNT ON;

    -- Only run if Email column was updated
    IF UPDATE(Email) 
    BEGIN
        INSERT INTO AuditLog (UserID, OldEmail, NewEmail, ChangeDate)
        SELECT 
            i.UserID,
            d.Email, -- from 'deleted' (old value)
            i.Email, -- from 'inserted' (new value)
            GETDATE()
        FROM inserted i
        JOIN deleted d ON i.UserID = d.UserID;
    END
END

This utilizes the concept of accessing modified records via the logical tables available only within the trigger scope.


Final Thoughts

Moving logic to the database layer comes with trade-offs. It binds you to a specific platform (T-SQL is not portable like standard SQL) and can increase the processing load on your database server. However, for operations requiring high data integrity, transactional safety, and complex batch processing, the tools provided by SQL Server—Cursors, Procedures, and Triggers—are indispensable.


Attribution: This blog post is a synthesis of concepts derived from the “Microsoft SQL Server programming” lecture notes.

This post is licensed under CC BY 4.0 by the author.