Erro de desempenho do índice de data e hora do SQL Server 2008

11

Estamos usando o SQL Server 2008 R2 e temos uma tabela muito grande (100M + linhas) com um índice de identificação primário e uma datetimecoluna com um índice não clusterizado. Estamos vendo um comportamento cliente / servidor altamente incomum, com base no uso de uma order bycláusula especificamente em uma coluna de data e hora indexada .

Eu li o seguinte post: /programming/1716798/sql-server-2008-ordering-by-datetime-is-too-slow, mas há mais coisas acontecendo com o cliente / servidor do que o que é comece descrito aqui.

Se executarmos a seguinte consulta (editada para proteger algum conteúdo):

select * 
from [big table] 
where serial_number = [some number] 
order by test_date desc

A consulta atinge o tempo limite sempre. No SQL Server Profiler, a consulta executada se parece com isso no servidor:

exec sp_cursorprepexec @p1 output,@p2 output,NULL,N'select * .....

Agora, se você modificar a consulta, diga o seguinte:

declare @temp int;
select * from [big table] 
where serial_number = [some number] 
order by test_date desc

O SQL Server Profiler mostra que a consulta executada se parece com isso para o servidor e FUNCIONA instantaneamente:

exec sp_prepexec @p1 output, NULL, N'declare @temp int;select * from .....

Por uma questão de fato, você pode até colocar um comentário vazio ('-;') em vez de uma declaração de declaração não utilizada e obter o mesmo resultado. Portanto, inicialmente apontamos o pré-processador sp como a causa raiz desse problema, mas se você fizer isso:

select * 
from [big table] 
where serial_number = [some number] 
order by Cast(test_date as smalldatetime) desc

Também funciona instantaneamente (você pode convertê-lo como qualquer outro datetimetipo), retornando o resultado em milissegundos. E o criador de perfil mostra a solicitação ao servidor como:

exec sp_cursorprepexec @p1 output, @p2 output, NULL, N'select * from .....

Portanto, isso exclui o sp_cursorprepexecprocedimento da causa completa do problema. Adicione a isso o fato de que sp_cursorprepexectambém é chamado quando nenhum 'pedido por' é usado e o resultado também é retornado instantaneamente.

Pesquisamos bastante esse problema no Google, e vejo problemas semelhantes publicados por outros, mas nenhum que o reduza a esse nível.

Então, outras pessoas testemunharam esse comportamento? Alguém tem uma solução melhor do que colocar SQL sem sentido na frente da instrução select para mudar o comportamento? Como o SQL Server deve chamar a ordem após a coleta dos dados, parece que esse é um erro no servidor que persiste por um longo tempo. Nós descobrimos que esse comportamento é consistente em muitas de nossas tabelas grandes e é reproduzível.

Editar% s:

Devo acrescentar que colocar um forceseektambém faz o problema desaparecer.

Devo adicionar para ajudar os pesquisadores, o erro de tempo limite do ODBC lançado é: [Microsoft] [Driver do ODBC SQL Server] Operação cancelada

Adicionado em 12/10/2012: Ainda em busca da causa raiz (além de ter construído uma amostra para fornecer à Microsoft, cruzarei os resultados aqui após o envio). Venho pesquisando no arquivo de rastreamento ODBC entre uma consulta de trabalho (com uma declaração de comentário / declaração) e uma consulta que não está funcionando. A diferença fundamental de rastreamento está publicada abaixo. Ocorre na chamada para a chamada SQLExtendedFetch após a conclusão de todas as discussões SQLBindCol. A chamada falha com o código de retorno -1 e o encadeamento pai entra no SQLCancel. Como somos capazes de produzir isso com os drivers ODBC do cliente nativo e herdado, ainda estou apontando para algum problema de compatibilidade no lado do servidor.

(clip)
MSSQLODBCTester 1664-1718   EXIT  SQLBindCol  with return code 0 (SQL_SUCCESS)
        HSTMT               0x001EEA10
        UWORD                       16 
        SWORD                        1 <SQL_C_CHAR>
        PTR                0x03259030
        SQLLEN                    51
        SQLLEN *            0x0326B820 (0)

