© Elizabeth Noble 2020
E. NoblePro T-SQL 2019https://doi.org/10.1007/978-1-4842-5590-2_4

4. Designing T-SQL

Elizabeth Noble1 
(1)
Roswell, GA, USA
 

In the earlier chapters in the section on “Building Understandable T-SQL,” I have covered the various data types available and how to select the correct data type. I have also discussed several database objects available when you write your T-SQL and the pros and cons of using each database object. In the previous chapter, I explained the importance of making your code readable for yourself and others. I will finish up this section by going over the various options when designing your T-SQL code.

When writing T-SQL for your applications, I am a huge fan of using stored procedures. There are many advantages to using stored procedures including the possibility to make your code more adaptable and reusable. Similarly, using parameters with your code can increase the flexibility of your T-SQL code. You can use stored procedures and parameters when designing solutions for more complex queries. In addition, you may want to consider other techniques when it comes to solving complex issues. There are several items to consider when designing T-SQL code including stored procedures, parameters, and complex query logic.

Using Stored Procedures

When writing T-SQL code, you have several options available. Depending on the implementation you use, we will determine what SQL Server does with that T-SQL code. You can write ad hoc T-SQL where each query is written without uniformity. While this code can be repeated, you will need to be careful to make sure that all formatting is exactly consistent, including whitespace. There is also what is referred to as prepared statements. This type of T-SQL code uses parameters. When the prepared statements are used, the values passed to the parameters are updated. Finally, there are stored procedures. These are database objects that are persistent. Each of these has their pros and cons. However, stored procedures generally are the most consistent when writing T-SQL.

There are several aspects to consider when writing T-SQL code for applications. In order to better understand how you want your applications to use T-SQL, it may be best to understand how SQL Server handles each of the T-SQL options available. When SQL Server executes a query, it must determine how it will go about executing that query. Before doing this, SQL Server will check to see if there is already an execution plan for this query. Depending on how the T-SQL is written will have some impact on how SQL Server verifies that the execution plan is already in place.

To show how stored procedures, ad hoc queries, and prepared statements affect the plan cache, I will walk through some examples. In Listing 4-1, you can see a statement to create the stored procedure.
/*-------------------------------------------------------------*
Name:             dbo.GetRecipe
Author:           Elizabeth Noble
Created Date:     April 20, 2019
Description: Get a list of all recipes in the database
Sample Usage:
      EXECUTE dbo.GetRecipe
*-------------------------------------------------------------*/
CREATE PROCEDURE dbo.GetRecipe
AS
      SELECT
            RecipeID,
            RecipeName,
            RecipeDescription,
            ServingQuantity,
            MealTypeID,
            PreparationTypeID,
            IsActive,
            DateCreated,
            DateModified
      FROM dbo.Recipe;
Listing 4-1

Create a Stored Procedure

In this example, I am creating a stored procedure that cannot be configured. Each time this stored procedure is executed, it will pull back the same types of data. This stored procedure will pull back information about all recipes that have been entered into the database. If you want to see the results for this stored procedure, you can execute the stored procedure as shown in Listing 4-2.
EXECUTE dbo.GetRecipe;
Listing 4-2

Execute the Stored Procedure

SQL Server will create this execution plan and save the execution plan to the query plan cache. If this stored procedure is run again in the future, SQL Server will check the plan cache to see if this stored procedure already exists in the cache. This plan will sit in the plan cache for as long as SQL Server deems the plan relevant. In Figure 4-1, you can see the execution plan associated with the stored procedure in Listing 4-2.
../images/480547_1_En_4_Chapter/480547_1_En_4_Fig1_HTML.jpg
Figure 4-1

Execution Plan for Stored Procedure

If the plan is in the cache, SQL Server will continue to reuse the same execution plan. This works well for stored procedures. However, there are other ways of accessing data in SQL Server. One of these methods is to write ad hoc queries. When writing ad hoc queries, you have the option to hard-code the values or parameterize them. An example of this type of query shown in Listing 4-3 has the same logic as a stored procedure in Listing 4-1.
SELECT
      RecipeID,
      RecipeName,
      RecipeDescription,
      ServingQuantity,
      MealTypeID,
      PreparationTypeID,
      IsActive,
      DateCreated,
      DateModified
