AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • Início
  • system&network
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • Início
  • system&network
    • Recentes
    • Highest score
    • tags
  • Ubuntu
    • Recentes
    • Highest score
    • tags
  • Unix
    • Recentes
    • tags
  • DBA
    • Recentes
    • tags
  • Computer
    • Recentes
    • tags
  • Coding
    • Recentes
    • tags
Início / dba / Perguntas / 202419
Accepted
PicoDeGallo
PicoDeGallo
Asked: 2018-03-28 06:59:25 +0800 CST2018-03-28 06:59:25 +0800 CST 2018-03-28 06:59:25 +0800 CST

DATEADD não está produzindo uma expectativa SARGable de uma busca de índice

  • 772

Eu tenho uma [UserActivity]tabela básica que captura um ActivityTypeIdpor UserIde o ActivityDateem que a atividade ocorreu.

Estou escrevendo uma consulta/procedimento armazenado que permite a entrada do @UserId, @ForTypeId, bem como o @DurationIntervale @DurationIncrementpara retornar resultados dinamicamente com base no número N de segundos/minutos/horas/dias/meses/anos. Dado que o datepartargumento dentro DATEADD/DATEDIFFnão permite parâmetros, eu tive que reverter um pouco de truque para obter os resultados desejados dentro da WHEREcláusula.

Inicialmente escrevi a consulta usando DATEDIFF, mas imediatamente após escrever e dar uma olhada no plano de execução, lembrei que não é uma função SARGable (junto com o fato de que os níveis de precisão podem oferecer para algumas datas caindo em um ano bissexto). Então, eu reescrevi a consulta para utilizar DATEPARTo pensamento de que eu atingiria uma busca de índice em vez de uma varredura de índice e geralmente teria um desempenho melhor.

Infelizmente, descobri que escrever a consulta como DATEADDoferece os mesmos resultados: uma verificação de índice está ocorrendo e o otimizador de consulta não está aproveitando o índice não clusterizado em relação ao [ActivityDate].

Eu li a postagem no blog de Aaron Bertrand, "Performance Surprises and Assumptions: DATEADD" e implementei as mudanças que ele descreveu para CONVERTa DATEADDparte na datetime2definição de coluna equivalente devido a truques estranhos envolvidos com datetime2. No entanto, o problema ainda estava presente mesmo depois de fazê-lo.

Para ilustrar melhor o cenário, aqui está uma definição de tabela comparável.

DROP TABLE IF EXISTS [dbo].[UserActivity]
IF OBJECT_ID('[dbo].[UserActivity]', 'U') IS NULL
BEGIN
    CREATE TABLE [dbo].[UserActivity] (
        [UserId] [int] NOT NULL
        ,[UserActivityId] [bigint] IDENTITY(1,1) NOT NULL
        ,[ActivityTypeId] [tinyint] NOT NULL
        ,[ActivityDate] [datetime2](0) NOT NULL CONSTRAINT [DF_UserActivity_ActivityDate] DEFAULT GETDATE()
        ,CONSTRAINT [PK_UserActivity] PRIMARY KEY CLUSTERED ([UserActivityId] ASC)
        ,INDEX [IX_UserActivity_UserId] NONCLUSTERED ([UserId] ASC)
        ,INDEX [IX_UserActivity_ActivityTypeId] NONCLUSTERED ([ActivityTypeId] ASC)
        ,INDEX [IX_UserActivity_ActivityDate] NONCLUSTERED ([ActivityDate] ASC)
    )
END;
GO

Preencha a tabela com dados fictícios recursivamente para 5 usuários diferentes com um aleatório ActivityTypeIdentre 1 e 10 com um novo a ActivityDatecada 4 minutos.

DECLARE @UserId int = (SELECT ISNULL((SELECT TOP (1) [UserId] + 1 FROM [dbo].[UserActivity] ORDER BY [UserId] DESC), 1))
;WITH [UserActivitySeed] AS (
    SELECT
        CONVERT(datetime2(0), '01/01/2018') AS 'ActivityDate'
    UNION ALL
    SELECT
        DATEADD(minute, 4, [ActivityDate])
    FROM
        [UserActivitySeed]
    WHERE
        [ActivityDate] < '2018-04-01')
INSERT INTO [dbo].[UserActivity] ([UserId], [ActivityTypeId], [ActivityDate])
SELECT
    @UserId
    ,ABS(CHECKSUM(NEWID()) % 9) + 1
    ,[ActivityDate]