MSSQLODBCTester 1664-1718   ENTER SQLExtendedFetch 
        HSTMT               0x001EEA10
        UWORD                        1 <SQL_FETCH_NEXT>
        SQLLEN                     1
        SQLULEN *           0x032677C4
        UWORD *             0x032679B0

MSSQLODBCTester 1664-1fd0   ENTER SQLCancel 
        HSTMT               0x001EEA10

MSSQLODBCTester 1664-1718   EXIT  SQLExtendedFetch  with return code -1 (SQL_ERROR)
        HSTMT               0x001EEA10
        UWORD                        1 <SQL_FETCH_NEXT>
        SQLLEN                     1
        SQLULEN *           0x032677C4
        UWORD *             0x032679B0

        DIAG [S1008] [Microsoft][ODBC SQL Server Driver]Operation canceled (0) 

MSSQLODBCTester 1664-1fd0   EXIT  SQLCancel  with return code 0 (SQL_SUCCESS)
        HSTMT               0x001EEA10

MSSQLODBCTester 1664-1718   ENTER SQLErrorW 
        HENV                0x001E7238
        HDBC                0x001E7B30
        HSTMT               0x001EEA10
        WCHAR *             0x08BFFC5C
        SDWORD *            0x08BFFF08
        WCHAR *             0x08BFF85C 
        SWORD                      511 
        SWORD *             0x08BFFEE6

MSSQLODBCTester 1664-1718   EXIT  SQLErrorW  with return code 0 (SQL_SUCCESS)
        HENV                0x001E7238
        HDBC                0x001E7B30
        HSTMT               0x001EEA10
        WCHAR *             0x08BFFC5C [       5] "S1008"
        SDWORD *            0x08BFFF08 (0)
        WCHAR *             0x08BFF85C [      53] "[Microsoft][ODBC SQL Server Driver]Operation canceled"
        SWORD                      511 
        SWORD *             0x08BFFEE6 (53)

MSSQLODBCTester 1664-1718   ENTER SQLErrorW 
        HENV                0x001E7238
        HDBC                0x001E7B30
        HSTMT               0x001EEA10
        WCHAR *             0x08BFFC5C
        SDWORD *            0x08BFFF08
        WCHAR *             0x08BFF85C 
        SWORD                      511 
        SWORD *             0x08BFFEE6

MSSQLODBCTester 1664-1718   EXIT  SQLErrorW  with return code 100 (SQL_NO_DATA_FOUND)
        HENV                0x001E7238
        HDBC                0x001E7B30
        HSTMT               0x001EEA10
        WCHAR *             0x08BFFC5C
        SDWORD *            0x08BFFF08
        WCHAR *             0x08BFF85C 
        SWORD                      511 
        SWORD *             0x08BFFEE6
(clip)

Foi adicionado um caso do Microsoft Connect em 10/10/2012:

https://connect.microsoft.com/SQLServer/feedback/details/767196/order-by-datetime-in-odbc-fails-for-clean-sql-statements#details

Devo também observar que procuramos os planos de consulta para as consultas funcionais e não funcionais. Ambos são reutilizados adequadamente com base na contagem de execução. Esvaziar os planos em cache e executar novamente não altera o sucesso da consulta.

DBtheDBA
fonte
O que acontece se você tentar select id, test_date from [big table] where serial_number = ..... order by test_date- eu só estou me perguntando se isso SELECT *tem um impacto negativo no seu desempenho. Se você tem um índice sem cluster em test_datee um índice agrupado na id(assumindo que é o que é chamado), esta consulta deve ser coberta por esse índice agrupado e, portanto, deve retornar muito rapidamente
marc_s
Desculpe, bom ponto. Eu deveria ter incluído que tentamos modificar o espaço da coluna selecionado (removendo o '*' etc.) fortemente com várias combinações. O comportamento descrito acima persistiu através dessas alterações.
DB16DBA #
Eu vinculei minhas contas agora a esse site. Se um moderador quiser mover a postagem para esse site, eu estou bem de qualquer maneira. Um dos meus desenvolvedores apontou esse site para mim depois que eu postei aqui.
DBDDBA #
Qual pilha de clientes está sendo usada aqui? Sem todo o texto de rastreamento, esse parece ser o problema. Tente encerrar a chamada original sp_executesqle veja o que acontece.
21812 Jon Jones,
1
Como é o plano de execução lenta? Parâmetro cheirando?
Martin Smith

