Pregunta

He estado analizando un "informe de error" recurrente (problema de rendimiento) en uno de nuestros sistemas relacionado con una operación de eliminación particularmente lenta.Larga historia corta:Parece que el CASCADE DELETE Las claves fueron en gran medida responsables y me gustaría saber (a) si esto tiene sentido y (b) por qué es así.

Tenemos un esquema de, digamos, widgets, aquellos que se encuentran en la raíz de un gran gráfico de tablas relacionadas y tablas relacionadas entre sí, etc.Para ser perfectamente claro, se desaconseja activamente la eliminación de esta tabla;es la "opción nuclear" y los usuarios no se hacen ilusiones en sentido contrario.Sin embargo, a veces simplemente hay que hacerlo.

El esquema se parece a esto:

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

Las definiciones de columnas se parecen a las siguientes:

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 demasiado aterrador, en realidad.A Widget pueden ser diferentes tipos, un Anvil es un tipo especial, por lo que la relación es 1:1 (o más exactamente 1:0...1).Luego hay una gran cantidad de datos, tal vez miles de filas de AnvilTestData por Anvil recopilados a lo largo del tiempo, que abordan la dureza, la corrosión, el peso exacto, la compatibilidad del martillo, los problemas de usabilidad y las pruebas de impacto con cabezas de dibujos animados.

Entonces cada Widget tiene un historial largo y aburrido de varios tipos de transacciones: producción, movimientos de inventario, ventas, investigaciones de defectos, RMA, reparaciones, quejas de clientes, etc.Puede haber entre 10.000 y 20.000 detalles para un solo widget, o ninguno, dependiendo de su antigüedad.

Entonces, como era de esperar, hay una CASCADE DELETE relación en todos los niveles aquí.si un Widget debe eliminarse, significa que algo salió terriblemente mal y debemos borrar cualquier registro de ese widget que haya existido, incluido su historial, datos de prueba, etc.De nuevo, la opción nuclear.

Todas las relaciones están indexadas, las estadísticas están actualizadas.Las consultas normales son rápidas.El sistema tiende a funcionar con bastante fluidez en todo excepto en las eliminaciones.

Llegando al punto aquí, finalmente, por varias razones solo permitimos eliminar un widget a la vez, por lo que una declaración de eliminación se vería así:

DELETE FROM Widgets
WHERE WidgetID = @WidgetID

Borrar bastante simple y de apariencia inofensiva... que tarda más de 2 minutos en ejecutarse, para un widget con ¡sin datos!

Después de revisar los planes de ejecución, finalmente pude elegir el AnvilTestData y WidgetHistoryDetails elimina como las suboperaciones con mayor costo.Así que experimenté apagando el CASCADE (pero manteniendo el FK real, simplemente configurándolo en NO ACTION) y reescribiendo el guión como algo muy parecido a lo siguiente:

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 "optimizaciones" dieron como resultado importantes aceleraciones, cada una de las cuales redujo casi un minuto el tiempo de ejecución, de modo que la eliminación original de 2 minutos ahora demora entre 5 y 10 segundos, al menos durante nuevo widgets, sin mucho historial ni datos de prueba.

Para que quede absolutamente claro, todavía hay una CASCADE de WidgetHistory a WidgetHistoryDetails, donde el fanout es mayor, solo eliminé el que se origina en Widgets.

Un mayor "aplanamiento" de las relaciones en cascada resultó en aceleraciones progresivamente menos dramáticas pero aún notables, hasta el punto en que eliminar una nuevo El widget fue casi instantáneo una vez que todas las eliminaciones en cascada de tablas más grandes se eliminaron y se reemplazaron con eliminaciones explícitas.

Estoy usando DBCC DROPCLEANBUFFERS y DBCC FREEPROCCACHE antes de cada prueba.He desactivado todos los desencadenantes que podrían estar causando más desaceleraciones (aunque de todos modos aparecerían en el plan de ejecución).Y también estoy probando con widgets más antiguos y notando una aceleración significativa allí;las eliminaciones que solían tardar 5 minutos ahora tardan entre 20 y 40 segundos.

Ahora bien, soy un ferviente partidario de la filosofía "SELECT no está roto", pero simplemente no parece haber ninguna explicación lógica para este comportamiento aparte de la aplastante y alucinante ineficiencia del CASCADE DELETE relaciones.

Entonces, mis preguntas son:

  • ¿Es este un problema conocido con DRI en SQL Server? (Parece que no pude encontrar ninguna referencia a este tipo de cosas en Google o aquí en SO;Sospecho que la respuesta es no.)

  • Si no, ¿hay otra explicación para el comportamiento que estoy viendo?

  • Si es un problema conocido, ¿por qué es un problema? ¿Existen mejores soluciones que podría utilizar?

¿Fue útil?

Solución

SQL Server es mejor en operaciones basadas en conjuntos, mientras que CASCADE las eliminaciones se basan, por su naturaleza, en registros.

SQL Server, a diferencia de los otros servidores, intenta optimizar las operaciones inmediatas basadas en conjuntos, sin embargo, funciona solo en un nivel de profundidad.Es necesario eliminar los registros en las tablas de nivel superior para eliminar los de las tablas de nivel inferior.

En otras palabras, las operaciones en cascada funcionan de arriba a abajo, mientras que su solución funciona de abajo hacia arriba, lo cual se basa más en conjuntos y es más eficiente.

Aquí hay un esquema de muestra:

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

y su plan:

  |--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)

Primero, SQL Server elimina registros de t_g, luego une los registros eliminados con t_p y elimina de este último, finalmente, une los registros eliminados de t_p con t_c y elimina de t_c.

Una única unión de tres tablas sería mucho más eficiente en este caso, y esto es lo que debe hacer con su solución alternativa.

Si te hace sentir mejor, Oracle no optimiza las operaciones en cascada de ninguna manera:ellos estan siempre NESTED LOOPS y que Dios te ayude si olvidaste crear un índice en la columna de referencias.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top