Tag: Stored Procedures

  • Build a Production-Style SQL Server Backend: A Capstone Ticket System Project

    Build a Production-Style SQL Server Backend: A Capstone Ticket System Project

    This combines every chapter of the advanced track into one realistic deliverable: the backend for TicketDesk, a small support-ticket system, built the way a real backend actually gets built — ambiguous edges, several defensible designs, and a requirement to justify your choices, not just produce code that runs.

    TicketDesk: everything, one schema(each requirement maps to a chapter)Customercreates a ticketusp_Ticket_Create (Ch.1, Ch.4)Ticketstatus, priorityusp_Ticket_Assign (Ch.4)Agentmust be activeTicketAuditAFTER UPDATE trigger,set-based (Ch.7)TicketCommentauthor_type, bodyvw_AgentWorkloadmind the JOIN type — 0-ticket agents must still show (Ch.6)one schema, everychapter of the course ✓the app’s service account:least-privilege, neverdb_owner (Ch.10) 📌

    Schema Requirements

    • Agent: agent_id, name, email (unique), is_active
    • Customer: customer_id, name, email (unique)
    • Ticket: ticket_id, customer_id (FK), assigned_agent_id (FK, nullable — unassigned tickets are a valid state), status, priority, created_at, resolved_at
    • TicketComment: comment_id, ticket_id (FK), author_type, body, created_at
    • TicketAudit: populated automatically by a trigger — old_status, new_status, changed_at

    The Architecture, Visualized

    Customer Ticket Agent TicketAudit TicketComment

    Business Logic Requirements — Mapped to Where You Learned Each One

    Requirement Chapter it draws on
    fn_GetOpenTicketCount(@agentId) — scalar or inline TVF, with a justification comment for which type you chose and why Ch.2
    usp_Ticket_Create — TRY/CATCH + transaction, OUTPUT parameter for the new ticket_id Ch.1, Ch.4
    usp_Ticket_Assign — THROWs if the agent is not active Ch.4
    usp_Ticket_Resolve — THROWs if the ticket is already closed Ch.4, Ch.5
    AFTER UPDATE trigger on Ticket — logs every status change to TicketAudit, correctly set-based for multi-row updates Ch.7
    vw_AgentWorkload — one row per active agent, including agents with zero open tickets (mind the JOIN type) Ch.6
    -- A skeleton for one requirement, deliberately incomplete — you decide the JOIN type
    CREATE VIEW dbo.vw_AgentWorkload AS
    SELECT a.agent_id, a.name, COUNT(t.ticket_id) AS open_ticket_count
    FROM dbo.Agent a
    -- ??? JOIN dbo.Ticket t ON t.assigned_agent_id = a.agent_id AND t.status IN ('open','in_progress')
    WHERE a.is_active = 1
    GROUP BY a.agent_id, a.name;

    The blank above is deliberate: pick the wrong JOIN type here and agents with zero open tickets silently vanish from the report — the exact LEFT JOIN + WHERE-vs-ON distinction from the Fundamentals course, now applied inside a view that a real dashboard would depend on.

    Performance & Security Requirements

    • Populate Ticket with 5,000+ rows and design a covering/filtered index for “open tickets by agent, ordered by priority” — prove it with before/after STATISTICS IO (Ch.8)
    • Create a least-privilege service account for the application — not db_owner, with explicit GRANTs you can justify one by one (Ch.10)

    Self-Check Before You Consider It Done

    Check Why it matters
    Run a multi-row UPDATE against Ticket’s status column and confirm every changed row appears in TicketAudit Catches the single-row-assumption trigger bug from Ch.7
    Call usp_Ticket_Assign against an inactive agent Confirms your THROW logic actually fires, not just compiles
    Query vw_AgentWorkload and confirm an agent with zero tickets still appears, with count 0 Confirms the correct JOIN type from the skeleton above
    Log in as your least-privilege service account and confirm it genuinely cannot do more than granted The only real proof least-privilege was actually applied, not just declared

    Why This Is the Right Capstone

    Every chapter of this track shows up here: functions, procedures with proper error handling, a correctly set-based trigger, a view with the right JOIN type, indexing backed by real measurement, and least-privilege security. It mirrors how a real backend ticket actually gets built — ambiguous edges, multiple valid designs, and a requirement to justify your decisions, not just produce working code.

    What comes next: Combined with the Fundamentals capstone, you now have two complete, defensible schemas behind you — a good portfolio starting point. The Performance Tuning course picks up exactly where this leaves off: given a schema like TicketDesk under real load, how do you diagnose and fix what’s actually slow, using evidence rather than guesswork.

    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    You’ve completed the full curriculum! Check out SQL Server Fundamentals and SQL Server for Developers & DBAs, coming soon as structured courses on this site.

  • Stored Procedures vs Functions in SQL Server: When to Choose Which

    Stored Procedures vs Functions in SQL Server: When to Choose Which

    These get confused constantly, including in interviews. Here’s the clean distinction, grounded in everything you’ve built across Chapters 2 and 4 rather than as a fresh set of rules to memorize.

    Procedure vs Function(the one rule, sketched out)Need to WRITE data, orcontrol a TRANSACTION?(or return multiple result sets?)YESNOPROCEDURE✓ INSERT / UPDATE / DELETE✓ BEGIN/COMMIT/ROLLBACK, TRY/CATCH✓ can return many result sets✗ cannot be called inside SELECTFUNCTION✓ callable inside a SELECT / JOIN✓ composable, read-only building block✗ cannot write data (compile error)✗ no BEGIN/COMMIT or TRY/CATCH

    Side by Side

    Stored Procedure Function (any type)
    Modify data (INSERT/UPDATE/DELETE) Yes No — compile error if attempted
    Manage transactions (BEGIN/COMMIT/ROLLBACK) Yes No
    Callable inside a SELECT statement No Yes
    Return multiple result sets Yes No — exactly one value or one table
    Use TRY/CATCH Yes No
    Precompiled and cached like a procedure Yes Scalar/mTVF: yes; iTVF: inlines instead (Chapter 2)

    The One-Sentence Rule

    Need to write data, manage a transaction, or return multiple result sets? → Procedure. Otherwise → Function.

    Functions’ composability inside SELECT statements is their key advantage — but only for read-only, single-result-shape logic. The moment you need to write data or control a transaction explicitly, you’re in stored procedure territory, no exceptions. This isn’t a style preference; it’s enforced by the engine, as Chapter 2’s “side-effecting operator” error demonstrated directly.

    A Realistic Mixed Scenario

    Real applications typically need both, working together: an iTVF to expose a reusable, JOIN-friendly “active customers this quarter” query, and a stored procedure that uses that same iTVF internally as part of a larger workflow that also writes an audit log row and sends the result back to the caller.

    CREATE PROCEDURE dbo.usp_GenerateQuarterlyReport @quarter INT AS
    BEGIN
        SET NOCOUNT ON;
        -- Reuses an iTVF from earlier in the chapter for the read-only part
        SELECT * FROM dbo.GetEmployeesByDepartment('Sales');
        -- Then does something only a procedure can: write an audit trail
        INSERT INTO dbo.ReportLog (report_name, generated_at) VALUES ('Quarterly Sales', SYSDATETIME());
    END;

    This is the natural end state once you’ve internalized both tools: functions for the composable, read-only building blocks; procedures for orchestrating them alongside anything with a side effect.

    Practice tip: Look back at any function you wrote in Chapter 2 and ask: “if I needed this to also log who called it, could I?” The answer is no — that’s the exact moment a function needs to become, or be wrapped by, a procedure instead.

    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server for Developers & DBAs, coming soon on this site.

  • Error Handling and Transactions in SQL Server Stored Procedures: The Pattern to Memorize

    Error Handling and Transactions in SQL Server Stored Procedures: The Pattern to Memorize

    Production procedures need to fail safely — rolling back cleanly and surfacing a useful error, not leaving data half-changed. This lesson combines Chapter 1’s TRY/CATCH with the transaction concepts formalized fully in Chapter 9, into the single pattern you’ll reuse in nearly every write-capable procedure you ever write.

    The Pattern to Memorize(TRY/CATCH + transactions, one flow)BEGIN TRYwrap the workBEGIN TRANSACTIONone logical unitdo the workUPDATE / THROW on bad rowsCOMMITno error thrown — success pathCATCHsomething threw an errorabove — error pathIF @@TRANCOUNT>0ROLLBACK, then THROWGotcha: @@ROWCOUNT resetsafter almost EVERY statement —check it right after the statementit’s meant to describe. 📌

    The Complete Pattern

    CREATE PROCEDURE dbo.usp_UpgradeCustomerTier
        @customerId INT, @newTier NVARCHAR(20)
    AS
    BEGIN
        SET NOCOUNT ON;
        BEGIN TRY
            BEGIN TRANSACTION;
    
            UPDATE dbo.Customer SET tier = @newTier WHERE customer_id = @customerId;
    
            IF @@ROWCOUNT = 0
                THROW 51010, 'No customer found with the given ID.', 1;
    
            COMMIT TRANSACTION;
        END TRY
        BEGIN CATCH
            IF @@TRANCOUNT > 0
                ROLLBACK TRANSACTION;
    
            DECLARE @errMsg NVARCHAR(4000) = ERROR_MESSAGE();
            THROW 51011, @errMsg, 1;
        END CATCH
    END;

    @@ROWCOUNT is worth calling out on its own — it’s a system variable holding the number of rows affected by the most recent statement, and it resets after nearly every statement, including a PRINT. Check it immediately after the statement it’s meant to describe, or its value won’t mean what you think.

    Why @@TRANCOUNT Matters

    If the error happens BEFORE BEGIN TRANSACTION runs, calling ROLLBACK with no active transaction raises its own new error — check @@TRANCOUNT first

    The pattern to memorize: BEGIN TRY → BEGIN TRANSACTION → do the work → COMMIT, with a CATCH block that checks @@TRANCOUNT > 0 before rolling back, then re-throws or logs the error. This guard matters even more once procedures start calling other procedures: if this procedure was itself called from inside someone else’s already-open transaction, @@TRANCOUNT will be higher than 1, and a naive unconditional ROLLBACK here would undo work the caller is still relying on.

    Proving It Actually Rolls Back

    -- Deliberately trigger the THROW path and confirm no partial update survives
    SELECT tier FROM dbo.Customer WHERE customer_id = 99999; -- confirm this ID doesn't exist first
    EXEC dbo.usp_UpgradeCustomerTier @customerId = 99999, @newTier = 'premium';
    -- Msg 51011: No customer found with the given ID.
    SELECT * FROM dbo.Customer WHERE tier = 'premium' AND customer_id = 99999; -- confirms: nothing changed
    Practice tip: Run the failing call above yourself and confirm the error message AND the absence of any change. Then try it again with a valid customer_id and confirm the COMMIT path works. Seeing both branches fire for real is what turns “the pattern to memorize” into something you actually understand instead of copy-paste boilerplate.

    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server for Developers & DBAs, coming soon on this site.

  • Stored Procedure Parameters in SQL Server: Input, Output, Default, and Table-Valued

    Stored Procedure Parameters in SQL Server: Input, Output, Default, and Table-Valued

    Four parameter patterns cover almost everything you’ll need to build — from a simple optional filter to passing an entire list of values into a procedure without ever concatenating a string.

    Four ways to pass data in and outDEFAULT parameter@tier NVARCHAR(20)=’standard’omit it → default usedOUTPUT parametervalue flows caller ⇆ procneeds OUTPUT on BOTH sides ⚠️TABLE-VALUED parampass a whole list, READONLYtype-safe, multi-columnOLD WAY: CSV string‘Dana Park,Elena Petrova’breaks on embedded commasno type safety, one column onlyupgrade toNEW WAY: Table-Valued ParamCREATE TYPE … AS TABLE(…)many columns, real types, READONLYRemember: OUTPUT is required at the CALL SITE too —omit it there and SQL Server treats it as input-only.

    Default Parameters (Optional Input)

    CREATE PROCEDURE dbo.usp_CountCustomersByTier
        @tier NVARCHAR(20) = 'standard'
    AS
    BEGIN
        SET NOCOUNT ON;
        SELECT COUNT(*) AS customer_count FROM dbo.Customer WHERE tier = @tier;
    END;
    GO
    EXEC dbo.usp_CountCustomersByTier; -- uses default
    EXEC dbo.usp_CountCustomersByTier @tier = 'premium'; -- overrides it

    OUTPUT Parameters (Returning Values to the Caller)

    CREATE PROCEDURE dbo.usp_GetCustomerCount
        @tier NVARCHAR(20), @total INT OUTPUT
    AS
    BEGIN
        SET NOCOUNT ON;
        SELECT @total = COUNT(*) FROM dbo.Customer WHERE tier = @tier;
    END;
    GO
    
    DECLARE @count INT;
    EXEC dbo.usp_GetCustomerCount @tier = 'standard', @total = @count OUTPUT;
    PRINT 'Standard customers: ' + CAST(@count AS VARCHAR(10));
    Common mistake: Forgetting the OUTPUT keyword on the calling side, not just in the CREATE PROCEDURE definition. Without it at the call site too, SQL Server silently treats the parameter as input-only — your @count variable stays whatever it was before the call, with no error raised.

    Table-Valued Parameters: The Modern Way to Pass a List

    CREATE TYPE dbo.CustomerNameList AS TABLE (name NVARCHAR(100));
    GO
    
    CREATE PROCEDURE dbo.usp_GetCustomersByNames
        @Names dbo.CustomerNameList READONLY
    AS
    BEGIN
        SET NOCOUNT ON;
        SELECT c.customer_id, c.name, c.email
        FROM dbo.Customer c
        INNER JOIN @Names n ON n.name = c.name;
    END;
    GO
    
    DECLARE @list dbo.CustomerNameList;
    INSERT INTO @list VALUES ('Dana Park'), ('Elena Petrova');
    EXEC dbo.usp_GetCustomersByNames @Names = @list;

    TVPs are the correct, set-based way to pass a list into a procedure — far better than the old pattern of passing a comma-separated string and splitting it inside the procedure, which is exactly the kind of row-by-row string manipulation Chapter 1’s WHILE-loop lesson warned against. They’re always READONLY: you can read from them, never modify the caller’s table — attempting an UPDATE/DELETE/INSERT against @Names inside the procedure body is a compile error, by design.

    The Old Way, for Comparison

    -- The pre-TVP pattern (2005 and earlier, still seen in legacy code):
    CREATE PROCEDURE dbo.usp_GetCustomersByNames_Legacy @NameCsv NVARCHAR(MAX) AS
    BEGIN
        SELECT c.* FROM dbo.Customer c
        INNER JOIN STRING_SPLIT(@NameCsv, ',') s ON s.value = c.name; -- fragile: commas in names break it
    END;

    Beyond fragility with embedded delimiters, the string-splitting approach also loses type safety entirely (everything is text until parsed) and can’t easily pass more than one column of data per “row.” A TVP’s table type can have as many columns as you need, each with its own real data type.

    Practice tip: Extend the CustomerNameList table type to include a second column (say, a minimum tier to filter by per name), and adjust the procedure and JOIN accordingly. Seeing a TVP carry more than one column per row is what makes its advantage over a comma-separated string genuinely click.

    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server for Developers & DBAs, coming soon on this site.

  • Creating Stored Procedures in SQL Server: SET NOCOUNT ON and the Basics

    Creating Stored Procedures in SQL Server: SET NOCOUNT ON and the Basics

    Chapter 2’s function types kept hitting the same wall: none of them can modify data or manage a transaction. A stored procedure is precompiled, reusable, parameterized T-SQL logic that removes that wall entirely — it can perform INSERT/UPDATE/DELETE, manage transactions, use full TRY/CATCH, return multiple result sets, and doesn’t have to return anything at all. This is where the procedural half of T-SQL really begins.

    Functions hit a wall. Procedures walk through it.FUNCTIONtries: INSERT/UPDATE/DELETEtries: TRY/CATCH, BEGIN TRAN✗ Msg 443 errorBLOCKEDSTORED PROCEDURE✓ INSERT / UPDATE / DELETE    ✓ BEGIN TRAN … COMMIT / ROLLBACK✓ full TRY/CATCH    ✓ multiple result sets    ✓ return nothing at allcompiled ONCE, plan reusedfirst EXEC compiles the plan;later calls skip straight to running it→ this is where parameter sniffing comes fromALTER PROCEDURE keepsEXECUTE grants. DROP + CREATEsilently wipes them — they don’tcome back automatically. ⚠️

    Your First Procedure

    CREATE PROCEDURE dbo.usp_GetCustomersByTier
        @tier NVARCHAR(20)
    AS
    BEGIN
        SET NOCOUNT ON; -- near-universal best practice, see below
        SELECT customer_id, name, email FROM dbo.Customer WHERE tier = @tier;
    END;
    GO
    
    EXEC dbo.usp_GetCustomersByTier @tier = 'premium';
    -- Equivalent, positional call — works but is fragile if parameter order ever changes:
    EXEC dbo.usp_GetCustomersByTier 'premium';

    The usp_ prefix is a long-standing naming convention (“user stored procedure”) — avoid the older sp_ prefix specifically, since SQL Server always checks the system master database first for anything named sp_*, adding a small but real, entirely avoidable lookup cost to every call.

    Why SET NOCOUNT ON Matters More Than It Looks

    Without it: every DML statement sends an extra “(N rows affected)” message to the client This measurably slows procedures with loops or many statements

    Without SET NOCOUNT ON, this extra network chatter can measurably slow down procedures that loop or run many statements, and can actively interfere with some client libraries and reporting tools that misinterpret the extra “rows affected” messages as additional result sets. Put it at the top of every procedure by default — there is essentially never a reason not to.

    A Procedure Precompiles — What That Actually Means

    Unlike an ad-hoc query sent fresh from an application each time, a stored procedure’s execution plan is compiled once (on first call, or after certain invalidating events like a statistics update) and reused on subsequent calls. This is a real, measurable performance advantage for frequently-run logic — but it’s also the exact mechanism behind parameter sniffing, a real gotcha covered fully once you reach the Performance Tuning course: the plan compiled for the first parameter value seen gets reused for every subsequent call, even ones with very differently-shaped data.

    ALTER, DROP, and Modifying Procedures Safely

    -- Change the body without dropping and losing permissions granted on it
    ALTER PROCEDURE dbo.usp_GetCustomersByTier
        @tier NVARCHAR(20)
    AS
    BEGIN
        SET NOCOUNT ON;
        SELECT customer_id, name, email, tier FROM dbo.Customer WHERE tier = @tier; -- added tier column
    END;
    GO
    
    DROP PROCEDURE IF EXISTS dbo.usp_GetCustomersByTier;
    Common mistake: Using DROP + CREATE to “update” a procedure in a production script. If any user or role was explicitly granted EXECUTE permission on that specific procedure, dropping it removes those grants entirely — they don’t automatically come back when you recreate it. ALTER PROCEDURE preserves permissions and is the safer choice for modifying an existing procedure.
    Practice tip: Create the example procedure above, then run EXEC sp_helptext 'dbo.usp_GetCustomersByTier' to see SQL Server hand back the exact source text it stored. This is a genuinely useful habit for inspecting procedures on a server where you don’t have the original script handy.

    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server for Developers & DBAs, coming soon on this site.