in

SQLServerCentral.com

The largest free SQL Server community.

Ken Kaufman

December 2007 - Posts

  • Table Variables, Temp Tables, Recompilations

     

    The general rule of thumb on "variable tables" VS "temp tables" is if your working with a small datasets or complex query plans use a temp table.  When working with simple queries or small datasets use variable tables.  This was built by design.  Variable tables are light in memory structures, locking is minimal, statistics aren't created, and plans don't recompile with changes to the table.  Temp tables are real table structures, and behave almost identical to normal tables, with a shorter life span.  Therefore there is an overhead, in creating and tearing down, with temp tables.  The question is why use temp tables at all to hold temporary storage.

     

    The trick is in recompilation.  Recompilation occurs mid stream during a batch, when circumstances change, from the initial plan, where SQL determines to recompile a more optimal plan.  This is often observed when a subplan is dependent on a previous subplan.  For example if a query inserts data into a work table, then selects off the work table.  If the precompiled plan expects 100 rows in the table, but gets 100K, the plan might need to be changed.   This is where the difference lays between the two data structures.  Since SQL keeps statistics on a temp table, it can recognize major changes and make the appropriate changes to the plan through a recompile.  However with that said a recompile is expensive and gains from the new plan might not justify the cost of the recompile.

     

    Recompiles on temp tables occur when the cardinality becomes greater then 6, and then again greater then 500.  I always look at 1000 being a threshold on whether to use a temp table over a table variable.  This is a personal rule of thumb, and you should always look at the complexity of the query itself which in the end has the final say on whether a query plan will be recompiled.  The following example uses a union join to force this situation.

     

    There are three portions to this script, the first creates the tables and populates them.  The second and third create stored procedures based on a temp table and a table variable.  The @rowcount variable in part one will be used to throttle the query into different states.

     

    --Part 1 Create table

    Declare @rowcount int

    set @rowcount = 5

    if exists(select name from sys.objects where name = 'table1')

    Begin

    drop table table1

    end

    if exists(select name from sys.objects where name = 'table2')

    Begin

    drop table table2

    end

    if exists(select name from sys.objects where name = 'table3')

    Begin

    drop table table3

    end

    --Create tables

    create table table1

    (PKInt int primary key,

    charfield varchar(20))

     

    create table table2

    (PKInt int primary key,

    charfield varchar(20))

     

    create table table3

    (PKInt int primary key,

    charfield varchar(20))

     

    Declare @Interval int

    set @interval = 1

    while @interval < @rowcount

    Begin

    Insert into table1

    values(@interval, 'Table1')

    set @interval = @interval + 2

    end

     

    set @interval =2

     

    while @interval < @rowcount

    Begin

    Insert into table2

    values(@interval, 'Table2')

    set @interval = @interval + 2

    end

     

    set @interval = 1

    while @interval < @rowcount

    Begin

    Insert into table3

    values(@interval, 'Table3')

    set @interval = @interval + 3

    end

     

    --Part2 create SP with table variable

    Create Procedure tableVariable

    as

    Declare @tableVar table

    (PKInt int primary key,

    charfield varchar(20))

     

    insert into @tableVar

    select * from   table1

    union

    select * from table2

     

    select t.Pkint, t.charfield, tv.charfield

    from table3 t inner join

    @tableVar tv on

    t.pkint = tv.pkint

    go

     

    --Part2 create SP with table variable

    Create Procedure temptable

    as

    create table #temptable

    (PKInt int primary key,

    charfield varchar(20))

     

    insert into #temptable

    select * from table1

    union

    select * from table2

     

    select t.Pkint, t.charfield, tt.charfield

    from table3 t inner join

    #temptable tt on

    t.pkint = tt.pkint

     

    Once these are in place run the following commands.  Display the execution plan for the newly created stored procedures.

     

    exec tablevariable

    go

    exec temptable

     

    You'll notice that both SP's are using a nested loop to join the temporary storage with table3, with a scan of table3 and a seek on the temp structure. Now go ahead and execute them and turn on the display actual execution plan.  This will allow you to see if the plan is recompiled with a new plan.  In addition execute "set statistics io on" so we can see how many IO's this is performing.  The freeproccache is for good measure so everything is clean when compiled

     

    dbcc freeproccache

    go

    set statistics io on

    go

    exec tablevariable

    go

    exec temptable

     

    Both held there plan, and if you look at the "select join" portion of the statement for reads both have an identical cost of 6.  This is inline with the statement above that if under 6 rows change then no recompile will take place.  Since we only had 5 no recompile occurred.   So lets prove out that theory by running the first set of statements to recreate the tables and set the @rowcount variable to 6.  Once complete repeat the above exercise of executing the SP's. 

     

    Declare @rowcount int

    set @rowcount = 6

     

    You'll notice the estimated plan is again identical nested loop with the temp storage doing a scan and table3 using a seek.  When you look at the actual plan the tablevariable SP kept the precompiled plan while the temptable SP recompiled and chose a different plan.  Still using a nested loop but this time using a seek against the temptable and a scan against table3.  The IO's also show the recompile improved IO's on the select join with the variable having 12 IO's while the temptable used only 6. 

     

    As stated before 500 is also a benchmark where a recompile will take place.  Lets take a look at what the difference is here.  Recreate the tables by setting @rowcount variable to 500.  By this time you should be convinced that the precompiled plan for both SP's will stay the same, so lets only focus on the output.  This time the temptable SP is no longer using a nested loop, but rather a merge join.  The comparitable difference is 1002 Reads for the tablevariable and 6 for the temptable.  I played this out a little further to get some more numbers.  Notice the expotential growth difference below.  As you can see the larger the changes to temp storage the more disparity occurs, which equals resources for the server, and user time waiting.

     

     

    Insert Rows

    Temp Reads

    Variable Reads

    5

    6

    6

    6

    6

    12

    500

    6

    1002

    1000

    7

    2003

    10000

    43

    20029

         

     

    Before you go out and change all your variable tables into temp tables, there are a few things to consider.  As mentioned above temp tables are real tables and have the associated overhead with them.  There's the metadata creation and maintenance.  There's heavier locking, and transactions are logged.  Of course the biggest overhead comes in the form of recompilations.  The reason execution plans are cached is because they're costly to recreate.  The gains made in IO's can easily be swallowed up by the cost of the recompilation.

     

     

  • Workaround for Monitoring Replication

    I’ve put in too many hours trying to get monitoring working from transactional replication latency.  I was unable to find anything online that mimicked my issue.  The issue was that in replication monitoring I was able to generate warnings for latency, but they weren’t writing to the application log to kick the alert.  As a work around I used a sql alert based on Perfmon sql counters, that looked for latency over 60 secs.  The counters I used were:
    sql server:Replication Dist/Dist Latency
    sql server:Replication Logreader/Logreader:Delivery Latency


    I know this is a hack but it brought me down an untraveled path in the vast landscape of sql server which is leveraging the alerts.  My only issue now is that they need to include all the Perfmon counters as it will save me a bundle in sitescope licenses.  I know this can be done using the system monitor alerts, but too much maintenance and email infrastructure to work. 

  • Formulating the Costs of Index Views

    Using an index view or materialized view (Oracle) allows for the denormalization of data, by storing joins, aggregates, and other runtime queries in permanent storage.  The benefit of this is that rather then performing these operations on demand, they’re already stored in a single flat data structure for retrieval.  The result is much faster return times.  There are plenty of articles out there for example http://www.microsoft.com/technet/prodtechnol/sql/2005/impprfiv.mspx  on the benefits of indexed views.  This blog will talk to determining if this approach is right for you by explaining how to perform quantitative and qualitative analysis of the costs.

    First off there’s no free lunch, it takes work to build this flat structure, as well as work to maintain it.  Every time a DML statement is run against an underlying table, data is pushed to the indexed view.  This process not only involves the IO of the write but CPU costs of any joins or computations.  There’s no better way to examine these costs then using the “showplan_all” command.  The following example uses the Northwind database, consisting of a two table join with an aggregation.  The purpose is to display the last three orders and the associated costs totals.  Lets start with the original query:


    select top 3
    o.orderDate, sum(od.UnitPrice * od.Quantity) as OrderCost
    from Orders o inner Join [Order Details] OD on
    o.OrderID = Od.OrderID
    where customerid = 'vinet'
    group by orderdate
    order by orderdate desc

    Now lets get the cost of this by running a showplan_all. 


    set showplan_all on
    go
    select top 3
    o.orderDate, sum(od.UnitPrice * od.Quantity) as OrderCost
    from Orders o
    inner Join
    [Order Details] OD on
    o.OrderID = Od.OrderID
    where customerid = 'vinet'
    group by orderdate
    order by orderdate desc

    Explaining this command (showplan_all)  is out of scope, so I’ll stay focused on the primary result field we’re concerned with “TotalSubtreeCost”.  Every statement is made up of different operators to perform it’s task.  The total subtreeCost is the estimation SQL Server makes on how expensive the operator will be.  This number is derived as a time value in seconds each operation is estimated to take.  An important note here is that it doesn’t relate to the time on your machine, but time run on standard equipment at Microsoft.  It’s simply an unchanging baseline to work against.  Scroll through the result sets to find this field. In the following output the sum of all the subtree’s is a cost of 0.29155515 seconds.

    TotalSubtreeCost
    0.04338021
    0.04338021
    0.04337971
    0.04336942
    0.04336812
    0.02707165
    0.01569226
    0.0032875
    0.01238386
    0.01624221
    0.29155515 - Total


    With a normalized baseline in place the following statement applies an index view.  Notice no predicate is given, since this will normally be applied as a parameter.


    Create view order_summary_view
    with schemabinding
    as
    select
     count_big(*) as Cnt, o.customerid, o.orderDate, sum(od.UnitPrice * od.Quantity) as OrderCost
    from dbo.Orders o
    inner Join
    dbo.[Order Details] OD on
    o.OrderID = Od.OrderID
    group by orderdate, customerid
    go
    create unique clustered index order_summary_view_index
    on order_summary_view(customerid asc, orderDate desc)

    Now execute a query to return the same results as the previous select statement, using the showplan_all. 


    select top 3 OrderDate, OrderCost from order_summary_view with (noexpand)

    where customerid = 'vinet'


    You’ll notice only a few operations are used with a total on the “TotalSubtreeCost” results, of 0.009864.  Compared to the normalized result it is only 3% of the cost.  It’s easy to see the advantages of an indexed view for retrieving data.  However the point of this blog is to determine the overall cost, not just on retrieving. 

    We need to determine the costs of data modifications so let’s first use the index view by examining the cost of an insert into an underlying table. 


    set showplan_all on
    go
    insert into [order details]
    (orderid, productid, unitprice, quantity, discount)
    values(10248, 17,  19.24, 10, 0)

    The plan looks much more inline with the first select statement, and so do the total cost results coming in at 0.746337 which is over double the cost of the original select statement.  Now I’ll drop the index view and rerun the above query. 


    set showplan_all off
    go
    Drop view order_summary_view
    go
    set showplan_all on
    go
    insert into [order details]
    (orderid, productid, unitprice, quantity, discount)
    values(10248, 18,  19.24, 10, 0)
    select top 1 * from [order details]

    The cost of inserting without an indexed view is 0.339465 more then 50% less then an insert with a clustered index.  With these numbers in hand we can now make a quantitative analysis of the cost of having an indexed view.  The way to do this is to take a sampling of your current activity and gather how many calls are made to retrieve the data and how many calls are made for data modification.  Once armed with this information use the following formula on each set, then compare the results:

    (Select_Calls * Select_totalcost) + (DML_Calls * DML_totalcost) = TrueCost

    In our case lets assume in a hour period we see 1000 select calls  and 100 DML calls  the comparison formula should look like so

    No Indexed View
    (1000 * .29155515) + (100 * 0.339465) = 325.50165

    Indexed View
    (1000 * 0.009864) + (100 * 0.746337) = 84.4977

    In this situation the quantitative analysis shows better overall performance by using an indexed view.  This exercise was fairly vanilla, one insert statement and one select statement.  Unfortunately the world doesn’t work this way, often you have several dml statements that will affect the underlining tables and several queries that will benefit from an indexed view.  In this case the formula would just incorporate all the calls, by adding on.  For example:

    (Select_Calls1 * Select_totalcost) + (Select_Calls’n’ * Select_totalcost) +  (DML1_Calls * DML_totalcost)  +  (DML1_Calls’n’ * DML_totalcost) = TrueCost

    Before moving on to the qualitative analysis I must call out few items to be fair.  The above is for demonstration only.  For example the first query without an indexed view would benefit significantly from a covered index, and a 10 to 1 ratio might not be realistic.  This last point is an excellent segway into qualitative analysis.

    Quantifying costs is a great measure but should not be your final determinant on which direction to go.  In the end run it’s all about user experience.  If you can slash 100 ms on a select statement with an index view, but means timeouts when a user inserts data, it provides a poor user experience.  Along those lines submitting data to a web interface is expected to take longer, users have just been trained that way.  So penalizing your writes for read speed might be acceptable. 

     When deciding on which direction to go, you need to run in a stress environment, and analyze the numbers for a balance.  In addition the previous example only analyzes a single query and not the entire environment.  You need to look at what the user experience is with 100 clients hitting the server.  With an indexed view your writing to two objects, and doubling your locking on both objects (Writes are in a transaction).  So you need to answer, how  this effects the other 99 users? 

     

     

     

     

     


     

Copyright Red Gate Software
Powered by Community Server (Commercial Edition), by Telligent Systems