sql >> Base de Datos >  >> RDS >> Database

Los beneficios de indexar claves externas

Las claves primarias y externas son características fundamentales de las bases de datos relacionales, como se señaló originalmente en el artículo de E. F. Codd, "Un modelo relacional de datos para grandes bancos de datos compartidos", publicado en 1970. La cita que se repite a menudo es:"La clave, la clave completa, y nada más que la llave, así que ayúdame, Codd".

Antecedentes:Claves primarias

Una clave principal es una restricción en SQL Server, que actúa para identificar de manera única cada fila en una tabla. La clave se puede definir como una sola columna no NULL o una combinación de columnas no NULL que genera un valor único y se usa para imponer la integridad de la entidad para una tabla. Una tabla solo puede tener una clave principal, y cuando se define una restricción de clave principal para una tabla, se crea un índice único. Ese índice será un índice agrupado de forma predeterminada, a menos que se especifique como un índice no agrupado cuando se define la restricción de clave principal.

Considere el Sales.SalesOrderHeader tabla en AdventureWorks2012 base de datos. Esta tabla contiene información básica sobre un pedido de ventas, incluida la fecha del pedido y el ID del cliente, y cada venta se identifica de forma única mediante un SalesOrderID. , que es la clave principal de la tabla. Cada vez que se agrega una nueva fila a la tabla, la restricción de clave principal (llamada PK_SalesOrderHeader_SalesOrderID ) se verifica para garantizar que no exista ninguna fila con el mismo valor para SalesOrderID .

Claves foráneas

Separadas de las claves primarias, pero muy relacionadas, están las claves foráneas. Una clave externa es una columna o combinación de columnas que es igual a la clave principal, pero en una tabla diferente. Las claves foráneas se utilizan para definir una relación y hacer cumplir la integridad entre dos tablas.

Para continuar usando el ejemplo anterior, el SalesOrderID la columna existe como clave externa en Sales.SalesOrderDetail tabla, donde se almacena información adicional sobre la venta, como la identificación del producto y el precio. Cuando se agrega una nueva venta al SalesOrderHeader tabla, no es necesario agregar una fila para esa venta a SalesOrderDetail tabla  Sin embargo, al agregar una fila a SalesOrderDetail tabla, una fila correspondiente para el SalesOrderID debe existe en el SalesOrderHeader mesa.

Por el contrario, al eliminar datos, una fila para un SalesOrderID específico se puede eliminar en cualquier momento desde el SalesOrderDetail tabla, pero para que se elimine una fila del SalesOrderHeader tabla, filas asociadas de SalesOrderDetail tendrá que eliminarse primero.

A diferencia de las restricciones de clave principal, cuando se define una restricción de clave externa para una tabla, SQL Server no crea un índice de forma predeterminada. Sin embargo, no es raro que los desarrolladores y administradores de bases de datos los agreguen manualmente. La clave externa puede ser parte de una clave primaria compuesta para la tabla, en cuyo caso existiría un índice agrupado con la clave externa como parte de la clave de agrupación. Alternativamente, las consultas pueden requerir un índice que incluya la clave externa y una o más columnas adicionales en la tabla, por lo que se crearía un índice no agrupado para admitir esas consultas. Además, los índices en claves externas pueden proporcionar beneficios de rendimiento para las uniones de tablas que involucran la clave principal y externa, y pueden afectar el rendimiento cuando se actualiza el valor de la clave principal o si se elimina la fila.

En el AdventureWorks2012 base de datos, hay una tabla, SalesOrderDetail , con SalesOrderID como clave foránea. Para el SalesOrderDetail tabla, SalesOrderID y SalesOrderDetailID se combinan para formar la clave principal, respaldada por un índice agrupado. Si el SalesOrderDetail la tabla no tenía un índice en el SalesOrderID columna, luego, cuando se elimina una fila de SalesOrderHeader , SQL Server tendría que verificar que no haya filas para el mismo SalesOrderID valor existe. Sin ningún índice que contenga el SalesOrderID columna, SQL Server necesitaría realizar una exploración completa de la tabla de SalesOrderDetail . Como puede imaginar, cuanto más grande sea la tabla a la que se hace referencia, más tardará la eliminación.

Un ejemplo

Podemos ver esto en el siguiente ejemplo, que usa copias de las tablas mencionadas del AdventureWorks2012 base de datos que se ha ampliado mediante un script que se puede encontrar aquí. El script fue desarrollado por Jonathan Kehayias (blog | @SQLPoolBoy) y crea un SalesOrderHeaderEnlarged tabla con 1 258 600 filas y un SalesOrderDetailEnlarged tabla con 4.852.680 filas. Después de ejecutar la secuencia de comandos, se agregó la restricción de clave externa utilizando las siguientes declaraciones. Tenga en cuenta que la restricción se crea con ON DELETE CASCADE opción. Con esta opción, cuando se emite una actualización o eliminación contra el SalesOrderHeaderEnlarged tabla, filas en las tablas correspondientes; en este caso, solo SalesOrderDetailEnlarged – se actualizan o eliminan.

Además, el índice agrupado predeterminado para SalesOrderDetailEnglarged se eliminó y se volvió a crear para tener solo SalesOrderDetailID como clave principal, ya que representa un diseño típico.

USE [AdventureWorks2012];
GO
 
/* remove original clustered index */
ALTER TABLE [Sales].[SalesOrderDetailEnlarged] 
  DROP CONSTRAINT [PK_SalesOrderDetailEnlarged_SalesOrderID_SalesOrderDetailID];
GO
 
/* re-create clustered index with SalesOrderDetailID only */
ALTER TABLE [Sales].[SalesOrderDetailEnlarged] 
  ADD CONSTRAINT [PK_SalesOrderDetailEnlarged_SalesOrderDetailID] PRIMARY KEY CLUSTERED
  (
    [SalesOrderDetailID] ASC
  )
  WITH
  (
     PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, 
     IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON
  ) ON [PRIMARY];
GO
 
/* add foreign key constraint for SalesOrderID */
ALTER TABLE [Sales].[SalesOrderDetailEnlarged] WITH CHECK 
  ADD CONSTRAINT [FK_SalesOrderDetailEnlarged_SalesOrderHeaderEnlarged_SalesOrderID] 
  FOREIGN KEY([SalesOrderID])
  REFERENCES [Sales].[SalesOrderHeaderEnlarged] ([SalesOrderID])
  ON DELETE CASCADE;
GO
 
ALTER TABLE [Sales].[SalesOrderDetailEnlarged] 
  CHECK CONSTRAINT [FK_SalesOrderDetailEnlarged_SalesOrderHeaderEnlarged_SalesOrderID];
GO

Con la restricción de clave externa y sin índice de soporte, se emitió una sola eliminación contra SalesOrderHeaderEnlarged tabla, lo que resultó en la eliminación de una fila de SalesOrderHeaderEnlarged y 72 filas de SalesOrderDetailEnlarged :

SET STATISTICS IO ON;
SET STATISTICS TIME ON;
 
DBCC DROPCLEANBUFFERS;
DBCC FREEPROCCACHE;
 
USE [AdventureWorks2012];
GO
 
DELETE FROM [Sales].[SalesOrderHeaderEnlarged] WHERE [SalesOrderID] = 292104;

Las estadísticas de E/S y la información de tiempo mostraron lo siguiente:

Tiempo de análisis y compilación de SQL Server:

Tiempo de CPU =8 ms, tiempo transcurrido =8 ms.

Tabla 'SalesOrderDetailEnlarged'. Recuento de escaneo 1, lecturas lógicas 50647, lecturas físicas 8, lecturas anticipadas 50667, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas anticipadas lob 0.
Tabla 'Worktable'. Recuento de escaneo 2, lecturas lógicas 7, lecturas físicas 0, lecturas anticipadas 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas anticipadas lob 0.
Tabla 'SalesOrderHeaderEnlarged'. Recuento de escaneo 0, lecturas lógicas 15, lecturas físicas 14, lecturas anticipadas 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas anticipadas lob 0.

Tiempos de ejecución de SQL Server:

Tiempo de CPU =1045 ms, tiempo transcurrido =1898 ms.

Usando SQL Sentry Plan Explorer, el plan de ejecución muestra un escaneo de índice agrupado contra SalesOrderDetailEnlarged ya que no hay índice en SalesOrderID :


