Pergunta

Eu tenho analisado um "relatório de bug" recorrente (edição do perf) em um de nossos sistemas relacionados a uma operação de exclusão particularmente lenta. Longa história curta: parece que o CASCADE DELETE As chaves eram amplamente responsáveis, e eu gostaria de saber (a) se isso faz sentido e (b) por que é o caso.

Temos um esquema de, digamos, widgets, aqueles que estão na raiz de um grande gráfico de tabelas relacionadas e tabelas relacionadas e assim por diante. Para ser perfeitamente claro, a exclusão desta tabela é ativamente desencorajada; É a "opção nuclear" e os usuários não estão sob ilusões em contrário. No entanto, às vezes só precisa ser feito.

O esquema se parece mais disso:

Widgets
   |
   +--- Anvils [1:1]
   |    |
   |    +--- AnvilTestData [1:N]
   |
   +--- WidgetHistory (1:N)
        |
        +--- WidgetHistoryDetails (1:N)

As definições de coluna se parecem com o seguinte:

Widgets (WidgetID int PK, WidgetName varchar(50))
Anvils (AnvilID int PK, WidgetID int FK/IX/UNIQUE, ...)
AnvilTestData (AnvilID int FK/IX, TestID int, ...Test Data...)
WidgetHistory (HistoryID int PK, WidgetID int FK/IX, HistoryDate datetime, ...)
WidgetHistoryDetails (HistoryID int FK/IX, DetailType smallint, ...)

Nada muito assustador, realmente. UMA Widget pode ser tipos diferentes, um Anvil é um tipo especial, de modo que o relacionamento é 1: 1 (ou mais precisamente 1: 0..1). Depois, há uma grande quantidade de dados - talvez milhares de linhas de AnvilTestData por Anvil coletados ao longo do tempo, lidando com dureza, corrosão, peso exato, compatibilidade com martelo, problemas de usabilidade e testes de impacto com cabeças de desenhos animados.

Então todo Widget Tem um histórico longo e chato de vários tipos de transações - produção, movimentos de inventário, vendas, investigações de defeitos, RMAs, reparos, reclamações de clientes, etc. Pode haver 10-20k detalhes para um único widget, ou nenhum, dependendo de dependendo de sua idade.

Então, sem surpresa, há um CASCADE DELETE relacionamento em todos os níveis aqui. Se um Widget Precisa ser excluído, isso significa que algo foi terrivelmente errado e precisamos apagar quaisquer registros desse widget já existente, incluindo seu histórico, dados de teste etc. Novamente, opção nuclear.

As relações são todas indexadas, as estatísticas estão atualizadas. As consultas normais são rápidas. O sistema tende a cantarolar bastante suavemente para tudo, exceto excluir.

Chegando ao ponto aqui, finalmente, por vários motivos, permitimos apenas a exclusão de um widget de cada vez, então uma declaração de exclusão ficaria assim:

DELETE FROM Widgets
WHERE WidgetID = @WidgetID

Excluir muito simples e inócuo de aparência ... Isso leva mais de 2 minutos para correr, para um widget com Sem dados!

Depois de passar pelos planos de execução, finalmente consegui escolher o AnvilTestData e WidgetHistoryDetails exclui como as sub-operações com o maior custo. Então eu experimentei com desligar o CASCADE (mas mantendo o FK real, apenas configurando -o como NO ACTION) e reescrever o script como algo muito parecido com o seguinte:

DECLARE @AnvilID int
SELECT @AnvilID = AnvilID FROM Anvils WHERE WidgetID = @WidgetID

DELETE FROM AnvilTestData
WHERE AnvilID = @AnvilID

DELETE FROM WidgetHistory
WHERE HistoryID IN (
    SELECT HistoryID
    FROM WidgetHistory
    WHERE WidgetID = @WidgetID)

DELETE FROM Widgets WHERE WidgetID = @WidgetID

Ambas as "otimizações" resultaram em acelerações significativas, cada uma delas quase um minuto inteiro de folga do tempo de execução, para que a exclusão original de 2 minutos agora leva cerca de 5 a 10 segundos-pelo menos para novo widgets, sem muito histórico ou dados de teste.

Só para ser absolutamente claro, ainda há um CASCADE a partir de WidgetHistory para WidgetHistoryDetails, onde o fanout é mais alto, eu apenas removi o que se originou Widgets.

Além "achatar" as relações em cascata resultou em acelerações progressivamente menos dramáticas, mas ainda perceptíveis, até o ponto em que a exclusão de um novo O widget foi quase instantâneo, uma vez que todas as deletas em cascata para tabelas maiores foram removidas e substituídas por exclusão explícita.

