There could be a beast lurking in your log files, robbing you of performance, and you might not even know its name. But fear not - I'll give you a weapon to kill the beast.
If you've never heard of "virtual log files" (VLFs), read these articles: they'll well-written, short, and extremely informative. You owe it to yourself to read the originals, but I'll sum them up briefly here.
VLFs: A Quick Overview
What we think of as a log file is actually a bunch of interconnected of disk space chunks called VLFs. A log file can consist of a few, or hundreds, or even thousands of VLFs, and a new one is created every time the log file needs to expand. In a perfect world, you could create your log file to be the biggest it would ever need to be; since the log file would never need to expand, there would be no new VLFs created. Problem solved.
Unfortunately, if you're reading this, you don't live in a perfect world. Very likely, you're responsible for databases that have too many VLFs, and having too many VLFs is bad, especially for performance. So, your first step is to check your databases' log files to see how many VLFs they have. Then, if they have too many VLFs, you can fix the problem with a few simple commands. Fortunately for you, I've created a stored procedure that does both steps, so you can spend your time on more interesting things.
(How many VLFs is "too many"? It depends. In this blog, our bias is toward providing tools to get things done; I leave theoretical questions to people far more qualified. I picked "50" because Kimberly said so, and that's good enough for me.)
The Weapon
The script below will create a stored procedure that will examine and, if necessary, fix, the VLF problem in all the databases in your instance. Usually, you'll only need to run this once in a very long while, but again, it depends. Note the "@ExecuteFix" argument - setting this flag to zero allows you to see what T-SQL code would be executed; setting it to one actually executes that T-SQL.
-- Drop if it already exists. IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[dba_FixExcessiveLogFileVlfs]') AND type in (N'P', N'PC')) DROP PROCEDURE [dbo].[dba_FixExcessiveLogFileVlfs] GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO ------------------------------------------------------------------------------- -- Display, logs, and fixes all databases with more than 50 Virtual Log Files. -- -- See: http://www.simple-talk.com/sql/database-administration/sql-server-transaction-log-fragmentation-a-primer/ -- See: http://sqlskills.com/blogs/kimberly/post/8-steps-to-better-transaction-log-throughput.aspx -- See: http://sqlblog.com/blogs/linchi_shea/archive/2009/02/09/performance-impact-a-large-number-of-virtual-log-files-part-i.aspx -- -- Created 2012-04-28 - Larry Leonard - http://SqlSoundings.blogspot.com/ ------------------------------------------------------------------------------- CREATE PROCEDURE [dbo].[dba_FixExcessiveLogFileVlfs] @ExecuteFix INT = 0 AS BEGIN SET NOCOUNT ON -- Set up temp tables. IF OBJECT_ID('tempdb..#stage') IS NOT NULL DROP TABLE #stage CREATE TABLE #stage ( FileID INT , FileSizeBytes BIGINT , StartOffset BIGINT , FSeqNo BIGINT , [Status] BIGINT , Parity BIGINT , CreateLSN NUMERIC(38) ) IF OBJECT_ID('tempdb..#results') IS NOT NULL DROP TABLE #results CREATE TABLE #results ( DatabaseName sysname , LogFileName sysname , VlfCount INT , LogFileSizeMB INT ) -- Gather the log file information into the temp tables. DECLARE @Sql NVARCHAR(MAX) SET @Sql = 'USE [?] ' + '' + 'INSERT INTO #stage ' + ' EXEC sp_executesql N''DBCC LOGINFO ([?])'' ' + '' + 'INSERT INTO #results ' + ' SELECT DB_NAME(), MIN(FILE_NAME(FileID)), COUNT(*), SUM(FileSizeBytes) / 1024 / 1024 ' + ' FROM #stage ' + ' ' + 'TRUNCATE TABLE #stage ' EXEC sp_msforeachdb @Sql -- Log the results. DECLARE @DatabaseName sysname DECLARE @LogFileName sysname DECLARE @VlfCount INT DECLARE @LogFileSizeMB INT DECLARE cur CURSOR LOCAL FOR SELECT DatabaseName , VlfCount , LogFileSizeMB FROM #results ORDER BY VlfCount DESC OPEN cur FETCH NEXT FROM cur INTO @DatabaseName, @VlfCount, @LogFileSizeMB WHILE @@FETCH_STATUS = 0 BEGIN RAISERROR('Database: %25s - Virtual Log Files: %4d - Size: %5d MB', 10, 1, @DatabaseName, @VlfCount, @LogFileSizeMB) WITH NOWAIT, LOG FETCH NEXT FROM cur INTO @DatabaseName, @VlfCount, @LogFileSizeMB END CLOSE cur DEALLOCATE cur -- Display the results. RAISERROR(' ', 10, 1) WITH NOWAIT SELECT * FROM #results ORDER BY VlfCount DESC -- Fix the log files with too many VLFs. We add two MB to the size because -- ALTER DATABASE requires that we make the log larger. Adding one doesn't -- work, because of rounding issues when dividing, I think. DECLARE cur CURSOR LOCAL FOR SELECT DatabaseName , LogFileName , VlfCount , LogFileSizeMB FROM #results WHERE VlfCount > 50 ORDER BY VlfCount DESC OPEN cur FETCH NEXT FROM cur INTO @DatabaseName, @LogFileName, @VlfCount, @LogFileSizeMB WHILE @@FETCH_STATUS = 0 BEGIN SET @Sql = 'USE ' + @DatabaseName + CHAR(10) + ' ' + CHAR(10) + 'CHECKPOINT ' + CHAR(10) + ' ' + CHAR(10) + 'DBCC SHRINKFILE (' + @LogFileName + ', 0, TRUNCATEONLY) ' + CHAR(10) + ' ' + CHAR(10) + 'ALTER DATABASE ' + @DatabaseName + CHAR(10) + ' MODIFY FILE ' + CHAR(10) + '( ' + CHAR(10) + ' NAME = ' + @LogFileName + CHAR(10) + ' , SIZE = ' + CAST(@LogFileSizeMB + 2 AS NVARCHAR) + ' MB ' + CHAR(10) + ') ' + CHAR(10) IF @ExecuteFix = 0 BEGIN RAISERROR('-- Proposed T-SQL code for database %20s: Log File: %10s', 10, 1, @DatabaseName, @LogFilename) WITH NOWAIT, LOG RAISERROR('%s', 10, 1, @Sql) WITH NOWAIT, LOG END ELSE BEGIN RAISERROR('Processing database %20s: Log File: %10s', 10, 1, @DatabaseName, @LogFilename) WITH NOWAIT, LOG EXEC sp_executesql @Sql END FETCH NEXT FROM cur INTO @DatabaseName, @LogFileName, @VlfCount, @LogFileSizeMB END CLOSE cur DEALLOCATE cur -- Done. IF OBJECT_ID('tempdb..#stage') IS NOT NULL DROP TABLE #stage IF OBJECT_ID('tempdb..#results') IS NOT NULL DROP TABLE #results END /* Testbed. EXEC dbo.dba_FixExcessiveLogFileVlfs @ExecuteFix = 0 */ GO
7 comments:
had to add recoveryunitld int to the beginning of #stage for SS 2012
Jim
Thanks so much for posting, very useful for me.
My environment has database names with underscores and decimals. Use brackets to fix in the string passed to 'sp_msforeachdb':
SET @Sql = 'USE [?] ' -- here
+ ''
+ 'INSERT INTO #stage '
+ ' EXEC sp_executesql N''DBCC LOGINFO ([?])'' ' -- here
+ ''
+ 'INSERT INTO #results '
+ ' SELECT DB_NAME(), MIN(FILE_NAME(FileID)), COUNT(*), SUM(FileSizeBytes) / 1024 / 1024 '
+ ' FROM #stage '
+ ' '
+ 'TRUNCATE TABLE #stage '
My databases have two log files. The current script only shrinks the first log file.
I have modified the line to add a GROUP BY FILEID and that way both of my log files are corrected.
+ ' SELECT DB_NAME(), MIN(FILE_NAME(FileID)), COUNT(*), SUM(FileSizeBytes) / 1024 / 1024 '
+ ' FROM #stage GROUP BY FILEID '
Be Careful about using this in production databases in FULL recovery mode. You will be trashing your LOG backup chain. In order to keep the LOG chain, run this to capture the script for growing the log file but use Andy Novak's procedure instead to shrink the log, by backing it up repeatedly. That way you preserve the log chain for restore purposes.
http://www.mssqltips.com/sqlservertip/2097/how-to-shrink-the-transaction-log-file-in-sql-server-2008/
Tim's comment about harming the log backup chain I believe refers to issuing a "BACKUP LOG with TRUNCATE_ONLY" or "BACKUP LOG with NO_LOG", which I don't see in the script on this page. That said, the article by Andy Novak is more likely to actually shrink the log for an active database since it incorporates a "Backup Log" step as well as Checkpoint commands, in a loop. Also, remember that simply getting to some magical ~number~ of VLFs is not an ideal goal. It is possible to have too few _OR_ too many VLFs, as kt addresses in a later blog post: http://www.sqlskills.com/blogs/kimberly/transaction-log-vlfs-too-many-or-too-few/
Hi Tim, I want to add square brackets around the Database Name, where I can this in the code to do this?
Post a Comment