Respostas:

6

Não há mistério, você obtém um plano bom (realmente) ou (realmente) ruim, basicamente aleatório, porque não há uma opção clara para o índice usar. Embora seja atraente para a cláusula ORDER BY e evite a classificação, o índice não clusterizado na coluna datetime é uma escolha muito ruim para esta consulta. O que tornaria um índice muito melhor para essa consulta seria um (serial_number, test_date). Melhor ainda, isso seria um bom candidato para uma chave de índice em cluster .

Como regra geral, as séries temporais devem ser agrupadas em cluster pela coluna de tempo, porque a grande maioria das solicitações está interessada em intervalos de tempo específicos. Se os dados também forem inerentemente particionados em uma coluna com baixa seletividade, como parece ser o seu número de série, essa coluna deverá ser adicionada como a mais à esquerda na definição de chave em cluster.

Remus Rusanu
fonte
Estou um pouco confuso aqui. Por que o plano seria baseado na the ordercláusula? O plano não deveria se limitar às wherecondições, pois a ordem só deveria ocorrer após as linhas terem sido buscadas? Por que o servidor tentou classificar os registros antes de ter todo o conjunto de resultados?
DBtheDBA
5
Isso também não explica por que adicionar um comentário no início da consulta afeta a duração da execução.
Cfradenburg 10/10/12
Além disso, nossas tabelas são quase sempre consultadas pelo número de série, e não test_date. Temos índices não agrupados em ambos e um cluster apenas na coluna id na tabela. É um armazenamento de dados operacionais, e a adição de índices agrupados em outras colunas apenas geraria divisões de página e pior desempenho.
DBtheDBA
1
@DBtheDBA: se você quiser reivindicar um "bug", precisará fazer uma investigação e divulgação adequadas. O esquema exato da sua tabela e das estatísticas exportadas, siga Como gerar um script dos metadados do banco de dados necessários para criar um banco de dados somente estatísticas no SQL Server 2005 e no SQL Server 2008 , especificamente todas as estatísticas importantes de script : Estatísticas de script e histogramas . Adicione-os às informações da postagem, juntamente com as etapas que reproduzem o problema.
Remus Rusanu 11/11
1
Lemos isso antes durante nossas pesquisas, e eu entendo o que você está dizendo, mas há uma falha fundamental em algo que o servidor está fazendo aqui. Nós reconstruímos a tabela e os índices e a reproduzimos em uma nova tabela. A opção recompilar não resolve o problema, o que é uma grande dica de que algo está errado. Não duvido que colocar índices agrupados em tudo possa resolver esse problema, mas não é uma solução para a causa raiz, é uma solução alternativa e cara em uma tabela grande.
22412 DBtheDBA
0

Documente os detalhes de como reproduzir o erro e enviá-lo em connect.microsoft.com. Eu verifiquei e não conseguia ver nada lá fora que estivesse relacionado a isso.

cfradenburg
fonte
Vou pedir ao meu DBA que digite um script amanhã para criar um ambiente para reproduzir. Eu não acho tão difícil assim. Vou postá-lo aqui também se alguém estiver interessado em experimentá-lo.
DBDDBA #
Poste o item de conexão também quando ele for aberto. Dessa forma, se alguém tiver esse problema, ele será apontado diretamente para ele. E quem assiste a essa pergunta pode querer votar no item, então é mais provável que a Microsoft preste atenção nele.
Cfradenburg 11/11/12
0

Minha hipótese é que você esteja com problemas no cache do plano de consulta. (Remus pode estar dizendo a mesma coisa que eu, mas de uma maneira diferente.)

Aqui estão alguns detalhes de como o SQL planeja o cache .

Encobrindo os detalhes: alguém executou essa consulta anteriormente, para um determinado [algum número]. O SQL analisou o valor fornecido, os índices e as estatísticas para as tabelas / colunas relevantes, etc. e construiu um plano que funcionou bem para esse determinado número. Em seguida, armazenou em cache o plano, executou-o e devolveu os resultados ao chamador.

