Skip to article
Data SQL Server 2008 Quick ID: 1201071

The Baker’s Dozen: 13 Miscellaneous SQL Server Tips

To use a music analogy, many installments of “The Baker’s Dozen” have been like “concept albums,” where most or all of the tips work towards a big picture. Then there are times where I present a series of random tips that are largely standalone and don’t form a pattern. In this article, I’m going to present 13 random tips for SQL Server and T-SQL programming.

Updated

To use a music analogy, many installments of “The Baker’s Dozen” have been like “concept albums,” where most or all of the tips work towards a big picture. Then there are times where I present a series of random tips that are largely standalone and don’t form a pattern. In this article, I’m going to present 13 random tips for SQL Server and T-SQL programming.

What’s on the Menu?

Getting right to the point, here are the 13 items on the menu for this article:

  • Baker’s Dozen Spotlight: A T-SQL tip for summarizing data by week, and calculating a 52-week moving average
  • Using SQL Server table-valued functions as a means of implementing security access by business entities
  • SQL Server covering indexes for performance
  • SQL Server SARGs (Searchable Arguments)
  • T-SQL SUM OVER capability to implement a % of Parent calculation
  • Programmatically disabling SQL Server constraints and triggers
  • Correlated subqueries and derived table subqueries and their execution plans
  • A little brain teaser on the UNION statement
  • Back to basics - determining when a subquery is necessary
  • The tinyint data type and when to use it
  • DISTINCT vs GROUP BY
  • Creating a result set of integers for a picklist
  • INSTEAD OF triggers to prevent deletions

The Demo Database for the Examples

With just a few exceptions, the examples use the AdventureWorks2008R2 database. You can find AdventureWorks2008R2 on the CodePlex site. If you’re still using SQL Server 2008 and not 2008R2, the examples will still work - you’ll just need to change any 2008R2 references to 2008.

Tip 1: Baker’s Dozen Spotlight: Summarizing Data by Week and Calculating a 52-week Moving Average

Scenario: I want to take daily sales information from the daily Purchase Order table in the AdventureWorks database and summarize sales by week ending date. For each week, I also want to calculate the average weekly sales going back over the last 52 weeks.

Listings 1 - 3, and Figure 1 demonstrate a result set that summarizes by week-ending date (using a UDF), and also generates a moving average by querying each week against the last 52 weeks - often used for an analytic chart that shows a trend of weeks (Figure 2). The highlights of the T-SQL code are as follows:

Figure 1: Results of Moving Average.
Figure 2: Chart showing data using Moving Average with AdventureWorks demo.

First, in order to summarize sales by week, I need a function that will convert any day to a Saturday date, or end of week date (Listing 1). Many developers have probably seen SQL functions that convert dates to Saturday dates - but it’s worth mentioning that when using a date data type, I can’t do the following (which developers have done for years with a datetime data type):

