I’ve been asked questions about these two features a number of times in the last year where the specific concepts for them have been severely confused.  Since I have used the same information each time I’ve answered these questions I figured it would be worth blogging about here as a reference as well.

Event Notifications were introduced in SQL Server 2005 and offer the ability to collect a very specific subset of SQL Trace (http://msdn.microsoft.com/en-us/library/ms190655.aspx) and DDL Events (http://msdn.microsoft.com/en-us/library/bb522542.aspx) through a Service Broker service and queue.  The use of Service Broker allows you to write stored procedures that can process the events asynchronously when they are placed on the queue through a functionality of Service Broker known as queue activation.  You can also make use of remote servicing that allows the event data to be sent from one SQL Server to another for processing which is very useful in larger environments where you want to centralize all of the events for reporting.

Extended Events were introduced in SQL Server 2008 and use entirely different architecture inside of the database engine for providing events.  Extended Events contain a number of events that are not available by any other means in SQL Server, but there are a number of events in SQL Trace that are not available in Extended Events in SQL Server 2008; for example the blocked process report is not available by Extended Events.  In SQL Server 2012 RTM, all of the events from SQL Trace have been implemented in Extended Events, and Extended Events will replace SQL Trace entirely in a future release of SQL Server since SQL Trace is a newly deprecated feature in 2012 (deprecated = slated for removal in 2-3 product life cycles, it does not mean removed entirely). 

The key difference between the two features is that Event Notifications allow automated processing of the events asynchronously through Service Broker.  There is no similar mechanism for Extended Events currently; even in SQL Server 2012 where Event Notifications still piggy back SQL Trace for event generation.  If you are interested in building an alerting infrastructure I would still use Event Notifications depending on the information that you are interested in gathering.  To build an alerting solution on top of Extended Events would require consistently polling the event session targets and tracking which events have already been seen by the solution in SQL Server 2008, and in SQL Server 2012 it would require the same kind of polling solution unless you wrote a .NET application to leverage the streaming API for Extended Events to harvest the events in real time.  Depending on the event frequency and target being used either of these solutions could lead to missed alerts or an expensive polling operation that reads the same information repeatedly.

Either feature can safely be used in a highly transactional system if the appropriate considerations are made in the implementation, and either feature can lead to performance impacts as well.  It all depends on the events being collected and how frequently they get fired.  The events that are available in Event Notifications are a generally safe subset of the events from SQL Trace and generally won't cause performance problems, but your mileage may very based on the frequency of the events firing.  As always you should test the event selection and monitor for impact after implementing a change to the system.

I hope this helps anyone interested in these two features.

The sqlserver.sql_text action in Extended Events is one of the worst named objects in Extended Events.  The name suggests that you are going to get back the sql_text that triggered the event being collected to fire, but in reality this is not the case.  I pressed internally with Microsoft to have this action renamed to sqlserver.inputbuffer in SQL Server 2012, which actually reflects what is being returned by the action.  However, there were concerns about this breaking existing scripts (a point I didn’t necessarily buy into since other actions, targets, and even events were renamed in SQL Server 2012, but the timing of recommending this change may have had a lot to do with this as well).  I made this recommendation after seeing a post on the MSDN Forums where the poster didn’t agree with the way this action behaved in their environment.

To demonstrate this, let’s take a look at an Event Session in SQL Server 2012 that captures the sqlserver.sql_batch_completed and sqlserver.sql_statement_completed events along with the sqlserver.sql_text action for both of the events:

-- If the Event Session exists Drop it
IF EXISTS (SELECT 1
            FROM sys.server_event_sessions
            WHERE name = 'SQLskills_sql_text_Action')
    DROP EVENT SESSION [SQLskills_sql_text_Action] ON SERVER;

-- Create Event Session to find the database with most SP Recompile Events
CREATE EVENT SESSION [SQLskills_sql_text_Action]
ON SERVER
ADD EVENT sqlserver.sql_batch_completed(
    ACTION (sqlserver.sql_text)),
ADD EVENT sqlserver.sql_statement_completed(
    ACTION (sqlserver.sql_text))
ADD TARGET package0.ring_buffer
WITH (TRACK_CAUSALITY = ON);
GO

ALTER EVENT SESSION [SQLskills_sql_text_Action]
ON SERVER
STATE=START;
GO

With this event session created, we can then run a couple of different test scenarios in the environment to show how this action is not the sql_text, but is instead the input_buffer for the event that is being fired:

-- Perform some Tests
SELECT PASSWORD = 'bar12345!!';
SELECT 'password' as Secret;
CREATE LOGIN foo WITH PASSWORD = 'bar12345!!';
SELECT 'reallylongstringwithpasswordincludedintext' AS Funny;
EXEC('SELECT ''reallylongstringwithpasswordincludedintext'' AS Funny');
GO

-- Perform some additional Tests
SELECT PASSWORD = 'bar12345!!';
GO
SELECT 'password' as Secret;
GO
CREATE LOGIN foo WITH PASSWORD = 'bar12345!!';
GO
SELECT 'reallylongstringwithpasswordincludedintext' AS Funny;
GO
EXEC('SELECT ''reallylongstringwithpasswordincludedintext'' AS Funny');

Once these statements have been executed, we can then drop the events from the event session to allow the ring_buffer target to be queried to show the differences between the statement and batch_text columns from the events, which consequently don’t exist in SQL Server 2008 or SQL Server 2008 R2, and the sql_text action output.

ALTER EVENT SESSION [SQLskills_sql_text_Action]
ON SERVER
DROP EVENT sqlserver.sql_batch_completed,
DROP EVENT sqlserver.sql_statement_completed;
GO

With the events removed, we can now query the target data from the ring_buffer to look at what was actually captured.

-- Query the XML to get the Target Data
SELECT
    event.value('(event/@name)[1]', 'varchar(50)') AS event_name,
    DATEADD(hh,
            DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP),
            event.value('(event/@timestamp)[1]', 'datetime2')) AS [timestamp],
    ISNULL(event.value('(event/data[@name="statement"]/value)[1]', 'nvarchar(max)'),
            event.value('(event/data[@name="batch_text"]/value)[1]', 'nvarchar(max)')) AS [stmt/btch_txt],
    event.value('(event/action[@name="sql_text"]/value)[1]', 'nvarchar(max)') as [sql_text]
FROM
(   SELECT n.query('.') as event
    FROM
    (
        SELECT CAST(target_data AS XML) AS target_data
        FROM sys.dm_xe_sessions AS s   
        JOIN sys.dm_xe_session_targets AS t
            ON s.address = t.event_session_address
        WHERE s.name = 'SQLskills_sql_text_Action'
          AND t.target_name = 'ring_buffer'
    ) AS sub
    CROSS APPLY target_data.nodes('RingBufferTarget/event') AS q(n)
) AS tab

Looking at the output, we can see that there is a different behavior and output from the sql_text action between the two different sets of tests.

image

The first set of tests, which were run in a single batch, have all of the sql_text output masked because of the inclusion of the work password in the input buffer text.  This is not new behavior in SQL Server, it actually existed in SQL Trace prior to SQL Server 2005 SP2, where the code path used to capture TextData was changed to output the post query parsing text, which only strips the text out for DDL operations that would actually contain password information, like the CREATE LOGIN event in the second set of tests highlighted in the green box above.  The code path used by the sql_text action is the same as the raw input buffer for the session which masks off the entire text if the work password appears anywhere in the text, which is why the last SELECT statement and dynamic string execution of a SELECT don’t return sql_text information in the first set of tests highlighted in red above.  Contrast this with the batch_text and statement column outputs from the events, and you can easily see that sql_text isn’t quite what you’d expect.

So how do you work around/with this information?  If you are on SQL Server 2008, my typical recommendation is to capture the tsql_stack action instead, which gives you the sql_handle and offset information to be able to parse the statements from cache (a topic for another blog post that I’ll write).  If you need the actual statement information or RPC information from a specific execution, the best thing to do in SQL Server 2012 is to use TRACK_CAUSALITY and and the appropriate event, rpc_starting/sql_statement_starting/completed to be able to correlate the activity_id back to the statement that actually triggered the event.

With the growing popularity of Extended Events in SQL Server 2012 with the UI enhancements that I’ve blogged about on a number of posts (SQL Server 2012 Extended Events Update - 1- Introducing the SSMS User Interface, SQL Server 2012 Extended Events Update - 2 - The SSMS UI Part 2, SQL Server 2012 Extended Events Update - 3 - Viewing Target Data).  To make use of Extended Events and the performance benefits of collecting data from SQL Server using Extended Events instead of SQL Trace, I’ve written a comprehensive converter to migrate existing trace definitions from SQL Trace to Extended Events for SQL Server 2012 RTM.  In the past the Extended Events team at Microsoft has blogged about how to perform this conversion through the use of SQLCLR in their blog post Migrating SQL Trace to Extended Events, however, due to changes in the catalog views for mapping Trace Events to Extended Events, as well as a lack of a comprehensive mapping of SQL Trace columns to their respective Extended Events event columns and actions, this method falls short of being a solution to converting from SQL Trace to Extended Events.

I personally spent a number of hours manually mapping the SQL Trace Events column mappings to the related Extended Events event column mappings to build a cross reference table of events between the two environments and the result is a comprehensive set of scripts that can migrate SQL Trace definitions into Extended Events with full comments of what columns have been mapped as Extended Events columns or Actions, and the columns and events that no longer match up based on the Extended Events event definitions.

To this effort, below is a TSQL Script that will convert SQL Trace definitions to Extended Events within your own environment.  The output of this script for the default trace in SQL Server 2012 is as follows:

IF EXISTS (SELECT 1 FROM sys.server_event_sessions WHERE name = 'XE_Default_Trace')
 DROP EVENT SESSION [XE_Default_Trace] ON SERVER;
GO
CREATE EVENT SESSION [XE_Default_Trace]
ON SERVER
/* Audit Login Failed is not implemented in Extended Events it may be a Server Audit Event */
/* Audit Database Scope GDR Event is not implemented in Extended Events it may be a Server Audit Event */
/* Audit Schema Object GDR Event is not implemented in Extended Events it may be a Server Audit Event */
/* Audit Addlogin Event is not implemented in Extended Events it may be a Server Audit Event */
/* Audit Login GDR Event is not implemented in Extended Events it may be a Server Audit Event */
/* Audit Login Change Property Event is not implemented in Extended Events it may be a Server Audit Event */
/* Audit Add Login to Server Role Event is not implemented in Extended Events it may be a Server Audit Event */
/* Audit Add DB User Event is not implemented in Extended Events it may be a Server Audit Event */
/* Audit Add Member to DB Role Event is not implemented in Extended Events it may be a Server Audit Event */
/* Audit Add Role Event is not implemented in Extended Events it may be a Server Audit Event */
/* Audit Backup/Restore Event is not implemented in Extended Events it may be a Server Audit Event */
/* Audit DBCC Event is not implemented in Extended Events it may be a Server Audit Event */
/* Audit Change Audit Event is not implemented in Extended Events it may be a Server Audit Event */
/* Audit Change Database Owner is not implemented in Extended Events it may be a Server Audit Event */
/* Audit Schema Object Take Ownership Event is not implemented in Extended Events it may be a Server Audit Event */
/* Audit Server Alter Trace Event is not implemented in Extended Events it may be a Server Audit Event */
ADD EVENT sqlserver.database_file_size_change(
 ACTION
 (
     sqlserver.client_app_name -- ApplicationName from SQLTrace
   , sqlserver.client_hostname -- HostName from SQLTrace
   , sqlserver.client_pid -- ClientProcessID from SQLTrace
   , package0.event_sequence -- EventSequence from SQLTrace
   , sqlserver.is_system -- IsSystem from SQLTrace
   , sqlserver.nt_username -- NTDomainName from SQLTrace
   , sqlserver.server_instance_name -- ServerName from SQLTrace
   , sqlserver.server_principal_name -- LoginName from SQLTrace
   , sqlserver.server_principal_sid -- LoginSid from SQLTrace
   , sqlserver.session_id -- SPID from SQLTrace
   , sqlserver.session_server_principal_name -- SessionLoginName from SQLTrace
 )
),
/* Log File Auto Grow is implemented as the sqlserver.database_file_size_change event in Extended Events */
/* Data File Auto Shrink is implemented as the sqlserver.database_file_size_change event in Extended Events */
/* Log File Auto Shrink is implemented as the sqlserver.database_file_size_change event in Extended Events */
ADD EVENT sqlserver.database_mirroring_state_change(
 ACTION
 (
     package0.event_sequence -- EventSequence from SQLTrace
   , sqlserver.is_system -- IsSystem from SQLTrace
   , sqlserver.request_id -- RequestID from SQLTrace
   , sqlserver.server_instance_name -- ServerName from SQLTrace
   , sqlserver.server_principal_sid -- LoginSid from SQLTrace
   , sqlserver.session_id -- SPID from SQLTrace
   , sqlserver.session_server_principal_name -- SessionLoginName from SQLTrace
   , sqlserver.transaction_id -- TransactionID from SQLTrace
 )
),
ADD EVENT sqlserver.errorlog_written(
 ACTION
 (
     sqlserver.client_app_name -- ApplicationName from SQLTrace
   , sqlserver.client_hostname -- HostName from SQLTrace
   , sqlserver.client_pid -- ClientProcessID from SQLTrace
   , sqlserver.database_id -- DatabaseID from SQLTrace
   , sqlserver.database_name -- DatabaseName from SQLTrace
   , package0.event_sequence -- EventSequence from SQLTrace
   , sqlserver.is_system -- IsSystem from SQLTrace
   , sqlserver.nt_username -- NTUserName from SQLTrace
   , sqlserver.nt_username -- NTDomainName from SQLTrace
   , sqlserver.request_id -- RequestID from SQLTrace
   , sqlserver.server_instance_name -- ServerName from SQLTrace
   , sqlserver.server_principal_name -- LoginName from SQLTrace
   , sqlserver.server_principal_sid -- LoginSid from SQLTrace
   , sqlserver.session_id -- SPID from SQLTrace
   , sqlserver.session_server_principal_name -- SessionLoginName from SQLTrace
   , sqlserver.transaction_id -- TransactionID from SQLTrace
   -- Severity not implemented in XE for this event
   -- State not implemented in XE for this event
   -- Error not implemented in XE for this event
 )
),
ADD EVENT sqlserver.full_text_crawl_started(
 ACTION
 (
     package0.event_sequence -- EventSequence from SQLTrace
   , sqlserver.is_system -- IsSystem from SQLTrace
   , sqlserver.session_id -- SPID from SQLTrace
   , sqlserver.session_server_principal_name -- SessionLoginName from SQLTrace
   , sqlserver.transaction_id -- TransactionID from SQLTrace
   -- ServerName not implemented in XE for this event
 )
),
ADD EVENT sqlserver.full_text_crawl_stopped(
 ACTION
 (
     package0.event_sequence -- EventSequence from SQLTrace
   , sqlserver.is_system -- IsSystem from SQLTrace
   , sqlserver.session_id -- SPID from SQLTrace
   , sqlserver.session_server_principal_name -- SessionLoginName from SQLTrace
   , sqlserver.transaction_id -- TransactionID from SQLTrace
   -- ServerName not implemented in XE for this event
 )
),
ADD EVENT sqlserver.hash_warning(
 ACTION
 (
     sqlserver.client_app_name -- ApplicationName from SQLTrace
   , sqlserver.client_hostname -- HostName from SQLTrace
   , sqlserver.client_pid -- ClientProcessID from SQLTrace
   , sqlserver.database_id -- DatabaseID from SQLTrace
   , sqlserver.database_name -- DatabaseName from SQLTrace
   , package0.event_sequence -- EventSequence from SQLTrace
   , sqlserver.is_system -- IsSystem from SQLTrace
   , sqlserver.nt_username -- NTUserName from SQLTrace
   , sqlserver.nt_username -- NTDomainName from SQLTrace
   , sqlserver.request_id -- RequestID from SQLTrace
   , sqlserver.server_instance_name -- ServerName from SQLTrace
   , sqlserver.server_principal_name -- LoginName from SQLTrace
   , sqlserver.server_principal_sid -- LoginSid from SQLTrace
   , sqlserver.session_id -- SPID from SQLTrace
   , sqlserver.session_resource_group_id -- GroupID from SQLTrace
   , sqlserver.session_server_principal_name -- SessionLoginName from SQLTrace
   , sqlserver.transaction_id -- TransactionID from SQLTrace
   , sqlserver.transaction_sequence -- XactSequence from SQLTrace
 )
),
ADD EVENT sqlserver.missing_column_statistics(
 ACTION
 (
     sqlserver.client_app_name -- ApplicationName from SQLTrace
   , sqlserver.client_hostname -- HostName from SQLTrace
   , sqlserver.client_pid -- ClientProcessID from SQLTrace
   , sqlserver.database_id -- DatabaseID from SQLTrace
   , sqlserver.database_name -- DatabaseName from SQLTrace
   , package0.event_sequence -- EventSequence from SQLTrace
   , sqlserver.is_system -- IsSystem from SQLTrace
   , sqlserver.nt_username -- NTUserName from SQLTrace
   , sqlserver.nt_username -- NTDomainName from SQLTrace
   , sqlserver.request_id -- RequestID from SQLTrace
   , sqlserver.server_instance_name -- ServerName from SQLTrace
   , sqlserver.server_principal_name -- LoginName from SQLTrace
   , sqlserver.server_principal_sid -- LoginSid from SQLTrace
   , sqlserver.session_id -- SPID from SQLTrace
   , sqlserver.session_resource_group_id -- GroupID from SQLTrace
   , sqlserver.session_server_principal_name -- SessionLoginName from SQLTrace
   , sqlserver.transaction_id -- TransactionID from SQLTrace
   , sqlserver.transaction_sequence -- XactSequence from SQLTrace
 )
),
ADD EVENT sqlserver.missing_join_predicate(
 ACTION
 (
     sqlserver.client_app_name -- ApplicationName from SQLTrace
   , sqlserver.client_hostname -- HostName from SQLTrace
   , sqlserver.client_pid -- ClientProcessID from SQLTrace
   , sqlserver.database_id -- DatabaseID from SQLTrace
   , sqlserver.database_name -- DatabaseName from SQLTrace
   , package0.event_sequence -- EventSequence from SQLTrace
   , sqlserver.is_system -- IsSystem from SQLTrace
   , sqlserver.nt_username -- NTUserName from SQLTrace
   , sqlserver.nt_username -- NTDomainName from SQLTrace
   , sqlserver.request_id -- RequestID from SQLTrace
   , sqlserver.server_instance_name -- ServerName from SQLTrace
   , sqlserver.server_principal_name -- LoginName from SQLTrace
   , sqlserver.server_principal_sid -- LoginSid from SQLTrace
   , sqlserver.session_id -- SPID from SQLTrace
   , sqlserver.session_resource_group_id -- GroupID from SQLTrace
   , sqlserver.session_server_principal_name -- SessionLoginName from SQLTrace
   , sqlserver.transaction_id -- TransactionID from SQLTrace
   , sqlserver.transaction_sequence -- XactSequence from SQLTrace
 )
),
ADD EVENT sqlserver.object_altered(
 ACTION
 (
     package0.attach_activity_id -- IntegerData from SQLTrace
   , sqlserver.client_app_name -- ApplicationName from SQLTrace
   , sqlserver.client_hostname -- HostName from SQLTrace
   , sqlserver.client_pid -- ClientProcessID from SQLTrace
   , package0.event_sequence -- EventSequence from SQLTrace
   , sqlserver.is_system -- IsSystem from SQLTrace
   , sqlserver.nt_username -- NTUserName from SQLTrace
   , sqlserver.nt_username -- NTDomainName from SQLTrace
   , sqlserver.request_id -- RequestID from SQLTrace
   , sqlserver.server_instance_name -- ServerName from SQLTrace
   , sqlserver.server_principal_name -- LoginName from SQLTrace
   , sqlserver.server_principal_sid -- LoginSid from SQLTrace
   , sqlserver.session_id -- SPID from SQLTrace
   , sqlserver.session_resource_group_id -- GroupID from SQLTrace
   , sqlserver.session_server_principal_name -- SessionLoginName from SQLTrace
   , sqlserver.transaction_id -- TransactionID from SQLTrace
   , sqlserver.transaction_sequence -- XactSequence from SQLTrace
   -- BigintData1 not implemented in XE for this event
 )
),
ADD EVENT sqlserver.object_created(
 ACTION
 (
     package0.attach_activity_id -- IntegerData from SQLTrace
   , sqlserver.client_app_name -- ApplicationName from SQLTrace
   , sqlserver.client_hostname -- HostName from SQLTrace
   , sqlserver.client_pid -- ClientProcessID from SQLTrace
   , package0.event_sequence -- EventSequence from SQLTrace
   , sqlserver.is_system -- IsSystem from SQLTrace
   , sqlserver.nt_username -- NTUserName from SQLTrace
   , sqlserver.nt_username -- NTDomainName from SQLTrace
   , sqlserver.request_id -- RequestID from SQLTrace
   , sqlserver.server_instance_name -- ServerName from SQLTrace
   , sqlserver.server_principal_name -- LoginName from SQLTrace
   , sqlserver.server_principal_sid -- LoginSid from SQLTrace
   , sqlserver.session_id -- SPID from SQLTrace
   , sqlserver.session_resource_group_id -- GroupID from SQLTrace
   , sqlserver.session_server_principal_name -- SessionLoginName from SQLTrace
   , sqlserver.transaction_id -- TransactionID from SQLTrace
   , sqlserver.transaction_sequence -- XactSequence from SQLTrace
   -- BigintData1 not implemented in XE for this event
 )
),
ADD EVENT sqlserver.object_deleted(
 ACTION
 (
     package0.attach_activity_id -- IntegerData from SQLTrace
   , sqlserver.client_app_name -- ApplicationName from SQLTrace
   , sqlserver.client_hostname -- HostName from SQLTrace
   , sqlserver.client_pid -- ClientProcessID from SQLTrace
   , package0.event_sequence -- EventSequence from SQLTrace
   , sqlserver.is_system -- IsSystem from SQLTrace
   , sqlserver.nt_username -- NTUserName from SQLTrace
   , sqlserver.nt_username -- NTDomainName from SQLTrace
   , sqlserver.request_id -- RequestID from SQLTrace
   , sqlserver.server_instance_name -- ServerName from SQLTrace
   , sqlserver.server_principal_name -- LoginName from SQLTrace
   , sqlserver.server_principal_sid -- LoginSid from SQLTrace
   , sqlserver.session_id -- SPID from SQLTrace
   , sqlserver.session_resource_group_id -- GroupID from SQLTrace
   , sqlserver.session_server_principal_name -- SessionLoginName from SQLTrace
   , sqlserver.transaction_id -- TransactionID from SQLTrace
   , sqlserver.transaction_sequence -- XactSequence from SQLTrace
   -- BigintData1 not implemented in XE for this event
 )
),
ADD EVENT sqlserver.plan_guide_unsuccessful(
 ACTION
 (
     sqlserver.client_app_name -- ApplicationName from SQLTrace
   , sqlserver.client_hostname -- HostName from SQLTrace
   , sqlserver.client_pid -- ClientProcessID from SQLTrace
   , sqlserver.database_id -- DatabaseID from SQLTrace
   , sqlserver.database_name -- DatabaseName from SQLTrace
   , package0.event_sequence -- EventSequence from SQLTrace
   , sqlserver.is_system -- IsSystem from SQLTrace
   , sqlserver.nt_username -- NTUserName from SQLTrace
   , sqlserver.nt_username -- NTDomainName from SQLTrace
   , sqlserver.request_id -- RequestID from SQLTrace
   , sqlserver.server_instance_name -- ServerName from SQLTrace
   , sqlserver.server_principal_name -- LoginName from SQLTrace
   , sqlserver.server_principal_sid -- LoginSid from SQLTrace
   , sqlserver.session_id -- SPID from SQLTrace
   , sqlserver.session_server_principal_name -- SessionLoginName from SQLTrace
   , sqlserver.transaction_id -- TransactionID from SQLTrace
   , sqlserver.transaction_sequence -- XactSequence from SQLTrace
   -- TextData not implemented in XE for this event
 )
),
ADD EVENT sqlserver.server_memory_change(
 ACTION
 (
     package0.event_sequence -- EventSequence from SQLTrace
   , sqlserver.is_system -- IsSystem from SQLTrace
   , sqlserver.request_id -- RequestID from SQLTrace
   , sqlserver.server_instance_name -- ServerName from SQLTrace
   , sqlserver.session_id -- SPID from SQLTrace
   , sqlserver.session_server_principal_name -- SessionLoginName from SQLTrace
   , sqlserver.transaction_id -- TransactionID from SQLTrace
   , sqlserver.transaction_sequence -- XactSequence from SQLTrace
 )
),
ADD EVENT sqlserver.server_start_stop(
 ACTION
 (
     sqlserver.client_app_name -- ApplicationName from SQLTrace
   , sqlserver.client_hostname -- HostName from SQLTrace
   , sqlserver.client_pid -- ClientProcessID from SQLTrace
   , package0.event_sequence -- EventSequence from SQLTrace
   , sqlserver.is_system -- IsSystem from SQLTrace
   , sqlserver.nt_username -- NTUserName from SQLTrace
   , sqlserver.nt_username -- NTDomainName from SQLTrace
   , sqlserver.request_id -- RequestID from SQLTrace
   , sqlserver.server_instance_name -- ServerName from SQLTrace
   , sqlserver.server_principal_name -- LoginName from SQLTrace
   , sqlserver.server_principal_sid -- LoginSid from SQLTrace
   , sqlserver.session_id -- SPID from SQLTrace
   , sqlserver.session_server_principal_name -- SessionLoginName from SQLTrace
 )
),
ADD EVENT sqlserver.sort_warning(
 ACTION
 (
     sqlserver.client_app_name -- ApplicationName from SQLTrace
   , sqlserver.client_hostname -- HostName from SQLTrace
   , sqlserver.client_pid -- ClientProcessID from SQLTrace
   , sqlserver.database_id -- DatabaseID from SQLTrace
   , sqlserver.database_name -- DatabaseName from SQLTrace
   , package0.event_sequence -- EventSequence from SQLTrace
   , sqlserver.is_system -- IsSystem from SQLTrace
   , sqlserver.nt_username -- NTUserName from SQLTrace
   , sqlserver.nt_username -- NTDomainName from SQLTrace
   , sqlserver.request_id -- RequestID from SQLTrace
   , sqlserver.server_instance_name -- ServerName from SQLTrace
   , sqlserver.server_principal_name -- LoginName from SQLTrace
   , sqlserver.server_principal_sid -- LoginSid from SQLTrace
   , sqlserver.session_id -- SPID from SQLTrace
   , sqlserver.session_resource_group_id -- GroupID from SQLTrace
   , sqlserver.session_server_principal_name -- SessionLoginName from SQLTrace
   , sqlserver.transaction_id -- TransactionID from SQLTrace
   , sqlserver.transaction_sequence -- XactSequence from SQLTrace
 )
)
ADD TARGET package0.event_file
(
 SET filename = 'C:\Program Files\Microsoft SQL Server\MSSQL11.MSSQLSERVER\MSSQL\Log\XE_Default_Trace.xel',
  max_file_size = 20,
  max_rollover_files = 5
)

sp_SQLskills_ConvertTraceToExtendedEvents.sql (55.41 kb)

This past week at the SQL Server Connections Conference in Las Vegas, I was asked about the permissions required for managing Extended Event Sessions in SQL Server.  In SQL Server 2008 and 2008R2, using Extended Events required the CONTROL SERVER permission for the instance of SQL Server (http://msdn.microsoft.com/en-us/library/bb677289(v=sql.105).aspx).  In SQL Server 2012, a much more granular permission is required, ALTER ANY EVENT SESSION (http://msdn.microsoft.com/en-us/library/bb677289.aspx) which reduces the level of access that you have to provide to end users that need to use an Extended Event Session.  What is even better is that you can attach this permission set to a Custom Server Role (http://msdn.microsoft.com/en-us/library/ee677610(v=sql.110).aspx), to apply it to end users in a much easier to implement method than having to manually add the ALTER ANY EVENT SESSION to the login when it is created.

In SQL Server 2012 there are number of new ways to view target data generated by Extended Events Sessions, including a live streaming view as the events actually generate from the server, similar to the way SQL Server Profiler functions.  For this blog post, I am going to use the Query Detail Tracking template that ships by default with SQL Server 2012, and instead of using the default ring_buffer target, make use of the event_file target to show how to use the UI functionality to read and process through files that are generated by Extended Events.

image

image

The Event Session has been setup with a 10MB maximum file size and 5 rollover files.  The Event Session is also configured to start automatically when the Wizard closes, and the option to Watch live data on screen as it is captured is also selected.  When you close the dialog, the Live Data viewer will open and connect to the SQL Server instance to begin reading the event stream from the server.

Live Data viewer options

There are number of commands that exist for the Live Data viewer that are accessible through the Extended Events menu or through the toolbar that displays when the Live Data viewer is the active window inside of SQL Server Management Studio.  The buttons on the toolbar are in the exact same order as the menu items in the Extended Events menu.

image

image

If you don't have enough screen real estate for the toolbar to display completely, some of the buttons may not display but they will still be available through the drop down at the end of the toolbar as shown below.

image

The default view for any new Event Session in the Live Data viewer only shows two columns in the gridview, the event name and the timestamp for when the event was generated in the server.  The reason for this limited view initially is that there are to many columns available in Extended Events, and it is impossible to provide a globally useful initial display in the environment.  Instead for each event, all of the columns are provided in the Details pane below the main gridview.

image

Choosing Columns

Columns can be added to the table view a number of ways.  The Choose Columns menu item in the Extended Events menu or on the toolbar can be used, or you can also click on the column names on the gridview and select the Choose Columns menu item from the context menu.

image

Additionally you can add a single column from the Details pane by right clicking on the column and selecting the Show Column in Table menu item from the context menu.

image

The Choose Columns menu items will open up a column chooser dialog that allows you to add one or multiple columns to the gridview.  You can also change the column order in the gridview by selecting a column and clicking the up and down arrows to position the column appropriately in the list.

image

One functionality that you only get inside of the column chooser, is the ability to create a Merged Column for display.  This allows you to take columns with different names but similar meaning, for example the statement column produced by the sql_statement_completed event and the batch_text column produced by the sql_batch_completed event, and display them in a single column to maximize the available real estate on the screen and simplify analyzing the data.

image

After creating the merged column, and adding a number of additional columns to the gridview, we now have a much more recognizable view of the data being generated by our Event Session.

image

Saving the display for future use

If we close out the Live Data Viewer, Management Studio will remember the layout that we last used with this Event Session the next time we open it against this server.  However, if this Event Session is something that we are going to regularly use across multiple servers in our environment, or if we have multiple session definitions that leverage similar sets of events with different parameters configured the gridview layout won't default back to our settings in every scenario.  For this reason, you can save the display settings so that you don't have to customize the UI repeatedly.  This can be accomplished from the Extended Events menu or toolbars Display Settings item.

image

Filtering Live Data

While the Event Session is running, the UI can be used to filter the event data in the grid view on the client side, allowing you to limit the amount of information that is currently being displayed without actually having to change what is being collected by the Event Session in the targets.

image

These are just some of the more basic features of using the Live Data view inside of SQL Server 2012 for Extended Events.  In the next post we'll take a look at some of the additional features that exist for working with data stored in files or when the Live Data viewer is in a disconnected state.

SQL Server 2008 R2 Service Pack 1 provides a new set of Events in Extended Events to collect performance counter data from the Windows OS that would be really useful to monitoring SQL Server.  The first place I can find that they were mentioned is on a blog post by Mark Weber, a PFE for SQL and SAP at Microsoft.  However, a few weeks ago a question was asked about these counters one of the forums and the question was around how to use them.  I looked at the Events and found out that they aren’t really useable in their current implementation, something that is disappointing since being able to collect the data provided by these Events would really benefit most DBA’s out there. 

If you look at the Events and columns, these events collect information about the Logical Disk, Processor, Process for the SQL instance, and System at 15 second intervals and makes the data available through Extended Events.

SELECT name, description
FROM  sys.dm_xe_objects
WHERE name like 'perfobject_%'

name

description

perfobject_process

Returns a set of counters associated with the Process performance object. The event occurs once every 15 seconds for both the SQL Server and SQL Agent processes.

perfobject_system

Returns a set of counters associated with the System performance object. The event occurs once every 15 seconds.

perfobject_logicaldisk

Returns a set of counters associated with the Logical Disk performance object. The event occurs once every 15 seconds for each hard or fixed disk drive.

perfobject_processor

Returns a set of counters associated with the Processor performance object. The event occurs once every 15 seconds for each processor in the system.

If we look at the columns, we’ll see that the columns actually represent the individual counters under the categories exposed by the Event names. For example, the perfobject_logicaldisk event returns the following columns:

SELECT
    object_name, 
    name AS column_name, 
    description
FROM  sys.dm_xe_object_columns
WHERE object_name = 'perfobject_logicaldisk'
  AND column_type = 'data'
ORDER BY object_name, name

object_name

column_name

description

perfobject_logicaldisk

average_disk_bytes_per_read

Shows the average number of bytes transferred from the disk during read operations.

perfobject_logicaldisk

average_disk_bytes_per_transfer

Shows the average number of bytes transferred to or from the disk during write or read operations.

perfobject_logicaldisk

average_disk_bytes_per_write

Shows the average number of bytes transferred to the disk during write operations.

perfobject_logicaldisk

average_disk_queue_length

Shows the average number of both read and write requests that were queued for the selected disk during the sample interval.

perfobject_logicaldisk

average_disk_read_queue_length

Shows the average number of read requests that were queued for the selected disk during the sample interval.

perfobject_logicaldisk

average_disk_seconds_per_read

Shows the average time, in seconds, of a read operation from the disk.

perfobject_logicaldisk

average_disk_seconds_per_transfer

Shows the time, in seconds, of the average disk transfer.

perfobject_logicaldisk

average_disk_seconds_per_write

Shows the average time, in seconds, of a write operation to the disk.

perfobject_logicaldisk

average_disk_write_queue_length

Shows the average number of write requests that were queued for the selected disk during the sample interval.

perfobject_logicaldisk

current_disk_queue_length

Shows the number of requests outstanding on the disk at the time that the performance data is collected.

perfobject_logicaldisk

disk_bytes_per_second

Shows the rate at which bytes are transferred to or from the disk during write or read operations.

perfobject_logicaldisk

disk_read_bytes_per_second

Shows the rate at which bytes are transferred from the disk during read operations.

perfobject_logicaldisk

disk_reads_per_second

Shows the rate at which read operations are performed on the disk.

perfobject_logicaldisk

disk_transfers_per_second

Shows the rate at which read and write operations are performed on the disk.

perfobject_logicaldisk

disk_write_bytes_per_second

Shows the rate at which bytes are transferred to the disk during write operations.

perfobject_logicaldisk

disk_writes_per_second

Shows the rate at which write operations are performed on the disk.

perfobject_logicaldisk

free_megabytes

Shows the unallocated space, in megabytes, on the disk drive. One megabyte is equal to 1,048,576 bytes.

perfobject_logicaldisk

instance_name

The logical disk drive name

perfobject_logicaldisk

percent_disk_read_time

Shows the percentage of time that the selected disk drive is busy servicing read or write requests.

perfobject_logicaldisk

percent_disk_time

Shows the percentage of time that the selected disk drive is busy servicing read requests.

perfobject_logicaldisk

percent_disk_write_time

Shows the percentage of time that the selected disk drive is busy servicing write requests.

perfobject_logicaldisk

percent_free_space

Shows the percentage of the total usable space on the selected logical disk drive that is free.

perfobject_logicaldisk

percent_idle_time

The percentage of time during the sample interval that the disk was idle.

perfobject_logicaldisk

split_io_per_second

The rate at which I/Os to the disk were split into multiple I/Os.

This all seems good, until we actually use the Events in an Event Session and take a look at the data being returned.

CREATE EVENT SESSION [XE_PerfCounters] 
ON SERVER 
ADD EVENT sqlserver.perfobject_logicaldisk 
ADD TARGET package0.ring_buffer;
GO

image

Unfortunately, the counters are returning Raw values for the Event and the necessary Base counters that are required to give these values any useful meaning have been left out of the Events data.  Looking at this in my test environment, it appears the counter values pulled for the perfobject_ Events are pulled directly from Win32_PerfRawData_PerfDisk_LogicalDisk, but if you look at the CookingType requirements for the counters in Win32_PerfFormattedData_PerfDisk_LogicalDisk the raw values have to be calculated by their base values for them to have meaning:

image

I’ve submitted Connect Item 725167 for this and I really hope that this one gets fixed in a future Cumulative Update or Service Pack.

In the first post in this series, SQL Server 2012 Extended Events Update - 1- Introducing the SSMS User Interface, we looked at how to use the New Session Wizard in SQL Server 2012 to define an Event Session.  In this post we’ll compare the Wizard to the standard New Event Session dialog that can also be used for creating and Event Session in SQL Server 2012.  The New Event Session dialog is the same dialog that is used for editing an existing Event Session on the server, and can be accessed from the Extended Events node in Object Explorer, just like the New Session Wizard can be.

image

Rather than opening up with an Introduction Page, the New Session dialog opens directly allowing you to begin configuring your session.  I like to think of the New Session dialog as the power user method of creating a new session, and as we’ll see in this post, it can actually takes less steps to configure an Event Session using the New Session dialog over the New Session Wizard.

image

The General page of the New Session dialog allows you to specify a name for the Event Session, as well as to select a template to create the Event Session from.  You also have the option to specify whether the Event Session should start automatically when SQL Server starts, whether to start the Event Session immediately after creating it with the dialog, and whether you want to immediate begin viewing the Event Session data in the Live Viewer.  If you compare this page to the first three, and final pages of the wizard you will see that we have a much more concise configuration using the dialog so far.  If you click on the Events page on the left hand side, the Events page will show in the dialog allowing you to customize the events being collected by the Event Session.

image

On first look, the Events page looks very similar to the Select Events To Capture page from the Wizard.  The Event search functionality, and the ability to filter the events by Category and Channel, as well as how you add and remove Events from the Event Session is identical.  However, in the New Session dialog, two additional buttons exist, a Configure button, (circled in red) that allows you to begin configuring the Events that have been added to the Event Session, and a Select button, (circled in green) that allows you to return to the Event selection screen from the configuration screen.  When the buttons are clicked, the screen will collapse/expand left and right.

image

The biggest difference between the functionality provided by the New Session Wizard versus the New Session dialog is the level of granularity that you have with assigning Actions or Global Fields, and Predicates or filtering, to the Events that have been added to the Event Session.  In the New Session Wizard, any Action or Predicate that is added to the Event Session, is added across the board to all of the Events in the session.  The same functionality can be achieved using the New Session dialog by using the multi-select functionality of the UI to select all of the Events, and then adding the appropriate Actions and Predicates.  However, typically we don’t actually need the Actions and Filters applied to every Event in the Event Session, and since Actions and global Predicates incur an overhead for data collection, even though it is incredibly small, as a performance best practice.  By selecting a single Event, new Actions can be added to the the Event, or as shown below, filtering can be applied at the individual Event level, which allows the filtering definition to be against any column on the Event, not just the shared subset of columns, or global files, across all Events.

image

If multiple events share the same columns, for example, the sqlserver.sql_batch_completed and sqlserver.sql_statement_completed Events in our session, you can also multi-select those Events and define filtering specific to both of those Events.

image

Complex Predicates can be defined through the use of the right-click context menu in the Filter table.  Keep in mind that Predicates in Extended Events allow short circuiting logic to occur, so the order of Predicates matters during evaluation time.  The context menu will allow you to insert a new Predicate above or below the currently selected Predicate in the UI, add or delete additional clauses to the existing Predicate list, group subsets of clauses together so that they evaluate as a complete set, ungroup previously grouped sets of clauses, and to toggle the Not operator which evaluates for the negation of the clause being configured.

image

A really good example of a complex predicate configuration can be seen by looking at the system_health Event Session and the Predicate on the sqlserver.error_reported event. 

image

The Event Fields tab, will allow you to turn on/off the collection of any customizable columns for the Event that is currently selected.  For example, the sql_batch_completed Event has a customizable column for the batch_text, which is turned on by default.  If you don’t need the batch text, for example, you may be collecting the tsql_frame action which is much smaller because you know that you will be able to get the batch information from the cache at the point you are analyzing the results, you can turn it off by unchecking the checkbox next to it.

image

The Data Storage page of the dialog, allows you to configure the targets for the Event Session.  The biggest difference here is that you get full use of all of the targets available in Extended Events, not just the event_file and ring_buffer targets provided by the Wizard, though these will typically be the targets that you will use the most.

image

The Advanced page, allows you to customize the Event Session Options to define how the session will be setup in the Extended Events Engine when it starts.

image

Once all of the configuration for an Event Session has been completed, you can script the Event Session DDL using the standard SSMS Script button at the top of the UI, or you can create the Event Session immediately by clicking OK.  If you need to change the Event Session definition after creating it, the Session Properties dialog can be opened from the right-click context menu in Object Explorer for the session.  The Session Properties dialog is exactly the same as the New Session dialog.

In the next post we’ll look at the target data viewer in SLQ Server 2012 and how to use it for analyzing the captured Events from an Event Session.

For the most part I have been relatively quiet about the coming changes in SQL Server 2012 with regards to Extended Events.  Primarily this has been to allow the new features of the product to become fully baked to ensure that the information would continue to be applicable as the product lifecycle progressed, and there have been a number of major changes that have made this decision a really good one.  With SQL Server 2012 in it’s RC0 phase, and based on the responses I have seen to a number of feedback items for bugs on Microsoft SQL Server Connect, like the recent one for the template issue I blogged about on my blog post, Workaround for Bug in Activity Tracking Event Session Template in 2012 RC0, I’ve decided to go ahead and start a new series of posts that outline the new features of Extended Events in SQL Server 2012.  I can’t think of a better way to start off a series on the new features in SQL Server 2012 for Extended Events than the new SQL Server Management Studio User Interface for Extended Events in a couple of blog posts.

For this initial post, we’ll take a look at some of the features that replace existing SQL Profiler functionality that most DBA’s tend to use in their day to day operations.  To start off this topic, the first thing you need to know is that there is a new node for Extended Events under the Management folder in Object Explorer for SQL Server 2012.

image

If you happen to be connecting to a SQL Server 2008 server using SSMS from SQL Server 2012, this node will not exist.  This is due to the fact that UI for SSMS in SQL Server 2012 are not backwards compatible with SQL Server 2008, even though Extended Events exist in SQL Server 2008.  At some point when SQL Server 2012 actually releases to manufacturing (RTM), I will release an update to my SSMS Addin for Extended Events that back ports compatibility for SQL Server 2008 to Management Studio 2012, allowing full integration between SSMS 2012 and SQL Server 2008.

Within the scope of SQL Server 2012, the UI provides a lot of new functionality that should simplify the implementation and usage of Extended Events for most DBA’s.  One of the best enhancements is the ability to create an event session using the New Session Wizard to define an event session based with the least number of steps possible, possibly using an template for the event session, or manually defining a custom configuration that is applied to all of the events configured for the session.

image

By opening the New Session Wizard, immediately a Introductory page is presented that can be bypassed by selecting the option to Do not show this page again:

image

Since this page will typically slow down the creation of an event session for use, I would typically check this option before clicking on Next. The Set Session Properties page will allow you to specify a name for the session as well as whether or not the event session will startup automatically when SQL Server starts.  This can be very useful for troubleshooting an infrequent problem that does not predictably occur and you need to ensure that the session data is collected whenever the problem next occurs.  Some examples of where this might be applied will be shown in future posts in this series.

image

The New Session Wizard provides the ability to create the new event session based on a previously created template, or one of the templates provided by default with SQL Server 2012.  Keep in mind that in the RC0 build, and unfortunately the RTM release of SQL Server 2012, the Activity Tracking template has a bug in the XML definition that will cause this UI to error out.  This was documented by Mike Wachal, the PM for Extended Events at Microsoft on his blog post Activity Tracking event session template is broken, but a fix for the problem in the XML is available in my blog post Workaround for Bug in Activity Tracking Event Session Template in 2012 RC0.  After replacing the broken template with the one attached to my previous blog post we can select it in the UI.

image

The alternative to using an existing template is to create a blank event session by selecting the option to Do not use a template.  If a template is selected, once you click Next the events from the template will be displayed in the Select Events To Capture page, which also displays the events available in Extended Events and information about the data returned by the events.  The event library can be searched a number of ways to simplify finding the correct events for the session.  The most common way to search, once you start learning the events that are available, would be to start typing the event name in the textbox (green circle below) and the results will dynamically begin filtering out the events that don’t match the search text.  If you click on a specific event in the table, the event description and information about the columns returned by the event will be displayed (purple box below).

image

However, if you don’t know the specific events that you want, but you know the general category that the events apply to you can make use of other search options in the UI as well.  Events in Extended Events are broken down by two attributes, a Category (Keyword in the DMVs) and a Channel, that make them compatible with Event Tracing for Windows (ETW).  The Category is similar to the trace category that the existing Trace events have and can be used for logically grouping events similar to the way SQL Profiler groups events in previous versions of SQL Server.  The Channel aligns with the channels you would see with ETW.  By default one of the Channels is excluded in the UI, the Debug Channel.  Debug events are focused towards internal debugging tasks and are not of general purpose use by most DBAs.  These events tend to be counting in nature, or can fire incredibly frequently. If you want to see the Debug events in the UI, you can click the drop down and check the checkbox for the Debug Channel and they will be available.

image

Additionally you can filter out specific categories by clicking the Category dropdown and unchecking specific Category names from the selection.  To add an Event to the session, you can double click on the event, or you can select multiple events using Ctrl or Shift + click on the event names and then clicking the arrow that points to the right.  Alternately, you can also remove events using a double click or by highlighting the event and clicking the arrow that points to the left.

image

After adding events to the session, the next page allows you to specify the Global Fields, known as actions in Extended Events, that you want added to each of the events in the session.  If you look at the columns being returned by the events in Extended Events, there are significantly fewer data elements being returned at the individual event level.  Many of the trace columns map to the Global Fields (actions) in Extended Events and can be added as needed to the events.  The goal was to minimize the size of the firing events allowing additional information to be added as needed to maximize the performance of Extended Events firing.

image

After selecting the global fields to add to the Event Session and clicking Next, the Set Session Event Filters page is displayed where you can define filtering (known as predicates) on the events in the session.  Any filters that were configured as a part of the template will be displayed in the upper table, while new filters that will be applied to all of the events in the session can be added to the bottom table.  New filters can only be created on the columns that are available for all of the events in the event session, which typically there won’t be any if using multiple events, or on the global fields available to Extended Events.

image

This is a very restrictive functionality of the New Session Wizard that was put in place to provide a parity for session creation to what most users would expect from SQL Server Profiler.  I’ll show more about how this is not the best thing when we look at the other parts of the new UI in SSMS in other posts.  Once the filters have been created, the next step is to define the event storage.  The New Session Wizard restricts you to the event_file and ring_buffer targets, which are going to be the most commonly used targets by most DBAs since they retain the full event data and do not apply additional processing to the events.  For an event session that is going to be collecting data for a long period of time, or data that generates at a fast rate, the event_file target should be used, and similar to SQL Trace you can setup the maximum file size and whether or not file rollover should occur.  If the event session is going to be collecting data for a short period of time or where the event predicates will restrict the session to only collecting a small amount of data, the ring_buffer target will generally be a good choice.

image

Once the data storage has been configured the session can be created by clicking Finish, or you can click Next to get to the Summary page which will allow you to review all the configured options for the Event Session and Script the session definition for further changes to the DDL if necessary.

image

Once the Event Session is created, the last page provides you the opportunity to start the event session and to open the Live Data Viewer for the event session which is similar to the SQL Server Profiler view from SQL Trace.

image

In the next blog post I’ll show the New Session dialog which is not a wizard based implementation and why it provides a much more robust method of creating an event session in SQL Server 2012.

In a word; YES! In a lot more words, not always in the way that we want it to, but there are plenty of cases where it actually works and changes are made to the product as a result.

Now with that said, it doesn’t work all the time, and it helps to realize that what is important to us as an individual user might not be important to the product as a whole.  Before you post comments, I am sure that there are plenty of cases out there where people can say that Microsoft Connect for SQL Server is broken.  I have personally been irritated to the point of posting negative comments on Twitter about the whole Connect process.  I feel that it is about time that I show the other side of the story as well and talk about some Connect successes that have occurred in the past year, and of course what better topic to do this with than Extended Events.  Over the next few weeks, I’ll post a couple of different examples of Connect actually working and bringing about changes to the product that are beneficial to the community, starting with this post.

Extended Events does not track insert statements

This Connect item is actually incorrectly titled and is based on some confusion about what the sqlserver.plan_handle action actually returns when executed in the engine.  I blogged about this with much more detail last year in my blog post; What plan_handle is Extended Events sqlserver.plan_handle action returning?

If we revisit the Connect item there is a note that the sql_statement_completed event in SQL Server 2012 now includes a parameterized_plan_handle customizable column that can be used to retrieve the parameterized plan handle for statements that are auto-parameterized by SQL Server during their execution.  Taking the same original demo code from my previous blog post, we can now see how this Connect item has improved the ability to find information about plan caching in SQL Server 2012:

-- Create the Event Session
IF EXISTS(SELECT * 
          FROM sys.server_event_sessions 
          WHERE name='SQLStmtEvents')
    DROP EVENT SESSION SQLStmtEvents 
    ON SERVER;
GO
CREATE EVENT SESSION SQLStmtEvents
ON SERVER
ADD EVENT sqlserver.sql_statement_completed(
    SET collect_parameterized_plan_handle = 1
    ACTION (sqlserver.client_app_name,
            sqlserver.plan_handle,
            sqlserver.sql_text,
            sqlserver.tsql_stack,
            package0.callstack,
            sqlserver.request_id)
--Change this to match the AdventureWorks, 
--AdventureWorks2008 or AdventureWorks2008 SELECT DB_ID('AdventureWorks2008R2')
WHERE sqlserver.database_id=9
)
ADD TARGET package0.ring_buffer
WITH (MAX_DISPATCH_LATENCY=5SECONDS, TRACK_CAUSALITY=ON)
GO
 
-- Start the Event Session
ALTER EVENT SESSION SQLStmtEvents 
ON SERVER 
STATE = START;
GO
 
-- Change database contexts and insert one row
USE AdventureWorks2008R2;
GO
INSERT INTO [dbo].[ErrorLog]([ErrorTime],[UserName],[ErrorNumber],[ErrorSeverity],[ErrorState],[ErrorProcedure],[ErrorLine],[ErrorMessage])
VALUES(getdate(),SYSTEM_USER,-1,-1,-1,'ErrorProcedure',-1,'An error occurred')
GO 10
 
-- Drop the Event
ALTER EVENT SESSION SQLStmtEvents
ON SERVER
DROP EVENT sqlserver.sql_statement_completed;
GO

-- Retrieve the Event Data from the Event Session Target
SELECT
    event_data.value('(event/@name)[1]', 'varchar(50)') AS event_name,
    event_data.value('xs:hexBinary((event/data[@name="parameterized_plan_handle"]/value)[1])', 'varbinary(64)') as parameterized_plan_handle,
    event_data.value('xs:hexBinary((event/action[@name="plan_handle"]/value)[1])', 'varbinary(64)') as plan_handle,
    event_data.value('(event/action[@name="sql_text"]/value)[1]', 'varchar(max)') AS sql_text
FROM(    SELECT evnt.query('.') AS event_data
        FROM
        (   SELECT CAST(target_data AS xml) AS TargetData
            FROM sys.dm_xe_sessions AS s
            JOIN sys.dm_xe_session_targets AS t
                ON s.address = t.event_session_address
            WHERE s.name = 'SQLStmtEvents'
              AND t.target_name = 'ring_buffer'
        ) AS tab
        CROSS APPLY TargetData.nodes ('RingBufferTarget/event') AS split(evnt) 
     ) AS evts(event_data)

If we look at the output of this, we will get the parameterized plan handle for each subsequent call of the statement after the initial call caches the parameterized plan into the cache.

image

If we plug one of the original plan_handle values from the sqlserver.plan_handle action into a query of sys.dm_exec_cached_plans() it will return nothing, but using the new parameterized_plan_handle value from the customizable column will give us the appropriate cached plan for the statement from cache:

-- Use the plan_handle from one of the Events action to get the query plan
DECLARE @plan_handle varbinary(64) = 0x06000900DFC9DD12608B18EE0100000001000000000000000000000000000000000000000000000000000000
SELECT * 
FROM sys.dm_exec_query_plan(@plan_handle)
GO

-- Use the parameterized_plan_handle from the same Events to get the query plan
DECLARE @plan_handle varbinary(64) = 0x06000900DD8D6D08601E70EE01000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
SELECT * 
FROM sys.dm_exec_query_plan(@plan_handle)
GO

image

Now, you might point out the different lengths of the plan handles in the above two queries.  If you look back at the source, the same code is being used to perform the xhexBinary conversion in the XML so the values are exactly the same as what was originally provided by the event and the action.  The non-parameterized plan is not cached because it is not likely to be reused, which is why we have the auto-parameterized plan in cache.

In SQL Server 2012 RC0 there are a number of event session templates provided that make creating a commonly used session easier using the Event Session Wizard in SQL Server Management Studio.  One of these has a bug in it’s definition XML file that was filed in the following connect item:

https://connect.microsoft.com/SQLServer/feedback/details/705840/the-object-sqlserver-event-sequence-does-not-exist#tabs

If you attempt to pick the Activity Tracking template you will get the following error:

image

The error is occurring because the event_sequence action is provided by package0 and not sqlserver. To work around this, you can edit the template file and replace the sqlserver package references with package0. The template is saved in the following location:

C:\Program Files (x86)\Microsoft SQL Server\110\Tools\Templates\sql\xevent\xe_activity.xml

If you do a find for:

<action package="sqlserver" name="event_sequence" />

and replace it with:

<action package="package0" name="event_sequence" />

th template will work correctly once saved. This will at least let you play around with this while Microsoft works out the bug in the template XML.  A copy of the corrected file is attached to this blog post as well.

xe_activity.xml (18.28 kb)

Just over a year ago I blogged about the enhancements that were made to the sqlserver.page_split Event in SQL Server 2012 to make it easier to identify what the splitting object was and the type of split that was being performed.  Sadly what I discovered writing that post was that even with the extra information about the split type, the event didn’t give you enough information to really focus on the problematic splits that lead to fragmentation and page density issues in the database.  I didn’t do a whole lot with this again until recently when a question was posted by Ami Levin (Blog | Twitter) on the MVP email list that commented that the page_split event was broken in SQL Server 2012 based on a presentation he’d seen by Guy Glantser (Blog | Twitter).

Let me start off by saying, the event isn’t broken, it tracks page splits, but it doesn’t differentiate between an end page split that occurs for an ever increasing index, versus a mid-page split for a random index that leads to fragmentation and page density issues in the database.  Both of these are technically splits inside the storage engine, even if we as DBA’s don’t really care about the end-page split for a increasing key value like an IDENTITY column in the database.  I had Ami pass my information along to the presenter and we traded a few emails on the subject of tracking splits with the specific focus on trying to pull out the mid-page, fragmenting splits.  While going through things for the third time, it dawned on me that this is incredibly simple, based one of the demo’s that was sent to me.  Just over a year ago, I also blogged about tracking transaction log activity in SQL Server 2012 using the sqlserver.transaction_log event, which can be used to track mid-page splits in a database.

Last year when I wrote about the sqlserver.transaction_log event, there were 10 columns output by the event in CTP1, but as of RC0, the events output has changed and only 9 columns are output by the event.

SELECT 
    oc.name, 
    oc.type_name, 
    oc.description
FROM sys.dm_xe_packages AS p
INNER JOIN sys.dm_xe_objects AS o
    ON p.guid = o.package_guid
INNER JOIN sys.dm_xe_object_columns AS oc
    ON oc.object_name = o.name
        AND oc.object_package_guid = o.package_guid
WHERE o.name = 'transaction_log'
  AND oc.column_type = 'data';

image

For the purposes of identifying the mid-page splits, we want to look at the operation column that is output by the event, which contains the specific operation being logged.  In the case of a mid-page split occurring, the operation will be a LOP_DELETE_SPLIT, which marks the delete of rows from a page as a result of the split.  To build our event session, we are going to need the map_key for the LOP_DELETE_SPLIT log_op map.  This can be obtained from the sys.dm_xe_map_values DMV:

SELECT *
FROM sys.dm_xe_map_values
WHERE name = 'log_op'
  AND map_value = 'LOP_DELETE_SPLIT';

With the map_key value, we have a couple of ways to collect the information with our targets.  We could collect everything into an event_file, but that doesn’t really make sense for this event.  Instead the best target for this type of information is the histogram target which will bucket our results based on how we configure the target and tell us how frequently the event fires based on our bucketing criteria.  If we don’t know anything about the server in question, we can start off with a very general event session that has a predicate on the operation only, and then aggregate the information in the histogram target based on the database_id to find the databases that have the most mid-page splits occurring in them in the instance.

-- If the Event Session exists DROP it
IF EXISTS (SELECT 1 
            FROM sys.server_event_sessions 
            WHERE name = 'SQLskills_TrackPageSplits')
    DROP EVENT SESSION [SQLskills_TrackPageSplits] ON SERVER

-- Create the Event Session to track LOP_DELETE_SPLIT transaction_log operations in the server
CREATE EVENT SESSION [SQLskills_TrackPageSplits]
ON    SERVER
ADD EVENT sqlserver.transaction_log(
    WHERE operation = 11  -- LOP_DELETE_SPLIT 
)
ADD TARGET package0.histogram(
    SET filtering_event_name = 'sqlserver.transaction_log',
        source_type = 0, -- Event Column
        source = 'database_id');
GO
        
-- Start the Event Session
ALTER EVENT SESSION [SQLskills_TrackPageSplits]
ON SERVER
STATE=START;
GO

This event session will allow you to track the worst splitting database on the server, and the event data can be parsed out of the histogram target.  To demonstrate this, we can create a database that has tables and indexes prone to mid-page splits and run a default workload to test the event session:

USE [master];
GO
-- Drop the PageSplits database if it exists
IF DB_ID('PageSplits') IS NOT NULL
BEGIN
    ALTER DATABASE PageSplits SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
    DROP DATABASE PageSplits;
END
GO
-- Create the database
CREATE DATABASE PageSplits
GO
USE [PageSplits]
GO
-- Create a bad splitting clustered index table
CREATE TABLE BadSplitsPK
( ROWID UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID() PRIMARY KEY,
  ColVal INT NOT NULL DEFAULT (RAND()*1000),
  ChangeDate DATETIME2 NOT NULL DEFAULT CURRENT_TIMESTAMP);
GO
--  This index should mid-split based on the DEFAULT column value
CREATE INDEX IX_BadSplitsPK_ColVal ON BadSplitsPK (ColVal);
GO
--  This index should end-split based on the DEFAULT column value
CREATE INDEX IX_BadSplitsPK_ChangeDate ON BadSplitsPK (ChangeDate);
GO
-- Create a table with an increasing clustered index
CREATE TABLE EndSplitsPK
( ROWID INT IDENTITY NOT NULL PRIMARY KEY,
  ColVal INT NOT NULL DEFAULT (RAND()*1000),
  ChangeDate DATETIME2 NOT NULL DEFAULT DATEADD(mi, RAND()*-1000, CURRENT_TIMESTAMP));
GO
--  This index should mid-split based on the DEFAULT column value
CREATE INDEX IX_EndSplitsPK_ChangeDate ON EndSplitsPK (ChangeDate);
GO
-- Insert the default values repeatedly into the tables
WHILE 1=1
BEGIN
    INSERT INTO dbo.BadSplitsPK DEFAULT VALUES;
    INSERT INTO dbo.EndSplitsPK DEFAULT VALUES;
    WAITFOR DELAY '00:00:00.005';
END
GO

If we startup this workload and allow it to run for a couple of minutes, we can then query the histogram target for our session to find the database that has the mid-page splits occurring.

-- Query the target data to identify the worst splitting database_id
SELECT 
    n.value('(value)[1]', 'bigint') AS database_id,
    DB_NAME(n.value('(value)[1]', 'bigint')) AS database_name,
    n.value('(@count)[1]', 'bigint') AS split_count
FROM
(SELECT CAST(target_data as XML) target_data
 FROM sys.dm_xe_sessions AS s 
 JOIN sys.dm_xe_session_targets t
     ON s.address = t.event_session_address
 WHERE s.name = 'SQLskills_TrackPageSplits'
  AND t.target_name = 'histogram' ) as tab
CROSS APPLY target_data.nodes('HistogramTarget/Slot') as q(n)

image

With the database_id of the worst splitting database, we can then change our event session configuration to only look at this database, and then change our histogram target configuration to bucket on the alloc_unit_id so that we can then track down the worst splitting indexes in the database experiencing the worst mid-page splits.

-- Drop the Event Session so we can recreate it 
-- to focus on the highest splitting database
DROP EVENT SESSION [SQLskills_TrackPageSplits] 
ON SERVER

-- Create the Event Session to track LOP_DELETE_SPLIT transaction_log operations in the server
CREATE EVENT SESSION [SQLskills_TrackPageSplits]
ON    SERVER
ADD EVENT sqlserver.transaction_log(
    WHERE operation = 11  -- LOP_DELETE_SPLIT 
      AND database_id = 8 -- CHANGE THIS BASED ON TOP SPLITTING DATABASE!
)
ADD TARGET package0.histogram(
    SET filtering_event_name = 'sqlserver.transaction_log',
        source_type = 0, -- Event Column
        source = 'alloc_unit_id');
GO

-- Start the Event Session Again
ALTER EVENT SESSION [SQLskills_TrackPageSplits]
ON SERVER
STATE=START;
GO

With the new event session definition, we can now rerun our problematic workload for a 2 minute period and look at the worst splitting indexes based on the alloc_unit_id’s that are in the histogram target:

 

-- Query Target Data to get the top splitting objects in the database:
SELECT
    o.name AS table_name,
    i.name AS index_name,
    tab.split_count,
    i.fill_factor
FROM (    SELECT 
            n.value('(value)[1]', 'bigint') AS alloc_unit_id,
            n.value('(@count)[1]', 'bigint') AS split_count
        FROM
        (SELECT CAST(target_data as XML) target_data
         FROM sys.dm_xe_sessions AS s 
         JOIN sys.dm_xe_session_targets t
             ON s.address = t.event_session_address
         WHERE s.name = 'SQLskills_TrackPageSplits'
          AND t.target_name = 'histogram' ) as tab
        CROSS APPLY target_data.nodes('HistogramTarget/Slot') as q(n)
) AS tab
JOIN sys.allocation_units AS au
    ON tab.alloc_unit_id = au.allocation_unit_id
JOIN sys.partitions AS p
    ON au.container_id = p.partition_id
JOIN sys.indexes AS i
    ON p.object_id = i.object_id
        AND p.index_id = i.index_id
JOIN sys.objects AS o
    ON p.object_id = o.object_id
WHERE o.is_ms_shipped = 0;

image

With this information we can now go back and change our FillFactor specifications and retest/monitor the impact to determine whether we’ve had the appropriate reduction in mid-page splits to accommodate the time between our index rebuild operations:

-- Change FillFactor based on split occurences
ALTER INDEX PK__BadSplit__97BD02EB726FCA55 ON BadSplitsPK REBUILD WITH (FILLFACTOR=70)
ALTER INDEX IX_BadSplitsPK_ColVal ON BadSplitsPK REBUILD WITH (FILLFACTOR=70)
ALTER INDEX IX_EndSplitsPK_ChangeDate ON EndSplitsPK REBUILD WITH (FILLFACTOR=80)
GO

-- Stop the Event Session to clear the target
ALTER EVENT SESSION [SQLskills_TrackPageSplits]
ON SERVER
STATE=STOP;
GO

-- Start the Event Session Again
ALTER EVENT SESSION [SQLskills_TrackPageSplits]
ON SERVER
STATE=START;
GO

With the reset performed we can again start up our workload generation and begin monitoring the effect of the FillFactor specifications on the indexes with our code.  After another 2 minute period, the following splits were noted.

image

With this information we can go back and again attempt to tune our FillFactor values for the worst splitting indexes and rinse/repeat until we determine the best FillFactor for each of the indexes to minimize splits.  This is an incredibly powerful tool for the DBA moving into SQL Server 2012, and will definitely change how we perform index fragmentation analysis and troubleshoot problems with excessive log generation in SQL Server 2012 onwards.

Cheers!

While setting up my new Availability Group using SQL Server 2012 RC0 tonight, I noticed an interesting new addition to Extended Events associated with Availability Group configuration in the Release Candidate.  When you setup an Availability Group in RC0, another default Event Session is created on the servers that participate in the Availability Group to provide monitoring of the health of the Availability Group overall.  The definition of the monitoring session is as follows:

CREATE EVENT SESSION [AlwaysOn_health] ON SERVER
ADD EVENT sqlserver.alwayson_ddl_executed,
ADD EVENT sqlserver.availability_group_lease_expired,
ADD EVENT sqlserver.availability_replica_automatic_failover_validation,
ADD EVENT sqlserver.availability_replica_manager_state_change,
ADD EVENT sqlserver.availability_replica_state_change,
ADD EVENT sqlserver.error_reported(
    WHERE ([error_number]=(9691) OR [error_number]=(35204) OR [error_number]=(9693) OR [error_number]=(26024) OR [error_number]=(28047) OR [error_number]=(26023) OR [error_number]=(9692) OR [error_number]=(28034) OR [error_number]=(28036) OR [error_number]=(28048) OR [error_number]=(28080) OR [error_number]=(28091) OR [error_number]=(26022) OR [error_number]=(9642) OR [error_number]=(35201) OR [error_number]=(35202) OR [error_number]=(35206) OR [error_number]=(35207) OR [error_number]=(26069) OR [error_number]=(26070) OR [error_number]>(41047) AND [error_number]<(41056) OR [error_number]=(41142) OR [error_number]=(41144) OR [error_number]=(1480) OR [error_number]=(823) OR [error_number]=(824) OR [error_number]=(829) OR [error_number]=(35264) OR [error_number]=(35265))),
ADD EVENT sqlserver.lock_redo_blocked
ADD TARGET package0.event_file(SET filename=N'AlwaysOn_health.xel',max_file_size=(5),max_rollover_files=(4))
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=ON)
GO

This Extended Event Session monitors a number of critical events in the system but one of the problems with figuring out what exactly this Event Session is monitoring is to figure out what all the predicate values on the sqlserver.error_reported event are actually firing on.  To that aspect of things, we can do a quick reuse of the predicate on this event be doing a replace SSMS on the [error_number] value with a replace for an alias to a query to sys.messages, on the message_id column from the DMV as follows:

SELECT message_id, severity, is_event_logged, text
FROM sys.messages AS m
WHERE m.language_id = SERVERPROPERTY('LCID')
  AND  (m.message_id=(9691)
        OR m.message_id=(35204)
        OR m.message_id=(9693)
        OR m.message_id=(26024)
        OR m.message_id=(28047)
        OR m.message_id=(26023)
        OR m.message_id=(9692)
        OR m.message_id=(28034)
        OR m.message_id=(28036)
        OR m.message_id=(28048)
        OR m.message_id=(28080)
        OR m.message_id=(28091)
        OR m.message_id=(26022)
        OR m.message_id=(9642)
        OR m.message_id=(35201)
        OR m.message_id=(35202)
        OR m.message_id=(35206)
        OR m.message_id=(35207)
        OR m.message_id=(26069)
        OR m.message_id=(26070)
        OR m.message_id>(41047)
        AND m.message_id<(41056)
        OR m.message_id=(41142)
        OR m.message_id=(41144)
        OR m.message_id=(1480)
        OR m.message_id=(823)
        OR m.message_id=(824)
        OR m.message_id=(829)
        OR m.message_id=(35264)
        OR m.message_id=(35265)
)

This will give us a list of the error messages that the Event Session will actually fire events for:

message_id

severity is_event_logged text
823 24 1 The operating system returned error %ls to SQL Server during a %S_MSG at offset %#016I64x in file '%ls'. Additional messages in the SQL Server error log and system event log may provide more detail. This is a severe system-level error condition that threatens database integrity and must be corrected immediately. Complete a full database consistency check (DBCC CHECKDB). This error can be caused by many factors; for more information, see SQL Server Books Online.
824 24 1 SQL Server detected a logical consistency-based I/O error: %ls. It occurred during a %S_MSG of page %S_PGID in database ID %d at offset %#016I64x in file '%ls'.  Additional messages in the SQL Server error log or system event log may provide more detail. This is a severe error condition that threatens database integrity and must be corrected immediately. Complete a full database consistency check (DBCC CHECKDB). This error can be caused by many factors; for more information, see SQL Server Books Online.
829 21 1 Database ID %d, Page %S_PGID is marked RestorePending, which may indicate disk corruption. To recover from this state, perform a restore.
1480 10 0 The %S_MSG database "%.*ls" is changing roles from "%ls" to "%ls" because the mirroring session or availability group failed over due to %S_MSG. This is an informational message only. No user action is required.
9642 16 0 An error occurred in a Service Broker/Database Mirroring transport connection endpoint, Error: %i, State: %i. (Near endpoint role: %S_MSG, far endpoint address: '%.*hs')
9691 10 0 The %S_MSG endpoint has stopped listening for connections.
9692 16 0 The %S_MSG endpoint cannot listen on port %d because it is in use by another process.
9693 16 0 The %S_MSG endpoint cannot listen for connections due to the following error: '%.*ls'.
26022 10 1 Server is listening on [ %hs <%hs> %d].
26023 16 1 Server TCP provider failed to listen on [ %hs <%hs> %d]. Tcp port is already in use.
26024 16 1 Server failed to listen on %hs <%hs> %d. Error: %#x. To proceed, notify your system administrator.
26069 10 1 Started listening on virtual network name '%ls'. No user action is required.
26070 10 1 Stopped listening on virtual network name '%ls'. No user action is required.
28034 10 0 Connection handshake failed. The login '%.*ls' does not have CONNECT permission on the endpoint. State %d.
28036 10 0 Connection handshake failed. The certificate used by this endpoint was not found: %S_MSG. Use DBCC CHECKDB in master database to verify the metadata integrity of the endpoints. State %d.
28047 10 1 %S_MSG login attempt failed with error: '%.*ls'. %.*ls
28048 10 1 %S_MSG login attempt by user '%.*ls' failed with error: '%.*ls'. %.*ls
28080 10 0 Connection handshake failed. The %S_MSG endpoint is not configured. State %d.
28091 10 0  Starting endpoint for %S_MSG with no authentication is not supported.
35201 10 0 A connection timeout has occurred while attempting to establish a connection to availability replica '%ls' with id [%ls]. Either a networking or firewall issue exists, or the endpoint address provided for the replica is not the database mirroring endpoint of the host server instance.
35202 10 0 A connection for availability group '%ls' from availability replica '%ls' with id  [%ls] to '%ls' with id [%ls] has been successfully established.  This is an informational message only. No user action is required.
35204 10 0 The connection between server instances '%ls' with id [%ls] and '%ls' with id [%ls] has been disabled because the database mirroring endpoint was either disabled or stopped. Restart the endpoint by using the ALTER ENDPOINT Transact-SQL statement with STATE = STARTED.
35206 10 0 A connection timeout has occurred on a previously established connection to availability replica '%ls' with id [%ls].  Either a networking or a firewall issue exists or the availability replica has transitioned to the resolving role.
35207 16 0 Connection attempt on availability group id '%ls' from replica id '%ls' to replica id '%ls' failed because of error %d, severity %d, state %d.
35264 10 0 AlwaysOn Availability Groups data movement for database '%.*ls' has been suspended for the following reason: "%S_MSG" (Source ID %d; Source string: '%.*ls'). To resume data movement on the database, you will need to resume the database manually. For information about how to resume an availability database, see SQL Server Books Online.
35265 10 0 AlwaysOn Availability Groups data movement for database '%.*ls' has been resumed. This is an informational message only. No user action is required.
41048 10 1 AlwaysOn Availability Groups: Local Windows Server Failover Clustering service has become unavailable. This is an informational message only. No user action is required.
41049 10 1 AlwaysOn Availability Groups: Local Windows Server Failover Clustering node is no longer online. This is an informational message only. No user action is required.
41050 10 1 AlwaysOn Availability Groups: Waiting for local Windows Server Failover Clustering service to start. This is an informational message only. No user action is required.
41051 10 1 AlwaysOn Availability Groups: Local Windows Server Failover Clustering service started. This is an informational message only. No user action is required.
41052 10 1 AlwaysOn Availability Groups: Waiting for local Windows Server Failover Clustering node to start. This is an informational message only. No user action is required.
41053 10 1 AlwaysOn Availability Groups: Local Windows Server Failover Clustering node started. This is an informational message only. No user action is required.
41054 10 1 AlwaysOn Availability Groups: Waiting for local Windows Server Failover Clustering node to come online. This is an informational message only. No user action is required.
41055 10 1 AlwaysOn Availability Groups: Local Windows Server Failover Clustering node is online. This is an informational message only. No user action is required.
41142 16 0 The availability replica for availability group '%.*ls' on this instance of SQL Server cannot become the primary replica. One or more databases are not synchronized or have not joined the availability group, or the WSFC cluster was started in Force Quorum mode. If the cluster was started in Force Quorum mode or the availability replica uses the asynchronous-commit mode, consider performing a forced manual failover (with possible data loss). Otherwise, once all local secondary databases are joined and synchronized, you can perform a planned manual failover to this secondary replica (without data loss). For more information, see SQL Server Books Online.
41144 16 0 The local availability replica of availability group '%.*ls' is in a failed state.  The replica failed to read or update the persisted configuration data (SQL Server error: %d).  To recover from this failure, either restart the local Windows Server Failover Clustering (WSFC) service or restart the local instance of SQL Server.

Based on this output, and the output of the following query:

SELECT name, description
FROM sys.dm_xe_objects
WHERE NAME IN (
'alwayson_ddl_executed',
'availability_group_lease_expired',
'availability_replica_automatic_failover_validation',
'availability_replica_manager_state_change',
'availability_replica_state_change',
'error_reported',
'lock_redo_blocked')

We can deduce the following about the Event Session:

CREATE EVENT SESSION [AlwaysOn_health] ON SERVER
--Occurs when AlwaysOn DDL is executed including CREATE, ALTER or DROP
ADD EVENT sqlserver.alwayson_ddl_executed,
--Occurs when there is a connectivity issue between the cluster and the Availability Group resulting
--in a failure to renew the lease
ADD EVENT sqlserver.availability_group_lease_expired,
--Occurs when the failover validates the readiness of replica as a primary. For instance, the failover
--validation will return false when not all databases are synchronized or not joined
ADD EVENT sqlserver.availability_replica_automatic_failover_validation,
--Occurs when the state of the Availability Replica Manager has changed.
ADD EVENT sqlserver.availability_replica_manager_state_change,
--Occurs when the state of the Availability Replica has changed.
ADD EVENT sqlserver.availability_replica_state_change,
--Occurs when an error is reported based on the previously listed table
ADD EVENT sqlserver.error_reported(
    WHERE ([error_number]=(9691) OR [error_number]=(35204) OR [error_number]=(9693) OR [error_number]=(26024) OR [error_number]=(28047) OR [error_number]=(26023) OR [error_number]=(9692) OR [error_number]=(28034) OR [error_number]=(28036) OR [error_number]=(28048) OR [error_number]=(28080) OR [error_number]=(28091) OR [error_number]=(26022) OR [error_number]=(9642) OR [error_number]=(35201) OR [error_number]=(35202) OR [error_number]=(35206) OR [error_number]=(35207) OR [error_number]=(26069) OR [error_number]=(26070) OR [error_number]>(41047) AND [error_number]<(41056) OR [error_number]=(41142) OR [error_number]=(41144) OR [error_number]=(1480) OR [error_number]=(823) OR [error_number]=(824) OR [error_number]=(829) OR [error_number]=(35264) OR [error_number]=(35265))),
--Occurs when the redo thread blocks when trying to acquire a lock.
ADD EVENT sqlserver.lock_redo_blocked
--Writes to the file target for persistence in the system beyond failovers and service restarts
ADD TARGET package0.event_file(SET filename=N'AlwaysOn_health.xel',max_file_size=(5),max_rollover_files=(4))
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=ON)
GO

What is really cool is that this Event Session is used by the Availability Groups Dashboard to provide an overall status of the health of the Availability Group in Management Studio.

While working on validating my demos for the 24 Hours of PASS and my PASS Summit 2011 Precon – Extended Events Deep Dive, I noticed a significant, and breaking change to the Event XML output for the raw event data in the ring_buffer and file_target in SQL Server Denali.  In SQL Server 2008 and 2008R2, the Event XML represented the output of XML data elements differently than in SQL Server Denali CTP3.  A good example of this is the xml_deadlock_report output, which I previously discussed in my SQL Server Central article,

In SQL Server 2008 and 2008R2, the query to retrieve the deadlock graph from the system_health session was (excluding the work around that was included in the original article since the xml_deadlock_report was fixed in a later Cumulative Update and the latest Service Pack for SQL Server 2008 and 2008 R2).

SELECT
    CAST(event_data.value('(data/value)[1]', 'varchar(max)')) AS XML) AS DeadlockGraph
FROM
(   SELECT XEvent.query('.') AS event_data
    FROM
    (    -- Cast the target_data to XML
        SELECT CAST(target_data AS XML) AS TargetData
        FROM sys.dm_xe_session_targets st
        JOIN sys.dm_xe_sessions s
            ON s.address = st.event_session_address
        WHERE name = 'system_health'
          AND target_name = 'ring_buffer'
    ) AS Data
    -- Split out the Event Nodes
    CROSS APPLY TargetData.nodes ('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData (XEvent)  
) AS tab (event_data)

If you run this same code in SQL Server Denali CTP3, the output will not be the xml_deadlock_report but instead the textual data that was included in the sub-nodes of the value node for the xml_deadlock_report events in the target.  This unfortunately has broken a number of my scripts that were initially written for SQL Server 2008 and 2008R2 that expect the XML output as text in the value element as follows:

<event name="xml_deadlock_report" package="sqlserver" id="123" version="1" timestamp="2011-09-15T23:29:02.851Z">
  <data name="xml_report">
    <type name="unicode_string" package="package0" />
    <value>&lt;deadlock-list&gt;
&lt;victim-list&gt;
  &lt;victimProcess id="process806e2088"/&gt;
  &lt;process-list&gt;
   &lt;process id="process806e2088" taskpriority="0" logused="10000" waitresource="DATABASE: 15 " waittime="1477" schedulerid="2" kpid="3720" status="suspended" spid="57" sbid="0" ecid="0" priority="0" trancount="1" lastbatchstarted="2011-09-15T19:29:01.370" lastbatchcompleted="2011-09-15T19:27:21.193" clientapp="Microsoft SQL Server Management Studio - Query" hostname="SQL2K8R2-IE2" hostpid="4464" loginname="SQLSKILLSDEMOS\administrator" isolationlevel="read committed (2)" xactid="68641" currentdb="15" lockTimeout="4294967295" clientoption1="671090784" clientoption2="390200"&gt;
    &lt;executionStack&gt;
     &lt;frame procname="" line="1" sqlhandle="0x01000100721ac42240ff1285000000000000000000000000"&gt;
     &lt;/frame&gt;
    &lt;/executionStack&gt;
    &lt;inputbuf&gt;
ALTER DATABASE DemoNCIndex SET MULTI_USER
    &lt;/inputbuf&gt;
   &lt;/process&gt;
   &lt;process id="process469b88" taskpriority="0" logused="10000" waitresource="DATABASE: 15 " waittime="1892" schedulerid="2" kpid="4188" status="suspended" spid="58" sbid="0" ecid="0" priority="0" trancount="0" lastbatchstarted="2011-09-15T19:29:00.957" lastbatchcompleted="2011-09-15T19:29:00.947" clientapp="Microsoft SQL Server Management Studio - Transact-SQL IntelliSense" hostname="SQL2K8R2-IE2" hostpid="4464" loginname="SQLSKILLSDEMOS\administrator" isolationlevel="read committed (2)" xactid="68638" currentdb="15" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056"&gt;
    &lt;executionStack&gt;
     &lt;frame procname="" line="1" sqlhandle="0x010001008af5b714605a1f85000000000000000000000000"&gt;
     &lt;/frame&gt;
    &lt;/executionStack&gt;
    &lt;inputbuf&gt;
use [DemoNCIndex]    &lt;/inputbuf&gt;
   &lt;/process&gt;
  &lt;/process-list&gt;
  &lt;resource-list&gt;
   &lt;databaselock subresource="FULL" dbid="15" dbname="" id="lock83168d80" mode="S"&gt;
    &lt;owner-list&gt;
     &lt;owner id="process469b88" mode="S"/&gt;
    &lt;/owner-list&gt;
    &lt;waiter-list&gt;
     &lt;waiter id="process806e2088" mode="X" requestType="wait"/&gt;
    &lt;/waiter-list&gt;
   &lt;/databaselock&gt;
   &lt;databaselock subresource="FULL" dbid="15" dbname="" id="lock83168d80" mode="S"&gt;
    &lt;owner-list&gt;
     &lt;owner id="process806e2088" mode="S"/&gt;
     &lt;owner id="process806e2088" mode="S"/&gt;
    &lt;/owner-list&gt;
    &lt;waiter-list&gt;
     &lt;waiter id="process469b88" mode="X" requestType="wait"/&gt;
    &lt;/waiter-list&gt;
   &lt;/databaselock&gt;
  &lt;/resource-list&gt;
&lt;/deadlock&gt;
&lt;/deadlock-list&gt;
</value>
    <text />
  </data>
</event>

Instead in SQL Server Denali CTP3, the event output is as follows:

<event name="xml_deadlock_report" package="sqlserver" timestamp="2011-09-17T18:49:03.654Z">
  <data name="xml_report">
    <type name="xml" package="package0" />
    <value>
      <deadlock>
        <victim-list>
          <victimProcess id="processf7034a18" />
        </victim-list>
        <process-list>
          <process id="processf7034a18" taskpriority="0" logused="144" waitresource="RID: 2:1:281:0" waittime="2394" ownerId="162349" transactionname="user_transaction" lasttranstarted="2011-09-17T11:48:48.410" XDES="0xff047120" lockMode="S" schedulerid="2" kpid="692" status="suspended" spid="58" sbid="0" ecid="0" priority="0" trancount="1" lastbatchstarted="2011-09-17T11:49:01.247" lastbatchcompleted="2011-09-17T11:48:48.410" lastattention="2011-09-17T11:39:47.393" clientapp="Microsoft SQL Server Management Studio - Query" hostname="WIN-QSTGAPD63IN" hostpid="3004" loginname="WIN-QSTGAPD63IN\Administrator" isolationlevel="read committed (2)" xactid="162349" currentdb="2" lockTimeout="4294967295" clientoption1="671090784" clientoption2="390200">
            <executionStack>
              <frame procname="adhoc" line="1" sqlhandle="0x02000000303b01237c6994b4eab30fb77cbb5a8e46f2b2540000000000000000000000000000000000000000">
SELECT Column2
FROM TableB    </frame>
            </executionStack>
            <inputbuf>
SELECT Column2
FROM TableB   </inputbuf>
          </process>
          <process id="processf7035168" taskpriority="0" logused="144" waitresource="RID: 2:1:271:0" waittime="7494" ownerId="162369" transactionname="user_transaction" lasttranstarted="2011-09-17T11:48:53.693" XDES="0xf7044dd0" lockMode="S" schedulerid="2" kpid="3244" status="suspended" spid="60" sbid="0" ecid="0" priority="0" trancount="1" lastbatchstarted="2011-09-17T11:48:56.150" lastbatchcompleted="2011-09-17T11:48:53.693" lastattention="1900-01-01T00:00:00.693" clientapp="Microsoft SQL Server Management Studio - Query" hostname="WIN-QSTGAPD63IN" hostpid="3004" loginname="WIN-QSTGAPD63IN\Administrator" isolationlevel="read committed (2)" xactid="162369" currentdb="2" lockTimeout="4294967295" clientoption1="671090784" clientoption2="390200">
            <executionStack>
              <frame procname="adhoc" line="2" stmtstart="4" sqlhandle="0x020000002e8952007a6c36a78a2aa436877a27f57a0725c80000000000000000000000000000000000000000">
SELECT Column1
FROM TableA    </frame>
            </executionStack>
            <inputbuf>

SELECT Column1
FROM TableA   </inputbuf>
          </process>
        </process-list>
        <resource-list>
          <ridlock fileid="1" pageid="281" dbid="2" objectname="tempdb.dbo.TABLEB" id="lockf7d4ff80" mode="X" associatedObjectId="2161727822326792192">
            <owner-list>
              <owner id="processf7035168" mode="X" />
            </owner-list>
            <waiter-list>
              <waiter id="processf7034a18" mode="S" requestType="wait" />
            </waiter-list>
          </ridlock>
          <ridlock fileid="1" pageid="271" dbid="2" objectname="tempdb.dbo.TABLEA" id="lockf7d51380" mode="X" associatedObjectId="2089670228247904256">
            <owner-list>
              <owner id="processf7034a18" mode="X" />
            </owner-list>
            <waiter-list>
              <waiter id="processf7035168" mode="S" requestType="wait" />
            </waiter-list>
          </ridlock>
        </resource-list>
      </deadlock>
    </value>
  </data>
</event>

If you compare the two bold sections to each other you will notice the difference.  In SQL Server 2008 and 2008R2, the value element is XML escaped entirely as text, but in SQL Server Denali CTP3, the value attribute contains a valid XML document as a child node in the XML itself.  This has a significant impact to how you actually access the XML data in Denali CTP3.  To read the XML Document, you have to switch from using the .value() XML function along with a CAST() operation to using a .query() operation on the Event XML specifying the deadlock node as a part of the .query() XPATH for it as shown in the following code example:

SELECT
    event_data.query('(event/data/value/deadlock)[1]') AS DeadlockGraph
FROM
(   SELECT XEvent.query('.') AS event_data
    FROM
    (    -- Cast the target_data to XML
        SELECT CAST(target_data AS XML) AS TargetData
        FROM sys.dm_xe_session_targets st
        JOIN sys.dm_xe_sessions s
            ON s.address = st.event_session_address
        WHERE name = 'system_health'
          AND target_name = 'ring_buffer'
    ) AS Data
    -- Split out the Event Nodes
    CROSS APPLY TargetData.nodes ('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData (XEvent)  
) AS tab (event_data)

This same thing applies to all of the XML data elements including the sqlserver.tsql_stack and sqlserver.tsql_frame Actions.  In addition other actions such as the sqlserver.plan_handle have similar changes that require changing the code to process the Event XML to capture the values being output.

Yesterday PASS announced the PASS Summit 2011 regular sessions and 4 new half-day sessions.  At the same time they also announced the top sessions from the SQLRally conference in Orlando, FL last month. I was thrilled to have been in the Top 5 for the conference overall. A few weeks ago PASS announced the Pre-conference Sessions and Spotlight Sessions.  This year I’ll be presenting a full day Pre-conference session on Extended Events, as well as a Spotlight Session on Event Notifications.  This will be the fourth time I have attended PASS Summit, and my third time presenting.

Below is the abstract for the Pre-conference session on Extended Events.

Extended Events Deep Dive [400]
Session Category: Pre-conference Session (7 hours)
Session Track: Enterprise Database Administration and Deployment
Speaker(s): Jonathan Kehayias

At PASS 2010 after the public release of CTP1 of SQL Server Denali, the product team demonstrated a replacement for SQL Profiler built on top of Extended Events. The writing is on the wall for SQL Trace and the future of diagnostic profiling in SQL Server is Extended Events. In this workshop you will learn Extended Events from the ground up; from the original implementation in SQL Server 2008 to the enhancements that have been made in SQL Server Denali that redefine data collection for troubleshooting in SQL Server.

Extended Events provide more information about the operations of SQL Server than have ever before been available in the product. Unfortunately, there is a significant learning gap between Extended Events and SQL Trace; and the lack of a UI has only made the learning curve that much steeper. This full-day deep dive workshop will explain the basic concepts of extended events, spin that information into some basic patterns, and build on those patterns to create complex custom functionality that will prepare you for the future. Learn firsthand from the developer of the SSMS Addin for Extended Events in SQL Server 2008 how to leverage Extended Events in your own environment.

I really look forward to this session based on the feedback I have gotten on the Extended Events sessions I’ve done the last two years at PASS.  One of the primary feedback items has been that the session was rushed and didn’t have enough time to cover the subject matter. Extended Events are such a drastic change in profiling system events from SQL Trace that there is a large learning curve that has to be made before it is possible to really begin leveraging the feature for diagnostics, and trying to cover the necessary background information in a 75 minute session is challenging at best.

The Event Notifications session is a updated version of a session I presented at SQLSaturday in Tampa, FL two years ago.  The session abstract is below.

Using Event Notifications in SQL Server 2005/2008 [300]
Session Category: Spotlight Session (90 minutes, Invitation only)
Session Track: Enterprise Database Administration and Deployment
Speaker(s): Jonathan Kehayias

Event Notifications are a powerful tool in the Database Administrators tool kit that are often overlooked and rarely used. This session will provide an overview of the Service Broker components used by Event Notifications and the difference between Event Notifications and other features of SQL Server like SQL Trace and DDL Triggers. It will look at the events that are available for use with Event Notifications and how to find information about the data returned by those events in the Books Online. The session demos will teach you how to leverage the functionality of Event Notifications to automate responses to events inside of SQL Server, and how to build a monitoring solution for problems like blocking and deadlocks.

This session was very popular the first time I presented it, and includes a number of functional demo’s that can be applied to any environment to immediately begin automating responses to events inside of SQL Server.

If you are attending PASS Summit 2011 this year, I hope to see you in one of my sessions.

The first SQL Rally was held last week in Orlando, FL, and I had the honor of being selected for one of the spotlight sessions by the community in the DBA track.  SQL Rally was a different experience from the regular PASS Summit; it wasn’t anywhere as big as the normal summit, but it was larger than most of the SQL Saturday events that I have attended.  If I had to make a comparison, I would say that SQL Rally was more on par with the experience I had attending SQL Bits 7 in York, UK last October, which seems to be right about where PASS wanted the experience to be.  I always enjoy attending events, large or small, where people are passionate about SQL Server. 

Since the event I have had a number of requests for the presentation materials and demos I used in my Deadlocking for Mere Mortals presentation on Friday afternoon.  Below is the session abstract, and attached to this blog post is a copy of the slides and demo’s for the presentation. 

Title:  Deadlocking for Mere Mortals
Speaker: 
Jonathan Kehayias
Category:  Summit Spotlight
Level: 200

Abstract:
While troubleshooting deadlocking in SQL Server has gotten easier in SQL Server 2005 and 2008, it continues to be a constant source of questions in the forums online. This session will look at the most common deadlocks asked about on the forums, and how to troubleshoot them using the various methods available to DBA’s in SQL Server 2005 and 2008; including Trace Flags, SQL Trace, Event Notifications, and Extended Events.

Session Goals

  • Understand why deadlocks occur in SQL Server
  • Understand how to capture deadlock graphs in various SQL Server versions.
  • Understand how to read the deadlock graph to determine the specific cause and how to mitigate against the deadlock.

SQL Rally 2010 - DBA200 - Deadlocking for Mere Mortals.zip (964.07 kb)

At SQL Connections, I presented a session titled “Learn SQL Server Internals with Extended Events” where I demonstrated a number ways to use Extended Events to learn about the internal workings of the database engine for SQL Server.  The morning of the session I was chatting with someone about a problem they had seen and the topic of proportional fill came up and how the database engine stripes data across multiple files in a user database.  From this discussion, I got the idea to play around with multiple database files and built a demo using Extended Events that showed how proportional fill worked inside of the database engine.  This wasn’t a planned demo for my presentation, and I had plenty of other demo’s that showed various SQL Server Internals, but it became a really good demo that I decided to throw into the mix, which put me way over budget for time.  I decided to leave it up the audience which demo they wanted to see for the last demo of the session, an originally planned one, or the one I wrote that morning for proportional fill; the majority wanted to see the one on proportional fill and it turned out to be the best demo of the entire session based on the crowd interest and feedback.  What was most interesting to me was the number of people attending the session that had never heard of the concept of proportional fill with SQL Server.  Proportional fill is the algorithm used by SQL Server to determine how much information is written to each of the files in a multi-file filegroup based on the proportion of free space within each file; which allows the files to become full at approximately the same time.  Proportional fill has nothing to do with the actual file sizes, it is strictly based on the free space within a file. 

To demonstrate proportional fill in SQL Server, we’ll take a look at how the page writes and I/O operations are distributed across varying configurations for a test database using the same test and event session for each configuration

Basic Example

To look at how proportional fill functions, we’ll start with a basic example, using a database with a separate filegroup for user objects that has been marked as the default filegroup for the database.  The UserObjects filegroup will have four data files, each 32MB in size.

-- Create a multi-file database with evenly sized files
CREATE DATABASE [MultipleDataFiles] ON  PRIMARY 
( NAME = N'MultipleDataFiles', 
    FILENAME = N'H:\SQLData\MultipleDataFiles.mdf' , 
    SIZE = 32768KB , 
    FILEGROWTH = 32768KB ), 
FILEGROUP [UserObjects] 
( NAME = N'MultipleDataFiles_UserObjects1', 
    FILENAME = N'H:\SQLData\MultipleDataFiles_UserObjects1.ndf' , 
    SIZE = 32768KB , 
    FILEGROWTH = 32768KB ), 
( NAME = N'MultipleDataFiles_UserObjects2', 
    FILENAME = N'H:\SQLData\MultipleDataFiles_UserObjects2.ndf' , 
    SIZE = 32768KB , 
    FILEGROWTH = 32768KB ), 
( NAME = N'MultipleDataFiles_UserObjects3', 
    FILENAME = N'H:\SQLData\MultipleDataFiles_UserObjects3.ndf' , 
    SIZE = 32768KB , 
    FILEGROWTH = 32768KB ), 
( NAME = N'MultipleDataFiles_UserObjects4', 
    FILENAME = N'H:\SQLData\MultipleDataFiles_UserObjects4.ndf' , 
    SIZE = 32768KB , 
    FILEGROWTH = 32768KB )
 LOG ON 
( NAME = N'MultipleDataFiles_log', 
    FILENAME = N'L:\SQLLogs\MultipleDataFiles_log.ldf' , 
    SIZE = 131072KB , 
    FILEGROWTH = 32768KB )
GO
ALTER DATABASE [MultipleDataFiles] 
MODIFY FILEGROUP [UserObjects] DEFAULT
GO

With the database created, we’ll create a table to load data into for our tests based on the AdventureWorks SalesOrderHeader table which will be the source for the test data:

- Create a table to load data into for the tests
USE [MultipleDataFiles]
GO
IF OBJECT_ID('SalesOrderHeader') IS NOT NULL
    DROP TABLE [dbo].[SalesOrderHeader]
GO
SET NOCOUNT ON
GO
CREATE TABLE [dbo].[SalesOrderHeader](
    [SalesOrderID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY,
    [RevisionNumber] [tinyint] NOT NULL,
    [OrderDate] [datetime] NOT NULL,
    [DueDate] [datetime] NOT NULL,
    [ShipDate] [datetime] NULL,
    [Status] [tinyint] NOT NULL,
    [OnlineOrderFlag] [bit] NOT NULL,
    [SalesOrderNumber] [nvarchar](25) NOT NULL,
    [PurchaseOrderNumber] [nvarchar](25) NULL,
    [AccountNumber] [nvarchar](15) NULL,
    [CustomerID] [int] NOT NULL,
    [SalesPersonID] [int] NULL,
    [TerritoryID] [int] NULL,
    [BillToAddressID] [int] NOT NULL,
    [ShipToAddressID] [int] NOT NULL,
    [ShipMethodID] [int] NOT NULL,
    [CreditCardID] [int] NULL,
    [CreditCardApprovalCode] [varchar](15) NULL,
    [CurrencyRateID] [int] NULL,
    [SubTotal] [money] NOT NULL,
    [TaxAmt] [money] NOT NULL,
    [Freight] [money] NOT NULL,
    [TotalDue] [money] NOT NULL,
    [Comment] [nvarchar](128) NULL,
    [rowguid] [uniqueidentifier] NOT NULL,
    [ModifiedDate] [datetime] NOT NULL
) 
GO

Now we’ll create our event session using dynamic SQL to add the database_id for our test database to the predicate for each of the events, restricting them to firing only for our test database to minimize the need to filter through the event session data later on.  The event session is going to collect the sqlserver.checkpoint_begin, sqlserver.checkpoint_end. sqlserver.file_written, sqlserver.file_write_completed, sqlserver.physical_page_write, sqlos.async_io_requested, and sqlos.async_io_completed events.  The checkpoint events are included in the event session to show that writes don’t immediately begin to occur to the data files, but instead occur in response to the checkpoint operations in the database engine.

-- Create our Event Session dynamically
IF EXISTS(SELECT * FROM sys.server_event_sessions WHERE name='MultipleDataFiles')
    DROP EVENT SESSION [MultipleDataFiles] ON SERVER;
DECLARE @sqlcmd nvarchar(4000) = '
CREATE EVENT SESSION MultipleDataFiles
ON SERVER
ADD EVENT sqlserver.checkpoint_begin
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlserver.checkpoint_end
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlserver.file_written
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlserver.file_write_completed
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlserver.physical_page_write
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlos.async_io_requested
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlos.async_io_completed
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+'))--,
ADD TARGET package0.asynchronous_file_target(
     SET filename=''C:\SQLskills\MultipleDataFiles.xel'',
         metadatafile=''C:\SQLskills\MultipleDataFiles.xem'')
WITH (MAX_MEMORY = 8MB, EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS, TRACK_CAUSALITY = ON, MAX_DISPATCH_LATENCY=5SECONDS)'
EXEC (@sqlcmd)

With the event session created we can run our data load:

-- Start the Event Session
ALTER EVENT SESSION MultipleDataFiles
ON SERVER
STATE=START
GO
 
-- Load data into the test database
INSERT INTO dbo.SalesOrderHeader
SELECT RevisionNumber, 
    DATEADD(DD, 1126+number, OrderDate), 
    DATEADD(DD, 1126+number, DueDate), 
    DATEADD(DD, 1126+number, ShipDate), 
    soh.Status, OnlineOrderFlag, SalesOrderNumber, 
    PurchaseOrderNumber, AccountNumber, 
    CustomerID, SalesPersonID, TerritoryID, 
    BillToAddressID, ShipToAddressID, 
    ShipMethodID, CreditCardID, CreditCardApprovalCode, 
    CurrencyRateID, SubTotal, TaxAmt, Freight, 
    TotalDue, Comment, rowguid, 
    DATEADD(DD, 1126+number, ModifiedDate)
FROM AdventureWorks2008R2.Sales.SalesOrderHeader AS soh
CROSS JOIN master.dbo.spt_values AS sv
WHERE sv.type = N'P'
  AND sv.number > 0 AND sv.number < 6
GO 3
 
-- Flush all dirty pages to disk
CHECKPOINT
GO
 
-- Stop the Event Session
ALTER EVENT SESSION MultipleDataFiles
ON SERVER
STATE=STOP
GO

The above script will load roughly 92MB of data into the test table which makes the insert operation smaller than the size of all four data files.  Note that there is an explicit checkpoint in the test to force all dirty pages to be written to the appropriate data files on disk.  Without this checkpoint, the file writes may appear to be imbalanced incorrectly due to the timing of the last automatic checkpoint and the dirty pages in cache when it occurred.  By manually checkpointing the database before ending the event session we ensure we have the file writes captured accurately.  To view the information captured by the event session we will read the event data into a table and then shred the XML into another intermediate table to allow for further analysis of the detailed information if you so desire.  Finally we’ll aggregate the events and pivot the results based on the file_id to see how SQL Server wrote to the database files for the database. 

-- Drop Results tables if they exist
IF OBJECT_ID('MultipleDataFileResults') IS NOT NULL
    DROP TABLE MultipleDataFileResults 
GO
IF OBJECT_ID('MultipleDataFileResultsParsed') IS NOT NULL
    DROP TABLE MultipleDataFileResultsParsed 
GO
 
-- Create results table to load data from XE files
CREATE TABLE MultipleDataFileResults
(RowID int identity primary key, event_data XML)
GO
 
-- Load the event data from the file target
INSERT INTO MultipleDataFileResults(event_data)
SELECT
    CAST(event_data AS XML) AS event_data
FROM sys.fn_xe_file_target_read_file('C:\SQLskills\MultipleDataFiles*.xel', 
                                     'C:\SQLskills\MultipleDataFiles*.xem', 
                                     null, null)
GO
 
-- Parse the event data
SELECT 
    RowID,
    event_data.value('(event/@name)[1]', 'varchar(50)') AS event_name,
    DATEADD(hh, 
            DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP), 
            event_data.value('(event/@timestamp)[1]', 'datetime2')) AS [timestamp],
    COALESCE(event_data.value('(event/data[@name="database_id"]/value)[1]', 'int'), 
             event_data.value('(event/action[@name="database_id"]/value)[1]', 'int')) AS database_id,
    event_data.value('(event/data[@name="mode"]/text)[1]', 'nvarchar(4000)') AS [mode],
    event_data.value('(event/data[@name="file_handle"]/value)[1]', 'nvarchar(4000)') AS [file_handle],
    event_data.value('(event/data[@name="offset"]/value)[1]', 'bigint') AS [offset],
    event_data.value('(event/data[@name="page_id"]/value)[1]', 'int') AS [page_id],
    event_data.value('(event/data[@name="file_id"]/value)[1]', 'int') AS [file_id],
    event_data.value('(event/data[@name="file_group_id"]/value)[1]', 'int') AS [file_group_id],
    event_data.value('(event/data[@name="wait_type"]/text)[1]', 'nvarchar(100)') AS [wait_type],
    event_data.value('(event/data[@name="duration"]/value)[1]', 'bigint') AS [duration],
    event_data.value('(event/action[@name="sql_text"]/value)[1]', 'nvarchar(4000)') AS [sql_text],
    event_data.value('(event/data[@name="cpu"]/value)[1]', 'int') AS [cpu],
    event_data.value('(event/data[@name="reads"]/value)[1]', 'bigint') AS [reads],
    event_data.value('(event/data[@name="writes"]/value)[1]', 'bigint') AS [writes],
    CAST(SUBSTRING(event_data.value('(event/action[@name="attach_activity_id"]/value)[1]', 'varchar(50)'), 1, 36) AS uniqueidentifier) as activity_id,
    CAST(SUBSTRING(event_data.value('(event/action[@name="attach_activity_id"]/value)[1]', 'varchar(50)'), 38, 10) AS int) as event_sequence
INTO MultipleDataFileResultsParsed
FROM MultipleDataFileResults
ORDER BY Rowid
 
-- Aggregate the results by the event name and file_id
SELECT file_id, [file_write_completed],[file_written], [physical_page_write]
FROM
(    SELECT event_name, file_id, COUNT(*) AS occurences
    FROM MultipleDataFileResultsParsed
    WHERE event_name IN ('file_write_completed', 'file_written', 'physical_page_write')
    GROUP BY event_name, file_id
) AS tab
PIVOT
(    MAX(occurences) 
    FOR event_name IN ([file_write_completed],[file_written], [physical_page_write])) AS pvt

The output of our pivot operation shows that the four data files in the UserObjects filegroup are written to relatively evenly, which should be expected based on the amount of free space being equal across the four data files.  One item that should become incredibly apparent is the importance of the transaction log which has twenty-three times the file writes occurring to it than the nearest data file. 

image

Different file size but same free space

As previously stated in the intro to this blog post, proportional fill is based on the amount of free space in each file in relation to the other files and not the actual file sizes themselves.  To demonstrate this, we’ll create a single file database with our table and then load approximately 31MB of data into the table.  Then we’ll increase the size of the first file to 63MB and add three additional files that are 32MB each to the database.

-- Delete target files from previous tests
EXECUTE sp_configure 'show advanced options', 1; RECONFIGURE;
EXECUTE sp_configure 'xp_cmdshell', 1; RECONFIGURE; 
EXEC xp_cmdshell 'DEL C:\SQLskills\MultipleDataFiles*';
EXECUTE sp_configure 'xp_cmdshell', 0; RECONFIGURE;
EXECUTE sp_configure 'show advanced options', 0; RECONFIGURE;
 
-- Drop the test database from the server
USE [master]
GO
IF DB_ID('MultipleDataFiles') IS NOT NULL
BEGIN
    ALTER DATABASE [MultipleDataFiles] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
    DROP DATABASE [MultipleDataFiles];
END
GO
-- Create a single-file database 
CREATE DATABASE [MultipleDataFiles] ON  PRIMARY 
( NAME = N'MultipleDataFiles', 
    FILENAME = N'H:\SQLData\MultipleDataFiles.mdf' , 
    SIZE = 32768KB , 
    FILEGROWTH = 32768KB ), 
FILEGROUP [UserObjects] 
( NAME = N'MultipleDataFiles_UserObjects1', 
    FILENAME = N'H:\SQLData\MultipleDataFiles_UserObjects1.ndf' , 
    SIZE = 32768KB , 
    FILEGROWTH = 32768KB )
LOG ON 
( NAME = N'MultipleDataFiles_log', 
    FILENAME = N'L:\SQLLogs\MultipleDataFiles_log.ldf' , 
    SIZE = 131072KB , 
    FILEGROWTH = 32768KB )
GO
ALTER DATABASE [MultipleDataFiles] 
MODIFY FILEGROUP [UserObjects] DEFAULT
GO
 
-- Create a table to load data into for the tests
USE [MultipleDataFiles]
GO
CREATE TABLE [dbo].[SalesOrderHeader](
    [SalesOrderID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY,
    [RevisionNumber] [tinyint] NOT NULL,
    [OrderDate] [datetime] NOT NULL,
    [DueDate] [datetime] NOT NULL,
    [ShipDate] [datetime] NULL,
    [Status] [tinyint] NOT NULL,
    [OnlineOrderFlag] [bit] NOT NULL,
    [SalesOrderNumber] [nvarchar](25) NOT NULL,
    [PurchaseOrderNumber] [nvarchar](25) NULL,
    [AccountNumber] [nvarchar](15) NULL,
    [CustomerID] [int] NOT NULL,
    [SalesPersonID] [int] NULL,
    [TerritoryID] [int] NULL,
    [BillToAddressID] [int] NOT NULL,
    [ShipToAddressID] [int] NOT NULL,
    [ShipMethodID] [int] NOT NULL,
    [CreditCardID] [int] NULL,
    [CreditCardApprovalCode] [varchar](15) NULL,
    [CurrencyRateID] [int] NULL,
    [SubTotal] [money] NOT NULL,
    [TaxAmt] [money] NOT NULL,
    [Freight] [money] NOT NULL,
    [TotalDue] [money] NOT NULL,
    [Comment] [nvarchar](128) NULL,
    [rowguid] [uniqueidentifier] NOT NULL,
    [ModifiedDate] [datetime] NOT NULL
) 
GO
 
-- Load ~31MB of data into the test database
INSERT INTO dbo.SalesOrderHeader
SELECT RevisionNumber, 
    DATEADD(DD, 1126+number, OrderDate), 
    DATEADD(DD, 1126+number, DueDate), 
    DATEADD(DD, 1126+number, ShipDate), 
    soh.Status, OnlineOrderFlag, SalesOrderNumber, 
    PurchaseOrderNumber, AccountNumber, 
    CustomerID, SalesPersonID, TerritoryID, 
    BillToAddressID, ShipToAddressID, 
    ShipMethodID, CreditCardID, CreditCardApprovalCode, 
    CurrencyRateID, SubTotal, TaxAmt, Freight, 
    TotalDue, Comment, rowguid, 
    DATEADD(DD, 1126+number, ModifiedDate)
FROM AdventureWorks2008R2.Sales.SalesOrderHeader AS soh
CROSS JOIN master.dbo.spt_values AS sv
WHERE sv.type = N'P'
  AND sv.number > 0 AND sv.number < 6
 
-- Grow the first data file to 63MB leaving 32MB free space
ALTER DATABASE [MultipleDataFiles] 
MODIFY FILE (    NAME = N'MultipleDataFiles_UserObjects1', 
                SIZE = 64512KB )
GO
 
-- Add second file with 32MB size
ALTER DATABASE [MultipleDataFiles] 
ADD FILE (    NAME = N'MultipleDataFiles_UserObjects2', 
            FILENAME = N'H:\SQLData\MultipleDataFiles_UserObjects2.ndf' , 
            SIZE = 32768KB , 
            FILEGROWTH = 32768KB ) 
TO FILEGROUP [UserObjects]
GO
 
-- Add third file with 32MB size
ALTER DATABASE [MultipleDataFiles] 
ADD FILE (    NAME = N'MultipleDataFiles_UserObjects3', 
            FILENAME = N'H:\SQLData\MultipleDataFiles_UserObjects3.ndf' , 
            SIZE = 32768KB , 
            FILEGROWTH = 32768KB ) 
TO FILEGROUP [UserObjects]
GO
 
-- Add fourth file with 32MB size
ALTER DATABASE [MultipleDataFiles] 
ADD FILE (    NAME = N'MultipleDataFiles_UserObjects4', 
            FILENAME = N'H:\SQLData\MultipleDataFiles_UserObjects4.ndf' , 
            SIZE = 32768KB , 
            FILEGROWTH = 32768KB ) 
TO FILEGROUP [UserObjects]
GO

With new database setup, the exact same test from the first demo can be run to view how proportional fill functions with one data file larger than the others, but with the same free space.

image

The impact of different free space amounts

Since proportional fill is free space based, lets look at the impact that having different free space in one file has to the writes that occur.  To setup this test, the database will be created with one file sized at 64MB and remaining files sized at 32MB.

-- Delete target files from previous tests
EXECUTE sp_configure 'show advanced options', 1; RECONFIGURE;
EXECUTE sp_configure 'xp_cmdshell', 1; RECONFIGURE; 
EXEC xp_cmdshell 'DEL C:\SQLskills\MultipleDataFiles*';
EXECUTE sp_configure 'xp_cmdshell', 0; RECONFIGURE;
EXECUTE sp_configure 'show advanced options', 0; RECONFIGURE;
 
-- Drop the database from the server
USE [master]
GO
IF DB_ID('MultipleDataFiles') IS NOT NULL
BEGIN
    ALTER DATABASE [MultipleDataFiles] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
    DROP DATABASE [MultipleDataFiles];
END
GO
-- Create a multi-file database with one file larger than the others
CREATE DATABASE [MultipleDataFiles] ON  PRIMARY 
( NAME = N'MultipleDataFiles', 
    FILENAME = N'H:\SQLData\MultipleDataFiles.mdf' , 
    SIZE = 32768KB , 
    FILEGROWTH = 32768KB ), 
FILEGROUP [UserObjects] 
( NAME = N'MultipleDataFiles_UserObjects1', 
    FILENAME = N'H:\SQLData\MultipleDataFiles_UserObjects1.ndf' , 
    SIZE = 65536KB , 
    FILEGROWTH = 32768KB ), 
( NAME = N'MultipleDataFiles_UserObjects2', 
    FILENAME = N'H:\SQLData\MultipleDataFiles_UserObjects2.ndf' , 
    SIZE = 32768KB , 
    FILEGROWTH = 32768KB ), 
( NAME = N'MultipleDataFiles_UserObjects3', 
    FILENAME = N'H:\SQLData\MultipleDataFiles_UserObjects3.ndf' , 
    SIZE = 32768KB , 
    FILEGROWTH = 32768KB ), 
( NAME = N'MultipleDataFiles_UserObjects4', 
    FILENAME = N'H:\SQLData\MultipleDataFiles_UserObjects4.ndf' , 
    SIZE = 32768KB , 
    FILEGROWTH = 32768KB )
 LOG ON 
( NAME = N'MultipleDataFiles_log', 
    FILENAME = N'L:\SQLLogs\MultipleDataFiles_log.ldf' , 
    SIZE = 131072KB , 
    FILEGROWTH = 32768KB )
GO
ALTER DATABASE [MultipleDataFiles] 
MODIFY FILEGROUP [UserObjects] DEFAULT
GO

The exact same test can be rerun and when the event data is parsed, the 64MB file will show roughly twice as many write operations and pages as the 32MB files, right in proportion to its free space.

  image

The impact of autogrowth

One question I had after beginning to look at this was, what impact autogrowth would have on data files that were evenly sized, and using the same sizes for autogrowth? We could easily test this by upping the number of executions for our insert operation from the original test to force the database to grow, but I was really interested in the impact over repeated growth operations, and being impatient I wasn’t really willing to wait for large insert operations to complete.  I instead went back a recreated the database using 8MB data files set to grow by 8MB.  Then I changed the GO 3 batch terminator for the INSERT in the test to a GO 5 to retest the impact of autogrowth.

-- Delete target files from previous tests
EXECUTE sp_configure 'show advanced options', 1; RECONFIGURE;
EXECUTE sp_configure 'xp_cmdshell', 1; RECONFIGURE; 
EXEC xp_cmdshell 'DEL C:\SQLskills\MultipleDataFiles*';
EXECUTE sp_configure 'xp_cmdshell', 0; RECONFIGURE;
EXECUTE sp_configure 'show advanced options', 0; RECONFIGURE;
 
-- Drop the database from the server
USE [master]
GO
IF DB_ID('MultipleDataFiles') IS NOT NULL
BEGIN
    ALTER DATABASE [MultipleDataFiles] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
    DROP DATABASE [MultipleDataFiles];
END
GO
-- Create a multi-file database with one file larger than the others
CREATE DATABASE [MultipleDataFiles] ON  PRIMARY 
( NAME = N'MultipleDataFiles', 
    FILENAME = N'H:\SQLData\MultipleDataFiles.mdf' , 
    SIZE = 32768KB , 
    FILEGROWTH = 32768KB ), 
FILEGROUP [UserObjects] 
( NAME = N'MultipleDataFiles_UserObjects1', 
    FILENAME = N'H:\SQLData\MultipleDataFiles_UserObjects1.ndf' , 
    SIZE = 8192KB , 
    FILEGROWTH = 8192KB ), 
( NAME = N'MultipleDataFiles_UserObjects2', 
    FILENAME = N'H:\SQLData\MultipleDataFiles_UserObjects2.ndf' , 
    SIZE = 8192KB , 
    FILEGROWTH = 8192KB ), 
( NAME = N'MultipleDataFiles_UserObjects3', 
    FILENAME = N'H:\SQLData\MultipleDataFiles_UserObjects3.ndf' , 
    SIZE = 8192KB , 
    FILEGROWTH = 8192KB ), 
( NAME = N'MultipleDataFiles_UserObjects4', 
    FILENAME = N'H:\SQLData\MultipleDataFiles_UserObjects4.ndf' , 
    SIZE = 8192KB , 
    FILEGROWTH = 8192KB )
 LOG ON 
( NAME = N'MultipleDataFiles_log', 
    FILENAME = N'L:\SQLLogs\MultipleDataFiles_log.ldf' , 
    SIZE = 131072KB , 
    FILEGROWTH = 32768KB )
GO
ALTER DATABASE [MultipleDataFiles] 
MODIFY FILEGROUP [UserObjects] DEFAULT
GO

The outcome of the autogrowth tests at five iterations for the INSERT batch seemed odd to me because one of the files was lagging the other three significantly.

image

I wasn’t satisfied with the initial results of a couple of iterations with five batch executions for the INSERT so I decided to validate the results at various scales including ten, twenty and fifty iterations of the INSERT operation.

Using GO 10

image

Using GO 20

image

Using GO 50

image

At the end of each of these series of tests, the files were always within a single autogrowth size of each other, so it is obvious that proportional fill is keeping things relatively equal throughout the tests, but there is the potential for hot spotting of a single data file when auto grow occurs, at least until the other data files grow as well.  In this test the auto grow numbers were kept small, primarily due to storage limitations on the SSD in my laptop, and in the configuration of the VM I was working on, but I am definitely going to make it a point to test this again at a later date using larger autogrowth numbers to see what the impact is longer term and whether the hot spots caused by autogrowth can impact performance significantly?  I have always espoused manual file size management, even in large environments so that certain factors like a filegroup with multiple files can be addressed at the same time.

So there you have it, evidence of how proportional fill functions inside of SQL Server.  Hope you found it interesting.

See you on the playground!

Two weeks ago at SQL Connections in Orlando, FL, I got to participate in a session that Paul and Kimberly do at the end of the conference titled, “Follow the Rabbit.”  The premise of the session is that Paul and Kimberly throw a big list of topics up on the screen and anyone in the audience can ask any question they have about one of those topics and Paul and Kimberly will try to answer it.  I wasn’t the only person to end up participating in this session answering questions, Maciej Pilecki another MVP who also recently passed the Microsoft Certified Masters Exam for SQL Server 2008 answered a number of questions as well.  One of my favorite questions that was asked during this session was “Does index fragmentation matter with SSD’s anymore?”  Paul’s answer to the question was very practical and dealt with the wasted space utilization that excessive index fragmentation can cause in a database that uses a uniqueidentifier column with newid() as the primary key, and given the cost per gigabyte for SSD storage this could be quite significant.  Paul pointed out a couple of other points that escape me, primarily because my mind immediately started churning ideas about how to test the impact that index fragmentation actually has on a database.

It should be no surprise that this blog post is going to use Extended Events, it has after all been my favorite feature in SQL Server since it was first released in an early CTP for SQL Server 2008.  One of the sessions that I presented at SQL Connections last month was on Extended Events and I made some pretty heavy use of the I/O related events as a part of that session.  I also made heavy usage of the I/O related events back in December for a couple of my XEvent a Day blog posts, so I knew that I could really get some detailed information back from the system about what kind of impact fragmentation really had on a database, and since I have a SSD in my laptop, which while not enterprise class is more than worthy of performing the tests on, I thought I’d give a whirl at figuring out how much impact fragmentation really had.

When I began working on this problem, I started out using Windows Server 2008R2 and SQL Server 2008R2, primarily because it is my standard VM platform for the Immersion Training, and for presentations and it was immediately available. I originally intended to use the I/O size information from the file_read_completed event to aggregate the I/O sizes being read from disk, and I soon learned that this was not available in SQL Server 2008 as I had originally thought.  I knew I had used it in the past so I went back to my XEvent a Day blog series and found An XEvent a Day (29 of 31) – The Future – Looking at Database Startup in Denali

Bummer!

I happened to have my old Denali CTP1 VM that I used to create that blog post on a external hard disk so I decided to make use of it for the investigation.  I had originally hoped to be able to create a reproducible demo that anyone could use in their SQL Server 2008 environment to see the impact of fragmentation, but since it is not possible this post will be based on the information available through SQL Server Denali instead.

The first thing that we will need is a database with two tables that have identical data in them; one fragmented and the other not. To create this, we’ll use a uniqueidentifier with newid() as the primary key for the first table and a uniqueidentifier with newsequentialid() as the primary key for the second table. Then we will load approximately 1000 pages worth of data into the first table and then copy that data into the second table, and rebuild the indexes on the second table to remove any residual fragmentation from it.

CREATE DATABASE FragmentationTest
GO
USE FragmentationTest
GO
CREATE TABLE GuidHighFragmentation
(UniqueID UNIQUEIDENTIFIER DEFAULT NEWID() PRIMARY KEY,
 FirstName nvarchar(50) NOT NULL,
 LastName nvarchar(50) NOT NULL)
GO
 
CREATE NONCLUSTERED INDEX IX_GuidHighFragmentation_LastName
ON GuidHighFragmentation(LastName)
GO
 
CREATE TABLE GuidLowFragmentation
(UniqueID UNIQUEIDENTIFIER DEFAULT NEWSEQUENTIALID() PRIMARY KEY,
 FirstName nvarchar(50) NOT NULL,
 LastName nvarchar(50) NOT NULL)
GO
 
CREATE NONCLUSTERED INDEX IX_GuidLowFragmentation_LastName
ON GuidLowFragmentation(LastName)
GO
 
INSERT INTO GuidHighFragmentation (FirstName, LastName)
SELECT TOP 1000
    a.name, b.name
FROM master.dbo.spt_values AS a
CROSS JOIN master.dbo.spt_values AS b
WHERE a.name IS NOT NULL 
    AND b.name IS NOT NULL
ORDER BY NEWID()
GO 70
 
INSERT INTO GuidLowFragmentation (FirstName, LastName)
SELECT FirstName, LastName
FROM GuidHighFragmentation
GO
 
ALTER INDEX ALL ON GuidLowFragmentation REBUILD

With our tables built, we can validate the fragmentation information by querying the sys.dm_index_physical_stats() DMF:

SELECT 
    OBJECT_NAME(ps.object_id),
    i.name,
    ps.index_id,
    ps.index_depth,
    avg_fragmentation_in_percent,
    fragment_count,
    page_count,
    avg_page_space_used_in_percent,
    record_count
FROM sys.dm_db_index_physical_stats(
        DB_ID(), 
        NULL, 
        NULL, 
        NULL, 
        'DETAILED') AS ps
JOIN sys.indexes AS i
    ON ps.object_id = i.object_id
        AND ps.index_id = i.index_id
WHERE index_level = 0

image

Next we will create our event session to capture the I/O events that are related to physical reads from disk.  The event session is being created using dynamic SQL so that each of the events has a predicate on the sqlserver.database_id for the current database.

-- Create an Event Session to investigate our IO operations
IF EXISTS(SELECT * FROM sys.server_event_sessions WHERE name='FragmentationEffect')
    DROP EVENT SESSION [FragmentationEffect] ON SERVER;
DECLARE @sqlcmd nvarchar(4000) = '
CREATE EVENT SESSION FragmentationEffect
ON SERVER
ADD EVENT sqlserver.sql_statement_starting
( ACTION (sqlserver.sql_text)),
ADD EVENT sqlserver.sql_statement_completed
( ACTION (sqlserver.sql_text)),
ADD EVENT sqlserver.file_read
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlserver.file_read_completed
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlserver.physical_page_read
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlos.async_io_requested
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlos.async_io_completed
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+'))--,
ADD TARGET package0.asynchronous_file_target(
     SET filename=''C:\SQLskills\EE_FragmentationEffect.xel'',
         metadatafile=''C:\SQLskills\EE_FragmentationEffect.xem'')
WITH (MAX_MEMORY = 8MB, EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS, 
      TRACK_CAUSALITY = ON, MAX_DISPATCH_LATENCY=5SECONDS)'
EXEC (@sqlcmd)
GO

This event session is configured to have MAX_MEMORY set at 8MB to account for the partitioning of the memory buffers and to ensure that there is ample buffer space for the session to try and mitigate against the potential for event loss.  The EVENT_RETENTION_MODE for the session can not be configured for NO_EVENT_LOSS since the sqlserver.physical_page_read event is a part of the event session.  TRACK_CAUSALITY is turned ON for the event session so that correlation between events can be made to tie the I/O operations back to the statement that generated them, and the MAX_DISPATCH_LATENCY has been set at five seconds because I am impatient and don’t want to have to wait the default of thirty seconds for the events to be dispatched to the targets while doing a demo.

Prior to starting the event session, a manual CHECKPOINT will be issued against the database to flush any dirty pages from the buffer cache to disk, and then the buffer cache will be cleared using DBCC DROPCLEANBUFFERS to ensure that the test statements have to physically read the data from disk into cache.

-- Issue checkpoint to flush dirty buffers to disk
CHECKPOINT
GO
 
-- Clear the Buffer Cache to force reads from Disk 
DBCC DROPCLEANBUFFERS 
GO 

With this completed, we can now start our event session and run the same query against each table to force a scan of all the pages in the table from disk, then stop the event session so that our target file only has events associated with this specific test in it.

-- Start the Event Session
ALTER EVENT SESSION FragmentationEffect
ON SERVER
STATE=START
GO
 
-- Aggregate the data from both tables 
SELECT LastName, COUNT(*)
FROM GuidLowFragmentation
GROUP BY LastName
GO        
SELECT LastName, COUNT(*)
FROM GuidHighFragmentation
GROUP BY LastName
GO
 
-- Wait for the events to dispatch to the target
WAITFOR DELAY '00:00:10'
GO
 
-- Stop the Event Session
ALTER EVENT SESSION FragmentationEffect
ON SERVER
STATE=STOP
GO

Now that we have our data, we need to read it from the file and break down the event data into columns to simplify analysis of the information.  To do this, we’ll first load the event data as XML into a staging table.  Trying to parse the XML while reading from the file through the DMF is incredibly slow by comparison.  Then we can shred the XML into a tabular format based on the events being collected.

-- Drop Results tables if they exist
IF OBJECT_ID('FragmentationEffectResults') IS NOT NULL
    DROP TABLE FragmentationEffectResults 
GO
IF OBJECT_ID('FragmentationEffectResultsParsed') IS NOT NULL
    DROP TABLE FragmentationEffectResultsParsed 
GO
 
-- Create results table to load data from XE files
CREATE TABLE FragmentationEffectResults
(RowID int identity primary key, event_data XML)
GO
 
-- Load the event data from the file target
INSERT INTO FragmentationEffectResults(event_data)
SELECT
    CAST(event_data AS XML) AS event_data
FROM sys.fn_xe_file_target_read_file('C:\SQLskills\EE_FragmentationEffect*.xel', 
                                     'C:\SQLskills\EE_FragmentationEffect*.xem', 
                                     null, null)
GO
 
-- Parse the event data
SELECT 
    RowID,
    event_data.value('(event/@name)[1]', 'varchar(50)') AS event_name,
    DATEADD(hh, 
            DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP), 
            event_data.value('(event/@timestamp)[1]', 'datetime2')) AS [timestamp],
    COALESCE(event_data.value('(event/data[@name="database_id"]/value)[1]', 'int'), 
             event_data.value('(event/action[@name="database_id"]/value)[1]', 'int')) AS database_id,
    event_data.value('(event/data[@name="mode"]/text)[1]', 'nvarchar(4000)') AS [mode],
    event_data.value('(event/data[@name="file_handle"]/value)[1]', 'nvarchar(4000)') AS [file_handle],
    event_data.value('(event/data[@name="offset"]/value)[1]', 'bigint') AS [offset],
    event_data.value('(event/data[@name="page_id"]/value)[1]', 'int') AS [page_id],
    event_data.value('(event/data[@name="file_id"]/value)[1]', 'int') AS [file_id],
    event_data.value('(event/data[@name="file_group_id"]/value)[1]', 'int') AS [file_group_id],
    event_data.value('(event/data[@name="size"]/value)[1]', 'bigint') AS [size],
    event_data.value('(event/data[@name="wait_type"]/text)[1]', 'nvarchar(100)') AS [wait_type],
    event_data.value('(event/data[@name="duration"]/value)[1]', 'bigint') AS [duration],
    event_data.value('(event/action[@name="sql_text"]/value)[1]', 'nvarchar(4000)') AS [sql_text],
    event_data.value('(event/data[@name="cpu"]/value)[1]', 'int') AS [cpu],
    event_data.value('(event/data[@name="reads"]/value)[1]', 'bigint') AS [reads],
    event_data.value('(event/data[@name="writes"]/value)[1]', 'bigint') AS [writes],
    CAST(SUBSTRING(event_data.value('(event/action[@name="attach_activity_id"]/value)[1]', 'varchar(50)'), 1, 36) AS uniqueidentifier) as activity_id,
    CAST(SUBSTRING(event_data.value('(event/action[@name="attach_activity_id"]/value)[1]', 'varchar(50)'), 38, 10) AS int) as event_sequence
INTO FragmentationEffectResultsParsed
FROM FragmentationEffectResults
ORDER BY Rowid

This is where the real fun begins.  There are too many events in results set to be useful in raw form, so what I decided to do was to locate the activity_id from TRACK_CAUSALITY for the sqlserver.sql_statement_starting events for both of the tables, and store those into variables for use in querying off the results set.  The first thing I looked at was how many times did each of the events actually fire for each of the tables.

DECLARE @FragmentationHighActivityID varchar(50),
        @FragmentationLowActivityID varchar(50)
        
SELECT @FragmentationHighActivityID = activity_id
FROM FragmentationEffectResultsParsed
WHERE event_name = 'sql_statement_starting'
  AND sql_text LIKE '%GuidHighFragmentation%'
 
SELECT @FragmentationLowActivityID = activity_id
FROM FragmentationEffectResultsParsed
WHERE event_name = 'sql_statement_starting'
  AND sql_text LIKE '%GuidLowFragmentation%'
 
-- Aggregate the results by the event name and file_id
SELECT event_name, 
    CASE activity_id
        WHEN @FragmentationHighActivityID THEN 'GuidHighFragmentation'
        WHEN @FragmentationLowActivityID THEN 'GuidLowFragmentation'
        ELSE 'UNKNOWN'
    END as QueryTable, 
    COUNT(*) AS occurences
FROM FragmentationEffectResultsParsed
WHERE activity_id IN (@FragmentationHighActivityID, @FragmentationLowActivityID)
  AND event_name IN ('async_io_completed', 'async_io_requested', 
        'file_read', 'file_read_completed', 'physical_page_read')
GROUP BY event_name, activity_id
ORDER BY activity_id, event_name

image

Impressive!  SQL Server had to do ten times the I/O operations against the fragmented table than it did against the non-fragmented table.  You might notice in the output that the number of physical_page_read events is larger than the number of actual pages in the indexes for the table.  Yes and no, I restricted the output of sys.dm_db_index_physical_stats to just level 0 of the indexes, or the leaf level.  There are two additional levels in the nonclustered indexes being scanned but they only account for nine additional pages for the fragmented index and five additional pages for the non-fragmented index.  The other pages are from the system metadata tables which have to be read by the engine as well, which can be confirmed by reading the parsed event data from the dbo.FragmentationEffectResultsParsed table.

Where the impact really gets interesting is when we take a look at the I/O sizes for file_read_completed events and aggregate the results based on the table and the I/O size being read from disk using the same activity_id’s from the previous query for aggregation.

-- Aggregate the file_read_completed events by the IO size and table
SELECT
    CASE activity_id
        WHEN @FragmentationHighActivityID THEN 'GuidHighFragmentation'
        WHEN @FragmentationLowActivityID THEN 'GuidLowFragmentation'
        ELSE 'UNKNOWN'
    END as QueryTable, 
    size/1024 as read_size_kb, 
    size/1024/8 as read_size_pages, 
    COUNT(*) AS occurences
FROM FragmentationEffectResultsParsed
WHERE activity_id IN (@FragmentationHighActivityID, @FragmentationLowActivityID)
  AND event_name IN ('file_read_completed')
GROUP BY database_id, file_id, size, activity_id
ORDER BY QueryTable, size desc
GO

image

I suspected that fragmentation would impact the ability of the database engine to do large sequential reads from the table index while scanning it, but I was not expecting the impact to be as bad as it actually was.  I ran this test numerous times to isolate out if it was some kind of a fluke and the results are consistently repeatable within a relatively low margin of change.  The smallest read size for the non-fragmented index was 64KB where the fragmented table did a majority of its I/O using 64KB reads or smaller.

Now I know what you are thinking, the non-fragmented index in this case is smaller than the fragmented one so it should do less total I/O.  You’d be right to think that, but keep in mind that they both contain the same number of records, demonstrating the wasted space that Paul talked about in the session at SQL Connections.  However, not to worry, we can fix the non-fragmented table and add additional records to it to push the index size over the size of the fragmented one in page count.

-- Add additional data to bring page count higher than the GuidHighFragmentation table!
INSERT INTO GuidLowFragmentation (FirstName, LastName)
SELECT TOP 10000
    a.name, b.name
FROM master.dbo.spt_values AS a
CROSS JOIN master.dbo.spt_values AS b
WHERE a.name IS NOT NULL 
    AND b.name IS NOT NULL
ORDER BY NEWID()
GO 10
 
-- Rebuild the indexes to remove any fragmentation
ALTER INDEX ALL ON GuidLowFragmentation REBUILD
GO

If we go back and rerun our query against sys.dm_db_index_physical_stats the non-fragmented index will have an additional 100000 records and just over 1000 pages.

image

Now we can delete the files from our original test, execute CHECKPOINT and flush the buffer cache and rerun the tests to see what affect more than double the index pages has on the non-fragmented indexes I/O operations.

image

For the event counts, even with more pages in the index, the non-fragmented table still incurs significantly less I/O operations.

image

For the I/O size, the smallest IO performed for the non-fragmented table is still 64KB, and there was a increase in reads at 256KB and 512KB sizes. 

While SSD’s might reduce the IO latency for random read operations, they don’t negate the need to continue to adhere to proper design principals for primary keys, fill factor on indexes, and index maintenance.  The actual runtimes of the two tests were nearly identical, and often flipped back and forth between which one took a few milliseconds longer than the other to complete for this demo.  However, keep in mind that these tables were less than 16MB data total until the non-fragmented table had additional records added to it.  Even at larger data sizes the performance of the two scans will be close enough that the average end user wouldn’t notice the difference, but under the covers the number of I/O operations being performed is significantly different.  One thing to consider is that an SSD while faster still has an upper limit to the number of I/O operations per second (IOPS) that it can perform.  Your average database might not be able to push that limit even with heavy fragmentation, but with the cost of SSD’s still at a premium I wouldn’t waste any of what was available if I had them in my server.

So to wrap up, the answer to the question “Does index fragmentation matter with SSD’s?”  It Depends on if you care about wasting space and wasting I/O more than anything else, but YES it still matters and it still has an impact.

One note about this post:

The demo for this post works with SQL Server 2008 and 2008 R2 with the exception that you don’t get the I/O size back from the file_read_completed event.  You can still see the impact that fragmentation has on the number of I/O operations being performed using the demo exactly as provided from this blog post with SQL Server 2008 and 2008 R2.  I chose to go the Denali route because the impact to the I/O size is quite interesting IMO.

See you on the playground!

The topic for today's post comes from a forums question and a subsequent Connect feedback item where someone noted that the plan_handle being returned by Extended Events using the sqlserver.plan_handle action was not available in the plan cache even when queried immediately following completion of the event that should have produced the plan cache entry.  To setup the appropriate context of this post, let's first take a look at the original repro for this problem.

-- Create the Event Session
IF EXISTS(SELECT * 
          FROM sys.server_event_sessions 
          WHERE name='SQLStmtEvents')
    DROP EVENT SESSION SQLStmtEvents 
    ON SERVER;
GO
CREATE EVENT SESSION SQLStmtEvents
ON SERVER
ADD EVENT sqlserver.sql_statement_completed(
    ACTION (sqlserver.client_app_name,
            sqlserver.plan_handle,
            sqlserver.sql_text,
            sqlserver.tsql_stack,
            package0.callstack,
            sqlserver.request_id)
--Change this to match the AdventureWorks, 
--AdventureWorks2008 or AdventureWorks2008 DB_ID()
WHERE sqlserver.database_id=5 
)
ADD TARGET package0.ring_buffer
WITH (MAX_DISPATCH_LATENCY=5SECONDS, TRACK_CAUSALITY=ON)
GO
 
-- Start the Event Session
ALTER EVENT SESSION SQLStmtEvents 
ON SERVER 
STATE = START;
GO
 
-- Change database contexts and insert one row
USE AdventureWorks2008;
GO
INSERT INTO [dbo].[ErrorLog]([ErrorTime],[UserName],[ErrorNumber],[ErrorSeverity],[ErrorState],[ErrorProcedure],[ErrorLine],[ErrorMessage])
VALUES(getdate(),SYSTEM_USER,-1,-1,-1,'ErrorProcedure',-1,'An error occurred')
GO
 
-- Retrieve the Event Data from the Event Session Target
SELECT
    event_data.value('(event/@name)[1]', 'varchar(50)') AS event_name,
    CAST(event_data.value('(event/action[@name="plan_handle"]/value)[1]', 'varchar(max)') AS XML) as plan_handle,
    event_data.value('(event/action[@name="sql_text"]/value)[1]', 'varchar(max)') AS sql_text
FROM(    SELECT evnt.query('.') AS event_data
        FROM
        (    SELECT CAST(target_data AS xml) AS TargetData
            FROM sys.dm_xe_sessions AS s
            JOIN sys.dm_xe_session_targets AS t
                ON s.address = t.event_session_address
            WHERE s.name = 'SQLStmtEvents'
              AND t.target_name = 'ring_buffer'
        ) AS tab
        CROSS APPLY TargetData.nodes ('RingBufferTarget/event') AS split(evnt) 
     ) AS evts(event_data)
     
-- Use the plan_handle from one of the Events to get the query plan
DECLARE @plan_handle varbinary(64) = 0x06000800DD8D6D0840015585000000000000000000000000
SELECT * 
FROM sys.dm_exec_query_plan(@plan_handle)

It doesn't matter how many times you run the INSERT statement in the above repro, you won't get a plan_handle back from the Event Session that you can query from the plan cache using sys.dm_exec_query_plan().  However, if you were to search the plan cache XML by parsing it, you would find a cached execution plan for the INSERT statement:

SELECT cp.plan_handle, st.text
FROM sys.dm_exec_cached_plans cp 
CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) st
WHERE st.text like '%INSERT%INTO%ErrorLog%'
-- Prevent caching this statement
OPTION(RECOMPILE);

The output of this will be similar to:

image

If you look at the text output you will notice that this isn't exactly the statement that was executed, so what happened here exactly?  When an adhoc statement is submitted to SQL Server, the optimizer will auto parameterize the inline literals with variables to compile the query plan for execution, and this is what is cached in by SQL Server in the case of this INSERT statement.  You can always tell that a plan was auto parameterized by the way that the variables are listed inside of the statement text for the plan.  The topic of plan caching and auto parameterization are covered in detail in the MSDN whitepaper Batch Compilation, Recompilation, and Plan Caching Issues in SQL Server 2005.

The problem in this case is not that Extended Events is returning incorrect information for the plan_handle; the handle that is being returned is valid at the point that the event fired for the adhoc request against SQL Server, the adhoc plan is not cached by the Database Engine, and instead the auto parameterized plan is what is actually cached.  The caching of the auto parameterized plan is actually an optimization inside of SQL Server in this case since the plan for the inline literals is not likely to be reused by the optimizer for subsequent adhoc SQL Statements performing the exact same INSERT operation but with different literals for the values being inserted.  This was discussed at some depth on the SQL Programmability & API Development Team Blog post: 4.0 Query Parameterization.  Under SQL Server 2005 RTM and SQL Server 2005 SP1, there was a significant problem with plan cache bloat related to single use adhoc query plans and a significant change was made to the caching behavior of SQL Server 2005 in SP2 that have been carried forward in the product as detailed in the SQL Programmability & API Development Team Blog post: 3.0 Changes in Caching Behavior between SQL Server 2000, SQL Server 2005 RTM and SQL Server 2005 SP2.

If we change the original repro for this problem to use a parameterized query the plan_handle returned by the sqlserver.plan_handle action will return the same plan_handle that is cached by SQL Server as show in the following modification of the original repro for this problem:

-- Create the Event Session
IF EXISTS(SELECT * 
          FROM sys.server_event_sessions 
          WHERE name='SQLStmtEvents')
    DROP EVENT SESSION SQLStmtEvents 
    ON SERVER;
GO
CREATE EVENT SESSION SQLStmtEvents
ON SERVER
ADD EVENT sqlserver.sql_statement_completed(
    ACTION (sqlserver.client_app_name,
            sqlserver.plan_handle,
            sqlserver.sql_text,
            sqlserver.tsql_stack,
            package0.callstack,
            sqlserver.request_id)
--Change this to match the AdventureWorks, 
--AdventureWorks2008 or AdventureWorks2008 DB_ID()
WHERE sqlserver.database_id=5 
)
ADD TARGET package0.ring_buffer
WITH (MAX_DISPATCH_LATENCY=5SECONDS, TRACK_CAUSALITY=ON)
GO
 
-- Start the Event Session
ALTER EVENT SESSION SQLStmtEvents 
ON SERVER 
STATE = START;
GO
 
-- Change database contexts and insert one row parameterized
USE AdventureWorks2008R2;
GO
DECLARE @ErrorTime datetime = GETDATE(),
        @UserName sysname = SYSTEM_USER,
        @ErrorNumber int = -1,
        @ErrorSeverity int = -1,
        @ErrorState int = -1,
        @ErrorProcedure nvarchar(126) = 'ErrorProcedure',
        @ErrorLine int = 10, 
        @ErrorMessage nvarchar(4000) = 'An error occured'
        
INSERT INTO [dbo].[ErrorLog]
    ([ErrorTime],[UserName],[ErrorNumber],[ErrorSeverity],[ErrorState],
     [ErrorProcedure],[ErrorLine],[ErrorMessage])
VALUES(@ErrorTime,@UserName,@ErrorNumber,@ErrorSeverity,@ErrorState,
        @ErrorProcedure,@ErrorLine,@ErrorMessage)
GO 5
 
-- Retrieve the Event Data from the Event Session Target
SELECT event_name,
    plan_handle,
    sql_text,
    query_plan
FROM
(
SELECT
    event_data.value('(event/@name)[1]', 'varchar(50)') AS event_name,
    CAST(event_data.value('(event/action[@name="plan_handle"]/value)[1]', 'varchar(max)') AS XML) as plan_handle,
    event_data.value('(event/action[@name="sql_text"]/value)[1]', 'varchar(max)') AS sql_text
FROM(    SELECT evnt.query('.') AS event_data
        FROM
        (    SELECT CAST(target_data AS xml) AS TargetData
            FROM sys.dm_xe_sessions AS s
            JOIN sys.dm_xe_session_targets AS t
                ON s.address = t.event_session_address
            WHERE s.name = 'SQLStmtEvents'
              AND t.target_name = 'ring_buffer'
        ) AS tab
        CROSS APPLY TargetData.nodes ('RingBufferTarget/event') AS split(evnt) 
     ) AS evts(event_data)
) AS tab
CROSS APPLY sys.dm_exec_query_plan(plan_handle.value('xs:hexBinary(substring((plan/@handle)[1], 3))', 'varbinary(max)')) as qp

The output of this execution will result in something similar to the following:

image

Note that the plan_handle is the same for every execution of the INSERT operation, and the plan is readily available from the plan cache because the statement executed was parameterized explicitly in this case by the declaration of variables.  This is yet another case for why explicit parameterization of queries is so incredibly important in SQL Server and why adhoc statements, despite the coding simplicity for application developers is a bad design pattern.

One item that you might notice that is different about the parameterized version of the repro for this is that I used the GO 5 operation to execute the parameterized version of the INSERT operation multiple times.  The reason behind this is that when I ran the initial test of the parameterized version of the INSERT tonight, the first plan was not cached by the Database Engine.  You might ask why this was the case, and there is a really good reason.  As a part of the SQLskills Immersion Training I take part in as an instructor, I had the 'optimize for adhoc workloads' sp_configure option set on my laptop, and this causes a plan stub to be generated for adhoc batches, which the above parameterize query actually is, for the first execution, and then the full plan is cached on the second execution only.  To mitigate against the second repro being a problem for someone I decided to have it execute the INSERT five consecutive times to force caching of the plan to show that parameterization does impact the actual plan handle returned by sqlserver.plan_handle action.

Last week, Denny Cherry (Blog|Twitter) asked why the timestamp on the events he was collecting using Extended Events in SQL Server 2008 was incorrect.  I’ve seen this a couple of times on the MSDN Forums, and its come up a couple of times in discussions with other MVP’s about Extended Events.  According to a feedback item I found on Connect for this problem, this is a bug that has been addressed and will be in SQL Server 2008 R2 SP1, and I can only assume a Cumulative Update for SQL Server 2008 that is released in the future as well. 

At the time that Denny asked about this, I happened to have a virtual machine on my laptop that had been running and suspended for a couple of months and I was able to see the issue occurring on it.  Up until this point I had never actually seen the issue occur personally, but with this virtual machine I was able to play around with a event session to see if there was anyway to work around this problem.  I started off with a basic event session that collected the sqlserver.sql_statement_starting event and then added different actions to it to see what other information was available.  I hit pay dirt with the sqlserver.collect_system_time action which reported the correct datetime value in UTC for the events, even when the event timestamp was incorrect as shown in the following eventdata xml:

<event name="sql_statement_starting" package="sqlserver" timestamp="2011-03-04T15:56:30.612Z">
  <data name="state">
    <type name="statement_starting_state" package="sqlserver" />
    <value>0</value>
    <text>Normal</text>
  </data>
  <data name="line_number">
    <type name="int32" package="package0" />
    <value>6</value>
  </data>
  <data name="offset">
    <type name="int32" package="package0" />
    <value>130</value>
  </data>
  <data name="offset_end">
    <type name="int32" package="package0" />
    <value>-1</value>
  </data>
  <data name="statement">
    <type name="unicode_string" package="package0" />
    <value>SELECT @@VERSION</value>
  </data>
  <action name="collect_system_time" package="package0">
    <type name="filetime" package="package0" />
    <value>2011-03-16T00:58:13.792Z</value>
  </action>
</event>

This is a fairly insidious bug that affects every event session on the server, including the default system_health session that is running on every SQL Server 2008 instance.  Unlike the default trace in SQL Server, the Extended Events system_health session can be modified to change the information that is being collected.  While I would not change the events or predicates, I would add the sqlserver.collect_system_time action to each of the events so that if you need information from the system_health session, you know when an event actually fired.  The script for the system_health session is available in the utables.sql script file that is in the Install folder under the instance root for each instance of SQL Server.  Using this as a base you can easily modify it as follows to add the additional action to each event.

-- Extended events default session
IF EXISTS(SELECT * FROM sys.server_event_sessions WHERE name='system_health')
    DROP EVENT SESSION system_health ON SERVER
GO
-- The predicates in this session have been carefully crafted to minimize impact of event collection
-- Changing the predicate definition may impact system performance
--
CREATE EVENT SESSION system_health ON SERVER
ADD EVENT sqlserver.error_reported
(
    ACTION (package0.callstack, sqlserver.session_id, sqlserver.sql_text, sqlserver.tsql_stack, sqlserver.collect_system_time)
    -- Get callstack, SPID, and query for all high severity errors ( above sev 20 )
    WHERE severity >= 20
    -- Get callstack, SPID, and query for OOM errors ( 17803 , 701 , 802 , 8645 , 8651 , 8657 , 8902 )
    OR (error = 17803 OR error = 701 OR error = 802 OR error = 8645 OR error = 8651 OR error = 8657 OR error = 8902)
),
ADD EVENT sqlos.scheduler_monitor_non_yielding_ring_buffer_recorded
(
    ACTION (sqlserver.collect_system_time)
),
ADD EVENT sqlserver.xml_deadlock_report
(
    ACTION (sqlserver.collect_system_time)
),
ADD EVENT sqlos.wait_info
(
    ACTION (package0.callstack, sqlserver.session_id, sqlserver.sql_text, sqlserver.collect_system_time)
    WHERE
    (duration > 15000 AND
        (   
            (wait_type > 31    -- Waits for latches and important wait resources (not locks ) that have exceeded 15 seconds.
                AND
                (
                    (wait_type > 47 AND wait_type < 54)
                    OR wait_type < 38
                    OR (wait_type > 63 AND wait_type < 70)
                    OR (wait_type > 96 AND wait_type < 100)
                    OR (wait_type = 107)
                    OR (wait_type = 113)
                    OR (wait_type > 174 AND wait_type < 179)
                    OR (wait_type = 186)
                    OR (wait_type = 207)
                    OR (wait_type = 269)
                    OR (wait_type = 283)
                    OR (wait_type = 284)
                )
            )
            OR
            (duration > 30000        -- Waits for locks that have exceeded 30 secs.
                AND wait_type < 22
            )
        )
    )
),
ADD EVENT sqlos.wait_info_external
(
    ACTION (package0.callstack, sqlserver.session_id, sqlserver.sql_text, sqlserver.collect_system_time)
    WHERE
    (duration > 5000 AND
        (  
            (    -- Login related preemptive waits that have exceeded 5 seconds.
                (wait_type > 365 AND wait_type < 372)
                OR (wait_type > 372 AND wait_type < 377)
                OR (wait_type > 377 AND wait_type < 383)
                OR (wait_type > 420 AND wait_type < 424)
                OR (wait_type > 426 AND wait_type < 432)
                OR (wait_type > 432 AND wait_type < 435)
            )
            OR
            (duration > 45000     -- Preemptive OS waits that have exceeded 45 seconds.
                AND
                (   
                    (wait_type > 382 AND wait_type < 386)
                    OR (wait_type > 423 AND wait_type < 427)
                    OR (wait_type > 434 AND wait_type < 437)
                    OR (wait_type > 442 AND wait_type < 451)
                    OR (wait_type > 451 AND wait_type < 473)
                    OR (wait_type > 484 AND wait_type < 499)
                    OR wait_type = 365
                    OR wait_type = 372
                    OR wait_type = 377
                    OR wait_type = 387
                    OR wait_type = 432
                    OR wait_type = 502
                )
            )
        )
    )
)
ADD TARGET package0.ring_buffer        -- Store events in the ring buffer target
    (SET MAX_MEMORY = 4096)
WITH (STARTUP_STATE = ON);
GO

When the fix for this bug is released in SQL Server, the customization of the system_health event session can be undone by running the original script in the utables.sql file, which will revert the event session configuration back to its default.

To close out this month’s series on Extended Events we’ll look at the DDL Events for the Event Session DDL operations, and how those can be used to track changes to Event Sessions and determine all of the possible outputs that could exist from an Extended Event Session.  One of my least favorite quirks about Extended Events is that there is no way to determine the Events and Actions that may exist inside a Target, except to parse all of the the captured data.  Information about the Event Session does exist in the Session Definition Catalog Views and Active Session DMV’s, but as you change an Event Sessions Events and Actions while it is running, the information in these change as well, so it is possible that a Target has Events and Actions that are not returned by the current information available about the Event Session.  This is where the DDL Events for the Event Session DDL operations can be useful, if the appropriate framework is deployed.

The DDL Events for Extended Events are not currently documented in the Books Online.  I only recently learned about them from Mike Wachal during a discussion about what I thought was missing from Extended Events.  This is simply an oversight in the documentation, and something that Mike has stated will be fixed, it doesn’t mean that the DDL Events are undocumented and subject to change without notice like other undocumented features of SQL Server.  We can find the DDL Events for Extended Events in the sys.event_notification_event_types.

SELECT
    type,
    type_name,
    parent_type
FROM sys.event_notification_event_types
WHERE type_name LIKE '%SESSION%'

image

These can be used just like any other DDL Event to create a DDL Trigger or Event Notification that takes action when one of the DDL operations occurs.  We can use this to log the DDL to track our changes over time, and we can also use it to create a tracking table of the possible outputs from our Event Session, ensuring that we know what information it may have collected when we parse the Event data from the Targets.  We can also use this information to simplify the generation of our XQuery XPATH statements to parse the data from the Targets with a little extra work. 

In all of my servers, I have a standard database named sqladmin that I keep DBA related information and objects.  For the examples, I will create this database and use it in all the code.  If you have a different database, the scripts can easily be changed to create the objects in that database.  The first thing we’ll do is create our database, and two tables, one for tracking the DDL operations and the other for tracking all of the possible outputs for our Event Session.

CREATE DATABASE sqladmin
GO
USE sqladmin
GO
CREATE TABLE dbo.XEvents_DDLOperations
( DDLEventData XML, 
  ChangeDate DATETIME DEFAULT(CURRENT_TIMESTAMP), 
  LoginName NVARCHAR(256) DEFAULT(SUSER_SNAME()),
  ProgramName NVARCHAR(256) DEFAULT(program_name())
);
GO
CREATE TABLE XEvents_SessionOutputs
(
    EventSessionName NVARCHAR(256),
    EventName NVARCHAR(256),
    EventID INT,
    ColumnID INT,
    ColumnName NVARCHAR(256),
    NodeType NVARCHAR(10),
    DataType NVARCHAR(50),
    XMLLocation NVARCHAR(10),
    TypePrecidence INT
)
GO

The XEvents_SessionOutputs table will have multiple rows for each Event Session defined on the server that track the EventName, the output ColumnName, the NodeType for the data element in the Event XML, the SQL DataType returned by the output, the XMLLocation for where the data of interest exists, and a TypePrecidence value that can be used when multiple Events return the same Data Element with different DataTypes, ensuring that we can pick the most compatible DataType for the output column.  The table also tracks the Event ID in the Event Session for the Event, the Column ID for the output column so that grouping and ordering can be performed during code generation from this table.

To get the SQL DataType that an output returns, we have to look at the output type_name in the Extended Events metadata for the output column or Action.  To make this easier to do and allow for code reuse, I create a view that maps the type_name in Extended Events to corresponding SQL DataType.  Since Maps can be a type_name in Extended Events, the view queries the sys.dm_xe_map_values DMV and calculates the maximum length of the map_value column for each Map, and then uses the nvarchar datatype and rounds the length up to the nearest power of 10 (ok, it doesn’t actually round but that is the effect of the math operations).  For the actual Event data columns in the base payload, the type_name is transposed to the equivalent SQL DataType that is compatible with XQuery.

CREATE VIEW dbo.XETypeToSQLType 
AS
    SELECT 
        XETypeName = mv.name, 
        SQLTypeName = 'nvarchar('+CAST(MAX(LEN(mv.map_value))-(MAX(LEN(mv.map_value))%10) + 10 AS VARCHAR(4))+')',
        XMLLocation = 'text',
        TypePrecidence = 5
    FROM sys.dm_xe_object_columns oc
    LEFT JOIN sys.dm_xe_map_values mv
        ON oc.type_package_guid = mv.object_package_guid
            AND oc.type_name = mv.name
    WHERE oc.column_type = 'data'
      AND mv.name IS NOT NULL
    GROUP BY mv.name
UNION ALL
    SELECT 
        XETypeName = o.name,
        SQLTypeName = CASE 
                            WHEN TYPE_NAME IN ('int8', 'int16', 'int32', 'uint8', 
                                    'uint16', 'uint32', 'float32') 
                                THEN 'int'
                            WHEN TYPE_NAME IN ('int64', 'uint64', 'float64')
                                THEN 'bigint'
                            WHEN TYPE_NAME = 'boolean'
                                THEN 'nvarchar(10)' --true/false returned
                            WHEN TYPE_NAME = 'guid'
                                THEN 'uniqueidentifier'
                            ELSE 'nvarchar(4000)'
                        END,
        XMLLocation = 'value',
        TypePrecidence = CASE 
                            WHEN TYPE_NAME IN ('int8', 'int16', 'int32', 'uint8', 
                                    'uint16', 'uint32', 'float32') 
                                THEN 1
                            WHEN TYPE_NAME IN ('int64', 'uint64', 'float64')
                                THEN 2
                            WHEN TYPE_NAME = 'boolean'
                                THEN 3 --true/false returned
                            WHEN TYPE_NAME = 'guid'
                                THEN 3
                            ELSE 5
                         END
    FROM sys.dm_xe_objects o
    WHERE object_type = 'type'
      AND TYPE_NAME != 'null'
GO

Using this view, we can create another view that queries the Session Definition Catalog Views, to retrieve the output columns for an Event Session in a format that matches our XEvents_SessionOutputs table.

CREATE VIEW dbo.XESession_OutputsFromDMVs
AS
        -- Find a list of all the possible output columns
        SELECT 
            ses.name AS EventSessionName,
            sese.name AS EventName,
            sese.event_id AS EventID,
            oc.column_id AS ColumnID,
            oc.name AS ColumnName,
            'data' AS NodeType,
            xetst.SQLTypeName AS DataType,
            xetst.XMLLocation,
            xetst.TypePrecidence
        FROM sys.server_event_sessions AS ses
        JOIN sys.server_event_session_events AS sese
            ON ses.event_session_id = sese.event_session_id
        JOIN sys.dm_xe_packages AS p 
            ON sese.package = p.name
        JOIN sys.dm_xe_object_columns AS oc 
            ON oc.object_name = sese.name
                AND oc.object_package_guid = p.guid
        JOIN XETypeToSQLType AS xetst
            ON oc.type_name = xetst.XETypeName
        WHERE oc.column_type = 'data'
    UNION
        SELECT 
            ses.name,
            sese.name,
            sesa.event_id,
            999 AS column_id,
            sesa.name,
            'action',
            xetst.SQLTypeName,
            xetst.XMLLocation,
            xetst.TypePrecidence
        FROM sys.server_event_sessions AS ses
        JOIN sys.server_event_session_events AS sese
            ON ses.event_session_id = sese.event_session_id
        JOIN sys.server_event_session_actions AS sesa
            ON ses.event_session_id = sesa.event_session_id
                AND sesa.event_id = sese.event_id
        JOIN sys.dm_xe_packages AS p
            ON sesa.package = p.name
        JOIN sys.dm_xe_objects AS o
            ON p.guid = o.package_guid
                AND sesa.name = o.name
        JOIN XETypeToSQLType AS xetst
            ON o.type_name = xetst.XETypeName
        WHERE o.object_type = 'action'
GO

We can then create a Server Level DDL Trigger for the DDL_EVENT_SESSION_EVENTS group that will log the DDL operation to the XEvents_DDLOperations table, and at the same time populate the output information in the XEvents_SessionOutputs table when an Event Session is created, add any new outputs when an Event Session is altered, and delete the Event Session information when an Event Session is dropped.  By adding new outputs when a Event Session is altered, we maintain a record of the original outputs, even if the Event was dropped from the Event Session.

CREATE TRIGGER XEvents_DDLTrigger
ON ALL SERVER 
FOR DDL_EVENT_SESSION_EVENTS
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @EventData XML = EVENTDATA();
    INSERT INTO sqladmin.dbo.XEvents_DDLOperations (DDLEventData)
    VALUES (@EventData);

    DECLARE @EventType NVARCHAR(256) = @EventData.value('(EVENT_INSTANCE/EventType)[1]', 'nvarchar(256)')
    DECLARE @SessionName NVARCHAR(256) = @EventData.value('(EVENT_INSTANCE/ObjectName)[1]', 'nvarchar(256)')

    IF @EventType = 'CREATE_EVENT_SESSION'
    BEGIN
        INSERT INTO sqladmin.dbo.XEvents_SessionOutputs 
            (EventSessionName, EventName, EventID, ColumnID, ColumnName, NodeType,
             DataType, XMLLocation, TypePrecidence)
        SELECT EventSessionName, EventName, EventID, ColumnID, ColumnName, NodeType,
             DataType, XMLLocation, TypePrecidence
        FROM sqladmin.dbo.XESession_OutputsFromDMVs
        WHERE EventSessionName = @SessionName
    END

    IF @EventType = 'ALTER_EVENT_SESSION'
    BEGIN
        -- Add any new outputs to the Table
        INSERT INTO sqladmin.dbo.XEvents_SessionOutputs
            (EventSessionName, EventName, EventID, ColumnID, ColumnName, NodeType,
             DataType, XMLLocation, TypePrecidence)
        SELECT vdmv.EventSessionName, vdmv.EventName, vdmv.EventID, vdmv.ColumnID, vdmv.ColumnName, vdmv.NodeType,
             vdmv.DataType, vdmv.XMLLocation, vdmv.TypePrecidence
        FROM sqladmin.dbo.XESession_OutputsFromDMVs vdmv
        LEFT JOIN sqladmin.dbo.XEvents_SessionOutputs xeso
            ON vdmv.EventSessionName = xeso.EventSessionName
                AND vdmv.EventName = xeso.EventName
                AND vdmv.ColumnName = vdmv.ColumnName
        WHERE vdmv.EventSessionName = @SessionName
          AND xeso.EventSessionName IS NULL
    END

    IF @EventType = 'DROP_EVENT_SESSION'
    BEGIN
        -- Delete the Output data for the Event Session
        DELETE sqladmin.dbo.XEvents_SessionOutputs
        WHERE EventSessionName = @SessionName
    END
END
GO

If we recreate the TrackResourceWaits Event Session from yesterday’s post and then query the XEvents_SessionOutputs table, we can see the outputs that we can expect from that Event Session:

SELECT *
FROM sqladmin.dbo.XEvents_SessionOutputs
WHERE EventSessionName = 'TrackResourceWaits'

image

Using this information, we can also write a query to generate our XQuery statements for each of the outputs, as well as a column definition stub if we wanted to create a table to hold this information for analysis.

SELECT 
    'event_data.value(''(event/'+NodeType+'[@name="'+ColumnName+'"]/'+XMLLocation+')[1]'', '''+DataType+''') AS '+QUOTENAME(ColumnName)+',' AS XQuery,
    QUOTENAME(ColumnName)+' '+DataType+', ' AS ColumnDefinition
FROM
(
    SELECT 
        ROW_NUMBER() OVER (PARTITION BY ColumnName ORDER BY TypePrecidence DESC) AS partitionid,
        EventSessionName,
        EventID,
        ColumnID,
        ColumnName,
        NodeType,
        DataType,
        XMLLocation
    FROM sqladmin.dbo.XEvents_SessionOutputs
) AS tab
WHERE EventSessionName = 'TrackResourceWaits'
  AND partitionid = 1
ORDER BY EventID, ColumnID

image

The information in the XQuery column can be copied and pasted into our TSQL Script for parsing the Event Data from the ring_buffer, pair_matching, or asynchronous_file_target Targets.  You could also use this as the basis for writing your own Extended Events Target Data code generator, similar to the one that Adam Machanic created a year ago.

That’s it for this months series on Extended Events.  You can find links to all of the posts on the round up post from December 1, An XEvent A Day: 31 days of Extended Events.  Hopefully its been informative, and you now have a better understanding of how Extended Events can be used inside of SQL Server 2008, 2008R2, and in Denali CTP1.

While attending PASS Summit this year, I got the opportunity to hang out with Brent Ozar (Blog|Twitter) one afternoon while he did some work for Yanni Robel (Blog|Twitter).  After looking at the wait stats information, Brent pointed out some potential problem points, and based on that information I pulled up my code for my PASS session the next day on Wait Statistics and Extended Events and made some changes to one of the demo’s so that the Event Session only focused on those potentially problematic waits that had been identified, and sent Brent the DDL so that he could give Extended Events a shot.  Within a few minutes, we were able track down to the statement level in a couple of stored procedures, the causes of those waits, and after some analysis Brent was able to offer some suggestions to Yanni about how to reduce the waits.

Understanding how SQL Server waits to continue execution can be key to improving performance since time spent waiting is time lost during the execution of a SQL Statement.  I love looking at wait statistics and the chapter that I wrote for SQL Server 2008 Internals and Troubleshooting was SQL Server Waits and Extended Events.  Information about wait statistics has been available in SQL Server for a long time, and many of the vendors that develop monitoring applications for SQL Server have polling methods that query sys.dm_os_waiting_tasks or sys.sysprocesses to capture wait information about the tasks that are currently active in the system.  However one of the shortcomings of a polling method is that it misses a lot of the wait information because it is a point in time snapshot only.  If the polling interval is every second, only the active waits that exist at that second are captured, and any waits that occur between the polling interval is missed.  This information is still accumulated in sys.dm_os_wait_stats, but it is impossible to track it back to the statement level from that DMV.

Extended Events offers us the ability to capture information about waits without missing any of the information.  Already in this series we’ve seen how to use Extended Events with the Bucketizer Target to count the occurrences of waits by type.  This isn’t really a great use of Extended Events since sys.dm_os_wait_stats counts the occurrences of the wait types already, and a differential analysis of the information contained in sys.dm_os_wait_stats can provide this information.  The purpose of that example was to discuss the bug that existed in the RTM release of SQL Server 2008 more than it was to provide a sensible use for the target.  However, if we wanted to break our waits down by database, we could bucket on the database_id, and begin to understand which database had the most waits associated with it, but not by the individual wait type.  To get to that level of information, we need to collect all of the waits and the associated information for them to do further analysis.

There are two Events in Extended Events associated with wait types; sqlos.wait_info and sqlos.wait_info_external.  Looking at the description of the Events in the Metadata DMV’s we can get an idea of when each Event will fire.

SELECT name, description 
FROM sys.dm_xe_objects
WHERE name LIKE 'wait_info%'

image

The sqlos.wait_info_external Event will fire for wait types that begin with PREEMPTIVE_ in the name, and the sqlos.wait_info Event will fire for the other wait types that occur on the server.  Glenn Alan Berry (Blog|Twitter) has a great script that filters queries sys.dm_os_wait_stats and filters out common waits that are not generally problematic.  You can find his script on his blog post Updated SQL 2005 and 2008 Diagnostic Queries.  You can use this script to identify the most common waits on a server, and then use that information to build an Event Session that captures the session and statement information for those individual wait types.  In SQL Server 2008, there are 484 wait types listed in sys.dm_os_wait_stats and there are 599 map_value’s for the wait_types Map in sys.dm_xe_map_values.  The reason this is different is that the Map was created from the header file for the wait types and there are padded values that exist in the Map that don’t really correspond to wait types that exist in SQL Server.  However, there are also a couple of Maps for the wait_types that don’t match the wait type in sys.dm_os_wait_stats, the most notable being the ASYNC_NETWORK_IO to NETWORK_IO.

To build the Event Session, we just need to query sys.dm_map_values for our wait_types and use the map_key's in the Predicate definition of the sqlos.wait_info or sqlos.wait_info_external Event as appropriate.  We can also do the same thing to build a generic Event Session that tracks the most common resource related waits.

SELECT map_key, map_value 
FROM sys.dm_xe_map_values
WHERE name = 'wait_types'
  AND ((map_key > 0 AND map_key < 22) -- LCK_ waits
            OR (map_key > 31 AND map_key < 38) -- LATCH_ waits
            OR (map_key > 47 AND map_key < 54) -- PAGELATCH_ waits
            OR (map_key > 63 AND map_key < 70) -- PAGEIOLATCH_ waits
            OR (map_key > 96 AND map_key < 100) -- IO (Disk/Network) waits
            OR (map_key = 107) -- RESOURCE_SEMAPHORE waits
            OR (map_key = 113) -- SOS_WORKER waits
            OR (map_key = 120) -- SOS_SCHEDULER_YIELD waits
            OR (map_key = 178) -- WRITELOG waits
            OR (map_key > 174 AND map_key < 177) -- FCB_REPLICA_ waits
            OR (map_key = 186) -- CMEMTHREAD waits
            OR (map_key = 187) -- CXPACKET waits
            OR (map_key = 207) -- TRACEWRITE waits
            OR (map_key = 269) -- RESOURCE_SEMAPHORE_MUTEX waits
            OR (map_key = 283) -- RESOURCE_SEMAPHORE_QUERY_COMPILE waits
            OR (map_key = 284) -- RESOURCE_SEMAPHORE_SMALL_QUERY waits
        )

Once we have the list of map_key’s defined, we can do a replace in SSMS and change map_key to wait_type and define the predicate for the sqlos.wait_info Event for our Event Session.

CREATE EVENT SESSION [TrackResourceWaits] ON SERVER 
ADD EVENT  sqlos.wait_info
(    -- Capture the database_id, session_id, plan_handle, and sql_text
    ACTION(sqlserver.database_id,sqlserver.session_id,sqlserver.sql_text,sqlserver.plan_handle)
    WHERE
        (opcode = 1 --End Events Only
            AND duration > 100 -- had to accumulate 100ms of time
            AND ((wait_type > 0 AND wait_type < 22) -- LCK_ waits
                    OR (wait_type > 31 AND wait_type < 38) -- LATCH_ waits
                    OR (wait_type > 47 AND wait_type < 54) -- PAGELATCH_ waits
                    OR (wait_type > 63 AND wait_type < 70) -- PAGEIOLATCH_ waits
                    OR (wait_type > 96 AND wait_type < 100) -- IO (Disk/Network) waits
                    OR (wait_type = 107) -- RESOURCE_SEMAPHORE waits
                    OR (wait_type = 113) -- SOS_WORKER waits
                    OR (wait_type = 120) -- SOS_SCHEDULER_YIELD waits
                    OR (wait_type = 178) -- WRITELOG waits
                    OR (wait_type > 174 AND wait_type < 177) -- FCB_REPLICA_ waits
                    OR (wait_type = 186) -- CMEMTHREAD waits
                    OR (wait_type = 187) -- CXPACKET waits
                    OR (wait_type = 207) -- TRACEWRITE waits
                    OR (wait_type = 269) -- RESOURCE_SEMAPHORE_MUTEX waits
                    OR (wait_type = 283) -- RESOURCE_SEMAPHORE_QUERY_COMPILE waits
                    OR (wait_type = 284) -- RESOURCE_SEMAPHORE_SMALL_QUERY waits
                )
        )
)
ADD TARGET package0.ring_buffer(SET max_memory=4096)
WITH (EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS,
      MAX_DISPATCH_LATENCY=5 SECONDS)
GO

 

Now that we have the Event Session defined, we can start it as needed to collect the resource wait information for our system.  The only concern with this Event Session is the Target being used.  If the Event Session is going to run for a long period of time, or if the waits on the server being monitored occur in large quantities, the Target should be changed away from the ring_buffer to the asynchronous_file_target.  I configured this session to only collect waits that exceed 100ms in duration.  If you want waits that have shorter durations this can easily be changed.  If you set the duration to be > 0 a lot of 1-5ms waits will be collected that aren’t generally interesting.  To query the wait information from this Event Session using the ring_buffer:

-- Extract the Event information from the Event Session 
SELECT 
    event_data.value('(event/@name)[1]', 'varchar(50)') AS event_name,
    DATEADD(hh, 
        DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP), 
        event_data.value('(event/@timestamp)[1]', 'datetime2')) AS [timestamp],
    COALESCE(event_data.value('(event/data[@name="database_id"]/value)[1]', 'int'), 
        event_data.value('(event/action[@name="database_id"]/value)[1]', 'int')) AS database_id,
    event_data.value('(event/action[@name="session_id"]/value)[1]', 'int') AS [session_id],
    event_data.value('(event/data[@name="wait_type"]/text)[1]', 'nvarchar(4000)') AS [wait_type],
    event_data.value('(event/data[@name="opcode"]/text)[1]', 'nvarchar(4000)') AS [opcode],
    event_data.value('(event/data[@name="duration"]/value)[1]', 'bigint') AS [duration],
    event_data.value('(event/data[@name="max_duration"]/value)[1]', 'bigint') AS [max_duration],
    event_data.value('(event/data[@name="total_duration"]/value)[1]', 'bigint') AS [total_duration],
    event_data.value('(event/data[@name="signal_duration"]/value)[1]', 'bigint') AS [signal_duration],
    event_data.value('(event/data[@name="completed_count"]/value)[1]', 'bigint') AS [completed_count],
    event_data.value('(event/action[@name="plan_handle"]/value)[1]', 'nvarchar(4000)') AS [plan_handle],
    event_data.value('(event/action[@name="sql_text"]/value)[1]', 'nvarchar(4000)') AS [sql_text]
FROM 
(    SELECT XEvent.query('.') AS event_data 
    FROM 
    (    -- Cast the target_data to XML 
        SELECT CAST(target_data AS XML) AS TargetData 
        FROM sys.dm_xe_session_targets st 
        JOIN sys.dm_xe_sessions s 
            ON s.address = st.event_session_address 
        WHERE name = 'TrackResourceWaits' 
          AND target_name = 'ring_buffer'
    ) AS Data 
    -- Split out the Event Nodes 
    CROSS APPLY TargetData.nodes ('RingBufferTarget/event') AS XEventData (XEvent)   
) AS tab (event_data)

 

In the result set, you will notice that some of the wait_info Events do not have an associated session_id, database_id, plan_handle, or sql_text value.  Depending on where the wait actually occurs in code, this information is not available to the firing Event, for example, the NETWORK_IO Event generally does not successfully collect these Actions.

While collecting session and statement level waits like this is certainly interesting, there are some considerations that have to be made whenever you look at wait information like this.  The first consideration is that, while a specific session or statement waited on a resource, that doesn’t necessarily mean that the problem exists within that session or statement.  Take for example a query that has to wait 500ms on ASYNC_IO_COMPLETION waits.  At the same time that query is executing there are 10 DSS queries running that scan large ranges of data from the database data files and generate a lot of IO activity.  Where exactly is the problem?  The root problem is that there is a disk IO bottleneck, but not necessarily related to the query that is waiting on disk IO, it could be another query performing Table Scan that is leading to the heavy IO activity.

As I have said previously in this series, one of my favorite aspects of Extended Events is that it allows you to look at what is going on under the covers in SQL Server, at a level that has never previously been possible.  SQL Server Denali CTP1 includes a number of new Events that expand on the information that we can learn about how SQL Server operates and in today’s blog post we’ll look at how we can use those Events to look at what happens when a database starts up inside of SQL Server.

First lets create our Event Session, which will collect a large number of events that relate to the operations that occur when a database starts in SQL Server.  

DECLARE @sqlcmd nvarchar(max) =
'IF EXISTS(SELECT * FROM sys.server_event_sessions WHERE
name=''MonitorStartupLogRecovery'')
   DROP EVENT SESSION [MonitorStartupLogRecovery] ON SERVER;
CREATE EVENT SESSION [MonitorStartupLogRecovery]
ON SERVER
ADD EVENT sqlserver.database_started 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.databases_log_file_used_size_changed 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.databases_log_flush 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.databases_log_flush_wait 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.file_read 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.file_read_completed 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.file_write_completed 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.file_written 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.log_block_cache 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.log_block_consume 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.log_blocks_uncache 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.log_cache_buffer_refcounter_change 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.log_consumer_act 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.log_flush_complete 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.log_flush_requested 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.log_flush_start 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.log_single_record 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.new_log_interest_flip 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.redo_single_record 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.redo_target_set 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + ')),
ADD EVENT sqlserver.transaction_log 
(WHERE (database_id = ' + CAST(DB_ID('AdventureWorks2008R2') AS varchar(3)) + '))
ADD TARGET package0.asynchronous_file_target(
     SET filename=''C:\SQLBlog\MonitorStartupLogRecovery.xel'',
         metadatafile=''C:\SQLBlog\MonitorStartupLogRecovery.xem'')
WITH (MAX_MEMORY = 8192KB, EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS, STARTUP_STATE = ON)'
EXEC(@sqlcmd)
GO

With the Event Session created, we can make some changes that write to our test database to see what happens when the database is recovered at startup. We are going to can make two changes to the database.  First we’ll begin a transaction and create a table with 10 rows of data in it without committing the transaction. 

USE AdventureWorks2008R2
GO
-- Begin a Transaction and leave it open
BEGIN TRANSACTION
-- Create the First Table
SELECT TOP 10 *
INTO TestTable
FROM Sales.SalesOrderDetail sod
GO

Now in a New Query Window, we’ll create a second table with 10 rows of data without opening a transaction, and then force a dirty shutdown of the Database Engine.

USE AdventureWorks2008R2
GO
-- Create a Second Table
SELECT TOP 10 *
INTO TestTable2
FROM Sales.SalesOrderDetail sod
GO
-- Flush changes to data file
CHECKPOINT
GO
-- Force Shutdown the Engine
SHUTDOWN

Once SHUTDOWN is issued, the process terminates, and the Database Engine will need to be restarted from the Services Snapin, the SQL Server Configuration Manager, or through SSMS.  When the Engine starts up, the Event Session will become active, and the Events will be logged to the package0.asynchronous_file_target for analysis.  Once recovery completes, we can drop the Event Session from the Server, so that the buffers flush, and we can then begin our analysis of the collected information.

USE tempdb
GO

-- Read the Raw Event data into a table
SELECT CAST(event_data AS XML) AS event_data
INTO TargetEvents
FROM sys.fn_xe_file_target_read_file('C:\SQLBlog\MonitorStartupLogRecovery*.xel', 'C:\SQLBlog\MonitorStartupLogRecovery*.xem', null, null)

-- Fetch the Event Data from the Raw Event Data into another table
SELECT 
    event_data.value('(event/@name)[1]', 'varchar(50)') AS event_name,
    DATEADD(hh, 
            DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP), 
            event_data.value('(event/@timestamp)[1]', 'datetime2')) AS [timestamp],
    COALESCE(event_data.value('(event/data[@name="database_id"]/value)[1]', 'int'), 
             event_data.value('(event/action[@name="database_id"]/value)[1]', 'int')) AS database_id,
    event_data.value('(event/data[@name="count"]/value)[1]', 'bigint') AS [count],
    event_data.value('(event/data[@name="start_log_block_id"]/value)[1]', 'bigint') AS [start_log_block_id],
    event_data.value('(event/data[@name="is_read_ahead"]/value)[1]', 'nvarchar(4000)') AS [is_read_ahead],
    event_data.value('(event/data[@name="private_consumer_id"]/value)[1]', 'bigint') AS [private_consumer_id],
    event_data.value('(event/data[@name="mode"]/text)[1]', 'nvarchar(4000)') AS [mode],
    event_data.value('(event/data[@name="file_handle"]/value)[1]', 'nvarchar(4000)') AS [file_handle],
    event_data.value('(event/data[@name="offset"]/value)[1]', 'bigint') AS [offset],
    event_data.value('(event/data[@name="file_id"]/value)[1]', 'int') AS [file_id],
    event_data.value('(event/data[@name="filegroup_id"]/value)[1]', 'int') AS [filegroup_id],
    event_data.value('(event/data[@name="size"]/value)[1]', 'bigint') AS [size],
    event_data.value('(event/data[@name="path"]/value)[1]', 'nvarchar(4000)') AS [path],
    event_data.value('(event/data[@name="duration"]/value)[1]', 'bigint') AS [duration],
    event_data.value('(event/data[@name="io_data"]/value)[1]', 'nvarchar(4000)') AS [io_data],
    event_data.value('(event/data[@name="resource_type"]/text)[1]', 'nvarchar(4000)') AS [resource_type],
    event_data.value('(event/data[@name="owner_type"]/text)[1]', 'nvarchar(4000)') AS [owner_type],
    event_data.value('(event/data[@name="transaction_id"]/value)[1]', 'bigint') AS [transaction_id],
    event_data.value('(event/data[@name="lockspace_workspace_id"]/value)[1]', 'nvarchar(4000)') AS [lockspace_workspace_id],
    event_data.value('(event/data[@name="lockspace_sub_id"]/value)[1]', 'int') AS [lockspace_sub_id],
    event_data.value('(event/data[@name="lockspace_nest_id"]/value)[1]', 'int') AS [lockspace_nest_id],
    event_data.value('(event/data[@name="resource_0"]/value)[1]', 'int') AS [resource_0],
    event_data.value('(event/data[@name="resource_1"]/value)[1]', 'int') AS [resource_1],
    event_data.value('(event/data[@name="resource_2"]/value)[1]', 'int') AS [resource_2],
    event_data.value('(event/data[@name="object_id"]/value)[1]', 'int') AS [object_id],
    event_data.value('(event/data[@name="associated_object_id"]/value)[1]', 'bigint') AS [associated_object_id],
    event_data.value('(event/data[@name="resource_description"]/value)[1]', 'nvarchar(4000)') AS [resource_description],
    event_data.value('(event/data[@name="database_name"]/value)[1]', 'nvarchar(4000)') AS [database_name],
    event_data.value('(event/data[@name="log_block_id"]/value)[1]', 'bigint') AS [log_block_id],
    event_data.value('(event/data[@name="log_block_size"]/value)[1]', 'int') AS [log_block_size],
    event_data.value('(event/data[@name="from_disk"]/value)[1]', 'nvarchar(4000)') AS [from_disk],
    event_data.value('(event/data[@name="incomplete"]/value)[1]', 'nvarchar(4000)') AS [incomplete],
    event_data.value('(event/data[@name="cache_buffer_pointer"]/value)[1]', 'nvarchar(4000)') AS [cache_buffer_pointer],
    event_data.value('(event/data[@name="consumer_id"]/value)[1]', 'bigint') AS [consumer_id],
    event_data.value('(event/data[@name="old_weight"]/value)[1]', 'int') AS [old_weight],
    event_data.value('(event/data[@name="new_weight"]/value)[1]', 'int') AS [new_weight],
    event_data.value('(event/data[@name="new_position"]/value)[1]', 'int') AS [new_position],
    event_data.value('(event/data[@name="last_log_block_id"]/value)[1]', 'bigint') AS [last_log_block_id],
    event_data.value('(event/data[@name="weight"]/value)[1]', 'int') AS [weight],
    event_data.value('(event/data[@name="address"]/value)[1]', 'nvarchar(4000)') AS [address],
    event_data.value('(event/data[@name="type"]/text)[1]', 'nvarchar(4000)') AS [type],
    event_data.value('(event/data[@name="current_count"]/value)[1]', 'int') AS [current_count],
    event_data.value('(event/data[@name="change_type"]/value)[1]', 'int') AS [change_type],
    event_data.value('(event/data[@name="activity_id"]/value)[1]', 'int') AS [activity_id],
    event_data.value('(event/data[@name="write_size"]/value)[1]', 'int') AS [write_size],
    event_data.value('(event/data[@name="rows"]/value)[1]', 'int') AS [rows],
    event_data.value('(event/data[@name="pending_writes"]/value)[1]', 'int') AS [pending_writes],
    event_data.value('(event/data[@name="pending_bytes"]/value)[1]', 'int') AS [pending_bytes],
    event_data.value('(event/data[@name="reason"]/text)[1]', 'nvarchar(4000)') AS [reason],
    event_data.value('(event/data[@name="waiters"]/value)[1]', 'int') AS [waiters],
    event_data.value('(event/data[@name="error"]/value)[1]', 'int') AS [error],
    event_data.value('(event/data[@name="slot_id"]/value)[1]', 'int') AS [slot_id],
    event_data.value('(event/data[@name="used_size"]/value)[1]', 'int') AS [used_size],
    event_data.value('(event/data[@name="reservation_size"]/value)[1]', 'bigint') AS [reservation_size],
    event_data.value('(event/data[@name="log_op_id"]/value)[1]', 'int') AS [log_op_id],
    event_data.value('(event/data[@name="log_op_name"]/value)[1]', 'nvarchar(4000)') AS [log_op_name],
    event_data.value('(event/data[@name="interest"]/value)[1]', 'nvarchar(4000)') AS [interest],
    event_data.value('(event/data[@name="cache_type"]/value)[1]', 'int') AS [cache_type],
    event_data.value('(event/data[@name="keys"]/value)[1]', 'nvarchar(4000)') AS [keys],
    event_data.value('(event/data[@name="stop_mark"]/value)[1]', 'nvarchar(4000)') AS [stop_mark],
    event_data.value('(event/data[@name="operation"]/text)[1]', 'nvarchar(4000)') AS [operation],
    event_data.value('(event/data[@name="success"]/value)[1]', 'nvarchar(4000)') AS [success],
    event_data.value('(event/data[@name="index_id"]/value)[1]', 'int') AS [index_id],
    event_data.value('(event/data[@name="log_record_size"]/value)[1]', 'int') AS [log_record_size],
    event_data.value('(event/data[@name="context"]/text)[1]', 'nvarchar(4000)') AS [context],
    event_data.value('(event/data[@name="replication_command"]/value)[1]', 'int') AS [replication_command],
    event_data.value('(event/data[@name="transaction_start_time"]/value)[1]', 'nvarchar(4000)') AS [transaction_start_time]
INTO Results
FROM TargetEvents

Now we can begin to analyze the information that we collected by querying the Results table.  Looking at the Results as a whole, we can see the database opened by reading the first page of the database and then the database boot page (page_id=9) and page 32 of the primary data file.  Then the engine scans each of the VLF’s of the transaction log.  We can tell that the log reads are scans of the VLF’s by looking at the DBCC LOGINFO information for the database and comparing the file_read offsets from the Event Session to the StartOffset of each of the VLF’s in the DBCC LOGINFO output.

DBCC LOGINFO

image

SELECT event_name, timestamp, database_id, file_id, mode, offset, 
    CASE WHEN file_id = 1 THEN offset/8192 ELSE NULL END AS page_id, size, 
    log_block_id, log_block_size, start_log_block_id, last_log_block_id, 
    from_disk, consumer_id, activity_id, log_op_id, log_op_name, change_type, 
    operation, object_id, index_id, log_record_size, slot_id, used_size, 
    reservation_size, write_size, rows, pending_writes, pending_bytes, 
    context, waiters
FROM Results

image 

After the startup scans the VLF’s 120K of information is read from the log file, and the log buffers start to be consumed to determine the redo start point for recovery.  We can filter our Event data to remove Events that while interesting are not necessary for our analysis at the moment, as well as to reduce the number of columns being returned from the data set.

SELECT 
    event_name, timestamp, file_id, mode, offset, size, log_block_id, 
    COALESCE(log_op_name, operation) as [operation], slot_id, 
    object_id, index_id, log_record_size, context, write_size, rows
FROM Results
WHERE event_name NOT IN ('log_consumer_act', 'log_single_record', 'log_cache_buffer_refcounter_change', 'file_read') 

image

With the filtered results, we can see the log reads into cache and the setting of the redo target.  If we scroll down further, we can get a better picture of what is happening.

image

The last active log blog was consumed, and the redo target was set at that log_block_id.  Then the log is reread starting at offset 318976 and the blocks get cached and the redo operations begin against the database.  Scrolling through the results further, we can see that the redo operations continue as the log blocks increase up to the Checkpoint operation that was executed immediately before the Shutdown of the instance occurred, at the log block that was set as the redo target originally.

image

At this point the data file begins to be read so that the undo operations can be performed before making the database available.

image

After the undo completes the database_started Event is raised and that database becomes available for use.

image

To validate that the changes we see occurring after the redo operations and before the database_started Event, we can set the database OFFLINE, recreate our Event Session, and then bring the database back ONLINE again, and then compare the logged Events when no changes have occurred to our original Events when known changes have occurred.  I am not going to do that in this blog post, but will instead leave that up to the reader to investigate on their own.

The Database Compression feature in SQL Server 2008 Enterprise Edition can provide some significant reductions in storage requirements for SQL Server databases, and in the right implementations and scenarios performance improvements as well.  There isn’t really a whole lot of information about the operations of database compression that is documented as being available in the DMV’s or SQL Trace.  Paul Randal pointed out on Twitter today that sys.dm_db_index_operational_stats() provides the page_compression_attempt_count and page_compression_success_count available.  Beyond that the only other documented information for monitoring Data Compression are the Page Compression Attempts/sec and Pages Compressed/sec Performance Counters of the SQL Server:Access Methods object in Perfmon (http://msdn.microsoft.com/en-us/library/cc280449.aspx). 

There is one thing in common about the documented methods of monitoring Data Compression, and that is they all only deal with Page compression, and not Row compression, and in Extended Events we find the same commonality as there are no Row compression Events in Extended Events.  There are two Page compression Events in Extended Events; sqlserver.page_compression_attempt_failed and sqlserver.page_compression_tracing.  These two Events can be used to track Page compression operations at multiple levels, including database, object, index, and even down to the individual page. The sqlserver.page_compression_tracing Event provides Start and End tracing of Page compression operations inside of the Database Engine and returns the database_id, index_id, rowset_id, page_id, and duration of the compression operation.  The sqlserver.page_compression_attempt_failed is really poorly named, and doesn’t provide information about failures in the sense that something broke, but provides information for why a page compression attempt did not actually change the compression of the data in the page.  It also returns the database_id, index_id, rowset_id, and page_id for the compression attempt, and it also includes a failure_reason column which correlates to the page_compression_failure_reason Map Value.

-- Get the payload information for the Events 
SELECT 
    object_name, 
    column_id, 
    name, 
    type_name
FROM sys.dm_xe_object_columns
WHERE object_name IN ('page_compression_tracing', 
                      'page_compression_attempt_failed')
  AND column_type = 'data'

image

To demonstrate how these Events function, I am going to use the LineItem table from the TPC-H Benchmark that was created by Quest Benchmark Factory using Level 2 for the table sizing, which makes the table just at 1.8GB in size.  All of the indexes on the table will be rebuilt using PAGE compression, and then 10,000 rows will be added to the table.  To setup the environment, first load the TPC-H LineItem table with the appropriate seed of data, this can be done with the free trial version of Benchmark Factory.  Then rebuild all of the indexes on the LineItem table using PAGE compression, and review the PAGE compression statistics from sys.dm_db_index_operational_stats for the database and object.

USE [TPCH]
GO
-- Rebuild the indexes with Page compression 
ALTER INDEX ALL ON dbo.H_Lineitem REBUILD WITH (DATA_COMPRESSION = PAGE)
GO
-- Look at the compression information in sys.dm_db_index_operational_stats
SELECT 
    database_id, 
    object_id, 
    index_id, 
    page_compression_attempt_count, 
    page_compression_success_count,
    (page_compression_attempt_count - page_compression_success_count) as page_compression_failure_count
FROM sys.dm_db_index_operational_stats(db_id('TPCH'), object_id('H_Lineitem'), null, null)
GO

image

Once the table and its indexes have been rebuilt using PAGE compression, we can then create our Event Session, start it, and add 10,000 rows to the LineItem table.  After we add the rows, we can then check the page compression statistics in sys.dm_db_index_operational_stats, and drop our Event Session from the server.

-- Create an Event Session to Track the Failed attempts
CREATE EVENT SESSION PageCompressionTracing
ON SERVER
ADD EVENT sqlserver.page_compression_attempt_failed,
ADD EVENT sqlserver.page_compression_tracing
ADD TARGET package0.asynchronous_file_target(
     SET filename='C:\SQLBlog\PageCompressionTracing.xel',
         metadatafile='C:\SQLBlog\PageCompressionTracing.xem')
WITH (MAX_MEMORY = 8MB, EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS, MAX_DISPATCH_LATENCY=5SECONDS)
GO
-- Start the Event Session
ALTER EVENT SESSION PageCompressionTracing
ON SERVER
STATE=START
GO
-- Insert 10000 rows into the H_Lineitem table
INSERT INTO H_Lineitem
    (l_orderkey, l_partkey, l_suppkey, l_linenumber, l_quantity, 
     l_extendedprice, l_discount, l_tax, l_returnflag, l_linestatus, 
     l_shipdate, l_commitdate, l_receiptdate, l_shipinstruct, l_shipmode, 
     l_comment)
SELECT TOP 10000 
     l_orderkey, l_partkey, l_suppkey, l_linenumber, l_quantity, 
     l_extendedprice, l_discount, l_tax, l_returnflag, l_linestatus, 
     l_shipdate, l_commitdate, l_receiptdate, l_shipinstruct, l_shipmode, 
     l_comment
FROM H_Lineitem
GO
-- Look at the compression information in sys.dm_db_index_operational_stats
SELECT 
    database_id, 
    object_id, 
    index_id, 
    page_compression_attempt_count, 
    page_compression_success_count,
    (page_compression_attempt_count - page_compression_success_count) as page_compression_failure_count
FROM sys.dm_db_index_operational_stats(db_id('TPCH'), object_id('H_Lineitem'), null, null)
GO
-- Drop the Event Session
DROP EVENT SESSION PageCompressionTracing
ON SERVER
GO

image

Now we can parse the Events that were captured by our Event Session and compare the information presented by sys.dm_db_index_operational_stats() with what was collected by Extended Events.

-- Create our result Analysis database
CREATE DATABASE [PageCompTestResults]
GO
USE [PageCompTestResults]
GO
-- Create intermediate temp table for raw event data
CREATE TABLE RawEventData
(Rowid int identity primary key, event_data xml)
GO
-- Read the file data into intermediate temp table
INSERT INTO RawEventData (event_data)
SELECT
    CAST(event_data AS XML) AS event_data
FROM sys.fn_xe_file_target_read_file('C:\SQLBlog\PageCompressionTracing*.xel', 
                                     'C:\SQLBlog\PageCompressionTracing*.xem', 
                                     null, null)
GO
-- Fetch the Event Data from the Event Session Target
SELECT 
    RowID,
    event_data.value('(event/@name)[1]', 'varchar(50)') AS event_name,
    DATEADD(hh, 
            DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP), 
            event_data.value('(event/@timestamp)[1]', 'datetime2')) AS [timestamp],
    COALESCE(event_data.value('(event/data[@name="database_id"]/value)[1]', 'int'), 
             event_data.value('(event/action[@name="database_id"]/value)[1]', 'int')) AS database_id,
    event_data.value('(event/data[@name="file_id"]/value)[1]', 'int') AS [file_id],
    event_data.value('(event/data[@name="page_id"]/value)[1]', 'int') AS [page_id],
    event_data.value('(event/data[@name="rowset_id"]/value)[1]', 'bigint') AS [rowset_id],
    event_data.value('(event/data[@name="failure_reason"]/text)[1]', 'nvarchar(150)') AS [failure_reason],
    event_data.value('(event/action[@name="system_thread_id"]/value)[1]', 'int') AS [system_thread_id],
    event_data.value('(event/action[@name="scheduler_id"]/value)[1]', 'int') AS [scheduler_id],
    event_data.value('(event/action[@name="cpu_id"]/value)[1]', 'int') AS [cpu_id]
INTO ParsedResults
FROM RawEventData
GO

After parsing out the data, we can begin to really leverage the information we’ve gathered.  If we join the ParsedResults table to sys.partitions for our TPCH database by rowset_id = hobt_id, we can get the object_id and index_id and aggregate the failure reasons up to the object and index level.

SELECT 
    pr.database_id, 
    p.object_id, 
    p.index_id,
    failure_reason, 
    COUNT(*) as failure_count
FROM TPCH.sys.partitions p
JOIN ParsedResults pr
    ON pr.rowset_id = p.hobt_id
WHERE event_name = 'page_compression_attempt_failed'
GROUP BY     pr.database_id, 
    p.object_id, 
    p.index_id,
    failure_reason
GO
-- Look at the compression information in sys.dm_db_index_operational_stats
SELECT 
    database_id, 
    object_id, 
    index_id, 
    page_compression_attempt_count, 
    page_compression_success_count,
    (page_compression_attempt_count - page_compression_success_count) as page_compression_failure_count
FROM sys.dm_db_index_operational_stats(db_id('TPCH'), object_id('H_Lineitem'), null, null)
GO

image

With this we can se that the Extended Events sqlserver.page_compression_attempt_failed Event tracks failures and attempts that are not counted in sys.dm_db_index_operational_stats().  The PageModCountBelowThreshold failure isn’t really a failed attempt at compression.  This reason shows that the page was evaluated for recalculation, and the modified counter for the page hadn’t passed the internal threshold for recalculation so the actual compression operation wasn’t performed.  If we look at the sqlserver.page_compression_tracing Event information, we can see how the numbers begin to come together to match what is output by sys.dm_db_index_operational_stats().

SELECT 
    pr.database_id, 
    p.object_id, 
    p.index_id,
    COUNT(*) as attempt_count
FROM TPCH.sys.partitions p
JOIN ParsedResults pr
    ON pr.rowset_id = p.hobt_id
WHERE event_name = 'page_compression_tracing'
  AND opcode = 'Begin'
GROUP BY     pr.database_id, 
    p.object_id, 
    p.index_id
GO
-- Look at the compression information in sys.dm_db_index_operational_stats
SELECT 
    database_id, 
    object_id, 
    index_id, 
    page_compression_attempt_count, 
    page_compression_success_count,
    (page_compression_attempt_count - page_compression_success_count) as page_compression_failure_count
FROM sys.dm_db_index_operational_stats(db_id('TPCH'), object_id('H_Lineitem'), null, null)
GO

image

We have 193 attempts by this Event, and we have 72 PageModCountBelowThreshold failures, matching our actual attempts of 121 from the DMF.  We can then subtract out the other failures and get the 93 successful operations matching the DMF as well.

Nearly two years ago Kalen Delaney blogged about Splitting a page into multiple pages, showing how page splits occur inside of SQL Server.  Following her blog post, Michael Zilberstein wrote a post, Monitoring Page Splits with Extended Events, that showed how to see the sqlserver.page_split Events using Extended Events.  Eladio Rincón also blogged about Using XEvents (Extended Events) in SQL Server 2008 to detect which queries are causing Page Splits, but not in relation to Kalen’s blog post.  Both of these blog posts demonstrate how to get the sqlserver.page_split Events, but as discussed in the comments section of Michael Zilberstein’s blog post, the Event fires for all page splits and Adam Machanic and I talked after Eladio’s blog post and opened a connect item to have the sqlserver.page_split Event extended in the product so that you know what kind of split is actually occurring.

https://connect.microsoft.com/SQLServer/feedback/details/388482/sql-server-extended-events-page-split-event-additions

The CTP1 release of Denali has significant changes to the sqlserver.page_split Event, that makes it easier to find the splitting object as well the type of split that is occurring.  Before we look at that, I am going to show the code required to get the object and index information from SQL Server 2008, which is based on Adam’s comments to use sys.dm_os_buffer_descriptors.  For the examples in this blog post I am going use Kalen’s multipage split example from her blog post referenced above.

	-- Create the table 
	USE tempdb;
	GO
	SET NOCOUNT ON
	GO
	IF EXISTS (SELECT * FROM sys.tables
	            WHERE name = 'split_page')
	    DROP TABLE split_page;
	GO
	CREATE TABLE split_page 
	(id INT IDENTITY(0,2) PRIMARY KEY,
	id2 bigint DEFAULT 0,
	data1 VARCHAR(33) NULL, 
	data2 VARCHAR(8000) NULL);
	GO
	-- fill page until no more rows fit
	INSERT INTO split_page DEFAULT VALUES;
	GO 385
	-- verify that there is only one data page 
	DBCC IND(tempdb, split_page, -1);
	-- Create MonitorPageSplits Extended Event Session 
	IF (SELECT 1 FROM sys.server_event_sessions WHERE name = 'MonitorPageSplits') IS NOT NULL 
	   DROP EVENT SESSION MonitorPageSplits ON SERVER 
	GO 
	CREATE EVENT SESSION MonitorPageSplits ON SERVER 
	ADD EVENT sqlserver.page_split 
	( 
	    ACTION (sqlserver.database_id, sqlserver.sql_text)   
	    WHERE sqlserver.database_id = 2 
	) 
	ADD TARGET package0.ring_buffer 
	WITH(MAX_DISPATCH_LATENCY = 1 SECONDS)
	GO 
	-- Start the MonitorPageSplits Event Session 
	ALTER EVENT SESSION MonitorPageSplits ON SERVER STATE = start; 
	GO 
	-- Now insert one more row, this time filling the VARCHARs to the maximum length. 
	SET IDENTITY_INSERT split_page  ON;
	GO
	INSERT INTO split_page (id, id2, data1, data2)
	      SELECT 111, 0, REPLICATE('a', 33), REPLICATE('b', 8000);
	GO
	SET IDENTITY_INSERT split_page  OFF;
	GO 
	ALTER EVENT SESSION MonitorPageSplits ON SERVER 
	DROP EVENT sqlserver.page_split; 
	GO 
	-- Wait to allow dispatch to complete
	WAITFOR DELAY '00:00:01.000' 
	GO
	SELECT oTab.*
	  , p.OBJECT_ID
	  , p.index_id
	  , OBJECT_NAME(p.OBJECT_ID)
	  , i.name
	FROM
	(
	SELECT 
	    XEvent            = XEvent.query('.') 
	  , time              = XEvent.value('(@timestamp)[1]','datetime') 
	  , FILE_ID           = XEvent.value('(data[@name=''file_id'']/value)[1]','int') 
	  , page_id           = XEvent.value('(data[@name=''page_id'']/value)[1]','int') 
	  , database_id       = XEvent.value('(action[@name=''database_id'']/value)[1]','int') 
	  , sql_text          = XEvent.value('(action[@name=''sql_text'']/value)[1]','varchar(max)') 
	FROM 
	( 
	   SELECT CAST(target_data AS XML) AS target_data 
	   FROM sys.dm_xe_session_targets xst 
	   JOIN sys.dm_xe_sessions xs ON xs.address = xst.event_session_address 
	   WHERE xs.name = 'MonitorPageSplits' 
	) AS tab (target_data) 
	CROSS APPLY target_data.nodes('/RingBufferTarget/event') AS EventNodes(XEvent) 
	) AS oTab
	LEFT JOIN sys.dm_os_buffer_descriptors AS obd
	   ON obd.database_id = oTab.database_id
	       AND obd.FILE_ID = oTab.FILE_ID
	       AND obd.page_id = oTab.page_id
	LEFT JOIN sys.allocation_units au
	   ON au.allocation_unit_id = obd.allocation_unit_id
	LEFT JOIN sys.partitions p 
	   ON p.partition_id = au.container_id  
	LEFT JOIN sys.indexes i
	   ON p.OBJECT_ID = i.OBJECT_ID
	       AND p.index_id = i.index_id
	-- verify that there is only one data page 
	DBCC IND(tempdb, split_page, -1);
	
	

The above code creates a table in tempdb, loads one page of data in it exactly as in Kalen’s blog post, and then creates an Event Session for the sqlserver.page_split Event in tempdb, that also collects the sqlserver.database_id and sqlserver.sql_text actions when the Event fires.  After triggering the page split, it drops the Event from the Event Session and then uses WAITFOR DELAY to allow the events to be buffered to the package0.ring_buffer Target.  Then it shreds the XML and joins to the DMV’s to get the object and index names.  The output of running the above script in SQL Server 2008 should be similar to the following, showing 10 split events and 10 additional pages in the database table.

 image

Note that the only two columns returned by the sqlserver.page_split Event are the file_id and page_id.  In SQL Server Denali CTP1, the sqlserver.page_split event now has a much larger Event payload associated with it.  It now returns the file_id, page_id, database_id (as a part of the event, not requiring an action), rowset_id, splitOperation, new_page_file_id, and the new_page_page_id associated with the page_split Event.   This makes the Event much more useful and allows it to be used without having to query the buffer descriptors to find the object association.  The following demo is identical to the demo for SQL Server 2008 listed above with the exception of that the XQuery is slightly different (a requirement to pull the new information from the XML).

	-- Create the table 
	USE tempdb;
	GO
	SET NOCOUNT ON
	GO
	IF EXISTS (SELECT * FROM sys.tables
	            WHERE name = 'split_page')
	    DROP TABLE split_page;
	GO
	CREATE TABLE split_page 
	(id INT IDENTITY(0,2) PRIMARY KEY,
	id2 bigint DEFAULT 0,
	data1 VARCHAR(33) NULL, 
	data2 VARCHAR(8000) NULL);
	GO
	-- fill page until no more rows fit
	INSERT INTO split_page DEFAULT VALUES;
	GO 385
	-- verify that there is only one data page 
	DBCC IND(tempdb, split_page, -1);
	-- Create MonitorPageSplits Extended Event Session 
	IF (SELECT 1 FROM sys.server_event_sessions WHERE name = 'MonitorPageSplits') IS NOT NULL 
	   DROP EVENT SESSION MonitorPageSplits ON SERVER 
	GO 
	CREATE EVENT SESSION MonitorPageSplits ON SERVER 
	ADD EVENT sqlserver.page_split 
	( 
	    ACTION (sqlserver.database_id, sqlserver.sql_text)   
	    WHERE sqlserver.database_id = 2 
	) 
	ADD TARGET package0.ring_buffer 
	WITH (MAX_DISPATCH_LATENCY = 1 SECONDS)
	GO 
	-- Start the MonitorPageSplits Event Session 
	ALTER EVENT SESSION MonitorPageSplits ON SERVER STATE = start; 
	GO 
	-- Now insert one more row, this time filling the VARCHARs to the maximum length. 
	SET IDENTITY_INSERT split_page  ON;
	GO
	INSERT INTO split_page (id, id2, data1, data2)
	      SELECT 111, 0, REPLICATE('a', 33), REPLICATE('b', 8000);
	GO
	SET IDENTITY_INSERT split_page  OFF;
	GO 
	ALTER EVENT SESSION MonitorPageSplits ON SERVER 
	DROP EVENT sqlserver.page_split; 
	
	GO
	SELECT 
	    event_time         = XEvent.value('(@timestamp)[1]','datetime') 
	  , orig_file_id      = XEvent.value('(data[@name=''file_id'']/value)[1]','int') 
	  , orig_page_id      = XEvent.value('(data[@name=''page_id'']/value)[1]','int') 
	  , database_id           = XEvent.value('(data[@name=''database_id'']/value)[1]','int') 
	  , OBJECT_ID         = p.OBJECT_ID
	  , index_id          = p.index_id
	  , OBJECT_NAME           = OBJECT_NAME(p.OBJECT_ID)
	  , index_name            = i.name
	  , rowset_id         = XEvent.value('(data[@name=''rowset_id'']/value)[1]','bigint') 
	  , splitOperation        = XEvent.value('(data[@name=''splitOperation'']/text)[1]','varchar(255)') 
	  , new_page_file_id  = XEvent.value('(data[@name=''new_page_file_id'']/value)[1]','int') 
	  , new_page_page_id  = XEvent.value('(data[@name=''new_page_page_id'']/value)[1]','int') 
	  , sql_text          = XEvent.value('(action[@name=''sql_text'']/value)[1]','varchar(max)') 
	FROM 
	( 
	   SELECT CAST(target_data AS XML) AS target_data 
	   FROM sys.dm_xe_session_targets xst 
	   JOIN sys.dm_xe_sessions xs ON xs.address = xst.event_session_address 
	   WHERE xs.name = 'MonitorPageSplits' 
	) AS tab (target_data) 
	CROSS APPLY target_data.nodes('/RingBufferTarget/event') AS EventNodes(XEvent) 
	LEFT JOIN sys.allocation_units au
	   ON au.container_id = XEvent.value('(data[@name=''rowset_id'']/value)[1]','bigint') 
	LEFT JOIN sys.partitions p 
	   ON p.partition_id = au.container_id  
	LEFT JOIN sys.indexes i
	   ON p.OBJECT_ID = i.OBJECT_ID
	       AND p.index_id = i.index_id
	-- View the Page allocations 
	DBCC IND(tempdb, split_page, -1);
	

If you run the above demo the output should be similar to the below (if you click on the picture, it will open up larger).  One thing that should become immediately obvious is that the same demo in Denali is doing 1/3rd of the page splits that occur in SQL Server 2008. 

image

The old_page_id and new_page_id tell where the page originated and moved to, and the splitOperation tells the type of split.  In this case only two of the type of splits are occurring; SPLIT_FOR_ROOT_NODE which occurs when the first page allocated is split into multiple pages, and SPLIT_FOR_INSERT which occurs as the inserts continue and the pages are split to accommodate the data.  There are a number of additional split operations that exist in SQL Server Denali CTP1 including, SPLIT_FOR_DELETE, SPLIT_FOR_GHOST, SPLIT_FOR_INTERNAL_NODE, and SPLIT_FOR_UPDATE.  I’ve tried to figure out how to correlate the output from DBCC IND with the data held in the Event Session for page splits to correlate the old_page_id and new_page_id to identify problematic splits, but haven’t finalized validation of my tests yet (hopefully I can finish this work and I’ll write an update to this blog post showing how to do this at some point in the near future).  One item that I have noted in my testing is that mid-page splits generally generate multiple sqlserver.page_split Events in the same operation, similar to the demonstrations used in this example, where as end-page splits for identity and sequential GUID inserts do not.  I am not certain that this is a valid conclusion to come to at this point and have further testing to do to investigate page splits more.

There are 7 Session level options that can be configured in Extended Events that affect the way an Event Session operates.  These options can impact performance and should be considered when configuring an Event Session.  I have made use of a few of these periodically throughout this months blog posts, and in today’s blog post I’ll cover each of the options separately, and provide further information about their usage.  Mike Wachal from the Extended Events team at Microsoft, talked about the Session options on his blog post, Option Trading: Getting the most out of the event session options, and I’d recommend giving it a read for additional information as well.

EVENT_RETENTION_MODE

The EVENT_RETENTION_MODE option specifies how the Event Session handles Event loss when Events generate faster than they can be dispatched to the Targets.  There are three possible values for this option; ALLOW_SINGLE_EVENT_LOSS, ALLOW_MULTIPLE_EVENT_LOSS, and NO_EVENT_LOSS.  This option directly affects the possible impact that an Event Session may have on the performance of a system while the Event Session is active.  A trade off occurs between performance impact and the guarantee whether all Events are captured or not.

ALLOW_SINGLE_EVENT_LOSS

The ALLOW_SINGLE_EVENT_LOSS value is the system default for all Event Sessions where the EVENT_RETENTION_MODE is not explicitly specified as a part of the Event Session definition.  This value allows single events to be dropped and lost from the session when the memory buffers for the Event Session are full and dispatch to the Targets can not keep up with the Event generation. 

ALLOW_MULTIPLE_EVENT_LOSS

The ALLOW_MULTIPLE_EVENT_LOSS value allows an entire memory buffer containing multiple events to be dropped and lost when the memory buffers are full and the Events are generating faster than the buffers can be dispatched to the Targets.  This can minimize the performance impact on the server at the trade off that many Events could potentially be lost, with the number of Events lost depending on the size of the Events being generated, the configuration of the MAX_MEMORY session option, and the MEMORY_PARTITION_MODE session option. 

NO_EVENT_LOSS

The NO_EVENT_LOSS value guarantees that all Events that fire are captured, but at the expense of possible system performance degradation when the Event Session is active.  If the memory buffers are all full and an Event fires, the task firing the Event will wait until space is available in a memory buffer for the Event to be buffered.  This option value is not recommended by the Extended Events team at Microsoft for most Event Sessions and should be used with extreme caution and only when it is absolutely necessary that every Event be captured, even at the expense of degraded performance of the system.

MAX_DISPATCH_LATENCY

The MAX_DISPATCH_LATENCY option specifies the time in seconds that Events are held in a memory buffer that is not full before being dispatched to the asynchronous session Targets.  The default value if the MAX_DISPATCH_LATENCY is not explicitly defined in the Session definition is 30 seconds, and the option has a minimum value of 1 second.  If a value of 0 or INFINITE is specified, the Events held in a memory buffer will not be dispatched until the memory buffer becomes full.

MAX_EVENT_SIZE

The MAX_EVENT_SIZE option specifies the maximum size in kilobytes or megabytes an individual Event can be.  The default value for this option when it is not explicitly set in the Session definition is 0KB, allowing the maximum Event size to be the size of a single memory buffer in the Event Session.  This option can be explicitly set to allow Events that are larger than a single memory buffer to be captured by the Event Session.  The minimum value for this option is 64KB.

MAX_MEMORY

The MAX_MEMORY option specifies the amount of memory in kilobytes or megabytes that is allocated to the memory buffers for the Event Session.  The value of this options is divided evenly amongst the memory buffers that are created for the Event Session based on the configuration of the MEMORY_PARTITION_MODE session option.  The MAX_MEMORY option can be used to increase the memory available for buffering Events when a large number of Events are expected to fire, minimizing Event loss due to full memory buffers.  The default value for this option is 4 megabytes (MB) or 4096 kilobytes (KB). 

Mike Wachal blogged about this option on the Extended Events blog Take it to the MAX (and beyond), and again in response to a number of questions that I sent him early on in this blog series when I was working on a large NUMA based server, Session memory – who’s this guy named Max and what’s he doing with my memory?

MEMORY_PARTITION_MODE

The MEMORY_PARTITION_MODE option specifies how the memory buffers for the Event Session are created and/or partitioned.  For servers with multiple processors and/or multiple NUMA nodes the memory buffers can become a bottleneck performance wise if multiple CPU’s are firing Events and have to wait on a memory buffer to buffer the Event information being collected.  There are three values for this option; NONE, PER_NODE, and PER_CPU. 

NONE

The NONE value specifies that a single set of memory buffers will be created for the Event Session.  In this configuration, three memory buffers are created for the Event Session, and the memory for the Event Session is divided evenly, to the nearest 64KB boundary, amongst the three memory buffers.  This is the default value for an Event Session if the MEMORY_PARTITION_MODE is not explicitly defined.

PER_NODE

The PER_NODE value specifies that a separate set of three memory buffers will be created.  In this configuration, three memory buffers are created for each NUMA node that exists for the SQL Server Instance, and the memory is divided evenly, to the nearest 64KB boundary, amongst all of the memory buffers.  

PER_CPU

The PER_CPU value specifies that a set of memory buffers is created for each CPUs/Scheduler that is assigned to the SQL Server Instance.  In this configuration, the number of memory buffers is 2.5 times the number of CPUs/Schedulers available, and the memory is divided evenly, to the nearest 64KB boundary, amongst all of the memory buffers.

STARTUP_STATE

The STARTUP_STATE option specifies whether an Event Session automatically starts in an Active state when the SQL Server Instance starts up.  There are two valid values for this option, ON and OFF, with OFF being the default.

TRACK_CAUSALITY

The TRACK_CAUSALITY option specifies whether causality tracking across multiple Events is turned ON or OFF.  The default configuration for this option is OFF.  When TRACK_CAUSALITY is turned on, an additional Action, package0.attach_activity_id, is added to each Event that fires in the Event Session.  This Action is a combination GUID and sequence number that allows related Events to be tracked for cause and effect analysis of the Events that fired in the order in which they have fired.

I should make note of the fact that in many cases, the options specified in the blog posts, may not be appropriate for a production implementation, and may have been made based on the fact that I just didn’t want to wait over multiple test cycles for Events to dispatch to the Targets.

In the spirit of today’s holiday, a couple of people have been posting SQL related renditions of holiday songs.  Tim Mitchell posted his 12 days of SQL Christmas, and Paul Randal and Kimberly Tripp went as far as to record themselves sing SQL Carols on their blog post Our Christmas Gift To You: Paul and Kimberly Singing!  For today’s post on Extended Events I give you the 12 days of Christmas, Extended Events style (all of these are based on true facts about Extended Events in SQL Server).

On the first day of Christmas Extended Events gave to me: a chance to write a useful GUI.
On the second day of Christmas Extended Events gave to me: two sqlserver packages.
On the third day of Christmas Extended Events gave to me: three new DDL Commands.
On the fourth day of Christmas Extended Events gave to me: four ETW channels.
On the fifth day of Christmas Extended Events gave to me: five in-memory targets.
On the sixth day of Christmas Extended Events gave to me: six keyword maps.
On the seventh day of Christmas Extended Events gave to me: seven session options.
On the eighth day of Christmas Extended Events gave to me: eight file io events.
On the ninth day of Christmas Extended Events gave to me: nine database_log events.
On the tenth day of Christmas Extended Events gave to me: ten new DMV’s.
On the eleventh day of Christmas Extended Events gave to me: eleven Operational events.
On the twelfth day of Christmas Extended Events gave to me: twelve sqlos Actions.

One of the actions inside of Extended Events is the package0.callstack and the only description provided by sys.dm_xe_objects for the object is 16-frame call stack.  If you look back at The system_health Session blog post, you’ll notice that the package0.callstack Action has been added to a number of the Events that the PSS team thought were of significance to include in the Event Session.  We can trigger an event that will by logged by our system_health Event Session by raising an error of severity >=20 with the RAISERROR functionality in TSQL.

-- Generate a Severity 20 Error to trigger system_health
-- sqlserver.error_reported Event
RAISERROR(50001, 20, 1, 'This is an Error!') WITH LOG

image

After raising the error, we can query the system_health Event Session for the callstacks that have been collected by adding into our XQuery a filter for the action node with the @name attribute = “callstack”

SELECT n.query('.') AS callstack
FROM
(
    SELECT CAST(target_data as xml)
    FROM sys.dm_xe_sessions AS s 
    INNER JOIN sys.dm_xe_session_targets AS t
        ON s.address = t.event_session_address
    WHERE s.name = 'system_health'
      AND t.target_name = 'ring_buffer'
) AS src (target_data)
CROSS APPLY target_data.nodes('RingBufferTarget/event/action[@name="callstack"]') as q(n)

This will only return the action nodes for the callstack and the XML fragment will be similar to the following:

    <action name="callstack" package="package0">
      <type name="callstack" package="package0" />
      <value>0x0000000001CD4F55
0x000000000113A310
0x0000000000BEA7D0
0x0000000001A3A0CC
0x0000000002FA3EAE
0x0000000000BC9616
0x0000000000BCABBB
0x0000000000BCA4D9
0x0000000000BCD10B
0x0000000000BC7C9B
0x0000000000B6163B
0x0000000000B612FA
0x0000000000B60E35
0x00000000010E0E50
0x00000000010E09A0
0x00000000010F9AB0</value>
      <text />
    </action>

 

So what is it about this information that would make it important enough to collect?  The callstack provides the most recent 16 frames inside of the sqlservr process.  If you create a dump file of the sqlservr process using sqldumper.exe, you canopen the mdmp file up in windbg, load the public symbols for sql  Server, and then walk the stack with the ln <stack address> command.  For example the above callstack resolves in windbg as:

(00000000`01cd4f10)   sqlservr!GenericEvent::CallNextAction+0x45   |  (00000000`01cd5000)   sqlservr!AutoSpinlockHolder<170,1,1>::~AutoSpinlockHolder<170,1,1>
(00000000`00b78be0)   sqlservr!_chkstk+0xf276c   |  (00000000`00b78c30)   sqlservr!IsWorktableRowset
(00000000`00bea640)   sqlservr!ErrorReportedAutoPublish::Publish+0x190   |  (00000000`00bea820)   sqlservr!CErrorReportingManager::CwchCallFormatMessage
(00000000`00b78be0)   sqlservr!_chkstk+0x1bd96d   |  (00000000`00b78c30)   sqlservr!IsWorktableRowset
(00000000`02fa3800)   sqlservr!CXStmtError::XretExecute+0x6ae   |  (00000000`02fa44b0)   sqlservr!CStmtDbcc::XretExecute
(00000000`00bc8f80)   sqlservr!CMsqlExecContext::ExecuteStmts<1,1>+0x55a   |  (00000000`00bc9e30)   sqlservr!CSessionTaskProxy::AddRef
(00000000`00bca630)   sqlservr!CMsqlExecContext::FExecute+0x58b   |  (00000000`00bcad60)   sqlservr!CExecParamTblHelperForExecParamTable::`vftable'
(00000000`00bca1c0)   sqlservr!CSQLSource::Execute+0x319   |  (00000000`00bca630)   sqlservr!CMsqlExecContext::FExecute
(00000000`00bcd1a0)   sqlservr!process_request+0x370   |  (00000000`00bcd6c0)   sqlservr!CAutoSetupCXCtxtS::~CAutoSetupCXCtxtS
(00000000`00bc7990)   sqlservr!process_commands+0x2b2   |  (00000000`00bc7c10)   sqlservr!CConnection::PNetConn
(00000000`00b61520)   sqlservr!SOS_Task::Param::Execute+0x11b   |  (00000000`00b616f0)   sqlservr!Worker::Reset
(00000000`00b61230)   sqlservr!SOS_Scheduler::RunTask+0xca   |  (00000000`00b61520)   sqlservr!SOS_Task::Param::Execute
(00000000`00b60da0)   sqlservr!SOS_Scheduler::ProcessTasks+0x95   |  (00000000`00b61090)   sqlservr!WorkDispatcher::DequeueTask
(00000000`010e0d40)   sqlservr!SchedulerManager::WorkerEntryPoint+0x110   |  (00000000`010e0ea0)   sqlservr!SOSQueueCounted<Worker,0>::Dequeue
(00000000`010e0940)   sqlservr!SystemThread::RunWorker+0x60   |  (00000000`010e0a10)   sqlservr!SchedulerManager::AcquireWorker
(00000000`010f9980)   sqlservr!SystemThreadDispatcher::ProcessWorker+0x12c   |  (00000000`010f9b00)   sqlservr!SEList<SystemThread,112>::Head

image

This information isn’t really of much use unless you have access to or understand the SQL Server Source code.  In the event that you have an issue, the PSS team can create a memory dump of the process, collect the output from the ring_buffer target, and walk the stack to see what lead to the Event firing.

It is possible to materialize the stack without having to actually perform a memory dump and without using windbg.  In the SQLCAT team blog post Resolving DTC Related Waits and Tuning Scalability of DTC, Trace Flag 3656 is documented as materializing the callstack if the sqlservr.pdb symbols file exists in the same directory as sqlservr.exe. 

NOTE: There is a reason that this functionality is not turned on by default in SQL Server.  It is not recommended that you enable this Trace Flag on a production server unless directed to do so by PSS as a part of a support case.  This Trace Flag can impact performance and should not be used lightly.

In SQL Server 2008, the symbols file is not included by default in the product.  To get the symbols file, you can use windbg and a memory dump.  For steps on how to do this, see http://blogs.msdn.com/b/askjay/archive/2009/12/29/basic-debugging-concepts-and-setup.aspx.  Once you open the memory dump file for the first time, the symbols are downloaded from the public symbols server and placed in the .sympath specified, in the case of the blog post mentioned it will be C:\symbols\public\sq\sqlservr.pdb\1E7168D2F78B4FBA911F507689D7DE902.  After copying the pdb to the Binn folder for the SQL instance, by default C:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\Binn, we can turn on the trace flag and requery our Event Session.

--Trace flag 3656 enables the call stacks to be resolved.  This requires that the 
--sqlservr.pdb file reside in the same directory as sqlservr.exe
DBCC TRACEON (3656, -1)  
GO
SELECT n.query('.') AS callstack
FROM
(
    SELECT CAST(target_data as xml)
    FROM sys.dm_xe_sessions AS s 
    INNER JOIN sys.dm_xe_session_targets AS t
        ON s.address = t.event_session_address
    WHERE s.name = 'system_health'
      AND t.target_name = 'ring_buffer'
) AS src (target_data)
CROSS APPLY target_data.nodes('RingBufferTarget/event/action[@name="callstack"]') as q(n)

The output of our callstack action is now:

<action name="callstack" package="package0">
  <type name="callstack" package="package0" />
  <value>GenericEvent::CallNextAction+45 [ @ 0+0x0
_chkstk+f276c [ @ 0+0x0
ErrorReportedAutoPublish::Publish+190 [ @ 0+0x0
_chkstk+1bd96d [ @ 0+0x0
CXStmtError::XretExecute+6ae [ @ 0+0x0
CMsqlExecContext::ExecuteStmts&lt;1,1&gt;+55a [ @ 0+0x0
CMsqlExecContext::FExecute+58b [ @ 0+0x0
CSQLSource::Execute+319 [ @ 0+0x0
process_request+370 [ @ 0+0x0
process_commands+2b2 [ @ 0+0x0
SOS_Task::Param::Execute+11b [ @ 0+0x0
SOS_Scheduler::RunTask+ca [ @ 0+0x0
SOS_Scheduler::ProcessTasks+95 [ @ 0+0x0
SchedulerManager::WorkerEntryPoint+110 [ @ 0+0x0
SystemThread::RunWorker+60 [ @ 0+0x0
SystemThreadDispatcher::ProcessWorker+12c [ @ 0+0x0</value>
  <text />
</action>

If you note, these match up to the stack output from windbg.  If you are interested in trying to figure out the internal stack of SQL Server, the package0.callstack event can certainly be useful, but in general it is not something that you will get much use of in general troubleshooting with Extended Events. 

While working on yesterday’s blog post The Future – fn_dblog() No More? Tracking Transaction Log Activity in Denali I did a quick Google search to find a specific blog post by Paul Randal to use it as a reference, and in the results returned another blog post titled, Investigating Multiple Transaction Log Files in SQL Server caught my eye so I opened it in a new tab in IE and went about finishing the blog post.  It probably wouldn’t have gotten my attention if it hadn’t been on the SqlServerPedia site.  When I was finished I went back and read through the post, and I found that some of the information presented in it was incorrect, so I attempted to post a comment, and not surprisingly the blog had moderation controls turned on, I have it turned on here if you aren’t a SQLBlog member so I don’t have a problem with that necessarily, and the comment didn’t show up on the site.

Interestingly enough, yesterday SQL Server Central had an editorial by Tim Mitchell titled Calling Out Bad Advice that discussed the problem of bad information on the internet and how to go about calling people out for publishing bad advice.  Lets face it, people are human, at least I am, and mistakes happen from time to time, either through our own misunderstandings of our personal experiences and what we perceived from the information we had, or by shear accident in some cases.  This afternoon I got an email back from the blog post author and we traded a few emails about the post, and in the end the author made changes to the original post which have been syndicated to SQLServerPedia already, so to see the original you have do something like look at the Google Cached Copy.  The author also posted a follow up blog post today on the subject.

So why this blog post?  Well even with the corrections, some of the conclusions are still wrong.

image

I am not trying to knock this guy for what he saw or perceived from the information he collected, but 2, 3 and 4 are still incorrect.  What’s great is we can prove this by using Extended Events in SQL Server 2008 and that is what the real purpose behind this blog post is.  To set things up, we first need to create a database that roughly matches the available information shown in the pictures of the original blog post.  The database will have a single database file, that I am sizing initially at 128MB and will have a fixed autogrowth value of 64MB.  The database will have four log files that are initially sized at 1MB each, and the first log file will have a fixed autogrowth value of 32MB, with the last three transaction log files having fixed growth values of just 1MB.  Don’t comment on this configuration, I understand completely that there is no reason to create multiple log files on the same disk array (half the purpose behind this post is to show that there is no benefit to having multiple log files like this, which is also the intended purpose behind the original blog post as well), and I wouldn’t do this in production, but it works perfectly for the tests that we are about to run.  Once the database is created, we’ll switch to that database, and dump out the transaction log VLF information using DBCC LOGINFO.

-- Create our Test database with
--        1 data file sized at 128MB with 64MB autogrowth
--        1 log file sized at 1MB with 32MB autogrowth
--        3 log files sized at 1MB with 1MB autogrowth
CREATE DATABASE [Test] ON  
PRIMARY 
    (    
        NAME = N'Test', 
        FILENAME = N'D:\SQLData\MSSQL10.MSSQLSERVER\MSSQL\DATA\Test.mdf', 
        SIZE = 131072KB, 
        FILEGROWTH = 65536KB
    )
LOG ON 
    (    
        NAME = N'Test_log', 
        FILENAME = N'L:\SQLData\MSSQL10.MSSQLSERVER\MSSQL\DATA\Test_log.ldf', 
        SIZE = 1024KB, 
        MAXSIZE = 131072KB, 
        FILEGROWTH = 32768KB
    ), 
    (    
        NAME = N'Test_log2',     
        FILENAME = N'L:\SQLData\MSSQL10.MSSQLSERVER\MSSQL\DATA\Test_log2.ldf', 
        SIZE = 1024KB, 
        MAXSIZE = 131072KB, 
        FILEGROWTH = 1024KB 
    ), 
    ( 
        NAME = N'Test_log3', 
        FILENAME = N'L:\SQLData\MSSQL10.MSSQLSERVER\MSSQL\DATA\Test_log3.ldf', 
        SIZE = 1024KB, 
        MAXSIZE = 131072KB, 
        FILEGROWTH = 1024KB 
    ), 
    ( 
        NAME = N'Test_log4', 
        FILENAME = N'L:\SQLData\MSSQL10.MSSQLSERVER\MSSQL\DATA\Test_log4.ldf', 
        SIZE = 1024KB, 
        MAXSIZE = 131072KB, 
        FILEGROWTH = 1024KB 
    )
GO
-- Switch to our Test database
USE [Test]
GO
-- Dump the VLF Usage information
DBCC LOGINFO
GO

image

Each of the log files VLF’s have been highlighted in a different color above to point out the separation of the four different files.  Note that the active VLF is in the first log file, FileId=2, as shown by the Status=2.  With our test database created, we can now set out to create the Extended Events Event Session that:

    1. The transaction logs are written to sequentially starting with the first VLF in FileId=2 and then when the last VLF in FileId=2 is full, the log begins writing log records to the first VLF of FileId=3 and when the last VLF in FileId=3 is full, the log begins writing log records to the first VLF of FileId=4 and when the last VLF in FileId=4 is full, the log begins writing log records to the first VLF of FileId=5 and when the last VLF in FileId=5 is full, the log circles back to the first VLF of FileId=2 which will still be active because we are going to work within a single explicit transaction for the duration of the test.  Since the file is full it has to be grown, and because it has a growth factor of 32MB it grows by 32MB and begins writing log records to the first VLF of the newly allocated space.
    2. The writes to the log files do not happen at the same time, they occur sequentially as the engine writes log records into each file, filling the VLF’s and has to move to the next file, or circle back to the beginning of the log when it reaches the end of the last log file.
    3. Whatever results were seen in the original thread by opening the log file with Apex tools were incorrect and misleading to the original poster, since log files were actually written to all of the files during the operation.  I have a couple of theories as to what could have happened that made the Apex tool show no log records that I will discuss later in this thread.

What Events would we want to capture to look at what is happening in our transaction log files when running the same workload from the original post?  Since we are going to be executing a number of statements, the sqlserver.sql_statement_starting and sqlserver.sql_statement_completed Events seem like a good starting point, and since we want to know what statement was executing, we’ll add the sql_text Action to these Events.  Since we are dealing with the transaction log files, the sqlserver.databases_log_file_size_changed, sqlserver.databases_log_file_used_size_changed, sqlserver.databases_log_flush_wait, sqlserver.databases_log_flush, sqlserver.databases_log_growth, and sqlserver.databases_log_truncation Events should probably be included to so we can track what’s going on with our log specifically, and to ensure that these Events only fire for our test database, we’ll dynamically build in a Predicate on the sqlserver.database_id Predicate source using the output of DB_ID() inside the testing database. 

Since the log is a file, we also will want to collect the file operation related events such as sqlserver.flush_file_buffers, sqlserver.file_read, sqlserver.file_written, sqlserver.file_read_completed, and sqlserver.file_write_completed, and we’ll dynamically set a database_id Predicate on these Events as well.  If you recall back to Friday of last week, I talked about a number of trace flags that provide further information about Backup, Restore and file operations in my blog post A Look at Backup Internals and How to Track Backup and Restore Throughput (Part 1).  One of those was Trace Flag 3004, which writes file zeroing information to the trace print output whenever a zeroing operation occurs.  I previously used this trace flag in my blog post Does the tempdb Log file get Zero Initialized at Startup?  Since the log files grew in the original tests, we can turn this trace flag on to track the file growths, and use the sqlserver.trace_print Event to capture the file operation messages, and to keep this Event focused to our tests only, we’ll dynamically set a Predicate for the current session_id using the sqlserver.session_id Predicate Source and the output of @@SPID.  Finally since this is all happening inside of an explicit transaction, we’ll also capture the sqlserver.database_transaction_begin and sqlserver.database_transaction_end events for the current database_id.

We have quite a large list of Events associated with this Event Session, and to ensure that we can perform analysis over all of the Event data from our tests, we’ll use the package0.asynchronous_file_target to hold our Event information.  We’ll also increase our buffer memory from the default 4MB to 8MB and set the Event Session up to ALLOW_SINGLE_EVENT_LOSS, which does exactly what it sounds like it does, and to correlate cause and effect we’ll also turn TRACK_CAUSALITY to ON for the session.

-- Create our Event Session dynamically
DECLARE @sqlcmd nvarchar(2000) = '
CREATE EVENT SESSION TransactionLogUsage
ON SERVER
--ADD EVENT sqlserver.sql_statement_starting
--( ACTION(sqlserver.sql_text)
--  WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
--ADD EVENT sqlserver.sql_statement_completed
--( ACTION(sqlserver.sql_text)
--  WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlserver.databases_log_file_size_changed
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlserver.databases_log_file_used_size_changed
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlserver.databases_log_flush_wait
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlserver.databases_log_flush
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlserver.databases_log_growth
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlserver.databases_log_truncation
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlserver.flush_file_buffers
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlserver.file_read
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlserver.file_written
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlserver.file_read_completed
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlserver.file_write_completed
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlserver.trace_print
(   WHERE (sqlserver.session_id = '+ cast(@@SPID as varchar(4))+')),
ADD EVENT sqlserver.database_transaction_begin
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')),
ADD EVENT sqlserver.database_transaction_end
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+'))
ADD TARGET package0.asynchronous_file_target(
     SET filename=''C:\SQLBlog\TransactionLogUsage.xel'',
         metadatafile=''C:\SQLBlog\TransactionLogUsage.xem'')
WITH (MAX_MEMORY = 8MB, EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS, TRACK_CAUSALITY = ON )'
EXEC (@sqlcmd)
GO

If you notice above, I have commented out the sqlserver.sql_statement_starting and sqlserver.sql_statement_completed Events in the Event Session.  It turns out that these two events are not needed in the Event Session to prove the points being made in this blog post.  Including these two events makes the time to process the 240K+ Events run in the 10-15 minute range on my 16 core test server, so its not likely something that you are going to do on a laptop VM, but they were included in my initial Event Session for this, and I wanted to show the thought process I followed to get from A to B and ultimately C.

With our Event Session created, we can finish setting up our environment to run the actual tests.  To do this we’ll create a table named LogTable with two columns that are, as best as I can tell from the limited information provided about the test table, the same as the table used in the original post.  We’ll then CHECKPOINT the database to cause log truncation to occur (you did create the database in SIMPLE recovery right?), turn on Trace Flag 3004 for our session, and then start the Event Session so that it collects the data from our Events during our test.

-- Create our Test Table
CREATE TABLE LogTable (RowID decimal(10,4), Data char(1024))
GO
-- Checkpoint the database to truncate and clear the log.
CHECKPOINT
GO
-- Turn on Trace Flag 3004 so we can see file zeroing ops.
DBCC TRACEON(3004)
GO
-- Start the Event Session
ALTER EVENT SESSION TransactionLogUsage
ON SERVER
STATE=START
GO

With the Event Session started and all our setup work completed we can now run the test script that was used in the original post to generate our test workload.  When the tests complete, we’ll dump out our VLF information again with DBCC LOGINFO, then ROLLBACK the transaction, switch to master and DROP our test database and the Extended Events Session from the server since they are no longer needed.

-- Run our tests
SET NOCOUNT ON

DECLARE @cnt decimal(10,4)=0
DECLARE @rows int=0
BEGIN TRAN
WHILE 1=1
BEGIN
    INSERT INTO LogTable VALUES (ROUND((RAND()* 1000000),0), SPACE(1024))
    
    SELECT @rows+=1
        
    SELECT @cnt = (size * 1.0 * 8.0)/1024.0 
    FROM  Test.sys.database_files
    WHERE data_space_id = 0
    AND [FILE_ID]=5
    
    IF @cnt>1.0
            BREAK
END

SELECT @rows;
GO
-- Pull Log VLF usage again
DBCC LOGINFO
GO
-- Rollback our transaction
ROLLBACK
GO
USE master
GO
-- Kill any connection to Test database
ALTER DATABASE [Test] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
GO
-- Drop the Test database
DROP DATABASE [Test]
GO
-- Drop the Event Session
DROP EVENT SESSION TransactionLogUsage
ON SERVER
GO

image

There is a reason that I dump out the VLF information before performing the ROLLBACK of the transaction.  As long as the transaction remains active, the VLF’s containing the active transaction can not be truncated and cleared.  In order to see the allocated VLF’s, we need the transaction active still.  Once again, I have highlighted each of the individual log files separately, and from the DBCC LOGINFO output we can look at the FileId and Status columns and see that our transaction log wrote information into all 4 of the files, filling them, and the wrapped back to the first file which had to be grown, and each of the subsequent log files were also grown by the database engine.  However, if you look at the CreateLSN information for the growth portion of each log file, you will notice that each file has its own Create LSN value for the second set of VLF’s, meaning that they were grown separately and at different times.  Still not convinced by DBCC LOGINFO?  Well we have the data to validate this and prove it unequivocally, but before we can look at the data, we need to retrieve it from the asynchronous_file_target files and shred the XML using XQuery.

-- Create our Analysis Database
CREATE DATABASE TLogUsageTestResults
GO
-- Switch to our Analysis Database
USE [TLogUsageTestResults]
GO
-- Create intermediate temp table for raw event data
CREATE TABLE RawEventData
(Rowid int identity primary key, event_data xml)

-- Create final results table for parsed event data
CREATE TABLE TestResults
([Rowid] int primary key, [event_name] varchar(50), [package_name] varchar(50),
 [timestamp] datetime2, [count] bigint, [increment] bigint, [database_id] int, 
 [mode] nvarchar(4000), [file_handle] nvarchar(4000), [offset] bigint, 
 [file_id] int, [file_group_id] int, [path] nvarchar(4000), [duration] bigint, 
 [io_data] nvarchar(4000), [succeeded] nvarchar(4000), [sql_text] nvarchar(4000), 
 [trace_message] nvarchar(4000), [source_database_id] int, [object_id] int, 
 [object_type] int, [cpu] int, [reads] bigint, [writes] bigint, 
 [state] nvarchar(4000), [offset_end] int, [nest_level] int, 
 [activity_id] uniqueidentifier, [event_sequence] int )

-- Read the file data into intermediate temp table
INSERT INTO RawEventData (event_data)
SELECT
    CAST(event_data AS XML) AS event_data
FROM sys.fn_xe_file_target_read_file('C:\SQLBlog\TransactionLogUsage*.xel', 
                                     'C:\SQLBlog\TransactionLogUsage*.xem', 
                                     null, null)

-- Query the Event data from the Target.
INSERT INTO TestResults
([Rowid], [event_name], [timestamp], [database_id], [count], [increment], 
 [mode], [file_handle], [offset], [file_id], [file_group_id], [path], 
 [duration], [io_data], [succeeded], [sql_text], [trace_message], [source_database_id], 
 [object_id], [object_type], [cpu], [reads], [writes], [state], [offset_end], 
 [nest_level], [activity_id], [event_sequence])

-- Fetch the Event Data from the Event Session Target
SELECT 
    RowID,
    event_data.value('(event/@name)[1]', 'varchar(50)') AS event_name,
    DATEADD(hh, 
            DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP), 
            event_data.value('(event/@timestamp)[1]', 'datetime2')) AS [timestamp],
    COALESCE(event_data.value('(event/data[@name="database_id"]/value)[1]', 'int'), 
             event_data.value('(event/action[@name="database_id"]/value)[1]', 'int')) AS database_id,
    event_data.value('(event/data[@name="count"]/value)[1]', 'bigint') AS [count],
    event_data.value('(event/data[@name="increment"]/value)[1]', 'bigint') AS [increment],
    event_data.value('(event/data[@name="mode"]/text)[1]', 'nvarchar(4000)') AS [mode],
    event_data.value('(event/data[@name="file_handle"]/value)[1]', 'nvarchar(4000)') AS [file_handle],
    event_data.value('(event/data[@name="offset"]/value)[1]', 'bigint') AS [offset],
    event_data.value('(event/data[@name="file_id"]/value)[1]', 'int') AS [file_id],
    event_data.value('(event/data[@name="file_group_id"]/value)[1]', 'int') AS [file_group_id],
    event_data.value('(event/data[@name="path"]/value)[1]', 'nvarchar(4000)') AS [path],
    event_data.value('(event/data[@name="duration"]/value)[1]', 'bigint') AS [duration],
    event_data.value('(event/data[@name="io_data"]/value)[1]', 'nvarchar(4000)') AS [io_data],
    event_data.value('(event/data[@name="succeeded"]/value)[1]', 'nvarchar(4000)') AS [succeeded],
    event_data.value('(event/action[@name="sql_text"]/value)[1]', 'nvarchar(4000)') AS [sql_text],
    event_data.value('(event/data[@name="message"]/value)[1]', 'nvarchar(4000)') AS [trace_message],
    event_data.value('(event/data[@name="source_database_id"]/value)[1]', 'int') AS [source_database_id],
    event_data.value('(event/data[@name="object_id"]/value)[1]', 'int') AS [object_id],
    event_data.value('(event/data[@name="object_type"]/value)[1]', 'int') AS [object_type],
    event_data.value('(event/data[@name="cpu"]/value)[1]', 'int') AS [cpu],
    event_data.value('(event/data[@name="reads"]/value)[1]', 'bigint') AS [reads],
    event_data.value('(event/data[@name="writes"]/value)[1]', 'bigint') AS [writes],
    event_data.value('(event/data[@name="state"]/text)[1]', 'nvarchar(4000)') AS [state],
    event_data.value('(event/data[@name="offset_end"]/value)[1]', 'int') AS [offset_end],
    event_data.value('(event/data[@name="nest_level"]/value)[1]', 'int') AS [nest_level],
    CAST(SUBSTRING(event_data.value('(event/action[@name="attach_activity_id"]/value)[1]', 'varchar(50)'), 1, 36) AS uniqueidentifier) as activity_id,
    CAST(SUBSTRING(event_data.value('(event/action[@name="attach_activity_id"]/value)[1]', 'varchar(50)'), 38, 10) AS int) as event_sequence
FROM RawEventData
ORDER BY Rowid
GO

-- Return our results
SELECT * 
FROM TestResults
WHERE event_name NOT IN ('sql_statement_starting' , 'sql_statement_completed')
ORDER BY RowID

If you scroll through the results you can see the writes occurring sequentially through each of the log files, and while FileId=4 is being written to, the Database Engine begins the growth of FileId=2 by 32MB.  If we change our query to only focus on the file_write_completed, databases_log_growth, and trace_print Events, we can see this a little easier.

-- Return our results
SELECT 
    Rowid, 
    event_name, 
    [timestamp], 
    [count], 
    database_id, 
    mode, 
    offset, 
    file_id, 
    duration, 
    trace_message 
FROM TestResults
WHERE event_name  IN ('file_write_completed' , 'databases_log_growth', 'trace_print')
ORDER BY RowID

image    image
Log Rollover from Log1 to Log2   Log Rollover from Log2 to Log3

 

image

   image
Autogrow of Log1   Log Rollover from Log3 to Log4

 

image    image
Log Rollover from Log4 back to Log1   Autogrow of Log2

 

image    image
Autogrow of Log3   Autogrow of Log4


We can see the first log file, FileID=2, grows before the log rollover from Log3, FileID=4, occurs to Log4, FileID=5, making space available in the first log file for the rollover when FileID=5 becomes full.  Log records are written all four of the log files before the log wraps back around to the first log file, debunking point numbers two and four of the conclusion.  The timestamps of the events shows that the additional log files are written to serially and not at the same time debunking point number three of the conclusion.  The reason that only a fraction of the log records are written to the three additional log files is proportionate to the difference in the autogrowth settings between the first log file at 32MB and the three additional log files at 1MB.  If the first log file was set to grow at 1MB, the majority of the log records would not be in the first log file.

Its been well documented that there is no performance benefit to having multiple log files in a database, and Paul Randal’s blog post, Importance of proper transaction log size management, was linked to in the original blog post that ultimately triggered this post.  The behavior demonstrated in this post isn’t a mystery, its documented in the Books Online (Transaction Log Physical Architecture), but sometime empirical evidence like this helps solidify that fact. 

I bet that made you look didn’t it?  Worry not, fn_dblog() still exists in SQL Server Denali, and I plan on using it to validate the information being returned by a new Event in SQL Server Denali CTP1, sqlerver.transaction_log, which brings with it the ability to correlate specific transaction log entries to the operations that actually caused them to occur.

There is no greater source of information about the transaction log in SQL Server than Paul Randal’s blog category Transaction Log.  It is also listed as the referenced pre-reading material for the Microsoft SQL Server MCM program Logging, Recovery, Log Maintenance topic.  In a number of his blog posts, Paul shows how to look at the transaction log by using an undocumented system function fn_dblog().  Note that I said it is undocumented, meaning its use is not supported by Microsoft, its functionality is subject to change at any point in time without notification, and its use is at your own risk.  Is it safe to use?  That’s a topic that is up for debate, but at the end of the day if you were to have a problem associated with its use you are on your own because its undocumented.

Why does any of this matter?  It matters because there is a lot of information that we can learn about the internal operations of SQL Server from the log operations that occur as the result of changes in our database.  Some examples of this would be:

A SQL Server DBA myth a day: (19/30) TRUNCATE TABLE is non-logged
Benchmarking: 1-TB table population (part 2: optimizing log block IO size and how log IO works)
Lock logging and fast recovery
How do checkpoints work and what gets logged
Finding out who dropped a table using the transaction log

Admittedly this isn’t necessarily information that you would start a conversation with at a party, unless of course you are surrounded by other ubergeeks SQL Server internal junkies, and its not really the kind of information that I use day to day in my work as a Database Administrator.  Prior to the introduction of Extended Events, some information about how SQL Server operated was only available inside of the transaction log records, and I am sure that there are still some items that you can only see inside of the log records.  Microsoft obviously recognized a demand to look the log operations generated by SQL Server in a supported fashion and added this functionality to Extended Events.

The sqlserver.transaction_log Event returns 10 data elements in its payload, which can be found in the sys.dm_xe_object_columns DMV.

SELECT 
    oc.name, 
    oc.type_name, 
    oc.description
FROM sys.dm_xe_packages AS p
INNER JOIN sys.dm_xe_objects AS o
    ON p.guid = o.package_guid
INNER JOIN sys.dm_xe_object_columns AS oc
    ON oc.object_name = o.name
        AND oc.object_package_guid = o.package_guid
WHERE o.name = 'transaction_log'
  AND oc.column_type = 'data'

image

The operation and context elements have corresponding Maps in sys.dm_xe_map_values that provide the different Log Operations and Contexts that are available through Extended Events.  There are currently 70 Log Operations and 29 Contexts for those operations available in SQL Server Denali CTP1. 

SELECT 
    name,
    map_key,
    map_value
FROM sys.dm_xe_map_values
WHERE name in ('log_op', 'log_context')
ORDER BY name, map_key

To show how this Event can be used, we’ll first create a database named TransactionLogDemo, and then switch our connection to that database.  We’ll then create an table that will be used to generate some Transaction Log events.  We’ll create our Event Session to capture the sqlserver.sql_statement_starting, sqlserver.sql_statement_completed, and sqlserver.transaction_log Events and we’ll add a Predicate to each Event to only fire for the TransactionLogDemo database.  To add the Predicate dynamically, we’ll use Dynamic SQL to create our Event Session since inline parameters cannot be used in the CREATE EVENT SESSION DDL. 

CREATE DATABASE TransactionLogDemo
GO
USE TransactionLogDemo
GO
CREATE TABLE CreateLogRecords
(RowID int identity primary key,
 RowData nvarchar(120))
GO
DECLARE @sqlcmd nvarchar(2000) = '
CREATE EVENT SESSION TransactionLogDemo
ON SERVER
ADD EVENT sqlserver.page_reference_tracker,
ADD EVENT sqlserver.sql_statement_starting
( ACTION(sqlserver.sql_text)
  WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')
),
ADD EVENT sqlserver.sql_statement_completed
( ACTION(sqlserver.sql_text)
  WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+')
),
ADD EVENT sqlserver.transaction_log
( WHERE (sqlserver.database_id = '+ cast(DB_ID() as varchar(3))+'))
ADD TARGET package0.asynchronous_file_target(
     SET filename=''C:\SQLBlog\TransactionLogDemoDenali.xel'',
         metadatafile=''C:\SQLBlog\TransactionLogDemoDenali.xem'')
WITH (MAX_MEMORY = 4MB, EVENT_RETENTION_MODE = NO_EVENT_LOSS, TRACK_CAUSALITY = ON )'
EXEC (@sqlcmd)
GO
CHECKPOINT
GO
-- Start the Event Session
ALTER EVENT SESSION TransactionLogDemo
ON SERVER
STATE=START
GO

Once the Event Session is created, we’ll call CHECKPOINT on the database so that the log can truncate (you did create the database in SIMPLE recovery right?) and clear allowing our later call to fn_dblog() to only return the log records specific to the operations that occur after the CHECKPOINT.  We’ll start our Event Session, and then insert 20 rows into the CreateLogRecords table, and then immediately delete all of the rows from the table, and stop our Event Session to end the collection of Events.

INSERT INTO CreateLogRecords (RowData)
SELECT REPLICATE('abc123', 20)
FROM master.dbo.spt_values a
WHERE a.type = 'P'
  AND a.number < 20
GO
DELETE CreateLogRecords
GO
-- Disable the Event Session
ALTER EVENT SESSION TransactionLogDemo
ON SERVER
STATE=STOP
GO

Once this is done, we can now query the package0.asynchronous_file_target to get our Event data from Extended Events, and then at the same time query fn_dblog() to get the log records from the Transaction Log as well so that we can validate what we’ve collected in our Event Session.

-- Fetch the Event Data from the Event Session Target
SELECT 
    XEvent.value('(event/@name)[1]', 'varchar(50)') AS event_name,
    DATEADD(hh, 
            DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP), 
            XEvent.value('(event/@timestamp)[1]', 'datetime2')) AS [timestamp],
    COALESCE(XEvent.value('(event/data[@name="database_id"]/value)[1]', 'int'), 
             XEvent.value('(event/action[@name="database_id"]/value)[1]', 'int')) AS database_id,
    XEvent.value('(event/data[@name="index_id"]/value)[1]', 'int') AS [index_id],
    XEvent.value('(event/data[@name="object_id"]/value)[1]', 'int') AS [object_id],
    XEvent.value('(event/data[@name="transaction_id"]/value)[1]', 'int') AS [transaction_id],
    XEvent.value('(event/data[@name="log_record_size"]/value)[1]', 'int') AS [log_record_size],
    XEvent.value('(event/data[@name="operation"]/text)[1]', 'varchar(50)') AS [operation],
    XEvent.value('(event/data[@name="context"]/text)[1]', 'varchar(50)') AS [context],
    XEvent.value('(event/data[@name="transaction_start_time"]/value)[1]', 'datetime2') AS [transaction_start_time],
    XEvent.value('(event/data[@name="replication_command"]/value)[1]', 'int') AS [replication_command],
    XEvent.value('(event/action[@name="sql_text"]/value)[1]', 'varchar(1000)') AS [sql_text],
    CAST(SUBSTRING(XEvent.value('(event/action[@name="attach_activity_id"]/value)[1]', 'varchar(50)'), 1, 36) AS uniqueidentifier) as activity_id,
    CAST(SUBSTRING(XEvent.value('(event/action[@name="attach_activity_id"]/value)[1]', 'varchar(50)'), 38, 10) AS int) as event_sequence
FROM (
    SELECT CAST(event_data AS XML) AS XEvent
    FROM sys.fn_xe_file_target_read_file('C:\SQLBlog\TransactionLogDemoDenali*.xel', 'C:\SQLBlog\TransactionLogDemoDenali*.xem', null, null)) as src
ORDER BY 
    DATEADD(hh, 
            DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP), 
            XEvent.value('(event/@timestamp)[1]', 'datetime2')),
    CAST(SUBSTRING(XEvent.value('(event/action[@name="attach_activity_id"]/value)[1]', 'varchar(50)'), 1, 36) AS uniqueidentifier),
    CAST(SUBSTRING(XEvent.value('(event/action[@name="attach_activity_id"]/value)[1]', 'varchar(50)'), 38, 10) AS int)
GO

-- Fetch Log records from log for comparison
SELECT 
    [Xact ID] as transaction_id, 
    [Log Record Fixed Length] as log_record_size, 
    [Operation] as operation, 
    SUBSTRING([context], 5, LEN(context)) as context, 
    [Begin Time]
FROM fn_dblog(null, null) 
GO

image

When we look at the output of the above two queries, first you’ll note that there are four log records that don’t have associated records in our Extended Event Session.  These are the log records generated by the CHECKPOINT operation and the ALTER EVENT SESSION command and occurred before the Event Session was actually collecting data.  The first two LOP_BEGIN_XACT records in the Event Session correspond to the transaction_id of the log records returned in rows 5 and 6 of the fn_dblog() output, but if you notice the Event Session is missing the transaction_start_time for the log operations, something I believe to be a bug in Denali CTP1 and which I’ve submitted a Connect item for (Denali - Transaction_Log Extended Event Returning Incorrect Data). 

On quick glance it appears that all of our log records are in the same order, but if we look at more closely, there is a LOP_MODIFY_ROW operation that is missing from our Event Session, but exists inside of the fn_dblog() output.

image

If you scroll down further, you’ll also see that there are two missing log records for the delete as well, LOP_MODIFY_HEAP and LOP_SET_BITS with a context of PFS.  However, the Extended Event Session captures the lock logging for the ALTER EVENT SESSION command that stopped the Event collection, whereas the output from fn_dblog() does not show that.

To cleanup from this Event Session, the following code can be run.

USE master
GO
DROP DATABASE TransactionLogDemo
GO
DROP EVENT SESSION TransactionLogDemo
ON SERVER
GO
EXECUTE sp_configure 'show advanced options', 1
GO
RECONFIGURE
GO
EXECUTE sp_configure 'xp_cmdshell', 1
GO
RECONFIGURE
GO
EXEC xp_cmdshell 'DEL C:\SQLBlog\TransactionLogDemoDenali*'
GO
EXECUTE sp_configure 'xp_cmdshell', 0
GO
RECONFIGURE
GO
EXECUTE sp_configure 'show advanced options', 0
GO
RECONFIGURE
GO

One of the biggest problems that I had with getting into Extended Events was mapping the Events available in Extended Events to the Events that I knew from SQL Trace.  With so many Events to choose from in Extended Events, and a different organization of the Events, it is really easy to get lost when trying to find things.  Add to this the fact that Event names don’t match up to Trace Event names in SQL Server 2008 and 2008 R2, and not all of the Events from Trace are implemented in SQL Server 2008 and 2008 R2, and it gets really confusing really fast.  For my presentation this year at SQLBits 7, I sat down with Excel and mapped out the Events that exist in Extended Events to their corresponding SQL Trace Event, and two of the slides in my deck were tables of these mappings. 

TraceCategory TraceEvent PackageName XEEventName
Deprecation Deprecation Announcement sqlserver deprecation_announcement
Deprecation Deprecation Final Support sqlserver deprecation_final_support
Errors and Warnings ErrorLog sqlserver errorlog_written
Errors and Warnings EventLog sqlserver error_reported
Errors and Warnings Exception sqlos exception_ring_buffer_recorded
Errors and Warnings User Error Message sqlserver error_reported
Full text FT:Crawl Aborted sqlserver error_reported
Locks Deadlock graph sqlserver xml_deadlock_report
Locks Lock:Acquired sqlserver lock_acquired
Locks Lock:Deadlock sqlserver lock_deadlock
Locks Lock:Released sqlserver lock_released
Locks Lock:Timeout sqlserver locks_lock_timeouts
Locks Lock:Timeout (timeout > 0) sqlserver locks_lock_timeout_greater_than_0
Stored Procedures RPC Output Parameter sqlserver rpc_completed
Stored Procedures RPC:Completed sqlserver rpc_completed
Stored Procedures RPC:Starting sqlserver rpc_starting
Stored Procedures SP:Completed sqlserver module_end
Stored Procedures SP:Recompile sqlserver sp_statement_starting
Stored Procedures SP:Starting sqlserver module_start
Stored Procedures SP:StmtCompleted sqlserver sp_statement_completed
Stored Procedures SP:StmtStarting sqlserver sp_statement_starting
TSQL SQL:StmtCompleted sqlserver sql_statement_completed
TSQL SQL:StmtRecompile sqlserver sql_statement_starting
TSQL SQL:StmtStarting sqlserver sql_statement_starting
User configurable UserConfigurable sqlserver user_settable

I have a script that creates a view for these in my administrative database, that is a part of my SQL Server 2008 configuration script for my environment. That script is attached to this blog post for use in your own environment.

In SQL Server Denali CTP1, a table has been added to the master database named dbo.trace_xe_event_map that provides a static mapping of each Trace Event to its corresponding Extended Events Event (This table should be in the sys schema to maintain uniformity in the product and I filed a Connect Feedback to move this that needs votes to have this changed).  Mike Wachal blogged about this table and how it and its partner table dbo.trace_xe_action_map can be used to migrate from SQL Trace to Extended Events in his blog post Migrating from SQL Trace to Extended Events.  He also includes a really cool SQLCLR Stored Procedure that will perform the conversion for you automagically. 

Today’s post will be somewhat short, but we’ll look at Customizable Fields on Events in Extended Events and how they are used to collect additional information.  Customizable Fields generally represent information of potential interest that may be expensive to collect, and is therefore made available for collection if specified by the Event Session.  In SQL Server 2008 and 2008 R2, there are 50 Events that have customizable columns in their payload.  In SQL Server Denali CTP1, there are 124 Events that have customizable columns in their payload. The customizable columns and Events that have them can be found with the following query.

SELECT 
    p.name AS package_name,
    o.name AS event_name,
    oc.name AS column_name,
    oc.column_type,
    oc.type_name,
    oc.description
FROM sys.dm_xe_packages p
JOIN sys.dm_xe_objects o
    ON p.guid = o.package_guid
JOIN sys.dm_xe_object_columns oc
    ON o.name = oc.object_name 
        AND o.package_guid = oc.object_package_guid
WHERE ((p.capabilities is null or p.capabilities & 1 = 0)
  AND (o.capabilities is null or o.capabilities & 1 = 0)
  AND (oc.capabilities is null or oc.capabilities & 1 = 0))
  AND o.object_type = 'event'
  AND oc.column_type = 'customizable'

If we look at a specific Event containing a customizable column, in this case the sqlserver.file_read_completed Event, we will see that there is a customizable column as well as a data column for the data collected by the customizable column.

SELECT 
    p.name AS package_name,
    o.name AS event_name,
    oc.column_id,
    oc.name AS column_name,
    oc.column_type,
    oc.type_name,
    oc.description
FROM sys.dm_xe_packages p
JOIN sys.dm_xe_objects o
    ON p.guid = o.package_guid
JOIN sys.dm_xe_object_columns oc
    ON o.name = oc.object_name 
        AND o.package_guid = oc.object_package_guid
WHERE ((p.capabilities is null or p.capabilities & 1 = 0)
  AND (o.capabilities is null or o.capabilities & 1 = 0)
  AND (oc.capabilities is null or oc.capabilities & 1 = 0))
  AND o.object_type = 'event'
  AND o.name = 'file_read_completed'
  AND oc.column_type <> 'readonly'
ORDER BY oc.column_type, oc.column_id
image

In the red box are the customizable columns, and the blue box has the associated data columns to the customizable columns.  The data columns will exist in the Event data from the Event firing, but they will only have a value in the Event data if the customizable column is set to collect the information.

CREATE EVENT SESSION CustomizableColumnDemo
ON SERVER
ADD EVENT sqlserver.file_read_completed
(
    WHERE (database_id = 4)    
)
ADD TARGET package0.ring_buffer
GO
ALTER EVENT SESSION CustomizableColumnDemo
ON SERVER
STATE=START
GO
DBCC DROPCLEANBUFFERS
GO
SELECT TOP 10 * FROM msdb.dbo.backupset
GO

If we query the Target data for the above Event, we’ll see that the path and io_data columns are included in the Event XML, but there is no value in the XML nodes.

SELECT CAST(target_data AS XML) as target_data
FROM sys.dm_xe_sessions AS s    
JOIN sys.dm_xe_session_targets AS t
    ON s.address = t.event_session_address
WHERE s.name = 'CustomizableColumnDemo'
  AND t.target_name = 'ring_buffer'
<event name="file_read_completed" package="sqlserver" id="83" version="1" timestamp="2010-12-20T03:14:20.393Z">
  <data name="mode">
    <type name="file_io_mode" package="sqlserver" />
    <value>0</value>
    <text>Contiguous</text>
  </data>
  <data name="duration">
    <type name="uint64" package="package0" />
    <value>0</value>
    <text />
  </data>
  <data name="file_handle">
    <type name="ulong_ptr" package="package0" />
    <value>0x0000000000000b38</value>
    <text />
  </data>
  <data name="offset">
    <type name="uint64" package="package0" />
    <value>14352384</value>
    <text />
  </data>
  <data name="database_id">
    <type name="uint16" package="package0" />
    <value>4</value>
    <text />
  </data>
  <data name="file_id">
    <type name="uint16" package="package0" />
    <value>1</value>
    <text />
  </data>
  <data name="file_group_id">
    <type name="uint16" package="package0" />
    <value>1</value>
    <text />
  </data>
  <data name="path">
    <type name="unicode_string" package="package0" />
    <value />
    <text />
  </data>
  <data name="io_data">
    <type name="binary_data" package="package0" />
    <value />
    <text />
  </data>
</event>

To set the customizable column to collect the data, in the ADD EVENT section of the CREATE EVENT SESSION or ALTER EVENT SESSION DDL command, the SET option is used to turn data collection on for the column.

DROP EVENT SESSION CustomizableColumnDemo
ON SERVER
GO
CREATE EVENT SESSION CustomizableColumnDemo
ON SERVER
ADD EVENT sqlserver.file_read_completed
(    
    SET collect_path = 1
    WHERE(database_id = 4)
)
ADD TARGET package0.ring_buffer
GO
ALTER EVENT SESSION CustomizableColumnDemo
ON SERVER
STATE=START
GO
DBCC DROPCLEANBUFFERS
GO
SELECT TOP 10 * FROM msdb.dbo.backupset
GO
ALTER EVENT SESSION CustomizableColumnDemo
ON SERVER
DROP EVENT sqlserver.file_read_completed
GO

Notice that the SET option does not use parenthesis, they are not allowed in the DDL definition.  By setting the collect_path customizable column to 1 the Event XML now contains the path to the data file that was read.

-- Query the XML to get the Target Data
SELECT 
    n.value('(event/@name)[1]', 'varchar(50)') AS event_name,
    n.value('(event/@package)[1]', 'varchar(50)') AS package_name,
    DATEADD(hh, 
            DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP), 
            n.value('(event/@timestamp)[1]', 'datetime2')) AS [timestamp],
    ISNULL(n.value('(event/data[@name="database_id"]/value)[1]', 'int'),
            n.value('(event/action[@name="database_id"]/value)[1]', 'int')) as [database_id],
    n.value('(event/data[@name="mode"]/value)[1]', 'nvarchar(50)') as [mode],
    n.value('(event/data[@name="duration"]/value)[1]', 'bigint') as [duration],
    n.value('(event/data[@name="file_handle"]/value)[1]', 'nvarchar(50)') as [file_handle],
    n.value('(event/data[@name="offset"]/value)[1]', 'int') as [offset],
    n.value('(event/data[@name="file_id"]/value)[1]', 'int') as [file_id],
    n.value('(event/data[@name="path"]/value)[1]', 'nvarchar(250)') as [path],
    n.value('(event/data[@name="id_data"]/value)[1]', 'nvarchar(max)') as [io_data]   
FROM
(    SELECT td.query('.') as n
    FROM 
    (
        SELECT CAST(target_data AS XML) as target_data
        FROM sys.dm_xe_sessions AS s    
        JOIN sys.dm_xe_session_targets AS t
            ON s.address = t.event_session_address
        WHERE s.name = 'CustomizableColumnDemo'
          AND t.target_name = 'ring_buffer'
    ) AS sub
    CROSS APPLY target_data.nodes('RingBufferTarget/event') AS q(td)
) as tab
GO

image

The increase in the number of Events with customizable columns in Denali CTP1 is, in my own opinion, a great step in the right direction for Extended Events.  The use of customizable columns to add data into the Event payload extends the flexibility of Extended Events by providing a mechanism to gather additional data related to Events that is specific to the Event and not globally available like Actions.

In yesterday’s blog post A Look at Backup Internals and How to Track Backup and Restore Throughput (Part 1), we looked at what happens when we Backup a database in SQL Server.  Today, we are going to use the information we captured to perform some analysis of the Backup information in an attempt to find ways to decrease the time it takes to backup a database.  When I began reviewing the data from the Backup in yesterdays post, I realized that I had made a mistake in the process and left Trace Flag 3213 off, which left some information that we’ll need out of the results, the Backup Buffer Configuration information.  For this post I turned Trace Flag 3213 on, and reran the Backup from yesterday, so the results may differ slightly but the same Backup command was used for the tests.

image
The Backup Buffer Configuration information tells us how many Buffers were allocated, and what the Max Transfer Size being used is.  In the screenshot above, this information is outlined in the red box.  The default Buffer Count is determined by SQL Server when the BUFFERCOUNT option is not specified in the BACKUP DATABASE command.  The calculation used is:

(NumberofBackupDevices * GetSuggestedIoDepth) + NumberofBackupDevices + (2*DatabaseDeviceCount)

This is covered in detail on Amit Banerjee’s blog post, Incorrect BufferCount data transfer option can lead to OOM condition.  For the Backup in yesterday’s post, and the one that will be the baseline for today’s post, the BUFFERCOUNT option was not specified, and the Backup, as shown above, used 7 Buffers and the default MaxTransferSize of 1MB for the backup.  If we look at the aggregated Event and wait_type information contained in the Event Session for the Backup we can begin to see what types of Backup bottlenecks we might have in the system.

SELECT  
    
ISNULL(wait_type, event_name) AS Operation,
    
SUM(duration) AS duration,
    
SUM(signal_duration) AS signal_duration,
    
COUNT(*) AS occurences
FROM #TestResults
WHERE (duration IS NOT NULL OR signal_duration IS NOT NULL)
  AND
ISNULL(wait_type, event_name) IN
        
('BACKUPBUFFER', 'BACKUPIO')
GROUP BY ISNULL(wait_type, event_name)
image

Looking at this information, we have a large number of BACKUPBUFFER waits occurring during the backup of the database and this may be a potential tuning opportunity to improve the performance of our database Backups.  To test this, we can change our Backup Script to include a 4GB MAXTRANSFERSIZE and a BUFFERCOUNT of 16.  I also chose to change the filenames in the Extended Events Session to simplify working with the Target Data for each test individually.  It is possible to specify the exact filename and metadatafile names when you read from the Target, but that requires more work than is needed in my opinion.  For the sake of brevity I am not going to repeat all of the Extended Events information in this blog post, but instead show the outcome of running various configurations of BUFFERCOUNT against a test server.

Test Number Backup File Count Buffer Count Max Transfer Size Backup Time (s) BACKUPBUFFER (wait ms) BACKUPIO (wait ms) BACKUPBUFFER (wait count) BACKUPIO (wait count)
1 1 7 1024 122.5 159471 62587 81616 22815
2 1 16 4096 105.2 90963 69091 14513 7982
3 1 32 4096 99.5 75236 88634 12298 8679
4 1 128 4096 95.9 70173 63435 8292 4679
5 1 256 4096 95.9 50988 48942 1538 1135
6 2 128 4096 96 152323 63800 12416 4925
7 2 256 4096 96.4 109565 46953 3067 1195

The same Event Session was used to gather the above metrics in seven repetitive tests.  The test server is a Dell R710 dual quad core system with 24GB RAM and HT enabled.  It has eight internal 15K RPM 146GB SAS drives that are configured into 2 RAID 1 drive pairs and a single 4 disk RAID 5 array.  One of the RAID 1 drive pairs was dedicated to the OS, and SQL Binaries.  The other was used for writing the backups, and the database data/log/tempdb files were placed on the RAID 5 array.  This isn’t the ideal configuration for a setup, but its what I had available at the moment to work with, and is similar to some of the configurations I’ve seen in the real world as well.  The Backups were segregated to a dedicated disk that was RAID 1 for this test to avoid RAID 5 write penalties, and to maximize the backup write throughput by isolating it from any other operations.

The above results can be interpreted a number of different ways.  As the BUFFERCOUNT increases the backup time decreases, and so does the amount of time spent waiting on Backup Buffers.  However, there is a tradeoff that is being made in memory consumption; specifically memory outside of the buffer pool from Virtual Address Space.  On 32 bit servers this can lead to Out of Memory exceptions, the topic of Amit’s blog post referenced previously in this blog post.  Test Number 5, with 256 buffers and a 4MB transfer size will use 1GB of memory as shown in the Backup Buffer Configuration Information.

image

On the test server used for this testing, the bottleneck is the disks that the backup file is being written to and further improvements in performance will require additional IO to accomplish.  The test database has 30GB of data in it, and with backup compression, the backup size is 7.8GB in size on disk.  For a full backup to take just over a minute and a half for this database is not that bad, but it is going to local disks and there is no safeguard for the data in the event of a catastrophic loss of the physical server entirely unless the data gets copied to another location in the network after a local backup occurs.

Today’s post is a continuation of yesterday’s post How Many Checkpoints are Issued During a Full Backup? and the investigation of Database Engine Internals with Extended Events.  In today’s post we’ll look at how Backup’s work inside of SQL Server and how to track the throughput of Backup and Restore operations.  This post is not going to cover Backups in SQL Server as a topic; if that is what you are looking for see Paul Randal’s TechNet Article Understanding SQL Server Backups.

Yesterday I mentioned that there is only one Event in Extended Events that has the word backup in it's name, and that Event is the sqlserver.backup_and_restore_throughput Event.  At first glance this Event looks pretty dull.  It only returns three columns, database_id, count, and increment, and doesn’t really tell us what count and increment mean in the metadata.

-- Get the Event columns
SELECT
    
OBJECT_NAME,
    
name,
    
type_name,
    
description
FROM sys.dm_xe_object_columns
WHERE OBJECT_NAME LIKE '%backup%'
  
AND column_type = 'data'

image

I could step you through what I did to look at this Event and figure out the meaning of things, but that would make an already long post longer.  Essentially I created an Event Session with just this Event and used the sqlserver.session_id Predicate to only capture it for a specific session_id that I was going to run a FULL backup from.  The count column is the total number of bytes that have been written to backups and the increment column is the current number of bytes that were written when the Event fired (we’ll see this more in a minute).  This was interesting to see so I started thinking about what kind of information I would want to know about Backups that related to the throughput and two items came to mind almost immediately; read operations from the database, and wait statistics related to the Backup occurring, both of which are available through Extended Events.  I also recalled that there were a few documented Trace Flags associated with Backup and Restore operations that output more verbose information through Trace Prints.  Trace Flag 3004, outputs what operations Backup and Restore are performing (How It Works: What is Restore/Backup Doing?).  Trace Flag 3213, outputs the Backup Buffer configuration information as discussed on the SQLCAT team blog series Tuning the Performance of Backup Compression in SQL Server 2008 and Tuning Backup Compression Part 2.  Trace Flag 3014, outputs additional information about Backup and File operations (How It Works: How does SQL Server Backup and Restore select transfer sizes).  There happens to be a sqlserver.trace_print Event that can capture the trace output as a part of our Event Session.

Using yesterday’s post as a foundation for the Event Session in today’s post, and the same Sample_Reporting Database, lets look at the Event Session that we’ll use to investigate Backups.

-- Create the Event Session
CREATE EVENT SESSION BackupMonitoring
ON SERVER
ADD EVENT sqlserver.sql_statement_starting
(   ACTION (sqlserver.database_id, sqlserver.sql_text)
    
WHERE (sqlserver.session_id = 97)),
ADD EVENT sqlserver.sql_statement_completed
(   ACTION (sqlserver.database_id, sqlserver.sql_text)
    
WHERE (sqlserver.session_id = 97)),
ADD EVENT sqlserver.databases_backup_restore_throughput
(   WHERE (sqlserver.session_id = 97)),
ADD EVENT sqlos.wait_info
(   ACTION (sqlserver.database_id)
    
WHERE (sqlserver.session_id = 97  AND duration > 0)),
ADD EVENT sqlos.wait_info_external
(   ACTION (sqlserver.database_id)
    
WHERE (sqlserver.session_id = 97  AND duration > 0)),
ADD EVENT sqlserver.trace_print
(   WHERE (sqlserver.session_id = 97)),
ADD EVENT sqlserver.file_read
(   WHERE (sqlserver.session_id = 97)),
ADD EVENT sqlserver.file_read_completed
(   WHERE (sqlserver.session_id = 97)),
ADD EVENT sqlserver.physical_page_read
(   WHERE (sqlserver.session_id = 97)),
ADD EVENT sqlserver.databases_log_cache_read
(   WHERE (database_id = 41)),
ADD EVENT sqlserver.databases_log_cache_hit
(   WHERE (database_id = 41)),
ADD EVENT sqlserver.databases_log_flush
(   WHERE (database_id = 41)),
ADD EVENT sqlserver.checkpoint_begin
(   WHERE (database_id = 41)),
ADD EVENT sqlserver.checkpoint_end
(   WHERE (database_id = 41))
ADD TARGET package0.asynchronous_file_target(
    
SET filename='C:\SQLBlog\BackupMonitoring1.xel',
        
metadatafile = 'C:\SQLBlog\BackupMonitoring1.xem')
GO
-- Alter the Session to Start it
ALTER EVENT SESSION BackupMonitoring
ON SERVER
STATE
=START
GO

There is a lot of information being collected in this Event Session.  We are going to get the sql_statement_starting and completed Events, the backup_restore_throughput Event, wait_info Event for SQLOS waits inside of SQL Server and the wait_info_external Event for preemptive waits outside of SQL Server, the trace_print Event to capture our Trace Flag outputs, the file_read, file_read_completed, and physical_page_read Events to capture read operations from the session_id performing the Backup, the database_log_cache_read, database_log_cache_hit, and database_log_flush Events to track transaction log cache operations during the Backup, and the checkpoint_begin and checkpoint_end Events to track checkpoint occurrence during the backup and how they might impact throughput.  If you notice, some of the Events are Predicated on the session_id, while others are predicated on the database_id, and this is very intentional in the definition of this Event Session.  Some Events do not fire in the context of a specific database_id, and some Events do not fire in the context of a specific session_id, and some will fire for both.  Where the database_id is a practical Predicate for the Event, and it is carried in the Events base payload, it is a natural item to use for a Predicate.  Restricting Events to a specific database_id or session_id will prevent Event capture from other operations occurring on the SQL Server.

With our Event Session defined and started, we can now run a Backup of the database and see what we capture.  I am going to show two different Backup configurations in this post, based on the information contained in the SQLCAT series on tuning Backup Performance in SQL Server 2008.  The first one uses a default configuration for the BUFFERCOUNT and MAXTRANSFERSIZE Backup options, but also uses Database Compression since it is available to minimize he backup file size, and maximize the throughput of the backup operation.

BACKUP DATABASE [Sample_Reporting]
TO  DISK = N'B:\SQLData\MSSQL10.MSSQLSERVER\MSSQL\Backup\Sample_Reporting1.bak'
WITH NOFORMAT,
    
NOINIT,  
    
NAME = N'Sample_Reporting-Full Database Backup Number 1',
    
SKIP,
    
NOREWIND,
    
NOUNLOAD,
    
COMPRESSION,
    
STATS = 5
GO

On this server, the backups are writing to a dedicated RAID1 disk array using two 146GB 15K RPM SAS drives.  When the backup completes we can begin our analysis of the Events captured by our Event Session.  To make it possible to perform various types of analysis of the data contained inside of the asynchronous_file_target, I am going to read the Raw XML Event data into a temporary table, and then shred the XML into a second temporary table, making it possible to just query the shredded data.

DROP TABLE #EventData
DROP TABLE #TestResults

-- Create intermediate temp table for raw event data
CREATE TABLE #EventData
(Rowid INT IDENTITY PRIMARY KEY, event_data XML)

-- Create final results table for parsed event data
CREATE TABLE #TestResults
(Rowid INT PRIMARY KEY, event_name VARCHAR(50), package_name VARCHAR(50),
[timestamp] datetime2, database_id INT, trace_print NVARCHAR(4000),
[count] bigint, increment bigint, wait_type NVARCHAR(100), opcode NVARCHAR(10),
duration bigint, max_duration bigint, total_duration bigint, signal_duration bigint,
completed_count bigint, source_database_id INT, [object_id] INT, object_type INT,
[state] NVARCHAR(50), offset bigint, offset_end INT, nest_level INT, cpu INT,
reads bigint, writes bigint, mode NVARCHAR(50), FILE_ID INT, page_id INT,
file_group_id INT, sql_text NVARCHAR(4000))

-- Read the file data into intermediate temp table
INSERT INTO #EventData (event_data)
SELECT
    
CAST(event_data AS XML) AS event_data
FROM sys.fn_xe_file_target_read_file('C:\SQLBlog\BackupMonitoring1*.xel', 'C:\SQLBlog\BackupMonitoring1*xem', NULL, NULL)

-- Query the Event data from the Target.
INSERT INTO #TestResults
(Rowid, event_name, package_name, [timestamp], database_id, trace_print,
[count], increment, wait_type, opcode, duration, max_duration, total_duration,
signal_duration, completed_count, source_database_id, [object_id], object_type,
[state], offset, offset_end, nest_level, cpu,  reads, writes, mode, FILE_ID,
page_id, file_group_id, sql_text)

SELECT
    
RowID,
    
event_data.value('(event/@name)[1]', 'varchar(50)') AS event_name,
    
event_data.value('(event/@package)[1]', 'varchar(50)') AS package_name,
    
DATEADD(hh,
            
DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP),
            
event_data.value('(event/@timestamp)[1]', 'datetime2')) AS [timestamp],
    
COALESCE(event_data.value('(event/data[@name="database_id"]/value)[1]', 'int'),
                
event_data.value('(event/action[@name="database_id"]/value)[1]', 'int')) AS database_id,
    
event_data.value('(event/data[@name="message"]/value)[1]', 'nvarchar(4000)') AS trace_print,
    
event_data.value('(event/data[@name="count"]/value)[1]', 'bigint')  AS [count],
    
event_data.value('(event/data[@name="increment"]/value)[1]', 'bigint')  AS [increment],
    
event_data.value('(event/data[@name="wait_type"]/text)[1]', 'nvarchar(100)') AS wait_type,
    
event_data.value('(event/data[@name="opcode"]/text)[1]', 'nvarchar(10)') AS opcode,
    
event_data.value('(event/data[@name="duration"]/value)[1]', 'bigint')  AS duration,
    
event_data.value('(event/data[@name="max_duration"]/value)[1]', 'bigint')  AS max_duration,
    
event_data.value('(event/data[@name="total_duration"]/value)[1]', 'bigint')  AS total_duration,
    
event_data.value('(event/data[@name="signal_duration"]/value)[1]', 'bigint')  AS signal_duration,
    
event_data.value('(event/data[@name="completed_count"]/value)[1]', 'bigint')  AS completed_count,
    
event_data.value('(event/data[@name="source_database_id"]/value)[1]', 'int')  AS source_database_id,
    
event_data.value('(event/data[@name="object_id"]/value)[1]', 'int')  AS OBJECT_ID,
    
event_data.value('(event/data[@name="object_type"]/value)[1]', 'int')  AS object_type,
    
event_data.value('(event/data[@name="state"]/text)[1]', 'nvarchar(50)') AS state,
    
event_data.value('(event/data[@name="offset"]/value)[1]', 'bigint')  AS offset,
    
event_data.value('(event/data[@name="offset_end"]/value)[1]', 'int')  AS offset_end,
    
event_data.value('(event/data[@name="nest_level"]/value)[1]', 'int')  AS nest_level,    
    
event_data.value('(event/data[@name="cpu"]/value)[1]', 'int')  AS cpu,    
    
event_data.value('(event/data[@name="reads"]/value)[1]', 'bigint')  AS reads,
    
event_data.value('(event/data[@name="writes"]/value)[1]', 'bigint')  AS writes,
    
event_data.value('(event/data[@name="mode"]/text)[1]', 'nvarchar(50)') AS mmode,
    
event_data.value('(event/data[@name="file_id"]/value)[1]', 'int')  AS FILE_ID,
    
event_data.value('(event/data[@name="page_id"]/value)[1]', 'int')  AS page_id,
    
event_data.value('(event/data[@name="file_group_id"]/value)[1]', 'int')  AS file_group_id,        
    
event_data.value('(event/action[@name="sql_text"]/value)[1]', 'nvarchar(4000)') AS sql_text
FROM #EventData
ORDER BY Rowid

-- Look at the Results.
SELECT
    
Rowid,
    
event_name,
    
database_id,
    
trace_print,
    
[count],
    
increment,
    
wait_type,
    
duration,
    
signal_duration,
    
cpu,
    
reads,
    
writes,
    
mode,
    
FILE_ID,
    
page_id,
    
file_group_id,
    
sql_text
FROM #TestResults
ORDER BY Rowid

In the above query, I am extracting all of the data elements from the Event data, even though in the final query I am not using all of the data.  I did this to have a complete example of how to shred the XML, and because we are storing it in a temp table, we may find that we want to come back and look at specific data elements that were excluded in the initial look at the results.  From our results we can begin to understand how Backup Operations work inside of SQL Server.

image

Here we can see the statement starting to execute, and the first output from the trace_print Event showing that the backup of the database was starting, along with the external waits associated with performing file operations to create a Backup file for the database.

image

Here the newly created Backup file is opened an 1K of write occur to the file before it becomes ready for the Backup.

image

Here we can see two operations being performed.  In the red outlined box, since we are doing a full Backup of the database, the differential bitmaps, pages that track which extents in a GAM interval have been modified since the last full or differential backup (Inside the Storage Engine: Anatomy of an extent), are cleared.  In the blue outlined box, we see the checkpoint triggered by the Backup operation begin, and in the four highlighted boxes in grey, we see two physical_page_reads occur for the database, one from the transaction log, and one from the primary data file.  These are the pages that are written to when Checkpoint occurs in the database.  The file_id 2 page_id 0 page is where the Log Sequence Number is written to the log file, and file_id 1 page_id 9 is the database boot page, where the LSN is also written to at checkpoint (Checkpoints and the Active Portion of the Log).  We also see the wait_info event for the PAGEIOLATCH_UP wait to update of this information.

image

Next the allocation bitmaps for the database are scanned and an estimate of the work is performed (red box) before writing 5K of metadata into the backup file. 

image

At this point the Backup process is ready to begin copying data to the Backup file.  Since this particular database only has 1 data file, only one reader is assigned to the backup (Optimising Backup & Restore Performance in SQL Server).  When the Backup starts on the file, an additional 1024 bytes (1K of information is written to the Backup file and the file read operations against the database data file(s) commences.

image

As the backup of the data file data begins, we see a change in the size of the increment being written to the Backup file, and now we have 1MB segments of data being written to the file.

image
We can also see that multiple 1MB segments are written within milliseconds of each other.  Now I could spend a lot of time running through the entire set of Events showing the same thing, but with 187,698 Events for a 110 second Backup, that would take forever.  Instead I am going to skip over all the interim file reads and Backup file writes and get to the end of the data file section.

image

Highlighted in black above, we see the completion of the first data file, followed by a trace_print event, in red, Padding MSDA with 196608 bytes (192K of space), and then the trace_print event, in blue, showing the completion of all Database files, which is also the beginning of the Transaction log portion of the backup.

image

Here we can see that the size of the log information being backed up, highlighted in black, is significantly different from the data file information which is to be expected since log records are very different from data records in composition.  When the log files done trace_print Event in red, and the trailing configuration writres in blue and the trace_print Event in orange marking the completion of the trailing configuration.

image

I don’t know what MBC done means, and I couldn’t find it online, but it completed here.  I think it might stand for MaxBufferCount, and the above line shows that all of the buffers have been written out for the backup. (Don’t quote me on that I am just taking a SWAG there!)

image

After the MBC is done, the backup history records are written into MSDB.

image 

And finally the backup completes.  So far all that we’ve done is look at all of the information that we can get, and there is a lot of it, but unless we can do something actionable with all this information, there is no real point in gathering it.  I originally intended to only cover one post on this subject but its become quite large, so I am splitting it into two posts and in tomorrow’s post we’ll look at how we can use the information captured in today’s post to validate whether or not changes to our backup process have a positive or negative impact on backup times and throughput.

This wasn’t my intended blog post for today, but last night a question came across #SQLHelp on Twitter from Varun (Twitter).

#sqlhelp how many checkpoints are issued during a full backup?

The question was answered by Robert Davis (Blog|Twitter) as:

Just 1, at the very start. RT @1sql: #sqlhelp how many checkpoints are issued during a full backup?

This seemed like a great thing to test out with Extended Events so I ran through the available Events in SQL Server 2008, and the only Event related to Backup is the sqlserver.databases_backup_restore_throughput Event, something which is a topic for another blog post, but that doesn’t matter because we can still do testing of this by using the Events available in Extended Events.  The sqlserver.sql_statement_starting, sqlserver.sql_statement_completed, sqlserver.checkpoint_begin and sqlserver.checkpoint_end Events can be used to test this with appropriate Predicate definitions.

To test this I used a copy of two databases on a development server.  One is a source database and the second is a reporting database.  I also duplicated the ETL process that extracts data from a source database and transforms it into the reporting schema so that I could test this under a workload that would be changing data and should cause checkpoints to occur inside of the reporting database.  Then I queried sys.databases (ok I actually used DB_ID(‘Sample_Reporting’)) to get the database_id for the Sample_Reporting database to use in the Predicate for the sqlserver.checkpoint_begin and sqlserver.checkpoint_end Events. 

image

Then I opened a new Query Window in SSMS and used that connections session_id in the Predicate for the sqlserver.sql_statement_starting and sqlserver.sql_statement_completed Events in the Event Session.  The result was the following Session definition.

-- Create the Event Session
CREATE EVENT SESSION BackupCheckPoints
ON SERVER
ADD EVENT sqlserver.sql_statement_starting
(    ACTION (sqlserver.database_id, sqlserver.sql_text)
    
WHERE (sqlserver.session_id = 113)),
ADD EVENT sqlserver.sql_statement_completed
(    ACTION (sqlserver.database_id, sqlserver.sql_text)
    
WHERE (sqlserver.session_id = 113)),
ADD EVENT sqlserver.checkpoint_begin
(    WHERE (database_id= 41)),
ADD EVENT sqlserver.checkpoint_end
(    WHERE (database_id = 41))
ADD TARGET package0.ring_buffer
GO
-- Alter the Session to Start it
ALTER EVENT SESSION BackupCheckpoints
ON SERVER
STATE
=START
GO

With the Event Session started, I then started a FULL backup of the Sample Reporting database, followed by starting the ETL processes.  When the FULL backup completed I dropped the Events from the Event Session so that no further Event collection occurred.

-- Drop Events to halt Event collection
ALTER EVENT SESSION BackupCheckPoints
ON SERVER
DROP EVENT sqlserver.sql_statement_starting,
DROP EVENT sqlserver.sql_statement_completed,
DROP EVENT sqlserver.checkpoint_begin,
DROP EVENT sqlserver.checkpoint_end

Now we can query the ring_buffer Target and see what has occurred during the FULL backup of the Sample_Reporting database.

-- Query the XML to get the Target Data
SELECT
    
n.value('(event/@name)[1]', 'varchar(50)') AS event_name,
    
n.value('(event/@package)[1]', 'varchar(50)') AS package_name,
    
DATEADD(hh,
            
DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP),
            
n.value('(event/@timestamp)[1]', 'datetime2')) AS [timestamp],
    
ISNULL(n.value('(event/data[@name="database_id"]/value)[1]', 'int'),
            
n.value('(event/action[@name="database_id"]/value)[1]', 'int')) AS [database_id],
    
n.value('(event/action[@name="sql_text"]/value)[1]', 'nvarchar(max)') AS [sql_text]
FROM
(    SELECT td.query('.') AS n
    
FROM
    
(
        
SELECT CAST(target_data AS XML) AS target_data
        
FROM sys.dm_xe_sessions AS s
        
JOIN sys.dm_xe_session_targets AS t
            
ON t.event_session_address = s.address
        
WHERE s.name = 'BackupCheckpoints'
          
AND t.target_name = 'ring_buffer'
    
) AS sub
    
CROSS APPLY target_data.nodes('RingBufferTarget/event') AS q(td)
)
AS tab
GO

image

As you can see in the above screenshot, multiple checkpoints can occur during a FULL backup of a database in SQL Server 2008.  According to Paul Randal, “Checkpoints exist for two reasons—to batch up write I/Os to improve performance and to reduce the time required for crash recovery” (http://technet.microsoft.com/en-us/magazine/2009.02.logging.aspx).  Since we are continuing to make changes to the data inside of the system while the FULL backup occurs, there is a continued need for CHECKPOINT’s to occur for the database.

If you don’t know anything about Ghost Cleanup, I recommend highly that you go read Paul Randal’s blog posts Inside the Storage Engine: Ghost cleanup in depth, Ghost cleanup redux, and Turning off the ghost cleanup task for a performance gain.  To my knowledge Paul’s posts are the only things that cover Ghost Cleanup at any level online.

In this post we’ll look at how you can use Extended Events to track the activity of Ghost Cleanup inside of your SQL Server.  To do this, we’ll first take a look at the ghost_cleanup Event and what it returns.

-- Find the Event
SELECT 
    p.name, 
    o.name, 
    o.description
FROM sys.dm_xe_packages AS p
JOIN sys.dm_xe_objects AS o
    ON p.guid = o.package_guid
WHERE o.name = 'ghost_cleanup'

-- Get the data columns for the Event
SELECT 
    name, 
    TYPE_NAME
FROM sys.dm_xe_object_columns
WHERE OBJECT_NAME = 'ghost_cleanup'
  AND column_type = 'data'

image

The ghost_cleanup Event is in the sqlserver Package and returns the file_id and page_id that the ghost cleanup process is working on.  Since most SQL Servers have multiple databases, we probably will want to track the database_id through an Action as well.  Since ghost cleanup is a background process, and we don’t know much about how it works, or how many Events are going to be generated, we could start off with the synchronous_event_counter Target, but in this case, I just want to capture Events and all of them, so we will just go with the asynchronous_file_target.

CREATE EVENT SESSION TrackGhostCleanup
ON SERVER
ADD EVENT sqlserver.ghost_cleanup
( ACTION(sqlserver.database_id))
ADD TARGET package0.asynchronous_file_target(
     SET filename='C:\SQLBlog\TrackGhostCleanup.xel',
         metadatafile='C:\SQLBlog\TrackGhostCleanup.xem')
WITH (MAX_MEMORY = 4MB, EVENT_RETENTION_MODE = NO_EVENT_LOSS )
GO
ALTER EVENT SESSION TrackGhostCleanup
ON SERVER
STATE=START

This is a really basic Event Session, it captures one Event, sqlserver.ghost_cleanup and collects the sqlserver.database_id Action when the Event fires.  The Event data is captured by the package0.asynchronous_file_target, and the Event Session is configured to not allow Event Loss.  After the starting the Event Session and allowing it to run, we can query the files for the captured events and see how ghost_cleanup is running on our instance.

-- Query the Event data from the Target.
SELECT 
    event_data.value('(event/@name)[1]', 'varchar(50)') AS event_name,
    event_data.value('(event/@package)[1]', 'varchar(50)') AS package_name,
    event_data.value('(event/@id)[1]', 'int') AS id,
    event_data.value('(event/@version)[1]', 'int') AS version,
    DATEADD(hh, 
            DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP), 
            event_data.value('(event/@timestamp)[1]', 'datetime2')) AS [timestamp],
    event_data.value('(event/action[@name="database_id"]/value)[1]', 'int') as database_id,
    event_data.value('(event/data[@name="file_id"]/value)[1]', 'int') as file_id,
    event_data.value('(event/data[@name="page_id"]/value)[1]', 'int') as page_id
FROM 
(SELECT
    CAST(event_data AS XML) AS event_data
 FROM sys.fn_xe_file_target_read_file('C:\SQLBlog\TrackGhostCleanup*.xel', 'C:\SQLBlog\TrackGhostCleanup*xem', null, null)
) as tab

From around 15 minutes of runtime on one of my development servers, over 17.5K Events have fired, much more than I initially anticipated, and after nearly 30 minutes of runtime, I had just over 37K Events. 

image

Some interesting information can be found in the Events.  In SQL Server 2008, the Ghost Cleanup process runs every 10 seconds, just as Paul has documented in his blog posts, which was a change from every 5 seconds in SQL Server 2005.

image

The process in 2008 cleans up 200 pages at a time, something Paul hasn’t specifically blogged about for SQL Server 2008.  Before anyone debates this, Paul’s statement “It will check through or cleanup a limited number of pages each time it wakes up - I remember the limit is 10 pages - to ensure it doesn't swamp the system.” from his Inside the Storage Engine: Ghost cleanup in depth is based on SQL Server 2005, which also ran ghost cleanup every 5 seconds instead of 10 seconds.  We can look at the Event information over subsequent 10 second intervals and see that 200 pages are cleaned up each time ghost_cleanup runs.

image

image

This has to be one of my favorite aspects of Extended Events.  You get to really learn about SQL Server Internals by just playing with SQL Server.  I have a couple more blog posts that show how you can learn about SQL Server Internals using Extended Events for this series, and if you are interested in a previous post on the subject check out TSQL Tuesday #11 – Physical IO’s Don’t Always Accumulate Wait Times.

Until tomorrow….

When working with SQL Trace, one of my biggest frustrations has been the limitations that exist in filtering.  Using sp_trace_setfilter to establish the filter criteria is a non-trivial task, and it falls short of being able to deliver complex filtering that is sometimes needed to simplify analysis.  Filtering of trace data was performed globally and applied to the trace affecting all of the events being collected.  Extended Events introduces a much better system of filtering using Predicates that are applied at the individual Event level, allow for short circuiting of evaluation, and provide the ability to create complex groups of independent criteria, ensuring only Events of interest are captured by the Event Session.

In yesterdays post, The system_health Session, I talked about the default system_health session that is running on every SQL Server 2008/2008R2 and Denali CTP1 instance out of the box.  The Predicate definition for the sqlos.wait_info event in the system_health session is a good example to follow for complex, short-circuiting Predicate definition in Extended Events.

ADD EVENT sqlos.wait_info
(
    ACTION (package0.callstack, sqlserver.session_id, sqlserver.sql_text)
    WHERE 
    (duration > 15000 AND 
        (    
            (wait_type > 31    -- Waits for latches and important wait resources (not locks) 
                            -- that have exceeded 15 seconds. 
                AND
                (
                    (wait_type > 47 AND wait_type < 54)
                    OR wait_type < 38
                    OR (wait_type > 63 AND wait_type < 70)
                    OR (wait_type > 96 AND wait_type < 100)
                    OR (wait_type = 107)
                    OR (wait_type = 113)
                    OR (wait_type > 174 AND wait_type < 179)
                    OR (wait_type = 186)
                    OR (wait_type = 207)
                    OR (wait_type = 269)
                    OR (wait_type = 283)
                    OR (wait_type = 284)
                )
            )
            OR 
            (duration > 30000        -- Waits for locks that have exceeded 30 secs.
                AND wait_type < 22
            ) 
        )
    )
),

Since Predicates perform short-circuit evaluation, where the criteria groups are evaluated in order and the first failure in the criteria causes the Predicate evaluation to stop and preventing the Event from being fired in the engine, the order of the criteria can directly impact the performance of an Event Session.  If we look at the definition for the sqlos.wait_info Event, the first Predicate criteria specifies that the duration of the wait has to be greater than 15 seconds.  Since the majority of waits in SQL Server generally occur with durations less than 15 seconds, the Predicate evaluations shortcut immediately and the Event does not fire.  If the wait exceeds the 15 second duration, the evaluation continues and checks that the wait_type matches one of defined values.  How do we know what these values are? 

When looking at an Event, all of the columns have a type_name associated with them that can be found in the sys.dm_xe_object_columns DMV as previously discussed in this series.  If we take a look at the type_name for the wait_info Event wait_type column, we’ll see that it has a type of wait_types.

SELECT 
    name, 
    type_name, 
    column_type
FROM sys.dm_xe_object_columns
WHERE object_name = 'wait_info'
  AND column_type <> 'readonly'
image

When a column has a non-standard type_name like this, it corresponds to a Map that has been loaded in Extended Events.  We can find a list of the wait_types that the Event will fire for by querying the sys.dm_xe_map_values DMV for the map_keys defined in the Event Session:

SELECT map_key, map_value
FROM sys.dm_xe_map_values
WHERE name = 'wait_types'
  AND 
    (map_key > 31    -- Waits for latches and important wait resources (not locks) 
                    -- that have exceeded 15 seconds. 
        AND
        (
            (map_key > 47 AND map_key < 54)
            OR map_key < 38
            OR (map_key > 63 AND map_key < 70)
            OR (map_key > 96 AND map_key < 100)
            OR (map_key = 107)
            OR (map_key = 113)
            OR (map_key > 174 AND map_key < 179)
            OR (map_key = 186)
            OR (map_key = 207)
            OR (map_key = 269)
            OR (map_key = 283)
            OR (map_key = 284)
        )
    )

The wait_types that correspond to the first complex grouping are:

map_key map_value
32 LATCH_NL
33 LATCH_KP
34 LATCH_SH
35 LATCH_UP
36 LATCH_EX
37 LATCH_DT
48 PAGELATCH_NL
49 PAGELATCH_KP
50 PAGELATCH_SH
51 PAGELATCH_UP
52 PAGELATCH_EX
53 PAGELATCH_DT
64 PAGEIOLATCH_NL
65 PAGEIOLATCH_KP
66 PAGEIOLATCH_SH
67 PAGEIOLATCH_UP
 
map_key map_value
68 PAGEIOLATCH_EX
69 PAGEIOLATCH_DT
97 IO_COMPLETION
98 ASYNC_IO_COMPLETION
99 NETWORK_IO
107 RESOURCE_SEMAPHORE
113 SOS_WORKER
175 FCB_REPLICA_WRITE
176 FCB_REPLICA_READ
177 HOLDER11
178 WRITELOG
186 CMEMTHREAD
207 TRACEWRITE
269 RESOURCE_SEMAPHORE_MUTEX
283 RESOURCE_SEMAPHORE_QUERY_COMPILE
284 RESOURCE_SEMAPHORE_SMALL_QUERY

If you look at the way the Predicate is defined, it is much closer to how you’d write a WHERE clause with complex filtering criteria, allowing groups of specific criteria to be defined within sets of parenthesis’s that are evaluated together, something that was impossible with SQL Trace.

In addition to being able to define Predicates based on the Event columns returned by an Event, it is possible to also define Predicates on the global state data available in the Extended Events Engine.  If you’ll recall, the global state predicates are available in the sys.dm_xe_objects DMV as pred_source object_type’s.

SELECT 
    p.name AS package_name,
    o.name AS predicate_name,
    o.description
FROM sys.dm_xe_packages AS p
INNER JOIN sys.dm_xe_objects AS o
    ON p.guid = o.package_guid
WHERE (p.capabilities IS NULL OR p.capabilities & 1 = 0)
  AND o.object_type = 'pred_source'
  image

Two of the predicate sources are special, the package0.counter and package0.partitioned_counter, and can be used to restrict the number of occurrences of an Events that are captured by an Event Session.  The following demonstration creates an Event Session that captures the first five occurrences of the sqlserver.sql_statement_completed Event, and then executes six statements in sequence.  When the target_data is queried the last statement SELECT @@SPID is not included in the results.

CREATE EVENT SESSION CounterPredicateDemo
ON SERVER
ADD EVENT sqlserver.sql_statement_completed
( ACTION (sqlserver.sql_text)
  WHERE (package0.counter <=5))
ADD TARGET package0.ring_buffer
WITH (MAX_DISPATCH_LATENCY = 1 SECONDS)
GO
ALTER EVENT SESSION CounterPredicateDemo
ON SERVER
STATE = START
GO
SELECT @@VERSION
GO
SELECT @@SERVERNAME
GO
SELECT @@SPID
GO
SELECT @@VERSION
GO
SELECT @@SERVERNAME
GO
SELECT @@SPID
GO
ALTER EVENT SESSION CounterPredicateDemo
ON SERVER
DROP EVENT sqlserver.sql_statement_completed
GO
-- Wait in a delay for Events to Buffer
WAITFOR DELAY '00:00:05'
GO

-- Query the XML to get the Target Data
SELECT 
    n.value('(event/@name)[1]', 'varchar(50)') AS event_name,
    n.value('(event/@package)[1]', 'varchar(50)') AS package_name,
    DATEADD(hh, 
            DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP), 
            n.value('(event/@timestamp)[1]', 'datetime2')) AS [timestamp],  
    n.value('(event/data[@name="object_id"]/value)[1]', 'int') AS [object_id],
    n.value('(event/data[@name="object_type"]/value)[1]', 'nvarchar(128)') AS [object_type],
    n.value('(event/data[@name="duration"]/value)[1]', 'int') AS [duration],
    n.value('(event/data[@name="cpu"]/value)[1]', 'int') AS [cpu],
    n.value('(event/data[@name="reads"]/value)[1]', 'int') AS [reads],
    n.value('(event/data[@name="writes"]/value)[1]', 'int') AS [writes],
    n.value('(event/action[@name="sql_text"]/value)[1]', 'nvarchar(max)') AS [sql_text]
FROM
(    SELECT td.query('.') as n
    FROM 
    (
        SELECT CAST(target_data AS XML) as target_data
        FROM sys.dm_xe_sessions AS s 
        JOIN sys.dm_xe_session_targets AS t 
            ON t.event_session_address = s.address
        WHERE s.name = 'CounterPredicateDemo'
          AND t.target_name = 'ring_buffer'
    ) AS sub
    CROSS APPLY target_data.nodes('RingBufferTarget/event') AS q(td)
) as tab
GO
-- Drop the Event Session
DROP EVENT SESSION CounterPredicateDemo
ON SERVER

image

This capabilities behind Predicate definition in Extended Events makes it much more flexible, and powerful for troubleshooting than SQL Trace.  It also makes Extended Events much more performant than Trace by preempting Event firing for Events that are not of interest.

Today’s post was originally planned for this coming weekend, but seems I’ve caught whatever bug my kids had over the weekend so I am changing up today’s blog post with one that is easier to cover and shorter.  If you’ve been running some of the queries from the posts in this series, you have no doubt come across an Event Session running on your server with the name of system_health.  In today’s post I’ll go over this session and provide links to references related to it.

When Extended Events was introduced in SQL Server 2008, the Produce Support Services group worked with the Extended Events developers to create the definition for an Event Session that could be shipped with SQL Server 2008, would startup automatically when SQL Server starts up, and contained Events of interest in troubleshooting common problems seen by the PSS Engineers.  Bob Ward (Blog|Twitter) blogged about the details of the system_health session that shipped with SQL Server 2008 in his blog post Supporting SQL Server 2008: The system_health session.  The script for this Event Session is inside of the utables.sql script file that is in the instance Install folder (for example c:\Program Files\Microsoft SQL Server\<InstanceDesignator>\MSSQL\Install) and can be used to recreate the Event Session if you inadvertently change it.

-- The predicates in this session have been carefully crafted to minimize impact of event collection
-- Changing the predicate definition may impact system performance
--
CREATE EVENT SESSION system_health ON SERVER
  • The sql_text and session_id for any sessions that encounter an error that has a severity >=20.
  • The sql_text and session_id for any sessions that encounter a memory-related error. The errors include 17803, 701, 802, 8645, 8651, 8657 and 8902.
ADD EVENT sqlserver.error_reported
(
    ACTION (package0.callstack, sqlserver.session_id, sqlserver.sql_text, sqlserver.tsql_stack)
    -- Get callstack, SPID, and query for all high severity errors ( above sev 20 )
    WHERE severity >= 20
    -- Get callstack, SPID, and query for OOM errors ( 17803 , 701 , 802 , 8645 , 8651 , 8657 , 8902 )
    OR (ERROR = 17803 OR ERROR = 701 OR ERROR = 802 OR ERROR = 8645 OR ERROR = 8651 OR ERROR = 8657 OR ERROR = 8902)
),
  • A record of any non-yielding scheduler problems. (These appear in the SQL Server error log as error 17883.)
ADD EVENT sqlos.scheduler_monitor_non_yielding_ring_buffer_recorded,
  • Any deadlocks that are detected.
ADD EVENT sqlserver.xml_deadlock_report,
  • The callstack, sql_text, and session_id for any sessions that have waited on latches (or other interesting resources) for > 15 seconds.
ADD EVENT sqlos.wait_info
(
    ACTION (package0.callstack, sqlserver.session_id, sqlserver.sql_text)
    WHERE 
    (duration > 15000 AND 
        (    
            (wait_type > 31    -- Waits for latches and important wait resources (not locks ) that have exceeded 15 seconds. 
                AND
                (
                    (wait_type > 47 AND wait_type < 54)
                    OR wait_type < 38
                    OR (wait_type > 63 AND wait_type < 70)
                    OR (wait_type > 96 AND wait_type < 100)
                    OR (wait_type = 107)
                    OR (wait_type = 113)
                    OR (wait_type > 174 AND wait_type < 179)
                    OR (wait_type = 186)
                    OR (wait_type = 207)
                    OR (wait_type = 269)
                    OR (wait_type = 283)
                    OR (wait_type = 284)
                )
            )
  • The callstack, sql_text, and session_id for any sessions that have waited on locks for > 30 seconds.
            OR 
            (duration > 30000        -- Waits for locks that have exceeded 30 secs.
                AND wait_type < 22
            ) 
        )
    )
),
  • The callstack, sql_text, and session_id for any sessions that have waited for a long time for preemptive waits. The duration varies by wait type. A preemptive wait is where SQL Server is waiting for external API calls.
ADD EVENT sqlos.wait_info_external
(
    ACTION (package0.callstack, sqlserver.session_id, sqlserver.sql_text)
    WHERE 
    (duration > 5000 AND
        (   
            (    -- Login related preemptive waits that have exceeded 5 seconds.
                (wait_type > 365 AND wait_type < 372)
                OR    (wait_type > 372 AND wait_type < 377)
                OR    (wait_type > 377 AND wait_type < 383)
                OR    (wait_type > 420 AND wait_type < 424)
                OR    (wait_type > 426 AND wait_type < 432)
                OR    (wait_type > 432 AND wait_type < 435)
            )
            OR 
            (duration > 45000     -- Preemptive OS waits that have exceeded 45 seconds. 
                AND 
                (    
                    (wait_type > 382 AND wait_type < 386)
                    OR    (wait_type > 423 AND wait_type < 427)
                    OR    (wait_type > 434 AND wait_type < 437)
                    OR    (wait_type > 442 AND wait_type < 451)
                    OR    (wait_type > 451 AND wait_type < 473)
                    OR    (wait_type > 484 AND wait_type < 499)
                    OR wait_type = 365
                    OR wait_type = 372
                    OR wait_type = 377
                    OR wait_type = 387
                    OR wait_type = 432
                    OR wait_type = 502
                )
            )
        )
    )
)
  • Capture Event information using the ring_buffer target.
ADD target package0.ring_buffer        -- Store events in the ring buffer target
    (SET max_memory = 4096)
  • Set the session to start automatically with SQL Server
WITH (startup_state = ON)
GO

In SQL Server Denali CTP1, two new Events have been added to the system_health session specific to SQLCLR.

  • A SQLCLR memory allocation failed.
ADD EVENT sqlclr.allocation_failure,
  • A SQLCLR virtual memory allocation failed.
ADD EVENT sqlclr.virtual_alloc_failure,

While the system_health session captures very useful information, it uses the ring_buffer Target to store the Event data.  In a scenario where the database engine fails completely the information that may have been captured by the system_health session will be lost when the process terminates.  Also since the Event Session uses the ring_buffer Target, it is possible that you may not receive back all of the Event data contained in the target, or the Events that you might have expected to exist.  Bob Ward talked about the limitation of the DMV’s to return 4MB of XML data and how this impacts the in memory Targets in Extended Events in his blog post You may not see the data you expect in Extended Event Ring Buffer Targets…. 

One of my favorite aspects of the system_health session is that it includes deadlock tracing through Extended Events by default.  However, in order to make use of the deadlock graph captured by Extended Events, you have to be on CU6 for SQL Server 2008 SP1 (http://support.microsoft.com/kb/978629), or you could try to hack your way around the bug as I showed in my article Retrieving Deadlock Graphs with SQL Server 2008 Extended Events, and Michael Zilberstein’s update to correct a problem with the code in that article, Parsing Extended Events xml_deadlock_report.  The deadlock graph in Extended Events will not open graphically in SSMS like a SQL Trace XML Deadlock Graph will due to changes in its output to support multi-victim deadlocks, which I covered in my blog post Changes to the Deadlock Monitor for the Extended Events xml_deadlock_report and Multi-Victim Deadlocks.

The lack of SSMS support for Extended Events, coupled with the fact that a number of the existing Events in SQL Trace were not implemented in SQL Server 2008, has no doubt been a key factor in its slow adoption rate.  Since the release of SQL Server Denali CTP1, I have already seen a number of blog posts that talk about the introduction of Extended Events in SQL Server, because there is now a stub for it inside of SSMS.  Don’t get excited yet, the functionality in CTP1 is very limited at this point, but the demonstration of what’s to come at the PASS keynote last month showed that more effort has been put into this than is currently available in CTP1, and we should see further improvements in coming releases.  To make working with Extended Events easier, two years ago I worked on a management UI that eventually turned into a SSMS Addin for Extended Events.  In this blog post I’ll go over the features of the SSMS Addin and how it can be used to work with Extended Events.

Features

The Extended Events Manager exists in two forms, a SSMS Addin, and a standalone Winforms application.  The project started out with the Winforms application, and has continued to maintain that as a development environment for functionality changes and new additions to the project, since debugging changes inside of the SSMS Addin proved more difficult than inside of the Winforms application.  This blog post will focus on the functionality of the Addin which matches the functionality of the stand alone applications with only a few difference that relate to the stand alone application not being a part of SSMS.  Currently the Addin only supports SQL Server 2008 and 2008R2, but changes are underway to allow it to support Denali as well.  T

Automatic Update Notifications

The Extended Events SSMS Addin will check for an Internet connection when SSMS starts up, and validate whether or not the latest build is being used.  If a new release is available, then a message box similar to the one below will be displayed.

image

By clicking the “Yes” button on the dialog box, Internet Explorer will be launched and will open the CodePlex project site for the application where you can then download the newest release for use.  The Automatic Update notifications can be turned off by unchecking the box on the popup, or turning off the feature in the Option Manager.

Using the Extended Events Manager

Using the Extended Events Manager is very simple, and a majority of the functionality was implemented to match the way functionality is exposed inside of SSMS already.  The Addin is available from the View menu and opens up the Event Session Explorer Window which can be docked like the Object Explorer inside of SSMS.  The Event Session Explorer will show the Extended Events Session information for the currently selected Server in the Object Explorer if the server is a SQL Server 2008/2008R2 instance, and automatically changes its Server connection when the selected server in the Object Explorer changes. 

Event Session Explorer Window

The Event Session Explorer displays a list of all the Event Sessions that exist inside of the current SQL Server Session Definition Catalogs.  Each Event Session node will show a list of Events and Targets configured in the Event Session. 

image

A majority of the functionality provided by the Extended Events Manager is available through context menu’s in the Event Session Explorer.  There are three different context menu’s available, the Server Level Menu, the Session Level Menu, and the Target Level Menu.

Server Level Menu

image

  • New Event Session

    The New Event Session Menu Item will open up the Event Session Editor with a blank Session form.

  • Refresh

    The Refresh Menu Item does exactly what its name implies and refreshes the Session Metadata held by the Tree View from the SQL Server.

  • View XEvent MetaData

    The View XEvent MetaData Menu Item loads the available Extended Events MetaData from the Extended Events Engine on the Server and displays it in the XEvent Metadata Viewer. The data is categorized by Package with SubNodes for each of the object types supported by the Extended Events Engine on the currently selected Server.

  • Addin Options

    The Addin Options Menu Item opens up the Option Manager for the Addin allowing configuration of the Options available.

Session Level Menu

image

  • Edit Event Session

    The Edit Event Session Menu Item will open up the Event Session Editor with the currently selected Event Sessions metadata bound to the form.

     

  • Drop Event Session

    The Drop Event Session Menu Item will immediately execute a DROP EVENT SESSION command against the database server for the currently selected Event Session.

     

  • Script Session As

    The Script Session As Menu Item provides scripting functionality similar to that provided by Management Studio for the currently selected Event Session. Currently a script can be created to a file or to the system Clipboard. A future release will also provide the ability to script to a new SQL Server Management Studio window.

     

  • Start/Stop Event Session

    The Start Event Session and Stop Event Session Menu Items are active based on the current status of the Event Session on the Server. If the Session is started, then the start option is disabled and the stop option is enabled. Both issue the appropriate ALTER EVENT SESSION command immediately against the SQL Server for the selected Event Session.

     

Target Level Menu

image

  • View Target Data

    The View Target Data Menu Item will open up the Target Data View and display the information contained within the currently selected target.

     

  • Drop Event Session

    The Query Target Data with TSQL Menu Item will make use of Adam Machanic’s Code Generator to generate a TSQL command that can be used to query the target data from the server in a SSMS Query Window.

     

    Metadata Viewer

    The Metadata Viewer allows browsing of the Extended Events Metadata contained inside of the Metadata DMV’s in SQL Server.

    image

    1. Tabbed Document Support

      The Metadata Viewer provides standard Tabbed document functionality inside of SSMS.

    2. MetaData Tree View

      Contains a list of the existing Event Sessions defined on the Server. The session definition can be seen by expanding the sub nodes under the session.

    3. Additional Information Box

      Will contain any additional information contained in the Event Session Metadata for the selected node.

    Event Session Editor

    The core functionality provided by the Extended Events Manager is the ability to create and edit Event Sessions without having to explicitly write TSQL.  The Event Session Editor allow full configuration of an Event Session including the ability to Add/Remove/Edit Events and Targets, and specify the Session Level Options.

    Main Window

    image

    The main window of the Event Session Editor exposes a majority of the information about the Event Session.  There are nine areas of interest in the Session Editor.

    1. Script Action

      image

      The Script Action button on the toolbar provides the same scripting functionality available in many of the windows natively available in SSMS including the ability to script changes to a New SSMS Window, a File, or the Clipboard.  An addition option is to script the changes to a Message Box popup, which was a debugging feature that was never intended to make it into a released build, since it has not intrinsic value over the other options in practical usage, but since it is there I figured I would mention it here.

    2. Session Name

      The name of the Extended Event Session.

    3. Add Event

      Opens the Event Editor UI for a new Event to be added to the Event Session.

      image
    4. Start Event Session Automatically

      Specifies that the Event Session should be altered to a started state when the Ok button is clicked.

    5. Event Grid View

      Displays current Events defined for the Event Session.  Direct editing of Actions and Predicate definitions can be performed by typing in the appropriate cell for the Event to be changed.  Alternatively, the entire Event definition can be edited by clicking the Edit button which will open the Event Editor and bind the Event definition to the Editor.

    6. Targets Grid View

      Displays current Targets defined for the Event Session.  Direct editing of Target Option definitions can be performed by typing in the appropriate cell for the Target to be changed.  Alternatively, the entire Target definition can be edited by clicking the Edit button which will open the Target Editor and bind the Targets definition to the Editor.

    7. Add Target

      Opens the Target Editor UI for a new Target to be added to the Event Session.

      image

    8. Ok/Cancel Buttons

      The Ok button will parse the Session to a script for execution against the SQL Server to change or create the Event Session.  Exceptions are handled and offer to display the specific errors, including line numbers with the Session Script for troubleshooting.  Should there be an unresolved issue it might be best to script the operation to SSMS and perform troubleshooting directly with the DDL command.  In this case, please provide the DDL script in error as a bug on the Codeplex site for the project.  The Cancel Button will close the Session Editor discarding all changes.

    9. Session Option Editor

      image

      The Session Option Editor displays all of the options for the Event Session.

    Target Data Viewer

    The Target Data Viewer shreds the Event Data for all of the Targets currently available in Extended Events and pivots the data so that it can be displayed in a Profiler like table in SSMS.  The target information is displayed at the top, the Event data held inside the target is displayed in the Gridview, and information about the selected row in the Gridview is displayed in the text window below it.  The target data can be incrementally refreshed from the server by clicking the Refresh button.

    image 

    Extended Event Manager Options

    The Extended Event Manager has two configurable options that control whether or not the Addin checks for updates at startup, and whether or not the Addin provides a warning before attempting to DROP an active Event Session from the current server.

    image

    In addition to the Addin specific options, the Addin also offers the ability to set personalized defaults for the Event Session Options that are used every time a new Event Session is created using the Addin.  This can simplify the customization of Event Sessions if you prefer to set Event Session Options that differ from the defaults provided by Extended Events.

    image 

    Using the Library with Powershell

    The Extended Events Manager has its own SMO like library DLL that is the foundation of all of the functionality available inside of the Addin and the stand alone application named XEvents.Library.dll.  This assembly can be used with Powershell to extend management of Extended Events to Powershell, by loading the library using Reflection:

    [reflection.assembly]::LoadFrom("C:\Program Files\SQL Server Extended Events\Extended Events SSMS Addin\XEvents.Library.dll")

    Once the assembly is loaded, a connection to a SQL Server can be made

    $Server = new-object ("XEvents.Library.SqlServer") "ServerName”

    From here, the common PowerShell commands apply. To see all the members of the Server:

    $Server | get-member

    To view the Sessions currently in the Extended Events Engine:

    $Server.EventSessions | % {$_.Name}

    Background on the Project

    When I first read about this new feature in SQL Server 2008 I tried looking into it, but I really got nowhere because there wasn’t a lot of information about it.  At the time only one blog popped up with anything to do with Extended Events in search engines.  Bob Beauchemin of SQL Skills had blogged about Extended Events as early as the July CTP of SQL Server 2008, back in August 2007.  Nearly a year later I started really working on learning Extended Events and like most people I had trouble wrapping my head around all the DMV’s Catalog Views, and learning what was actually available through Extended Events.  I wrote a really nasty XML generator that queried the DMV’s and provided back a XML representation of the Metadata that root nodes for each Package and then child nodes for each of the types of Objects contained in each Package, all the way down to the object column level.  This made the Metadata a bit more digestible, but after a few days of expanding and contracting nodes I set out to create a basic UI that made working with Extended Events easier.

    When I completed the first build of the UI and had tested things to the point that I considered it to be “stable” I posted the project and source code up on CodePlex and sent an email off to Bob asking for some feedback on the project.  Bob provided a ton of feedback early on in the projects development and blogged about it shortly after the initial build was released in August of 2008.  I spent some time reworking pieces of the project here and there, and as I published newer builds of the project, I sent it out to a couple of other people that I had begun talking/blogging about Extended Events, and one of those people, my good friend Kevin Kline (Blog|Twitter) recommended that I enter the application in the SQL Server 2008 Heroes contest that was being put on by Microsoft in conjunction with that year’s PASS Summit.  When it was picked as a finalist and won, I was surprised because at the time it was just something I was playing around with.

    After PASS 2008 I got an email from Adam Machanic (Blog|Twitter) asking if I would be interested in some help with the project to move it from being a basic project into a completed tool.  Many of the changes that ensued over the next year with the code base, including a complete rewrite of the underlying Library DLL came about because of email exchanges with Adam and the suggestions he provided.  However, the one thing I really disliked about the Extended Event Manager was that it was at this point still a stand alone application, and I really hated having to open another application to play with Extended Events.  Over this period of time I had been trading emails with Jerome Halmans at Microsoft, constantly asking questions about Extended Events and sending him updates whenever I published a new build of the UI.  At some point he mentioned that it would be great if it could be integrated into SSMS directly, so I set out to figure out how to integrate the UI functionality in SQL Server Management Studio as an Addin, which was not then, and is still not now, something that is actually supported by Microsoft.

    If you do any kind of searching online for SSMS Addin today, you will find all kinds of references for how to build one in SQL Server 2005, and SQL Server 2008, but one person that you are most certainly going to find is Mladen Prajdic (Blog|Twitter), the author of the SSMS Tools Pack.  Right around the point that I was ready give up on ever figuring out how to build a Addin for SSMS, I got a comment from Mladen on twitter about the Extended Events Manager, that opened up conversations between us about the problems I was facing in my attempts to move it from being a standalone application into a SSMS Addin.  Mladen answered a number of questions for me, and even looked at a couple of code files that I was having problems with through email exchanges, and the result was the first release of the SSMS Addin for Extended Events in July 2009.  There have been a few revisions to the Addin since its initial release, mainly performance enhancements to the Target data processing methodologies, and some fixes to compatibility issues with SQL Server 2008 R2 due to changes in the SSMS codebase.

    I owe a lot of people for their input, feedback, ideas, and assistance with this project.  I may have been the person slinging the code, but the project is as much the combined work of all the people mentioned above, without which I probably never would have continued as far as I did with the project.

    Future of the Project

    If you look at the project page on Codeplex, one of the first things you will probably notice is that I haven’t done any real work on it in over a year, and that is essentially the case.  The last build of the project, was completely bug free and perfect, was release right before I changed jobs, and then went back to school, so I haven’t had a lot of time to dedicate to the project, and I haven’t really had a lot of motivation to since there hasn’t been a lot of feedback on what bugs or issues people have encountered when using the Addin.  I know that there are some bugs in the current release, every now and then I encounter one and ask myself when am I going to finally fix that.

    Currently I have been reworking the Library code base to work with SQL Server Denali CTP1 and provide backwards compatibility with SQL Server 2008 and 2008R2.  This has proven to be somewhat of a challenge for a number of reasons, mainly I haven’t written much .NET code over the last year and I have had to put some effort into learning what some blocks of code actually do again.  In addition to that I have had to make some significant changes to support changes that occurred in Denali, and doing that while ensuring backwards compatibility is maintained is slow, but from the XEvents.Library DLL standpoint, I have made all the necessary changes to support all three releases of SQL Server. Lets just hope that Microsoft doesn’t change to much in the remaining CTP’s, RC’s and ultimately the RTM of Denali! (Mike I know you are reading this, hint hint!) 

    Since the Visual Studio environment that Management Studio runs on top of was changed out, that changes a number of things associated with the Addin portion of the Extended Events Manager, and I have yet to figure out how to actually register an Addin in Denali CTP1 yet, but I do know that its possible and already been done so I am sure that I will get there at some point.  For now I am reworking the UI components using the stand alone application approach that I have used in the past for building and testing the controls that are actually registered in the Addin in SSMS.  Hopefully sometime in January 2011 I will be able to release at least the stand alone application in a Denali build and then be in a position to focus on figuring out how to hack an Addin back into SSMS.

  • Yesterday’s blog post Targets Week – etw_classic_sync_target covered the ETW integration that is built into Extended Events and how the etw_classic_sync_target can be used in conjunction with other ETW traces to provide troubleshooting at a level previously not possible with SQL Server.  In today’s post we’ll look at how to use multiple targets to simplify analysis of Event collection.

    Why Multiple Targets?

    You might ask why you would want to use multiple Targets in an Event Session with Extended Events?  The best answer is because each of the Targets handles the Event data in different ways, and by combining their uses, you can easily simplify the task of tracking down problems.  Two days ago I talked about the pair_matching Target and how it only retains Events that have not been matched.  Two years ago out of nowhere, one of the production databases I supported started having transaction log growth problems.  After growing the log for the second time in a day, I started looking at the database because something was obviously not right. 

    The first place I looked was the sys.databases DMV and specifically the log_reuse_wait_desc column, which provides the reason that the transaction log for a database is not truncating.  When I queried this DMV, I found that the log_reuse_wait_desc was ACTIVE_TRANSACTION, meaning that the database had an open transaction.  So I ran DBCC OPENTRAN to get some more information about the open transaction (you can query the DMV’s for this information as well) and found that the transaction had been open for over 8 hours.  I queried sys.dm_exec_sessions for the session_id and found that the session was still being used and had submitted a request within the last minute, so it seemed to be an orphaned transaction.

    SELECT 
        session_id,
        login_time, 
        last_request_start_time, 
        last_request_end_time
    FROM sys.dm_exec_sessions
    WHERE session_id = 76

    After discussions with the Analyst for the application and the applications vendor, it was decided that the session should be killed forcing a ROLLBACK of the transaction and allowing the log to be truncate.  (I’ll discuss why this might prove be problematic later in this post)  This resolved the problem, at least until the next day when the database began running out of space in the transaction log again, and once again had an open transaction that had been open for hours on a session that was sill being used by the application.  What was really interesting was there was no correlation between the previous days open transactions begin time, and the begin time of the second occurrence of the problem, so it seemed to be a completely random occurrence which was not going to be easy to troubleshoot. 

    I created a server side trace and tried over the next two days to figure out what the issue actually was, but didn’t make much head way until I expanded the trace to have the statement starting and completed events along with the Errors and Warnings Events in the trace.  When the problem reoccurred, I was able to read through the trace files using filtering to minimize the Trace Events captured down to the specific spid that held the open transaction and the events that occurred five minutes before and after the transaction_begin_time for the open transaction.  While looking at the event information I found an Attention Event and was able to deduce what had happened.

    The application was a ASP.NET application, and the vendor used the CommandTimeout default which is 30 seconds.  What happened was that a process was invoked that called a stored procedure to archive information from a transactional table into an archive table inside of the database, and the number of rows being archived caused the stored procedures execution to exceeded 30 seconds resulting in a timeout in the ASP.NET application, and the application silently handled the exception by doing nothing.  The problem was that the stored procedure issued a BEGIN TRANSACTION before archiving the rows, and when the timeout occurred and the Attention Event was raised, the command aborted leaving the open transaction and creating the problem with the log not truncating. 

    The connection was returned to the ASP.NET Connection Pool, and was constantly being reused by the application to do who knows what other operations, which is where killing the connection was potentially a very bad thing to do.  All of the activity performed by this session was performed under the open transaction, so by killing the session, all of the activity would be rolled back.  With no way to tell what exactly would be rolled back, killing the session should not be taken lightly.

    For the remainder of this post I am going to show a repro of this particular problem and how to use Multiple Targets in Extended Events to simplify the troubleshooting this.

    Setting Up the Demo

    To setup a reproduction of this problem you will need two instances of SSMS open.  One of them will be used to connect to the SQL Server normally, and the other will be used to act like the ASP.NET application that originally had the problem.  To setup the second instance of SSMS to act like the ASP.NET application, we are going to set the Execution Timeout using the Options of the Connect to Database Engine window, and we are also going to add an Additional Connection Parameter to the connection to set the Application Name on the connection to “Some Poorly Written App” as shown in the below screenshots from SSMS.

    image   image

    For the remainder of this blog post I am going to refer to the two different instances of SSMS as Normal and PoorApp in the hopes that this prevents confusion. 

    In the Normal SSMS we will create a database and some objects to support the repro of the problem:

    CREATE DATABASE [MultiTargetDemo]
    GO
    USE [MultiTargetDemo]
    GO
    CREATE TABLE dbo.RandomObjectsArchive
    (ArchiveID int identity primary key,
     TableName nvarchar(128),
     IndexName nvarchar(128),
     ColumnName nvarchar(128))
    GO
    CREATE TABLE dbo.TrackArchiveRunTimes
    (RuntimeID int identity primary key,
     ArchiveRuntime datetime DEFAULT(CURRENT_TIMESTAMP))
    GO
    CREATE PROCEDURE dbo.GenerateRandomObjects
    AS
    BEGIN TRANSACTION
        INSERT INTO dbo.TrackArchiveRunTimes
        DEFAULT VALUES;
    
        INSERT INTO RandomObjectsArchive
            (TableName, IndexName, ColumnName)
        SELECT TOP 10000 a.name, i.name, c.name
        FROM sys.objects AS a
            CROSS JOIN sys.indexes AS i
            CROSS JOIN sys.columns AS c
            CROSS JOIN master.dbo.spt_values AS sv
        WHERE sv.type = 'P' 
          AND sv.number < 6 --Adjust to increase runtime
        ORDER BY NEWID() DESC
    COMMIT TRANSACTION
    GO
    USE [master]
    GO

    The GenerateRandomObjects stored procedure Inserts a row into a tracking table that tracks when the stored procedure was executed, and then simulates a long running archive process by doing something that you should never do in production code.  The sv.number predicate in the query can be increased or decreased based on the performance of the system being tested against to ensure that the stored procedure runs longer than the Execution Timeout setting, which on my PoorApp SSMS instance was set to 10 seconds.  Increasing the value by 1 has an exponential impact on the performance degradation of the stored procedure, so any changes should be made incrementally to ensure that you don’t create a tempdb bloat problem with the Cartesian product of the query being executed.

    Setting Up the Event Session

    To troubleshoot this problem using Extended Events we will create an Event Session that captures the following Events:

    sqlserver.database_transaction_begin
    sqlserver.database_transaction_end
    sqlserver.sql_statement_starting
    sqlserver.sql_statement_completed
    sqlserver.sp_statement_starting
    sqlserver.sp_statement_completed
    sqlserver.rpc_starting
    sqlserver.rpc_completed
    sqlserver.module_start
    sqlserver.module_end
    sqlserver.error_reported

    We’ll add the following Actions to each of the Events:

    sqlserver.session_id
    sqlserver.database_id
    sqlserver.tsql_stack

    and add the sqlserver.sql_text Action to the starting Events so that we can track what is actually being executed.  Every Event in the Event Session will have a Predicate on the sqlserver.client_app_name so that the Event only fires for connections and requests from “Some Poorly Written App”. 

    IF EXISTS(SELECT * 
             FROM sys.server_event_sessions 
             WHERE name='OrphanedTransactionHunter') 
        DROP EVENT SESSION [OrphanedTransactionHunter] ON SERVER; 
    CREATE EVENT SESSION OrphanedTransactionHunter
    ON SERVER
    ADD EVENT sqlserver.database_transaction_begin
    (    ACTION(sqlserver.session_id, sqlserver.database_id, sqlserver.tsql_stack)
        WHERE (sqlserver.client_app_name = 'Some Poorly Written App')),
    ADD EVENT sqlserver.database_transaction_end
    (    ACTION(sqlserver.session_id, sqlserver.database_id, sqlserver.tsql_stack)
        WHERE (sqlserver.client_app_name = 'Some Poorly Written App')),
    ADD EVENT sqlserver.sql_statement_starting
    (    ACTION(sqlserver.session_id, sqlserver.database_id, sqlserver.tsql_stack, sqlserver.sql_text)
        WHERE (sqlserver.client_app_name = 'Some Poorly Written App')),
    ADD EVENT sqlserver.sql_statement_completed
    (    ACTION(sqlserver.session_id, sqlserver.database_id, sqlserver.tsql_stack)
        WHERE (sqlserver.client_app_name = 'Some Poorly Written App')),
    ADD EVENT sqlserver.sp_statement_starting
    (    ACTION(sqlserver.session_id, sqlserver.database_id, sqlserver.tsql_stack, sqlserver.sql_text)
        WHERE (sqlserver.client_app_name = 'Some Poorly Written App')),
    ADD EVENT sqlserver.sp_statement_completed
    (    ACTION(sqlserver.session_id, sqlserver.database_id, sqlserver.tsql_stack)
        WHERE (sqlserver.client_app_name = 'Some Poorly Written App')),
    ADD EVENT sqlserver.rpc_starting
    (    ACTION(sqlserver.session_id, sqlserver.database_id, sqlserver.tsql_stack, sqlserver.sql_text)
        WHERE (sqlserver.client_app_name = 'Some Poorly Written App')),
    ADD EVENT sqlserver.rpc_completed
    (    ACTION(sqlserver.session_id, sqlserver.database_id, sqlserver.tsql_stack)
        WHERE (sqlserver.client_app_name = 'Some Poorly Written App')),
    ADD EVENT sqlserver.module_start
    (    ACTION(sqlserver.session_id, sqlserver.database_id, sqlserver.tsql_stack, sqlserver.sql_text)
        WHERE (sqlserver.client_app_name = 'Some Poorly Written App')),
    ADD EVENT sqlserver.module_end
    (    ACTION(sqlserver.session_id, sqlserver.database_id, sqlserver.tsql_stack)
        WHERE (sqlserver.client_app_name = 'Some Poorly Written App')),
    ADD EVENT sqlserver.error_reported
    (    ACTION(sqlserver.session_id, sqlserver.database_id, sqlserver.tsql_stack)
        WHERE (sqlserver.client_app_name = 'Some Poorly Written App'))
    ADD TARGET package0.ring_buffer,
    ADD TARGET package0.pair_matching
    ( SET begin_event = 'sqlserver.database_transaction_begin',
          begin_matching_actions = 'sqlserver.session_id',
          end_event = 'sqlserver.database_transaction_end',
          end_matching_actions = 'sqlserver.session_id',
          respond_to_memory_pressure = 1
    )
    WITH (MAX_DISPATCH_LATENCY=5 SECONDS, TRACK_CAUSALITY=ON)
    GO
    
    ALTER EVENT SESSION OrphanedTransactionHunter
    ON SERVER
    STATE=START
    GO

    Fired Events will be dispatched to two different Targets, the package0.ring_buffer to capture the Raw Data (in a true production environment, the package0.asynchronous_file_target would generally be a better Target for Raw Data capture of any volume), and the package0.pair_matching Target which has been configured to match on the sqlserver.database_transaction_begin/end Events based on the sqlserver.session_id Action.  To ensure that we can track the relationship between events, the Event Session will have TRACK_CAUSALITY set to ON, and to minimize the time it takes for Events to be dispatched for our test, the MAX_DISPATCH_LATENCY will be set to 5 seconds.

    Putting It All Together

    With the Event Session running, we can change over to our PoorApp SSMS instance and execute the GenerateRandomObjects stored procedure inside of the MultiTargetDemo database.

    EXECUTE MultiTargetDemo.dbo.GenerateRandomObjects

    When this executes, the command will timeout and leave the transaction open, simulating the original problem exactly.  Once the query times out, switch back to the Normal SSMS Instance and in a new window execute the stored procedure again and allow it complete its execution.  Since the default timeout of 0 is used in the Normal SSMS Instance, the execution will not time out.  Then we can look at the sys.databases DMV and see that the log_reuse_wait_desc is ACTIVE_TRANSACTION.

    SELECT log_reuse_wait_desc 
    FROM sys.databases
    WHERE database_id = DB_ID('MultiTargetDemo')

    image

    If we look at DBCC OPENTRAN for the MultiTargetDemo database we will see our orphaned transaction:

    DBCC OPENTRAN([MultiTargetDemo])

    image

    As I mentioned earlier in this post, the transaction can also be seen in the DMV’s:

    SELECT 
        dtst.session_id, 
        dtdt.database_id,
        dtst.transaction_id, 
        dtat.name,
        dtdt.database_transaction_begin_time
    FROM sys.dm_tran_session_transactions AS dtst
    JOIN sys.dm_tran_active_transactions AS dtat
        ON dtst.transaction_id = dtat.transaction_id
    JOIN sys.dm_tran_database_transactions AS dtdt
        ON dtdt.transaction_id = dtst.transaction_id
    WHERE database_id = DB_ID('MultiTargetDemo')

    image

    Now that we have our problem reproduced, lets look at how we can use the information captured by our Extended Event Session to track it back to the source of the problem.  First we’ll query the pair_matching Target to find out information about the sqlserver.database_transaction_begin Event that was unmatched.

    -- Query the XML to get the Target Data
    SELECT 
        n.value('(event/@name)[1]', 'varchar(50)') AS event_name,
        n.value('(event/@package)[1]', 'varchar(50)') AS package_name,
        DATEADD(hh, 
                DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP), 
                n.value('(event/@timestamp)[1]', 'datetime2')) AS [timestamp],
        n.value('(event/action[@name="session_id"]/value)[1]', 'int') as session_id,
        n.value('(event/action[@name="database_id"]/value)[1]', 'int') as [database_id],
        n.value('(event/action[@name="tsql_stack"]/value)[1]', 'nvarchar(max)') as tsql_stack,    
        n.value('(event/action[@name="attach_activity_id"]/value)[1]', 'varchar(50)') as attach_activity_id
    FROM
    (    SELECT td.query('.') as n
        FROM 
        (
            SELECT CAST(target_data AS XML) as target_data
            FROM sys.dm_xe_sessions AS s 
            JOIN sys.dm_xe_session_targets AS t 
                ON t.event_session_address = s.address
            WHERE s.name = 'OrphanedTransactionHunter'
              AND t.target_name = 'pair_matching'
        ) AS sub
        CROSS APPLY target_data.nodes('PairingTarget/event') AS q(td)
    ) as tab
    -- We are interested in unmatched sqlserver.database_transaction_begin Events
    WHERE n.value('(event/@name)[1]', 'varchar(50)') = 'database_transaction_begin'
    ORDER BY session_id, activity_id
    GO
    

    image

    From this we can see our orphaned transaction event, and find the attach_activity_id of that Event.  The attach_activity_id Action is added to the Events in an Event Session when TRACK_CAUSALITY is turned ON.  There are two pieces of information contained in the attach_activity_id Action, the activity Guid (the first 36 characters of the value) and the sequence number for the Event, the number following the Guid.  The Guid can be used to find related Events, and the sequence number can be used to determine the order that the Events occurred.  By using the Guid from the attach_activity_id Action from our first query, we can query the ring_buffer Target and parse out the specific Events we are interested in looking at further.

    -- Query the XML to get the Target Data
    SELECT 
        n.value('(event/@name)[1]', 'varchar(50)') AS event_name,
        n.value('(event/@package)[1]', 'varchar(50)') AS package_name,
        DATEADD(hh, 
                DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP), 
                n.value('(event/@timestamp)[1]', 'datetime2')) AS [timestamp],  
        n.value('(event/action[@name="database_id"]/value)[1]', 'int') as [database_id],
        n.value('(event/action[@name="session_id"]/value)[1]', 'int') as [session_id],
        n.value('(event/data[@name="object_id"]/value)[1]', 'int') AS [object_id],
        n.value('(event/data[@name="object_type"]/value)[1]', 'nvarchar(128)') AS [object_type],
        n.value('(event/data[@name="object_name"]/value)[1]', 'nvarchar(128)') AS [object_name],
        n.value('(event/data[@name="error"]/value)[1]', 'int') AS [error],
        n.value('(event/data[@name="severity"]/value)[1]', 'int') AS [severity],
        n.value('(event/data[@name="state"]/value)[1]', 'int') AS [state],
        n.value('(event/data[@name="user_defined"]/value)[1]', 'bit') AS [user_defined],
        n.value('(event/data[@name="message"]/value)[1]', 'nvarchar(4000)') AS [message],
        n.value('(event/data[@name="duration"]/value)[1]', 'int') AS [duration],
        n.value('(event/data[@name="row_count"]/value)[1]', 'int') AS [row_count],
        n.value('(event/data[@name="cpu"]/value)[1]', 'int') AS [cpu],
        n.value('(event/data[@name="reads"]/value)[1]', 'int') AS [reads],
        n.value('(event/data[@name="writes"]/value)[1]', 'int') AS [writes],
        n.value('(event/action[@name="tsql_stack"]/value)[1]', 'nvarchar(max)') AS [tsql_stack],
        n.value('(event/data[@name="offset"]/value)[1]', 'int') AS [offset],
        n.value('(event/data[@name="offset_end"]/value)[1]', 'int') AS [offset_end],
        n.value('(event/data[@name="nest_level"]/value)[1]', 'int') AS [nest_level],           
         n.value('(event/action[@name="sql_text"]/value)[1]', 'nvarchar(max)') AS [sql_text],
        CAST(SUBSTRING(n.value('(event/action[@name="attach_activity_id"]/value)[1]', 'varchar(50)'), 1, 36) AS uniqueidentifier) AS activity_id,
        CAST(SUBSTRING(n.value('(event/action[@name="attach_activity_id"]/value)[1]', 'varchar(50)'), 38, 10) AS int) AS event_sequence
    FROM
    (    SELECT td.query('.') as n
        FROM 
        (
            SELECT CAST(target_data AS XML) as target_data
            FROM sys.dm_xe_sessions AS s 
            JOIN sys.dm_xe_session_targets AS t 
                ON t.event_session_address = s.address
            WHERE s.name = 'OrphanedTransactionHunter'
              AND t.target_name = 'ring_buffer'
        ) AS sub
        CROSS APPLY target_data.nodes('RingBufferTarget/event') AS q(td)
    ) as tab
    -- We are interested in unmatched sqlserver.database_transaction_begin Events
    WHERE SUBSTRING(n.value('(event/action[@name="attach_activity_id"]/value)[1]', 'varchar(50)'), 1, 36) = 'ADCF379A-4BCA-41BA-9B08-4C2265894392'
    ORDER BY session_id, event_sequence
    GO
    

     

    With a little bit more work, we can reduce the XML parsing to only the important data elements that we need, and we can parse the tsql_stack Action to retrieve the related Event level statement_text from the sys.dm_exec_sql_text() DMF, since the sql_text Action did not have the intended information.

    -- Query the XML to get the Target Data
    SELECT 
        event_name,
        timestamp,
        database_id, 
        OBJECT_NAME(st.objectid, st.dbid) AS ObjectName,
        SUBSTRING(st.text, (tsql_stack.value('(/frame/@offsetStart)[1]', 'int')/2)+1, 
            ((CASE tsql_stack.value('(/frame/@offsetEnd)[1]', 'int')
                WHEN -1 THEN DATALENGTH(st.text)
                ELSE tsql_stack.value('(/frame/@offsetEnd)[1]', 'int')
                END - tsql_stack.value('(/frame/@offsetStart)[1]', 'int'))/2) + 1) AS statement_text,
        duration,
        activity_id,
        event_sequence
    FROM
    (
    SELECT 
        n.value('(event/@name)[1]', 'varchar(50)') AS event_name,
        DATEADD(hh, 
                DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP), 
                n.value('(event/@timestamp)[1]', 'datetime2')) AS [timestamp],  
        n.value('(event/action[@name="database_id"]/value)[1]', 'int') as [database_id],
        n.value('(event/data[@name="duration"]/value)[1]', 'int') AS [duration],
        CAST(n.value('(event/action[@name="tsql_stack"]/value)[1]', 'nvarchar(max)') AS XML) AS [tsql_stack],
        CAST(SUBSTRING(n.value('(event/action[@name="attach_activity_id"]/value)[1]', 'varchar(50)'), 1, 36) AS uniqueidentifier) AS activity_id,
        CAST(SUBSTRING(n.value('(event/action[@name="attach_activity_id"]/value)[1]', 'varchar(50)'), 38, 10) AS int) AS event_sequence
    FROM
    (    SELECT td.query('.') as n
        FROM 
        (
            SELECT CAST(target_data AS XML) as target_data
            FROM sys.dm_xe_sessions AS s 
            JOIN sys.dm_xe_session_targets AS t 
                ON t.event_session_address = s.address
            WHERE s.name = 'OrphanedTransactionHunter'
              AND t.target_name = 'ring_buffer'
        ) AS sub
        CROSS APPLY target_data.nodes('RingBufferTarget/event') AS q(td)
    ) as tab
    ) as tab2
    CROSS APPLY sys.dm_exec_sql_text(tsql_stack.value('xs:hexBinary(substring((/frame/@handle)[1], 3))', 'varbinary(max)')) AS st
    -- We are interested in Events in activity_id sequence of the orphaned transaction only
    WHERE activity_id = 'ADCF379A-4BCA-41BA-9B08-4C2265894392'
    ORDER BY session_id, event_sequence
    GO
    

    image

    The highlighted value shows that the stored procedure execution ended at the Execution Timeout limit that was set for the PoorApp SSMS Instance.  Beyond that we can track each of the statements and see that when the execution ended, it was in the INSERT INTO RandomObjectsArchive statement in the stored procedure allowing us to target our efforts at troubleshooting to the specific problem in a short amount of time.

    Yesterday’s post, Targets Week – pair_matching, looked at the pair_matching Target in Extended Events and how it could be used to find unmatched Events.  Today’s post will cover the etw_classic_sync_target Target, which can be used to track Events starting in SQL Server, out to the Windows Server OS Kernel, and then back to the Event completion in SQL Server.

    What is the etw_classic_sync_target Target?

    The etw_classic_sync_target Target is the target that hooks Extended Events in SQL Server into Event Tracing for Windows (ETW).  Event Tracing for Windows is a general purpose, high speed tracing mechanism provided by the Windows Server OS that allows for in-depth analysis and correlation of events from multiple applications, as well as the Windows Kernel.  ETW was first introduced in Windows Server 2000, was then expanded on in Windows Server 2003, and Windows Server 2008 and Windows Server 2008 R2 built significantly on the ETW tracing available in the OS.  For a background on ETW as a concept, I’d recommend that you read Event Tracing: Improve Debugging And Performance Tuning With ETW.

    It might surprise you to know that ETW integration with SQL Server wasn’t entirely new in Extended Events.  The first integration with ETW actually occurred in SQL Server 2005 and was talked about on the SQL Query Processing Team’s blog post Using ETW for SQL Server 2005 back in 2006.  The ETW integration in SQL Server 2005 was trace based integration, and is similar to as well as different from the ETW integration that exists through Extended Events.  There are two providers available currently in ETW in the Windows Server OS, a classic provider (Windows Server 2000 and newer), and a manifest based provider (Windows Vista and Server 2008 and newer) (http://msdn.microsoft.com/en-us/library/aa363668(VS.85).aspx#providers).  The etw_classic_sync_target uses the classic provider for buffering events to ETW to ensure that backwards compatibility is maintained for the supported Operating Systems that SQL Server can run on.

    Unlike the other targets available in Extended Events, the output of the etw_classic_sync_target is not available inside of SQL Server through a DMV or even by querying a DMF, since the events are buffered to ETW which is an OS based mechanism.  Currently, there can only be one ETW Session for Extended Events at a time, and that session is named XE_DEFAULT_ETW_SESSION.  The XE_DEFAULT_ETW_SESSION is created the first time a ETW Target is registered in an Event Session and is reused by subsequent Event Sessions that register an ETW Target in SQL Server.  If multiple Event Sessions utilize the etw_classic_sync_target on a server, even if they exist in multiple instances of SQL Server, the Events fired by the Event Sessions all use the XE_DEFAULT_ETW_SESSION session in ETW.  This makes isolation of Events to single instance impossible under the current design unless the Event Sessions are run independently instead of concurrently.

    Unlike the other Targets available in Extended Events, the ETW session created the first time that the etw_classic_sync_target is registered in an active Event Session is not removed when the etw_classic_sync_target is dropped from the Event Session, or when the Event Session is stopped.  The only way to remove the XE_DEFAULT_ETW_SESSION is with command line commands to one of the ETW consumers available in Window; either logman or xperf if installed.  Also in contrast to the other targets, the XE_DEFAULT_ETW_SESSION requires manual flushing to ensure that Events are processed before removing the XE_DEFAULT_ETW_SESSION ETW session in the OS.  

    Configuration Options

    There are five configuration options for the etw_classic_sync_target Target in Extended Events.  All of the configuration options are optional.  The default_etw_session_logfile_path can be used to specify the path to the log file created by the ETW Session for logging the Events.  Once this file path has been set, it can not be changed while the XE_DEFAULT_ETW_SESSION ETW session exists in Windows, the default file location is %TEMP%\XEEtw.etl.  If you are utilizing the etw_classic_sync_target Target in multiple Event Sessions or multiple Instance of SQL Server on the same OS, it is important to maintain consistency in the definition of this option.  The default_etw_session_buffer_size_kb specifies the default size of the in-memory buffers for the ETW session, the default buffer size is 128KB.  The default_etw_session_logfile_size_mb specifies the size of the file used to store the events sent to the ETW session, the default size is 20MB.  The retries option specifies the number of attempts that the Extended Event Engine will retry publishing the events to the ETW Session if the initial attempt to publish the events fails, the default value is 0 retry attempts, meaning that the Event will be dropped if they fail on the first attempt.  The default_xe_session_name specifies the name of the ETW session to create in the ETW subsystem for the Event Session, the default is XE_DEFAULT_ETW_SESSION.

    -- Target Configurable Fields
    SELECT 
       oc.name AS column_name,
       oc.column_id,
       oc.type_name,
       oc.capabilities_desc,
       oc.description
    FROM sys.dm_xe_packages AS p
    JOIN sys.dm_xe_objects AS o 
        ON p.guid = o.package_guid
    JOIN sys.dm_xe_object_columns AS oc 
        ON o.name = oc.OBJECT_NAME 
       AND o.package_guid = oc.object_package_guid
    WHERE(p.capabilities IS NULL OR p.capabilities & 1 = 0)
      AND (o.capabilities IS NULL OR o.capabilities & 1 = 0)
      AND o.object_type = 'target'
      AND o.name = 'etw_classic_sync_target'

    image

    Understanding the Target Data Format

    Unlike the other Targets in Extended Events, that etw_classic_sync_target Target data format depends on a number of factors, specifically on which ETW consumer, and what options are specified for the consumer for exporting the information into a user consumable format.  To be perfectly honest as much as I have played with the etw_classic_sync_target, I have yet to figure out all of the possible options for consuming the ETW session data that can be generated.  There are a number of available tools for consuming ETW session data, including logman, tracerpt, and xperf. 

    A wide variety of output formats is possible including text, CSV, and XML, and when using Windows Vista or Windows Server 2008 as the system of analysis, xperfview can be be used to provide a graphical output of the ETW session data from from the .etl file.  For this reason I will not attempt to cover all of the formats available for consuming ETW session information, but will instead leave that up to the reader to investigate.

    Querying/Parsing the Target Data

    One of the topics not yet covered in this series is the fact that inside of Extended Events in SQL Server, every Event has an associated Channel and Keyword associated with it that maps to a Channel and Keyword in ETW.  Inside of ETW, the channel defines the intended audience for the Event, and the Keyword provides an application specific grouping of events.  This information can be queried from the sys.dm_xe_object_columns DMV by joining it to the sys.dm_xe_map_values DMV as follows:

    -- Event ETW Keyword/Channel pairings
    SELECT 
        package_name,
        object_name,
        CHANNEL as channel_name,
        KEYWORD as keyword_name
    FROM
    (
    SELECT 
        p.name AS package_name, 
        o.name AS object_name,
        oc.name AS column_name,
        mv1.map_value
    FROM sys.dm_xe_packages p
    JOIN sys.dm_xe_objects o
        ON p.guid = o.package_guid
    JOIN sys.dm_xe_object_columns oc
        ON o.package_guid = oc.object_package_guid
            AND o.name = oc.object_name
    LEFT JOIN sys.dm_xe_map_values mv1 
        on oc.type_name = mv1.name 
            and oc.column_value = mv1.map_key
    WHERE oc.name IN ('CHANNEL', 'KEYWORD')
        -- Filter out private internal use only objects
      AND (p.capabilities IS NULL OR p.capabilities & 1 = 0)
      AND (o.capabilities IS NULL OR o.capabilities & 1 = 0)
      AND (oc.capabilities IS NULL OR oc.capabilities & 1 = 0)
    ) AS tab
    PIVOT
    ( 
        MAX(map_value)
        FOR column_name IN ([CHANNEL], [KEYWORD])
    ) as pvt
    ORDER BY CHANNEL, KEYWORD, package_name, object_name
    

     

    When planning Event Sessions in general, using the Channels and Keywords of Events to identify events of interest can be very useful, especially when first learning Extended Events.  In relation to ETW, they provide the integration

     

    IF EXISTS(SELECT * 
             FROM sys.server_event_sessions 
             WHERE name='etw_test_session') 
        DROP EVENT SESSION [etw_test_session] ON SERVER; 
    CREATE EVENT SESSION [etw_test_session] 
    ON SERVER 
    ADD EVENT sqlserver.file_read( 
         ACTION (sqlserver.database_id, sqlserver.session_id)), 
    ADD EVENT sqlserver.file_read_completed( 
         ACTION (sqlserver.database_id, sqlserver.session_id)), 
    ADD EVENT sqlos.async_io_requested( 
         ACTION (sqlserver.database_id, sqlserver.session_id)), 
    ADD EVENT sqlos.async_io_completed( 
         ACTION (sqlserver.database_id, sqlserver.session_id)), 
    ADD EVENT sqlos.wait_info( 
         ACTION (sqlserver.database_id, sqlserver.session_id)), 
    ADD EVENT sqlserver.sql_statement_starting( 
         ACTION (sqlserver.database_id, sqlserver.plan_handle, 
                sqlserver.session_id, sqlserver.sql_text)), 
    ADD EVENT sqlserver.sql_statement_completed( 
         ACTION (sqlserver.database_id, sqlserver.plan_handle, 
                sqlserver.session_id, sqlserver.sql_text)) 
    -- ADD ETW target 
    ADD TARGET package0.etw_classic_sync_target (
           SET default_etw_session_logfile_path = N'C:\SQLBlog\sqletwtarget.etl')
    WITH (MAX_MEMORY = 4096KB, 
         EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS, 
         MAX_DISPATCH_LATENCY = 5 SECONDS, 
         MAX_EVENT_SIZE = 4096KB, 
         MEMORY_PARTITION_MODE = PER_CPU, 
         TRACK_CAUSALITY = ON, 
         STARTUP_STATE = OFF) 
    GO

    This Event Session will capture SQL statements from start to complete as well as the file read operations performed by the database engine to satisfy the request.  To get the OS Kernel information using ETW we will need to start a Kernel

    logman start "NT Kernel Logger" /p "Windows Kernel Trace" (process,thread,disk) /o C:\SQLBlog\systemevents.etl /ets

    image

    With the NT Kernel Logger started and capturing kernel level process, thread, and disk events into the systemevents.etl file, we can now start our Extended Events Session in SQL Server, and run our test workload.  To ensure that we get physical reads from disk the following example will clear the Buffer Cache before starting the Event Session.

    USE [AdventureWorks2008] 
    GO 
    -- Clear the Buffer Cache to force reads from Disk 
    DBCC DROPCLEANBUFFERS 
    GO 
    
    -- Start the Event Session so we capture the Events caused by running the test 
    ALTER EVENT SESSION etw_test_session 
    ON SERVER 
    STATE=START 
    GO 

    If you run this on a SQL Server where you use a minimal privilege AD Account for the SQL Server Service Account, you will get an error similar to the following:

    Msg 25641, Level 16, State 0, Line 2
    For target, "CE79811F-1A80-40E1-8F5D-7445A3F375E7.package0.etw_classic_sync_target", the parameter "default_etw_session_logfile_path" passed is invalid.  The operating system returned error 5 (ACCESS_DENIED) while creating an ETW tracing session.  Ensure that the SQL Server startup account is a member of the 'Performance Log Users' group and then retry your command.

    If this occurs, the Service Account does not have sufficient privileges to use the ETW provider, and it will be necessary to add the SQL Service Account to the Performance Log Users group on the SQL Server and then restart the SQL Server Database Engine service for the permissions change to take effect.  (Yet another pre/post Installation Checklist item that needs to be performed!)  Once the Event Session is started, we can run a query to generate some Events and cause physical reads to occur from disk.

    -- Run the Simple SELECT against AdventureWorks 
    SELECT SUM(TotalDue), SalesPersonID 
    FROM Sales.SalesOrderHeader 
    GROUP BY SalesPersonID 
    GO 

    Once the query completes we can stop our Event SessionWith we can stop the NT Kernel Logger using the logman utility again:

    logman update "NT Kernel Logger" /fd /ets
    logman stop "NT Kernel Logger" /ets

    image

    With the Kernel Logger stopped, we can then stop our Event Session inside of SQL Server:

    ---- Start the Event Session so we capture the Events caused by running the test 
    ALTER EVENT SESSION etw_test_session 
    ON SERVER 
    STATE=STOP

    However, even though we stopped the Event Session in SQL Server, the XE_DEFAULT_ETW_SESSION still exists in the Windows OS.

    logman query -ets

    image

    Or if you are using Windows Server 2008/2008R2, the Performance Monitor can show you the Event Session:

    image

    In either case we need to flush the buffers for the XE_DEFAULT_ETW_SESSION and in this case stop it.

    logman update XE_DEFAULT_ETW_SESSION /fd /ets
    logman stop XE_DEFAULT_ETW_SESSION /ets

    image

    With the two ETW sessions stopped, we can now use tracerpt to merge the trace files together and output them to a CSV file. 

    (Note: The following commands are specific to Windows Server 2008/2008R2 if you are using Windows Server 2003, tracerpt does not have a –of option and will output the merged results in CSV format by default.  The default in Windows Server 2008/2008R2 is XML format.)

    tracerpt C:\SQLBlog\systemevents.etl C:\SQLBlog\sqletwtarget.etl -o C:\SQLBlog\ETW_Merged.csv -of CSV

    image

    With the results merged in a CSV there are a number of options available for how to work with the data.  If we just open up the CSV file and look at scroll down through the information we can see that the SQL Server async_io_requested Events that lead to file_read events, the setting of wait_info Event and the subsequent Kernel level DiskIO Read Event.

    image

    As cool as this seems like it is, if you have done any work with ETW in Windows, you know that the future is even brighter than this simple example begins to touch on.  The Windows Performance Analyzer and xperf offer a way to read ETW trace files and generate a graphical presentation of the information held within them.  For example if we use xperf to view the Kernel Logger file:

    image

    However, the Kernel Logger uses the newer Manifest based provider in Windows Server 2008 and 2008R2, and since SQL Server 2008 Extended Events uses the classic provider, xperf doesn’t recognize the Event names for the Events contained in the ETW trace file, and instead you get a bunch of Guid’s that require manual deciphering.

    image 

    If you happen to be running SQL Server 2008/2008R2 on Windows Server 2003, which I happen to be in most of my demonstration VM’s that I use for speaking (Windows Server 2003 takes significantly less disk space than 2008 and its at a premium on my laptops 120GB SSD), the sqletwtarget.etl and systemevents.etl files generated by this demo will have the same version and xperf can be used to open both files together and merge their respective Event Views:

    image

    The ProviderIds window is not expanded here, but it has the same Guids that the first example had.  Keep in mind that the above merged view came from a different system than the original two xperf views, but they used the same exact demo to generate.

    Considerations for Usage

    I have often commented in presentations that the etw_classic_sync_target Target is not something that the average DBA is going to make meaningful use of in troubleshooting problems with SQL Server.  At PASS this year, I had some eyebrows raised when I mentioned this in my presentation on Extended Events, but I stand by that statement, even after trying to brush up on ETW for this blog post, I’ve ran into numerous complications associated with actually consuming the ETW session information, that required that I perform further research to figure things out.  There is certainly meaningful information available through the use of the etw_classic_sync_target Target with Extended Events, when merged with Kernel level tracing as demonstrated later in this blog post.  However, when focusing on general troubleshooting, the etw_classic_sync_target should not be considered the target of choice.

    What’s next?

    Now that we have looked at all of the Targets currently available in Extended Events, in the next post we’ll look at how to utilize multiple targets in an Event Session to simplify the troubleshooting of problems in SQL Server.

    Yesterday’s post, Targets Week – synchronous_event_counter, looked at the counter Target in Extended Events and how it could be used to determine the number of Events a Event Session will generate without actually incurring the cost to collect and store the Events.  Today’s post is coming late, I know, but sometimes that’s just how the ball rolls.  My original planned demo’s for today’s post turned out to only work based on a fluke, though they were very consistent at working as expected, and as a result I had to rework a lot of this post this evening instead of letting it auto-post in the morning.  Today's post will cover the pair_matching Target, which can be used to find Events that didn’t have a corresponding matching Event based on the Targets configuration.

    What is the pair_matching Target?

    The pair_matching Target works by matching a Begin Event with an End Event based on the specified match Columns and Actions, and drops the matched pairs of Events from the Target so that only unmatched Events remain.   However, life would be grand if it was only that simple to use.  The Books Online example How to: Determine Which Queries Are Holding Locks, uses the pair_matching Target with the sqlserver.lock_acquired and sqlserver.lock_released events for matching, to try and show how to find queries that haven’t released their Locks.  The problem is, there is not a 1:1 relationship between lock_acquired and lock_released Events.  Lock escalation can kick in and multiple granular locks are acquired but the escalation to a less granular Lock only requires a single lock_released Event to fire. 

    In the Using SQL Server 2008 Extended Events whitepaper I wrote, I showed how to track down orphaned transactions using the sqlserver.database_transaction_begin and sqlserver.database_transaction_end Events and matching on the sqlserver.session_id Action.  The reason that this example works is that only one explicit transaction can be open for an session_id, even if you issue multiple BEGIN TRANSACTION commands, a single ROLLBACK undoes every operation performed since the first BEGIN TRANSACTION.  Yes this is a tangent, but fear not, I am coming back to why this matters to the pair_matching Target.  It matters to the pair_matching target because the begin and end Events must uniquely match each other in a manner that is 1:1 or the pair_matching Target is not going to work as you expect it to, as in the “Which Queries Are Holding Locks” example in the BOL.

    Like the ring_buffer, bucketizer, and the synchronous_event_counter, the pair_matching Target is a memory resident Target that holds event data in memory while the Event Session is active on the SQL Server.  When the Event Session is stopped, the memory buffers allocated to the synchronous_event_counter target are freed and any information contained in the target is lost.

    Configuration Options

    There are seven configuration options for the pair_matching Target, with two of them being mandatory.  The pair_matching Target requires that the begin_event and end_event for matching be specified for the Target.  In addition to specifying the events, it is also possible to restrict the match criteria by specifying an ordered comma separated list of column names for the begin_matching_columns and end_matching_columns configuration options.  If the matching requires the use of Actions, an ordered comma separated list of Action names in the format of <package_name.action_name> can be specified for the begin_matching_actions and end_matching_actions configuration options.  The final configuration option respond_to_memory_pressure determines whether or not the Target responds to memory pressure, and stops adding new orphaned events when there is memory pressure in the SQL Server.

    -- Target Configurable Fields
    SELECT 
       oc.name AS column_name,
       oc.column_id,
       oc.type_name,
       oc.capabilities_desc,
       oc.description
    FROM sys.dm_xe_packages AS p
    JOIN sys.dm_xe_objects AS o 
        ON p.guid = o.package_guid
    JOIN sys.dm_xe_object_columns AS oc 
        ON o.name = oc.OBJECT_NAME 
       AND o.package_guid = oc.object_package_guid
    WHERE(p.capabilities IS NULL OR p.capabilities & 1 = 0)
      AND (o.capabilities IS NULL OR o.capabilities & 1 = 0)
      AND o.object_type = 'target'
      AND o.name = 'pair_matching'

    image

    Understanding the Target Data Format

    Like the other memory resident Targets, the pair_matching Target returns its information by querying the sys.dm_xe_session_targets DMV, and it returns the Target data in an XML format that is not schema bound, but that has a standardized format.  The pair_matching Target XML document, closely matches the output of the ring_buffer Target.  The root node of the XML is the <PairingTarget> node which has attributes for the number of truncated Events, the count of the current number of orphans held in the Target, the number of matched Event pairs that have been made by the Target, and the number of orphans that have been dropped due to memory pressure in SQL Server.  The <PairingTarget> node has child <event> nodes that match the XML document of the <event> nodes in the ring_buffer and asynchronous_file_target Targets.  A simplified representation of the XML output by the pair_matching Target is below:

    <PairingTarget truncated="" orphanCount="" matchedCount="" memoryPressureDroppedCount="">
      <event name="" package="" id="" version="" timestamp="">
        <data name="">
          <type name="" package="" />
          <value />
          <text />
        </data>
        <action name="" package="">
          <type name="" package="" />
          <value />
          <text />
        </action>
      </event>
    </PairingTarget>

    Querying/Parsing the Target Data

    Like the other memory resident Targets in Extended Events, the pair_matching Target data is only exposed by querying the sys.dm_xe_session_targets DMV.  Mike Wachal at Microsoft, traded emails with me, and dug into the source code for the pair_matching target yesterday trying to help me with some questions for this post, especially as they related to the legitimacy of the demo’s being planned for this post.  In the end Mike sent me a demo and permission to post it here in lieu of the questionable ones that I had been trading emails with him about.  It is with much appreciation to Mike and the Extended Events Development team for their assistance with this blog post and the consistent back and forth with emails that they provided yesterday.

    Mike provided an example that I will show in its entirety for this weeks wrap up post on Saturday, but I am going to show a shorter sample of the demo to show how to use the pair_matching Target and query the Target data from it.  When SQL Server executes a statements, generally the sqlserver.sql_statement_starting Event is fired when the statement begins executing and the sqlserver.sql_statement_completed Event is fired when the statement completes.  However, when the client sets an execution time, also known as a CommandTimeout in .NET, if the execution duration exceeds that timeout, the statement never completes inside of SQL Server.  I have run into problems with the default timeout of 30 seconds in .NET more times than I ever care to think about in my career. 

    To demonstrate a execution time out using SQL Server Management Studio, you can open a New Query window, and in the connection dialog click on the Connection Properties tab and change the Execution time-out option from 0 (zero) to a positive integer value.  For the purposes of this blog post example, I am going to use 5 seconds as the execution timeout for one query window that will generate the unmatched event.

    image

    The first thing we need to do is setup our Event Session to capture our Events and Actions, and configure our pair_matching Target.

    -- Create the Event Session
    CREATE EVENT SESSION FindAttentionEvents
    ON SERVER
    ADD EVENT sqlserver.sql_statement_starting
    (    ACTION(sqlserver.session_id, sqlserver.tsql_stack)
    ),
    ADD EVENT sqlserver.sql_statement_completed
    (    ACTION(sqlserver.session_id, sqlserver.tsql_stack)
    )
    ADD TARGET package0.pair_matching
    (    SET begin_event = 'sqlserver.sql_statement_starting',
            begin_matching_actions = 'sqlserver.session_id, sqlserver.tsql_stack',
            end_event = 'sqlserver.sql_statement_completed',
            end_matching_actions = 'sqlserver.session_id, sqlserver.tsql_stack',
            respond_to_memory_pressure = 0
    )
    WITH (MAX_DISPATCH_LATENCY=5 SECONDS, TRACK_CAUSALITY=ON)
    
    -- Start the Event Session
    ALTER EVENT SESSION FindAttentionEvents
    ON SERVER
    STATE=START
    GO

    Now in the New Query window that had the connection option for execution timeout set to 5 seconds, run the following commands:

    SELECT TOP 100 *
    FROM sys.objects
    GO
    
    SELECT TOP 100 *
    FROM sys.columns
    GO
    
    WAITFOR DELAY '00:00:10'
    GO
    

    In my test system the output for this is:

    image

    If we flip back to a normal Query window and query the Target data, we will see multiple matched Events and one orphaned Event, for the above failure.

    -- Create XML variable to hold Target Data
    DECLARE @target_data XML
    SELECT @target_data = 
        CAST(target_data AS XML)
    FROM sys.dm_xe_sessions AS s 
    JOIN sys.dm_xe_session_targets AS t 
        ON t.event_session_address = s.address
    WHERE s.name = 'FindAttentionEvents'
      AND t.target_name = 'pair_matching'
    
    -- Query XML variable to get Target Execution information
    SELECT 
        @target_data.value('(PairingTarget/@orphanCount)[1]', 'int') AS orphanCount,
        @target_data.value('(PairingTarget/@matchedCount)[1]', 'int') AS matchedCount,
        @target_data.value('(PairingTarget/@memoryPressureDroppedCount)[1]', 'int') AS memoryPressureDroppedCount
    
    -- Query the XML variable to get the Target Data
    SELECT 
        n.value('(event/@name)[1]', 'varchar(50)') AS event_name,
        n.value('(event/@package)[1]', 'varchar(50)') AS package_name,
        n.value('(event/@id)[1]', 'int') AS id,
        n.value('(event/@version)[1]', 'int') AS version,
        DATEADD(hh, 
                DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP), 
                n.value('(event/@timestamp)[1]', 'datetime2')) AS [timestamp],
        n.value('(event/data[@name="source_database_id"]/value)[1]', 'int') as [source_database_id],
        n.value('(event/data[@name="object_id"]/value)[1]', 'int') as [object_id],
        n.value('(event/data[@name="object_type"]/value)[1]', 'varchar(60)') as [object_type],
        n.value('(event/data[@name="state"]/text)[1]', 'varchar(50)') as [state],
        n.value('(event/data[@name="offset"]/value)[1]', 'int') as [offset],
        n.value('(event/data[@name="offset_end"]/value)[1]', 'int') as [offset_end],
        n.value('(event/data[@name="nest_level"]/value)[1]', 'int') as [nest_level],
        n.value('(event/action[@name="session_id"]/value)[1]', 'int') as session_id,
        n.value('(event/action[@name="tsql_stack"]/value)[1]', 'varchar(max)') as tsql_stack,    
        n.value('(event/action[@name="attach_activity_id"]/value)[1]', 'varchar(50)') as activity_id    
    FROM
    (    SELECT td.query('.') as n
    FROM @target_data.nodes('PairingTarget/event') AS q(td)
    ) as tab
    ORDER BY session_id, activity_id
    GO

    If you pay close attention to the above XQuery of the Target data, you should catch that there is a difference between the above and previous examples.  I have a price for the first person that is not Adam Machanic (sorry dude, but you told me about it so that would be unfair) to comment with what that difference is and why it is important to take note of.  The output of the above query on my test system after running the demo is:

    image

    Now with this Demo, it is important that you reset the environment that you tested it in if you followed the instructions and changed the execution timeout in SQL Server Management Studio.  If you don’t change it back, you will try and run a query that takes longer than 5 seconds and it will timeout on you.  Also don’t forget to cleanup the Event Session by dropping it from the catalog.

    -- Cleanup from the demonstration
    DROP EVENT SESSION FindAttentionEvents 
    ON SERVER

    Considerations for Usage

    The pair_matching Target can be a very useful tool in finding unmatched Events, but as previously pointed out in this blog post, you have to be very careful what you are providing for match criteria, and the Events have to have a 1:1 correlation for the begin_event and end_event or the target will produce incorrect results.

    What’s next?

    Now that we have looked at the ring_buffer, asynchronous_file_target, bucketizers, synchronous_event_counter, and the pair_matching Targets, in the next post we’ll look at the last Target available for use in Extended Events, the etw_classic_sync_target, which can be used to track Events firing in SQL Server out into the Windows OS Kernel and back into SQL Server to see end to end what occurred inside the Server.

    Yesterday’s post, Targets Week - Bucketizers, looked at the bucketizer Targets in Extended Events and how they can be used to simplify analysis and perform more targeted analysis based on their output.  Today’s post will be fairly short, by comparison to the previous posts, while we look at the synchronous_event_counter target, which can be used to test the impact of an Event Session without actually incurring the cost of Event collection.

    What is the synchronous_event_counter?

    The synchronous_event_count simply put, is a Target that counts the number of Events that fire for a given Event Session.  It can be used to test whether or not the defined Predicates on Events in an Event Session perform the level of filtering expected, without having to actually perform full Event collection using one of the raw Event data targets like the ring_buffer or asynchronous_file_target.  The Target is synchronous however, due to the fact that it only counts the number of times each Event fires, its impact is minimized in comparison to the other synchronous targets available.  Like the ring_buffer and bucketizer, the synchronous_event_counter Target is a memory resident Target that holds event data in memory while the Event Session is active on the SQL Server.  When the Event Session is stopped, the memory buffers allocated to the synchronous_event_counter target are freed and any information contained in the target is lost.

    Configuration Options

    There are no configuration options for the synchronous_event_counter Target. (see I said this was going to be a short post comparatively).

     -- Target Configurable Fields
    SELECT 
       oc.name AS column_name,
       oc.column_id,
       oc.type_name,
       oc.capabilities_desc,
       oc.description
    FROM sys.dm_xe_packages AS p
    JOIN sys.dm_xe_objects AS o 
        ON p.guid = o.package_guid
    JOIN sys.dm_xe_object_columns AS oc 
        ON o.name = oc.OBJECT_NAME 
       AND o.package_guid = oc.object_package_guid
    WHERE(p.capabilities IS NULL OR p.capabilities & 1 = 0)
      AND (o.capabilities IS NULL OR o.capabilities & 1 = 0)
      AND o.object_type = 'target'
      AND o.name = 'synchronous_event_counter'

    image

    Understanding the Target Data Format

    Like the ring_buffer and bucketizer Targets, the synchronous_event_counter Target returns its information by querying the sys.dm_xe_session_targets DMV, and it returns the Target data in an XML format that is not schema bound, but that has a standardized format.  The synchronous_event_counter Target has a very simple XML document, much like the bucketizer Targets.  The root node of the XML is the <CounterTarget> node which has a child node <Packages> which has a child <Package> node for each package that identifies the package using an @name attribute.  Each <Package> node will have one or more <Event> nodes based on the number of Events defined in the Event Session for that particular package.  The <Event> nodes each will contain two attributes, the name of the event and the count for its occurrence since the Event Session started.  A simplified representation of the XML output by the synchronous_event_counter Target is below:

    <CounterTarget truncated="">
      <Packages>
        <Package name="">
          <Event name="" count="" />
        </Package>
      </Packages>
    </CounterTarget>

    Querying/Parsing the Target Data

    Like the other memory resident Targets in Extended Events, the synchronous_event_counter Target data is only exposed by querying the sys.dm_xe_session_targets DMV.  The following example will demonstrate how the synchronous_event_counter can be used to test the number of Events that an Event Session will generate:

    -- Create an Event Session to Track Recompiles
    IF EXISTS(SELECT * FROM sys.server_event_sessions WHERE name='CounterTargetDemo')
        DROP EVENT SESSION [CounterTargetDemo] ON SERVER;
    CREATE EVENT SESSION [CounterTargetDemo]
    ON SERVER
    ADD EVENT sqlserver.sql_statement_starting,
    ADD EVENT sqlos.wait_info
    (    WHERE (duration > 0))
    ADD TARGET package0.synchronous_event_counter
    GO
    
    -- Start the Event Session
    ALTER EVENT SESSION [CounterTargetDemo]
    ON SERVER
    STATE=STOP
    GO
    
    -- Wait for Events to generate and then Query Target
    
    -- Query the Target
    SELECT 
        n.value('../@name[1]', 'varchar(50)') as PackageName,
        n.value('@name[1]', 'varchar(50)') as EventName,
        n.value('@count[1]', 'int') as Occurence
    FROM
    (
    SELECT CAST(target_data AS XML) as target_data
    FROM sys.dm_xe_sessions AS s 
    JOIN sys.dm_xe_session_targets AS t 
        ON t.event_session_address = s.address
    WHERE s.name = 'CounterTargetDemo'
      AND t.target_name = 'synchronous_event_counter'
    ) as tab
    CROSS APPLY target_data.nodes('CounterTarget/Packages/Package/Event') as q(n)
    
    -- Drop the Event Session
    IF EXISTS(SELECT * FROM sys.server_event_sessions WHERE name='CounterTargetDemo')
        DROP EVENT SESSION [CounterTargetDemo] ON SERVER;

    This session on one of my test servers generated the following output while running for less than five seconds:

    image

    Based on this number of Events being fired, it may be determined that the predicates for the session need to provide further filtering of the Events, or if it is determined that the Predicates are filtering the Events as intended, this at least lets us know that the number of Events firing will require the use of the asynchronous_file_target, if the plan is to look at the Events raw data.

    Considerations for Usage

    The only real consideration associated with the synchronous_event_counter Target is that it is an synchronous Target.  However, since it is only counting the occurrences of the Events defined in the Event Session and is not actually buffering the data for dispatch its impact is not generally a concern.

    What’s next?

    Now that we have looked at the ring_buffer, asynchronous_file_target, bucketizers, and synchronous_file_target Targets, in the next post we’ll look at the pair_matching Target, which can be used to match up related Events based on specific criteria to discard the matched pairs, leaving unmatched and potentially problematic Events for analysis. 

    Yesterday’s post, Targets Week - asynchronous_file_target, looked at the asynchronous_file_target Target in Extended Events and how it outputs the raw Event data in an XML document.  Continuing with Targets week today, we’ll look at the bucketizer targets in Extended Events which can be used to group Events based on the Event data that is being returned.

    What is the bucketizer?

    The bucketizer performs grouping of Events as they are processed by the target into buckets based on the Event data and the Targets configuration.  There are two bucketizer targets in Extended Events; a synchronous_bucketizer and an asynchronous_bucketizer.  The only difference between the two is the manner in which the Event data is processed; either synchronously on the connection that generated the Event, or asynchronously after being dispatched to the target based on the MAX_DISPATCH_LATENCY for the Event Session, or when the dispatch buffer becomes full.  Since the two bucketizers are identical in every way, except for their processing, this blog post will use the asynchronous_bucketizer for all further references.  The bucketizers are a memory resident target, similar to the ring_buffer and like the ring_buffer, only contain the grouped Event data when the Event Session is active.  When the Event Session is stopped, the memory buffers allocated to the bucketizer target are freed and all data contained in the target disappears.  The bucketizer targets can be used to simplify troubleshooting by identifying the events that are occurring the most, and then allowing more focused Event collection for further analysis.  Further analysis could include using either the ring_buffer or the asynchronous_file_target to look at the actual Event data being generated, or changing the bucketizer Targets configuration to group event occurrences based on a different criteria.

    Configuration Options

    The ring_buffer like most of the targets has configuration options that can be found in the sys.dm_xe_object_columns DMV.

    -- Target Configurable Fields
    SELECT 
        oc.name AS column_name,
        oc.column_id,
        oc.type_name,
        oc.capabilities_desc,
        oc.description
    FROM sys.dm_xe_packages AS p
    JOIN sys.dm_xe_objects AS o 
        ON p.guid = o.package_guid
    JOIN sys.dm_xe_object_columns AS oc 
        ON o.name = oc.OBJECT_NAME 
        AND o.package_guid = oc.object_package_guid
    WHERE (p.capabilities IS NULL OR p.capabilities & 1 = 0)
      AND (o.capabilities IS NULL OR o.capabilities & 1 = 0)
      AND o.object_type = 'target'
      AND o.name = 'asynchronous_bucketizer'
    

    In SQL Server 2008, 2008R2, and SQL Server Denali CTP1, there are four configuration options for the asynchronous_bucketizer Target.  The slots option sets the maximum number of buckets the target will collect.  Once this number of buckets is reached, new events that do not apply to an existing bucket are dropped by the target and not grouped.  The filtering_event_name option is used to set the name of the specific Event in the Event Session to filter on.  The source_type option is used to specify whether the source being used for bucketing is a part of the Event data or an Action that has been added to the Events contained in the Event Session.  The source option specifies the source that will be used to generate the buckets for grouping in the target.

    image

    As shown above the source is the only required option for the asynchronous_bucketizer Target.  However, when the source is an Action the source_type option is also required to specify that the source is an Action.  When using one of the Event Data elements as the source, only the Data element (also known as a column name) needs to provided to the source.  When using an Action for the source, the Package name must be specified along with the Action name in the format of packagename.actionname.  Likewise when specifying a filtering_event_name, the Package name must also be provided in the format of packagename.eventname.

    Understanding the Target Data Format

    The bucketizer Targets like the other Targets already output the data in XML format, and the XML is not schema bound, but has a predictable format.  Inside the Extended Events Engine, the bucketing data is maintained in a binary format that minimizes the amount of memory necessary for the Targets memory buffers.  The bucketing data is materialized into an XML document when the Target information is queried using the sys.dm_xe_session_targets DMV, allowing it to be used for analysis.  The asynchronous_bucketizer XML document contains a parent XML <BucketizerTarget> Node that contains attributes about the Targets operation since the Event Session was started including the number of truncated Events and the maximum number of buckets contained in the Target.  The bucket groups are contained in <Slot> nodes that have two attributes; the count is the number of events that have occurred and the trunc is the number of bytes that have been truncated.  The <Slot> node contains a <value> node that contains the source that the bucket belongs to.  A simplified representation of the XML document for the asynchronous_bucketizer target is:

    <BucketizerTarget truncated="" buckets="">
      <Slot count="" trunc="">
        <value></value>
      </Slot>
    </BucketizerTarget>

    Querying/Parsing the Target Data

    The asynchronous_bucketizer targets simplistic XML output makes querying it relatively simple compared to the targets that we’ve already looked at this week.  However, unlike the other ring_buffer and asychronous_file_target, the asychronous_bucketizer can not be parsed using Adam Machanic’s Extended Events Code Generator.  The simplicity of the XML and its standard output doesn’t really require specialized code to generate a easily usable table output for this.  The Extended Events SSMS Addin for SQL Server 2008  TargetDataViewer will shred the XML but its not even worth using for this particular target, since the XQuery is very simple, and you can do a lot more with the TSQL depending on the Event Session that your create.  To demonstrate the usage of the asynchronous_bucketizer, we’ll look at a couple of examples.  The first example will show how to track recompiles by database_id to find the databases that have the most recompiles occurring.

    -- Create an Event Session to Track Recompiles
    IF EXISTS(SELECT * FROM sys.server_event_sessions WHERE name='BucketizerTargetDemoRecompiles')
        DROP EVENT SESSION [BucketizerTargetDemoRecompiles] ON SERVER;
    CREATE EVENT SESSION [BucketizerTargetDemoRecompiles]
    ON SERVER
    ADD EVENT sqlserver.sql_statement_starting
    (    ACTION (sqlserver.database_id) -- database_id to bucket on
         WHERE (state=1) -- recompile state from dm_xe_map_values
    ),
    ADD EVENT sqlserver.sp_statement_starting
    (    ACTION (sqlserver.database_id) -- database_id to bucket on
         WHERE (state=1) -- recompile state from dm_xe_map_values
    )
    ADD TARGET package0.asynchronous_bucketizer
    (     SET source_type=1, -- specifies bucketing on Action 
             source='sqlserver.database_id' -- Action to bucket on
    )
    WITH (MAX_DISPATCH_LATENCY = 5 SECONDS)
    GO
    ALTER EVENT SESSION [BucketizerTargetDemoRecompiles]
    ON SERVER
    STATE=START

    The above session collects the sql_statement_starting and sp_statement_starting Events, adds the database_id Action to the Event so that we can bucket on it, and then filters the Events to only fire if the state for the Event matches the map_key in sys.dm_xe_map_values for Recompile.  If the server being tested on doesn’t have a high recompile rate, an easy way to trigger Recompiles is to update the statistics on the tables inside of a database.

    EXECUTE sp_MSforeachtable 'UPDATE STATISTICS ?'

    To view the bucketized data from the target, we query sys.dm_xe_session_targets for our session and target using CAST to convert the target_data to XML in a derived table, and then using a CROSS APPLY of the .node() method to split on the <Slot> nodes.

    SELECT 
        DB_NAME(n.value('(value)[1]', 'int')) AS DatabaseName,
        n.value('(@count)[1]', 'int') AS EventCount,
        n.value('(@trunc)[1]', 'int') AS EventsTrunc
    FROM
    (SELECT CAST(target_data as XML) target_data
    FROM sys.dm_xe_sessions AS s 
    JOIN sys.dm_xe_session_targets t
        ON s.address = t.event_session_address
    WHERE s.name = 'BucketizerTargetDemoRecompiles'
      AND t.target_name = 'asynchronous_bucketizer') as tab
    CROSS APPLY target_data.nodes('BucketizerTarget/Slot') as q(n)

    With the <Slot> nodes split, pulling the <value> node and attributes is very simple, and since we bucketed on database_id, we can use the DB_NAME() function in SQL to return the database name associated with the database_id in the <value> node.

    Considerations for Usage

    The bucketizer targets are great for simplifying analysis of Event data to determine who to best proceed with further troubleshooting.  However, in SQL Server 2008, and 2008R2 a bug exists that causes incorrect output from the bucketizers when used to bucket on the wait_info event wait_type Data element.  This was fixed in SQL Server 2008 Service Pack 2 (http://support.microsoft.com/kb/2285068), and is not a problem in SQL Server Denali CTP1, but as of this writing has yet to be corrected in SQL Server 2008 R2 (at least the CU’s I have tested, there may be a newer one that I have missed, but I didn’t find one in a search).  To demonstrate this problem the following Event Session can be used:

    IF EXISTS(SELECT * FROM sys.server_event_sessions WHERE name='BucketizerTargetDemoWaits')
        DROP EVENT SESSION [BucketizerTargetDemoWaits] ON SERVER;
    CREATE EVENT SESSION [BucketizerTargetDemoWaits]
    ON SERVER
    ADD EVENT sqlos.wait_info
    (    ACTION (sqlserver.database_id)
        WHERE (duration > 0)) 
    ADD TARGET package0.asynchronous_bucketizer(
         SET filtering_event_name='sqlos.wait_info', source_type=0, source='wait_type')
    WITH (MAX_DISPATCH_LATENCY = 5 SECONDS)
    GO
    ALTER EVENT SESSION [BucketizerTargetDemoWaits]
    ON SERVER
    STATE=START

    The above Event Session will return valid map_key values for the wait_types Map in sys.dm_xe_map_values on SQL Server 2008 Service Pack 2 and SQL Server Denali CTP1, but will have erroneous information in the <value> node on SQL Server 2008 RTM and SP1 and SQL Server 2008 R2.  To query the bucketed waits from the target, use the following query:

    SELECT 
        mv.map_value AS WaitType,
        n.value('(@count)[1]', 'int') AS EventCount,
        n.value('(@trunc)[1]', 'int') AS EventsTrunc,
        n.value('(value)[1]', 'int') AS MapKey
    FROM
    (SELECT CAST(target_data as XML) target_data
    FROM sys.dm_xe_sessions AS s 
    JOIN sys.dm_xe_session_targets t
        ON s.address = t.event_session_address
    WHERE s.name = 'BucketizerTargetDemoWaits'
      AND t.target_name = 'asynchronous_bucketizer') as tab
    CROSS APPLY target_data.nodes('BucketizerTarget/Slot') as q(n)
    JOIN sys.dm_xe_map_values as mv
        ON mv.map_key = n.value('(value)[1]', 'int')
    WHERE mv.name = 'wait_types'

    What’s next?

    Well, at this point we are nearly half way through the Targets in Extended Events, and tomorrow we’ll continue our investigation by looking at the synchronous_event_counter, which can be used to help determine the impact an Event Session may have without having to perform full Event collection.

    Yesterday’s post, Targets Week - ring_buffer, looked at the ring_buffer Target in Extended Events and how it outputs the raw Event data in an XML document.  Today I’m going to go over the details of the other Target in Extended Events that captures raw Event data, the asynchronous_file_target.

    What is the asynchronous_file_target?

    The asynchronous_file_target holds the raw format Event data in a proprietary binary file format that persists beyond server restarts and can be provided to another person via ftp or email for remote disconnected analysis of the events.  The asynchronous_file_target has two types of files that are associated with it, the log files which contain the Event data, and the metadata file which contains information about the Events contained in the log files, allowing correct parsing of the log files and the Events and associated Actions contained within them.  Depending on the options configured for the asynchronous_file_target, there may be multiple log files associated with a started Event Session, but there will only be one metadata file created for the duration of that Event Sessions collection.  Subsequent collections by the same Event Session, for example, stopping it and starting it again at a later time, will create a new metadata file associated with that collection by the Event Session.  These files exist as a set and must be maintained together for the log files to be read.

    Configuration Options

    The asynchronous_file_target like the ring_buffer, has configuration options that can be found in the sys.dm_xe_object_columns DMV.

    -- Target Configurable Fields
    SELECT 
        oc.name AS column_name,
        oc.column_id,
        oc.type_name,
        oc.capabilities_desc,
        oc.description
    FROM sys.dm_xe_packages AS p
    JOIN sys.dm_xe_objects AS o 
        ON p.guid = o.package_guid
    JOIN sys.dm_xe_object_columns AS oc 
        ON o.name = oc.OBJECT_NAME 
        AND o.package_guid = oc.object_package_guid
    WHERE (p.capabilities IS NULL OR p.capabilities & 1 = 0)
      AND (o.capabilities IS NULL OR o.capabilities & 1 = 0)
      AND o.object_type = 'target'
      AND o.name = 'asynchronous_file_target'
    

     

    In SQL Server 2008, 2008R2, and SQL Server Denali CTP1, there are five configuration options for the asynchronous_file_target.  The filename specifies the path and name of the log files and is a required to add the asynchronous_file_target to an Event Session.  The max_file_size option functions the same as SQL Trace maxfilesize option, limiting the size of each file before rollover occurs.  The max_rollover_files option functions the same as the SQL Trace maxrolloverfiles option, specifying the number of rollover files to maintain in the file system, and can be used in conjunction with the max_file_size option to prevent the SQL Server from running out of disk space during Event collection.  The increment option is similar to the AutoGrowth settings for a database in SQL Server, and specifies the size in megabytes that the log files grow, allowing the files to grow incrementally and reducing the number of times a log file has to grow while Events are being dispatched and buffered to the Target.  The metadatafile option specifies the path and name of the metadata file for the target.

    image

    Notice that the only mandatory option for the file target is the filename for the log files.  When the asynchronous_file_target is used in an Event Session, if the metadatafile option is not explicitly set, the asynchronous_file_target will use the same path and filename specified in the filename option with a .xem extension for the metadata file automatically. 

    Understanding the Target Data Format

    Like the ring_buffer, the asynchronous_file_target stores Event data in its raw format.  Inside the log files, the Event data is maintained in a binary format that minimizes the amount of space necessary to store the Events, maximizing the number of Events that can be stored inside the log files.  Unlike the ring_buffer target however, the asynchronous_file_target is queried not through the sys.dm_xe_session_targets DMV, but through the sys.fn_xe_file_target_read_file() DMF.  The sys.fn_xe_file_target_read_file() DMF requires four input parameters; @path which is the path, filename, and extension mask to the log files, @mdpath which is the path, filename, and extension mask to metadata file, @initial_file_name which is the exact path and filename of a file to start reading from and when specified requires the final parameter @initial_offset which is the offset inside that file from which to begin reading the events.

    The sys.fn_xe_file_target_read_file() DMF returns a single row for each instance of Event data that is contained inside of the log files being read.  The Event data is materialized into an XML document in the event_data column output by the DMF.when the Target information is queried using the sys.fn_xe_file_target_read_file() DMF, allowing it to be used for Event analysis.  Like the ring_buffer Target, the Event data returned by the sys.fn_xe_file_target_read_file() DMF is not schema bound, but it has exactly the same XML format as an individual <event> node in the ring_buffer Targets output making it very easy to parse the Events contained in either target with very similar XQuery’s.

    Querying/Parsing the Target Data

    Since the asynchronous_file_target returns the Event data as XML, we have to do the same type of XQuery work to retrieve the Event data from it as we did with the ring_buffer target from yesterday.  After reading yesterday’s post, Adam Machanic (Blog|Twitter) pointed out in a comment on Twitter that the slow XML parsing is an optimizer bug that is handled with a derived table in his Extended Events Code Generator.  Adam is absolutely correct, and if you attended my session on Extended Events this year at PASS you’d recall that I didn’t have performance issues in my demo’s for querying the Target data generated by my demo’s.  I used a very different parsing method in my PASS demo’s than I showed yesterday, and I plan to cover that method in a later post in this series already.  However, if you want to see the gist of how to work around the performance issue take a look at the code output by Adam’s code generator.

    I am going to reuse yesterday’s demo as a basis for looking at the asynchronous_file_target for simplicity as well as to show the similarity of the XQuery used for querying the Event data.  The basic Event Session captures the error_reported Event and to trigger an error performs a SELECT against a non-existent table.

    -- Create an Event Session to capture Errors Reported
    CREATE EVENT SESSION DemoPersistedEvents
    ON SERVER
    ADD EVENT sqlserver.error_reported
    ADD TARGET package0.ring_buffer,
    ADD TARGET package0.asynchronous_file_target(
         SET filename='D:\SQLData\MSSQL10.MSSQLSERVER\MSSQL\Log\DemoPersistedEvents.xel')
    WITH (MAX_DISPATCH_LATENCY = 1 SECONDS)
    GO
    -- Alter the Event Session and Start it.
    ALTER EVENT SESSION DemoPersistedEvents
    ON SERVER
    STATE=START
    GO
    -- SELECT from a non-existent table to create Event
    SELECT *
    FROM master.schema_doesnt_exist.table_doesnt_exist
    GO
    -- Drop the Event to halt Event collection
    ALTER EVENT SESSION DemoPersistedEvents
    ON SERVER
    DROP EVENT sqlserver.error_reported
    GO

    The first thing we need to know to query our asynchronous_file_target is the filename and metafilename for the files that we want to query from.  If the event session is active and running, we can get this information by querying the Active Session DMV’s.

    SELECT 
        soc.column_name,
        soc.column_value
    FROM sys.dm_xe_sessions s
    JOIN sys.dm_xe_session_object_columns soc
        ON s.address = soc.event_session_address
    WHERE s.name = 'DemoPersistedEvents'
      AND soc.object_name = 'asynchronous_file_target'

    image

    Notice that the metatdatafile option is NULL, meaning that we were lazy and didn’t explicitly define the metadata file information in our Event Session so now we have to figure it out in order to query the target data from the log files.  One way to find the information would be to open up the path on the server to the log file that was specified:

    image

    Notice that the Extended Events Engine automatically created a metadata file with the same name as the log file, but a different extension, .xem.  Also notice that the file names for both the log file and the metadata file have changed from what was actually defined in the Event Session.  The Engine adds a _0_ and a long integer value that represents the number of milliseconds between January 1, 1600 and the date and time that the file was generated by the Extended Events Engine.  Subsequent files will have a different long integer value that is larger in value allowing you to easily sort the log files from oldest to newest or vice versa.  To query the data contained in the log files, you have two options.  First you can explicitly provide the filenames as shown above, or you can use wildcards in the names and the engine will find the correct matching files and begin reading them.

    DECLARE @path nvarchar(260), @mdpath nvarchar(260)
    
    -- Get the log file name and substitute * wildcard in
    SELECT 
        @path = LEFT(column_value, LEN(column_value)-CHARINDEX('.', REVERSE(column_value))) 
            + '*' 
            + RIGHT(column_value, CHARINDEX('.', REVERSE(column_value))-1)
    FROM sys.dm_xe_sessions s
    JOIN sys.dm_xe_session_object_columns soc
        ON s.address = soc.event_session_address
    WHERE s.name = 'DemoPersistedEvents'
      AND soc.object_name = 'asynchronous_file_target'
      AND soc.column_name = 'filename'
    
    -- Get the metadata file name and substitute * wildcard in 
    SELECT 
        @mdpath = LEFT(column_value, LEN(column_value)-CHARINDEX('.', REVERSE(column_value))) 
            + '*' 
            + RIGHT(column_value, CHARINDEX('.', REVERSE(column_value))-1)
    FROM sys.dm_xe_sessions s
    JOIN sys.dm_xe_session_object_columns soc
        ON s.address = soc.event_session_address
    WHERE s.name = 'DemoPersistedEvents'
      AND soc.object_name = 'asynchronous_file_target'
      AND soc.column_name = ' metadatafile'
    
    -- Set the metadata filename if it is NULL to the log file name with xem extension
    SELECT @mdpath = ISNULL(@mdpath, 
                            LEFT(@path, LEN(@path)-CHARINDEX('*', REVERSE(@path))) 
                            + '*xem')
    
    -- Query the Event data from the Target.
    SELECT
        module_guid,
        package_guid,
        object_name,
        event_data,
        file_name,
        file_offset
    FROM sys.fn_xe_file_target_read_file(@path, @mdpath, null, null)
    

     

    image

     

    The DMF outputs the module_guid, package_guid, and object_name associated with the Event, the event_data as a XML document, but in string format requiring that it be CAST/CONVERT’d to XML for parsing, the file_name of the log file that the Event data was read from and the file_offset inside the file for the event.  Using a CAST to XML and performing a CROSS APPLY of the <event> nodes and the same XQuery’s as in yesterday’s post we can query the Event data from the asynchronous_file_target.

    -- Query the Event data from the Target.
    SELECT 
        n.value('(@name)[1]', 'varchar(50)') AS event_name,
        n.value('(@package)[1]', 'varchar(50)') AS package_name,
        n.value('(@id)[1]', 'int') AS id,
        n.value('(@version)[1]', 'int') AS version,
        DATEADD(hh, 
                DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP), 
                n.value('(@timestamp)[1]', 'datetime2')) AS [timestamp],
        n.value('(data[@name="error"]/value)[1]', 'int') as error,
        n.value('(data[@name="severity"]/value)[1]', 'int') as severity,
        n.value('(data[@name="duration"]/value)[1]', 'int') as state,
        n.value('(data[@name="user_defined"]/value)[1]', 'varchar(5)') as user_defined,
        n.value('(data[@name="message"]/value)[1]', 'varchar(max)') as message
    FROM 
    (SELECT
        CAST(event_data AS XML) AS event_data
     FROM sys.fn_xe_file_target_read_file(@path, @mdpath, null, null)
    ) as tab
    CROSS APPLY event_data.nodes('event') as q(n)

    Like the ring_buffer Target, the asynchronous_file_target also has an entry in sys.dm_xe_session_targets, but instead of returning the Event data, it returns information about the targets operation.

    select 
        target_data.value('(FileTarget/@truncated)[1]', 'int') as truncated,
        target_data.value('(FileTarget/Buffers/@logged)[1]', 'int') as logged,
        target_data.value('(FileTarget/Buffers/@dropped)[1]', 'int') as dropped
    FROM
    (SELECT CAST(target_data AS XML) AS target_data
    FROM sys.dm_xe_sessions AS s 
    JOIN sys.dm_xe_session_targets AS t 
        ON t.event_session_address = s.address
    WHERE s.name = 'DemoPersistedEvents'
      AND t.target_name = 'asynchronous_file_target'
    ) as tab

    The file_name and file_offset information in the the sys.fn_xe_file_target_read_file output can be used to perform differential reads from the asynchronous_file_target.  To demonstrate this we can create an Event Session that will capture a lot of Events in a short period of time.

    (Note: I wouldn’t create an unfiltered Event Session on the starting and completed events like this on a production server without first evaluating its potential impact.  While this should be safe, if it causes you a problem, its your server not mine.)

    IF EXISTS(SELECT * FROM sys.server_event_sessions WHERE name='FileTargetDemo')
        DROP EVENT SESSION [FileTargetDemo] ON SERVER;
    CREATE EVENT SESSION [FileTargetDemo]
    ON SERVER
    ADD EVENT sqlserver.sql_statement_starting,
    ADD EVENT sqlserver.sql_statement_completed,
    ADD EVENT sqlserver.sp_statement_starting,
    ADD EVENT sqlserver.sp_statement_completed,
    ADD EVENT sqlserver.rpc_starting,
    ADD EVENT sqlserver.rpc_completed,
    ADD EVENT sqlserver.module_start,
    ADD EVENT sqlserver.module_end
    ADD TARGET package0.asynchronous_file_target(
         SET filename='D:\SQLData\MSSQL10.MSSQLSERVER\MSSQL\Log\FileTargetDemo.xel', 
             metadatafile='D:\SQLData\MSSQL10.MSSQLSERVER\MSSQL\Log\FileTargetDemo.xem',
             max_file_size = 5,
             max_rollover_files = 5)
    WITH(MAX_DISPATCH_LATENCY = 5SECONDS)
    GO
    
    -- Start the Event Collection
    ALTER EVENT SESSION [FileTargetDemo]
    ON SERVER
    STATE=STOP
    GO
    
    -- Take a pause and allow events to be generated
    
    
    -- Query the target data from the files.
    SELECT 
        object_name,
        CAST(event_data as xml) as event_data,
        file_name, 
        file_offset
    FROM sys.fn_xe_file_target_read_file('D:\SQLData\MSSQL10.MSSQLSERVER\MSSQL\Log\FileTargetDemo*xel', 
                'D:\SQLData\MSSQL10.MSSQLSERVER\MSSQL\Log\FileTargetDemo*xem', 
                null,
                null)

    If you scroll through the output to where the file_offset changes, you can grab the file_name and file_offset for the last event in the first file_offset.

    image

    Then requery the target passing that file_name and file_offset into the @initial_file_name and @intitial_offset parameters of the sys.fn_xe_file_target_read_file DMF to have the DMF begin reading from the last entry of the provided offset forward.

    -- Query the target data from the files.
    SELECT 
        object_name,
        CAST(event_data as xml) as event_data,
        file_name, 
        file_offset
    FROM sys.fn_xe_file_target_read_file('D:\SQLData\MSSQL10.MSSQLSERVER\MSSQL\Log\FileTargetDemo*xel', 
                'D:\SQLData\MSSQL10.MSSQLSERVER\MSSQL\Log\FileTargetDemo*xem',             
                'D:\SQLData\MSSQL10.MSSQLSERVER\MSSQL\Log\FileTargetDemo_0_129360796797990000.xel',
                0)

    image

    If you’ve run the demo’s in this blog post to this point, don’t forget to cleanup the system.

    IF EXISTS(SELECT * FROM sys.server_event_sessions WHERE name='FileTargetDemo')
        DROP EVENT SESSION [FileTargetDemo] ON SERVER;
    GO
    IF EXISTS(SELECT * FROM sys.server_event_sessions WHERE name='DemoPersistedEvents')
        DROP EVENT SESSION [DemoPersistedEvents] ON SERVER;
    GO

    Considerations for Usage

    The asynchronous_file_target will probably be the preferred target for most people interested in performing long term analysis of Events collected, or performing short term analysis using an Event Session that is expected to generate a large number of events and event loss due to the FIFO nature of the ring_buffer is not acceptable.  However, there are a couple of considerations associated with this target.  The first is that the log files and metadata file are a set, and have to be maintained together.  If you send someone a log file without the metadata file, they won’t be able to read the information contained in the log file.  The second consideration associated with this target is that the only way to read the information contained inside of the log files, as of the date of this blog post being published, is to copy them to a system that is running SQL Server 2008 or 2008R2 and query the files using the there is no way to retrieve the information contained inside of the log files, without querying the sys.fn_xe_file_target_read_file() DMF using TSQL. 

    What’s next?

    Now that we have looked at the asynchronous_file_target Target, in the next post we’ll look at the bucketizer Targets which can be used to group occurrences of Events based on the Event data being returned.

    Yesterdays post, Managing Event Sessions, showed how to manage Event Sessions in Extended Events Sessions inside the Extended Events framework in SQL Server. In today's post, we’ll take a look at how to find information about the defined Event Sessions that already exist inside a SQL Server using the Session Definition DMV’s and how to find information about the Active Event Sessions that exist using the Active Session DMV’s.

    Session Definition DMV’s

    The Session Definition DMV’s provide information about the Event Sessions that have been defined in the Extended Events Engine and may or may not be actively running against the SQL Server Instance.  Five DMV’s provide information about the Event Sessions that exist inside of the Extended Events Engine; sys.server_event_sessions, sys.server_event_session_events, sys.server_event_session_actions, sys.server_event_session_targets, and sys.server_event_session_fields.

    sys.server_event_sessions

    The sys.server_event_sessions DMV provides information about the Event Sessions that exist inside of the Extended Events Engine.  The Session level options for the Event Session can be retrieved from this DMV, to determine how the Event Session is configured.

    	-- Session level information for current Event Sessions
    	SELECT 
    	   s.name,
    	   s.max_memory,
    	   s.event_retention_mode_desc,
    	   s.max_dispatch_latency,
    	   s.max_event_size,
    	   s.memory_partition_mode_desc,
    	   s.track_causality,
    	   s.startup_state
    	FROM sys.server_event_sessions s
    	

    sys.server_event_session_events

    The sys.server_event_session_events DMV provides information about the specific Events that are defined in the Event Sessions maintained by the Extended Events Engine.  This DMV also returns the defined Predicates for the Events that are included for collection in Event Sessions on the server.  The event_session_id column can be used to join this DMV to sys.server_event_sessions as shown below.

    	-- Get events in a session
    	SELECT 
    	   ses.name AS session_name,
    	   sese.package AS event_package,
    	   sese.name AS event_name,
    	   sese.predicate AS event_predicate
    	FROM sys.server_event_sessions AS ses
    	JOIN sys.server_event_session_events AS sese
    	    ON ses.event_session_id = sese.event_session_id
    	

    sys.server_event_session_actions

    The sys.server_event_session_actions DMV contains one row for each of the Actions that have been added to an Event in an Event Session.  If the same Action was added to multiple Events, there would be a separate row per Event and Action pair in the Event Session.  The event_session_id and event_id columns are used to join this DMV to the sys.server_event_session_events DMV.

    	-- Get actions 
    	SELECT 
    	   ses.name AS session_name,
    	   sese.package AS event_package,
    	   sese.name AS event_name,
    	   sese.predicate AS event_predicate,
    	   sesa.package AS action_package,
    	   sesa.name AS action_name
    	FROM sys.server_event_sessions AS ses
    	JOIN sys.server_event_session_events AS sese
    	    ON ses.event_session_id = sese.event_session_id
    	JOIN sys.server_event_session_actions AS sesa
    	     ON ses.event_session_id = sesa.event_session_id
    	    AND sese.event_id = sesa.event_id
    	

    sys.server_event_session_targets

    The sys.server_event_session_targets DMV contains one row for each of the configured Targets that are defined for an Event Session.  The event_session_id column is used to join this DMV to the sys.server_event_sessions DMV.

    	-- Get target information
    	SELECT 
    	   ses.name AS session_name,
    	   sest.name AS target_name
    	FROM sys.server_event_sessions AS ses
    	JOIN sys.server_event_session_targets AS sest
    	         ON ses.event_session_id = sest.event_session_id
    	

    sys.server_event_session_fields

    The sys.server_event_session_fields DMV contains one row for each of the configured options for each Target defined for an Event Session.  The event_session_id and target_id columns are used to join this DMV to the sys.server_event_session_targets DMV.

    	-- Get target option information
    	SELECT 
    	   ses.name AS session_name,
    	   sest.name AS target_name,
    	   sesf.name AS option_name,
    	   sesf.value AS option_value
    	FROM sys.server_event_sessions AS ses
    	JOIN sys.server_event_session_targets AS sest
    	         ON ses.event_session_id = sest.event_session_id
    	JOIN sys.server_event_session_fields AS sesf
    	     ON sest.event_session_id = sesf.event_session_id
    	    AND sest.target_id = sesf.OBJECT_ID
    	

    Active Session DMV’s

    The Active Session DMV’s provide information about the Event Sessions that are currently in a started state on a SQL Server Instance.  Five DMV’s make up the group of Active Session DMV’s; sys.dm_xe_sessions, sys.dm_xe_session_events, sys.dm_xe_session_event_actions, sys.dm_xe_session_targets, and sys.dm_xe_session_object_columns.

    sys.dm_xe_sessions

    The sys.dm_xe_sessions DMV contains one row for each active Event Session (STATE=START) in the SQL Server Instance, and provides information about the configuration of the Session buffers.  Information about the size, and number of buffers is returned for the regular sized and large sized buffers associated with the Event Session.  An Event Session will have large sized buffers when the MAX_EVENT_SIZE configured is larger than the regular buffer size.  In general, most Events will be buffered to the regular buffers.  Information about event loss associated with the buffers being full and buffers that are full and pending dispatch is also contained in this DMV.

    	-- Look at Active Session Information
    	SELECT 
    	   s.name, 
    	   s.pending_buffers,
    	   s.total_regular_buffers,
    	   s.regular_buffer_size,
    	   s.total_large_buffers,
    	   s.large_buffer_size,
    	   s.total_buffer_size,
    	   s.buffer_policy_flags,
    	   s.buffer_policy_desc,
    	   s.flags,
    	   s.flag_desc,
    	   s.dropped_event_count,
    	   s.dropped_buffer_count,
    	   s.blocked_event_fire_time,
    	   s.create_time,
    	   s.largest_event_dropped_size
    	FROM sys.dm_xe_sessions AS s
    	
    	

    sys.dm_xe_session_targets

    The sys.dm_xe_session_targets DMV will contain one row for each Target that exists for an active Event Session.  Information about the Target such as the Target name (ring_buffer, pair_matching, etc.) and Target execution statistics are returned by this DMV.  For memory resident Targets, the target_data columns will return an XML document containing the information about the Events that have been dispatched to the Target and are still available.  For persisted Targets, the target_data column still contains an XML document, but only statistics about the Target will be contained in the document.  More specific information about the target_data column will be provided in the next week as we look at each Target individually.  The event_session_address column is used to join this DMV to the address column in the sys.dm_xe_sessions DMV.

    	-- Target information for a running session
    	SELECT 
    	   s.name AS session_name,
    	   t.target_name AS target_name,
    	   t.execution_count AS execution_count,
    	   t.execution_duration_ms AS execution_duration,
    	   CAST(t.target_data AS XML) AS target_data
    	FROM sys.dm_xe_sessions AS s
    	JOIN sys.dm_xe_session_targets AS t
    	   ON s.address = t.event_session_address
    	

    sys.dm_xe_session_events

    The sys.dm_xe_session_events DMV contains one row for each Event that is defined in an Active Event Session.  The predicate definition for each event, if defined, is included in the output of this DMV.  However, the predicate is not the same as returned by sys.server_event_session_events if standard logical operators were used in the Event definition.  Instead the Predicates are converted to use Predicate Comparators in text form, and for complex Predicates, the length can exceed the allowable output.  When this occurs, “Predicate too large for display” will be returned by the DMV.  The event_session_address column is used to join this DMV to the address column in the sys.dm_xe_sessions DMV.

    	-- Event Information for a running session
    	SELECT s.name AS session_name,
    	       e.event_name AS event_name,
    	       e.event_predicate AS event_predicate
    	FROM sys.dm_xe_sessions AS s
    	JOIN sys.dm_xe_session_events AS e
    	     ON s.address = e.event_session_address
    	

    sys.dm_xe_session_event_actions

    The sys.dm_xe_session_event_actions DMV contains one row for each Action that is defined on an Event in an Active Event Session.  If the same Action is defined on multiple Events in the Event Session, one row will be returned for each Event/Action pair.  The event_session_address and event_name columns are used to join this DMV to the address column in the sys.dm_xe_session_events DMV.

    	-- Event Information with Actions for a running session
    	SELECT s.name AS session_name,
    	       e.event_name AS event_name,
    	       e.event_predicate AS event_predicate,
    	       ea.action_name AS action_name
    	FROM sys.dm_xe_sessions AS s
    	JOIN sys.dm_xe_session_events AS e
    	     ON s.address = e.event_session_address
    	JOIN sys.dm_xe_session_event_actions AS ea
    	     ON e.event_session_address = ea.event_session_address
    	    AND e.event_name = ea.event_name
    	
    	

    sys.dm_xe_session_object_columns

    The sys.dm_xe_session_object_columns DMV contains one row for each of the configured options for a Target that is defined in an Active Event Session, as well as one row for each of the customizable Data Elements for a Event that is defined in an Active Event Session.  The event_session_address and event_name columns are used to join this DMV to the address column in the sys.dm_xe_session_events DMV.  The event_session_address and target_name columns are used to join this DMV to the address column in the sys.dm_xe_session_targets DMV.

    	-- Configurable event and target column information
    	SELECT DISTINCT s.name AS session_name, 
    	       oc.OBJECT_NAME, 
    	       oc.object_type, 
    	       oc.column_name, 
    	       oc.column_value
    	FROM sys.dm_xe_sessions AS s
    	JOIN sys.dm_xe_session_targets AS t
    	     ON s.address = t.event_session_address
    	JOIN sys.dm_xe_session_events AS e
    	     ON s.address = e.event_session_address
    	JOIN sys.dm_xe_session_object_columns AS oc
    	     ON s.address = oc.event_session_address
    	    AND ((oc.object_type = 'target' AND t.target_name = oc.OBJECT_NAME) 
    	       OR (oc.object_type = 'event' AND e.event_name = oc.OBJECT_NAME))
    	
    	

    What’s next?

    Now that we understand how to query the Extended Events Metadata, how to manage Event Sessions, and how to determine what Event Sessions have been created in a SQL Server, the next week of this series will focus on the specific targets of Extended Events and how to query the data contained in them.  The next post will look at the ring_buffer target and the data that it exposes.

    Yesterdays post, Querying the Extended Events Metadata, showed how to discover the objects available for use in Extended Events.  In today's post, we’ll take a look at the DDL Commands that are used to create and manage Event Sessions based on the objects available in the system.  Like other objects inside of SQL Server, there are three DDL commands that are used with Extended Events; CREATE EVENT SESSION, ALTER EVENT SESSION, and DROP EVENT SESSION.  The command names are self explanatory and their purposes should be clear to most SQL Server DBA’s.  The books online covers the syntax in detail so I won’t rehash all of that in this post, but will instead provide examples for each that cover specific areas of the commands.

    CREATE EVENT SESSION

    Creating an Event session adds the session definition to the Extended Events Engine making it available for event collection.  Creating an Event Session requires that at least one event be added to the session, but an Event Session does not need to have a target added to it to be created.  An individual event or target can only  be used once in an Event Session, and the complex predicates that are possible in Extended Events mitigates the need to have the same event multiple times in a session.  Session options are optional and if left unspecified the defaults documented in the books online will be used for the Event Session.  An example Event Session that shows all of the options used is:

    	CREATE EVENT SESSION [TrackTempdbFileWrites] ON SERVER
    	ADD EVENT sqlserver.file_write_completed(
    	   SET collect_path = 1
    	   ACTION (sqlserver.sql_text)
    	   WHERE database_id = 2),
    	ADD EVENT sqlserver.file_written(
    	   WHERE database_id = 2)
    	ADD TARGET package0.ring_buffer,
    	ADD TARGET package0.asynchronous_bucketizer(
    	     SET filtering_event_name='sqlserver.file_write_completed', source_type=0, source='file_id')
    	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
    	

    This event session captures the sqlserver.file_write_completed event and specifies that the event should collect the path to the file, which is a customizable column on the event, to execute the sql_text action to collect the sql_text that caused the event to fire, if it is available, and to only fire the event for database_id = 2, which happens to be tempdb.  Notice that in the Event definition, only the ACTION specification is defined inside of a set of parenthesis, which is defined the the BOL as required in the <event_definition> specification:

    <event_definition>::=
    {
        ADD EVENT [event_module_guid].event_package_name.event_name
             [ ( {
                     [ SET { event_customizable_attribute = <value> [ ,...n] } ]
                     [ ACTION ( { [event_module_guid].event_package_name.action_name [ ,...n] } ) ]
                     [ WHERE <predicate_expression> ]
            } ) ]
    }

    Modifications to a specific event are enclosed in a set of parenthesis, and then only the Action is constrained to provide a set of parenthesis in its definition.  However, the <predicate_expression> can optionally include parenthetical notation to group predicate sets similar to what is possible in the TSQL WHERE clause.  The session also captures the file_written event when the database_id = 2.

    The session utilizes two targets, not because it necessarily provides additional meaning to the event consumption, but because I needed a valid example that shows how multiple targets are defined on a single event session at creation.  The ring_buffer target is defined and will capture the raw event data using the default configuration options for the target, and the asynchronous_bucketizer target will count the occurrences of the sqlserver.file_write_completed events based on their specific source data element (file_id), allowing for easy identification of the files that are being written to the most frequently.

    The session options provided in the above script correspond to the default session options that are defined if an Event Session is created without specifying the WITH() clause at all.  The maximum buffer space allocated for the Session is 4MB, and the session allows single events to be lost if the events are generated faster than they can be dispatched to the targets, the maximum amount of time that an event can be in the buffers before being dispatched to the targets is 30 seconds, the maximum event size is not set, no partitioning of buffers is defined, causality tracking is not turned on, and the session will not start automatically when the SQL Server service starts.

    ALTER EVENT SESSION

    Creating an Event Session does nothing more than that in Extended Events; it simply catalogs the Event Session definition inside of the Extended Events Engine, if it passes the syntax check, and makes the Event Session available for use.  To actually begin Event collection, the Event Session must be ALTERed to start the Event Collection.  This is accomplished with the ALTER EVENT SESSION DDL command.

    	-- ALTER the Event Session to Start it
    	ALTER EVENT SESSION [TrackTempdbFileWrites]
    	ON SERVER
    	STATE=START
    	GO
    	
    	

    Once a Event Session is set to STATE=START, it becomes active inside of the Extended Events Engine and begins collecting Events that have been defined in the Session Definition.  An active Event Session can be modified to ADD EVENTs, DROP EVENTs, ADD TARGETs, and DROP TARGETs while the Event Session remains active inside the Extended Events Engine.  When an Event Session only has in memory targets defined on it, dropping all of the defined Events from the Event Session stops the Event Session from capturing further Events to allow for captured Event analysis while preserving the captured Event information for analysis.  If we run a relevant workload against the tempdb database based on the Event Session created above this can be demonstrated.

    	USE [tempdb]
    	GO
    	IF OBJECT_ID('Test') IS NOT NULL
    	   DROP TABLE Test
    	CREATE TABLE Test (rowid INT IDENTITY PRIMARY KEY, exampledata VARCHAR(4000))
    	GO
    	INSERT INTO Test(exampledata) VALUES (REPLICATE('abcd', 1000))
    	GO 100
    	ALTER EVENT SESSION [TrackTempdbFileWrites]
    	ON SERVER
    	DROP EVENT sqlserver.file_write_completed,
    	DROP EVENT sqlserver.file_written
    	GO
    	SELECT CAST(target_data AS XML) 
    	FROM sys.dm_xe_session_targets st
    	JOIN sys.dm_xe_sessions s ON st.event_session_address = s.address
    	WHERE s.name = 'TrackTempdbFileWrites'
    	GO
    	

    After dropping the events from the session, no further events are captured by the Event Session, which can be shown by running the example workload again and re-querying the targets.

    	USE [tempdb]
    	GO
    	IF OBJECT_ID('Test') IS NOT NULL
    	   DROP TABLE Test
    	CREATE TABLE Test (rowid INT IDENTITY PRIMARY KEY, exampledata VARCHAR(4000))
    	GO
    	INSERT INTO Test(exampledata) VALUES (REPLICATE('abcd', 1000))
    	GO 100
    	SELECT CAST(target_data AS XML) 
    	FROM sys.dm_xe_session_targets st
    	JOIN sys.dm_xe_sessions s ON st.event_session_address = s.address
    	WHERE s.name = 'TrackTempdbFileWrites'
    	GO
    	
    	

    While dropping events from a session that utilizes in memory targets allows for captured data evaluation, ALTERing the Session to change the event definition can be useful to react to the event information that has been captured, to provide more specific predicates, minimizing the number of events captured. To change the definition of a specific event already defined on an Event Session requires that the Event Session be ALTERed with a DROP EVENT definition for the specific Event and then that the Event Session be ALTERed with an ADD EVENT clause to add the Event back to the Event Session with the new Predicate Definition. For the purposes of the demo presented in this blog post, since we have already dropped the sqlserver.file_write_completed event, all we have to do is issue an ALTER EVENT SESSION command with an ADD EVENT specification that adds the new Event specification back to the Event Session.

    	-- Add the sqlserver.file_write_completed back with new predicates
    	ALTER EVENT SESSION [TrackTempdbFileWrites]
    	ON SERVER
    	ADD EVENT sqlserver.file_write_completed(
    	   SET collect_path = 1
    	   ACTION (sqlserver.sql_text)
    	   WHERE database_id = 2 AND FILE_ID = 1)
    	
    	

    Rerunning the workload against the new event session will show that the Event Session now only captures the sqlserver.file_write_completed Events based on the new criteria.

    	USE [tempdb]
    	GO
    	IF OBJECT_ID('Test') IS NOT NULL
    	   DROP TABLE Test
    	CREATE TABLE Test (rowid INT IDENTITY PRIMARY KEY, exampledata VARCHAR(4000))
    	GO
    	INSERT INTO Test(exampledata) VALUES (REPLICATE('abcd', 1000))
    	GO 100
    	SELECT CAST(target_data AS XML) 
    	FROM sys.dm_xe_session_targets st
    	JOIN sys.dm_xe_sessions s ON st.event_session_address = s.address
    	WHERE s.name = 'TrackTempdbFileWrites'
    	GO
    	

    If the Event Session definition proves to be to much for the ring_buffer target to maintain the data in a useable fashion, it may be preferable to instead capture the events to the asynchronous_file_target, which isn't constrained by the memory configuration of the ring_buffer target. At add the asynchronous_file_target, an ALTER EVENT SESSION statement can be used with the ADD TARGET command to add the new target to the Event Session, and a subsequent ALTER EVENT SESSION statement can be used with the DROP TARGET command to remove the ring_buffer target once its captured data has been consumed from the ring_buffer target.

    	-- Add a file target
    	ALTER EVENT SESSION [TrackTempdbFileWrites]
    	ON SERVER
    	ADD TARGET package0.asynchronous_file_target
    	( SET filename = 'C:\SQLBlog\TrackTempdbFileWrites.xel',
    	     metadatafile = 'C:\SQLBlog\TrackTempdbFileWrites.mta')
    	GO
    	-- Drop a ring_buffer target   
    	ALTER EVENT SESSION [TrackTempdbFileWrites]
    	ON SERVER
    	DROP TARGET package0.ring_buffer
    	GO
    	
    	

    The only part of an Event Session that can not be modified without restarting the Event Session is the Session Options. To change a Session Option, for example the EVENT_RETENTION_MODE from ALLOW_SINGLE_EVENT_LOSS to NO_EVENT_LOSS, requires that the Event Session first be stopped and then restarted. An attempt to change a session level option for an active Event Session results in the following Error:

    Msg 25707, Level 16, State 1, Line 2 Event session option "event_retention_mode" cannot be changed while the session is running. Stop the event session before changing this session option.

    To change an Event Session level option, requires that the Event Session first be stopped, the option be changed with ALTER EVENT SESSION, and then the Event Session be started again.

    	-- Stop the Event Seession first
    	ALTER EVENT SESSION [TrackTempdbFileWrites]
    	ON SERVER
    	STATE=STOP
    	GO
    	-- Change Event Retention Mode
    	ALTER EVENT SESSION [TrackTempdbFileWrites]
    	ON SERVER
    	WITH (EVENT_RETENTION_MODE = NO_EVENT_LOSS)
    	GO
    	-- Start the Event Seession after the change
    	ALTER EVENT SESSION [TrackTempdbFileWrites]
    	ON SERVER
    	STATE=START
    	GO
    	

    DROP EVENT SESSION

    To DROP a Event Session from the Extended Events Engine to prevents its future use, the DROP EVENT SESSION DDL command can be used. The DROP command can be used against an Active Event Session as well as against a Sessions that are simply cataloged in the Engine and not active.

    	DROP EVENT SESSION [TrackTempdbFileWrites] ON SERVER
    	

    What’s next?

    Now that we have how to manage Event Session in Extended Events, in the next post we’ll look at how to find information about the Event Sessions that have been defined in a SQL Server Instance.

    In yesterdays post, An Overview of Extended Events, I provided some of the necessary background for Extended Events that you need to understand to begin working with Extended Events in SQL Server.  After receiving some private feedback on the initial post, I have changed the post naming convention associated with the post to reflect “2 of 31” instead of 2/31, which apparently caused some confusion in Paul Randall’s and Glenn Berry’s series which were mentioned in the round up post for this series.

    In today’s post we’ll look at the DMV’s that contain the Extended Events Metadata, and how to query those to find out information about the objects that can be used in Extended Events.

    What No UI Support?

    One of the first things you’ll notice is that there is no native UI support in Extended Events.  This was a problem very early on and makes working with Extended Events difficult for most people.  If you are the type of person that doesn’t want to write code, you can download the Extended Events SSMS Addin I wrote from Codeplex, and it will go a long way in simplifying working with Extended Events.  However, to really exploit the power of Extended Events, you are going to have to write some things manually.  I generally use the Addin to create my basic Event Session definition and script that to a SSMS editor window, where I then go back and manipulate the DDL as needed.  I’ll cover the Addin in detail in a later post, but one of the items included in it is a Metadata Viewer that allows you to browse the package metadata in a TreeView to see all of the objects available in the Engine.

    image 

    The Metadata DMV’s

    Since there is no UI support, all of the metadata for Extended Events is exposed through a set of DMV’s that I refer to as the Metadata DMV’s.  There are other sets of DMV’s associated with Extended Events that will be covered in later posts as well and I have similar reference names for them as logical groups.  There are four DMV’s that contain metadata information about Extended Events in SQL Server; sys.dm_xe_packages, sys.dm_xe_objects, sys.dm_xe_object_columns, and sys.dm_xe_map_values.  Not all of the objects that are returned by the DMV’s are useable in Extended Events.  To identify the objects that can not be used, the first three DMV’s include a bitwise integer column named capabilities that can be checked with a bitwise logical AND against the number one to determine if the object is for internal usage only.

    sys.dm_xe_packages

    The sys.dm_xe_packages DMV contains a single entry for each of the packages that has been registered in the Extended Events Engine.  The packages each have a unique guid associated with them that is used to identify the objects that belong to that package.  As mentioned yesterday, there are four packages in SQL Server 2008, but one of those, the SecAudit package is for internal usage only by Server Audits.  When we filter out the internal packages using the previously mentioned bitwise logical AND against the capabilities column, only the useable packages remain.

    	-- Extended Event Packages
    	SELECT
    	    name,
    	    guid,
    	    description
    	FROM sys.dm_xe_packages p
    	WHERE (p.capabilities IS NULL OR p.capabilities & 1 = 0)
    	
    	

    In yesterday's post I talked about the changes between SQL Server 2008 and SQL Server Denali CTP1 with regards to the number of available packages, and the modules that load them. The sys.dm_xe_packages DMV includes the module_address column that maps to the base_address column in the sys.dm_os_loaded_modules DMV, which can be used to see the specific modules name that loaded the package.

    	SELECT
    	    p.name,
    	    p.description,
    	    lm.name 
    	FROM sys.dm_xe_packages p
    	JOIN sys.dm_os_loaded_modules lm
    	    ON p.module_address = lm.base_address
    	WHERE (p.capabilities IS NULL OR p.capabilities & 1 = 0)
    	
    	

    The output for SQL Server 2008 is:

    image

    The output for SQL Server Denali CTP1 is:

    image 

    sys.dm_xe_objects

    The sys.dm_xe_objects DMV contains information about all of the objects (events, actions, predicates, targets, types and maps) available in the packages registered in the Extended Events Engine.  It is joined to the sys.dm_xe_packages DMV by the package_guid column to map specific objects back to the package that loaded them.  The object_type column determines the type of object and can be used to filter on to only find specific object types.  The following examples show how to use this DMV to find the specific objects by type.

    Events

    	-- Event objects
    	SELECT p.name AS package_name,
    	        o.name AS event_name,
    	        o.description
    	FROM sys.dm_xe_packages AS p
    	JOIN sys.dm_xe_objects AS o 
    	     ON p.guid = o.package_guid
    	WHERE (p.capabilities IS NULL OR p.capabilities & 1 = 0)
    	   AND (o.capabilities IS NULL OR o.capabilities & 1 = 0)
    	   AND o.object_type = 'event'
    	

    Actions

    	-- Actions
    	SELECT p.name AS package_name,
    	        o.name AS action_name,
    	        o.description
    	FROM sys.dm_xe_packages AS p
    	JOIN sys.dm_xe_objects AS o
    	      ON p.guid = o.package_guid
    	WHERE (p.capabilities IS NULL OR p.capabilities & 1 = 0)
    	   AND (o.capabilities IS NULL OR o.capabilities & 1 = 0)
    	   AND o.object_type = 'action' 
    	

    Targets

    	-- Targets
    	SELECT p.name AS package_name,
    	        o.name AS target_name,
    	        o.description
    	FROM sys.dm_xe_packages AS p
    	JOIN sys.dm_xe_objects AS o ON p.guid = o.package_guid
    	WHERE (p.capabilities IS NULL OR p.capabilities & 1 = 0)
    	   AND (o.capabilities IS NULL OR o.capabilities & 1 = 0)
    	   AND o.object_type = 'target'
    	

    Predicates

    Predicates are a special type of object in Extended Events because there are two different types of predicate objects in the Metadata; source objects which provide the global state data elements for filtering on and comparators which provide the textual comparisons that can be performed between a data element and the specified value.

    Predicate Sources
    	-- State Data Predicates
    	SELECT p.name AS package_name,
    	        o.name AS source_name,
    	        o.description
    	FROM sys.dm_xe_objects AS o
    	JOIN sys.dm_xe_packages AS p
    	      ON o.package_guid = p.guid
    	WHERE (p.capabilities IS NULL OR p.capabilities & 1 = 0)
    	   AND (o.capabilities IS NULL OR o.capabilities & 1 = 0)
    	   AND o.object_type = 'pred_source'
    	
    Predicate Comparators
    	-- Comparison Predicates
    	SELECT p.name AS package_name,
    	        o.name AS source_name,
    	        o.description
    	FROM sys.dm_xe_objects AS o
    	JOIN sys.dm_xe_packages AS p
    	      ON o.package_guid = p.guid
    	WHERE (p.capabilities IS NULL OR p.capabilities & 1 = 0)
    	   AND (o.capabilities IS NULL OR o.capabilities & 1 = 0)
    	   AND o.object_type = 'pred_compare'
    	

    Maps

    	-- Maps
    	SELECT p.name AS package_name,
    	        o.name AS source_name,
    	        o.description
    	FROM sys.dm_xe_objects AS o
    	JOIN sys.dm_xe_packages AS p
    	      ON o.package_guid = p.guid
    	WHERE (p.capabilities IS NULL OR p.capabilities & 1 = 0)
    	   AND (o.capabilities IS NULL OR o.capabilities & 1 = 0)
    	   AND o.object_type = 'map'
    	

    Types

    	-- Types
    	SELECT p.name AS package_name,
    	        o.name AS source_name,
    	        o.description
    	FROM sys.dm_xe_objects AS o
    	JOIN sys.dm_xe_packages AS p
    	      ON o.package_guid = p.guid
    	WHERE (p.capabilities IS NULL OR p.capabilities & 1 = 0)
    	   AND (o.capabilities IS NULL OR o.capabilities & 1 = 0)
    	   AND o.object_type = 'Type'
    	
    	

    sys.dm_xe_object_columns

    The sys.dm_xe_object_columns DMV provides the information about the columns, or data elements, that exist for a specific object.  There are three types of columns that are returned from this DMV; readonly, data, and customizable.  Readonly columns are additional system metadata about an event, that allows the integration with Event Tracing for Windows.  Data columns are the data elements that are returned by default when the event fires.  Customizable columns have different usages depending on the object type and are covered in detail below.

    Event Data Elements

    To know if an event is going to be useful or not often requires knowing the data elements returned when the event fires. This can then be used with the requirements to determine what actions to add to a specific Event during Session creation.

    	-- Event Columns
    	SELECT oc.name AS column_name,
    	        oc.column_type AS column_type,
    	        oc.column_value AS column_value,
    	        oc.description AS column_description
    	FROM sys.dm_xe_packages AS p
    	JOIN sys.dm_xe_objects AS o
    	      ON p.guid = o.package_guid
    	JOIN sys.dm_xe_object_columns AS oc
    	      ON o.name = oc.OBJECT_NAME
    	     AND o.package_guid = oc.object_package_guid
    	WHERE (p.capabilities IS NULL OR p.capabilities & 1 = 0)
    	   AND (o.capabilities IS NULL OR o.capabilities & 1 = 0)
    	   AND (oc.capabilities IS NULL OR oc.capabilities & 1 = 0)
    	   AND o.object_type = 'event'
    	   AND o.name = 'wait_info'
    	

    Configurable Event Elements

    Part of the flexibility behind Extended Events, and one of the design features that keeps it highly performant is the inclusion of Event Data Elements that can collect additional data in the base payload for an event that may be more expensive than acceptable for general uses, but may be useful at times, and acceptable to expend the additional impact on. When these types of information exist, the Event will have a customizable data element that has to be turned on in the Event Session definition to collect the additional information. An example of this is the collect_path element for the file_write_completed Event. By default this Event won't collect the file path, but if this information is needed or considered to be important, it can be turned on and will be collected when the event fires.

    	-- Configurable Event Columns
    	SELECT oc.name AS column_name,
    	        oc.column_type AS column_type,
    	        oc.column_value AS column_value,
    	        oc.description AS column_description
    	FROM sys.dm_xe_packages AS p
    	JOIN sys.dm_xe_objects AS o
    	      ON p.guid = o.package_guid
    	JOIN sys.dm_xe_object_columns AS oc
    	      ON o.name = oc.OBJECT_NAME
    	     AND o.package_guid = oc.object_package_guid
    	WHERE (p.capabilities IS NULL OR p.capabilities & 1 = 0)
    	   AND (o.capabilities IS NULL OR o.capabilities & 1 = 0)
    	   AND (oc.capabilities IS NULL OR oc.capabilities & 1 = 0)
    	   AND o.object_type = 'event'
    	   AND o.name = 'file_write_completed'
    	   AND oc.column_type = 'customizable'
    	

    Target Configurable Options

    Some of the targets available in Extended Events have configurable elements that are required to use the target.  An example of this would be the asynchronous_file_target, which requires that you provide a filename for the file.  Other options are optional like the max_file_size and max_rollover_files options for the asynchronous_file_target.

    	-- Target Configurable Fields
    	SELECT oc.name AS column_name,
    	        oc.column_id,
    	        oc.type_name,
    	        oc.capabilities_desc,
    	        oc.description
    	FROM sys.dm_xe_packages AS p
    	JOIN sys.dm_xe_objects AS o
    	      ON p.guid = o.package_guid
    	JOIN sys.dm_xe_object_columns AS oc
    	      ON o.name = oc.OBJECT_NAME
    	     AND o.package_guid = oc.object_package_guid
    	WHERE (p.capabilities IS NULL OR p.capabilities & 1 = 0)
    	   AND (o.capabilities IS NULL OR o.capabilities & 1 = 0)
    	   AND o.object_type = 'target'
    	   AND o.name = 'asynchronous_file_target'
    	

    sys.dm_xe_map_values

    The sys.dm_xe_map_values DMV provides the key/value pairs for each of the Maps defined in the system.  Maps are linked by their object_package_guid to the specific package that created them.

    	-- Map Values
    	SELECT name, map_key, map_value 
    	FROM sys.dm_xe_map_values
    	WHERE name = 'wait_types'	

    In SQL Server Denali CTP1, it may seem like there are duplicate Maps for sqlserver, but remember that there are two sqlserver packages, and they are loaded from different modules, so each module has to load its own Maps into the Engine.  It would be theoretically possible for the modules to have different Map definitions for the same Map name in the Engine, if there was a change in one module that didn’t occur in the other module.  Hopefully we don’t actually find that to be true one day.

    What’s next?

    Now that we have information about the objects available in Extended Events, in the next post we’ll look at the DDL commands used to create and manage Extended Events Sessions.

    First introduced in SQL Server 2008, Extended Events provided a new mechanism for capturing information about events inside the Database Engine that was both highly performant and highly configurable.  Designed from the ground up with performance as a primary focus, Extended Events may seem a bit odd at first look, especially when you compare it to SQL Trace.  However, as you begin to work with Extended Events, you will most likely change how you think about tracing problems, and will find the power in the design of Extended Events.  This series of blog posts should help you get a jumpstart to using Extended Events if you haven’t already.  This post will review Extended Events from a high level, providing the basic background upon which the rest of the series will build on.

    The Extended Events Engine

    The Extended Events engine is the central service that manages the objects and resources for Extended Events.  The engine manages a pool of workers known as dispatchers that are responsible for processing event information from the memory buffers to the targets based on the session options.  The engine also maintains the information for Event Sessions that have been created in the instance, and the metadata about the packages that have been registered by modules inside the engine.  The engine itself contains no object metadata.  The engine essentially provides the platform for which packages can be registered and event sessions can be created, and event information can be consumed.

    Packages

    Packages are registered by modules in the Extended Events Engine and contain the information about what Extended Events objects the module contains.  Packages are the top level containers of metadata for the other objects that exist inside of Extended Events.  Packages can contain a combination of events, actions, targets, maps, predicates and types.  In SQL Server 2008, there are only four packages; sqlserver, sqlos, package0, and a private internal use only package SecAudit, which is used by Server Audits.  All of these packages are loaded by the sqlservr.exe module.  In Denali CTP1, there are three additional packages, one more for sqlserver but loaded from different module, a new sqlclr package that is loaded from sqlservr.exe,  and a new ucs (unified communications stack) package that is loaded from sqlservr.exe.  In addition to these changes the package0 and sqlos modules are no longer loaded by sqlservr.exe and instead are loaded by sqldk.dll.

    Events

    Events provide information about the execution of the module that loaded the package containing the event.  An event corresponds to a specific point in code where something of interest occurs inside the Database Engine.  In SQL Server 2008, there are 253 events that can be used to capture information and as previously mentioned, while there are a lot of events, not all of the common events from SQL Trace were implemented in Extended Events.  In SQL Server Denali CTP1, the number of events has expanded to 446, and now includes a corresponding event for all of the events from SQL Trace.  Each event returns information specific to the point in code corresponding to the event, which I refer to as is basic payload of information.  The amount of information returned in the base payload is set by the events definition in the package metadata, but can be added to through the use of Actions.

    Actions

    Actions are bound to individual events in the definition of the Event Session in the engine, and perform a specified task when the event fires.  Actions generally do two different types of tasks, they either collect additional information that is added to the firing events base payload, generally global state information like session_id, sql_text, etc. and they can perform tasks inside the engine like inserting a debug_break, or performing a memory dump for analysis.  In SQL Server 2008 there are 35 actions that can be used.  In SQL Server Denali CTP 1 there are 43 actions that can be used and include new ones that map to Trace Columns like context_info, database_name, and event_sequence that were unavailable in SQL Server 2008.

    Targets

    Targets are the event destinations for Extended Events.  Most of the targets are memory resident, meaning that they exist in memory, and only while an event session is active in a started state on the instance.  These can be useful for short term troubleshooting, when a long term persisted version of the events is not necessary.  A file target also exists which functions similar to the Trace File in SQL Trace and collects the event information in files on the file system that can then be transferred for analysis by another person at a later point in time.  Two types of targets exist in Extended Events, synchronous targets which have the events buffered to them by the executing thread, and asynchronous targets that have the events dispatched to them by the dispatchers in Engine when the buffers fill up, or the maximum duration for dispatch defined by the session is exceeded for an event.  A more detailed coverage of the specific targets will be done a little bit later in this series.

    Maps

    Maps provide lookup information for the information that is available inside Extended Events.  Maps provide a key/value pairing for specific types of information that can be used in defining event sessions, and correlating the information captured by event sessions.  An example of a map would be the wait_types, which maps the engine key to logical name for all wait_types that can be fired inside of SQL Server.

    Predicates

    Predicates provide the filtering mechanism inside of Extended Events and are defined on individual events and control the circumstances under which the event actually fires.  Predicates in Extended Events offer short-circuiting, where the first false evaluation in the predicate string terminates the evaluation and prevents the event from firing, making evaluation of properly defined predicates highly performant.  Predicates can be defined on any of the data returned by the events base payload, as well as on global state information that is available inside of the engine.  Predicates can be defined two different ways, using common filtering clause criteria such as <,>, >=, <=, =, <>, LIKE, NOT LIKE, etc. but can also be defined using textual comparators that are included in package metadata.  Inside the Engine when a session is started, any predicate using common filtering clause gets converted to its corresponding textual comparator.

    Types

    Types define the data type for the information elements inside of Extended Events.  There are 29 different types in SQL Server 2008, but only 28 inside of SQL Server Denali CTP1; the database_context type was removed.  In addition to the package defined types, some event data elements will define the data type as a map name, which provides information about the type of information that will be returned from the data element.

    Event Sessions

    Event Sessions are a collection of events, their corresponding actions and predicates, and the targets that will be the destinations for the events being collected.  Defined Event Sessions exist in the Extended Events Engine until they are explicitly dropped allowing the session to be started and stopped as needed without having to recreate the session, even beyond service restarts.  Event Sessions have a number of configuration options that control how the session functions, including the ability to start automatically when the service starts up, the maximum duration of time between an event firing and being dispatched to the targets, the amount of memory available to the event session for buffering, how that memory is partitioned, and when/if/how event loss can occur when the buffer space fills up faster than the dispatchers can deliver the events.  and have a number.  An event session requires at least one event at creation, but does not require a target, allowing side acting actions like a memory dump to occur without having to actually dispatch the event for consumption.

    What’s next?

    With the basics out of the way, in the next post we’ll look at the Metadata DMV’s and how to get the information about the objects available in Extended Events.  For more information about Extended Events in the mean time take a look at the Using SQL Server 2008 Extended Events whitepaper I wrote on MSDN.

    Back in April, Paul Randal (Blog|Twitter) did a 30 day series titled A SQL Server Myth a Day, where he covered a different myth about SQL Server every day of the month.  At the same time Glenn Berry (Blog|Twitter) did a 30 day series titled A DMV a Day, where he blogged about a different DMV every day of the month.  Being so inspired by these two guys, I have decided to attempt a month long series on Extended Events that I am going to call A XEvent a Day.  I originally wanted to do this series during the month of November, but with school requirements and preparations for PASS Summit, I just couldn’t make it work out.  Instead I am going to end 2010 with a bang and at the same time double my blog post count for they year by doing it in December.

    This post will be the master post and will have a link to each of the posts throughout the month as I post them.

    An XEvent a Day (1 of 31) – An Overview of Extended Events
    An XEvent a Day (2 of 31) – Querying the Extended Events Metadata
    An XEvent a Day (3 of 31) – Managing Event Sessions
    An XEvent a Day (4 of 31) – Querying the Session Definition and Active Session DMV’s
    An XEvent a Day (5 of 31) – Targets Week – ring_buffer
    An XEvent a Day (6 of 31) – Targets Week – asynchronous_file_target
    An XEvent a Day (7 of 31) – Targets Week – bucketizers
    An XEvent a Day (8 of 31) – Targets Week – synchronous_event_counter
    An XEvent a Day (9 of 31) – Targets Week – pair_matching
    An XEvent a Day (10 of 31) – Targets Week – etw_classic_sync_target
    An XEvent a Day (11 of 31) – Targets Week – Using multiple targets to simplify analysis
    An XEvent a Day (12 of 31) – Using the Extended Events SSMS Addin
    An XEvent a Day (13 of 31) – The system_health Session
    An XEvent a Day (14 of 31) – A Closer Look at Predicates
    An XEvent a Day (15 of 31) – Tracking Ghost Cleanup
    An XEvent a Day (16 of 31) – How Many Checkpoints are Issued During a Full Backup?
    An XEvent a Day (17 of 31) – A Look at Backup Internals and How to Track Backup and Restore Throughput (Part 1)
    An XEvent a Day (18 of 31) – A Look at Backup Internals and How to Track Backup and Restore Throughput (Part 2)
    An XEvent a Day (19 of 31) – Using Customizable Fields
    An XEvent a Day (20 of 31) – Mapping Extended Events to SQL Trace
    An XEvent a Day (21 of 31) – The Future – Tracking Blocking in Denali
    An XEvent a Day (22 of 31) – The Future – fn_dblog() No More? Tracking Transaction Log Activity in Denali
    An XEvent a Day (23 of 31) – How it Works – Multiple Transaction Log Files
    An XEvent a Day (24 of 31) – What is the package0.callstack Action?
    An XEvent a Day (25 of 31) – The Twelve Days of Christmas
    An XEvent a Day (26 of 31) – Configuring Session Options
    An XEvent a Day (27 of 31) – The Future - Tracking Page Splits in SQL Server Denali CTP1
    An XEvent a Day (28 of 31) – Tracking Page Compression Operations
    An XEvent a Day (29 of 31) – The Future – Looking at Database Startup in Denali
    An XEvent a Day (30 of 31) – Tracking Session and Statement Level Waits
    An XEvent a Day (31 of 31) – Event Session DDL Events

    It is time again for another TSQL Tuesday, this time hosted by my good friend and fellow MVP, Sankar Reddy(Blog|Twitter).   This month’s topic is Misconceptions about SQL Server, and as Sankar points out in this months, there are so many misconceptions out there that almost anyone can blog about this topic, in fact I might even blog more than just one post on this topic since I have a few half finished blog posts that would fit into this topic.

    For this post I am going to look at a recent misconception from the MSDN Forums, that physical reads of a data file, correlate to wait stats inside the database engine.  The background on this is rooted in two Dynamic Management Objects inside the database engine; the Dynamic Management Function (DMF) sys.dm_io_virtual_file_stats() which returns I/O statistics for data and log files and the Dynamic Management View (DMV) sys.dm_os_wait_stats which returns information about all the waits encountered by threads that have executed.  These objects reflect cumulative totals for their information, generally since the server was last restarted.  While it is possible to reset the wait statistic information by using DBCC SQLPERF ('sys.dm_os_wait_stats', CLEAR), the I/O statistics can only be cleared by restarting the server.  When using either of these objects for analysis of a specific time, it is important to take a snapshot of the data held in them at the start of the analysis, generally by INSERTing the information contained by the object into a temporary table and then performing a differential analysis of the data against a later SELECT from the objects (more on this in a minute).

    The misconception on the forum thread was that I/O stalls as reflected in the sys.dm_io_virtual_file_stats() DMF, would correlate to I/O related wait counts, and wait times in the sys.dm_os_wait_stats DMV.  Prior to SQL Server 2008, this kind of thing would be impossible to prove without having indepth knowledge of the Database Engine code.  However, one of the new features in SQL Server 2008, Extended Events, actually allows us to define a Event Session to take a look at this in depth, and not only disprove the misconception, but potentially explain the what and why behind it.

    To start off lets look at the snapshot information that lead to the misconception for a simple query against AdventureWorks:

    USE [AdventureWorks]
    GO
    -- Clear the Buffer Cache to force reads from Disk
    DBCC DROPCLEANBUFFERS
    GO
    -- Drop temporary tables if they exist
    IF OBJECT_ID('tempdb..#io_stats') IS NOT NULL
    DROP TABLE #io_stats
    GO
    IF OBJECT_ID('tempdb..#wait_stats') IS NOT NULL
    DROP TABLE #wait_stats
    GO
    -- Create the Snapshots of the Dynamic Management Objects
    SELECT *
    INTO #io_stats
    FROM sys.dm_io_virtual_file_stats(NULL, NULL)
    GO
    SELECT *
    INTO #wait_stats
    FROM sys.dm_os_wait_stats
    GO
    -- Turn on Statistics IO so we can see the IO ops
    SET STATISTICS IO ON
    GO
    -- Run the Simple SELECT against AdventureWorks
    SELECT SUM(TotalDue), SalesPersonID
    FROM Sales.SalesOrderHeader
    GROUP BY SalesPersonID
    GO
    -- Turn Off Statistics IO
    SET STATISTICS IO OFF
    GO
    -- Get the Differential Wait Statistic Information
    SELECT
      
    wait_type               = t1.wait_type,
      
    waiting_tasks_count     = t1.waiting_tasks_count - t2.waiting_tasks_count,
      
    wait_time_ms            = t1.wait_time_ms - t2.wait_time_ms,
      
    max_wait_time_ms        = t1.max_wait_time_ms - t2.max_wait_time_ms,
      
    signal_wait_time_ms     = t1.signal_wait_time_ms - t2.signal_wait_time_ms
    FROM sys.dm_os_wait_stats AS t1
    JOIN #wait_stats AS t2
      
    ON t1.wait_type = t2.wait_type
    WHERE t1.wait_time_ms - t2.wait_time_ms > 0
    GO
    -- Get the Differential IO Statistic Information
    SELECT
      
    database_name           = DB_NAME(t1.database_id),
      
    logical_name            = mf.name,
      
    physical_name           = mf.physical_name,
      
    num_of_reads            = t1.num_of_reads - t2.num_of_reads,
      
    num_of_bytes_read       = t1.num_of_bytes_read - t2.num_of_bytes_read,
      
    io_stall_read_ms        = t1.io_stall_read_ms - t2.io_stall_read_ms,
      
    num_of_writes           = t1.num_of_writes - t2.num_of_writes,
      
    num_of_bytes_written    = t1.num_of_bytes_written - t2.num_of_bytes_written,
      
    io_stall_write_ms       = t1.io_stall_write_ms - t2.io_stall_write_ms,
      
    io_stall                = t1.io_stall - t2.io_stall,
      
    size_on_disk_bytes      = t1.size_on_disk_bytes,
      
    file_handle             = t1.file_handle
    FROM sys.dm_io_virtual_file_stats(NULL, NULL) AS t1
    JOIN #io_stats AS t2
      
    ON t1.database_id = t2.database_id
          
    AND t1.FILE_ID = t2.FILE_ID
    JOIN sys.master_files AS mf
      
    ON mf.database_id = t1.database_id
          
    AND mf.FILE_ID = t1.FILE_ID
    WHERE (t1.num_of_reads - t2.num_of_reads > 0)
      OR (
    t1.num_of_writes - t2.num_of_writes > 0)
    GO

    The expectation in the forums thread was that the num_of_reads, and the io_stall_read_ms values from sys.dm_io_virtual_file_stats() would have an equivalent amount of waiting_tasks_count and wait_time_ms in sys.dm_os_wait_stats, but as can be seen from the below output, this is not the case.

    image

    Before I continue with this post, let me first provide some information about the hardware basis for these tests.  The information contained in this blog post comes from my VMware Workstation SQL Server 2008 VM running on my laptop which is a a Dell Inspiron 1400 which has 4GB RAM, and a 120GB OCZ Summit SSD.  The VM has a single vCPU and 1GB RAM allocated to it.  Now I would be remiss in thinking that this configuration wouldn’t raise some eyebrows, but rest assured that I have validated these findings on multiple enterprise class VM’s as well as physical servers including a dual quad core Dell R710 with 32GB RAM and a 8 Disk DAS that I have used in other tests for blog posts in the past.  The results have been consistent across all of my tests and the output from my laptop VM allows me to completely isolate the workload being performed in SQL to a single execution easily.

    So why don’t these numbers line up?  To explain this further we can use an Extended Event Session to look at the internal workings when the example workload query is executed:

    IF EXISTS(SELECT *
            
    FROM sys.server_event_sessions
            
    WHERE name='test_session')
       
    DROP EVENT SESSION [test_session] ON SERVER;
    CREATE EVENT SESSION [test_session]
    ON SERVER
    ADD EVENT sqlserver.file_read(
        
    ACTION (sqlserver.database_id, sqlserver.session_id)),
    ADD EVENT sqlserver.file_written(
        
    ACTION (sqlserver.database_id, sqlserver.session_id)),
    ADD EVENT sqlserver.file_read_completed(
        
    ACTION (sqlserver.database_id, sqlserver.session_id)),
    ADD EVENT sqlserver.file_write_completed(
        
    ACTION (sqlserver.database_id, sqlserver.session_id)),
    ADD EVENT sqlos.async_io_requested(
        
    ACTION (sqlserver.database_id, sqlserver.session_id)),
    ADD EVENT sqlos.async_io_completed(
        
    ACTION (sqlserver.database_id, sqlserver.session_id)),
    ADD EVENT sqlos.wait_info(
        
    ACTION (sqlserver.database_id, sqlserver.session_id)),
    ADD EVENT sqlserver.sql_statement_starting(
        
    ACTION (sqlserver.database_id, sqlserver.plan_handle,
               
    sqlserver.session_id, sqlserver.sql_text)),
    ADD EVENT sqlserver.sql_statement_completed(
        
    ACTION (sqlserver.database_id, sqlserver.plan_handle,
               
    sqlserver.session_id, sqlserver.sql_text))
    ADD TARGET package0.ring_buffer
    WITH (MAX_MEMORY = 4096KB,
        
    EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS,
        
    MAX_DISPATCH_LATENCY = 5 SECONDS,
        
    MAX_EVENT_SIZE = 4096KB,
        
    MEMORY_PARTITION_MODE = PER_CPU,
        
    TRACK_CAUSALITY = ON,
        
    STARTUP_STATE = OFF)

    Some items about this Event Session that should be noted:

    1. The Event Session uses the TRACK_CAUSALITY session option which appends a sequential GUID to each event allowing the event sequences to be followed.
    2. The Event Session captures the sql_statement_starting and sql_statement_completed events allowing isolation of the associated events between these two events by the attach_activity_id action, which is collected by causality tracking.
    3. The Event Session captures the following IO related Events:
      • async_io_requested
      • async_io_completed
      • file_read
      • file_read_completed
      • file_written
      • file_write_completed
      • wait_info
    4. The database_id and session_id actions are captured for each event, and the sql_text and plan_handle actions are captured for the sql_statement_% events.
    5. The event data is captured to a ring_buffer target which could result in event loss on a busy system, in which case a asynchronous_file_target would be more appropriate.
    6. The Event Session is configured with 4MB of buffer space that is partitioned per CPU, and a MAX_DISPATCH_LATENCY of 5 seconds.
    7. In the situation where the Session Buffers fill up faster than they can be dispatched single events can be lost to allow collection of incoming events.

    To use the Event Session to further investigate what is going on here, we can change our original workload script to start the Event Session before running the test query, and then to drop the Events from the Event Session to disable Event collection while leaving the captured Events available in the ring_buffer target:

    USE [AdventureWorks]
    GO
    -- Clear the Buffer Cache to force reads from Disk
    DBCC DROPCLEANBUFFERS
    GO
    -- Drop temporary tables if they exist
    IF OBJECT_ID('tempdb..#io_stats') IS NOT NULL
    DROP TABLE #io_stats
    GO
    IF OBJECT_ID('tempdb..#wait_stats') IS NOT NULL
    DROP TABLE #wait_stats
    GO
    -- Create the Snapshots of the Dynamic Management Objects
    SELECT *
    INTO #io_stats
    FROM sys.dm_io_virtual_file_stats(NULL, NULL)
    GO
    SELECT *
    INTO #wait_stats
    FROM sys.dm_os_wait_stats
    GO
    -- Turn on Statistics IO so we can see the IO ops
    SET STATISTICS IO ON
    GO
    -- Start the Event Session so we capture the Events caused by running the test
    ALTER EVENT SESSION test_session
    ON SERVER
    STATE
    =START
    GO
    -- Run the Simple SELECT against AdventureWorks
    SELECT SUM(TotalDue), SalesPersonID
    FROM Sales.SalesOrderHeader
    GROUP BY SalesPersonID
    GO
    -- Disable Event collection by dropping the Events from the Event Session
    ALTER EVENT SESSION test_session
    ON SERVER
       
    DROP EVENT sqlos.async_io_requested,
       
    DROP EVENT sqlos.async_io_completed,
       
    DROP EVENT sqlos.wait_info,
      
    DROP EVENT sqlserver.file_read,
      
    DROP EVENT sqlserver.file_written,
      
    DROP EVENT sqlserver.file_read_completed,
      
    DROP EVENT sqlserver.file_write_completed,
      
    DROP EVENT sqlserver.sql_statement_starting,
      
    DROP EVENT sqlserver.sql_statement_completed
    GO
    -- Turn Off Statistics IO
    SET STATISTICS IO OFF
    GO
    -- Get the Differential Wait Statistic Information
    SELECT
      
    wait_type               = t1.wait_type,
      
    waiting_tasks_count     = t1.waiting_tasks_count - t2.waiting_tasks_count,
      
    wait_time_ms            = t1.wait_time_ms - t2.wait_time_ms,
      
    max_wait_time_ms        = t1.max_wait_time_ms - t2.max_wait_time_ms,
      
    signal_wait_time_ms     = t1.signal_wait_time_ms - t2.signal_wait_time_ms
    FROM sys.dm_os_wait_stats AS t1
    JOIN #wait_stats AS t2
      
    ON t1.wait_type = t2.wait_type
    WHERE t1.wait_time_ms - t2.wait_time_ms > 0
    GO
    -- Get the Differential IO Statistic Information
    SELECT
      
    database_name           = DB_NAME(t1.database_id),
      
    logical_name            = mf.name,
      
    physical_name           = mf.physical_name,
      
    num_of_reads            = t1.num_of_reads - t2.num_of_reads,
      
    num_of_bytes_read       = t1.num_of_bytes_read - t2.num_of_bytes_read,
      
    io_stall_read_ms        = t1.io_stall_read_ms - t2.io_stall_read_ms,
      
    num_of_writes           = t1.num_of_writes - t2.num_of_writes,
      
    num_of_bytes_written    = t1.num_of_bytes_written - t2.num_of_bytes_written,
      
    io_stall_write_ms       = t1.io_stall_write_ms - t2.io_stall_write_ms,
      
    io_stall                = t1.io_stall - t2.io_stall,
      
    size_on_disk_bytes      = t1.size_on_disk_bytes,
      
    file_handle             = t1.file_handle
    FROM sys.dm_io_virtual_file_stats(NULL, NULL) AS t1
    JOIN #io_stats AS t2
      
    ON t1.database_id = t2.database_id
          
    AND t1.FILE_ID = t2.FILE_ID
    JOIN sys.master_files AS mf
      
    ON mf.database_id = t1.database_id
          
    AND mf.FILE_ID = t1.FILE_ID
    WHERE (t1.num_of_reads - t2.num_of_reads > 0)
      OR (
    t1.num_of_writes - t2.num_of_writes > 0)
    GO

    Looking at the raw output from this second test, we can see that it has identical num_of_reads, and num_of_bytes_read as the original query (there may be a slight variance here depending on the number of tests executed, but over 10 repeated tests, 7 of the tests returned identical results, with 2 of the remaining 3 showing 14 reads with just over

    image

    To perform analysis of the Events that fired while the Event Session was capturing data, we will use a PIVOT query to rotate the information captured into a common format for analysis using the following query:

    DROP TABLE #Results

    -- Extract the Event information from the Event Session
    SELECT
      
    -- Add a RowID column to provide guaranteed Event ordering for analysis
      
    IDENTITY(INT,1,1) AS RowID,
      
    table2.*
    -- INSERT the immediate results into a temp table for further analysis
    INTO #Results
    FROM
    SELECT XEvent.query('.') AS XEvent
      
    FROM
      
    (   -- Cast the target_data to XML
          
    SELECT CAST(target_data AS XML) AS TargetData
          
    FROM sys.dm_xe_session_targets st
          
    JOIN sys.dm_xe_sessions s
              
    ON s.address = st.event_session_address
          
    WHERE name = 'test_session'
            
    AND target_name = 'ring_buffer'    ) AS Data
      
    -- Split out the Event Nodes
      
    CROSS APPLY TargetData.nodes ('RingBufferTarget/event') AS XEventData (XEvent)    ) AS table1
    -- Use a TVF CROSS APPLY to split out the final result columns for PIVOT
    CROSS APPLY
    SELECT *
      
    FROM
      
    (   -- Get the Event Name
          
    SELECT
              
    'event_name' AS 'name',
              
    RTRIM(LTRIM(XEvent.value('(event/@name)[1]', 'nvarchar(max)'))) AS value
          
    UNION ALL
          
    -- Get the Event Timestamp
          
    SELECT
              
    'timestamp' AS 'name',
              
    RTRIM(LTRIM(XEvent.value('(event/@timestamp)[1]', 'nvarchar(40)'))) AS value
          
    UNION ALL
          
    -- Get the Event data columns
          
    SELECT
              
    RTRIM(LTRIM(XData.value('@name[1]','nvarchar(max)'))) AS 'name',
              
    RTRIM(LTRIM(COALESCE(NULLIF(XData.value('(text)[1]','nvarchar(max)'), ''),
                        
    XData.value('(value)[1]','nvarchar(max)')))) AS 'value'
          
    FROM XEvent.nodes('event/data') AS Data (XData)
          
    UNION ALL
          
    -- Get the Event action columns
          
    SELECT
              
    XData.value('@name[1]','nvarchar(max)') AS 'name'
              
    ,COALESCE(NULLIF(XData.value('(text)[1]','nvarchar(max)'), ''),
                        
    XData.value('(value)[1]','nvarchar(max)')) AS 'value'
          
    FROM XEvent.nodes('event/action') AS Data (XData)   ) AS tab
       PIVOT
      
    (   MAX(value)
          
    -- Specify columns for PIVOT Operation
          
    FOR name IN
              
    (   [event_name], [timestamp], [database_id], [session_id],
                  
    [file_handle], [wait_type], [opcode], [duration],
                  
    [signal_duration], [mode], [file_id], [succeeded],
                  
    [page_id], [extent_id], [sql_text], [plan_handle], [cpu],
                  
    [reads], [writes], [state], [attach_activity_id]
              
    )
       )
    AS pvt
    ) AS table2

    With the data PIVOT’d into a temporary table we can do some basic analysis of what was captured by the Event Session.

    DECLARE @attach_id VARCHAR(100)
    DECLARE @start_event_id INT
    DECLARE
    @end_event_id INT

    -- Set the variables for the start_event_id, and attach_id
    SELECT
      
    @start_event_id = RowID,
      
    @attach_id = SUBSTRING(attach_activity_id, 1,
                   (
    LEN(attach_activity_id)-CHARINDEX('-', REVERSE(attach_activity_id))+1))
    FROM #Results
    WHERE event_name = 'sql_statement_starting'
    AND sql_text LIKE '%SELECT SUM(TotalDue)%'

    -- Set the variable for the end_event_id
    SELECT
      
    @end_event_id = RowID
      
    FROM #Results
    WHERE event_name = 'sql_statement_completed'
    AND sql_text LIKE '%SELECT SUM(TotalDue)%'


    -- SELECT the rows from the captured Events that are between the start and
    -- end Event with the attach_activity_id for the start Event
    SELECT
      
    RowID,
      
    event_name,
      
    [attach_activity_id],
      
    database_id,
      
    FILE_ID,
      
    file_handle,
      
    mode,
      
    wait_type,
      
    opcode,
      
    duration,
      
    signal_duration,
      
    [sql_text],
      
    [plan_handle],
      
    [cpu],
      
    [reads],
      
    [writes],
      
    [state]
    FROM #Results AS r
    WHERE attach_activity_id LIKE @attach_id+'%'
     
    AND RowID >= @start_event_id
     
    AND RowID <= @end_event_id

    With this output, we can look at the order of engine events that fired, and from a quick glance it would seem that IO operations do have PAGEIOLATCH_SH waits associated with them as highlighted below:

    image

    What is quickly evident is that while the wait type might get set, it has a zero duration, yet this alone is not enough information to support the analysis here, we really have to work through the events to determine what exactly might be going on here.  One thing that immediately stands out to me is that the PAGEIOLATCH_SH waits may not have long durations associated with them, in fact the screenshots for this post show zero durations for the events, but the file_read_completed events have durations associated with them.

    image

    So in the interests of trying to better explain the output from sys.dm_io_virtual_file_stats() lets aggregate the totals for all the wait types captured in association with this statement executing, as well as aggregate the file_read_completed durations with the wait_info durations to see how they compare to the output from sys.dm_io_virtual_file_stats().

    DECLARE @attach_id VARCHAR(100)
    DECLARE @start_event_id INT
    DECLARE
    @end_event_id INT

    -- Set the variables for the start_event_id, end_event_id, and attach_id
    SELECT
      
    @start_event_id = RowID,
      
    @attach_id = SUBSTRING(attach_activity_id, 1,
                   (
    LEN(attach_activity_id)-CHARINDEX('-', REVERSE(attach_activity_id))+1))
    FROM #Results
    WHERE event_name = 'sql_statement_starting'
    AND sql_text LIKE '%SELECT SUM(TotalDue)%'

    SELECT
      
    @end_event_id = RowID
      
    FROM #Results
    WHERE event_name = 'sql_statement_completed'
    AND sql_text LIKE '%SELECT SUM(TotalDue)%'


    -- Aggregate the wait types associated with the Event exectuion
    SELECT
      
    wait_type,
      
    COUNT(*) AS waiting_task_count,
      
    SUM(CAST(duration AS INT)) AS duration
    FROM #results
    WHERE attach_activity_id LIKE @attach_id+'%'
     
    AND RowID >= @start_event_id
     
    AND RowID <= @end_event_id
     
    AND opcode = 'end'
    GROUP BY wait_type

    -- Aggregate the IO durations with the waits for the Event execution
    SELECT SUM(CAST(duration AS INT)) AS IO_and_wait_duration
    FROM #Results AS r
    WHERE attach_activity_id LIKE @attach_id+'%'
     
    AND RowID >= @start_event_id
     
    AND RowID <= @end_event_id
     
    AND event_name IN ('file_read_completed', 'wait_info')

    image

    When we compare this to the original output from sys.dm_io_virtual_file_stats(), we get closer to the total io_stall_read_ms values, though we are still not precisely there.

    image

    So what accounts for the difference here?  Well, I could speculate, but that isn’t really the purpose of this post, and it would only feed into further misconceptions about what the correlation of the values presented here might actually be.  What is really clear here is that the information contained in sys.dm_io_virtual_file_stats() can not be directly correlated to the output from sys.dm_os_wait_stats.   As shown in this simple, ok maybe its a bit complex because Extended Events has yet to be made simple to use, there is duration associated with file read operations that is not directly attributable to any wait type, which should be expected.

    At the beginning of the year, I wrote an article titled Retrieving Deadlock Graphs with SQL Server 2008 Extended Events that detailed how to use the default system_health session to retrieve deadlock graphs from SQL Server 2008 without having to use SQL Profiler, SQL Trace or enabling a Trace flag on the SQL Server.  In writing that article I happened upon a bug that is covered in the article with a work around in the Deadlock XML that is output by the Extended Events Engine.  That bug was filed with Microsoft and is covered in the following connect feedback:

    https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=404168&wa=wsignin1.0

    When I first encountered the bug, I traded emails with Jerome Halmans, one of the developers for Extended Events, who confirmed the bug, and helped me validate the workaround to generate valid XML.  During this exchange it was brought up that the Deadlock Monitor in SQL Server was reworked to add support for Multi-Victim Deadlocks, and the new XML output in Extended Events can be used to identify such a monster.

    This sparked my interests and I tried for a few days tirelessly to actually create a multi-victim deadlock to no avail.  I was warned that they can be difficult to actually produce, but it is possible to do.  I eventually gave up trying a few days later, due mostly to the fact that I was going to be gone for two months, but it was something in my list of things to figure out.

    Fast forward to last week when I got an email from Yuxi Bai, a member of the SQL Development team who actually worked on the Deadlock Monitor.  He had read my article on SQL Server Central and noted that there was a discrepancy in the explanation of the bug in the XML when compared to the actual work around that corrects the bug.  The explanation of the defect in the article is over simplified, and was written based on my first findings when looking at the bug.  It is actually slightly more complex than the article makes out, which I figured out before the article was published and I changed the code for the workaround so that it correctly generated a deadlock graph.  A small difference in wording makes the article correct, and I have submitted a correction to the article.

    Since I had his attention anyway, I decided to ask again about creating a Multi-Victim Deadlock, and this time I got the missing hint that I needed to actually trigger one.  Set one SPID to a higher Deadlock Priority than the other two, and voila, I was able to trigger a multi-victim deadlock.  All was cool, and I thought I was set to write a neat article about the new Deadlock XML, only to find that the XML from my workaround was now once again invalid.  It took a few minutes to figure out, but an invalid end tag is generated when multiple victims exist in the new output.  So I shot an email back to Yuxi to get validation, and to determine if a new connect item needed to be submitted.  Since it falls within the scope of the existing feedback, invalid XML in the Deadlock Event for Extended Events, which has not been released as fixed in a Cumulative Update or Service Pack, it was filed as a part of the existing connect item and will be fixed in the same release.

    So what exactly does a multi-victim deadlock look like?  Using the attached scripts in the following order, a multi-victim deadlock can be triggered in SQL Server 2008:

    1. Run the setup script
    2. Run Transaction 1 to the -- Stop Here tag.
    3. Run Transaction 3 to the -- Stop Here tag.
    4. Run Transaction 2 to the -- Stop Here tag.
    5. Run the remainder of Transaction 1.
    6. Run the remainder of Transaction 3
    7. Run the remainder of Transaction 2.   Deadlock should be triggered and rollback Transactions 1 and 3.

    The deadlock graph will look as follows if you use the work around code from the SQL Server Central Article, excluding the outer CAST as xml of the deadlock graphs which will raise an exception:

    <deadlock-list>
      <deadlock>
        <victim-list>
            <victimProcess id="processb1b390">
            <victimProcess id="process5ac6718"/>
          </victim-list>
        <process-list>
          <process id="processb1b390" taskpriority="5" logused="0" waitresource="OBJECT: 2:37575172:0 " waittime="8810" 
    ownerId="1467" transactionname="user_transaction" lasttranstarted="2009-05-26T23:31:06.170" XDES="0x57f1b30" 
    lockMode="IX" schedulerid="1" kpid="60524" status="suspended" spid="54" sbid="0" ecid="0" priority="-5" 
    trancount="2" lastbatchstarted="2009-05-26T23:31:35.467" lastbatchcompleted="2009-05-26T23:31:06.170" 
    clientapp="Microsoft SQL Server Management Studio - Query" hostname="LT-JKEHAYIAS" hostpid="2724" 
    loginname="OUTBACKNT\JKehayias" isolationlevel="read committed (2)" xactid="1467" currentdb="2" 
    lockTimeout="4294967295" clientoption1="671090784" clientoption2="390200">
            <executionStack>
              <frame procname="" line="3" stmtstart="38" sqlhandle="0x020000006f183c102e5e14b67437dc81884f5c2bc608d7d1"> 
    </frame>
            </executionStack>
            <inputbuf>  -- X LOCK ON R2 UPDATE r2 SET rowid = rowid +1 FROM r2 WITH (XLOCK, HOLDLOCK)      </inputbuf>
          </process>
          <process id="process5ac6718" taskpriority="5" logused="0" waitresource="OBJECT: 2:53575229:0 " waittime="13580" 
    ownerId="1500" transactionname="user_transaction" lasttranstarted="2009-05-26T23:31:13.840" XDES="0x677c280" 
    lockMode="IX" schedulerid="1" kpid="61076" status="suspended" spid="55" sbid="0" ecid="0" priority="-5" 
    trancount="2" lastbatchstarted="2009-05-26T23:31:30.700" lastbatchcompleted="2009-05-26T23:31:13.840" 
    clientapp="Microsoft SQL Server Management Studio - Query" hostname="LT-JKEHAYIAS" hostpid="2724" 
    loginname="OUTBACKNT\JKehayias" isolationlevel="read committed (2)" xactid="1500" currentdb="2" 
    lockTimeout="4294967295" clientoption1="671090784" clientoption2="390200">
            <executionStack>
              <frame procname="" line="2" stmtstart="34" sqlhandle="0x02000000f52a3407c874ca4873bf6b08bffe3c14823c1f39">        </frame>
            </executionStack>
            <inputbuf> -- X LOCK ON R3 UPDATE r3 SET rowid = rowid +1 FROM r3 WITH (XLOCK, HOLDLOCK)      
    </inputbuf>
          </process>
          <process id="process5ac61c0" taskpriority="-5" logused="284" waitresource="OBJECT: 2:21575115:0 " waittime="2691" 
    ownerId="1531" transactionname="user_transaction" lasttranstarted="2009-05-26T23:31:24.153" XDES="0x677dbe0" 
    lockMode="IX" schedulerid="1" kpid="3460" status="suspended" spid="56" sbid="0" ecid="0" priority="5" 
    trancount="2" lastbatchstarted="2009-05-26T23:31:41.577" lastbatchcompleted="2009-05-26T23:31:24.153" 
    clientapp="Microsoft SQL Server Management Studio - Query" hostname="LT-JKEHAYIAS" hostpid="2724" 
    loginname="OUTBACKNT\JKehayias" isolationlevel="read committed (2)" xactid="1531" currentdb="2" 
    lockTimeout="4294967295" clientoption1="671090784" clientoption2="390200">
            <executionStack>
              <frame procname="" line="2" stmtstart="34" sqlhandle="0x020000007107001868d80e0fb28607c5ab8111e7495ccb44">        </frame>
            </executionStack>
            <inputbuf> -- X LOCK ON R3 UPDATE r1 SET rowid = rowid +1 FROM r1 WITH (XLOCK, HOLDLOCK) 
    </inputbuf>
          </process>
        </process-list>
        <resource-list>
          <objectlock lockPartition="0" objid="37575172" subresource="FULL" dbid="2" objectname="" id="lock48b7300" mode="X" 
    associatedObjectId="37575172">
            <owner-list>
              <owner id="process5ac61c0" mode="X"/>
            </owner-list>
            <waiter-list>
              <waiter id="processb1b390" mode="IX" requestType="wait"/>
            </waiter-list>
          </objectlock>
          <objectlock lockPartition="0" objid="53575229" subresource="FULL" dbid="2" objectname="" id="lock48b6440" mode="X" 
    associatedObjectId="53575229">
            <owner-list>
              <owner id="process5ac61c0" mode="X"/>
            </owner-list>
            <waiter-list>
              <waiter id="process5ac6718" mode="IX" requestType="wait"/>
            </waiter-list>
          </objectlock>
          <objectlock lockPartition="0" objid="21575115" subresource="FULL" dbid="2" objectname="" id="lock48b6d00" mode="S" 
    associatedObjectId="21575115">
            <owner-list>
              <owner id="process5ac6718" mode="S"/>
              <owner id="processb1b390" mode="S"/>
            </owner-list>
            <waiter-list>
              <waiter id="process5ac61c0" mode="IX" requestType="wait"/>
            </waiter-list>
          </objectlock>
        </resource-list>
      </deadlock>
    </deadlock-list>
    

    Note that the first victim is missing the closing back slash for the XML node:

    <victimProcess id="processb1b390">

    it should actually be:

    <victimProcess id="processb1b390"/>

    This should be fixed in a coming update/service pack for SQL Server 2008.  Now what is really important here is that the only place you can get a multi-victim deadlock graph is Extended Events.  If you run a profiler trace or enable trace flag 1222 on the server, you will get two single deadlock graphs, and it is up to you to correlate that the two deadlocks are actually one deadlock.  The same thing occurs in SQL Server 2005.

    While writing this blog post, I happened to notice another difference between the two deadlock graphs.  If you look at the <executionStack> in the Extended Events Deadlock graph, the <frame> information is only partially populated when compared to a Deadlock graph from SQL Server profiler.  I sent this back to Jerome and Yuxi as a potential additional problem and learned that this was intentionally missing from the deadlock graph and with good reason.  To retrieve this information requires that additional locks be taken inside the Database Engine which can delay publishing of the Event.  The information is also readily available by querying sys.dm_exec_text() or sys.fn_get_sql() with the provided sqlhandle in the <frame> attributes.

    Theme design by Nukeation based on Jelle Druyts