FROM dbo.Recipe;
Listing 4-3

Ad Hoc Query Logic

The logic in Listings 4-1 and 4-3 is the same. The largest difference is in how SQL Server handles the two. For both queries, it is a little harder to see. However, what happens is SQL Server checks the plan cache to see if the same plan exists. The plans that are stored in the plan cache are not based on the core logic but are based on how the actual ad hoc query or stored procedure execution is written. In order to look at the plan cache in SQL Server, you can use the query is available in Listing 4-4. This query is from Microsoft Books Online.
SELECT cplan.usecounts, cplan.objtype, qtext.text, qplan.query_plan
FROM sys.dm_exec_cached_plans AS cplan
CROSS APPLY sys.dm_exec_sql_text(plan_handle) AS qtext
CROSS APPLY sys.dm_exec_query_plan(plan_handle) AS qplan
ORDER BY cplan.usecounts DESC
Listing 4-4

Query to View Plans in the Cache

As you will be able to see in Table 4-1 below, the ad hoc query is even stored as an ad hoc object type, whereas SQL Server can properly tell that the second row is a stored procedure. In Table 4-1, you can see the results in the plan cache from running the stored procedure and the same code as an ad hoc query.
Table 4-1

Plan Cache for Ad Hoc Query and Stored Procedure

Use Count

Object Type

Query Text

1

Ad hoc

SELECT         RecipeID,         RecipeName,         RecipeDescription,         ServingQuantity,         MealTypeID,         PreparationTypeID,         IsActive,         DateCreated,         DateModified  FROM dbo.Recipe;

1

Proc

CREATE PROCEDURE dbo.GetRecipe    AS          SELECT               RecipeID,               RecipeName,               RecipeDescription,               ServingQuantity,               MealTypeID,               PreparationTypeID,               IsActive,               DateCreated,               DateModified        FROM dbo.Recipe;

Where this all can become important is when users or applications are writing code inconsistently. In this case, I modify the query from Listing 4-3 by moving the first column in the select statement to the same row as the SELECT clause. If I modify the query from Listing 4-3, you can see how the query plan cache changes since running the query in Listing 4-5.
SELECT RecipeID,
      RecipeName,
      RecipeDescription,
      ServingQuantity,
      MealTypeID,
      PreparationTypeID,
      IsActive,
      DateCreated,
      DateModified
FROM dbo.Recipe;
Listing 4-5

Ad Hoc Query Modified

I would hope that SQL Server can tell that the query in Listing 4-3 matches the same code as Listing 4-5. The only thing that has really changed is the formatting of the query itself. Rerunning the query from Listing 4-4, I can check the plan cache in Table 4-2.
Table 4-2

Plan Cache for Modified Ad Hoc Query

Use Count

Object Type

Query Text

1

Ad hoc

SELECT         RecipeID,         RecipeName,         RecipeDescription,         ServingQuantity,         MealTypeID,         PreparationTypeID,         IsActive,         DateCreated,         DateModified  FROM dbo.Recipe;

1

Ad hoc

SELECT RecipeID,         RecipeName,         RecipeDescription,         ServingQuantity,         MealTypeID,         PreparationTypeID,         IsActive,         DateCreated,         DateModified  FROM dbo.Recipe;

1

Proc

CREATE PROCEDURE dbo.GetRecipe    AS          SELECT               RecipeID,               RecipeName,               RecipeDescription,               ServingQuantity,               MealTypeID,               PreparationTypeID,               IsActive,               DateCreated,               DateModified        FROM dbo.Recipe;

You can see in Table 4-2 that there are now three entries in the plan cache. Instead of SQL Server reusing the execution plan from the first time the ad hoc query was called, SQL Server has generated an entirely new execution plan. Each time SQL Server creates a new execution plan, it uses additional resources to create the plan. This can cause it to take a couple of seconds longer for SQL Server to return the query results. Each query plan executed is saved in the plan cache. Having multiple execution plans saved for each query can fill up the execution plan cache. This can cause other query execution plans to be removed from the plan cache prematurely. The removal of a query plan from the cache will cause SQL Server to expend additional resources to create a new execution for that other query. This is something to consider when determining how your applications will call T-SQL code.