estou a usar DBCC DROPCLEANBUFFERS e DBCC FREEPROCCACHE antes de cada teste. Desativei todos os gatilhos que podem estar causando mais desacelerações (embora elas apareçam no plano de execução de qualquer maneira). E também estou testando contra widgets mais antigos e percebendo uma aceleração significativa lá também; Os exclusão que costumavam levar 5 minutos agora levam 20-40 segundos.

Agora eu sou um ardente defensor da filosofia "Select não quebrada", mas simplesmente não parece haver nenhuma explicação lógica para esse comportamento além de esmagar, ineficiência impressionante do CASCADE DELETE relacionamentos.

Então, minhas perguntas são:

  • Este é um problema conhecido com o DRI no SQL Server? (Eu não conseguia encontrar nenhuma referência a esse tipo de coisa no Google ou aqui em SO; suspeito que a resposta seja não.)

  • Caso contrário, há outra explicação para o comportamento que estou vendo?

  • Se é um problema conhecido, por que é um problema e há melhores soluções alternativas que eu poderia estar usando?

Foi útil?

Solução

SQL Server é melhor em operações baseadas em set, enquanto CASCADE As deleções são, por natureza, baseadas em registros.

SQL Server, ao contrário dos outros servidores, tenta otimizar as operações imediatas baseadas em conjuntos, no entanto, ele funciona apenas um nível de profundidade. Ele precisa excluir os registros nas tabelas de nível superior para excluir as nas tabelas de nível inferior.

Em outras palavras, as operações em cascata funcionam para baixo, enquanto sua solução funciona para baixo, o que é mais baseado em conjuntos e eficientes.

Aqui está um esquema de amostra:

CREATE TABLE t_g (id INT NOT NULL PRIMARY KEY)

CREATE TABLE t_p (id INT NOT NULL PRIMARY KEY, g INT NOT NULL, CONSTRAINT fk_p_g FOREIGN KEY (g) REFERENCES t_g ON DELETE CASCADE)

CREATE TABLE t_c (id INT NOT NULL PRIMARY KEY, p INT NOT NULL, CONSTRAINT fk_c_p FOREIGN KEY (p) REFERENCES t_p ON DELETE CASCADE)

CREATE INDEX ix_p_g ON t_p (g)

CREATE INDEX ix_c_p ON t_c (p)

, esta consulta:

DELETE
FROM    t_g
WHERE   id > 50000

e seu plano:

  |--Sequence
       |--Table Spool
       |    |--Clustered Index Delete(OBJECT:([test].[dbo].[t_g].[PK__t_g__176E4C6B]), WHERE:([test].[dbo].[t_g].[id] > (50000)))
       |--Index Delete(OBJECT:([test].[dbo].[t_p].[ix_p_g]) WITH ORDERED PREFETCH)
       |    |--Sort(ORDER BY:([test].[dbo].[t_p].[g] ASC, [test].[dbo].[t_p].[id] ASC))
       |         |--Table Spool
       |              |--Clustered Index Delete(OBJECT:([test].[dbo].[t_p].[PK__t_p__195694DD]) WITH ORDERED PREFETCH)
       |                   |--Sort(ORDER BY:([test].[dbo].[t_p].[id] ASC))
       |                        |--Merge Join(Inner Join, MERGE:([test].[dbo].[t_g].[id])=([test].[dbo].[t_p].[g]), RESIDUAL:([test].[dbo].[t_p].[g]=[test].[dbo].[t_g].[id]))
       |                             |--Table Spool
       |                             |--Index Scan(OBJECT:([test].[dbo].[t_p].[ix_p_g]), ORDERED FORWARD)
       |--Index Delete(OBJECT:([test].[dbo].[t_c].[ix_c_p]) WITH ORDERED PREFETCH)
            |--Sort(ORDER BY:([test].[dbo].[t_c].[p] ASC, [test].[dbo].[t_c].[id] ASC))
                 |--Clustered Index Delete(OBJECT:([test].[dbo].[t_c].[PK__t_c__1C330188]) WITH ORDERED PREFETCH)
                      |--Table Spool
                           |--Sort(ORDER BY:([test].[dbo].[t_c].[id] ASC))
                                |--Hash Match(Inner Join, HASH:([test].[dbo].[t_p].[id])=([test].[dbo].[t_c].[p]))
                                     |--Table Spool
                                     |--Index Scan(OBJECT:([test].[dbo].[t_c].[ix_c_p]), ORDERED FORWARD)

Primeiro, SQL Server exclui registros de t_g, então se junta aos registros excluídos com t_p e exclui o último, finalmente, se junta a registros excluídos de t_p com t_c e exclui de t_c.

Uma única junção de três tabela seria muito mais eficiente neste caso, e é isso que você faz com sua solução alternativa.

Se isso faz você se sentir melhor, Oracle não otimiza as operações em cascata de forma alguma: elas são sempre NESTED LOOPS E Deus o ajude se você esqueceu de criar um índice na coluna de referência.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top