O Que Ninguém Te Contou Sobre o SHRINK no SQL Server

O comando SHRINK é um dos mais mal compreendidos do SQL Server. Muitos profissionais o utilizam para reduzir arquivos de banco de dados, mas sem compreender os efeitos colaterais que ele pode causar — como fragmentação severa e queda de desempenho. Neste post, você vai entender, com exemplos práticos, o que o comando realmente faz, quando utilizá-lo (com muito cuidado!) e por que o SHRINK não deve fazer parte de nenhuma rotina de manutenção automática.

O que é o SHRINK?

O SHRINK é a única operação no SQL Server capaz de reduzir o tamanho físico de um arquivo de banco de dados ou de log. Ele pode ser feito de duas formas:

  • DBCC SHRINKDATABASE: reduz todos os arquivos (dados e log) do banco.

  • DBCC SHRINKFILE: reduz um arquivo específico.

Ao contrário do que muitos pensam, o SHRINK não comprime dados. Ele apenas move páginas que estão no final do arquivo para áreas livres no início, liberando espaço no final e, assim, reduzindo o tamanho do arquivo.

Veja no Script abaixo como utilizar o comando DBCC SHRINKDATABASE.

use master
go

/*****************************************
 Cria Banco e Tabela com volume de dados
******************************************/
DROP DATABASE IF exists HansOnDB
go
CREATE DATABASE HansOnDB
go

use HansOnDB
go

DROP TABLE IF exists dbo.Vendas
go
CREATE TABLE dbo.Vendas (
SalesOrderID int NOT NULL,
SalesOrderDetailID int NOT NULL,
CarrierTrackingNumber nvarchar(25) NULL,
OrderQty smallint NOT NULL,
ProductID int NOT NULL,
SpecialOfferID int NOT NULL,
UnitPrice money NOT NULL,
UnitPriceDiscount money NOT NULL,
LineTotal  AS (isnull((UnitPrice*((1.0)-UnitPriceDiscount))*OrderQty,(0.0))),
rowguid uniqueidentifier ROWGUIDCOL  NOT NULL,
ModifiedDate datetime NOT NULL)
go

INSERT dbo.Vendas
(SalesOrderID, SalesOrderDetailID, CarrierTrackingNumber, OrderQty, ProductID, SpecialOfferID, UnitPrice, UnitPriceDiscount, rowguid, ModifiedDate)
SELECT SalesOrderID, SalesOrderDetailID, CarrierTrackingNumber, OrderQty, ProductID, SpecialOfferID, UnitPrice, UnitPriceDiscount, rowguid, ModifiedDate 
FROM AdventureWorks.Sales.SalesOrderDetail
go 10
/********************* Fim Prepara ************************/

SELECT count(*) FROM dbo.Vendas
-- 1213170

-- Exclui metade das linhas
DELETE FROM dbo.Vendas
WHERE SalesOrderDetailID % 2 = 0

SELECT
--fg.name AS [Filegroup Name],
mf.name AS [Logical File Name],
mf.physical_name AS [Physical File Name],
CONVERT(decimal(10,2), mf.size / 128.0) AS [Space Reserved (MB)],
CONVERT(decimal(10,2), FILEPROPERTY(mf.name, 'SpaceUsed') / 128.0) AS [Space Used (MB)]
FROM sys.master_files mf
LEFT JOIN sys.filegroups fg ON mf.data_space_id = fg.data_space_id
WHERE mf.database_id = DB_ID()
/*
Logical File Name    Physical File Name                Space Reserved (MB)    Space Used (MB)
HansOnDB            C:\MSSQL_Data\HansOnDB.mdf        136.00                100.00
HansOnDB_log        C:\MSSQL_Data\HansOnDB_log.ldf    392.00                100.77
*/

DBCC SHRINKDATABASE (HansOnDB)
GO


/*****************8
 Exclui Banco
*******************/
use master
go

ALTER DATABASE HansOnDB SET  SINGLE_USER WITH ROLLBACK IMMEDIATE
go
DROP DATABASE HansOnDB
go

O Problema: Fragmentação

Durante o processo de movimentação de páginas, o SQL Server não respeita a ordem lógica dos dados. Isso gera fragmentação interna nos índices, o que afeta diretamente o desempenho de queries, principalmente aquelas que usam buscas por intervalo.

Veja um exemplo prático.

/*****************************************
 Cria Banco e Tabela com volume de dados
******************************************/
DROP DATABASE IF exists HansOnDB
go
CREATE DATABASE HansOnDB
go
ALTER DATABASE HansOnDB SET RECOVERY simple
go

use HansOnDB
go