While a stored procedure gives us a consistent way to call that same type of code more than once, stored procedures can be fairly limiting if the exact same base query will be used for every scenario. I will often want to write a general piece of T-SQL code that can be used for various scenarios. In these types of situations, I find myself wanting to write queries or stored procedures that use parameters.

Using Parameters

In addition to using stored procedures to help T-SQL code be more reusable, you can also use parameters with your T-SQL code. Parameters can be used with ad hoc queries, prepared statements, or stored procedures. It is also possible to use parameters as an input or output. Regardless of where parameters are used, parameters are what allow you to write queries that can be used in a variety of situations. In Listing 4-6, you can see that this is a stored procedure that will return recipe information based on the RecipeID that has been provided in the stored procedure.
/*-------------------------------------------------------------*
Name:             dbo.GetRecipeByRecipeID
Author:           Elizabeth Noble
Created Date:     April 20, 2019
Description: Get recipe information when a recipe ID is provided
Sample Usage:
      DECLARE @RecipeID INT;
      SET @RecipeID = 1;
      EXECUTE dbo.GetRecipeByRecipeID @RecipeID
*-------------------------------------------------------------*/
CREATE PROCEDURE dbo.GetRecipeByRecipeID
      @RecipeID   INT
AS
      SELECT
            RecipeID,
            RecipeName,
            RecipeDescription,
            ServingQuantity,
            MealTypeID,
            PreparationTypeID,
            IsActive,
            DateCreated,
            DateModified
      FROM dbo.Recipe
      WHERE RecipeID = @RecipeID;
Listing 4-6

Create Stored Procedure with a Parameter

The preceding stored procedure allows you to pass in any RecipeID and the stored procedure will return a predefined set of recipe information based on that RecipeID. This also allows the T-SQL code to be called in a much simpler method. Looking at Listing 4-7, I have written code to execute the stored procedure passing a hard-coded value as part of the execution.
EXECUTE dbo.GetRecipeByRecipeID 1;
Listing 4-7

Execute Stored Procedure with Hard-Coded Value

While it is possible to use this method to call the stored procedure, using variables would be a much more dynamic way to execute this stored procedure. One such method is to declare a variable with a specific data type and then set that variable to a specific value. This more closely simulates how an application might call a stored procedure. The application code would generally have a variable already declared and use the same variable when executing the stored procedure. The process of declaring a variable, setting that variable to a value, and executing the stored procedure using that variable can be seen in Listing 4-8.
DECLARE @RecipeID INT;
SET @RecipeID = 1
EXECUTE dbo.GetRecipeByRecipeID @RecipeID;
Listing 4-8

Execute Stored Procedure with a Variable

To get a comparison of the plan cache for the stored procedure in Listing 4-6 vs. an ad hoc query, I will execute the ad hoc query that will use the same overall logic as Listing 4-6. However, this value will not be parameterized. In Listing 4-9, you can see the query with the hard-coded value for the RecipeID.
SELECT
      RecipeID,
      RecipeName,
      RecipeDescription,
      ServingQuantity,
      MealTypeID,
      PreparationTypeID,
      IsActive,
      DateCreated,
      DateModified
FROM dbo.Recipe
WHERE RecipeID = 1;
Listing 4-9

Run Ad Hoc Query with Hard-Coded Value

Another method for executing the same stored procedure would be to use a query like the one in Listing 4-9 but to use a parameter as part of the where clause. This will perform that same overall logic as Listings 4-7, 4-8, and 4-9. In Listing 4-10, I have used the same code by declaring a variable and parameterized the query overall.
DECLARE @RecipeID INT;
SET @RecipeID = 1
SELECT
      RecipeID,
      RecipeName,
      RecipeDescription,
      ServingQuantity,
      MealTypeID,
      PreparationTypeID,
      IsActive,
      DateCreated,
      DateModified
FROM dbo.Recipe
WHERE RecipeID = @RecipeID;
Listing 4-10

Run Ad Hoc Query with a Parameter

While all these queries are pulling back the same data, the way SQL Server handles these four queries can be surprisingly different. In my test case, I cleared the plan cache and then executed the T-SQL code in Listings 4-7, 4-8, 4-9, and 4-10. Even though each of these queries is returning the same results, you will see in Table 4-3 how SQL Server has calculated or used execution plans for each of these queries.
Table 4-3

