---
title: "Índice de DocumentUsers (versão 2.21.0)"
description: "Como o Signer preenche as colunas desnormalizadas e cria o índice da tabela DocumentUsers ao atualizar de uma versão anterior à 2.21.0 para a 2.21.0 ou superior (inclusive a 3.x), e o que fazer se o processo automático falhar."
canonical: "https://docs.lacunasoftware.com/articles/signer/on-premises/documentusers-index"
locale: "pt-BR"
product: "Signer"
last_updated: "2026-10-16"
---

# Í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**.

> **Aviso**
>
> 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:

```text
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:

1. **Migração (`DenormalizeDocumentUserWithTrigger`).** Executada na primeira inicialização. Adiciona as duas colunas (vazias) em `DocumentUsers`, remove o índice antigo `IX_DocumentUsers_UserId` e cria o *trigger* `TR_Documents_SyncDocumentUsers` em `Documents`, 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.
2. **Enfileiramento na inicialização.** O `DocumentUserBackfillStarter` verifica a configuração `DocumentUserDenormalizationCompleted` e a existência do índice. Se uma das duas condições não for atendida, enfileira no Hangfire o `DocumentUserDenormalizationJob` (fila *Low*, até 3 tentativas). Isso se repete a cada inicialização enquanto o processo não estiver completo.
3. **Job `DocumentUserDenormalizationJob`.** Executa, nesta ordem:
   1. **Preenchimento (backfill)** das colunas das linhas existentes, em lotes percorrendo a chave `Id` (tamanho do lote em `DocumentUserDenormalizationBatchSize`, padrão `10000`; timeout de cada lote em `DocumentUserDenormalizationBackfillCommandTimeoutSeconds`, padrão `300`). A cada 20 lotes o job grava o ponto de progresso (`DocumentUserDenormalizationBackfillCursor`), então uma nova tentativa continua de onde parou.
   2. **Marca a conclusão**, gravando `DocumentUserDenormalizationCompleted = True` na tabela `Settings`.
   3. **Cria o índice** `IX_DocumentUsers_UserId_DocumentStatusCode_DocumentUpdateDateUtc`, com timeout de `DocumentUserDenormalizationIndexCommandTimeoutSeconds` (padrão `7200`). 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.

> **Perigo**
>
> 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 o `DocumentUserDenormalizationJob`. Quando termina, ele aparece como concluído.
* Nos logs da aplicação, procure por `Backfill DocumentUser denormalization`, `Creating IX_DocumentUsers_...` e `created successfully`.
* No banco de dados:

```sql
-- 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*](https://docs.lacunasoftware.com/articles/signer/on-premises/settings#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.

```sql
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:

```sql
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.

```sql
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 espera `CXCONSUMER` é benigna). Em uma instância com cerca de 129 milhões de linhas em `DocumentUsers` no 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 = ON` ao `WITH` para que uma interrupção não descarte o progresso (consulte `sys.index_resumable_operations` e use `ALTER INDEX ... RESUME` ou `ABORT`). `RESUMABLE = ON` não pode ser combinado com `SORT_IN_TEMPDB = ON`; remova essa opção.

### 4. Confirmar e reiniciar

Execute as consultas da seção [Como acompanhar](https://docs.lacunasoftware.com/articles/signer/on-premises/documentusers-index#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.
