SQL Stored Procedures: Parameter Sniffing - 10s to 4 Min
Parameter sniffing: a 500,000-order test account cached a table-scan plan, causing 4-minute waits for normal users.
20+ years shipping high-throughput database systems. Everything here is grounded in real deployments.
- ✓Deep production experience
- ✓Understanding of internals and trade-offs
- ✓Experience debugging complex systems
- A stored procedure is a named, precompiled SQL block stored in the database — called by name, not by sending SQL text each time
- Benefits: precompiled execution plan, reduced network round-trips, centralised data logic, database-level security control
- Parameter sniffing: the plan cached on first call may be suboptimal for different parameter values on later calls
- Dynamic SQL inside procedures re-introduces injection risk — always use sp_executesql with parameters, never string concatenation
- SQL Server: avoid the sp_ prefix — it forces a master database lookup before checking the current database
- Use procedures for complex multi-step logic; use ORMs for straightforward CRUD — do not over-engineer simple operations
Stored procedures are precompiled SQL code blocks stored in the database server, designed to encapsulate business logic, enforce security, and improve performance by reducing network round trips. They exist because raw SQL queries sent from application code often suffer from repeated parsing, plan generation, and permission checks—overhead that compounds under load.
In production systems handling thousands of requests per second, a well-tuned stored procedure can execute in milliseconds while its inline SQL equivalent chokes on plan cache bloat and parameter sniffing anomalies. Companies like Stack Overflow and GitHub rely on them for critical paths, but misuse—especially ignoring parameter sniffing—can turn a 10-second query into a 4-minute disaster when the first call's plan gets cached for wildly different data distributions.
In the ecosystem, stored procedures sit between raw inline SQL (flexible but prone to injection and plan cache pollution) and ORMs like Entity Framework or Hibernate (convenient but often generate inefficient queries and hide database complexity). You should use stored procedures when you need deterministic performance, fine-grained permission control (e.g., granting EXECUTE without direct table access), or complex transactional logic that benefits from server-side execution.
Avoid them when your schema changes frequently—altering procedures requires deployment coordination—or when you need dynamic query generation that ORMs handle more gracefully. The real trap is assuming all stored procedures are equal: scalar functions, table-valued functions, and parameterized procedures each have distinct execution semantics and plan caching behaviors that directly impact whether you get 10-second responses or 4-minute timeouts.
Parameter sniffing is the silent killer: SQL Server caches the execution plan based on the first call's parameter values, so a procedure optimized for a rare ID lookup gets reused for a common value returning millions of rows, causing index scans, memory grants, and blocking. Production-ready procedures mitigate this with OPTION (RECOMPILE), OPTIMIZE FOR UNKNOWN, or local variable tricks—but most developers don't know these exist until their Monday morning alert fires.
Permissions compound the problem: lazy DBAs grant db_owner to app accounts, bypassing the security model that makes procedures valuable, while proper setups use schema-bound procedures with EXECUTE AS or certificate signing to enforce least privilege. Understanding these mechanics separates engineers who ship reliable systems from those who debug production fires at 3 AM.
A stored procedure is like a saved recipe in the restaurant's official cookbook. Instead of every waiter describing the dish to the chef from scratch each time ('take two eggs, add flour, heat at 180...'), they just say 'make the carbonara'. The chef knows the recipe, has the tools ready, and executes it faster because the prep work was done in advance. Stored procedures work the same way — the database has the query precompiled and ready, so calling it is faster than sending raw SQL every time.
Stored procedures aren't just saved SQL; they're your database's contract with the application. Without them, you lose query plan stability, hand over performance to unpredictable ORM-generated garbage, and expose your schema to every junior dev with write access. When you skip proper parameter handling, that 10-second report suddenly takes four minutes, and your DBA starts getting midnight pages.
Why Parameter Sniffing Turns 10s Queries Into 4 Min Disasters
Parameter sniffing is a SQL Server optimization where the query optimizer uses the parameter values from the first execution to build a cached plan. That plan is then reused for all subsequent calls, regardless of the actual parameter values. The core mechanic: the optimizer sees the first value, assumes it's representative, and generates an execution plan optimized for that specific data distribution.
When the first execution uses a highly selective value (e.g., a rare customer ID), the plan might use an index seek with a nested loop. But if the next call uses a value matching 40% of the rows, that same plan forces millions of row-by-row lookups instead of a scan. The plan is cached, so every subsequent call pays the price. This is not a bug — it's the optimizer doing exactly what it was told, but with incomplete information.
Use parameter sniffing to your advantage when you know the first call's parameters are statistically typical. Disable it (via OPTION (OPTIMIZE FOR UNKNOWN) or query hints) when the workload has wildly varying selectivity — like reporting queries that toggle between "last hour" (few rows) and "last year" (most rows). In practice, this is the single most common cause of plan instability in OLTP systems.
Anatomy of a Production-Ready Stored Procedure
A robust stored procedure handles more than just running a query. It validates inputs, wraps operations in a transaction, handles errors explicitly, and returns meaningful result codes to the caller. The pattern below is a standard template for secure, transactional procedures.
SET NOCOUNT ON at the top suppresses the 'N rows affected' messages that can interfere with application code that checks rowcounts. BEGIN TRY / BEGIN CATCH provides structured error handling — without it, a runtime error leaves the transaction open and the caller gets an obscure error code. XACT_STATE() inside the catch checks whether the transaction is still committable before attempting a rollback.
CREATE PROCEDURE sp_UpdateProductStock @ProductID INT, @QuantityAdjustment INT, @UserID INT AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; -- Update stock level UPDATE Products SET StockLevel = StockLevel + @QuantityAdjustment, LastUpdatedBy = @UserID WHERE ProductID = @ProductID; -- Log the movement INSERT INTO InventoryAudit (ProductID, ChangeAmount, ChangedBy, LogDate) VALUES (@ProductID, @QuantityAdjustment, @UserID, GETDATE()); COMMIT TRANSACTION; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; -- Raise error to the calling application THROW; END CATCH END;
XACT_STATE() in the CATCH block is critical — if the transaction is in an uncommittable state (-1), you cannot commit, you can only roll back. Missing this check causes silent data integrity failures.XACT_STATE() are the three non-negotiable elements of a production-ready stored procedure.Parameter Sniffing — The Silent Performance Trap
When SQL Server first executes a stored procedure, it compiles an execution plan optimised for the specific parameter values provided on that call. This plan is then cached and reused for all subsequent calls, regardless of whether the new parameters would benefit from a different plan. This is parameter sniffing.
For most procedures, sniffing is beneficial — the compiled plan is reused without the overhead of recompilation. The problem occurs when the data distribution is highly variable. A parameter value that returns 5 rows might benefit from an index seek. A parameter value that returns 5 million rows might benefit from a table scan. When the cached plan is optimised for one extreme, it performs terribly for the other.
The three main fixes: WITH RECOMPILE forces a fresh plan on every call (eliminates the benefit of caching but prevents stale plans). OPTION(OPTIMIZE FOR UNKNOWN) generates a plan based on column statistics rather than the sniffed parameter value — a compromise. Local variables instead of direct parameters prevent sniffing entirely by breaking the parameter-value link.
-- PROBLEM: Plan cached for small customer gets reused for large customer CREATE PROCEDURE usp_GetCustomerOrders @CustomerID INT AS BEGIN SET NOCOUNT ON; SELECT order_id, total, created_at FROM orders WHERE customer_id = @CustomerID ORDER BY created_at DESC; END; -- FIX 1: WITH RECOMPILE -- generates fresh plan every call -- Use when data distribution is highly variable and recompilation cost is acceptable ALTER PROCEDURE usp_GetCustomerOrders @CustomerID INT WITH RECOMPILE AS BEGIN SET NOCOUNT ON; SELECT order_id, total, created_at FROM orders WHERE customer_id = @CustomerID ORDER BY created_at DESC; END; -- FIX 2: OPTION(OPTIMIZE FOR UNKNOWN) -- plan uses statistics, not sniffed value ALTER PROCEDURE usp_GetCustomerOrders @CustomerID INT AS BEGIN SET NOCOUNT ON; SELECT order_id, total, created_at FROM orders WHERE customer_id = @CustomerID ORDER BY created_at DESC OPTION(OPTIMIZE FOR (@CustomerID UNKNOWN)); END; -- FIX 3: Local variable -- breaks the parameter-value link ALTER PROCEDURE usp_GetCustomerOrders @CustomerID INT AS BEGIN SET NOCOUNT ON; DECLARE @LocalID INT = @CustomerID; -- breaks sniffing SELECT order_id, total, created_at FROM orders WHERE customer_id = @LocalID ORDER BY created_at DESC; END; -- Monitor procedure stats to detect sniffing problems early SELECT OBJECT_NAME(object_id) AS procedure_name, execution_count, total_elapsed_time / execution_count AS avg_elapsed_us, total_logical_reads / execution_count AS avg_logical_reads FROM sys.dm_exec_procedure_stats WHERE OBJECT_NAME(object_id) = 'usp_GetCustomerOrders';
When to Use Stored Procedures vs ORMs vs Inline SQL
Stored procedures are not always the right tool. The choice depends on the complexity of the logic, your team's SQL proficiency, deployment pipeline maturity, and whether you need database-level security.
Stored procedures are the right choice when: you need to grant users or services access to specific operations without exposing the underlying tables (database-level security); the logic involves multiple steps that must be atomic and the business rules are unlikely to change frequently; performance is critical and you want precompiled plans with minimal network overhead; or you are working in a corporate environment where database changes go through a DBA-controlled deployment process.
ORMs are the right choice for: straightforward CRUD operations where the ORM generates correct, parameterised SQL; when rapid iteration is needed and SQL refactoring via migrations is part of your workflow; teams where SQL expertise is limited and the ORM abstraction reduces errors.
Inline parameterised SQL is appropriate for: complex queries that are unique to one endpoint and do not warrant a named procedure; when the query logic needs to live with the application code for maintainability.
-- USE STORED PROCEDURE: multi-step atomic operation with business rules -- Cannot easily express this in a single ORM call CREATE PROCEDURE usp_PlaceOrder @CustomerID INT, @ProductID INT, @Quantity INT AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; -- 1. Check inventory DECLARE @Stock INT; SELECT @Stock = quantity FROM products WHERE product_id = @ProductID; IF @Stock < @Quantity RAISERROR('Insufficient stock', 16, 1); -- 2. Deduct inventory UPDATE products SET quantity = quantity - @Quantity WHERE product_id = @ProductID; -- 3. Create order INSERT INTO orders (customer_id, product_id, quantity, created_at) VALUES (@CustomerID, @ProductID, @Quantity, GETUTCDATE()); -- 4. Update customer lifetime value UPDATE customers SET lifetime_orders = lifetime_orders + 1 WHERE customer_id = @CustomerID; COMMIT TRANSACTION; END TRY BEGIN CATCH IF XACT_STATE() <> 0 ROLLBACK TRANSACTION; THROW; END CATCH; END; -- USE ORM: simple CRUD -- no business logic, let the framework handle it -- Python SQLAlchemy example: -- product = db.query(Product).filter_by(id=product_id).first() -- db.add(Order(customer_id=cid, product_id=product_id, quantity=qty)) -- db.commit() -- This is cleaner and more maintainable for straightforward operations
Types of Stored Procedures: Know What You're Actually Calling
Most devs treat every stored procedure the same. That's how you get production incidents. SQL Server ships with four distinct types, and mistaking one for another is how you accidentally invoke an OS shell from a query window or deploy CLR code that pegs the CPU at 100%.
System stored procedures (sp_ prefix) are Microsoft's internal utilities. They're great for metadata queries and admin tasks, but never modify them – that's how replication breaks. User-defined procedures (usp_ or no prefix) are your bread and butter. They encapsulate business logic and should be the default choice for application-facing operations.
Extended stored procedures (xp_ prefix) are legacy landmines. They call out to external DLLs and bypass SQL Server's security model entirely. CLR procedures are the modern replacement – they run .NET inside SQL Server's memory space. Useful for regex or complex math, but they add deployment overhead and make migration a nightmare. Use them only when T-SQL genuinely can't solve the problem.
// io.thecodeforge — database tutorial -- See exactly what type every proc in your database is SELECT name, type_desc, CASE WHEN name LIKE 'sp_%' THEN 'System' WHEN name LIKE 'xp_%' THEN 'Extended' WHEN name LIKE 'clr_%' THEN 'CLR' ELSE 'User-Defined' END AS procedure_category FROM sys.procedures ORDER BY procedure_category, name;
Permissions: The Lazy DBA's Nightmare and How to Fix It
The most common stored procedure mistake isn't a bad query plan – it's granting db_owner to a service account because 'the proc wasn't working'. That's how you get a ransomware bill. Stored procedures have a superpower called ownership chaining that most devs ignore.
When a procedure references a table, SQL Server checks permissions on the procedure itself, not the underlying table – as long as the procedure owner and table owner are the same. This means you can give an app user EXECUTE permission on a stored procedure and nothing else. No direct table access, no SELECT *, no accidental data exports.
Always use schemas to control ownership. Put all application procedures in a dedicated schema (like Sales or Inventory) and map that schema to a single owner. Then grant EXECUTE to the service account. Any attempt to query tables directly fails. Any attempt to inject a SELECT into your WHERE clause fails. The procedure becomes the only gateway to the data.
// io.thecodeforge — database tutorial -- Step 1: Create a dedicated schema and assign ownership CREATE SCHEMA Inventory AUTHORIZATION dbo; GO -- Step 2: Create a proc in that schema CREATE PROCEDURE Inventory.GetCurrentStock @ProductId INT AS BEGIN SET NOCOUNT ON; SELECT StockLevel, LastUpdated FROM Inventory.StockLevels WHERE ProductId = @ProductId; END; GO -- Step 3: Grant only EXECUTE to the app user CREATE USER AppUser FOR LOGIN AppLogin; GRANT EXECUTE ON SCHEMA::Inventory TO AppUser; -- AppUser can run the proc but can't touch the table directly
User-Defined Stored Procedures: The Scaffolding That Keeps Your Database From Collapsing
Production databases run on user-defined stored procedures — the ones you write. Not system procedures, not auto-generated ORM spaghetti. These are your contracts between application code and the data layer.
Why? Because a user-defined procedure is the only place you enforce business logic, parameter validation, and error handling in one atomic unit. ORMs give you SELECT * with a WHERE clause and pray. Your user-defined proc gives you SET NOCOUNT ON, explicit transactions, and a TRY-CATCH that logs the exact parameter values that blew up.
The HOW is brutal but necessary: every user-defined proc must start with parameter validation, use schema-qualified names, and never blindly trust client input. You are the last line of defense before corrupt data hits your fact tables.
// io.thecodeforge — database tutorial CREATE OR ALTER PROCEDURE dbo.usp_InsertOrder @CustomerID INT, @OrderTotal DECIMAL(10,2), @OrderDate DATETIME = NULL AS BEGIN SET NOCOUNT ON; -- Validation: production does not tolerate garbage IF @CustomerID IS NULL OR @CustomerID < 1 THROW 50001, 'CustomerID is required and must be positive.', 1; IF @OrderTotal <= 0 THROW 50002, 'OrderTotal must be greater than zero.', 1; SET @OrderDate = ISNULL(@OrderDate, GETUTCDATE()); BEGIN TRY INSERT INTO dbo.Orders (CustomerID, OrderTotal, OrderDate) VALUES (@CustomerID, @OrderTotal, @OrderDate); SELECT SCOPE_IDENTITY() AS NewOrderID; END TRY BEGIN CATCH THROW; END CATCH END;
Temporary Stored Procedures: Your Secret Weapon for Complex Batch Processing
You've got a 50-step ETL that runs once a quarter. You need to test a refactor without polluting your production schema. Enter the temporary stored procedure — #procname in tempdb.
Why use them? Because they die when your session ends. No cleanup scripts. No orphaned objects that the next DBA will curse. They live in the session-scoped namespace of tempdb, invisible to every other connection. Perfect for dynamic SQL generation, iterative transformations, or modularizing a monster batch script that management refuses to let you touch.
The HOW: prefix with # for session scope, ## for global (don't use global in prod — that's chaos). Temporary procs can access all your permanent tables, views, and functions. They support output parameters, error handling, everything a normal proc does — but they vanish like a good DevOps deployment.
// io.thecodeforge — database tutorial -- Create a temp proc for a one-time data transformation CREATE OR ALTER PROCEDURE #usp_TransformSalesData @BatchID INT AS BEGIN SET NOCOUNT ON; -- Transform raw staging data into fact table UPDATE dbo.FactSales SET NetAmount = RawAmount - Discount, ProcessedDate = GETUTCDATE() WHERE BatchID = @BatchID AND NetAmount IS NULL; SELECT @@ROWCOUNT AS RowsUpdated; END; -- Execute within same session EXEC #usp_TransformSalesData @BatchID = 101;
Use Transact-SQL
Transact-SQL (T-SQL) is the engine that makes stored procedures powerful. Most developers write basic SELECT/INSERT statements and miss the procedural capabilities that turn a script into a real program. T-SQL adds variables, loops, conditional logic, error handling (TRY...CATCH), and dynamic execution (sp_executesql). These features allow you to build procedures that adapt at runtime — filter by optional parameters, iterate over result sets without cursors using window functions, and roll back partial failures atomically. The WHY: Stored procedures exist to encapsulate complex logic close to the data. Without T-SQL's control flow, you're just saving a query in a file. The HOW: Start with DECLARE for variables, use IF/ELSE for branching, and wrap transactions in TRY...CATCH. Avoid cursor loops unless necessary; prefer set-based operations with ROW_NUMBER() or recursive CTEs. sp_executesql with parameters beats concatenated strings for security and plan reuse. Master T-SQL's procedural layer, and your stored procedures become reliable, maintainable programs — not query debt.
// io.thecodeforge — database tutorial CREATE PROCEDURE GetOrdersByStatus @Status NVARCHAR(20), @StartDate DATE = NULL AS BEGIN SET NOCOUNT ON; DECLARE @Sql NVARCHAR(MAX); SET @Sql = N'SELECT OrderId, OrderDate, Status FROM Orders WHERE Status = @St'; IF @StartDate IS NOT NULL SET @Sql += N' AND OrderDate >= @Sd'; EXEC sp_executesql @Sql, N'@St NVARCHAR(20), @Sd DATE', @St = @Status, @Sd = @StartDate; END;
EXEC() invites injection and plan cache bloat. Always use sp_executesql with typed parameters.Best Practices for Writing SQL Stored Procedures
Writing a stored procedure that works is easy. Writing one that survives production traffic and six months of maintenance is not. The WHY: A badly written procedure causes deadlocks, bloats the plan cache, and becomes unreadable spaghetti when the next developer inherits it. Best practices exist to prevent these predictable failures. The HOW: First, name clearly — usp_VerbNoun describes action and object. Always set SET NOCOUNT ON to suppress extra rowcount messages that break client drivers. Use BEGIN TRY...BEGIN CATCH for error handling; never let unhandled exceptions bubble up. Avoid SELECT * — always list columns explicitly to prevent breakage on schema changes. Use schema-qualified object names (dbo.Orders, not Orders) to avoid ambiguity. For parameters, match data types exactly to column definitions to avoid implicit conversions that kill index seeks. Keep transactions short — hold locks for the minimum time. Add a comment block at the top describing purpose, author, and change history. Finally, test with both a single row and 100k rows — parameter sniffing hurts only when you don't know your data distribution.
// io.thecodeforge — database tutorial CREATE PROCEDURE usp_GetInventoryByWarehouse @WarehouseId INT AS BEGIN SET NOCOUNT ON; BEGIN TRY SELECT ProductId, QuantityOnHand, ReorderLevel FROM dbo.Inventory WHERE WarehouseId = @WarehouseId ORDER BY ProductId; END TRY BEGIN CATCH DECLARE @ErrMsg NVARCHAR(4000) = ERROR_MESSAGE(); RAISERROR(@ErrMsg, 16, 1); RETURN -1; END CATCH; END;
User-Defined Stored Procedures: When Generic Just Won't Cut It
Why do you need user-defined stored procedures when system stored procedures already exist? Because your database has unique business logic that no one-size-fits-all solution can handle. User-defined stored procedures let you encapsulate custom validation, complex calculations, or multi-step operations that must execute atomically. The why is simple: you control the schema, the logic, and the execution plan. Before writing one, define the boundary between presentation and data logic. A user-defined procedure should never format output for a specific frontend — it should return data in a neutral, normalized shape. After you've decided what the procedure owns, keep it small: one business operation per procedure. Name it with a consistent prefix like usp_ for user stored procedures, and avoid dynamic SQL unless you have an ironclad justification. Test parameter combinations before deployment to catch plan instability. Remember: user-defined doesn't mean ungoverned — it means your team has signed up to maintain this contract for the life of the application.
// io.thecodeforge — database tutorial // User-defined stored proc for order summary CREATE OR ALTER PROCEDURE usp_GetOrderSummary @CustomerId INT, @StartDate DATE AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; SELECT o.OrderId, o.OrderDate, SUM(ol.Quantity * ol.UnitPrice) AS Total FROM dbo.Orders o INNER JOIN dbo.OrderLines ol ON o.OrderId = ol.OrderId WHERE o.CustomerId = @CustomerId AND o.OrderDate >= @StartDate GROUP BY o.OrderId, o.OrderDate ORDER BY o.OrderDate DESC; END;
Related Content: Where Stored Procedures Fit in the Database Ecosystem
Stored procedures don't live in isolation. They're the middle layer between your schema and your application, but their performance depends on the layers above and below. Related content you should study includes indexing strategies (without proper indexes, even the cleanest procedure will crawl), transaction and lock management (long procedures can block writes), and execution plan caching (understanding plan reuse prevents recompilation storms). Also review error handling patterns — TRY/CATCH in T-SQL differs drastically from application-level exception handling. For modern workflows, study how stored procedures interact with Entity Framework or Dapper: you'll need to map result sets correctly and avoid implicit transactions. Finally, monitoring tools like Query Store or extended events give visibility into procedure performance over time. Each of these topics directly affects whether your stored procedure runs in milliseconds or minutes. The why is clear: a stored procedure is only as good as the infrastructure supporting it. Without indexing, isolation levels, and monitoring, you're flying blind.
// io.thecodeforge — database tutorial // Quick index health check for proc support SELECT OBJECT_NAME(s.object_id) AS ProcName, i.name AS IndexName, i.type_desc, s.user_seeks, s.user_scans FROM sys.dm_db_index_usage_stats s INNER JOIN sys.indexes i ON s.object_id = i.object_id AND s.index_id = i.index_id WHERE OBJECT_NAME(s.object_id) LIKE 'usp_%' ORDER BY s.user_scans DESC;
Parameter Sniffing Caused a 10-Second Stored Procedure to Randomly Take 4 Minutes
- Parameter sniffing is silent — the procedure appears to work correctly until an atypical parameter value poisons the cached plan
- Use WITH RECOMPILE for procedures with highly variable parameter distributions (e.g., customer IDs spanning 5 to 500,000 rows)
- Monitor procedure execution statistics with sys.dm_exec_procedure_stats to detect plan cache issues proactively
| Approach | Performance | Security control | Testability | Best for |
|---|---|---|---|---|
| Stored Procedure | Best — precompiled plan | Database-level GRANT EXECUTE | Harder — requires DB state | Complex multi-step logic, security isolation |
| ORM | Good — parameterised | Application-level | Easy — unit testable | CRUD operations, rapid iteration |
| Inline parameterised SQL | Good | Application-level | Easy | Complex one-off queries close to application code |
| Raw string query | Varies | None — injection risk | Fragile | Never use in production |
| File | Command / Code | Purpose |
|---|---|---|
| update_inventory_procedure.sql | CREATE PROCEDURE sp_UpdateProductStock | Anatomy of a Production-Ready Stored Procedure |
| parameter_sniffing_fixes.sql | CREATE PROCEDURE usp_GetCustomerOrders | Parameter Sniffing |
| procedure_vs_orm_decision.sql | CREATE PROCEDURE usp_PlaceOrder | When to Use Stored Procedures vs ORMs vs Inline SQL |
| IdentifyProcedureTypes.sql | SELECT | Types of Stored Procedures |
| SecurePermissionsPattern.sql | CREATE SCHEMA Inventory AUTHORIZATION dbo; | Permissions |
| OrderInsert_Validate.sql | CREATE OR ALTER PROCEDURE dbo.usp_InsertOrder | User-Defined Stored Procedures |
| TempProc_BatchTransform.sql | CREATE OR ALTER PROCEDURE #usp_TransformSalesData | Temporary Stored Procedures |
| UseTransactSQL.sql | CREATE PROCEDURE GetOrdersByStatus | Use Transact-SQL |
| BestPractices.sql | CREATE PROCEDURE usp_GetInventoryByWarehouse | Best Practices for Writing SQL Stored Procedures |
| CreateUserProcedure.sql | CREATE OR ALTER PROCEDURE usp_GetOrderSummary | User-Defined Stored Procedures |
| RelatedCheck.sql | SELECT | Related Content |
Key takeaways
XACT_STATE() are non-negotiable in every production stored procedure.Common mistakes to avoid
3 patternsUsing the sp_ prefix for custom stored procedures in SQL Server
Ignoring parameter sniffing for procedures with variable data distributions
Using dynamic SQL with string concatenation inside a stored procedure
Interview Questions on This Topic
What is parameter sniffing in SQL Server and how do you fix it?
What is the difference between a stored procedure and a function in SQL?
When would you choose a stored procedure over an ORM query, and vice versa?
Frequently Asked Questions
Calling a stored procedure does not inherently prevent injection. If the procedure builds dynamic SQL with string concatenation internally, it is still vulnerable. Using parameterised inputs at the call site (passing parameters, not concatenated strings) prevents injection for the procedure's fixed queries. For dynamic SQL inside procedures, always use sp_executesql with parameter declarations — never concatenate user-controlled values into the SQL string.
For the first call, yes — procedures avoid the parsing and compilation overhead. For subsequent calls, the difference depends on whether execution plan caching (for inline SQL) offsets the procedure's precompile advantage. In practice, the difference is usually small. The bigger performance consideration is parameter sniffing — a well-designed inline query with OPTION hints can outperform a procedure with a stale cached plan.
20+ years shipping high-throughput database systems. Everything here is grounded in real deployments.
That's SQL Advanced. Mark it forged?
7 min read · try the examples if you haven't