{"id":2694,"date":"2014-11-09T10:59:35","date_gmt":"2014-11-09T18:59:35","guid":{"rendered":"http:\/\/3.209.169.194\/blogs\/kimberly\/?p=2694"},"modified":"2020-10-30T12:44:10","modified_gmt":"2020-10-30T19:44:10","slug":"high-performance-procedures","status":"publish","type":"post","link":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/high-performance-procedures\/","title":{"rendered":"Building High Performance Stored Procedures"},"content":{"rendered":"<p>[NOTE: Update 12 Nov 2014. The most recent bug (KB article\u00a0<a href=\"http:\/\/support.microsoft.com\/kb\/2965069\" target=\"_blank\" rel=\"noopener noreferrer\">KB2965069<\/a>) is THANKFULLY very unlikely for most environments (mentioned below). As a result, I&#8217;m glad to say that we can use OPTION (RECOMPILE) as a much\u00a0easier (and safer) solution. So, while I&#8217;ll still leave solution 3 as an option if you run into troubles with OPTION (RECOMPILE), I can luckily say that if you&#8217;re on the latest SP\/CU &#8211; the problems are incredibly unlikely. The BEST hybrid solution is shown below as solution 2.]<\/p>\n<p>In my SQL PASS Summit 2014 session last week, I spoke about building high performance stored procedures and properly dealing with parameter sniffing problems&#8230; if you&#8217;re interested, the session was recorded for PASStv and you can watch it <a href=\"http:\/\/bit.ly\/1xoDfH8\" target=\"_blank\" rel=\"noopener noreferrer\">here<\/a>.<\/p>\n<p><strong>UPDATE October 2020:<\/strong> Recently, I gave this session for a PASS DBA Virtual Chapter (September 2020) and you can view this through the PASS site (PASS.org \/ Learn (drop-down menu) \/ All Recordings \/ Filter for Tripp). The first in the list will be the most recent delivery. I did a long demo explaining Code Coverage testing vs. Plan Stability testing. This is a great session to have your developers watch! Then, for more detail: consider the Pluralsight courses on Optimizing Procedural Code (Part 1 and Part 2). <strong>Thanks!<\/strong><\/p>\n<p>It all ties to something incredibly common in development &#8211; I call it the multipurpose\u00a0procedure. Simply put, it&#8217;s one application dialog with many possible options and then sitting behind it &#8211; ONE stored procedure. This one stored procedure is supposed to handle every possible case of execution with multiple parameters. For example, imagine a dialog to search\u00a0for a customer.<\/p>\n<p><a href=\"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-content\/uploads\/2014\/11\/CustomerSearch.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-2696\" src=\"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-content\/uploads\/2014\/11\/CustomerSearch.jpg\" alt=\"CustomerSearch\" width=\"709\" height=\"453\" srcset=\"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-content\/uploads\/2014\/11\/CustomerSearch.jpg 709w, https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-content\/uploads\/2014\/11\/CustomerSearch-300x191.jpg 300w\" sizes=\"auto, (max-width: 709px) 100vw, 709px\" \/><\/a><\/p>\n<p>The user can enter in any combination of these elements and the procedure&#8217;s header looks like this:<\/p>\n<pre><pre class=\"brush: sql; title: ; toolbar: true; wrap-lines: true; notranslate\" title=\"\">\nCREATE PROC [dbo].[GetCustomerInformation]\n(\n    @CustomerID      INT = NULL\n    , @LastName\t     VARCHAR (30) = NULL\n    , @FirstName     VARCHAR (30) = NULL\n    , @MiddleInitial CHAR(1) = NULL\n    , @EmailAddress  VARCHAR(128) = NULL\n    , @Region_no     TINYINT = NULL\n    , @Cust_code     TINYINT = NULL\n)\n...<\/pre>\n<p>And then the procedure&#8217;s main WHERE clause looks like this:<\/p>\n<pre><pre class=\"brush: sql; title: ; toolbar: true; wrap-lines: true; notranslate\" title=\"\">\nWHERE ([C].[CustomerID] = @CustomerID OR @CustomerID IS NULL)\n    AND ([C].[lastname] LIKE @LastName OR @LastName IS NULL)\n    AND ([C].[firstname] LIKE @FirstName OR @FirstName IS NULL)\n    AND ([C].[middleinitial] = @MiddleInitial OR @MiddleInitial IS NULL)\n    AND ([C].[EmailAddress] LIKE @EmailAddress OR @EmailAddress IS NULL)\n    AND ([C].[region_no] = @Region_no OR @Region_no IS NULL)\n    AND ([C].[cust_code] = @Cust_code OR @Cust_code IS NULL)GO<\/pre>\n<p>Or, possibly like this:<\/p>\n<pre><pre class=\"brush: sql; title: ; toolbar: true; wrap-lines: true; notranslate\" title=\"\">\nWHERE [C].[CustomerID] = COALESCE(@CustomerID, [C].[CustomerID])\n    AND [C].[lastname] LIKE COALESCE(@lastname, [C].[lastname])\n    AND [C].[firstname] LIKE COALESCE(@firstname, [C].[firstname])\n    AND [C].[middleinitial] = COALESCE(@MiddleInitial, [C].[middleinitial])\n    AND [C].[Email] LIKE COALESCE(@EmailAddress, [C].[Email])\n    AND [C].[region_no] = COALESCE(@Region_no, [C].[region_no])\n    AND [C].[cust_code] = COALESCE(@Cust_code, [C].[Cust_code]);<\/pre>\n<p>Or, maybe even like this:<\/p>\n<pre><pre class=\"brush: sql; title: ; toolbar: true; wrap-lines: true; notranslate\" title=\"\">\nWHERE [C].[CustomerID] = CASE WHEN @CustomerID IS NULL THEN [C].[CustomerID] ELSE @CustomerID END\n    AND [C].[lastname] LIKE CASE WHEN @lastname IS NULL THEN [C].[lastname] ELSE @lastname END\n    AND [C].[firstname] LIKE CASE WHEN @firstname IS NULL THEN [C].[firstname] ELSE @firstname END\n    AND [C].[middleinitial] = CASE WHEN @MiddleInitial IS NULL THEN [C].[middleinitial] ELSE @MiddleInitial END\n    AND [C].[Email] LIKE CASE WHEN @EmailAddress IS NULL THEN [C].[Email] ELSE @EmailAddress END\n    AND [C].[region_no] = CASE WHEN @Region_no IS NULL THEN [C].[region_no] ELSE @Region_no END\n    AND [C].[cust_code] = CASE WHEN @Cust_code IS NULL THEN [C].[cust_code] ELSE @Cust_code END<\/pre>\n<p>But, no matter which of these it looks like &#8211; <span style=\"color: #ff0000;\"><strong>they&#8217;re all going to perform horribly<\/strong><\/span>. An OSFA procedure does not optimize well and the end result is that you&#8217;ll get one plan in cache. If that was\u00a0generated from\u00a0a different combination of parameters\u00a0than the ones you&#8217;re executing, you might get an absolutely abysmal plan. The concept is fairly simple &#8211; when a procedure executes and there isn&#8217;t already a plan in cache (for that procedure), then SQL Server has to generate one. To do so it &#8220;sniffs&#8221; the input parameters and optimizes based on the parameters sniffed. This is the plan that gets stored with the procedure and saved for subsequent executions. Depending on your workload characteristics, you might end up with a very common plan in cache and other users also executing the common parameters might see reasonable performance (especially if the WHERE\u00a0does NOT use LIKE and has nothing but equality-based criteria [those aren&#8217;t quite as bad as this one]). But, there will still be cases where an atypical execution performs horribly.<\/p>\n<p>And,\u00a0unfortunately, sometimes the worst combination happens &#8211; a very atypical execution is the one that gets sniffed and then everyone suffers.<\/p>\n<p>So, the admin might update statistics or force a recompile (or, even restart SQL Server) to try and get around this problem. These things &#8220;fix&#8221; the immediate problem by kicking the plan out of cache but they are NOT A SOLUTION.<\/p>\n<h2>Solution 1: The Simple Solution &#8211; OPTION (RECOMPILE)<\/h2>\n<p>Since SQL Server 2005, we&#8217;ve had an option to add OPTION (RECOMPILE) to the offending statement. When OPTION (RECOMPILE) works &#8211; it works incredibly well. However, there have been bugs and you&#8217;ll want to make sure that you&#8217;re on the latest SP or CU. It&#8217;s definitely had a bit of a\u00a0checkered past. Here&#8217;s a list of issues and links to KB articles \/ bugs around this problem:<\/p>\n<ul>\n<li><a href=\"http:\/\/support.microsoft.com\/kb\/2965069\" target=\"_blank\" rel=\"noopener noreferrer\">KB2965069<\/a>\u00a0FIX: Incorrect result when you execute a query that uses WITH RECOMPILE option in SQL Server 2012 or SQL Server 2014. Fixed in:\n<ul>\n<li>Cumulative Update 4 for SQL Server 2014<\/li>\n<li>Cumulative Update 2 for SQL Server 2012 SP2<\/li>\n<li>Cumulative Update 11 for SQL Server 2012 SP1<\/li>\n<\/ul>\n<\/li>\n<li><a href=\"http:\/\/support.microsoft.com\/kb\/968693\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"broken_link\">KB968693<\/a>\u00a0FIX:\u00a0A query that uses parameters and the RECOMPILE option returns incorrect results when you run the query in multiple connections concurrently in SQL Server 2008. Fixed in:\n<ul>\n<li>SQL Server 2008 CU4<\/li>\n<li>SQL Server 2008 R2 CU1<\/li>\n<\/ul>\n<\/li>\n<li>I don&#8217;t have a KB article link for this one but in both SQL Server 2008 and SQL Server 2008 R2 there are versions where some performance related features were\u00a0unable. For example,\u00a0filtered indexes could not be used inside\u00a0of stored procedures even if you used OPTION (RECOMPILE) because of changes with the behavior or bugs. So, I&#8217;d fall back on using DSE (shown by solution 3).<\/li>\n<\/ul>\n<p>Anyway, despite these issues (the first one listed above &#8211; is INCREDIBLY unlikely), <strong>I LOVE the simplicity of OPTION(RECOMPILE)<\/strong> because all you have to do is change the offending statement like this:<\/p>\n<pre><pre class=\"brush: sql; title: ; toolbar: true; wrap-lines: true; notranslate\" title=\"\">\nSELECT ...\nFROM ...\nWHERE ...\nOPTION (RECOMPILE);\n<\/pre>\n<p>The reason I love this is for the simplicity of the statement. The reason that I have an issue with just doing this as a standard is two-fold:<\/p>\n<ul>\n<li>First, if you do this too often (or, for extremely frequently executed procedures), you might end up using too much CPU.<\/li>\n<li>Second, I&#8217;ve seen some environments where this is used to fix ONE problem and then it becomes a standard for ALL problems. This DEFINITELY should NOT become a standard coding practice. You only want to use this if your testing shows parameter sniffing problems and plan unstability. Stored procedures that have stable plans should be compiled, saved, and reused.<\/li>\n<\/ul>\n<p>A better solution is one where you recompile for unstable plans but for stable plans &#8211; you place those in cache for reuse.<\/p>\n<h2>A Hybrid Solution &#8211; recompile when unstable, cache when stable<\/h2>\n<p>My main problem with adding OPTION (RECOMPILE) to many statements is\u00a0that it can be costly to do when you&#8217;re executing thousands of procedures over and over again. And, if you really think about it, not all combinations really need to be recompiled.<\/p>\n<p>For example, take an easier combination of just 3 parameters: CustomerID, Lastname, and Firstname. The possible combinations for execution will be:<\/p>\n<ol>\n<li>CustomerID alone<\/li>\n<li>Lastname alone<\/li>\n<li>Firstname alone<\/li>\n<li>CustomerID and Lastname together<\/li>\n<li>CustomerID and Firstname together<\/li>\n<li>Firstname and Lastname together<\/li>\n<li>Or, all three &#8211; CustomerID, Lastname, and Firstname<\/li>\n<\/ol>\n<p>My questions for you to consider are:<\/p>\n<ul>\n<li>Are all of those going to want the same execution plan?<\/li>\n<li>And, what if they supply wildcards?<\/li>\n<\/ul>\n<p>The most important consideration is that ANY query that submits CustomerID should NOT be recompiled, right? Think about it &#8211; if someone says I want to search for all of the people that have an &#8216;e&#8217; in their last name (Lastname LIKE &#8216;%e%&#8217;) AND that have an &#8216;a&#8217; in their first name (Firstname LIKE &#8216;%a%&#8217;) AND that have a CustomerID of 123456789 then all of a sudden this becomes an incredibly simple query. We&#8217;re going to go an lookup CustomerID 123456789 and then check to see if they have an &#8216;a&#8217; in their first name and an &#8216;e&#8217; in their last name. But, the lookup is incredibly simple. So, ANY execution where CustomerID is supplied should NOT be recompiled; the plan for those executions is STABLE. However, Lastname and Firstname searches might be highly unstable &#8211; especially if there&#8217;s a leading wildcard. So, if wildcard is present then let&#8217;s recompile if they don&#8217;t supply at least 3 characters for BOTH the first name and last name &#8211; preceding the wildcard character. And, it&#8217;s also OK if they don&#8217;t supply a wildcard at all.\u00a0Then, the overall number of recompilations will be much lower &#8211; we&#8217;ll save CPU and we&#8217;ll get better performance by using a lot of STABLE, precompiled plans.<\/p>\n<p>To do this, there are TWO versions. Solution 2 uses OPTION (RECOMPILE) strategically. Solution 3 using dynamic string execution instead.<\/p>\n<h3>Solution 2 &#8211; recompile when unstable [by adding\u00a0OPTION (RECOMPILE)], cache when stable (using sp_executesql)<\/h3>\n<p>There are absolutely some criteria when plan stability can be predicted. For these, we will build the statement to concatenate only the non-NULL parameters and then execute normally using\u00a0sp_executesql. For where it&#8217;s unknown or unstable, we&#8217;ll recompile by\u00a0OPTION (RECOMPILE) to the statement. For all executions, use sp_executesql. And, of course, this is ONLY substitution for parameters. And, this should only be done for the most frequently executed procedures. You\u00a0don&#8217;t have to do this for every procedure\u00a0but it&#8217;s extremely beneficial for things that are heavily executed.<\/p>\n<pre><pre class=\"brush: sql; title: ; toolbar: true; wrap-lines: true; notranslate\" title=\"\">\n----------------------------------------------\n-- Solution 2\n----------------------------------------------\n\nCREATE PROC [dbo].[GetCustomerInformation]\n(\n    @CustomerID\tBIGINT = NULL\n    , @LastName\tVARCHAR (30) = NULL\n    , @FirstName VARCHAR (30) = NULL\n    , @MiddleInitial CHAR(1) = NULL\n    , @EmailAddress VARCHAR(128) = NULL\n    , @Region_no TINYINT = NULL\n    , @Member_code TINYINT = NULL\n)\nAS\nIF (@CustomerID IS NULL\n    AND @LastName IS NULL\n    AND @FirstName IS NULL\n    AND @MiddleInitial IS NULL\n    AND @EmailAddress IS NULL\n    AND @Region_no IS NULL\n    AND @Member_code IS NULL)\nBEGIN\n    RAISERROR ('You must supply at least one parameter.', 16, -1);\n    RETURN;\nEND;\n\nDECLARE @ExecStr NVARCHAR (4000),\n        @Recompile  BIT = 1;\n\nSELECT @ExecStr =\n\tN'SELECT COUNT(*) FROM [dbo].[Customers] AS [C] WHERE 1=1';\n\nIF @CustomerID IS NOT NULL\n\tSELECT @ExecStr = @ExecStr\n\t\t+ N' AND [C].[CustomerID] = @CustID';\n\nIF @LastName IS NOT NULL\n\tSELECT @ExecStr = @ExecStr\n\t\t+ N' AND [C].[LastName] LIKE @LName'; \n\nIF @FirstName IS NOT NULL\n\tSELECT @ExecStr = @ExecStr\n\t\t+ N' AND [C].[Firstname] LIKE @FName';\n\nIF @MiddleInitial IS NOT NULL\n\tSELECT @ExecStr = @ExecStr\n\t\t+ N' AND [C].[MiddleInitial] = @MI';\n\nIF @EmailAddress IS NOT NULL\n\tSELECT @ExecStr = @ExecStr\n\t\t+ N' AND [C].[EmailAddress] LIKE @Email';\n\nIF @Region_no IS NOT NULL\n\tSELECT @ExecStr = @ExecStr\n\t\t+ N' AND [C].[Region_no] = @RegionNo';\n\nIF @Member_code IS NOT NULL\n\tSELECT @ExecStr = @ExecStr\n\t\t+ N' AND [C].[Member_code] = @MemberCode';\n\n-- These are highly limited sets\nIF (@CustomerID IS NOT NULL)\n    SET @Recompile = 0\n\nIF (PATINDEX('%[%_?]%', @LastName) &amp;amp;amp;amp;gt;= 4\n        OR PATINDEX('%[%_?]%', @LastName) = 0)\n    AND (PATINDEX('%[%_?]%', @FirstName) &amp;amp;amp;amp;gt;= 4\n        OR PATINDEX('%[%_?]%', @FirstName) = 0)\n    SET @Recompile = 0\n\nIF (PATINDEX('%[%_?]%', @EmailAddress) &amp;amp;amp;amp;gt;= 4\n        OR PATINDEX('%[%_?]%', @EmailAddress) = 0)\n    SET @Recompile = 0\n\nIF @Recompile = 1\nBEGIN\n    --SELECT @ExecStr, @Lastname, @Firstname, @CustomerID;\n    SELECT @ExecStr = @ExecStr + N' OPTION(RECOMPILE)';\nEND;\n\nEXEC [sp_executesql] @ExecStr\n    , N'@CustID bigint, @LName varchar(30), @FName varchar(30)\n    , @MI char(1), @Email varchar(128), @RegionNo tinyint\n    , @MemberCode tinyint'\n    , @CustID = @CustomerID\n    , @LName = @LastName\n    , @FName = @FirstName\n    , @MI = @MiddleInitial\n    , @Email = @EmailAddress\n    , @RegionNo = @Region_no\n    , @MemberCode = @Member_code;\nGO\n<\/pre>\n<h4><span style=\"color: #993300;\"><strong>Solution 2 is the preferred and best method to create a balance where you don&#8217;t just recompile every time. This will give you better long-term scalability.<\/strong>\u00a0<\/span><\/h4>\n<h3>Solution 3\u00a0&#8211; recompile when unstable\u00a0[by using DSE], cache when stable\u00a0(using sp_executesql)<\/h3>\n<p>In the past, if I&#8217;ve ever run into problems with OPTION (RECOMPILE), I always consider (albeit, not lightly) rewriting the statement using\u00a0a dynamic string instead; this always works! But, yes, it&#8217;s a pain to write. It&#8217;s a pain to troubleshoot. And, above all, yes, you have to be careful of SQL injection. There are ways to<span class=\"apple-converted-space\">\u00a0reduce and even\u00a0<\/span>eliminate<span class=\"apple-converted-space\">\u00a0the potential for problems (and, not all DSE can). Above all, you do need to make sure you have clean input (limiting only to valid input for the type of column such as characters for names and emails, etc.).\u00a0<\/span>Check out this blog post\u00a0if you want more information\u00a0<a href=\"https:\/\/www.sqlskills.com\/blogs\/kimberly\/little-bobby-tables-sql-injection-and-execute-as\/\" target=\"_blank\" rel=\"noopener noreferrer\">Little Bobby Tables, SQL Injection and EXECUTE AS<\/a>.<\/p>\n<p>Based on that blog post, the solution to this problem is the following code.<\/p>\n<pre><pre class=\"brush: sql; title: ; toolbar: true; wrap-lines: true; notranslate\" title=\"\">\n-- To reduce the potential for SQL injection, use loginless\n-- users. Check out the &amp;amp;amp;amp;quot;Little Bobby Tables&amp;amp;amp;amp;quot; blog post for\n-- more information on what I'm doing in this code \/ execution\n-- to reduce the potential surface area of the DSE here.\n\nCREATE USER [User_GetCustomerInformation]\nWITHOUT LOGIN;\nGO\n\nGRANT SELECT ON [dbo].[Customers]\nTO [User_GetCustomerInformation];\nGO\n\n-- You'll need this if you want to review the showplan\n-- for these executions.\nGRANT SHOWPLAN TO [User_GetCustomerInformation];\nGO\n\n----------------------------------------------\n-- Solution 3\n----------------------------------------------\n\nCREATE PROC [dbo].[GetCustomerInformation]\n(\n @CustomerID BIGINT = NULL\n , @LastName VARCHAR (30) = NULL\n , @FirstName VARCHAR (30) = NULL\n , @MiddleInitial CHAR(1) = NULL\n , @EmailAddress VARCHAR(128) = NULL\n , @Region_no TINYINT = NULL\n , @Cust_code TINYINT = NULL\n)\nWITH EXECUTE AS N'User_GetCustomerInformation'\nAS\nIF (@CustomerID IS NULL\n AND @LastName IS NULL\n AND @FirstName IS NULL\n AND @MiddleInitial IS NULL\n AND @EmailAddress IS NULL\n AND @Region_no IS NULL\n AND @Cust_code IS NULL)\nBEGIN\n RAISERROR ('You must supply at least one parameter.', 16, -1);\n RETURN;\nEND;\n\nDECLARE @spexecutesqlStr NVARCHAR (4000),\n @ExecStr NVARCHAR (4000),\n @Recompile BIT = 1;\n\nSELECT @spexecutesqlStr =\n N'SELECT * FROM [dbo].[Customers] AS [C] WHERE 1=1';\n\nSELECT @ExecStr =\n N'SELECT * FROM [dbo].[Customers] AS [C] WHERE 1=1';\n\nIF @CustomerID IS NOT NULL\nBEGIN\n SELECT @spexecutesqlStr = @spexecutesqlStr\n + N' AND [C].[CustomerID] = @CustID';\n SELECT @ExecStr = @ExecStr\n + N' AND [C].[CustomerID] = CONVERT(BIGINT, ' + CONVERT(NVARCHAR(30),@CustomerID) + N')';\nEND\n\nIF @LastName IS NOT NULL\nBEGIN\n SELECT @spexecutesqlStr = @spexecutesqlStr\n + N' AND [C].[LastName] LIKE @LName';\n SELECT @ExecStr = @ExecStr\n + N' AND [C].[LastName] LIKE CONVERT(VARCHAR(30),' + QUOTENAME(@LastName, '''''') + N')';\nEND\n\nIF @FirstName IS NOT NULL\nBEGIN\n SELECT @spexecutesqlStr = @spexecutesqlStr\n + N' AND [C].[Firstname] LIKE @FName';\n SELECT @ExecStr = @ExecStr\n + N' AND [C].[FirstName] LIKE CONVERT(VARCHAR(30),' + QUOTENAME(@FirstName, '''''') + N')';\nEND\n\nIF @MiddleInitial IS NOT NULL\nBEGIN\n SELECT @spexecutesqlStr = @spexecutesqlStr\n + N' AND [C].[MiddleInitial] = @MI';\n SELECT @ExecStr = @ExecStr\n + N' AND [C].[MiddleInitial] = CONVERT(CHAR(1), ' + QUOTENAME(@MiddleInitial, '''''') + N')';\nEND\n\nIF @EmailAddress IS NOT NULL\nBEGIN\n SELECT @spexecutesqlStr = @spexecutesqlStr\n + N' AND [C].[EmailAddress] LIKE @Email';\n SELECT @ExecStr = @ExecStr\n + N' AND [C].[EmailAddress] LIKE CONVERT(VARCHAR(128), ' + QUOTENAME(@EmailAddress, '''''') + N')';\nEND\n\nIF @Region_no IS NOT NULL\nBEGIN\n SELECT @spexecutesqlStr = @spexecutesqlStr\n + N' AND [C].[Region_no] = @RegionNo';\n SELECT @ExecStr = @ExecStr\n + N' AND [C].[Region_no] = CONVERT(TINYINT, ' + CONVERT(NVARCHAR(5),@Region_no) + N')';\nEND\n\nIF @Cust_code IS NOT NULL\nBEGIN\n SELECT @spexecutesqlStr = @spexecutesqlStr\n + N' AND [C].[Cust_code] = @MemberCode';\n SELECT @ExecStr = @ExecStr\n + N' AND [C].[Cust_code] = CONVERT(TINYINT, ' + CONVERT(NVARCHAR(5), @Cust_code) + N')';\nEND\n\n-- These are highly limited sets\nIF (@CustomerID IS NOT NULL)\n SET @Recompile = 0\n\nIF (PATINDEX('%[%_]%', @LastName) &amp;amp;amp;amp;gt;= 4\n OR PATINDEX('%[%_]%', @LastName) = 0)\n AND (PATINDEX('%[%_]%', @FirstName) &amp;amp;amp;amp;gt;= 4\n OR PATINDEX('%[%_]%', @FirstName) = 0)\n SET @Recompile = 0\n\nIF (PATINDEX('%[%_]%', @EmailAddress) &amp;amp;amp;amp;gt;= 4\n OR PATINDEX('%[%_]%', @EmailAddress) = 0)\n SET @Recompile = 0\n\nIF @Recompile = 1\nBEGIN\n -- Use this next line for testing\n -- SELECT @ExecStr -- For testing\n EXEC (@ExecStr);\nEND\nELSE\nBEGIN\n -- Use this next line for testing\n -- SELECT @spexecutesqlStr, @Lastname, @Firstname, @CustomerID;\n EXEC [sp_executesql] @spexecutesqlStr\n , N'@CustID bigint, @LName varchar(30), @FName varchar(30)\n , @MI char(1), @Email varchar(128), @RegionNo tinyint\n , @CustomerCode tinyint'\n , @CustID = @CustomerID\n , @LName = @LastName\n , @FName = @FirstName\n , @MI = @MiddleInitial\n , @Email = @EmailAddress\n , @RegionNo = @Region_no\n , @CustomerCode = @Cust_code;\nEND;\nGO\n<\/pre>\n<p>This solution should NOT be used as a standard. But, it is an option for\u00a0handling some cases where OPTION (RECOMPILE) isn&#8217;t giving you the plans \/ performance that you need. This\u00a0solution is more complicated for developers to code. But, I&#8217;m not saying you have to do this for absolutely everything. Just work through the most critical procedures that are causing you the most grief.<\/p>\n<h2>The RIGHT way to handle multipurpose procedures<\/h2>\n<p>The hybrid solution (solution 2) is more complicated for developers to code. Remember,\u00a0I&#8217;m not saying you have to do this for absolutely everything. Just work through the most critical procedures that are causing you the most grief (because they&#8217;re the most heavily executed procedures). The hybrid\u00a0solution really gives you the best of all worlds here:<\/p>\n<ul>\n<li>It reduces CPU by only recompiling for unstable combinations<\/li>\n<li>It allows every combination executed with sp_executesql to get its own plan (rather than one per procedure)<\/li>\n<li>It allows stable plans to be put in cache and reused for subsequent executions (which reduces your impact to CPU and gives you long-term scalability)<\/li>\n<\/ul>\n<p><strong>Above all, you\u00a0need to test that this works as expected but not only will you get better plans but you&#8217;ll also scale better!<\/strong><\/p>\n<p>Cheers,<br \/>\nkt<\/p>\n<p><strong>For more information<\/strong><\/p>\n<ul>\n<li>Check out my PASS presentation DBA-313\u00a0<a href=\"http:\/\/bit.ly\/1xoDfH8\" target=\"_blank\" rel=\"noopener noreferrer\">here<\/a>.<\/li>\n<li>Check out my Pluralsight course:\u00a0SQL Server: Optimizing Ad Hoc Statement Performance\u00a0<a href=\"http:\/\/www.pluralsight.com\/courses\/sqlserver-optimizing-adhoc-statement-performance\" target=\"_blank\" rel=\"noopener noreferrer\">here<\/a>.<\/li>\n<li>Check out my Pluralsight course: SQL Server: Optimizing Stored Procedure Performance\u00a0<a href=\"http:\/\/www.pluralsight.com\/courses\/sqlserver-optimizing-stored-procedure-performance\" target=\"_blank\" rel=\"noopener noreferrer\">here<\/a>.<\/li>\n<li>Check out my Pluralsight course: SQL Server: Optimizing Stored Procedure Performance, Part 2 (focused on session settings and further troubleshooting tips\/tricks) <a href=\"https:\/\/www.pluralsight.com\/courses\/sqlserver-optimizing-stored-procedure-performance-part2\" target=\"_blank\" rel=\"noopener noreferrer\">here<\/a>.<\/li>\n<li>Check out our Immersion Event:\u00a0IEPTO1: Immersion Event on Performance Tuning and Optimization \u2013 Part 1 <a href=\"https:\/\/www.sqlskills.com\/sql-server-training\/iepto1\/\" target=\"_blank\" rel=\"noopener noreferrer\">here<\/a>.<\/li>\n<li>Check out our Immersion Event:\u00a0IEPTO2: Immersion Event on Performance Tuning and Optimization \u2013 Part 2 <a href=\"https:\/\/www.sqlskills.com\/sql-server-training\/iepto2\/\" target=\"_blank\" rel=\"noopener noreferrer\">here<\/a>.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>[NOTE: Update 12 Nov 2014. The most recent bug (KB article\u00a0KB2965069) is THANKFULLY very unlikely for most environments (mentioned below). As a result, I&#8217;m glad to say that we can use OPTION (RECOMPILE) as a much\u00a0easier (and safer) solution. So, while I&#8217;ll still leave solution 3 as an option if you run into troubles with [&hellip;]<\/p>\n","protected":false},"author":7,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[19,20,27,28,50,53,54,62,65,66,67,68,91,92,78],"tags":[],"class_list":["post-2694","post","type-post","status-publish","format-standard","hentry","category-conference-resourcesqa","category-conferences","category-events","category-execute-as","category-optimizing-procedural-code","category-plan-cache","category-pluralsight","category-sp_executesql","category-sql-server-2005","category-sql-server-2008","category-sql-server-2008-r2","category-sql-server-2012","category-sql-server-2014","category-sqlintersection","category-tips"],"_links":{"self":[{"href":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-json\/wp\/v2\/posts\/2694","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-json\/wp\/v2\/users\/7"}],"replies":[{"embeddable":true,"href":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-json\/wp\/v2\/comments?post=2694"}],"version-history":[{"count":0,"href":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-json\/wp\/v2\/posts\/2694\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-json\/wp\/v2\/media?parent=2694"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-json\/wp\/v2\/categories?post=2694"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-json\/wp\/v2\/tags?post=2694"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}