sql >> Base de Datos >  >> RDS >> Sqlserver

Actualización de tablas de SQL Server con menos interrupciones mediante el cambio de partición

Un requisito común en ETL y varios escenarios de informes es cargar silenciosamente una tabla de preparación de SQL Server en segundo plano, para que los usuarios que consultan los datos no se vean afectados por las escrituras y viceversa. El truco es cómo y cuándo dirige a los usuarios a la nueva versión actualizada de los datos.

Ejemplo simplificado de una tabla de etapas:una analogía del mercado de agricultores

Entonces, ¿qué es una tabla de etapas en SQL? Una mesa de preparación se puede entender más fácilmente con un ejemplo del mundo real:supongamos que tiene una mesa llena de verduras que está vendiendo en el mercado de agricultores local. A medida que sus vegetales se venden y trae nuevo inventario:

  • Cuando traiga una carga de vegetales nuevos, le tomará 20 minutos limpiar la mesa y reemplazar el caldo restante con el producto más nuevo.
  • No desea que los clientes se sienten allí y esperen 20 minutos a que se produzca el cambio, ya que la mayoría obtendrá sus verduras en otro lugar.

Ahora, ¿qué pasaría si tuviera una segunda mesa vacía donde carga las verduras nuevas y, mientras lo hace, los clientes aún pueden comprar las verduras más viejas de la primera mesa? (Supongamos que no es porque las verduras más viejas se estropearon o son menos deseables).

Actualización de tablas en SQL Server

Hay varios métodos para recargar tablas completas mientras se consultan activamente; Hace dos décadas, me aproveché desenfrenadamente de sp_rename — Jugaría un juego de trileros con una instantánea vacía de la mesa, recargaría felizmente la instantánea y luego solo realizaría el cambio de nombre dentro de una transacción.

En SQL Server 2005, comencé a usar esquemas para mantener instantáneas de las tablas que simplemente transfería usando la misma técnica de juego de shell, sobre la cual escribí en estas dos publicaciones:

  • Tiros con truco:Schema Switch-a-Roo
  • Schema Switch-a-Roo, Parte 2

La única ventaja de transferir objetos entre esquemas en lugar de cambiarles el nombre es que no hay mensajes de advertencia sobre el cambio de nombre de un objeto, lo que ni siquiera es un problema, excepto que los mensajes de advertencia llenan los registros del historial del agente mucho más rápido.

Ambos enfoques aún requieren un bloqueo de modificación de esquema (Sch-M), por lo que deben esperar a que cualquier transacción existente libere sus propios bloqueos. Una vez que adquieren su bloqueo Sch-M, bloquean cualquier consulta posterior que requiera bloqueos de estabilidad de esquema (Sch-S)... que es casi todas las consultas. Puede convertirse rápidamente en una pesadilla de cadena de bloqueo, ya que cualquier consulta nueva que necesite Sch-S tiene que ponerse en cola detrás de Sch-M. (Y no, no puede evitar esto usando RCSI o NOLOCK en todas partes, ya que incluso esas consultas aún requieren Sch-S. No puede adquirir Sch-S con un Sch-M puesto, ya que son incompatibles; Michael J. Swart habla de eso aquí).

Kendra Little realmente me abrió los ojos sobre los peligros de la transferencia de esquemas en su publicación, "Puesta en escena de los datos:Bloquear el peligro con ALTER SCHEMA TRANSFER". Allí muestra por qué la transferencia de esquemas puede ser peor que el cambio de nombre. Más tarde detalló una tercera y mucho menos impactante forma de intercambiar tablas, que ahora uso exclusivamente:cambio de partición. Este método permite que el conmutador espere con una prioridad más baja, que ni siquiera es una opción con las técnicas de cambio de nombre o transferencia de esquema. Joe Sack entró en detalles sobre esta mejora agregada en SQL Server 2014:"Explorando las opciones de espera de bloqueo de baja prioridad en SQL Server 2014 CTP1".

Ejemplo de cambio de partición de SQL Server

Veamos un ejemplo básico, siguiendo la esencia completa de Kendra aquí. Primero, crearemos dos nuevas bases de datos:

CREATE DATABASE NewWay;
CREATE DATABASE OldWay;
GO

En la nueva base de datos, crearemos una tabla para guardar nuestro inventario de verduras y dos copias de la tabla para nuestro juego de trileros:

USE NewWay;
GO
 
