Showing posts with label Statistics. Show all posts
Showing posts with label Statistics. Show all posts

Tuesday, October 20, 2009

IO Stats – What Are You Missing?

In this post, I will address a common misconception that IO statistics are always reliable.  The truth of the matter is IO stats can sometimes yield incorrect information, which in turn may influence bad coding habits. I have had developers tell me that the scalar UDF query is better because it has less IO than the set based TVF query.  In situations like these it is important to express the message that you must account for more than IO when implementing optimization techniques, but in some cases IO can be misinterpreted. The question on the table is, “How can IO statistics be wrong?”  The short answer is IO stats are mostly correct and only under certain circumstances IO statistics are misrepresented in SSMS.  So what are these magical circumstances? The IO statistics become invalid anytime a scalar UDF is used.  The optimizer only accounts for the base table and not any of the IO encountered inside the scalar UDF, which misconstrues the query IO.  Let’s look at an example.

First I will create the tables, with data.

USE [tempdb]
GO
 
IF OBJECT_ID('tempdb.dbo.t1') IS NOT NULL
BEGIN
    DROP TABLE tempdb.dbo.t1;
END
GO
 
CREATE TABLE t1(
id INT,
col CHAR(1)
);
GO
 
INSERT INTO t1 VALUES (1,'a');
INSERT INTO t1 VALUES (2,'b');
GO
 
IF OBJECT_ID('tempdb.dbo.t2') IS NOT NULL
BEGIN
    DROP TABLE tempdb.dbo.t2;
END
GO
 
CREATE TABLE t2(
t2_id INT IDENTITY(1,1),
t1_id INT,
col CHAR(1)
);
GO
 
INSERT INTO t2 VALUES (1,'c');
INSERT INTO t2 VALUES (1,'d');
INSERT INTO t2 VALUES (1,'e');
INSERT INTO t2 VALUES (1,'f');
INSERT INTO t2 VALUES (2,'d');
INSERT INTO t2 VALUES (2,'g');
GO

The next step is to create our scalar UDF.

CREATE FUNCTION dbo.fn_ConcatenateCols(@id INT)
RETURNS VARCHAR(8000)
AS
BEGIN
    DECLARE @rtn varchar(8000)
    
    SELECT @rtn = COALESCE(@rtn + ',','') + t2.col
    FROM dbo.t2
    WHERE t2.t1_id = @id
    
    RETURN @rtn
END
GO

Now that I have all my sample DDL in place, we can run a simple test to measure our IO.

SET NOCOUNT ON 
GO
SET STATISTICS IO ON
GO
 
--Missing I/O
SELECT id,dbo.fn_ConcatenateCols(id)
FROM [dbo].[t1];
 
SET STATISTICS IO OFF
GO
/*
id          
----------- --------
1           c,d,e,f
2           d,g
 
Table 't1'. Scan count 1, logical reads 1, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
*/

So what is missing from the results above?  If you look closely you will see that t2 is nowhere in the IO stats.  Let’s start a profiler trace and run the same query again.  Open SQL Server profiler and use the standard template. Once the profile is tracing, run the same query again. If you want the most accurate number of reads make sure to turn off query results Tools –> Options –> Query Results –> SQL Server –> Results To Grid –> Discard Results After Execution. This time around you will see the number of reads is 9 , as shown below.

image

As you can see IO statistics are a lot different than the actual number of logical reads, using IO Statistics.  This behavior can be a huge surprise to many unsuspecting victims.  Yes I did use the word victim :).  I say victim because this usually occurs to an individual that expects IO to be presented correctly and trusts Microsoft enough to not question their information.  This “victim” never thinks twice about questioning the information returned, which can be a huge performance problem. 

The take away is developers should always be careful when using scalar functions because they can really degrade performance and never trust anyone’s word,  not even Microsoft’s or mine. Always test yourself.  If you have not done so, I recommend reading my post on correlated subqueries, as it does apply to functions as well, http://jahaines.blogspot.com/2009/06/correlated-sub-queries-for-good-or-evil.html.  In my next post, I will show you how to get rid scalar functions and use Inline TVFs to optimize performance, while encapsulating code logic.

Until next time, happy coding.

Friday, August 7, 2009

Are all covering indexes created equal?

I have been getting a lot of questions regarding indexes over the past few weeks, so I thought I would make another post about indexes. In this post, I will be focusing on covering indexes.  A covering index is an index that covers all the columns for a given query.  A covering index can be created by either, adding the columns to the index key, or by adding the columns to the include clause of the create index statement. I will focus on the key differences between adding columns to the index key and adding columns to the INCLUDE clause.  I will look at the differences in size, statistics, and the execution plan.

Let’s get started by creating our test table.

USE [tempdb]
GO
 
SET NOCOUNT ON;
GO
 
