Enormes dados e desempenho no SQL Server


20

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:

Tabelas de preparação 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_executesqlporque 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 SignalValuestabela 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 TimestampMinutecujo 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:

Informação do 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 INSERTeditados e SELECTeditados. 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 SignalValuestabela e a SignalValuestabela, o destino da INSERT, não possui registros nela.

Plano de execução


3
Você já experimentou durabilidade atrasada?
Martin Smith

2
Quais índices existiam com inserções de produção lentas?
Paparazzo

Até agora, acho que não há dados suficientes aqui para descobrir o que realmente está consumindo tanto tempo. É CPU? É IO? Como você parece estar recebendo 30 mil linhas por segundo, não parece IO para mim. Entendo esse direito que você está bem perto de alcançar seu objetivo de desempenho? Você precisa de 50 mil linhas por segundo, portanto, um lote de 100 mil a cada 2 segundos deve ser suficiente. No momento, um lote parece levar 3 segundos. Lance o plano de execução real de uma execução representativa. Qualquer sugestão que não ataque as operações que consomem mais tempo é discutível.
usr

Publiquei o plano de execução.
Brandon

Respostas:


7

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

Na captura de tela, você SOMENTE tem 8 GB de memória RAM total e 6 GB alocados ao SQL Server. Isso é muito baixo para o que você está tentando alcançar.

Eu sugiro que você atualize a memória para um valor mais alto - 256 GB e aprimore suas CPUs VM também.

Nesse momento, você precisa investir em hardware para sua carga de trabalho.

Consulte também o guia de desempenho para carregamento de dados - ele descreve maneiras inteligentes de carregar os dados com eficiência.

Meu tempdb é 8mb.

Com base na sua edição .. você deve ter um tempdb sensato - de preferência vários arquivos de dados tempdb com o mesmo tamanho e a instância ativada para TF 1117 e 1118.

Eu sugiro que você faça um exame de saúde profissional e comece a partir daí.

Altamente recomendado

  1. Aumente as especificações do servidor.

  2. Faça com que uma pessoa profissional * faça uma verificação de integridade da instância do servidor de banco de dados e siga as recomendações.

  3. Uma vez a. e B. feito, mergulhe no ajuste de consultas e em outras otimizações, como analisar estatísticas de espera, planos de consulta etc.

Nota: Sou um especialista profissional em servidores sql na hackhands.com - uma empresa de visão plural, mas de maneira alguma sugerindo que você me contrate para obter ajuda. Estou apenas sugerindo que você busque ajuda profissional com base apenas em suas edições.

HTH.


Estou tentando montar uma proposta (leia-se: implorando) mais hardware para isso. Com isso em mente e sua resposta aqui, não há mais nada do ponto de vista de configuração ou otimização de consulta do SQL Server que você sugeriria para tornar isso mais rápido?
Brandon

1

Conselho geral para tais problemas com big data, quando de frente para uma parede e nada funciona:

Um ovo será cozido por 5 minutos. 10 ovos serão cozidos ao mesmo tempo, se houver eletricidade e água suficientes.

Ou, em outras palavras:

Primeiro, olhe para o hardware; segundo, separe a lógica do processo (remodelação de dados) e faça-a em paralelo.

É bem possível criar particionamento vertical personalizado de forma dinâmica e automatizada, por contagem de tabelas e por tamanho de tabela; Se eu tiver trimestre_1_2017, trimestre_2_2017, trimestre_3_2017, trimestre_4_2017, trimestre_1_2018 ... e não souber onde estão meus registros e quantas partições tenho, execute as mesmas consultas em todas as partições personalizadas ao mesmo tempo, sessões e montagem separadas o resultado a ser processado para a minha lógica.


O problema do OP parece estar lidando com a inserção e o acesso a dados recém-inseridos, mais do que processando dados de semanas ou meses atrás. O OP menciona o particionamento de dados a cada minuto em seu registro de data e hora (portanto, 60 partições, dividindo os dados atuais em intervalos separados); dividir por quarto provavelmente não ajudaria muito. Seu argumento é bem aceito em geral, mas é improvável que ajude alguém nesta situação específica.
RDFozz

-1

Eu farei a seguinte verificação / otimização:

  1. Certifique-se de que o arquivo de dados e logs do banco de dados de produção não cresça durante a operação de inserção (pré-cresça, se necessário)

  2. Não use

    select * into [dest table] from [source table];

    mas em vez disso, pré-defina a [tabela de destino]. Além disso, em vez de soltar a [tabela de destino] e recriá-la, truncarei a tabela. Dessa forma, se necessário, em vez de usar a tabela temporária, eu usaria a tabela regular. (Também posso criar o índice na [tabela de destino] para facilitar o desempenho da consulta de junção)

  3. Em vez de usar sql dinâmico, prefiro usar nomes de tabela codificados com alguma lógica de codificação para escolher qual tabela operar.

  4. Também monitorarei o desempenho de E / S de memória, CPU e disco para ver se há falta de recursos durante grande carga de trabalho.

  5. Como você mencionou que pode lidar com a inserção soltando os índices no lado da produção, eu verificaria se existem muitas divisões de páginas; nesse caso, diminuiria o fator de preenchimento dos índices e reconstruiria os índices antes de considerar a possibilidade de soltar os índices.

Boa sorte e ame sua pergunta.


Obrigado pela resposta. Eu havia definido o tamanho do banco de dados como 1 GB e aumentado 1 GB, antecipando que as operações de crescimento levariam algum tempo, o que ajudou na velocidade inicialmente. Vou tentar implementar o pré-crescimento hoje. Eu implementei a tabela [dest] como uma tabela regular, mas não vi muito ganho de desempenho. Não tive muito tempo nos últimos dias, mas tentarei chegar aos outros hoje.
Brandon
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.