FROM
    [UserActivitySeed] OPTION (MAXRECURSION 32767);

GO 5

ALTER INDEX ALL ON [dbo].[UserActivity] REBUILD;

Abaixo está a primeira consulta que escrevi com DATEDIFF. Observe que estou excluindo os predicados @UserIde @ForTypeIdintencionalmente para evitar essas pesquisas de chave e reduzir o ruído nos planos anexados.

Como você encontrará em PasteThePlan para esta consulta , ele está realizando uma verificação de índice conforme o esperado, uma vez que DATEDIFFnão é SARGable.

DECLARE @UserId int = 1
DECLARE @ForTypeId int = 3
DECLARE @DurationInterval varchar(6) = 'hour'
DECLARE @DurationIncrement int = 1

SELECT
    COUNT(UA.[UserActivityId]) AS 'ActivityTypeCount'
FROM
    [dbo].[UserActivity] UA
WHERE
    -- Exclude the @UserId and @ForTypeId predicates.
    -- UA.[UserId] = @UserId
    -- AND UA.[ActivityTypeId] = @ForTypeId
    -- AND 
    CASE
        WHEN @DurationInterval IN ('year', 'yy', 'yyyy') THEN DATEDIFF(SECOND, UA.[ActivityDate], GETDATE()) / 3600.0 / 24.0 / 365.25
        WHEN @DurationInterval IN ('month', 'mm', 'm') THEN DATEDIFF(SECOND, UA.[ActivityDate], GETDATE()) / 3600.0 / 24.0 / 365.25 * 12
        WHEN @DurationInterval IN ('day', 'dd', 'd') THEN DATEDIFF(SECOND, UA.[ActivityDate], GETDATE()) / 3600.0 / 24.0
        WHEN @DurationInterval IN ('hour', 'hh') THEN DATEDIFF(SECOND, UA.[ActivityDate], GETDATE()) / 3600.0
        WHEN @DurationInterval IN ('minute', 'mi', 'n') THEN DATEDIFF(SECOND, UA.[ActivityDate], GETDATE()) / 60.0
        WHEN @DurationInterval IN ('second', 'ss', 's') THEN DATEDIFF(SECOND, UA.[ActivityDate], GETDATE())
    END < @DurationIncrement

Abaixo está a DATEADDconsulta. PasteThePlan aqui. Infelizmente, uma busca de índice não está ocorrendo. Esta pode ser uma suposição incorreta da minha parte, mas estou perplexo por que isso não está ocorrendo.

DECLARE @UserId int = 1
DECLARE @ForTypeId int = 3
DECLARE @DurationInterval varchar(6) = 'hour'
DECLARE @DurationIncrement int = 1

SELECT
    COUNT(UA.[UserActivityId]) AS 'ActivityTypeCount'
FROM
    [dbo].[UserActivity] UA
WHERE
    -- Exclude the @UserId and @ForTypeId predicates.
    -- UA.[UserId] = @UserId
    -- AND UA.[ActivityTypeId] = @ForTypeId
    -- AND 
    (
        (@DurationInterval IN ('year', 'yy', 'yyyy') AND UA.[ActivityDate] > CONVERT(datetime2(0), DATEADD(YEAR, -@DurationIncrement, GETDATE())))
        OR
        (@DurationInterval IN ('month', 'mm', 'm') AND UA.[ActivityDate] > CONVERT(datetime2(0), DATEADD(MONTH, -@DurationIncrement, GETDATE())))
        OR
        (@DurationInterval IN ('day', 'dd', 'd') AND UA.[ActivityDate] > CONVERT(datetime2(0), DATEADD(DAY, -@DurationIncrement, GETDATE())))
        OR
        (@DurationInterval IN ('hour', 'hh') AND UA.[ActivityDate] > CONVERT(datetime2(0), DATEADD(HOUR, -@DurationIncrement, GETDATE())))
        OR
        (@DurationInterval IN ('minute', 'mi', 'n') AND UA.[ActivityDate] > CONVERT(datetime2(0), DATEADD(MINUTE, -@DurationIncrement, GETDATE())))
        OR
        (@DurationInterval IN ('second', 'ss', 's') AND UA.[ActivityDate] > CONVERT(datetime2(0), DATEADD(SECOND, -@DurationIncrement, GETDATE())))
        )

