Como implementar um algoritmo baseado em conjunto / UDF

13

Eu tenho um algoritmo que eu preciso executar em todas as linhas de uma tabela com 800K linhas e 38 colunas. O algoritmo é implementado no VBA e faz um monte de cálculos usando valores de algumas colunas para manipular outras colunas.

Atualmente, estou usando o Excel (ADO) para consultar o SQL e usar o VBA com cursores do lado do cliente para aplicar o algoritmo por loop em todas as linhas. Funciona, mas leva 7 horas para ser executado.

O código VBA é complexo o suficiente para que seria muito trabalhoso recodificá-lo em T-SQL.

Eu li sobre integração CLR e UDFs como possíveis rotas. Também pensei em colocar o código VBA em uma tarefa de script SSIS para aproximar-me do banco de dados, mas tenho certeza de que existe uma metodologia especializada para esse tipo de problema de desempenho.

Idealmente, eu seria capaz de executar o algoritmo contra o maior número possível de linhas (todas?) De uma maneira baseada em conjunto paralelo.

Qualquer ajuda baseada em como obter o melhor desempenho com esse tipo de problema.

--Editar

Obrigado pelos comentários, estou usando o MS SQL 2014 Enterprise, aqui estão mais alguns detalhes:

O algoritmo encontra padrões característicos nos dados de séries temporais. As funções no algoritmo executam suavização polinomial, janelas e localizam regiões de interesse com base nos critérios de entrada, retornando uma dúzia de valores e alguns resultados booleanos.

Minha pergunta é mais sobre metodologia do que o algoritmo real: se eu quiser obter computação paralela em várias linhas ao mesmo tempo, quais são minhas opções.

Vejo que é recomendado re-codificar no T-SQL, o que é muito trabalhoso, mas possível; no entanto, o desenvolvedor do algoritmo trabalha no VBA e ele muda com frequência, portanto, eu preciso me manter sincronizado com a versão do T-SQL e validar novamente a cada mudança.

O T-SQL é a única maneira de implementar funções baseadas em conjunto?

medwar19
fonte
3
O SSIS pode oferecer alguma paralelização nativa, desde que você projete bem o fluxo de dados. Essa é a tarefa que você procuraria, pois precisa fazer esse cálculo linha por linha. Mas, dito isso, a menos que você possa nos fornecer detalhes específicos (esquema, cálculos envolvidos e o que esses cálculos esperam realizar), é impossível ajudá-lo a otimizar. Eles dizem que escrever coisas em assembly pode contribuir para o código mais rápido, mas se, como eu, você é horrível, não será nada eficiente
billinkc
2
Se você processar cada linha independentemente, poderá dividir 800K linhas em Nlotes e executar Ninstâncias do seu algoritmo em Nprocessadores / computadores separados. Por outro lado, qual é o seu principal gargalo - transferir os dados do SQL Server para o Excel ou cálculos reais? Se você alterar a função VBA para retornar imediatamente algum resultado fictício, quanto tempo levaria todo o processo? Se ainda demorar horas, o gargalo está na transferência de dados. Se demorar alguns segundos, você precisará otimizar o código VBA que faz os cálculos.
Vladimir Baranov
É o filtro que é chamado como um procedimento armazenado: SELECT AVG([AD_Sensor_Data]) OVER (ORDER BY [RowID] ROWS BETWEEN 5 PRECEDING AND 5 FOLLOWING) as 'AD_Sensor_Data' FROM [AD_Points] WHERE [FileID] = @FileID ORDER BY [RowID] ASC No Management Studio esta função que é chamado para cada uma das linhas leva 50mS
medwar19
1
Portanto, a consulta que leva 50 ms e executa 800000 vezes (11 horas) é o que está demorando. O @FileID é exclusivo para cada linha ou há duplicatas para minimizar o número de vezes que você precisa para executar a consulta? Você também pode pré-calcular a média de rolagem de todos os IDs de arquivo em uma tabela de teste de uma só vez (use a partição no FileID) e, em seguida, consulte essa tabela sem a necessidade de uma função de janela para cada linha. A melhor configuração para a tabela intermediária parece que deve estar com um índice clusterizado (FileID, RowID).
Mikael Eriksson
1
O melhor de tudo seria se você pudesse remover a necessidade de tocar no banco de dados de cada linha. Isso significa que você precisa passar pelo TSQL e provavelmente ingressar na consulta avg sem interrupção ou buscar informações suficientes para cada linha, para que tudo o que o algoritmo precise esteja ali na linha, talvez codificado de alguma forma se houver várias linhas filho envolvidas (xml) .
Mikael Eriksson

