¿Por qué no obtengo un registro mínimo al insertar en tablas indexadas?

14

Estoy probando inserciones de registro mínimas en diferentes escenarios y, por lo que he leído, INSERTAR EN SELECCIONAR en un montón con un índice no agrupado usando TABLOCK y SQL Server 2016+ debería registrar mínimamente, sin embargo, en mi caso, al hacerlo, obtengo registro completo. Mi base de datos está en el modelo de recuperación simple y obtengo inserciones mínimamente registradas en un montón sin índices y TABLOCK.

Estoy usando una copia de seguridad antigua de la base de datos de Stack Overflow para probar y he creado una réplica de la tabla Posts con el siguiente esquema ...

CREATE TABLE [dbo].[PostsDestination](
    [Id] [int] NOT NULL,
    [AcceptedAnswerId] [int] NULL,
    [AnswerCount] [int] NULL,
    [Body] [nvarchar](max) NOT NULL,
    [ClosedDate] [datetime] NULL,
    [CommentCount] [int] NULL,
    [CommunityOwnedDate] [datetime] NULL,
    [CreationDate] [datetime] NOT NULL,
    [FavoriteCount] [int] NULL,
    [LastActivityDate] [datetime] NOT NULL,
    [LastEditDate] [datetime] NULL,
    [LastEditorDisplayName] [nvarchar](40) NULL,
    [LastEditorUserId] [int] NULL,
    [OwnerUserId] [int] NULL,
    [ParentId] [int] NULL,
    [PostTypeId] [int] NOT NULL,
    [Score] [int] NOT NULL,
    [Tags] [nvarchar](150) NULL,
    [Title] [nvarchar](250) NULL,
    [ViewCount] [int] NOT NULL
)
CREATE NONCLUSTERED INDEX ndx_PostsDestination_Id ON PostsDestination(Id)

Luego trato de copiar la tabla de publicaciones en esta tabla ...

INSERT INTO PostsDestination WITH(TABLOCK)
SELECT * FROM Posts ORDER BY Id 

Al mirar fn_dblog y el uso del archivo de registro, puedo ver que no obtengo un registro mínimo de esto. He leído que las versiones anteriores a 2016 requieren la marca de seguimiento 610 para iniciar sesión mínimamente en las tablas indexadas, también he intentado configurar esto, pero aún así no me alegro.

Supongo que me estoy perdiendo algo aquí?

EDITAR - Más información

Para agregar más información, estoy usando el siguiente procedimiento que he escrito para tratar de detectar un registro mínimo, tal vez tengo algo mal aquí ...

/*
    Example Usage...

    EXEC sp_GetLogUseStats
   @Sql = '
      INSERT INTO PostsDestination
      SELECT TOP 500000 * FROM Posts ORDER BY Id ',
   @Schema = 'dbo',
   @Table = 'PostsDestination',
   @ClearData = 1

*/

CREATE PROCEDURE [dbo].[sp_GetLogUseStats]
(   
   @Sql NVARCHAR(400),
   @Schema NVARCHAR(20),
   @Table NVARCHAR(200),
   @ClearData BIT = 0
)
AS

IF @ClearData = 1
   BEGIN
   TRUNCATE TABLE PostsDestination
   END

/*Checkpoint to clear log (Assuming Simple/Bulk Recovery Model*/
CHECKPOINT  

/*Snapshot of logsize before query*/
CREATE TABLE #BeforeLogUsed(
   [Db] NVARCHAR(100),
   LogSize NVARCHAR(30),
   Used NVARCHAR(50),
   Status INT
)
INSERT INTO #BeforeLogUsed
EXEC('DBCC SQLPERF(logspace)')

/*Run Query*/
EXECUTE sp_executesql @SQL

/*Snapshot of logsize after query*/
CREATE TABLE #AfterLLogUsed(    
   [Db] NVARCHAR(100),
   LogSize NVARCHAR(30),
   Used NVARCHAR(50),
   Status INT
)
INSERT INTO #AfterLLogUsed
EXEC('DBCC SQLPERF(logspace)')

