12 March 2013

To improve the I/O performance


This blog post is in the response of the T-SQL Tuesday #004: IO by Mike Walsh. The subject of this month is IO. Here is my quick blog post on how Cover Index can Improve Performance by Reducing IO.
Let us kick off this post with disclaimers about Index. Index is a very complex subject and should be exercised with experts. Too many indexes, and in particular, too many covering indexes can hamper the performance. Again, indexes are very important aspect of performance tuning. In this post, I am demonstrating very limited capacity of Index. We will create covering index for query and see how its performance improves as IO reduces. Please note that do not run this scripts on production server as they contain DBCC commands which can create performance issues. This script is created for demo purpose only.
Let us run the following query and see the DBCC outcome along with the enabling of actual execution plan. Estimated Execution plan may work but I have used actual execution plan in this demo.
USE [AdventureWorks]
GO
SET STATISTICS IO ON
GO
-- Clean the buffers
-- Do not run these DBCC on production server
DBCC FREEPROCCACHE
DBCC DROPCLEANBUFFERS
GO
-- Run Select Statement
SELECT ProductID, ProductNumber, Color
FROM Production.Product
WHERE ProductID < 500
GO
The result of the statistics is as follows.
Table 'Product'. Scan count 1, logical reads 6, physical reads 3, read-ahead reads 16, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
The result of the execution plan mouseover displays many different information. Please pay attention to IO cost.


























Now let us create covering index on the query which we have just ran. We will include all the columns from the select statement in our newly created index.
-- Create Covering Index
IF EXISTS (SELECT * FROM sys.indexes WHERE OBJECT_ID =OBJECT_ID(N'[Production].[Product]') AND name = N'AK_Product_Cover')
DROP INDEX [AK_Product_Cover] ON [Production].[Product]
GO
CREATE UNIQUE NONCLUSTERED INDEX [AK_Product_Cover] ON [Production].[Product]
(
ProductID, ProductNumber, Color
) ON [PRIMARY]
GO
Now as cover index is created, let us run the above query once again. I will keep the actual execution plan and Statistics IO on.
-- Clean the buffers
DBCC FREEPROCCACHE
DBCC DROPCLEANBUFFERS
GO
-- Run the following Select
-- Use the hint if the query does not use the index only
SELECT ProductID, ProductNumber, Color
FROM Production.Product --WITH (INDEX(AK_Product_Cover))
WHERE ProductID < 500
GO
The above query will pick up the newly created index right away. If due to any reason, it does not use our index for demo purpose, you can use hint to use that index. Let us examine the results once the query runs.
Table 'Product'. Scan count 1, logical reads 3, physical reads 1, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
The result of the execution plan mouseover displays many different information. Please pay attention to IO cost.






























Let us compare the statistics here first.
Before Cover Index: IO Cost 0.006088
logical reads 6, physical reads 3, read-ahead reads 16,
After Cover Index: IO Cost 0.0038657
logical reads 3, physical reads 1, read-ahead reads 0
Logical read is almost half and Physical reads are much reduced after the creation of  cover index. Additionally, read-ahead reads are reduced with big numbers as well. Now when comparing execution plans, the IO cost is reduced to almost half after creating the covering index.
It is quite clear from this demo that IO is reduced due to cover index.
Let us compare the performance of the both the queries here. I have forced the indexes on them to simulate their original situation.
-- Comparing Performance
SELECT ProductID, ProductNumber, Color
FROM Production.Product WITH (INDEX(PK_Product_ProductID))
WHERE ProductID < 500
GO
SELECT ProductID, ProductNumber, Color
FROM Production.Product WITH (INDEX(AK_Product_Cover))
WHERE ProductID < 500
GO
It is quite clear from this demo that IO is reduced due to cover index, and additionally, the performance improves.











Alternative to Cursors


