Eu escrevi um aplicativo com um back-end do SQL Server que coleta e armazena e uma quantidade extremamente grande de registros. Eu calculei que, no pico, a quantidade média de registros está em algum lugar na faixa de 3-4 bilhões por dia (20 horas de operação).
Minha solução original (antes de eu ter feito o cálculo real dos dados) era fazer meu aplicativo inserir registros na mesma tabela que é consultada por meus clientes. Isso travou e queimou com bastante rapidez, obviamente, porque é impossível consultar uma tabela que está com tantos registros inseridos.
Minha segunda solução foi usar 2 bancos de dados, um para dados recebidos pelo aplicativo e outro para dados prontos para o cliente.
Meu aplicativo receberia dados, dividiria em lotes de ~ 100k registros e seria inserido em massa na tabela de preparação. Depois de ~ 100k registros, o aplicativo criaria outra tabela temporária com o mesmo esquema de antes e começaria a inseri-la nessa tabela. Ele criaria um registro em uma tabela de tarefas com o nome da tabela que possui 100 mil registros e um procedimento armazenado no lado do SQL Server moveria os dados da (s) tabela (s) de preparo para a tabela de produção pronta para o cliente e soltaria o tabela tabela temporária criada pelo meu aplicativo.
Ambos os bancos de dados têm o mesmo conjunto de 5 tabelas com o mesmo esquema, exceto o banco de dados intermediário que possui a tabela de tarefas. O banco de dados temporário não possui restrições de integridade, chave, índices etc ... na tabela em que a maior parte dos registros residirá. Mostrado abaixo, o nome da tabela é SignalValues_staging
. O objetivo era fazer com que meu aplicativo introduzisse os dados no SQL Server o mais rápido possível. O fluxo de trabalho de criação de tabelas em tempo real para que elas possam ser facilmente migradas funciona muito bem.
A seguir, são apresentadas as 5 tabelas relevantes do meu banco de dados temporário, além da minha tabela de tarefas:
O procedimento armazenado que escrevi lida com a movimentação dos dados de todas as tabelas temporárias e a inserção na produção. Abaixo está a parte do meu procedimento armazenado que insere na produção a partir das tabelas de preparação:
-- Signalvalues jobs table.
SELECT *
,ROW_NUMBER() OVER (ORDER BY JobId) AS 'RowIndex'
INTO #JobsToProcess
FROM
(
SELECT JobId
,ProcessingComplete
,SignalValueStagingTableName AS 'TableName'
,(DATEDIFF(SECOND, (SELECT last_user_update
FROM sys.dm_db_index_usage_stats
WHERE database_id = DB_ID(DB_NAME())
AND OBJECT_ID = OBJECT_ID(SignalValueStagingTableName))
,GETUTCDATE())) SecondsSinceLastUpdate
FROM SignalValueJobs
) cte
WHERE cte.ProcessingComplete = 1
OR cte.SecondsSinceLastUpdate >= 120
DECLARE @i INT = (SELECT COUNT(*) FROM #JobsToProcess)
DECLARE @jobParam UNIQUEIDENTIFIER
DECLARE @currentTable NVARCHAR(128)
DECLARE @processingParam BIT
DECLARE @sqlStatement NVARCHAR(2048)
DECLARE @paramDefinitions NVARCHAR(500) = N'@currentJob UNIQUEIDENTIFIER, @processingComplete BIT'
DECLARE @qualifiedTableName NVARCHAR(128)
WHILE @i > 0
BEGIN
SELECT @jobParam = JobId, @currentTable = TableName, @processingParam = ProcessingComplete
FROM #JobsToProcess
WHERE RowIndex = @i
SET @qualifiedTableName = '[Database_Staging].[dbo].['+@currentTable+']'
SET @sqlStatement = N'
--Signal values staging table.
SELECT svs.* INTO #sValues
FROM '+ @qualifiedTableName +' svs
INNER JOIN SignalMetaData smd
ON smd.SignalId = svs.SignalId
INSERT INTO SignalValues SELECT * FROM #sValues
SELECT DISTINCT SignalId INTO #uniqueIdentifiers FROM #sValues
DELETE c FROM '+ @qualifiedTableName +' c INNER JOIN #uniqueIdentifiers u ON c.SignalId = u.SignalId
DROP TABLE #sValues
DROP TABLE #uniqueIdentifiers
IF NOT EXISTS (SELECT TOP 1 1 FROM '+ @qualifiedTableName +') --table is empty
BEGIN
-- processing is completed so drop the table and remvoe the entry
IF @processingComplete = 1
BEGIN
DELETE FROM SignalValueJobs WHERE JobId = @currentJob
IF '''+@currentTable+''' <> ''SignalValues_staging''
BEGIN
DROP TABLE '+ @qualifiedTableName +'
END
END
END
'
EXEC sp_executesql @sqlStatement, @paramDefinitions, @currentJob = @jobParam, @processingComplete = @processingParam;
SET @i = @i - 1
END
DROP TABLE #JobsToProcess
Uso sp_executesql
porque os nomes das tabelas temporárias são apresentados como texto dos registros na tabela de tarefas.
Esse procedimento armazenado é executado a cada 2 segundos usando o truque que aprendi nesta postagem do dba.stackexchange.com .
O problema que não consigo resolver por toda a vida é a velocidade com que as inserções na produção são executadas. Meu aplicativo cria tabelas temporárias de preparação e as preenche com registros incrivelmente rapidamente. A inserção na produção não pode acompanhar a quantidade de tabelas e, eventualmente, há um excesso de tabelas na casa dos milhares. A única maneira de conseguir acompanhar os dados recebidos é remover todas as chaves, índices, restrições, etc ... na SignalValues
tabela de produção . O problema que enfrento é que a tabela termina com tantos registros que se torna impossível consultar.
Eu tentei particionar a tabela usando o [Timestamp]
como uma coluna de particionamento sem sucesso. Qualquer forma de indexação diminui tanto as inserções que elas não conseguem acompanhar. Além disso, eu precisaria criar milhares de partições (uma a cada minuto? Hora?) Anos de antecedência. Eu não conseguia descobrir como criá-los em tempo real
Eu tentei criar particionamento, adicionando uma coluna computada para a mesa chamado TimestampMinute
cujo valor era, em INSERT
, DATEPART(MINUTE, GETUTCDATE())
. Ainda muito lento.
Tentei torná-lo uma tabela com otimização de memória, de acordo com este artigo da Microsoft . Talvez eu não entenda como fazê-lo, mas o MOT tornou as inserções mais lentas de alguma forma.
Verifiquei o Plano de Execução do procedimento armazenado e descobri que (acho?) A operação mais intensiva é
SELECT svs.* INTO #sValues
FROM '+ @qualifiedTableName +' svs
INNER JOIN SignalMetaData smd
ON smd.SignalId = svs.SignalId
Para mim, isso não faz sentido: adicionei o registro do relógio de parede ao procedimento armazenado que provou o contrário.
Em termos de registro de tempo, essa instrução específica acima é executada em ~ 300ms em 100k registros.
A declaração
INSERT INTO SignalValues SELECT * FROM #sValues
executa em 2500-3000ms em 100k registros. Excluindo da tabela os registros afetados, por:
DELETE c FROM '+ @qualifiedTableName +' c INNER JOIN #uniqueIdentifiers u ON c.SignalId = u.SignalId
leva mais 300ms.
Como posso tornar isso mais rápido? O SQL Server pode lidar com bilhões de registros por dia?
Se for relevante, este é o SQL Server 2014 Enterprise x64.
Configuração de hardware:
Esqueci de incluir hardware na primeira passagem desta pergunta. Minha culpa.
Eu prefácio isso com estas instruções: Eu sei que estou perdendo algum desempenho por causa da minha configuração de hardware. Eu tentei várias vezes, mas por causa do orçamento, do nível C, do alinhamento dos planetas, etc ... não há nada que eu possa fazer para obter uma melhor configuração, infelizmente. O servidor está sendo executado em uma máquina virtual e não posso aumentar a memória porque simplesmente não temos mais.
Aqui estão as informações do meu sistema:
O armazenamento é anexado ao servidor VM via interface iSCSI em uma caixa NAS (isso prejudicará o desempenho). A caixa NAS possui 4 unidades em uma configuração RAID 10. São unidades de disco giratório WD WD4000FYYZ de 4 TB com interface SATA de 6 GB / s. O servidor possui apenas um armazenamento de dados configurado para que tempdb e meu banco de dados estejam no mesmo armazenamento de dados.
O DOP máximo é zero. Devo alterar isso para um valor constante ou apenas deixar o SQL Server lidar com isso? Eu li sobre o RCSI: Estou correto ao supor que o único benefício do RCSI vem com as atualizações de linha? Nunca haverá atualizações para nenhum desses registros específicos, eles serão INSERT
editados e SELECT
editados. O RCSI ainda me beneficiará?
Meu tempdb é 8mb. Com base na resposta abaixo de jyao, alterei os #sValues para uma tabela regular para evitar o tempdb por completo. O desempenho foi praticamente o mesmo. Tentarei aumentar o tamanho e o crescimento do tempdb, mas, como o tamanho de #sValues será mais ou menos sempre do mesmo tamanho, não prevejo muito ganho.
Eu tomei um plano de execução que anexei abaixo. Esse plano de execução é uma iteração de uma tabela intermediária - 100k registros. A execução da consulta foi bastante rápida, em torno de 2 segundos, mas lembre-se de que isso não possui índices na SignalValues
tabela e a SignalValues
tabela, o destino da INSERT
, não possui registros nela.