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 / 18637
Accepted
Mark S. Rasmussen
Mark S. Rasmussen
Asked: 2012-06-01 06:49:05 +0800 CST2012-06-01 06:49:05 +0800 CST 2012-06-01 06:49:05 +0800 CST

Varreduras inesperadas durante a operação de exclusão usando WHERE IN

  • 772

Eu tenho uma consulta como a seguinte:

DELETE FROM tblFEStatsBrowsers WHERE BrowserID NOT IN (
    SELECT DISTINCT BrowserID FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID IS NOT NULL
)

tblFEStatsBrowsers tem 553 linhas.
tblFEStatsPaperHits tem 47.974.301 linhas.

tblFEStatsNavegadores:

CREATE TABLE [dbo].[tblFEStatsBrowsers](
    [BrowserID] [smallint] IDENTITY(1,1) NOT NULL,
    [Browser] [varchar](50) NOT NULL,
    [Name] [varchar](40) NOT NULL,
    [Version] [varchar](10) NOT NULL,
    CONSTRAINT [PK_tblFEStatsBrowsers] PRIMARY KEY CLUSTERED ([BrowserID] ASC)
)

tblFEStatsPaperHits:

CREATE TABLE [dbo].[tblFEStatsPaperHits](
    [PaperID] [int] NOT NULL,
    [Created] [smalldatetime] NOT NULL,
    [IP] [binary](4) NULL,
    [PlatformID] [tinyint] NULL,
    [BrowserID] [smallint] NULL,
    [ReferrerID] [int] NULL,
    [UserLanguage] [char](2) NULL
)

Há um índice clusterizado em tblFEStatsPaperHits que não inclui BrowserID. A execução da consulta interna exigirá, portanto, uma varredura completa da tabela de tblFEStatsPaperHits - o que está totalmente OK.

Atualmente, uma varredura completa é executada para cada linha em tblFEStatsBrowsers, o que significa que tenho 553 varreduras completas da tabela de tblFEStatsPaperHits.

Reescrever apenas para WHERE EXISTS não altera o plano:

DELETE FROM tblFEStatsBrowsers WHERE NOT EXISTS (
    SELECT * FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID = tblFEStatsBrowsers.BrowserID
)

No entanto, conforme sugerido por Adam Machanic, adicionar uma opção HASH JOIN resulta no plano de execução ideal (apenas uma única varredura de tblFEStatsPaperHits):

DELETE FROM tblFEStatsBrowsers WHERE NOT EXISTS (
    SELECT * FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID = tblFEStatsBrowsers.BrowserID
) OPTION (HASH JOIN)

Agora, isso não é tanto uma questão de como consertar isso - posso usar a OPTION (HASH JOIN) ou criar uma tabela temporária manualmente. Estou mais me perguntando por que o otimizador de consulta usaria o plano que usa atualmente.

Como o QO não possui nenhuma estatística na coluna BrowserID, suponho que esteja assumindo o pior - 50 milhões de valores distintos, exigindo assim uma tabela de trabalho em memória/tempdb bastante grande. Dessa forma, a maneira mais segura é realizar verificações para cada linha em tblFEStatsBrowsers. Não há relacionamento de chave estrangeira entre as colunas BrowserID nas duas tabelas, portanto, o QO não pode deduzir nenhuma informação de tblFEStatsBrowsers.

É este, tão simples quanto parece, o motivo?

Atualização 1
Para fornecer algumas estatísticas: OPÇÃO (HASH JOIN):
208.711 leituras lógicas (12 varreduras)

OPÇÃO (LOOP JOIN, HASH GROUP):
11.008.698 leituras lógicas (~scan per BrowserID (339))

Sem opções:
11.008.775 leituras lógicas (~scan per BrowserID (339))

Atualização 2
Excelentes respostas, todos vocês - obrigado! Difícil escolher apenas um. Embora Martin tenha sido o primeiro e Remus forneça uma excelente solução, tenho que dar ao Kiwi por pensar nos detalhes :)