Respostas:

8

No que diz respeito à metodologia, acredito que você está latindo para a b-tree errada ;-).

O que nós sabemos:

Primeiro, vamos consolidar e revisar o que sabemos sobre a situação:

  • Cálculos um pouco complexos precisam ser realizados:
    • Isso precisa acontecer em todas as linhas desta tabela.
    • O algoritmo muda frequentemente.
    • O algoritmo ... [usa] valores de algumas colunas para manipular outras colunas
    • O tempo de processamento atual é: 7 horas
  • A mesa:
    • contém 800.000 linhas.
    • possui 38 colunas.
  • O back-end do aplicativo:
  • O banco de dados é o SQL Server 2014, Enterprise Edition.
  • Existe um procedimento armazenado chamado para cada linha:

    • Isso leva 50 ms (em média, presumo) para ser executado.
    • Retorna aproximadamente 4000 linhas.
    • A definição (pelo menos em parte) é:

      SELECT AVG([AD_Sensor_Data])
                 OVER (ORDER BY [RowID] ROWS BETWEEN 5 PRECEDING AND 5 FOLLOWING)
                 as 'AD_Sensor_Data'
      FROM   [AD_Points]
      WHERE  [FileID] = @FileID
      ORDER BY [RowID] ASC

O que podemos supor:

Em seguida, podemos analisar todos esses pontos de dados juntos para ver se podemos sintetizar detalhes adicionais que nos ajudarão a encontrar um ou mais gargalos e apontar para uma solução ou, pelo menos, descartar algumas soluções possíveis.

A direção atual dos comentários nos comentários é que o principal problema é a transferência de dados entre o SQL Server e o Excel. Esse é realmente o caso? Se o procedimento armazenado for chamado para cada uma das 800.000 linhas e demorar 50 ms por cada chamada (ou seja, por cada linha), isso adicionará até 40.000 segundos (não ms). E isso é equivalente a 666 minutos (hhmm ;-), ou pouco mais de 11 horas. No entanto, o processo todo levou apenas 7 horas para ser executado. Já temos 4 horas sobre o tempo total e ainda adicionamos tempo para fazer os cálculos ou salvar os resultados novamente no SQL Server. Então, algo não está bem aqui.

Observando a definição do Stored Procedure, existe apenas um parâmetro de entrada para @FileID; não há nenhum filtro ativado @RowID. Portanto, suspeito que um dos dois cenários a seguir esteja acontecendo:

  • Na verdade, esse procedimento armazenado não é chamado por cada linha, mas por cada um @FileID, que parece abranger aproximadamente 4000 linhas. Se as 4000 linhas retornadas declaradas forem uma quantidade bastante consistente, haverá apenas 200 delas agrupadas nas 800.000 linhas. E 200 execuções com 50 ms cada equivale a apenas 10 segundos nas 7 horas.
  • Se esse procedimento armazenado realmente for chamado para todas as linhas, a primeira vez que uma nova @FileIDfor passada levará um pouco mais para puxar novas linhas para o Buffer Pool, mas as próximas execuções 3999 normalmente retornarão mais rapidamente porque já estão sendo em cache, certo?

Eu acho que incidindo sobre este "filtro" procedimento armazenado ou qualquer transferência de dados do SQL Server para o Excel, é um arenque vermelho .

No momento, acho que os indicadores mais relevantes de desempenho sem brilho são:

  • Existem 800.000 linhas
  • A operação funciona em uma linha por vez
  • Os dados estão sendo salvos no SQL Server, portanto, "[usa] valores de algumas colunas para manipular outras colunas " [minha fase é ;-)]