Comparison of Plan Cache for Variables and Hard-Coded Values

Use Counts

Object Type

Text

2

Proc

CREATE PROCEDURE dbo.GetRecipeByRecipeID        @RecipeID   INT  AS          SELECT               RecipeID,               RecipeName,               RecipeDescription,               ServingQuantity,               MealTypeID,               PreparationTypeID,               IsActive,               DateCreated,               DateModified        FROM dbo.Recipe        WHERE RecipeID = @RecipeID;

1

Ad hoc

DECLARE @RecipeID INT;    SET @RecipeID = 1    EXECUTE dbo.GetRecipeByRecipeID @RecipeID;

1

Ad hoc

DECLARE @RecipeID INT;    SET @RecipeID = 1    SELECT         RecipeID,         RecipeName,         RecipeDescription,         ServingQuantity,         MealTypeID,         PreparationTypeID,         IsActive,         DateCreated,         DateModified  FROM dbo.Recipe  WHERE RecipeID = @RecipeID;

1

Ad hoc

SELECT         RecipeID,         RecipeName,         RecipeDescription,         ServingQuantity,         MealTypeID,         PreparationTypeID,         IsActive,         DateCreated,         DateModified  FROM dbo.Recipe  WHERE RecipeID = 1;

1

Prepared

(@1 tinyint)SELECT [RecipeID], [RecipeName], [RecipeDescription], [ServingQuantity], [MealTypeID], [PreparationTypeID], [IsActive], [DateCreated], [DateModified] FROM [dbo].[Recipe] WHERE [RecipeID]=@1

In Table 4-3, you can see that the stored procedure has been executed twice. The SELECT statement with the hard-coded value and the SELECT statement with the parameter value each have their own execution plan. This may not have a large impact when you are discussing only a handful of queries. However, if your entire environment has a significant number of queries that are not stored procedures, you may want to check your plan cache to see how they are being handled.

This is not the only topic you need to consider when writing your queries. Another potential option has to do with what is commonly referred to as parameter sniffing. Parameter sniffing does not sound as dangerous as it can be. The key takeaway when understanding what parameter sniffing is and how it affects you is to consider how your data is shaped. For many companies, not all data stored in a data table is evenly distributed. In Listing 4-11, you can see the stored procedure that I am creating to test for parameter sniffing.
/*-------------------------------------------------------------*
Name:             dbo.GetRecipeAndIngredientByMealTypeID
Author:           Elizabeth Noble
Created Date:     April 20, 2019
Description: Get all recipes and their ingredients by meal type
Sample Usage:
      EXECUTE dbo.GetRecipeAndIngredientByMealTypeID 1
*-------------------------------------------------------------*/
CREATE PROCEDURE dbo.GetRecipeAndIngredientByMealTypeID
      @MealTypeID     INT
AS
      SELECT
            rec.RecipeName,
            ingr.IngredientName,
            ingr.IsActive,
            ingr.DateCreated,
            ingr.DateModified
      FROM dbo.Recipe rec
            INNER JOIN dbo.RecipeIngredient recingr
            ON rec.RecipeID = recingr.RecipeID
            LEFT OUTER JOIN dbo.Ingredient ingr
            ON recingr.IngredientID = ingr.IngredientID
      WHERE rec.MealTypeID = @MealTypeID
      ORDER BY rec.RecipeName, ingr.IngredientName;
Listing 4-11

Stored Procedure to Find Recipe and Ingredient by Meal Type

In the case of recipes, you may have a significant difference in the quantity of the types of recipes that are stored in the tables. If you had hundreds or thousands of breakfast recipes but only a handful of dinner recipes, you may find yourself in a scenario where parameter sniffing would affect your application performance. In our scenario of meal types, you may execute a stored procedure wanting breakfast recipes. The first time you call your stored procedure, SQL Server will use the parameter provided to generate an execution plan. That execution plan will end up stored in the plan cache. To test for parameter sniffing, you may want to execute the stored procedure with a given parameter as shown in Listing 4-12.
EXECUTE dbo.GetRecipeAndIngredientByMealTypeID 3;
Listing 4-12

Execute Stored Procedure with a Parameter with Many Records

For this stored procedure execution, I get the following execution plan as shown in Figure 4-2.
../images/480547_1_En_4_Chapter/480547_1_En_4_Fig2_HTML.jpg
Figure 4-2

