Wednesday, April 09, 2008
One of the areas I managed in SQL Server had to do with the code that automatically builds statistics and uses them during query optimization to come up with a good plan.  Today I'm going to talk a bit about how statistics are built and how this works with parallelism and partitioning.

First things first.  There are two ways in which statistics are created in SQL Server:
1. You create an index.
2. You run a query that needs statistics when they do not exist and the server has "auto-create statistics" enabled.  (That's the simple definition - there are actually caveats I am skipping)

Why does creating an index also create statistics?  Well, the main cost in creating statistics is reading all the pages into memory.  The thought is that if you want to create an index, you have already paid the expensive cost and the server might as well go ahead and create the statistics object for you instead of re-reading those pages later.  So, you get these "for free" (well, almost free).

When you run a query that tries to perform an operation where cardinality estimates would be improved by having statistical information about a column, the server can try to create statistics during the compilation process.  Since compilation time needs to be kept to a minimum, these are usually done over a _sample_ of the pages to avoid making the compilation of a simple query as expensive as an index build.

So the basic plan to create an index is a singlethreaded plan that is something like this:

INSERT (New B-Tree)
  |
Sort
  |
SCAN(Heap or some index)

So during this plan's execution, there is an implicit side-effect to also create this statistics object.

For auto-stats, there is a separate query that is run.  The syntax is not public, but you can see artifacts of this if you look at the profiler and you've turned on the various auto-stats and plan outputs:

drop table stat1
create table stat1(col1 int, col2 int, col3 binary(6000))
declare @i int
set @i=0
while @i < 1000
begin
insert into stat1(col1, col2) values (rand()*1000, rand()*1000)
set @i=@i+1
end


select * from stat1 where col2 > 5


Execution Tree
--------------
Stream Aggregate(DEFINE:([Expr1004]=STATMAN([t1].[dbo].[stat1].[col2])))
  |--Sort(ORDER BY:([t1].[dbo].[stat1].[col2] ASC))
       |--Table Scan(OBJECT:([t1].[dbo].[stat1]))


What is this?  Well, this is the plan that is used to generate the statistics object.  It scans a table, sorts it (into ascending order), and then feeds it into this magical thing called statman.

Now, the details of statman are undocumented, but you can infer that it is a special internal aggregate function that is being run inside of a group by operation (stream aggregate in the plan).  This means that it is collapsing all the rows into some BLOB and then it does something with this.


Next time I hope to talk about parallel statistics build.

Happy querying!

Conor Cunningham
Wednesday, April 09, 2008 8:50:58 PM (Central Standard Time, UTC-06:00)  #    Comments [0]  | 
Tuesday, April 08, 2008
I've returned from a small trip and I will be preparing my next SQL post soon.

I've been struggling with slow POP3 sync behavior on my Outlook 2007/Vista 64 box, and I finally found a hammer to beat it back into submission.

The problem - I sync manually, and when I do the application becomes basically so slow as to be non-responsive.  I deleted a bunch of mail to get my mailbox down to a reasonable size (150MB) - still there.  I deleted RSS feeds, blaming XML for my woes again :).  That didn't work either.

I eventually found this:
http://support.microsoft.com/default.aspx?scid=kb;EN-US;935400

Something to do with the new Vista TCP window size algorithm not working well with "legacy" hardware.  No only is the download slow, but using Outlook becomes unbearable as well, so I can't really do anything with the application...  I didn't get timeout errors (at least not frequently), but I certainly had bad feelings about my email experience.

When I disabled the new TCP window behavior, all went back to what was expected.... Now I have to go find all those RSS feeds again :).  So here's to "netsh interface tcp set global autotuninglevel=disabled".  It worked for me, at least so far.

I think that this is a case where Microsoft has an opportunity to compare their offering to gmail and determine "hey, I wonder why people think that a webui is better - they are _so_ much slower...." well, in some cases the thick client is actually slower, and that doesn't make MS look very good.  So I hope this workaround avoids frustration for others :).

I'll also hope that MS could add something to outlook in the next service pack when it realizes that it downloads 15KB in 2 minutes from my POP3 servers.  Perhaps they can add a popup or special error in the next service pack of outlook 2007 to point people in the right direction.


My setup:
Vista x64 SP1
netgear gigabit 8 port switch
linksys wrt54g NAT
some no-name cable modem that my cable company gave to me.


Tuesday, April 08, 2008 7:01:01 PM (Central Standard Time, UTC-06:00)  #    Comments [0]  | 
Monday, March 31, 2008
Based on my previous post describing the differences between ANSI-89 and ANSI-92+ join syntaxes and recommendations, I had a follow-up question in a comment which was (paraphrased)

What do I do with non-join WHERE clauses - how should I write those?

Example:

SELECT p.FirstName + ' works in ' + d.DepartmentName AS 'Who Works Where'
FROM Person AS p
JOIN Department AS d
ON p.DepartmentID = d.DepartmentID
  AND p.JobType = 'Salaried'
(so, the question is about the filter condition on p.JobType).

Answer: it doesn't generally matter, but I'd recommend that you put it in the WHERE clause for readability.

I'll explain the why now.  A little background first:

