Filtrando dados ordenados por rowversion

8

Eu tenho uma tabela de dados SQL com a seguinte estrutura:

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)
)

O número de IDs distintos varia de 3000 a 50000.
O tamanho da tabela varia até mais de um bilhão de linhas.
Um ID pode cobrir entre algumas linhas até 5% da tabela.

A única consulta mais executada nesta tabela é:

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

Agora preciso implementar a recuperação incremental de dados em um subconjunto de IDs, incluindo atualizações.
Em seguida, usei um esquema de solicitação no qual o chamador fornece uma versão específica da linha, recupera um bloco de dados e usa o valor máximo da versão da linha dos dados retornados para a chamada subsequente.

Eu escrevi este procedimento:

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

Onde @MaxRowsvariará entre 500.000 e 2.000.000, dependendo de como o cliente desejar os dados.


Eu tentei abordagens diferentes:

  1. Indexação em (Id, RV):
    CREATE NONCLUSTERED INDEX IDX_IDRV ON Data(Id, RV) INCLUDE(Date, Value);

Usando o índice, a consulta buscar as linhas onde RV = @Cursorpara cada Idno @Ids, leia as seguintes linhas em seguida, mesclar o resultado e classificar.
A eficiência depende então da posição relativa do @Cursorvalor.
Se estiver próximo do final dos dados (ordenado por RV), a consulta será instantânea e, se não, a consulta poderá levar alguns minutos (nunca deixe que ela seja executada até o final).

o problema dessa abordagem é que ele @Cursorestá próximo do final dos dados e a classificação não é dolorosa (nem mesmo necessária se a consulta retornar menos linhas do que @MaxRows) ou está mais atrasada e a consulta precisa classificar as @MaxRows * LEN(@Ids)linhas.

  1. Indexação no RV:
    CREATE NONCLUSTERED INDEX IDX_RV ON Data(RV) INCLUDE(Id, Date, Value);

Usando o índice, a consulta procura a linha onde RV = @Cursor, em seguida, lê todas as linhas descartando os IDs não solicitados até atingir @MaxRows.
A eficiência depende da% de IDs solicitados ( LEN(@Ids) / COUNT(DISTINCT Id)) e de sua distribuição.
Id% mais solicitado significa menos linhas descartadas, o que significa leituras mais eficientes, menos% Id id solicitado significa mais linhas descartadas, o que significa mais leituras para a mesma quantidade de linhas resultantes.

O problema dessa abordagem é que, se os IDs solicitados contiverem apenas alguns elementos, talvez seja necessário ler o índice inteiro para obter as linhas desejadas.

  1. Usando índice filtrado ou visualizações indexadas
    CREATE NONCLUSTERED INDEX IDX_RVClient1 ON Data(Id, RV) INCLUDE(Date, Value)
    WHERE Id IN (/* list of Ids for specific client*/);

Ou

    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);

Esse método permite planos de execução de consulta e indexação perfeitamente eficientes, mas apresenta desvantagens: 1. Praticamente, terei que implementar o SQL dinâmico para criar índices ou visualizações e modificar o procedimento de solicitação para usar o índice ou visualização correta. 2. Terei que manter um índice ou visualização pelo cliente existente, incluindo armazenamento. 3. Toda vez que um cliente precisar modificar sua lista de IDs solicitados, terei que eliminar o índice ou visualizar e recriá-lo.


Não consigo encontrar um método que atenda às minhas necessidades.
Estou procurando idéias melhores para implementar a recuperação incremental de dados. Essas idéias podem implicar uma reformulação do esquema solicitante ou do esquema do banco de dados, embora eu prefira uma abordagem de indexação melhor, se houver.

Paciv
fonte
Crosspost com stackoverflow.com/questions/11586004/… . Eu removi a versão Oracle no momento porque descobri que ORA_ROWSCN não é indexável (e dificilmente através de visualizações materializadas indexadas).
Paciv
Como o campo de data se encaixa? Uma linha com um ID e Data específicos pode ser atualizada na tabela? E se assim for, é a data também atualizado (como um timestamp adicional?)
8kb
Parece que, para a tentativa GetData (), a ordem de deve incluir o ID (ordem de RV, ID). Você pode comentar sobre o uso de um índice de (Rv, Id)? Também usando ">" max rowversion da chamada anterior parece que perderá registros entre os blocos se as linhas tiverem a mesma rowversion (isso não é possível?).
crokusek
@ 8kb: as instruções de atualização executadas na tabela modificam apenas a Valuecoluna. @ crokusek: Não pedir por RV, ID em vez de RV apenas aumenta a carga de trabalho de classificação sem nenhum benefício, não entendo o raciocínio por trás do seu comentário. Pelo que li, o RV deve ser único, a menos que insira dados especificamente nessa coluna, o que o aplicativo não faz.
Paciv 8/08
O cliente pode aceitar resultados na ordem (Id, Rv) e fornecer um argumento LastId além do argumento LastRowVersion para eliminar a classificação de RV entre os IDs? Meus comentários anteriores foram todos baseados na suposição de que o RV tinha duplicatas. O índice filtrado por cliente parecia interessante.
crokusek