/******************* Prepara ambiente **********************/
-- Vendas
DROP TABLE IF exists dbo.Vendas
go
CREATE TABLE dbo.Vendas (
SalesOrderID int identity NOT NULL CONSTRAINT pk_Vendas PRIMARY KEY,
RevisionNumber tinyint NOT NULL,
OrderDate datetime NOT NULL,
DueDate datetime NOT NULL,
ShipDate datetime NULL,
Status tinyint NOT NULL,
SalesOrderNumber  AS (isnull(N'SO'+CONVERT(nvarchar(23),SalesOrderID),N'*** ERROR ***')),
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(1000) NULL,
CurrencyRateID int NULL,
SubTotal money NOT NULL,
TaxAmt money NOT NULL,
Freight money NOT NULL,
TotalDue  AS (isnull((SubTotal+TaxAmt)+Freight,(0))),
Comment nvarchar(128) NULL,
rowguid uniqueidentifier ROWGUIDCOL  NOT NULL,
ModifiedDate datetime NOT NULL)
go

-- Vendas_Itens
DROP TABLE IF exists dbo.Vendas_Itens
go
CREATE TABLE dbo.Vendas_Itens (
Vendas_ID int not null identity CONSTRAINT pk_Vendas_Itens PRIMARY KEY,
SalesOrderID int NOT NULL,
SalesOrderDetailID int NOT NULL,
CarrierTrackingNumber varchar(1000) NULL,
OrderQty smallint NOT NULL,
ProductID int NOT NULL,
SpecialOfferID int NOT NULL,
UnitPrice money NOT NULL,
UnitPriceDiscount money NOT NULL,
LineTotal  AS (isnull((UnitPrice*((1.0)-UnitPriceDiscount))*OrderQty,(0.0))),
rowguid uniqueidentifier ROWGUIDCOL  NOT NULL,
ModifiedDate datetime NOT NULL)
go

-- Insert
INSERT dbo.Vendas
(RevisionNumber, OrderDate, DueDate, ShipDate, [Status], CustomerID, SalesPersonID, TerritoryID, BillToAddressID, ShipToAddressID, ShipMethodID, CreditCardID, CreditCardApprovalCode, CurrencyRateID, SubTotal, TaxAmt, Freight, Comment, rowguid, ModifiedDate)
SELECT RevisionNumber, OrderDate, DueDate, ShipDate, [Status], CustomerID, SalesPersonID, TerritoryID, BillToAddressID, ShipToAddressID, ShipMethodID, CreditCardID, CreditCardApprovalCode + replicate('A',500), CurrencyRateID, SubTotal, TaxAmt, Freight, Comment, rowguid, ModifiedDate
FROM AdventureWorks.Sales.SalesOrderHeader

INSERT dbo.Vendas_Itens
(SalesOrderID, SalesOrderDetailID, CarrierTrackingNumber, OrderQty, ProductID, SpecialOfferID, UnitPrice, UnitPriceDiscount, rowguid, ModifiedDate)
SELECT SalesOrderID, SalesOrderDetailID, CarrierTrackingNumber + replicate('A',500), OrderQty, ProductID, SpecialOfferID, UnitPrice, UnitPriceDiscount, rowguid, ModifiedDate 
FROM AdventureWorks.Sales.SalesOrderDetail
go 60
/********************* Fim Prepara ************************/


SELECT count(*) FROM dbo.Vendas -- 1.887.900
SELECT count(*) FROM dbo.Vendas_Itens -- 7.279.020

-- Quantidade de Linhas e Data Pages
SELECT o.name as Tabela, rows as QtdLinhas, 
data_pages as Paginas8k,
(data_pages * 8) / 1024 as Tamanho_MB 
FROM sys.partitions p 
JOIN sys.allocation_units a ON p.hobt_id = a.container_id
JOIN sys.objects o ON o.object_id = p.object_id
WHERE o.name in ('Vendas','Vendas_Itens') 
and index_id < 2
/*
Tabela            QtdLinhas    Paginas8k    Tamanho_MB
Vendas            1887900        151681        1185
Vendas_Itens    7279020        313471        2448
*/

Para analisar se existe fragmentação utilizamos a função de sistema sys.dm_db_index_physical_stats(), possui 5 parâmetros:

  • database_id - ID do Banco de Dados, NULL retorna todos os bancos, para escolher um banco utilizar: db_id('').

  • object_id - ID do Objeto -> NULL retorna todos os objetos, para escolher um objeto utilizar: object_id('<banco.schema.tabela>').

  • index_id - ID do Índice -> se utilizar NULL retorna todos os índices. Para identificar o ID do índice conssultar sys.indexes.

  • partition_number - Número da Partição, NULL retorna todas as partições.

  • mode - Nível de análise, opções: Limited (padrão), Sampled e Detailed.

