Overview
If you want the really short story, then check out this analogy - hopefully you come back here and read the full story. Introduced in SQL Server 7.0 was the ability automatically grow and to shrink the physical size of database data and transaction log files. Auto grow of files doesn't happen before the file is full, it happens when new space is needed (like when an insert is performed), so the user will wait the time it takes to grow until the modification is completed.
Auto grow and shrink can be very useful under special circumstances, for example after archiving data to some other location. However, we often see DBA's doing shrink on a regular basis and the purpose of this article is to explain some of the downsides of shrink and what actually happens when you shrink a database file. Also, it is worth noticing that the auto grow functionality was mainly added so the dba wouldn't be paged in the middle of the night because the database is full. It was never intended to be a high performance feature or to replace the need to manage space usage, especially at the high end
Acknowledgements
I like to thank Mark Allison and Kalen Delaney who provided valuable suggestions and input for this article.
More information
You can shrink a database file using either DBCC SHRINKDATABASE (which targets all files for the database) or DBCC SHRINKFILE (which targets a particular database file). I prefer SHRINKFILE. I will not go through the details of the commands here; they are documented in SQL Server Books Online. Let us first determine what actually happens when you shrink a database file:
Shrinking of data file
When you shrink a data file, SQL Server will first move pages towards the beginning of the file. This frees up space at the end of the file and the file can then be shrunk (or as I like to view it: "cut off at the end").
Management of transaction log file
This section is only to serve as a brief introduction to the topic of transaction log file management. See below for reference.
Each modification performed in the database is reflected in the transaction log file. Needless to say, these log records need to be removed (or actually: overwritten) sooner or later - or else we would end up with a huge log file, or a full log file.
One way is to set the database to simple recovery model. This mean that SQL Server will automatically mark inactive log records as "OK to overwrite" (a.k.a. "truncate the log" or as I like to call it: "empty the log").
Another option is to have the database in full or bulk_logged recovery model and perform regular transaction log backups (BACKUP LOG). The transaction log is emptied when you backup the log. Note that the log is *not* emptied for other backup types (like BACKUP DATABASE).
See the SQL Server Books Online reference http://msdn.microsoft.com/en-us/library/ms345583.aspx for details. Make sure you read all the subsections. It is worth some 30 minutes of your time to understand how to manage the transaction log. Trust me on this.
Shrinking of transaction log file
SQL Server cannot move log records from the end of the log file toward the beginning of the log file. This means that SQL Server can only cut down the file size if the file is empty at the end of the file. The end-most log record sets the limit of how much the transaction log can be shrunk. A transaction log file is shrunk in units of Virtual Log Files (VLF). You can see the VLF layout using the undocumented DBCC LOGINFO command, which returns one row per virtual log file for the database: DBCC LOGINFO('myDatabase')
FileId | FileSize | StartOffset | FSeqNo | Status | Parity | CreateLSN |
2 | 253952 | 8192 | 11 | 0 | 128 | 0 |
2 | 253952 | 262144 | 13 | 0 | 128 | 0 |
2 | 270336 | 516096 | 12 | 0 | 128 | 7000000025000288 |
2 | 262144 | 786432 | 14 | 2 | 128 | 9000000008400246 |
The interesting column is "Status". 0 means that the VLF is not in use and 2 means that it is in use. In my example, I have 2 at the end of the file (read result from top to bottom) and this means that the file cannot currently be shrunk. How to handle this depends on your recovery model. You should adjust and replace options and database appropriately for below code. If you are uncertain, then check the command in the product documentation:USE dbname
CHECKPOINT
--First param below is fileno for log file, often 2. Check with sys.database_files
--Second is desired size, in MB.
DBCC SHRINKFILE(2, 500)
DBCC SQLPERF(LOGSPACE) --Optional
DBCC LOGINFO --Optional
USE dbname
BACKUP LOG dbname TO DISK = 'C:\x\dbname.trn'
--First param below is fileno for log file, often 2. Check with sys.database_files
--Second is desired size, in MB.
DBCC SHRINKFILE(2, 500)
DBCC SQLPERF(LOGSPACE) --Optional
DBCC LOGINFO --Optional
--Script to show that shrink produces a lot of log record.
SET NOCOUNT ON
USE master
IF DB_ID('shrink_test') IS NOT NULL DROP DATABASE shrink_test
GO
CREATE DATABASE shrink_test
ON PRIMARY
(NAME = shrink_test_data, FILENAME = N'c:\shrink_test_data.mdf'
,SIZE = 5MB, MAXSIZE = 200MB, FILEGROWTH = 10%)
LOG ON
(NAME = shrink_test_log, FILENAME = N'c:\shrink_test_log.ldf'
,SIZE = 3MB, MAXSIZE = 200MB, FILEGROWTH = 10%)
GO
ALTER DATABASE shrink_test SET RECOVERY FULL
--Make sure the database isn't in auto-truncate mode for the log
BACKUP DATABASE shrink_test TO DISK = 'NUL'
USE shrink_test
CREATE TABLE t
(
c1 INT IDENTITY CONSTRAINT PK_shrink_test PRIMARY KEY NONCLUSTERED
,c2 CHAR(3000) DEFAULT 'hello'
)
DECLARE @i INT
SET @i = 1
WHILE @i <= 40000
BEGIN
INSERT INTO t DEFAULT VALUES
IF @i%500 = 0 --Truncate log on every 500'th row
BACKUP LOG shrink_test TO DISK = 'NUL'
SET @i = @i + 1
END
SELECT COUNT(c1) AS "Number of rows before delete, should be 40000"
FROM t
GO
--Delete some rows, in a loop so log doesn't grow!
DECLARE @i INT
SET @i = 1
WHILE @i <= 20000
BEGIN
DELETE FROM t WHERE c1 = @i
IF @i%500 = 0 --Truncate log on every 500'th row
BACKUP LOG shrink_test TO DISK = 'NUL'
SET @i = @i + 1
END
SELECT COUNT(c1) AS "Number of rows after delete, shuld be 20000"
FROM t
--Empty the log
BACKUP LOG shrink_test TO DISK = 'NUL'
--Database file should be a bit larger (160MB on my machine), logfile smaller (3MB)
SELECT name, size*8192/(1024*1024) AS sizeInMB FROM sysfiles
GO
--This shrink might now produce a lot of log record as 20000 rows will be moved!
CHECKPOINT
DBCC SHRINKFILE (shrink_test_data, 40)
--Database file should now be small, but logfile large
SELECT name, size*8192/(1024*1024) AS sizeInMB FROM shrink_test..sysfiles
--My result, the data file shrunk to 80MB and the log file grew from 3MB to 146MB!!!
USE master
GO
DROP DATABASE shrink_test