Índice de DocumentUsers (versão 2.21.0)
Esta página explica a otimização da listagem de documentos por usuário (SIG-780), introduzida na versão 2.21.0, e o que acontece quando uma instância atualiza de uma versão anterior à 2.21.0 para a 2.21.0 ou superior, incluindo a 3.x. Instâncias que já estão na 2.21.0 ou superior já passaram por esse processo e não são afetadas.
Em tabelas DocumentUsers muito grandes, o processo gera um período de maior lentidão e maior consumo de banco de dados depois da atualização. É fortemente recomendado fazer a atualização em um horário de pouco uso. A aplicação continua funcionando durante o processo.
O que essa otimização melhora
A listagem de documentos de um usuário precisa filtrar e ordenar por situação e data de atualização do documento. Para evitar um JOIN com a tabela Documents a cada listagem, o Signer copia essas duas informações para a tabela DocumentUsers (colunas DocumentStatusCode e DocumentUpdateDateUtc) e cria um índice de cobertura sobre elas:
IX_DocumentUsers_UserId_DocumentStatusCode_DocumentUpdateDateUtc
chaves: UserId, DocumentStatusCode, DocumentUpdateDateUtc (decrescente)
includes: DocumentId, IsPending, SignerId, ApproverId, SignRuleId, ApproverRuleId
Enquanto o preenchimento e o índice não terminam, a listagem de documentos fica mais lenta, e documentos anteriores à atualização podem aparecer fora de ordem ou não aparecer na listagem por usuário.
Como o processo automático funciona
O processo tem três etapas, e só a primeira acontece durante a migração:
- Migração (
DenormalizeDocumentUserWithTrigger). Executada na primeira inicialização. Adiciona as duas colunas (vazias) emDocumentUsers, remove o índice antigoIX_DocumentUsers_UserIde cria o triggerTR_Documents_SyncDocumentUsersemDocuments, que mantém as colunas sincronizadas a partir daí. É rápida: não preenche os dados existentes e não cria o índice novo, para não bloquear a atualização. - Enfileiramento na inicialização. O
DocumentUserBackfillStarterverifica a configuraçãoDocumentUserDenormalizationCompletede a existência do índice. Se uma das duas condições não for atendida, enfileira no Hangfire oDocumentUserDenormalizationJob(fila Low, até 3 tentativas). Isso se repete a cada inicialização enquanto o processo não estiver completo. - Job
DocumentUserDenormalizationJob. Executa, nesta ordem:- Preenchimento (backfill) das colunas das linhas existentes, em lotes percorrendo a chave
Id(tamanho do lote emDocumentUserDenormalizationBatchSize, padrão10000; timeout de cada lote emDocumentUserDenormalizationBackfillCommandTimeoutSeconds, padrão300). A cada 20 lotes o job grava o ponto de progresso (DocumentUserDenormalizationBackfillCursor), então uma nova tentativa continua de onde parou. - Marca a conclusão, gravando
DocumentUserDenormalizationCompleted = Truena tabelaSettings. - Cria o índice
IX_DocumentUsers_UserId_DocumentStatusCode_DocumentUpdateDateUtc, com timeout deDocumentUserDenormalizationIndexCommandTimeoutSeconds(padrão7200). A criação é ONLINE (não bloqueia a tabela) no Azure SQL Database, SQL Managed Instance e SQL Server Enterprise ou Developer, e OFFLINE nas demais edições. O índice só é criado depois do preenchimento, para que as duas operações não disputem a tabela.
- Preenchimento (backfill) das colunas das linhas existentes, em lotes percorrendo a chave
Se a criação ONLINE falhar por qualquer erro de SQL, inclusive timeout, o job tenta criar o índice de forma OFFLINE, o que bloqueia a tabela DocumentUsers durante toda a criação. Em tabelas grandes, evite esse cenário aumentando o timeout antes da atualização ou criando o índice manualmente (veja abaixo).
Como acompanhar
- No painel do Hangfire (
/hangfire), acompanhe oDocumentUserDenormalizationJob. Quando termina, ele aparece como concluído. - Nos logs da aplicação, procure por
Backfill DocumentUser denormalization,Creating IX_DocumentUsers_...ecreated successfully. - No banco de dados:
-- Preenchimento concluído? (deve retornar 'True')
SELECT [Value] FROM Settings
WHERE [Key] = 'DocumentUserDenormalizationCompleted' AND AgentId IS NULL AND SubscriptionId IS NULL;
-- Linhas ainda sem preenchimento (deve retornar 0)
SELECT COUNT_BIG(*) FROM DocumentUsers WHERE DocumentStatusCode IS NULL OR DocumentUpdateDateUtc IS NULL;
-- Índice criado?
SELECT name, is_disabled, fill_factor FROM sys.indexes
WHERE name = 'IX_DocumentUsers_UserId_DocumentStatusCode_DocumentUpdateDateUtc' AND object_id = OBJECT_ID('DocumentUsers');
O que fazer se o processo automático falhar
O job falhou se ele esgotou as 3 tentativas no Hangfire, ou se os logs mostram timeout ou deadlock (erro SQL 1205). Siga os passos na ordem, e pare assim que o processo concluir.
1. Ajustar as configurações e reiniciar
Se a falha foi timeout, aumente as configurações abaixo (veja Configurações General) e reinicie a aplicação. O job é enfileirado novamente na inicialização e, se o preenchimento estava em andamento, continua do último ponto gravado.
DocumentUserDenormalizationBatchSize: reduza para diminuir o tempo de bloqueio de cada lote em instâncias com tráfego ativo.DocumentUserDenormalizationBackfillCommandTimeoutSeconds: timeout de cada lote do preenchimento.DocumentUserDenormalizationIndexCommandTimeoutSeconds: timeout da criação do índice.
2. Preencher manualmente as colunas
Se o preenchimento não concluir, execute o script abaixo no banco de dados. Ele percorre a tabela pela chave Id (clustered) em um único passo progressivo, em lotes, e mostra o progresso. Pode ser interrompido e reexecutado sem prejuízo.
SET NOCOUNT ON;
DECLARE @last uniqueidentifier = '00000000-0000-0000-0000-000000000000';
DECLARE @step int = 5000, @rows int = 1;
DECLARE @totalDone bigint = 0, @batch int = 0;
DECLARE @t0 datetime2 = SYSUTCDATETIME();
DECLARE @msg nvarchar(200);
WHILE @rows > 0 BEGIN
;WITH batch AS (
SELECT TOP (@step) Id
FROM DocumentUsers
WHERE Id > @last
ORDER BY Id
)
UPDATE du
SET du.DocumentStatusCode = d.StatusCode,
du.DocumentUpdateDateUtc = d.UpdateDateUtc
FROM DocumentUsers du WITH (ROWLOCK)
INNER JOIN batch b ON b.Id = du.Id
INNER JOIN Documents d ON d.Id = du.DocumentId
WHERE du.DocumentStatusCode IS NULL OR du.DocumentUpdateDateUtc IS NULL;
DECLARE @updated int = @@ROWCOUNT;
SET @totalDone += @updated;
SET @batch += 1;
SELECT @last = MAX(Id), @rows = COUNT(*)
FROM (SELECT TOP (@step) Id FROM DocumentUsers WHERE Id > @last ORDER BY Id) x;
SET @msg = CONCAT('Lote ', @batch, ' | atualizadas ', @updated, ' | total ', @totalDone,
' | ', DATEDIFF(second, @t0, SYSUTCDATETIME()), 's | last=', CONVERT(char(36), @last));
SET @msg = REPLACE(@msg, '%', '%%');
RAISERROR(@msg, 0, 1) WITH NOWAIT;
END
RAISERROR('Preenchimento concluido.', 0, 1) WITH NOWAIT;
Depois, confirme que a contagem de linhas sem preenchimento é 0 (consulta acima) e grave a conclusão, para que o job não repita o preenchimento:
IF EXISTS (SELECT 1 FROM Settings WHERE [Key] = 'DocumentUserDenormalizationCompleted' AND AgentId IS NULL AND SubscriptionId IS NULL)
UPDATE Settings SET [Value] = 'True'
WHERE [Key] = 'DocumentUserDenormalizationCompleted' AND AgentId IS NULL AND SubscriptionId IS NULL;
ELSE
INSERT INTO Settings (Id, [Key], [Value], AgentId, SubscriptionId) VALUES (NEWID(), 'DocumentUserDenormalizationCompleted', 'True', NULL, NULL);
3. Criar o índice manualmente
Só depois de confirmar que o preenchimento terminou. Faça isso em uma janela de baixo tráfego e em uma única sessão: duas criações ONLINE simultâneas na mesma tabela entram em deadlock na troca final de locks, e o IF NOT EXISTS não protege contra execução concorrente. Em edições sem suporte a ONLINE (SQL Server Standard e Express), use ONLINE = OFF, que bloqueia a tabela durante a criação.
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_DocumentUsers_UserId_DocumentStatusCode_DocumentUpdateDateUtc' AND object_id = OBJECT_ID('DocumentUsers'))
CREATE INDEX [IX_DocumentUsers_UserId_DocumentStatusCode_DocumentUpdateDateUtc] ON [DocumentUsers] ([UserId] ASC, [DocumentStatusCode] ASC, [DocumentUpdateDateUtc] DESC)
INCLUDE ([DocumentId], [IsPending], [SignerId], [ApproverId], [SignRuleId], [ApproverRuleId])
WITH (ONLINE = ON, FILLFACTOR = 80, SORT_IN_TEMPDB = ON);
Cuidados:
- Deadlock. O build ONLINE disputa locks com as escritas da aplicação e pode causar deadlock (erro 1205); a vítima pode ser uma transação da aplicação em vez do build. Depois da criação, verifique nos logs da aplicação se houve erros 1205 no período.
- Azure SQL Database. A criação de índice é sempre registrada integralmente no log de transações, então o tempo depende do limite de log do nível de serviço. Níveis baixos, como o S7 de 800 DTU, sofrem throttle (espera
LOG_RATE_GOVERNOR; a esperaCXCONSUMERé benigna). Em uma instância com cerca de 129 milhões de linhas emDocumentUsersno S7, a criação levou muito tempo e uma das tentativas terminou em deadlock. Se puder, escale o nível de serviço antes de iniciar o build, pois trocar o nível no meio derruba a criação em andamento. - Criação retomável (opcional). No SQL Server 2019 ou superior e no Azure SQL, adicione
RESUMABLE = ONaoWITHpara que uma interrupção não descarte o progresso (consultesys.index_resumable_operationse useALTER INDEX ... RESUMEouABORT).RESUMABLE = ONnão pode ser combinado comSORT_IN_TEMPDB = ON; remova essa opção.
4. Confirmar e reiniciar
Execute as consultas da seção Como acompanhar: a conclusão deve estar True, nenhuma linha pendente e o índice presente. Reinicie a aplicação. Com a conclusão gravada e o índice criado, o DocumentUserBackfillStarter não enfileira mais o job.