Eu suspeito que:

  • Embora exista espaço para melhorias na recuperação e nos cálculos de dados, torná-los melhores não equivaleria a uma redução significativa no tempo de processamento.
  • o principal gargalo está na emissão de 800.000 UPDATEdeclarações separadas , que são 800.000 transações separadas.

Minha recomendação (com base nas informações atualmente disponíveis):

  1. Sua maior área de melhoria seria atualizar várias linhas ao mesmo tempo (ou seja, em uma transação). Você deve atualizar seu processo para trabalhar em termos de cada FileIDum deles RowID. Então:

    1. leia todas as 4000 linhas de um determinado FileIDem uma matriz
    2. a matriz deve conter elementos representando os campos que estão sendo manipulados
    3. percorrer a matriz, processando cada linha como você faz atualmente
    4. uma vez que todas as linhas da matriz (ou seja, para este particular FileID) tenham sido calculadas:
      1. iniciar uma transação
      2. chame cada atualização por cada RowID
      3. se não houver erros, confirme a transação
      4. se ocorreu um erro, reverter e manipular adequadamente
  2. Se o seu índice de cluster ainda não estiver definido como (FileID, RowID)você deve considerar isso (como @MikaelEriksson sugeriu em um comentário sobre a Pergunta). Isso não ajudará essas UPDATEs singleton, mas pelo menos melhoraria um pouco as operações agregadas, como o que você está fazendo nesse procedimento armazenado "filtro", pois todas elas são baseadas FileID.

  3. Você deve considerar mover a lógica para uma linguagem compilada. Eu sugeriria a criação de um aplicativo .NET WinForms ou mesmo do console. Prefiro o Console App, pois é fácil agendar via SQL Agent ou Windows Scheduled Tasks. Não importa se é feito em VB.NET ou C #. O VB.NET pode ser um ajuste mais natural para o seu desenvolvedor, mas ainda haverá alguma curva de aprendizado.

    Não vejo nenhuma razão neste momento para mudar para SQLCLR. Se o algoritmo for alterado com frequência, isso seria irritante e teria que reimplantar o Assembly o tempo todo. A reconstrução de um aplicativo de console e a colocação do .exe na pasta compartilhada adequada na rede, de modo que você execute o mesmo programa e sempre esteja atualizado, deve ser bastante fácil de fazer.

    Eu não acho que mover o processamento totalmente para o T-SQL ajudaria se o problema é o que suspeito e você está apenas fazendo uma atualização de cada vez.

  4. Se o processamento for movido para o .NET, você poderá usar TVPs (Parâmetros com Valor de Tabela) para passar a matriz para um Stored Procedure que chamaria um UPDATEque JOINs para a variável de tabela TVP e, portanto, é uma transação única. . O TVP deve ser mais rápido do que fazer 4000 INSERTs agrupados em uma única transação. Mas o ganho resultante do uso de TVPs acima de 4000 INSERTs em uma transação provavelmente não será tão significativo quanto a melhoria observada ao passar de 800.000 transações separadas para apenas 200 transações de 4000 linhas cada.

    A opção TVP não está disponível nativamente para o lado do VBA, mas alguém apresentou uma solução alternativa que pode valer a pena testar:

    Como melhoro o desempenho do banco de dados ao passar do VBA para o SQL Server 2008 R2?

  5. SE o processo de filtro estiver sendo usado apenas FileIDna WHEREcláusula, e se esse processo estiver realmente sendo chamado por cada linha, você poderá economizar algum tempo de processamento armazenando em cache os resultados da primeira execução e usando-os pelo restante das linhas FileID, certo?

  6. Depois de conseguir o processamento feito por FileID , então podemos começar a falar de processamento paralelo. Mas isso pode não ser necessário nesse momento :). Dado que você está lidando com três partes não ideais bastante importantes: transações Excel, VBA e 800k, qualquer conversa sobre SSIS, paralelogramos ou quem sabe o que é otimização prematura / coisas do tipo carroça antes do cavalo . Se conseguirmos reduzir esse processo de 7 horas para 10 minutos ou menos, você ainda estaria pensando em outras maneiras de torná-lo mais rápido? Existe um prazo de conclusão que você tem em mente? Lembre-se de que, uma vez concluído o processamento em um FileID Por isso, se você tivesse um aplicativo de console do VB.NET (ou seja, linha de comando .EXE), não haveria nada impedindo a execução de alguns desses FileIDs por vez :), seja pela etapa CmdExec do SQL Agent ou pelas Tarefas agendadas do Windows, etc.

