Atualizando uma tabela com eficiência usando JOIN

8

Tenho uma tabela com os detalhes das famílias e outra com os detalhes de todas as pessoas associadas às famílias. Para a tabela doméstica, tenho uma chave primária definida usando duas colunas - [tempId,n]. Para a tabela de pessoas, eu tenho uma chave primária definida usando 3 de suas colunas[tempId,n,sporder]

Usando a classificação ditada pela indexação em cluster nas chaves primárias, gerei um ID exclusivo para cada família [HHID]e [PERID]registro de cada pessoa (o trecho abaixo é para gerar PERID):

 ALTER TABLE dbo.persons
 ADD PERID INT IDENTITY
 CONSTRAINT [UQ dbo.persons HHID] UNIQUE;

Agora, meu próximo passo é associar cada pessoa às famílias correspondentes, ou seja; mapear a [PERID]para a [HHID]. A faixa de pedestres entre as duas tabelas é baseada nas duas colunas [tempId,n]. Para isso, tenho a seguinte declaração de junção interna.

UPDATE t1
  SET t1.HHID = t2.HHID
  FROM dbo.persons AS t1
  INNER JOIN dbo.households AS t2
  ON t1.tempId = t2.tempId AND t1.n = t2.n;

Eu tenho um total de 1928783 registros domésticos e 5239842 registros pessoais. O tempo de execução é atualmente muito alto.

Agora, minhas perguntas:

  1. É possível otimizar ainda mais essa consulta? De maneira mais geral, quais são as regras básicas para otimizar uma consulta de junção?
  2. Existe outra construção de consulta que pode alcançar o resultado desejado com melhor tempo de execução?

Fiz upload do plano de execução gerado pelo SQL Server 2008 para todo o script no SQLPerformance.com

sriramn
fonte

Respostas:

19

Tenho certeza de que as definições da tabela estão próximas disso:

CREATE TABLE dbo.households
(
    tempId  integer NOT NULL,
    n       integer NOT NULL,
    HHID    integer IDENTITY NOT NULL,

    CONSTRAINT [UQ dbo.households HHID] 
        UNIQUE NONCLUSTERED (HHID),

    CONSTRAINT [PK dbo.households tempId, n]
    PRIMARY KEY CLUSTERED (tempId, n)
);

CREATE TABLE dbo.persons
(
    tempId  integer NOT NULL,
    sporder integer NOT NULL,
    n       integer NOT NULL,
    PERID   integer IDENTITY NOT NULL,
    HHID    integer NOT NULL,

    CONSTRAINT [UQ dbo.persons HHID]
        UNIQUE NONCLUSTERED (PERID),

    CONSTRAINT [PK dbo.persons tempId, n, sporder]
        PRIMARY KEY CLUSTERED (tempId, n, sporder)
);

Não tenho estatísticas para essas tabelas ou para seus dados, mas pelo menos a seguir definirá a cardinalidade da tabela correta (a contagem de páginas é uma suposição):

UPDATE STATISTICS dbo.persons 
WITH 
    ROWCOUNT = 5239842, 
    PAGECOUNT = 100000;

UPDATE STATISTICS dbo.households 
WITH 
    ROWCOUNT = 1928783, 
    PAGECOUNT = 25000;

Análise do plano de consulta

A consulta que você tem agora é:

UPDATE P
SET HHID = H.HHID
FROM dbo.households AS H
JOIN dbo.persons AS P
    ON P.tempId = H.tempId
    AND P.n = H.n;

Isso gera o plano bastante ineficiente:

Plano padrão

Os principais problemas deste plano são a junção e a classificação do hash. Ambos exigem uma concessão de memória (a junção de hash precisa criar uma tabela de hash, e a classificação precisa de espaço para armazenar as linhas enquanto a classificação progride). O Plan Explorer mostra que esta consulta recebeu 765 MB:

Concessão de memória

É bastante memória do servidor para dedicar a uma consulta! Mais exatamente, essa concessão de memória é corrigida antes do início da execução, com base na contagem de linhas e nas estimativas de tamanho.

Se a memória for insuficiente no tempo de execução, pelo menos alguns dados para o hash e / ou classificação serão gravados no disco tempdb físico . Isso é conhecido como 'derramamento' e pode ser uma operação muito lenta. Você pode rastrear esses derramamentos (no SQL Server 2008) usando os eventos do Profiler Hash Warnings e Sort Warnings .

A estimativa para a entrada de construção da tabela de hash é muito boa:

Entrada Hash