IF EXISTS(SELECT 1 FROM sys.tables WHERE NAME = 'TestData')
BEGIN
    DROP TABLE dbo.[TestData];
END
GO
 
CREATE TABLE dbo.TestData(
RowNum INT PRIMARY KEY,
SomeId INT,
SomeCode INT
);
GO
 
INSERT INTO dbo.[TestData] (
    [RowNum],
    [SomeId],
    [SomeCode]
) 
SELECT TOP 1000000 
    ROW_NUMBER() OVER (ORDER BY t1.NAME) AS RowNum,
    ABS(CHECKSUM(NEWID()))%2500+1 AS SomeId, 
    CASE 
        WHEN ROW_NUMBER() OVER (ORDER BY t1.NAME) % 2 = 0
        THEN 1
    ELSE 0
    END AS SomeCode
FROM 
    Master.dbo.SysColumns t1,
    Master.dbo.SysColumns t2
GO

Now that we have our test table, I will create a covering index using the INCLUDE clause.

CREATE INDEX ncl_idx_TestData_Cover1 ON dbo.TestData(SomeID) INCLUDE(SomeCode);
GO

I will issue a simple query to select data from the table, using the new index.

SELECT 
    someid,
    somecode
FROM dbo.[TestData]
WHERE
    someid = 500 
    AND somecode = 1

The results of this query are inconsequential for our purposes, so I will not post them here; however, I will post the key attributes of the execution plan.

image 

The above screenshot shows that our covering index is used.  The primary thing to note here is deviation between the actual and the estimated number of rows. I will get into why these numbers deviate later in the post.

Let’s have a look at the space consumed by the index.  I will  issue a system stored procedure called sp_spaceused to obtain the space consumption details.

EXEC sp_spaceused 'dbo.Testdata' 

image

As you can see from the screenshot above, our index size is 18008 KB or  17.58 MB = 18008 / 1024. Now that we have our index size, let’s look at the statistics. I will be using the DBCC command show_statistics to display statistical information about our index.

DBCC show_statistics('dbo.Testdata','ncl_idx_TestData_Cover1')

image