Qual é a causa disso? O comportamento que estou vendo é resultado do meu uso de ORnegação de qualquer potencial para que ele chegue a usar o índice? Estou ignorando algo meticulosamente óbvio aqui?

ATUALIZAÇÃO: Minha segunda pergunta acima me levou a realizar uma consulta anterior às ORoperações. A consulta executou a busca de índice, então algo está ocorrendo durante essas comparações que o SQL Server não gosta. PasteThePlan aqui.

DECLARE @DurationIncrement int = 1

SELECT
    COUNT(UA.[UserActivityId]) AS 'ActivityTypeCount'
FROM
    [dbo].[UserActivity] UA
WHERE
    UA.[ActivityDate] > CONVERT(datetime2(0), DATEADD(HOUR, -@DurationIncrement, GETDATE()))

ATUALIZAÇÃO: Solução compartilhada aqui.

sql-server index
  • 4 4 respostas
  • 908 Views

4 respostas

  • Voted
  1. Best Answer
    Daniel Hutmacher
    2018-03-28T07:18:24+08:002018-03-28T07:18:24+08:00

    A ORcondição é avaliada em tempo de compilação, em vez de em tempo de execução, o que significa que sua WHEREcondição não gera uma busca.

    E apenas para limpar o código, refatorei o seu CONVERTpara tornar o código um pouco mais legível.

    Eu tentaria mudar a WHEREcláusula para:

    UA.[ActivityDate]>CONVERT(datetime2(0), (CASE
        WHEN @DurationInterval IN ('year', 'yy', 'yyyy') THEN DATEADD(year, -@DurationIncrement, GETDATE())
        WHEN @DurationInterval IN ('month', 'mm', 'm')   THEN DATEADD(month, -@DurationIncrement, GETDATE())
        WHEN ...
        END))
    

    Não tenho acesso a um ambiente onde posso verificar isso, mas por favor me avise se funcionar.

    • 9
  2. Mark Sinkinson
    2018-03-28T07:24:49+08:002018-03-28T07:24:49+08:00

    Na compilação, o SQL Server não sabe o valor de @DurationIntervale, portanto, compila o plano mais adequado para recuperar os dados para qualquer cenário possível.

    Você pode provar isso adicionando uma WITH (FORCESEEK)opção à consulta, que mostra que, para fazer uma Busca de Índice para uma determinada consulta, haverá uma busca individual para cada ORcondição.

    https://www.brentozar.com/pastetheplan/?id=HkE3lkuqf

    insira a descrição da imagem aqui

    A varredura é determinada como uma maneira mais otimizada de recuperar os dados do que 6 buscas.

    @Daniel Hutmacher fornece uma solução ideal que executa uma busca de índice único em arquivos IX_UserActivity_ActivityDate. Como alternativa, você pode adicionar um OPTION(RECOMPILE), embora isso force a recompilação toda vez que a consulta for executada, potencialmente causando mais danos do que benefícios.

    • 7
  3. David Spillett
    2018-03-28T07:17:56+08:002018-03-28T07:17:56+08:00

    Uma consulta de "pia de cozinha" como essa (várias cláusulas de filtragem distintas, uma ou mais das quais é usada dependendo do valor de uma entrada) nunca será sargável, mesmo que todas as suas cláusulas individuais sejam.

    As duas opções rápidas são dividi-los em procedimentos individuais e chamar cada um conforme necessário por um procedimento mestre ou usar SQL ad-hoc.

    Para um artigo detalhado descrevendo várias opções para esse tipo de consulta/procedimento, consulte http://www.sommarskog.se/dyn-search.html

    • 6
  4. PicoDeGallo
    2018-03-28T12:59:49+08:002018-03-28T12:59:49+08:00

    Para referência futura, esta é a solução que encontrei com base na resposta proposta de Daniel Hutmatcher.

    DECLARE @UserId int = 1
    DECLARE @ForTypeId int = 3
    DECLARE @DurationInterval varchar(6) = 'hour'
    DECLARE @DurationIncrement int = 1
    
    SELECT
        COUNT(UA.[UserActivityId]) AS 'ActivityTypeCount'
    FROM
        [dbo].[UserActivity] UA
    WHERE
        -- Exclude the @UserId and @ForTypeId predicates.
        -- UA.[UserId] = @UserId
        -- AND UA.[ActivityTypeId] = @ForTypeId
        -- AND 
        UA.[ActivityDate] > CONVERT(datetime2(0),
        (CASE
            WHEN @DurationInterval IN ('year', 'yy', 'yyyy') THEN DATEADD(YEAR, -@DurationIncrement, GETDATE())
            WHEN @DurationInterval IN ('month', 'mm', 'm') THEN DATEADD(MONTH, -@DurationIncrement, GETDATE())
            WHEN @DurationInterval IN ('day', 'dd', 'd') THEN DATEADD(DAY, -@DurationIncrement, GETDATE())
            WHEN @DurationInterval IN ('hour', 'hh') THEN DATEADD(HOUR, -@DurationIncrement, GETDATE())
            WHEN @DurationInterval IN ('minute', 'mi', 'n') THEN DATEADD(MINUTE, -@DurationIncrement, GETDATE())
            WHEN @DurationInterval IN ('second', 'ss', 's') THEN DATEADD(SECOND, -@DurationIncrement, GETDATE())
        END))
    
    • 3