CREATE TABLE dbo.Vegetables_NewWay
(
  VegetableID int,
  Name        sysname,
  WhenPicked  datetime,
  BackStory   nvarchar(max)
);
GO
 
-- we need to create two extra copies of the table.
 
CREATE TABLE dbo.Vegetables_NewWay_prev
(
  VegetableID int,
  Name        sysname,
  WhenPicked  datetime,
  BackStory   nvarchar(max)
);
GO
 
CREATE TABLE dbo.Vegetables_NewWay_hold
(
  VegetableID int,
  Name        sysname,
  WhenPicked  datetime,
  BackStory   nvarchar(max)
);
GO

Creamos un procedimiento que carga la copia provisional de la tabla y luego usa una transacción para cambiar la copia actual.

CREATE PROCEDURE dbo.DoTheVeggieSwap_NewWay
AS
BEGIN
  SET NOCOUNT ON;
 
  TRUNCATE TABLE dbo.Vegetables_NewWay_prev;
 
  INSERT dbo.Vegetables_NewWay_prev
    SELECT TOP (1000000) s.session_id, o.name, s.last_successful_logon, 
      LEFT(m.definition, 500)
    FROM sys.dm_exec_sessions AS s
    CROSS JOIN model.sys.all_objects AS o
    INNER JOIN model.sys.all_sql_modules AS m
    ON o.[object_id] = m.[object_id];
 
  -- need to take Sch-M locks here:
 
  BEGIN TRANSACTION;
    ALTER TABLE dbo.Vegetables_NewWay 
      SWITCH TO dbo.Vegetables_NewWay_hold
      WITH (WAIT_AT_LOW_PRIORITY 
            (MAX_DURATION = 1 MINUTES,
             ABORT_AFTER_WAIT = BLOCKERS));
 
    ALTER TABLE dbo.Vegetables_NewWay_prev
      SWITCH TO dbo.Vegetables_NewWay;
  COMMIT TRANSACTION;
 
  -- and now users will query the new data in dbo
  -- can switch the old copy back and truncate it 
  -- without interfering with other queries
 
  ALTER TABLE dbo.Vegetables_NewWay_hold
	SWITCH TO dbo.Vegetables_NewWay_prev;
 
  TRUNCATE TABLE dbo.Vegetables_NewWay_prev;
END
GO

La belleza de WAIT_AT_LOW_PRIORITY es que puedes controlar completamente el comportamiento con ABORT_AFTER_WAIT opción:

ABORT_AFTER_WAIT
configuración
Descripción/síntomas
YO MISMO Esto significa que el interruptor se dará por vencido después de n minutos.

Para la sesión que intenta realizar el cambio, esto aparecerá como el mensaje de error:

Se excedió el tiempo de espera de la solicitud de bloqueo.
BLOQUEADORES Esto dicta que el interruptor esperará hasta n minutos, luego forzarse a sí mismo al frente de la línea matando a todos los bloqueadores delante .

Las sesiones que intentan interactuar con la tabla que se ve afectada por la operación de cambio verán una combinación de estos mensajes de error:

Su sesión se ha desconectado debido a una operación DDL de alta prioridad.

No se puede continuar con la ejecución porque la sesión está en estado de interrupción.

Se produjo un error grave en el comando actual. Los resultados, si los hay, deben descartarse.

NINGUNO Esto dice que el interruptor esperará felizmente hasta que llegue su turno, independientemente de MAX_DURATION .

Este es el mismo comportamiento que obtendría con el cambio de nombre, la transferencia de esquema o el cambio de partición sin WAIT_AT_LOW_PRIORITY .

Los BLOCKERS La opción no es la forma más amigable de manejar las cosas, ya que ya está diciendo que está bien a través de esta operación de preparación/cambio para que los usuarios vean datos que están un poco desactualizados. Probablemente preferiría usar SELF y haga que la operación vuelva a intentarse en los casos en que no pueda obtener los bloqueos necesarios en el tiempo asignado. Sin embargo, mantendría un registro de la frecuencia con la que falla, especialmente las fallas consecutivas, porque quiere asegurarse de que los datos nunca se vuelvan demasiado obsoletos.

En comparación con la forma antigua de cambiar entre esquemas

Así es como habría manejado el cambio antes:

USE OldWay;
GO
 
-- create two schemas and two copies of the table
 
CREATE SCHEMA prev AUTHORIZATION dbo;
GO
 