/*Return before and after log size*/
SELECT 
   CAST(#AfterLLogUsed.Used AS DECIMAL(12,4)) - CAST(#BeforeLogUsed.Used AS DECIMAL(12,4)) AS LogSpaceUsersByInsert
FROM 
   #BeforeLogUsed 
   LEFT JOIN #AfterLLogUsed ON #AfterLLogUsed.Db = #BeforeLogUsed.Db
WHERE 
   #BeforeLogUsed.Db = DB_NAME()

/*Get list of affected indexes from insert query*/
SELECT 
   @Schema + '.' + so.name + '.' +  si.name AS IndexName
INTO 
   #IndexNames
FROM 
   sys.indexes si 
   JOIN sys.objects so ON si.[object_id] = so.[object_id]
WHERE 
   si.name IS NOT NULL
   AND so.name = @Table
/*Insert Record For Heap*/
INSERT INTO #IndexNames VALUES(@Schema + '.' + @Table)

/*Get log recrod sizes for heap and/or any indexes*/
SELECT 
   AllocUnitName,
   [operation], 
   AVG([log record length]) AvgLogLength,
   SUM([log record length]) TotalLogLength,
   COUNT(*) Count
INTO #LogBreakdown
FROM 
   fn_dblog(null, null) fn
   INNER JOIN #IndexNames ON #IndexNames.IndexName = allocunitname
GROUP BY 
   [Operation], AllocUnitName
ORDER BY AllocUnitName, operation

SELECT * FROM #LogBreakdown
SELECT AllocUnitName, SUM(TotalLogLength)  TotalLogRecordLength 
FROM #LogBreakdown
GROUP BY AllocUnitName

Inserción en un montón sin índices y TABLOCK utilizando el siguiente código ...

EXEC sp_GetLogUseStats
   @Sql = '
      INSERT INTO PostsDestination
      SELECT * FROM Posts ORDER BY Id ',
   @Schema = 'dbo',
   @Table = 'PostsDestination',
   @ClearData = 1

Obtengo estos resultados

ingrese la descripción de la imagen aquí

Con un crecimiento de archivo de registro de 0.0024 mb, tamaños de registro de registro muy pequeños y muy pocos de ellos, estoy feliz de que esto esté usando un registro mínimo.

Si luego creo un índice no agrupado en id ...

CREATE INDEX ndx_PostsDestination_Id ON PostsDestination(Id)

Luego ejecute mi mismo inserto nuevamente ...

ingrese la descripción de la imagen aquí

No solo no obtengo un registro mínimo en el índice no agrupado, sino que también lo he perdido en el montón. Después de hacer algunas pruebas más, parece que si hago una ID agrupada, se registra mínimamente, pero por lo que he leído 2016+, debería iniciar sesión mínimamente en un montón con índice no agrupado cuando se usa tablock.

EDICION FINAL :

He informado sobre el comportamiento a Microsoft en el UserVoice de SQL Server y lo actualizaré si recibo una respuesta. También escribí todos los detalles de los escenarios de registro mínimos que no pude poner a trabajar en https://gavindraper.com/2018/05/29/SQL-Server-Minimal-Logging-Inserts/

Gavin
fuente
3
Paul White tiene una respuesta útil relacionada aquí .
Erik Darling,

Respuestas:

12

Puedo reproducir sus resultados en SQL Server 2017 utilizando la base de datos Stack Overflow 2010, pero no (todas) sus conclusiones.

El registro mínimo en el montón no está disponible cuando se usa INSERT...SELECTcon TABLOCKun montón con un índice no agrupado, lo cual es inesperado . Mi conjetura es INSERT...SELECTque no puede soportar cargas masivas usando RowsetBulk(montón) al mismo tiempo que FastLoadContext(b-tree). Solo Microsoft podría confirmar si se trata de un error o por diseño.

El índice no agrupado en el montón se registra mínimamente (suponiendo que TF610 esté activado o que se use SQL Server 2016+, habilitado FastLoadContext) con las siguientes advertencias:

  • Solo las filas insertadas en páginas recién asignadas se registran mínimamente.
  • Las filas agregadas a la primera página de índice no se registran mínimamente, si el índice estaba vacío al comienzo de la operación.

Las 497 LOP_INSERT_ROWSentradas que se muestran para el índice no agrupado corresponden a la primera página del índice. Como el índice estaba vacío de antemano, estas filas están completamente registradas. Las filas restantes están todas mínimamente registradas . Si el indicador de rastreo documentado 692 está habilitado (2016+) para deshabilitar FastLoadContext, todas las filas de índice no agrupadas se registran mínimamente.


He descubierto que el registro mínimo se aplica a ambos el montón y el índice no agrupado cuando mayor carga de la misma tabla (con índice) usando BULK INSERTdesde un archivo:

BULK INSERT dbo.PostsDestination
FROM 'D:\SQL Server\Posts.bcp'
WITH (TABLOCK, DATAFILETYPE = 'native');

Tomo nota de esto para completar. La carga masiva utilizando INSERT...SELECTdiferentes rutas de código, por lo que el hecho de que los comportamientos difieran no es del todo inesperado.


Para obtener detalles completos sobre el registro mínimo con RowsetBulky FastLoadContextcon, INSERT...SELECTconsulte mi serie de tres partes en SQLPerformance.com:

  1. Registro mínimo con INSERTAR ... SELECCIONAR en tablas de montón
  2. Registro mínimo con INSERTAR ... SELECCIONAR en tablas agrupadas vacías
  3. Registro mínimo con INSERT ... SELECT y contexto de carga rápida

Otros escenarios de tu publicación de blog

Los comentarios están cerrados, así que los abordaré brevemente aquí.

Índice agrupado vacío con Trace 610 o 2016+

Esto se registra mínimamente usando FastLoadContextsin TABLOCK. Las únicas filas registradas por completo son las insertadas en la primera página porque el índice agrupado estaba vacío al comienzo de la transacción.

Índice agrupado con datos y seguimiento 610 o 2016+

Esto también se registra mínimamente usando FastLoadContext. Las filas agregadas a la página existente se registran completamente, el resto se registra mínimamente.

Índice agrupado con índices no agrupados y TABLOCK o Trace 610 / SQL 2016+

Esto también se puede registrar mínimamente FastLoadContextsiempre que un operador independiente mantenga el índice no agrupado, DMLRequestSortse establezca en verdadero y se cumplan las otras condiciones establecidas en mis publicaciones .

Paul White 9
fuente
2

El siguiente documento es antiguo pero sigue siendo una excelente lectura.

En SQL 2016, el indicador de seguimiento 610 y ALLOW_PAGE_LOCKS están activados de forma predeterminada, pero alguien puede haberlos deshabilitado.

Guía de rendimiento de carga de datos

(3) Dependiendo del plan elegido por el optimizador, el índice no agrupado en la tabla puede estar completamente o mínimamente registrado.

La declaración SELECT puede ser el problema porque tiene un TOP y ORDER BY. Está insertando datos en la tabla en un orden diferente al índice, por lo que SQL podría estar haciendo una gran cantidad de clasificación en segundo plano.

ACTUALIZACIÓN 2

En realidad, es posible que obtenga un registro mínimo. Con TraceFlag 610 activado, el registro se comporta de manera diferente, SQL reservará suficiente espacio en el registro para realizar una reversión si las cosas salen mal, pero en realidad no usará el registro.

Probablemente esto cuente el espacio reservado (no utilizado)

EXEC('DBCC SQLPERF(logspace)')

Este código se divide Reservado de Usado

SELECT
    database_transaction_log_bytes_used
    ,database_transaction_log_bytes_reserved
    ,*
FROM sys.dm_tran_database_transactions 
WHERE database_id = DB_ID()

Supongo que el registro mínimo (en lo que respecta a Microsoft) en realidad se trata de realizar la menor cantidad de E / S en el registro, y no cuánto del registro está reservado.

Echa un vistazo a este enlace .

ACTUALIZACIÓN 1

Intente usar TABLOCKX en lugar de TABLOCK. Con Tablock todavía tiene un bloqueo compartido, por lo que SQL podría estar iniciando sesión en caso de que se inicie otro proceso.

TABLOCK puede necesitar ser usado junto con HOLDLOCK. Esto aplica el Tablock hasta el final de su transacción.

También ponga un candado en la tabla fuente [Publicaciones], el registro puede estar teniendo lugar porque la tabla fuente podría cambiar mientras se lleva a cabo su transacción. Paul White logró un registro mínimo cuando el origen no era una tabla SQL.

pacreely
fuente