Filtrar datos ordenados por versión de fila


8

Tengo una tabla de datos SQL con la siguiente estructura:

CREATE TABLE Data(
    Id uniqueidentifier NOT NULL,
    Date datetime NOT NULL,
    Value decimal(20, 10) NULL,
    RV timestamp NOT NULL,
 CONSTRAINT PK_Data PRIMARY KEY CLUSTERED (Id, Date)
)

El número de identificadores distintos varía de 3000 a 50000.
El tamaño de la tabla varía hasta más de mil millones de filas.
One Id puede cubrir entre unas pocas filas hasta el 5% de la tabla.

La consulta más ejecutada en esta tabla es:

SELECT Id, Date, Value, RV
FROM Data
WHERE Id = @Id
AND Date Between @StartDate AND @StopDate

Ahora tengo que implementar la recuperación incremental de datos en un subconjunto de ID, incluidas las actualizaciones.
Luego usé un esquema de solicitud en el que la persona que llama proporciona una versión de fila específica, recupera un bloque de datos y usa el valor máximo de versión de fila de los datos devueltos para la llamada posterior.

He escrito este procedimiento:

CREATE TYPE guid_list_tbltype AS TABLE (Id uniqueidentifier not null primary key)
CREATE PROCEDURE GetData
    @Ids guid_list_tbltype READONLY,
    @Cursor rowversion,
    @MaxRows int
AS
BEGIN
    SELECT A.* 
    FROM (
        SELECT 
            Data.Id,
            Date,
            Value,
            RV,
            ROW_NUMBER() OVER (ORDER BY RV) AS RN
        FROM Data
             inner join (SELECT Id FROM @Ids) Ids ON Ids.Id = Data.Id
        WHERE RV > @Cursor
    ) A 
    WHERE RN <= @MaxRows
END

Donde @MaxRowsoscilará entre 500,000 y 2,000,000 dependiendo de qué tan fragmentado el cliente quiera sus datos.


He intentado diferentes enfoques:

  1. Indización en (Id, RV):
    CREATE NONCLUSTERED INDEX IDX_IDRV ON Data(Id, RV) INCLUDE(Date, Value);

Utilizando el índice, la consulta busca las filas en las que RV = @Cursorpara cada uno Idde @Ids, leen las siguientes filas a continuación, se funden el resultado y tipo.
La eficiencia depende entonces de la posición relativa del @Cursorvalor.
Si está cerca del final de los datos (ordenado por RV), la consulta es instantánea y, si no, la consulta puede demorar hasta minutos (nunca permita que se ejecute hasta el final).

El problema con este enfoque es que @Cursorestá cerca del final de los datos y el orden no es doloroso (ni siquiera es necesario si la consulta devuelve menos filas que @MaxRows) o está más atrás y la consulta tiene que ordenar las @MaxRows * LEN(@Ids)filas.

  1. Indexación en RV:
    CREATE NONCLUSTERED INDEX IDX_RV ON Data(RV) INCLUDE(Id, Date, Value);

Usando el índice, la consulta busca la fila donde RV = @Cursorluego lee cada fila descartando los Ids no solicitados hasta que llegue @MaxRows.
La eficiencia entonces depende del% de Ids solicitados ( LEN(@Ids) / COUNT(DISTINCT Id)) y su distribución.
Más% Id solicitado significa menos filas descartadas, lo que significa lecturas más eficientes,% Id menos solicitado significa más filas descartadas, lo que significa más lecturas para la misma cantidad de filas resultantes.

El problema con este enfoque es que si los ID solicitados contienen solo unos pocos elementos, es posible que tenga que leer todo el índice para obtener las filas deseadas.

  1. Uso de índice filtrado o vistas indizadas
    CREATE NONCLUSTERED INDEX IDX_RVClient1 ON Data(Id, RV) INCLUDE(Date, Value)
    WHERE Id IN (/* list of Ids for specific client*/);

O

    CREATE VIEW vDataClient1 WITH SCHEMABINDING
    AS
    SELECT
        Id,
        Date,
        Value,
        RV
    FROM dbo.Data
    WHERE Id IN (/* list of Ids for specific client*/)
    CREATE UNIQUE CLUSTERED INDEX IDX_IDRV ON vDataClient1(Id, Rv);

Este método permite una indexación perfectamente eficiente y planes de ejecución de consultas, pero viene con desventajas: 1. Prácticamente, tendré que implementar SQL dinámico para crear los índices o vistas y modificar el procedimiento de solicitud para usar el índice o vista correctos. 2. Tendré que mantener un índice o vista por cliente existente, incluido el almacenamiento. 3. Cada vez que un cliente tendrá que modificar su lista de ID solicitados, tendré que soltar el índice o la vista y volver a crearlo.


Parece que no puedo encontrar un método que se adapte a mis necesidades.
Estoy buscando mejores ideas para implementar la recuperación incremental de datos. Esas ideas podrían implicar reelaborar el esquema de solicitud o el esquema de la base de datos, aunque preferiría un mejor enfoque de indexación si existe.


Crosspost con stackoverflow.com/questions/11586004/… . Eliminé la versión de Oracle por el momento porque descubrí que ORA_ROWSCN no es indexable (y apenas a través de vistas materializadas indexadas).
Paciv

¿Cómo encaja el campo de fecha? ¿Se puede actualizar una fila con un ID y una Fecha particulares en la tabla? Y si es así, ¿también se actualiza la fecha (como una marca de tiempo adicional?)
8kb

Parece que para el intento de GetData (), el orden debe incluir el Id (ordenar por RV, Id). ¿Puedes comentar sobre el uso de un índice de (Rv, Id)? También usar ">" max rowversion de la llamada anterior parece que perderá registros entre fragmentos si las filas tienen la misma versión de fila (¿no es eso posible?).
crokusek