CREATE SCHEMA hold AUTHORIZATION dbo;
GO
 
CREATE TABLE dbo.Vegetables_OldWay
(
  VegetableID int,
  Name sysname,
  WhenPicked datetime,
  BackStory nvarchar(max)
);
GO
 
CREATE TABLE prev.Vegetables_OldWay
(
  VegetableID int,
  Name sysname,
  WhenPicked datetime,
  BackStory nvarchar(max)
);
GO
 
CREATE PROCEDURE dbo.DoTheVeggieSwap_OldWay
AS
BEGIN
  SET NOCOUNT ON;
 
  TRUNCATE TABLE prev.Vegetables_OldWay;
 
  INSERT prev.Vegetables_OldWay
    SELECT TOP (1000000) s.session_id, o.name, s.last_successful_logon, 
      LEFT(m.definition, 500)
    FROM sys.dm_exec_sessions AS s
    CROSS JOIN model.sys.all_objects AS o
    INNER JOIN model.sys.all_sql_modules AS m
    ON o.[object_id] = m.[object_id];
 
  -- need to take Sch-M locks here:
  BEGIN TRANSACTION;
    ALTER SCHEMA hold TRANSFER dbo.Vegetables_OldWay;
    ALTER SCHEMA dbo  TRANSFER prev.Vegetables_OldWay;
  COMMIT TRANSACTION;
 
  -- and now users will query the new data in dbo
  -- can transfer the old copy back and truncate it without 
  -- interfering with other queries:
 
  ALTER SCHEMA prev TRANSFER hold.Vegetables_OldWay;
  TRUNCATE TABLE prev.Vegetables_OldWay;
END
GO

Ejecuté pruebas de concurrencia utilizando dos ventanas de SQLQueryStress de Erik Ejlskov Jensen:una para repetir una llamada al procedimiento cada minuto y la otra para ejecutar 16 subprocesos como este, miles de veces:

BEGIN TRANSACTION;
 
UPDATE TOP (1) dbo.<table> SET name += 'x';
SELECT TOP (10) name FROM dbo.<table> ORDER BY NEWID();
WAITFOR DELAY '00:00:02';
 
COMMIT TRANSACTION;

Puede mirar el resultado de SQLQueryStress, o sys.dm_exec_query_stats, o Query Store, y verá algo parecido a los siguientes resultados (pero le recomiendo usar una herramienta de monitoreo de rendimiento de SQL Server de calidad si realmente quiere optimizar proactivamente los entornos de bases de datos):

Duración y tasas de error Transferencia de esquema ABORT_AFTER_WAIT:
SELF
ABORT_AFTER_WAIT:
BLOQUEADORES
Duración media:transferencia/cambio 96,4 segundos 68,4 segundos 20,8 segundos
Duración promedio:DML 18,7 segundos 2,7 segundos 2,9 segundos
Excepciones:transferencia/cambio 0 0,5/minuto 0
Excepciones – DML 0 0 25,5/minuto

Tenga en cuenta que las duraciones y los recuentos de excepciones dependerán en gran medida de las especificaciones de su servidor y de lo que suceda en su entorno. También tenga en cuenta que, si bien no hubo excepciones para las pruebas de transferencia de esquema al usar SQLQueryStress, es posible que tenga tiempos de espera más estrictos según la aplicación que lo consuma. Y fue mucho más lento en promedio, porque el bloqueo se acumuló de manera mucho más agresiva. Nadie quiere excepciones, pero cuando hay una compensación como esta, es posible que prefiera algunas excepciones aquí y allá (dependiendo de la frecuencia de la operación de actualización) en lugar de que todos esperen más tiempo todo el tiempo.

Cambio de partición frente a cambio de nombre/transferencia de esquema para actualizar tablas de SQL Server

El cambio de partición le permite elegir qué parte de su proceso asume el costo de la concurrencia. Puede dar preferencia al proceso de cambio, por lo que los datos son más fiables y actualizados, pero esto significa que algunas de sus consultas fallarán. Por el contrario, puede priorizar las consultas, a costa de un proceso de actualización más lento (y la falla ocasional allí). El objetivo principal es que el cambio de partición de SQL Server es un método superior para actualizar las tablas de SQL Server en comparación con las técnicas anteriores de cambio de nombre/transferencia de esquema en casi todos los puntos, y puede usar una lógica de reintento más sólida o experimentar con tolerancias de duración para llegar al punto ideal. para su carga de trabajo.