Por que uma operação de Delete em tabela pequena está demorando no SQL Server?

Fala pessoal, tudo bem?

Depois que escrevi sobre a diferença de FK e índice nesse post aqui tive algumas outras ideias para exemplificar sobre outros problemas que vemos no dia a dia.

Para resumir, temos uma tabela de 10 linhas e ao tentar apagar um registro estava demorando uma eternidade. O que podemos avaliar inicialmente caso não ocorra um erro logo de cara.

  • Ocorre algum bloqueio ao realizar a operação?
  • Existe trigger na tabela?
  • Existe relacionamentos na tabela?
  • Se existir relacionamentos, as colunas relacionais estão devidamente indexadas?

Para simular esse cenário executei o script:

-- Cria tabelas
CREATE table categoria (id int identity(1,1) PRIMARY key, descricao varchar(1000))

Create table produto (id int identity(1,1) PRIMARY key, descricao varchar(1000), id_categoria int)

-- Cria relacionamento
ALTER TABLE produto
ADD FOREIGN KEY (id_categoria) REFERENCES categoria (id);
GO

-- insere dados na tabela categoria
INSERT INTO dbo.categoria
(
    descricao
)
VALUES
('brinquedo' ),
('comida' ),
('automóvel' ),
('pc' ),
('calçado' ),
('Roupas' )
GO
-- SELECT * FROM categoria

-- insere dados na tabela produto
INSERT INTO dbo.produto
(
    descricao,
    id_categoria
)
select top 120000 c.name , 1 from sys.tables t cross join syscolumns c
GO

INSERT INTO dbo.produto
(
    descricao,
    id_categoria
)
select top 230000 c.name , 3 from sys.tables t cross join syscolumns c
GO

INSERT INTO dbo.produto
(
    descricao,
    id_categoria
)
select top 230000 c.name , 3 from sys.tables t cross join syscolumns c
GO

INSERT INTO dbo.produto
(
    descricao,
    id_categoria
)
select top 230000 c.name , 3 from sys.tables t cross join syscolumns c
GO

INSERT INTO dbo.produto
(
    descricao,
    id_categoria
)
select top 430000 c.name , 4 from sys.tables t cross join syscolumns c
GO

INSERT INTO dbo.produto
(
    descricao,
    id_categoria
)
select top 400000 c.name , 4 from sys.tables t cross join syscolumns c
GO

INSERT INTO dbo.produto
(
    descricao,
    id_categoria
)
select top 400000 c.name , 4 from sys.tables t cross join syscolumns c
GO 10

INSERT INTO dbo.produto
(
    descricao,
    id_categoria
)
select top 120000 c.name , 6 from sys.tables t cross join syscolumns c
GO

-- verifica tamanho da tabela
EXEC sp_spaceused categoria
GO
EXEC sp_spaceused produto
GO

No nosso caso, é devido a falta de índice em um foreign key, mas para avaliar se existem bloqueios você pode utilizar a procedure sp_whoisactive ou utilizar o script que disponibilizei nesse post.

Para validar se existe trigger na tabela que vc está tentando executar o delete você pode usar a consulta abaixo, ou navegar no object explorer do ssms, até encontrar a tabela expandir as propriedades e abrir a pasta “Triggers”.

SELECT * FROM sys.triggers WHERE OBJECT_NAME(parent_id) = 'categoria'

Após gerar a massa de dados, tentei executar o delete em uma categoria que não tinha dados relacionados na outra tabela.

Por causa do relacionamento explícito através da FK, durante a operação de delete o engine do SQL precisa garantir que não existem dados na tabela produto com o id da categoria a ser excluído, por isso o alto volume de logical reads e a alta duração para excluir apenas uma linha.

Após criar o índice para cobrir a fk com o script abaixo, a operação de deleção ficou mais rápida, passando a ler apena 3 páginas da tabela produto e a duração de 0 milisec.

CREATE INDEX Ix_produto_id_categoria ON produto (id_categoria)

Como na declaração de criação da constraint não incluímos a especificação de cascade, um erro será gerado devido a integridade dos dados.

Para fazer o delete eu identifiquei quantas linhas haviam em cada categoria.

Em seguida efetuei o delete na id categoria 5.

Tivemos o erro:

Msg 547, Level 16, State 0, Line 106
The DELETE statement conflicted with the REFERENCE constraint “FK__produto__id_cate__031C6FA4”. The conflict occurred in database “dba”, table “dbo.produto”, column ‘id_categoria’.

Para que o erro não ocorra podemos implementar na “aplicação” ou via trigger uma lógica para apagar os registros da tabela produto que depende do relacionamento com a tabela categoria, ou alterar a especificação da FK incluindo a propriedade cascade.

Temos duas opções para o cascade e são elas.

DELETE CASCADE : Quando criamos uma chave estrangeira usando esta opção, ela exclui as linhas de referência na tabela filho quando a linha referenciada é excluída na tabela pai que possui uma chave primária.

UPDATE CASCADE: Quando criamos uma chave estrangeira usando UPDATE CASCADE, as linhas de referência são atualizadas na tabela filha quando a linha referenciada é atualizada na tabela pai que possui uma chave primária.

Conseguimos criar uma nova fk entre as tabelas, porém o ideal é apagar a já existente e criar a nova para garantir as regras de exclusão e podemos utilizar no nosso exemplo os scripts abaixo:

-- drop a constraint anterior sem cascade
ALTER TABLE produto DROP CONSTRAINT FK__produto__id_cate__031C6FA4
GO

-- Cria relacionamento com delete cascade
ALTER TABLE produto
ADD FOREIGN KEY (id_categoria) REFERENCES categoria (id)
ON DELETE CASCADE	
GO

EXEC sp_help produto
GO

Agora que temos a constraint configurada com o delete cascade, vamos novamente tentar fazer o delete.

Tivemos um alto volume de leitura de páginas e pelo plano de execução podemos observar que foi realizado o spill para o tempdb devido as 120000

E então, depois de alterar a fk com a opção delete cascade, foi realizada a exclusão do registro da tabela categoria e os 120 mil registros da tabela produto, em um tempo aceitável de aproximadamente 2 segundos, pois estava com o índice criado.

Por hoje é isso pessoal.
Abs

Deixe um comentário