Execution Plan for Stored Procedure with a Parameter That Has Many Records

If you come back later and want to rerun the stored procedure for dinner recipes, SQL Server will reuse the original execution plan to pull back the data. However, the first execution plan that SQL Server created may perform worse when trying to find recipes with the meal type of dinner. If I were to clear the plan cache and rerun the stored procedure in a different order, I can verify that the issue is related to parameter sniffing. In Listing 4-13, you can see the T-SQL code used to generate a new execution plan now that the plan cache has been cleared.
EXECUTE dbo.GetRecipeAndIngredientByMealTypeID 2;
Listing 4-13

Execute Stored Procedure with a Parameter with Few Records

When I execute this stored procedure, I find that a different plan cache is created. You can see the other execution plan that is created in Figure 4-3.
../images/480547_1_En_4_Chapter/480547_1_En_4_Fig3_HTML.jpg
Figure 4-3

Execution Plan for Stored Procedure with a Parameter That Has Many Records

If I run the same T-SQL from Listing 4-12, I now get the same execution plan. This is a sign that this query is encountering parameter sniffing. There are several options available to handle parameter sniffing. Ideally, you may be able to redesign your query to use different indexes.

Unfortunately, it may not always be possible to modify the query in this manner. This may cause you to look for other options like updating the query using the WITH RECOMPILE query hint. The downside of using this method is that the query must be compiled each time the query is executed. This incurs both an additional cost in terms of hardware and time for each query execution. That is when you may want to look for better alternatives. It is possible to modify the query to the cached plan for certain parameters which are more common and recompile the stored procedure for all other scenarios. The stored procedure in Listing 4-14 has been modified to use recompile for some values.
/*-------------------------------------------------------------*
Name:             dbo.GetRecipeAndIngredientByMealTypeID
Author:           Elizabeth Noble
Created Date:     April 20, 2019
Description: Get all recipes and their ingredients by meal type
Sample Usage:
      EXECUTE dbo.GetRecipeAndIngredientByMealTypeID 1
*-------------------------------------------------------------*/
CREATE PROCEDURE dbo.GetRecipeAndIngredientByMealTypeID
      @MealTypeID     INT
AS
DECLARE @QueryString    NVARCHAR(1000);
      -- Original query to be executed
      SELECT @QueryString = N'
      SELECT
            rec.RecipeName,
            ingr.IngredientName,
            ingr.IsActive,
            ingr.DateCreated,
            ingr.DateModified
      FROM dbo.Recipe rec
            INNER JOIN dbo.RecipeIngredient recingr
            ON rec.RecipeID = recingr.RecipeID
            LEFT OUTER JOIN dbo.Ingredient ingr
            ON recingr.IngredientID = ingr.IngredientID
      WHERE rec.MealTypeID = @MelTypID
      ORDER BY rec.RecipeName, ingr.IngredientName'
 -- If the parameters do not provide a stable execution plan
 ---- Add OPTION(RECOMPILE) to the query
IF (@MealTypeID <> 2)
BEGIN
      SELECT @QueryString = @QueryString + N' OPTION(RECOMPILE)';
END
-- Execute the query string
EXECUTE [sp_executesql] @QueryString,
      N'@MelTypID INT',
      @MelTypID = @MealTypeID;
GO
Listing 4-14

Execute the Stored Procedure

In Listing 14-14, the execution plan is considered stable when MealTypeID of 2 is provided. When the stored procedure is run with a MealTypeID of 2, the stored procedure will execute the T-SQL code without generating a new execution plan. However, if a different value is provided for the MealTypeID, then SQL Server will generate a new execution plan each time. This type of scenario works well when most of the time the query is executed; it will receive the parameter of MealTypeID of 2. This keeps SQL Server from generating an execution plan every time the stored procedure is called. If one or two times out of every ten times the stored procedure is executed and the parameter is not set as MealTypeID of 2, then SQL Server will generate a new execution plan. This method allows you to minimize the cost of recompiling the execution and limiting this to happen much less frequently. While SQL Server is working as intended, it is the shape of the data that is contributing to the performance issue.

Using Complex Logic