A consulta abaixo retorna o percentual de fragmentação das duas tabelas criadas e populadas no exemplo anterior.

SELECT
OBJECT_NAME(ips.object_id) AS Tabela,
i.name AS Indice,
index_level as Nivel_Indice,
ips.index_type_desc,
ips.avg_fragmentation_in_percent
FROM sys.dm_db_index_physical_stats (DB_ID(), OBJECT_ID('dbo.Vendas'), NULL, NULL, 'DETAILED') AS ips
JOIN sys.indexes AS i   ON ips.object_id = i.object_id AND ips.index_id = i.index_id
WHERE index_level = 0

UNION ALL

SELECT
OBJECT_NAME(ips.object_id) AS Tabela,
i.name AS Indice,
index_level as Nivel_Indice,
ips.index_type_desc,
ips.avg_fragmentation_in_percent
FROM sys.dm_db_index_physical_stats (DB_ID(), OBJECT_ID('dbo.Vendas_Itens'), NULL, NULL, 'DETAILED') AS ips
JOIN sys.indexes AS i   ON ips.object_id = i.object_id AND ips.index_id = i.index_id
WHERE index_level = 0

Como as tabelas acabaram de ser criadas, não existe fragmentação.

Agora vamos simular atividade com INSERT, UPDATE e DELETE nas duas tabelas, provocando fragmentação.

/*******************************
 Atividade gera fragmentação
********************************/
UPDATE dbo.Vendas SET CreditCardApprovalCode = CreditCardApprovalCode + replicate('A',100)
WHERE SalesOrderID >= 1000000 and SalesOrderID < 2000000

UPDATE dbo.Vendas_Itens SET CarrierTrackingNumber = CarrierTrackingNumber + replicate('A',100)
WHERE Vendas_ID >= 3000000 and Vendas_ID < 4000000
go

DELETE top(100000) FROM dbo.Vendas
WHERE 1=1
--and SalesOrderID % 2 = 0 
and SalesOrderID >= 0 and SalesOrderID < 500000

DELETE top(200000) FROM dbo.Vendas_Itens
WHERE 1=1
--and Vendas_ID % 2 = 0
and Vendas_ID >= 1000000 and Vendas_ID < 2000000

INSERT dbo.Vendas
(RevisionNumber, OrderDate, DueDate, ShipDate, [Status], CustomerID, SalesPersonID, TerritoryID, BillToAddressID, ShipToAddressID, ShipMethodID, CreditCardID, CreditCardApprovalCode, CurrencyRateID, SubTotal, TaxAmt, Freight, Comment, rowguid, ModifiedDate)
SELECT RevisionNumber, OrderDate, DueDate, ShipDate, [Status], CustomerID, SalesPersonID, TerritoryID, BillToAddressID, ShipToAddressID, ShipMethodID, CreditCardID, CreditCardApprovalCode, CurrencyRateID, SubTotal, TaxAmt, Freight, Comment, rowguid, ModifiedDate
FROM AdventureWorks.Sales.SalesOrderHeader

INSERT dbo.Vendas_Itens
(SalesOrderID, SalesOrderDetailID, CarrierTrackingNumber, OrderQty, ProductID, SpecialOfferID, UnitPrice, UnitPriceDiscount, rowguid, ModifiedDate)
SELECT SalesOrderID, SalesOrderDetailID, CarrierTrackingNumber, OrderQty, ProductID, SpecialOfferID, UnitPrice, UnitPriceDiscount, rowguid, ModifiedDate 
FROM AdventureWorks.Sales.SalesOrderDetail
go 5

Agora execute novamente a consulta que analisa a fragmentação, você vai ver valores altos para ambas as tabelas.

Agora vamos executar o SHRINKFILE, você vai ver a fragmentação aumentar!

DBCC SHRINKFILE (HansOnDB,1)
TabelaFragmentação Antes do SHRINKFragmentação Após o SHRINK
Vendas76,35%77,21%
Vendas_Itens24,29%25,39%

O SHRINK não resolve a fragmentação, na verdade piora o cenário!

Como Corrigir a Fragmentação?

Após o SHRINK, o correto é reconstruir os índices para restaurar a organização das páginas:

ALTER INDEX pk_Vendas ON dbo.Vendas REBUILD
ALTER INDEX pk_Vendas_Itens ON dbo.Vendas_Itens REBUILD

E o Arquivo de Log?