Mais tarde, outra pessoa está executando a mesma consulta, para um valor diferente de [algum número]. Esse valor específico resulta em um número totalmente diferente de linhas de resultado e o mecanismo deve criar um plano diferente para esta instância da consulta. Mas não é assim que funciona. Em vez disso, o SQL recebe a consulta e (mais ou menos) faz uma pesquisa que diferencia maiúsculas de minúsculas do cache da consulta, procurando uma versão pré-existente da consulta. Quando encontra o anterior, apenas usa esse plano.

A idéia é que economize o tempo necessário para decidir sobre o plano e construí-lo. O buraco na ideia é quando a mesma consulta é executada com valores que produzem resultados totalmente diferentes. Eles deveriam ter planos diferentes, mas não o fazem. Quem executou a consulta primeiro ajuda a definir o comportamento para todos que a executam posteriormente.

Um exemplo rápido: selecione * de [pessoas] onde sobrenome = 'SMITH' - sobrenome muito popular nos EUA. GO selecione * de [pessoas] onde sobrenome = 'BONAPARTE' - NÃO sobrenome popular nos EUA.

Quando a consulta para BONAPARTE é executada, o plano que foi criado para o SMITH será reutilizado. Se o SMITH causou uma varredura de tabela (o que pode ser bom , se as linhas na tabela são 99% SMITH), o BONAPARTE também receberá uma varredura de tabela. Se o BONAPARTE foi executado antes do SMITH, um plano usando um índice pode ser construído e usado e depois usado novamente para o SMITH (o que pode ser melhor com a varredura de tabela). As pessoas podem não perceber que o desempenho do SMITH é ruim, pois esperam um desempenho ruim, pois a tabela inteira deve ser lida e a leitura do índice e o salto para a tabela não são percebidos diretamente.

Com relação às mudanças que devem mudar, suspeito que o SQL esteja vendo isso como uma consulta totalmente diferente e criando um novo plano, específico ao seu valor de [algum número].

Para testar isso, faça uma alteração sem sentido na consulta, como adicionar alguns espaços entre FOR e o nome da tabela ou coloque um comentário no final. É rápido? Nesse caso, é porque essa consulta é um pouco diferente do que está no cache, então o SQL fez o que faz para consultas "novas".

Para uma solução, eu examinaria três coisas. Primeiro, verifique se suas estatísticas estão atualizadas. Essa realmente deve ser a primeira coisa que você faz quando uma consulta parece estranha ou aleatória. Seu DBA deve estar fazendo isso, mas as coisas acontecem. A maneira usual de garantir estatísticas atualizadas é reindexar suas tabelas, o que não é necessariamente algo leve, mas também existem opções para apenas atualizar as estatísticas.

A segunda coisa a se pensar é adicionar índices de acordo com as sugestões de Remus. Com um índice melhor / diferente, um valor versus outro pode ser mais estável e não variar muito.

Se isso não ajudar, a terceira coisa a tentar é forçar um novo plano toda vez que você executar a instrução, usando a palavra-chave RECOMPILE:

selecione * da [tabela grande] em que serial_number = [algum número] faça o pedido por test_date desc OPTION (RECOMPILE)

Há um artigo descrevendo uma situação semelhante aqui . Francamente, eu só tinha visto RECOMPILE aplicado a procedimentos armazenados antes, mas parece funcionar com instruções SELECT "regulares" para. Kimberly Tripp nunca me enganou.

Você também pode procurar no recurso chamado " guias de plano ", mas é mais complexo e pode ser um exagero.

darin strait
fonte
Para cobrir algumas dessas preocupações: 1. As estatísticas foram atualizadas, estão sendo atualizadas. 2. Tentamos indexar de várias maneiras (cobrindo índices, etc.), mas o problema parece estar mais ligado ao order byuso em relação a um índice de data e hora especificamente. 3. Tentei sua idéia com a opção RECOMPILE, ela ainda falhou, o que me surpreendeu um pouco, eu esperava que funcionasse, embora não saiba se é uma solução para a produção.
DBtheDBA