In SQL Server (actually all QPs of any note), the SQL string is converted into an operator tree and then there's a lot of magic to rewrite that query tree into alternative forms of the query.  While some people think of this as "if I try different syntaxes, the query may execute faster", the optimizer is actually doing something at a much deeper level - it is essentially rewriting the tree using a series of rules about equivalences.  It's a lot more like doing a math proof than trying different syntaxes - it deals with associativity, commutivity, etc. (Conor bats cobwebs out of people's heads- yes, you guys did this stuff in school).  It's a big set theory algebra machine.  So, you put in a tree repesenting the syntax at one end and get a query plan out the other side.

So, I'll ask the question from a slightly different perspective and answer that too to help explain the "why":

"Does putting filters in the join condition of an inner join impact the plan choice from the optimizer?".

Answer: no - at least not in most cases. 

When the SQL Server Optimizer starts working on this query, it will do a number of things very early in the optimization process. One thing is called "simplification", and most of you can guess what will happen there.  One core task in simplication is "predicate pushdown", where the query is rewritten to push the filter conditions in WHERE clauses towards the tables on which the predicates are defined.  This mostly enables index matching later in optimization.  It also enables computed column matching.

So, these predicates are pushed down in both cases.  You lose a lot in query readability by trying this form of rewrite for very little gain.

There is one case where I'd consider doing this, but it really requires that you have uber knowledge of the QP.  However, this seems like a good challenge, so I'll explain the situation and let you guys write in if you can find an example of it:

You know that the QP uses relational algebra equivalence rules to rewrite a query tree (so A join B is equivalent to B join A, filter .a(a join b) == (select * from a where filter.a) join b, etc.

One could imagine that some of the fancier operators may not fit as easily into the relational algebra rewrite rules.  (Or, they are just so complex that the cost of trying such rewrites outweighs the benefit).

Can you find operators where (filter (OPERATOR (SCAN TABLE)) is not equivalent to (OPERATOR (filter (SCAN TABLE))? 

Obviously inner join is a bad place to start.  I'll throw out some not-so-random areas for you to try:
* updates
* xml column manipulations
* SELECT list items on objects that change semantics based on how many times they are executed in a query (rand()?  think functions)
* play with group by (this one is tricky)
* OVER clause?
* UNION/UNION ALL, INTERSECT, ...

So, there are some cases where the QP will do these rewrites, and there are some places where it can't/won't (or at least doesn't do it always).  In a few of these cases, the intent of the query can be preserved by manually rewriting the query to push the predicate "down" the query tree towards the source tables.  However, I would not recommend this unless you really know what you are doing - the query rewrite needs to be equivalent or else you may not get the right results back from your query!


Bottom line - I think that the query is far more readable with non-join predicates in the WHERE clause.  Whenever I try to optimize queries, I usually push them into this format so that I can wrap my head around what the query is trying to accomplish.


Happy querying!

Conor

Monday, March 31, 2008 7:47:00 PM (Central Standard Time, UTC-06:00)  #    Comments [0]  | 
Saturday, March 29, 2008
I had a request from a reader that I'll answer today about when to do joins in the ON clause and when to do them in the WHERE clause.  For example:

SELECT * FROM A, B WHERE A.a = B.b

vs.

SELECT * FROM A INNER JOIN B ON (A.a = B.b)

The short answer is that both are the same (at least for inner joins), but I prefer and encourage you to use the latter format (and I will explain why).

Earlier versions of ANSI SQL did not contain the ON clause for join conditions - it used the where clause for everything.  This was fine for inner joins, but as database applications started using outer joins, there were problems that arose with this approach.  Some of you may remember the original ANSI-89-era syntax for *= and =*.  These were used on the predicates to define the behavior for an outer join.  In this case, we'll preserve non-matching rows from A in addition to the normal rows returned from a join:

SELECT * FROM A, B where A.a *= B.b
which is equvalent to:
SELECT * FROM A LEFT OUTER JOIN B on (A.a = B.b)


This "hack" worked fine until people started using multiple predicates for things and also started doing multiple outer joins in one query.  Then we were left with a big, ambiguous mess about which *=, =* applied to which join.  So, ANSI banished *= and =* and SQL Server has been threatening to follow for quite sometime.  I honestly never use the old-style join syntax, so I don't even recall the exact deprecation state.  It is dead to me already ;).

The broader concept is that predicates are "attached" to joins using the ON clause.  This is very helpful when you are trying to figure out what should happen in a query.  It helps semantically define the set of rows that should return from the join.

So, if I start nesting various inner and outer joins in a big, nasty query, all of a sudden it is very nice to have an ON clause to define what should go where.

SELECT * FROM A INNER JOIN (SELECT B.* FROM B LEFT OUTER JOIN C ON (B.col1=C.col1 and B.foo.C.bar)) AS I1 ON A.col1=I1.col1;

As applications get more complex, it is not uncommon to have 10s of tables in a query.

Internal to the SQL Server query processor (actually pretty much all query processors), there is a tree format for the query operations.  The exact representation will vary from vendor to vendor, but each one of these SQL syntax pieces transates to some set of relational operators in this query tree.  Putting your query syntactically into this format gets things much closer to the internal algebra of the query processor in addition to making things easier to read as queries get more complex.

Actually, if I were to go build my own QP, I'd seriously consider adding a query tree mechanism in addition to SQL (this concept is not new and is not mine).  OLEDB had a concept like this in the earlier public betas, for example.  Obviously the implementor would want to retain the ability to change the internal implementation, but a tree of commands is actually far easier to grok than SQL, once you get used to the idea.  Other technologies expose a graph structure to you (video codecs/transforms in windows, msbuild is an XML file representing a tree, etc).  SQL as a textual language exists historically.  It's also a nice way to write queries :).

The only other area where I get concerned is when people turn off ANSI_NULLs.  It is one of those historical features that should basically never be used.  I could imagine cases where some comparisons in joins behave differently in the ON clause vs. afterwards in an WHERE clause.  I don't want to pollute people's minds, as my attempts to go back and re-learn the quirks on this for this post left me baffled since NULL=NULL returns TRUE only for some syntax constructs.  So, I don't have a case where it is broken, but I'll leave you with the "ANSI_NULLs off is bad" message and list it as a potential reason.

Will you get wrong results if you use the old-style join syntax?  no.  The world will still turn.  So, this is really a recommendation based on style and sanity.  I would recommend that you get used to the newer style - it may help you write more powerful applications and think more like the QP.  For some applications, this might let you write more powerful features for your users.


Thanks,

Conor Cunningham
Saturday, March 29, 2008 2:31:04 PM (Central Standard Time, UTC-06:00)  #    Comments [2]  | 
Wednesday, March 26, 2008
I don't know about you, but groupby is one of my favorite operators.  There are a TON of interesting optimizations that a QP considers when you start adding group by into queries, especially when you have joins and then a group by.  TPC-H benchmark wars among the large database vendors are won and lost on many such optimizations.

So, if you are doing relational OLAP (ROLAP) or are otherwise running group by queries over lots of well-normalized data, then I suggest you brush up a little on your knowledge of group by - it will help you understand when queries are behaving and when they are not.

Here's the paper I tell people to read on the subject.  It's written for the person implementing a database, but anyone who can read query plans should get the basics from the paper.  It is the basis for most of the hard-core optimizations (and tricky problems) that face all query processors today. 

The basic idea is caused by understanding that an aggregate function can be split into multiple operations and done in parts.  Some parts can be done earlier in a query, saving a lot of work.  If these results can be combined later, you might be able to speed up a query by computing these partial results early and then combining them at the end of the query.  The usual savings is that you don't have to materialize the results of a join when you only care about the aggregate over some piece of it.

Most of the core aggregate functions defined in SQL can be decomposed.  If you have a set of rows {x} := concat({y}, {z}), then
SUM({x}) == SUM({y}) + SUM({z}).
COUNT == COUNT + COUNT
MAX == MAX (MAX(), MAX())
...

Not all aggregate operations can be decomposed in this manner, but many of them can. 

If you take this concept and then apply it a query with some joins:

select sum(col1) from a join b join c

Then the idea of local-global aggregation is that you can do part of the sum before joins and pass up the SUM for each group instead of all the rows from that group. 

This idea becomes more powerful when you start throwing more complex operations into a query processor, such as partitioning or parallelism.  Often, you want to perform partial aggregations on these groups to minimize the amount of data you have to send between threads or perhaps between nodes on a NUMA machine.  All of this is the fun stuff that makes databases interesting - you can

Not every aggregate can be pushed below every join - there are rules about what can and can't be done and maintain the same results from a query.  For example, you may need to consider whether the aggregate function can handle seeing additional NULL values and return the same result or not.

If you go look at the SQL CLR user-defined aggregate definition, you'll see the exposed pieces of some of this capability in the SQL Server query optimizer.  I won't spoil all of your fun, but go take a look. 

Happy Querying!

Conor

Wednesday, March 26, 2008 9:01:48 PM (Central Standard Time, UTC-06:00)  #    Comments [0]  | 
Sunday, March 23, 2008
So I was playing today with the sparse column implementation in the latest SQL 2008 CTP, and I was specifically looking to see how this thing would work with the query processor.  I wanted to know how much information is exposed for the QP to make smart decisions when this functionality is used in complex queries with cost-based plan choices.  If the QP doesn't have the information, then sometimes the query plans will be sub-optimal because, well, garbage-in garbage-out.  While the SQL Server QP does a tremendous job at making complex plan choices compared to some of the other commercial alternatives, there are still limits on what the Optimizer can model in a reasonable amount of time.  As such, there are seams where the product tends to not work as well as one would hope.  This will always be true.  While I suppose that will also keep me employable, it is useful to understand those limits because it will help you know where to look or, if it's really hard, when to ask for help.

The SQL Server QP knows a couple of things about the data stored in a table in the storage engine:
1. How many physical pages it uses
2. How many rows it has in it (approximately)
3. Single-column statistics over a sample of the data
4. A basic notion of column interdependance to help in estimating queries with multiple scalar predicates.

From 1 and 2 it can derive the average row width.  That's useful for determining things like "how big will my sort be" if the query needs to sort.  That's a good thing - it leads to reasonable estimates for many choices in the QP.

So let's add sparse columns into the mix.  Sparse columns are useful for data with lots of NULLs.  Often this is a result of a non-traditional third-normal form database problem or, perhaps someone who is not a database person not really trying to make something into a database problem early enough in its lifecycle.  The point is that commercial database systems have a sweet spot around handling data sets with known (and small) sets of columns that can be stored in tables.  There is a TON of expressiveness available in query processors that manipulate this data because this format of data is better supported than other formats.

None of this really means that your problem is going to easily fit into a nice third-normal form system.  Often there are legacy or performance concerns that push an application away from that sweet spot.  Over time, various technologies have tried to bridge that gap (property tables, XML, and object-relational mappings).  Each of them have their own reasons to be, and I don't want to get into them in depth in my post.  I'm going to talk about how the QP deals with these from a modeling perspective.

I built two examples to explore how SQL Server 2008 reasons about sparse columns.  One example creates lots of traditional, nullable float columns while the other is exactly the same except that it uses the sparse attribute.

A few things I learned immediately:
1. Sparse columns don't change the maximum number of columns you can create in a table.  On the surface, this seems unfortunate, since it will limit the kinds of applications that can use the feature. 
2. It does seem to use less space per row.  This isn't hard, as the row format for SQL Server has a null bitmap and also needs 2 bytes per column to store the variable offset pointers.

create table sp1(aaa int)
create table sp2(aaa int)

declare @i int
set @i=0
while (@i < 990)
begin
declare @sql nvarchar(400);
declare @s nvarchar(20);
set @s = @i;
set @sql = 'alter table sp1 add col' + @s + ' float sparse'
exec sp_executesql @sql
set @i=@i+1
end

declare @i int
set @i=0
while (@i < 990)
begin
declare @sql nvarchar(400);
declare @s nvarchar(20);
set @s = @i;
set @sql = 'alter table sp2 add col' + @s + ' float'
exec sp_executesql @sql
set @i=@i+1
end
declare @i int
set @i=0
while @i < 20000 
begin
insert into sp1(col2) values (123.4)
set @i=@i+1
end

declare @i int
set @i=0
while @i < 20000 
begin
insert into sp2(col2) values (123.4)
set @i=@i+1
end
If we run "set statistics io on" and then run "select * from sp1" and "select * from sp2", you'd like to see some difference in IOs:

sp1:
(20000 row(s) affected)
Table 'sp1'. Scan count 1, logical reads 86, physical reads 0, read-ahead reads 80, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

sp2:
(20000 row(s) affected)
Table 'sp2'. Scan count 1, logical reads 20000, physical reads 1, read-ahead reads 19978, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Well, that's good - the sparse format on largely sparse data saves space.  We can confirm that with a quick look into the system tables:
SELECT o.name AS table_name, au.type_desc, au.used_pages
FROM sys.allocation_units AS au
    JOIN sys.partitions AS p ON au.container_id = p.partition_id
    JOIN sys.objects AS o ON p.object_id = o.object_id
WHERE o.name in (N'sp1', N'sp2')
table_name                                                                                                                       type_desc                                                    used_pages
-------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------ --------------------
sp1                                                                                                                              IN_ROW_DATA                                                  87
sp1                                                                                                                              ROW_OVERFLOW_DATA                                            0
sp2                                                                                                                              IN_ROW_DATA                                                  20001

(3 row(s) affected)

We've now confirmed that we actually do have fewer pages.  This is also good.

Now let's see how far into the QP this extends.  Does the QP model the costs for these two queries differently?

SP1 TotalSubtreeCost: 0.08824496
SP2 TotalSubtreeCost: 14.83936

And that, my friends, is a "good thing".  This means that sparse columns are going to help your complex queries when you use a table with sparse columns in it.  The easiest way to implement this is to simply ignore the new feature in the QP, and obviously someone did a good job to make sure that it was costed properly. 

I don't believe that there are additional statistical structures to tell the QP which things are on/off row.  This will show up in a small number of scenarios (similar to how LOB data can be on/off row).  This is outside of the model for how the QP reasons about plan cost, at least from what I've seen from SQL 2008 and from what was publicly said about 2005.

Thanks all,

Conor Cunningham


Sunday, March 23, 2008 7:35:01 PM (Central Standard Time, UTC-06:00)  #    Comments [1]  | 
Monday, March 17, 2008
So now that I have the latest CTP working again on my main machine, it's far less troublesome to go research things and post what I find.  Tonight I'll talk a little about datetime vs. date, as dates are on my mind for whatever reason.

So the "old" SQL Server datetime type only goes back to 1752, which seems very odd until you remember that current notions of date and time are really not that old.  In 1582, Pope Gregory XIII fixed the calendar because they realized that the year was actually slightly off compared to what the calendar said - each year the date of Easter ended up getting slightly further from what was intended.

Modern computer science students (at least ones who did the assignments *I* did in school :) will remember the funky rule for calculating a leap year - it is a leap year if the year is divisible by 4 but not 100 except for when also divisible by 400.  The year 2000 was just such a case, so we had a leap year then.  2100 will not be a leap year.  They had different fancy math in the original Julian calendar (as in Caesar) with a special month that happened every 377 years or so.  Later they tried to fix this because they wouldn't actually do the month every 377 years.  I promise - I'm not making this up.

By the time the figured it out, they were off by several days.  In modern terms, the OS shipped, everyone installed it, and it has a fatal bug that impacts every customer on upgrade to the hotfix :(.  So while today such bugs can put you out of business, back then the church had a bit more market power than the average customer might have today.  So, they decided to fix things.  So they changed the calendar and skipped 10 days in the process to fix the client.  Even worse, different clients (countries) installed the patch on different days, so they all changed dates differently.

It so happens that the British Empire (the "pink bits" on the old maps) adopted this in 1752.  That's almost 200 years after the Pope did his thing.  So, since SQL Server was first done in the US, Jan 1. 1753 is the first legal date because all of the math before that is simply dizzying.  Alaska actually didn't switch until the late 1800s since Russia controlled it before that, and the Orthodox calendar celebrates its Christmas as Easter later than those in the West because of this very issue. 

So if I try this with the old datetime type, I get:
create table datetbl(col1 datetime)
insert into datetbl(col1) values ('17510101')


Msg 242, Level 16, State 3, Line 1
The conversion of a varchar data type to a datetime data type resulted in an out-of-range value.
The statement has been terminated.

So I figure I'll try this out on the new date type:

create table datetbl2(col1 date)
insert into datetbl2(col1) values ('17510101')

(1 row(s) affected)

Well, the legal date range is listed as 0001 to 9999.  So let's check the math and see what happens...

create table datetbl3(col1 date, col2 date)
insert into datetbl3(col1, col2) values ('17510101', '17530101')
select DATEDIFF(dd, col1, col2) from datetbl3
731

Well, it does not appear that they skipped too many days there.  They even added a leap year since 1752 is divisible by 4 but not by 100 except for 400.  So instead of 730 we get 731.

I looked at Books online, but so far I haven't found any reference to the difference in the DATEDIFF function or in the discussion of valid ranges.  While I am sure that a number of people will not care that much and will mostly just want to use historical dates mostly with low precision, it's still important to make sure that a core system does calculations correctly.

Perhaps they will add a comment into BOL or a warning for datediff and other function uses around the various switch points for dates.  Perhaps not.  However, it's important for you, the programmer, to know the issues when dealing with old dates.

I originally learned about all of this stuff in detail when I reverse-engineered the SQL Server Expression Service Code while I was building SQL Server for Windows CE/SQL Mobile/SQL Server Compact Edition/whatever it is called now.  I've found this area to be fascinating because it takes something we take for granted and just slaps you in the face a few times.  I hope you have a bit more insight now as to the 1753 date limitation and perhaps will go crack open that history book :).

Thanks,

Conor Cunningham
Monday, March 17, 2008 7:47:07 PM (Central Standard Time, UTC-06:00)  #    Comments [0]  | 
Well, I was able to get things to finally install using the information from my previous post and using a named instance (different from the default instance I used previously).  So, while I'd still like to track down the keys that define an installation instance with enough detail to remove them, I think I've gotten close enough that others should be able to use this as a template to avoid an OS re-install.

I'd like to extend a HUGE thank you to the Microsoft SQL Server Setup Dev Team who spent time looking at this problem and providing a workaround for me.  Please accept my gratitude.

I am sure that they are hard at work getting setup ready to RTM, which will include problems like the one I've seen. 

So, please post up if you have other questions about what I did - I'll answer them if I can.

Thanks,

Conor

Monday, March 17, 2008 7:00:45 PM (Central Standard Time, UTC-06:00)  #    Comments [1]  | 
Friday, March 14, 2008
You may recall my previous posts on my trouble with SQL 2008 CTP6.  I've made some progress on fixing my machine that I thought I'd share with you.  I now get past the following error:

D:\temp\sqlctp6\servers>setup
The following error occurred:
MsiGetProductInfo failed to retrieve ProductVersion for package with Product Code = '{00E75F61-A126-4CE1-90B8-42295052F1AC}'. Error code: 1605.

I useded the SysInternals (err, Microsoft) Process Monitor Tool and watched for keys found/not found during the failed install.  This found a few keys in HKCR\Installer\UpgradeCodes that were being found early in the setup100.exe process.

(Fair notice - modifying the registry on your computer is your problem, not mine :).

So take the key:
00E75F61

Reverse it.  I think I had 615fe700, but it was late and I was tired.  It might have been 16f57e00.  Anyways, you will see some keys under HKCR\Installer\UpgradeCodes.  There are actually 2-3 places in the registry searched for each key  I've been killing all three of them for each key - there are about 7-10 keys.  The registry section looks like this:



The hex digits you have in the error will correspond to the right hand side of this picture.  The key I've been deleting is it's parent, which is the key being opened in the "key found"/"key not found" stuff in the process monitor log.

Here's what the log looked like for me:

136791  11:53:49.4922341 PM      setup100.exe    2812       CloseFile              D:\temp\sqlctp6\tools\setup\sqlrun_bids.msi                SUCCESS             

136793  11:53:49.4923712 PM      setup100.exe    2812       WriteFile              C:\Program Files (x86)\Microsoft SQL Server\100\Setup Bootstrap\Log\20080313_2353\WOPR_20080313_2353_Detail_ComponentUpdateSetup.txt SUCCESS                Offset: 7,591, Length: 62

136795  11:53:49.4956628 PM      setup100.exe    2812       RegOpenKey                HKLM\Software\Microsoft\Windows\CurrentVersion\Installer\Managed\S-1-5-21-2888934283-224128331-3030229123-1000\Installer\UpgradeCodes\87674BD65E9A5D1409951D671E37BDA4          NAME NOT FOUND         Desired Access: Read

136796  11:53:49.4957796 PM      setup100.exe    2812       RegOpenKey                HKCU\Software\Microsoft\Installer\UpgradeCodes\87674BD65E9A5D1409951D671E37BDA4      NAME NOT FOUND                Desired Access: Read

136797  11:53:49.4958199 PM      setup100.exe    2812       RegOpenKey                HKCR\Installer\UpgradeCodes\87674BD65E9A5D1409951D671E37BDA4 SUCCESS              Desired Access: Read

136798  11:53:49.4958553 PM      setup100.exe    2812       RegEnumValue                HKCR\Installer\UpgradeCodes\87674BD65E9A5D1409951D671E37BDA4 SUCCESS              Index: 0, Name: 16F57E00621A1EC4098B249205251FCA, Type: REG_SZ, Length: 2, Data:

136799  11:53:49.4958960 PM      setup100.exe    2812       RegOpenKey                HKLM\Software\Microsoft\Windows\CurrentVersion\Installer\Managed\S-1-5-21-2888934283-224128331-3030229123-1000\Installer\UpgradeCodes\87674BD65E9A5D1409951D671E37BDA4          NAME NOT FOUND         Desired Access: Read

136800  11:53:49.4959398 PM      setup100.exe    2812       RegOpenKey                HKCU\Software\Microsoft\Installer\UpgradeCodes\87674BD65E9A5D1409951D671E37BDA4      NAME NOT FOUND                Desired Access: Read

136801  11:53:49.4959604 PM      setup100.exe    2812       RegCloseKey                HKCR\Installer\UpgradeCodes\87674BD65E9A5D1409951D671E37BDA4 SUCCESS             

136802  11:53:49.5146648 PM      setup100.exe    2812       RegOpenKey                HKLM\Software\Microsoft\Windows\CurrentVersion\Installer\Managed\S-1-5-21-2888934283-224128331-3030229123-1000\Installer\Products\16F57E00621A1EC4098B249205251FCA        NAME NOT FOUND         Desired Access: Read

Now the installer gets past this and tries to install the engine and then fails, but I will call this progress ;).

Notice - I had deleted all of my physical files for SQL Server from the machine, so killing the registry keys seemed like a reasonable next step.  I can't promise you it's a good idea since I don't have things working yet, but I hope this helps the many of you who mailed me and found me via search engines.

Thanks,

Conor Cunningham
Friday, March 14, 2008 5:15:30 PM (Central Standard Time, UTC-06:00)  #    Comments [1]  | 
Wednesday, March 12, 2008
Well, I spun the wheel of database topics I have here in my room, and today I think I'll talk about updates and views.... specifically inserts through non-indexed views.  Since I haven't blogged about this previously, I'll start at the beginning.  There are many, many update topics, so don't feel left out - comment if there's an update topic that interests you and I'll get to it.

UPDATEs are challenging for different reasons than SELECT statements.  While a SELECT statement can have huge combinatorical challenges in terms of the number of different plan choices to consider, UPDATEs often have a relatively small number of plan choices but instead have a number of very difficult performance tradeoffs about which kind of plan tweaks will cause the plan to finish the fastest.  You can imagine that there is a labor tradeoff between making a system work really well for SELECT plan exploration and making a system that can handle all of the detailed tweaks for UPDATE plans - most things are neutral, but getting that last 10% of performance out in one area may impact the other's ability to innovate.  So, balance is required on the part of the database implementor.

So if I were building my first database engine and I wanted to update a base table (a "heap"), I may have the following:

create table z1 (col1 int, col2 decimal, col3 nvarchar(100));
insert into z1 (col1, col2, col3) values (1, 2, 'i like cheese')
So if I implement my storage engine with a series of equally sized pages, I can probably figure out how to load each of them into memory and look for a place to store a record that's a linearization of (1, 2, 'i like cheese') on disk. 

Let's start making things more complex.

create table z2 (col1 int primary key, col2 decimal, col3 nvarchar(100));
insert into z2 (col1, col2, col3) values (1, 2, 'i like cheese')
So if I add a primary key, this is implemented in SQL Server as a clustered index.  This replaces the heap.  So, now to build my own storage engine that does this I'd have a B-Tree implemented and I would find the right place in my B-Tree to insert my new record.  I may need to split some pages to make things fit.  Here's the plan in SQL Server:

  |--Clustered Index Insert(OBJECT:([t1].[dbo].[z2].[PK__z2__357D0D3E03317E3D]), SET:([t1].[dbo].[z2].[col1] = RaiseIfNullInsert([@1]),[t1].[dbo].[z2].[col2] = [Expr1003],[t1].[dbo].[z2].[col3] = [Expr1004]), DEFINE:([Expr1003]=CONVERT_IMPLICIT(decimal(18,0),[@2],0), [Expr1004]=CONVERT_IMPLICIT(nvarchar(100),[@3],0)))

This is nice because there is still just the one structure to manage.  I just load it up, insert my row, and I am a happy man.

Now, I'll blog some other day about what happens with multiple indexes and such - today I want to talk about inserts against views, as this has lots of nasty details in its implementation.

create view v1 as select col1, col2, col3 from z2 where col2 between 2 and 100
ok, so I've added a view against z2 that shows a subset of the rows.  That's not too bad, or is it?  Well, if I insert/update the base table, my plan is the same as before, so that's easy enough.  However, if I try to run an insert against the _view_, what should happen?  The view isn't stored anywhere.  However, in this case, there is a single table underneath it and the rows from the base table can be "mapped" up through the view, so perhaps I could reverse that operation and translate a request to update the view to be an update against the base table. 

insert into v1 (col1, col2, col3) values (1, 2, 'foo')

  |--Clustered Index Insert(OBJECT:([t1].[dbo].[z2].[PK__z2__357D0D3E03317E3D]), SET:([t1].[dbo].[z2].[col1] = RaiseIfNullInsert([@1]),[t1].[dbo].[z2].[col2] = [Expr1003],[t1].[dbo].[z2].[col3] = [Expr1004]), DEFINE:([Expr1003]=CONVERT_IMPLICIT(decimal(18,0),[@2],0), [Expr1004]=CONVERT_IMPLICIT(nvarchar(100),[@3],0)))

Hey, that's pretty neat - SQL Server performs an insert against the base table that looks very similar to what we saw before.  Very nice of them :).

Ok, let's talk about the case when the row doesn't meet the filter:
insert into v1 (col1, col2, col3) values (3, 250, 'foo')
  |--Clustered Index Insert(OBJECT:([t1].[dbo].[z2].[PK__z2__357D0D3E03317E3D]), SET:([t1].[dbo].[z2].[col1] = RaiseIfNullInsert([@1]),[t1].[dbo].[z2].[col2] = [Expr1003],[t1].[dbo].[z2].[col3] = [Expr1004]), DEFINE:([Expr1003]=CONVERT_IMPLICIT(decimal(18,0),[@2],0), [Expr1004]=CONVERT_IMPLICIT(nvarchar(100),[@3],0)))

Hrm.  Well, that seems to work too... The ANSI committee got this far when working on their ANSI SQL specification, and they added a neat little keyword "with check option" that allows inserts, updates, and deletes against views to make sure that the resulting row would continue to be exposed through the view after the change before it is allowed.
create view v2 as select col1, col2, col3 from z2 where col2 between 2 and 100 with check option


insert into v2 (col1, col2, col3) values (1, 250, 'foo')

So if we run the same query as before against this view with "with check option" defined, we get this crazy looking plan:




What's all this then?  Well, some of this requires a few more operators of the day before I can fully explain the plan.  However, in this case it's doing the following:

1. create a dummy row that has the values you want to insert into it. 
2. insert it into the clustered index.
3. perform a check to see if this row matches the filter condition(s)
4. Assert that the check succeeded.  If not, fail the query and roll back the transaction.

All of that from a harmless little insert statement...

Well, let's do something else that is theoretically invertable and see how far this support goes:

create view v3 as select col1 + 1 as a, col2, col3 from z2 
insert into v3 (a, col2, col3) values (1, 250, 'foo')
Msg 4406, Level 16, State 1, Line 1
Update or insert of view or function 'v3' failed because it contains a derived or constant field.

So I claim that this could be supported as an updatable view type because the domain of col1 is integer and the scalar expression on it is invertable.  Unfortunately, none of the vendors really support this to my knowledge.  So, instead of it being a straight reference to a column in z2, we could create an expression that is the proper query needed to make the view v3 consistent by inserting a different row into z2. 

I've done two operators - basically the two easiest ones in any query processor.  However, lots of different operators can be supported and have some set of rules about invertability that can be used to perform inserts, updates, and deletes against them.

So that's scratching the surface on updates.  I'll try to post up a few more entries on different parts of the system, but I hope you've learned something today about how view updatability is implemented.

Thanks,

Conor
Wednesday, March 12, 2008 7:42:41 PM (Central Standard Time, UTC-06:00)  #    Comments [0]  | 
Saturday, March 08, 2008
So while I hope that I've demonstrated that I can say some interesting things about database engine design and implementation, I also want to post blog entries on the design of data-driven applications, as this is a topic I've been pondering a lot lately.

I'll start with something that seems completely off-topic that isn't...

So for Christmas I bought my wife an "experience" - She's always liked Ms. Pac-Man, so I figured that I would find a way to let her play anytime she wants.  I also didn't want a big, bulky arcade machine in the house - those things give off lots of heat and moving them isn't much fun.

I eventually settled on buying her an X-Arcade Joystick, hooking that up to a HTPC connected to my Television, and getting MAME set up to play it on an emulator.  My wife also likes q-bert and is getting quite good at it ;).

My wife and I also have a 2-year old daughter who is already smarter than us.  While we were playing this game, my daughter enjoyed watching the "videos" (the intermission videos played after level 2, after level 5, etc).  She thought they were wonderful - but she also got upset when we couldn't get to a video.  Eventually I started getting into the 3rd map in the game (after the second video), which for all you too young to remember looks like this:




So this is the "cranberry" level, and my daughter has cried because I can't get to the  level when I play.  I never would have expected to have disappointed my daughter for not playing a video game well enough, but it goes to show you that her expectations _start_ in life at a much higher level than mine will ever be.

She believes that I should always be able to get to the cranberry level (and stay there for as long as she is remotely interested).  She also believes that she can watch any television show at any time - she's never had to live without Tivo... I know I can't go back, but to her it must seem like we're living in the trees and grunting or something.  She also believes that pocket calculators are telephones, and she tries very hard to call my parent's cat on my wife's calculator when she gets ahold of it.  It *is* very funny, but honestly how far off is it?  Most cell phones have some calculator built into them now.  Why not the other way around?

As I've been outside of Microsoft for a bit, I am fortunate to get to see things from a different perspective.  If you think about it, databases haven't really changed that much in awhile.  SQL has been around a long time, and the core features get better each release but the paradigm in the server doesn't change much at all.  The UI tools certainly haven't changed - Access 2008 or whatever it is called will probably look a LOT like Access95.  SQL Server Management Studio is still basically a listview of objects and a query window.  Database tools are an area where I am trying very hard to go back and think more like my daughter - why CAN'T I get access to all of my data with a single query?  Why can't I backup my data more easily?   Sure, there have been various advances (XML, LINQ, ER Queries, Hibernate, S3, that Cloud SQL Server that got announced last week), and some of them are very cool, but how many of those do I expect to be around in 20 years?  How many really make my life better?  If you take a moment and think about it, there's a lot of complexity in today's systems, and the often don't "just work" unless you spend a LOT of money getting someone to write custom software for you - even then, it might not work that well when you get done.

So, I'll challenge you - remember that day you got your first Tivo - remember the day before that.  What is the one thing that you're going to do to make something that cool?  If you write setup, how are you going to make that better?  If you write a database tool, why am I going to LOVE that tool like I would a Tivo?  Don't make me post up pictures of my daughter crying - I'll do it!  Get out there and make something cool.  We'll all be happier if you do.

Thanks,

Conor Cunningham
Saturday, March 08, 2008 4:12:10 PM (Central Standard Time, UTC-06:00)  #    Comments [1]  | 
So as you proceed up the river into the jungle, searching for answers about how the query optimizer works, I'll ask you one question: Did you know that there's actually a lot of stuff that the Optimizer team just tells you?  It's in the product, and I'm constantly suprised by how little attention they get.  You can learn all sorts of things by looking at the data.  Now, not all of it is documented, but that's usually just because one needs leeway to change the internals rather than some deep, dark secret that needs to be kept.

Now that you're hooked ;), I'll tell you a bit about an optimizer DMV called sys.dm_exec_query_optimizer_info.  It tells you all sorts of things about how your querys are optimized.  It's a bag of counters for all sorts of things that, with a trained eye, can give you lots of insight into what is happening.

This guy is actually documented, at least partially, on MSDN. 




(That's from my SQL 2008 install, btw).

Some of these fields are "undocumented".  search 0, 1, and 2 are in that category.  I won't talk about them except to say that the names aren't really obfuscated too much. 

To learn about a particular query, you find a nice, quiet server and:

1. select * from this table, store the results somewhere
2. optimize a query of interest,
3. select from this table again, then compare the current totals to the originals.

I think that this is one use of this DMV - trying to figure out why a query takes a long time to optimize.

The other use of the DMV is to get a good statistical picture of a running system.  Say that I'm a DBA and I want to know how many queries in my application have hints or _need_ hints to work well.  Well, this will tell you.  Granted, it doesn't separate recompiles from compiles, and if you have a system where plans are getting kicked out of the cache things may be a bit skewed, but I can tell you that this is far better than simply guessing.  Often the DB application developer doesn't realize that they've built an application that requires a lot of hinting or a lot of compilations, and you can see this in more detail than you get with the performance counters.

I've already talked about "trivial plans", which are not documented in this DMV but are widely known in the other outputs of the system.  I'll let you guys guess about the search 0, 1, and 2 stuff - if you can back up your guess with a public post, book, or other form of comment I'll confirm if you get it right.

Have a great weekend, ya'll.

Conor Cunningham
Saturday, March 08, 2008 3:41:56 PM (Central Standard Time, UTC-06:00)  #    Comments [2]  | 
Thursday, March 06, 2008
I've been mostly working with a very kind soul from the SQL Server Installer Dev Team trying to fix my box.  So far my luck hasn't been too good, but I'll keep at it.

Things that are interesting to post if you have a blog and go through the same exercise that I did:
* which versions of CTP4/5? and CTP6 did you install - x86 or x64 - the registry keys are different.
For me, I think it was the x86 versions in both cases on my Vista Ultimate x64 box.

I learned that the version isn't really visible in the resource fork you can see for the setup.exe, which in hindsight is something I will do if I ever build another installer ;).

I'll write up some query stuff soon - send me a mail if there's a topic that causes you to lose some sleep and I'll see if there is anything interesting to say on it.

I'm also trying to put together a posting about my experiences with Ms. Pac-Man and my 2-year old daughter.

Conor

Thursday, March 06, 2008 8:55:41 PM (Central Standard Time, UTC-06:00)  #    Comments [0]  | 
Sunday, March 02, 2008
After fighting off a cold all week, I've had some more time to go play with CTP6 on my secondary machine.

Today we'll do some experiments to see how the Optimizer picks plans for filtered indexes.  This will help you figure out how to build your database schemas and queries to take advantage of this new kind of index.

I was involved in the inception of the idea to build filtered indexes into SQL Server, but I didn't have anything to do with the implementation.  So, what I'll cover today is just me playing with a feature to see what it can do. 

I thought that it would be good to play with disjoint ranges and see whether they can be "covered" by a filtered index.  In my last blog post, I covered how a basic, single range case *is* covered by a filtered index when the predicates match up correctly.  In this post, I'll try multiple ranges to see what happens.

Same database setup as before (just a table with wide rows and enough rows to make index choice obvious to the optimizer):

use t1

create database t1
use t1
drop table t1
create table t1(col1 int, col2 nchar(2000), col3 time)

create index i1 on t1(col1) where col1 > 5 and col1 < 20



create index i1 on t1(col1) where col1 > 5 and col1 < 20
declare @p int
set @p =0
while @p < 20000
begin
insert into t1(col1) values (rand()*10000)
set @p=@p+1
end
create index i2 on t1(col1) where col1 > 25 and col1 < 40


OK I added a second index over a different range in the same column. This is not a case that the
SQL Server 2005 optimizer had to handle.

query 1: a query with a disjunction (OR) of ranges (BETWEEN/AND).
select col1 from t1 where (col1 > 5 and col1 < 20) or (col1 > 25 and col1 < 40)
|--Table Scan(OBJECT:([t1].[dbo].[t1]), WHERE:([t1].[dbo].[t1].[col1]>(5) AND [t1].[dbo].[t1].[col1]<(20) OR [t1].[dbo].[t1].[col1]>(25) AND [t1].[dbo].[t1].[col1]<(40)))

Hrm. Well, there doesn't seem to be code in CTP that does index unions for cases like this, unfortunately.
Hinting isn't an option either because the index hint code returns an error if the hinted index does not
completely cover the index. On my machine, I get:

(43 row(s) affected)

SQL Server Execution Times:
CPU time = 47 ms, elapsed time = 2661 ms.

That elapsed time is mostly reading pages off my IDE drive. To be clear, this is the _cold cache_ time,
which means that I ran "DBCC dropcleanbuffers" before my run. This throws out all of the buffer pool
pages and forces a read from disk. One assumption in the costing model for the SQL Server QP is that the
pages are _not_ in memory already (and there are cases where this becomes interesting). For this example,
it just means that we have a big table scan with lots of IOs to do (and is therefore slow).

Second example: IN lists
select col1 from t1 where col1 in (6, 7, 8, 26)
|--Table Scan(OBJECT:([t1].[dbo].[t1]), WHERE:([t1].[dbo].[t1].[col1]=(6) OR [t1].[dbo].[t1].[col1]=(7) OR [t1].[dbo].[t1].[col1]=(8) OR [t1].[dbo].[t1].[col1]=(26)))

Well, this is consistent.  IN is generally just a list of ORs as far as the QP is concerned.  Runtime for this query should be very similar to what we saw in our last example:

(7 row(s) affected)

 SQL Server Execution Times:
   CPU time = 63 ms,  elapsed time = 2708 ms.

OK, so what's an enterprising SQL developer to do?  Well, you can rewrite the query as a UNION ALL as long as you know that the ranges are disjoint and cover the predicate properly.
select col1 from t1 with (index=i1) where (col1 > 5 and col1 < 20) 
union all 
select col1 from t1 with (index=i2) where (col1 > 25 and col1 < 40)
SQL Server parse and compile time:
   CPU time = 0 ms, elapsed time = 0 ms.

(43 row(s) affected)

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

That's a lot nicer.

I have no idea if this is something that they intend to add for this release of the product.  I'm not really even sure how common this is.  How often do you want to apply a disjunction of ranges to multiple partial indexes where you don't want to create an index over the whole table?  No clue.  If you have such an example, I'd love to hear about it - drop me a line at conor@sqlskills.com

In the meantime, this is just a post on something interesting I learned while playing with the new CTP6 for SQL Server 2008 that I thought you might like to know.

Happy Querying!

Conor Cunningham

Sunday, March 02, 2008 3:05:32 PM (Central Standard Time, UTC-06:00)  #    Comments [0]  | 
Monday, February 25, 2008
(I got CTP 6 running on another machine, so I'm working except for parallel plans now since it's only a single-proc.  You guys will have to wait on that operator of the day article for now :)

Kudos to my former teammembers for getting filtered indexes into CTP6.  It's pretty darn neat, and I'll show you a few tidbits that are interesting.

So, filtered indexes are not really a new "feature" in some sense.  You can do everything in a filtered index with an indexed view.  Of course, indexed view matching is only supported in the Enterprise edition of the software, so perhaps not everyone has seen those benefits. 

Indexed views are a tricky feature - generalized tree pattern matching is hard (read: CPU expensive), and if you've looked in the BOL the list of restrictions is so long that reminds me of filling out tax forms.  However, the other side effect that occurs when you have indexed views is that the optimizer has to go through additional phases of its search in order to apply them.  The optimizer has a couple of buckets of rules it can run, and most plans find a nice plan in the first or second round of rules.  Generalized indexed view matching is restricted to the last set of buckets, which usually means that there are a lot more rules that have to run before that view can be matched.  The bottom line is that the compilation cost isn't bad for a single query (usually), but it's the sort of thing that can make the difference between an application that can be used in UI response-time requirements or not.

Enter the filtered index.  This is a recognition that there are a lot of indexed views that don't need tons and tons of fancy equations, joins, etc.  These are single-tabled views that have simple predicates.  Once you get into this ballpark, you can bolt this on to the super-efficient index matching code and you can enable a whole new class of application from what you could build previously.  This is why I am excited about this feature.

I haven't looked to see where this feature will fall into the SKU matrix yet, and I'm sure that they're still pondering that very question.  However, you guys should play with this on CTP6 - it's nifty!

So, first things first.  Let's build one of these guys and see if we can match it:

create database t1
use t1
drop table t1
create table t1(col1 int, col2 nchar(2000), col3 time)

create index i1 on t1(col1) where col1 > 5 and col1 < 20
declare @p int 
set @p =0
while @p < 20000
begin
insert into t1(col1) values (rand()*10000)
set @p=@p+1
end
ok so I've filled this table up with a lot of useless data and created a nice little filtered index.
select * from t1  where col1 > 5 and col1 < 20
  |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000], [Expr1007]) OPTIMIZED WITH UNORDERED PREFETCH)
       |--Compute Scalar(DEFINE:([Expr1006]=BmkToPage([Bmk1000])))
       |    |--Index Scan(OBJECT:([t1].[dbo].[t1].[i1]))
       |--RID Lookup(OBJECT:([t1].[dbo].[t1]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)

select * from t1  where col1 > 5 and col1 < 10
  |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000]) OPTIMIZED)
       |--Compute Scalar(DEFINE:([Expr1006]=BmkToPage([Bmk1000])))
       |    |--Index Seek(OBJECT:([t1].[dbo].[t1].[i1]), SEEK:([t1].[dbo].[t1].[col1] < (10)) ORDERED FORWARD)
       |--RID Lookup(OBJECT:([t1].[dbo].[t1]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)

so in the first example I've created a query against the table that directly matches the index condition.  It matches and even generates an index scan (slightly fa