E, você sempre pode adotar uma abordagem em fases e fazer algumas melhorias de cada vez. Por exemplo, começando com as atualizações FileIDe, portanto, usando uma transação para esse grupo. Então, veja se você consegue fazer o TVP funcionar. Em seguida, veja como pegar esse código e movê-lo para o VB.NET (e os TVPs funcionam no .NET para que sejam portados corretamente).


O que não sabemos ainda pode ajudar:

  • O procedimento armazenado "filtro" é executado por RowID ou FileID ? Temos a definição completa desse procedimento armazenado?
  • Esquema completo da tabela. Qual a largura dessa mesa? Quantos campos de comprimento variável existem? Quantos campos são NULLable? Se algum for NULLable, quantos contêm NULLs?
  • Índices para esta tabela. É particionado? A compactação ROW ou PAGE está sendo usada?
  • Qual é o tamanho dessa tabela em termos de MB / GB?
  • Como a manutenção de índice é tratada para esta tabela? Quão fragmentados são os índices? Quão atualizadas são as estatísticas?
  • Algum outro processo grava nesta tabela enquanto esse processo de 7 horas está ocorrendo? Possível fonte de discórdia.
  • Algum outro processo é lido nesta tabela enquanto esse processo de 7 horas está ocorrendo? Possível fonte de discórdia.

ATUALIZAÇÃO 1:

** Parece haver alguma confusão sobre o que VBA (Visual Basic for Applications) e o que pode ser feito com ele, portanto, isso é apenas para garantir que estamos todos na mesma página da web:


ATUALIZAÇÃO 2:

Mais um ponto a considerar: como as conexões estão sendo tratadas? O código VBA está abrindo e fechando a conexão a cada operação ou abre a conexão no início do processo e fecha no final do processo (ou seja, 7 horas depois)? Mesmo com o pool de conexões (que, por padrão, deve estar habilitado para o ADO), ainda deve haver um grande impacto entre abrir e fechar uma vez, em vez de abrir e fechar 800.200 ou 1.600.000 vezes. Esses valores são baseados em pelo menos 800.000 UPDATEs mais 200 ou 800k EXECs (dependendo da frequência com que o procedimento armazenado do filtro está realmente sendo executado).

Esse problema de muitas conexões é mitigado automaticamente pela recomendação que descrevi acima. Ao criar uma transação e realizar todas as atualizações dentro dessa transação, você manterá essa conexão aberta e a reutilizará para cada uma UPDATE. Se a conexão é mantida aberta ou não a partir da chamada inicial para obter as 4000 linhas de acordo com o especificado FileIDou fechada após a operação "get" e aberta novamente para as UPDATEs, é muito menos impactante, pois agora estamos falando de uma diferença de 200 ou 400 conexões totais em todo o processo.

ATUALIZAÇÃO 3:

Eu fiz alguns testes rápidos. Lembre-se de que este é um teste de pequena escala e não exatamente a mesma operação (puro INSERT vs EXEC + UPDATE). No entanto, as diferenças de tempo relacionadas à maneira como as conexões e transações são tratadas ainda são relevantes, portanto, as informações podem ser extrapoladas para causar um impacto relativamente semelhante aqui.

Parâmetros de teste:

  • SQL Server 2012 Developer Edition (64 bits), SP2
  • Mesa:

     CREATE TABLE dbo.ManyInserts
     (
        RowID INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
        InsertTime DATETIME NOT NULL DEFAULT (GETDATE()),
        SomeValue BIGINT NULL
     );
  • Operação:

    INSERT INTO dbo.ManyInserts (SomeValue) VALUES ({LoopIndex * 12});
  • Total de inserções por cada teste: 10.000
  • Redefinições para cada teste: TRUNCATE TABLE dbo.ManyInserts;(dada a natureza desse teste, executar o FREEPROCCACHE, FREESYSTEMCACHE e DROPCLEANBUFFERS não parecia agregar muito valor).
  • Modelo de recuperação: SIMPLES (e talvez 1 GB grátis no arquivo de log)
  • Os testes que usam transações usam apenas uma única conexão, independentemente de quantas transações.

