Archive and track IO stats with fn_virtualfilestats
Tagged:  •  

Using the SQL function fn_virtualfilestats, you can retrieve cumulative IO information for each database since SQL server started. The following T-SQL script creates a multi-function sproc that when scheduled via SQL Agent job, will periodically save a copy of current fn_virtualfilestats and calculate the difference between the last stored file stats.

The following describes the usage information:

FileStats - Archived fn_virtualfilestats
Usage: FileStats @option , [ @db_id | @db_name ], [@start [, @end]]
 @option - Action to perform
    log     - Adds current file stats to the archive
    log_db  - Adds current file stats for specified
              @db_name or @db_id only
    exclude - Adds @db_name or @db_id to exclusion
              list.  If @db_name/@db_id are not
              specified, current exclusions will be
              displayed.
    include - Removes @db_name or @db_id from the
              exclusion list.  By default all databases
              are included.
    bydb    - Shows total stats grouped by db_name
              optional totals over time ranges
              between @start to @end
    bytime  - Shows stat grouped by time and db optionaly
              between @start to @end
    bystat  - Shows for each stat, optionaly
              between @start to @end
    delete  - Deletes all entries in the stats table
              optionally, only those entries that have
              a date older than @start
 @db_name - Optional database name
 @db_id   - Optional system database ID
 @start   - Optional datetime for filtering results
            or for deleting older stats
 @end     - Optional datetime for filtering results

Scheduling a SQL agent job to run the following, however often you want to update the stats:

EXEC FileStats @option = 'log'

To prune off old data from the FileStatsData table, run the following shows deleting all entries prior to 12/1/2004:

EXEC FileStats @option = 'delete', @start = '12/1/2004'

Databases can also be excluded from any of the results commands using the following:

EXEC FileStats @option = 'exclude', @db_name = 'master'

The script was created using Query Analyzer's template parameters so the database, stored procedure and the two tables that are all created can be named as needed. Open the script in QA and press Ctrl+Shift+M to bring up the replace dialog to specify all of the object names.

/************************************************************************
* File Stats - Archived fn_virtualfilestats
* <Stats_Proc,sysname,FileStats> - Name of that stored proc
* <Data_Table,sysname,FileStatsData> - Name of the results table
* <Exclude_Table,sysname,FileStatsExcludes> - Table of db's to exclude
************************************************************************/
IF EXISTS(SELECT * FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_NAME = '<Stats_Proc,sysname,FileStats>')
	DROP PROCEDURE <Stats_Proc,sysname,FileStats>
	GO