A operação de SHRINK em arquivos de log é ainda mais delicada. Diferente do arquivo de dados, o log é dividido em VLFs (Virtual Log Files) e não permite o mesmo tipo de movimentação interna. Isso significa que o SHRINK só funcionará se a porção ativa do log estiver no início do arquivo.

Vamos ver este comportamento na prática, execute o script abaixo.

/**************************
 Cria Banco HandsOn
***************************/
DROP DATABASE IF exists HandsOn
go
CREATE DATABASE HandsOn 

-- Altera Recovery Model
ALTER DATABASE HandsOn SET RECOVERY FULL
go

use HandsOn
go

--  Cria tabela 
DROP TABLE IF exists HandsOn.dbo.Cliente
go
CREATE TABLE HandsOn.dbo.Cliente ( 
Cliente_ID int not null identity CONSTRAINT pk_Cliente PRIMARY KEY,
Nome char(1200) not null,
Renda bigint null)
go

-- Backup FULL
BACKUP DATABASE HandsOn TO DISK = 'C:\_LIVE\Backup\HandsOn_Full.bak' WITH format, compression

Para visualizar os Logs Virtuais execute a consulta abaixo.

SELECT * FROM sys.dm_db_log_info(db_id('HandsOn'))

Os Logs Virtuais em uso aparecem com valor 2 em “vlf_status”. Com a instrução abaixo você obtém o tamanho atual do arquivo de Log no disco.

SELECT db.[name] as Banco, mf.[name] Arquivo, (mf.size * 8) / 1024 as Tamanho_MB
FROM sys.master_files mf
JOIN sys.databases db ON mf.database_id = db.database_id
WHERE mf.[type] = 1 and db.[name] = 'HandsOn'

Agora vamos gerar atividade provocando o crescimento do arquivo de Log.

set nocount on

INSERT HandsOn.dbo.Cliente (Nome,Renda) VALUES
('Bla Bla Bla...',12345),
('Bla Bla Bla...',12345),
('Bla Bla Bla...',12345),
('Bla Bla Bla...',12345),
('Bla Bla Bla...',12345)
go 50000

Executando novamente o comando que retorna os Logs Virtuais, você vai ver um aumento na sua quantidade.

SELECT * FROM sys.dm_db_log_info(db_id('HandsOn'))

Neste momento o arquivo de Log está lotado de informações, agora vamos executar um Backup do Log para “limpar” internamente o arquivo.

BACKUP LOG HandsOn TO DISK = 'C:\_LIVE\Backup\HandsOn_01.trn' WITH NOINIT, COMPRESSION

Analisando novamente os Logs Virtuais, repare que o último está com “vlf_status” 2, o restante vazio. O problema é que o SHRINK não move este Log Virtual para o início do arquivo, por este motivo não consegue reduzir o arquivo. Executando o SHRINK não vai reduzir nada!

DBCC SHRINKFILE ('HandsOn_log',20)

Analise os Logs Virtuais novamente, repare que o SHRINK já preparou o terreno para a redução posterior, gerando atividade no banco e levando a porção ativa para o início do arquivo. Agora basta você executar um Backup do Log seguido por outro SHRINK que você vai conseguir reduzir o arquivo!

BACKUP LOG HandsOn TO DISK = 'C:\_LIVE\Backup\HandsOn_02.trn' WITH NOINIT, COMPRESSION
DBCC SHRINKFILE ('HandsOn_log',20)

Quando Usar SHRINK?

O SHRINK não deve ser utilizado regularmente, muito menos de forma automática. Use-o apenas em cenários pontuais como:

  • Exclusão em massa de dados históricos.

  • Liberação emergencial de espaço em disco.

  • Migração de dados para outro banco.


Fui, mas volto com mais SQL Server em breve!

✍️ Sobre o autor
O Prof. Landry é especialista em Microsoft SQL Server desde 1999, Microsoft Trainer, Professor Universitário e criador do canal SQL Server Expert no YouTube, com conteúdo técnico semanal para DBAs e profissionais de dados.

🚀 Quer aprender mais sobre SQL Server?
👉 Me acompanhe no LinkedIn e inscreva-se no canal para não perder nenhuma dica prática!

1
Subscribe to my newsletter

Read articles from SQL Server Expert directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

SQL Server Expert
SQL Server Expert

O Prof. Landry Duailibe é especialista em Microsoft SQL Server desde 1999, Microsoft Certified Trainer, professor universitário e criador do canal SQL Server Expert no YouTube, onde compartilha conteúdo técnico semanal para DBAs e profissionais de dados. Já ajudou milhares de alunos a evoluírem suas habilidades com SQL Server e conquistarem melhores oportunidades na área de dados.