Estou lidando com problemas de desempenho com um aplicativo do Windows que usa o SQL Server Express (2014) no back-end.
Consegui fazer isso funcionar muito melhor, principalmente revisando o lado da indexação do SQL Server, mas há um relatório específico que ainda está sendo executado muito lentamente.
Olhando para o que está fazendo, parece estar fazendo um loop no aplicativo e consultando milhares de SELECT *
consultas muito simples em uma tabela WHERE = Primary Key
, recuperando apenas um registro em cada caso. E quando digo IDÊNTICO, quero dizer idêntico, não está nem variando a chave primária para obter coisas diferentes, está pedindo exatamente o mesmo registro de volta do banco de dados aparentemente cada vez que precisa, até cem vezes em apenas alguns segundos.
Este é um relatório de exemplo que leva cerca de 10 a 15 segundos para ser executado quando o servidor está silencioso - quantas vezes a consulta é executada, adicionei como um comentário:
SELECT * FROM "Patient" WHERE "_Recno" = 35051 -- (runs 106 times)
SELECT * FROM "Client" WHERE "_Recno" = 15607 -- (99 times)
SELECT * FROM "SpeciesEntry" WHERE "_Recno" = 180 -- (97)
SELECT * FROM "Table" WHERE "_Recno" = 9 -- (97)
SELECT * FROM "DefaultEntry" WHERE "_Recno" = 2615 -- (96)
SELECT * FROM "Table" WHERE "_Recno" = 34 -- (96)
SELECT * FROM "DefaultEntry" WHERE "_Recno" = 2562 -- (84)
SELECT * FROM "Table" WHERE "_Recno" = 33 -- (84)
SELECT * FROM "Treatment" WHERE "_Recno" = 1682 -- (33)
SELECT * FROM "Treatment" WHERE "_Recno" = 1819 -- (33)
SELECT * FROM "Treatment" WHERE "_Recno" = 927 -- (33)
SELECT * FROM "Treatment" WHERE "_Recno" = 934 -- (33)
SELECT * FROM "Treatment" WHERE "_Recno" = 935 -- (33)
SELECT * FROM "Treatment" WHERE "_Recno" = 940 -- (33)
SELECT * FROM "Treatment" WHERE "_Recno" = 942 -- (33)
SELECT * FROM "Treatment" WHERE "_Recno" = 944 -- (33)
SELECT * FROM "OptionWP" WHERE "_Recno" = 103 -- (3)
SELECT * FROM "OptionWP" WHERE "_Recno" = 54 -- (1)
SELECT * FROM "PatientEstimate" WHERE "_Recno" = 8928 -- (1)
SELECT * FROM "Phrase" WHERE "_Recno" = 9718 -- (1)
SELECT * FROM "Table" WHERE "_Recno" = 4 -- (1)
SELECT * FROM "BreedEntry" WHERE "_Recno" = 3283 -- (1)
O número após a consulta é o número de vezes que a consulta exata está sendo executada, por exemplo, a consulta SELECT * FROM "Patient" WHERE "_Recno" = 35051
está sendo executada 106 vezes, com esse _Recno . Na verdade, existem 1.031 consultas sendo executadas para criar este relatório (neste caso, varia) - as 23 ou mais acima são as consultas distintas .
Agora, cada consulta acima é executada muito, muito rápido, estamos falando de algumas dezenas de microssegundos em cada caso. Na verdade, se você somar todas as 1.031 consultas usadas para fazer este relatório, o tempo total usado para todas elas é de apenas 59.193 microssegundos, ou apenas 59 milissegundos.
Portanto, o problema e o atraso parecem ser a sobrecarga - embora haja apenas cerca de 59 ms de tempo real do banco de dados, o relatório leva cerca de 10 a 15 segundos para ser executado para o cliente, pois está indo e voltando com mais de 1.000 consultas.
Observe que, na maioria dos casos, o aplicativo cliente e o SQL Server estão na mesma máquina e várias instâncias do cliente são acessadas por meio do RDP. Em alguns casos, o cliente está em uma máquina diferente na LAN e imagino que o desempenho seria pior lá. Mas você pode considerar que, na maioria dos casos, não deve haver um problema de rede, pois o aplicativo cliente e o SQL Server estão na mesma caixa física.
Os dez segundos são até meio aceitáveis, o problema é que em horários mais movimentados isso pode aumentar para até um minuto ou mais.
Alguma ideia de como lidar com a otimização disso? Se fosse um aplicativo que eu tivesse acesso à fonte, obviamente substituiria tudo isso por uma ou algumas consultas que usavam junções, mas isso não é uma opção, o aplicativo é uma caixa preta - tudo o que posso fazer é otimizar do lado do SQL Server.
Falando com o cliente, embora o desempenho seja ruim, esteja ele usando o RDP ou a instalação de um aplicativo cliente remoto, o desempenho é muito pior com o aplicativo cliente remoto e isso é mais um problema para eles. Portanto, qualquer sugestão sobre coisas que eu possa observar para melhorar o desempenho lá, em relação à rede ou qualquer outra coisa, seria apreciada. Uma coisa a observar é que esta caixa do SQL 2014 agora está virtualizada, antes eles estavam usando, acho, 2008 ou 2012, mas não foi virtualizado - eles dizem que esse relatório era mais rápido naquela época. Eles têm outros motivos para querer que seja virtualizado; removê-lo da virtualização não é uma opção.
Ele se conecta usando a autenticação do Windows e (tenho certeza) TCP/IP. Acho que não conseguiria mudar isso. Não está caindo e restabelecendo as conexões, pelo menos, parece estar usando o pool de conexões.
Eu uso o Hibernate no meu trabalho diário e já me deparei com esse tipo de cenário antes, com o ORM gerando milhares de consultas, e minha solução usual é olhar para a estratégia de busca (carregamento preguiçoso vs carregamento ansioso) no código ou mesmo no caso de relatórios, geralmente reescreve tudo em SQL. Neste caso, embora o software seja o que é, um executável do Windows, e não há nada que eu possa fazer sobre isso, só posso abordar o lado SQL.
Meu entendimento é que o fornecedor não oferece mais suporte a esta versão específica e voltou para uma versão que usa arquivos simples em vez de SQL. Isso não funcionaria para o cliente - eles têm esse banco de dados integrado a várias outras coisas. É um software de nicho e, como muito desse tipo de software, é tecnicamente terrível no back-end, mas possui a funcionalidade de que o usuário do nicho precisa. De qualquer forma, não posso mudar o software, apenas o que se passa no SQL Server. Era inutilizável e eu o tornei utilizável, então fiz progressos trabalhando dentro dessas restrições.
Não há bloqueio ou bloqueio, verifiquei isso. Na verdade, esse foi o principal problema de desempenho que consertei, mas não houve nada bloqueando o tempo suficiente para ser registrado no último mês ou mais. A memória não é realmente um problema, pois o SQL Server Express é limitado a 1 GB de qualquer maneira. Ao olhar para ele, embora eu não ache que haja um problema de memória, disco, se houver, parece ser o maior ponto de estrangulamento de hardware.
O fato de as consultas serem idênticas não é o problema e, de certa forma, ajuda, pois o Plano de Execução é armazenado em cache e as páginas de dados necessárias para a consulta ainda devem ser armazenadas em cache. O problema tenderia a ser a sobrecarga por conexão de autenticação e inicialização da sessão.
A primeira coisa a verificar é: o "pooling de conexões" está sendo usado? Você pode testar isso usando o SQL Server Profiler, selecione o evento "RPC:Completed" na categoria "Stored Procedures", certifique-se de que "TextData", "ClientProcessID" e "SPID" estejam marcados para esse evento (no pelo menos, você pode selecionar outras colunas, se quiser). Em seguida, vá em "Filtros de coluna", selecione "TextData" e na condição "Curtir" adicione a seguinte condição:
exec sp[_]reset[_]connection
. Agora execute esse rastreamento. Se você vir instâncias de exec sp_reset_connection chegando, isso se deve ao uso do pool de conexões. Mas isso não significa necessariamente que é este aplicativo que o está usando. Então olhe para um dos "SPID"O campo na extrema direita/extremidade tem a consulta mais recente executada. Isso deve confirmar que a sessão é o aplicativo em questão (pelo que está fazendo).
SE você não conseguir encontrar nenhuma evidência de uso do pool de conexões em geral, ou pelo menos usado para este aplicativo, a string de conexão usada pelo aplicativo precisa ser atualizada para o pool de conexões habilitado.
Com base nas informações acima da pergunta, parece que há vários clientes se conectando, correto? Nesse caso, o pool de conexões, embora provavelmente ainda seja uma boa ideia e útil, será menos eficaz, pois cada cliente mantém seu próprio pool de conexões. Ou seja, 5 instâncias (ou qualquer número delas) ainda criarão 5 pools separados para conexões, e cada um reduzirá a sobrecarga de inicialização da conexão para seu respectivo aplicativo, mas não pode reduzir a sobrecarga além disso/para uma única conexão compartilhada). Lembre-se também de que, mesmo com o pool de conexões, se um aplicativo não fechar corretamente suas conexões antes de tentar abrir novas, você ainda terá várias conexões/sessões provenientes de uma determinada instância do aplicativo.
Nesse caso, e no caso de um único aplicativo que não está usando o pool de conexão e não pode ter sua String de conexão atualizada para usar o pool de conexão, então, se possível atualizar o aplicativo, seria bastante útil implementar uma camada de cache como Redis ou memcache (e acredito que AWS e Azure oferecem soluções de cache baseadas em nuvem). Esses acessos repetitivos geralmente podem ser armazenados em cache e ignorar completamente o RDBMS (por um período de tempo especificado, é claro), o que é uma grande parte do motivo pelo qual essas coisas existem.
Agora que acabei de acompanhar os comentários recentes sobre a questão, parece que provavelmente nem a string de conexão nem qualquer parte do aplicativo podem ser modificadas. Nesse caso, não há muito o que fazer, exceto possivelmente verificar se as conexões feitas a partir do aplicativo que está sendo executado no mesmo servidor do SQL Server estão usando Memória Compartilhada ou TCP/IP para conectar e se é TCP /IP para as conexões do mesmo servidor, verifique se o protocolo de Memória Compartilhada está habilitado para SQL Server e, se não estiver, habilite-o. Esta não é uma melhoria garantida, pois é possível que a string de conexão force o protocolo a ser TCP/IP (por exemplo, usando a sintaxe de:
server=tcp:{something}
), mas ainda vale a pena tentar, pois essa sintaxe provavelmente não está sendo usada.ATUALIZAR
do comentário sobre esta resposta:
Isso pode muito bem indicar o problema, ou pelo menos uma grande parte dele. Se uma conexão foi feita várias horas a dias atrás e a sessão começou imediatamente depois disso, o aplicativo é um aplicativo de desktop que faz uma única conexão e sessão na qual executa todas as consultas (semelhante ao funcionamento das guias de consulta do SSMS) ou o código do aplicativo incorretamente não está fechando o objeto de conexão todas as vezes, caso em que você pode estar vendo muitas conexões simultâneas, muitas das quais foram efetivamente abandonadas, mas ainda ocupando memória (e no SQL Server Express, pode haver uma conexão limite de qualquer maneira).
Tente a seguinte consulta, adaptada da consulta original acima:
Se o pool de conexões estiver sendo usado, você verá linhas com valores altos para o
MillisecondsBetweenConnectionAndSessionStart
campo, mas valores muito mais baixos para oMillisecondsBetweenSessionStartAndLastActivity
campo. A razão é que uma conexão é estabelecida e reutilizada. Cada vez que a conexão é reutilizada, ologin_time
redefine para oSqlConnection.Open
evento mais recente e imediatamente executa a consulta.Se você tiver um aplicativo de desktop com uma conexão estável (como SSMS), verá o comportamento oposto ao pool de conexão: em vez disso, você terá linhas com valores baixos para o
MillisecondsBetweenConnectionAndSessionStart
campo, mas valores muito mais altos para oMillisecondsBetweenSessionStartAndLastActivity
campo. A razão é que a conexão, uma vez estabelecida, nunca é fechada e simplesmente tem consultas continuadas a serem executadas nela.Se você tiver um aplicativo que não está fechando suas conexões, mas ainda está abrindo novas conexões (devido a um erro ou mal-entendido de como funciona o pool de conexões - estando o pool ativado ou não), você não apenas terá muito de linhas, mas terão um valor baixo para o
MillisecondsBetweenConnectionAndSessionStart
campo, mas valores muito mais altos para oMillisecondsBetweenLastActivityAndNow
campo. Isso acontece se uma conexão for feita, usada uma vez e depoisSqlConnection.Open()
chamada antes de chamarSqlConnection.Close()
ouSqlConnection.Dispose()
(Dispose()
chamaráClose()
e será chamada automaticamente se oSqlConnection
objeto tiver sido criado em umausing()
construção).Se você não pode alterar as consultas, não pode otimizar isso. A partir dos números que você postou, quase todo o tempo não é gasto na execução da consulta.
Portanto, mesmo que você otimize a execução da consulta para zero, isso não terá muito impacto.
(Na verdade, eu não compro o número 10us. Mesmo um
SELECT NULL
leva ~ 150us de ponta a ponta quando executado usando ADO.NET no transporte de memória compartilhada em uma conexão aberta. Mas qualitativamente pode ser verdade.)