A estimativa para a entrada de classificação é menos precisa:

Classificar entrada

Você precisaria usar o Profiler para verificar, mas suspeito que a classificação se espalhe para tempdb nesse caso. Também é possível que a tabela de hash também se espalhe, mas isso é menos claro.

Observe que a memória reservada para esta consulta é dividida entre a tabela de hash e a classificação, porque elas são executadas simultaneamente. A propriedade do plano de frações de memória mostra a quantidade relativa da concessão de memória que se espera que seja usada por cada operação.

Por que classificar e hash?

A classificação é introduzida pelo otimizador de consulta para garantir que as linhas cheguem ao operador Atualização de Índice em Cluster em ordem de chave em cluster. Isso promove o acesso seqüencial à tabela, que geralmente é muito mais eficiente que o acesso aleatório.

A junção de hash é uma opção menos óbvia, porque suas entradas são de tamanhos semelhantes (para uma primeira aproximação, pelo menos). A junção de hash é melhor quando uma entrada (a que cria a tabela de hash) é relativamente pequena.

Nesse caso, o modelo de custo do otimizador determina que a junção de hash é a mais barata das três opções (hash, mesclagem, loops aninhados).

Melhorando a performance

O modelo de custo nem sempre acerta. Ele tende a superestimar o custo da junção de mesclagem paralela, especialmente à medida que o número de encadeamentos aumenta. Podemos forçar uma junção de mesclagem com uma dica de consulta:

UPDATE P
SET HHID = H.HHID
FROM dbo.households AS H
JOIN dbo.persons AS P
    ON P.tempId = H.tempId
    AND P.n = H.n
OPTION (MERGE JOIN);

Isso produz um plano que não requer tanta memória (porque a junção de mesclagem não precisa de uma tabela de hash):

Mesclar plano

A classificação problemática ainda está lá, porque a junção de mesclagem preserva apenas a ordem de suas chaves de junção (tempId, n), mas as chaves agrupadas são (tempId, n, sporder). Você pode achar que o plano de junção de mesclagem não tem desempenho melhor que o plano de junção de hash.

Junção de loops aninhados

Também podemos tentar uma junção de loops aninhados:

UPDATE P
SET HHID = H.HHID
FROM dbo.households AS H
JOIN dbo.persons AS P
    ON P.tempId = H.tempId
    AND P.n = H.n
OPTION (LOOP JOIN);

O plano para esta consulta é:

Plano de loops aninhados em série

Esse plano de consulta é considerado o pior pelo modelo de custo do otimizador, mas possui alguns recursos muito desejáveis. Primeiro, a junção de loops aninhados não requer uma concessão de memória. Segundo, ele pode preservar a ordem das chaves da Personstabela para que não seja necessária uma classificação explícita. Você pode achar que esse plano tem um desempenho relativamente bom, talvez até bom o suficiente.

Loops aninhados paralelos

A grande desvantagem do plano de loops aninhados é que ele é executado em um único encadeamento. É provável que essa consulta se beneficie do paralelismo, mas o otimizador decide que não há vantagem em fazer isso aqui. Isso também não é necessariamente correto. Infelizmente, não há dica de consulta interna para obter um plano paralelo, mas há uma maneira não documentada:

UPDATE t1
  SET t1.HHID = t2.HHID
  FROM dbo.persons AS t1
  INNER JOIN dbo.households AS t2
  ON t1.tempId = t2.tempId AND t1.n = t2.n
OPTION (LOOP JOIN, QUERYTRACEON 8649);

A ativação do sinalizador de rastreamento 8649 com a QUERYTRACEONdica produz este plano:

Plano de loops aninhados paralelos

Agora, temos um plano que evita a classificação, não requer memória extra para a associação e usa o paralelismo de maneira eficaz. Você deve encontrar esta consulta com desempenho muito melhor que as alternativas.

Mais informações sobre paralelismo no meu artigo Forçando um plano de execução de consulta paralela :

Paul White 9
fonte
1

Observando o seu plano de consulta, é possível que o seu problema real não seja a junção em si, mas o processo real de atualização.

Pelo que pude ver, é provável que você esteja atualizando todos os registros de pessoas em seu banco de dados e atualizando índices (não consigo ver quais outros índices existem, então não sei se isso pode ser um fator)

Se esta for uma tarefa pontual, você pode desativar os índices, executar a atualização e reconstruir os índices?

Depois de preencher os dados, você pode adicionar uma cláusula where à sua consulta para atualizar apenas os registros que precisam ser atualizados

TomGough
fonte