TOP (1) POR GRUPO de tabla muy grande (100,000,000+)

8

Preparar

Tengo una gran mesa de ~ 115,382,254 filas. La tabla es relativamente simple y registra las operaciones del proceso de aplicación.

CREATE TABLE [data].[OperationData](
    [SourceDeciveID] [bigint] NOT NULL,
    [FileSource] [nvarchar](256) NOT NULL,
    [Size] [bigint] NULL,
    [Begin] [datetime2](7) NULL,
    [End] [datetime2](7) NOT NULL,
    [Date]  AS (isnull(CONVERT([date],[End]),CONVERT([date],'19000101',(112)))) PERSISTED NOT NULL,
    [DataSetCount] [bigint] NULL,
    [Result] [int] NULL,
    [Error] [nvarchar](max) NULL,
    [Status] [int] NULL,
 CONSTRAINT [PK_OperationData] PRIMARY KEY CLUSTERED 
(
    [SourceDeviceID] ASC,
    [FileSource] ASC,
    [End] ASC
))

CREATE TABLE [model].[SourceDevice](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [Name] [nvarchar](50) NULL,
 CONSTRAINT [PK_DataLogger] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
))

ALTER TABLE [data].[OperationData]  WITH CHECK ADD  CONSTRAINT [FK_OperationData_SourceDevice] FOREIGN KEY([SourceDeviceID])
REFERENCES [model].[SourceDevice] ([ID])

La tabla está agrupada en alrededor de 500 grupos y en una base diaria.

particiones

ingrese la descripción de la imagen aquí

Además, la tabla está bien indexada por PK, las estadísticas están actualizadas y el INDEXer se defrauda todas las noches.

Los SELECT basados ​​en índices son rápidos como un rayo y no tuvimos ningún problema con eso.

Problema

Necesito saber la última fila (SUPERIOR) [End]y particionado por [SourceDeciveID]. Para obtener lo último [OperationData]de cada dispositivo fuente.

Pregunta

Necesito encontrar una manera de resolver esto de una buena manera y sin llevar el DB a los límites.


Esfuerzo 1

El primer intento fue obvio GROUP BYo SELECT OVER PARTITION BYconsulta. El problema aquí también es obvio, cada consulta tiene que escanear por orden de partición / encontrar la fila superior. Por lo tanto, la consulta es muy lenta y tiene un impacto de E / S muy alto.

Ejemplo de consulta 1

;WITH cte AS
(
   SELECT *,
         ROW_NUMBER() OVER (PARTITION BY [SourceDeciveID] ORDER BY [End] DESC) AS rn
   FROM [data].[OperationData]
)
SELECT *
FROM cte
WHERE rn = 1

Ejemplo de consulta 2

SELECT *
FROM [data].[OperationData] AS d 
CROSS APPLY 
(
   SELECT TOP 1 *
   FROM [data].[OperationData] 
   WHERE [SourceDeciveID] = d.[SourceDeciveID]
   ORDER BY [End] DESC
) AS ds

¡HA FALLADO!

Esfuerzo 2

Creé una tabla de ayuda para mantener siempre una referencia a la fila SUPERIOR.