CREATE PROCEDURE <Stats_Proc,sysname,FileStats>(
	@option varchar(255) = NULL,
	@db_id smallint = NULL,
	@db_name sysname = NULL,
	@start datetime = NULL,
	@end datetime = NULL
)
AS
	-- Make sure tables exist
	IF (@option != 'create_tables')
		EXEC <Stats_Proc,sysname,FileStats> @option = 'create_tables'

	IF (@option IS NULL) -- usage
		BEGIN
		PRINT '<Stats_Proc,sysname,FileStats> - Archived fn_virtualfilestats'
		PRINT 'Usage: <Stats_Proc,sysname,FileStats> @option , [ @db_id | @db_name ], [@start [, @end]]'
		PRINT CHAR(9) + '@option - Action to perform'
		PRINT CHAR(9) + '   log     - Adds current file stats to the archive'
		PRINT CHAR(9) + '   log_db  - Adds current file stats for specified'
		PRINT CHAR(9) + '             @db_name or @db_id only'
		PRINT CHAR(9) + '   exclude - Adds @db_name or @db_id to exclusion'
		PRINT CHAR(9) + '             list.  If @db_name/@db_id are not'
		PRINT CHAR(9) + '             specified, current exclusions will be'
		PRINT CHAR(9) + '             displayed.'
		PRINT CHAR(9) + '   include - Removes @db_name or @db_id from the'
		PRINT CHAR(9) + '             exclusion list.  By default all databases'
		PRINT CHAR(9) + '             are included.'
		PRINT CHAR(9) + '   bydb    - Shows total stats grouped by db_name'
		PRINT CHAR(9) + '             optional totals over time ranges'
		PRINT CHAR(9) + '             between @start to @end'
		PRINT CHAR(9) + '   bytime  - Shows stat grouped by time and db optionaly'
		PRINT CHAR(9) + '             between @start to @end'
		PRINT CHAR(9) + '   bystat  - Shows for each stat, optionaly'
		PRINT CHAR(9) + '             between @start to @end'
		PRINT CHAR(9) + '   delete  - Deletes all entries in the stats table'
		PRINT CHAR(9) + '             optionally, only those entries that have'
		PRINT CHAR(9) + '             a date older than @start'
		PRINT CHAR(9) + '@db_name - Optional database name'
		PRINT CHAR(9) + '@db_id   - Optional system database ID'
		PRINT CHAR(9) + '@start   - Optional datetime for filtering results'
		PRINT CHAR(9) + '           or for deleting older stats'
		PRINT CHAR(9) + '@end     - Optional datetime for filtering results'

		RETURN
		END
	IF (@option = 'avg') -- Returns entries averaged over time
		BEGIN
		IF (@start IS NOT NULL AND @end IS NULL)
			SET @end = GETDATE()

		IF (DB_ID(@db_name) IS NOT NULL)
			SET @db_id = DB_ID(@db_name)

		SELECT DB_Name(DbId) AS DBName
			, SUM(DeltaNumberReads)
				/ CAST(SUM(DeltaTimeStamp / 1000) AS float) AS NumberReadsPerSec
			, SUM(DeltaNumberWrites)
				/ CAST(SUM(DeltaTimeStamp / 1000) AS float) AS NumberWritesPerSec
			, SUM(DeltaBytesRead)
				/ CAST(SUM(DeltaTimeStamp / 1000) AS float) AS BytesReadPerSec
			, SUM(DeltaBytesWritten)
				/ CAST(SUM(DeltaTimeStamp / 1000) AS float) AS BytesWrittenPerSec
			, SUM(DeltaIOStallMS)
				/ CAST(SUM(DeltaTimeStamp / 1000) AS float) AS IOStallMSPerSec
		FROM <Data_Table,sysname,FileStatsData>
		WHERE DbId NOT IN (SELECT db_id FROM <Exclude_Table,sysname,FileStatsExcludes>)
			AND (@start IS NULL OR Added BETWEEN @start AND @end)
			AND (@db_id IS NULL OR DbId = @db_id)
			AND DeltaNumberReads IS NOT NULL -- Skip First entries
		GROUP BY DB_Name(DbId)
		ORDER BY DB_Name(DbId)
		
		RETURN
		END
	IF (@option = 'bytime') -- Returns entries that are grouped by time and group
		BEGIN
		IF (@start IS NOT NULL AND @end IS NULL)
			SET @end = GETDATE()

		IF (DB_ID(@db_name) IS NOT NULL)
			SET @db_id = DB_ID(@db_name)

		SELECT DB_Name(DbId) AS DBName
			, Added
			, SUM(DeltaNumberReads) AS NumberReads
			, SUM(DeltaNumberWrites) AS NumberWrites
			, SUM(DeltaBytesRead) AS BytesRead
			, SUM(DeltaBytesWritten) AS BytesWritten
			, SUM(DeltaIOStallMS) AS IOStallMS
		FROM <Data_Table,sysname,FileStatsData>
		WHERE DbId NOT IN (SELECT db_id FROM <Exclude_Table,sysname,FileStatsExcludes>)
			AND (@start IS NULL OR Added BETWEEN @start AND @end)
			AND (@db_id IS NULL OR DbId = @db_id)
			AND DeltaNumberReads IS NOT NULL -- First entries
		GROUP BY DB_Name(DbId), Added
		ORDER BY Added DESC, DB_Name(DbId)
		
		RETURN
		END

	IF (@option = 'bydb') -- Returns grouped by db
		BEGIN
		IF (@start IS NOT NULL AND @end IS NULL)
			SET @end = GETDATE()

		IF (DB_ID(@db_name) IS NOT NULL)
			SET @db_id = DB_ID(@db_name)

		SELECT DB_Name(DbId) AS DBName
			, SUM(DeltaNumberReads) AS NumberReads
			, SUM(DeltaNumberWrites) AS NumberWrites
			, SUM(DeltaBytesRead) AS BytesRead
			, SUM(DeltaBytesWritten) AS BytesWritten
			, SUM(DeltaIOStallMS) AS IOStallMS
		FROM <Data_Table,sysname,FileStatsData>
		WHERE DbId NOT IN (SELECT db_id FROM <Exclude_Table,sysname,FileStatsExcludes>)
			AND (@start IS NULL OR Added BETWEEN @start AND @end)
			AND (@db_id IS NULL OR DbId = @db_id)
			AND DeltaNumberReads IS NOT NULL -- First entries
		GROUP BY DB_Name(DbId)
		ORDER BY DB_Name(DbId)
		
		RETURN
		END
	IF (@option = 'bystat') -- Returns for each stat in descending order
		BEGIN
		IF (@start IS NOT NULL AND @end IS NULL)
			SET @end = GETDATE()

		IF (DB_ID(@db_name) IS NOT NULL)
			SET @db_id = DB_ID(@db_name)

		SELECT SUM(DeltaNumberReads) AS NumberReads,
			DB_Name(DbId) AS DBName
		FROM <Data_Table,sysname,FileStatsData>
		WHERE DbId NOT IN (SELECT db_id FROM <Exclude_Table,sysname,FileStatsExcludes>)
			AND (@start IS NULL OR Added BETWEEN @start AND @end)
			AND (@db_id IS NULL OR DbId = @db_id)
		GROUP BY DB_Name(DbId)
		ORDER BY SUM(DeltaNumberReads) DESC

		SELECT SUM(DeltaNumberWrites) AS NumberWrites,
			DB_Name(DbId) AS DBName
		FROM <Data_Table,sysname,FileStatsData>
		WHERE DbId NOT IN (SELECT db_id FROM <Exclude_Table,sysname,FileStatsExcludes>)
			AND (@start IS NULL OR Added BETWEEN @start AND @end)
			AND (@db_id IS NULL OR DbId = @db_id)
		GROUP BY DB_Name(DbId)
		ORDER BY SUM(DeltaNumberWrites) DESC

		SELECT SUM(DeltaBytesRead) AS BytesRead,
			DB_Name(DbId) AS DBName
		FROM <Data_Table,sysname,FileStatsData>
		WHERE DbId NOT IN (SELECT db_id FROM <Exclude_Table,sysname,FileStatsExcludes>)
			AND (@start IS NULL OR Added BETWEEN @start AND @end)
			AND (@db_id IS NULL OR DbId = @db_id)
		GROUP BY DB_Name(DbId)
		ORDER BY SUM(DeltaBytesRead) DESC

		SELECT SUM(DeltaBytesWritten) AS BytesWritten,
			DB_Name(DbId) AS DBName
		FROM <Data_Table,sysname,FileStatsData>
		WHERE DbId NOT IN (SELECT db_id FROM <Exclude_Table,sysname,FileStatsExcludes>)
			AND (@start IS NULL OR Added BETWEEN @start AND @end)
			AND (@db_id IS NULL OR DbId = @db_id)
		GROUP BY DB_Name(DbId)
		ORDER BY SUM(DeltaBytesWritten) DESC


		SELECT SUM(DeltaIOStallMS) AS IOStallMS,
			DB_Name(DbId) AS DBName
		FROM <Data_Table,sysname,FileStatsData>
		WHERE DbId NOT IN (SELECT db_id FROM <Exclude_Table,sysname,FileStatsExcludes>)
			AND (@start IS NULL OR Added BETWEEN @start AND @end)
			AND (@db_id IS NULL OR DbId = @db_id)
		GROUP BY DB_Name(DbId)
		ORDER BY SUM(DeltaIOStallMS) DESC

		RETURN
		END
	IF (@option = 'log' OR @option = 'log_db') -- Logs all db's or specified db to stats tbl
		BEGIN
		IF (@option = 'log_db')	-- Log only specified db
			BEGIN
				IF (DB_NAME(@db_id) IS NULL AND DB_ID(@db_name) IS NULL)
					BEGIN
					PRINT 'Invalid @db_name or no @db_id specified'
					--RETURN
					END
				IF (@db_id IS NULL)
					SET @db_id = DB_ID(@db_name)

				IF (@db_id IS NULL)
					BEGIN
					PRINT 'Invalid @db_name or no @db_id specified'
					RETURN
					END
			END

		-- Last group of stats to calculate deltas
		SELECT DbId, FileId, MAX(Added) AS Added
		INTO #last_stat_key
		FROM <Data_Table,sysname,FileStatsData>
		WHERE (@db_id IS NULL OR @db_id = DbId)
		GROUP BY DbId, FileId

		INSERT INTO <Data_Table,sysname,FileStatsData>
		(DbId, FileId, TimeStamp, NumberReads, NumberWrites,
		 BytesRead, BytesWritten, IoStallMS, Added, DeltaTimeStamp,
		 DeltaNumberReads, DeltaNumberWrites, DeltaBytesRead,
		 DeltaBytesWritten, DeltaIoStallMS, DeltaAdded)
		SELECT cur_filestats.DbId, cur_filestats.FileId, cur_filestats.TimeStamp,
			cur_filestats.NumberReads, cur_filestats.NumberWrites, cur_filestats.BytesRead,
			cur_filestats.BytesWritten, cur_filestats.IoStallMS, GETDATE() AS Added,
			cur_filestats.Timestamp - last_filestat.Timestamp AS DeltaTimeStamp,
			cur_filestats.NumberReads - last_filestat.NumberReads AS DeltaNumberReads,
			cur_filestats.NumberWrites - last_filestat.NumberWrites AS DeltaNumberWrites,
			cur_filestats.BytesRead - last_filestat.BytesRead AS DeltaBytesRead,
			cur_filestats.BytesWritten - last_filestat.BytesWritten AS DeltaBytesWritten,
			cur_filestats.IoStallMS - last_filestat.IoStallMS AS DeltaIoStallMS,
			DATEDIFF(ss,last_filestat.Added,GETDATE()) AS DeltaAdded
		FROM :: fn_virtualfilestats(-1,-1) cur_filestats
			LEFT OUTER JOIN #last_stat_key ON
				cur_filestats.DbId = #last_stat_key.DbId
				AND
				cur_filestats.FileId = #last_stat_key.FileId
			LEFT OUTER JOIN <Data_Table,sysname,FileStatsData> last_filestat ON
				last_filestat.DbId = #last_stat_key.DbId
				AND
				last_filestat.FileId = #last_stat_key.FileId
				AND
				last_filestat.Added = #last_stat_key.Added
		WHERE (@db_id IS NULL OR @db_id = cur_filestats.DbID)


		RETURN
		END

	IF (@option = 'exclude') -- Add a db exclude
		BEGIN
		IF (@db_id IS NULL)
			SET @db_id = DB_ID(@db_name)

		IF (@db_id IS NOT NULL)
			BEGIN
			IF NOT EXISTS (SELECT * FROM <Exclude_Table,sysname,FileStatsExcludes>
					WHERE db_id = @db_id)
				INSERT INTO <Exclude_Table,sysname,FileStatsExcludes> VALUES (@db_id)

			PRINT DB_NAME(@db_id) + ' excluded'
			END
		ELSE -- Show current excludes
			SELECT db_id, DB_NAME(@db_id)
			FROM <Exclude_Table,sysname,FileStatsExcludes>
		RETURN
		END

	IF (@option = 'include') -- Removes a db exclude
		BEGIN
		IF (@db_id IS NULL)
			SET @db_id = DB_ID(@db_name)

		DELETE FROM <Exclude_Table,sysname,FileStatsExcludes>
		WHERE db_id = @db_id

		PRINT DB_NAME(@db_id) + ' exclusion removed'
		
		RETURN
		END
	IF (@option = 'delete')
		BEGIN
		IF (@start IS NULL)
			TRUNCATE TABLE <Data_Table,sysname,FileStatsData>
		ELSE
			DELETE FROM <Data_Table,sysname,FileStatsData>
			WHERE Added <= @start
		PRINT 'Stats reset'
		RETURN
		END
	IF (@option = 'create_tables')	-- Verifies tables exist
		BEGIN
		IF NOT EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES
				WHERE TABLE_NAME = '<Data_Table,sysname,FileStatsData>')
			BEGIN
			CREATE TABLE <Data_Table,sysname,FileStatsData>(
				DbId smallint,
				FileId smallint,
				TimeStamp int,
				NumberReads bigint,
				NumberWrites bigint,
				BytesRead bigint,
				BytesWritten bigint,
				IoStallMS bigint,
				Added datetime,
				DeltaTimeStamp int NULL,
				DeltaNumberReads bigint NULL,
				DeltaNumberWrites bigint NULL,
				DeltaBytesRead bigint NULL,
				DeltaBytesWritten bigint NULL,
				DeltaIoStallMS bigint,
				DeltaAdded bigint,
				PRIMARY KEY (DbId, FileId, Added))
			PRINT 'Created <Data_Table,sysname,FileStatsData> table'
			END

		IF NOT EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES
				WHERE TABLE_NAME = '<Exclude_Table,sysname,FileStatsExcludes>')
			BEGIN
			CREATE TABLE <Exclude_Table,sysname,FileStatsExcludes>(
				db_id smallint
			)
			PRINT 'Created <Exclude_Table,sysname,FileStatsExcludes> table'
			END
		RETURN
		END
GO
AttachmentSize
Filestats.sql11.74 KB