THE SQL Server Blog Spot on the Web
Welcome to SQLblog.com - The SQL Server blog spot on the web Sign in | Join | Help
in Search

Linchi Shea

  • Large Query Performance and Query Parallelism

    [2008/08/25] This post has been modified significantly to correct some inaccurate statements because I mis-read Joe Chang's post.

    Joe Chang posted some interesting results using the TPC-H queries with the scale factor set to 10. I happened to have done something similar, and naturally noticed a rather significant difference between his results and mine. [2008/08/25: Okay the difference is not as significant as I had thought.]

    My results were obtained on SQL Server 2005 Enterprise x64 Edition running on Windows Server 2003 Enterprise x64 Edition. The test server, whose make and model will remain undisclosed,  had four 2.93GHz quad-core Intel Tigerton sockets (Xeon X7350) with 64GB of RAM with 20GB allocated to the SQL Server buffer pool. The TPC-H scale factor was set to 10, and the data were generated with DBGEN from tpc.org.

    My results are as follows:

    Query    MAXDOP 1    MAXDOP 4   MAXDOP 8     MAXDOP 16
    tpcH Query1 64845 17080 9373 4865
    tpcH Query2 571 180 110 97
    tpcH Query3 17687 3272 1792 1334
    tpcH Query4 23079 3859 2293 1633
    tpcH Query5 34255 5040 2646 1935
    tpcH Query6 2250 595 332 204
    tpcH Query7 18299 3562 1754 1513
    tpcH Query8 9636 3245 2042 1809
    tpcH Query9 50470 14094 7559 4460
    tpcH Query10 19363 4432 2289 1630
    tpcH Query11 4558 674 424 390
    tpcH Query12 10522 3815 2718 982
    tpcH Query13 26823 7577 4425 2121
    tpcH Query14 2356 624 357 227
    tpcH Query15 1705 1421 280 170
    tpcH Query16 7960 3461 2155 1681
    tpcH Query17 1070 325 220 276
    tpcH Query18 68366 19719 10174 5829
    tpcH Query19 2330 711 441 843
    tpcH Query20 968 314 221 695
    tpcH Query21 80858 14565 8149 6072
    tpcH Query22 9418 1762 986 664
             
    Total (second) 457 110 61 39

    All the numbers in the above table are query elapsed times in milliseconds except the very last line highlighted in yellow, which are in seconds. The last line contains the total elapsed time for Query1 through Query22 for each MAXDOP setting. All the results were obtained with all the database pages cached in the buffer pool. The same TPC-H query stream (i.e. Query 1 through Query 22) was repeated for 10 times with all the results from the first run thrown out, and the max elapsed time and the min elapsed time for each query for each of the subsequent runs were also thrown out before the elapsed times were averaged.

    So what's the difference between Joe's results and mine?

    Although there are some differences in the elapsed times in terms of the absolute numbers, the differences are minor enough to be no cause for alarm, especially when you take into consideration the platform differences and probably some differences in the test setup.

    However, the deline in the elapsed times in my test results is more significant than it is in Joe's results when MAXDOP is changed from 1 through 16. The difference is not as dramatic as I had throught, and initially commented on in the original version of this post (because I was reading the wrong line for whatever reason).

    Given that TPC-H has been around for a long time and all DBMS vendors have tried very hard to optimize their products to performance well on TPC-H, I do not expect SQL Server to be an exception, and am rather pleased to see that it scales very well with MAXDOP on these queries.

    I'm a bit embarrassed that I didn't read Joe's results correctly first time around, but felt that it's time well spent to be able to cross check independent tests results from realistic envrionments.


  • MSIZAP is Your Friend

    If you ask for my opinion on Windows Installer, I’d have to think what polite words to use. But almost every Microsoft product uses it, and that includes SQL Server. So we are stuck with it and have to live with it for now and for the forseeable future.

     

    One of major problems with Windows Installer is that its install packages can be so complex that it sometimes confuses the heck out of the Installer. I don’t know about you, but I have run into enough install/patch problems, and wasted countless number of hours when a SQL Server edition, version, or hotfix simply refuses to install and spits out strange error messages that turn out to have nothing to do with the real problems. Trying to fix these installer problems is like performing brain surgery on yourself—the chance of success is just not very high, at least judging from my own experience and even with help from the Microsoft support engineers.

     

    Now, there are times when you can afford to obliterate an installed product, or whatever remains in its mangled state, and those are times you may find msizap.exe extremely useful and can save you tons of grief.

     

    What msizap.exe can do for you is to wipe the slate clean by removing the file and registry settings that make up the Windows Installer configuration information for an installed product. Note that it removes the file and registry settings, not the files and registry keys themselves. If there are files of the product left in the file system, and registry keys/values left in the registry, you would have to delete them yourself.

     

    Now, msizap.exe can be a very dangerous tool to use because it basically pulls the carpet from underneath a product. And you have to be really careful not to apply it to a wrong product. In addition, msizap.exe is not exactly intuitive to use as you need to deal with the product codes, which are GUID strings.

     

    Fortunately, Microsoft has a more user friendly tool on top of msizap.exe. This more user friendly tool is called Windows Installer CleanUp Utility. You can find it easily via Google or in this KB article. When you launch this Installer CleanUp utility, it’ll automatically search the msi database, and list all the product names and their corresponding product codes for you to select. Then, you can simply select the product(s) whose msi configuration information you want to zap. And that’s all you need to do! Once the slate is wipted clean, the chance of the product install goes through becomes very high.

     

    The msizap.exe utility has saved me in several occasions, and I'm sure you’d be thankful that you have msizap.exe around should you run into a similar situation.


  • Performance Impact: Bookmark Lookup is Expensive - Even in Memory

    It’s well known that bookmark lookup (aka key lookup in case of a clustered index) is not cheap, especially when it comes to retrieving a lot of data. So I’m not going to rehash the pros and cons of bookmark lookup or why bookmark lookup is expensive. But I’ve noticed that when it comes to discussing bookmark lookup, all the literature seems to being focusing on their implications on storage I/Os. There is nothing wrong with that. In practice, it ultimately does come down to the fact that when reading a large amount of data, random I/Os are much more expensive than sequential I/Os, and bookmark lookup tends to incur random I/Os.

     

    What I’d like to highlight in this post is that even if all or most of the pages are in memory already, bookmark lookup is still very expensive compared to scan.

     

    To show this is the case, I piggybacked on the test setup described in my last post. Let me briefly recap the test setup. A single table was used, and its definition is as follws:

    CREATE TABLE test (
          i           int NOT NULL,
          j           int NOT NULL,   
          dt          datetime,
          filler      char(5000) NOT NULL
    )

    Two indexes were created on the table:

     

    CREATE CLUSTERED INDEX cix_test ON test(i);
    CREATE INDEX cx_test ON test(j);

    And the table was populated with 2,000,000 rows with the following INSERT statement in a loop (local variable @i going from 1 to 2,000,000):

    INSERT test(i, j, dt, filler)
    SELECT @i,
           CASE WEHN @i % 2 = 0 THEN @i ELSE 2000000 - @i END,
           getdate(),
           'abc'

    Then, the following two queries were separately run multiple times on a server with 32GB of RAM (25GB of which was allocated to the SQL2005 instance buffer pool):

     

    SELECT max(dt) FROM test;
    SELECT max(dt) FROM test WITH (index(cx_test));

     

    After the second run, all the pages were cached in memory (the table was about 16GB in size). Notice that with the first query SQL Server used clustered index scan to produce the result, whereas with the second query the index hint forced SQL Server to scan the nonclustered index and then use bookmark lookup to produce the result.

     

    I’ll look at the storage I/O implication of these two access paths in a separate post, but for now what do you think might be the difference in the elapsed time of these two query-processing methods when all the pages were cached in memory?

     

    The following chart shows the difference:

    For this test, the bookmark lookup method took almost three times as long as did the clustered index scan method.

     

    Why such a huge difference? When all the pages were cached in the buffer pool, we can’t explain the query elapsed time difference with the difference in storage I/O efficiency. We can, however, explain the difference with the difference in the number of pages SQL Server engine must visit in the buffer pool.

     

    The output from SET STATISTICS IO ON for each method is as follows:

     

    Clustered Index Scan:

    Scan count 1, logical reads 2008931, physical reads 0, read-ahead reads 0 …

     

    Bookmark Lookup:

    Scan count 1, logical reads 8130363, physical reads 0, read-ahead reads 0 …

     

    Clearly, the access method with bookmark lookup was not efficient, visiting four times as many pages as did the access method with clustered index scan.

     

    Also note that compared with the size of the table, the size of the nonclustered index was insignificant. There were about 5,000 pages in the leaf pages of the nonclustered index, whereas there were 2,000,000 leaf pages in the clustered index. So the cost of processing the query with the nonclustered index was dominated by the bookmark or key lookups.

     

    Of course, what I’ve discussed in this post is probably more academic than practical as you can’t expect all the pages being cached when processing large reporting queries in most real environments, and therefore the real dominate factor is not how many pages SQL Server needs to traverse in memory, but how many pages SQL Server has to bring into memory and how these pages are brought into memory.

     

    It's not my intention to leave you an impression that I'm picking on the bookmark lookup operation because it's just a bad method to retrieve data. That's not the case at all because for some queries, it is an excellent method. But that's not the focus of this post, and there are plenty of discussions on the advantages of the bookmark lookup operation you can find elsewhere.

     

    Test Environment

    SQL Server 2005 Enterprise x64 Edition ran on Windows Server 2003 Enterprise x64 Edition with Service Pack 2. The SQL2005 build was 9.0.3239 (i.e. SQL Server 2005 CU7) with 25GB allocated to the buffer pool. The server was a HP BL680 G5 including four 2.4GHz quad-core Xeon E7340 processors (aka Tigerton) with 2x4MB L2 cache and 32GB of RAM.

     


  • Performance Impact: Some Data Points on Read-Ahead

    In the next series of posts, I'll focus on SQL Server I/O, revisiting some common issues and taking a closer look at some others. In each post and as always, I'll make the case with specific data points from my tests. For the first two posts in this series, let me check out the read-ahead technique used by SQL server.

    Read-ahead is an important I/O optimization technique used by SQL Server.  Intuitively, if SQL Server can correctly forecast the need for more pages and read these pages ahead of time when they are needed for query processing in memory, your query is expected to perform better. Bob Dorr in his classic SQL Server 2000 I/O Basics whitepaper has an excellent description on how SQL Server read-ahead works. Every SQL Server professional with any interest at all in the storage engine should read that paper.

    But just how important is the read-ahead technique in query processing? Let's look at some results from an extremely simple test.

    First of all, a single table was used in the test, and here is the definition:

    CREATE TABLE test (
          i           int NOT NULL,
          j           int NOT NULL,   
          dt          datetime,
          filler      char(5000) NOT NULL
    )

    I then created a clustered index on the i column (for this test column j was not used and you can ignore it):

    CREATE CLUSTERED INDEX cix_test ON test(i);

    And the table was populated with 2,000,0000 rows with the following INSERT statement in a loop with @i going from 1 to 2,000,000:

    INSERT test(i, j, dt, filler)
    SELECT @i, CASE WEHN @i % 2 = 0 THEN @i ELSE 2000000 - @i END, getdate(), 'abc'

    With the statistics updated, I then ran the following SELECT query in two test scenarios:

    DBCC DROPCLEANBUFFERS;
    go
    SELECT max(dt) FROM test;

    The two test scenarios are as follows:

    Test Scenario Description
    Read-ahead enabled This is the default SQL Server behavior.
    Read-ahead disabled Read-ahead was disabled with trace flag 652: DBCC TRACEON(-1, 652)

    First, let's look at the elapsed time of the above SELECT query in the two scenarios (the numbers reported in the following table are averages over several test runs):

    Test Scenario Elapsed Time of SELECT (second)
    Read-ahead enabled 80
    Read-ahead disabled 210

    In both scenarios, the SELECT query was correctly processed with a clustered index scan. When read-ahead was disabled, the clustered index scan took almost times as long as did the clustered index scan when read-ahead was not disabled. For this query, the performance difference was astounding.

    I don't have access to the source code that controls read-ahead so I can't tell exactly why and how it made such a huge difference from the code logic perspective. However, from Bob Dorr's description in the SQL Server 2000 I/O Basics whitepaper, it's rather clear that SQL Server read-ahead is quite aggressive in exploiting the performance capacity of the storage I/O subsystem.  So we can turn to observing the storage I/O behavior for explanation. The following table summarizes the values of the key I/O counters observed during the test runs:

    I/O Counter Value When Read-ahead is Enabled Value When Read-ahead is Disabled
    Bytes/Read > 350KB/read ~ 64KB/read
    Megabytes/sec 180 ~ 200MB/sec ~ 80MB/sec
    SQL Server Readahead ~20,000 readahead pages/sec 0

    Clearly, with read-ahead, SQL Server was able to take advantage of large sized I/Os (e.g. ~350KB per read). Large-sized I/Os are generally much more efficient than smaller-sized I/Os, especially when you actually need all the data read from the storage as was the case with the test query. From the table above, it's evident that the read throughput was significantly higher when read-ahead was enabled than it was when read-ahead was disabled. In other words, without read-ahead, SQL Server was not pushing the storage I/O subsystem hard enough, contributing to a significantly longer query elapsed time.

    That is, the table was about 16,000MB in size. At ~200MB/sec, it would take about 80 seconds to read 16,000MB, and at ~80MB/sec, it would take about 200 seconds to read the same amount of data. And these numbers (i.e. 80 seconds and 200 seconds) match nicely the recorded query elaped times which are reported in the second table above.

    In the next post, I'll check out the impact of disabling read-ahead on bookmark or key lookups when a nonclustered index is used.


  • SQL Server 2008 Data Compression: Impact of Data Distribution

    In this post, I continue to explore the implications of SQL Server 2008 data compression. It is particularly worth highlighting the fact that SQL Server 2008 data compression is performed at the page level instead of the table level. In other words, when SQL Server 2008 goes about compressing data, it does it one page at a time, and its compression algorithm considers only the data on that page.

    The page level approach to data compression has important implications, one of which is that what kind of compression ratio you get on a table depends on how the data in the table is distributed. That is, it is possible that with the exactly same set of data in the table, you can get radically different compression ratios if data is shuffled around in the table.

    To drive this point home, let me show you an extreme example with the following table:

    create table customer (

         cid   int identity,

         filler char(100)   NOT NULL,

    )

    With this table, I tested two scenarios:

    Scenario 1: Clustered index was created on column cid:

    create clustered index cus_cx on customer(cid);

    Scenario 2: Clustered index was created on column filler

    create clustered index cus_cx on customer(filler);

    Again, with the exactly same set of data in the table, in each scenario I compressed the data, and compared the table size before the compression and after the compression.  The data compression was done with ALTER TABLE statement as follows:

    ALTER TABLE customer REBUILD

                WITH (DATA_COMPRESSION = PAGE, MAXDOP=8);

    The following chart shows the table size before and after compression for each test scenario:

     

    Clearly, when the table was clustered on the column cid, the data compression ratio was very low (slightly greater than 1), whereas when the table was clustered on the column filler, the data compression ratio was very good at ~10 (the compressed table was about 10 times smaller).

    Why?

    First we need to know exactly what data was in the table, i.e. we need to know the characteristics of the data. Well, the data was populated with the following script:

    declare @i int

    set @i = 1

    begin tran

    while @i <= 8000000

    begin

          insert customer(filler)

          select left(cast(@i % 80 as varchar(8)) + '00000000', 7) + replicate('a', 92)

     

          if (@i % 10000) = 0

          begin

               commit tran

               begin tran

          end

          select @i = @i + 1

    end

    commit tran

    With this script, the first 80 rows would have the cid values going from 1 through 80, and the filler column would have 80 distinct values. With the cid values going from 81 through 160, the filler column would have another 80 distinct values, but as a set they were identical to the first 80 values. This pattern repeated every 80 rows because of the expression @i % 80 in the script.

    Note that the table row was sized such that each page would contain ~80 rows. This means that if there was a clustered index on column cid, every page would have 80 distinct values in the filler column. When SQL Server tried to compress such a page, there wasn’t much to compress. Now, when the clustered index was created on the filler column instead, every page was filled with duplicate values in the filler column, making the page extremely conducive to data compression.

    Again, this post is to help highlight the behavior of SQL Server 2008 data compression. The behavior presented here is neither good nor bad. It’s just a natural consequence of the page-level algorithm. Oracle’s data compression also adopts a page/block level algorithm, and therefore would see the same behavior.

    Note that if all the values are distinct, no matter how you shuffle the data you won't improve its compression ratio.


  • Dedicated Admin Connection

    Dedicated Admin Connection (DAC) is one of the highly touted features introduced in SQL Server 2005. Now that it has been around for a few years, I wonder how it is actually being used or whether it's being used at all in the real world.

    I have not run into any real production scenarios where I'm forced to use DAC. But that may just be due to my own narrow exposure to SQL Server problems. If you have used it to solve real problems (not just for testing or folling around), I'd very much like to know what kind of problems they were.


  • SQL Server 2008 Page Compression: Performance impact on table scans

    My previous post shows that data compression may not come for free, although hopefully by the RTM time the adverse performance impact on inserts will have been reduced significantly. In this post, I want to show you that data compression can enhance read performance. Intuitively, data compression may reduce the number of pages SQL Server must read into memory from disk, and the savings in terms of the number of pages can be significant, and therefore there may be a positive impact on reads.

    To continue our tradition of letting data points do the talking, I ran the following simple script with the customer table when (1) it was page compressed and when (2) it was not page compressed. This was the same customer table I described in an earlier post on page compression and multiple processors.

    dbcc dropcleanbuffers;
    declare @dt datetime;
    select @dt = getdate();

    select avg(c_balance) from customer;

    select datediff(second, @dt, getdate());
    go
     

    Note that there was no index on the c_balance column, and I verified that the actual execution plan was a table scan (well a clustered index scan). DBCC DROPCLEANBUFFER was included to ensure pages were read from disk, not already cached. The following chart summarizes the results.

    Wow! The elapsed time difference between the compressed data and uncompressed data was huge. It took 52 seconds to scan the table when it's not compressed, but it took only 3 seconds when the table was compressed. Note that when uncompressed, the table had 2264 pages, and when compressed it had 534 pages.

    Two more points to note: First, I did not try to explain why the improvement in table scan performance seemed to be disproportional to the reduction in the table size as the result of page compression. But I did run these tests many times to make sure I was not going to give you incorrect data. Second, I would caution you not to get too hung up on these specific numbers. As they often say, your mileage may vary. Instead, focus on the big picture impression that page compression may give your reporting queries a performance boost. In a real app, life is not so simple, and you would have to strike a balance between better reporting queries and potentially longer batch/data feed processing time when you consider page compression.

     


  • SQL Server 2008 Page Compression: Performance impact on inserts

    Before I begin, let me be very clear that the results I report in this post were obtained with SQL Server 2008 CTP6. Microsoft is still working on improving the performance of data compression. And in particular, the performance of inserting into a compressed table is expected to be significantly enhanced in SQL Server 2008 RTM.

    In my previous post, I have shown that data compression can result in significant savings in storage space. In this post, I want to highlight the fact that not everything comes for free. Let's take a look at inserting into a B-tree table, i.e. a table with a clustered index. What is the performance impact of page compression on inserts?

    Note that you must use TABLOCK hint for pages to receive page-level compression when the pages of a heap table are being populated with the INSERT statement. So, heap tables are much less interesting, and I'll hence focus only on B-tree tables.

    In my tests, I ran the following script:

    truncate table customer2;
    -- enable page compression
    alter table customer2 rebuild with (data_compression=page);
    go
    declare @dt datetime;
    select @dt = getdate();
    insert customer2 select * from customer;
    select 'insert into customer2 with compression', datediff(second, @dt, getdate());
    go
    truncate table customer2;
    -- disable compression
    alter table customer2 rebuild with (data_compression=none);
    go
    declare @dt datetime;
    select @dt = getdate();
    insert customer2 select * from customer;
    select 'insert into customer2 without compression', datediff(second, @dt, getdate());
    go
     

    The script measures the difference in the elapsed time of inserting 3,000,000 rows into (1) an empty table when page compression is enabled, and (2) an empty table when no data compression is enabled. The script was run multiple times to ensure that the elapsed times recorded were consistent. The following chart shows the results:

    Clearly, the impact of page compression on inserts was rather significant in the tests. Without data compression, inserting 3,000,000 rows took ~80 seconds, whereas it took 217 seconds when page compression was enabled on the target B-tree table. No free lunch!

    Again, these numbers were obtained with SQL Server 2008 CTP6, a work in progress build. I'll re-run the test once SQL Server 2008 RTM is released.

    Now that I have shown that there can be performance penalty in using data compression, next time I'll show that data compression can also help improve your performance.


  • SQL Server 2008 Page Compression: Compression ratios with real-world databases

    In my previous post on data compression, I looked at how rebuilding a table with page compression works with multiple processors via the MAXDOP option. In this post, I'll focus on what compression ratios I have seen in the real-world databases. Now, if you understand how SQL Server 2008 data compression works, you know that what compression ratio you may get really depends on to what extent SQL Server can find duplicate values on the data/index pages. Consequently, one real-world database can have a very high compression ratio while another may have a very low compression ratio: there is no typical compression ratio to expect.

    That said, it is still interesting to report what compression ratios I have seen with real customer databases instead of 'cooked' data. Before I give the results, let me define what I mean by compression ratio:

            Compression Ratio = Uncompressed Size / Compressed Size

    To see what kind of compression ratios I may run into with real-world databases, I randomly selected 14 customer databases. IN this blog post, these databases have been renamed to DB1, DB2, ..., DB13, and DB14 to shield their identities. For each database, the following steps were performed:

    1. Record the data size of sp_spaceused for the database. This is the uncompressed data size.
    2. For each user table in the database, rebuild it with page compression. This was done by generating and executing a script that contains the following ALTER TABLE statement for each table:
                alter table <the_table> rebuild with (data_compression=page, maxdop=8);
    3. Record the data size of sp_spaceused for the database. This is the compressed data size.
    4. For each user table in the database, rebuild the table with no compression, which effectively decompresses the table. This was done by generating and executing a script that contains the following ALTER TABLE statement for each table:
                alter table <the_table> rebuild with (data_compression=none, maxdop=8);
    5. Record the data size of sp_spaceused for the database. This is the decompressed data size.

    The data size obtained in Step 3 was used as database compressed size, and the data size obtained in Step 5 was used as the database uncompressed size. The data size obtained in Step 1 was not used because it may be polluted by fragmentation. If we used the data size from Step 1, we could potentially be using a size that was inflated by fragmentati