CREATE TABLE [data].[LastOperationData](
    [SourceDeciveID] [bigint] NOT NULL,
    [FileSource] [nvarchar](256) NOT NULL,
    [End] [datetime2](7) NOT NULL,
 CONSTRAINT [PK_LastOperationData] PRIMARY KEY CLUSTERED 
(
    [SourceDeciveID] ASC
)

ALTER TABLE [data].[LastOperationData]  WITH CHECK ADD  CONSTRAINT [FK_LastOperationData_OperationData] FOREIGN KEY([SourceDeciveID], [FileSource], [End])
REFERENCES [data].[OperationData] ([SourceDeciveID], [FileSource], [End])

Para llenar la tabla, se creó un activador para agregar / actualizar siempre la fila de origen si [End]se inserta una columna más alta .

CREATE TRIGGER [data].[OperationData_Last]
   ON  [data].[OperationData]
   AFTER INSERT
AS 
BEGIN
    SET NOCOUNT ON;

    MERGE [data].[LastOperationData] AS [target]
    USING (SELECT [SourceDeciveID], [FileSource], [End] FROM inserted) AS [source] ([SourceDeciveID], [FileSource], [End])  
    ON ([target].[SourceDeciveID] = [FileSource].[SourceDeciveID])

    WHEN MATCHED AND [target].[End] < [source].[End] THEN
        UPDATE SET [target].[FileSource] = source.[FileSource], [target].[End] = source.[End]

    WHEN NOT MATCHED THEN  
        INSERT ([SourceDeciveID], [FileSource], [End])  
        VALUES (source.[SourceDeciveID], source.[FileSource], source.[End]);

END

El problema aquí es que también tiene un gran impacto de IO y no sé por qué.

Como puede ver aquí en el plan de consulta , también ejecuta una exploración en toda la [OperationData]tabla.

Tiene un gran impacto general en mi base de datos. estadísticas

¡HA FALLADO!

Steffen Mangold
fuente
2
En su primer bloque de código, no puedo ver de dónde proviene la primera columna del índice agrupado, ¿es correcto?
George.Palacios
Sí, lo siento, SSMS no lo incluye en el CREATE TABLEscript, pero dentro del plan de consulta verá las particiones. Editaré la pregunta.
Steffen Mangold
¿No es un índice adicional porque está incluido dentro de lo PRIMARY KEY CLUSTEREDque cree que puede ayudar?
Steffen Mangold
Soryy eso fue un error, modifiqué los nombres de la pregunta para que sea más clara, la corregí.
Steffen Mangold
@ ypercubeᵀᴹ sí porque de SELECT [SourceID], [Source], [End] FROM insertedalguna manera escanea una tabla en el [OperationData].
Steffen Mangold

Respuestas:

9

Si tiene una tabla de SourceIDvalores y un índice en su tabla principal (SourceID, End) include (othercolumns), simplemente use OUTER APPLY.

SELECT d.*
FROM dbo.Sources s
OUTER APPLY (SELECT TOP (1) *
    FROM data.OperationData d
    WHERE d.SourceID = s.SourceID
    ORDER BY d.[End] DESC) d;

Si sabe que solo está después de su nueva partición, puede incluir un filtro en End, como AND d.[End] > DATEADD(day, -1, GETDATE())

Editar: debido a que su índice agrupado está activado SourceID, Source, End), coloque Fuente en su tabla de Fuentes también y únase a eso también. Entonces no necesitas el nuevo índice.

SELECT d.*
FROM dbo.Sources s -- Small table
OUTER APPLY (SELECT TOP (1) *
    FROM data.OperationData d -- Big table quick seeks
    WHERE d.SourceID = s.SourceID
    AND d.Source = s.Source
    AND d.[End] > DATEADD(day, -1, GETDATE()) -- If you’re partitioning on [End], do this for partition elimination
    ORDER BY d.[End] DESC) d;
Rob Farley
fuente
El índice realmente aceleró la consulta. Un segundo problema que surge es que un índice no particionado en una tabla tan grande es casi imposible de mantener. En toda nuestra tabla de "big data" trabajamos con indexador particionado. Se pueden mantener en línea partición por partición. Tan pronto como el indexador está particionado, el problema es el anterior porque tiene que ejecutar cada partición.
Steffen Mangold
1
@SteffenMangold: Los menos datos en un índice la mejor (con tal de que tiene todo lo necesario) y excluyendo las vistas materializadas, el índice agrupado tiene la máxima cantidad de datos posibles. Los índices agrupados están presentes porque la norma es obtener todos los datos por clave. En este caso, está obteniendo todos los datos, pero realmente no los está obteniendo por la clave, lo está obteniendo por parte de la clave. Necesita un índice que pueda consultarse con parte de la clave.
jmoreno
Lo siento mucho, pero hay una Sourcetabla que hace referencia a la sourceIDcolumna. La fuente de la columna es solo un nombre de archivo. Es un nombre poco confuso. Para cada Sourcedispositivo (sourceID) podría haber solo una entrada única para un archivo source(columna) en una marca de tiempo. Además, no puedo eliminar la partición porque el más nuevo Endestá fragmentado ampliamente. Por eso se me ocurrió la solución de activación. Creo que una consulta en vivo no funcionará aquí.
Steffen Mangold
@Rob Farley Edité la pregunta para que sea más clara
Steffen Mangold el
Con la partición, encontrará que hace todas esas búsquedas en cada partición. Con el predicado adicional, puede hacerlo para que no se moleste con todos ellos, y solo haga algunos. Que sea un mes si es necesario.
Rob Farley