There are some basic operations in SQL Server that are not significantly complex. These basic operations can include inserting, reading, updating, and deleting table from single tables. Some of these operations may involve a couple of joins, but at some point in time, there may be a need for more complex logic. One of the challenges that happens while working with T-SQL is dealing with complex logic.

When dealing with complex logic, it is important to keep a couple of things in mind. The first step in dealing with complex logic is breaking down the request into smaller parts. Part of this logic will be to figure out what data you need to start with and how to whittle that data down into a smaller data set. You will also want to focus on taking all the requirements and breaking those requirements down into simplified steps.

Many of these query requests can involve either dealing with databases that were not designed to take advantage of the strengths for relational databases and systems that were not designed to be integrated or interacting with legacy applications. Oftentimes, we do not have control over what we are requested to do, and we are lucky if we get to design how the data is stored in the database.

These types of scenarios can also involve various types of coding methodologies that do not necessarily coincide with best practices. This includes needing to loop through data including recursion. There are also times that you may want to use correlated subqueries or work with various strings such as XML or JSON. While many of these options may seem like they are the perfect solution, many of these options are overly complex. This is where the balancing act between making T-SQL code easy to understand and having T-SQL code that performs well can be tricky. My goal here is to show how using what seems like more advanced query techniques may be contributing to poor performance in your applications.

It is often tempting with writing T-SQL to write your code in a way that matches the specific acceptance criteria or business request. Sometimes this will work well, and you will end up with code that performs very well. In other instances, not only can trying to write to solve complex problem be difficult and frustrating, but if the T-SQL code is not written in a way that works best for SQL Server, you have the potential to come across significant performance issues. I have found that SQL Server generally works best if you can keep your code simple and straightforward. This also means that using new functionality in SQL Server may not always yield the best results in terms of performance, even if the code is easier to write.

The most common case I see where the T-SQL code that is written does not always consider how SQL Server performs best is involving loops. SQL Server allows many options for looping through data, and while the code will return the correct results, I often find the cost on SQL Server more significant than figuring out a different way of interacting with the same data. Looking back at Listing 2-29, I have pasted the same logic in Listing 4-15.
SET NOCOUNT ON;
DECLARE @RecipeID INT,
    @RecipeName VARCHAR(25),
    @message VARCHAR(50);
PRINT '-------- Recipe Listing --------';
DECLARE recipe_cursor CURSOR DYNAMIC
FOR
SELECT RecipeID, RecipeName
FROM dbo.Recipe
ORDER BY RecipeID;
OPEN recipe_cursor
FETCH NEXT FROM recipe_cursor
INTO @RecipeID, @RecipeName
WHILE @@FETCH_STATUS = 0
BEGIN
    PRINT ' '
    SELECT @message = '----- Ingredients For Recipe: ' + @RecipeName + '-----'
    PRINT @message
      SELECT ing.IngredientName, srv.ServingPortionQuantity, srv.ServingPortionUnit
      FROM dbo.Ingredient ing
            INNER JOIN dbo.RecipeIngredient recing
            ON ing.IngredientID = recing.IngredientID
            INNER JOIN dbo.ServingPortion srv
            ON recing.ServingPortionID = srv.ServingPortionID
      WHERE recing.RecipeID = @RecipeID
      FETCH NEXT FROM recipe_cursor INTO @RecipeID, @RecipeName
END
CLOSE recipe_cursor;
DEALLOCATE recipe_cursor;
Listing 4-15

Creating a Dynamic Cursor

This query will give us the exact results we want, but SQL Server will have to repeat the logic inside the cursor for each row that the cursor will be analyzing. When the query logic inside the cursor is written well, there can be minimal impact. However, the real challenges happen when the T-SQL code inside the query either performs slowly or uses a significant amount of hardware. Then the server and even the applications can feel the pain every time the code inside the cursor is executed. While it is easy to blame cursors specifically, there are other scenarios like WHILE loops that have the same potential for issues.

Another of the larger struggles here is that even if the code performs well when it is first created, there is still the potential for the performance of this code to decrease over time as the database grows or the shape of the data changes. In those situations, what once appeared to be a great solution can quickly become one of the largest headaches. There are some options available to get the same output but write the T-SQL code in a manner that is more efficient and less resource intensive. In Listing 4-16, you will see some comments that are trying to outline the steps needed to get the same data output as would be returned in Listing 4-15.
-- Create Report Heading for All Recipes
-- Repeat the following per recipe
---- Create Section Subheading per Recipe
---- List All Ingredients Per Recipe
Listing 4-16