query sql-server-2008-r2
  • 3 3 respostas
  • 4406 Views

3 respostas

  • Voted
  1. Best Answer
    Paul White
    2012-06-04T01:51:28+08:002012-06-04T01:51:28+08:00

    "Estou me perguntando por que o otimizador de consulta usaria o plano que usa atualmente."

    Em outras palavras, a questão é por que o plano a seguir parece mais barato para o otimizador, em comparação com as alternativas (que são muitas ).

    Plano original

    O lado interno da junção está essencialmente executando uma consulta da seguinte forma para cada valor correlacionado de BrowserID:

    DECLARE @BrowserID smallint;
    
    SELECT 
        tfsph.BrowserID 
    FROM dbo.tblFEStatsPaperHits AS tfsph 
    WHERE 
        tfsph.BrowserID = @BrowserID 
    OPTION (MAXDOP 1);
    

    Verificação de acertos de papel

    Observe que o número estimado de linhas é 185.220 (não 289.013 ), pois a comparação de igualdade exclui implicitamente NULL(a menos que ANSI_NULLSseja OFF). O custo estimado do plano acima é de 206,8 unidades.

    Agora vamos adicionar uma TOP (1)cláusula:

    DECLARE @BrowserID smallint;
    
    SELECT TOP (1)
        tfsph.BrowserID 
    FROM dbo.tblFEStatsPaperHits AS tfsph 
    WHERE 
        tfsph.BrowserID = @BrowserID 
    OPTION (MAXDOP 1);
    

    Com TOP (1)

    O custo estimado agora é de 0,00452 unidades. A adição do operador físico Top define uma meta de linha de 1 linha no operador Top. A questão torna-se então como derivar um 'objetivo de linha' para o Clustered Index Scan; ou seja, quantas linhas a varredura deve processar antes que uma linha corresponda ao BrowserIDpredicado?

    A informação estatística disponível mostra 166 valores distintos BrowserID(1/[All Density] = 1/0.006024096 = 166). O cálculo de custos assume que os valores distintos são distribuídos uniformemente pelas linhas físicas, de modo que a meta da linha no Clustered Index Scan é definida como 166,302 (considerando a alteração na cardinalidade da tabela desde que as estatísticas amostradas foram reunidas).

    O custo estimado de varredura das 166 linhas esperadas não é muito grande (mesmo executado 339 vezes, uma vez para cada alteração de BrowserID) - o Clustered Index Scan mostra um custo estimado de 1,3219 unidades, mostrando o efeito de escala da meta de linha. Os custos do operador sem escala para E/S e CPU são mostrados como 153,931 e 52,8698 , respectivamente:

    Custos estimados dimensionados da meta de linha

    Na prática, é muito improvável que as primeiras 166 linhas varridas do índice (em qualquer ordem em que sejam retornadas) contenham um de cada um dos BrowserIDvalores possíveis. No entanto, o DELETEplano tem um custo total de 1,40921 unidades e é selecionado pelo otimizador por esse motivo. Bart Duncan mostra outro exemplo desse tipo em um post recente intitulado Row Goals Gone Rogue .

    Também é interessante notar que o operador Top no plano de execução não está associado ao Anti Semi Join (em particular o 'curto-circuito' que Martin menciona). Podemos começar a ver de onde vem o Top desabilitando primeiro uma regra de exploração chamada GbAggToConstScanOrTop :

    DBCC RULEOFF ('GbAggToConstScanOrTop');
    GO
    DELETE FROM tblFEStatsBrowsers 
    WHERE BrowserID NOT IN 
    (
        SELECT DISTINCT BrowserID 
        FROM tblFEStatsPaperHits WITH (NOLOCK) 
        WHERE BrowserID IS NOT NULL
    ) OPTION (MAXDOP 1, LOOP JOIN, RECOMPILE);
    GO
    DBCC RULEON ('GbAggToConstScanOrTop');
    

    GbAggToConstScanOrTop desativado

    Esse plano tem um custo estimado de 364.912 , e mostra que o Top substituiu um Group By Aggregate (agrupamento pela coluna correlacionada BrowserID). A agregação não se deve à redundância DISTINCTno texto da consulta: é uma otimização que pode ser introduzida por duas regras de exploração, LASJNtoLASJNonDist e LASJOnLclDist . Desativar esses dois também produz este plano:

    DBCC RULEOFF ('LASJNtoLASJNonDist');
    DBCC RULEOFF ('LASJOnLclDist');
    DBCC RULEOFF ('GbAggToConstScanOrTop');
    GO
    DELETE FROM tblFEStatsBrowsers 
    WHERE BrowserID NOT IN 
    (
        SELECT DISTINCT BrowserID 
        FROM tblFEStatsPaperHits WITH (NOLOCK) 
        WHERE BrowserID IS NOT NULL
    ) OPTION (MAXDOP 1, LOOP JOIN, RECOMPILE);
    GO
    DBCC RULEON ('LASJNtoLASJNonDist');
    DBCC RULEON ('LASJOnLclDist');
    DBCC RULEON ('GbAggToConstScanOrTop');
    

    Plano de Spool

    Esse plano tem um custo estimado de 40729,3 unidades.

    Sem a transformação de Group By para Top, o otimizador 'naturalmente' escolhe um plano hash join com BrowserIDagregação antes do anti semi join:

    DBCC RULEOFF ('GbAggToConstScanOrTop');
    GO
    DELETE FROM tblFEStatsBrowsers 
    WHERE BrowserID NOT IN 
    (
        SELECT DISTINCT BrowserID 
        FROM tblFEStatsPaperHits WITH (NOLOCK) 
        WHERE BrowserID IS NOT NULL
    ) OPTION (MAXDOP 1, RECOMPILE);
    GO
    DBCC RULEON ('GbAggToConstScanOrTop');
    

    Nenhum Plano DOP 1 Principal

    E sem a restrição MAXDOP 1, um plano paralelo:

    Nenhum plano paralelo superior

    Outra maneira de 'consertar' a consulta original seria criar o índice ausente nos BrowserIDrelatórios do plano de execução. Os loops aninhados funcionam melhor quando o lado interno é indexado. Estimar a cardinalidade para semijunções é desafiador na melhor das hipóteses. Não ter uma indexação adequada (a tabela grande nem tem uma chave exclusiva!) não ajudará em nada.

    Escrevi mais sobre isso em Row Goals, Part 4: The Anti Join Anti Pattern .

    • 61
  2. Martin Smith
    2012-06-03T04:38:51+08:002012-06-03T04:38:51+08:00

    Quando executo seu script para criar um banco de dados somente de estatísticas e a consulta na pergunta, obtenho o seguinte plano.

    Plano

    As Cardinalidades da Tabela mostradas no plano são

    • tblFEStatsPaperHits:48063400
    • tblFEStatsBrowsers:339

    Assim, estima que precisará realizar a varredura em tblFEStatsPaperHits339 vezes. Cada varredura tem o predicado correlacionado tblFEStatsBrowsers.BrowserID=tblFEStatsPaperHits.BrowserID AND tblFEStatsPaperHits.BrowserID IS NOT NULLque é inserido no operador de varredura.

    No entanto, o plano não significa que haverá 339 varreduras completas. Como está sob um operador anti semi join, assim que a primeira linha correspondente em cada varredura é encontrada, pode causar um curto-circuito no restante. O custo estimado da subárvore para este nó é 1.32603e o plano inteiro tem um custo de 1.41337.

    Para o Hash Join dá o plano abaixo

    Hash Join

    O plano geral tem 418.415um custo (cerca de 300 vezes mais caro do que o plano de loops aninhados) com a varredura de índice clusterizada completa única tblFEStatsPaperHitscustando 206.8apenas um. Compare isso com a 1.32603estimativa de 339 varreduras parciais fornecidas anteriormente (Custo médio estimado de varredura parcial = 0.003911592).

    Portanto, isso indicaria que cada varredura parcial está custando 53.000 vezes menos do que uma varredura completa. Se os custos fossem dimensionados linearmente com a contagem de linhas, isso significaria que, em média, seria necessário processar apenas 900 linhas em cada iteração antes de encontrar uma linha correspondente e causar um curto-circuito.

    No entanto, não acho que os custos sejam dimensionados dessa maneira linear. Acho que eles também incorporam algum elemento de custo inicial fixo. Tentando vários valores de TOPna seguinte consulta

    SELECT TOP 147 BrowserID 
    FROM [dbo].[tblFEStatsPaperHits] 
    

    147fornece o custo de subárvore estimado mais próximo de 0.003911592em 0.0039113. De qualquer forma, está claro que está baseando o custo na suposição de que cada varredura terá que processar apenas uma pequena proporção da tabela, na ordem de centenas de linhas em vez de milhões.

    Não tenho certeza exatamente em que matemática ele baseia essa suposição e realmente não se soma às estimativas de contagem de linhas no restante do plano (as 236 linhas estimadas saindo da junção de loops aninhados implicariam que havia 236 casos em que nenhuma linha correspondente foi encontrada e uma verificação completa foi necessária). Presumo que este seja apenas um caso em que as suposições de modelagem feitas caem um pouco e deixam o plano de loops aninhados significativamente abaixo do custo.

    • 22
  3. Remus Rusanu
    2012-06-03T13:25:37+08:002012-06-03T13:25:37+08:00

    No meu livro, mesmo uma varredura de 50 milhões de linhas é inaceitável ... Meu truque usual é materializar os valores distintos e delegar ao mecanismo mantê-lo atualizado:

    create view [dbo].[vwFEStatsPaperHitsBrowserID]
    with schemabinding
    as
    select BrowserID, COUNT_BIG(*) as big_count
    from [dbo].[tblFEStatsPaperHits]
    group by [BrowserID];
    go
    
    create unique clustered index [cdxVwFEStatsPaperHitsBrowserID] 
      on [vwFEStatsPaperHitsBrowserID]([BrowserID]);
    go
    

    Isso fornece um índice materializado de uma linha por BrowserID, eliminando a necessidade de verificar 50 milhões de linhas. O mecanismo irá mantê-lo para você e o QO irá usá-lo 'como está' na declaração que você postou (sem qualquer sugestão ou reescrita de consulta).

    A desvantagem é, obviamente, a contenção. Qualquer operação de inserção ou exclusão tblFEStatsPaperHits(e eu acho que é uma tabela de log com inserções pesadas) terá que serializar o acesso a um determinado BrowserID. Existem maneiras de tornar isso viável (atualizações atrasadas, registro em dois estágios, etc.) se você estiver disposto a comprá-lo.

    • 20

relate perguntas

  • Como obter os nomes dos amigos de um usuário?

  • Consulta entre duas tabelas relacionadas

  • Alinhamento de data e extração de par correspondente melhor feito com TSQL ou C #?

  • LIKE para selecionar a existência independente da palavra em qualquer parte do texto

  • Randomizando o conteúdo da tabela e armazenando-o de volta na tabela

Sidebar

Stats

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

    Como ver a lista de bancos de dados no Oracle?

    • 8 respostas
  • Marko Smith

    Quão grande deve ser o mysql innodb_buffer_pool_size?

    • 4 respostas
  • Marko Smith

    Listar todas as colunas de uma tabela especificada

    • 5 respostas
  • Marko Smith

    restaurar a tabela do arquivo .frm e .ibd?

    • 10 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

    Como selecionar a primeira linha de cada grupo?

    • 6 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
    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
    pedrosanta Listar os privilégios do banco de dados usando o psql 2011-08-04 11:01:21 +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
  • Martin Hope
    bernd_k Quando devo usar uma restrição exclusiva em vez de um índice exclusivo? 2011-01-05 02:32:27 +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