SET @ReturnDate = 
 @InputDate + 
  (@@DateFirst - 
      datepart(weekday,@InputDate 

That’s because Microsoft restricted the date data type to only use the DateAdd function: the “+” operator is not valid for the date datatype.

SET @ReturnDate = 
  DateAdd(day,
         (@@DateFirst - 
            datepart(weekday,@InputDate)),
          @InputDate)

Second, remember that the date range for the result set is the Fiscal Year 2008. Also note that the result set in Figure 1 and the chart in Figure 2 both show all weeks, regardless of whether any purchase orders existed for the week. Many reports/charts need to display all weeks, regardless of whether transactions occurred. Analytically, weeks without data might be as significant as weeks with data. Therefore, a routine (function) that produces all the possible week ending dates will be helpful, especially if many reports need it.

So Listing 2 contains a second function, a table-valued function called CreateDateRange, to create a list of week ending dates for the date range we need. The function uses a recursive query to build a range of dates.

;WITH DateCTE(WeekEndDate) AS
   SELECT @StartDate AS WeekEndDate
      UNION ALL 
   SELECT DateAdd(d,7,WeekEndDate) 
          AS WeekEndDate
        FROM DateCTE
         WHERE WeekEndDate < @EndDate )

Finally, let’s talk about a calculation involving a 52-week moving average. Essentially, for any one week, we want to get the average sales over the last 52 weeks. For instance, in the tooltip for the plotted point in Figure 2, the week of 4/19/2008 has a 52-week average of roughly $791,835. This means that for the date range of 4/21/2007 to 4/19/2008, the average weekly sales was roughly $791,835. (Subsequently, the 52-week average for the week ending 4/26/2008 would be the average weekly sales from 4/28/2007 to 4/26/2008. This is why we call it a “moving average”, because the range of 52 weeks “moves” with each subsequent week.

Tip 2: Using Table-Valued Functions for Role-based Security

Scenario: I have a SQL query that summarizes purchase orders by Vendor, for a specific ship method. I want our shipping managers to be able to run the query, without actually having SELECT access rights to the table, and so that they can only see summarized vendor orders for their specific ship method.

I frequently go back and forth between OLAP databases and relational databases. I bring this up only because OLAP databases provide dimensional security roles, which make addressing this scenario very easy. However, SQL Server relational databases don’t have built-in row-based security, so we need to implement something.

Because a stated requirement was no SELECT rights to the table, you might be thinking of a stored procedures or a view. Initially this might seem fine, but there are some issues. First, a SQL Server View cannot receive parameters, and we’d need one for the ship method. A stored procedure can certainly receive parameters - and could be a solution - but I’ll add an additional requirement - that database tasks run by a ship manager might want to use the results of the query as an intermediate result set, and subsequently join to other data. Result sets from stored procedures don’t work perfectly in this scenario - you can’t directly join the results of a stored procedure without introducing another table or temp table. Ironically, a view might be better - but we already know that views can’t receive parameters. So what can we do?

A feature in SQL Server 2005 that, to this day, doesn’t get enough attention is table-valued functions. I showed a TVF in Tip #1 to produce a date range - as it turns out, we can use TVFs in this scenario as well.

Listing 4 shows how table-valued functions can be used in conjunction with views to consolidate queries for role-based security. The code walks through creating a TVF with a parameter and then a set of views - and then sets rights for two logins to the views. This helps to overcome a limitation of SQL views - that you can’t pass parameters to them.

Tip 3: SQL Server Covering Indexes

Suppose you’re querying against an invoice table, based on a customer key - and you plan to return non-indexed columns (quantities, dollar amounts, etc.). Even if the query can optimize the customer key, SQL Server still must perform a lookup into the table (or clustered index) to return the other columns.

You can increase performance and lower the number of reads by creating a COVERING INDEX. This type of index will allow you to “bring along other columns for the ride” so that SQL Server won’t have to read into the table or clustered index. In other words, the index covers the entire query.

For example, Listing 5 shows a query that sums product line item dollar amounts for specific products - and the query generates an execution plan with an index scan (Figure 3).

Figure 3: Before non-clustered covering index.

You can optimize the query by creating a non-clustered index that also “covers” the dollar amount. Note the improved execution plan that does an index seek (Figure 4). Also note the STATISTICS IO, and the reduced number of reads when using a covering index.

Figure 4: After non-clustered covering index.

Covering indexes come at a cost - they increase the size of the index, and might introduce performance issues when rows are add/deleted. So their benefits and costs need to be examined - but there is no question that they can greatly enhance query performance.

Covering indexes….can help with performance, but they’re also larger and might slow down insertions.

Also note that the columns you “include” are not part of the index key itself.

Tip 4: SQL Server SARGs (Searchable Arguments) and Performance

Take a look at Listing 6 - two queries that retrieve orders for March of 2007. Which is faster - the one that uses the YEAR and MONTH function, or the one that queries BETWEEN two dates?

Answer: the latter, hands down. The reason? SQL Server cannot optimize certain functions. These are not considered SARGable (Search Argument-enabled). SQL Server needs to use an index scan (Figure 5) for the YEAR and month function, but can use an index seek (Figure 6) for the BETWEEN statement.

Figure 5: Query using non-SARGable expression (index scan).
Figure 6: Query using SARGable expression (index seek).

Also, IO Statistics reports that the optimized query does eight times fewer logical reads, and has a much lower execution cost.

From my last Baker’s Dozen issue, I also showed that LIKE is more optimized than SUBSTRING, for the very same reasons.

Tip 5: Using T-SQL SUM OVER to Calculate a % of Total

Some people know that we can RANK “over” a set of data, but how many realized that you can do a SUM “over” a set of data? Listing 5 and Figure 7 show how to SUM over a larger set of data, to implement a % of Parent calculation.

Figure 7: Query using SUM OVER to calculate % of Parent.

This is another example of the “windowing” functions that were introduced in SQL Server 2005, to aggregate/rank “over” a set/window of rows.

Tip 6: Programmatically Disabling SQL Server Constraints and Triggers

In certain data warehousing scenarios, we might want to disable database table constraints and triggers, to optimize performance. Listing 6 shows how we can programmatically disable constraints and triggers, and then re-enable them.

It might seem odd to talk about disabling table constraints. But keep in mind that many ETL scenarios will perform all necessary validations and controls - as if the table constraints didn’t exist to begin with.

Tip 7: Correlated Subqueries vs Derived Table Subqueries

In my last article I talked about a derived table subquery for handling multiple 1-many relationships. In the example, it was impossible to perform all the logic in one SELECT statement, so we needed to pre-aggregate each child table independently.

We can also use correlated subqueries - yes, the much-maligned correlated subquery that, as it turns out, can generate the same execution plan and the same IO Statistics!!!

The much-maligned correlated subquery approach will often generate the same execution plan and IO Statistics. However, a developer should aways check the execution plan and IO Statistics.

Listing 7 shows how we can use correlated subquery expressions to retrieve the summarized purchase order and sales order totals by product. Note that you should ALWAYS view execution plans and IO Statistics before assuming that the database engine will generate a good execution plan for a correlated subquery.

Tip 8: A Brain Teaser on UNION

So you think you know UNION? Maybe you do…or then again….

Did you know that a UNION will perform a duplicate check and a UNION ALL won’t? Did you know you needed an identical number of columns? Did you know that you can only use one ORDER BY, at the end? Did you know that when the UNION performs a duplicate check, it does so across ALL columns?

Listing 8 has a UNION example that appends rows from an employee table and a company table into one result set. See if you can follow along and guess the number of resulting rows at the end. (Sorry, no cash prize if you guess the correct number of rows).

Tip 9: So When Is a Subquery Necessary?

Scenario: I want to see each vendor and their highest order amount - but only if the highest order amount is $150,000 or more.

In my classes where I teach T-SQL programming, a common question is, “How do I know when I NEED a subquery?” To answer this question, we need to understand the data, the SQL language, and also some general patterns.

First, a vendor might have multiple orders that are over $150,000 and are the same order amount (i.e. Vendor 123 might have three orders of $200,000). So we can’t assume that we’ll only be showing one order.

This is a situation where we need to make multiple “dives” into the table…we can’t do it as a single query (you can try, but you won’t succeed!). We need to do the following, as separate steps:

First, we need to determine the highest dollar amount by vendor (for those orders greater than $150,000) and place that into a temporary result set or subquery.

Second, we need to take that list and dive “back” into the table to get the corresponding order IDs for that specific vendor and dollar amount. (Remember, there could be more than one order.)

Listing 11 shows an example where a subquery is needed to first retrieve maximum order amounts by customer, and then turn back around and retrieve the specific orders.

Tip 10: The tinyint data type

There’s no code sample for this one - just a brief discussion about the use of the tinyint datatype in SQL Server. This data type is perfect for status codes and business enumeration values. A tinyint can go from a range of 0-255, and only occupies one byte of storage per value

Tip 11: DISTINCT vs GROUP BY

Suppose we wanted to get a distinct list of names and see how many names are duplicated? Suppose we also wanted to know the number of duplications?

Some developers will use DISTINCT and some will use GROUP BY. Keep in mind that we are talking about two different behaviors. DISTINCT is great to produce a unique list but it won’t tell us if “John Smith” exists five times, if “Mary Wilson” exists four times, etc. For that, we need a GROUP BY with a COUNT aggregation, as well as a HAVING to filter on the number of COUNT aggregations. Listing 12 illustrates the difference between the two statements.

Tip 12: Creating a Result Set of Integers

A while back, a friend of mine was trying to figure out how to write a query to produce a result set of 20 integers (a sequence of numbers from 1-20).

Sure, you can create a variable and use a WHILE but that requires multiple lines of code, and you might be in a situation where the ONLY thing you can write is a SELECT statement.

Yes, you could also do 20 UNION statements. There’s also another way, using the recursive capabilities of a common table expression. Listing 13 shows a query to create a result set of 20 sequences using basic recursion.

Tip 13: INSTEAD OF Triggers for DELETE Scenarios

In some data warehousing scenarios (and even some OLTP databases), DELETE is a four-letter word. Companies might mark rows as inactive, but might not want to DELETE rows.

You might think that if there are PK/FK constraints, then it wouldn’t be possible to DELETE a row that had child transactions. That’s assuming that PK/FK constraints exist….one could argue they SHOULD….but that’s not a given.

So the developer team might implement an inactive status…..but suppose someone isn’t fully aware (but has rights), and tries to manually DELETE a row? Well, what’s needed is a big (and intelligent) watchdog that will sit in front of table/row, stop any deletion attempts and turn them into updates on the inactive status.

Well, SQL Server has such a watchdog - the database trigger - and in this case, an INSTEAD OF trigger. Essentially we can create an INSTEAD OF trigger on a DELETE. This trigger will fire when a process attempts to DELETE a row - and the code in the trigger can update the row’s status flag “instead of” deleting the row. Listing 14 shows an example of an INSTEAD OF trigger on DELETE actions. Note that the trigger code reads the system DELETED table to capture the rows that the application/user tried to delete - and instead, updates the rows with an inactive status.

Next Time Around in the Baker’s Dozen

In my next article, I’ll talk about some of the great new features in SQL Server 2012 (“Denali”). SQL Server 2012 has a new Column Store index that can greatly help the performance of certain data warehousing queries, several new T-SQL features, new and improved functionality in SSIS, and an entirely new landscape for creating business intelligence applications. Stay tuned!

Listing 1: Scalar function to get Week Ending Date

use AdventureWorks2008R2
go      
      
CREATE FUNCTION [dbo].[WeekEndingDate] 
( @InputDate date  )
RETURNS Date       
AS      
BEGIN      
   DECLARE @ReturnDate DATE 
   SET @ReturnDate = 
       dateadd(day,
              (@@DateFirst - datepart(weekday,@InputDate)),
               @InputDate)
   RETURN @ReturnDate
END      
GO      

Listing 2: TVF to create a range of Week Ending dates

use AdventureWorks2008R2
go      
      
CREATE FUNCTION [dbo].[CreateDateRange]
(@StartDate Date, @EndDate Date)
RETURNS       
@WeekList TABLE (WeekEndDate Date)
AS      
BEGIN      
      
;WITH DateCTE(WeekEndDate) AS
   SELECT @StartDate AS WeekEndDate
      UNION ALL 
        SELECT DateAdd(d,7,WeekEndDate) AS WeekEndDate
          FROM DateCTE WHERE WeekEndDate < @EndDate )
      
   INSERT INTO @WeekList 
     SELECT * FROM DateCTE 
         OPTION (MAXRECURSION 1000) 
      
RETURN       
END      
GO      

Listing 3: Query to use the TVF and scalar UDF

use AdventureWorks2008R2
go      
      
DECLARE @StartDate DATE = dbo.WeekEndingDate('7-1-2007')
DECLARE @EndDate DATE = dbo.WeekEndingDate('6-30-2008')
      
DECLARE @StartDateOneYearAgo DATE = 
       dbo.WeekEndingDate(DATEADD(YEAR,-1,@StartDate))
      
;WITH TempWeeklyOrderCTE AS
  (SELECT dbo.WeekEndingDate(OrderDate) as WeekEndDate, 
           SUM(TotalDue) AS TotalDue
               FROM Purchasing.PurchaseOrderHeader
             WHERE OrderDate BETWEEN
                   @StartDateOneYearAgo AND @EndDate
   GROUP BY dbo.WeekEndingDate(OrderDate) )
       
                   
SELECT DateListCTE.WeekEndDate, 
       TempWeeklyOrderCTE.TotalDue, 
       AVG(TempCTEHistory.TotalDue) AS WeeklyMovingAvg
 FROM dbo.CreateDateRange(@StartDate, @EndDate) DateListCTE
  LEFT JOIN TempWeeklyOrderCTE
      ON DateListCTE.WeekEndDate = 
                  TempWeeklyOrderCTE.WeekEndDate   
      
  LEFT JOIN TempWeeklyOrderCTE AS TempCTEHistory 
      ON TempCTEHistory.WeekEndDate BETWEEN
              DATEADD(WEEK,-53,DateListCTE.WeekEndDate) AND
                      DateListCTE.WeekEndDate  
       WHERE  DateListCTE.WeekEndDate BETWEEN
              @StartDate AND @EndDate
GROUP BY DateListCTE.WeekEndDate, TempWeeklyOrderCTE.TotalDue
ORDER BY DateListCte.WeekEndDate  

Listing 4: Using Table-Valued Functions to facilitate Views for security

use AdventureWorks2008R2
go      
CREATE FUNCTION dbo.GetSummarizedPurchaseOrdersForShipMethod   
( @ShipMethodID int)
returns table              --  an in-memory table      
as      
   return
         select Vendor.Name, SUM(TotalDue) as TotDollars
                 from Purchasing.PurchaseOrderHeader POH
                   join Purchasing.Vendor on POH.VendorID = 
                                 Vendor.BusinessEntityID  
                          where ShipMethodID = @ShipMethodID 
                     group by Vendor.Name
go      
      
      
CREATE VIEW vwSummarizedOrdersShipper_XRQ as
     select * from  dbo.GetSummarizedPurchaseOrdersForShipMethod(1)
    
go      
                          
CREATE VIEW vwSummarizedOrdersShipper_ZY as
     select * from  dbo.GetSummarizedPurchaseOrdersForShipMethod(2)
      
go      
      
CREATE LOGIN ShipLogin_XRQ WITH PASSWORD = 'J345#$)thb';
CREATE LOGIN ShipLogin_ZY WITH PASSWORD = 'Uor80$23b';
      
      
CREATE USER ShipUser_XRQ FOR LOGIN Shiplogin_XRQ;
CREATE USER ShipUser_ZY FOR LOGIN Shiplogin_ZY;
      
      
grant select on vwSummarizedOrdersShipper_XRQ to ShipUser_XRQ
grant select on vwSummarizedOrdersShipper_ZY to ShipUser_ZY
      
execute as login = 'ShipLogin_XRQ'
      
select * from vwSummarizedOrdersShipper_XRQ   
-- User can see the summarized data for ship method 1      
      
select * from vwSummarizedOrdersShipper_ZY   
-- User cannot see the summarized data for ship method 2      
      
select * from Purchasing.PurchaseOrderHeader   
-- User cannot see Purchasing.PurchaseOrderHeader       
      
select SUSER_NAME()
      
  select * from  dbo.GetSummarizedPurchaseOrdersForShipMethod(2) 
 -- Ship Login1 cannot run that function
      
  select * from  dbo.GetSummarizedPurchaseOrdersForShipMethod(1) 
-- Ship Login1 cannot run that function,       
-- regardless of the parameter....because shiplogin1 does not have       
-- EXECUTE rights to the function      
  
revert      

Listing 5: Effects of a non-clustered covering index

--Scan count: Number of index or table scans performed.      
      
--Logical reads: Number of pages read from the data cache.      
      
--Physical reads: Number of pages read from disk.      
      
--Read-ahead reads: Number of pages placed into the cache       
--for the query.      
      
set statistics io on      
set statistics time on      
      
-- before....IO cost is .049, CPU cost is .009,       
-- # of logical reads is 66      
      
SELECT  Product.Name, SUM(LineTotal) as ProductTotal
   from Production.Product
      join Purchasing.PurchaseOrderDetail POD
           ON Product.ProductID = POD.ProductID
               WHERE ProductSubcategoryID = 22
      group by Product.Name           
      
      
CREATE NONCLUSTERED INDEX [OrderDetails_IX_ProductID] ON 
         Purchasing.PurchaseOrderDetail 
(
   [ProductID] ASC ) INCLUDE (  [LineTotal] )
      
--after...IO cost is .003   CPU cost is .00019,       
-- # of logical reads is 16      

Listing 6: Difference between SARGable and non-SARGable

set statistics io on      
set statistics time on      
      
      
CREATE INDEX IX_OrderDate2 on Purchasing.PurchaseOrderHeader 
        (OrderDate)
include ( VendorID, TotalDue)
      
      
-- INDEX SCAN, non-sargable, 16 logical reads      
select PurchaseOrderID, VendorID, OrderDate, TotalDue from 
  Purchasing.PurchaseOrderHeader
       where yEAR(OrderDate) = 2007 and MONTH(OrderDate) = 3
  
  
-- INDEX SEEK, sargable , two logical reads      
select PurchaseOrderID, OrderDate, TotalDue from 
   Purchasing.PurchaseOrderHeader
    where OrderDate >= '3-1-2007' and OrderDate <= '3-31-2007'

Listing 7: SUM OVER to calculate % of Total

SELECT ShipName, TotShipDollars, 
       SUM(TotShipDollars) over() as GrandTotal , 
       TotShipDollars / SUM(TotShipDollars) over() as PctOfTotal
from       
 (SELECT SM.Name AS ShipName,  SUM(POH.TotalDue) as TotShipDollars
   FROM Purchasing.ShipMethod  SM
        JOIN Purchasing.PurchaseOrderHeader POH 
                ON SM.ShipMethodID = poh.ShipMethodID
                group by SM.Name) TempAlias

Listing 8: Disabling triggers and constraints

ALTER TABLE Production.Product DISABLE TRIGGER ALL
ALTER TABLE Production.Product NOCHECK CONSTRAINT ALL
      
ALTER TABLE Production.Product ENABLE TRIGGER ALL
ALTER TABLE Production.Product NOCHECK CONSTRAINT ALL

Listing 9: Many aggregations using CTEs and correlated subqueries

--Correlated subquery for multiple 1-many relationships      
      
SELECT  Product.Name as ProductName, 
      ( select SUM(LineTotal) as POLineTotal
              from Purchasing.PurchaseOrderDetail  POD
                         where pod.ProductID = Product.ProductID)
 as POLineTotal , 
      
      (  select  SUM(LineTotal) as SOLineTotal
              from Sales.SalesOrderDetail   SOD
                         where sod.ProductID = Product.ProductID) 
 as SOLineTotal                                
      
    FROM Production.Product
    order by POLineTotal desc
      
      
--From my last article…same result set, with derived table subquery      
      
      
;with POCTE as 
   ( select pod.ProductID, SUM(LineTotal) as POLineTotal
              from Purchasing.PurchaseOrderDetail  POD
                           GROUP BY  pod.ProductID )  ,   
SOCTE  AS
   ( select sod.ProductID, SUM(LineTotal) as SOLineTotal
              from Sales.SalesOrderDetail   SOD
                GROUP BY  sod.ProductID )                
               
SELECT  Product.Name as ProductName, POCTE.POLineTotal ,  
        SOCTE.SOLineTotal
    FROM Production.Product
           left join POCTE  ON Product.ProductID = POCTE.ProductID
           LEFT JOIN SOCTE ON Product.ProductID = SOCTE.ProductID
           order by POLineTotal DESC

Listing 10: A brain-teaser on the UNION statement

CREATE TABLE dbo.EmpList (EmployeeName varchar(50), 
                           Address varchar(50),ZipCode varchar(5), 
                          PhoneNumber varchar(10))
      
CREATE TABLE dbo.ClientList (ClientName varchar(50), 
                           Address varchar(50),ZipCode varchar(5))
      
--Insert 4 rows, although the first two are duplicates      
      
insert into dbo.EmpList values ('Kevin Goff', '111 Main Street', 
                               '11111', '111-1111')
      
insert into dbo.EmpList values ('Kevin Goff', '111 Main Street', 
                               '11111', '111-1111')
      
insert into dbo.EmpList values ('Kevin Goff', '111 Main Street', 
                               '11112', '111-1111')
      
insert into dbo.EmpList values ('John Smith', '222 Main Street', 
                               '22222', '222-2222')
      
      
--Insert three rows, although the first two are duplicates…and      
--the row for Kevin Goff will not count as a duplicate, because      
--the EmpList table had a phone number      
      
insert into dbo.ClientList values ('Mark Smith', '333 Main Street',
                                '33333')
      
insert into dbo.ClientList values ('Mark Smith', '333 Main Street',
                                '33333')
      
insert into dbo.ClientList values ('Kevin Goff', '111 Main Street',
                                '11111')
      
SELECT EmployeeName AS Name  , Address, ZipCode as PostalCode ,
         PhoneNumber, 'E' as RecType
   FROM dbo.EmpList
UNION       
   SELECT ClientName AS Name , Address, ZipCode , 
       'No Phone' as PhoneNumber, 'C' as RecType
   from dbo.ClientList
ORDER BY Postalcode, Name

Listing 11: When is a subquery necessary?

USE AdventureWorks2008R2
GO      
      
--for vendors with orders         >        = 150K, retrieve the vendor and      
--the maximum order amount      
      
;WITH MaxOrdersCTE AS
  (SELECT VendorID, MAX(TotalDue) as TotalVendorOrder
      from Purchasing.PurchaseOrderHeader
         WHERE TotalDue > 150000
         group by VendorID)
         
--query back into order table to retrieve the order information      
select  POH.VendorID , POH.PurchaseOrderID, TotalVendorOrder 
   from MaxOrdersCTE         
      JOIN Purchasing.PurchaseOrderHeader POH 
             ON POH.VendorID = MaxOrdersCTE.VendorID
             AND POH.TotalDue = MaxOrdersCTE.TotalVendorOrder
      ORDER BY POH.VendorID, POH.PurchaseOrderID

Listing 12: DISTINCT vs GROUP BY

Use AdventureWorks2008R2
go      
      
select COUNT(*) from Person.Person   
      
SELECT       
DISTINCT           
LastName, FirstName, MiddleName
FROM Person.Person
ORDER BY LastName, FirstName
  
      
SELECT       
LastName, FirstName, MiddleName, COUNT(*) as NumberOfNames   
FROM Person.Person
group by LastName, FirstName, MiddleName
having COUNT(*) > 1   
order by NumberOfNames desc, LastName, FirstName

Listing 13: Creating a table result of integer sequences

;WITH IntPickList AS 
     (SELECT 1 AS IntItem
           UNION ALL
               SELECT IntItem + 1 AS IntItem FROM IntPickList
                    WHERE IntItem <= 20)
SELECT * FROM IntPickList

Listing 14: An INSTEAD OF Trigger

CREATE TABLE dbo.SimpleVendorMaster 
   (VendorID int identity, VendorName varchar(50), 
    ActiveFlag bit, LastUpdate datetime default getdate(), 
    LastUser varchar(50) default suser_sname())
GO      
   
      
INSERT INTO SimpleVendorMaster (VendorName, ActiveFlag) 
                             VALUES ('Vendor A', 1)
INSERT INTO SimpleVendorMaster (VendorName, ActiveFlag) 
                             VALUES ('Vendor B', 1)
INSERT INTO SimpleVendorMaster (VendorName, ActiveFlag) 
                            VALUES ('Vendor C', 1)
GO      
 
   
      
CREATE TRIGGER [dbo].[DeleteTriggerSimpleVendorMaster] ON
      [dbo].[SimpleVendorMaster]
INSTEAD OF DELETE      
AS      
   SET NOCOUNT ON
   BEGIN
       DECLARE @DeleteCount int
       SET @DeleteCount = (SELECT COUNT(*) FROM deleted)
       IF (@DeleteCount > 0)
           UPDATE dbo.SimpleVendorMaster SET ActiveFlag = 0, 
                         LastUser = SUSER_NAME(),
                         LastUpdate = GETDATE()
                         WHERE VendorID IN 
                                (SELECT VendorID FROM deleted)
   END                         
go      
   
DELETE FROM dbo.SimpleVendorMaster WHERE VendorID > 1

Open original image