Um dos aplicativos da Web em que estou trabalhando usa NLog para registrar informações de contexto de depuração e erro no banco de dados. Basicamente, ele executa um número moderado de inserções (aprecio algumas dezenas de milhares por dia) usando o seguinte padrão:
<commandText>
insert into dbo.nlog
(log_date, log_level_id, log_level, logger, log_message, machine_name, log_user_name, call_site, thread, exception, stack_trace, full_exception_info)
values(@timestamp, dbo.func_get_nlog_level_id(@level), @level, @logger, @message, @machinename, @username, @call_site, @threadid, @log_exception, @stacktrace, @FullExceptionInfo);
</commandText>
<parameter name="@timestamp" layout="${longdate}"/>
<parameter name="@level" layout="${level}"/>
<parameter name="@logger" layout="${logger}"/>
<parameter name="@message" layout="${message}"/>
<parameter name="@machinename" layout="${machinename}"/>
<parameter name="@username" layout="${windows-identity:domain=true}"/>
<parameter name="@call_site" layout="${callsite:filename=true}"/>
<parameter name="@threadid" layout="${threadid}"/>
<parameter name="@log_exception" layout="${exception}"/>
<parameter name="@stacktrace" layout="${stacktrace}"/>
<parameter name="@FullExceptionInfo" layout="${gdc:FullExceptionInfo}"/>
Para minimizar o impacto do registro, as consultas do banco de dados são emitidas de forma assíncrona (em um encadeamento diferente). No entanto, tenho que ter cuidado para não ficar sem threads do Thread Pool.
Para melhor desempenho na consulta do log, coloquei dois índices para as colunas mais utilizadas, log_data e log_user_name . No entanto, entendo que isso terá um impacto negativo no desempenho das inserções. Há também um índice clusterizado em enter_date , conforme mostrado por sp_help
:
IX_nlog_entered_date clustered located on PRIMARY entered_date
P1: tudo bem ter esses índices ou é melhor não tê-los e sofrer a penalidade por raramente consultar a tabela? Ou talvez haja uma abordagem melhor.
A consulta é feita usando consultas simples como as seguintes:
-- just see the latest logged activity
SELECT TOP 1000 *
FROM nlog
ORDER BY nlog_id DESC
ou assim:
SELECT TOP 200*
FROM nlog
WHERE log_user_name = 'domain\username'
ORDER BY nlog_id DESC
Claramente, isso pode bloquear a tabela durante a execução, atrasando algumas inserções. Acho que usar WITH(NOLOCK)
deveria ser uma boa opção, mas muitas vezes as pessoas se esquecem disso.
P2: como posso minimizar o impacto da leitura na mesa? Estou pensando em negar o acesso de leitura à tabela e, em vez disso, criar um procedimento armazenado para realizar a leitura com NOLOCK
, mas isso leva a mais complexidade.
Depois de um tempo, os registros antigos devem ser excluídos. Pelo que sei, excluir muitas linhas de tabelas grandes é uma consulta pesada. A aplicação Web tem um período designado (à noite) para realizar trabalhos de manutenção, mas gostaria de melhorar esta etapa. Então, a terceira pergunta:
P3: como posso minimizar o impacto de grandes exclusões? . Estou pensando em particionar tabelas por entered_date
(tem o padrão deGETDATE()
), mas não sei se é uma boa ideia.
Definições de tabelas e índices
CREATE TABLE [dbo].[nlog](
[nlog_id] [int] IDENTITY(1,1) NOT NULL,
[entered_date] [datetime2](7) NOT NULL CONSTRAINT [DF_nlog_log_time] DEFAULT (getdate()),
[log_app_name] [nvarchar](255) NULL,
[log_date] [nvarchar](64) NULL,
[log_level_id] [tinyint] NOT NULL,
[log_level] [nvarchar](64) NULL,
[logger] [nvarchar](1024) NULL,
[log_message] [nvarchar](max) NULL,
[machine_name] [nvarchar](255) NULL,
[log_user_name] [nvarchar](255) NULL,
[call_site] [nvarchar](4000) NULL,
[thread] [nvarchar](255) NULL,
[exception] [nvarchar](max) NULL,
[stack_trace] [nvarchar](max) NULL,
[full_exception_info] [nvarchar](max) NULL,
CONSTRAINT [PK_nlog] PRIMARY KEY NONCLUSTERED
(
[nlog_id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 95) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
CREATE CLUSTERED INDEX [IX_nlog_entered_date] ON [dbo].[nlog]
(
[entered_date] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 95) ON [PRIMARY]
GO
Índices
A decisão de adicionar um ou mais índices não clusterizados é uma avaliação que só você pode fazer com precisão, com base nos resultados do teste e levando em consideração suas prioridades locais. Dito isso, o impacto no desempenho de adicionar um pequeno número de índices não clusterizados estreitos geralmente é pequeno, do ponto de vista do banco de dados.
De forma mais geral, a indexação deve ser orientada por prioridades, opções de design e carga de trabalho. Seu esquema parece diferir do padrão NLog por ter nlog_id como uma chave primária não clusterizada, com o índice clusterizado em enter_date . Supondo que essa foi uma decisão deliberada, a maioria das consultas na tabela deve ser baseada em um intervalo de datas, em vez dos
top (n) ... order by nlog_id
exemplos fornecidos na pergunta. Seus exemplos provavelmente prefeririam uma chave primária clusterizada e um índice não clusterizado na data_entrada .Com 50.000 linhas por dia, a tabela cresce apenas cerca de 18 milhões de linhas por ano. Este é um número muito pequeno no esquema das coisas. A pergunta não menciona explicitamente nenhum problema de desempenho atual, seja para leituras ou gravações.
Leitura
O acesso a consultas ad hoc é difícil de gerenciar. Com acesso direto à tabela, não há nada que impeça que alguém escreva um desastre de uma consulta (por exemplo, uma junção cruzada paralela acidental) que possa afetar a instância como um todo.
Fornecer acesso apenas por meio de exibições, procedimentos armazenados e funções com valor de tabela embutida geralmente é uma ideia muito melhor. Se a tabela de log for apenas anexada fora das janelas de manutenção, pode ser apropriado usar o nível de isolamento de leitura não confirmada (explicitamente) nos novos módulos de acesso a dados.
Como alternativa, se o aplicativo puder tolerar uma alteração da implementação de bloqueio padrão de isolamento de leitura confirmada, você pode verificar a habilitação da implementação de controle de versão de linha (MVCC) conhecida como isolamento de instantâneo de leitura confirmada (RCSI) no SQL Server. Esta não é uma mudança a ser feita levianamente.
Exclusões grandes
Não é possível dizer se o particionamento é a solução certa para você com base nas informações fornecidas na pergunta. O principal benefício do particionamento é permitir a exclusão ou arquivamento praticamente instantâneo de toda a partição . Existem complexidades e custos envolvidos no lado da manutenção para conseguir isso, e o particionamento pode ter efeitos complexos nos planos de execução de consultas existentes que precisariam ser testados.
Se a exclusão/arquivamento for relativamente regular e realizada durante uma janela de manutenção, a abordagem simples pode ser a melhor. A exclusão sempre pode ser executada em lotes de tamanho adequado, com backups de log de transação entre os backups conforme necessário.
Inserções
Se o sistema puder tolerar um pequeno atraso antes que os dados de log apareçam na tabela, as inserções provavelmente devem ser agrupadas, talvez usando os próprios recursos de buffer do NLog. Eu não uso o NLog, mas a documentação sugere várias opções de buffer que você deve procurar.
Dada a taxa atual de inserções, isso provavelmente não é necessário do ponto de vista do banco de dados. Dito isso, inserir (digamos) cem linhas em uma única transação será mais eficiente do que cem transações separadas inserindo uma linha por vez.
Ler e escrever muito em uma tabela pode ser difícil de manter o desempenho. Os índices podem realmente prejudicar o desempenho da inserção e o bloqueio também é um aspecto importante a ser lembrado.
Existe outra maneira de corrigir esses problemas - não grave diretamente no banco de dados. Por exemplo, grave as mensagens no MSMQ e use um serviço do Windows para inseri-las (em lote) no banco de dados. Então índices, bloqueio e exclusão não são mais problemas.