The main thing to note here is that the column “SomeCode” is not available in the density vector.  The density vector is located in the second result set produced by DBCC show_statistics.  Essentially, density is the uniqueness of the column data. For example, the density of some id is .0004.  The density calculation can be expressed as 1/(#Distinct Values). In the case of “SomeId” the density can be calculated as 1/2500= .0004.  The “SomeId,RowNum” density is calculated as 1/1000000 = .000001. I know the number of distinct values is equal to the number of rows because “RowNum” is a unique column.  If you want to calculate density programmatically, you can use a group by clause and a little creativity.

Note: you can use differing columns in the group by clause to get their respective density.

SELECT 1./SUM(cnt)
FROM (
SELECT 1 AS cnt
FROM dbo.testdata
GROUP BY someid,RowNum
) AS a

The optimizer uses density to estimate cardinality (estimated number of rows) when building a query plan.  Obviously, the better the cardinality the better the query plan. 

A benefit of the INCLUDE clause is the ability to supersede index limitations, such as the key column limit of  16, the  index key size limitation (900 bytes), and even include columns with data types like VARCHAR(MAX), VARBINARY(MAX), TEXT, IMAGE etc..

Next, I will create the second covering index, which includes all columns in the index key. I will drop the original covering  index, so I can accurately measure the index size.

DROP INDEX ncl_idx_TestData_Cover1 ON dbo.[TestData];
GO
 
----===== Covering index that includes both columns in the index key
CREATE INDEX ncl_idx_TestData_Cover2 ON dbo.TestData(SomeID,SomeCode)
GO

Now let’s run the same select query again.

SELECT 
    someid,
    somecode
FROM dbo.[TestData]
WHERE
    someid = 500 
    AND somecode = 1

image

As you can see the estimated number of rows deviates much less, from the actual, than the first covering index.  The difference in cardinality estimates is pretty negligible for this sample, but in a real world scenario, the deviation can be much larger.

Let’s have a look at the space used.

Note: You can use the same code as before.

image

As shown above, the index size is larger, when columns are added to the index key.  The new covering index is 18024 KB or 18024 / 1024 = 17.60 MB.  This index is larger because the columns are stored at all levels of the index, whereas, the include clause stores the column values at the leaf level only.  So why would you add columns to the index key?  This answer brings us to the statistics maintained by the new index. 

Note: You can use the same code as before but change the name of the index to ncl_idx_TestData_Cover2.

image

If you had not noticed from the screen shot above, SQL Server is maintaining the density of the “SomeCode” column in relation to the “SomeId” column, which was not the case for covering index 1.  This is why the optimizer is able to more accurately estimate cardinality.  In this case, the density value is represented as .0002.  Adding columns to the index key allows the optimizer to make better cardinality estimates, which may translate into more optimal query plans.

So what is the verdict?  My findings indicate that all covering indexes are  not created equal.  Both types of covering index has its own strengths and weaknesses. The type you choose will depend on your query and your business requirements.  There are a lot of advantages to using both types of covering indexes.  You will have to weigh performance against manageability and choose which is best for your environment.

References:

Friday, June 12, 2009

SQL Server Auto Statistics

I was asked a question, on the MSDN forums, regarding a custom function to automatically update statistics when a threshold is breached.  I went on to say that SQL Server already handles this for us automatically. Also that he should use sp_updatestats or UPDATE STATISTICS, if manual statistic updates were needed.  I wanted to briefly discuss some of the SQL Server built-in functionality regarding statistics. SQL Server has two options enabled by default, which help the database engine create and manage statistics.  These two options are AUTO_CREATE_STATISTICS and AUTO_UPDATE_STATISTICS. 

First let’s talk a about what statistics are and how the optimizer uses them.  Statistics are the information used to represent the distribution of column values, within a table or indexed view.  The optimizer uses this statistical information to make cardinality estimates.  So what does this mean…  it means the optimizer uses the information to make a estimate of the number of rows, in the query result. The more accurate the estimate, the better the query plan is.  In some cases, good statistics can make the difference between getting an index seek and getting an index scan.

Now that we know more about what statistics are, let’s talk about how SQL Server creates statistics.  The AUTO_CREATE_STATISTICS option allows the database engine to automatically create statistics on individual columns that appear in query predicates.  You can check statistics by querying the sys.stats catalog view.  When statistics are automatically created they are prefixed with “_WA”, but also have the flag auto_created set to 1.

SELECT 
    OBJECT_NAME(s.object_id) AS object_name,
    COL_NAME(sc.object_id, sc.column_id) AS column_name,
    s.name AS statistics_name
FROM sys.stats s 
INNER JOIN sys.stats_columns sc
    ON  s.stats_id = sc.stats_id 
        AND s.OBJECT_ID = sc.Object_id    
INNER JOIN sys.tables t
    ON t.OBJECT_ID = s.object_id    
WHERE 
    s.name like '_WA%'
    AND s.auto_created = 1
    AND t.TYPE = 'U'
    AND t.is_ms_shipped = 0
ORDER BY 
    s.name;

You should note that auto created statistics only are present on a single column, to obtain more dense statistical data you will need to create an index or manual statistics. Now that we have statistics, how does this help our query run faster? In short, if a query predicate contains a column with statistics, the optimizer does not have to guess at the number of rows affected by the query, thus the optimizer has enough information to intelligently create the query plan.

Now that we have statistics, we can rely on the database engine to keep them up. SQL Server uses AUTO_UPDATE_STATISTICS (enabled by default) to manage statistics.  Statistics are checked before query compilation or before executing a cached query plan. If a table has just been issued a large insert/delete transaction,the next query may experience slowness or delay because the statistics  have to be checked and updated. In this type of situation it is best to update the statistics manually, after the DML operation.  So how does the optimizer know the statistics are out-of-date? When checking statistics, the optimizer compares the number of data modifications, since the last update, against a threshold.  If the number of data modifications is greater than the threshold, the statistics are updated.  The technical threshold limits are listed below.

Statistics are considered out-of-date when:

  1. The table size has gone from 0 to >0 rows.

  2. The number of rows in the table when the statistics were gathered was 500 or less, and the column modification counter of the leading column of the statistics object has changed by more than 500 since then.

  3. The table had more than 500 rows when the statistics were gathered, and the column modification counter of the leading column of the statistics object has changed by more than 500 + 20% of the number of rows in the table when the statistics were gathered.

  4. If the statistics object is defined on a temporary table, it is out of date as discussed above, except that there is an additional threshold for recomputation at 6 rows, with a test otherwise identical to test 2 in the previous list.

You can view the statistical information for an object by executing DBCC SHOW_STATISTICS.  Here is a sample using the Adventure Works database:

DBCC show_statistics('[Production].ProductProductPhoto','_WA_Sys_00000002_01142BA1')

DBCC SHOW_STATISTICS: http://msdn.microsoft.com/en-us/library/ms174384.aspx

This is how SQL Server creates and manages statistics.  Remember that just because SQL Server does this for you, does not mean you can forget about it.  You always want to stay on top of statistics, like indexes. In some cases, AUTO_UPDATE_STATISTICS is not enough and using sp_updatestats every so often may help alleviate query performance issues. The bottom line is, if your statistics are bad, there is a good chance the optimizer may miss index seeks or choose a sub optimal joins.There is a lot more to know about statistics and I have hardly scratched the surface here.  If you would like to learn more about SQL Server Statistics, you should take a look at the referenced links, as these have a wealth of information.

References:

http://msdn.microsoft.com/en-us/library/ms190397.aspx

http://technet.microsoft.com/en-us/library/cc966419.aspx