Plan de consulta sin índice en la clave externa

El índice no agrupado para admitir SalesOrderDetailEnlarged luego se creó usando la siguiente declaración:

USE [AdventureWorks2012];
GO
 
/* create nonclustered index */
CREATE NONCLUSTERED INDEX [IX_SalesOrderDetailEnlarged_SalesOrderID] ON [Sales].[SalesOrderDetailEnlarged]
(
  [SalesOrderID] ASC
)
WITH
(
  PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, 
  ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON
)
ON [PRIMARY];
 
GO

Se ejecutó otra eliminación para un SalesOrderID que afectó una fila en SalesOrderHeaderEnlarged y 72 filas en SalesOrderDetailEnlarged :

SET STATISTICS IO ON;
SET STATISTICS TIME ON;
 
DBCC DROPCLEANBUFFERS;
DBCC FREEPROCCACHE;
 
USE [AdventureWorks2012];
GO
 
DELETE FROM [Sales].[SalesOrderHeaderEnlarged] WHERE [SalesOrderID] = 697505;

Las estadísticas de E/S y la información de tiempo mostraron una mejora espectacular:

Tiempo de análisis y compilación de SQL Server:

Tiempo de CPU =0 ms, tiempo transcurrido =7 ms.

Tabla 'SalesOrderDetailEnlarged'. Recuento de escaneo 1, lecturas lógicas 48, lecturas físicas 13, lecturas anticipadas 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas anticipadas lob 0.
Tabla 'Worktable'. Recuento de escaneo 2, lecturas lógicas 7, lecturas físicas 0, lecturas anticipadas 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas anticipadas lob 0.
Tabla 'SalesOrderHeaderEnlarged'. Recuento de escaneo 0, lecturas lógicas 15, lecturas físicas 15, lecturas anticipadas 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas anticipadas lob 0.

Tiempos de ejecución de SQL Server:

Tiempo de CPU =0 ms, tiempo transcurrido =27 ms.

Y el plan de consulta mostró una búsqueda de índice del índice no agrupado en SalesOrderID , como se esperaba:


Plan de consulta con índice en la clave externa

El tiempo de ejecución de la consulta se redujo de 1898 ms a 27 ms, una reducción del 98,58 %, y lecturas para SalesOrderDetailEnlarged la tabla disminuyó de 50647 a 48, una mejora del 99,9 %. Aparte de los porcentajes, considere solo la E/S generada por la eliminación. El SalesOrderDetailEnlarged la tabla tiene solo 500 MB en este ejemplo, y para un sistema con 256 GB de memoria disponible, una tabla que ocupa 500 MB en el caché del búfer no parece una situación terrible. Pero una tabla de 5 millones de filas es relativamente pequeña; la mayoría de los grandes sistemas OLTP tienen tablas con cientos de millones de filas. Además, no es raro que existan varias referencias de clave externa para una clave principal, donde una eliminación de la clave principal requiere eliminaciones de varias tablas relacionadas. En ese caso, es posible ver duraciones extendidas para las eliminaciones, lo que no solo es un problema de rendimiento, sino también un problema de bloqueo, según el nivel de aislamiento.

Conclusión

Por lo general, se recomienda crear un índice que conduzca a la(s) columna(s) de la clave externa, para admitir no solo las uniones entre las claves principal y externa, sino también las actualizaciones y las eliminaciones. Tenga en cuenta que esta es una recomendación general, ya que hay escenarios de casos extremos en los que no se usó el índice adicional en la clave externa debido al tamaño de tabla extremadamente pequeño, y las actualizaciones de índice adicionales en realidad afectaron negativamente el rendimiento. Al igual que con cualquier modificación de esquema, las adiciones de índice deben probarse y monitorearse después de la implementación. Es importante asegurarse de que los índices adicionales produzcan los efectos deseados y no afecten negativamente el rendimiento de la solución. También vale la pena señalar cuánto espacio adicional requieren los índices para las claves externas. Es esencial tener esto en cuenta antes de crear los índices y, si brindan un beneficio, debe tenerse en cuenta para la planificación de la capacidad en el futuro.