Resultados:

Test                                   Milliseconds
-------                                ------------
10k INSERTs across 10k Connections     3968 - 4163
10k INSERTs across 1 Connection        3466 - 3654
10k INSERTs across 1 Transaction       1074 - 1086
10k INSERTs across 10 Transactions     1095 - 1169

Como você pode ver, mesmo que a conexão do ADO ao banco de dados já esteja sendo compartilhada em todas as operações, é garantido que o agrupamento em lotes usando uma transação explícita (o objeto ADO deve ser capaz de lidar com isso) é significativamente garantido (ou seja, mais de 2x melhoria) reduza o tempo total do processo.

Solomon Rutzky
fonte
Existe uma boa abordagem "intermediária" para o que srutzky está sugerindo, que é usar o PowerShell para obter os dados necessários do SQL Server, chamar seu script VBA para trabalhar com os dados e, em seguida, chamar um SP de atualização no SQL Server , passando as chaves e os valores atualizados de volta ao SQL server. Dessa maneira, você combina uma abordagem baseada em conjunto com o que você já possui.
9789 Steve # 1315
@SteveMangiameli Olá Steve e obrigado pelo comentário. Eu teria respondido mais cedo, mas ficaria doente. Estou curioso para saber como a sua ideia é muito diferente do que estou sugerindo. Todas as indicações são de que o Excel ainda é necessário para executar o VBA. Ou você está sugerindo que o PowerShell substitua o ADO e, se for muito mais rápido na E / S, valerá a pena, mesmo que seja apenas para substituir apenas a E / S?
Solomon Rutzky
1
Não se preocupe, feliz por se sentir melhor. Não sei se seria melhor. Não sabemos o que não sabemos e você fez uma ótima análise, mas ainda precisa fazer algumas suposições. A E / S pode ser significativa o suficiente para substituir por si própria; nós simplesmente não sabemos. Eu só queria apresentar outra abordagem que possa ser útil com as coisas que você sugeriu.
9788 Steve Jobsiam15:
@SteveMangiameli Thanks. E obrigado por esclarecer isso. Eu não tinha certeza de sua direção exata e achei melhor não assumir. Sim, concordo que ter mais opções é melhor, pois não sabemos quais restrições existem sobre quais alterações podem ser feitas :).
Solomon Rutzky
Ei srutzky, obrigado pelos pensamentos detalhados! Voltei a testar no lado do SQL, otimizando índices e consultas e tentando encontrar os gargalos. Investi em um servidor adequado agora, com 36cores e 1TB de SSDs PCIe despojados, enquanto a IO estava atolada. Agora, chame o código VB diretamente no SSIS, que parece abrir vários threads para execuções paralelas.
medwar19
2

IMHO e trabalhando com a suposição de que não é possível codificar novamente o sub VBA no SQL, você já pensou em permitir que o script VBA conclua a avaliação no arquivo do Excel e depois grave os resultados no SQL Server via SSIS?

Você pode ter o sub-início e o fim do VBA invertendo um indicador em um objeto do sistema de arquivos ou no servidor (se você já configurou a conexão para gravar novamente no servidor) e, em seguida, use uma expressão SSIS para verificar esse indicador. disablepropriedade de uma determinada tarefa em sua solução SSIS (para que o processo de importação aguarde até que o sub VBA seja concluído, se você estiver preocupado com a possibilidade de ultrapassar sua agenda).

Além disso, você pode ter o script VBA iniciado programaticamente (um pouco instável, mas eu usei a workbook_open()propriedade para disparar tarefas "disparar e esquecer" dessa natureza no passado).

Se o tempo de avaliação do script VB começar a se tornar um problema, você poderá ver se o desenvolvedor do VB está disposto e é capaz de portar seu código em uma tarefa de script VB na solução SSIS - na minha experiência, o aplicativo Excel gera muita sobrecarga quando trabalhando com dados neste volume.

Peter Vandivier
fonte