Example of Cursor Alternative
Suppose we have table "ProductSales" that stores the information about each product sales. Now we want to calculate the Total Sales Quantity and Amount of each and every product.
We can solve this problem by following three methods.
  1.  CREATE TABLE ProductsSales
  2. (
  3.  ID int IDENTITY(1,1NOT NULL,
  4.  ProductID int NOT NULL,
  5.  ProductName varchar(50NOT NULL,
  6.  Qty int NOT NULL,
  7.  Amount decimal(102NOT NULL )
  8. GO
  9. SELECT * FROM ProductsSales
  10. --We have the table with below data 











Problem solution methods
1.                 Using Cursor
1.   SET NOCOUNT ON
2.  DECLARE @ProductID INT
3.  DECLARE @ProductName VARCHAR(100)
4.  DECLARE @TotalQty INT
5.  DECLARE @Total INT
6.  DECLARE @TProductSales TABLE
7.  (
8.   SNo INT IDENTITY(1,1),
9.   ProductID INT,
10.ProductName VARCHAR(100),
11.TotalQty INT,
12.GrandTotal INT
13.)
14.--Declare Cursor
15.DECLARE Cur_Product CURSOR FOR SELECT DISTINCT ProductID FROM ProductsSales
16.--Open Cursor
17.OPEN Cur_Product
18.--Fetch Cursor
19.FETCH NEXT FROM Cur_Product INTO @ProductID
20.WHILE @@FETCH_STATUS = 0
21.BEGIN
22.SELECT @ProductName = ProductName FROM ProductsSales WHERE ProductID = @ProductID
23.SELECT @TotalQty = SUM(Qty),@Total = SUM(Amount) FROM ProductsSales WHERE ProductID = @ProductID
24.INSERT INTO @TProductSales(ProductID,ProductName,TotalQty,GrandTotal) VALUES(@ProductID,@ProductName,@TotalQty,@Total)
25.FETCH NEXT FROM Cur_Product INTO @ProductID END
26.--Close and Deallocate Cursor
27.CLOSE Cur_Product
28.DEALLOCATE Cur_Product
29.--See Calculated data
30.SELECT * FROM @TProductSales 









2.                 Using Table Variable
1.   SET NOCOUNT ON
2.  DECLARE @ProductID INT
3.  DECLARE @ProductName VARCHAR(100)
4.  DECLARE @TotalQty INT
5.  DECLARE @Total INT
6.  DECLARE @i INT =1
7.  DECLARE @count INT
8.   --Declare Table variables for storing data
9.  DECLARE @TProduct TABLE ( SNo INT IDENTITY(1,1),
10.ProductID INT
11.)
12.DECLARE @TProductSales TABLE
13.(
14.SNo INT IDENTITY(1,1),
15.ProductID INT,
16.ProductName VARCHAR(100),
17.TotalQty INT,
18.GrandTotal INT
19.)
20.--Insert data to Table variable @Product
21.INSERT INTO @TProduct(ProductID)
22.SELECT DISTINCT ProductID FROM ProductsSales ORDER BY ProductID ASC
23.-- Count number of rows
24.SELECT @count = COUNT(SNo) FROM @TProduct WHILE (@i <= @count)
25.BEGIN
26.SELECT @ProductID = ProductID FROM @TProduct WHERE SNo = @i
27.SELECT @ProductName = ProductName FROM ProductsSales WHERE ProductID = @ProductID
28.SELECT @TotalQty = SUM(Qty),@Total = SUM(Amount) FROM ProductsSales WHERE ProductID = @ProductID
29.INSERT INTO @TProductSales(ProductID,ProductName,TotalQty,GrandTotal) VALUES(@ProductID,@ProductName,@TotalQty,@Total)
30.SELECT @i = @i + 1
31.END
32.--See Calculated data
33.SELECT * FROM @TProductSales 









3.                 Using Temporary Table
1.   SET NOCOUNT ON
2.  DECLARE @ProductID INT
3.  DECLARE @ProductName VARCHAR(100)
4.  DECLARE @TotalQty INT
5.  DECLARE @Total INT
6.  DECLARE @i INT =1
7.  DECLARE @count INT
8.  --Create Temporary Tables for storing data
9.  CREATE TABLE #TProduct ( SNo INT IDENTITY(1,1),
10.ProductID INT
11.)
12.CREATE TABLE #TProductSales
13.(
14.SNo INT IDENTITY(1,1),
15.ProductID INT, ProductName VARCHAR(100), TotalQty INT, GrandTotal INT )
16.--Insert data to temporary table #Product
17.INSERT INTO #TProduct(ProductID) SELECT DISTINCT ProductID FROM ProductsSales ORDER BY ProductID ASC
18.SELECT @count = COUNT(SNo) FROM #TProduct
19.WHILE (@i <= @count)
20.BEGIN
21.SELECT @ProductID = ProductID FROM #TProduct WHERE SNo = @i
22.SELECT @ProductName = ProductName FROM ProductsSales WHERE ProductID = @ProductID
23.SELECT @TotalQty = SUM(Qty),@Total = SUM(Amount) FROM ProductsSales WHERE ProductID = @ProductID
24.INSERT INTO #TProductSales(ProductID,ProductName,TotalQty,GrandTotal) VALUES(@ProductID,@ProductName,@TotalQty,@Total)
25.SELECT @i = @i + 1
26.END
27.--See Calculated data
28.SELECT * FROM #TProductSales
29.--Now Drop Temporary Tables
30.DROP TABLE #TProduct
31.DROP TABLE #TProductSales