Simplifying T-SQL Code Logic with Comments

When working with queries where the logic is not straightforward, I will start by breaking down the overall request into smaller pieces. Ideally, the process of breaking the acceptance criteria down will allow me to focus on where I can start minimizing data. I will look at the query from two different perspectives. The first is determining how I can start paring the data down to only the data I will need in my result. I usually try to do that as early as possible in my query design. I will also try to look for the sections where I know the T-SQL code needed will be simple.

When referring to Listing 4-15, I can tell that I need a way to pull all ingredients per recipe. I also want to find a way to insert some sort of header between each recipe. I will eventually want to figure out how to insert the header row. However, I will first write a query to pull back all recipes and their ingredients. I have written that query in Listing 4-17.
-- Create Report Heading for All Recipes
-- Repeat the following per recipe
---- Create Section Subheading per Recipe
---- List All Ingredients Per Recipe
      SELECT
            rec.RecipeName,
            ing.IngredientName,
            srv.ServingPortionQuantity,
            srv.ServingPortionUnit
      FROM dbo.Ingredient ing
            INNER JOIN dbo.RecipeIngredient recing
            ON ing.IngredientID = recing.IngredientID
            INNER JOIN dbo.Recipe rec
            ON recing.RecipeID = rec.RecipeID
            INNER JOIN dbo.ServingPortion srv
            ON recing.ServingPortionID = srv.ServingPortionID
      ORDER BY rec.RecipeName, ing.IngredientName
Listing 4-17

Get All Ingredients for All Recipes

Once I have a starting point of data, I can continue to add additional pieces of logic. I will start with the main part of the data I am trying to pull back and branch out from there. In this example, I start adding header information as shown in Listing 4-18.
-- Create Report Heading for All Recipes
PRINT '-------- Recipe Listing --------';
-- Repeat the following per recipe
---- Create Section Subheading per Recipe
      SELECT
            '----- Ingredients For Recipe: ' + rec.RecipeName + '-----' AS 'SectionHeader'
      FROM  dbo.Recipe rec
---- List All Ingredients Per Recipe
      SELECT
            rec.RecipeID,
            rec.RecipeName,
            ing.IngredientName,
            srv.ServingPortionQuantity,
            srv.ServingPortionUnit
      FROM dbo.Ingredient ing
            INNER JOIN dbo.RecipeIngredient recing
            ON ing.IngredientID = recing.IngredientID
            INNER JOIN dbo.Recipe rec
            ON recing.RecipeID = rec.RecipeID
            INNER JOIN dbo.ServingPortion srv
            ON recing.ServingPortionID = srv.ServingPortionID
      ORDER BY rec.RecipeName, ing.IngredientName
Listing 4-18

Get All the Data Pulled Together

After adding some header information, I am ready to start getting the output to match the current output. In Listing 4-19, you can see all the code that was required to match the original output.
SET NOCOUNT ON;
-- Create Report Heading for All Recipes
PRINT '-------- Recipe Listing --------';
PRINT "
-- Create a temporary table to store the recipe information
      CREATE TABLE #RecipeList
      (
            OrderedList             INT,
            RecipeID                INT,
            SectionHeader           VARCHAR(100)
      );
-- Repeat the following per recipe
---- Create Section Spacing per Recipe
      INSERT INTO #RecipeList (OrderedList, RecipeID, SectionHeader)
      SELECT
            0 AS 'OrderedList',
            rec.RecipeID,
            " AS 'SectionHeader'
      FROM  dbo.Recipe rec
      WHERE rec.RecipeID < 3
---- Create Section Subheading per Recipe
      INSERT INTO #RecipeList (OrderedList, RecipeID, SectionHeader)
      SELECT
            10 AS 'OrderedList',
            rec.RecipeID,
            '----- Ingredients For Recipe: ' + rec.RecipeName AS 'SectionHeader'
      FROM  dbo.Recipe rec
      WHERE rec.RecipeID < 3
