The post New Course: “SQL Server 2012: Nonclustered Columnstore Indexes” appeared first on Joe Sack.
]]>The intent behind this course was to provide a fast ramp-up on the aspects of this feature that you should consider when looking at whether or not this is a viable option for your SQL Server relational data warehouse workloads. The course covers the nonclustered columnstore index feature fundamentals, batch-execution mode and segment elimination considerations, data modification techniques, and how to plan for deployment.
Even if you’re unsure of whether or not you’ll be using nonclustered columnstore indexes anytime soon, it is part of the database engine, so from a core SQL Server perspective it is a good idea to familiarize yourself with this topic. With vNext of SQL Server, Microsoft is introducing clustered columnstore indexes, so I think we’ll see a significant uptick in overall usage in the future.
The post New Course: “SQL Server 2012: Nonclustered Columnstore Indexes” appeared first on Joe Sack.
]]>The post Exploring Columnstore Index Batch Sizes appeared first on Joe Sack.
]]>To start off with, I’ll create a table that uses six different supported nonclustered columnstore index data types:
USE [CS]; GO CREATE TABLE dbo.[FactContrived] ( col00 TINYINT NOT NULL DEFAULT 255, col01 SMALLINT NOT NULL DEFAULT 32767, col02 INT NOT NULL DEFAULT 2147483647, col03 BIGINT NOT NULL DEFAULT 9223372036854775807, col04 SMALLMONEY NOT NULL DEFAULT 214748.3647, col05 MONEY NOT NULL DEFAULT 922337203685477.5807); GO
I’ll populate this table with 1,048,576 rows, all using the same value for each row (contrived by-design):
SET NOCOUNT ON; GO INSERT dbo.[FactContrived] DEFAULT VALUES; GO INSERT dbo.[FactContrived] SELECT [col00], [col01], [col02], [col03], [col04], [col05] FROM dbo.[FactContrived]; GO 20
Next I’ll create a nonclustered columnstore index on each column in the table:
CREATE NONCLUSTERED COLUMNSTORE INDEX [NCI_FactContrived] ON dbo.[FactContrived] ([col00], [col01], [col02], [col03], [col04], [col05] ) WITH (MAXDOP = 1); GO
Once created, I’ll check the page count statistics with the following query:
SELECT [partition_id],
SUM([in_row_used_page_count]) AS [in_row_used_page_count],
SUM([lob_used_page_count]) AS [lob_used_page_count],
SUM([row_count]) AS [row_count]
FROM sys.[dm_db_partition_stats]
WHERE [object_id] = OBJECT_ID('FactContrived') AND
[index_id] = INDEXPROPERTY
(OBJECT_ID('FactContrived'),
'NCI_FactContrived',
'IndexId')
GROUP BY [partition_id];
GO
This query returns the following results:
So as expected, the nonclustered columnstore index is stored within LOB pages (1,050 total).
Next I’ll check the segment metadata for the specific partition_id:
SELECT [column_id], [segment_id], [encoding_type], [row_count], [primary_dictionary_id], [secondary_dictionary_id], [min_data_id], [max_data_id], [on_disk_size] FROM sys.[column_store_segments] WHERE [partition_id] = 72057594039762944;
This returns the following 7 rows:
Observations on this output:
Next I’ll check the dictionary metadata:
SELECT [column_id], [dictionary_id], [type], [entry_count], [on_disk_size] FROM sys.column_store_dictionaries WHERE [partition_id] = 72057594039762944;
This returns the following 6 rows (no dictionary for our column_id 7):
Notice each dictionary just has one entry (since each column has just one unique value in the table across the 1,048,576 rows). Also notice the very small on_disk_size is bytes – and the dictionary type of “1” which according to BOL is a “Hash dictionary containing int values.”
Now I’ll execute a query that leverages the columnstore index using batch execution mode:
SELECT [col00], MAX([col00]) AS [MaxCol00] FROM [dbo].[FactContrived] GROUP BY [col00] OPTION (RECOMPILE); GO
The plan (via SQL Sentry Plan Explorer) is as follows:
The Plan Tree tab shows that batch execution mode was used for the Columnstore Index Scan and the Hash Match (Partial Aggregate) operators:
As for thread distribution, while the plan had a degree of parallelism of 8 and and one branch with 8 reserved threads, only one thread has rows associated with it:
Now regarding the actual number of batches, we see the following:
So with 1,166 batches of rows across 1,048,576 rows, we’re looking at an average batch size of 899.29 rows.
Experimenting with referencing the other columns of this table, we’ll see the same number of batches each time (I’ll spare you the repeated results).
Now when columnstore indexes were first discussed a year or so ago, you would see references to batch sizes of approximately 1,000 rows. For example, the SQL Server Columnstore Index FAQ talks about how a batch “typically represents about 1000 rows of data.” Then a few months after SQL Server 2012 I recall seeing references to it being approximately 900 rows. The results of this particular example show we’re closer to that 900 row value.
Now for contrast – I went ahead and dropped the nonclustered columnstore index, truncated the table, and repopulated it with random values instead of using one unique value per column. Below is a revisiting of the metadata for the new data distributions after adding back a nonclustered columnstore index…
Partition Stats
Notice we have more LOB pages for the random data vs. the uniform data – as expected.
Segment Metadata
Notice the higher on_disk_size and the varying min/max data ranges per segment. Also notice that with the random, more unique data, there only one of the columns (our tinyint column) has a primary_dictionary.
Dictionary Metadata
And we see one dictionary entry for our tinyint column.
Revisiting our original test query, the plan shape changes somewhat (additional Parallel Gather Streams), given that we now actually have 256 rows returned based on the tinyint column:
As for row distribution across threads, we still see one thread handling the batch-mode operations and then a spread of the row-mode rows across the other operators:
Now as for the number of batches, we see the following:
So 1,280 batches over 1,048,576 rows. Averaging 819.2 rows per batch, vs. our previous test’s 899.29 rows.
There is more to explore on this subject – but that’s all for today as I’m about to jump on a plane to Chicago to help deliver IE2: Immersion Event on Performance Tuning.
The post Exploring Columnstore Index Batch Sizes appeared first on Joe Sack.
]]>The post Which LOB pages are associated with a specific columnstore segment? appeared first on Joe Sack.
]]>How do I track which LOB pages are associated with a specific columnstore index segment?
Jonathan Kehayias and I discussed this the other day and hashed out a few options for tracking this.
To illustrate this topic and keep it at a granular level – I used a simple table named FactIDDemo with a bigint FactID column that had a unique clustered index on it. Again – exploratory and not intended to be a realistic implementation pattern.
I loaded the table with 1,048,576 rows. And the segment statistics were as follows:
SELECT [partition_id], [segment_id], [row_count], [min_data_id], [max_data_id] FROM sys.[column_store_segments] WHERE [partition_id] = 72057594043236352 ;
How many LOB pages were allocated for this single segment?
SELECT [partition_id], [object_id], [index_id], [partition_number], [in_row_used_page_count], [lob_used_page_count], [used_page_count], [row_count] FROM sys.[dm_db_partition_stats] WHERE [partition_id] = 72057594043236352;
We see 351 used LOB pages and executing DBCC IND confirms this as well, outputting the page ids accordingly:
DBCC IND('BigFactTable', 'FactIDDemo', 2);
That command returned 351 rows – one of which was the IAM page and the remainder text pages.
I also created the following XE session to validate page access (testing on a cold cache using my session ID as well):
CREATE EVENT SESSION [Columnstore Page Access] ON SERVER ADD EVENT sqlserver.physical_page_read( ACTION(sqlserver.session_id) WHERE ([sqlserver].[session_id]=(57))) WITH (MAX_MEMORY=4096 KB,EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS, MAX_DISPATCH_LATENCY=30 SECONDS,MAX_EVENT_SIZE=0 KB,MEMORY_PARTITION_MODE=NONE, TRACK_CAUSALITY=OFF,STARTUP_STATE=OFF) GO
And I used the following test query to initiate the physical page read events:
SELECT COUNT([FactID]) AS [FactIDCount] FROM [dbo].[FactIDDemo] WHERE [FactID] BETWEEN 1 AND 1048576; GO
This query resulted in 349 events related directly to the columnstore index access. That is different from the 351 page count from sys.[dm_db_partition_stats] and DBCC IND output. The XE event didn’t capture the IAM page reference (in my case, page 1870780) – and it also didn’t retrieve page 1870777 which was a pagetype 3 (LOB page) and when I looked at it via DBCC PAGE, didn’t show BLOB_FRAGMENT sections.
Segments are the unit of transfer for columnstore index access. While this is the logical unit of transfer, a segment is stored as one or more LOB pages – and to track that physical access, it seems that the sqlserver.physical_page_read is a viable way of doing so.
The post Which LOB pages are associated with a specific columnstore segment? appeared first on Joe Sack.
]]>The post Columnstore Segment Population Skew appeared first on Joe Sack.
]]>Anyhow, this is a quick post on segment population skew based on parallel nonclustered Columnstore index creations.
I’ll use the same 123,695,104 row FactInternetSales table I used almost a year ago to demonstrate. I’ll create the following nonclustered Columnstore index just on one column, to keep things simple:
CREATE NONCLUSTERED COLUMNSTORE INDEX [NCSI_FactInternetSales] ON [dbo].[FactInternetSales] ( [ProductKey] );
The index takes 31 seconds to create on my laptop and it was created using 8 threads (which I can confirm via the SQL Server execution plan in, this case, SQL Sentry Plan Explorer):
Adding up the actual rows by thread, we get the 123,695,104 row count.
Now if we look at sys.column_store_segments, we can see that the last few segments were populated with less than the maximum 1,048,576 rows:
SELECT [partition_id], [column_id], [segment_id], [row_count] FROM sys.column_store_segments WHERE [row_count] = 1048576 AND [column_id] = 2;
Now the purpose of this short post is to show what happens if we remove parallelism from the overall Columnstore index build (aside from increasing build time and reducing the memory grant):
DROP INDEX [NCSI_FactInternetSales] ON [dbo].[FactInternetSales]; GO CREATE NONCLUSTERED COLUMNSTORE INDEX [NCSI_FactInternetSales] ON [dbo].[FactInternetSales] ( [ProductKey] )WITH (DROP_EXISTING = OFF, MAXDOP = 1); GO
Now instead of running in 31 seconds with 8 schedulers, this serial index build took (not surprisingly) 2 minutes and 10 seconds to build.
How many segments fell beneath the 1,048,576 row count?
This time, just one segment, the last one to be populated. With 117 segments (segment_id 0 through segment_id 117) populated at 1,048,576 per segment, and 123,695,104 rows – our 118th segment has the remaining 1,011,712 rows.
Should the more tightly packed segments provide meaningful performance gains versus the parallel-built, partially filled version? I haven’t tested this yet, but I will at some point. Let me know if you get a chance to do so before I do. My wild guess would be that the benefit would be minor, at best – but as with most things I would like to see for myself.
The post Columnstore Segment Population Skew appeared first on Joe Sack.
]]>The post Exploring Columnstore Index Metadata, Segment Distribution and Elimination Behaviors appeared first on Joe Sack.
]]>Today I thought I would take a few minutes to explore columnstore index behavior on a 123,695,104 row fact table. My previous tests had been limited to < 10 million rows, which doesn’t really showcase columnstore index capabilities. So I thought I would experiment with higher numbers – although I still want to start the billion row testing scenarios at some point.
The experimentations took place on a SQL Server instance capped at 8GB of RAM max server memory and 8 logical processors. I used the FactInternetSales table from AdventureWorksDWDenali – increasing it to 123,695,104 rows.
I covered the entire fact table for my initial exploration (although as you’ll see, I made a few changes both to this index and also moving from a heap table to a clustered index):
CREATE NONCLUSTERED COLUMNSTORE INDEX [NonClusteredColumnStoreIndex-20120225-092018] ON [dbo].[FactInternetSales]
(
[ProductKey],
[OrderDateKey],
[DueDateKey],
[ShipDateKey],
[CustomerKey],
[PromotionKey],
[CurrencyKey],
[SalesTerritoryKey],
[SalesOrderNumber],
[SalesOrderLineNumber],
[RevisionNumber],
[OrderQuantity],
[UnitPrice],
[ExtendedAmount],
[UnitPriceDiscountPct],
[DiscountAmount],
[ProductStandardCost],
[TotalProductCost],
[SalesAmount],
[TaxAmt],
[Freight],
[CarrierTrackingNumber],
[CustomerPONumber],
[OrderDate],
[DueDate],
[ShipDate]
) WITH (DROP_EXISTING = OFF)
GO
Remember that we can have up to 1,024 columns and that the concept of key columns and INCLUDE don’t apply here. Nor does the ASC or DESC keywords or creation of the columnstore index as clustered (only nonclustered is supported at this point).
Regarding the data types, we can cover the standard “business” ones – but not the “max”, LOB and xml flavors.
<quick aside> By the way – a shout out to Eric Hanson’s Columnstore Index references on the TechNet Wiki. These have been invaluable in understanding columnstore indexing and also revealing tools to help fish for your own answers. I love seeing this kind of high quality content coming directly from the Program Managers. I really hope that more PMs start updating their associated feature areas on TechNet Wiki in the future. When I first heard about TechNet Wiki I was more than a little dubious. We already have blogs, forums, BOL, papers, videos, etc. Why do we need another information channel? Seeing Eric’s content has totally shifted my opinion on this. The information is timely, fresh (you see it get updated frequently) and easily discoverable. It also allows for community participation and questions – which, if managed well – only helps the associated quality of the content. I don’t believe TechNet Wiki replaces BOL or other information channels, but I feel it has definitely earned a seat at the table. Again, I hope other PMs follow this lead – or alternatively use their blogs to heavily describe their feature areas (much like the great blog post series on the AlwaysOn Readable Secondary feature from Sunil Agarwal). Just doing periodic updates on their pet features can help do wonders for feature adoption and comprehension. </quick aside>
Back on task… The following are the various questions I asked and the various observations made….
Was tempdb used in creating the columnstore index on a heap table?
I intentionally had tempdb configured to an initial small size with auto-growth enabled in order to see if it was used at all during the CREATE COLUMNSTORE INDEX against the 123 million row table (I could have looked through counters and other ways too – this was just a side effect observation). The answer? It was not used (staying at a very small value of 10 MBs).
How do things look in sys.indexes and sys.index_columns?
Nothing terribly interesting – except that the is_included_column is a value of “1”:
Anything interesting in sys.column_store_dictionaries?
Most definitely… This was a pretty illuminating catalog to query. A few of the observations:
· There were 149 rows across the 26 columns defined in the index.
· 25 columns had one row – and 1 column had 125 rows. Not surprisingly – the SalesOrderNumber was the column with 125 entries. This is also the column I populated with a random GUID that I converted to nvarchar(20) for new entries to the table. The values for this column, while not constrained, were nearly unique in my test (with the exception of the base data).
· The on disk (dictionary) storage for SalesOrderNumber was by far the worst (not surprisingly). It was 2,328,411,337 in bytes. Compare that with 21,384 bytes for an integer column, 74,584 bytes for a datetime column and 1,088 bytes for a money data type column. So this would point to a design decision around not including 100% unique values as part of the index – or if you do, choosing a much more compression friendly data type mapped to an associated dimension table.
· The entry_count column_store_dictionaries column was also very interesting. For example, the int data type ProductKey had 158 entries. The ShipDate datetime column had 1,126 entries. The SalesOrderNumber had 123,662,365 entries. No magic here (and the difference between those entries and the 123,695,104 row count was that the initial data populated didn’t use my random GUID values).
· The type column from the results told me about the type of dictionary… For the columns with “1”, that maps to hash dictionary for integer based columns. A type of “4” represented my float columns. Type “3” represented the string values.
· The flags (“internal use only”) column from column_store_dictionaries was “0” for all columns except for UnitPriceDiscountPct (float), DiscountAmount (float), OrderDate/DueDate/ShipDate (datetime). The flags column was “2” for the CustomerPONumber (nvarchar(25)) and CarrierTrackingNumber (nvarchar(25)). Flags was 0 for everything else, so I’m definitely curious about this.
Before I continued, I dropped and recreated the columnstore index, this time without the SalesOrderNumber. It wasn’t going to provide any value for my next set of explorations.
With the SalesOrderNumber column removed, leaving the other columns on the columnstore index, what was the on-disk footprint?
310,400 bytes for the dictionary size – for 123 million rows (per sys.column_store_dictionaries). I double checked sys.dm_db_partition_stats as well and the lob_used_page_count was 304,579. So the page count is around 2.3 GB – and the dictionary size is 310 KB. Again, this is for 123,695,104 rows. The heap itself is 2,876,461 pages – or roughly 22.4 GB.
Anything interesting in sys.column_store_segments?
Another great catalog view to explore with many interesting ways to slice and dice this data:
· All 26 columns had 120 segments associated with them. But what is interesting is that this is for 26 columns – even though my columnstore index was recreated with just 25 columns. So it was segmenting based on each column of the heap (seemingly). If you enable trace flag 646 and 3605, you’ll see during index creation a message like “Column 25 was omitted from VertiPaq during column index build.” So it made me wonder if this really did represent 120 segments for the omitted column?
· The average row count per segment was 1,030,792 rows and a maximum of 1,048,576.
· The minimum rows for a segment was 2,416.
· The 2,416 row count was for all columns on segment number 118. This was not the last segment. Segment 119 was the last segment (first segment is 0-based) for each column and each one of these had 1,040,352. Eric Hanson gave an explanation on a forum which talks about segments being built on parallel threads and when hitting the end of the table, not having enough rows to completely fill segments.
· As for encoding_type, that was also interesting. Some columns had both an encoding_type of 1 AND 2. Only the first five columns of the table had an encoding type of “1” and then columns 1 through 25 had an encoding type of “2”. Columns 21 and 22 (Freight and CarrierTrackingNumber) had an encoding of 3. Freight is money data type and CarrierTrackingNumber is nvarchar(25). Column 26 (ShipDate datetime) had an encoding type of “4”. In BOL – encoding_type is defined as “type of encoding used for that segment”. Sigh.
· The min_data_id and max_data_id value rangers were also interesting. Sometimes they reflected the actual column data ranges (for example in the date columns) and sometimes not. Looking at “Understanding Segment Elimination” – Eric Hanson describes this behavior – saying that the values in these ranges can also reference “into a dictionary”.
Anything interesting in sys.column_store_index_stats?
The only noteworthy value was the number_of_segments (3,120).
Can I see some segment elimination for my queries? (for non-string types on a heap table)
Remember that I created a columnstore index on a heap. Let’s say we look at the segment ranges for the ProductKey column (column id 1). Here are the associated ranges:
SELECT column_id, min_data_id, max_data_id, segment_id
FROM sys.column_store_segments
WHERE column_id = 1
ORDER BY column_id, min_data_id, max_data_id, segment_id;
All 120 segments have identical min and max ranges:
What about other columns – like the DueDate (datetime)?
Same thing applies – each segment, with the exception of a trailing row, have the same ranges.
What about segment elimination on a table with a clustered index?
So next I dropped the columnstore index, and created a clustered index on ProductKey. I’m not saying this is a great choice for the clustered index key – but rather, I’m just trying to understand the behavior in comparison to a heap and also look at any potential segment elimination.
In the following query, I’m pulling total order quantity for three products:
— Segment elimination written to error log
DBCC TRACEON(3605, –1);
GO
DBCC TRACEON(646, –1);
GO
SELECT ProductKey,
[DueDateKey],
SUM(OrderQuantity) Total_OrderQuantity
FROM [dbo].[FactInternetSales]
GROUP BY ProductKey, DueDateKey
HAVING ProductKey IN
(478, 343, 574);
I first validated that batch mode was indeed being used:
I then looked at the SQL Error Log (per the trace flag) – and I saw the storage engine skipped 28 “row group” (segments):
If I look at the segment meta data though for column_id 1, and I see blocks of segments covering the same ranges (instead of the same ranges across all segments):
SELECT segment_id, min_data_id, max_data_id
FROM sys.column_store_segments
WHERE column_id = 1
ORDER BY segment_id
By the way, the 28 segment elimination I saw in the error log for column 1 translated to 27,226,112 rows across (row_count from sys.column_store_segments). The query itself takes less than 1 second to return 569 rows – from a 120 million+ row table.
Will I get segment elimination on other columns not part of the clustered index key?
I tried the following query that filtered on DueDateKey:
SELECT ProductKey,
[DueDateKey],
SUM(OrderQuantity) Total_OrderQuantity
FROM [dbo].[FactInternetSales]
GROUP BY ProductKey, DueDateKey
HAVING DueDateKey = 20040522
Nope. While the query still ran in less than a second – using batch instead of row mode for the columnstore index scan, segment elimination event did not occur. Looking at the min and max ranges from sys.column_store_segments helps answer why segment elimination wasn’t possible:
SELECT column_id, min_data_id, max_data_id, segment_id
FROM sys.column_store_segments
WHERE column_id = 3 AND
20040522 BETWEEN min_data_id AND max_data_id
This returns all 120 rows:
What if the DueDateKey is a secondary key column on the clustered index?
I dropped the columnstore index and recreated the clustered index with ProductKey and DueDateKey. I then recreated the columnstore index.
Checking the DueDateKey range in sys.column_store_segments, I see the ranges are no longer identical:
That seemed promising, so I executed the query filtering by DueDateKey:
Sure enough – I had segment elimination, even though this was defined as the second column in the clustered index key. And what’s more, my ProductKey column segment elimination was still working (I tested the earlier queries again). This raises some interesting questions around new clustered index key strategies when you know your table will primarily be used with a columnstore index.
Okay – that’s enough for today. I still have many other scenarios I’d like to try out, and I’ll share here when I get the opportunity.
The post Exploring Columnstore Index Metadata, Segment Distribution and Elimination Behaviors appeared first on Joe Sack.
]]>The post Row and batch execution modes and columnstore indexes appeared first on Joe Sack.
]]>In my last post I hinted towards some interesting findings regarding parallelism and columnstore indexes. I’ll talk about what I observed in this post.
Just as a quick level-set, columnstore indexing provides two new areas of functionality within SQL Server 2012 that can potentially improve query performance for typical relational data warehouse workloads. The features contributing to this potential performance gain include the Vertipaq columnstore technology (which uses compression and stores the data by column instead of row) and the new method of query processing that processes blocks of column data in batches. You can benefit from from the columnstore storage alone – but you may also see further query performance improvements if the query runs in "batch" execution mode versus "row".
One key point to underscore– just because you add a columnstore index to a table doesn’t mean that your query will use “batch” execution mode.
Let’s walk through an example of row vs. batch using the AdventureWorksDWDenali database. The test server used in this illustration has four visible schedulers (not including the DAC) and 8GB of RAM.
Also – in order to increase the cost of the queries against this table and make it worth the columnstore index's time, I put together the below query to bump up the row count in FactInternetSales from 60,398 rows to 483,184 rows (SalesOrderNumber was part of the primary key, so I populated it with NEWID() and kept the other column values static from the existing rows and then used "GO 3" to populate it a few times):
INSERT dbo.FactInternetSales
(ProductKey, OrderDateKey, DueDateKey, ShipDateKey, CustomerKey, PromotionKey, CurrencyKey, SalesTerritoryKey, SalesOrderNumber, SalesOrderLineNumber, RevisionNumber, OrderQuantity, UnitPrice, ExtendedAmount, UnitPriceDiscountPct, DiscountAmount, ProductStandardCost, TotalProductCost, SalesAmount, TaxAmt, Freight, CarrierTrackingNumber, CustomerPONumber, OrderDate, DueDate, ShipDate)
SELECT ProductKey, OrderDateKey, DueDateKey, ShipDateKey, CustomerKey, PromotionKey, CurrencyKey, SalesTerritoryKey, LEFT(CAST(NEWID() AS NVARCHAR(36)),20), SalesOrderLineNumber, RevisionNumber, OrderQuantity, UnitPrice, ExtendedAmount, UnitPriceDiscountPct, DiscountAmount, ProductStandardCost, TotalProductCost, SalesAmount,
TaxAmt, Freight, CarrierTrackingNumber, CustomerPONumber, OrderDate, DueDate, ShipDate
FROM dbo.FactInternetSales
GO 3
The following query is executed against a table with no columnstore index (I’ve also enabled the actual Execution Plan to show afterwards – and also execute the query twice in order to measure results with the data in cache):
SET STATISTICS IO ON
SET STATISTICS TIME ONSELECT c.CommuteDistance,
d.CalendarYear,
SUM(f.SalesAmount) TotalSalesByCommuteDistance
FROM dbo.FactInternetSales as f
INNER JOIN dbo.DimCustomer as c ON
f.CustomerKey = c.CustomerKey
INNER JOIN dbo.DimDate d ON
d.DateKey = f.OrderDateKey
GROUP BY c.CommuteDistance,
d.CalendarYear
The results for STATISTICS IO were as follows:
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'FactInternetSales'. Scan count 1, logical reads 20289, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DimCustomer'. Scan count 1, logical reads 979, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DimDate'. Scan count 1, logical reads 66, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
And the results of the SET STATISTICS TIME ON were as follows:
SQL Server Execution Times:
CPU time = 562 ms, elapsed time = 625 ms.
As for the execution plan, if we hover over the Clustered Index Scan for FactInternetSales you’ll see the “Actual Execution Mode”.
So we see a value of “Row” (the execution mode we’re used to) and “Actual Number of Batches” of 0.
Now let’s create a columnstore index:
CREATE NONCLUSTERED COLUMNSTORE INDEX [CSI_FactInternetSales] ON [dbo].[FactInternetSales]
(
[OrderDateKey],
[CustomerKey],
[SalesAmount]
)
GO
Re-executing the query after creating the index, I see the following STATISTICS IO output:
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'FactInternetSales'. Scan count 1, logical reads 597, physical reads 0, read-ahead reads 1167, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DimCustomer'. Scan count 1, logical reads 979, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DimDate'. Scan count 1, logical reads 66, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
That’s a reduction in logical reads against FactInternetSales from 20289 down to 597. Very likeable.
What about STATISTICS TIME?
SQL Server Execution Times:
CPU time = 453 ms, elapsed time = 485 ms.
Okay so CPU time was 562 ms now it is 453 ms. Elapsed time was 625 ms and now it is 485 ms.
What about the execution plan, were we using batch execution mode?
No. Still using “Row” with a columnstore index scan. So things brings me to what I observed a few days ago.
One thing I didn’t mention is that in my test environment – my max degree of parallelism was set to “1”.
What happens if I uncap the max degree of parallelism for my 4-scheduler server and re-execute the query?
After lifting the cap on max degree of parallelism, the query executes using a parallel plan and also switches from “row” to “batch” mode. Notice also the “Actual Number of Batches” – which shows a value of 1,482.
This is all nice – but what about the query performance and I/O. Any further reductions?
The I/O results are as follows:
Table 'DimCustomer'. Scan count 5, logical reads 1072, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DimDate'. Scan count 3, logical reads 56, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'FactInternetSales'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
What’s this? Zero for all stats related to FactInternetSales? I even dropped clean buffers to double-check, and it still displayed all zeros. (More on this for another day.)
What about the execution statistics?
SQL Server Execution Times:
CPU time = 78 ms, elapsed time = 114 ms.
Now we're talking big improvements. CPU time was 562 ms without columnstore, then 453 ms with columnstore + row processing and then 78 ms for columnstore + batch processing. Elapsed time was 625 ms without columnstore and then 485 ms for columnstore + row processing and then 114 ms for columnstore + batch processing. So more non-trivial reductions.
So a few key findings in closing:
· Columnstore + row processing can provide a performance boost (getting column based storage and compression)
· Columnstore + batch processing can provide additional performance improvements on top of the columnstore index I/O benefits
· When I capped parallelism, I didn’t see batch processing – so check your execution plan and be aware that there are other factors that impact whether batch processing vs. row processing as well
The post Row and batch execution modes and columnstore indexes appeared first on Joe Sack.
]]>The post The case of the columnstore index and the memory grant appeared first on Joe Sack.
]]>This post covers examples from Denali CTP3, version 11.0.1440.
I was working with Denali’s columnstore index feature this last week and was testing it on a virtual machine when I encountered the following error message when trying to create a new index:
The statement has been terminated.
Msg 8657, Level 17, State 5, Line 2
Could not get the memory grant of 91152 KB because it exceeds the maximum configuration limit in workload group 'default' (2) and resource pool 'default' (2). Contact the server administrator to increase the memory usage limit.'
Now at the time the VM I was using was constrained for resources. It was configured to use 1 GB of RAM.
So I shut down the VM and added in 7 GB more of RAM. Given that the “max server memory (MB)” wasn’t capped, I was then able to create the columnstore index successfully. Regarding the memory requirements, BOL states that we need [approximately] 8 MBs times the # of columns in the index times the DOP. Also, the more string data type columns involved, the higher the memory needed for creating the index.
So I thought I would test out these requirements in a more controlled fashion and see how close the estimates were to the reality.
In this test, I used a VM with 4 processors and 8 GB of RAM, but unlike with my previous experience I capped the “max server memory (MB)” to a much lower value to reproduce the issue I had earlier. I was adding the columnstore index to the dbo.FactInternetSales table from the AdventureWorksDWDenali database (which can be downloaded here).
For the first step, I capped the max server memory for my test Denali SQL Server instance to a very low value (300 MBs did the trick in this case):
EXEC sp_configure 'show advanced options', 1
RECONFIGURE
EXEC sp_configure 'max server memory (MB)', 300
RECONFIGURE
Next I attempted to create the following columnstore index:
USE [AdventureWorksDWDenali]
GO
CREATE NONCLUSTERED COLUMNSTORE
INDEX [NCSI_FactInternetSales]
ON [dbo].[FactInternetSales]
(
[ProductKey],
[OrderDateKey],
[DueDateKey],
[ShipDateKey],
[CustomerKey],
[PromotionKey],
[CurrencyKey],
[SalesTerritoryKey],
[SalesOrderNumber],
[SalesOrderLineNumber],
[RevisionNumber],
[OrderQuantity],
[UnitPrice],
[ExtendedAmount],
[UnitPriceDiscountPct],
[DiscountAmount],
[ProductStandardCost],
[TotalProductCost],
[SalesAmount],
[TaxAmt],
[Freight],
[CarrierTrackingNumber],
[CustomerPONumber],
[OrderDate],
[DueDate],
[ShipDate])
GO
This failed with the following error:
The statement has been terminated.
Msg 8657, Level 17, State 5, Line 2
Could not get the memory grant of 143560 KB because it exceeds the maximum configuration limit in workload group 'default' (2) and resource pool 'default' (2). Contact the server administrator to increase the memory usage limit.
So the memory grant request was just over 140 MB. Looking at the requirements of the columnstore index request and taking into account the equation in BOL, we have 8 MBs x 25 columns x 4 available processors = 800 MB. Quite a bit higher than the 140 MB it needed at runtime. But this was assuming 4 processors in the equation. If we factor in just 1 proc being used – we have 200 MB which is closer to what was being requested and factors in the varying data types and sizes for columns defined in FactInternetSales.
What if I capped the DOP but keep the memory capped at the low value? I gave it a try:
EXEC sp_configure 'max degree of parallelism', 1
RECONFIGURE
Attempting a creation of the same index gave the following error message and same memory grant value:
The statement has been terminated.
Msg 8657, Level 17, State 5, Line 1
Could not get the memory grant of 143560 KB because it exceeds the maximum configuration limit in workload group 'default' (2) and resource pool 'default' (2). Contact the server administrator to increase the memory usage limit.
So in this case, capping the DOP didn’t reduce the memory requirements.
What about using the MAXDOP hint with the CREATE statement (WITH (MAXDOP = 1))? Again this returned the same memory grant requirement of 143560 KB.
What about capping the default workload group itself (not recommending this as standard practice – but rather to further explore memory grant requirements for columnstore)?
ALTER WORKLOAD GROUP [default]
WITH(max_dop=1)
GO
ALTER RESOURCE GOVERNOR RECONFIGURE
GO
And even after this change, the memory grant requirements remained the same. So I reverted the DOP options to get back to my previous state:
EXEC sp_configure 'max degree of parallelism', 0
RECONFIGURE
ALTER WORKLOAD GROUP [default]
WITH(max_dop=0)
GO
ALTER RESOURCE GOVERNOR RECONFIGURE
GO
Regarding the maximum memory grant request itself, the error message gives us enough of a hint on where to look (workload group 'default' (2) and resource pool 'default' (2)). So I ran the following query to return the request_max_memory_grant_percent value for the default workload group:
SELECT request_max_memory_grant_percent
FROM sys.resource_governor_workload_groups
WHERE name = 'default'
The value returned was 25%. So with my 300 MB cap, we’re talking 75 MB. Not the 140 MB we need.
I then bumped up the max memory grant percent (but left the max server memory at the low value) just to see what would happen:
ALTER WORKLOAD GROUP [default] WITH(request_max_memory_grant_percent=70)
GO
ALTER RESOURCE GOVERNOR RECONFIGURE
GO
Sure enough – my CREATE NONCLUSTERED COLUMNSTORE INDEX was allowed to execute – but while it executed, it didn't complete. Instead it ran for 2 minutes and 59 seconds before getting the following error message:
The statement has been terminated.
Msg 8645, Level 17, State 1, Line 1
A timeout occurred while waiting for memory resources to execute the query in resource pool 'default' (2). Rerun the query.
I tried creating the index a second time so that I could see what was going on in sys.dm_exec_query_memory_grants – but it executed immediately the second time around. So I set back the max memory grant to the default 25 % – dropped the index and tried to recreate and got the error again. I then set the max memory grant back to 70% and had the timeout again – but this time I was ready for it and I executed a query against sys.dm_exec_query_memory_grants:
SELECT scheduler_id, dop, requested_memory_kb, required_memory_kb, ideal_memory_kb
FROM sys.dm_exec_query_memory_grants
The results were as follows:
What jumped out at me was the DOP value of “1” (even though my SQL Server instance’s “max degree of parallelism” was set to “0” and I had 4 available prcs and the “default” pool dop was also set to 0).
Now it seems that the requested memory was remaining the same because the plan was assuming to be maxdop of “1” all along. Which made me wonder if the requested memory would increase if I added a hint for the creation of the columnstore index to more than one processor (I was reaching, I know, but I'm a fiddler by nature)? Before testing this, I set back the max memory grant percent to 25%:
ALTER WORKLOAD GROUP [default] WITH(request_max_memory_grant_percent=25)
GO
ALTER RESOURCE GOVERNOR RECONFIGURE
GO
Then I used WITH (MAXDOP = 4) for the query. The result? Still asking for a memory grant of 143560 KB. And repeating the test of bumping up the max memory grant and checking the dop value in sys.dm_exec_query_memory_grants – it remained at “1”.
So this post was more of an exploration and if I saw this case in the wild I would ask more directly about available memory for the SQL Server instance and/or increasing the max server memory if there was sufficient availability already.
I would also ask about the necessity of each column being included in the columnstore index definition. For example – if I needed only half of the columns from FactInternetSales, we’re talking about a 60MB memory grant requirement versus a 140 MB one.
During my work last week with columnstore indexes, there were also some interesting findings related to parallelism. I’ll save this for another post.
The post The case of the columnstore index and the memory grant appeared first on Joe Sack.
]]>