@ 8kb: las declaraciones de actualización que se ejecutan en la tabla solo modifican la Valuecolumna. @crokusek: No ordenar por RV, ID en lugar de RV solo aumentará la carga de trabajo de clasificación sin ningún beneficio, no entiendo el razonamiento detrás de su comentario. Por lo que he leído, RV debería ser único a menos que inserte datos específicamente en esa columna, lo que no hace la aplicación.
Paciv

¿Puede el cliente aceptar los resultados en orden (Id, Rv) y proporcionar un argumento LastId además del argumento LastRowVersion para eliminar la ordenación de RV entre los identificadores? Mis comentarios anteriores se basaron en el supuesto de que RV tenía duplicados. El índice filtrado por cliente parecía interesante.
crokusek

Respuestas:


5

Una solución es que la aplicación del cliente recuerde el máximo rowversionpor ID. El tipo de tabla definida por el usuario cambiaría a:

CREATE TYPE
    dbo.guid_list_tbltype
AS TABLE 
    (
    Id      uniqueidentifier PRIMARY KEY, 
    LastRV  rowversion NOT NULL
    );

La consulta en el procedimiento puede reescribirse para usar el APPLYpatrón (consulte mis artículos de SQLServerCentral parte 1 y parte 2 : se requiere inicio de sesión gratuito). La clave para un buen rendimiento aquí es ORDER BY: evita la búsqueda previa desordenada en la unión de bucles anidados. Esto RECOMPILEes necesario para permitir que el optimizador vea la cardinalidad de la variable de tabla en el momento de la compilación (probablemente resultando en un plan paralelo deseable).

ALTER PROCEDURE dbo.GetData

    @IDs        guid_list_tbltype READONLY,
    @MaxRows    bigint

AS
BEGIN

    SELECT TOP (@MaxRows)
        d.Id,
        d.[Date],
        d.Value,
        d.RV
    FROM @Ids AS i
    CROSS APPLY
    (
        SELECT
            d.*
        FROM dbo.Data AS d
        WHERE
            d.Id = i.Id
            AND d.RV > i.LastRV
    ) AS d
    ORDER BY
        i.Id,
        d.RV
    OPTION (RECOMPILE);

END;

Debería obtener un plan de consulta posterior a la ejecución como este (el plan estimado será serial):

plan de consulta


Correcto, una de las soluciones de cambio de diseño es hacer que el cliente recuerde el MAX(RV)por Id (o un sistema de suscripción donde la aplicación interna recuerda todos los pares Id / RV) y yo uso este patrón para otro cliente. Otra solución era obligar al cliente a recuperar siempre todos los ID (lo que hace que el problema de indexación sea trivial). Todavía no cubre la pregunta necesidad particular: recuperación incremental de un subconjunto de ID con un solo contador global proporcionado por el cliente.
Paciv el

2

Si es posible, rediseñaría la mesa. Si podemos tener VersionNumber como un entero incremental sin espacios, que la tarea de recuperar el siguiente fragmento es un escaneo de rango totalmente trivial. Todo lo que necesitamos es el siguiente índice:

CREATE NONCLUSTERED INDEX IDX_IDRV ON Data(Id, VersionNumber) INCLUDE(Date, Value);

Por supuesto, debemos asegurarnos de que VersionNumber comience con uno y no tenga huecos. Esto es fácil de hacer con restricciones.


¿Te refieres a un global o un Id local VersionNumber? En cualquier caso, no puedo ver cómo eso ayudará con la pregunta, ¿podría dar más detalles?
Paciv el

0

Lo que hubiera hecho:

En este caso, su PK debe ser un campo de identidad "clave sustituta" que se incremente automáticamente.
Como ya está en los miles de millones, sería mejor ir con un BigInt.
Llamémoslo DataID .
Esta voluntad:

  • Agregue 8 bytes a cada registro en su índice agrupado.
  • Ahorre 16 bytes en cada registro en cada índice no agrupado.
  • Lo que tenía era una "Clave natural": un UniqueIdentifyer (16 bytes) con una fecha y hora (8 bytes).
  • ¡Eso es 24 bytes en cada registro de índice para hacer referencia al índice agrupado!
  • Es por eso que tenemos claves sustitutas como enteros incrementales más pequeños.


Configure su nuevo BigInt PK ( DataID ) para usar un índice agrupado :
Esto:

  • Asegúrese de que los registros creados más recientemente se colocan cerca del final.
  • Permita una indexación más rápida con otros índices no agrupados.
  • Permitir la expansión futura como un FK a otras tablas.


Cree un índice no agrupado alrededor (Fecha, Id).
Esta voluntad:

  • Acelere sus consultas más utilizadas.
  • Puede agregar "Valor", pero aumentará el tamaño de su índice, lo que lo hace más lento.
  • Sugeriría probarlo dentro y fuera del índice para ver si hay una gran diferencia en el rendimiento.
  • Recomiendo no usar "Incluir" si lo agrega.
  • Simplemente agregue algo así (Fecha, Id, Valor), pero solo si su prueba muestra que mejora el rendimiento.


Cree un índice no agrupado en (RV, ID).
Esta voluntad:

  • Mantenga siempre sus índices lo más pequeños posible.
  • A menos que note incrementos enormes en el rendimiento al tener la Fecha y el Valor en sus Índices, le sugiero que los deje fuera para ahorrar espacio en disco. Pruébalo sin ellos primero.
  • Si agrega Fecha o Valor, no use "Incluir", en su lugar agréguelos al orden del Índice.
  • Gracias al incremento de DataID en nuevas inserciones en su PK en clúster, sus RV recientes generalmente aparecerán cerca del final (a menos que esté actualizando franjas de datos del pasado todo el tiempo).
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.