---- Add the column headings for ingredients per recipe
      INSERT INTO #RecipeList (OrderedList, RecipeID, SectionHeader)
      SELECT
            15 AS 'OrderedList',
            rec.RecipeID,
            CAST('IngredientName' AS CHAR(25)) + ' ' +
                  CAST('ServingPortionQuantity' AS CHAR(22)) + ' ' +
                  CAST('ServingPortionUnit' AS CHAR(19)) AS 'SectionHeader'
      FROM  dbo.Recipe rec
      WHERE rec.RecipeID < 3
      INSERT INTO #RecipeList (OrderedList, RecipeID, SectionHeader)
      SELECT
            20 AS 'OrderedList',
            rec.RecipeID,
            '------------------------- ---------------------- ------------------' AS 'SectionHeader'
      FROM  dbo.Recipe rec
      WHERE rec.RecipeID < 3
---- List All Ingredients Per Recipe
      INSERT INTO #RecipeList
      (
            OrderedList,
            RecipeID,
            SectionHeader
      )
      SELECT
            25 AS 'OrderedList',
            rec.RecipeID,
            CAST(ing.IngredientName AS CHAR(25)) + ' ' +
                  CAST(srv.ServingPortionQuantity AS CHAR(22)) + ' ' +
                  CAST(srv.ServingPortionUnit AS CHAR(19)) AS 'SectionHeader'
      FROM dbo.Ingredient ing
            INNER JOIN dbo.RecipeIngredient recing
            ON ing.IngredientID = recing.IngredientID
            INNER JOIN dbo.Recipe rec
            ON recing.RecipeID = rec.RecipeID
            INNER JOIN dbo.ServingPortion srv
            ON recing.ServingPortionID = srv.ServingPortionID
      WHERE rec.RecipeID < 3
      ORDER BY rec.RecipeName, ing.IngredientName
      SELECT
            SectionHeader
      FROM #RecipeList
      ORDER BY RecipeID, OrderedList
      DROP TABLE #RecipeList
Listing 4-19

Rewritten Query to Avoid Using a Cursor

The original cursor gave the output shown in Figure 4-4 when the results were written to text.
../images/480547_1_En_4_Chapter/480547_1_En_4_Fig4_HTML.jpg
Figure 4-4

Output from Cursor

To confirm that our rewrite from Listing 4-19 is correct, I captured the results to text when executing the same query. The output in Figure 4-5 appears to be the same.
../images/480547_1_En_4_Chapter/480547_1_En_4_Fig5_HTML.jpg
Figure 4-5

Output from Modified Query

However, these outputs are only the same when they are exported to text. When dealing with small sets of data, the extra code and level of work may not seem to be required. As is usually the case, most performance issues do not become apparent until the performance is significantly and negatively impacted. In the case of these two methods, when working with two records, the performance appeared the same. However, when handling over 10,000 recipes, it becomes immediately clear that the query from Listing 4-19 performs significantly better.

In this chapter, I have covered how and why you would want to use stored procedures. This includes creating code that is reusable and consistent. I have also shown you how to use parameters. For the most part, parameters will help make your code more adaptable and dynamic. Parameters are helpful for many different scenarios. While parameters are very useful in many different situations, you will also need to make sure your T-SQL is not being negatively affected by parameter sniffing. I also covered a common situation where you may need to solve complex problems using T-SQL. You will often find your T-SQL performs better when you keep the code more straightforward. This may mean that the code is not as readable or clean, but SQL Server will have a better idea how to get an optimal execution plan.

This also concludes the current section on writing understandable T-SQL. This is T-SQL that uses the best data type for each scenario. This is usually a data type that takes up the least amount of space possible and provides the necessary accuracy. You will also want to understand the various SQL Server database objects available that you can use to build your T-SQL. You will need to determine the benefits vs. the challenges of using one database object vs. another. In some cases, you will need to decide if code readability or database performance is more important. Another aspect of writing understandable T-SQL is formatting, naming, and commenting your T-SQL so that others can quickly understand what you have written.

When working with T-SQL, there will be times where you want to write T-SQL that can be called consistently more than once. This T-SQL code may also become more flexible using parameters. When faced with writing T-SQL that is not straightforward, it will help you break the code into segments to simplify what needs to be written. Once you feel comfortable writing T-SQL that is understandable, you will be ready to start focusing on writing T-SQL that is efficient and minimizes performance impact on SQL Server.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.76.218