Respostas:

5

Uma solução é que o aplicativo cliente lembre-se do máximo rowversionpor ID. O tipo de tabela definido pelo usuário mudaria para:

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

A consulta no procedimento pode ser reescrita para usar o APPLYpadrão (consulte meus artigos do SQLServerCentral parte 1 e parte 2 - é necessário fazer login grátis). A chave para um bom desempenho aqui é: ORDER BY- evita a pré-busca não ordenada na junção de loops aninhados. A RECOMPILEé necessária para permitir que o optimizador ver a cardinalidade da variável de tabela no tempo de compilação (provavelmente resultante de um plano paralelo desejável).

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;

Você deve obter um plano de consulta pós-execução como este (o plano estimado será serial):

plano de consulta

Paul White 9
fonte
Certo, uma das soluções de alteração de design é fazer com que o cliente se lembre do MAX(RV)por ID (ou um sistema de assinatura em que o aplicativo interno se lembre de todos os pares de ID / RV) e eu uso esse padrão para outro cliente. Uma outra solução era forçar o cliente a recuperar sempre todos os IDs (o que torna o problema de indexação trivial). Ele ainda não cobre a necessidade específica de pergunta: recuperação incremental de um subconjunto de IDs com apenas um contador global fornecido pelo cliente.
Paciv
2

Se possível, eu reformularia a tabela. Se pudermos ter VersionNumber como um número inteiro incremental sem intervalos, a tarefa de recuperar o próximo pedaço é uma varredura de intervalo totalmente trivial. Tudo o que precisamos é do seguinte índice:

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

Obviamente, precisamos garantir que o VersionNumber comece com um e não tenha lacunas. Isso é fácil de fazer com restrições.

AK
fonte
Você quer dizer um local global ou um ID VersionNumber? Em ambos os casos, não consigo ver como isso ajudará na pergunta. Você poderia elaborar mais?
Paciv 02/11/12
0

O que eu teria feito:

Nesse caso, sua PK deve ser um campo de identidade "Chave substituta" que é incrementado automaticamente.
Como você já está na casa dos bilhões, seria melhor usar um BigInt.
Vamos chamá-lo de DataID .
Isso vai:

  • Adicione 8 bytes a cada registro no seu Índice de Cluster.
  • Economize 16 bytes em todos os registros em todos os índices não agrupados.
  • O que você tinha era uma "Chave Natural": um UniqueIdentifyer (16 bytes) com um DateTime (8 bytes).
  • São 24 bytes em cada registro de índice para fazer referência ao índice de cluster!
  • É por isso que temos Chaves substitutas como números inteiros incrementais menores.


Defina seu novo BigInt PK ( DataID ) para usar um Índice de Cluster
:

  • Verifique se os registros criados mais recentemente são colocados perto do final.
  • Permita uma indexação mais rápida com outros índices não agrupados.
  • Permitir expansão futura como um FK para outras tabelas.


Crie um índice não agrupado em torno de (data, ID).
Isso vai:

  • Acelere suas consultas mais usadas.
  • Você pode adicionar "Valor", mas isso aumentará o tamanho do seu índice, o que o tornará mais lento.
  • Sugiro tentar dentro e fora do Índice para ver se há uma grande diferença no desempenho.
  • Eu recomendo não usar "Incluir" se você o adicionar.
  • Basta seguir o exemplo (Data, ID, Valor) - mas apenas se o teste mostrar que melhora o desempenho.


Crie um índice não clusterizado em (RV, ID).
Isso vai:

  • Sempre mantenha seus índices o menor possível.
  • A menos que você note enormes ganhos de desempenho com a Data e o Valor em seus índices, sugiro que você os deixe de fora para economizar espaço em disco. Experimente sem eles primeiro.
  • Se você adicionar Data ou Valor, não use "Incluir"; adicione-os à ordem do Índice.
  • Graças ao DataID Incrementing em novas inserções no seu PK em cluster, seus RVs recentes geralmente aparecerão perto do fim (a menos que você esteja atualizando faixas de dados do passado o tempo todo).
MikeTeeVee
fonte