relate perguntas

  • Quais são as principais causas de deadlocks e podem ser evitadas?

  • Quanto "Padding" coloco em meus índices?

  • Como determinar se um Índice é necessário ou necessário

  • O que significa "índice" em RDBMSs? [fechado]

  • Como criar um índice condicional no MySQL?

Sidebar

Stats

  • Perguntas 205573
  • respostas 270741
  • best respostas 135370
  • utilizador 68524
  • Highest score
  • respostas
  • Marko Smith

    conectar ao servidor PostgreSQL: FATAL: nenhuma entrada pg_hba.conf para o host

    • 12 respostas
  • Marko Smith

    Como fazer a saída do sqlplus aparecer em uma linha?

    • 3 respostas
  • Marko Smith

    Selecione qual tem data máxima ou data mais recente

    • 3 respostas
  • Marko Smith

    Como faço para listar todos os esquemas no PostgreSQL?

    • 4 respostas
  • Marko Smith

    Listar todas as colunas de uma tabela especificada

    • 5 respostas
  • Marko Smith

    Como usar o sqlplus para se conectar a um banco de dados Oracle localizado em outro host sem modificar meu próprio tnsnames.ora

    • 4 respostas
  • Marko Smith

    Como você mysqldump tabela (s) específica (s)?

    • 4 respostas
  • Marko Smith

    Listar os privilégios do banco de dados usando o psql

    • 10 respostas
  • Marko Smith

    Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL?

    • 4 respostas
  • Marko Smith

    Como faço para listar todos os bancos de dados e tabelas usando o psql?

    • 7 respostas
  • Martin Hope
    Jin conectar ao servidor PostgreSQL: FATAL: nenhuma entrada pg_hba.conf para o host 2014-12-02 02:54:58 +0800 CST
  • Martin Hope
    Stéphane Como faço para listar todos os esquemas no PostgreSQL? 2013-04-16 11:19:16 +0800 CST
  • Martin Hope
    Mike Walsh Por que o log de transações continua crescendo ou fica sem espaço? 2012-12-05 18:11:22 +0800 CST
  • Martin Hope
    Stephane Rolland Listar todas as colunas de uma tabela especificada 2012-08-14 04:44:44 +0800 CST
  • Martin Hope
    haxney O MySQL pode realizar consultas razoavelmente em bilhões de linhas? 2012-07-03 11:36:13 +0800 CST
  • Martin Hope
    qazwsx Como posso monitorar o andamento de uma importação de um arquivo .sql grande? 2012-05-03 08:54:41 +0800 CST
  • Martin Hope
    markdorison Como você mysqldump tabela (s) específica (s)? 2011-12-17 12:39:37 +0800 CST
  • Martin Hope
    Jonas Como posso cronometrar consultas SQL usando psql? 2011-06-04 02:22:54 +0800 CST
  • Martin Hope
    Jonas Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL? 2011-05-28 00:33:05 +0800 CST
  • Martin Hope
    Jonas Como faço para listar todos os bancos de dados e tabelas usando o psql? 2011-02-18 00:45:49 +0800 CST

Hot tag

sql-server mysql postgresql sql-server-2014 sql-server-2016 oracle sql-server-2008 database-design query-performance sql-server-2017

Explore

  • Início
  • Perguntas
    • Recentes
    • Highest score
  • tag
  • help

Footer

AskOverflow.Dev

About Us

  • About Us
  • Contact Us

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve