Livro SQL Server - Plano de Execução

Fazer download em pdf ou txt
Fazer download em pdf ou txt
Você está na página 1de 515

Machine Translated by Google

Machine Translated by Google

Planos de execução do SQL Server

Terceira edição

Por

SQL Server 2008 até 2017


e Banco de Dados SQL do Azure

Por Grant Fritchey

Publicado por Redgate Publishing 2018


Machine Translated by Google

Primeira edição 2008

Direitos autorais Grant Fritchey 2008, 2012, 2018

ISBN 978-1-910035-22-1

O direito de Grant Fritchey de ser identificado como o autor deste trabalho foi afirmado por ele de acordo com o Copyright, Designs and
Patents Act 1988.

Todos os direitos reservados. Nenhuma parte desta publicação pode ser reproduzida, armazenada ou introduzida em um sistema de
recuperação, ou transmitida, de qualquer forma ou por qualquer meio (eletrônico, mecânico, fotocópia, gravação ou outro) sem o
consentimento prévio por escrito do editor. Qualquer pessoa que faça qualquer ato não autorizado em relação a esta publicação pode ser
responsabilizada por processos criminais e ações civis por danos.

Este livro é vendido sob a condição de que não seja, por meio de troca ou de outra forma, emprestado, revendido, alugado ou distribuído de
qualquer outra forma sem o consentimento prévio do editor em qualquer forma diferente da qual foi publicado e sem um condição
semelhante, incluindo esta condição sendo imposta ao editor subsequente.

Revisor Técnico: Hugo Kornelis

Editor: Tony Davis

Datilografado por Gower Associates


Machine Translated by Google

Conteúdo

Capítulo 1: Apresentando o Plano de Execução 26

O que acontece quando uma consulta é enviada? 27

Fase de compilação da consulta 28

Análise de consulta 28

Vinculação de consulta 28

Otimização de consultas 29

Fase de execução da consulta 31

Trabalhando com o otimizador 32

A importância das estatísticas 33

O cache do plano e a reutilização do plano 35

Planejar o envelhecimento 35

Limpando manualmente o cache do plano 36

Evitando a rotatividade de cache: parametrização de consultas 37

Recompilação do plano 38

Introdução aos planos de execução 38

Permissões necessárias para visualizar planos de execução 39


Machine Translated by Google

Formatos de plano de execução 39

planos XML 40

Planos de texto 40

Planos gráficos 41

Recuperando planos em cache 41

Planos para consultas ad hoc: planos estimados e reais 41

Os planos estimados e reais serão diferentes? 42

Capturando planos gráficos no SSMS 44

Capturando nosso primeiro plano 46

Os componentes de um plano de execução gráfica 47

Operadores 48

Setas de fluxo de dados 49

Custos estimados do operador 50

Custo total estimado da consulta em relação ao lote 51

Propriedades do operador 51

Dicas de ferramentas 53

Salvando planos de execução 55


Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura 57

A linguagem dos planos de execução 57

Operadores comuns 57

Lendo um plano: da direita para a esquerda ou da esquerda para a direita? 60

Operadores de streaming versus bloqueio 62

O que procurar em um plano de execução 63

Primeiro operador 63

Avisos 64

Número estimado versus número real de linhas 65

Custo do operador 65

Fluxo de dados 66

Operadores extras 67

Operadores de leitura 67

As informações por trás do primeiro operador 68

Nível de otimização 71

Lista de parâmetros 73

QueryHash e QueryPlanHash 75

DEFINIR opções 75

Outras ferramentas e técnicas úteis ao ler planos 76

Estatísticas de E/S e temporização usando comandos SET 77


Machine Translated by Google

Incluir estatísticas do cliente 78

SQL Trace e Profiler 78

Eventos estendidos 78

Capítulo 3: Operadores de leitura de dados 80

Lendo um índice 80

Verificações de índice 81

Verificação de índice clusterizado 82

Verificação de índice 85

As digitalizações são "ruins?" 86

Buscas de índice 87

Busca de índice agrupado 87

Busca de índice (não agrupado) 89

Principais pesquisas 91

Lendo uma pilha 94

Varredura de Tabela 94

Pesquisa RID 96

Capítulo 4: Unindo Dados 99

Operações de junção lógica 100

Cumprindo Comandos JOIN 100

Operador de loops aninhados 102


Machine Translated by Google

Propriedades do número estimado e real de linhas 104

Propriedade de referências externas 106

Propriedades de Religar e Rebobinar 107

Correspondência de Hash (juntar) 109

Como funcionam as junções do Hash Match 110

Tabelas de hash e hash 111

Considerações de desempenho para junções de correspondência de hash 111

Calcular escalar 113

Mesclar associação 115

Como funcionam as associações de mesclagem 115

Considerações de desempenho para junções de mesclagem 117

Junção adaptável 120

Outros usos de operadores de junção 124

Concatenar dados 126

Capítulo 5: Classificação e agregação de dados 129

Consultas com ORDER BY 129

Operações de classificação 130

Operações de classificação e a propriedade Ordered de Index Scans 131

Lidando com Sortes caras 132

Classificação N principal 133


Machine Translated by Google

Classificação distinta 135

Classificar avisos 136

Agregando dados 140

Agregado de fluxo 140

Correspondência de hash (agregado) 143

Filtrando agregações usando HAVING 146

Planos com agregações e spools 148

Carretel de mesa 149

Carretel de índice 150

Trabalhando com funções de janela 152

Capítulo 6: Planos de Execução para Modificações de Dados 159

Planos para INSERTs 159

Operador INSERT 161

Operador de varredura constante 162

Operador de inserção de índice clusterizado 165

Operador de declaração 167

Planos para ATUALIZAÇÕES 168

Operador de carretel de mesa (Eager Spool) 169

Operador de atualização de índice clusterizado 170

Planos para DELETEs 171


Machine Translated by Google

Um plano DELETE simples 171

Um plano DELETE por índice 173

Planos para consultas MERGE 177

Capítulo 7: Planos de execução para instruções T-SQL comuns 185

Procedimentos armazenados 185

Subconsultas 191

Tabelas derivadas usando APPLY 195

Expressões de Tabela Comuns 199

Visualizações 206

Visualizações padrão 206

Visualizações indexadas 208

Funções 212

Funções escalares 212

Funções com valor de tabela 216

Capítulo 8: Examinando o uso do índice 221

Índices padrão 221

Como o otimizador seleciona quais índices usar 222

Custos estimados e estatísticas 222

Estimativas de seletividade e cardinalidade 223

Índices e seletividade 223


Machine Translated by Google

Cabeçalho de estatísticas 226

Gráfico de densidade 226

O histograma 227

Usando índices de cobertura 230

O que pode dar errado? 231

Problemas com estatísticas 232

Problemas com sniffing de parâmetros 236

Procedimentos armazenados e sniffing de parâmetros 237

O que fazer se a detecção de parâmetros causar problemas de desempenho 240

Índices de armazenamento de colunas 241

Usando um índice columnstore para uma consulta de agregação 242

Empilhamento agregado 245

Nenhuma operação de busca no índice columnstore 246

Empilhamento de predicado em um índice columnstore 246

Modo de lote versus modo de linha 247

Índices otimizados para memória 248

Usando tabelas e índices com otimização de memória 248

Nenhuma opção para buscar um índice de hash para um intervalo de valores 253

Planos com procedimentos armazenados compilados nativamente 254


Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano 258

Consultando o cache do plano 258

Planejar reutilização e consultas ad hoc 260

O custo da compilação excessiva do plano 264

Parametrização simples para consultas ad hoc "triviais" 266

Parametrização simples em ação 266

Parametrização simples "insegura" 270

Programação para reutilização de planos: parametrização de consultas 273

Declarações preparadas 274

Procedimentos armazenados 278

O que pode dar errado com a reutilização do plano para consultas parametrizadas? 281

Corrigindo problemas com a reutilização do plano se você não puder reescrever a consulta 281

Otimizar para cargas de trabalho ad hoc 282

Parametrização forçada 285

Guias de plano 288

Guias de plano de modelo 289

Guias de plano SQL 291

Guias de plano de objetos 293

Visualizando, validando, desabilitando e removendo guias de plano 295


Machine Translated by Google

Forçar plano 296

Usando guias de plano para forçar o plano 297

Usando o Query Store para forçar o plano 300

Capítulo 10: Controlando os planos de execução com dicas 303

Os perigos de usar dicas 303

Dicas de consulta 304

HASH | GRUPO DE PEDIDO 305

MERGE | HASH | CONCAT UNIÃO 307

CIRCUITO | MERGE | HASH JOIN 309

RÁPIDO n 314

FORÇAR ORDEM 316

MAXDOP 319

OTIMIZAR PARA 322

RECOMPILAR 327

EXPANDIR VISUALIZAÇÕES 331

IGNORE_NONCLUSTERED_COLUMNSTORE_INDEX 332

Dicas de associação 333

Dicas de tabela 335

NOEXPAND 336
Machine Translated by Google

ÍNDICE() 337

FORCESEK/FORCESCAN 341

Capítulo 11: Paralelismo em Planos de Execução 344

Controlando a execução de consultas paralelas 344

Grau máximo de paralelismo 345

Limite de custo para paralelismo 347

Bloqueadores de execução de consultas paralelas 348

Execução de consulta paralela 349

Examinando um plano de execução paralela 350

Os planos paralelos são bons ou ruins? 358

Capítulo 12: Processamento em modo de lote 360

Processamento em modo de lote definido 360

Planejar consultas que são executadas no modo de lote 361

Modo em lote anterior ao SQL Server 2016 364

Misturando índices columnstore e rowstore 366

Concessão de memória adaptável no modo de lote 369

Perda de processamento em modo de lote 372

Capítulo 13: O XML dos Planos de Execução 374

Um breve tour pelo XML por trás de um plano 374

O XML para um plano estimado 374


Machine Translated by Google

O XML para um plano real 381

Salvando e Compartilhando com Segurança Planos de Execução 382

Quando você realmente precisará do plano XML 383

Use o plano XML para forçar o plano 383

Propriedades do primeiro operador ao capturar planos usando Extended Events 384

Pré-SQL Server 2012: detalhes completos do "índice ausente" 385

Consultando o cache do plano 386

Por que consultar o XML dos planos? 387

Consulte o XML do plano para operadores específicos 388

Consultando o XML para informações de índice ausentes 389

Capítulo 14: Planos para tipos de dados especiais e cursores 393

XML 394

Planos para consultas que convertem dados relacionais em XML (FOR XML) 394

Planos para consultas FOR XML básicas 394

Retornando XML como tipo de dados XML 397

Planos para o modo explícito para consultas XML 399

Planos para consultas que convertem XML em dados relacionais (OPENXML) 401

Planos para consultar XML usando XQuery 405

Planos para consultas que usam o método .exist 406

Planos para consultas que usam o método .query 408


Machine Translated by Google

Quando usar XQuery 412

Notação de Objeto JavaScript 413

Dados hierárquicos 418

Dados espaciais 420

Cursores 424

Cursor estático 424

Cursor de conjunto de teclas 431

Cursor dinâmico 434

Capítulo 15: Automatizando a captura do plano 436

Por que automatizar a captura do plano? 436

Ferramentas para automatizar a captura de planos 437

Automatizando a captura de planos usando Extended Events 438

Crie uma sessão de evento usando a GUI do SSMS 439

Criar uma sessão de evento em T-SQL 444

Visualizando os dados do evento 445

Garantir sessões de eventos "leves" ao capturar o plano 450

Automatizando a captura do plano usando o SQL Trace 452

Trace eventos para planos de execução 452

Criando um rastreamento XML do Showplan usando o Profiler 453

Criando um rastreamento do lado do servidor 456


Machine Translated by Google

Capítulo 16: O Repositório de Consultas 458

Comportamento do repositório de consultas 458

Opções do repositório de consultas 460

Recuperando Planos do Repositório de Consultas 462

Relatórios SSMS 462

Visão geral dos relatórios do repositório de consultas 463

O relatório Principais consultas que consomem recursos 466

Recuperar planos do Repositório de Consultas usando T-SQL 468

Planos de controle usando forçar plano 472

Como forçar um plano 473

Forçamento de plano automatizado 478

Remover planos do repositório de consultas 483

Capítulo 17: Ferramentas SSMS para explorar planos de execução 486

A pergunta 486

As ferramentas do SQL Server Management Studio 17 488

Analisar o Plano de Execução Real 489

Comparar plano de exibição 491

Localizar nó 496

Planos de execução ao vivo 498

Estatísticas ao vivo por operador usando sys.dm_exec_query_profiles 499


Machine Translated by Google

Usando o evento estendido query_thread_profile 501

Planos de execução ao vivo no SSMS 502

Visualizando o plano de execução ao vivo no Activity Monitor 503

Outras Ferramentas do Plano de Execução 505

Explorador de planos 505

Compreensão 505

Pacote de Ferramentas SSMS - Analisador de Plano de Execução 505

Ferramentas de monitoramento de desempenho do SQL Server 506


Machine Translated by Google

Sobre o autor
Grant Fritchey é um MVP do SQL Server com mais de 30 anos de experiência em TI, incluindo tempo
gasto em suporte, desenvolvimento e administração de banco de dados.

Grant trabalha com SQL Server desde a versão 6.0, em 1995. Desenvolveu em VB, VB.Net, C# e Java.
Grant ingressou na Redgate como evangelista de produtos em janeiro de 2011.

Ele escreve artigos para publicação no SQL Server Central, Simple Talk e outros sites da comunidade e
publicou vários livros, incluindo o que você está lendo agora e SQL Server Query Performance Tuning, 5th
Edition (Apress, 2018). Grant também escreve sobre este tópico e outros em https://scarydba.com.

Você pode contatá-lo através de [email protected].

Sobre o Revisor Técnico


Hugo Kornelis trabalha em TI há quase 35 anos, sendo que os últimos 20 foram focados quase que
totalmente no SQL Server.

Dentro da comunidade do SQL Server, Hugo respondeu a milhares de perguntas em vários fóruns online.
Ele também bloga em https://sqlserverfast.com/blog/, contribuiu com artigos para SQL Server Central e
Simple Talk e é autor de um curso da Pluralsight sobre design de banco de dados relacional. Ele foi
palestrante em muitas conferências na Europa e algumas no resto do mundo. Em reconhecimento às suas
contribuições à comunidade, a Microsoft premiou Hugo SQL Server MVP e Data Platform MVP 11 vezes
(2006–2016).

Hugo começou a documentar seu impressionante conhecimento de planos de execução em


sqlserverfast.com, que é um excelente recurso para quem terminou de ler este livro e quer saber ainda
mais sobre todos os detalhes em seus planos de execução.
Você encontrará artigos que expõem padrões interessantes ou incomuns em planos de execução e
descrevem exatamente como cada um funciona, bem como The SQL Server Execution Plan Reference
(https://sqlserverfast.com/epr/), que, eventualmente, listará todos os operadores, com seu comportamento
exato e todas as suas propriedades.

19
Machine Translated by Google

Introdução

Frequentemente, uma consulta T-SQL que você escreveu se comporta de maneira inesperada e causa tempos
de resposta lentos para os usuários do aplicativo e contenção de recursos no servidor. Às vezes, você não
escreveu a consulta incorreta; ele veio de um aplicativo de terceiros ou foi código gerado por uma camada de
Mapeamento Relacional de Objetos usada incorretamente. Em qualquer uma dessas situações, e em milhares de
outras, o ajuste de consultas torna-se bastante difícil.

Muitas vezes, é muito difícil dizer, apenas observando o código T-SQL, por que uma consulta está sendo
executada lentamente. SQL é uma linguagem declarativa e uma consulta T-SQL descreve apenas o conjunto de
dados que queremos que o SQL Server retorne. Ele não informa ao SQL Server como executar a consulta para
recuperar esses dados.

Quando submetemos uma consulta ao SQL Server, vários processos do servidor entram em ação cujo trabalho
coletivo é gerenciar a consulta ou modificação dos dados. Especificamente, um componente do mecanismo de
banco de dados relacional chamado Query Optimizer tem a função de examinar o texto da consulta enviada e
definir uma estratégia para executá-la. A estratégia assume a forma de um plano de execução, que contém uma
série de operadores, cada um descrevendo uma ação a ser executada nos dados.

Portanto, se uma consulta tiver um desempenho ruim e você não conseguir entender o motivo, o plano de
execução informará não apenas qual conjunto de dados está voltando, mas também o que o SQL Server fez e
em que ordem para obter esses dados . Ele revelará como os dados foram recuperados e de quais tabelas e
índices, quais tipos de junções foram usados, em que ponto ocorreu a filtragem e a classificação e muito mais. Esses
detalhes geralmente destacam a origem provável de qualquer problema.

O que o plano de execução revela

Um plano de execução é, literalmente, um conjunto de instruções sobre como executar uma consulta. O otimizador
passa cada plano para o mecanismo de execução, que executa a consulta de acordo com essas instruções. O
otimizador também armazena planos em uma área de memória chamada cache de planos, para que possa reutilizar
estratégias de execução existentes sempre que possível.

Durante o desenvolvimento e teste, você pode solicitar o plano com muita facilidade, usando alguns botões no SQL
Server Management Studio. Ao investigar um problema de consulta em um sistema de produção ativo, muitas vezes
você pode recuperar o plano usado para essa consulta do cache do plano ou do Repositório de consultas.

20
Machine Translated by Google

Armado com o plano de execução, você tem uma janela exclusiva para o que está acontecendo nos
bastidores do SQL Server e muitas informações sobre como o SQL Server decidiu resolver o T-SQL que você
passou para ele. Você pode ver coisas como:

• a ordem em que o otimizador escolheu acessar as tabelas referenciadas na consulta


• quais índices ele usou em cada tabela e como os dados foram extraídos deles
• quantas linhas o otimizador pensou que um operador retornaria, com base em sua
compreensão estatística das estruturas de dados e dados subjacentes e quantas linhas foram
encontradas na realidade
• como chaves e restrições referenciais afetam a compreensão do otimizador do
dados e, portanto, o comportamento de suas consultas
• como os dados estão sendo unidos entre as tabelas em sua consulta
• quando a filtragem e a classificação ocorreram, como os cálculos e agregações foram realizados
e muito mais.

Os planos de execução são uma de suas principais ferramentas para entender como o SQL Server faz o que
faz. Se você for um profissional de dados de qualquer tipo, haverá momentos em que precisará se aprofundar
em um plano de execução e, portanto, precisará saber para o que está olhando e como proceder.

Por isso escrevi este livro. Meu objetivo era reunir em um único local o máximo possível de informações
úteis sobre os planos de execução. Vou orientá-lo no processo de lê-los e mostrar como entender as
informações que eles apresentam a você. Especificamente, abordarei:

• como capturar planos de execução usando métodos manuais e automáticos


• um método documentado para interpretar os planos de execução, para que você possa entendê-los
em seu próprio ambiente
• como o SQL Server representa e interpreta os objetos comuns do SQL Server, como índices,
exibições, procedimentos armazenados, tabelas derivadas e assim por diante, em planos de execução
• como controlar os planos de execução com dicas e guias de plano, e por que isso é uma
faca de dois gumes
• como o Query Store trabalha e coleta dados sobre planos de execução e como você pode controlá-
los usando o Query Store.

Esses tópicos e muitos outros, todos relacionados aos planos de execução e seu comportamento, são
abordados ao longo deste livro. Concentro-me sempre nos detalhes dos planos de execução e em como os
comportamentos do SQL Server se manifestam nos planos de execução.

21
Machine Translated by Google

À medida que trabalhamos em cada tópico, explicarei todos os elementos individuais do plano de execução, como
cada operador funciona, como eles interagem e as condições em que cada operador trabalha com mais eficiência.
Com esse conhecimento, você terá tudo o que precisa para lidar com cada plano de execução, independentemente
da complexidade, e entender o que ele faz.

Corrigindo problemas de consulta usando planos de execução

Os planos de execução fornecem todas as informações de que você precisa para entender como o SQL
Server executou suas consultas. Paradoxalmente, porém, dado que a maioria das pessoas olha para um plano
de execução na esperança de melhorar o desempenho de uma consulta, este livro não é, e não poderia ser, um
livro sobre ajuste de desempenho de consulta. Os dois tópicos estão ligados, mas separados. Se você está
procurando especificamente por informações sobre como otimizar o T-SQL ou construir índices eficientes, então
você precisa de um livro dedicado a esses tópicos.

O plano de execução também não é o primeiro lugar a ser observado, se você precisar ajustar o desempenho
em um sistema de produção. Você verificará configurações incorretas de servidores ou configurações de banco de
dados, procurará pontos óbvios de contenção de recursos no servidor, que podem estar causando problemas
graves de bloqueio e bloqueio e assim por diante. Nesse ponto, se o desempenho ainda estiver lento, você
provavelmente terá reduzido a causa a algumas tabelas "quentes" e uma ou duas consultas nessas tabelas. Em
seguida, você pode examinar os planos e procurar as possíveis causas do problema.

No entanto, os planos de execução não são necessariamente projetados para ajudar o usuário ocasional a
encontrar rapidamente a causa de um problema de consulta, no calor do combate ao mau desempenho do SQL Server.
Você precisa primeiro ter investido tempo em aprender a "linguagem" do plano e como lê-lo, e o que levou o SQL
Server a escolher esse plano e esses operadores para executar sua consulta.

E este livro é esse investimento.

À medida que você trabalha com ele, você começará a reconhecer cada um dos diferentes operadores que o SQL
Server pode usar para acessar os dados em uma tabela, ou para unir duas tabelas, ou para agrupar e agregar dados.
À medida que você aprender como esses operadores funcionam e como eles processam os dados que recebem,
você começará a reconhecer por que alguns operadores são projetados para lidar com um pequeno número de
linhas e por que outros são melhores para conjuntos de dados maiores. Você começará a entender as "propriedades"
dos dados (como exclusividade e ordenação lógica) que permitirão que determinados operadores trabalhem com
mais eficiência.

Ao fazer conexões entre tudo isso e o comportamento e o desempenho de suas consultas, de repente você
descobrirá que tem uma expectativa do que um plano revelará antes

22
Machine Translated by Google

você até olha para ele, com base em sua compreensão da lógica da consulta e dos dados. Portanto, quaisquer
operadores inesperados no plano chamarão sua atenção e você saberá onde procurar possíveis problemas e
o que fazer sobre eles.

Você está agora no estágio em que pode usar planos para resolver problemas. Normalmente o otimizador faz
boas escolhas de plano. Ocasionalmente, erra. As possíveis causas são muitas. Talvez esteja faltando
informações críticas sobre o banco de dados, devido à falta de chaves ou restrições.
Adicioná-los pode melhorar o desempenho da consulta. Às vezes, sua compreensão estatística dos dados é
imprecisa ou desatualizada. Ele pode simplesmente não ter meios eficientes para recuperar o conjunto de dados
inicial e você precisa adicionar um índice ou modificar um existente. Às vezes, nossa lógica de consulta
simplesmente derrota a otimização eficiente, e o melhor caminho é reescrever, embora isso nem sempre seja
possível ao solucionar problemas de um sistema de produção.

O trabalho deste livro é ensiná-lo a ler o plano, para que você possa entender o que está causando o mau
desempenho. É então seu trabalho descobrir a melhor forma de corrigi-lo, armado com a compreensão dos planos
de execução que darão uma chance muito maior de sucesso.

Esse conhecimento também é extremamente valioso ao escrever novas consultas ou atualizar o código existente.
Depois de verificar se o código retorna os resultados corretos, você pode testar seu desempenho.
Está dentro das expectativas? Caso contrário, antes de rasgar a consulta e tentar novamente, observe o
plano, porque você pode ter cometido um erro simples que significa que o SQL Server não está executando-
o com a eficiência possível.

Se você puder testar a consulta em diferentes cargas de dados, poderá avaliar se o desempenho da consulta
será dimensionado sem problemas quando a consulta atingir um banco de dados de tamanho de produção
completo. À medida que o volume de dados cresce e os dados mudam, o otimizador geralmente cria um plano
diferente. Ainda é um plano eficiente? Caso contrário, talvez você possa tentar reescrever a consulta ou modificar
as estruturas de dados, para evitar problemas de desempenho antes que o código chegue à produção!

Antes de implantar o código T-SQL, todo desenvolvedor de banco de dados e DBA deve adquirir o hábito de
examinar o plano de execução de qualquer consulta que esteja além de um determinado nível de complexidade,
se for executada em um banco de dados de produção em larga escala.

23
Machine Translated by Google

Mudanças nesta terceira edição

A maneira como penso sobre como usar os planos de execução e como lê-los mudou muito ao longo dos anos. Agora
reorganizei o livro para refletir isso. Após os primeiros capítulos terem estabelecido uma compreensão dos fundamentos
do otimizador e como capturar planos de execução, os capítulos posteriores se concentram mais nos métodos de leitura
de planos, não apenas no que está nos operadores e suas propriedades. E, é claro, a Microsoft continuou a fazer
alterações no SQL Server, portanto, há novos operadores e mecanismos que devem ser cobertos.

Alguns dos novos tópicos incluem:

• automatizar a captura de planos de execução usando Extended Events

• novos avisos e operadores

• processamento em modo de lote

• processamento de consulta adaptável

• funcionalidade adicional adicionada ao SQL Server 2014, 2016 e 2017, bem como ao Banco de Dados
SQL do Azure.

Há muito mais mudanças porque, com a ajuda do meu editor de tecnologia, Hugo Kornelis, e do editor de longa data
(e sofredor), Tony Davis, basicamente reescrevemos este livro do zero.

Com o hiato ocasional, este livro levou mais de três anos para ser reescrito e, durante esse tempo, três versões do SQL
Server foram lançadas e quem sabe quantas mudanças no Azure foram introduzidas. A Microsoft também separou as
versões do SQL Server Management System (SSMS) do produto principal, de modo que cada vez mais novas
funcionalidades foram introduzidas, mais rapidamente. Fiz o meu melhor para acompanhar, e o texto deve estar
atualizado para maio de 2018. Quaisquer alterações que surgiram depois disso, não estarão nesta edição do livro.

Exemplos de código
Ao longo deste livro, fornecerei código T-SQL que você deve executar por conta própria, para gerar planos de execução.
A partir do seguinte URL, você pode obter todo o código necessário para experimentar os exemplos deste livro:

https://scarydba.com/resources/ExecutionPlansV3.zip.

24
Machine Translated by Google

A maior parte do código será executada em todas as edições e versões do SQL Server, a partir
do SQL Server 2012. A maioria, embora não todo, do código funcionará no Banco de Dados SQL do Azure.
Salvo indicação em contrário, todos os exemplos foram escritos e testados no banco de dados de
exemplo do SQL Server, AdventureWorks2014, e você pode obter uma cópia dele no GitHub: https://
bit.ly/2yyW1kh.

Se você testar o código em uma versão diferente do AdventureWorks ou se a Microsoft atualizar o


AdventureWorks2014, as estatísticas poderão ser alteradas e você poderá ver um plano de execução
diferente daquele exibido no livro. Se você estiver trabalhando com procedimentos e scripts diferentes dos
fornecidos, lembre-se de que os procedimentos armazenados criptografados não exibirão um plano de
execução.

Os planos de execução inicial serão simples e fáceis de ler a partir das amostras apresentadas no texto. À
medida que as consultas e os planos se tornam mais complicados, o livro irá descrever a situação mas, para
ver os planos gráficos de execução ou o conjunto completo de XML, será necessário que você gere os planos.
Então, por favor, leia este livro ao lado de sua máquina, se possível, para que você possa tentar executar cada
consulta você mesmo!

25
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução


Um plano de execução é um conjunto de instruções para executar uma consulta. Concebido pelo SQL Server Query
Optimizer, um plano de execução descreve o conjunto de operações que o mecanismo de execução precisa realizar para
retornar os dados exigidos por uma consulta.

O plano de execução é sua janela para o SQL Server Query Optimizer e o mecanismo de execução de consultas. Ele
revelará quais tabelas e índices uma consulta acessou, em que ordem, como eles foram acessados, quais tipos de
junções foram usados, quantos dados foram recuperados inicialmente e em que ponto ocorreu a filtragem e a classificação.
Ele mostrará como as agregações foram realizadas, como as colunas calculadas foram derivadas, como e onde as chaves
estrangeiras foram acessadas e muito mais.

Quaisquer problemas criados pela consulta serão frequentemente aparentes no plano de execução, tornando-o uma
excelente ferramenta para solucionar problemas de consultas com baixo desempenho. Em vez de adivinhar por que uma
consulta está enviando sua E/S através do telhado, você pode examinar seu plano de execução para identificar a operação
exata e a seção associada do código T-SQL que está causando o problema. Por exemplo, o plano pode revelar que uma
consulta está lendo cada linha em uma tabela ou índice, mesmo que apenas uma pequena porcentagem dessas linhas
esteja sendo usada na consulta. Ao modificar o código dentro da cláusula WHERE, o SQL Server poderá criar um novo
plano que use um índice para localizar diretamente (ou buscar) apenas as linhas necessárias.

Este capítulo apresentará os planos de execução. Exploraremos os fundamentos para obter um plano de execução e
iniciaremos o processo de aprender a lê-los, abordando os seguintes tópicos:

• Um breve histórico sobre o otimizador de consultas – os planos de execução são resultado das operações
do otimizador, por isso é útil saber pelo menos um pouco sobre o que o otimizador faz e como ele funciona.

• O cache do plano e a reutilização do plano – os planos de execução geralmente são armazenados em uma
área da memória chamada cache do plano e podem ser reutilizados. Discutiremos por que a reutilização do
plano é importante.

• Planos de execução reais e estimados - esclarecendo a confusão sobre estimativas


versus planos de execução reais e como eles diferem.

• Capturando um plano de execução – capturamos um plano para uma consulta simples e apresentamos
alguns dos elementos básicos de um plano e as informações que eles contêm.

26
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

O que acontece quando uma consulta é enviada?


Toda vez que enviamos uma consulta ao SQL Server, vários processos do servidor entram em ação; seu trabalho
coletivamente é gerenciar a consulta ou modificação desses dados. Dentro do mecanismo relacional, a consulta
é analisada pelo analisador, vinculada pelo algebrizador e, finalmente, otimizada pelo otimizador de consulta,
onde ocorre a parte mais importante do trabalho. Coletivamente, nos referimos a esses processos como
compilação de consultas. O mecanismo relacional do SQL Server pega a entrada, que é o texto SQL da
consulta enviada, e a compila em um plano para executar essa consulta. Em outras palavras, o processo gera
um plano de execução, efetivamente uma série de instruções para o processamento da consulta.

Figura 1-1: Compilação e execução da consulta.

O plano gerado é armazenado em uma área de memória chamada cache do plano. Na próxima vez que o
otimizador vir o mesmo texto de consulta, ele verificará se existe um plano para esse texto SQL no cache do plano.
Se isso acontecer, ele passará o plano armazenado em cache para o mecanismo de execução de consulta,
ignorando o processo de otimização completo.

O mecanismo de execução de consulta executará a consulta, de acordo com as instruções estabelecidas no


plano de execução. Ele gerará chamadas para o mecanismo de armazenamento, o processo que gerencia o
acesso ao disco e à memória no SQL Server, para recuperar e manipular dados conforme exigido pelo plano.

27
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

Fase de compilação da consulta

Como os planos de execução são criados e gerenciados a partir do mecanismo relacional, é aí que
focaremos nossa atenção neste livro. As seções a seguir revisam brevemente o que acontece durante a
compilação da consulta, abrangendo a análise, a associação e, principalmente, a fase de otimização do
processamento da consulta.

Análise de consulta

Quando uma solicitação para executar uma consulta T-SQL chega ao SQL Server, seja uma consulta ad hoc
de uma linha de comando ou programa de aplicativo, ou uma consulta em um procedimento armazenado,
função definida pelo usuário ou gatilho, o processo de compilação e execução da consulta pode começar e a
ação começa no mecanismo relacional.

À medida que o T-SQL chega ao mecanismo relacional, ele passa por um processo que verifica se o T-SQL está
escrito corretamente, se está bem formado. Este processo é a análise de consulta . Se uma consulta não for
analisada corretamente, por exemplo, se você digitar SELETC em vez de SELECT, a análise será interrompida e
o SQL Server retornará um erro para a origem da consulta. A saída do processo Parser é uma árvore de análise,
ou árvore de consulta (ou ainda é chamada de árvore de sequência). A árvore de análise representa as etapas
lógicas necessárias para executar a consulta solicitada.

Vinculação de consulta

Se a string T-SQL tiver sido analisada corretamente, a árvore de análise passará para o algebrizador,
que executa um processo chamado vinculação de consulta. O algebrizador resolve todos os nomes dos vários
objetos, tabelas e colunas referidos na string de consulta. Ele identifica, no nível de coluna individual, todos os
tipos de dados (varchar(50) versus datetime e assim por diante) para os objetos que estão sendo acessados. Ele
também determina a localização de agregações, como SUM e MAX, dentro da consulta, um processo chamado
de vinculação de agregação.

Esse processo de algebrização é importante porque a consulta pode ter aliases ou sinônimos, nomes
que não existem no banco de dados, que precisam ser resolvidos, ou a consulta pode se referir a objetos
que não estão no banco de dados. Quando os objetos não existem no banco de dados, o SQL Server retorna
um erro nesta etapa, definindo o nome do objeto inválido (exceto no caso de resolução de nomes adiada).
Como exemplo, o algebrizador localizaria rapidamente a tabela Person.Person no banco de dados AdventureWorks.
No entanto, a tabela Product.Person, que não existe, causaria um erro e todo o processo de compilação seria
interrompido.

28
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

Procedimento armazenado e resolução de nomes adiada

Ao criar um procedimento armazenado, seu texto de instrução é analisado e armazenado na exibição de


catálogo de ules sys.sql_mod . No entanto, as tabelas referenciadas pelo texto não precisam existir no
banco de dados neste momento. Isso dá mais flexibilidade porque, por exemplo, o texto pode fazer referência
a uma tabela temporária que não é criada pelo procedimento armazenado e ainda não existe, mas sabemos
que existirá em tempo de execução. No tempo de execução, o processador de consultas encontra os nomes
dos objetos referenciados, em sys.sql_modules, e verifica se eles existem.

O algebrizador gera um binário chamado árvore do processador de consultas, que é então passado para o
otimizador de consultas. A saída também inclui um hash, um valor codificado que representa a consulta.
O otimizador usa o hash para determinar se já existe um plano para essa consulta armazenado no cache do plano e
se o plano ainda é válido. Um plano não é mais considerado válido após algumas alterações na tabela (como adicionar
ou eliminar índices) ou quando as estatísticas usadas na otimização foram atualizadas desde que o plano foi criado e
armazenado. Se houver um plano em cache válido, o processo será interrompido aqui e o plano em cache será
reutilizado.

Otimização de consultas
O otimizador de consulta é um software que considera muitas maneiras alternativas de obter o resultado da consulta
solicitada, conforme definido pela árvore do processador de consulta passada a ele pelo algebrizador.
O otimizador estima um "custo" para cada forma alternativa possível de alcançar o mesmo resultado e tenta
encontrar um plano que seja barato o suficiente, no menor tempo possível.

A maioria das consultas enviadas ao SQL Server estará sujeita a uma otimização completa baseada em custo
processo, resultando em um plano baseado em custos. Algumas consultas muito simples podem tomar um "fast
track" e receber o que é conhecido como um plano trivial.

Otimização completa baseada em custos

O processo completo de otimização baseado em custo leva três entradas:

• A árvore do processador de consultas – dá ao otimizador conhecimento da estrutura lógica da


consulta e das tabelas e índices subjacentes.
• Estatísticas – estatísticas de índice e coluna dão ao otimizador uma compreensão do volume e
distribuição de dados nas estruturas de dados subjacentes.
• Restrições – as chaves primárias, restrições de referência obrigatórias e confiáveis e quaisquer outros tipos
de restrições em vigor nas tabelas e colunas que compõem a consulta informam ao otimizador os limites
de dados possíveis armazenados nas tabelas referenciadas.

29
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

Usando essas entradas, o otimizador aplica seu modelo, essencialmente um conjunto de regras, para
transformar a árvore de consulta lógica em um plano contendo um conjunto de operadores que,
coletivamente, executarão fisicamente a consulta. Cada operador executa uma tarefa dedicada. O otimizador
usa vários operadores para acessar índices, realizar junções, agregações, classificações, cálculos e assim por
diante. Por exemplo, o otimizador tem um conjunto de operadores para implementar condições de junção lógica
na consulta enviada. Ele tem um operador especializado para uma implementação de loops aninhados , um
para uma correspondência de hash, um para uma mesclagem e um para uma associação adaptativa.

O otimizador irá gerar e avaliar muitos planos possíveis, para cada candidato testando diferentes métodos
de acesso a dados, tentando diferentes tipos de junção, reorganizando a ordem de junção, tentando
diferentes índices e assim por diante. Geralmente, o otimizador escolherá o plano que seus cálculos sugerem
que terá o menor custo total, em termos da soma dos custos estimados de CPU e processamento de E/S.

Durante esses cálculos, o otimizador atribui um número a cada uma das etapas dentro do plano, representando
sua estimativa da quantidade combinada de CPU e tempo de E/S de disco que pensa que cada etapa levará.
Esse número é o custo estimado para essa etapa. O acúmulo de custos para cada etapa é o custo estimado
para o próprio plano de execução. Em breve, abordaremos os custos estimados e por que eles são estimativas
com mais detalhes.

A avaliação do plano é um processo heurístico . O otimizador não está tentando encontrar o melhor
plano possível, mas sim o plano de menor custo no menor número de iterações possíveis, ou seja, no
menor período de tempo. A única maneira de o otimizador chegar a um plano perfeito seria levar um tempo
infinito. Ninguém quer esperar tanto tempo em suas consultas.

Tendo selecionado o plano de menor custo que pôde encontrar dentro do número alocado de iterações, o
componente de execução da consulta usará esse plano para executar a consulta e retornar os dados
necessários. Conforme observado anteriormente, o otimizador também armazenará o plano no cache do plano.
Se enviarmos uma solicitação subsequente com texto SQL idêntico, ele ignorará todo o processo de compilação
e simplesmente enviará o plano em cache para execução. Uma consulta parametrizada será analisada e, se
um plano com um hash de consulta correspondente for encontrado no cache, o restante do processo entrará
em curto-circuito.

30
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

Planos triviais

Para consultas muito simples, o otimizador pode simplesmente decidir aplicar um plano trivial, em vez de passar
pelo processo completo de otimização baseado em custo. As regras do otimizador para decidir quando ele pode
simplesmente usar um plano trivial não são claras e provavelmente complexas. No entanto, por exemplo, uma
consulta muito simples, como uma instrução SELECT em uma única tabela sem agregações ou cálculos, conforme
mostrado na Listagem 1-1, receberia um plano trivial.

SELECIONAR d.Nome
DE Recursos Humanos . Departamento AS d
ONDE d.DepartmentID = 42;

Listagem 1-1

Adicionar ainda mais uma tabela, com um JOIN, tornaria o plano não trivial. Além disso, se houver índices
adicionais na tabela, ou se existir a possibilidade de paralelismo (discutido mais no Capítulo 11), você obterá uma
otimização adicional do plano.

Também vale a pena notar aqui que essa consulta se enquadra nas regras cobertas pela parametrização
automática, portanto, o valor codificado de "42" será substituído por um parâmetro quando o plano for
armazenado em cache, para permitir a reutilização do plano. Abordaremos isso com mais detalhes no Capítulo 9.

Todas as instruções de linguagem de manipulação de dados (DML) são otimizadas até certo ponto, mesmo
que recebam apenas um plano trivial. No entanto, alguns tipos de instrução DDL (Data Definition Language)
podem não ser otimizados. Por exemplo, se uma instrução CREATE TABLE for analisada corretamente, haverá
apenas uma "maneira correta" para o sistema SQL Server criar uma tabela.
Outras instruções DDL, como usar ALTER TABLE para adicionar uma restrição, passarão pelo processo de
otimização.

Fase de execução da consulta


O mecanismo de execução de consulta executa a consulta de acordo com as instruções definidas no plano de
execução. Em tempo de execução, o mecanismo de execução não pode alterar o plano do otimizador. No
entanto, pode, em certas circunstâncias, forçar a recompilação de um plano. Por exemplo, se enviarmos ao
processador de consultas um lote ou um procedimento armazenado contendo várias instruções, todo o lote será
compilado de uma só vez, com planos produzidos para cada instrução. Mesmo que tenhamos controle de fluxo
IF…THEN ou CASE em nossas consultas, todas as instruções dentro do lote serão compiladas. Em tempo de
execução, cada plano é verificado para garantir que ainda seja válido. Quanto aos planos obtidos no cache do
plano, se a instrução associada ao plano fizer referência a tabelas que foram alteradas ou tiveram

31
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

estatísticas atualizadas desde que o plano foi compilado, então o plano não é mais considerado válido.
Se isso ocorrer, a execução será interrompida temporariamente, o processo de compilação será invocado e o otimizador
produzirá um novo plano, apenas para a instrução afetada no lote
ou procedimento.

Introduzido no SQL Server 2017, há também a possibilidade de execução intercalada quando o objeto que está sendo
referenciado na consulta é uma função definida pelo usuário com valor de tabela de várias instruções. Durante uma
execução intercalada, o otimizador gera um plano para a consulta, da maneira usual, então a fase de otimização é pausada,
a subárvore pertinente de um determinado plano é executada para obter as contagens de linhas reais e o otimizador usa as
contagens de linhas reais para otimizar o restante da consulta. Abordaremos a execução intercalada e as funções definidas
pelo usuário com valor de tabela de várias instruções com mais detalhes no Capítulo 8.

Trabalhando com o otimizador


A maioria dos desenvolvedores de aplicativos, ao escrever o código do aplicativo, está acostumada a exercer um
controle rigoroso, não apenas sobre o resultado necessário de um pedaço de código, mas também sobre como, passo a
passo, esse resultado deve ser alcançado. A maioria das linguagens compiladas funciona dessa maneira. O SQL Server e o T-
SQL se comportam de maneira diferente.

O otimizador de consulta, não o desenvolvedor do banco de dados, decide como uma consulta deve ser executada.
Nós nos concentramos apenas em projetar uma consulta T-SQL para descrever logicamente o conjunto de dados necessário.
Não tentamos e não devemos tentar ditar ao SQL Server como executá-lo.

O que isso significa na prática é a necessidade de escrever SQL eficiente, o que geralmente significa usar uma abordagem
baseada em conjunto que descreva da forma mais sucinta possível, no menor número de instruções possível, apenas o
conjunto de dados necessário. Este é o tópico para um outro livro, e um que já foi escrito por Itzik Ben-Gan, Inside SQL Server
T-SQL Querying.

No entanto, além disso, existem algumas maneiras práticas que o desenvolvedor de banco de dados ou DBA pode ajudar o
otimizador a gerar planos eficientes e evitar a geração desnecessária de planos:

• manter estatísticas precisas e atualizadas

• promover a reutilização do plano.

32
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

A importância das estatísticas


Conforme discutimos, o otimizador escolherá o plano de menor custo, com base no custo estimado.
O principal motivador dessas estimativas são as estatísticas sobre seus índices e dados. Em última análise, isso significa
que a qualidade da escolha do plano é limitada pela qualidade das estatísticas que o otimizador tem disponível para as
tabelas e índices de destino.

Não queremos que o otimizador leia todos os dados em todas as tabelas referenciadas em uma consulta toda vez
que tentar gerar um plano. Em vez disso, o otimizador conta com estatísticas, informações agregadas com base em
uma amostra dos dados, que fornecem as informações usadas pelo otimizador para representar toda a coleção de
dados.

O custo estimado de um plano de execução depende em grande parte de suas estimativas de cardinalidade, em outras
palavras, seu conhecimento de quantas linhas há em uma tabela e suas estimativas de quantas dessas linhas satisfazem as
várias condições de busca e junção e assim por diante.

Novo estimador de cardinalidade no SQL Server 2014


No SQL Server 2014, o estimador de cardinalidade no SQL Server foi atualizado pela primeira vez
desde o SQL Server 7.0. É muito provável que você veja uma diferença nos planos gerados no SQL
Server 2014 em comparação com as versões anteriores, apenas por causa da atualização do estimador
de cardinalidade, sem falar nas atualizações de outros processos dentro do otimizador.

Essas estimativas de cardinalidade baseiam-se em estatísticas coletadas em colunas e índices dentro do banco de dados
que descrevem a distribuição dos dados, ou seja, o número de valores diferentes presentes e quantas ocorrências de cada
valor. Isso, por sua vez, determina a seletividade dos dados.
Se uma coluna for única, ela terá a maior seletividade possível e a seletividade diminui à medida que o nível de
exclusividade diminui. Uma coluna como "gênero", por exemplo, provavelmente terá uma baixa seletividade.

Se existirem estatísticas para uma coluna ou índice relevante, o otimizador as usará em seus cálculos. Se as estatísticas
não existirem, por padrão, elas serão criadas imediatamente, para que o otimizador as consuma.

As informações que compõem as estatísticas são divididas em três subseções:

• o cabeçalho - dados gerais sobre um determinado conjunto de estatísticas

• o gráfico de densidade - a seletividade, exclusividade, dos dados e, o mais importante

• um histograma – uma tabulação de contagens da ocorrência de um determinado valor, tiradas de até 200 pontos
de dados que são escolhidos para melhor representar os dados completos na tabela.

33
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

São esses "dados sobre os dados" que fornecem as informações necessárias para que o otimizador faça
seus cálculos. A medida chave é a seletividade, ou seja, a porcentagem de linhas que passam nos critérios
de seleção. A pior seletividade possível é 1,0 (ou 100%), o que significa que todas as linhas passarão. A
cardinalidade para um determinado operador no plano é então simplesmente a seletividade desse operador
multiplicada pelo número de linhas de entrada.

A dependência que o otimizador tem das estatísticas significa que suas estatísticas precisam ser tão precisas
quanto possível, ou o otimizador pode fazer escolhas ruins para os planos de execução que cria.
As estatísticas, por padrão, são criadas e atualizadas automaticamente no sistema para todos os índices ou para
qualquer coluna usada como Predicado, como parte de uma cláusula WHERE ou critérios JOIN.

A atualização automática das estatísticas que ocorre, supondo que esteja ativada, apenas amostra um
subconjunto dos dados para reduzir o custo da operação. Isso significa que, com o tempo, as estatísticas podem
se tornar um reflexo cada vez menos preciso dos dados reais. Tudo isso pode levar o SQL Server a fazer
escolhas ruins de planos de execução.

Há outras considerações estatísticas também, em torno dos tipos de objetos que escolhemos usar em nosso
código SQL. Por exemplo, variáveis de tabela nunca têm estatísticas geradas sobre elas, então o otimizador
faz suposições sobre elas, independentemente de seu tamanho real. Antes do SQL Server 2014, essa suposição
era para uma linha. O SQL Server 2014 e o SQL Server 2016 agora assumem cem linhas em funções definidas
pelo usuário de várias instruções, mas permanecem com uma linha para todos os outros objetos. O SQL Server
2017 pode, em alguns casos, usar a execução intercalada para chegar a contagens de linhas mais precisas para
essas funções.

As tabelas temporárias têm estatísticas geradas nelas e suas estatísticas são armazenadas no mesmo
tipo de histograma que as tabelas permanentes, e o otimizador pode fazer uso dessas estatísticas. Em
lugares onde as estatísticas são necessárias, digamos, por exemplo, ao fazer um JOIN para uma tabela
temporária, você pode ver vantagens em usar uma tabela temporária sobre uma variável de tabela.
No entanto, uma discussão mais aprofundada de tais tópicos está além do escopo deste livro.

Como você pode ver em toda a discussão sobre estatísticas, sua criação e manutenção têm um grande impacto
em seus sistemas. Mais importante, as estatísticas têm um grande impacto em seus planos de execução. Para
obter mais informações sobre este tópico, consulte o artigo de Erin Stellato Managing SQL Server Statistics in
Simple Talk (http://preview.tinyurl.com/yaae37gj).

34
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

O cache do plano e a reutilização do plano

Todos os processos descritos anteriormente, necessários para gerar planos de execução, têm um custo de CPU
associado. Para consultas simples, o SQL Server gera um plano de execução em menos de um milissegundo, mas
para consultas muito complexas, pode levar segundos ou até minutos para criar um plano de execução.

Portanto, o SQL Server armazenará planos em uma seção de memória chamada cache de planos e reutilizará
esses planos sempre que possível, para reduzir essa sobrecarga. Idealmente, se o otimizador encontrar uma consulta
que já viu antes, ele pode ignorar todo o processo de otimização e apenas selecionar o plano do cache.

No entanto, existem alguns motivos pelos quais o plano para uma consulta executada anteriormente pode não
estar mais no cache. Ele pode ter sido retirado do cache para dar lugar a novos planos, ou forçado a sair devido à
pressão da memória ou alguém limpando manualmente o cache. Além disso, certas alterações no esquema de banco
de dados subjacente, ou estatísticas associadas a esses objetos, podem fazer com que os planos sejam recompilados
(ou seja, recriados do zero).

Planejar o envelhecimento

Cada plano tem um valor de "idade" associado que é o custo estimado da CPU para compilar o plano multiplicado pelo
número de vezes que ele foi usado. Assim, por exemplo, um plano com um custo de compilação estimado de 10 que foi
referenciado 5 vezes tem um valor de "idade" de 50. A ideia é que os planos frequentemente referenciados que são
caros para compilar permanecerão no cache por tanto tempo que possível. Os planos passam por um processo natural
de envelhecimento. O processo lazywriter , um processo interno que trabalha para liberar todos os tipos de cache
(incluindo o cache do plano), verifica periodicamente os objetos no cache e diminui esse valor em um a cada vez.

Os planos permanecerão no cache, a menos que haja um motivo específico para que eles precisem ser removidos.
Por exemplo, se o sistema estiver sob pressão de memória, os planos podem ser envelhecidos e apagados de
forma mais agressiva. Além disso, os planos com o valor de idade mais baixo podem ser forçados a sair do cache se o
cache estiver cheio e a memória for necessária para armazenar planos mais recentes. Isso pode se tornar um problema
se o otimizador estiver sendo forçado a produzir um volume muito alto de planos, muitos dos quais são usados apenas
uma vez por uma consulta, forçando constantemente planos mais antigos a serem liberados do cache.
Este é um problema conhecido como cache churn, que discutiremos novamente em breve.

35
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

Limpando manualmente o cache do plano

Às vezes, durante o teste, você pode querer liberar todos os planos do cache, ver quanto tempo um plano
leva para compilar ou investigar como pequenos ajustes de consulta podem levar a planos ligeiramente
diferentes. O comando DBCC FREEPROCCACHE limpará o cache de todos os bancos de dados no servidor.
Em um ambiente de produção, isso pode resultar em um impacto significativo e sustentado no desempenho, pois
cada consulta subsequente é uma consulta "nova" e deve passar pelo processo de otimização. Podemos liberar
apenas consultas ou planos específicos fornecendo um plan_handle ou sql_handle. Você pode recuperar esses
valores do próprio cache do plano usando exibições de gerenciamento dinâmico (DMVs), como sys.dm_exec_query_

stats ou o Query Store (consulte o Capítulo 16). Depois de ter o valor, basta executar o DBCC
FREEPROCCACHE(<plan_handle>) para remover um plano específico do cache do plano.

Da mesma forma, podemos usar DBCC FLUSHPROCINDB(db_id) para remover todos os planos de um banco de
dados específico, mas o comando não está documentado oficialmente. O SQL Server 2016 introduziu um novo
método totalmente documentado para remover todos os planos de um único banco de dados, que é executar o
seguinte comando no banco de dados de destino:

ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE


Critérios para reutilização do plano

Quando enviamos uma consulta ao servidor, o processo algebrizador cria um valor de hash para a consulta. O
otimizador armazena o valor de hash na propriedade QueryHash do plano de execução associado (abordado
com mais detalhes no Capítulo 2). O trabalho do QueryHash é identificar consultas com a mesma lógica ou lógica
muito semelhante (há casos raros em que consultas logicamente diferentes terminam com o mesmo valor de hash,
conhecido como colisões de hash).

Para cada consulta enviada, o otimizador procura um valor QueryHash correspondente entre os planos no
cache de planos. Se encontrado, ele executa uma comparação detalhada do texto SQL da consulta enviada e
do texto SQL associado ao plano armazenado em cache. Se eles corresponderem exatamente (incluindo
espaços e retornos de carro), isso retornará o plan_handle, um valor que identifica exclusivamente o plano na
memória. Este plano pode ser reutilizado, se o seguinte também for verdadeiro:

• o plano foi criado usando as mesmas opções SET (consulte o Capítulo 2) – caso contrário, haverá
vários planos criados mesmo que os textos SQL sejam idênticos
• os IDs do banco de dados correspondem - consultas idênticas em bancos de dados diferentes terão
planos separados.

Observe que também é possível que a falta de qualificação de esquema para os objetos referenciados na consulta
leve a planos separados para usuários diferentes.

36
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

Geralmente, no entanto, um plano será reutilizado se todos os quatro itens acima corresponderem (QueryHash, texto SQL,
opções SET, ID do banco de dados). Nesse caso, todo o custo do processo de otimização é ignorado e o plano de execução
no cache do plano é reutilizado.

Evitando a rotatividade de cache: parametrização de consultas

É uma prática recomendada importante escrever consultas de forma que o SQL Server possa reutilizar os planos no cache.
Se enviarmos consultas ad hoc para o SQL Server e usarmos valores literais codificados, para a maioria dessas consultas,
o SQL Server será forçado a concluir todo o processo de otimização e compilar um novo plano a cada vez. Em um servidor
ocupado, isso pode levar rapidamente ao inchaço do cache e a planos mais antigos serem forçados a sair do cache com
relativa rapidez.

Por exemplo, digamos que enviamos a consulta na Listagem 1-2.

SELECT p.ProductID ,
p.Name AS ProductName ,
pi.Shelf ,
l.Name AS LocationName
A PARTIR DE Produção.Produto p
INNER JOIN Produção.ProdutoInventário AS pi
ON pi.ProductID = p.ProductID
INNER JOIN Produção.Localização AS l
ON l.LocationID = pi.LocationID
WHERE l.Name = 'Pintar';
VAI

Listagem 1-2

Em seguida, enviamos a mesma consulta novamente, mas para um nome de local diferente (digamos, 'Ferramenta
Berços' em vez de 'Pintura'). Isso resultará em dois planos separados armazenados em cache, mesmo que as duas
consultas sejam essencialmente as mesmas (elas terão o mesmo QueryHash
valores, assumindo que nenhuma outra alteração seja feita).

Para garantir a reutilização do plano, é melhor usar procedimentos armazenados ou consultas parametrizadas, em que as
variáveis dentro da consulta são identificadas com parâmetros, em vez de literais codificados, e simplesmente passamos os
valores de parâmetro necessários em tempo de execução. Dessa forma, o texto SQL que o otimizador vê será "gravado",
maximizando a possibilidade de reutilização do plano.

Elas também são chamadas de "consultas preparadas" e são criadas a partir do código do aplicativo. Para obter um
exemplo de uso de declarações preparadas, consulte este artigo no Technet (http://preview.tinyurl.com/
ybvc2vcs). Você também pode parametrizar uma consulta usando sp_executesql de dentro do seu código T-SQL.

37
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

Outra maneira de mitigar a rotatividade de consultas ad hoc é usar uma configuração de servidor
chamada Otimizar para cargas de trabalho ad hoc. Ativar isso fará com que o otimizador crie o que é
conhecido como "plano stub" no cache do plano, em vez de colocar o plano inteiro lá na primeira vez que um
plano é criado. Isso significa que os planos de uso único ocuparão radicalmente menos memória no cache do
plano.

Recompilação do plano

Certos eventos e ações, como alterações em um índice usado por uma consulta, podem fazer com que um
plano seja recompilado, o que significa simplesmente que o plano existente será marcado para recompilação
e um novo plano será gerado na próxima vez que a consulta for chamada. É importante lembrar disso, pois
recompilar planos de execução pode ser uma operação muito cara. Isso só se torna um problema se nossas
ações como programadores forçam o SQL Server a realizar recompilações excessivas.

Discutiremos recompilações com mais detalhes no Capítulo 9, mas as ações a seguir podem levar à
recompilação de um plano de execução (consulte http://preview.tinyurl.com/y947r969 para obter uma lista completa):

• alterando a estrutura de uma tabela, visualização ou função referenciada pela consulta


• alterando ou descartando um índice usado pela consulta
• atualizar as estatísticas usadas pela consulta
• chamando a função sp_recompile
• mistura de DDL e DML em um único lote
• alterando certas opções SET dentro do T-SQL do lote
• alterações nas opções do cursor dentro da consulta
• compilações adiadas
• muda para um conjunto de linhas remoto se você estiver usando uma função como OPENQUERY.

Introdução aos planos de execução


Os planos de execução nos ajudam a escrever código T-SQL eficiente, solucionar problemas de
comportamento T-SQL existente ou monitorar e gerar relatórios em nossos sistemas. Como os usamos e os
visualizamos depende de nós, mas primeiro precisamos entender quais informações estão contidas nos planos
e como interpretar essas informações. Uma das melhores maneiras de aprender sobre os planos de execução é
vê-los em ação, então vamos começar.

38
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

Permissões necessárias para visualizar planos de execução

Para visualizar os planos de execução das consultas, você deve ter as permissões corretas no banco de dados. Se você
for sysadmin, dbcreator ou db_owner, não precisará de nenhuma outra permissão. Se você estiver concedendo essa
permissão a desenvolvedores que não estarão em uma dessas funções privilegiadas, eles precisarão receber a permissão
ShowPlan no banco de dados que está sendo testado. Execute a instrução na Listagem 1-3.

GRANT SHOWPLAN TO [nome de usuário];

Listagem 1-3

A substituição do nome de usuário permitirá que o usuário visualize os planos de execução para esse banco de dados.
Além disso, para executar as consultas no DMO (Dynamic Management Objects), será necessário VIEW SERVER STATE
ou VIEW DATABASE STATE, dependendo do DMO em questão. Exploraremos mais os DMOs no Capítulo 15.

Formatos de plano de execução

O SQL Server pode gerar o plano de execução de três maneiras diferentes:

• como um plano XML

• como um plano de texto

• como um plano gráfico.

O que você escolher dependerá do nível de detalhes que deseja ver e dos métodos usados para capturar ou visualizar
esse plano.

Em cada formato, podemos recuperar o plano de execução sem executar a consulta (portanto, sem informações de tempo
de execução), que é conhecido como plano estimado, ou podemos recuperar o plano com informações de tempo de
execução adicionadas, o que obviamente requer a execução da consulta e é conhecido como o plano real. Embora,
estritamente falando, os termos real e estimado sejam exclusivos de planos gráficos, é comum vê-los aplicados a todos os
formatos de planos de execução e, para simplificar, usaremos esses termos para cada formato aqui.

39
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

planos XML

Os planos XML apresentam um conjunto completo de dados disponíveis em um plano, todos exibidos no formato
XML estruturado. O formato XML é ótimo para transmitir para outros profissionais de dados se você quiser ajuda em
um plano de execução ou precisar compartilhar com colegas de trabalho. Usando XQuery, também podemos
consultar os dados XML diretamente (consulte o Capítulo 13).

Podemos usar um dos dois comandos a seguir para recuperar o plano no formato XML:

• SET SHOWPLAN_XML ON – gera o plano estimado (ou seja, a consulta não é


executado).

• SET STATISTICS_XML ON – gera o plano de execução real (ou seja, com informações de tempo
de execução).

Os planos XML são extremamente úteis, mas principalmente para consulta, não para leitura de planos no estilo
padrão, pois o XML não é legível por humanos. Embora esses tipos de plano sejam úteis, é mais provável que
você use planos gráficos para simplesmente navegar no plano de execução.

Todo plano de execução gráfica é, na verdade, XML nos bastidores. No SSMS, basta clicar com o botão direito
do mouse no próprio plano. No menu de contexto, selecione Mostrar XML do plano de execução… para abrir uma
janela com o XML do plano de execução.

Planos de texto

Estes podem ser bastante difíceis de ler, mas informações detalhadas estão imediatamente disponíveis. Seu
formato de texto significa que podemos copiá-los ou exportá-los para um software de manipulação de texto, como
o Bloco de Notas ou o Word, e depois executar pesquisas neles. Embora os detalhes que eles fornecem estejam
imediatamente disponíveis, há menos detalhes gerais da saída do plano de execução nesses tipos de plano,
portanto, eles podem ser menos úteis do que os outros tipos de plano.

Os planos de texto estão na lista de descontinuação da Microsoft. Eles não estarão disponíveis em uma versão
futura do SQL Server. Não recomendo usá-los.

No entanto, aqui estão os comandos possíveis que podemos usar para recuperar o plano em formato de texto:

• SET SHOWPLAN_ALL ON – recupera o plano de execução estimado para a consulta.


• SET STATISTICS PROFILE ON - recupera o plano de execução real para
A pergunta.

• SET SHOWPLAN_TEXT ON – recupera o plano estimado, mas com um número muito limitado
conjunto de dados, para uso com ferramentas como osql.exe.

40
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

Planos gráficos
Os planos gráficos são o formato de plano de execução mais comumente visualizado. Eles são rápidos e fáceis
de ler. Podemos visualizar os planos de execução estimados e reais em formato gráfico e a estrutura gráfica facilita
muito a compreensão da maioria dos planos. No entanto, os dados detalhados do plano estão ocultos nas dicas de
ferramentas e nas folhas de propriedades , tornando-o um pouco mais difícil de acessar, exceto em uma abordagem
de um operador por vez.

Recuperando planos em cache


Há alguma confusão em relação aos diferentes tipos de plano e o que eles realmente significam.
Já ouvi desenvolvedores falarem sobre planos estimados e reais como se fossem dois planos completamente
diferentes. Espero que esta seção esclareça as coisas. O ponto importante é que o otimizador de consulta produz o
plano e há apenas um plano de execução válido para uma consulta, a qualquer momento.

Ao solucionar problemas de uma consulta de longa duração retrospectivamente, muitas vezes precisaremos
recuperar o plano armazenado em cache para essa consulta do cache do plano. Conforme discutido anteriormente,
uma vez que o otimizador seleciona um novo plano para uma consulta, ele o coloca no cache do plano e o passa
para o mecanismo de execução de consulta para execução. Obviamente, o otimizador nunca executa nenhuma
consulta, apenas formula o plano com base em seu conhecimento das estruturas de dados subjacentes e no
conhecimento estatístico dos dados. Os planos em cache não contêm nenhuma informação de tempo de execução,
exceto as contagens de linhas em planos intercalados.

Podemos recuperar esse plano em cache manualmente, por meio dos Objetos de Gerenciamento Dinâmico ou usando
uma ferramenta como Eventos Estendidos. Abordaremos técnicas para automatizar a captura do plano em cache mais
adiante neste livro (Capítulo 15).

Planos para consultas ad hoc: planos estimados e reais


Na maioria das vezes neste livro, porém, recuperaremos o plano de execução simplesmente executando consultas ad
hoc no SSMS. No momento em que enviamos a consulta, temos a opção de solicitar o plano estimado ou o plano
real.

Se solicitarmos o plano estimado , não executamos a consulta; apenas submetemos a consulta para inspeção pelo
otimizador, para ver o plano associado. Se existir no cache do plano um plano que corresponda exatamente ao texto
da consulta enviada, o otimizador simplesmente retornará esse plano armazenado em cache. Se não houver
correspondência, o otimizador realiza o processo de otimização e

41
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

retorna o novo plano. No entanto, como não há intenção de executar a consulta, as próximas duas etapas
são ignoradas (ou seja, colocar o plano no cache, se for um novo plano, e enviá-lo para execução). Como
os planos estimados nunca acessam dados, eles são muito úteis durante o desenvolvimento para testar
consultas grandes e complexas que podem levar muito tempo para serem executadas.

Se, ao enviarmos a consulta, solicitarmos um plano com informações de tempo de execução (o que o
SSMS chama de plano real ), todas as três etapas do processo serão executadas.

Se houver um plano em cache que corresponda exatamente ao texto da consulta enviada, o otimizador
simplesmente passará o plano em cache para o mecanismo de execução, que o executará e adicionará
os valores de tempo de execução solicitados ao plano exibido. Se não houver um plano armazenado em
cache, o otimizador produz um novo plano, o coloca no cache e o repassa para execução e, novamente,
vemos o plano com informações de tempo de execução. Por exemplo, veremos valores de tempo de
execução para o número de linhas retornadas e o número de execuções de cada operador, juntamente com
os valores estimados do otimizador. Observe que o SQL Server não armazena em nenhum lugar uma
segunda cópia do plano com as informações de tempo de execução. Esses valores são simplesmente
injetados na cópia do plano, seja exibido no SSMS ou emitido por outros meios.

Os planos estimados e reais serão diferentes?


Essencialmente, a resposta para isso é "Não". Conforme enfatizado anteriormente, há apenas um plano
de execução válido para uma consulta em um determinado momento, e os planos estimado e real não
serão diferentes.

Você pode ver diferenças na paralelização entre o plano de tempo de execução e o plano estimado, mas
isso não significa que o mecanismo de execução "alterou" o plano. Em tempo de compilação, se o otimizador
calcular que o custo do plano pode exceder o limite de custo para paralelismo, ele produzirá uma versão
paralela do plano (consulte o Capítulo 11). No entanto, o mecanismo tem a palavra final sobre se a consulta é
executada em paralelo, com base na atividade atual do servidor e nos recursos disponíveis. Se os recursos
forem muito escassos, ele simplesmente eliminará o paralelismo e executará uma versão serial do plano.

Às vezes, você pode gerar um plano estimado e, posteriormente, um plano real para a mesma consulta e ver
que os planos são diferentes. De fato, o que terá acontecido aqui é que, no tempo entre as duas requisições,
algo aconteceu para invalidar o plano existente no cache, obrigando o otimizador a realizar uma otimização
completa e gerar um novo plano. Por exemplo, as alterações nos dados ou nas estruturas de dados podem
ter causado a recompilação do SQL Server

42
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

o plano. Alternativamente, processos ou objetos dentro da consulta, como a linguagem de


definição de dados (DDL) intercalada e a linguagem de manipulação de dados (DML), resultam em
uma recompilação do plano de execução.

Se você solicitar um plano real e, em seguida, recuperar do cache o plano para a consulta que você acabou
de executar (veremos como fazer isso no Capítulo 9), você verá que o plano armazenado em cache é o mesmo
que seu plano real, exceto que o plano real tem informações de tempo de execução.

Um caso em que os planos estimados e reais serão genuinamente diferentes é quando o plano
estimado não funcionará. Por exemplo, tente gerar um plano estimado para o código simples na Listagem 1-4.

CREATE TABLE TempTable


(
ID INT IDENTITY(1, 1) ,
Dsc NVARCHAR(50)
);
INSERT INTO TempTable
( Dsc
)
SELECIONE [Nome]
A PARTIR DE [Vendas].[Loja];
SELECIONAR *
FROM TempTable;
DROP TABLE TempTable;

Listagem 1-4

Você receberá este erro:

Msg 208, Nível 16, Estado 1, Linha 7


Nome de objeto inválido 'TempTable'.

O otimizador executa as instruções por meio do algebrizador, processo descrito anteriormente que é
responsável por verificar os nomes dos objetos do banco de dados, mas, como o SQL Server ainda não
executou a consulta, a tabela temporária ainda não existe.

O plano será marcado para resolução de nomes adiada. Em outras palavras, enquanto o lote é analisado,
vinculado e compilado, a consulta SELECT é excluída da compilação porque o algebrizador a marcou como
adiada. A captura do plano estimado não executa a consulta e, portanto, não cria a tabela temporária, e essa
é a causa do erro. Em tempo de execução, a consulta será compilada e agora existe um plano. Se você
executar a Listagem 1-4 e solicitar o plano de execução real, ele funcionará perfeitamente.

43
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

Um segundo caso em que os planos estimados e reais serão diferentes, novo no SQL Server 2017,
é quando o otimizador usa execução intercalada. Se solicitarmos um plano estimado para uma
consulta que contém uma função com valor de tabela de várias instruções (MSTVF), o otimizador
usará uma estimativa de cardinalidade fixa de 100 linhas para o MSTVF. No entanto, se solicitarmos um
plano real, o otimizador primeiro gerará o plano usando essa estimativa fixa e, em seguida, executará a
subárvore que contém o MSTVF para obter as contagens de linhas reais retornadas e recompilar o plano
com base nessas contagens de linhas reais. Obviamente, esse plano será armazenado no cache do
plano, portanto, as solicitações subsequentes de um plano estimado ou real retornarão o mesmo plano.

Capturando planos gráficos no SSMS


No SSMS, podemos capturar os planos estimados e reais para uma consulta, e há várias maneiras
de fazer isso, em cada caso. Talvez o mais comum, ou pelo menos o caminho que costumo fazer, seja
usar os ícones da barra de ferramentas. A Figura 1-2 mostra o ícone Exibir plano de execução
estimado .

Figura 1-2: Capturando o plano estimado.

Alguns ícones à direita, temos o ícone Include Actual Execution Plan , conforme mostrado na
Figura 1-3.

44
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

Figura 1-3: Capturando o plano real.

Alternativamente, para qualquer tipo de plano, você pode:

• clique com o botão direito do mouse na janela de consulta e selecione a mesma opção no menu de contexto
• clique na opção Consulta na barra de menu e selecione a mesma opção
• use o atalho de teclado (CTRL+L para estimado; CTRL+M para real dentro
SSMS ou CTRL+ALT+L e CTRL+ALT+M para o mesmo no Visual Studio).

Para planos estimados, temos que clicar no ícone, ou usar um dos métodos alternativos, cada vez que queremos
capturar esse tipo de plano para uma consulta. Para o plano real, cada um desses métodos atua como uma opção
"ligar/desligar" para a janela de consulta. Quando o plano real é ativado, em cada execução, o SQL Server captura
um plano de execução real para todas as consultas executadas nessa janela, até que você o desative novamente
para cada janela de consulta no SSMS.

Finalmente, há uma maneira adicional de visualizar um plano de execução gráfico, um plano de execução ao vivo.
A exibição do plano é baseada em um DMV, sys.dm_exec_query_statistics_xml, introduzido no SQL Server 2014.
Esse DMV retorna estatísticas ao vivo para as operações em um plano de execução. A exibição gráfica dessa DMV foi
introduzida no SQL Server 2016. Você a ativa ou desativa de maneira semelhante ao que faz com um plano de
execução real. A Figura 1-4 mostra o botão.

45
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

Figura 1-4: Habilitando o plano de execução ao vivo.

Exploraremos isso completamente no Capítulo 17.

Capturando nosso primeiro plano

É hora de capturar nosso primeiro plano de execução. Começaremos com uma consulta relativamente simples que,
no entanto, fornece uma visão bastante completa de tudo o que você fará ao ler os planos de execução.

Conforme observado na introdução deste livro, recomendamos que você siga os exemplos, executando o script
relevante e visualizando os planos. Ocasionalmente, especialmente quando chegarmos a exemplos mais complexos
mais adiante no livro, você poderá ver um plano diferente daquele apresentado no livro. Isso pode ocorrer porque
estamos usando versões diferentes do SQL Server (diferentes níveis de service pack e atualizações cumulativas),
edições diferentes ou versões ligeiramente diferentes do banco de dados de exemplo AdventureWorks . Usamos
AdventureWorks2016 neste livro; outras versões são ligeiramente diferentes e, mesmo que você use a mesma versão,
seu esquema ou estatísticas podem ter sido alterados ao longo do tempo. Portanto, enquanto a maioria dos planos
que você obtém deve ser muito semelhante, se não idêntico, ao que exibimos aqui, não se surpreenda se você tentar
o código e ver algo diferente.

Abra uma nova guia de consulta no SSMS e execute a consulta mostrada na Listagem 1-5.

USE AdventureWorks2014;
VAI
SELECT p.Sobrenome + ', ' p.Título, + p.Nome,

pp.Número de telefone
DE Pessoa.Pessoa AS p
INNER JOIN Pessoa.PessoaTelefone AS pp

46
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

ON pp.BusinessEntityID = p.BusinessEntityID
INNER JOIN Person.PhoneNumberType AS pnt
ON pnt.PhoneNumberTypeID = pp.PhoneNumberTypeID
WHERE pnt.Name = 'Célula'
AND p.LastName = 'Dempsey';
VAI

Listagem 1-5

Clique no ícone Exibir plano de execução estimado e na guia plano de execução você verá o plano
de execução estimado, conforme mostrado na Figura 1-5.

Figura 1-5: Plano de execução estimado.

Observe que não há uma guia Resultados , porque na verdade não executamos a consulta. Agora,
realce o ícone Incluir Plano de Execução Real e execute a consulta. Desta vez, você verá o conjunto
de resultados reajustado (uma única linha) e a guia Plano de execução exibirá o plano de execução
real, que também deve ter a aparência mostrada na Figura 1-5.

Os componentes de um plano de execução gráfica


Agora vamos explorar cada seção do plano da Figura 1-5 com mais detalhes, mas ainda em um nível
alto. Não começaremos a explorar os detalhes de operadores individuais até o Capítulo 3.
Você notará que é bastante difícil ler os detalhes do plano na Figura 1-5. Aqui, e ao longo do livro,
seguiremos um método em que mostro todo o plano e, em seguida, detalhamos as seções do plano para
discutir partes ou segmentos individuais do plano.

A maioria das pessoas começa no lado direito, ao ler os planos, onde você encontrará os operadores
que lêem os dados das tabelas e índices base. A partir daí seguimos o fluxo de dados, conforme
indicado pelas setas, da direita para a esquerda até chegar ao operador SELECT , onde o

47
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

linhas são passadas de volta para o cliente. No entanto, é igualmente válido ler o plano da esquerda para a
direita, que é a ordem em que os operadores são chamados – essencialmente os dados são puxados da
direita para a esquerda, pois cada operador chama o operador filho à sua direita, mas discuta isso com mais
detalhes no Capítulo 3.

Operadores
Os operadores, representados como ícones no plano, são a força motriz do plano. Cada operador implementa
um algoritmo específico projetado para realizar uma tarefa especializada. Os operadores em um plano nos
dizem exatamente como o SQL Server escolheu executar uma consulta, como como ele escolheu acessar os
dados em uma determinada tabela, como ele escolheu unir esses dados a linhas em uma segunda tabela, como
e onde ele escolheu para realizar quaisquer agregações, classificação, cálculos e assim por diante.

Neste exemplo, vamos começar do lado direito do plano, com os operadores mostrados na Figura 1-6.

Figura 1-6: Dois operadores de acesso a dados e um operador de junção.

Aqui vemos dois operadores de acesso a dados passando dados para um operador de junção. O primeiro
operador é um Index Seek, que está extraindo dados da tabela Person usando um índice não clusterizado,
Person.IX_Person_LastName_FirstName_MiddleName. Cada linha de qualificação (linhas em que o sobrenome
é Dempsey) passa para um operador de loops aninhados , que extrairá dados adicionais, não mantidos no índice
não clusterizado, do operador de pesquisa de chave .

Cada operador tem um elemento físico e um elemento lógico. Por exemplo, na Figura 1-6, Nested Loops é o
operador físico e Inner Join é a operação lógica que ele executa.

48
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

Assim, o componente lógico descreve o que o operador realmente faz (uma operação INNER JOIN) e a
parte física é como o otimizador escolheu implementá-lo (usando um algoritmo de loops aninhados).

Do primeiro operador Nested Loops , os dados fluem para um operador Compute Scalar . Para cada
linha, ele executa sua tarefa necessária (neste caso, concatenando o nome e o sobrenome com uma
vírgula) e, em seguida, passa para o operador à sua esquerda. Esses dados são unidos com linhas
correspondentes na tabela PersonPhone e, por sua vez, com linhas correspondentes na tabela PhoneNum
berType. Finalmente, os dados fluem para o operador SELECT .

Figura 1-7: Seção mais ampla do plano mostrando mais operadores.

O ícone SELECT é aquele que você fará referência com frequência para os dados importantes que
ele contém. Claro, cada operador contém dados importantes (veja as propriedades do operador
seção, um pouco mais adiante), mas o que diferencia o operador SELECT é que ele contém dados sobre
o plano como um todo, enquanto outros ícones apenas expõem informações sobre o próprio operador.

Setas de fluxo de dados

As setas representam a direção do fluxo de dados entre os operadores e a espessura da seta reflete a
quantidade de dados passados, uma seta mais grossa significa mais linhas. A espessura da seta é
outra pista visual sobre onde podem estar os problemas de desempenho. Por exemplo, você pode ver
uma seta grande e grossa emergindo de um operador de acesso a dados, no lado direito do plano, mas
setas muito finas à esquerda, pois sua consulta retorna apenas duas linhas.
Este é um sinal de que muitos dados foram processados para produzir essas duas linhas de saída. Isso
pode ser inevitável para os requisitos funcionais da consulta, mas também pode ser algo que você pode
evitar.

49
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

Você pode passar o ponteiro do mouse sobre essas setas e ele mostrará o número de linhas que representa em uma dica de
ferramenta que você pode ver na Figura 1-8. Em um plano de execução que contém estatísticas de tempo de execução (o
plano real), a espessura é determinada pelo número real, e não pelo número estimado de linhas.

Figura 1-8: Dica de ferramenta para a seta de fluxo de dados.

Custos estimados do operador


Abaixo de cada ícone individual em um plano é exibido um número como uma porcentagem. Este número representa o
custo estimado para aquela operadora em relação ao custo estimado do plano como um todo. Esses números são mais
bem pensados como "unidades de custo", com base nos cálculos matemáticos de CPU e E/S previstos no otimizador. Os
custos estimados são úteis como medidas, mas esses custos não representam medidas reais de CPU e E/S reais.
Geralmente, há uma correlação entre o alto custo estimado dentro do plano e os custos reais de desempenho mais altos, mas
esses ainda são apenas valores estimados.

A origem dos valores de custo estimados


A história diz que o desenvolvedor encarregado de criar planos de execução no SQL Server 7 usou
sua estação de trabalho como base para esses números, e eles nunca foram atualizados. Veja o blog
de Danny Ravid em: http://preview.tinyurl.com/yawet2l3.

Todos os operadores terão um custo associado, e mesmo um operador exibindo 0% terá um pequeno custo associado,
que você pode ver nas propriedades do operador (que discutiremos em breve).

Se você comparar os custos da operadora e do plano lado a lado para o plano estimado e real da mesma consulta, verá que
eles são idênticos. Somente o otimizador gera esses valores de custo, o que significa que todos os custos em todos os
planos são estimativas, com base no conhecimento estatístico dos dados do otimizador.

50
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

Custo total estimado da consulta em relação ao lote

Na parte superior de cada plano de execução é exibido o máximo da string de consulta que couber na
janela e um "custo (relativo ao lote)" de 100%.

Figura 1-9: Consulta e o custo estimado da consulta no topo do plano de execução.

Assim como cada consulta pode ter vários operadores, e cada um desses operadores terá um custo
relativo à consulta, você também pode executar várias consultas em um lote e obter planos de execução
para elas. Cada plano terá então custos diferentes. O custo estimado da consulta total é dividido pelo custo
estimado de todas as consultas em um lote. Cada operador em um plano exibe seus custos estimados em
relação ao plano do qual faz parte, não ao lote como um todo.

Nunca perca de vista o fato de que os custos que você vê, mesmo em planos reais, são um custo
estimado, não métricas de desempenho reais, medidas. Se você concentrar seus esforços de ajuste
exclusivamente nas consultas ou operadores com altos custos estimados e descobrir que as estimativas de
custo estão incorretas, talvez você esteja procurando na área errada a causa dos problemas de desempenho.

Propriedades do operador

Clique com o botão direito do mouse em qualquer ícone dentro de um plano de execução gráfico e
selecione o item de menu Propriedades para obter uma lista detalhada de informações sobre essa
operação. Cada operador executa uma tarefa distinta e, portanto, cada operador terá um conjunto distinto
de dados de propriedade. A grande maioria das informações úteis para ajudá-lo a ler e entender os planos
de execução está contida na janela Propriedades de cada operador. É um bom hábito adquirir ao ler um
plano de execução deixar a janela Propriedades aberta e fixada em sua janela do SSMS o tempo todo.
Infelizmente, devido aos caprichos da GUI do SSMS, às vezes você pode precisar clicar em dois lugares
para obter as propriedades que deseja exibir corretamente.

A Figura 1-10 compara a janela Properties para o mesmo operador Index Seek no canto superior
direito da Figura 1-5, que executa uma operação de busca em um índice não clusterizado na tabela
Person. O painel esquerdo é do plano estimado e o painel direito é do plano real.

51
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

Figura 1-10: Comparando as propriedades do operador Index Seek para os planos


estimados e reais.

Como você pode ver, no plano real, vemos o número real e estimado de linhas que passaram por esse
operador, bem como o número real de vezes que o operador foi executado. Aqui vemos que o otimizador
estimou 1,3333 linhas e 2 foram realmente retornadas.

Ao comparar as propriedades de um operador, para os planos estimado e real, procure diferenças muito
grandes entre o número estimado e o número real de linhas retornadas, como uma contagem de linhas
estimada de 100 e uma contagem de linhas real de 100.000 (ou vice-versa). vice-versa).
Se uma consulta que retorna centenas de milhares de linhas usa um plano que o otimizador criou para
retornar 10 linhas, provavelmente será muito ineficiente e você precisará investigar a possível causa.
Pode ser que a contagem de linhas tenha mudado significativamente desde que o plano foi gerado, mas
as estatísticas ainda não foram atualizadas automaticamente, ou pode ser causado por problemas com a
detecção de parâmetros ou por outros problemas. Voltaremos a este tópico em detalhes no Capítulo 9.

52
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

Não entrarei em detalhes aqui sobre todas as propriedades e seus significados, mas mencionarei brevemente algumas
às quais você se referirá com bastante frequência:

• Número real de linhas – o número real de linhas retornadas de acordo com as estatísticas de tempo de
execução. A disponibilidade desse valor em planos reais é a maior diferença entre esses e os planos em
cache (ou planos estimados). Fique atento às grandes diferenças entre este valor e o valor estimado.

• Valores definidos – valores introduzidos por este operador, como as colunas retornadas, ou expressões
computadas da consulta, ou valores internos introduzidos pelo processador de consulta.

• Número estimado de linhas - calculado com base nas estatísticas disponíveis para o
otimizador para a tabela ou índice em questão. Eles são úteis para comparar com o Número Real de
Linhas.

• Custo estimado do operador - o custo estimado do operador como um valor (bem como
uma porcentagem). Este é um custo estimado mesmo em planos reais.
• Objeto – o objeto acessado, como o índice sendo acessado por uma varredura ou uma operação de
busca.
• Lista de saída – colunas retornadas.
• Predicado – um Predicado de pesquisa "empurrado para baixo".
• Cardinalidade da Tabela – número de linhas na tabela.

Você notará que algumas das propriedades, como Objeto, têm um ícone de triângulo à esquerda, indicando que podem
ser expandidas. Algumas das descrições de propriedades mais longas têm reticências no final, o que nos permite abrir
uma nova janela, facilitando a leitura do texto mais longo.
Quase todas as propriedades, quando você clica nelas, exibem uma descrição na parte inferior do painel
Propriedades .

Todos esses detalhes estão disponíveis para nos ajudar a entender o que está acontecendo na consulta em
questão. Podemos percorrer os vários operadores, observando como o custo da subárvore se acumula, como o
número de linhas muda e assim por diante. Com esses detalhes, podemos identificar consultas que usam
quantidades excessivas de CPU ou tabelas que precisam de mais índices ou identificar outros problemas de desempenho.

Dicas de ferramentas

Associado a cada um dos ícones e às setas, há uma janela pop-up chamada dica de ferramenta, que você pode
acessar passando o ponteiro do mouse sobre o ícone ou a seta. Eu já usei um desses na Figura 1-8. Essencialmente,
a dica de ferramenta para um operador é uma versão reduzida do

53
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

janela Propriedades . Vale a pena notar que a dica de ferramenta e as propriedades de determinados
operadores mudam conforme o próprio SQL Server muda. Você pode ver diferenças nas dicas de
ferramentas entre uma versão do SQL Server e a próxima. A maioria dos exemplos neste livro são do SQL
Server 2016.

A Figura 1-11 mostra a janela de dica de ferramenta para o operador SELECT para o plano de execução
estimado para a consulta na Listagem 1-4.

Figura 1-11: Dica de ferramenta para o operador SELECT .

As propriedades do operador SELECT geralmente são particularmente interessantes, pois fornecem


informações relacionadas ao plano como um todo. Por exemplo, vemos os dois valores de propriedade a
seguir (entre outros, vários dos quais revisaremos em detalhes no Capítulo 2):

• Tamanho do plano em cache – quanta memória o plano gerado por esta consulta irá ocupar no
cache do plano. Este é um número útil ao investigar problemas de desempenho de cache porque
você poderá ver quais planos estão ocupando mais memória.
• Grau de Paralelismo - se este plano foi projetado para usar (ou usou)
vários processadores. Este plano usa um único processador, conforme mostrado pelo valor de 1.
(Consulte o Capítulo 11.)

54
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

Na Figura 1-11, também vemos a instrução que representa toda a consulta que o SQL Server está
processando. Você pode não ver a instrução se for muito longa para caber na janela de dica de ferramenta. A
mesma coisa se aplica a outras propriedades em outros operadores. Este é mais um motivo para focar no uso
da janela Propriedades ao trabalhar com planos de execução.

As informações disponíveis nas dicas de ferramentas podem ser extremamente limitadas. Mas é bastante rápido
ver as informações disponíveis neles, pois tudo o que você precisa fazer é passar o mouse para obter as dicas.
Para obter uma visão mais consistente e detalhada das informações sobre as operações dentro de um plano
de execução, você deve usar a janela Propriedades completa.

Salvando planos de execução


Podemos salvar um plano de execução da interface gráfica do plano de execução clicando com o botão direito
do mouse no plano de execução e selecionando Salvar plano de execução como. Já no SQL Server 2005,
tivemos que mudar o filtro para "*.*" e, ao digitar o nome do arquivo que queríamos salvar, adicionar .sqlplan
como extensão. Felizmente, o SQL Server 2008 e posterior seleciona automaticamente o tipo de
arquivo .sqlplan .

O que estamos salvando é simplesmente um arquivo XML. Um dos benefícios de extrair um plano XML e
salvá-lo como um arquivo separado é que podemos compartilhá-lo com outras pessoas. Por exemplo,
podemos enviar o plano XML de uma consulta lenta para um amigo DBA e pedir sua opinião sobre como
reescrever a consulta. Assim que o amigo receber o plano XML, ele poderá abri-lo no Management Studio e
revisá-lo como um plano gráfico de execução.

Você também pode ver o XML subjacente de um plano clicando com o botão direito do mouse no plano
e selecionando Mostrar XML do plano de execução no menu de contexto. Isso abrirá o XML bruto em outra
janela onde você pode navegar no XML manualmente, se desejar. Como alternativa, você pode abrir o
arquivo .sqlplan no Bloco de Notas. Exploraremos o XML nos planos de execução em detalhes no Capítulo 13.

Resumo
Neste capítulo, descrevemos brevemente o papel do otimizador de consulta na produção do plano de
execução de uma consulta e como ele seleciona o plano de menor custo, com base em seu conhecimento das
estruturas de dados e conhecimento estatístico da distribuição de dados. Também abordamos o cache do plano,
a importância da reutilização do plano e como promovê-lo.

55
Machine Translated by Google

Capítulo 1: Apresentando o Plano de Execução

Exploramos os diferentes formatos de planos de execução e, em seguida, focamos nos planos


gráficos de execução, como ler esses planos e os vários componentes desses planos. Vamos
gastar muito tempo nos planos gráficos ao interpretar planos de execução individuais, portanto, é
importante entender as informações disponíveis nos planos.

Também tentei esclarecer qualquer confusão sobre o que os termos "plano estimado" e "plano
real" realmente significam. Já ouvi pessoas falarem sobre "planos estimados e reais" como se
fossem dois planos completamente diferentes, ou que o plano estimado pode ser de alguma forma
"impreciso". Espero que este capítulo tenha dissipado esses mal-entendidos.

56
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura


O objetivo deste capítulo é mostrar como começar a ler os planos de execução gráfica. Ainda vamos manter um nível
relativamente alto, usando algumas consultas simples e filtros básicos para explicar a mecânica de ler um plano e o que
procurar em um plano. Nos capítulos subsequentes, começaremos a detalhar os vários operadores individuais e suas
propriedades.

Especificamente, abordaremos:

• uma breve revisão dos operadores de plano de execução mais comuns - categorizados por seus
função básica.

• o básico de como ler um plano gráfico - lemos um plano da direita para a esquerda ou da esquerda
para a direita? Ambos!

• o que procurar em um plano - alguns sinais de alerta importantes e propriedades do operador que
muitas vezes pode ajudar a identificar rapidamente possíveis problemas.

• o operador SELECT – contém muitas informações úteis sobre o plano como


todo.

A linguagem dos planos de execução


De certa forma, aprender a ler planos de execução é como aprender uma nova linguagem, exceto que essa linguagem
é baseada em uma série de operadores, cada um dos quais representado como um ícone em um plano gráfico.
Felizmente, ao contrário de uma linguagem, o número de palavras (operadores) que devemos aprender é mínimo.
Existem aproximadamente 85 operadores disponíveis e a maioria das consultas usa apenas um pequeno subconjunto
deles.

Operadores comuns
Books Online (http://preview.tinyurl.com/y97wndcf) lista todos os operadores em (mais ou menos) ordem alfabética.
Isso é bom como referência, mas não é a maneira mais fácil de aprendê-los, então vamos deixar de ser "alfabeticamente
corretos" aqui.

57
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

Um plano de execução gráfico exibe três tipos distintos de operador:


• Os Operadores Físicos (e suas operações lógicas associadas) aparecem como ícones
azuis e representam a execução da consulta. Eles incluem operadores DML e paralelismo.
Esse é o único tipo de operador que você verá em um plano de execução real.
• Operadores de cursor têm ícones amarelos e representam operações de cursor T-SQL.
• Elementos de linguagem são ícones verdes e representam elementos de linguagem T-SQL, como
como ATRIBUIR, DECLARAR, SE, ENQUANTO, e assim por diante.

O foco deste capítulo, e do livro, está nos operadores físicos e suas operações lógicas
correspondentes. No entanto, também abordaremos os operadores de cursor no Capítulo 14, e haverá
alguns mergulhos em algumas das informações especiais disponíveis nos operadores de elemento de
linguagem.

Um operador físico representa o algoritmo físico escolhido pelo otimizador para implementar a operação
lógica necessária. Cada operador físico está associado a uma ou mais operações lógicas. Geralmente, o
nome do operador físico será seguido entre colchetes pelo nome da operação lógica associada (embora a
Microsoft não seja totalmente consistente sobre isso). Por exemplo, Nested Loops (Inner Join), onde
Nested Loops é a implementação física da operação lógica, Inner Join.

O otimizador tem à sua disposição conjuntos de operadores para ler dados, combinar dados, ordenar e
agrupar dados, modificar dados e assim por diante. Cada operador executa uma única tarefa especializada.
A tabela a seguir lista alguns dos operadores físicos mais comuns, categorizados de acordo com sua
finalidade básica.

Lendo dados Combinando dados Agrupando e ordenando dados

Varredura de tabela/índice Loops aninhados Ordenar

Busca de índice Mesclar associação Agregado de fluxo

Olho para cima Correspondência de hash Correspondência de hash (agregado)

Verificação Constante Junção adaptável Agregado da janela

Seqüência Segmento

Concatenação Carretel de Janela

Trocar

58
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

Manipulando dados Modificando dados atuação

Calcular escalar Inserção de tabela/índice Bitmap

Filtro Atualização de tabela/índice Carretéis

Topo Exclusão de tabela/índice Paralelismo

Projeto de sequência Mesclagem de Tabela/Índice

Afirmar

Dividir

Colapso

Quais operadoras de plano você vê com mais frequência como desenvolvedor ou DBA depende muito da natureza
da carga de trabalho. Para uma carga de trabalho OLTP, você esperará ver muito Index Seek
e operadores de loops aninhados , característicos de consultas frequentes que retornam quantidades
relativamente pequenas de dados. Para um sistema de BI, é provável que você veja mais verificações de índice,
pois geralmente são mais eficientes ao ler uma grande proporção de dados em uma tabela, e junções de junção
de mesclagem ou correspondência de hash , que são algoritmos de junção que se tornam mais eficientes ao unir
dados maiores. fluxos de dados.

Entender todos os mecanismos internos de um determinado operador só é possível se você executar um


depurador no SQL Server. Eu absolutamente não recomendo que você faça isso, mas se você estiver procurando
por um conhecimento profundo dos detalhes internos do operador, recomendo o blog de Paul White (http://
preview.tinyurl.com/y75n6f5z).

Geralmente, no entanto, podemos aprender muito sobre o que um operador está fazendo observando como
eles funcionam e se relacionam entre si nos planos de execução. A chave é começar tentando entender a
mecânica básica do plano como um todo, e então detalhar os operadores "interessantes". Esses podem ser os
operadores com o custo estimado mais alto, como uma varredura ou busca de índice de alto custo, ou pode ser um
operador de "bloqueio", como um Sort (mais sobre bloqueio versus operadores de streaming em breve). Tendo
escolhido um ponto de partida, observe as propriedades desses operadores, onde todos os detalhes sobre o
operador estão disponíveis. Cada operador tem um conjunto diferente de características. Por exemplo, eles
gerenciam a memória de maneiras diferentes. Alguns operadores, principalmente Sort, Hash Match e Adaptive
Join, exigem uma quantidade variável de memória para serem executados. Como tal, uma consulta com um desses
operadores pode ter que aguardar a memória disponível antes da execução, possivelmente afetando negativamente
o desempenho.

59
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

Lendo um plano: da direita para a esquerda ou da esquerda para a direita?

Devemos ler um plano de execução da direita para a esquerda ou da esquerda para a direita? A resposta, como
discutimos brevemente no Capítulo 1, é que geralmente lemos os planos de execução da direita para a esquerda,
seguindo as setas do fluxo de dados, mas é igualmente válido e frequentemente útil ler da esquerda para a direita.

Vamos dar uma olhada em um exemplo muito simples. A Listagem 2-1 mostra uma consulta simples no banco
de dados AdventureWorks2014, recuperando detalhes da tabela Person.Person, dentro de um determinado
intervalo de datas.

SELECIONAR PARTE SUPERIOR ( 5 )

BusinessEntityID ,
PersonType ,
NomeEstilo ,
Título ,
Nome ,
Sobrenome ,
ModificadoData
DE Pessoa.Pessoa
WHERE ModifiedDate >= '20130601'
AND ModifiedDate <= CURRENT_TIMESTAMP ;

Listagem 2-1

A Figura 2-1 mostra o plano de execução resultante.

Figura 2-1: Plano de execução simples, leia da direita para a esquerda.

Se lermos o plano da direita para a esquerda, seguindo a direção do fluxo de dados, a primeira ação no plano é ler os
dados da tabela Person, por meio de um Clustered Index Scan. Os dados passam para o operador Top , que por sua
vez passa as primeiras cinco linhas de volta para o SELECT. Esta é uma maneira perfeitamente válida de ler o plano, e
é a maneira como a maioria das pessoas lê um. No entanto, esses dados

60
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

ordem de fluxo pode implicar que, primeiro, o Clustered Index Scan lê os dados no Person
table e passa as linhas que correspondem à condição de pesquisa na cláusula WHERE (há mais de 13 K linhas
qualificadas) e, em seguida, o operador Top envia apenas as cinco primeiras.

Claro, isso seria altamente ineficiente, e não é o que acontece, como você pode ver pela seta fina entre os
operadores Clustered Index Scan e Top . O Clustered Index Scan lê apenas 5 linhas da tabela Person.

Figura 2-2: Número real de linhas processadas.

De fato, este exemplo ilustra claramente que, durante a execução do plano, os operadores são chamados da
esquerda para a direita, portanto, se seguirmos a ordem em que os operadores são chamados, devemos ler o
plano da esquerda para a direita.

Cada operador suporta um método GetNext ("Give me the next row") e a primeira ação neste caso é uma
chamada GetNext do operador Top para o Clustered Index Scan, que passa a primeira linha qualificada, filtrada de
acordo com a cláusula WHERE, back to Top e, em seguida, o ciclo se repete para cada linha, continuamente
transmitindo linhas de volta para o cliente. Uma vez que o operador Top tenha todas as linhas de que precisa, cinco
linhas neste caso, a execução é interrompida, de modo que o restante da tabela nunca é lido.

61
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

Operadores de streaming versus bloqueio


Muitos dos operadores que você vê nos planos serão operadores sem bloqueio, também conhecidos como streaming.
Um operador de streaming cria dados de saída ao mesmo tempo em que recebe a entrada. Em outras palavras, ele
passará as linhas para o próximo operador assim que tiver realizado sua tarefa naquela linha.

Alguns operadores, no entanto, são operadores de bloqueio e devem reunir todo o conjunto de dados de entrada e, em
seguida, realizar seu trabalho em todo o conjunto de dados, antes de passar qualquer linha. Adicionar PEDIDO
BY ModifiedDate para a Listagem 2-1 e execute novamente a consulta, solicitando o plano de execução real, conforme
mostrado na Figura 2-3.

Figura 2-3: Plano de execução mostrando os operadores de bloqueio.

O Clustered Index Scan (discutido em detalhes posteriormente no Capítulo 3) é um operador de streaming e passa as
linhas à medida que são lidas no índice. Uma varredura indica que lerá todas as linhas na tabela, ou índice, até que todas
as linhas sejam processadas (a menos que um operador diferente, como Top
no exemplo anterior, termina a execução mais cedo). Quando encontra uma linha que se enquadra no intervalo de datas
necessário, passa essa linha para o próximo operador, neste caso, um Sort.

O operador Sort reordena os dados, representando aqui a instrução ORDER BY na consulta.


O operador Sort é um operador de bloqueio. Essa operação lógica é uma classificação N Top devido à operação TOP
na consulta. Ele deve ler cada linha de seu operador filho, neste caso, mais de 13 mil linhas de qualificação, classificá-las
de acordo com os critérios especificados, ModifiedDate, e então passar as cinco primeiras linhas. Nesse tipo de situação,
especialmente para uma entrada muito grande, esses operadores de bloqueio podem diminuir o desempenho.

Alguns operadores são apenas semi-bloqueados e devem concluir apenas parte de seu trabalho antes de
liberar a primeira linha. Por exemplo, o operador de junção Hash Match processa primeiro
todas as linhas de sua primeira entrada, mas processa e retorna as linhas da segunda entrada à medida que as lê.

62
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

A Microsoft não mantém uma lista definitiva de operadores bloqueadores e não bloqueadores. Em
vez disso, você pode inferir o comportamento deles pelas definições e relacionamentos dentro do
plano. Novamente, a chave para entender os planos de execução é começar a entender o que os
operadores fazem e como isso afeta sua consulta.

O aviso mostrado no plano da Figura 2-3, o pequeno ponto de exclamação, será discutido na próxima
seção.

O que procurar em um plano de execução


À medida que as consultas se tornam complexas, seus planos de execução podem rapidamente se tornar bastante
complicados e difíceis de entender, independentemente de lermos o plano da direita para a esquerda ou da esquerda para a direita.
Em vez de vasculhar todos os operadores, muitas vezes podemos identificar possíveis problemas
observando alguns sinais de alerta importantes e examinando as propriedades por trás de certos
operadores importantes.

As recomendações a seguir não excluem a necessidade de entender o plano como um todo e seus
operadores, mas podem ajudá-lo a ler um plano um pouco mais rápido do que tentar rastrear todos os
caminhos de dados e todos os comportamentos um de cada vez.

Discutiremos por que cada um deles são "indicadores" importantes para fontes de possíveis problemas,
mas não detalharemos exemplos específicos. Ao longo do restante do livro, expandiremos essas
recomendações, com exemplos específicos.

Primeiro operador
O primeiro operador, no lado esquerdo do plano de execução, é o SELECT/INSERT/
UPDATE/DELETE (e às vezes outros, como MERGE), e na primeira vez que você olhar para um plano
de execução, sempre vale a pena examinar suas propriedades.

Enquanto a janela Propriedades para outras operadoras revela informações específicas da ação daquela
operadora, a primeira operadora oferece muitas informações sobre o plano em si e sua geração. Inclui
informações como tempo, CPU e memória necessários para compilar o plano, as configurações de
conexão ANSI, se o otimizador concluiu a otimização ou encerrou o processo de otimização
antecipadamente porque foi encontrado um plano bom o suficiente ou não encontrou o que considerava
um plano ideal (isso é chamado de "tempo limite").

63
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

Figura 2-4: Propriedades do primeiro operador.

Analisaremos alguns detalhes desse operador mais adiante neste capítulo e continuaremos, ao
longo do livro, a explorar as informações interessantes que ele fornece.

Ao capturar planos usando Eventos Estendidos (consulte o Capítulo 15), você pode não ver o primeiro
operador e todas as ótimas informações que ele fornece, o que é uma pena. No entanto, a maioria das
informações importantes ainda está disponível nos planos capturados por meio de Eventos Estendidos, dentro
do XML que define o plano.

Avisos
Dentro de um plano de execução, você pode ver (no SQL Server 2012 e posterior) pequenos ícones
aparecerem em um operador, especificamente um ponto de exclamação amarelo ou vermelho. Estes são avisos.
Nem todo aviso indica um problema grave, mas sempre que você vir um, verifique as propriedades desse ícone,
que conterá uma descrição do aviso.

64
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

Figura 2-5: Plano de execução com aviso.

A Figura 2-5 mostra um aviso no SELECT (neste caso causado por uma incompatibilidade de alocação de
memória), mas há outros tipos de aviso, como um aviso em um operador Sort que foi derramado no disco, e
veremos vários como os encontramos nos planos de execução ao longo do livro.

Número estimado versus número real de linhas

É muito importante lembrar que todos os custos que você verá em um plano são baseados em estimativas de
cardinalidade, nunca em linhas reais e contagens de execução. Portanto, esses custos são tão precisos quanto
as estimativas de cardinalidade do otimizador.

Uma das primeiras coisas a verificar em um plano antes de aprofundar, e certamente antes de analisar os
custos associados a operadores individuais, é comparar as contagens de linhas estimadas e reais e certificar-
se de que estejam dentro de margens razoáveis, para confirmar a precisão da cardinalidade estimativas
associadas aos custos estimados. Às vezes, você verá um operador com um custo estimado muito alto, porque
o otimizador estimou que precisaria processar muitas linhas, quando na verdade precisava processar muito
poucas linhas (ou vice-versa, para custos estimados baixos).

Se as contagens de linhas estimadas e reais diferirem significativamente, você precisa descobrir a causa e
corrigi-la primeiro. Só então você pode olhar para o custo estimado dos operadores.

Custo do operador
Tendo verificado que as estimativas de cardinalidade foram precisas, podemos procurar os operadores mais
caros como meio de determinar onde concentrar nossos esforços iniciais. Muitas vezes é útil comparar o custo
de um operador com outro dentro do plano. No entanto, não podemos comparar o custo da operadora em um
plano com o custo da operadora em um segundo plano porque as estimativas de custo são construções
matemáticas e não se prestam diretamente a esse tipo de comparação.

65
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

Além disso, alguns operadores, e vamos discuti-los à medida que avançamos, não têm custos associados a
eles ou são custos "fixos" com base em suposições dentro do otimizador, que podem ou não ser precisas. Por
exemplo, um operador Compute Scalar sempre tem um custo fixo muito baixo (zero-ponto-lotes-de-zeros-um), o
que geralmente é bom, mas ocasionalmente enganoso, como veremos no Capítulo 4.

Portanto, embora as estimativas de custo sejam importantes e as usaremos, lembre-se de que elas não podem ser
confiadas cegamente como uma medida precisa do custo real dentro do plano.

Sugestões de "Índice ausente"


Muitas vezes, você verá uma mensagem na parte superior de um plano dizendo que há um índice
ausente que "reduzirá o custo" de uma operadora em uma porcentagem impressionante. Trate-os
apenas como sugestões, em vez de seguir em frente e criar cada índice sugerido. Lembre-se, um índice
que pode ajudar uma única consulta, que é tudo o que um determinado plano de execução representa,
pode ser prejudicial ao desempenho de sua carga de trabalho como um todo. Além disso, pode haver
mais de um índice sugerido. Você verá apenas um no topo do plano. Verifique o primeiro operador para
ver se há sugestões adicionais.

Fluxo de dados

Conforme discutido anteriormente, o fluxo de dados dentro de um plano de execução é definido pelas setas
que conectam um operador ao próximo. Essas setas, por representarem o fluxo de dados, são frequentemente
chamadas de pipes. A espessura do tubo é baseada na contagem real de linhas quando disponível (plano de
execução real) e em estimativas de outra forma (plano em cache ou estimado). Um tubo mais grosso indica que
mais dados estão sendo processados; um tubo mais fino indica menos dados.
Em alguns casos, alguns dos operadores em um plano real não relatam uma contagem de linhas real, caso em
que a contagem de linhas estimada é usada para definir o tamanho do tubo.

Fique atento não apenas para "tubos gordos", mas também para transições abruptas na espessura do tubo enquanto
você lê o plano de execução. Por exemplo, um tubo muito grosso no início de um plano se estreitando para um tubo
muito fino no lado esquerdo do plano sugere que a filtragem está ocorrendo tardiamente.
Pequenos tubos que ficam cada vez maiores sugerem que sua consulta está de alguma forma multiplicando dados.

66
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

Operadores extras

Na verdade, não existe um operador extra; cada operador em um plano executa uma função específica. A ideia
de um operador "extra" é uma que eu inventei como uma boa maneira de ajudar as pessoas a começarem a ler
os planos de execução. Aqui está como funciona.

Toda vez que você está lendo um plano e vê um operador que nunca viu, ou um operador que viu e entende,
mas não consegue determinar por que está no local em que está dentro do plano, isso é um operador "extra". É
uma operadora que você não conhece, ou não entende porque está afetando o plano.

Sua resposta é simples: entenda o que é o operador e o que ele está fazendo e então ele não é mais um operador
"extra".

Operadores de leitura

Detalharemos os vários operadores de leitura no próximo capítulo. Os que vamos focar aqui são a varredura e
a busca. Um operador de varredura (uma Varredura de Índice ou Varredura de Tabela) é apenas um indicador
de um tipo de acesso a dados que lê as páginas em um índice ou tabela. No entanto, é um tipo de acesso a dados
que indica, frequentemente, que muitas linhas estão sendo acessadas.

Um operador de busca é um indicador de outro tipo de acesso a dados que usa a estrutura de um índice para
identificar um ponto inicial e, possivelmente, um ponto final, para uma varredura direcionada pelas páginas de
um índice. Uma busca indica, na maioria das vezes, que apenas um pequeno número de linhas está sendo
acessado.

A maioria das pessoas ao ler os planos tem uma mentalidade de "examina mal, procura o bem". Na verdade,
nenhuma dessas operações é boa ou ruim, por definição. O que você deseja procurar em um plano são
varreduras de alto custo que recuperam conjuntos de dados limitados (às vezes indicando um índice ausente ou
mal estruturado) ou buscas que recuperam conjuntos de dados extremamente grandes.

67
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

As informações por trás do primeiro operador

Muitas pessoas com o hábito de ler planos da direita para a esquerda imediatamente focam sua atenção
nos operadores de acesso a dados do lado direito. Esquecem de olhar as propriedades do primeiro operador,
o que é uma pena, pois estão perdendo muitas informações valiosas sobre o plano como um todo. Espero que
esta seção corrija isso. Como você verá, há muitas informações disponíveis no primeiro operador sobre o
processo pelo qual o otimizador passou para chegar a esse plano.

É por isso que o primeiro operador em um plano, lendo da esquerda para a direita, é um bom ponto de
partida para explorar o plano de execução de qualquer consulta. A Microsoft define essas operações como
"Elementos de idioma". Eles representam o processo que a consulta está realizando. O nome oficial do
primeiro operador é o operador Result Showplan , mas todos os rótulos nos planos e a dica de ferramenta
se referem a ele por um nome diferente: SELECT em uma consulta SELECT, UPDATE em uma UPDATE
query, e vários outros nomes são possíveis. Em vez de confundir as coisas, usaremos seu nome real, como
SELECT, em vez de nos referirmos a ele como Result Showplan.

Vamos começar com uma consulta simples na tabela HumanResources.Department no banco de dados
AdventureWorks2014.

SELECT d.DepartmentID,
d.Nome,
d.Nome do Grupo
DE Recursos Humanos . Departamento AS d
WHERE d.GroupName = 'Fabricação';

Listagem 2-2

Execute a consulta no SSMS e capture o plano de execução para essa consulta, conforme
mostrado na Figura 2-6.

68
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

Figura 2-6: Plano simples.

O plano tem apenas dois operadores, um Clustered Index Scan, que discutiremos no Capítulo 3, e o SELECT. Ao explorar
as informações fornecidas pelo operador SELECT , use a janela Propriedades completa , pois a dica de ferramenta, mostrada
na Figura 2-7, fornece apenas um subconjunto das informações disponíveis e quase nenhuma das mais importantes.

Figura 2-7: As dicas de ferramentas geralmente não exibem propriedades importantes.

Para abrir a janela Propriedades completa, conforme mostrado na Figura 2-8, basta clicar com o botão direito do mouse
no operador SELECT e selecionar Propriedades no menu de contexto. No restante do livro, usaremos apenas a janela
Propriedades , portanto, faz sentido fixar essa janela na área de trabalho do SSMS. Isso evitará a necessidade de clicar com
o botão direito do mouse em cada operador e você pode simplesmente selecionar o operador a partir desse ponto.

69
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

Figura 2-8: Página de propriedades completa para o operador SELECT.

Todos os valores de propriedade são armazenados com o plano e são visíveis no XML, bem como no plano gráfico.
Não vou explicar todas as propriedades aqui, mas vou começar listando algumas que são ocasionalmente úteis e depois
descrever, com um pouco mais de detalhes, algumas das que você usará regularmente:

• Tamanho do plano em cache – Essa propriedade é importante porque indica quanta memória esse plano
ocupará no cache do plano do SQL Server.
• CardinalityEstimationModelVersion – A partir do SQL Server 2014, um novo
O estimador de cardinalidade pode ser usado pelo otimizador. Você pode dizer se o plano em questão
está usando o novo ou o antigo. O valor na Figura 2-8 é 140, significando o novo estimador. Se fosse 70,
seria a versão antiga do SQL Server 7.
• CompileCPU, CompileMemory, CompileTime – Os recursos usados para produzir o plano. O tempo está
em milissegundos. A memória está em kilobytes.

70
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

• RetrievedFromCache – Isso é um nome impróprio. Em vez de informar que esse plano foi
retirado do cache, basicamente diz que esse plano foi armazenado no cache. Você só verá
um valor "Falso" aqui se o plano em questão não estiver armazenado em cache.

• QueryTimeStats – Introduzida no SQL Server 2016, esta propriedade mostra o tempo


de execução da consulta, quando você está capturando uma consulta real.

Nível de otimização
Isso mostra o nível de otimização necessário para produzir o plano. Geralmente, você verá "Trivial" ou
"Full". Um plano trivial, como este, só pode ser resolvido de uma maneira pelo otimizador, conforme
descrito no Capítulo 1. Exatamente o que torna um plano trivial é a falta de escolhas possíveis para o
otimizador. Por exemplo, uma instrução SELECT * em uma única tabela sem uma cláusula WHERE só
pode ser resolvida de uma maneira. Outro exemplo é uma instrução INSERT em uma tabela usando
VALUES. Isso só pode ser resolvido de uma única maneira pelo otimizador, tornando o plano trivial.

A otimização completa significa apenas que não é um plano trivial, mas na verdade não informa a
extensão do trabalho que o otimizador colocou na otimização desse plano específico. Para ver o nível
de otimização em ação, adicionaremos um JOIN à consulta, como você pode ver na Listagem 2-3.

SELECT d.DepartmentID,
d.Nome,
d.Nome do Grupo,
edh.InícioData
DE Recursos Humanos . Departamento AS d
INNER JOIN HumanResources.EmployeeDepartmentHistory AS edh
ON edh.DepartmentID = d.DepartmentID
WHERE d.GroupName = 'Fabricação';

Listagem 2-3

A Figura 2-9 mostra o plano de execução real.

71
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

Figura 2-9: Plano de execução ilustrando a otimização COMPLETA.

Não examinaremos todo o plano agora, pois ele contém operadores que não discutiremos até mais tarde neste
livro. No entanto, se observarmos as propriedades do operador SELECT , veremos FULL
nível de otimização, conforme mostrado na Figura 2-10.

Figura 2-10: Subconjunto de propriedades do operador SELECT.

Também vemos um valor para uma propriedade relacionada chamada Reason For Early Rescisão da
Otimização de Instrução.

Se um plano for produzido por meio do processo de otimização COMPLETO , haverá um motivo para o otimizador
interromper o processamento e apresentar seu plano selecionado. Para consultas simples, o motivo que você
normalmente verá aqui é Good Enough Plan Found. Isso significa que após pelo menos uma das fases de
otimização, o custo estimado do plano mais barato ficou abaixo do limite para entrar na próxima fase e, portanto, o
otimizador selecionou esse plano como bom o suficiente.

72
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

Para consultas mais complexas, se este valor de propriedade não for relatado, indica que o plano foi
simplesmente aquele selecionado pelo processo de otimização completo após concluir todas as otimizações
possíveis em qualquer fase que o otimizador escolheu para executar o plano.

Você verá dois outros valores nesta propriedade, Timeout e Memory Limit Exceeded. Um valor de Timeout
indica que o otimizador tentou passar por todo o processo de otimização, mas não teve êxito. Em vez disso,
ele passou por tantas tentativas de otimização quanto julgou necessárias para a consulta, mas não encontrou
o que considerava ser um plano matematicamente bom o suficiente. Assim, retornou o plano de menor custo
que havia encontrado até então.

Um valor de Memory Limit Exceeded significa uma consulta extremamente grande e complexa em
estruturas muito complexas. O plano gerado provavelmente não é o ideal para a consulta se você tiver um
tempo limite ou limite de memória excedido. No entanto, sem simplificar sua consulta ou sua estrutura, é
improvável que você obtenha um plano melhor.

Lista de parâmetros

Em nossa consulta na Listagem 2-2, a consulta de tabela única, codificamos permanentemente o valor
fornecido para GroupName, na cláusula WHERE. Em outras palavras, não usamos parâmetros ou variáveis
locais. No entanto, a janela Propriedades exibe uma Lista de Parâmetros, cuja visualização expandida é
mostrada na Figura 2-11, onde vemos um parâmetro denominado @1 e seus valores de tempo de
compilação e tempo de execução correspondentes.

Figura 2-11: Propriedades SELECT mostrando a Lista de Parâmetros.

Por se tratar de uma consulta muito simples, o otimizador conseguiu realizar um processo chamado de
parametrização simples. Este é um processo em que o otimizador reconhece que, se você estivesse
usando um parâmetro em vez do valor codificado fornecido, ele seria capaz de criar um plano de execução
que pode ser reutilizado. Então, ele substitui um parâmetro próprio. Nesse caso, o otimizador parametrizou
nosso argumento de pesquisa para que a cláusula WHERE de nossa consulta seja agora

73
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

ONDE d.GroupName = @1. Como resultado, podemos ver esse parâmetro no operador SELECT de nossas consultas.
Quando você vê esse tipo de parametrização, também é importante inspecionar a consulta (no operador SELECT ) para
verificar qual dos valores codificados na consulta original é substituído por qual parâmetro.

Sem parametrização simples, se fôssemos executar a consulta na Listagem 2-2 novamente, mas com um valor
diferente na condição de pesquisa, como WHERE d.GroupName = 'Sales
e Marketing', o texto da consulta foi alterado, nenhum plano corresponderá e o otimizador gerará um novo plano,
mesmo que tenhamos executado o que é essencialmente a mesma consulta.

No entanto, com nossa consulta recém-parametrizada, o texto da consulta permanece estático de uma execução para
a próxima, e o SQL Server troca o valor necessário para o parâmetro @1 em cada execução subsequente. Supondo
que nenhuma opção SET seja alterada, o otimizador reutilizará o plano existente. A Figura 2-12 mostra a Lista de
Parâmetros para uma segunda execução da consulta, com um valor diferente fornecido na condição de pesquisa.

Figura 2-12: Propriedades SELECT com valores de Compiled e Runtime variados.

No entanto, você notará que não vemos uma Lista de Parâmetros nas propriedades SELECT para a consulta de
duas tabelas na Listagem 2-3. O otimizador só pode realizar parametrização simples para consultas simples de uma
tabela. A melhor maneira de promover a reutilização do plano é parametrizar ativamente suas consultas, usando
procedimentos armazenados.

Sempre que um parâmetro é usado, o valor passado para esse parâmetro é usado para comparar com as estatísticas
da coluna ou índice que está sendo usado. Isso é conhecido como "sniffing de parâmetro" (ou "sniffing variável"). O
uso do valor específico leva o otimizador a fazer melhores escolhas com base em suas estatísticas. Portanto, você pode
consultar o operador SELECT para obter os valores de compilação e de tempo de execução dos parâmetros para
entender como a detecção de parâmetros foi resolvida em qualquer consulta.
Discutiremos a detecção de parâmetros e os problemas ocasionais que ela causa com mais detalhes quando
chegarmos aos procedimentos armazenados.

74
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

QueryHash e QueryPlanHash

O QueryHash é um valor de hash da consulta, que é armazenado com o plano e usado pelo otimizador
para identificar planos com lógica igual ou muito semelhante. Conforme discutido no Capítulo 1, se o
valor de uma consulta enviada corresponder ao QueryHash para um plano no cache, o otimizador
analisa o texto SQL e, se for idêntico, pode reutilizar o plano, assumindo que não há diferença nas
opções SET ou ID do banco de dados . O QueryHash pode ser muito útil em situações em que você
está lidando com T-SQL ad hoc ou dinâmico e precisa identificar se há várias consultas semelhantes no
sistema para as quais planos separados estão sendo criados.

O QueryPlanHash é como o valor QueryHash , mas para o próprio plano. Ele identifica os planos que
são iguais em termos das operações que executam e da ordem em que as executam.

Deixando de lado os casos em que o otimizador realiza a "auto-parametrização", podemos ter casos
como os seguintes:
• Se fizermos uma alteração apenas em valores literais, e isso não afetar o plano, podemos ver
vários planos no cache, cada um com o mesmo QueryHash e o mesmo Query PlanHash.

• Se alterarmos apenas os literais, mas resultar em um plano diferente, veremos


vários planos, cada um com o mesmo QueryHash, mas valores diferentes para Query
PlanHash.
• Se fizermos uma alteração lógica na consulta que não afete o plano de execução, poderemos
ver vários planos no cache, cada um com um QueryHash diferente
mas o mesmo QueryPlanHash.

DEFINIR opções

A Figura 2-13 mostra as configurações de conexão ANSI e outras opções SET que foram usadas quando
o plano foi criado. Esses são valores muito úteis porque, como mencionado acima, alterar essas
configurações pode resultar em vários planos no cache para o que são, em todos os outros aspectos,
consultas idênticas.

75
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

Figura 2-13: Configurações ANSI nas propriedades SELECT.

Outras ferramentas e técnicas úteis ao ler


Planos
Um dos principais (mas não os únicos) usos dos planos de execução é entender como uma consulta está sendo executada,
a fim de entender por que ela está tendo um desempenho ruim.

Dessa forma, geralmente é muito útil coletar métricas de desempenho junto com seus planos de execução, especialmente
quando você está tentando ajustar uma consulta em seu ambiente de desenvolvimento. Há várias maneiras de coletar
métricas de consulta:
• DEFINIR ESTATÍSTICAS IO/HORA
• Incluir estatísticas do cliente

• Rastreamento SQL (Profiler)


• Eventos Estendidos

• Repositório de consultas (abordado no Capítulo 16)

Na verdade, existem algumas outras maneiras, mas essas são as mais usadas e as mais úteis. Vou recomendar
que você use Extended Events para métricas detalhadas e Query Store, quando possível, para métricas agregadas.
Existem várias razões para isso, mas vamos começar usando STATISTICS IO/TIME.

76
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

Estatísticas de E/S e temporização usando comandos SET


As pessoas costumam usar STATISTICS IO/TIME para capturar o desempenho de consulta individual
ao ajustar uma consulta. Tudo o que fazemos é cercar a consulta com os comandos SET, conforme mostrado
na Listagem 2-4.

SET STATISTICS IO ON;


DEFINIR O TEMPO DAS ESTATÍSTICAS;
SELECT d.DepartmentID,
d.Nome,
d.Nome do Grupo
DE Recursos Humanos . Departamento AS d
WHERE d.GroupName = 'Fabricação';
DEFINIR ESTATÍSTICAS IO OFF;
SET STATISTICS TIME OFF;

Listagem 2-4

Observe a saída completa desses valores para a execução de uma única consulta, conforme mostrado na
Listagem 2-5.

SQL Server analisa e compila o tempo:


Tempo de CPU = 0 ms, tempo decorrido = 0 ms.
(2 linhas afetadas)
Tabela 'Departamento'. Contagem de varredura 1, leituras lógicas 2, leituras físicas 0, leituras
antecipadas 0, leituras lógicas lob 0, leituras físicas lob 0, leituras antecipadas lob 0.

(1 linha(s) afetada)
Tempos de execução do SQL Server:
Tempo de CPU = 0 ms, tempo decorrido = 6 ms.
Tempos de execução do SQL Server:
Tempo de CPU = 0 ms, tempo decorrido = 0 ms.

Listagem 2-5

Sem alguém explicando exatamente o que procurar, você pode dizer o número de leituras e exatamente quanto
tempo a consulta levou para ser executada? Uma vez explicado, com certeza, mas a saída aqui é bastante obscura.
A única vantagem é que a E/S é dividida por tabela, o que pode ser útil às vezes; por isso, dependendo da situação,
usarei STATISTICS IO, mas com a seguinte ressalva: capturar STATISTICS IO pode impactar negativamente no
tempo de execução devido à sobrecarga adicional de transferir as informações de E/S para o cliente após

77
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

é capturado. Se você está tentando ajustar uma consulta e quer ver se ela está rodando mais rápido ou mais devagar,
além de capturar o número de leituras, você precisa que suas medidas sejam precisas e elas simplesmente não serão
com STATISTICS IO.

Além disso, também nem sempre revela todo o trabalho realizado. Por exemplo, se você tiver um código que faz
muitas chamadas para uma função definida pelo usuário, ele não contará essa E/S, enquanto os eventos estendidos
contam.

Incluir estatísticas do cliente


Se você estiver investigando consultas que são executadas rapidamente, mas com frequência, a sobrecarga de
mostrar os resultados na grade ou no texto geralmente é significativa o suficiente para invalidar as medições de desempenho.

Uma técnica útil nesses casos é alterar as opções de consulta para descartar os resultados após a execução,
adicionar um número alto após os comandos GO para que a consulta seja executada muitas vezes (por exemplo, GO
100 para executar uma consulta 100 vezes) e usar SSMS Incluir a opção Client Statistics para ver o tempo decorrido.

SQL Trace e Profiler


A GUI do Profiler usa um mecanismo de buffer diferente do Trace Events, que pode afetar diretamente seu servidor de
forma que a coleta de métricas possa impactar negativamente o servidor ou até mesmo derrubá-lo. Eu não recomendo
nunca executar o Profiler em seu servidor de produção, e executá-lo em um servidor de desenvolvimento pode invalidar
a coleta de métricas. Os eventos de rastreamento não podem ser filtrados no ponto de captura. Em vez disso, todos os
eventos de rastreamento são capturados e filtrados posteriormente, aumentando radicalmente sua sobrecarga em seu
sistema. Além disso, o Trace e o Profiler estão na lista para descontinuação. Isso significa que em uma próxima edição do
SQL Server eles não estarão mais disponíveis. É hora de parar de usá-los.

Eventos estendidos
Minha recomendação é capturar suas métricas de E/S e tempo usando Eventos Estendidos.
Eles estão em suporte ativo da Microsoft. Eles oferecem uma filtragem melhor e mais eficaz do que o Trace. Eles operam
mais abaixo na pilha de chamadas no SQL Server, portanto, têm um impacto muito menor no desempenho. Sua medida
de desempenho e leituras é clara e fácil de entender. Ao trabalhar no SQL Server 2012 ou superior, há uma interface
gráfica totalmente funcional para observar as métricas coletadas.

78
Machine Translated by Google

Capítulo 2: Introdução aos Planos de Leitura

Por todas essas razões, recomendo fortemente que você use os Eventos Estendidos. A Listagem 2-6 oferece
um mecanismo básico para capturar procedimentos armazenados e lotes.

CRIAR SESSÃO DE EVENTO ConsultaDesempenho NO SERVIDOR


ADICIONAR EVENTO sqlserver.rpc_completed (
WHERE (sqlserver.database_name = N'AdventureWorks2014')),
ADICIONAR EVENTO sqlserver.sql_batch_completed (
ONDE (sqlserver.database_name = N'AdventureWorks2014'))
ADD TARGET package0.event_file (SET filename = N'QueryPerformance')
COM (MAX_MEMORY = 4096 KB,
EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS,
MAX_DISPATCH_LATENCY = 3 SEGUNDOS,
MAX_EVENT_SIZE = 0 KB,
MEMORY_PARTITION_MODE = NONE,
TRACK_CAUSALITY = OFF,
STARTUP_STATE = OFF);

Listagem 2-6

Resumo
Este capítulo introduziu os fundamentos da leitura de planos de execução, começando com a definição
da "linguagem" usada pelos próprios planos. Também introduzimos um conjunto básico de coisas a serem
procuradas nos planos de execução. Isso pode funcionar como um guia para ler todos os planos de execução,
não importa o tamanho. Basta lembrar que os detalhes do plano são muito importantes e as informações
apresentadas aqui são apenas um guia. Cobrimos as informações muitas vezes negligenciadas por trás do
primeiro operador. Completamos com algumas ferramentas e técnicas úteis que são frequentemente usadas
lado a lado com planos de execução para coletar estatísticas úteis de execução.

79
Machine Translated by Google

Capítulo 3: Operadores de leitura de dados


Neste capítulo, vamos examinar os operadores de leitura de dados, que representam a
diferentes mecanismos do otimizador para leitura de dados. Eles também podem atuar como um
mecanismo de filtragem, para passar as linhas de qualificação para o próximo operador.

Abordaremos os seguintes operadores em detalhes:


• Verificação de Índice Agrupado

• Varredura de índice (sem cluster)


• Busca de Índice Agrupado

• Busca de Índice (não agrupado)


• Pesquisa de chave (agrupada)
• Varredura de Tabela

• Pesquisa RID (heap).

À medida que avançamos, você aprenderá como os operadores funcionam e começará a aprofundar seu conhecimento
dos planos de execução em geral, os vários operadores que eles usam e como ler o plano e entender as escolhas do
otimizador sobre como a consulta deve ser executada .

Lendo um índice
Os índices tradicionais do SQL Server, que excluem otimização de memória, columnstore, índices de texto
completo e outros, consistem em 8 K páginas conectadas em uma estrutura b+tree. Estes são frequentemente
chamados de árvore equilibrada, árvore espessa ou mesmo árvore Bayer, em homenagem ao pesquisador
principal que os desenvolveu.

A maioria das tabelas de substituição em um banco de dados SQL Server deve ter um índice clusterizado.
As páginas em nível de folha de um índice clusterizado armazenam as linhas de dados, ordenadas de acordo
com todas as colunas da chave de índice clusterizado. Um índice clusterizado não é uma "cópia" da tabela. É a
tabela, com uma estrutura b+tree construída sobre ela, para que os dados sejam organizados pela chave de
agrupamento. Isso explica por que só podemos criar um índice clusterizado por tabela.

80
Machine Translated by Google

Capítulo 3: Operadores de leitura de dados

Além de um índice clusterizado, a maioria das tabelas possui um ou mais índices não clusterizados, projetados
para melhorar o desempenho de consultas críticas, frequentes e caras. Um índice não clusterizado tem a
mesma estrutura b+tree, mas as páginas em nível de folha não contêm as linhas de dados, apenas os dados para
as colunas de chave de índice, mais as colunas de chave de índice clusterizado (supondo que a tabela não seja um
heap), mais quaisquer colunas que adicionamos opcionalmente ao índice usando a cláusula INCLUDE.

Existem basicamente três classes de operadores que o SQL Server pode usar para acessar dados em um índice:
scan, seek ou lookup.

Verificações de índice

Em uma operação de verificação, o SQL Server navega para a primeira ou última página de nível de folha do índice e, em
seguida, verifica para frente ou para trás nas páginas de folha. Uma varredura geralmente lê todas as páginas no nível
folha do índice, mas pode ler apenas uma parte do índice em alguns casos.

Uma varredura geralmente ocorre quando todas as linhas precisam ser lidas para satisfazer a definição da consulta.
Você também pode ver uma varredura quando tantas linhas precisam ser lidas que a varredura de todas levaria menos
tempo do que navegar na estrutura do índice para encontrá-las (também conhecido como "procurando", discutido em breve).
Às vezes, o otimizador escolhe uma varredura porque não há índice utilizável para as colunas de Predicado ou porque
a consulta é escrita de tal forma que não é possível realizar uma busca no índice (por exemplo, uma função em uma
coluna levará a digitalizações).

Se ocorrer uma varredura em um índice clusterizado, veremos o operador Clustered Index Scan e, se estiver em um
índice não clusterizado, veremos um operador Index Scan (não clusterizado) . É a mesma operação em ambos os
casos. No caso de uma tabela de heap, uma tabela sem um índice clusterizado, você verá um Table Scan, que é
efetivamente a mesma operação, apenas feita em uma estrutura diferente, o heap em oposição a um índice. Isso será
discutido mais adiante no capítulo.

81
Machine Translated by Google

Capítulo 3: Operadores de leitura de dados

Verificação de índice clusterizado

A Listagem 3-1 mostra uma consulta simples na tabela Employee, procurando pessoas com aniversários de mais
de 50 anos.

SELECT e.LoginID,
e.JobTitle,
e.Data de nascimento
FROM HumanResources.Employee AS e
WHERE e.Data de Nascimento < DATAADD (ANO, -50, GETUTCDATE());

Listagem 3-1

A Figura 3-1 mostra o plano de execução real.

Figura 3-1: Plano de execução com uma varredura de índice clusterizado.

O otimizador escolheu um operador Clustered Index Scan para recuperar os dados necessários. Se a sua
janela Property já estiver aberta, clique em Clustered Index Scan para carregá-la com as informações desse
operador. Caso contrário, clique com o botão direito do mouse no ícone e selecione Propriedades na
menu contextual.

Você notará muitas propriedades que se repetem de um operador para outro. Algumas dessas propriedades
podem ser úteis para entender como o operador funciona e o que ele está fazendo, mas algumas propriedades
são relatadas para muitos operadores, mas são interessantes apenas no contexto de operadores específicos. Por
exemplo, Rebinds e Rewinds (estimado e real) só são importantes ao lidar com o operador Nested Loops , mas
não há junções desse tipo nesse plano, portanto, nesse caso, esses valores são inúteis para você.

82
Machine Translated by Google

Capítulo 3: Operadores de leitura de dados

Figura 3-2: Propriedades do operador Clustered Index Scan.

Algumas das propriedades são auto-explicativas. Observando a Figura 3-2, próximo à parte
inferior das Propriedades, você encontra a propriedade Object . Isso indica a qual objeto esse
operador faz referência. Nesse caso, o índice clusterizado usado foi HumanResources.Employee.PK_
Employee_BusinessEntityID.
Outras propriedades interessantes podem incluir a Lista de Saída. Estas são as colunas que são
saídas da operação. Perto do topo, porém, você também verá Valores Definidos. Estes são os
valores adicionados ao processo por este operador. Neste caso, a Lista de Saída e os Valores
Definidos são os mesmos, mas em outros casos, como quando um cálculo é feito em um operador
Compute Scalar (discutido no próximo capítulo), ou em qualquer outro operador, você verá
informações adicionais em Valores Definidos.

83
Machine Translated by Google

Capítulo 3: Operadores de leitura de dados

Conforme discutido em detalhes nos capítulos anteriores, todas as propriedades que começam com "Estimated",
como Estimated I/O Cost e Estimated CPU Cost são medidas atribuídas pelo otimizador, mas não representam
medidas reais de I/O e CPU. Mesmo em um plano real, esses valores representam as estimativas do otimizador
com base nas estatísticas. O custo estimado de cada operadora contribui para o custo estimado geral do plano.

Como capturamos um plano de execução real, vemos o número estimado de linhas


e o Número Real de Linhas, que é o número estimado e real de linhas geradas pelo operador. Nesse caso, o
operador gera 26 linhas (o número de linhas com BirthDate há mais de 50 anos). Você também pode ver o
número de linhas que foram acessadas por meio da propriedade Número de linhas lidas . Nesse caso, é 290,
ou todo o índice clusterizado.

A propriedade Ordered é False, indicando que o otimizador não exigiu que os dados fossem recuperados na
ordem da chave de índice. Se adicionarmos uma cláusula ORDER BY e.BusinessEntityID à Listagem 3-1, esse
valor de propriedade mudaria para True, pois poderia usar a ordem de chave agrupada para executar essa
operação. O otimizador pode optar por usar a ordem do índice para suas varreduras. Isso pode ser muito útil se
um dos próximos operadores na linha precisar de dados ordenados, pois nenhuma operação de classificação
extra é necessária, possivelmente tornando esse plano de execução mais eficiente, dependendo das necessidades
da consulta.

A propriedade Predicate é importante, e mostra o Predicate aplicado por este operador (clique nas reticências
para ver o texto completo):

[AdventureWorks2014].[Recursos Humanos].[Funcionário].[Data de Nascimento] como [e].


[Data de Nascimento]<dataadd(ano,(-50),getutcdate())

O operador é uma varredura e lê todas as páginas no nível folha do índice. Em outras palavras, ele lê todas as
linhas da tabela, 290 neste caso (consulte o valor da propriedade Table Cardinality ).
Embora uma varredura geralmente leia todas as linhas, ela nem sempre as retorna todas. Aqui, ele avalia o
Predicado para cada uma das 290 linhas que lê e gera apenas as 26 linhas que correspondem à condição. Essa
é uma diferença importante entre um predicado e um predicado de busca (que veremos em breve, quando
discutirmos as operações de busca de índice). Embora a filtragem pareça semelhante em cada caso, o último lê
apenas as linhas que correspondem à condição.

Então, por que vemos uma varredura neste caso? Simplesmente porque o otimizador não tem um índice disponível
que corresponda à nossa coluna Predicado. A chave de índice clusterizado está em BusinessEntityID
portanto, os dados no nível folha são organizados por essa coluna. O operador de varredura precisa varrer todas
as páginas-folha para encontrar as linhas correspondentes. Ler uma página é uma leitura lógica, portanto, o número
de leituras lógicas necessárias para retornar os dados dependerá do número de páginas no nível folha do índice.

84
Machine Translated by Google

Capítulo 3: Operadores de leitura de dados

Verificação de índice

Uma Varredura de Índice é igual a uma Varredura de Índice Agrupado. É apenas contra um tipo diferente
de objeto. Vamos examinar a consulta na Listagem 3-2.

SELECT e.LoginID,
e.BusinessEntityID
DE HumanResources.Employee AS e;

Listagem 3-2

Essa pequena consulta está recuperando apenas dois valores, LoginID e BusinessEntityID.
Acontece que há um índice na tabela HumanResources.Employee, AK_
Employee_LoginID. A Figura 3-3 mostra o plano de execução.

Figura 3-3: Plano de execução com uma varredura de índice.

Como a consulta em questão não tem uma cláusula WHERE, há pouco que o otimizador possa fazer para
escolher como ele irá recuperar as informações. Tem que fazer uma varredura. No entanto, com base nas
colunas selecionadas, ele tem a opção de fazer essa varredura. Nosso índice, AK_
Employee_LoginID é digitado na coluna LoginID. Como a chave de índice clusterizado para esta tabela está
em BusinessEntityID, essa chave está incluída no índice não clusterizado.
Isso significa que o otimizador pode escolher esse índice para satisfazer a consulta. Além disso, como o
tamanho desse índice, medido em número de páginas, é menor que o índice de chave primária, as varreduras
desse índice serão mais rápidas e usarão menos recursos.

Fora os motivos para a escolha deste índice, o processo de varredura é o mesmo. Ele está recuperando os
dados do nível folha do índice.

85
Machine Translated by Google

Capítulo 3: Operadores de leitura de dados

As digitalizações são "ruins?"

As varreduras não são uma coisa "ruim". Se quisermos todos, ou a maioria dos dados de uma tabela de tamanho modesto, eles
podem ser uma operação muito eficiente. Em nosso exemplo Clustered Index Scan , o fato de o operador processar 290 linhas
para produzir apenas 21 não terá um impacto significativo no desempenho na maioria dos sistemas. No entanto, e se o otimizador
optasse por usar uma varredura para gerar 21 linhas de uma tabela contendo não 300, mas 3 milhões de linhas? Nesse ponto,
estamos realizando muitas leituras lógicas desnecessárias e talvez seja necessário ajustar a consulta para fazer melhor uso de nosso
índice existente ou adicionar um índice que permitirá que o otimizador escolha um plano em que o SQL Server engine precisará
apenas ler as páginas contendo as 21 linhas que precisamos retornar.

Conforme discutido anteriormente, há outros motivos pelos quais podemos ver uma operação de varredura. Às vezes,
nossa lógica de consulta faz com que o otimizador escolha uma varredura quando existe um índice que ele poderia,
teoricamente, buscar. Um exemplo disso seria quando você tem uma consulta que incorpora a coluna indexada em uma
expressão. Isso impede que o otimizador seja capaz de determinar quais dos valores armazenados nessa coluna podem
corresponder, porque ele precisa avaliar a expressão para cada linha e, portanto, precisa varrer todo o índice.

Também é possível que as estatísticas de um índice se tornem obsoletas ao longo do tempo. Nesses casos, o otimizador pode
superestimar o número de linhas que provavelmente serão retornadas, optando por varrer quando uma busca poderia ter sido
mais eficiente.

Às vezes, nossa consulta pode simplesmente exigir todas ou a maioria das linhas, portanto, uma varredura é a maneira mais
eficiente de fazer isso. No exemplo da Listagem 3-2, a falta de uma cláusula WHERE forçou o otimizador a solicitar o retorno de
todas as linhas da tabela.

Uma pergunta óbvia a ser feita, se você vir uma Varredura de Índice em seu plano de execução, é se você está processando mais
linhas do que o necessário. O caso de negócios, ou o aplicativo, pode solicitar todas as linhas de uma tabela, mas depois filtrá-las
no cliente ou no aplicativo. Não é razoável adiar tais solicitações. Você também pode ver um número inesperado de linhas em que
sabe que está filtrando em um índice bem estruturado com estatísticas atualizadas e ainda vê uma verificação. Nesse caso, você
deve questionar por que e como uma varredura está sendo usada.

O processamento de linhas desnecessárias desperdiça recursos do SQL Server e prejudica o desempenho geral.
É por isso que uma verificação pode ser um indicador de um problema em potencial, mas uma verificação não é, por definição,
uma coisa ruim.

86
Machine Translated by Google

Capítulo 3: Operadores de leitura de dados

Buscas de índice

Em uma operação de busca, o SQL Server navega diretamente para a(s) página(s) que contém as linhas qualificadas ou
para o início/fim de um intervalo de linhas e processa apenas as linhas que precisa produzir.

Assim como uma varredura não é necessariamente "ruim", uma busca nem sempre é "boa". Uma busca é uma maneira
eficiente de recuperar um pequeno número de linhas de uma tabela relativamente grande. No entanto, um operador de busca
às vezes pode se tornar altamente ineficiente, por exemplo, se estatísticas imprecisas fizeram com que o otimizador
subestimasse massivamente o número de linhas que o operador precisará processar.

Uma busca ocorre quando:

• existe um índice que corresponde a uma coluna de Predicado usada na consulta e o índice
cobre a consulta (pode fornecer todas as colunas que a consulta precisa)

• um índice corresponde à coluna Predicado usada na consulta, não cobre a consulta, mas o Predicado é altamente
seletivo (retorna apenas uma pequena porcentagem das linhas).

Se ocorrer uma busca em um índice clusterizado, veremos o operador Clustered Index Seek e, se estiver em um índice
não clusterizado, veremos um operador Index Seek (não clusterizado) . É a mesma operação em ambos os casos.

Busca de índice agrupado

Vamos examinar uma nova consulta.

SELECT e.BusinessEntityID,
e.NationalIDNumber,
e.LoginID,
e.Férias,
e.SickLeaveHours
FROM HumanResources.Employee AS e
WHERE e.BusinessEntityID = 226;

Listagem 3-3

87
Machine Translated by Google

Capítulo 3: Operadores de leitura de dados

Execute esta consulta e capture o plano real e você verá o Clustered Index Seek
operador, escolhido pelo otimizador para ler o índice clusterizado na tabela Employee.

Figura 3-4: Um plano simples mostrando um operador Clustered Index Seek.

Agora que nossa consulta contém um Predicado de pesquisa (BusinessEntityID) que corresponde à chave
do índice clusterizado, o uso desse índice pelo SQL Server se torna análogo a procurar uma palavra no
índice de um livro para obter as páginas exatas que contêm essa palavra. O operador de busca usa os valores
de chave para identificar a linha, ou linhas, de dados necessários e navega pela estrutura b+tree diretamente
para essas páginas.

Isso significa que uma Busca de Índice lê apenas as páginas que contêm dados incluídos no filtro. Para
retornar uma única linha ao usar um índice, como no exemplo, o SQL Server executa apenas três leituras
lógicas para recuperar os dados. Isso inclui as páginas que ele lê enquanto percorre a árvore b+ do índice
para encontrar a página de nível de folha onde a linha está armazenada, além da leitura da página de nível
de folha.

Dessa forma, as buscas podem reduzir significativamente a E/S em comparação com uma varredura, supondo que o filtro
defina um subconjunto pequeno o suficiente de todo o conjunto de dados. Obviamente, as páginas em nível de folha de um
índice clusterizado armazenam as linhas de dados reais, portanto, nenhuma etapa extra é necessária para retornar todos os
dados exigidos pela consulta.

A Figura 3-5 mostra uma seção das propriedades para nossa Busca de Índice Agrupado.

Figura 3-5: Propriedades do operador Index Seek.

88
Machine Translated by Google

Capítulo 3: Operadores de leitura de dados

O índice usado, mostrado na propriedade Object , é o mesmo do exemplo da Listagem 3-1, especificamente
o PK_Employee_BusinessEntityID, que é a restrição PRIMARY KEY e o índice clusterizado para esta tabela.
Nesse caso, o índice foi criado automaticamente para impor a restrição; são objetos diferentes, mas com a

mesmo nome.

Um operador de busca possui uma propriedade chamada Seek Predicates, que exibe cada um dos predicados
usados para definir as linhas que precisam ser lidas:

Seek Keys[1]: Prefixo: [AdventureWorks2014].[HumanResources].[Employee].


BusinessEntityID = Operador Escalar(CONVERT_IMPLICIT(int,[@1],0))

Mais uma vez, podemos ver os efeitos da parametrização simples. Desta vez, também vemos uma
função CONVERT_IMPLICIT aplicada ao valor do parâmetro @1, para BusinessEntityID, pois o valor que
fornecemos (226) é inferido como um smallint e precisa ser convertido em um int para permitir uma busca. O
otimizador escolhe o tipo de dado para parametrização simples com base no tamanho do valor passado para
ele. Se passássemos um valor maior, ele criaria o parâmetro como um int e criaria um segundo plano de
execução. No entanto, como você pode ver, isso não afetou a escolha de uma operação Index Seek ; algumas
conversões de tipo são prejudiciais e levam a uma varredura quando uma busca deveria ser possível, outras não.

Busca de índice (não agrupado)

Vamos executar uma consulta simples na tabela Person.Person.

SELECT p.BusinessEntityID,
p.Sobrenome,
p.Nome
DE Pessoa.Pessoa AS p
WHERE p.LastName LIKE 'Jaf%';

Listagem 3-4

Essa consulta tira proveito de um índice não clusterizado (IX_Person_LastName_First Name_MiddleName)


na tabela, como você pode ver no plano de execução na Figura 3-6.

89
Machine Translated by Google

Capítulo 3: Operadores de leitura de dados

Figura 3-6: Plano mostrando o operador Index Seek no índice não clusterizado.

Um operador de busca em um índice não clusterizado funciona da mesma maneira que um operador de
busca em um índice clusterizado. Como tal, não há novas propriedades para ver para este operador em
comparação com a Busca de Índice Agrupado. No entanto, vale a pena notar que para este Index Seek (não agrupado)
operador, vemos as propriedades Predicate e Seek Predicates .

O Predicate se parece com isso e corresponde essencialmente à nossa cláusula WHERE:

[AdventureWorks2014].[Pessoa].[Pessoa].[Sobrenome] como [p].[Sobrenome] como N'Jaf%'

A propriedade Procurar predicados mostra o seguinte:

Chaves de busca[1]:
Início: [AdventureWorks2014].[Pessoa].[Pessoa].Sobrenome >= Escalar
Operador (Operador),
Fim: [AdventureWorks2014].[Pessoa].[Pessoa].Sobrenome < Escalar
Operador(N'JaG')

Em vez de um LIKE 'Jaf%', como foi passado na consulta, o otimizador modificou a lógica que usa para que um
filtro adicional seja adicionado da seguinte forma (menos um pouco de formatação):

Person.LastName >= 'Jaf' AND Person.LastName < 'JaG'.

Este é um bom exemplo do tipo de trabalho realizado pelo otimizador, conforme descrito no Capítulo 1. Nesse caso,
o otimizador otimizou a cláusula WHERE Predicate, reescrevendo-a de um LIKE
condição para um intervalo definido por uma condição AND. Isso se baseia no fato de que todos os valores que
correspondem à condição LIKE logicamente precisam estar no intervalo especificado. Dependendo da ordenação,
o intervalo também pode conter valores que não correspondem à condição LIKE. Portanto, o último não é removido,
mas repetido na propriedade Predicate .

90
Machine Translated by Google

Capítulo 3: Operadores de leitura de dados

Não há nada de novo para vermos no operador SELECT no plano, exceto observar que essa instrução,
diferente de muitas das instruções simples que estamos usando como exemplos, não passou por uma
parametrização simples. Isso ocorre porque um predicado LIKE pode ser tratado de diferentes maneiras,
dependendo se o padrão de correspondência de texto começa com um curinga e, portanto, o otimizador
não pode fazer a parametrização.

Conforme observado anteriormente, para um índice não clusterizado, as páginas em nível de folha
contêm apenas as colunas indexadas, além de colunas do índice clusterizado (BusinessEntityID, neste
exemplo), além de quaisquer colunas que incluímos usando a cláusula INCLUDE. Neste exemplo, todas
as colunas exigidas pela consulta estão contidas no nível folha do índice não clusterizado. Em outras
palavras, este é um índice de cobertura para esta consulta.

Principais pesquisas

Um operador Key Lookup (Clustered) ocorre além de um Index Seek (ou às vezes um Index Scan),
quando o índice usado não cobre a consulta. O otimizador usa uma pesquisa de chave para o índice
clusterizado, que recuperará valores para colunas não disponíveis no índice não clusterizado.

Vamos pegar a mesma consulta da Listagem 3-4 e modificá-la um pouco para que também retornemos a
coluna NameStyle, conforme mostrado na Listagem 3-5.

SELECT p.BusinessEntityID,
p.Sobrenome,
p.Nome,
p.NomeEstilo
DE Pessoa.Pessoa AS p
WHERE p.LastName LIKE 'Jaf%';

Listagem 3-5

Se executarmos essa consulta e capturarmos o plano, ela deverá ser parecida com a Figura 3-7.

91
Machine Translated by Google

Capítulo 3: Operadores de leitura de dados

Figura 3-7: Um plano com um operador Key Lookup.

O otimizador ainda escolheu um operador Index Seek (não clusterizado) no mesmo índice não
clusterizado que vimos anteriormente, IX_Person_LastName_FirstName_MiddleName.
No entanto, em termos de colunas exigidas pela consulta, o nível folha do índice armazena apenas
LastName, FirstName (já que fazem parte da chave de índice) e Busines sEntityID (a chave de
índice clusterizado). Ele não contém a coluna NameStyle e, portanto, vemos o operador Key Lookup
adicional , que usa os valores de chave do índice clusterizado para recuperar o valor correspondente
para a coluna NameStyle do nível folha do índice clusterizado.

Um operador de loops aninhados , que combina os resultados dessas duas operações, sempre
acompanha uma pesquisa de chave. Não examinaremos esse operador até o próximo capítulo.

Vamos revisar algumas das propriedades para este operador Key Lookup :

Figura 3-8: Propriedades mostrando a Lista de Saída de colunas.

92
Machine Translated by Google

Capítulo 3: Operadores de leitura de dados

A propriedade Object mostra PK_Person_BusinessEntityID, que é o índice clusterizado nesta


tabela e o destino da Pesquisa de Chave. A lista de saída expandida confirma que a saída
desse operador é a coluna NameStyle.
A propriedade Procurar predicados mostra o seguinte:

Chaves de busca[1]: Prefixo: [AdventureWorks2014].[Pessoa].[Pessoa].


BusinessEntityID = Operador Escalar([AdventureWorks2014].[Pessoa].[Pessoa].
[BusinessEntityID] como [p].[BusinessEntityID])

Se observarmos os valores de Estimated e Actual Number of rows, veremos que é 1 linha, em


cada caso, portanto, o operador Key Lookup foi executado apenas uma vez.

Figura 3-9: Propriedades comparando o número estimado de linhas e o número de execuções.

93
Machine Translated by Google

Capítulo 3: Operadores de leitura de dados

Uma pesquisa de chave, dependendo do número de linhas retornadas, pode ser uma indicação de que o desempenho
da consulta pode se beneficiar de um índice de cobertura, embora nunca seja uma boa ideia criar um índice de cobertura
para cada consulta que usa uma pesquisa, porque isso resultar em um crescimento desenfreado de índices pouco usados.
Um Key Lookup se torna caro somente quando é executado muitas vezes, porque cada lookup é um Clustered Index
Seek que fará com que várias leituras lógicas (geralmente três), percorram a estrutura b+tree até a página que contém os
dados.

Se uma pesquisa de chave parecer problemática, é um bom hábito verificar se todas as colunas retornadas são
necessárias para o aplicativo consumidor. Se estiverem, tente cobrir a consulta estendendo um índice existente, em vez de
criar um novo.

Um índice de cobertura é criado tendo todas as colunas necessárias como parte da chave do índice ou usando a operação
INCLUDE para armazenar colunas extras no nível folha do índice para que estejam disponíveis para uso com o índice.

Lendo uma pilha


Um heap é uma tabela sem um índice clusterizado e, portanto, as linhas não são armazenadas em nenhuma ordem (além
da "ordem de chegada"). Podemos adicionar índices não clusterizados a um heap. Nesse caso, o índice não clusterizado
tem o local, o identificador de linha, onde a linha é armazenada no heap em vez do valor da chave clusterizada.

Existem apenas duas maneiras de o SQL Server ler dados de um heap: por meio de uma verificação ou de uma pesquisa.

Varredura de Tabela

As varreduras de tabela ocorrem apenas em tabelas heap, então vamos experimentar agora algumas consultas em tabelas
sem um índice clusterizado.

94
Machine Translated by Google

Capítulo 3: Operadores de leitura de dados

SELECT dl.DatabaseUser,
dl.PostTime,
dl.Evento,
dl.DatabaseLogID
A PARTIR DE dbo.DatabaseLog AS dl;

Listagem 3-6

Essa consulta resulta no plano de execução exibido na Figura 3-10.

Figura 3-10: Plano de execução com operador Table Scan.

Não há nada de novo no operador SELECT , então podemos ir direto para o outro operador deste plano, Table Scan. Ao ler
um índice, o operador equivalente é um Clustered Index Scan.

Uma Verificação de Tabela pode ocorrer por vários motivos, mas geralmente é porque não há índices não
clusterizados úteis na tabela, e o otimizador de consulta precisa pesquisar em cada linha para identificar as linhas a serem
retornadas. Outra causa comum de um Table Scan é uma consulta que solicita todas as linhas de uma tabela, como é o caso
deste exemplo.

Quando todas, ou a maioria, das linhas de uma tabela são retornadas, independentemente de existir um índice ou não,
geralmente é mais rápido varrer cada linha e devolvê-las do que procurar cada linha em um índice. Por último, às vezes,
especialmente para uma tabela com poucas linhas, a varredura da tabela é mais rápida, mesmo quando pode haver um índice
seletivo.

Se o número de linhas em uma tabela for relativamente pequeno, as Varreduras de Tabela geralmente não são um problema.
Por outro lado, se a tabela for grande e forem processadas muito mais linhas do que o necessário para a consulta, convém
investigar maneiras de reescrever a consulta para ler menos linhas ou adicionar um índice apropriado para acelerar o
desempenho.

95
Machine Translated by Google

Capítulo 3: Operadores de leitura de dados

Pesquisa RID

Podemos colocar critérios de filtro em uma consulta que pode resultar em uma Pesquisa RID como na Listagem 3-7.

SELECT dl.DatabaseUser,
dl.PostTime,
dl.Event,
dl.DatabaseLogID DE
dbo.DatabaseLog AS dl ONDE= 1;
dl.DatabaseLogID

Listagem 3-7

Essa consulta resulta em um plano de execução diferente do anterior.

Figura 3-11: Plano de execução mostrando um operador RID Lookup.

Temos um operador Index Seek e um operador RID Lookup (Heap) e um operador Nested Loops combinando os
dois fluxos.

96
Machine Translated by Google

Capítulo 3: Operadores de leitura de dados

RID Lookup é o equivalente de heap da operação Key Lookup . Como mencionado anteriormente, índices
não clusterizados nem sempre possuem todos os dados necessários para satisfazer uma consulta. Quando
não, uma operação adicional é necessária para obter esses dados. Quando há um índice clusterizado na
tabela, ele usa um operador Key Lookup conforme descrito acima. Quando não há índice clusterizado, a
tabela é um heap e deve pesquisar dados usando um identificador interno conhecido como Row ID
ou RID.

Para retornar os resultados dessa consulta, o otimizador de consulta primeiro executa uma busca de
índice na chave primária. Embora esse índice seja útil para identificar as linhas que atendem aos critérios
da cláusula WHERE, todas as colunas de dados necessárias não estão presentes no índice. Como nós
sabemos disso? Nas Propriedades para a Busca de Índice, vemos o valor Bmk1000 na Lista de Saída.

Figura 3-12: Lista de saída nas propriedades do Index Seek.

Este "Bmk1000" é uma coluna adicional, não referenciada na consulta. É o RID, ou seja, a localização
da linha no heap, e será usado no operador Nested Loops para unir os dados da operação RID Lookup .
O prefixo Bmk é uma reminiscência de quando esses tipos de operações de pesquisa eram chamados de
"Pesquisas de favoritos".

Se observarmos os Predicados Seek do operador RID Lookup , conforme mostrado na Figura 3-13, você
verá que o valor Bmk1000 é usado novamente:

Figura 3-13: Predicados de busca definidos nas propriedades do operador Index Seek.

Bmk1000 é o valor da chave, que é um identificador de linha ou RID, do índice não clusterizado. Nesse
caso, o SQL Server teve que pesquisar apenas uma linha, o que não é grande coisa do ponto de vista do
desempenho. Se uma pesquisa RID retornar muitas linhas, no entanto, talvez seja necessário examinar
atentamente a consulta para ver como você pode melhorar seu desempenho usando menos E/S de disco –
talvez reescrevendo a consulta, adicionando um índice clusterizado , ou usando um índice de cobertura.

97
Machine Translated by Google

Capítulo 3: Operadores de leitura de dados

Resumo
Este capítulo explicou todos os vários mecanismos envolvidos na leitura de dados em planos de
execução usando varreduras, buscas e pesquisas em relação a índices e varreduras e pesquisas RID
em tabelas de heap. Um operador de varredura em um plano não é necessariamente uma coisa ruim,
nem uma busca necessariamente ideal. Você precisa ler as propriedades dos operadores nos planos
de execução para entender o que cada operador está fazendo, quantas linhas ele processou, quantas
linhas ele retornou, como o mecanismo de filtragem funcionou e assim por diante. Este será um tema
comum em todo o resto do livro.

98
Machine Translated by Google

Capítulo 4: Unindo Dados


No capítulo anterior, mantivemos as coisas simples e nos apegamos às consultas de tabela única. No entanto, em qualquer
banco de dados real, a maioria dos planos de execução que você já viu terá pelo menos um operador de junção. Afinal, o
que é um banco de dados relacional sem as junções entre tabelas? O SQL Server é um mecanismo de banco de dados
relacional, o que significa que parte da operação projetada é combinar dados de diferentes tabelas em conjuntos de dados
únicos. O plano de execução expõe os operadores que o otimizador usa para combinar dados.

Este capítulo trata principalmente de várias operações de junção lógica em T-SQL. Ao implementar a junção, o SQL Server
pegará as duas entradas de dados, geralmente uma de cada tabela, e combinará os dados de acordo com os critérios de
junção. O otimizador pode optar por implementar a junção usando um dos quatro operadores de junção física:

• Loops aninhados – Para cada linha no conjunto de dados superior, realize uma pesquisa na outra
conjunto de dados para valores correspondentes.

• Correspondência de hash – Usando cada linha no conjunto de dados superior, crie uma tabela de hash, que será
então testada usando as linhas do segundo conjunto de dados para encontrar qualquer valor correspondente.

• Merge Join – Leia dados de ambas as entradas simultaneamente e mescle as duas


entradas, juntando cada valor de linha correspondente. Isso requer que ambas as entradas sejam classificadas
na(s) coluna(s) de junção.

• Adaptive Join – Introduzido no SQL Server 2017, este operador implementa ambos
os algoritmos Nested Loops e Hash Match, e escolhe a opção com o menor custo em tempo de execução,
quando o número real de linhas na entrada superior é conhecido.

Conforme discutiremos, o operador de junção física escolhido pelo otimizador dependerá do tamanho dos dois fluxos de
dados de entrada e de como eles são ordenados.

Tendo abordado isso, consideraremos brevemente outras tarefas que o otimizador pode realizar usando operadores JOIN,
bem como outras maneiras de combinar dados, como por meio do comando UNION T-SQL, e como o SQL Server
implementa essas operações.

99
Machine Translated by Google

Capítulo 4: Unindo Dados

Operações de junção lógica

Os operadores de junção acima implementam oito operações de junção lógica e duas operações que combinam
dados de uma forma que não é realmente considerada uma junção, como segue:
• Junção interna

• União Externa (Esquerda, Direita ou Completa)

• Semi Junção (Esquerda ou Direita)


• Anti Semi Junção (Esquerda ou Direita)
• Concatenação e União.

As duas primeiras podem ser especificadas diretamente no T-SQL, enquanto as Semi Joins são a operação lógica
associada a EXISTS (ou NOT EXISTS) e IN, e Concatenation e Union
estão associados a UNION ALL e UNION.

O otimizador escolherá o que considera ser o operador físico de menor custo (Nested Loops, Hash Match ou
Merge Join) para implementar as condições de junção lógica descritas na instrução T-SQL.

Cumprindo Comandos JOIN


Esta seção trata explicitamente de como o otimizador usa operadores de junção para cumprir os comandos T-
SQL JOIN.

Vamos começar com a consulta na Listagem 4-1, que recupera as informações do funcionário do banco de
dados AdventureWorks2014, concatenando o FirstName e o LastName
colunas para retornar as informações de maneira mais agradável.

SELECT e.JobTitle, a.City,


p.LastName + ', ' + p.FirstName AS EmployeeName
FROM HumanResources.Employee AS e
INNER JOIN Pessoa.BusinessEntityAddress AS bea
ON e.BusinessEntityID = bea.BusinessEntityID
INNER JOIN Pessoa.Endereço AS a
ON bea.AddressID = a.AddressID
INNER JOIN Pessoa.Pessoa AS p
ON e.BusinessEntityID = p.BusinessEntityID;

Listagem 4-1

100
Machine Translated by Google

Capítulo 4: Unindo Dados

A Figura 4-1 mostra o plano de execução completo e real para esta consulta.

Figura 4-1: Plano de execução mostrando duas junções de loops aninhados.

Este plano tem mais operadores do que qualquer outro que vimos até agora, mas, como em todos os planos,
podemos lê-lo começando no canto superior direito e seguindo as setas de dados para a esquerda, ou lendo da
esquerda para a direita, seguindo a ordem em que os operadores são chamados.

Se estivéssemos tentando ajustar essa consulta, poderíamos ser tentados a simplesmente entrar e olhar para os
operadores com o maior custo estimado, ou seja, o Clustered Index Seek contra a tabela Person.Person (27%), ou
o Index Scan na tabela Tabela Person.Address (48%) ou o operador de junção Hash Match (16%).

No entanto, uma abordagem melhor é primeiro levar algum tempo para entender amplamente o que o plano faz.
Lendo da direita para a esquerda, ele primeiro une as linhas correspondentes nas tabelas Employee e Busines
sEntityAddress usando um operador Nested Loops e, em seguida, usa um operador Hash Match para unir linhas
nesse fluxo de dados com linhas na tabela Address, com base nos valores de AddressID correspondentes, e, em
seguida, usa outro operador de loops aninhados para unir essas linhas com linhas correspondentes na tabela
Person (em BusinessEntityID). Por fim, ele adiciona um valor escalar calculado a cada linha e o retorna.

Vamos nos concentrar na função de cada um dos operadores de junção, dentro do contexto do plano como um
todo, então vamos começar no canto superior direito do plano e dar uma olhada mais detalhada no primeiro
operador de junção de loops aninhados .

101
Machine Translated by Google

Capítulo 4: Unindo Dados

Operador de loops aninhados

Um operador de loops aninhados , geralmente chamado de iteração aninhada , pega um conjunto de


dados, chamado de "entrada externa", e o compara, uma linha por vez, com outro conjunto de dados,
chamado de "entrada interna" (no plano gráfico, eles correspondem aos dois pipes que alimentam o
operador Nested Loops : a entrada externa na parte superior e a entrada interna na parte inferior). Isso
soa muito como um cursor e, na verdade, é um. Na verdade, neste caso, são dois cursores. O primeiro
cursor é o conjunto de dados de entrada externo. Ele será processado uma linha por vez.
O segundo cursor é a entrada interna, que será processada uma linha por vez para cada linha da entrada
externa. Como resultado, o operador (ou operadores) na entrada interna, a ramificação inferior no plano
gráfico, será executado várias vezes, uma vez para cada linha encontrada na entrada externa.

Um operador de loops aninhados pode ser altamente eficiente, desde que a entrada externa seja pequena
e seja barato pesquisar a entrada interna, que no caso de operações de junção simples geralmente é obtida
pela indexação da "tabela interna" na coluna de junção.

O plano de execução na Figura 4-1 tem dois operadores de junção de loops aninhados . Vamos
começar com uma vista explodida do canto superior direito da planta e dar uma olhada em uma delas
com mais detalhes.

Figura 4-2: Loops aninhados se unem em uma entrada interna e externa.

102
Machine Translated by Google

Capítulo 4: Unindo Dados

Na Figura 4-2, uma iteração de loops aninhados orienta a junção de linhas correspondentes
na tabela Employee e BusinessEntityAddress. Observe que, neste exemplo, uma Inner Join é a
operação lógica associada a esse operador físico.

A entrada externa para este operador Nested Loops são os dados produzidos por uma varredura do índice
clusterizado na tabela Employee. Ele verifica todo o índice, reajustando cada linha (290 linhas, neste caso).
Para cada uma dessas linhas, o operador Nested Loops chama o operador na entrada interna, procurando
linhas na tabela BusinessEntityAddress com um valor Busi nessEntityID correspondente. Nesse caso, isso
significa que ele executa 290 operações de busca de índice no índice clusterizado. A Figura 4-3 mostra as
propriedades do operador Nested Loops .

Figura 4-3: Página de propriedades do operador Nested Loops.

Como acontece com a maioria dos operadores, há um conjunto comum de propriedades em exibição,
algumas das quais não se aplicam e algumas são mais úteis que outras. As subseções a seguir revisam
algumas das propriedades que são de interesse neste caso.

103
Machine Translated by Google

Capítulo 4: Unindo Dados

Propriedades do número estimado e real de linhas


Muitas vezes, é interessante comparar o Número Real de Linhas, 290, com o Número Estimado de
Linhas, 275,573 (provando que isso é um cálculo, já que você não pode retornar 0,573 linhas).

Não vale a pena se preocupar com uma diferença tão pequena, mas uma discrepância maior pode ser
uma indicação de que o otimizador usou estimativas imprecisas do número de linhas que precisarão ser
processadas ao selecionar o plano, o que pode resultar em uma escolha de plano abaixo do ideal.
Existem muitas causas possíveis para isso. Por exemplo, talvez o otimizador tenha que gerar um plano para
uma consulta contendo um Predicado em uma coluna com estatísticas ausentes ou obsoletas, ou o otimizador
pode ter reutilizado um plano em que o volume ou distribuição de dados em uma coluna mudou
significativamente desde que as estatísticas foram criado ou atualizado pela última vez. Como alternativa, a
distribuição de dados em uma coluna pode ser muito não uniforme, dificultando as estimativas precisas de
cardinalidade, ou a consulta pode conter uma lógica que anula as estimativas precisas. A detecção de
parâmetros pode ter ocorrido, resultando em um plano gerado para um valor de parâmetro de entrada com
uma contagem de linhas estimada que é atípica das contagens de linhas para valores de entrada subsequentes.
O Capítulo 8 discute o sniffing de parâmetros com algum detalhe.

Há outro operador de loops aninhados na Figura 4-1, que pega as 290 linhas da junção Hash Match
(discutida em breve) como a entrada externa e, portanto, executa 290 operações de busca separadas do
índice clusterizado na tabela Person interna, juntando correspondência linhas dessa tabela. Como o Índice
Agrupado Busca na Pessoa é estimado como a operação mais cara do plano, vale a pena dar uma olhada
em suas propriedades (veja a Figura 4-4).

Novamente, a primeira coisa é verificar se não há disparidade selvagem entre o número estimado e o número
real de linhas processadas. Inicialmente, parece que pode haver, já que o número estimado de linhas é
apenas 1, mas o número real de linhas é 290. No entanto, o SSMS é inconsistente na forma como relata
esses números; a contagem de linhas estimada é por execução, e o otimizador estimou que essa Busca de
Índice Agrupado será executada 275,573 vezes, para uma estimativa de 275,573 linhas retornadas. A
contagem real de linhas é simplesmente o número total de linhas processadas, que é 290 (uma média de 1
linha retornada por execução).

104
Machine Translated by Google

Capítulo 4: Unindo Dados

Figura 4-4: Operador de loops aninhados mostrando estatísticas de tempo de execução.

O fato de o otimizador estimar que executará essa Busca de Índice Agrupado na tabela Pessoas cerca de 257 vezes
explica, pelo menos em parte, por que ele é o operador de maior custo no plano. Estima -se que a busca de índice
clusterizado na tabela BusinessEntityAddress seja executada com ainda mais frequência, 290 vezes, mas como essa tabela
usa muito menos bytes por linha, ela tem um nível a menos de páginas de índice, reduzindo a quantidade de trabalho por
busca de três para dois leituras lógicas.

Dedicar um tempo para entender como as operações interagem permitirá que você entenda por que os custos são distribuídos
da maneira como são.

105
Machine Translated by Google

Capítulo 4: Unindo Dados

Propriedade de referências externas

Há duas maneiras de o operador Nested Loops resolver uma condição de junção. Uma maneira é através
da propriedade Outer References . Nesse caso, os operadores na entrada interna da junção, a ramificação
inferior em um plano gráfico, usam valores da entrada externa para entregar os resultados.
Se dez valores forem empurrados da entrada externa para a entrada interna, referida como Referências
externas, isso implica que a entrada interna será executada dez vezes, procurando linhas correspondentes.
A entrada interna retornará apenas linhas correspondentes e, portanto, o operador de loops aninhados não
precisa fazer nenhum trabalho em termos de validação de dados correspondentes.

Você pode ver a propriedade Outer References na dica de ferramenta ou na página de propriedades do
operador Nested Loops , conforme mostrado na Figura 4-5.

Figura 4-5: Detalhes de referências externas da junção de loops aninhados.

Você pode ver que, nesse caso, os valores da coluna BusinessEntityID estão sendo enviados para a
entrada interna. A coluna BusinessEntityID é a coluna principal de um índice utilizável em
BusinessEntityAddress, portanto, ao enviá-la para a entrada interna, ela facilita uma operação de busca
(consulte a Figura 4-2).

Aliás, o outro valor enviado, Expr1008, não tem outra referência em nenhum lugar no plano de execução,
mesmo se você pesquisar o XML. Portanto, é provável que seja um fato artístico do processo de comparação
no operador Clustered Index Seek .

106
Machine Translated by Google

Capítulo 4: Unindo Dados

A segunda maneira que o operador Nested Loops pode resolver uma condição de junção é por meio
da propriedade Predicate . Isso acontece quando a entrada interna não tem valores empurrados, então
ela sempre retornará os mesmos resultados em todas as execuções subsequentes. Aqui, os loops aninhados
O operador aplica o predicado de junção às linhas retornadas da entrada interna e passa apenas as linhas
correspondentes. Veremos um exemplo disso no Capítulo 5.

Propriedades de Religar e Rebobinar


Tanto um Rebind quanto um Rewind contam o número de vezes que o método Init() é chamado por um
operador, mas o fazem em circunstâncias diferentes. O método Init() inicializa o operador e configura todas
as estruturas de dados necessárias. Na maioria dos casos, isso acontece uma vez para uma operadora, em
qualquer plano. No entanto, um operador de loops aninhados executa sua entrada interna uma vez para
cada linha na entrada externa. Isso significa que o método Init() nos operadores na entrada interna pode ser
chamado mais de uma vez.

Cada execução é um Rebind ou um Rewind. Um Rebind ocorre para a primeira execução da


entrada interna e, em seguida, cada vez que os valores da coluna empurrados para baixo da entrada
externa mudam (ou seja, quando os valores marcados por Referências externas mudam).

Um retrocesso ocorre quando os valores não são alterados ou quando não há referências externas
(portanto, a condição de junção é resolvida usando um predicado, dentro do operador de loops aninhados ).
No último caso, você sempre verá um único Rebind para a primeira execução e, a partir desse ponto, uma
série de Rewinds.

Para o operador de loops aninhados representado na Figura 4-5, a junção é resolvida usando valores na
coluna BusinessEntityID como referências externas e há 290 valores distintos para essa coluna (é a chave
primária). Nocionalmente, isso significa que todas as 290 execuções da entrada interna são Rebinds.

No entanto, a Figura 4-6 mostra as propriedades do Clustered Index Seek, que é a entrada interna do
operador Nested Loops , e podemos ver que Rebinds e Rewinds são zero em cada caso.

107
Machine Translated by Google

Capítulo 4: Unindo Dados

Figura 4-6: Propriedades da Busca de Índice Agrupado.

É claro que saber se o valor da entrada externa foi alterado só é útil para o otimizador se os resultados da execução anterior da
entrada interna, para o mesmo valor, estiverem armazenados em algum lugar. Por exemplo, os operadores de Spool salvam seus
resultados em uma tabela de trabalho, um Sort salva-os na memória e uma Table Valued Function preenche uma variável de
tabela. Quando esses operadores estão presentes, o otimizador pode agilizar o processo de execução porque, se souber que tem
as linhas de que precisa armazenadas em algum lugar, quando ocorre um Rewind , não há necessidade de refazer todo o trabalho
para produzi-las novamente.

Portanto, Rebinds e Rewinds só são relevantes, e os valores das propriedades só são preenchidos, quando o operador Nested
Loops interage com um dos seguintes operadores, cada um dos quais pode salvar os resultados de sua execução anterior:

• Carretel de Índice

• Consulta Remota

• Carretel de contagem de linhas


• Classificar

• Carretel de Mesa
• Função com valor de tabela.

108
Machine Translated by Google

Capítulo 4: Unindo Dados

Nós não descreveremos nenhum dos operadores listados acima até o Capítulo 5, então não vamos
passar por um exemplo aqui. No entanto, digamos que a entrada externa de uma junção de loops
aninhados produz 14 linhas, a condição de junção é resolvida usando referências externas e há 10 valores
distintos na coluna referências externas. A entrada interna é um Index Spool, cujas propriedades mostram
que as 14 execuções dessa entrada interna compreendem 10 Rebinds e 4 Rewinds .

Figura 4-7: Religa e rebobina para um spool de índice.

Para cada Rewind, não há necessidade de executar nenhum operador downstream (à direita) do spool,
pois os valores correspondentes já estão armazenados na tabela de trabalho do spool. Isso significa que
cada um desses operadores executa apenas 10 vezes, uma vez para cada Rebind da entrada interna.

Correspondência de Hash (juntar)

O otimizador pode usar um operador Hash Match para implementar qualquer uma das operações lógicas
de JOIN, embora só possa usá-lo para implementar um UNION nos casos em que a entrada do teste não
tenha duplicatas e não seja usada para concatenação (UNION ALL), que é feito pelo operador
Concatenate . Uma Hash Match também pode agregar dados de uma única entrada de dados, mas
vamos nos concentrar exclusivamente em implementações de junção aqui, abordando a agregação no
Capítulo 5.

109
Machine Translated by Google

Capítulo 4: Unindo Dados

Quando usado para implementar operações de junção lógica, o operador Hash Match faz uma única passagem por duas
entradas de dados. Uma entrada de dados (a "construção") é armazenada na memória, em uma chamada tabela de hash, e
então essa estrutura é usada para comparar dados sondando ou comparando a partir de outra entrada de dados, para chegar
ao conjunto de saída correspondente .

Como funcionam as junções do Hash Match

A Figura 4-8 mostra uma vista explodida da seção do plano para a Listagem 4-1 que contém um Hash Match, neste caso
usado para implementar uma junção interna.

Figura 4-8: Junção de correspondência de hash mostrando duas entradas.

Em um operador de junção Hash Match , a entrada superior é chamada de entrada Build e a entrada inferior é chamada
de entrada Probe . Neste exemplo, a entrada Build são as 290 linhas produzidas pelo primeiro operador Nested Loops no
plano, discutido acima. Esta é de longe a menor das duas entradas.

O operador Hash Match lê a entrada Build , faz o hash da coluna de junção (neste caso AddressID) e armazena os
valores da coluna e seus hashes em uma tabela de hash na memória.
Em seguida, ele lê as linhas na entrada do Probe uma linha por vez, neste caso as 19614 linhas que resultam de uma Varredura
de Índice Não Clusterizado na tabela Endereço. Para cada linha, ele produz um valor de hash para a coluna AddressID que pode
ser comparado aos hashes na tabela de hash, procurando valores correspondentes.

110
Machine Translated by Google

Capítulo 4: Unindo Dados

Tabelas de hash e hash


Hashing é uma técnica programática em que os dados são convertidos em um número simples para tornar a busca por esses
dados muito mais eficiente. Por exemplo, o SQL Server converte uma linha de dados em uma tabela em um valor derivado das
colunas dessa linha que são designadas como entrada para a função de hash.

Uma tabela de hash é uma estrutura de dados na qual o SQL Server tenta dividir todos os elementos em categorias de
tamanhos iguais, ou buckets, para permitir acesso rápido aos elementos. A função de hash determina em qual bucket um
elemento vai. Por exemplo, o SQL Server pode pegar uma coluna de uma tabela, transformá-la em um valor de hash e
armazenar as linhas correspondentes na memória, dentro da tabela de hash, no bucket apropriado.

A Figura 4-9 mostra as propriedades Hash Keys Build e Hash Keys Probe para o operador de junção Hash Match .
Essas propriedades revelam quais colunas de cada entrada são hash pelo operador, ao construir a tabela de hash e comparar
as linhas da entrada do Probe .

Figura 4-9: Hash Keys Build e Hash Keys Probe valores.

Considerações de desempenho para junções de correspondência de hash

Um operador de junção de correspondência de hash está bloqueando durante a fase de compilação. Ele precisa reunir todos
os dados para construir uma tabela de hash antes de realizar suas operações de junção e produzir a saída.
O otimizador tenderá a escolher junções de correspondência de hash apenas nos casos em que as entradas não forem
classificadas de acordo com a coluna de junção. As junções de correspondência de hash podem ser eficientes nos casos em que há

111
Machine Translated by Google

Capítulo 4: Unindo Dados

não há índices utilizáveis ou onde partes significativas do índice serão verificadas. Se as entradas já estiverem
classificadas na coluna de junção ou forem pequenas e baratas para classificar, o otimizador poderá optar por usar
uma junção de mesclagem .

No entanto, uma junção de correspondência de hash geralmente é a melhor opção quando você tem duas
entradas não classificadas, ambas grandes ou uma pequena e uma grande. O otimizador sempre escolherá o que
estima ser a menor das entradas de dados para ser a entrada Build, que fornece os valores na tabela de hash. O
objetivo é muitos baldes de hash com poucas linhas por balde (ou seja, colisões de hash mínimas, o menor número
possível de valores de hash duplicados). Isso faz com que encontrar linhas correspondentes no Probe
entrada rápida, mesmo com duas entradas grandes, porque o otimizador só precisa procurar correspondências na
cesta com o mesmo valor de hash, em vez de varrer todas as linhas.

Os problemas de desempenho com o Hash Match só ocorrem realmente quando a entrada Build é muito maior
do que o otimizador antecipou, de modo que excede a concessão de memória e, posteriormente, transborda para o
disco.

Assim, dado que a seção do nosso plano, na Figura 4-6, contém o que o otimizador considera o segundo e o
terceiro operadores mais caros do plano, no Index Scan na tabela Address, e o Hash Match se junta, deve
tentamos "sintonizar" essas operações? Às vezes, você pode. Embora uma junção de correspondência de
hash possa representar a maneira atual e mais eficiente para o otimizador de consulta unir duas tabelas, é
possível que possamos ajustar nossa consulta para disponibilizar ao otimizador técnicas de junção mais eficientes,
como o uso de loops aninhados ou junção de mesclagem operadores. Por exemplo, ver uma combinação de
hash em um plano de execução às vezes indica:

• um índice ausente ou inutilizável


• uma cláusula WHERE com um cálculo ou conversão que o torna não-SARGable
(um termo comumente usado que significa que o argumento de pesquisa, "sarg," não pode ser usado);
isso significa que ele não usará um índice existente.

No entanto, depende simplesmente do que está acontecendo na consulta. Geralmente, você não ajusta
operadores individuais; você os usa para entender o plano de execução. Algumas operadoras caras podem ser
direcionadas, outras são estimadas como caras, mas na verdade não são, e algumas são realmente caras, mas
ainda são um elemento essencial do plano mais barato em geral. Uma junção de correspondência de hash
geralmente se enquadra na última categoria, pois as alternativas são loops aninhados com muitas execuções da
entrada interna ou usando classificações para habilitar uma junção de mesclagem (coberto posteriormente). Neste
caso, sem cláusula WHERE, o Hash Match é simplesmente um mecanismo eficiente para juntar todos os dados para
satisfazer a consulta em questão.

112
Machine Translated by Google

Capítulo 4: Unindo Dados

Calcular escalar

À medida que cada linha emerge do segundo operador Nested Loops , em nosso plano na Figura 4-1,
ela passa para um operador Compute Scalar . Este não é um tipo de operação de junção, mas como
aparece em nosso plano, vamos cobri-lo aqui.

Figura 4-10: Operador Compute Scalar.

A Figura 4-11 mostra a janela Propriedades para este operador.

Figura 4-11: Propriedades do operador Compute Scalar.

113
Machine Translated by Google

Capítulo 4: Unindo Dados

Isso é simplesmente uma representação de uma operação para produzir um ou mais valores escalares
simples, geralmente a partir de um cálculo – neste caso, o alias EmployeeName, que combina as
colunas Contact.LastName e Contact.FirstName com uma vírgula entre elas. Embora esta não tenha
sido uma operação de custo zero, 0,000027, a estimativa de custo é tão trivial no contexto da consulta
que é essencialmente gratuita. Você pode ver o que esta operação está fazendo observando a definição
da propriedade destacada, Valores Definidos, mas para realmente ver o que a operação está fazendo,
clique nas reticências no lado direito da página de propriedades. Isso abrirá a definição da expressão
conforme mostrado na Figura 4-12.

Figura 4-12: Valores definidos do operador Compute Scalar.

Embora o operador Compute Scalar neste caso seja muito direto e claro, isso nem sempre será
o caso. Essas operações não são totalmente custeadas pelo otimizador, portanto, você pode ver
situações em que as estimativas para o trabalho envolvido estão radicalmente erradas. O valor é
calculado como 0,0000001 * (Número estimado de linhas), independentemente da complexidade ou do
número de cálculos que estão sendo feitos. Além disso, a representação lógica de onde o Compute
Scalar ocorre dentro do plano é representada aqui; não é necessariamente onde o processo físico ocorre
dentro do plano. É por isso que às vezes você não vê valores para o número real de linhas ou execuções
reais em um operador Compute Scalar , em um plano de execução real; se todos os cálculos forem
processados em outro lugar, o operador não será executado e, portanto, não poderá rastrear esses
números.

Devido à falta de custos estimados precisos, você deve entender exatamente o que uma
operação Compute Scalar representa em seu plano de execução, pois ela pode representar um custo
oculto, especialmente quando funções escalares definidas pelo usuário (UDFs) estão envolvidas.

114
Machine Translated by Google

Capítulo 4: Unindo Dados

Mesclar associação

Um operador Merge Join funciona apenas a partir de dados ordenados. Ele pega os dados de duas entradas e usa
o fato de que os dados em cada entrada são ordenados na coluna de junção para simplesmente mesclar as duas
entradas, unindo linhas com base nos valores correspondentes, o que pode ser feito com muita facilidade porque a
ordem dos valores será idêntico. Um Merge Join é um operador sem bloqueio; à medida que une cada linha, com
valores correspondentes na coluna de junção, ele a passa para o próximo operador upstream.

Se cada entrada de dados for ordenada pela coluna de junção, esta pode ser uma das operações de junção mais
eficientes. No entanto, os dados frequentemente não são ordenados e, portanto, classificá-los para um Merge Join
requer a adição de um operador Sort para garantir que funcione; o requisito de classificação pode tornar os planos
com uma operação Merge Join menos eficientes, dependendo de como a classificação é satisfeita.

No entanto, como uma junção de mesclagem garante que a saída do próprio processo de junção também seja
ordenada, às vezes pode ser melhor pagar o custo de uma única operação de classificação para garantir a saída
ordenada para operações adicionais de junção de mesclagem em um plano.

Como funcionam as associações de mesclagem

Para demonstrar um operador Merge Join , precisamos de uma nova consulta.

SELECT c.CustomerID
FROM Sales.SalesOrderDetail AS sod
INNER JOIN Sales.SalesOrderHeader AS soh
ON sod.SalesOrderID = soh.SalesOrderID
INNER JOIN Vendas.Cliente AS c
ON soh.CustomerID = c.CustomerID;

Listagem 4-2

A Figura 4-13 mostra o plano de execução para esta consulta.

115
Machine Translated by Google

Capítulo 4: Unindo Dados

Figura 4-13: Plano de execução mostrando um Merge Join.

Aqui, o otimizador selecionou um operador Merge Join para realizar o INNER JOIN
entre as tabelas Customer e SalesOrderHeader, com base nos valores correspondentes de CustomerID.
Como a consulta não especificou uma cláusula WHERE, uma varredura foi executada em cada tabela
para retornar todas as linhas em cada tabela. Além disso, você notará que a ordem das operações de
junção não é a mesma especificada pela consulta. O otimizador pode optar por reorganizar a ordem das
tabelas dentro do plano como achar melhor, para chegar ao melhor plano possível. Aqui, a entrada com
valores exclusivos garantidos, a tabela Customer, é usada como a entrada principal, portanto, temos uma
junção um-para-muitos.

Os dados na entrada superior, o Clustered Index Scan na tabela Customer, são ordenados por CustomerID.
A entrada inferior são os dados de uma varredura de índice não clusterizado na tabela Sale sOrderHeader.
Novamente, esse índice não clusterizado é ordenado por CustomerID. Em outras palavras, ambas as entradas
de dados são ordenadas na coluna de junção, conforme confirmado, nas Propriedades do operador Merge
Join .

Figura 4-14: Propriedades do Merge Join mostrando os valores da propriedade Where.

Uma vez que o Merge Join uniu duas das tabelas, o otimizador une a terceira tabela às duas primeiras
usando uma junção Hash Match , conforme discutido anteriormente. Finalmente, as linhas unidas são retornadas.

116
Machine Translated by Google

Capítulo 4: Unindo Dados

Considerações de desempenho para junções de mesclagem

A chave para o desempenho de uma junção de mesclagem é que as entradas são classificadas pelas colunas de junção.
Podemos ver que os resultados das varreduras são classificados se consultarmos as propriedades desses
operadores. A Figura 4-13 mostra o operador Clustered Index Scan , com um valor de propriedade Ordered de True,
o que significa que o otimizador requer que a entrada seja ordenada.

Figura 4-15: Propriedades de varredura mostrando o valor ordenado.

Se você vir uma propriedade Ordered definida como False, isso não significa que os dados que estão sendo
recuperados não sejam, de fato, ordenados; significa apenas que o otimizador não exige que os dados sejam
ordenados para satisfazer o restante do plano.

Portanto, neste exemplo, a saída das varreduras é ordenada pelas colunas de junção e nenhuma classificação adicional
é necessária. Se uma ou mais entradas não estiverem ordenadas e o otimizador de consulta optar por classificar os
dados em uma operação separada antes de executar uma junção de mesclagem, isso pode indicar que você precisa
reconsiderar sua estratégia de indexação, especialmente se a operação de classificação for para uma grande entrada
de dados. Você poderia, por exemplo, modificar um índice existente para que o otimizador possa evitar a necessidade
da operação de classificação ?

A junção de mesclagem neste exemplo é para uma junção de um para muitos, como podemos ver inspecionando o
valor da propriedade Muitos para muitos para o operador, que é False.

117
Machine Translated by Google

Capítulo 4: Unindo Dados

Figura 4-16: Propriedades da junção de mesclagem mostrando o valor de muitos para muitos.

No entanto, uma junção de mesclagem para uma condição de junção de muitos para muitos pode ser muito mais
cara e o desempenho muito pior. Considere o exemplo na Listagem 4-3.

SET STATISTICS IO ON;


SELECT sod.ProductID,
sod.SalesOrderID,
pv.BusinessEntityID,
pv.Preço Padrão
FROM Sales.SalesOrderDetail AS sod
INNER JOIN Compras.ProdutoVendor AS pv
SOBRE
pv.IDProduto = sod.IDProduto;
DEFINIR ESTATÍSTICAS IO OFF;

Listagem 4-3

A Figura 4-17 mostra o plano de execução para esta consulta.

118
Machine Translated by Google

Capítulo 4: Unindo Dados

Figura 4-17: Plano de execução com uma junção de mesclagem de muitos para muitos.

O otimizador tenta inferir exclusividade nas colunas de junção de cada entrada, examinando os índices exclusivos,
bem como os elementos do plano, como um operador Distinto ou Agregado em uma ramificação do plano. Se uma
das entradas da junção fosse garantida como única, seria a entrada principal, Many to Many seria False e a junção
seria eficiente. No entanto, neste caso, ambas as entradas podem e têm várias linhas com o mesmo valor ProductID.
No Merge Join, Many to Many é verdadeiro, e o join se torna menos eficiente. Isso pode ser visto na saída SET
STATISTICS IO:

(74523 linhas afetadas)


Tabela 'Mesa de trabalho'. Contagem de varredura 19, leituras lógicas 18013, leituras
físicas 0, leituras antecipadas 0, leituras lógicas lob 0, leituras físicas lob 0, leituras
antecipadas lob 0.
Tabela 'ProdutoVendor'. Contagem de varredura 1, leituras lógicas 7, leituras físicas 1,
leituras antecipadas 8, leituras lógicas lob 0, leituras físicas lob 0, leituras antecipadas
lob 0.
Tabela 'Detalhe do PedidoDeVendas'. Contagem de varredura 1, leituras lógicas 250, leituras
físicas 0, leituras antecipadas 326, leituras lógicas lob 0, leituras físicas lob 0, leituras
antecipadas lob 0.

O problema é que, para uma junção de muitos para muitos , as linhas da entrada inferior devem ser copiadas para
uma tabela de trabalho em tempdb. Se uma nova linha da entrada superior tiver o mesmo valor na coluna de junção
que a anterior, a tabela temporária será usada para retroceder até o início das duplicatas conforme necessário na
comparação. Se os dados da entrada superior forem alterados, a tabela temporária será limpa e carregada com novas
linhas correspondentes na parte inferior. As estatísticas de E/S demonstram o impacto dessa atividade extra na tabela
temporária: o número de leituras lógicas é superior a 98% do número total de leituras lógicas da consulta como um todo.

Nesse caso, há duplicatas para ProductID em ambas as tabelas, portanto, pouco podemos fazer para alterar isso. No
entanto, não é incomum ver operadores Merge Join com muitos para muitos

119
Machine Translated by Google

Capítulo 4: Unindo Dados

definido como True onde poderia ter sido False. Isso geralmente está relacionado a restrições ausentes nas
tabelas ou à incorporação de colunas em expressões (como conversões de tipo de dados implícitas ou
explícitas). O otimizador só pode inferir a exclusividade corretamente se houver uma restrição de exclusividade
em uma coluna que não esteja incorporada em uma expressão.

Junção adaptável

Introduzido no SQL Server 2017 e também disponível no Banco de Dados SQL do Azure e no SQL Data
Warehouse do Azure, o Adaptive Join é uma nova operação de junção. Atualmente, ele só funciona com o
modo de lote (consulte o Capítulo 12), mas isso pode mudar à medida que as atualizações cumulativas são
lançadas ou em atualizações do Azure.

O otimizador pode escolher um operador Adaptive Join para adiar a escolha exata do algoritmo de junção
física, seja uma Hash Match ou um Nested Loops, até o tempo de execução, quando o número real de linhas
na entrada superior for conhecido em vez de estimado.

Para ver o Adaptive Join em ação, precisamos de um plano de modo de lote, que requer um índice de
armazenamento de colunas. A Listagem 4-4 cria um índice columnstore não clusterizado em Production.
Tabela de histórico de transações.

Depois de terminar de testar o exemplo nesta seção, retorne a esta listagem e execute o lote DROP INDEX para
remover o índice columnstore.

DROP INDEX SE EXISTE ix_csTest ON Production.TransactionHistory;


VAI
CRIAR ÍNDICE DE COLUMNSTORE NÃO CLUSTERED IX_CSTest
ON Production.TransactionHistory
(
ID da transação,
ID do produto,
Custo real
);

Listagem 4-4

120
Machine Translated by Google

Capítulo 4: Unindo Dados

Com esse índice em vigor, a execução da consulta simples na Listagem 4-5 (no SQL Server 2017 ou no Banco de
Dados SQL do Azure, com o nível de compatibilidade do banco de dados definido como pelo menos 140 em ambos os
casos) resultará em um Adaptive Join.

SELECT p.Name AS ProductName,


th.Custo Real
FROM Production.TransactionHistory AS th
JUNTE -SE à Produção.Produto AS p
ON p.ProductID = th.ProductID
ONDE th.Custo Real > 0
AND th.Custo Real < .21;

Listagem 4-5

A Figura 4-18 mostra o plano de execução real.

Figura 4-18: Plano de execução mostrando um Adaptive Join.

A primeira coisa que quero destacar sobre esse plano é o aviso que temos no SELECT
operador, que é um aviso de concessão de memória excessiva . Trataremos desse aviso no Capítulo 12.

A primeira coisa que você provavelmente notará sobre o operador Adaptive Join é que, ao contrário de todos os
outros operadores de junção que vimos até agora, ele tem três entradas. A entrada superior é uma varredura de um
índice columnstore não clusterizado (não abordaremos as especificidades dos planos que envolvem índices de
armazenamento de colunas até o Capítulo 12). As entradas inferiores, um Index Scan plus Filter e um Clustered Index
Seek são, respectivamente, os operadores para dar suporte a uma junção de correspondência de hash ou a uma
junção de loops aninhados .

121
Machine Translated by Google

Capítulo 4: Unindo Dados

Como um índice columnstore não tem estatísticas da mesma forma que um índice rowstore, nem sempre há
uma maneira fácil para o otimizador estimar com precisão o número de linhas retornadas.

Todas as operações necessárias para qualquer tipo de junção são definidas e armazenadas com o plano de execução
no momento da compilação. Se esse plano fosse recuperado do cache do plano ou do Repositório de Consultas, ele
mostraria as duas ramificações possíveis para dar suporte aos dois tipos de junção possíveis. Em suma, você não
pode dizer qual caminho foi tomado sem considerar as propriedades de um plano real. Qualquer plano estimado
mostrará ambas as ramificações possíveis.

Assim como para o operador de junção Hash Match , o operador Adaptive Join possui uma fase Build, que
armazena as linhas para a entrada superior em uma tabela de hash na memória, razão pela qual há uma
concessão de memória. O operador está bloqueando durante esta fase.

Depois que a entrada principal é processada e armazenada na tabela de hash, o número exato de linhas é
conhecido. Esse número agora é usado para decidir se deve prosseguir como uma junção de correspondência de
hash ou de loops aninhados . Essa determinação é feita comparando o número de valores na tabela de hash com
um limite determinado pelo otimizador. Para qualquer operação de junção, esse valor pode variar dependendo das
estruturas de dados, da consulta e das estatísticas nos índices. Você pode verificar o valor que está sendo usado
observando as propriedades do operador Adaptive Join .

Figura 4-19: A propriedade Adaptive Threshold Rows.

Se o número de linhas na tabela de hash estiver acima desse valor, neste caso 18 linhas ou mais, uma junção
de correspondência de hash será usada. A tabela de hash usará a ramificação superior das duas entradas para
coletar os dados necessários e, a partir desse ponto, agirá como um Hash Match
Junte. Nesse caso, isso significaria uma varredura de índice na tabela Product usando o índice
AK_Product_Name. Se o número de linhas na tabela de hash ficar abaixo do valor limite, o método de loops
aninhados será usado, resultando em uma busca de índice clusterizado na tabela de produtos, usando um
índice completamente diferente, PK_Product_
ProductID, para cada uma das linhas na tabela de hash.

Existem três maneiras, dentro do plano de execução, de dizer qual das duas opções foi usada durante a execução.
Cada método obviamente requer que você capture um plano de execução real.
O primeiro método é observar as propriedades da própria Adaptive Join . A Figura 4-20 mostra que, neste caso,
o Tipo de Junção Real é HashMatch.

122
Machine Translated by Google

Capítulo 4: Unindo Dados

Figura 4-20: Propriedades da junção adaptativa mostrando o tipo de junção real e estimado.

Na parte inferior está o Tipo de Junção Estimado, também HashMatch. Portanto, o número estimado de linhas e o
número real de linhas foram razoavelmente precisos. O limite de linha foi atingido, portanto, a Junção Adaptável usou a
tabela de hash para concluir o processo de junção como uma junção de Correspondência de Hash .

Outra maneira de ver que tipo de junção foi usada é observar as duas entradas nos planos. A Figura 4-21 mostra a dica de
ferramenta para cada alimentação de tubo para a Adaptive Join. A dica de ferramenta superior é para a entrada de junção
de correspondência de hash e a parte inferior é para a entrada de junção de loops aninhados .

Figura 4-21: Duas dicas de ferramentas mostrando o número real de linhas.

123
Machine Translated by Google

Capítulo 4: Unindo Dados

Você pode ver que a entrada de cima tinha 5 linhas reais e a entrada de baixo tinha 0, indicando que, neste caso,
o Adaptive Join consumiu a primeira das duas entradas possíveis.

Finalmente, você pode olhar no final da ramificação, o ponto de acesso de dados dentro do plano de execução para
contar o número de execuções. Esse pode ser o método mais confiável, pois, mesmo se zero linhas fossem
retornadas, pelo menos uma execução de um dos operadores ainda seria gravada.
A Figura 4-22 ilustra a ramificação inferior que não foi executada:

Figura 4-22: Propriedades que não mostram execuções para uma Busca de Índice.

Você também pode usar Eventos Estendidos para capturar "faltas" de Junção Adaptável , usando o evento
adaptive_join_skipped para descobrir por que uma Junção Adaptável não pôde ser usada pelo otimizador, para
uma consulta específica.

Para resumir, o Adaptive Join oferece ao otimizador o melhor dos dois mundos (quase). Se as contagens de
linhas reais forem baixas, a ramificação de loops aninhados do plano será executada. Isso acaba custando um
pouco mais do que se o otimizador tivesse acabado de escolher uma junção de loops aninhados durante a
otimização, mas se tivesse escolhido a junção de correspondência de hash durante a otimização, para o que
acabou sendo uma contagem de linhas baixa, teria sido muito menos eficiente plano. Para altas contagens de linhas,
a ramificação de loops aninhados do plano adaptável será executada, o que resulta em um custo de plano muito
semelhante ao de uma junção de correspondência de hash padrão.

Outros usos de operadores de junção


O otimizador usa os operadores de junção física para cumprir outras tarefas além do T-SQL JOIN
palavra-chave. No Capítulo 3, por exemplo, vimos o otimizador usar um operador Nested Loops para combinar
dados de um Index Seek e seus dados de Key Lookup associados .

Além disso, às vezes o otimizador usa um operador de junção para implementar uma solicitação de não junção em
uma consulta, como APPLY ou EXISTS. Salvaremos a cobertura de APPLY até o Capítulo 7, mas vamos dar uma
breve olhada aqui em como o otimizador implementa as operações EXISTS. Essas são algumas vezes chamadas
de Semi Joins, porque mesmo que as fontes precisem ser combinadas, os dados retornados ainda são de uma única
fonte. A Listagem 4-6 mostra um exemplo simples.

124
Machine Translated by Google

Capítulo 4: Unindo Dados

SELECT bom.ProductAssemblyID,
bom.PerAssemblyQty
DE Production.BillOfMaterials AS bom
ONDE EXISTE ( SELECIONE *
DE Production.BillOfMaterials AS bom2
ONDE bom.BillOfMaterialsID = bom2.ComponentID
E bom2.EndDate NÃO É NULO
);

Listagem 4-6

Quando executamos essa consulta, o plano de execução é um pouco diferente das operações de junção diretas
listadas anteriormente.

Figura 4-23: Plano de execução mostrando Right Semi Join.

O otimizador selecionou um plano que executa uma varredura do índice clusterizado duas vezes para satisfazer a
consulta e, em seguida, os resultados são reunidos usando uma operação de junção Hash Match . No entanto, este
Hash Match é designado como Right Semi Join, ao contrário dos anteriores que eram todos Inner Joins.

Ao contrário de uma Outer Join, que retornará todas as combinações válidas de linhas das duas entradas mais
uma única cópia de cada linha não correspondida da entrada superior, uma Semi Join retorna uma única cópia de
cada linha de uma entrada que tenha pelo menos uma linha correspondente na outra entrada. Ele não adiciona
linhas da outra entrada aos dados; ele é usado apenas para a existência de uma linha correspondente.

O otimizador utiliza, neste caso, um operador Hash Match para realizar o processamento lógico de Semi Join .
Uma tabela de hash de valores do primeiro conjunto de dados é criada e, em seguida, as sondagens do segundo
conjunto de dados são usadas para localizar valores correspondentes. Se algum valor corresponder, a linha do
segundo conjunto de dados será retornada e nenhuma outra comparação será feita.

125
Machine Translated by Google

Capítulo 4: Unindo Dados

Existem semijunções direita e esquerda. O otimizador determina em qual direção irá executar as funções
dependendo do restante das operações necessárias para satisfazer a consulta em questão.

Você também pode ver os tipos de junção lógica Anti Semi Join usados em um plano de execução. Como
sugerido pelo nome, estas são as operações inversas das operações Semi Join : elas retornam uma única
cópia de cada linha de uma entrada que não tem uma correspondência na outra entrada (semelhante a NOT
EXISTE).

Concatenar dados
Por fim, além de juntar dados, é possível concatenar dados. O tipo mais comum de concatenação de dados é
por meio da palavra-chave UNION ALL. No entanto, você também pode ver as operações de concatenação
ocorrerem em um plano de execução de outros tipos de consultas.
Por exemplo, usar variáveis em uma cláusula IN pode resultar em uma operação de concatenação dentro de
um plano de execução. Um operador de concatenação sempre terá duas ou mais entradas e simplesmente
processa cada uma das entradas em ordem, de cima para baixo, e as concatena.

Vejamos um exemplo simples de concatenação.

SELECT p.Sobrenome,
p.BusinessEntityID
DE Pessoa.Pessoa AS p
UNIÃO TODOS
SELECT p.Nome,
p.ID do produto
DA Produção.Produto AS p;

Listagem 4-7

Esta consulta combina uma lista da coluna Person.LastName com Product.Name


coluna. O plano de execução se parece com a Figura 4-24.

126
Machine Translated by Google

Capítulo 4: Unindo Dados

Figura 4-24: Plano de execução mostrando o operador de concatenação.

Este plano de execução é muito simples. O operador Concatenação primeiro chama a entrada superior,
passando as linhas recuperadas para seu pai, até receber todas as linhas. Depois disso passa para a
segunda entrada, repetindo o mesmo processo. Cada um dos operadores de acesso a dados está
simplesmente recuperando todos os dados dos índices referenciados. Nesse caso, existem apenas os dois
conjuntos de dados, mas a Concatenação pode ter quantas entradas forem necessárias. Se observarmos
as propriedades do operador, mostradas na Figura 4-25, você poderá ver como as informações são resolvidas.

Figura 4-25: Propriedades do operador Concatenação.

127
Machine Translated by Google

Capítulo 4: Unindo Dados

Os Valores Definidos foram expandidos para que você possa ver a saída combinada,
definida como Union1002, que consiste nas colunas LastName e Name das respectivas
tabelas.

Resumo
Este capítulo representa um passo importante para aprender a ler planos de execução gráfica.
No entanto, como discutimos no início do capítulo, focamos apenas em operadores de junção e
analisamos apenas consultas simples.

Portanto, se você decidir analisar uma consulta de 2.000 linhas e obter um plano de execução
gráfico quase tão longo, não espere poder analisá-lo imediatamente. Aprender a ler e analisar
planos de execução leva tempo e esforço. No entanto, tendo adquirido alguma experiência, você
descobrirá que se torna cada vez mais fácil ler e analisar, mesmo para os planos de execução mais
complexos. Você já tem conhecimento suficiente para começar. Apenas lembre-se de seguir os
pontos-chave para procurar em um plano. Eles atuarão como guias à medida que você avança nas
operações do plano.

128
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados


Neste capítulo, exploramos os planos de execução para consultas que classificam, agregam e manipulam dados. Em alguns
casos, veremos que os planos podem ficar radicalmente mais complicados rapidamente, mas os mecanismos para ler e entender
esses planos realmente não mudam.

Especificamente, abordaremos:

• Classificando dados - consultas com ORDER BY e os operadores o otimizador


pode usar para realizar a ordenação de dados.

• Agregação de dados – consultas que usam GROUP BY, ou que realizam


agregações, abrangendo:

• Funções de agregação padrão, como SUM, COUNT e assim por diante

• Filtrando agregações usando HAVING

• Funções de janela – como o otimizador executa essas consultas.

Consultas com ORDER BY


Ao recuperar dados de uma tabela, não há uma ordem definida na qual esses dados serão retornados. Se quisermos
garantir a ordem em que os dados são retornados, precisamos usar a cláusula ORDER BY para estabelecer essa ordem. Se
o otimizador puder recuperar os dados de um índice no qual os dados já estão na ordem necessária e todos os operadores no
plano preservarem essa ordem, nenhuma operação adicional será necessária. Caso contrário, um operador Sort será necessário
no plano. Conforme discutimos no Capítulo 2, Sort é um operador de bloqueio; ele deve reunir todas as linhas de que precisa antes
de passar a primeira linha para o operador de chamada.

Abordaremos as seguintes variedades de operação de classificação:

• Classificar

• Classificação N principais

• Classificação Distinta

Também veremos o que pode fazer com que os avisos de classificação apareçam no plano e o que isso significa.

129
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

Operações de classificação

Vamos começar com uma instrução SELECT muito simples, retornando dados da tabela ProductInventory,
ordenados de acordo com a localização da prateleira.

SELECT pi.Prateleira
A Produção.ProdutoInventário AS pi
PARTIR DO PEDIDO pi.Prateleira;

Listagem 5-1

A Figura 5-1 mostra o plano de execução.

Figura 5-1: Plano de execução mostrando um operador Sort.

Seguindo o fluxo de dados da direita para a esquerda, vemos um Clustered Index Scan na tabela
Production.ProductInventory. O otimizador não teve escolha a não ser varrer todas as linhas, já que
nossa consulta não forneceu nenhuma filtragem de cláusula WHERE. O Clustered Index Scan passa
1069 linhas para o operador Sort ; podemos ver isso passando o mouse sobre a seta que leva ao Sort
operador, para abrir a janela de dica de ferramenta ou observando o Número real de linhas no painel
Propriedades para a verificação.

O Clustered Index Scan passa as linhas na ordem em que são lidas do índice, neste caso, provavelmente
ordenadas por ProductID. Qualquer ordem não é garantida, e sabemos disso porque a propriedade
Ordered está definida como False, o que significa que o otimizador não precisa que as linhas retornadas
do índice estejam em qualquer ordem (mais informações sobre a propriedade Ordered em breve).

130
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

Figura 5-2: Propriedades do Clustered Index Scan mostrando um scan não ordenado.

Como não há índice na coluna Shelf, o otimizador deve usar um operador Sort na execução da consulta para
obter a ordenação necessária. Uma vez que o Sort tem todas as 1069 linhas, ele ordena os dados por Shelf e
as linhas passam de volta para o SELECT chamador e de volta para o cliente.

Se uma cláusula ORDER BY não especificar a ordem, a ordem padrão será crescente, como você verá nas
propriedades do ícone Sort na Figura 5-3.

Figura 5-3: Propriedade Order By dentro do operador Sort.

Operações de classificação e a propriedade Ordered de Index Scans


O mecanismo de execução pode usar os seguintes métodos de recuperação para preencher um Index
Scan (clustered e nonclustered):

• Ordenado – basta seguir a estrutura do índice até a primeira página folha e, em seguida, o
ponteiros de página até o final do índice ou até que todos os dados necessários sejam coletados.
Os dados são retornados em ordem lógica de índice, mas se os dados devem vir do disco, o
padrão de acesso é aleatório.
• IAM – é como um Table Scan e usa páginas de mapa de alocação de índice para localizar páginas
alocadas para índice. Os dados são retornados em ordem "semi-aleatória", mas o acesso ao disco
é sequencial, desde que a página de dados não esteja fragmentada no nível do sistema operacional.

131
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

Se o otimizador definir Ordered como False, isso significa que ele não se importa com a ordem. Nesse caso, em tempo de
execução, o mecanismo pode escolher qualquer método de recuperação, se puder garantir o retorno dos resultados corretos
(nem sempre possível para o IAM).

O otimizador define Ordered como True se precisar que os dados estejam em ordem. Nesse caso, o mecanismo sempre
usará o método de recuperação ordenado. Por exemplo, se em vez de ORDER BY Shelf, essa consulta usou ORDER BY
ProductID, o otimizador de consulta definirá a propriedade Ordered como True. Agora que os dados, conforme recuperados
por meio do índice, já estão na ordem lógica correta, não há necessidade de um operador Sort no plano de execução.

Figura 5-4: Uma varredura de índice agrupado mostrando uma varredura ordenada na dica de ferramenta.

Lidando com Sortes caras


Na Figura 5-1, estima-se que a operação Sort seja responsável por 76% do custo da consulta.
Isso não é motivo para pânico. Existem apenas dois operadores em todo este plano e, portanto, 76% é bastante razoável
como porcentagem de todo o trabalho que está sendo feito. Se houvesse 5 ou 10 operadores e um deles representasse
76% do custo estimado, isso seria muito mais preocupante no geral.

132
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

No entanto, se a classificação consumir uma parte significativa do custo total estimado de uma consulta e a
consulta estiver sendo executada lentamente ou causando problemas, talvez seja necessário revisá-la cuidadosamente
e verificar se é possível otimizá-la.

Uma operação de classificação , como qualquer outra operação cara, pode não ser problemática por si só.
A primeira coisa que você precisa fazer é estabelecer por que a operação está lá; pode estar lá simplesmente
para cumprir uma cláusula ORDER BY, mas há outras razões. Você também pode ver o operador Sort adicionado
pelo otimizador quando os dados devem ser ordenados para uma operação Merge Join , apenas como exemplo.
Em planos mais complexos, o propósito de um Sort pode não ser imediatamente óbvio, pois pode ser necessário
para outras partes do plano de execução. Depois de entender por que a classificação está lá, a próxima pergunta a
ser feita é: "A classificação é realmente necessária?"

Você pode encontrar casos em que uma cláusula ORDER BY foi adicionada a uma consulta quando não era
necessária. Os desenvolvedores geralmente usam um ORDER BY ao desenvolver e depurar uma consulta
porque é mais fácil verificar os resultados dessa maneira e esquecer de retirá-lo, mesmo que não seja necessário
no código de produção final.

Além disso, o SQL Server geralmente realiza a operação Sort dentro da execução da consulta devido à falta de um
índice apropriado. Com o índice apropriado, neste caso um índice ordenado por Shelf, os dados podem vir pré-
ordenados. Nem sempre é possível, ou desejável, criar um novo índice, mas se for, você pode economizar a
sobrecarga de classificação. Se fosse decidido que as fileiras não precisavam ser devolvidas ordenadas pela Shelf,
poderíamos estar em uma situação mais fácil.

Se os dados devem ser ordenados por Shelf e não podemos criar um índice, as alternativas são limitadas, a menos
que tenhamos permissão para alterar a lógica da consulta. Notavelmente, por exemplo, essa consulta não possui
cláusula WHERE. A consulta está retornando mais linhas do que o estritamente necessário?
Mesmo que exista uma cláusula WHERE, você precisa garantir que ela limite o número de linhas apenas ao
número necessário de linhas a serem classificadas, não linhas que nunca serão usadas. Independentemente
disso, a operação de classificação ainda será cara, apenas porque a classificação não é uma operação barata.

Se um plano de execução tiver vários operadores de classificação , revise a consulta para ver se todos são
necessários ou se você pode reescrever o código para que menos classificações atinjam o objetivo da consulta.
Obviamente, isso nem sempre é possível ou mesmo desejável. No entanto, como o operador Sort é muito caro,
vale a pena garantir que você precise ordenar os dados.

Classificação N principal

Um tipo diferente de operação de classificação pode ser executado quando o número de linhas a serem retornadas
é limitado. Considere a consulta na Listagem 5-2.

133
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

SELECIONAR TOPO (50)


p.Sobrenome,
p.Nome
DE Pessoa.Pessoa AS p
ORDER BY p.FirstName DESC;

Listagem 5-2

Esta consulta seleciona os sobrenomes e nomes das 50 pessoas que vêm por último no alfabeto,
quando classificadas pelo primeiro nome. A Figura 5-5 mostra como o otimizador resolve essa consulta.

Figura 5-5: Um plano de execução exibindo um operador Top N Sort.

Não há índice que possa satisfazer a cláusula ORDER BY na consulta. No entanto, há um índice diferente
do índice clusterizado na tabela que contém o FirstName e o LastName
colunas, IX_Person_LastName_FirstName_MiddleName. Esse índice conterá apenas as colunas-chave
definidas mais a coluna-chave clusterizada, portanto, será um índice menor que o índice clusterizado. Portanto,
escaneá-lo será mais barato, razão pela qual foi escolhido pelo otimizador. Todas as 19.972 linhas serão
digitalizadas e alimentadas no operador Sort .

O operador Sort neste caso é um tipo exclusivo, Top N Sort. Como o operador Sort normal , este é um
operador de bloqueio. Ele recuperará todas as 19.972 linhas, classificará os dados e retornará as primeiras
50 linhas. Isso é definido dentro das propriedades.

Figura 5-6: Propriedades do operador Top N Sort.

Você também pode vê-lo no tubo de dados que se afasta do operador Sort no plano de execução
mostrado na Figura 5-5. Abaixo de 100 linhas, um mecanismo de classificação que usa mais CPU do
que memória está em jogo, para ajudar no gerenciamento de memória. Acima de 100 linhas, mecanismos
mais intensivos de memória são usados, porque o custo da CPU seria muito alto.

134
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

Classificação distinta

Às vezes, o otimizador pode optar por usar uma operação de classificação para atender a uma consulta que não especifica
uma cláusula ORDER BY. A intenção da Listagem 5-3 é retornar uma lista das combinações exclusivas das partes de um
nome, LastName, FirstName, MiddleName, Suffix.

SELECIONAR DISTINTO
p.Sobrenome,
p.Nome,
p.Nome do meio,
p. Sufixo
DE Pessoa.Pessoa AS p;

Listagem 5-3

A Figura 5-7 mostra o plano de execução resultante, uma varredura do índice clusterizado seguido por uma operação de
classificação.

Figura 5-7: Um plano de execução com um operador Distinct Sort.

Desta vez, vemos um Distinct Sort. O otimizador está usando a operação Sort , não apenas para ordenar os dados, mas
também para eliminar duplicatas. Você pode ver o que está acontecendo expandindo as Propriedades do operador Sort para
observar a propriedade Order By , mostrada na Figura 5-8.

Figura 5-8: Operador Properties of Sort (Distinct Sort) demonstrando a classificação


em todas as colunas.

Ao classificar todas as colunas na lista SELECT, as linhas duplicadas são imediatamente adjacentes e, portanto, podem ser
facilmente ignoradas quando o operador Sort retorna os dados classificados.

135
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

Classificar avisos

O operador Sort é muito dependente das estimativas de linha fornecidas ao otimizador porque precisa de
memória para realizar a classificação. Quando uma quantidade inadequada de memória é alocada para uma
classificação, os dados são armazenados em tempdb por meio de um processo conhecido como spill. Isso é
tão problemático para o desempenho que, no SQL Server 2012 e posterior, você recebe um aviso no próprio
plano de execução (ou em um Evento Estendido, a partir do SQL Server 2008).

A Listagem 5-4 mostra uma consulta aparentemente simples que retorna os dados em ordem
decrescente de ModifiedDate.

SELECT sod.CarrierTrackingNumber,
sod.LineTotal
FROM Sales.SalesOrderDetail AS sod
ONDE sod.UnitPrice = sod.LineTotal
ORDEM POR sod.ModifiedDate DESC;

Listagem 5-4

A Figura 5-9 mostra o plano de execução real que essa consulta gera.

Figura 5-9: Um plano de execução que gerou um aviso de classificação.

Há várias coisas que valem a pena explorar neste plano de execução, mas a que deve aparecer
imediatamente é o símbolo de aviso no operador Sort abaixo.

Figura 5-10: O aviso de classificação, ampliado para facilitar a visualização.

Se você passar o mouse sobre o operador, a dica de ferramenta mostrará uma mensagem sobre o aviso,
mas os detalhes estão nas propriedades, então vamos lá primeiro. A mensagem completa do aviso é
mostrada na Figura 5-11.

136
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

Figura 5-11: Descrição completa do aviso de classificação.

O aviso estabelece especificamente o que aconteceu. Foram usadas 346 páginas adicionais no
tempdb , apesar da memória ser alocada para 2.928 KB. Por quê isso aconteceu? Essa informação
também está disponível nas propriedades. A Figura 5-12 tem a folha de propriedades completa com
alguns fatos destacados.

Figura 5-12: Diferença entre as linhas estimada e real que levam a um derramamento.

137
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

Como você pode ver, o número estimado de linhas é 12.131,7. O número real de linhas foi 74.612. Isso é quase
seis vezes mais linhas sendo processadas do que o SQL Server esperava. Embora a concessão de memória
inclua alguma margem de erro, não havia memória suficiente alocada para lidar com tantos dados. É por isso que
a operação Sort foi forçada a vazar para tempdb.
Sua investigação então precisa determinar onde as estimativas deram errado. A maneira de fazer isso é percorrer
os outros operadores no plano de execução.

Os dados que estão sendo lidos do disco são provenientes do Clustered Index Scan do índice
PK_SalesOrderDetail, na extremidade direita do plano na Figura 5-9. O número estimado de linhas
é 121.317 e o número real de linhas é o mesmo.
Isso significa que a operação inicial ocorreu conforme o esperado.

Os próximos dois operadores são Compute Scalar. O primeiro tem um par de cálculos mostrados na Figura
5-13.

Figura 5-13: Detalhes do primeiro operador Compute Scalar.

Esses dois cálculos são benignos e estão diretamente relacionados aos dados com os quais estamos
trabalhando na consulta. O próximo operador Compute Scalar é simplesmente um alias dos cálculos do
operador anterior. LineTotal é uma coluna computada na definição da tabela, e é assim que você pode ver
isso no plano de execução.

138
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

Figura 5-14: Cálculo feito pelo segundo operador Compute Scalar.

Nenhum desses processos afetará as estimativas de linha. A próxima operação é o operador Filtro
(abordado com mais detalhes posteriormente no capítulo). Um operador Filter inspeciona os dados em
cada linha que recebe com o objetivo de eliminar as linhas que não são necessárias; somente as linhas que
atendem aos critérios de Predicado são passadas para o operador de chamada.

Normalmente, esse tipo de operação é feito em nível de tabela ou índice, por meio de buscas e varreduras.
No entanto, como estamos lidando com valores calculados, o LineTotal, esses cálculos devem ser realizados
antes que o conjunto de dados possa ser filtrado. Podemos ver o cálculo do Predicado nas propriedades do
operador. Todos os colchetes e nomes de objetos totalmente qualificados podem dificultar um pouco a leitura.
O cálculo principal é sod.UnitPrice = sod.LineTotal.

Figura 5-15: Detalhes da propriedade Predicate do operador Filter.

No entanto, esse cálculo em si não é o problema. Em vez disso, precisamos olhar para o número estimado
de linhas processadas pelo operador de filtro , 12.131,7. Em outras palavras, das 121.317 linhas que foram
lidas do índice clusterizado, o otimizador assumiu que apenas 10% corresponderia à condição Predicate.
Esta é uma estimativa fixa, que o otimizador usa porque não pode saber ao certo quantos valores irão
corresponder, ao comparar com um valor calculado.

Na verdade, 74.612 foram retornados, e essa é a causa das estimativas de memória inadequadas para o
operador Sort e o derramamento subsequente para tempdb.

139
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

Agregando dados
Um dos usos mais comuns para os dados, depois de coletados e limpos, é aplicar alguma matemática
a eles para obter o número de registros (COUNT), o valor médio de uma coluna (AVG), o valor máximo
(MAX) , e outros. Esses cálculos exigem que combinemos os dados em um processo conhecido como
"agregação".

A agregação é um recurso poderoso no T-SQL que nos permite, em muitos casos, realizar esses
tipos de cálculos de maneira muito mais eficiente, pois podemos agregar os dados à medida que
os recuperamos. Em resumo, se obtivermos operações de agregação no início de um plano,
estaremos frequentemente trabalhando com menos dados no restante do plano, tornando-o mais
eficiente. Também estamos economizando grandes quantidades de tráfego de rede, se a alternativa for
agregar no cliente.

Esta seção explorará os mecanismos pelos quais o SQL Server agrega informações, com base nos
dados, nas estruturas de dados e no código T-SQL que você escreveu.

Agregado de fluxo

O primeiro operador de agregação que veremos é o Stream Aggregate. Este operador usa
dados classificados para construir um conjunto de valores agregados. Usaremos a consulta
simples na Listagem 5-5 para criar uma contagem agregada do número de valores TerritoryID na
tabela Sales.Customer.

SELECT c.TerritoryID,
CONTAR(*)
DE Vendas.Cliente AS c
GROUP BY c.TerritoryID;

Listagem 5-5

140
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

Se executarmos essa consulta e capturarmos o plano de execução, veremos o operador Stream Aggregate
em uso.

Figura 5-16: Plano de execução com um operador Stream Aggregate.

Lendo este plano na ordem do fluxo de dados, vemos que ele usa o IX_Customer_TerritoryID
índice não clusterizado para varrer os dados. Esses dados fluem para o operador Stream Aggregate ,
que agrega os dados e, em seguida, para um operador Compute Scalar antes de retornar como um conjunto
de resultados.

O primeiro requisito para o uso do operador Stream Aggregate é que os dados sejam classificados pelas
colunas que estão sendo agregadas. Se verificarmos as propriedades do operador Index Scan , veremos
que a propriedade Ordered está configurada para True, o que significa que os dados serão acessados na
ordem lógica em que estão armazenados no índice (por TerritoryID), e portanto não Classificação adicional
operador é necessário. Isso ajuda a explicar por que o otimizador optou por usar esse índice não clusterizado
para recuperar os dados.

Figura 5-17: Propriedades do Index Scan mostrando uma operação ordenada.

Podemos olhar as propriedades do operador Stream Aggregate para ver como os dados estão sendo
processados. A Figura 5-18 mostra as propriedades da cláusula GROUP BY de nossa consulta.

141
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

Figura 5-18: Propriedades Group By do operador Stream Aggregate.

A propriedade Defined Values divulga os cálculos que estamos solicitando dessa


agregação.

Figura 5-19: Saída dos valores agregados mostrados como Valores Definidos.

As agregações ocorrem dentro do operador Stream Aggregate , conforme ele lê os dados ordenados.
O AggType de countstar indicou que, neste caso, está realizando uma contagem agregada para cada
valor de TerritoryID.

Por que, então, existe um operador Compute Scalar dentro deste plano? A Figura 5-20 mostra
suas propriedades.

Figura 5-20: Operador Compute Scalar mostrando a conversão de dados.

142
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

O tipo de dados de saída da agregação countstar das propriedades do operador mostradas na


Figura 5-19 é BIGINT. O otimizador adicionou um operador Compute Scalar para realizar uma
conversão implícita desses dados para um tipo de INT, antes de retorná-lo no conjunto de resultados
da consulta porque esta consulta está solicitando um COUNT (que resulta como INT). Isso está
alterando os dados para um INT. Se usarmos COUNT_BIG na consulta, o Compute Scalar
seria removido.

O operador Stream Aggregate geralmente é direto. Ele calcula as informações à medida que as
recupera, em um fluxo, porque os dados são ordenados. Isso pode fazer para uma operação muito
eficiente. No entanto, a exigência de que os dados sejam ordenados implica que, dependendo das
estruturas de dados envolvidas, uma operação de classificação pode fazer parte do plano. Isso pode
levar a um desempenho ruim do Stream Aggregate, sugerindo a necessidade de um índice novo ou
diferente para oferecer melhor suporte à recuperação de dados de maneira ordenada.

Correspondência de hash (agregado)

Vamos considerar outra consulta agregada simples em uma única tabela, onde queremos saber o
desconto médio oferecido, para cada preço unitário.

SELECT sod.UnitPrice,
AVG(sod.UnitPriceDesconto)
FROM Sales.SalesOrderDetail AS sod
GRUPO POR sod.UnitPrice;

Listagem 5-6

A Figura 5-21 mostra o plano de execução real.

Figura 5-21: Plano de execução gerado com um operador de agregação Hash Match.

143
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

O fluxo de dados da execução da consulta começa com um Clustered Index Scan, porque todas as
linhas são retornadas pela consulta; não há cláusula WHERE para filtrar as linhas. Em seguida, o
otimizador agrega essas linhas para iniciar o processo de cálculo de agregação do AVG solicitado. Para
contar o número de linhas para cada UnitPrice, o otimizador opta por executar um operador Hash Match
(Aggregate) .

No Capítulo 4, examinamos o operador Hash Match(Join) para junções. Este mesmo jogo de hash
O operador também pode ocorrer quando realizamos agregações em uma consulta ou porque o
otimizador decide usar agregação por algum outro motivo. Assim como uma Hash Match com uma
junção, uma Hash Match com uma agregação faz com que o SQL Server crie uma tabela de hash
temporária na memória na qual ele armazena os resultados de todos os cálculos agregados; ele pode
contar linhas, rastrear valores mínimos e máximos, calcular uma soma e assim por diante.

Neste exemplo, para cada valor na coluna GROUP BY, que é UnitPrice, ele armazena uma linha
com esse UnitPrice, uma contagem de linhas e um desconto total. À medida que constrói a tabela
de hash, aumenta a contagem e o desconto total sempre que processa uma linha com o mesmo
UnitPrice.

Como regra geral, a memória usada por um Hash Match(Aggregate) geralmente será menor que a
usada por um Hash Match(Join), pois o operador join deve criar uma tabela de hash para todos os
dados, enquanto que para o operador agregado, a tabela de hash contém apenas a chave de agregação
e os resultados da computação. Certamente, pode-se vislumbrar exceções; por exemplo, se tivermos uma
tabela muito pequena composta por duas colunas, mas uma consulta com um número muito grande de
cálculos agregados, mas geralmente a regra será verdadeira.

Podemos ver como as agregações são realizadas observando as propriedades do Hash


Match(Aggregate), mostrado na Figura 5-22.

144
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

Figura 5-22: Propriedades do operador agregado Hash Match detalhando a


função do operador.

Destacado no topo você pode ver que existem dois agregados, e nenhum deles é a média. O
primeiro é um cálculo COUNT * sendo executado para obter uma contagem de linhas para cada
UnitPrice, retornada como Expr1006. A segunda agregação é uma SOMA do UnitPriceDiscount
coluna para cada UnitPrice, retornada como Expr1007. Mais abaixo, você pode ver como a
tabela de hash está sendo criada na coluna UnitPrice.

Como você pode ver na Lista de Saída, UnitPrice, Expr1006 e Expr1007 são passados para
um operador Compute Scalar , que realiza o cálculo abaixo para cada valor UnitPrice.

145
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

[Expr1001] = Operador Escalar(CASE WHEN [Expr1006]=(0) THEN NULL


ELSE [Expr1007]/CONVERT_IMPLICIT(dinheiro,[Expr1006],0) END)

Se um determinado valor UnitPrice, conforme expresso por Expr1006, não tiver linhas, isso retornará
NULL para esse UnitPrice. Se houver linhas para esse UnitPrice, o UnitPriceDiscount médio é
calculado dividindo Expr1007 por Expr1006, primeiro tendo convertido Expr1006 em um tipo de dados
MONEY, usando o comando CONVERT_IMPLICIT.

Muitas vezes, as agregações em consultas podem ser operações comparativamente caras,


dependendo do número de linhas que precisam ser agregadas. No entanto, é quase sempre muito mais
eficiente agregar no servidor e enviar um número limitado de linhas para o cliente do que enviar todos os
dados e agregar no cliente. Além disso, nos casos em que os dados agregados são usados no restante
de uma consulta maior ou armazenados em uma tabela temporária e depois unidos a outros dados, a
economia fica ainda maior porque todos os operadores subsequentes trabalham em muito menos linhas.

Uma tática ao tentar ajustar uma agregação é adicionar um índice de cobertura ou remover
colunas desnecessárias para que um índice existente se torne cobertura, classificado nas colunas
GROUP BY. Isso permitirá que o otimizador use o Stream Aggregate em vez do Hash Match
Aggregate.

Você também pode pré-agregar dados usando uma exibição indexada, embora essa tática incorra na
sobrecarga de manter os dados na exibição, bem como na tabela, quando os dados são modificados.

Filtrando agregações usando HAVING

O otimizador usa o operador Filter para limitar a saída às linhas que atendem aos critérios especificados.
Na Listagem 5-7, adicionamos uma cláusula HAVING, para limitar o conjunto de resultados apenas às
linhas em que o desconto médio do preço unitário é maior que 0,2.

146
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

SELECT sod.UnitPrice,
AVG(sod.UnitPriceDesconto)
FROM Sales.SalesOrderDetail AS sod
Agrupar por sod.UnitPrice
TENDO AVG(sod.UnitPriceDiscount) > .2

Listagem 5-7

A Figura 5-23 mostra o plano de execução, que agora contém um operador Filter após o
Compute Scalar.

Figura 5-23: O plano de execução usa um operador Filter para satisfazer a cláusula HAVING.

O operador Filter limita a saída aos valores da coluna, UnitPriceDiscount, que possuem um valor médio
maior que .2, para satisfazer a cláusula HAVING. Isso é feito aplicando um Predicate na saída do
operador Compute Scalar , como podemos ver nas propriedades do operador Filter .

Figura 5-24: Propriedades do Filtro em operação mostrando o


cálculo de filtragem.

Nesse caso, a natureza da cláusula HAVING significava que o otimizador não tinha como verificar o
Predicado sem antes fazer a agregação. O Hash Match (Aggregate) recebe 121317 linhas e passa
287 (passe o mouse sobre as setas do fluxo de dados para ver isso), que é o número processado pelo
operador Filter .

No entanto, se houver uma maneira de filtrar antes da agregação, o otimizador geralmente a


encontrará. Para oferecer um exemplo trivial, se alterássemos a cláusula HAVING para sod.UnitPrice >
800, o otimizador é sensato o suficiente para, essencialmente, reescrever HAVING para WHERE,
caso em que a filtragem é empurrada para o Clustered Index Scan, como você verá executando a
consulta modificada e examinando a propriedade Predicate deste operador (reescrevendo a consulta
para usar WHERE em vez de HAVING terá o mesmo efeito).

147
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

Figura 5-25: Um plano de execução que mostra a filtragem ocorrendo durante a agregação.

Ao filtrar em linhas agregadas, o otimizador não tem escolha a não ser adicionar um operador Filtro ao plano,
após a conclusão da agregação. Nocionalmente, isso adiciona um custo extra mínimo ao plano. No entanto,
isso é mais do que compensado pela necessidade de retornar menos linhas ao cliente. Além disso, quando os
dados agregados e filtrados são usados em outro lugar em uma consulta maior, a economia é ainda maior. Se o
otimizador puder encontrar uma maneira de aplicar a filtragem mais cedo, ele o fará.

Planos com agregações e spools


Um operador Spool usa uma tabela de trabalho temporária para armazenar dados que podem precisar
ser reutilizados várias vezes em um plano de execução. Esta seção revisará alguns exemplos em que os spools
são usados para armazenar os resultados de cálculos de agregação para planos que usam junções de loops
aninhados . No entanto, os spools podem aparecer em muitas outras situações em que, armazenando os
resultados em uma tabela de trabalho, o otimizador pode reutilizar esses dados várias vezes, em vez de ter que
executar conjuntos de operadores várias vezes.

Existem vários tipos de spool, representados pelos seguintes operadores físicos: Index Spool, Rowcount
Spool, Table Spool e Window Spool. Aqui, consideraremos apenas o Spool de Tabela e o Spool de
Índice, pois aparecem no contexto de consultas que contêm agregações. O SQL Server sempre terá um índice
clusterizado para armazenar os dados de qualquer spool; um Spool de Índice terá um índice não clusterizado
adicional para facilitar a recuperação dos dados.

Existem dois tipos lógicos de operador Spool , Lazy Spool e Eager Spool. Um carretel preguiçoso
é um operador de streaming. Ele solicita uma linha de seu operador filho, armazena-a e a passa para seu pai,
passando o controle de volta para esse pai. Um Eager Spool, por outro lado, é um operador de bloqueio, que
chamará seu nó filho até que tenha todas as linhas, e só então retornará a primeira linha de sua tabela de
trabalho. Geralmente, o otimizador evitará o Eager Spool, mas é ideal para certas situações, como proteção de
Halloween (abordado no Capítulo 6).

148
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

Carretel de mesa

Vamos começar com um exemplo de agregação que usa um Spool de Tabela. A consulta na Listagem
5-8 usa uma subconsulta para calcular o valor total do imposto pago pelos clientes, de acordo com a
região de vendas (TerritoryID).

SELECT sp.BusinessEntityID,
sp.TerritoryID,
( SELECT SUM(TaxAmt)
FROM Sales.SalesOrderHeader AS soh
WHERE soh.TerritoryID = sp.TerritoryID)
FROM Sales.SalesPerson AS sp
ONDE sp.TerritoryID NÃO É NULO
ORDEM POR sp.TerritoryID;

Listagem 5-8

A Figura 5-26 mostra o plano de execução.

Figura 5-26: Um plano de execução usando um Spool de Tabela com agregação.

A entrada externa do operador de junção Nested Loops é uma varredura do índice clusterizado na
tabela SalesPerson, que retorna 14 linhas (classificadas por TerritoryID). Isso significa que a entrada
interna, um Table Spool, será executada 14 vezes.

A primeira execução da entrada interna é sempre um Rebind, portanto, o Table Spool chama uma linha
da Hash Match, que por sua vez chama uma linha da Clustered Index Scan em SalesOrderHeader. O
operador Hash Match usa uma tabela de hash temporária para calcular o valor total do imposto coletado
para cada valor TerritoryID distinto no cabeçalho SalesOrder. Existem 10 valores distintos de TerritoryID e,
em algum momento, ele começará a retornar cada uma dessas 10 linhas para o Table Spool, que os
armazena em sua tabela de trabalho enquanto os repassa (é um Lazy Spool), até que tenha passado todos
10 linhas.

149
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

Se examinarmos as propriedades do operador Nested Loops , veremos que ele satisfaz a condição de
junção usando um Predicate (consulte a seção Operador de Nested Loops do Capítulo 4 para uma discussão
deste tópico). Essencialmente, a entrada interna é estática e produzirá o mesmo resultado para cada valor na
entrada externa.

Para cada uma das outras 13 linhas retornadas de SalesPerson para o operador Nested Loops , a entrada
externa precisa retroceder. É aqui que entra o carretel de mesa . Ao invés de chamar o Hash Match
novamente, 13 vezes, é utilizada a tabela de trabalho definida pelo Table Spool .

Figura 5-27: Propriedades mostrando como o Table Spool foi usado para filtrar os dados.

Se você inspecionar as propriedades do Table Spool , verá 13 Rewinds e 1 Rebind. O Hash Match e o
Clustered Index Scan são executados apenas uma vez cada, para o Rebind inicial, para carregar os dados
no Spool de Tabela.

Este é um exemplo simples de como o otimizador pode usar um Table Spool para tornar as consultas de
agregação mais eficientes, onde um único Table Spool reutilizou suas próprias informações. No entanto,
muitas vezes, você encontrará casos em que um spool compartilha suas informações com outros spools .
operadoras do mesmo plano. Se você verificar as propriedades do Spool de Tabela, verá que ele tem um
valor de ID de nó de 4. Se um segundo spool reutilizasse dados desse primeiro spool, nas propriedades do
segundo spool você veria ambos seu próprio valor de ID de nó e um valor de ID de nó primário , que neste
caso seria 4. Veremos um exemplo disso no Capítulo 6.

Carretel de índice

Para ver um operador Index Spool , precisamos apenas adicionar um índice útil que o otimizador possa usar
para localizar as linhas com valores de TerritoryID correspondentes, na tabela SalesOrderHeader.

150
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

CRIAR ÍNDICE IX_SalesOrderHeader_TerritoryID


ON Sales.SalesOrderHeader
(
TerritoryID
)
INCLUIR
(
Valor do imposto

);

Listagem 5-9

Agora, execute novamente a consulta na Listagem 5-8. A Figura 5-28 mostra o plano de execução,
que é semelhante ao plano anterior, exceto que agora, para a entrada interna da junção de Loops
aninhados , vemos um Index Seek na tabela SalesOrderHeader, uma agregação de streaming em vez do
bloqueio de Hash Match agregação e, em seguida, um Spool de Índice em vez de um Spool de Tabela.

Figura 5-28: Um plano de execução usando um operador Index Spool para agregação.

As 14 linhas retornadas pela varredura da tabela SalesPerson são ordenadas pelo Sort
operação em TerritoryID. Examine as propriedades do operador Nested Loops e você verá que ele
satisfaz a condição de junção usando os valores TerritoryID como Referências Externas. Isso significa
que cada um dos valores das 14 linhas é enviado para a entrada interna, que retorna apenas as
linhas correspondentes com base na operação Index Seek .

Como antes, a primeira execução da entrada interna é um Rebind. O valor de 1, o valor da


primeira linha TerritoryID, é enviado para os outros operadores. O Index Spool primeiro inicializa seus
operadores filhos. O Stream Aggregate começa a solicitar linhas do Index Seek, que usa o valor
empurrado para baixo para encontrar linhas correspondentes no índice. O spool passa as linhas
correspondentes para o Stream Aggregate, que retorna uma única linha, o resultado da agregação
para TaxAmt, para o Index Spool, que o armazena em uma tabela de trabalho indexada e o retorna
para aninhados Loops.

151
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

A segunda e terceira linhas que entram em loops aninhados também têm um valor TerritoryID de 1,
portanto, as próximas duas execuções de Index Spool são Rewinds. Spool de índice não chamará Stream
Aggregate e, em vez disso, retornará imediatamente os resultados armazenados anteriormente da tabela de trabalho.

Para a quarta linha, temos um valor TerritoryID de 2, um novo valor. A alteração de dados força o
Index Spool a registrar um Rebind inicializando os outros operadores novamente com o novo valor
pressionado. Esta será a quarta execução do Index Spool, mas apenas a segunda execução de cada um
dos operadores filho.

Esse padrão se repete até que todas as 14 linhas sejam processadas. Observe as propriedades do
Index Spool e você verá que existem 10 Rebinds e 4 Rewinds . Observe as propriedades do Stream
Aggregate ou do Index Seek e você verá apenas 10 execuções, correspondendo aos 10 valores distintos
para TerritoryID nas 14 linhas.

Lembre-se de descartar o índice criado na Listagem 5-9 antes de continuar.

DROP INDEX IX_SalesOrderHeader_TerritoryID ON Vendas.


SalesOrderHeader;

Listagem 5-10

Trabalhando com funções de janela


Introduzida no SQL Server 2008, a cláusula OVER define como classificar e particionar os dados, aos
quais uma função agregada pode ser aplicada. Uma função Window é essencialmente aquela que opera
em uma janela ou partição de dados, conforme definido na cláusula OVER. As funções de classificação,
ROW_NUMBER, RANK, DENSE_RANK e NTILE, são todas funções de janela.
Funções agregadas, como SUM ou AVG, também suportam a cláusula OVER, mas não são consideradas
funções Window.

A consulta na Listagem 5-10 particiona os dados de acordo com o valor CustomerID e, dentro
de cada partição, ordena os dados por data do pedido. Para cada partição, aplicamos a função de
classificação ROW_NUMBER, que simplesmente numera cada linha em cada partição, portanto, se um
cliente fizer 5 pedidos nesse período, haverá 5 linhas em sua partição, numeradas de 1 a 5, com o pedido
mais antigo tendo um RowNum de 1.

152
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

SELECT soh.CustomerID,
soh.SubTotal,
ROW_NUMBER() OVER (PARTIÇÃO POR soh.CustomerID
ORDER BY soh.OrderDate ASC) AS RowNum,
soh.Data do pedido
FROM Sales.SalesOrderHeader AS soh
WHERE soh.OrderDate BETWEEN '20130101'
E '20130701'

Listagem 5-11

A Figura 5-29 mostra o plano de execução resultante.

Figura 5-29: Plano de execução para satisfazer uma função de janela usando um segmento
e um operador de sequência.

Como não há índice que suporte a cláusula WHERE, o otimizador opta por varrer o índice clusterizado.
Devolve as encomendas que se enquadram no período requerido. Essas linhas são classificadas por
CustomerID e secundariamente por OrderDate em preparação para dividir os dados em partições.

Em seguida, encontramos dois novos operadores que ainda não exploramos, Segment e
Sequence Project (Compute Scalar). Sempre que você vê um operador com o qual não está
familiarizado ou operadores familiares cuja função não está imediatamente clara para você, geralmente
é um bom lugar para começar.

Um operador Segment divide os dados em uma série de partições, ou segmentos, com base na coluna
ou colunas de partição, definidas na consulta. Nesse caso, optamos por particionar os dados por
CustomerID. Se examinarmos a propriedade Group By deste operador, veremos que os dados estão
sendo agrupados na coluna CustomerID. Também podemos ver que uma coluna de saída é criada,
Segment1002, que marca o início de cada novo segmento.

153
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

Figura 5-30: Propriedades do operador Segment mostrando a segmentação dos dados.

Todos esses dados passam para o operador Sequence Project (Compute Scalar) , que é usado
exclusivamente por funções de classificação, e trabalha com um conjunto ordenado de dados, com marcas de
segmento adicionadas pelo operador Segment .

Na Figura 5-31, podemos ver que, nesse caso, o operador Sequence Project simplesmente conta o número
de linhas em cada segmento e atribui um número sequencial a elas, como se tivesse uma coluna IDENTITY
atribuída a cada partição.

Figura 5-31: Propriedades do operador Sequence mostrando a função da operação.

Esse exemplo é bom, mas não mostra as agregações que são possíveis quando você começa a usar as
funções de janelas. A Listagem 5-11 adiciona uma coluna adicional à consulta, o valor médio do Subtotal, em
um determinado CustomerID, para o intervalo de dados em questão.

154
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

SELECT soh.CustomerID,
soh.SubTotal,
AVG (soh.SubTotal) OVER (PARTITION BY soh.CustomerID) AS
Subtotal médio,
ROW_NUMBER() OVER (PARTITION BY soh.CustomerID ORDER BY soh.
OrderDate ASC ) AS RowNum
FROM Sales.SalesOrderHeader AS soh
ONDE soh.EncomendaData
ENTRE '20130101' E '20130701';

Listagem 5-12

Se examinarmos o plano de execução para esta consulta, veremos, na Figura 5-32, um que é muito mais
complexo do que os outros até agora no livro.

Figura 5-32: Um plano mais complexo mostrando funções de janela adicionais.

Isso é difícil de ler, então vamos detalhar partes do plano de execução. A Figura 5-33 mostra a seção primária
relativa à recuperação de dados.

Figura 5-33: Detalhes do plano da Figura 5-32.

Assim como antes, não há índice que suporte nossa cláusula WHERE, então vemos um Clustered
Index Scan. Os dados são ordenados novamente por meio de uma operação de classificação e, em
seguida, são passados para o agora familiar operador de segmento . De lá, ele passa para um Table Spool
(Lazy Spool), que é a entrada externa de um operador de junção de loops aninhados . A entrada interna é outro loop anin
join, para o qual a Figura 5-34 mostra as entradas externas e internas.

155
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

Figura 5-34: Detalhes adicionais do plano da Figura 5-32.

Na Figura 5-34, você pode ver onde estamos reutilizando os dados armazenados no operador Table Spool .
Este operador lida com dados segmentados alterando ligeiramente seu comportamento. Em operação
normal, um Lazy Spool lê uma linha, armazena-a e repassa-a imediatamente. No entanto, neste caso, o
Spool de Tabela lê todas as linhas de um segmento de dados e, em seguida, envia a linha desse segmento
para as operações a seguir.

Os dados do Spool de Tabela são passados para um operador Stream Aggregate . A operação
Stream Aggregate pode ser usada porque os dados são ordenados com base no operador Sort que
vemos na Figura 5-33. Se observarmos as propriedades do Stream Aggregate , podemos entender o
que ele está fazendo nesse plano de execução.

Figura 5-35: Propriedades do operador Stream Aggregate.

156
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

Há dois novos valores sendo criados, uma contagem dos valores dentro do agregado do CustomerID
e uma soma da coluna SubTotal nesse mesmo agregado. Tudo isso é então passado para um operador
Compute Scalar que realiza outro cálculo.

Figura 5-36: Cálculo dentro do operador Compute Scalar.

Isso está criando um novo valor, Expr1001, que será nulo ou um cálculo médio dos valores criados no
Stream Aggregate. Resumindo, esta parte do processo está satisfazendo a função AVG chamada na
consulta na Listagem 5-11. A saída do Operador Escalar é então executada por outro operador de
Loops Aninhados , que se refere ao nosso armazenamento temporário no Spool de Tabela. Por quê?

É aqui que as coisas ficam divertidas. Devemos agregar nossos dados para chegar a uma média,
para que o número de linhas retornadas mude. Você pode ver isso se observar a saída de linhas
real do operador Stream Aggregate e compará-la com o número de saídas de linhas da segunda
instância do operador Table Spool na Figura 5-34. A saída agregada é 2.464 e a saída de
armazenamento temporário é 2.784. O Nested Loops é necessário para juntar a saída da operação
de agregação com as informações que estão sendo armazenadas temporariamente no Table Spool.
Tudo isso é passado para o outro operador Nested Loops (originalmente mostrado na Figura 5-33)
para ser combinado com a saída do Table Spool para processamento final da consulta, conforme
mostrado na Figura 5-37.

157
Machine Translated by Google

Capítulo 5: Classificação e agregação de dados

Figura 5-37: Detalhes do plano da Figura 5-32 mostrando os operadores de segmento e


sequência.

Esta seção final do plano de execução é onde vemos as funções necessárias para suportar a
função ROW_NUMBER(), da consulta original na Listagem 5-10. Não há operação de
classificação final porque eu eliminei a cláusula ORDER BY na consulta na Listagem 5-11, apenas
para simplificar um pouco as coisas.

Através de tudo isso agora, você pode ver como as funções Window podem ser usadas para
agregações, como essas funções e métodos são atendidos no plano de execução e como você lê
um plano de execução para entender quais funções estão sendo executadas e onde. A leitura do
plano é possível porque você pode ver a criação de valores como Expr1004
e Expr1005 dentro do Stream Aggregate a ser seguido pelo seu uso para criar uma média
representada por Expr1001 criada no operador Compute Scalar . Você também pode ver como
cada um dos operadores Table Spool é usado para mover os dados pelo processamento necessário
para chegar à saída solicitada.

Resumo
Este capítulo se concentrou principalmente na ordenação e agregação de dados. Você viu vários
exemplos de planos de execução que mostraram como seguir propriedades e valores conforme
eles se movem entre operadores dentro de um plano de execução. Esse é um dos fundamentos
para ler seu próprio plano de execução e você o verá repetidas vezes ao longo do livro. Durante
toda essa discussão, levantamos o custo de algumas operações. Apenas lembre-se de que nenhuma
operação é inerentemente problemática. Cada um representa apenas as melhores tentativas do
otimizador de resolver a consulta em questão. Não se concentre em eliminar ou alterar qualquer
operador; concentre-se na consulta em questão.

158
Machine Translated by Google

Capítulo 6: Planos de Execução para Dados


Modificações
Todos os planos de execução anteriores no livro foram para consultas SELECT. No entanto, o otimizador também
gera planos de execução para todas as consultas de modificação de dados emitidas para o banco de dados, para
instruir o mecanismo de execução sobre a melhor forma de realizar a alteração de dados solicitada. Este capítulo
examinará as características dos planos de execução para consultas INSERT, UPDATE, DELETE e MERGE. Você
encontrará os planos de execução para consultas de modificação de dados muito úteis. Você verá como as colunas
IDENTITY são resolvidas durante os INSERTs e como as restrições referenciais são gerenciadas durante os
DELETEs, apenas para citar alguns dos processos expostos no plano de execução. Você também poderá usar
esses planos para ajustar suas consultas de modificação de dados, assim como faria com uma consulta SELECT.

Planos para INSERTs


As consultas INSERT são sempre executadas em uma única tabela. Isso levaria você a acreditar que seus planos
de execução serão simples. No entanto, para considerar colunas IDENTITY, colunas computadas, verificações de
integridade referencial e outras estruturas de tabela, os planos de execução para consultas de inserção podem
ser bastante complicados.

A Listagem 6-1 mostra uma consulta INSERT muito simples.

INSERIR EM Pessoa.Endereço
(
Endereço Linha 1,
Endereço linha 2,
Cidade,
StateProvinceID,
Código postal,
desculpe,
Data modificada
)
VALORES
( N'1313 Mockingbird Lane', -- AddressLine1 - nvarchar(60)
N'Porão', -- AddressLine2 - nvarchar(60)
N'Springfield', -- Cidade - nvarchar(30)

159
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

79, -- StateProvinceID - int


N'02134', -- Código Postal - nvarchar(15)
MUDANÇA(), -- rowguid - identificador exclusivo
GETDATA() ); -- ModifiedDate - datetime

Listagem 6-1

Assim como para qualquer outra consulta, podemos capturar o plano de execução estimado ou real.
Conforme discutido no Capítulo 1, se solicitarmos o plano estimado, não executamos a consulta e, portanto,
não inserimos nenhum dado; simplesmente enviamos a consulta para inspeção pelo otimizador, para ver o plano.

Se quisermos ver informações de tempo de execução, executamos a consulta, solicitando o plano real. Se
quisermos ver o plano real sem modificar os dados, podemos envolver a consulta em uma transação e reverter
essa transação após capturar o plano.

Nesse caso, vamos apenas capturar o plano estimado, conforme mostrado na Figura 6-1.

Figura 6-1: Plano estimado mostrando um INSERT.

A estrutura física da tabela que a consulta INSERT acessa pode afetar o plano de execução resultante. Esta
tabela tem uma coluna IDENTITY e uma restrição FOREIGN KEY.

Assim como nas consultas SELECT que examinamos, podemos ler esse plano da direita para a esquerda
(ordem de fluxo de dados) ou da esquerda para a direita (ordem de chamada do operador). No entanto, antes
de tentarmos seguir as várias etapas do plano, começaremos examinando o "primeiro operador" porque, como
descobrimos no Capítulo 2, ele contém muitas informações úteis sobre o plano.

160
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

Operador INSERT
A Figura 6-2 mostra as propriedades do operador INSERT para este plano.

Figura 6-2: Propriedades do operador INSERT.

Apesar do maior número de operadoras neste plano, o otimizador ainda o classificou como um
plano trivial. Observe também que o otimizador executou uma parametrização simples nesta
consulta, trocando os valores codificados fornecidos na cláusula VALUES na Listagem 6-1, por
parâmetros, a fim de promover a reutilização do plano.

Podemos ver como os parâmetros foram resolvidos olhando para o ParameterizedText


valor da propriedade, mostrado na Listagem 6-2 (após copiar e colar e aplicar formatação para
torná-lo legível).

161
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

(@1 nvarchar(4000),@2 nvarchar(4000),@3 nvarchar(4000),@4 int,@5 nvarchar(4000))

INSERIR EM [Pessoa]. [Endereço]


([Endereço Linha 1],
[Endereço linha 2],
[Cidade],
[StateProvinceID],
[Código postal],
[guia de linha],
[Data modificada]
)
VALORES (@1,
@2,
@3,
@4,
@5,
mudança(),
getdata()
)

Listagem 6-2

Vamos agora percorrer o plano, lendo da direita para a esquerda, seguindo o fluxo de dados. Começamos
com um operador que é novo para nós: Constant Scan.

Operador de varredura constante

O operador Constant Scan introduz nos resultados uma ou mais linhas, originadas de uma "varredura
de uma tabela interna de constantes". Em outras palavras, as linhas vêm das propriedades do próprio
operador, especificamente das propriedades Values , e não de quaisquer dados externos
fonte.

Um Constant Scan gera uma ou mais linhas, consistindo em uma ou mais colunas, e possui
muitas funções possíveis dentro de um plano de execução. Para entender o seu papel em qualquer

162
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

plano de execução, você precisa ver quais valores ele produz e onde no plano esses valores são usados. Para
fazer isso, precisamos examinar as propriedades detalhadas dos operadores.

Você pode ver quais colunas ele retornou da propriedade Lista de saída e os valores de linha da propriedade
Valores . A Figura 6-3 mostra as propriedades do Constant Scan para uma consulta trivial (SELECT * FROM
(VALUES (1,2),(3,4),(5,6)) AS x(a,b);), mostrando que o operador gera duas colunas (Union1006, Union1007) e três
linhas.

Figura 6-3: Os valores definidos do operador Constant Scan.

Em casos menos triviais, é útil seguir os nomes das colunas fornecidos na Lista de Saída
ao longo do plano para ver onde mais eles são usados e por que eles são necessários.

Para Constant Scan na Figura 6-1, a Output List está em branco e a propriedade Values está ausente,
indicando que o operador gera uma única linha vazia. Também podemos ver que a linha está vazia passando o
mouse sobre o tubo de saída de dados do Constant Scan. Observe que o tamanho da linha
é 9 B (que indica apenas o cabeçalho da coluna).

Figura 6-4: A dica de ferramenta mostrando uma linha vazia retornou uma Constant Scan.

Às vezes, em um plano, você verá que um Constant Scan retorna uma linha vazia, essencialmente um espaço
reservado para informações que serão adicionadas por outros operadores dentro do plano, como um Compute
Scalar. Na Figura 6-1, o Constant Scan é seguido não por um, mas por dois deles.

163
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

O primeiro operador Compute Scalar lê cada uma das linhas do Constant Scan (neste caso, apenas
uma linha) e para cada linha chama uma função chamada getidentity, como você pode ver na propriedade
Defined Values desse operador.

[Expr1002] = Operador escalar(getidentity((373576369),(11),NULL))

É aqui que o SQL Server gera um valor de identidade, para a coluna AddressID, que é a Chave Primária
e é uma coluna IDENTITY. Os dois primeiros valores passados são o object_id e o database_id. Não
sei o que o terceiro parâmetro representa, mas aqui é um valor NULL.

O fato de esta operação preceder o INSERT e quaisquer verificações de integridade, dentro do plano,
ajuda a explicar por que, quando um INSERT falha, você ainda obtém uma lacuna nos valores de
IDENTITY de uma tabela. A entrada para este operador foi uma única linha vazia e, portanto, sua saída,
após adicionar Expr1002, é apenas uma única linha com uma coluna contendo o valor IDENTITY.

O segundo operador Compute Scalar lê a linha do operador anterior e adiciona a ela uma série de
colunas para a maioria dos valores parametrizados na consulta, além do novo valor de identificador
exclusivo (guid) e a data e hora da função GETDATE.

A propriedade Defined Values , na Figura 6-5, ilustra tudo isso.

Figura 6-5: Valores definidos do operador Compute Scalar.

As strings codificadas na consulta foram convertidas em variáveis com um tipo de dados de nvar
char(4000). A expressão para cada valor de coluna os converte de seu tipo de dados inferido para o tipo
de dados da coluna correspondente na tabela.

A saída deste segundo Compute Scalar, conforme confirmado por sua propriedade Output List , é
uma única linha com colunas contendo o valor IDENTITY (Expr1002) definido anteriormente, os valores
de parâmetro (Expr1003 – 1006), o valor guid (Expr1007) e o getdate
valor (Expr1008).

164
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

A razão pela qual temos 7 valores de coluna para inserir (sem incluir a identidade) e apenas 6 valores definidos
é que o tipo de dados inferido para a variável StateProvinceID é um INT, portanto, isso não precisa de
conversão.

O operador Clustered Index Insert recebe essa única linha, contendo todos esses valores.

Operador de inserção de índice clusterizado

O operador Clustered Index Insert representa a inserção de nossos dados no índice clusterizado. No
plano de execução da Figura 6-1, esta operação representa a maior parte do custo estimado deste plano
(92%). Provavelmente a propriedade mais importante deste operador, para este exemplo, é a propriedade
Object , mostrada na Figura 6-6.

Figura 6-6: Vários índices em exibição no operador Clustered Index Insert.

Você vê que a inserção afeta quatro índices diferentes, um sendo o índice clusterizado no qual inserimos a
nova linha e os outros três sendo três índices não clusterizados nesta tabela, aos quais os dados também
precisam ser adicionados. Nesse caso, esses índices não clusterizados adicionais são modificados adicionando-
os à lista de objetos do operador de modificação de índice clusterizado. A alternativa é que eles podem ser
modificados de dentro de seus próprios operadores (um plano por índice; veremos um plano DELETE por
índice mais tarde).

Índices filtrados e visualizações indexadas ou materializadas são sempre modificadas de dentro de seus
próprios operadores.

165
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

Você pode ver os parâmetros que foram criados e formatados no ScalarOperator


propriedade que está dentro da propriedade Predicate .

Figura 6-7: Parâmetros avaliados no ScalarOperator.

Esses dados são divididos nas propriedades do operador, mas são divididos individualmente, portanto, não
os tornam mais fáceis de ler. Destaquei o valor @4 do StateProvinceID, mencionado anteriormente, destacando
o fato de que ele lê essa variável diretamente, enquanto todas as outras colunas são definidas usando as
expressões Expr1003 e assim por diante, geradas anteriormente no operador Compute Scalar .

O próximo item de interesse é o valor da propriedade Output List , o Person.Address.


StateProvinceId conforme mostrado na Figura 6-8. Como essa coluna é uma FOREIGN KEY, o SQL Server
precisa verificar a integridade referencial.

Figura 6-8: Propriedade Lista de saída de uma inserção de índice clusterizado.

Chegamos agora ao familiar operador de junção Nested Loops (a parte final do plano é reproduzida na
Figura 6-9).

166
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

Figura 6-9: Seção do plano de execução com o operador Nested Loops.

O Nested Loops recebe a linha com o StateProvinceID que já foi inserido e, em seguida, chama
o Clustered Index Seek, que lê a coluna PRIMARY KEY da tabela pai para verificar se o valor que
estamos inserindo existe nessa coluna. Você notará que o operador Nested Loops está marcado
como Left Semi Join. Isso significa que ele está procurando apenas uma única correspondência
em vez de encontrar todas as correspondências. A saída dos loops aninhados
join é uma nova expressão, que é testada pelo próximo operador, Assert.

Operador de declaração

Um operador Assert verifica se uma determinada condição, ou condições, podem ser atendidas,
todas listadas na propriedade Predicate , que retorna NULL se todas forem atendidas. Cada valor
não NULL resulta em uma reversão; a mensagem de erro exata é determinada pelo valor real.

Neste exemplo, o operador Assert verifica se o valor de Expr1012 não é NULL. Ou, em outras
palavras, que os dados inseridos no campo Person.Address.StateProvinceId correspondiam a um
dado na tabela Person.StateProvince; esta foi a verificação referencial. Você pode ver isso na
propriedade Predicate na Figura 6-10.

167
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

Figura 6-10: A propriedade Predicate do operador Assert.

Planos para ATUALIZAÇÕES

As consultas UPDATE também funcionam em uma tabela por vez. Dependendo da estrutura da tabela e
das colunas a serem atualizadas, o efeito no plano de execução pode ser tão significativo quanto o mostrado
acima para a consulta INSERT. Considere a consulta UPDATE na Listagem 6-3.

ATUALIZAR Pessoa.Endereço
SET Cidade = 'Munro',
ModifiedDate = GETDATE()
WHERE Cidade = 'Monroe';

Listagem 6-3

A Figura 6-11 mostra o plano de execução estimado (não está incluída uma dica de índice ausente
sugerindo um possível índice na coluna Cidade, para ajudar no desempenho da consulta).

Figura 6-11: Plano de execução mostrando uma ATUALIZAÇÃO.

Mais uma vez, podemos começar a ler este plano verificando o operador UPDATE para ver o que está lá. No
entanto, neste caso, nada de novo é introduzido. Este plano passou por FULL
otimização e um "plano bom o suficiente encontrado" foi o motivo da rescisão antecipada.

Percorrendo o plano, lendo da direita para a esquerda, o primeiro operador é um Index Scan na tabela, que
varre todas as linhas neste índice e retornará apenas aquelas linhas WHERE
[City] = 'Monroe' (veja a propriedade Predicate do Index Scan).

168
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

O otimizador estima que retornará apenas 4,6 linhas, o que ajuda a explicar por que um índice em City foi
sugerido pelo otimizador. Como sempre, a criação ou não depende inteiramente da importância da consulta
em sua carga de trabalho ou de sua frequência de execução.

O operador Index Scan é chamado pelo próximo operador, um Table Spool (Eager Spool).

Operador de carretel de mesa (Eager Spool)

Como discutimos no Capítulo 5, o operador Table Spool fornece um mecanismo para armazenar os dados
recebidos em uma tabela de trabalho, para que possam ser reutilizados, talvez várias vezes, dentro de um
plano de execução. No entanto, esta é a primeira vez que encontramos um Eager Spool, que continua
solicitando linhas de seu operador filho até que tenha todas elas, e só então passará a primeira linha. Isso
significa que é um operador de bloqueio, que o otimizador geralmente tentará evitar. No entanto, neste caso,
é exatamente o comportamento necessário; ele existe para prevenir o problema do Halloween (veja: http://
en.wikipedia.org/wiki/Halloween_Problem).

O spool lê todas as linhas a serem atualizadas e as armazena em sua tabela de trabalho, e esses dados são
referenciados em todo o restante do processamento da consulta. Ao usar apenas essa tabela de trabalho
para direcionar o restante da consulta, garantimos que não veremos dados já atualizados novamente.

Os próximos três operadores são todos operadores Compute Scalar , que já vimos antes. Nesse caso,
eles são usados para avaliar expressões e produzir um valor escalar calculado, como a função GETDATE()
usada na consulta.

Após esses cálculos simples e claros, há também cálculos que criam o valor Expr1012 , derivado
do valor Expr1006 , que são menos fáceis de explicar.
Potencialmente, eles desempenham algum papel em garantir que os dados atualizados sejam atualizados
corretamente e com segurança, mas também podem ser um artefato de como o plano de execução é
gerado. Um operador Compute Scalar tem um custo muito baixo, a ponto de o otimizador às vezes nem se
preocupar em remover cálculos que não são mais necessários.

169
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

Operador de atualização de índice clusterizado

Agora chegamos ao núcleo da consulta UPDATE, o operador Clustered Index Update . Este operador lê
seus dados de entrada, usa-os para identificar as linhas a serem atualizadas e as atualiza. Se você examinar
a propriedade Object , verá que dois objetos estão sendo atualizados: o próprio índice clusterizado e um
índice não clusterizado que tem a coluna City como uma de suas chaves.

Neste exemplo, o operador Clustered Index Update está atualizando as linhas passadas de um Index
Scan, mas em alguns casos ele pode localizar as linhas a serem atualizadas sozinho, com base em um Predicate.
A Listagem 6-4 cria uma tabela muito simples, carrega uma linha nela e executa um UPDATE nessa linha.

CREATE TABLE dbo.Mytable (id INT IDENTITY(1, 1) PRIMARY KEY


AGRUPADO,
val VARCHAR(50));
INSERT dbo.Mytable (val)
VALUES ('whoop' -- val
);
UPDATE dbo.Mytable
SET val = 'GRITO'
ONDE id = 1;

Listagem 6-4

O plano de execução do UPDATE é muito simples, pois todo o trabalho é realizado diretamente no operador
Clustered Index Update . As linhas são filtradas e atualizadas no local. Você pode ver os detalhes observando
as propriedades do operador, a propriedade Seek Predicate , em particular.

170
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

Figura 6-12: Um plano de execução simples para um UPDATE.

Planos para DELETEs


Que tipo de plano de execução é criado para uma consulta DELETE? Vamos descobrir!

Um plano DELETE simples


Execute o código na Listagem 6-5 e capture o plano de execução real.

INICIAR TRAN;
DELETE FROM Person.EmailAddress WHERE
BusinessEntityID = 42; GO ROLLBACK TRAN;

Listagem 6-5

A Figura 6-13 mostra o plano de execução real.

Figura 6-13: Plano de execução simples para um DELETE.

171
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

Nem todos os planos de execução são complicados e difíceis de entender. Nesse caso, o operador
Clustered Index Delete define as linhas no índice clusterizado que precisam ser excluídas e as
exclui. Nem todos os planos DELETE parecerão tão simples se o otimizador precisar validar a
integridade referencial para a operação DELETE, mas, neste caso, isso não aconteceu.

O operador DELETE mostra um plano TRIVIAL e parametrização simples para ajudar a promover a
reutilização do plano. A Figura 6-14 mostra as propriedades do Clustered Index Delete.

Figura 6-14: Propriedades do operador DELETE de índice agrupado.

Como vimos anteriormente neste capítulo, a propriedade Object mostra que mais do que apenas o
índice clusterizado foi modificado. Mesmo com esse plano de execução muito simples, você pode ver
que a modificação do índice não clusterizado é coberta por esse operador. Além disso, você pode ver
como a linha ou linhas que serão excluídas são encontradas por meio do operador Seek Predicate .
Finalmente, dentro da expressão, você vê que ocorreu uma parametrização simples porque não
estamos comparando o valor real de 42 que foi fornecido, mas sim @1, um parâmetro.

172
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

Um plano DELETE por índice


Nos exemplos até agora, todos os índices não clusterizados foram modificados no mesmo operador que modifica o
índice clusterizado. Você verá isso referido, ocasionalmente, como um plano "estreito".
Outra maneira pela qual o otimizador pode escolher modificar todos os índices não clusterizados necessários em uma
tabela é processar a modificação de cada um separadamente, chamado de plano "amplo" ou "por índice".

Para ver um exemplo de um plano DELETE amplo, primeiro criaremos uma visualização materializada e, em seguida,
excluiremos alguns dados.

CRIAR OU ALTERAR VISUALIZAÇÃO dbo.TransactionHistoryView


COM LIGAÇÃO DE ESQUEMAS

COMO

SELECT COUNT_BIG(*) AS ProductCount,


th.ProductID
FROM Production.TransactionHistory AS th
Agrupar por th.ProductID
VAI
CRIAR ÍNDICE AGRUPADO ÚNICO TransactionHistoryCount
ON dbo.TransactionHistoryView(ProductID)
VAI
INICIAR TRAN;
DELETE FROM Production.TransactionHistory
ONDE ProductID = 711;
ROLLBACK TRAN;

Listagem 6-6

O plano de execução resultante é muito mais complexo do que antes.

Figura 6-15: Um plano de execução DELETE por índice.

Ao ler este plano, vamos começar pelo lado esquerdo, seguindo a ordem de execução. Há duas coisas que devemos
abordar aqui, antes de voltarmos para a ordem de fluxo de dados das operações. A Figura 6-16 mostra os dois primeiros
operadores do plano.

173
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

Figura 6-16: O operador DELETE recebendo informações do operador Sequence.

Depois do operador DELETE, que já discutimos neste capítulo, o próximo operador, na ordem de
execução, é o operador Sequence . Leva um certo número de entradas, neste caso duas, e as processa
em ordem precisa, de cima para baixo. As entradas são objetos relacionados nos quais os dados devem
ser modificados e as operações devem ser executadas na sequência correta. Em nosso exemplo, o
otimizador precisa excluir dados de um índice clusterizado e seus índices não clusterizados associados e,
em seguida, de um segundo índice clusterizado que define a exibição materializada.

Com um operador Sequence , quase sempre como parte de um UPDATE ou DELETE, cada entrada
representa um objeto diferente dentro do banco de dados. Mesmo que vários valores possam ser retornados
de várias entradas para o operador Sequence , apenas a entrada inferior, a entrada final, é passada.

Isso torna a Sequência um operador de bloqueio parcial, pois todo o processamento de uma entrada
deve ser concluído antes que a próxima seja iniciada. Somente quando todas as outras entradas forem
concluídas e a entrada inferior for iniciada, a Sequência começará a passar as linhas recebidas para o
próximo operador. Entender que estamos lidando com o operador Sequence tornará o restante do plano
mais fácil de entender.

A Figura 6-17 mostra os operadores que compõem a entrada superior para o operador Sequência .

Figura 6-17: Os operadores no topo inserem um operador Sequence.

O início do processamento na direção do fluxo de dados começa com uma operação Index Seek no
índice não clusterizado IX_TransactionHistory_ProductID. A saída desse índice é uma listagem de
valores TransactionID que correspondem ao valor de entrada de 711, fornecido para ProductID, da
Listagem 6-6.

174
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

Essa listagem de valores de TransactionID então vai para a operação Clustered Index Delete que
cuidará da remoção de todos os dados do índice clusterizado que define a tabela.
A Figura 6-18 mostra a saída do operador Clustered Index Delete .

Figura 6-18: A propriedade Lista de saída de uma exclusão de índice clusterizado.

Se você verificar a saída, verá a coluna ProductID, que será usada em outro lugar no plano. A saída é
então carregada em um operador Table Spool para uso posterior. Sempre que você começar a lidar com
spools de tabela, é sempre uma boa ideia obter o valor NodeID (neste caso, é 2), que você pode encontrar
nas Propriedades ou na dica de ferramenta (mais sobre isso em breve).

O Spool de Tabela é apenas armazenamento temporário para uso posterior no plano e nada mais é feito
com esses dados durante esse processo, exceto para carregá-lo no Spool para uso posterior. A operação
lógica é um Eager Spool. Um Eager Spool primeiro coletará todas as informações dos operadores
anteriores antes de passar qualquer linha. Isso significa que todas as linhas que correspondem aos nossos
critérios, ProductID = 711, já foram excluídas, antes que o restante do plano receba quaisquer dados desse
operador.

Isso completa a entrada principal para o operador Sequence . A Figura 6-19 mostra a entrada inferior.

Figura 6-19: Preencha a entrada inferior do Operador de Sequência.

Vamos detalhar isso um pouco mais, para facilitar a leitura, com a Figura 6-20 mostrando a extremidade
direita do plano, até o operador Nested Loops .

175
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

Figura 6-20: Identificando as linhas correspondentes na visualização materializada.

Começamos com outro operador Table Spool . Este operador Table Spool possui seu próprio NodeID, mostrando
onde ele se enquadra no processamento do plano. No entanto, ele tem uma informação adicional, o ID do Nó
Primário, indicando que está reutilizando os dados armazenados no Spool de Tabela localizado na entrada superior.

Figura 6-21: Propriedades do operador Table Spool.

Todas essas informações foram carregadas uma vez da saída do Clustered Index Delete
operador, na entrada superior, e agora será reutilizado neste conjunto de operações na entrada inferior.

O próximo operador é um operador Stream Aggregate (consulte o Capítulo 5), que obtém a saída dos valores
excluídos no índice clusterizado e os agrega para fazer com que correspondam aos dados na visualização
materializada. A junção de loops aninhados adiciona os dados correspondentes, pois estão armazenados atualmente
na visualização materializada.

A Figura 6-22 mostra a próxima seção da entrada inferior do operador Sequence .

176
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

Figura 6-22: O DELETE da visualização materializada.

O Compute Scalar calcula o novo valor para uso na visualização materializada subtraindo o número de
linhas excluídas por ProductID (conforme calculado no Stream Aggregate) dos dados armazenados
originalmente. O operador Table Spool tem seu próprio NodeID e nenhum Parent NodeID, portanto,
não está reutilizando dados de outro lugar. Neste caso, está novamente protegendo contra o Problema
do Dia das Bruxas. Por fim, vemos uma atualização de índice clusterizado que modifica os dados na
própria visão materializada.

Este exemplo ilustra a maneira alternativa de manter índices em planos de modificação de dados.
Cabe ao otimizador decidir usar um dos métodos ou uma mistura. Essa decisão é, como sempre,
baseada em estimativas sobre o custo de manter os índices em ordem aleatória, versus o custo de
salvar as linhas em um Spool de Tabela, classificá-las e, em seguida, manter os índices com dados
pré-ordenados. Embora este exemplo tenha mostrado um plano DELETE, as mesmas opções se
aplicam aos planos INSERT, UPDATE e MERGE.

Largue a visão materializada antes de continuarmos.

DROP INDEX TransactionHistoryCount ON dbo.TransactionHistoryView;


VAI
DROP VIEW dbo.TransactionHistoryView;
VAI

Listagem 6-7

Planos para consultas MERGE


Com o SQL Server 2008, a Microsoft introduziu a consulta MERGE. Este é um método para modificar
dados em seu banco de dados em uma única consulta, em vez de uma consulta para INSERTs, uma
para UPDATEs e outra para DELETEs. O apelido para isso é um "upsert". A aplicação mais simples
da consulta MERGE é executar um UPDATE se houver valores de chave existentes em uma tabela, ou
um INSERT se eles não existirem. A consulta nas linhas UPDATEs ou INSERTs da Listagem 6-7 para
a tabela Purchasing.Vendor.

177
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

DECLARE @BusinessEntityId INT = 42, @AccountNumber


NVARCHAR(15) = N'SSHI', @Name NVARCHAR(50) = N'Shotz
Beer', @CreditRating TINYINT = 2, @PreferredVendorStatus
BIT = 0, @ActiveFlag BIT = 1, @PurchasingWebServiceURL
NVARCHAR(1024) = N'http:// shotzbeer.com', @ModifiedDate
DATETIME = GETDATE(); COMEÇAR A TRANSAÇÃO; MERGE
Purchasing.Vendor AS v USING ( SELECT @BusinessEntityId, @AccountNumber,
@Name, @CreditRating, @PreferredVendorStatus, @ActiveFlag, @PurchasingWebServiceURL,
@ModifiedDate) AS vn (BusinessEntityId, AccountNumber,

NAME, CreditRating, PreferredVendorStatus, ActiveFlag, PurchasingWebServiceURL,


ModifiedDate)
ATIVADO (v.AccountNumber = vn.AccountNumber)
QUANDO COMPARADO,
ENTÃO ATUALIZAR SET v.Name = vn.NAME,
v.CreditRating = vn.CreditRating, v.PreferredVendorStatus
= vn.PreferredVendorStatus, v.ActiveFlag = vn.ActiveFlag, v.PurchasingWebServiceURL
=

vn.PurchasingWebServiceURL,
v.ModifiedDate = vn.ModifiedDate
QUANDO NÃO CORRESPONDE
INSERT (ID da Entidade Empresarial,
Número da conta,
Nome,
Classificação de crédito,
Status de fornecedor preferencial,
ActiveFlag,
ComprasWebServiceURL,
Data modificada)

178
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

VALUES (vn.BusinessEntityId, vn.AccountNumber, vn.NAME,


vn.CreditRating, vn.PreferredVendorStatus, vn.ActiveFlag,
vn.PurchasingWebServiceURL, vn.ModifiedDate);
TRANSAÇÃO DE ROLLBACK;

Listagem 6-8

Essa consulta usa a chave alternativa, a coluna AccountNumber, em Compras.


Tabela de fornecedores. Se o valor fornecido (neste caso, 'SSHI') corresponder a um valor de chave nesta coluna,
a consulta executará um UPDATE e, se não corresponder, executará um INSERT.
A Figura 6-22 mostra o plano de execução.

Figura 6-23: Plano completo para a consulta MERGE.

Como você pode ver, esse plano é um pouco grande para o livro, então vou dividir esse plano na ordem da
direita para a esquerda.

Figura 6-24: Carregando o Constant Scan e verificando uma linha.

Esta primeira seção do plano contém uma série de etapas para se preparar para as principais operações que estão
por vir. O Constant Scan gera uma linha vazia, um espaço reservado para dados para que todos os operadores
tenham informações para trabalhar, mesmo que seja um conjunto vazio. Os Loops Aninhados
O operador usa esta linha vazia para conduzir uma única execução de sua entrada interna, onde o Index Seek contra
o índice não clusterizado Vendor.AK_Vendor_AccountNumber puxará de volta quaisquer linhas a serem atualizadas
(ou seja, que correspondam ao Predicado Seek fornecido). Esperaríamos no máximo uma linha, já que é um índice
UNIQUE, mas, neste caso, as Propriedades para o fluxo de dados entre o Index Seek e o primeiro Compute Scalar
revelam zero linhas retornadas.

179
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

Figura 6-25: Propriedades do operador Compute Scalar.

Para cada linha que recebe, o operador Compute Scalar cria um valor TrgPrb1001 e o define com o
valor 1, como você verá no valor da propriedade Defined Values para o operador.

O operador Nested Loops combina a coluna vazia do Constant Scan com os dados (se houver) do
Compute Scalar, usando uma Left Outer Join. Se, como no nosso caso, nenhum dado for retornado
pelo Compute Scalar, ele ainda retornará uma linha, usando valores NULL. O efeito disso é que o
valor 1 é passado para TrgPrb1001 se o Index Seek encontrar uma linha, ou NULL se não encontrar.
Isso é usado posteriormente no plano para determinar se existem linhas para UPDATE ou DELETE.

A próxima parte do plano é uma série de operações Compute Scalar , conforme mostrado na Figura 6-25.

Figura 6-26: Vários cálculos em relação aos dados para determinar o que fazer com eles.

A parte difícil de ler um plano como este é tentar descobrir o que cada um dos operadores Compute
Scalar faz. Isso é revelado pelos valores de propriedade Defined Values e Output List . Trabalhando
da direita novamente, o primeiro operador Compute Scalar na Figura 6-25 realiza um cálculo:

[Action1003] = Operador Escalar(ForceOrder(CASE WHEN [TrgPrb1001] IS


NOT NULL THEN (1) ELSE (4) END))

Esse operador Compute Scalar cria um novo valor, chamado Action1003 no meu caso, e como
TrgPrb1001 é nulo, o valor é definido como "4". Dependendo da sua versão do SQL Server e das
atualizações aplicadas, você poderá ver valores diferentes para Action1003 ou Expr1005 ou qualquer
um dos vários valores gerados no plano, mesmo que você tenha um plano idêntico. Isso simplesmente
reflete pequenas alterações no otimizador e na ordem em que ele inicializa cada uma dessas expressões.

180
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

O próximo operador Compute Scalar carrega todos os valores de variáveis na linha e executa dois
outros cálculos:

[Expr1004] = Operador Escalar([@ActiveFlag]), [Expr1005] = Operador


Escalar([@PurchasingWebServiceURL]), [Expr1006] = Operador
Escalar([@PreferredVendorStatus]), [Expr1007] = Operador Escalar([@CreditRating] ),
[Expr1008] = Operador escalar(CASE WHEN [Action1003]=(4) THEN [@BusinessEntityId]
ELSE [AdventureWorks2014].[Purchasing].[Vendor].[BusinessEntityID] como [v].
[BusinessEntityID] END), [Expr1009] = Operador Escalar([@

ModifiedDate]), [Expr1010] = Operador escalar([@Name]), [Expr1011]


= Operador escalar(CASE WHEN [Action1003]=(4) THEN [@AccountNumber]
ELSE [AdventureWorks2014].[Compras].[Fornecedor].[AccountNumber] como [v].
[AccountNumber] END)

Observando a expressão para Expr1011, podemos começar a entender o que está acontecendo.
A primeira saída Compute Scalar , TrgPrb1001, determinou se a linha existia na tabela. Se
existisse, o segundo Compute Scalar teria definido Action1003 igual a 1, significando que a linha
existia, e esse novo Compute Scalar teria usado o valor da tabela, mas, em vez disso, está avaliando
Action1003 e escolhendo a variável @
AccountNumber, pois é necessário um INSERT. A mesma lógica é usada em Expr1008 para o valor
BusinessEntityId. O resultado deste Compute Scalar é que todas as expressões mantêm o valor correto
para INSERT ou UPDATE, conforme determinado pelo Action1003.

Movendo-se para a esquerda, o próximo operador Compute Scalar valida o que é Action1003 e define
um novo valor, Expr1023, com base nesta fórmula:

[Expr1023] = Operador Escalar(CASE WHEN [Ação1003] = (1) THEN


(0) ELSE [Ação1003] END)

Sabemos que Action1003 está definido como 4, portanto, essa expressão será definida como 4.

O operador Compute Scalar final define dois valores iguais a eles mesmos, por algum motivo que
não está completamente claro para mim. Pode ser algum processo interno dentro do otimizador que é
evidenciado aqui no plano de execução. Finalmente, estamos prontos para prosseguir com o restante
do plano de execução.

181
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

Figura 6-27: Etapas finais na operação de mesclagem.

O Clustered Index Merge recebe todas as informações adicionadas ao fluxo de dados pelos
vários operadores e as usa para determinar se a ação é um INSERT, um UPDATE ou um
DELETE e executa essa ação. Você pode ver o resultado na Coluna de Ação
propriedade do operador, na Figura 6-27, que mostra um valor de Action1003.

Figura 6-28: Valores da coluna de ação para o operador Clustered Index Merge.

Claro, neste caso, é apenas um INSERT ou UPDATE. Você pode até ver as informações na
propriedade Predicate do operador.

Figura 6-29: Valores de predicado do operador Clustered Index Merge.

182
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

Apropriadamente, neste caso, devido a todo o trabalho que a operação Merge deve realizar na modificação de
dois índices, o otimizador estima que esta operação representará 75% do custo do plano de execução.

Em seguida, um operador Assert executa uma verificação em relação a uma restrição no banco de dados,
validando se os dados estão dentro de um determinado intervalo. Os dados passam para o operador de loops
aninhados , que é usado para recuperar valores usados para validação de que a integridade referencial de
BusinessEntityId está intacta, por meio da busca de índice clusterizado na tabela BusinessEntity. Esta ação só
é executada neste caso, pois se trata de uma operação INSERT, conforme determinado anteriormente pela
definição do valor de Action1003. O operador Nested Loops possui uma função Pass Through, que ignora a
chamada da entrada interna, em outros casos. Podemos ver isso na Figura 6-30.

Figura 6-30: A avaliação que determina se é um Pass Through.

As informações coletadas por essa junção passam para outro operador Assert , que valida a integridade
referencial, assumindo que foi uma ação INSERT. A consulta é então concluída.

Como você pode ver, muita ação acontece dentro dos planos de execução, mas, com uma análise cuidadosa, é
possível identificar a maior parte do que está acontecendo.

Antes da consulta MERGE, você pode ter feito uma consulta desse tipo dinamicamente. Você tinha
procedimentos diferentes para cada um dos processos ou consultas diferentes dentro de uma cláusula IF.
De qualquer forma, você acabou com vários planos de execução no cache, para cada processo. Este não é
mais o caso. Se você modificar a consulta na Listagem 6-7 e alterar um valor simples como na Listagem 6-9…


@AccountNumber NVARCHAR(15) = 'SPEEDCO0001',

Listagem 6-9

…a mesma consulta exata com o mesmo plano de execução agora atualizará os dados para valores em que o
AccountNumber é igual ao passado pelo parâmetro. Portanto, este plano, com o operador Merge , cria um único
plano reutilizável para todas as operações de manipulação de dados que ele suporta.

183
Machine Translated by Google

Capítulo 6: Planos de Execução para Modificações de Dados

Resumo
Este capítulo tratou dos planos para consultas de modificação de dados relativamente simples.
As principais lições são que você lê essas consultas da mesma forma que lê uma consulta
SELECT e usa as mesmas ferramentas, como propriedades e custos estimados, para tentar
entender como e por que o otimizador implementou o plano dessa maneira.

184
Machine Translated by Google

Capítulo 7: Planos de Execução para Comum


Instruções T-SQL
Nos Capítulos 2 a 6, lidamos com consultas T-SQL de instrução única. Como vimos, algumas vezes até
mesmo essas consultas relativamente simples podem gerar planos de execução complicados. Neste
capítulo, estenderemos nosso escopo para considerar planos para instruções e objetos T-SQL comuns,
como procedimentos armazenados, subconsultas, tabelas derivadas, expressões de tabela comuns,
exibições e funções.

À medida que as instruções T-SQL se tornam mais complexas, os planos que o otimizador cria podem
ficar maiores e mais demorados para decifrar. No entanto, assim como uma grande instrução T-SQL pode
ser dividida em uma série de etapas simples, os grandes planos de execução são simplesmente extensões
dos mesmos planos simples que já examinamos, apenas com mais e diferentes operadores.

Novamente, lembre-se de que os planos que você vê, se você acompanhar, podem variar um pouco do que é
mostrado no texto, devido a diferentes níveis de service pack, hot-fixes, diferenças no banco de dados
AdventureWorks, suas estatísticas e dados.

Procedimentos armazenados

O melhor lugar para começar é com procedimentos armazenados, que podem incluir uma única consulta
ou uma série inteira de consultas. No último caso, você verá vários planos de execução, mas a maneira
como você lida com cada um desses planos não é diferente de qualquer outro plano de execução.

A Listagem 7-1 mostra um procedimento armazenado TaxRateByState, cuja intenção é retornar


informações sobre alíquotas de imposto que são menores que um valor definido, neste caso 7.5. Este é
um exemplo típico de um procedimento que provavelmente foi construído ao longo do tempo, por alguém
que não é especialista em T-SQL. Envolve uma série de etapas para reunir alguns dados, manipular esses
dados e retornar um conjunto de resultados. Existem circunstâncias em que esta abordagem se justifica, mas
outras em que não é a solução ideal.

CRIAR OU ALTERAR PROCEDIMENTO Sales.TaxRateByState @CountryRegionCode NVARCHAR(3)

COMO

DEFINIR NOCOUNT ON;


CRIAR TABELA #TaxRateByState

185
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

(
SalesTaxRateID INT NOT NULL,
TaxRateName NVARCHAR(50) COLLATE DATABASE_DEFAULT NOT NULL,
TaxRate SMALLMONEY NOT NULL,
TaxType WHITENOT NULL,
StateName NVARCHAR(50) COLLATE DATABASE_DEFAULT NOT NULL
);
INSIRA EM #TaxRateByState (

SalesTaxRateID,
TaxRateName,
Taxa de imposto,
Tipo de imposto,
Nome do Estado
)
SELECT st.SalesTaxRateID, st.Name,
st.TaxRate, st.TaxType,
sp.Name AS StateName
FROM Sales.SalesTaxRate
AS st

JOIN Person.StateProvince AS sp ON
st.StateProvinceID = sp.StateProvinceID
WHERE sp.CountryRegionCode = @CountryRegionCode; DELETE
#TaxRateByState WHERE TaxRate < 7,5; SELECT soh.SubTotal,
soh.TaxAmt, trbs.TaxRate, trbs.TaxRateName

FROM Sales.SalesOrderHeader AS soh


JOIN Sales.SalesTerritory AS st ON
st.TerritoryID = soh.TerritoryID JOIN
Person.StateProvince AS sp
ON sp.TerritoryID = st.TerritoryID JOIN
#TaxRateByState AS trbs
ON trbs.StateName = sp.Name;
VAI

Listagem 7-1

Seria possível escrever a mesma lógica em uma única consulta, sem a necessidade de uma
tabela temporária. No entanto, este é o tipo de código que você encontra em sistemas da vida real, e

186
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

às vezes você só precisa entender a causa do problema de desempenho, por meio do plano, e decidir sobre
uma correção, sem necessariamente ter tempo, ou mesmo oportunidade, para fazer
uma reescrita completa.

Além disso, observe que NVARCHAR(3) não é o melhor tipo de dados para uso no parâmetro @CountryRegion
Code; CHAR(3) seria muito mais eficiente e sensato. No entanto, NVARCHAR(3) é o tipo de dados usado para
essa coluna, na tabela, portanto, o procedimento armazenado segue o exemplo, para evitar problemas de
conversão de tipo de dados.

Podemos executar o procedimento armazenado passando um valor, conforme mostrado na Listagem 7-2.

EXEC Sales.TaxRateByState @CountryRegionCode = N'US';


VAI

Listagem 7-2

A Figura 7-1 mostra o plano de execução real resultante, que é um pouco mais complexo do que os que vimos
anteriormente.

Figura 7-1: Vários planos de execução de um único procedimento armazenado.

187
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Um ponto interessante é que não temos um procedimento armazenado à vista. Em vez disso, o otimizador
trata o T-SQL dentro do procedimento armazenado da mesma maneira como se tivéssemos escrito e executado
a instrução SELECT, por meio da janela de consulta.

Quanto mais instruções forem adicionadas a um determinado procedimento armazenado, mais planos de
execução você verá. No caso de algum tipo de consulta em loop, você pode ver centenas de planos de execução.
A captura de todos os planos de execução nesses casos pode causar problemas de desempenho com o SSMS.
Se você estiver lidando com essa situação, sua abordagem deve ser usar um plano estimado sempre que
possível. Se você precisar ver um plano real, capture planos para instruções individuais usando uma sessão de
Evento Estendido filtrada ou use instruções SET STATISTICS XML ON e OFF, se puder modificar o código.

O procedimento armazenado na Listagem 7-1 tem cinco instruções, mas vemos apenas três planos de execução
na Figura 7-1. A instrução Data Definition Language (DDL) para criar a tabela temporária, #TaxRateByState, não
obtém um plano de execução. Uma instrução DDL só pode ser resolvida de uma maneira, para que não passe
por otimização, portanto, não há plano de execução. Também não vemos um plano para a instrução SET
NOCOUNT. Um plano estimado mostrará um operador T-SQL para essas instruções, mas não qualquer tipo de
plano de execução mais completo.

Assim como quando executamos um lote contendo duas ou mais consultas, para um procedimento
armazenado contendo duas ou mais instruções, o plano de execução mostra o custo estimado de cada
consulta, em relação ao lote. Esses valores aparecem como o custo da consulta (relativo ao lote), no início de
cada plano, e podemos usá-los para identificar o plano que precisa de mais atenção, para ajuste de desempenho.
Como sempre, porém, trate esses custos estimados com cautela e use-os apenas se não houver grande
disparidade entre as contagens de linhas estimadas e reais.

A consulta 1 representa cerca de 3% do custo total e é o plano para preencher a tabela temporária com
informações de alíquota de imposto para cada estado do país fornecido, neste caso os EUA. Não vamos
explorar o plano em detalhes, mas vale a pena dar uma olhada nas propriedades do operador INSERT.

Figura 7-2: Propriedades do operador INSERT mostrando a Lista de Parâmetros.

188
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

As propriedades de valor interessantes aqui estão na Lista de Parâmetros, que contém o Valor Compilado
do Parâmetro, o valor do parâmetro que o otimizador usou para compilar o plano para o procedimento
armazenado. Abaixo está o Parameter Runtime Value, mostrando o valor quando esta consulta foi chamada.

Quando executamos o lote na Listagem 7-2, para executar o procedimento armazenado, o SQL Server
primeiro compila apenas o lote e define o valor de @CountryRegionCode como N'US'. Em seguida, ele
executa o comando EXEC e verifica no cache do plano para ver se há um plano para executar o procedimento
armazenado. Nesse caso, não há, então ele invocará o compilador novamente para criar um plano para o
procedimento. Nesse ponto, o otimizador pode "farejar" o valor do parâmetro e gerar um plano, usando
estatísticas para esse valor. Se executarmos o procedimento armazenado novamente com um valor de
parâmetro diferente, desta vez haverá um plano que o otimizador pode reutilizar e veremos um valor de tempo
de execução diferente, mas o mesmo valor compilado.

Figura 7-3: Propriedades do operador SELECT com alterações nos parâmetros.

Isso só é significativo se o valor compilado retornar uma contagem de linhas muito "atípica" em comparação
com a maioria dos valores usados para executar o procedimento. A seção sobre Índices e seletividade, no
Capítulo 8, fornece mais informações sobre detecção de parâmetros e valores compilados, portanto, não
entraremos em mais detalhes aqui.

A consulta 2 é o plano para excluir linhas que ficam abaixo do valor limite da nossa taxa de imposto, que
neste caso deixa apenas 5 linhas na tabela temporária.

A consulta 3 se junta à nossa tabela temporária, e várias outras, para retornar nossos resultados. Essa
consulta parece ser o ponto de partida para nossa investigação séria, pois o otimizador acha que ela
representa a maior parte (96%) do custo de execução do procedimento armazenado, conforme mostrado na Figura 7-4.

189
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Figura 7-4: O plano de execução da Consulta 3, 96% do custo estimado do lote.

Visualmente, não é um plano muito complexo, mas há muita coisa acontecendo. Começando à direita, temos um
operador de junção Nested Loops onde a entrada externa é uma varredura da tabela temporária, que retorna 5
linhas. Isso incorrerá em 5 execuções da entrada interna, um Index Seek na tabela StateProvince. A saída desse
operador de junção de loops aninhados é a entrada externa de outra junção de loops aninhados , portanto,
obtemos 5 execuções na entrada interna, uma pesquisa de chave
no índice clusterizado da tabela StateProvince para recuperar os valores não armazenados no índice não
clusterizado, neste caso, os valores TerritoryID.

A saída da segunda junção de Loops Aninhados é a entrada Build para um operador de junção Hash Match ,
onde a entrada Probe é um Clustered Index Seek em relação ao SalesOrderHeader
tabela.

O operador Hash Match lê a entrada Build, faz o hash da coluna de junção (neste caso, TerritoryID) e
armazena os valores da coluna e seus hashes em uma tabela de hash na memória.
Em seguida, ele lê as linhas na entrada do Probe uma linha de cada vez, neste caso 31465 linhas e, para cada
linha, produz um valor de hash para a coluna TerritoryID que pode comparar com os hashes na tabela de hash,
procurando correspondência valores e começa a ajustar novamente as linhas correspondentes (23752 no total).

Como você pode ver, os planos de execução para procedimentos armazenados não são especiais e não são
diferentes de outros planos de execução. Você só precisa identificar o plano, ou planos, que estão causando o
problema e avaliar possíveis correções.

190
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Subconsultas
Uma abordagem comum e útil, mas ocasionalmente problemática, para consultar dados é selecionar
informações de outras tabelas dentro da consulta, mas não como parte de uma instrução JOIN. Em vez
disso, incorporamos uma instrução SELECT em outro SELECT, INSERT, UPDATE ou DELETE
declaração. Podemos usar uma subconsulta em qualquer parte da consulta onde as expressões são
permitidas, mas você as verá mais comumente nas cláusulas WHERE, SELECT e FROM.

A Listagem 7-3 ilustra uma subconsulta correlacionada que acessa a tabela


Production.ProductionListPriceHistory. Esta tabela mantém um histórico de preços para cada produto e os
intervalos de datas para os quais um determinado preço era válido. É bastante comum ver subconsultas
usadas assim, para tabelas que contêm dados "versionados". Nesse caso, usamos isso para garantir que
vejamos apenas a "versão" mais recente do preço de tabela de cada produto.

No entanto, por razões que discutiremos ao examinar o plano, não é necessariamente a solução
ideal.

SELECT p.Nome,
p.ProdutoNúmero,
ph.ListPrice
DA Produção.Produto AS p
INNER JOIN Produção.ProductListPriceHistory AS ph
ON p.ProductID = ph.ProductID
AND ph.StartDate = ( SELECT TOP (1)
ph2.StartDate
DA Produção.
ProductListPriceHistory AS ph2
ONDE ph2.ProductID = p.ProductID
ORDEM POR ph2.StartDate DESC);

Listagem 7-3

Observe que a subconsulta faz referência aos valores ProductID da consulta externa, portanto, para cada
linha da consulta externa, o valor ProductID dessa linha é conectado à subconsulta e comparado com o
valor ProductID da tabela ProductListPriceHistory. Como resultado, a subconsulta é executada uma vez
para cada linha retornada pela consulta externa. O TOPO (1)
A cláusula ORDER BY garante que, em cada caso, a subconsulta retorne apenas a linha mais recente
(mostrando o preço de lista atual). Dependendo da consulta, às vezes o otimizador pode descobrir uma
maneira mais eficiente de alcançar os resultados desejados. Como veremos, esta não é uma dessas
situações.

191
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

A Figura 7-5 mostra o plano de execução real.

Figura 7-5: Plano de execução de uma subconsulta.

Lendo o plano da direita para a esquerda, vemos duas Varreduras de Índice Agrupado, uma em
Production.Product e outra em Production.ProductListPriceHistory. Esses dois fluxos de dados são combinados
usando o operador Merge Join , usando ProductID como a coluna de junção; você pode ver isso na propriedade
Where (join columns) no operador Merge Join .

Figura 7-6: Colunas de junção de mesclagem definidas.

Como o Merge Join requer que ambas as entradas de dados sejam ordenadas na chave de junção, neste caso o
ProductID, você verá que a propriedade Ordered está definida como True para cada uma das verificações.

192
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Isso significa que o mecanismo de execução usará o método de recuperação Ordered para preenchê-los
(consulte o Capítulo 5), e os dados serão recuperados na ordem do índice lógico, em cada caso. Neste
exemplo, ambos os índices clusterizados são ordenados por ProductID.

Figura 7-7: Varredura de Índice Agrupado mostrando que é Ordenado.

Assim, o Merge Join simplesmente pega os dados de duas entradas e usa o fato de que os dados em
cada entrada são ordenados na coluna de junção para mesclá-los, unindo linhas com base nos valores
correspondentes. Você pode consultar o Capítulo 4 para obter mais detalhes sobre como os vários tipos
de Merge Join funcionam.

Existem 395 linhas mescladas, que são as 395 linhas com entradas de preço de tabela. Aliás, essa é
claramente uma distribuição de dados atípica, pois há 504 produtos na tabela Produtos e, em geral, você
espera que haja uma ou mais entradas de lista de preços para cada produto. De qualquer forma, essas linhas
formam a entrada externa para um operador de junção de loops aninhados , o que implica que a entrada
interna será executada 395 vezes. Se você verificar a propriedade Referências externas dos loops
aninhados, verá que os valores das colunas ProductID e StartDate estão sendo enviados para a entrada interna.

O índice clusterizado na tabela ProductListPriceHistory está ativado (ProductID, StartDate) e, para cada
execução, procuramos linhas que correspondam ao ProductID
valor empurrado para baixo da entrada externa. No entanto, o operador TOP garante que ele leia apenas
a linha com a StartDate mais recente (lembre-se de que a ordem de execução é da esquerda para a direita).
O operador Filter passará ou rejeitará essa única linha, dependendo se houver uma correspondência em
StartDate (o outro valor de coluna pressionado). A Figura 7-8 mostra o valor da propriedade Predicate expandida
para o operador Filter .

193
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Figura 7-8: Detalhes da propriedade Predicate.

Alguns outros pontos a serem observados aqui. Primeiramente, o Filtro é executado 395 vezes (assim como
seus operadores filhos). Ele retorna a linha mais recente para cada um dos 293 valores de ProductID
distintos; você pode ver na Lista de Saída que ela não retorna nenhum dado, apenas um shell de linha vazio
para cada linha que passa em seus critérios de filtro. A linha em si está vazia porque a única coisa que o
Nested Loops precisa para tomar sua decisão é a presença ou ausência de uma linha. Por fim, observe que
a Expressão de Inicialização é Falsa nesse caso, o que significa que os operadores filhos serão chamados
para cada execução. Se você visse o Predicado de Expressão de Inicialização, os operadores filho só
seriam chamados para linhas que atendessem a essa condição de Predicado.

Felizmente, está claro que o problema fundamental com esse plano é o número de execuções da entrada
interna da junção de loops aninhados . Imagine alguns números diferentes: digamos que temos 200
produtos e uma média de 15 preços por produto no histórico ProductListPrice History. O Merge Join
produzirá 3.000 linhas, portanto, a entrada externa do operador Nested Loops tem 3.000 linhas e a entrada
interna é executada 3.000 vezes, lendo as mesmas 200 linhas repetidamente. Isso causaria um grande
número de leituras lógicas; o otimizador provavelmente escolherá um plano diferente nessas condições, se
puder encontrar um.

Há muitas maneiras de tentar otimizar essa consulta e não posso abordá-las todas aqui. Uma opção
seria substituir a lógica SELECT TOP(N)…ORDER BY por SELECT MAX(ph2.StartDate)…. Se você tentar
isso, verá uma mudança de uma junção de loops aninhados para duas junções de mesclagem e uma
melhoria no desempenho. Experimente e leia o plano. Outra opção é usar uma tabela derivada em vez de
uma subconsulta e veremos como isso funciona na próxima seção.

194
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Tabelas derivadas usando APPLY


Uma das maneiras de acessar dados por meio do T-SQL é por meio de uma tabela derivada. Se você não
estiver familiarizado com eles, pense em uma tabela derivada como uma tabela virtual criada dinamicamente de dentro
uma instrução SELECT.

Você cria tabelas derivadas escrevendo uma subconsulta dentro de um conjunto de parênteses no FROM
cláusula de uma consulta externa. Depois de aplicar um alias, essa subconsulta é tratada como uma tabela
pelo código T-SQL. Antes do SQL Server 2005, qualquer tabela derivada precisava ser totalmente independente
da consulta principal. No entanto, o SQL Server 2005 introduziu o operador APPLY, que nos permite criar uma
correlação entre a consulta principal e a tabela derivada. O operador APPLY avaliará a subconsulta (ou função
com valor de tabela) uma vez para cada linha produzida pela parte da cláusula FROM à esquerda da cláusula
APPLY. Esta é a definição lógica; o otimizador é, obviamente, livre para encontrar uma implementação diferente e
mais rápida, se puder.

Existem duas formas do operador APPLY, CROSS APPLY e OUTER APPLY. O primeiro combina cada linha da
entrada esquerda com cada linha retornada da entrada direita. O último faz o mesmo, mas também retém a linha
da entrada esquerda se nada for retornado da entrada direita, usando valores NULL para colunas originadas da
entrada direita. Se você não estiver familiarizado com o operador Apply , confira http://bit.ly/1FFmldl (é uma entrada
MSDN para SQL Server 2008R2, mas ainda está correta).

Em meu próprio código, um lugar em que uso tabelas derivadas com frequência é ao lidar com dados que mudam
ao longo do tempo, para os quais devo manter o histórico. Essa abordagem de consulta, mostrada na Listagem
7-4, é uma alternativa à subconsulta que vimos na Listagem 7-3. Ele produz os mesmos resultados da Listagem
7-3, mas usa o operador APPLY. A grande diferença é que os dados ficam disponíveis para o restante da consulta,
quando a subconsulta está no FROM, tornando-a uma tabela derivada. Para uma subconsulta usada em qualquer
outro lugar da consulta, seu resultado só está disponível no local em que é especificado.

SELECT p.Nome,
p.ProdutoNúmero,
ph.ListPrice
DA Produção.Produto p
APLICAÇÃO CRUZADA

(
SELECIONAR PARTE SUPERIOR (1)

ph2.ProductID,
ph2.ListPrice

195
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

FROM Production.ProductListPriceHistory ph2


ONDE ph2.ProductID = p.ProductID
ENCOMENDAR POR ph2.StartDate DESC
) ph;

Listagem 7-4

A introdução do operador APPLY altera substancialmente o plano de execução, conforme mostrado na Figura
7-9.

Figura 7-9: Plano de execução do comando APPLY.

Neste plano, vemos que a entrada externa para o operador Nested Loops é uma Verificação de Índice
Agrupado das tabelas Produtos, que produz 504 linhas. Isso implica que a entrada interna será executada
504 vezes. Os valores da coluna ProductID são enviados como referências externas e usados para buscar
linhas correspondentes no ProductListPriceHistory
tabela, e o operador TOP novamente garante que cada operação de busca retorne apenas a linha com o preço
de lista mais recente.

Então, qual método de escrever essa consulta você acha que é o mais eficiente? Uma maneira de descobrir
é capturar e comparar as métricas de desempenho para cada execução de consulta (duração, número de
leituras lógicas executadas e assim por diante).

Conforme discutido no Capítulo 2, a maneira de fazer isso com menor impacto é usando Eventos Estendidos.
Além disso, quando você vai medir o desempenho (duração), é uma boa ideia parar de capturar os planos de
execução porque isso introduz um efeito substancial do observador. A Figura 7-10 mostra os resultados,
capturados usando a sessão Extended Events fornecida na Listagem 2-6.

196
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Figura 7-10: Resultados de desempenho para o comando APPLY.

Embora ambas as consultas tenham retornado conjuntos de resultados idênticos, a subconsulta na cláusula ON
(Listagem 7-3) usa menos leituras lógicas (811) em comparação com a consulta usando APPLY e uma tabela
derivada (Listagem 7-4), o que causou 1.024 leituras lógicas.

A explicação simples para a diferença é que na subconsulta correlacionada a entrada interna cara da junção
de loops aninhados é executada 395 vezes (uma vez por preço de lista) e na consulta de tabela derivada é
executada uma vez por produto (504 vezes). Como observado anteriormente, estamos lidando com uma distribuição
de dados bastante estranha neste caso, onde 211 produtos não têm preço de tabela e os 293 restantes têm um ou
mais preços de tabela. Com uma distribuição de dados mais típica, consistindo em vários preços de lista para todos
ou a maioria dos produtos, poderíamos facilmente esperar que a versão da tabela derivada superasse a subconsulta.

As coisas ficam mais interessantes se adicionarmos a cláusula WHERE na Listagem 7-5 à consulta externa de
cada uma das listagens anteriores.

ONDE p.ProductID = 839

Listagem 7-5

Quando reexecutamos a Listagem 7-3 com a cláusula WHERE adicionada, obtemos o plano mostrado
na Figura 7-11.

Figura 7-11: Novo plano de execução após adicionar uma cláusula WHERE.

O operador Filter desapareceu, mas, mais interessante, o otimizador mudou a ordem de avaliação; o operador
TOP agora aparece na parte do plano para resolver a consulta externa onde, antes, estava na parte do plano
para resolver a subconsulta. Primeiro, ele encontra a única linha solicitada da tabela Product e, em seguida,
avalia imediatamente a subconsulta para localizar

197
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

a StartDate mais recente para esse ProductID. Se você verificar as propriedades da Busca de Índice Agrupado mais
à direita em ProductListPriceHistory, verá que ele faz referência ao alias ph2 , que nos informa que está avaliando a
subconsulta.

A próxima junção interna para ProductListPriceHistory está em ProductID e StartDate, com StartDate sendo
pressionado a partir da entrada externa (consulte a propriedade Referências externas da junção de loops
aninhados ). Além disso, se você verificar o Seek Predicates
propriedade do Clustered Index Seek à esquerda, que exibe cada um dos predicados usados para definir as linhas
que precisam ser lidas, ele faz referência a ProductID e StartDate.

O resultado final é que, em vez de Index Scans e das ineficiências causadas pela execução da entrada interna de um
Nested Loops join centenas de vezes, agora temos três operações Clustered Index Seek , com uma distribuição de
custo estimada igual, e dois Nested Loops joins . O Merge Join que vimos na Figura 7-5 era apropriado quando
estávamos lidando com varreduras dos dados, mas não foi usado, nem aplicável, quando a introdução da cláusula
WHERE reduziu o conjunto de dados. A entrada interna de cada junção de loops aninhados é executada apenas uma
vez, pois a cláusula WHERE significa que a entrada externa produz apenas uma única linha.

Se adicionarmos a cláusula WHERE à Listagem 7-4 (APPLY e uma tabela derivada), veremos o plano mostrado
na Figura 7-12.

Figura 7-12: Como a cláusula WHERE altera o plano APPLY.

Esse plano é quase idêntico ao visto na Figura 7-9, com a única mudança sendo que a Varredura de Índice Agrupado
mudou para Busca de Índice Agrupado. Essa mudança foi possível porque a inclusão da cláusula WHERE permite
que o otimizador aproveite o índice clusterizado para identificar a linha necessária, em vez de ter que varrer todas elas
para encontrar a linha correta a ser retornada.

Vamos comparar as estatísticas de E/S para cada uma das consultas:

198
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Figura 7-13: Métricas de desempenho após adicionar uma cláusula WHERE.

Agora, com a adição de uma cláusula WHERE, a consulta derivada é mais eficiente, com apenas 4
leituras lógicas versus a consulta subselecionada com 6 leituras lógicas e um aumento marginal na
velocidade. Se você executar a consulta com frequência, verá que a consulta APPLY é consistentemente mais rápida.
Se aumentarmos os volumes de dados, é muito provável que você veja o desempenho do operador APPLY
ainda melhor do que o outro método.

Com a cláusula WHERE em vigor, a subconsulta tornou-se relativamente mais cara para manter quando
comparada à velocidade fornecida pelo APPLY. Compreender o plano de execução faz uma diferença real
na decisão de quais construções T-SQL aplicar ao seu próprio código. Apenas lembre-se de que você deve
usar os melhores dados representativos possíveis em seus testes, a fim de obter comportamentos e
desempenho semelhantes ao seu ambiente de produção. Lembre-se também de que, à medida que os
dados mudam, a distribuição desses dados pode mudar, o que pode resultar em diferenças nos planos de
execução e diferenças no desempenho. Se seus dados forem modificados com frequência, talvez seja
necessário reavaliar as consultas regularmente.

Expressões de Tabela Comuns


O SQL Server 2005 introduziu a Common Table Expression (CTE), uma construção T-SQL com
comportamento semelhante a tabelas derivadas. Um CTE é um "conjunto de resultados temporário" que
existe apenas no escopo de uma única instrução SQL. Ele permite o acesso à funcionalidade dentro de
uma única instrução SQL que anteriormente só estava disponível através do uso de funções, tabelas
temporárias, cursores e assim por diante. Ao contrário de uma tabela derivada, uma CTE pode ser auto-
referenciada e referenciada repetidamente em uma única consulta. Também diferente de uma tabela
derivada, um CTE não pode ser correlacionado, mesmo quando usado com APPLY. Para mais detalhes
sobre CTEs, confira este artigo no Simple Talk: http://bit.ly/1NCr8k0.

Apesar da descrição de um CTE como um conjunto de resultados temporário, não assuma que o CTE
é processado de maneira separada do restante do T-SQL. Fundamentalmente, esta ainda é uma tabela
derivada, assim como os outros exemplos que já vimos. A principal diferença será quando o CTE for auto-
referenciado. Um CTE recursivo sempre usa dois (ou, raramente, mais)

199
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

consultas, combinadas com UNION ALL. A primeira consulta, conhecida como "membro âncora", pode
ser executada sozinha para produzir um resultado. A segunda consulta, o "membro recursivo", faz
referência ao próprio CTE. Ele usa os dados provenientes do membro âncora para produzir mais linhas,
mas recursivamente continua a produzir ainda mais dados usando as linhas que ele mesmo produz.
Esta é a definição lógica; veremos como ele executa em breve.

O procedimento armazenado interno, dbo.uspGetEmployeeManagers, no Adventure Works, usa


um CTE chamado EMP_cte em um exercício recursivo clássico, listando funcionários e seus gerentes.

CRIAR OU ALTERAR PROCEDIMENTO dbo.uspGetEmployeeManagers


@BusinessEntityID INT
COMO

COMEÇAR
DEFINIR NOCOUNT ON;
-- Use consulta recursiva para listar todos os funcionários necessários para um
Gerente
WITH EMP_cte(BusinessEntityID, OrganizationNode, FirstName,
LastName, JobTitle,
RecursionLevel) -- nome e colunas do CTE
AS (
SELECT e.BusinessEntityID, e.OrganizationNode, p.FirstName,
p.Sobrenome,
e.JobTitle, 0 -- Obtém o Employee inicial
DE Recursos Humanos . Empregado e
INNER JOIN Pessoa.Pessoa AS p
ON p.BusinessEntityID = e.BusinessEntityID
ONDE e.BusinessEntityID = @BusinessEntityID
UNIÃO TODOS
SELECT e.BusinessEntityID, e.OrganizationNode, p.FirstName,
p.Sobrenome,
e.JobTitle, RecursionLevel + 1 -- Junção recursiva
membro para ancorar
-- e para o próximo
membro recursivo
DE Recursos Humanos . Empregado e
INNER JOIN EMP_cte
ON e.OrganizationNode = EMP_cte.OrganizationNode.
ObterAncestral(1)
INNER JOIN Pessoa.Pessoa p
ON p.BusinessEntityID = e.BusinessEntityID
)

200
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

-- Junte-se novamente ao Funcionário para retornar o nome do gerente


SELECT EMP_cte.RecursionLevel, EMP_cte.BusinessEntityID, EMP_
cte.FirstName,
EMP_cte.LastName, EMP_cte.OrganizationNode.ToString()
AS OrganizationNode, p.FirstName AS ' ManagerFirstName ',
p.LastName AS 'ManagerLastName' -- Seleção externa de
o CTE
DE EMP_cte
INNER JOIN HumanResources.Funcionário e
ON EMP_cte.OrganizationNode.GetAncestor(1) =
e.OrganizationNode
INNER JOIN Pessoa.Pessoa p
ON p.BusinessEntityID = e.BusinessEntityID
ORDER BY RecursionLevel, EMP_cte.OrganizationNode.ToString()
OPÇÃO (MAXRECURSÃO 25)
FIM;
VAI

Listagem 7-6

Você pode ver o membro âncora, a primeira consulta no UNION ALL dentro do CTE, que retornará
dados com base no valor BusinessEntityID que é passado para ele como um parâmetro. É comentado
no código como -- Obter o Employee inicial. A recursão ocorre então na segunda consulta dentro do
UNION ALL. É comentado como -- Junte-se ao membro recursivo à âncora e ao próximo membro
recursivo. Ele usa a função GetAncestor para recuperar dados adicionais com base naqueles definidos
no membro âncora.

Vamos executar este procedimento e capturar o plano real.

EXEC dbo.uspGetEmployeeManagers
@BusinessEntityID = 9;

Listagem 7-7

Como mostra a Figura 7-14, o plano de execução é razoavelmente complexo e será impossível de ler
como está neste livro.

201
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Figura 7-14: Plano de execução recursiva completo de um CTE.

No entanto, nosso trabalho árduo nos capítulos anteriores agora está valendo a pena. Não há operadores
neste plano que você não tenha visto antes, portanto, mesmo sendo um plano grande, com paciência, deve
ser relativamente fácil de entender. Vamos dividir o plano em seções, começando com a seção superior
direita, mostrada na Figura 7-15.

Figura 7-15: Parte do plano de execução do CTE mostrando o acesso inicial aos dados.

Vamos ler esta seção do plano da esquerda para a direita (ordem lógica de chamada), começando com o
operador Index Spool , porque esse operador, em conjunto com um operador Table Spool que encontraremos
em breve, marca essencialmente o início do o processo de recursão, no CTE. Conforme discutido no Capítulo
5, um operador Spool usa uma tabela de trabalho temporária para armazenar dados que podem precisar ser
usados várias vezes, ou reutilizados, dentro de um plano de execução. A natureza recursiva da consulta acima
exige que o SQL Server armazene os dados à medida que ele cria recursivamente o conjunto de resultados.
Esse Index Spool é um Lazy Spool, um operador de streaming que solicita uma linha de seu operador
filho, armazena-a e a passa imediatamente para seu pai, o que o precede logicamente passando o controle
de volta para esse pai.

Nesse caso, o operador Index Spool tem um valor Node ID de 4 e está armazenando os resultados de um
operador Concatenation , que resolve a operação UNION ALL vista na Listagem 7-6. Conforme discutido
no Capítulo 4, esse operador simplesmente processa cada uma de suas entradas em ordem, de cima para
baixo, e as concatena. Um operador de concatenação sempre terá duas ou mais entradas. Ele chama a
entrada superior, passando as linhas recuperadas para seu pai, até que tenha recebido todas as linhas. Depois
disso passa para a segunda entrada, repetindo o mesmo processo.

202
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Nesse caso, a entrada superior coleta os dados para o "membro âncora" do CTE. Ele executa uma
junção de loops aninhados dos dados de duas buscas de índice agrupado em fontes
HumanRe.Employee e Person.Person. Isso produz uma única linha (para o funcionário com
BusinessEntityID de 9). Temos então dois operadores Compute Scalar , cada um dos quais retorna
uma expressão, ambos definidos como zero. Um é para o nível de recursão e o outro para a coluna
derivada, chamada RecursionLevel, no CTE.

Depois que todas as linhas da entrada superior são processadas, o operador Concatenação alterna
para sua segunda entrada e nunca retorna à primeira entrada. A Figura 7-16 exibe a entrada inferior
para o operador Concatenação , que resolve o membro recursivo.

Figura 7-16: Parte do plano de execução do CTE mostrando o uso do Table Spool.

É onde as coisas começam a ficar interessantes. Esta seção do plano encontra cada um dos
gerentes (gerente direto, gerente do gerente e assim por diante). O SQL Server implementa o
método de recursão por meio do operador Table Spool , combinado com o Index Spool na entrada
superior. O ID do Nó Primário para o Spool de Tabela é 4, indicando que ele consome os dados
carregados anteriormente no operador Spool de Índice . Você pode ver isso na Figura 7-17, junto com
alguns outros valores de propriedade para o Table Spool.

Figura 7-17: Propriedades de spool de tabela mostrando o ID do nó primário e com pilha.

203
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

A propriedade With Stack, definida como True, conforme mostrado na Figura 7-17, é uma parte
necessária da consulta recursiva. Armazenar dados como uma pilha significa que os novos dados são
sempre adicionados no topo e os dados são sempre lidos do topo. Após serem lidos, os dados são
removidos. Quando você vê uma propriedade With Stack definida como True, o comportamento do
Index Spool é alterado para o de uma "pilha". Isso é crucial para conduzir a avaliação recursiva do
CTE. À medida que o membro recursivo é executado, o Spool de Tabela lê e remove a linha âncora
do spool. O restante desse fragmento de plano encontra o gerenciador do valor âncora. O gerenciador é
armazenado no spool pelo operador Index Spool (NodeID 4), e essa linha é lida e removida quando o
Table Spool está pronto para solicitar a próxima linha. A partir daí, a recursão continua. O trabalho do
operador Assert , no lado esquerdo da Figura 7-16, é verificar a MAXRECUR SION(25) na consulta,
abortando a execução quando esse nível for excedido.

Assim, o Table Spool (Node ID 14) produz uma cópia dos dados armazenados pelo Index Spool
operador (ID do nó 4). Quando o operador é chamado pela primeira vez, ele produzirá uma cópia da
linha âncora e, em seguida, o que for armazenado posteriormente, nas chamadas subsequentes. O
operador Table Spool percorre as linhas do Index Spool e une os dados aos dados das tabelas
definidas na segunda parte da definição UNION ALL, dentro do CTE.

O Spool de Tabela retorna quatro linhas. O operador Compute Scalar , próximo ao Table Spool, é
usado para calcular o nível de recursão atual adicionando um ao valor. Esse fluxo de dados forma a
entrada externa para uma associação de loops aninhados , que se une à tabela Employee em uma
função interna, GetAncestor, que, por sua vez, une-se à tabela Person em Busines sEntityID. A entrada
interna executa a junção de Loops Aninhados entre o Employee
e tabelas Pessoa. A Figura 7-18 mostra as propriedades do Clustered Index Scan da tabela
Employee, onde você pode ver o número de vezes que esse scan foi executado.

O número estimado de execuções é 4 e o número estimado de linhas é 290 e, portanto, quatro


vezes 290 são 1160 linhas no total, o que corresponde exatamente ao valor do número real de
linhas .

204
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Figura 7-18: Varredura de Índice Agrupado da tabela Employee.

Temos então um operador Filtro . O otimizador decidiu fazer uma varredura completa da
tabela Employee e então, neste operador Filter , comparar o OrganizationNode
de cada linha para o GetAncestor da linha do CTE e mantenha apenas as linhas correspondentes.
Para as três primeiras linhas processadas (a do membro âncora e as duas primeiras retornadas do
membro recursivo), esse filtro mantém apenas uma linha, a do gerente direto do funcionário. A quarta
linha processada é o CEO, que não tem gerente, então o filtro agora não retorna nenhuma linha e a
recursão é interrompida. Portanto, a seção mais à direita do plano retorna quatro linhas no total: uma do
membro âncora e três do membro recursivo, listando os gerentes do funcionário até o CEO.

Portanto, temos uma linha da âncora e três linhas do membro recursivo, fornecendo as quatro
linhas no total que emergem da operação de concatenação , mas apenas três linhas são retornadas
nos resultados finais. Depois que o processo de recursão é concluído, fazemos mais uma junção interna
de cada linha retornada ao gerente, em que ponto, a última linha retornada do CTE recursivo, o CEO, não
consegue encontrar dados para suas colunas ManagerFirstName e ManagerLast Name e assim a linha é
perdida.

205
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Visualizações

Uma visualização é essencialmente apenas uma "consulta armazenada". Em outras palavras, uma forma lógica de
representar os dados como se estivessem em uma tabela, sem criar uma nova tabela. Os vários usos das visualizações
estão bem documentados (impedindo que certas colunas sejam selecionadas, reduzindo a complexidade para usuários
finais e assim por diante). Aqui, vamos nos concentrar apenas no que acontece dentro de um plano de execução ao
trabalhar com uma visualização.

Uma nota de cautela em relação aos pontos de vista. As visualizações não são tabelas, como ficará claro quando
examinarmos seus planos de execução, mas se parecem com tabelas e, portanto, há uma tendência de usá-las como
tabelas, unindo uma visualização à outra ou aninhando várias visualizações dentro de outras visualizações.
Isso leva a um desempenho de consulta horrível, porque a complexidade dos planos de execução sobrecarrega o
otimizador. Essa má prática, um cheiro de código comum, deve ser evitada.

Visualizações padrão

A visualização Sales.vIndividualCustomer fornece um resumo dos dados do cliente, exibindo informações como
nome, endereço de e-mail, endereço físico e informações demográficas. Uma consulta muito simples para obter um cliente
específico seria algo como a Listagem 7-13. Embora usar SELECT * não seja a melhor maneira de escrever consultas,
neste caso estou fazendo isso para ilustrar o que acontece quando uma consulta é executada em uma exibição e todos os
dados referenciados por essa exibição são recuperados.

SELECIONAR *
Sales.vCliente Individual
A PARTIR DE

WHERE BusinessEntityId = 8743;

Listagem 7-8

A Figura 7-19 mostra o plano de execução gráfico resultante.

Figura 7-19: O plano completo da consulta em relação a uma visualização.

206
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Este é outro plano que é muito difícil de ler na página impressa, então a Figura 7-20 mostra uma vista explodida de
apenas cinco operadores no lado direito do plano.

Figura 7-20: Subseção do plano mostrando os operadores padrão.

O que aconteceu com a visualização, vIndividualCustomer, que referenciamos nesta consulta?


Lembre-se de que, enquanto o SQL Server trata as exibições de maneira semelhante às tabelas, uma exibição
é apenas uma definição de consulta armazenada, que fica sobre as tabelas base (e possivelmente outras
exibições) das quais elas derivam. Durante a ligação de consulta (veja o Capítulo 1), o algebrizador "expande" a
visão, ou seja, a substitui por sua definição e, em seguida, o resultado é passado para o otimizador. Assim, o
otimizador nunca vê a visão, apenas a consulta que a define. O otimizador simplesmente otimiza o acesso às oito
tabelas e às sete junções definidas nessa visualização.

Resumindo, embora uma visualização possa facilitar a codificação, ela não altera de forma alguma a necessidade
do otimizador de consulta executar as ações definidas na visualização. Esse é um ponto importante a ser
lembrado, pois os desenvolvedores costumam usar visualizações para mascarar a complexidade de uma consulta.

O que acontece se alterarmos a consulta para usar uma lista de colunas na instrução SELECT?

SELECT ic.BusinessEntityID,
ic. Título,
ic.Sobrenome,
ic.FirstName
FROM Sales.vIndividualCustomer AS ic
WHERE BusinessEntityID = 8743;

Listagem 7-9

Isso resulta em um plano de execução bem diferente, mostrado na Figura 7-21.

207
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Figura 7-21: Mesma visualização, mas um plano de execução diferente.

Observe como a forma do plano de execução e o número de operadores são diferentes na Figura 7-21, quando
comparados com a Figura 7-19, embora estejamos consultando a mesma visualização. Isso ocorre porque uma etapa
do processo chamada "simplificação" eliminará as tabelas que não são necessárias para satisfazer a consulta. Nesse
caso, sem fazer referência a todas as colunas, o otimizador pode eliminá-las do plano.

Vale a pena notar que você provavelmente poderia escrever uma consulta que referencie ainda menos tabelas. O
processo de simplificação nem sempre captura todas as tabelas em excesso possíveis. Por exemplo, a tabela
EmailAddress ainda está sendo referenciada no plano.

Visualizações indexadas

Uma exibição indexada, também chamada de exibição "materializada" ou mesmo exibição "persistente", é
essencialmente uma "exibição mais um índice clusterizado". Um índice clusterizado armazena os dados da coluna, bem
como os dados do índice, portanto, a criação de um índice clusterizado em uma exibição resulta no que é efetivamente
uma nova tabela física no banco de dados. As exibições indexadas geralmente podem acelerar o desempenho de muitas
consultas, pois os dados são armazenados diretamente na exibição indexada, negando a necessidade de unir e
pesquisar os dados de várias tabelas toda vez que a consulta é executada.

Criar uma exibição indexada é, no mínimo, uma operação cara. Felizmente, também é uma operação única, que
podemos agendar quando nosso servidor estiver menos ocupado. As exibições indexadas também vêm com um
custo de manutenção interno para o SQL Server. Se as tabelas base na exibição indexada forem relativamente
estáticas, haverá pouca sobrecarga associada à manutenção de exibições indexadas.
No entanto, é bem diferente se as tabelas base estiverem sujeitas a modificações frequentes. Por exemplo, se
uma das tabelas subjacentes estiver sujeita a cem instruções INSERT por minuto, cada INSERT terá que ser atualizado
na exibição indexada. Como um DBA, você deve decidir se a sobrecarga associada à manutenção interna de uma
exibição indexada vale os ganhos fornecidos pela criação da exibição indexada em primeiro lugar.

208
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

As consultas que contêm agregações são boas candidatas para exibições indexadas porque a criação
das agregações só precisa ocorrer uma vez, quando o índice é criado, e os resultados agregados
podem ser retornados com uma consulta SELECT simples, em vez de ter a sobrecarga adicional de
executar o agrega por meio de um GROUP BY cada vez que a consulta é executada. Há também uma
economia substancial de E/S quando a agregação é feita em uma exibição indexada.

Por exemplo, uma das exibições indexadas fornecidas com AdventureWorks2014 é


vStateProvinceCountryRegion. Você pode ver a consulta completa na Listagem 7-10.
Lá eu largo e recrio a exibição e, em seguida, crio o índice clusterizado que a torna uma exibição
indexada.

DROP VIEW Person.vStateProvinceCountryRegion;


VAI
CRIAR OU ALTERAR VISUALIZAÇÃO Person.vStateProvinceCountryRegion
COM LIGAÇÃO DE ESQUEMAS

COMO

SELECT sp.StateProvinceID,
sp.StateProvinceCode,
sp.IsOnlyStateProvinceFlag,
sp.Name AS StateProvinceName,
sp.TerritoryID,
cr.CountryRegionCode,
cr.Name AS CountryRegionName
FROM Person.StateProvince sp
INNER JOIN Pessoa.PaísRegião cr
ON sp.CountryRegionCode = cr.CountryRegionCode;
VAI
CRIAR ÍNDICE AGRUPADO EXCLUSIVO IX_vStateProvinceCountryRegion
ON Person.vStateProvinceCountryRegion
(
StateProvinceID ASC,
PaísRegiãoCódigo ASC
);
VAI

Listagem 7-10

Se eu executar a consulta na Listagem 7-10 e tentar capturar o plano de execução, haverá um;
mesmo que cada uma dessas instruções seja uma instrução DDL. Isso porque, para satisfazer a
declaração final que cria o índice na visão, a consulta que define a visão deve ser executada. A Figura
7-22 mostra o plano de execução para esta consulta.

209
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Figura 7-22: Plano de execução para a criação de uma Visualização Indexada.

Isso se parece com alguns dos planos que vimos no Capítulo 6. Estamos selecionando a partir das duas tabelas definidas
na exibição e um operador Nested Loops é usado para juntar os dados antes de fornecê-los a um operador Index Insert
(Clustered) . Este é o processo de criação da exibição indexada.

Podemos executar uma consulta a partir da visualização e ver o plano de execução.

SELECT vspcr.StateProvinceCode,
vspcr.IsOnlyStateProvinceFlag,
vspcr.CountryRegionName
FROM Person.vStateProvinceCountryRegion AS vspcr ;

Listagem 7-11

O plano de execução resultante dessa consulta reflete não um índice regular, mas uma exibição indexada, supondo que
você esteja usando Enterprise ou Developer Edition. Se você estiver usando a Standard Edition, anterior ao SQL Server 2016
SP1 ou Express Edition, em que nenhuma das exibições indexadas corresponde por padrão, será necessário usar a dica
WITH NOEXPAND para ver o mesmo comportamento.

Figura 7-23: Plano de execução em relação a uma exibição indexada.

210
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

De nossa experiência anterior com planos de execução contendo visualizações, você poderia esperar
ver duas tabelas e a junção no plano de execução. Em vez disso, vemos uma única operação de varredura
de índice agrupado . Em vez de executar cada etapa da exibição, o otimizador foi direto para o índice
clusterizado que a torna uma exibição indexada.

Como os índices que definem uma exibição indexada estão disponíveis para o otimizador, eles também
estão disponíveis para consultas que nem mesmo se referem à exibição. Por exemplo, a consulta na Listagem
7-12 fornece um plano de execução muito semelhante ao mostrado na Figura 7-23, porque o otimizador
reconhece o índice como a melhor maneira de acessar os dados (novamente, isso pressupõe o uso de Enter
prize ou Edição do desenvolvedor).

SELECT sp.Name AS StateProvinceName,


cr.Name AS CountryRegionName
FROM Person.StateProvince sp
INNER JOIN Pessoa.PaísRegião cr
ON sp.CountryRegionCode = cr.CountryRegionCode;

Listagem 7-12

No entanto, à medida que a consulta cresce em complexidade, esse comportamento não é automático nem
garantido. Por exemplo, considere a consulta na Listagem 7-13.

SELECIONE a.Cidade,
v.StateProvinceName,
v.CountryRegionName
A PARTIR DE Pessoa. Endereço de um
JUNTE Person.vStateProvinceCountryRegion v
ON a.StateProvinceID = v.StateProvinceID
ONDE a.AddressID = 22701;

Listagem 7-13

Se você esperava ver uma junção entre a exibição indexada e a tabela Person.Address, ficaria desapontado.

211
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Figura 7-24: Plano de execução da visualização indexada expandida.

Em vez de usar o índice clusterizado que suporta a visualização materializada, como vimos na Figura 7-23, o
algebrizador executa o mesmo tipo de expansão de índice que fazia quando apresentado com uma visualização
regular. A consulta que define a visualização é totalmente resolvida, substituindo as tabelas que a compõem em
vez de usar o índice clusterizado fornecido com a visualização.

O algebrizador no SQL Server sempre expandirá as exibições. O otimizador tem um processo que determina que
o acesso direto à tabela será menos dispendioso do que usar a exibição indexada. Novamente, há uma maneira
de contornar isso com a dica NOEXPAND, abordada no Capítulo 10.

Funções
Existem dois tipos de funções definidas pelo usuário no SQL Server:

• Funções escalares – retornam um único valor.


• Funções com valor de tabela – retornam uma tabela.

Seu comportamento dentro dos planos de execução pode ser um tanto enganoso.

Funções escalares
Vamos começar com uma função escalar que faz parte do AdventureWorks2014, chamada dbo.
ufnGetStock. A Listagem 7-14 mostra a consulta.

CRIAR OU ALTERAR FUNÇÃO dbo.ufnGetStock(@ProductID int)


RETORNOS int
COMO

212
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

-- Retorna o nível de estoque do produto.


COMEÇAR
DECLARE @ret int;
SELECT @ret = SUM(p.Quantidade)
DA Produção.ProdutoInventário p
ONDE p.ProductID = @ProductID
AND p.LocationID = '6'; -- Apenas olhe para o inventário no armazenamento misc

SE (@ret É NULO)
SET @ret = 0
VOLTAR @ret
FIM;
VAI

Listagem 7-14

Podemos ver a função em ação com uma consulta procurando por níveis de estoque apenas de
produtos pretos.

SELECT p.Nome,
dbo.ufnGetStock(p.ProductID) AS StockLevel
DA Produção.Produto AS p
WHERE p.Cor = 'Preto';

Listagem 7-15

Se executarmos a consulta e capturarmos o plano de execução real, não haverá muito o que fazer, conforme mostrado
na Figura 7-25.

Figura 7-25: Apresentando a função escalar em um plano.

O Clustered Index Scan faz sentido porque não há índice que possa dar suporte à cláusula WHERE na coluna
Color. Assim, todo o índice deve ser escaneado e então o Predicado aplicado para retornar apenas as 93 linhas
com a Cor preta. Para ver o que o operador Compute Scalar está fazendo, devemos entrar nas propriedades e
ver os Valores Definidos para ver o cálculo.

213
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Figura 7-26: Cálculo de função dentro do operador Compute Scalar.

Como você pode ver, essa é a execução da função escalar. Então, isso é praticamente tudo o que
precisamos olhar, certo? Não exatamente. Essa UDF está acessando dados por meio da consulta na Listagem 7-14.
Esse acesso não pode ser visto em nenhum lugar na Figura 7-27. Em vez de capturar um plano real para a
Listagem 7-15, se capturarmos um plano estimado, informações diferentes são exibidas.

Figura 7-27: Plano estimado mostrando toda a extensão dos planos necessários para a função.

Em vez de um único plano de execução, existem dois. O segundo plano representa a função escalar. Este
é um custo oculto por trás do operador Compute Scalar no plano mostrado na Figura 7-25. O plano na
Figura 7-27 apresenta muitas funcionalidades.

214
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Lendo o plano da esquerda, o primeiro operador que vemos é um operador T-SQL rotulado como UDF,
representando a função definida pelo usuário. Não há propriedades de destaque além de um custo
estimado. Indo para a direita vemos três sub-ramos (na verdade, três planos), um para cada uma das
declarações na UDF.

O primeiro operador que encontramos na ramificação superior é um SELECT. Veremos um SELECT


operador para cada instrução SELECT em uma UDF. Se tivéssemos uma UDF com três instruções
SELECT, cada uma delas teria seus valores para Plan Hash, Nível de Otimização e assim por diante.
Essa sub-ramificação é usada para a consulta que calcula @Ret, agregando dados do Inventário de
Produtos. Ele usa um Clustered Index Seek para encontrar dados correspondentes e, em seguida,
um Stream Aggregate e Compute Scalar para produzir o resultado desejado. Já vimos todos esses
operadores antes, ao longo do livro, mas esta é a primeira vez que eles foram escondidos!

No segundo sub-ramo, vemos um operador COND . Esta é uma Condicional, neste caso
realizando a verificação NULL que você pode ver dentro da função na Listagem 7-14. Se @ret for
NULL, o operador COND chama o operador ASSIGN , que define @ret como 0.

A sub-ramificação final mostra o operador RETURN, que representa a instrução RETURN da Listagem
7-14.

Como mostra o plano na Figura 7-30, há mais coisas acontecendo nos bastidores com uma função
escalar do que é imediatamente aparente. Isso é especialmente verdadeiro para uma função escalar
que está acessando dados. Se fôssemos capturar os resultados STATISTICS IO para executar a Listagem
7-17, ela relataria apenas 15 leituras lógicas para retornar as 93 linhas. Infelizmente, conforme observado
no Capítulo 2, ele não conta E/S adicionais resultantes de chamadas para a função definida pelo usuário.
A função definida pelo usuário é chamada do Compute Scalar do plano "principal", uma vez para cada
uma das 93 linhas retornadas da tabela Product. Isso significa que cada uma das etapas do plano de
execução da própria UDF é executada 93 vezes.

Se você capturar as métricas de desempenho, usando nossa sessão Extended Events (Listagem 2.6),
você verá que, de fato, ela realiza 211 leituras lógicas e que a consulta faz referência não a 93, mas a
365 linhas. Cada uma das 93 execuções da UDF faz um Index Seek para encontrar todas as linhas para
um ProductID específico, processando 365 linhas no total, mas realizando muitas E/S desnecessárias
para devolvê-las. Se tivéssemos evitado a UDF e apenas escrito uma junção entre as duas tabelas, as
chances são de que o mesmo número de linhas teria sido escrito, mas usando muito menos leituras
lógicas.

215
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Funções com valor de tabela


As funções com valor de tabela definidas pelo usuário vêm em duas variedades diferentes com dois modos
diferentes de comportamento. A primeira é a função de valor de tabela inline (iTVF). Às vezes, elas são chamadas
de visualizações parametrizadas devido à forma como operam. A segunda é a função com valor de tabela de
várias instruções. Eles permitem consultas complexas que consistem em várias instruções. Essas funções são
expostas nos planos de execução de maneiras diferentes.

A Listagem 7-16 mostra como podemos reescrever a função da Listagem 7-14 como em iTVF.

CRIAR FUNÇÃO dbo.GetStock (@ProductID INT)


TABELA DE DEVOLUÇÕES

COMO

RETORNA
(
SELECT SUM(pi.Quantity) AS QuantitySum FROM
Production.ProductInventory AS pi WHERE pi.ProductID =
@ProductID AND pi.LocationID = '6'

);

Listagem 7-16

Para usar a função em uma consulta, teremos que modificar um pouco a Listagem 7-15.

SELECT p.Name,
gs.QuantitySum FROM
Production.Product AS p CROSS APPLY
dbo.GetStock(p.ProductID) AS gs WHERE p.Color = 'Black';

Listagem 7-17

O plano de execução real resultante é completamente diferente do que vimos para a função escalar.

216
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Figura 7-28: Plano para uma função com valor de tabela.

A pergunta mais imediata que você pode ter é: por que não há um operador de agregação no plano?
Como a SOMA é calculada? A resposta é que o otimizador usa as informações da consulta para definir o
iTVF (o filtro no LocationID) junto com os metadados (o fato de haver um índice único no ProductID) para
concluir que por produto, haverá no máximo uma linha com LocationID = 6. Como nunca pode haver mais
de 1 linha por produto, a agregação por produto é desnecessária.

Lendo da esquerda vemos um operador Merge Join , que está realizando uma Outer Join direita
entre as tabelas ProductInventory e Product. Vemos uma verificação de índice clusterizado
na tabela ProductInventory, com um Predicate pressionado em LocationID. O Compute Scalar é uma
conversão implícita do valor de Quantity para um inteiro. A quantidade é definida como SMALLINT, mas
a agregação SUM converte automaticamente em INT. Sem a agregação no plano, a conversão deve ser
feita em um Compute Scalar. Esses dados são mesclados com os dados de uma Verificação de Índice
Agrupado do Produto.

Ao contrário da função escalar anterior, a função inline é totalmente exposta em um único plano de
execução. Um plano estimado da Listagem 7-17 seria o mesmo da Figura 7-28, menos os valores de
tempo de execução. Não há custos ocultos e as linhas necessárias para atender à consulta são refletidas
com precisão no plano de execução.

Uma UDF com valor de tabela de várias instruções se comporta de maneira completamente diferente. A Listagem 7-18
mostra como podemos reescrever nossa função inline para ser uma UDF de várias instruções.

217
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

CRIAR FUNÇÃO dbo.GetStock2 (@ProductID INT)


RETORNOS @GetStock TABLE (QuantitySum int NULL)
AO
COMEÇAR
INSERIR @GetStock (

QuantidadeSoma
)
SELECT SUM(pi.Quantity) AS QuantitySum FROM
Production.ProductInventory AS pi WHERE pi.ProductID =
@ProductID AND pi.LocationID = '6'; RETORNA;

FIM

Listagem 7-18

Se modificarmos a Listagem 7-17 para usar essa função e, em seguida, executarmos a consulta, o plano de execução
será alterado conforme a Figura 7-29.

Figura 7-29: Plano de execução de função com valor de tabela de várias instruções.

Você pode ver facilmente que estamos mais uma vez diante de uma situação em que há funcionalidade oculta.
Temos um novo operador, Table Valued Function, na entrada interna de uma junção de loops aninhados .

O valor de propriedade mais importante a ser examinado para o operador Função com valor de tabela é o Número
estimado de linhas, que é 100.

218
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Figura 7-30: Propriedades do operador Table Valued Function.

Na verdade, as linhas estimadas retornadas para uma função com valor de tabela de várias instruções
sempre serão 100 linhas. O estimador de cardinalidade usa um valor codificado para variáveis de tabela.
Antes do SQL Server 2014, esse valor era 1. Do SQL Server 2014 em diante, esse valor é 100. Essa
contagem de linhas é completamente separada da realidade.

Nesse caso, uma estimativa de 100 linhas retornadas, por execução, e uma estimativa de 93 execuções
(uma para cada linha produzida pela entrada externa), totalizando 9.300 linhas. Na verdade, ele retorna
apenas 1 linha por execução, 93 no total.

Para ver a funcionalidade por trás do operador Table Valued Function , devemos olhar novamente
para o plano estimado. A Figura 7-31 mostra a função completa.

219
Machine Translated by Google

Capítulo 7: Planos de execução para instruções T-SQL comuns

Figura 7-31: Plano estimado mostrando a funcionalidade completa da Função de Valor de Tabela.

Você pode ver que, nessa situação, a função multi-instrução se parece muito com a função escalar
original. A única adição é o operador Table Insert que é necessário para carregar a variável de tabela
dentro da função. Mais uma vez, isso representa um custo oculto para a consulta.
Se observarmos a E/S dos Eventos Estendidos para a função GetStock e a compararmos com a função
GetStock2, veremos que eles passam de 44 leituras para 1141 leituras. O otimizador simplesmente não
recebe informações adequadas para fazer boas escolhas, ao lidar com uma função definida pelo usuário
com várias instruções.

Resumo
Este capítulo demonstrou o tipo de planos de execução que podemos esperar ver quando nosso
código usa procedimentos armazenados, exibições, tabelas derivadas, CTEs e funções definidas pelo
usuário. Eles são mais complexos do que os que vimos nos capítulos anteriores, mas todos os princípios
são os mesmos; não há nada de especial em planos de execução maiores e mais complicados, exceto
que seu tamanho e nível de complexidade exigem mais tempo para lê-los. Se você seguir os mesmos
padrões de uso das informações no primeiro operador para entender como o mecanismo está
resolvendo a consulta e, em seguida, ler as propriedades para entender como as informações estão
fluindo entre os operadores, tudo bem.

220
Machine Translated by Google

Capítulo 8: Examinando o uso do índice


É difícil subestimar o impacto que um conjunto de índices cuidadosamente selecionado terá na qualidade dos
planos gerados pelo otimizador e no desempenho de suas consultas.
No entanto, nem sempre podemos resolver um problema de desempenho apenas adicionando um índice. É perfeitamente
possível ter muitos índices, então devemos ser criteriosos em seu uso.

Precisamos garantir que os índices que escolhemos criar sejam bem projetados e seletivos para os predicados
usados por suas consultas mais importantes. Isso também significa garantir que suas estatísticas reflitam com
precisão os dados armazenados no índice.

Este capítulo descreverá como o otimizador usa essas estatísticas para fazer estimativas de seletividade e
cardinalidade, e o que pode dar errado, seja porque as estatísticas não são confiáveis, ou porque o otimizador usou
estatísticas precisas para gerar um plano que foi bom para alguma execução de um algoritmo parametrizado.
consulta, mas ruim para os outros.

Por fim, examinaremos alguns dos recursos importantes do plano de execução que você verá para
consultas que usam dois tipos de índice relativamente novos, índices Columnstore e índices otimizados para
memória.

Índices padrão

Para uma carga de trabalho OLTP típica, incluindo os tipos de consultas de exemplo vistas neste livro, nossa
estratégia de indexação dependerá principalmente de índices clusterizados e não clusterizados padrão:

• Índices clusterizados – o principal meio de armazenar e acessar a maioria das tabelas


dentro do armazenamento relacional padrão do SQL Server.
• Índices não clusterizados – um método secundário de acesso a dados, em suporte ao índice clusterizado
em uma tabela, projetado para melhorar o desempenho de consultas frequentes e caras na carga de
trabalho.

Geralmente, se um índice adequado estiver disponível, o otimizador de consulta escolherá um plano


eficaz que o utilize. Se não houver, você corre o risco de ter planos de execução insatisfatórios e
desempenho de consulta insatisfatório.

221
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Quando uma tabela é alterada para adicionar um índice clusterizado, ela substitui a tabela heap por um índice que
armazena todos os dados da tabela, ordenados de forma que seja fácil acessar linhas com base no valor da chave
de cluster ou em um intervalo de valores de chave consecutivos. A maioria das tabelas terá um índice clusterizado,
além de um ou mais índices não clusterizados. Um índice não clusterizado é semelhante, pois sua intenção é
facilitar o acesso a dados por determinados valores de chave, mas, em vez de armazenar todos os dados, ele
armazena apenas os valores de chave de índice, com um ponteiro para o local dos dados completos, geralmente o
valores da chave de índice clusterizado ou, para uma tabela de heap, um valor interno conhecido como identificador
de linha. Um índice não clusterizado também pode armazenar colunas de dados adicionais no nível folha com o uso
do operador INCLUDE.

Uma parte importante de qualquer esforço de ajuste envolve a escolha do índice clusterizado correto e, em
seguida, um conjunto de índices não clusterizados de suporte, para cada tabela no banco de dados. Como
discutimos ao longo do livro, não estamos tentando cobrir todas as consultas com um índice. Em vez disso, nosso
objetivo é criar o conjunto mínimo de índices que será mais benéfico para o otimizador, ajudando-o a resolver, o
mais barato possível, as consultas mais importantes, caras e frequentes em nossa carga de trabalho.

Como o otimizador seleciona quais índices usar


Já vimos muitos exemplos do otimizador escolhendo usar determinados índices para localizar e recuperar os
dados que a consulta precisa ler ou modificar. Às vezes, no entanto, o otimizador, desconcertantemente, escolhe
um plano diferente que ignora o que parece ser um índice útil. Há sempre uma razão para isso, revelada pelo plano
de execução, muitas vezes examinando os custos estimados para os operadores, contagens de linhas estimadas e
reais, bem como outros comportamentos e propriedades de cada operador de leitura de índice e sua interação com
outros operadores no plano de execução, como veremos em breve.

Primeiro, precisamos recapitular um pouco como o otimizador escolhe quais índices usar (é essencialmente
o mesmo processo para qualquer operador).

Custos estimados e estatísticas


Como discutimos no Capítulo 1, o otimizador escolherá o plano de custo mais baixo, com base nos valores de custo
estimados . Ele escolherá o plano que seus cálculos sugerem que terá o menor custo total, em termos da soma dos
custos estimados de CPU e processamento de E/S. O custo estimado de cada operadora contribui para o custo
estimado geral do plano.

222
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

A precisão dos custos estimados do otimizador depende em grande parte da precisão de seu conhecimento estatístico
dos dados: seus dados sobre os dados. Essas estatísticas, coletadas automaticamente para cada índice e também
para muitas colunas, fornecem informações agregadas ao otimizador, com base em uma amostra dos dados. Eles
descrevem, esperançosamente com precisão, o volume e a distribuição de todos os dados na tabela.

Por exemplo, as estatísticas usadas pelo otimizador incluem um gráfico de densidade, que prevê a "singularidade"
dos dados em uma coluna (o número de valores diferentes presentes) e um histograma, que prevê o número de
ocorrências de cada valor. O otimizador precisa conhecer essas informações com precisão, porque é um fator chave
em suas decisões sobre quais índices usar e como.

Estimativas de seletividade e cardinalidade


A principal medida para o otimizador determinar se deve usar um índice e como ler esse índice é a provável
seletividade de um predicado de consulta que o índice pode suportar.
A seletividade de um predicado, para um determinado índice, é a proporção esperada de linhas correspondentes.
Conte o número total de linhas na tabela (z), conte o número de valores distintos (x) para uma determinada coluna,
ou combinação de colunas, em todas as linhas e, em seguida, (x/ z) fornece a seletividade do índice , para um
predicado de igualdade comparando a coluna (ou colunas) com valores desconhecidos.

Um índice altamente seletivo terá um valor de seletividade baixo. Por exemplo, uma seletividade de 0,01 (1%)
significa que o otimizador espera que 1% do total de linhas na tabela corresponda ao predicado. Por outro lado, a
pior seletividade possível é 1,0 (ou 100%), o que significa que cada linha corresponderá à condição do predicado.

A cardinalidade de um determinado operador em um plano, mostrada no Número estimado de linhas


propriedade, é calculado com base na seletividade de cada predicado no filtro, alguns outros dados disponíveis nas
estatísticas e algumas suposições sobre os dados nas tabelas. A natureza dos cálculos varia dependendo do operador.
Por exemplo, para uma junção de mesclagem, o número estimado de linhas é baseado nas cardinalidades estimadas
dos dois fluxos de entrada e alguns cálculos muito complexos nos histogramas desses dois fluxos de entrada (se
disponíveis).

Índices e seletividade
Essencialmente, uma consulta é resolvida por uma cadeia de operações sucessivas nos dados, conforme descrito em
seu plano de execução. Portanto, uma estratégia de indexação que possa ajudar o otimizador a reduzir a quantidade
de dados manipulados o mais rápido possível na cadeia provavelmente funcionará melhor.

223
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Para fazer isso, precisamos que um índice seja seletivo, para os predicados de filtragem usados pelas
consultas que você pretende que ele ajude. Se existir um índice que corresponda a uma coluna de
predicado usada por determinadas consultas na carga de trabalho e se o otimizador avaliar que, para uma
determinada consulta, a seletividade do predicado é suficientemente alta, ele considerará o índice como um
bom candidato para usar no plano. Normalmente, isso significa que a cardinalidade estimada será baixa, ou
seja, apenas algumas linhas serão acessadas, o que diminuirá o custo geral estimado do operador.

Para demonstrar como o otimizador toma decisões sobre como ler dados de tabelas, criaremos uma
cópia da tabela SalesOrderDetail, no AdventureWorks. Vamos supor que, em algum momento, um
desenvolvedor adicionou alguns índices não clusterizados que ele achou que poderiam ajudar em
determinadas consultas.

DROP TABLE SE EXISTE NewOrders ;


VAI
SELECT SalesOrderID,
SalesOrderDetailID,
Número de rastreamento da operadora,
Quantidade do pedido,

ID do produto,
ID da Oferta Especial,
Preço unitário,
Preço unitárioDesconto,
Linha total,
desculpe,
Data modificada
INTO dbo.NewOrders
FROM Sales.SalesOrderDetail;
VAI
ALTER TABLE dbo.NewOrders
ADICIONAR CONSTRAINT PK_NewOrders_SalesOrderID_SalesOrderDetailID PRIMARY KEY
CLUSTERED
(
SalesOrderID,
SalesOrderDetailID
);
CRIAR ÍNDICE NÃO CLUSTERADO IX_NewOrders_ProductID
ON dbo.NewOrders (ProductID);
VAI
CRIAR ÍNDICE NÃO CLUSTERADO IX_NewOrders_OrderQty
ON dbo.NewOrders(OrderQty);
VAI

Listagem 8-1

224
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Executaremos a seguinte consulta simples para retornar os detalhes do pedido para uma quantidade de pedido
conhecida (20) e capturar o plano de execução real.

SELECT OrderQty,
SalesOrderID,
SalesOrderDetailID,
Linha total
DE dbo.NewOrders
ONDE OrdemQty = 20;

Listagem 8-2

A Figura 8-1 mostra o plano de execução. Vemos que o otimizador optou por usar um Index Seek
em nosso índice não clusterizado em OrderQty, mesmo que esse índice não esteja cobrindo essa consulta. Um
total de 46 linhas são retornadas do Index Seek e, como o índice não está cobrindo, isso resulta em 46 execuções
do Key Lookup.

Figura 8-1: O processo de seleção do índice.

Para nos ajudar a entender as decisões que o otimizador tomou, podemos examinar as estatísticas do
índice IX_NewOrders_OrderQty, usando o comando DBCC SHOW_STATIS TICS.

DBCC SHOW_STATISTICS('dbo.NewOrders',
'IX_NewOrders_OrderQty');

Listagem 8-3

225
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Isso retorna três conjuntos de resultados, o primeiro mostrando o cabeçalho, com detalhes gerais
sobre as estatísticas, o segundo o gráfico de densidade e, finalmente, o histograma com a
tabulação das contagens para cada valor de coluna indexado amostrado nas estatísticas.

Cabeçalho de estatísticas

O cabeçalho exibe o nome do índice, o número de linhas na tabela e o número de linhas amostradas
pelo algoritmo de criação/atualização de estatísticas para gerar as estatísticas, neste caso, todas as
12.317 linhas. Também mostra que existem 40 linhas, ou etapas, neste histograma.

Figura 8-2: As informações do cabeçalho nas estatísticas para IX_NewOrders_OrderQty.

Existem apenas até 200 pontos de dados ou etapas no histograma. Neste caso, há 40 passos.
Como existem 41 valores distintos na coluna OrderQty, isso pode parecer surpreendente, mas isso
é simplesmente uma consequência de como funciona o algoritmo de construção do histograma; ele
simplesmente tenta identificar os pontos de dados mais "interessantes", com um máximo de 200, em
uma única passagem dos dados.

Gráfico de densidade

O gráfico de densidade fornece ao otimizador suas estimativas do número de valores distintos em uma
coluna ou índice. Quanto menor a densidade, maior a "singularidade" e mais seletivo é o índice. Uma
coluna exclusiva em uma tabela de 10.000 linhas tem uma densidade de 1/10.000 ou 0,0001.
Um predicado de igualdade nesta coluna tem uma seletividade de 0,0001 (ou 0,01%), exatamente o
mesmo número, porque eles são calculados da mesma maneira.

No entanto, densidade e seletividade não são a mesma coisa. Por exemplo, a densidade também é
usada para estimar o número de linhas após um operador de agregação: se a mesma tabela de 10.000
linhas tiver 5 valores distintos para Cor, a densidade de Cor será 1/5 ou 0,2; o número estimado de
linhas quando você agrupa por cor é então calculado como 1/0,2, o que nos traz de volta a 5.

226
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

A Figura 8-3 mostra que a densidade da coluna OrderQty é 0,02439024.

Figura 8-3: O gráfico de densidade para IX_NewOrders_OrderQty.

O otimizador pode usar o gráfico de densidade para estimar a seletividade de um predicado, para um
predicado de igualdade comparando a coluna (ou colunas) com valores desconhecidos. Se uma consulta usa
um predicado em OrderQty e o otimizador não pode "farejar" o valor do parâmetro ou variável, ele simplesmente
pega o valor de densidade da coluna OrderQty, que é 0,02439024, multiplica pelo número total de linhas na
tabela (121317) e estima uma cardinalidade de 2.958,95 linhas.

Se estivermos executando um predicado de desigualdade em relação a valores desconhecidos, o otimizador


sempre usará uma seletividade estimada padrão de 30% e nenhuma densidade será usada.

As outras linhas no gráfico de densidade referem-se à densidade para predicados que usam uma combinação
de OrderQty e os valores da coluna chave do índice clusterizado, também armazenados no índice.
Como você pode ver, para este índice, a densidade de um predicado em uma combinação de OrderQty
e SalesOrderID é cerca de 1.000 vezes menor que apenas para OrderQty, o que significa que um predicado de
igualdade nessa combinação de colunas é cerca de 1.000 vezes mais seletivo do que um predicado em OrderQty.
Este nível de densidade torna o índice uma opção muito atrativa para o otimizador, para um predicado de igualdade
nestas colunas, comparando com valores desconhecidos.

O histograma
Frequentemente, o otimizador conhece o valor do parâmetro ou da variável com o qual está comparando,
seja porque o detectou ou porque o codificamos. Nesses casos, o otimizador usa o histograma para obter
uma estimativa melhor da cardinalidade do predicado.

Na Listagem 8-2, onde fornecemos um valor OrderQty codificado permanentemente de 20 e no histograma, esse
valor corresponde exatamente a um dos intervalos definidos pelo RANGE_HI_KEY. O otimizador lê um valor de
cardinalidade (contagem de linhas) de 46, da coluna EQ_ROWS para essa linha.

227
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Figura 8-4: Um extrato do histograma para IX_NewOrders_OrderQty.

Se não houver uma correspondência exata, o otimizador usa uma abordagem ligeiramente diferente
para as estimativas de contagem de linhas. Por exemplo, se alterarmos o valor literal de OrderQty para
35, na Listagem 8-2, podemos ver que há uma correspondência para 34 e 36 na coluna RANGE_HI_KEY,
mas nenhuma correspondência para 35. Como RANGE_HI_KEY define o topo de um intervalo, o valor
de 35 fica dentro do intervalo definido por 36, e o otimizador usa o valor AVG_RANGE_ROWS para
essa linha como a estimativa de contagem de linhas, 2 linhas. Ele deriva o valor AVG_RANGE_ROWS
simplesmente dividindo RANGE_ROWS (o número estimado de linhas que compõem o intervalo definido
pelo RANGE_HI_KEY) por DISTINCT_RANGE_ROWS (número de valores distintos dentro do intervalo).
Você pode ver uma estimativa de número de linha diferente, dependendo da sua versão do SQL Server
ou AdventureWorks, ou se você modificou suas estruturas de banco de dados, reconstruiu índices ou
atualizou suas estatísticas.

228
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Armado com sua estimativa de cardinalidade (46 linhas), o otimizador calcula o custo total estimado de realizar uma
busca seguida de 46 pesquisas e compara-o com suas alternativas (neste caso, simplesmente realizando uma única
varredura do índice clusterizado) e escolhe a opção mais barata. Quanto maior a contagem de linhas estimada, mais
pesquisas precisarão ser executadas e haverá um ponto de inflexão em que o otimizador decide simplesmente verificar
o índice clusterizado.

Neste exemplo, o ponto de inflexão está em torno de 400 linhas. Se você executar a Listagem 8-2 com um valor literal
de 11 (estimado em 392 linhas), ainda veremos o plano de busca/pesquisa, mas usaremos um valor de 12 (estimado
em 466 linhas) e ele dará dicas, e veremos a verificação de índice clusterizado .

Figura 8-5: Uma varredura de índice clusterizado causada por uma alteração nas linhas estimadas.

E se reescrevermos a Listagem 8-2 para usar uma variável local, em vez de um literal embutido em código?

DECLARE @OrderQuantity SMALLINT


SET @OrderQuantity = 20
SELECT OrderQty,
SalesOrderID,
SalesOrderDetailID,
Linha total
DE dbo.NewOrders
WHERE OrderQty = @OrderQuantity;

Listagem 8-4

Quando executarmos isso, veremos o plano com a varredura de índice clusterizado, embora em termos de número real
de linhas retornadas, estejamos abaixo do ponto de inflexão. A razão é que o otimizador não pode farejar o valor
fornecido, quando usamos variáveis locais (a menos que a recompilação em nível de instrução ocorra devido a uma dica
OPTION (RECOMPILE)) e, portanto, ele simplesmente usa o gráfico de densidade para estimar uma cardinalidade de
2.958,95 linhas , conforme descrito anteriormente, que podemos confirmar na planilha Propriedades para a Verificação
de índice clusterizado. Esse número estimado de linhas está muito acima do ponto de inflexão para o otimizador
escolher uma varredura de preferência às buscas mais pesquisas.

229
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Figura 8-6: Propriedades mostrando o número estimado de linhas.

Se modificarmos a cláusula WHERE na Listagem 8-4 para usar uma condição de pesquisa de
desigualdade, OrderQty > @OrderQuantity, você verá que o otimizador volta a usar uma estimativa de
cardinalidade codificada de 30% das linhas em a tabela, estimando 36.395,1 linhas quando apenas 164
são retornadas. Isso sempre resultará no plano com a varredura enquanto, para um valor OrderQty de 20,
o otimizador escolheria o plano de busca/pesquisa nos casos em que conhece ou pode farejar o valor, pois
pode novamente usar o histograma para obter precisão estimativas de cardinalidade.

Usando índices de cobertura


Nos exemplos anteriores, nosso índice na coluna OrderQty não cobriu nenhuma de nossas consultas.
Quando o otimizador optou por usar o índice, os planos incorreram no custo extra de realizar pesquisas
no índice clusterizado, para recuperar os valores de coluna não contidos no índice não clusterizado.

Conforme discutido no Capítulo 3, criamos um índice de cobertura tendo todas as colunas necessárias
como parte da chave do índice ou usando a operação INCLUDE para armazenar colunas extras no nível
folha do índice para que fiquem disponíveis para uso com o índice.

Uma pesquisa sempre adiciona algum custo extra, mas quando o número de linhas é pequeno, esse custo
extra também é pequeno, e o custo extra pode ser uma compensação aceitável em relação ao custo total
de toda a aplicação de adicionar um índice de cobertura.

Lembre-se de que adicionar um índice, por mais seletivo que seja, tem um preço durante INSERTs,
UPDATEs, DELETEs e MERGEs, pois os dados dentro de cada índice são reordenados, adicionados
ou removidos. Precisamos pesar a importância, a frequência de execução e o tempo de execução real
da consulta em relação à sobrecarga causada pela adição de um índice extra ou pela adição de uma
coluna extra à cláusula INCLUDE de um índice existente.

230
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Se essa fosse uma consulta crítica ou frequente, poderíamos considerar a substituição do índice existente
por um que incluísse a coluna LineTotal para cobrir a consulta e talvez outras colunas, se isso significasse que
o mesmo índice também abrangeria várias outras consultas na carga de trabalho .

O que pode dar errado?


Há muitas razões pelas quais o otimizador pode ser incapaz de usar o que parece ser um índice muito
adequado, ou parece ignorá-lo, e não podemos abordá-las neste livro.

Às vezes, é um problema com o código. Por exemplo, uma incompatibilidade entre o tipo de dados do
parâmetro e o tipo de coluna força a conversão implícita na coluna indexada e isso impedirá que o
otimizador busque o índice. Às vezes, uma consulta contém lógica que anula estimativas precisas.
Predicados complexos são mais difíceis de estimar do que predicados simples. Predicados de desigualdade
às vezes são mais difíceis de estimar do que predicados de igualdade e, nos casos em que os valores de
parâmetro ou variável não podem ser detectados, o otimizador simplesmente usa uma estimativa de
seletividade codificada (30%). Expressões com uma coluna incorporada são mais difíceis de estimar do que
expressões em que a coluna está sozinha e a expressão está do outro lado.

Às vezes, o otimizador escolhe o que parece ser um índice menos ideal porque é, de fato, mais
barato no geral, talvez porque esse índice apresente os dados em uma ordem que facilite uma junção
de mesclagem ou agregação de fluxo posteriormente no plano, em vez de mais equivalentes caros. Ou
porque permite que o otimizador observe ORDER BY sem precisar adicionar um operador Sort .

Não podemos cobrir todos os casos, portanto, nesta seção, focaremos apenas nos problemas que ocorrem
quando as estimativas de seletividade e cardinalidade do otimizador não correspondem à realidade. O
otimizador pensa que um operador precisará processar apenas 10 linhas, mas processa 10.000 ou vice-versa.

Se o otimizador não puder estimar com precisão quantas linhas estão envolvidas em cada operação no
plano ou reutilizar um plano com contagens de linhas estimadas que não são mais válidas, ele poderá
ignorar até mesmo índices bem construídos e altamente seletivos ou usar índices inadequados e, portanto,
criar planos de execução abaixo do ideal. Esses problemas geralmente se manifestam em grandes
discrepâncias entre as contagens de linhas reais e estimadas no plano e as causas potenciais
são numerosos.

231
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Problemas com estatísticas


Com relação às estatísticas, o otimizador pode usar um plano abaixo do ideal por vários motivos possíveis:

• Estatísticas ausentes – nenhuma estatística está disponível na coluna usada no predicado, talvez
porque certas opções do banco de dados impeçam sua criação, como o AUTO_
Opção CREATE_STATISTICS sendo definida como OFF.
• Estatísticas obsoletas – teve que gerar um plano para uma consulta contendo um predicado em
uma coluna com estatísticas que não foram atualizadas recentemente e não refletem mais com
precisão a distribuição real.
• Reutilização de um plano em cache abaixo do ideal – o otimizador reutilizou um plano que era
bom quando foi criado, mas o volume ou distribuição de dados mudou significativamente desde
então e o plano não é mais ideal.
• Distribuição de dados distorcida – o otimizador teve que gerar um plano para uma consulta
contendo um predicado em uma coluna onde a distribuição de dados era muito não
uniforme, dificultando estimativas precisas de cardinalidade.

Vamos ver um exemplo. A Listagem 8-5 captura um plano de execução real para uma consulta simples em
nossa tabela NewOrders. Em seguida, insere novas linhas. Ele insere apenas 5% do número total atualmente
na tabela, que está abaixo do limite necessário para acionar uma atualização automática de estatísticas, mas o
faz de uma maneira projetada para distorcer a distribuição de dados.

Em seguida, ele recaptura o plano para a mesma consulta. Por fim, ele atualiza manualmente as estatísticas e
captura o plano pela última vez. Se estiver acompanhando, você também pode considerar criar e iniciar a
sessão Extended Events que mostro no Capítulo 2 (Listagem 2-6), para capturar as métricas de E/S e de tempo
para cada consulta.

232
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

SET STATISTICS XML ON; GO


SELECT OrderQty,
CarrierTrackingNumber FROM
dbo.NewOrders WHERE
ProductID = 897; GO SET STATISTICS
XML OFF; GO --Modifica os dados BEGIN
TRAN; INSERT INTO dbo.NewOrders
(SalesOrderID,

Número de rastreamento da operadora,


Quantidade do pedido,

ID do produto,
ID da Oferta Especial,
Preço unitário,
Preço unitárioDesconto,
LineTotal,
rowguid,
Data modificada)
SELECT TOP (5) PERCENT
SalesOrderID,
CarrierTrackingNumber,
OrderQty, 897, SpecialOfferID,
UnitPrice, UnitPriceDiscount,
LineTotal, rowguid, ModifiedDate
FROM Sales.SalesOrderDetail
ORDER BY SalesOrderID; GO
SET STATISTICS XML ON; GO
SELECT OrderQty,
CarrierTrackingNumber FROM
dbo.NewOrders WHERE ProductID = 897;
GO SET STATISTICS XML OFF; VAI

233
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

--Atualizar estatísticas manualmente UPDATE


STATISTICS dbo.NewOrders GO SET STATISTICS
XML ON; GO SELECT OrderQty,
CarrierTrackingNumber FROM dbo.NewOrders
WHERE ProductID = 897; GO SET STATISTICS XML
OFF; GO ROLLBACK TRAN; --Atualizar estatísticas
manualmente ATUALIZAR ESTATÍSTICAS
dbo.NewOrders; VAI

Listagem 8-5

Usando instruções XML SET STATISTICS, juntamente com a separação do código em lotes, podemos
capturar apenas os planos de execução para esses lotes específicos e omitir os outros planos, como
o que é gerado para a instrução INSERT. Primeiro, aqui está o plano para a consulta antes de inserir
as linhas extras.

Figura 8-7: O plano de execução inicial antes da atualização das estatísticas.

O otimizador optou por buscar o índice não clusterizado em ProductID. O índice não cobre a
consulta, mas estima que a busca retornará apenas 50.817 linhas. Ele obtém essa estimativa
da coluna de valor AVG_RANGE_ROWS do histograma para o índice IX_ ProductID_NewOrders,
conforme descrito anteriormente.

234
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Na verdade, ele retorna apenas duas linhas, mas mesmo assim o otimizador estima que a sobrecarga
extra do operador Key Lookup , para cerca de 51 linhas, é pequena o suficiente para preferir essa rota
à varredura do índice clusterizado.

A Figura 8-7 mostra o plano depois que "distorcemos" os dados com nossa instrução INSERT.

Figura 8-8: Plano de execução ineficiente para estatísticas desatualizadas.

Vemos o mesmo plano. O otimizador simplesmente encontrou uma consulta que viu antes, selecionou
o plano existente do cache e o passou para o mecanismo de execução.

No entanto, agora o número real de linhas para a busca de índice é 6068, então a pesquisa de chave
é executado 6068 vezes. A consulta inicial teve 52 leituras lógicas, mas a consulta subsequente teve
19385, conforme medido em Eventos Estendidos.

Por fim, atualizamos as estatísticas, para que o plano em cache seja invalidado, fazendo com que um novo
seja compilado. Com estatísticas atualizadas, o plano agora está refletido na Figura 8-7.

Figura 8-9: Plano de execução correto para estatísticas atualizadas.

Esta é uma estratégia boa e apropriada para a consulta nesta tabela, como é agora. Como uma grande
porcentagem da tabela agora corresponde aos critérios definidos na cláusula WHERE da Listagem 8-2, o
Clustered Index Scan faz sentido. Além disso, o número de leituras caiu para 1.723, embora o mesmo
número exato de linhas esteja sendo retornado.

235
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Este exemplo ilustra a importância das estatísticas para ajudar o otimizador a fazer boas escolhas e como essas
escolhas afetam o comportamento dos índices que podemos ver nos planos de execução gerados. Estatísticas
ruins resultarão em más escolhas de plano. Uma discussão sobre a manutenção de estatísticas está fora do escopo
deste livro, mas certamente você deve sempre deixar AUTO_UPDATE_STATISTICS ativado e possivelmente
considerar executar UPDATE
STATISTICS como um trabalho de manutenção agendado para tabelas grandes, se necessário. Para distorções de
dados que afetam consultas importantes, considere investigar as estatísticas filtradas.

Problemas com sniffing de parâmetros


Em consultas parametrizadas corretamente, e quando usamos objetos escritos corretamente, como procedimentos
armazenados e funções, o otimizador pode espiar o valor passado para um parâmetro e usá-lo para comparar com as
estatísticas da chave de índice (ou da coluna) , especificamente o histograma.
Isso é conhecido como sniffing de parâmetro e permite que o otimizador obtenha estimativas precisas de
cardinalidade, em vez de depender de "médias", com base na densidade estatística do índice ou coluna ou em
estimativas codificadas (como 30%).

Quando o SQL Server executa o lote para executar um procedimento armazenado, por exemplo, ele primeiro compila
o lote. Neste ponto, ele define o valor de qualquer variável e avalia qualquer expressão. Em seguida, ele executa o
comando EXEC, verificando no cache do plano para ver se há um plano para executar o procedimento armazenado.
Se não houver um, ele invoca o compilador novamente para criar um plano para o procedimento. Nesse ponto, o
otimizador pode "farejar" o valor do parâmetro detectado ao executar o comando EXEC no lote.

Em alguns casos, o sniffing de parâmetros é inequivocamente nosso amigo. Por exemplo, digamos que temos
uma tabela Orders de um milhão de linhas que consultamos usando um predicado de desigualdade (como um
intervalo de datas) e retornamos apenas um pequeno subconjunto dos dados, normalmente resultados da última semana.
Sem a detecção de parâmetros, sempre obteremos um plano gerado para acomodar uma contagem de linhas
estimada de 300.000 (30% de 1 milhão), o que provavelmente será um plano ruim, se as consultas normalmente
retornarem apenas dezenas ou centenas de linhas.

Em outros casos, como se nossas consultas filtram na coluna PRIMARY KEY ou em uma chave com uma distribuição
de dados uniforme, a detecção de parâmetros é amplamente irrelevante.

Muitas vezes, estamos em algum lugar no meio, e o sniffing de parâmetro problemático ocorre quando as consultas
filtram chaves com distribuição de dados desigual e o otimizador reutiliza um plano em cache gerado para um valor de
parâmetro de entrada sniffed com uma contagem de linhas estimada que acaba sendo atípica de a linha conta para
valores de entrada subsequentes.

236
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Procedimentos armazenados e sniffing de parâmetros

Na Listagem 8-6, simplesmente transformamos nossa consulta NewOrders da Listagem 8-2 em um


procedimento armazenado, mas, para manter as coisas interessantes, com a pequena diferença de que o
parâmetro @OrderQty é opcional.

CRIAR OU ALTERAR PROCEDIMENTO dbo.OrdersByQty @OrderQty


SMALLINT = NULL
COMO

SELECT SalesOrderID,
SalesOrderDetailID, OrderQty,
LineTotal FROM dbo.NewOrders

ONDE
(
OrderQty = @OrderQty
OU @OrderQty É NULO
);
VAI

Listagem 8-6

Já sabemos que se fornecermos um valor literal de OrderQty=20 para a consulta original, o otimizador criará um
plano com a busca de índice não clusterizado e as pesquisas de chave (consulte a Figura 8-1). A Figura 8-10 mostra
o plano real quando executamos este procedimento fornecendo um valor OrderQty de 20.

Figura 8-10: O sniffing de parâmetros resulta em um plano com Key Lookups.

237
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

O otimizador usou o sniffing de parâmetros e criou um plano otimizado para um valor de parâmetro de 20, que podemos ver
nas propriedades do operador SELECT .

Figura 8-11: Lista de parâmetros mostrando os mesmos valores de parâmetros de


tempo de execução e tempo de compilação.

Isso significa que vemos a mesma combinação de pesquisa de chave e índice não clusterizado, mas com a diferença de
que aqui o otimizador verifica em vez de buscar o índice não clusterizado (explicarei o porquê, em breve).

As métricas de tempo e E/S nos informam que o SQL Server realiza 424 leituras lógicas e o tempo de execução foi de cerca
de 10 milissegundos.

Se o otimizador não tivesse conseguido detectar o valor do parâmetro, sabemos que ele teria usado o gráfico de densidade
para o índice não clusterizado para estimar uma cardinalidade de 2.958,95 linhas e escolhido uma varredura de índice
clusterizado (consulte a Figura 8-3). Portanto, este é um exemplo do otimizador fazendo bom uso de sua capacidade de
amostrar os dados diretamente por meio de sniffing de parâmetros para chegar a um plano de execução mais eficiente; varrer
o índice não clusterizado menor e realizar algumas pesquisas de chave é mais barato do que varrer o índice clusterizado.

No entanto, o sniffing de parâmetros pode ter um lado mais sombrio. Vamos reexecutar o procedimento armazenado e passar
um valor diferente.

EXEC dbo.OrdersByQty @OrderQty = 1;


VAI

Listagem 8-7

Ele reutiliza o plano de execução do cache, mas agora 74.954 linhas correspondem ao valor do parâmetro, em vez de 46, o
que significa 74.954 execuções da Pesquisa de Chave, em vez de 46. Ele executa 239.186 leituras lógicas e leva cerca de
1.400 ms.

Se você encontrar problemas de desempenho com procedimentos armazenados, vale a pena verificar as propriedades
do primeiro operador do plano para ver se os valores de compilação e de tempo de execução de algum parâmetro são
diferentes.

238
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Figura 8-12: Lista de parâmetros mostrando diferentes valores de parâmetros de


tempo de execução e tempo de compilação.

Se forem, essa é a sua dica para investigar o sniffing de parâmetro 'ruim' como a causa. Claro, aqui, sabemos que o
otimizador escolheria um plano diferente para a Listagem 8-7 se estivesse começando do zero. A Listagem 8-8 recupera o
valor plan_handle para nosso procedimento armazenado, do DMV sys.dm_exec_procedure_stats e o usa para liberar
apenas esse plano único do cache de procedimento.

DECLARE @PlanHandle VARBINARY(64);


SELECT @PlanHandle = deps.plan_handle
A PARTIR DE
sys.dm_exec_procedure_stats DEps AS
WHERE deps.object_id = OBJECT_ID('dbo.OrdersByQty');
SE @PlanHandle NÃO FOR NULL
COMEÇAR

DBCC FREEPROCCACHE(@PlanHandle);
FIM
VAI

Listagem 8-8

Execute a Listagem 8-7 novamente e o otimizador usa o histograma para obter uma contagem de linhas estimada de
74954 (local), e você verá o plano de varredura de índice clusterizado e apenas 1512 leituras lógicas em vez de 239186.

Finalmente, por que o otimizador usa um Index Scan, em vez do operador Seek na Figura 8-10? Se verificarmos as
propriedades do Index Scan, veremos que a condição Predicate é OrderQty = @OrderQty OR @OrderQty IS NULL. A
razão é simplesmente que o otimizador deve sempre garantir que um plano seja seguro para reutilização. Se ele selecionou
a busca de índice esperada com um predicado de busca de OrderQty = @OrderQty, então o que aconteceria se esse
plano fosse reutilizado quando nenhum valor para @OrderQty fosse fornecido? O predicado de busca seria uma igualdade
com NULL e nenhuma linha seria retornada, quando é claro que a intenção seria retornar linhas para todas as quantidades
do pedido.

239
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

O que fazer se a detecção de parâmetros causar problemas de desempenho

Existem muitas maneiras possíveis de resolver problemas relacionados à detecção de parâmetros,


dependendo da situação exata. Se a distribuição de dados for "irregular" com muitas variações nas contagens
de linhas retornadas, dependendo do valor do parâmetro de entrada, isso geralmente aumentará a
probabilidade de detecção de parâmetros problemáticos.

Nesses casos, você pode considerar adicionar a dica OPTION (RECOMPILE) ao final da consulta afetada
(ou consultas). Por exemplo, se um procedimento armazenado tiver três consultas e apenas uma delas
apresentar sniffing incorreto, adicione apenas a dica à consulta afetada; recompilar todos os três é um
desperdício de recursos.

Isso forçará o SQL Server a recompilar o plano para essa consulta todas as vezes e otimizá-lo para o valor
específico passado. O uso dessa dica em nosso procedimento armazenado OrderByQty corrigiria o
problema com a detecção de parâmetros problemáticos e significaria que o otimizador poderia escolha um
plano com a combinação usual de Pesquisa de Índice / Pesquisa de Chave (em vez da Pesquisa de Índice /
Pesquisa de Chave vista na Figura 8-10, pois ele saberá que o plano nunca será reutilizado.

No entanto, a desvantagem com a solução OPTION (RECOMPILE), geralmente, são as compilações


extras que ela causa. Para procedimentos armazenados e outros módulos de código, todas as instruções,
incluindo aquela com OPTION(RECOMPILE), ainda estarão no cache do plano, mas o plano para a instrução
OPTION(RECOMPILE) ainda será recompilado para cada execução, o que significa que seu plano não é
reutilizado. Quando usamos a dica para consultas ad hoc, o otimizador marca o plano criado para que ele
não seja armazenado em cache.

Uma alternativa é persuadir o otimizador a sempre escolher um plano específico; como o problema é
causado pelo otimizador que otimiza a consulta com base em um valor de parâmetro inadequado, a solução
pode ser especificar qual valor de parâmetro o otimizador deve usar para criar o plano, usando a dica de
consulta OPTION (OPTIMIZE FOR <value>). Abordaremos as dicas em detalhes no Capítulo 10. Outra
alternativa é usar uma técnica de forçar plano, discutida no Capítulo 9.

Obviamente, isso depende de sabermos o melhor valor de parâmetro a ser escolhido, um que geralmente
resultará em um plano de execução eficiente ou pelo menos bom o suficiente. Por exemplo, a partir do
exemplo anterior, podemos optar por otimizar para um valor OrderQty de 20, se sentirmos que o plano na
Figura 8-10 geralmente seria o melhor plano. O problema que você pode encontrar aqui é que os dados
mudam com o tempo e esse valor pode não funcionar mais bem no futuro.

240
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Outra alternativa é gerar um plano genérico, otimizando para um valor desconhecido.


Podemos fazer isso, neste caso, adicionando a opção OPTIMIZE FOR (@OrderQty
UNKNOWN)) para a consulta em nosso procedimento armazenado. O otimizador usará o gráfico de
densidade para chegar a uma estimativa de cardinalidade (neste caso, sempre estimando que 2.958,95 linhas
retornarão), e veremos o plano na Figura 8-9.

O problema surge quando não é bom o suficiente, para certos valores, de tal forma que o desempenho
sofre indevidamente onde um plano mais específico funcionaria melhor. Em suma, tudo é uma troca. Nem
sempre há uma única resposta correta.

Índices de armazenamento de colunas

Os índices Columnstore eram um novo tipo de índice introduzido no SQL Server 2012, além dos tipos de
índice existentes. Com um índice columnstore, a arquitetura de armazenamento é diferente.
Ele não usa a árvore B como mecanismo de armazenamento primário (embora parte dos dados possa ser
armazenada em uma árvore B) e armazena dados por coluna em vez de por linha. Portanto, em vez de
armazenar quantas linhas couberem em uma página de dados, o índice columnstore pega todos os valores
de uma única coluna e os armazena em uma ou mais páginas.

Um índice columnstore clusterizado substitui a tabela heap por um índice que armazena todos os dados da
tabela em uma estrutura de coluna. Um índice columnstore não clusterizado pode ser aplicado a qualquer
tabela, juntamente com índices clusterizados e não clusterizados "rowstore" tradicionais.

Os índices CS atingem alta compactação de dados e são projetados para melhorar o desempenho de análises,
relatórios e consultas de agregação, como as encontradas em um data warehouse. Em outras palavras, as
cargas de trabalho típicas para índices CS envolvem uma combinação de tabelas grandes (milhões ou até bilhões
de linhas) e consultas que operam em todas as linhas ou em grandes seleções. Na verdade, consultas simples
que recuperam uma única linha ou pequenos subconjuntos de linhas geralmente têm um desempenho muito pior
com os índices columnstore do que com os índices tradicionais, porque no primeiro caso o SQL Server precisa
ler uma página para cada coluna na tabela para reconstruir cada fila.

Além disso, a natureza do armazenamento do índice columnstore faz com que colocar menos de 100.000
linhas no índice seja muito menos eficiente do que armazenar mais do que esse valor de linhas. Citando a
documentação da Microsoft, você deve considerar o uso de um índice columnstore clusterizado em uma tabela
quando: Cada partição tiver pelo menos um milhão de linhas. Os índices Columnstore têm rowgroups em
cada partição. Se a tabela for muito pequena para preencher um rowgroup em cada partição, você não
obterá os benefícios da compactação do columnstore e do desempenho da consulta.

241
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Além de ter uma arquitetura diferente, os índices columnstore também suportam um novo tipo de modelo
de execução de consultas, otimizado para hardware moderno, chamado modo batch, sendo o modelo
tradicional o modo linha. Não abordaremos planos para consultas que usam o modelo de execução em modo
de lote até o Capítulo 12.

Esta seção se concentrará exclusivamente em como os índices columnstore são expostos nos planos de
execução e em algumas propriedades importantes às quais você precisa prestar atenção ao trabalhar
com esses índices. Para obter mais detalhes sobre índices columnstore e seu uso no ajuste de consulta,
seu comportamento e mecanismos de armazenamento e manutenção, sugiro os seguintes recursos:

• Índices Columnstore: visão geral – a documentação da Microsoft:

http://bit.ly/1djYOCW
• SQL Server Central Stairway to Columnstore Indexes – escrito por Hugo Kornelis,
o revisor técnico deste livro:
http://bit.ly/2CBiXoQ
• Índices Columnstore: o que há de novo – inclui uma tabela útil que resume o suporte
para vários recursos CS do SQL Server 2012 em diante:
http://bit.ly/2oD9keB
• Série Columnstore de Niko Neugebauer – cobertura extensa e abrangente de todos os
aspectos do uso de índices columnstore, embora os primeiros artigos cubram o básico:
http://www.nikoport.com/columnstore/

Usando um índice columnstore para uma consulta de agregação


Apesar de ter sido projetado para consultas analíticas em tabelas muito grandes, ocasionalmente você
pode ver um desempenho aprimorado usando o índice columnstore mesmo em um sistema OLTP, se,
por exemplo, você tiver consultas de relatórios que extraem dados de tabelas muito grandes. Um número
mínimo de linhas recomendadas para realmente ver grandes ganhos de desempenho é um milhão.

Começaremos com uma consulta simples na tabela TransactionHistory, sem nenhum índice columnstore
criado. Essa tabela não é uma candidata ideal para um índice columnstore, pois contém apenas 113 K linhas
e está sujeita a cargas de trabalho de estilo OLTP, em vez de estilo DW. No entanto, os índices columnstore
são adequados para consultas de agregação, portanto, este exemplo simples serve perfeitamente como uma
primeira demonstração de como os índices columnstore funcionam.

242
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

SELECT p.Nome,
COUNT(th.ProductID) AS CountProductID,
SUM(th.Quantity) AS SumQuantity,
AVG(th.ActualCost) AS AvgActualCost
FROM Production.TransactionHistory AS th
JUNTE -SE à Produção.Produto AS p
ON p.ProductID = th.ProductID
GROUP BY th.ProductID,
p.Nome;

Listagem 8-9

O plano de execução mostrado na Figura 8-13 ilustra parte da carga potencial no servidor dessa
consulta.

Figura 8-13: Plano de execução para uma consulta de agregação (sem índice Columnstore).

Nossa consulta não tem cláusula WHERE, então o otimizador decide sensatamente varrer o índice
clusterizado para recuperar todos os dados da tabela TransactionHistory. Vemos então um operador
Hash Match (Aggregate) . Conforme discutido no Capítulo 5, o SQL Server cria uma tabela de hash
temporária na memória na qual armazena os resultados de todos os cálculos agregados. Nesse caso, a
tabela de hash é criada na coluna ProductID e, para cada valor ProductID distinto, ela armazena uma
contagem de linhas, Quantidade total e ActualCost total, aumentando as contagens e os totais sempre
que processa uma linha com o mesmo ProductID. Um Compute Scalar calcula o AVG solicitado, dividindo
a contagem de linhas para cada ProductID pelo custo real total (ele também realiza algumas conversões
de tipo de dados). Esse fluxo de dados forma a entrada Build para um operador Hash Match (inner join),
em que a entrada Probe é um Index Scan na tabela Product, para unir a coluna Name.

Essa consulta simples retorna 441 linhas e em meus testes as retornou em 127ms, em média, com
803 leituras lógicas. Vamos ver o que acontece quando adicionamos um columnstore não clusterizado
à tabela.

243
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

CRIAR ÍNDICE DE COLUMNSTORE NÃO CLUSTERADO ix_csTest


ON Production.TransactionHistory
(
ID do produto,
Quantidade,
Custo real,
ReferenceOrderID,
ReferenceOrderLineID,
Data modificada
);

Listagem 8-10

Se executarmos novamente a consulta da Listagem 8-9, veremos mudanças significativas no desempenho. Em


meus testes, o tempo de consulta caiu de uma média de 127ms para uma média de 55ms, e o número de
leituras lógicas despencou de 803 para 84, porque a estrutura columnstore permite que o mecanismo leia apenas
as colunas solicitadas e pule as outras colunas em a mesa. Você provavelmente verá variação no número de
leituras, devido à forma como o columnstore cria o índice e compacta os dados.

A Figura 8-14 mostra o plano de execução.

Figura 8-14: Plano de execução para uma consulta de agregação (com índice Columnstore).

Já vimos o Adaptive Join antes, no Capítulo 4, então não descreveremos essa parte do plano novamente aqui.
Observe que você só verá esse operador se o nível de compatibilidade do banco de dados estiver definido como
140 ou superior.

Usaremos este plano e um para uma consulta semelhante com um filtro de cláusula WHERE, para explorar as
diferenças que você encontrará nos planos de execução, quando o otimizador optar por acessar os dados usando
um índice columnstore.

244
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Empilhamento agregado
A primeira diferença do plano que vimos antes de criar o índice CS é que agora vemos um
Columnstore Index Scan. Se observarmos sua folha de propriedades, alguns dos valores podem
parecer confusos a princípio, pois parece sugerir que o número estimado de linhas retornadas é 113443,
mas o número real de linhas é 0!

Figura 8-15: Propriedades Columnstore Index Scan mostrando linhas agregadas localmente.

Esse é um recurso especial dos índices CS em ação, chamado de "empilhamento agregado",


introduzido no SQL Server 2016, em que parte ou toda a agregação é feita pela própria verificação. Isso
é possível devido aos mecanismos de armazenamento dinâmico do índice columnstore. Os resultados da
agregação são "injetados diretamente" no operador de agregação, neste caso o operador Hash Match
(Aggregate) . A seta do operador exibe apenas as linhas que não podem ser agregadas localmente. Isso
explica por que o operador Hash Match (Aggregate) parece tornar a seta mais grossa (adicionando linhas
efetivamente).

Na Figura 8-15, o valor do Número Real de Linhas Agregadas Localmente indica o número de
linhas que foram agregadas na varredura e não foram retornadas "da maneira normal" para a
Correspondência de Hash (Agregado), neste caso todas as linhas (113443 ). Em um operador de
Varredura de Índice de Armazenamento de Colunas , o Número Real de Linhas é o número de linhas
que não foram agregadas na varredura e, portanto, foram retornadas "normalmente", nesse caso, zero linhas.

245
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Nenhuma operação de busca no índice columnstore

Vamos adicionar um filtro simples à nossa consulta de agregação anterior.

SELECT p.Nome,
COUNT(th.ProductID) AS CountProductID,
SUM(th.Quantity) AS SumQuantity,
AVG(th.ActualCost) AS AvgActualCost
FROM Production.TransactionHistory AS th
JUNTE -SE à Produção.Produto AS p
ON p.ProductID = th.ProductID
ONDE th.TransactionID > 150000
GROUP BY th.ProductID,
p.Nome;

Listagem 8-11

O plano tem a mesma forma e tem os mesmos operadores que o da Figura 8-14; ainda vemos o Columnstore
Index Scan. Não há operador Seek para um índice columnstore, simplesmente devido à forma como o índice está
organizado; os dados em um índice columnstore não são classificados de forma alguma, portanto, não há como
localizar valores específicos diretamente.

Empilhamento de predicado em um índice columnstore


Se examinarmos as propriedades do Columnstore Index Scan no plano para a Listagem 8-11, veremos que o
predicado da cláusula WHERE foi pressionado.

Figura 8-16: Predicado no índice columnstore.

O empilhamento de predicado em um Columnstore Index Scan é ainda mais importante do que em um


Rowstore Index Scan, porque predicados enviados podem resultar na eliminação de rowgroup
(às vezes, por motivos históricos, incorretamente chamado de eliminação de segmento). Em um índice columnstore,
cada partição é dividida em unidades chamadas rowgroups e cada rowgroup contém até cerca de um milhão de
linhas que são compactadas no formato columnstore ao mesmo tempo.

246
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

A eliminação do grupo de linhas é visível em SET STATISTICS IO, observando a contagem de


"segmentos ignorados" .

(434 linhas afetadas)


Tabela 'Histórico de Transações'. Contagem de varredura 2, leituras lógicas 0, leituras físicas
0, leituras antecipadas 0, leituras lógicas lob 63, leituras físicas lob 0, leituras antecipadas lob
0.
Tabela 'Histórico de Transações'. O segmento lê 1, segmento ignorado 0.
Tabela 'Mesa de trabalho'. Contagem de varredura 0, leituras lógicas 0, leituras físicas 0,
leituras antecipadas 0, leituras lógicas lob 0, leituras físicas lob 0, leituras antecipadas lob 0.

Tabela 'Produto'. Contagem de varredura 1, leituras lógicas 6, leituras físicas 0, leituras


antecipadas 0, leituras lógicas lob 0, leituras físicas lob 0, leituras antecipadas lob 0.

Neste exemplo, em uma pequena tabela, todos os dados estão em um único rowgroup, portanto, não vemos a
eliminação do rowgroup, é claro. No entanto, se você tiver uma tabela de 60 milhões de linhas, o empilhamento de
predicado poderá levar à eliminação do grupo de linhas e você verá uma melhoria no desempenho da consulta.

Modo de lote versus modo de linha


Cobrimos o modo de Lote em detalhes no Capítulo 12, e os únicos detalhes que quero destacar aqui são o Modo
de Execução Real e o Modo de Execução Estimado para o operador Columnstore Index Scan , ambos sendo
Lote neste caso (veja a Figura 8-15 ).

Isso indica que o plano foi otimizado para operação em modo de lote e, portanto, estamos vendo todo o potencial
do índice columnstore. Se uma consulta for inesperadamente lenta ao usar um índice de armazenamento de
colunas, vale a pena comparar os modos de execução real e estimado. Se o primeiro mostrar a linha e o último, o
lote, você terá um plano otimizado para o modo de lote que, por algum motivo, teve que retornar ao modo de linha
durante a execução. Isso é muito ruim para o desempenho da consulta, mas é apenas um problema no SQL Server
2012, onde um plano de modo de lote pode retornar ao modo de linha quando uma operação de hash é transferida
para tempdb.

247
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Índices otimizados para memória


Índices executam o mesmo propósito para tabelas com otimização de memória que para tabelas baseadas
em disco que usamos até agora. No entanto, são estruturas muito diferentes, representando um redesenho completo
das estruturas de acesso e bloqueio de dados, e projetadas especificamente para obter o melhor desempenho
possível por estarem na memória.

As tabelas com otimização de memória, introduzidas no SQL Server 2014, dão suporte a dois novos tipos
de índice não clusterizado:

• Índices de hash – um tipo completamente novo de índice, para tabelas com otimização de memória,
usado para realizar pesquisas em valores específicos. É essencialmente uma matriz de baldes de
hash, onde cada balde aponta para a localização de uma linha de dados, na memória.
• Índices de intervalo – usados para recuperar intervalos de valores e mais semelhantes ao índice familiar B-
tree, exceto que essas contrapartes com otimização de memória usam uma estrutura de armazenamento
Bw-tree diferente.

Novamente, tabelas e índices com otimização de memória são projetados para atender aos requisitos de desempenho
específicos de sistemas OLTP de taxa de transferência muito alta, com muitas inserções por segundo, mas também
inserções, atualizações e exclusões. Em outras palavras, o tipo de situação em que você provavelmente
experimentará o gargalo de travas de página na memória ao acessar tabelas baseadas em disco.

Mesmo se você não estiver enfrentando problemas de trava de memória, mas tiver um banco de dados
extremamente pesado para gravação, poderá ver alguns benefícios das tabelas com otimização de memória. Caso
contrário, o único outro uso regular de tabelas com otimização de memória é aprimorar o desempenho das
variáveis de tabela.

Novamente, nosso objetivo nesta seção é apenas examinar alguns dos principais recursos dos planos de execução
para consultas que acessam tabelas e índices com otimização de memória. Para mais detalhes sobre seu design
e uso, bem como as várias advertências que podem impedi-lo de usá-los, sugiro a documentação online da
Microsoft (http://bit.ly/2EQl2Lc) e o livro de Kalen Delaney sobre o assunto ( http://bit.ly/2BpDxXI).

Usando tabelas e índices com otimização de memória


A listagem 8-12 cria um banco de dados de teste e, nele, três tabelas com otimização de memória (copiadas do
AdventureWorks 2014) e as preenche com dados. Ajuste os valores das propriedades do arquivo, FILENAME,
SIZE e FILEGROWTH, conforme adequado ao seu sistema.

248
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

CRIAR BANCO DE DADOS InMemoryTest


NO PRIMÁRIO (NOME = InMemTestData,
NOME DO ARQUIVO = 'C:\Data\InMemTest.mdf',
TAMANHO = 10 GB, CRESCIMENTO DO ARQUIVO = 10
GB),
FILEGROUP InMem CONTÉM MEMORY_OPTIMIZED_DATA (NAME = InMem,
NOME DO ARQUIVO = 'c:\
data\inmem.ndf')
LOG ON (NOME = InMemTestLog, FILENAME
= 'C:\Data\InMemTestLog.ldf', SIZE = 5GB, FILEGROWTH =
1GB);

GO
--Mover para o novo banco de dados USE
InMemoryTest; GO --Cria algumas tabelas
CREATE TABLE dbo.Address (AddressID
INTEGER NOT NULL IDENTITY PRIMARY
KEY NONCLUSTERED HASH

COM
(BUCKET_COUNT = 128),
AddressLine1 VARCHAR(60) NOT NULL,
Cidade VARCHAR(30) NOT NULL,
StateProvinceID INT NOT NULL)
COM (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA); GO CREATE TABLE
dbo.StateProvince (StateProvinceID INTEGER NOT NULL PRIMARY KEY NÃO CLUSTERED,

StateProvinceName VARCHAR(50) NOT


NULO,
PaísRegiãoCódigo NVARCHAR(3) NOT
NULO)
COM (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA); CREATE TABLE
dbo.CountryRegion (CountryRegionCode NVARCHAR(3) NOT NULL PRIMARY KEY NONCLUSTERED,

PaísRegiãoNome NVARCHAR(50) NOT


NULO)
COM (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);

249
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

--Adicionar dados às tabelas --


Consultas de banco de dados cruzadas não podem ser usadas com tabelas na
memória SELECT a.AddressLine1, a.City, a.StateProvinceID INTO dbo.AddressStage
FROM AdventureWorks2014.Person.Address AS a; INSERT INTO dbo.Address
(AddressLine1, City, StateProvinceID)

SELECT a.AddressLine1, a.City,


a.StateProvinceID
FROM dbo.AddressStage
AS a; DROP TABLE dbo.AddressStage;
SELECIONE sp.StateProvinceID,
sp.Name, sp.CountryRegionCode INTO
dbo.ProvinceStage DE

AdventureWorks2014.Person.StateProvince
AS sp; INSERT INTO dbo.StateProvince (StateProvinceID,
StateProvinceName, CountryRegionCode)

SELECT ps.StateProvinceID, ps.Name,


ps.CountryRegionCode
FROM dbo.ProvinceStage
AS ps; DROP TABLE dbo.ProvinceStage;
SELECT cr.CountryRegionCode, cr.Name
INTO dbo.CountryStage FROM

AdventureWorks2014.Person.CountryRegion
AS cr; INSERT INTO dbo.CountryRegion (CountryRegionCode,
CountryRegionName)

SELECT cs.CountryRegionCode,
cs.Name FROM
dbo.CountryStage AS cs DROP TABLE
dbo.CountryStage; VAI

Listagem 8-12

Antes de nos aprofundarmos, vamos primeiro executar uma consulta que acessa as tabelas padrão do Adventure
Works baseadas em disco, para comparação.

250
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

SELECT a.AddressLine1, a.City,


sp.Name, cr.Name
FROM Person.Address
AS a JOIN
Person.StateProvince AS sp

ON sp.StateProvinceID = a.StateProvinceID
JOIN Person.CountryRegion AS cr ON
cr.CountryRegionCode = sp.CountryRegionCode
ONDE a.AddressID = 42;

Listagem 8-13

Ele produz um plano de execução padrão sem surpresas reais ou novas lições a serem aprendidas.

Figura 8-17: Plano de execução para consulta acessando tabelas padrão.

Podemos executar essencialmente a mesma consulta padrão em nossa tabela InMemoryTest, graças
ao componente Query Interop do OLTP na memória, que permite que o T-SQL interpretado faça
referência a tabelas com otimização de memória.

USE InMemoryTest; GO
SELECT a.AddressLine1,
a.City, sp.StateProvinceName,
cr.CountryRegionName

FROM dbo.Address AS a JOIN


dbo.StateProvince AS sp ON
sp.StateProvinceID = a.StateProvinceID JOIN
dbo.CountryRegion AS cr ON cr.CountryRegionCode =
sp.CountryRegionCode
ONDE a.AddressID = 42;

Listagem 8-14

251
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

O plano de execução que produz também não é muito anormal.

Figura 8-18: Plano de execução para consulta acessando tabelas com otimização de memória.

No entanto, existem algumas diferenças:

• Podemos ver um novo operador Index Seek (NonClusteredHash) para acessar


a tabela de endereços.

• Examine a propriedade Storage de qualquer um dos operadores Index Seek e você verá que é
MemoryOptimized em vez de RowStore.
• Os custos estimados para as buscas são menores porque o índice com otimização de memória é
considerado mais eficiente.

Sobre o último ponto, lembre-se que um custo menor estimado não significa necessariamente que essas
operações custem mais ou menos. Você não pode comparar efetivamente os custos das operações de um
determinado plano com os custos das operações de outro plano. São apenas estimativas. As estimativas para um
plano regular levam em conta o fato de que alguns dos custos estarão acessando dados do disco, enquanto as
estimativas de custo para planos na memória serão apenas recuperar dados da memória.

As consultas padrão em tabelas com otimização de memória gerarão um plano de execução completamente
padrão. Você poderá entender quais índices foram acessados e como eles são acessados. Internamente há
muita coisa acontecendo, mas visivelmente, no plano gráfico, não há muito o que ver.

Fica mais interessante quando olhamos para uma consulta ligeiramente diferente.

252
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Nenhuma opção para buscar um índice de hash para um intervalo de valores

Vamos modificar um pouco a consulta, procurando um intervalo de endereços em vez de apenas um.

SELECT a.AddressLine1,
Uma cidade,
sp.StateProvinceName,
cr.CountryRegionName
FROM dbo.Address AS a
JOIN dbo.StateProvince AS sp
ON sp.StateProvinceID = a.StateProvinceID
JOIN dbo.CountryRegion AS cr
ON cr.CountryRegionCode = sp.CountryRegionCode
ONDE a.Endereço ID ENTRE 42
E 52;

Listagem 8-15

Se eu executar a consulta equivalente no banco de dados AdventureWorks normal, obterei um plano


de execução conforme mostrado na Figura 8-19.

Figura 8-19: Plano de execução padrão com operadores Index Seek.

O operador BETWEEN não afeta se o índice clusterizado é usado para uma operação de busca.
Ainda é um mecanismo eficiente para recuperar dados do índice clusterizado na tabela Endereço.
Compare isso com o plano de execução em relação ao índice de hash com otimização de memória.

253
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Figura 8-20: Plano de execução para consulta usando índice de hash com otimização de memória.

Em vez de uma busca no índice de hash, vemos uma varredura de tabela na tabela Endereço.
Isso ocorre porque o índice de hash não é propício para seleções de valores de intervalo, mas é otimizado para
pesquisas de ponto. Observe também que o otimizador não pode enviar um predicado de pesquisa para uma
varredura ao executar no modo de interoperabilidade , portanto, ele deve passar todas as 19.614 linhas para
o operador Filtro .

Se esse fosse o tipo comum de consulta sendo executado nessa tabela, precisaríamos ter um índice não
clusterizado com otimização de memória na tabela para oferecer melhor suporte a esse tipo de consulta. Você
pode usar seus planos de execução para avaliar esse tipo de informação em tabelas e consultas com otimização
de memória.

Planos com procedimentos armazenados compilados nativamente

Um objeto adicional que foi introduzido com tabelas com otimização de memória é o procedimento
armazenado compilado nativamente. Atualmente, o comportamento aqui é diferente das consultas padrão, conforme
demonstrado acima. A Listagem 8-17 cria um procedimento armazenado compilado nativamente a partir da consulta
na Listagem 8-15.

CREATE OR ALTER PROC dbo.AddressDetails @AddressIDMin INT, @


EndereçoIDMax INT
COM NATIVE_COMPILATION, SCHEMABINDING, EXECUTE COMO PROPRIETÁRIO COMO
COMECE ATÔMICO COM (NÍVEL DE ISOLAMENTO DE TRANSAÇÃO = INSTANTÂNEO, IDIOMA
= N'us_english')
SELECT a.AddressLine1,
Uma cidade,
sp.StateProvinceName,
cr.CountryRegionName

254
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

FROM dbo.Address AS a JOIN


dbo.StateProvince AS sp ON
sp.StateProvinceID = a.StateProvinceID JOIN
dbo.CountryRegion AS cr ON cr.CountryRegionCode =
sp.CountryRegionCode WHERE a.AddressID BETWEEN
@AddressIDMin @AddressIDMax ; E

END
GO
EXECUTE dbo.AddressDetails @AddressIDMin = 42, -- int @AddressIDMax
= 52; --int

Listagem 8-16

Não podemos executar a consulta e obter um plano de execução real. Essa é uma limitação com os
procedimentos compilados. Podemos obter um plano estimado.

Figura 8-21: Plano de execução para consulta acessando um procedimento armazenado compilado nativamente.

Ainda vemos o Table Scan na tabela Address, porque não há índice de suporte, mas desta vez, mas se examinarmos
suas propriedades, veremos que o pushdown de predicado é suportado no código compilado nativamente.

Figura 8-22: Empilhamento de predicado em código compilado nativamente.

255
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Uma varredura em uma tabela com otimização de memória é mais rápida e diferente internamente do que uma
tabela padrão, mas se a tabela tiver alguns milhões de linhas, ainda levará tempo para varrer todas elas, e um
índice Bw-tree ainda seria útil para esta consulta. Mesmo que tenha escolhido alterar a tabela para fornecer um
índice, o plano em si não irá recompilar e mostrar as diferenças, apenas escolherá o índice em tempo de execução.

Observe que todos os custos estimados são zero porque a Microsoft está custeando esses procedimentos de uma
nova maneira que não é refletida externamente. Não há um único valor além de zero em nenhum dos custos
estimados dentro de nenhuma das propriedades para qualquer um dos operadores. Vejamos as propriedades do
operador SELECT .

Figura 8-23: Propriedades do operador SELECT mostrando custos estimados de zero para
código compilado nativamente.

Isso representa o conjunto completo de propriedades disponíveis. Nenhuma das propriedades úteis que discutimos
anteriormente no livro, como o motivo da rescisão antecipada , existe aqui. Isso ocorre devido às diferenças em
como esses planos são armazenados (por exemplo, esse plano não está no cache do plano) e como eles são
gerados.

No momento da redação deste artigo, os planos de execução do SQL Server 2017, quando usados com os
procedimentos armazenados otimizados de memória compilada, são menos úteis. A falta de contagens de linhas
e custos afeta sua capacidade de tomar decisões com base nos planos, mas eles ainda fornecem boas
informações, o que deve permitir que você veja as ações realizadas quando a consulta é executada e descubra
por que uma consulta é lenta.

256
Machine Translated by Google

Capítulo 8: Examinando o uso do índice

Resumo
É difícil exagerar o impacto dos índices e suas estatísticas de suporte na qualidade dos planos que o otimizador
gera.

Você nem sempre pode resolver um problema de desempenho apenas adicionando um índice. É perfeitamente
possível ter muitos índices, então você deve ser criterioso em seu uso. Você precisa garantir que o índice seja
seletivo e deve fazer as escolhas apropriadas em relação à adição ou inclusão de colunas em seus índices,
clusterizados e não clusterizados.

Você também precisará ter certeza de que suas estatísticas refletem com precisão os dados armazenados
no índice porque a escolha do índice usado no plano é baseada na contagem de linhas estimada do
otimizador e nos custos estimados do operador, e as contagens de linhas estimadas são baseadas em
Estatisticas. Se você usar valores de parâmetro de entrada codificados, o otimizador poderá usar estatísticas
para esse valor específico, mas o SQL Server perderá a capacidade de reutilizar planos para essas consultas. Se
o otimizador puder farejar parâmetros, como quando usamos um procedimento armazenado, ele pode usar
estatísticas precisas, mas um plano reutilizado com base em um parâmetro farejado pode sair pela culatra se o
próximo parâmetro tiver uma contagem de linhas muito diferente.

257
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

Todos os processos que o otimizador precisa realizar para gerar planos de execução têm um custo. Custa
tempo e recursos de CPU para elaborar uma estratégia de execução para uma consulta. Para consultas simples,
o SQL Server pode gerar um plano em menos de um milissegundo, mas em sistemas OLTP típicos há muitas
dessas consultas curtas e rápidas e os custos podem aumentar. Se a carga de trabalho também incluir consultas
complexas de agregação e relatórios, o otimizador levará mais tempo para criar um plano de execução para cada
uma.

Portanto, faz sentido que o SQL Server queira evitar pagar o custo de gerar um plano toda vez que precisar
executar uma consulta, e é por isso que tenta ao máximo reutilizar as estratégias de execução de consulta
existentes. O otimizador os salva como planos reutilizáveis, em uma área de memória chamada cache de planos.
Idealmente, se o otimizador encontrar uma consulta que já viu antes, ele pega uma estratégia de execução pronta
para ela do cache do plano e a passa diretamente para o mecanismo de execução. Dessa forma, o SQL Server
gasta valiosos recursos de CPU executando nossas consultas, em vez de sempre ter que primeiro elaborar um
plano e depois executá-lo.

O SQL Server fará o possível para promover a reutilização do plano automaticamente, mas há limites para o que
ele pode fazer sem nossa ajuda como programadores. Felizmente, munidos de algumas técnicas simples, podemos
garantir que nossas consultas sejam parametrizadas corretamente e que os planos sejam reutilizados com a maior
frequência possível; Eu vou te mostrar exatamente o que você precisa fazer. Também exploraremos alguns dos
problemas que podem ocorrer com a reutilização de planos e o que você pode fazer a respeito.

Consultando o cache do plano


Conforme discutido no Capítulo 1, quando submetemos qualquer consulta para execução, o otimizador gera um
plano, caso ainda não exista, que possa ser reutilizado e o armazena em uma área do buffer pool chamada cache
do plano. Nosso objetivo como programadores, DBAs e desenvolvedores de banco de dados é ajudar a promover
o uso eficiente dessa memória, o que significa que o plano de uma consulta é reutilizado do cache e não criado ou
recriado cada vez que a consulta é chamada, a menos que alterações nas estruturas ou estatísticas exigem a
recompilação do plano.

258
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

O cache de planos possui quatro armazenamentos de cache que armazenam planos (consulte https://bit.ly/2mgrS6s
para obter mais detalhes). Os planos compilados nos quais estamos interessados serão armazenados nos planos SQL
armazenamento de cache (CACHESTORE_SQLCP) ou armazenamento de planos de objeto (CACHESTORE_OBJCP),
dependendo do tipo de objeto (objtype):

• O armazenamento de planos SQL contém planos para consultas ad hoc, que possuem um tipo de obj de
Adhoc, bem como planos para consultas parametrizadas automaticamente e instruções preparadas,
ambas com um tipo de obj de Preparado.

• O armazenamento de planos de objetos contém planos para procedimentos, funções, gatilhos e alguns
outros tipos de objeto e cada plano terá um valor de ID de objeto associado. Planos para procedimentos
armazenados, funções escalares definidas pelo usuário ou funções com valor de tabela de várias instruções
têm um objtype de Proc e gatilhos têm um objtype
de Gatilho.

Para examinar os planos atualmente no cache, bem como explorar a reutilização de planos, podemos consultar um
conjunto de objetos de gerenciamento dinâmico (DMOs) relacionados à execução. Sempre que executamos uma
consulta ad hoc, um lote ou um objeto como um procedimento armazenado, o otimizador armazena o plano. Um
identificador, chamado plan_handle, identifica exclusivamente o plano de consulta em cache para cada consulta, lote ou
procedimento armazenado que foi executado.

Podemos fornecer o plan_handle como um parâmetro para o sys.dm_exec_sql_text


função para retornar o texto SQL associado a um plano, bem como ao sys.dm_exec_
função query_plan, para retornar o plano de execução no formato XML. Vários DMOs armazenam o plan_handle,
mas neste capítulo, usaremos principalmente:

• sys.dm_exec_cached_plans – retorna uma linha para cada plano armazenado em cache e fornece
informações como o tipo de plano, o número de vezes que foi usado e seu tamanho.

• sys.dm_exec_query_stats – retorna uma linha para cada instrução de consulta em


cada plano armazenado em cache e fornece estatísticas de execução, agregadas ao longo do tempo em
que o plano esteve em cache. Muitas das colunas são contadores e fornecem informações sobre quantas
vezes o plano foi executado e os recursos que foram utilizados.

Há também alguns DMOs que fornecem estatísticas de execução agregadas semelhantes a


sys.dm_exec_query_stats, mas para objetos específicos, cada um dos quais terá um plano separado, com um
valor object_id associado. Temos sys.dm_exec_proce dure_stats para procedimentos armazenados,
sys.dm_exec_trigger_stats para gatilhos e sys.dm_exec_function_stats para funções escalares definidas pelo usuário.
Embora as funções com valor de tabela de várias instruções obtenham um plano, com um valor object_id, esses planos
apenas

259
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

aparecem em sys.dm_exec_query_stats. As exibições em linha e as funções com valor de tabela não


obtêm um plano separado porque seu comportamento é incorporado ao plano para a consulta que as faz
referência.

Todos os DMOs anteriores são para investigar planos para consultas que concluíram a execução. No entanto,
como o plano de execução já está armazenado no cache quando a execução é iniciada, também podemos
examinar o plano para consultas que ainda estão em execução, usando o sys.dm_
exec_requests DMV. Isso é útil se o seu sistema estiver enfrentando pressão de recursos agora, devido a
consultas em execução no momento, provavelmente de longa duração. Esse DMV armazena o plan_handle e uma
variedade de outras informações, incluindo estatísticas de execução, para qualquer consulta em execução no momento,
seja ad hoc, uma instrução preparada ou parte de um módulo de código.

Usando esses DMOs, podemos construir consultas simples que, para cada plan_handle, retornarão, por
exemplo, o texto da consulta associada e um valor XML que representa o plano armazenado em cache para
essa consulta, juntamente com muitas outras informações úteis. Veremos alguns exemplos ao longo do capítulo,
embora eu não esteja cobrindo os DMOs em detalhes.

Mais sobre os DMOs

Você pode consultar a documentação da Microsoft (http://bit.ly/2m1F6CA), ou o excelente livro de Louis Davidson
e Tim Ford, Performance Tuning with SQL Server Dynamic Management Views (https://bit.ly/2Je3evr), que é
disponível como um e-book gratuito. As consultas de diagnóstico de Glenn Berry (http://bit.ly/Q5GAJU) incluem
muitos exemplos sobre o uso de DMOs para consultar o cache. Finalmente, você pode pular a escrita de suas
próprias consultas e usar o sp_WhoIsActive de Adam Machanic
(http://whoisactive.com/).

Planejar reutilização e consultas ad hoc


Quando uma consulta é enviada, o mecanismo primeiro calcula o QueryHash e procura os valores correspondentes
no cache do plano. Se algum for encontrado, ele fará uma comparação detalhada do texto SQL completo.
Se eles forem idênticos, supondo que também não haja diferenças nas opções SET ou ID do banco de dados, ele
pode ignorar o processo de compilação e simplesmente enviar o plano em cache para execução.
Isso é uma reutilização eficiente de planos no trabalho e gostaríamos de promovê-la o máximo possível. Infelizmente,
o uso de consultas ad hoc com literais embutidos em código, para citar um exemplo, derrota a reutilização do plano.

A Listagem 9-1 limpa o cache do plano e, em seguida, executa um lote que consiste em três consultas ad hoc,
que concatenam as colunas de nome na tabela Person do AdventureWorks. A primeira e a segunda consultas são
idênticas em tudo, exceto no valor fornecido para BusinessEntityID, e a segunda e a terceira diferem apenas na
formatação de espaço em branco.

260
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;


VAI
' ' ' '
SELECT ISNULL(p.Título, '') + FROM + p.Nome + + p.Sobrenome
Pessoa.Pessoa AS p
WHERE p.BusinessEntityID = 5;
' ' ' '
SELECT ISNULL(p.Título, '') + FROM + p.Nome + + p.Sobrenome
Pessoa.Pessoa AS p
WHERE p.BusinessEntityID = 6;
' ' ' '
SELECT ISNULL(p.Title, '') + + p.FirstName + + p.LastName FROM Person.Person AS p WHERE
p.BusinessEntityID = 6;
VAI

Listagem 9-1

Os planos para cada consulta são os mesmos em cada caso, consistindo em apenas três operadores.
Se você examinar os valores QueryHash e QueryPlanHash do operador SELECT, verá que eles são idênticos para
cada plano. No entanto, vamos ver o que está armazenado no cache do plano. Todos os DMOs usados nessa consulta têm
escopo de servidor, portanto, o contexto do banco de dados para a consulta é irrelevante.

SELECT cp.usecounts,
cp.objtype,
cp.plan_handle,
DB_NAME(st.dbid) AS DatabaseName,
OBJECT_NAME(st.objectid, st.dbid) AS ObjectName,
st.texto,
qp.query_plan
DE sys.dm_exec_cached_plans AS cp
CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) AS st
CROSS APPLY sys.dm_exec_query_plan(cp.plan_handle) AS qp
WHERE st.text LIKE '%Person%'
AND st.dbid = DB_ID('AdventureWorks2014')
AND st.text NOT LIKE '%dm[_]exec[_]%' ;

Listagem 9-2

A Figura 9-1 mostra o conjunto de resultados, com uma entrada.

Figura 9-1: Resultados da consulta ao cache do plano.

261
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

Quando enviamos ao processador de consultas um lote, ou um procedimento armazenado ou função, contendo


várias instruções, todo o lote será compilado de uma vez e, portanto, o otimizador produziu um plano para
todo o lote Adhoc. Se você verificar o valor da coluna de texto, verá que é o texto SQL de todo o lote. A coluna
final no conjunto de resultados, query_plan, contém a representação XML do plano de execução da consulta.
Ao visualizar os resultados na visualização em grade, esses valores XML são exibidos como hiperlinks, e
podemos clicar em um para mostrar a forma gráfica do plano de execução. Como você pode ver, o otimizador
produz um plano para o lote, que contém planos individuais para cada instrução do lote.

Figura 9-2: Três planos de execução que parecem iguais apesar de serem de três consultas.

A primeira coluna do conjunto de resultados, na Figura 9-1, usecounts, nos informa o número de vezes
que um plano foi consultado no cache. Nesse caso, é uma vez, e a única maneira de o plano para este lote
ser reutilizado é se enviarmos exatamente o mesmo lote novamente; mesma formatação, mesmos valores
literais. Se executarmos novamente apenas parte do mesmo lote, como a última consulta, depois de executar
novamente a Listagem 9-2, veremos uma nova entrada e um novo plano gerado.

A DMV sys.dm_exec_query_stats nos mostra uma visão ligeiramente diferente disso, pois retorna uma linha
para cada instrução de consulta em um plano armazenado em cache.

262
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

SELECIONAR SUBSTRING(
dest.text,
(deqs.statement_start_offset / 2) + 1, (CASE
deqs.statement_end_offset WHEN -1 THEN
DATALENGTH(dest.text)

ELSE
deqs.statement_end_offset - deqs.
statement_start_offset END ) / 2
+1)
AS
QueryStatement,
deqs.creation_time, deqs.execution_count,
deqp.query_plan FROM
sys.dm_exec_query_stats AS deqs CROSS
APPLY sys.dm_exec_query_plan(deqs.plan_handle)
AS deqp CROSS APPLY sys.dm_exec_sql_text(deqs.plan_handle) AS dest

WHERE dest.text LIKE '%Person%'


AND deqp.dbid = DB_ID('AdventureWorks2016')
E dest.text NÃO É COMO '%dm[_]exec[_]%'
ORDER BY deqs.execution_count DESC,
deqs.creation_time;

Listagem 9-3

Para ver algumas diferenças em contagens e lotes, execute a instrução final no lote da Listagem 9-1
duas vezes. A Figura 9-3 mostra os resultados após executar toda a Listagem 9-1 uma vez e, em
seguida, essas duas execuções adicionais.

Figura 9-3: Várias execuções do cache do plano.

Claro, eu poderia ter optado, na Listagem 9-3, por retornar muitas outras colunas contendo
estatísticas úteis de execução, como as leituras e gravações físicas e lógicas agregadas e o tempo
de CPU, resultante de todas as execuções de cada plano, pois essas informações foi armazenado no
cache.

263
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

O custo da compilação excessiva do plano


Um dos piores criminosos pelo uso indevido do cache do plano é o uso excessivo desnecessário de consultas ad
hoc não parametrizadas. Às vezes, eles são gerados dinamicamente por uma biblioteca de aplicativos mal escrita ou por
uma camada ORM (Object-Relational Mapping) configurada incorretamente entre o aplicativo e o banco de dados. Você
também vê muito mais compilações de planos quando uma ferramenta ORM é mal codificada para que ela crie diferentes
definições de parâmetros com base no comprimento da string que está sendo passada, por exemplo VARCHAR(3) para 'Dog'
ou VARCHAR(5)
para 'Cavalo'.

SQL dinâmico é qualquer SQL declarado como um tipo de dados string, e uma consulta ad hoc é qualquer consulta em
que o texto da consulta é enviado diretamente ao SQL Server, em vez de ser incluído em um módulo de código
(procedimento armazenado, função escalar definida pelo usuário, -statement função definida pelo usuário ou gatilho). Os
exemplos incluem consultas não parametrizadas digitadas no SSMS e consultas SQL dinâmicas enviadas por meio de
EXEC(@sql) ou sp_excutesql, bem como qualquer consulta enviada e enviada de um programa cliente, que pode ser
parametrizada, em uma instrução preparada ou pode ser apenas uma string não parametrizada, dependendo de como o
código do cliente é construído.

Em casos extremos, as consultas não parametrizadas são executadas de forma iterativa, linha por linha, em vez de uma
única consulta baseada em conjunto. A Listagem 9-4 usa nossa consulta anterior em algumas iterações. A primeira iteração
codifica o valor @id (para BusinessEntityID) em uma string SQL dinâmica e passa a string para o comando EXECUTE.

A segunda iteração usa o procedimento sp_executesql para criar uma instrução preparada contendo uma string
parametrizada, para a qual passamos valores de parâmetro. Essa abordagem permite a reutilização do plano. Não se
preocupe muito com os detalhes aqui, pois discutiremos as declarações preparadas mais adiante no capítulo. O ponto-chave
aqui é que queremos comparar o trabalho realizado pelo SQL Server para executar o mesmo SQL ad hoc várias vezes, em
um caso em que ele não pode reutilizar planos e em outro em que pode.

Obviamente, ambas as abordagens iterativas ainda são altamente ineficientes, uma vez que podemos obter o conjunto de
resultados desejado de uma maneira baseada em conjunto, com uma única execução de uma consulta.

DECLARE @ii INT;


DECLARE @IterationsToDo INT = 500;
DECLARE @id VARCHAR(8);
SELECIONAR @ii = 1;
ENQUANTO @ii <= @IterationsToDo

264
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

COMEÇAR

SELECIONE @ii = @ii + 1,


@id = CONVERT(VARCHAR(5), @ii);
'' ''
+ Nome + ''
EXECUTE ('SELECT ISNULL(Título, '''') + + LastName FROM
''
Person.Person WHERE BusinessEntityID =' + @id);
FIM;
VAI
DECLARE @ii INT;
DECLARE @IterationsToDo INT = 500;
DECLARE @id VARCHAR(8);
SELECIONAR @ii = 1;
ENQUANTO @ii <= @IterationsToDo
COMEÇAR

SELECIONE @ii = @ii + 1,


@id = CONVERT(VARCHAR(5), @ii);
EXEC sys.sp_executesql N'
'' '' '' ''
SELECT ISNULL(Título, '''') + + Nome + + Sobrenome
FROM Person.Person WHERE BusinessEntityID = @id',
N'@id int',
@id = @ii;
FIM;
VAI

Listagem 9-4

Se você capturar métricas de desempenho usando Eventos Estendidos, verá que a primeira iteração executa
cerca de 3.500 leituras lógicas e leva 368.890 microssegundos, a segunda executa 1.500 leituras lógicas e
leva 26.329 microssegundos. Observe que STATISTICS IO não mostra o trabalho extra; você vê apenas o
trabalho feito diretamente pela consulta, não o trabalho extra feito em nome da consulta, para planejar o
gerenciamento de cache.

A abordagem, usando strings ad hoc, dinâmicas e não parametrizadas, inunda o cache do plano com
500 cópias de uso único do mesmo plano (você pode executar a Listagem 9-2 para verificar). As leituras
lógicas extras que isso requer, sobre a abordagem iterativa que reutiliza o plano, é um trabalho extra
associado à compilação e armazenamento desses planos. São apenas 4 leituras lógicas extras por iteração,
mas se o seu sistema for inundado com consultas ad hoc não parametrizadas, todo esse trabalho extra será
adicionado rapidamente.

Causa problemas maiores também. Aumenta a quantidade de processamento de CPU que o servidor deve
executar, compilando e armazenando novos planos de forma contínua e desnecessária. Ele também
desperdiça recursos de memória, usando memória cache de buffer para armazenar planos que serão usados
apenas uma vez. A menos que você tenha o luxo de memória de servidor suficiente para acomodar todos os parâmetros

265
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

combinação de cada consulta, pode levar ao "cache churn", onde planos mais antigos, aqueles que podem ser
úteis, planos reutilizáveis, são continuamente despejados para dar espaço para a enxurrada de planos de
consulta ad hoc. Em casos graves, pode levar à pressão da memória.

Se você estiver enfrentando esses problemas, há várias maneiras de consultar o cache do plano para
confirmar ou refutar se ele está relacionado ao uso excessivo de consultas ad hoc. Por exemplo, a
consulta simples na Listagem 9-5 informará a proporção de cada tipo de plano compilado no cache.

SELECT decp.objtype,
CAST(100,0 * COUNT(*) / SUM(COUNT(*)) OVER () COMO DECIMAL(5,
2)) AS plan_In_Cache
FROM sys.dm_exec_cached_plans AS decp
GROUP BY decp.objtype
ORDER BY planos_In_Cache;

Listagem 9-5

Os resultados desta consulta não significam muito, como uma execução única. Você precisará monitorar os
valores ao longo do tempo e entender quais são os números esperados para o seu sistema, juntamente com
métricas paralelas, como Solicitações de Lote/s e Compilações SQL/s, usando Perfmon ou rastrear eventos
diretamente com Eventos Estendidos. Você também pode recuperar os tipos de plano do Repositório de
Consultas.

Vários recursos online fornecem scripts mais detalhados para examinar o uso e abuso do cache do plano;
veja, por exemplo, https://bit.ly/2EfYOkl.

Parametrização simples para consultas ad hoc "triviais"


Para consultas muito simples de uma tabela, o otimizador pode reconhecer que, se uma consulta fornecesse
um parâmetro em vez de um literal, seria capaz de criar um plano de execução que poderia ser reutilizado.
Nesses casos, o otimizador tentará criar um parâmetro automaticamente para você, por meio de um
processo chamado "parametrização simples". Isso só funciona para planos de execução que se qualificam
como planos triviais (consulte o Capítulo 1), pois é somente para estes que o otimizador pode ter certeza de
que o mesmo plano funcionará bem, independentemente do valor do parâmetro fornecido.

Parametrização simples em ação


Encontramos parametrização simples no Capítulo 2, mas não a abordamos em detalhes, então vamos vê-la
em ação novamente. Execute a Listagem 9-6 e capture o plano real.

266
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

SELECIONE a.IDEndereço,
a.Linha de Endereço1,
Uma cidade
DE Pessoa.Endereço AS a
ONDE a.AddressID = 42;

Listagem 9-6

A Figura 9-4 mostra o plano de execução muito simples. Destaquei a primeira indicação visível de que
o otimizador executou uma parametrização simples. Você pode ver que a consulta destacada é diferente
da consulta que escrevi e executei, porque o valor codificado para AddressID foi substituído por um parâmetro
chamado @1.

Figura 9-4: Primeira evidência visual de parametrização simples.

Se o texto da consulta for mais longo, talvez você não veja essa pista no plano de execução gráfica.
O melhor lugar para procurar é nas propriedades do operador SELECT , especificamente na
Lista de Parâmetros.

Figura 9-5: Propriedades SELECT mostrando evidências de parametrização simples.

267
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

Assim como vemos para procedimentos armazenados ou qualquer outra consulta parametrizada, a Lista de Parâmetros
mostra o nome de quaisquer parâmetros, seus valores de tempo de compilação e de execução e seus tipos de
dados. Não temos controle sobre a nomenclatura desses parâmetros; eles serão simplesmente listados na ordem em
que o otimizador os cria. Também não temos controle sobre os tipos de dados; o otimizador escolhe o tipo de dado para
parametrização simples com base no tamanho do valor passado para ele. Você também pode ver que o mecanismo de
consulta respeitou a parametrização, observando o valor na parte inferior da Figura 9-5, StatementParameterizationType.

Se este valor for 0, não ocorreu parametrização. Neste caso o valor é 2, indicando parametrização simples.

Execute novamente a Listagem 9-6, mas com um valor codificado de 100, e você verá que o valor do tempo de
compilação permanece em 42, mas o valor do tempo de execução muda para 100. Se consultarmos sys.dm_
exec_cached_plans (consulte a Listagem 9-2), veremos a seguinte saída.

Figura 9-6: Planear contagens de uso do cache do plano.

A entrada inferior na saída mostra que o otimizador reutilizou o plano existente que ele criou para a consulta
parametrizada automaticamente, transformando-o efetivamente em uma instrução preparada.
Na coluna de texto , podemos ver o parâmetro usado (@1) e seu tipo de dados, neste caso tinyint. Para números
inteiros, o otimizador usa o menor tipo de dados que pode ajustar o valor. Se tivéssemos passado um valor de,
digamos, 300 em vez de 42, o tipo de dados seria um smallint
em vez de um tinyint. Isso pode significar que mesmo quando ocorre uma parametrização simples, ainda podemos ter
mais de um plano em cache para a mesma consulta trivial, mas com diferenças no tamanho do parâmetro. Esta não é
uma grande preocupação, mas é algo para estar ciente.

As duas primeiras entradas na Figura 9-6 são para consultas ad hoc individuais (com literais embutidos em código).
No entanto, se você clicar nos links para os planos de consulta para cada uma dessas entradas, verá que eles consistem
apenas em um operador SELECT . A primeira coisa que o SQL Server faz quando emitimos uma consulta é procurar uma
correspondência textual exata no cache do plano. Isso é feito antes da parametrização simples, e obviamente requer que
a consulta de pré-parametrização seja armazenada.
No entanto, esses planos "espaços reservados" nunca são concluídos ou executados. Você pode confirmar isso
consultando sys.dm_exec_query_stats (Listagem 9-3), que mostra apenas um único plano para essa consulta, executado
duas vezes.

268
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

Você também pode usar o Repositório de Consultas para recuperar as contagens de execução, contagens de
compilação, o tipo de plano e o tipo de parametrização. A Listagem 9-7 mostra as informações disponíveis.

SELECT qsqt.query_sql_text,
qsq.query_parameterization_type_desc,
qsq.count_compiles,
qsp.is_trivial_plan,
qsrs.count_executions
DE sys.query_store_query AS qsq
JOIN sys.query_store_query_text AS qsqt
ON qsqt.query_text_id = qsq.query_text_id
JOIN sys.query_store_plan AS qsp
ON qsp.query_id = qsq.query_id
JOIN sys.query_store_runtime_stats AS qsrs
ATIVADO qsrs.plan_id = qsp.plan_id
WHERE qsqt.query_sql_text LIKE '%@1%';

Listagem 9-7

Os resultados seriam semelhantes à Figura 9-7.

Figura 9-7: Resultados do Query Store mostrando várias execuções.

O otimizador deve ter certeza de que qualquer possível consulta que possa usar o plano parametrizado
automaticamente será executada com segurança e não a aplicará em casos que possam causar instabilidade no
plano. Em suma, é muito cauteloso em sua aplicação de parametrização simples e é facilmente dissuadido.

Conforme observado anteriormente, um pré-requisito é que o plano seja trivial, como foi para nossa consulta
na Listagem 9-6 e conforme indicado por um Nível de Otimização de TRIVIAL na Figura 9-5 e o is_
indicador trivial_plan na Figura 9-7. No entanto, isso não significa que qualquer plano trivial será parametrizado
automaticamente. Se você capturar os planos reais para as consultas na Listagem 9-1 e verificar as propriedades
do operador SELECT , verá que eles também obtêm planos triviais, mas não verá nenhuma lista de parâmetros.
Nesse caso, a parametrização simples é derrotada pela inclusão da função ISNULL na consulta (remova-a e ela
funciona). No Capítulo 3 (Listagem 3-4), vimos um caso semelhante, onde a parametrização simples foi derrotada
pelo uso de um predicado LIKE.

269
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

O que acontece se precisarmos nos juntar a outra tabela em nossa consulta?

SELECIONE a.IDEndereço,
a.Linha de Endereço1,
Uma cidade,
bea.BusinessEntityID
DE Pessoa.Endereço AS a
JOIN Person.BusinessEntityAddress AS bea
ON bea.AddressID = a.AddressID
ONDE a.AddressID = 42;
Listagem 9-8

A Figura 9-8 mostra as propriedades relevantes do plano resultante. Como você pode ver, o Nível de
Otimização será FULL, em vez de TRIVIAL. Como um plano trivial é uma pré-condição da
parametrização simples, não veremos parâmetros.

Figura 9-8: Propriedades SELECT mostrando o Nível de Otimização.

Existem muitas outras cláusulas e condições que anularão a parametrização simples se incluídas em
uma consulta, como GROUP BY, DISTINCT, TOP, UNION, INTO, BULK
INSERIR, COMPUTAR e outros. Para obter mais detalhes, consulte a documentação da Microsoft em
https://bit.ly/2LS6Api.

Parametrização simples "insegura"


A parametrização simples e as regras que a regem não são tão simples quanto parecem. Tente capturar um
plano real para a consulta na Listagem 9-9.

SELECT Pessoa.Nome + Pessoa.Título ' '


+ Pessoa.Sobrenome,

DE Pessoa.Pessoa
WHERE Pessoa.Sobrenome = 'Diaz';
Listagem 9-9

270
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

As propriedades do operador SELECT mostram uma lista de parâmetros, aparentemente indicando


que o otimizador fez uma parametrização simples. Mas fez isso? Olhe mais alto e você verá que o
Nível de Otimização é FULL, e anteriormente eu disse que TRIVIAL era um pré-requisito para
parametrização simples.

Figura 9-9: Propriedades SELECT mostrando parametrização, mas não realmente.

De fato, a parametrização simples não ocorreu. Altere 'Diaz' para 'Brown' na Listagem 9-9, execute-o
novamente e, em seguida, consulte sys.dm_exec_cached_plans ou sys.dm_exec_
query_stats DMO. Você verá dois planos, um para cada execução, cada um sem parâmetros. Também
podemos ver que a propriedade StatementParameterizationType, visível apenas se o Repositório de
Consultas estiver habilitado no banco de dados, e um valor encontrado apenas em planos reais por
ser uma métrica de tempo de execução, está definido com o valor 0. Isso indica que nenhum parâmetro
foi usado no a execução da consulta.

O plano de consulta capturado no Repositório de Consultas também mostra a tentativa de


parametrização, incluindo a versão parametrizada da instrução. No entanto, o valor da coluna
query_parameter ization_type será zero, indicando que não houve parametrização, e a coluna
query_sql_text mostra o texto da consulta original, não a versão parametrizada que mostraria se a
parametrização tivesse sido bem-sucedida.

271
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

Nem todos os detalhes do processo de parametrização simples são totalmente documentados, portanto, o que
se segue é apenas uma "especulação educada", com base no entendimento e observações atuais. Parece
que há duas fases. A primeira fase, antes da compilação real, examina apenas o texto da consulta para
determinar se a consulta pode se qualificar para parametrização simples. Uma longa lista de palavras-chave é
verificada e, caso nenhuma delas ocorra na consulta, ela será parametrizada e entregue ao otimizador. Caso
contrário, a consulta é enviada ao otimizador inalterada, com todas as constantes no lugar.

O otimizador, como sempre, primeiro verificará se a otimização TRIVIAL se aplica. Além da mesma lista
de palavras-chave verificadas para parametrização simples, agora também considera outros objetos de banco
de dados, como restrições, índices e assim por diante. Nesse estágio, o otimizador pode concluir que a
parametrização simples não é segura. A parametrização é desfeita e a consulta original não parametrizada é
compilada.

Infelizmente, essa série de eventos faz com que o SSMS mostre (e capture o Query Store) o plano de
execução como se estivesse parametrizado. O fato de a propriedade StatementParameter izationType ter o
valor zero (consulte a Figura 9-9) é o único indicador de que o plano de execução exibido não é o plano
que foi usado.

Obviamente, quando uma consulta se qualifica para parametrização simples na primeira verificação e também
se qualifica para otimização trivial na segunda verificação, a versão parametrizada da consulta será compilada
e todos os planos mostrados no SSMS, no Query Store e nos DMOs, mostrará a versão parametrizada.

Se você simplesmente omitir a coluna Título da Listagem 9-9 e executá-la novamente, verá que a
parametrização simples agora é bem-sucedida.

A inclusão da coluna Título, na Listagem 9-9, exigiu uma Pesquisa de Chave, o que significa que é um
limite no qual uma varredura de índice clusterizado é a melhor opção; sem Título, o índice é abrangente e
sempre será utilizado. Provavelmente, isso explica por que a parametrização simples agora é "segura".

Por fim, você verá no valor do tipo de dados do parâmetro que, para strings, o otimizador escolhe um
comprimento máximo muito longo e, portanto, poderá reutilizar esse plano para strings de entrada muito mais
longas.

272
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

Figura 9-10: Propriedades SELECT mostrando parametrização simples bem-sucedida.

Programação para Reutilização do Plano: Parametrização


Consultas
Como vimos anteriormente, se simplesmente codificarmos os valores diretamente em uma string SQL
dinâmica e depois passá-los diretamente para o SQL Server para execução usando o comando EXECUTE ou por
qualquer outro método, o otimizador não poderá reutilizar um plano armazenado em cache para uma execução
subsequente em que o A string SQL difere apenas pelo valor codificado. Embora a reutilização do plano seja nosso
foco aqui, um problema muito maior com essa abordagem é sua vulnerabilidade a ataques de SQL Injection. Eu
não posso cobrir o último tópico aqui, mas com prazer vou encaminhá-lo para Erland Sommarskog (http://www.sommarskog.
se/dynamic_sql.html) para mais detalhes.

Para evitar essa vulnerabilidade ao emitir SQL dinâmico e garantir que seus planos sejam reutilizados em
vez de regenerados a cada vez, precisamos parametrizar o texto SQL, para que o otimizador veja exatamente o
mesmo texto SQL sempre que você executar a consulta. No entanto, como indica a discussão anterior, não
podemos confiar na parametrização simples do otimizador para nada além das consultas mais triviais e, às vezes,
nem mesmo para essas.

273
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

Como codificadores T-SQL, precisamos promover a reutilização de planos, usando parâmetros em nossas
consultas. A partir do código do aplicativo, podemos fazer isso criando uma instrução preparada, usando o ODBC ADO.
NET e APIs OLEDB. Isso parametriza a consulta e, em seguida, passamos os valores dos parâmetros, para
cada execução do texto SQL parametrizado.

No SQL Server, a melhor abordagem, especialmente para consultas mais complexas para as quais precisamos
passar parâmetros de entrada (e saída) e que desejamos reutilizar, usamos módulos de código, como
procedimentos armazenados ou funções. No entanto, também podemos criar instruções preparadas usando
sp_executesql ou mesmo sp_prepare.

Declarações preparadas
A Listagem 9-10 mostra como criar uma instrução parametrizada no SQL Server usando sp_
executesql (veja a Listagem 9-4 para outro exemplo).

DECLARE @sql NVARCHAR(400);


DECLARE @param NVARCHAR(400);
SELECIONE @sql =
N'SELECT p.Nome,
p.ProdutoNúmero,
th.ReferenceOrderID
Produção.Produto AS p
A PARTIR DE

JUNTE Production.TransactionHistory AS th
ON th.ProductID = p.ProductID
WHERE th.ReferenceOrderID = @ReferenceOrderID;';
SELECT @param = N'@ReferenceOrderID int';
EXEC sys.sp_executesql @sql, @param, 53465;

Listagem 9-10

Quando o SQL Server compila o lote que contém a instrução preparada, ele definirá os valores de quaisquer
variáveis e, em seguida, executará o comando EXECUTE e, neste ponto, poderá detectar os valores dos parâmetros.
Isso significa que ele pode usar estatísticas para obter uma estimativa de contagem de linhas muito precisa para o
predicado (72 linhas). A Figura 9-11 mostra o plano resultante.

274
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

Figura 9-11: Plano de execução mostrando os parâmetros sniffados.

De maneira semelhante, a Listagem 9-11 mostra como definir parâmetros por meio de instruções
preparadas em seu aplicativo (este exemplo usa C#), fazendo uso da API de OLEDB ou ODBC.

usando System.Collections.Generic; usando


System.Text; usando System.Data; usando
System.Data.SqlClient; namespace ExecuteSQL
{

programa de classe
{
static void Main(string[] args) {

string connectionString = "Fonte de dados=MySQLInstance;Da


tabase=AdventureWorks2014;Segurança Integrada=true"; tente {

using (SqlConnection myConnection = new SqlConnecti


on(connectionString)) { myConnection.Open(); SqlCommand prepStatement = myConnection.

CriarComando();
prepStatement.CommandText = @"SELECT p.Name,
p.ProdutoNúmero,

275
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

th.ReferenceOrderID
DA Produção.Produto AS p
JOIN Production.TransactionHistory
AS th
ON th.ProductID = p.ProductID
ONDE th.ReferenceOrderID = @
ReferenceOrderID";
prepStatement.Parameters.Add("@ReferenceOrderID,"
SqlDbType.Int);
prepStatement.Prepare();
prepStatement.Parameters["@ReferenceOrderID"].Value
= 53465;
prepStatement.ExecuteReader();
}
}
catch (SqlException e)
{
Console.WriteLine(e.Message);
Console.Read();
}
}
}
}

Listagem 9-11

Se você executar isso e examinar o cache do plano (Listagem 9-2 ou 9-3), você encontrará o plano
mostrado na Figura 9-11. Se você observar o operador SELECT como fizemos ao longo deste capítulo,
você verá que @ReferenceOrderID foi parametrizado e que o valor foi farejado, com um valor de compilação
de 53465 e que o Tipo de Parametrização de Declaração tem um valor de 1, que significa que o usuário
parametrizou explicitamente a consulta, conforme mostrado na Figura 9-12.

Diferentes tipos de instruções preparadas se comportam de maneira diferente. Um aplicativo .NET


pode construir uma consulta em um objeto StringBuilder, depois prepará-la e executá-la;
tecnicamente, essa é uma instrução preparada, mas teria todas as características do SQL dinâmico.

276
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

Figura 9-12: Propriedades SELECT mostrando a parametrização da instrução preparada.

Da mesma forma, também podemos criar uma instrução preparada em SQL usando o procedimento armazenado
sp_prepare integrado, embora não haja muita necessidade prática para isso e, novamente, ele se comporta de
maneira um pouco diferente.

DECLARE @sql NVARCHAR(400); DECLARE


@param NVARCHAR(400); DECLARE
@PreparedStatement INT; DECLARE @MyID INT;
SELECT @sql = N'SELECT p.Nome,

p.ProductNumber,
th.ReferenceOrderID
A PARTIR DE
Produção.Produto AS p
JUNTE Produção.TransactionHistory AS th ON th.ProductID =
p.ProductID WHERE th.ReferenceOrderID = @ReferenceOrderID;';
SELECT @param = N'@ReferenceOrderID int'; SELECT @MyID = 53465; EXEC sp_prepare
@PreparedStatement SAÍDA, @param, @sql; EXEC sp_execute @PreparedStatement, @MyID;
EXEC sp_unprepare @PreparedStatement;

Listagem 9-12

277
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

Usando esta técnica, a compilação ocorre em duas etapas: primeiro, preparar (sem valores) e depois executar
(com valores). O plano é gerado durante a etapa de preparação e, como não há valores, os parâmetros não podem
ser rastreados e são tratados como variáveis locais normais. Isso é diferente do que mostramos com o código C#
da Listagem 9-11.

Portanto, declarações preparadas criadas dessa forma sempre causam otimização para valores
desconhecidos, e assim o otimizador usará o gráfico de densidade para chegar a uma estimativa de cardinalidade,
neste caso 3,05 linhas, e gerará um plano adequado, que é bastante diferente de aquele que vimos na Figura 9-10.
Você terá que limpar o cache para ver este plano, caso contrário você verá uma reutilização do plano para a
Listagem 9-9, porque o texto SQL é idêntico em cada caso.

Figura 9-13: Plano de execução da consulta da instrução sp_prepare.

Este é o mesmo plano que veríamos se simplesmente definissemos o valor de @ReferenceOrderID


usando uma variável local (DECLARE @ReferenceOrderID INT). Embora possa parecer um parâmetro, os dois
se comportam de maneira diferente e são tratados de maneiras diferentes pelo otimizador, como vimos no
Capítulo 8.

Nesse caso, a eficiência desse plano diminuirá, quanto mais linhas forem retornadas pelas principais entradas
em cada uma das junções de loops aninhados . No entanto, neste caso, não é um problema de desempenho
significativo e o plano é bom o suficiente para todos os valores que podem ser repassados.

Como vimos, quando parametrizamos o SQL usando sp_executesql, usamos uma instrução preparada com base
em código ou um procedimento armazenado com base nessa consulta, obtemos o plano do otimizador para o valor
do parâmetro sniffed, mas podemos ver um desempenho errático como resultado.

Procedimentos armazenados

Já vimos muitos exemplos neste livro, especialmente no Capítulo 7, de encapsulamento de uma consulta
parametrizada em um procedimento armazenado. Quando você chama um procedimento armazenado, um plano
é criado e colocado em um cache associado ao ID do objeto do procedimento. Isso torna a reutilização do plano
direta e simples, tanto para trabalhar quanto para entender.

278
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

A Listagem 9-13 usa a mesma consulta das duas listagens anteriores, mas desta vez em um
procedimento armazenado.

CREATE OR ALTER PROC dbo.ProductTransactionHistoryByReference (@ ReferenceOrderID INT)

COMO

COMEÇAR

SELECT p.Name,
p.ProductNumber,
th.ReferenceOrderID FROM
Production.Product AS p JOIN
Production.TransactionHistory AS th
ON th.ProductID = p.ProductID ONDE
th.ReferenceOrderID = @ReferenceOrderID;
FIM
VAI

Listagem 9-13

Posso executar o procedimento armazenado com o comando na Listagem 9-14.

EXEC dbo.ProductTransactionHistoryByReference @ReferenceOrderID = 41798;

Listagem 9-14

Uma grande vantagem de investigar planos em cache para procedimentos armazenados é que agora posso recuperar
seu plano diretamente do cache. Nesse caso, será o plano otimizado para contagens de linhas estimadas baixas,
onde a junção mais à esquerda é um loop aninhado (Figura 9-13).

SELECT DB_NAME(deps.database_id) AS DatabaseName, deps.cached_time,


deps.min_elapsed_time, deps.max_elapsed_time,
deps.last_elapsed_time, deps.total_elapsed_time, deqp.query_plan
FROM sys.dm_exec_procedure_stats AS deps CROSS APPLY
sys.dm_exec_query_plan(deps.plan_handle) AS deqp WHERE
deps.object_id = OBJECT_ID('AdventureWorks2014.dbo.ProductTra
nsactionHistoryByReference');

Listagem 9-15

279
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

Essa consulta retornará todos os vários tempos de execução (em microssegundos), que são
armazenados com o plano em cache e são atualizados enquanto o objeto permanecer no cache e não
for recompilado. O cached_time mostra quando o objeto foi adicionado ao cache. A Figura 9-14 mostra os
resultados da execução da Listagem 9-15, após duas execuções da Listagem 9-14.

Figura 9-14: Métricas de execução do procedimento armazenado.

O tempo de compilação está incluído nas métricas *_elapsed_time, portanto, a primeira execução (6900
microssegundos) é substancialmente mais lenta que a segunda (80). Se executarmos o procedimento uma
terceira vez, mas com um valor de parâmetro de 53465, você verá que o last_elapsed_time
é mais longo (cerca de 12 K microssegundos, no meu caso) porque o plano otimizado para retornar 3
linhas agora está retornando 72. Este não é um problema de desempenho significativo, mas seria mais
preocupante se houvesse valores de parâmetro que retornassem significativamente mais linhas .

A listagem 9-15, usando object_id como filtro, é a melhor maneira de investigar planos para procedimentos
armazenados. No entanto, também podemos examinar os planos para instruções individuais em um
procedimento armazenado, usando sys.dm_exec_query_stats.

SELECT dest.texto,
deqp.query_plan,
deqs.execution_count,
deqs.max_worker_time,
deqs.max_logical_reads,
deqs.max_logical_writes
FROM sys.dm_exec_query_stats AS deqs
CROSS APPLY sys.dm_exec_query_plan(deqs.plan_handle) AS deqp
CROSS APPLY sys.dm_exec_sql_text(deqs.sql_handle) AS dest
WHERE dest.text LIKE 'CREATE PROC dbo.ProductTransactionHistoryByRe
referência%';

Listagem 9-16

Usei a instrução LIKE e o filtro 'CREATE…', pois a coluna de texto neste caso mostra a definição do objeto
do procedimento (ou função ou gatilho) que foi chamado.

280
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

O que pode dar errado com a reutilização do plano para consultas parametrizadas?

Depois que o otimizador gera um plano para uma instrução preparada ou procedimento armazenado, todas as
execuções subsequentes usarão esse plano, até que o plano seja, por qualquer motivo, removido do cache. Como
discutimos brevemente acima e com mais detalhes no Capítulo 8, se a distribuição de linhas em um índice for muito desigual,
o otimizador escolherá planos muito diferentes, dependendo do valor do parâmetro fornecido. Nesses casos, a detecção de
parâmetros às vezes pode causar problemas de desempenho.

Se você puder alterar o texto da consulta, as soluções comuns incluirão o uso de várias dicas de consulta, como
OPTION (RECOMPILE) se desejar que o otimizador produza um novo plano em cada execução da instrução à qual é
aplicado. Para procedimentos armazenados e outros módulos de código, todas as instruções ainda estarão no cache
do plano, mas o plano para a instrução OPTION(RECOMPILE) ainda será recompilado para cada execução, o que
significa que seu plano não é reutilizado. Para consultas parametrizadas ad hoc (incluindo instruções preparadas), o uso
dessa dica significa que o plano não é armazenado. Em ambos os casos, isso significa que você perde na redução de
recompilações, mas pelo menos ainda economiza espaço no cache do plano.

A alternativa se você não quiser recompilar é usar o Repositório de Consultas para forçar um plano. Outra opção é usar
várias formas da dica OPTION (OPTIMIZE FOR…), se você quiser que o otimizador sempre use um plano para um valor
de parâmetro específico, ou sempre use um plano "genérico", baseado em estatísticas médias.

Veremos algumas dessas dicas brevemente mais tarde, quando discutirmos os guias de plano e a imposição de planos.
As dicas serão abordadas em detalhes no Capítulo 10 e o Repositório de Consultas no Capítulo 16.

Corrigindo problemas com a reutilização do plano se você não puder


Reescreva a consulta

Existem dois tipos distintos de problemas que podemos precisar corrigir e que são especialmente difíceis de corrigir com
código de fornecedor de terceiros que você não pode alterar. Uma é a pressão sobre os recursos de memória e CPU,
causada pelo otimizador compilando um volume muito alto de planos de consulta ad hoc que não pode ser reutilizado, devido
a uma carga de trabalho que consiste em consultas ad hoc não parametrizadas.

O segundo é o desempenho errático de consultas parametrizadas ao reutilizar planos em cache, causados por casos
de sniffing de parâmetros "ruins".

281
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

Otimizar para cargas de trabalho ad hoc

Vamos imaginar que um aplicativo de terceiros, no qual você não tem controle sobre o texto SQL enviado,
está gerando um grande número de consultas ad hoc, muitas das quais são executadas apenas uma vez.
Outra possibilidade é que uma ferramenta ORM, que deveria estar usando consultas parametrizadas, esteja
mal configurada e gere consultas ad hoc.
Qualquer uma dessas situações resulta em aumento do cache do plano e é um fator que contribui para a
pressão de memória no servidor.

Provavelmente, a primeira opção que você deve considerar nesse tipo de situação é habilitar a configuração
de todo o servidor para otimizar cargas de trabalho ad hoc. Eu enfatizo todo o servidor porque essa configuração
afetará todos os bancos de dados no servidor e você precisará testar seu impacto cuidadosamente antes de
optar por habilitá-lo em produção. A partir do SQL Server 2016, porém, você pode usar as definições de
configuração do escopo do banco de dados para habilitar ou desabilitar essa configuração no nível do banco
de dados.

Com essa configuração habilitada, o otimizador de consulta ainda otimiza cada consulta da maneira usual,
mas com uma diferença crítica. Em vez de armazenar imediatamente um plano em cache, ele armazena um
stub de plano ou espaço reservado. Se a mesma consulta for executada uma segunda vez, o plano deverá ser
compilado novamente e agora será adicionado ao cache para reutilização futura. Isso reduz significativamente
a quantidade de memória que o cache de planos usa para gerenciar planos de execução que são executados
apenas uma vez, ao custo de uma compilação adicional para consultas chamadas mais de uma vez.

A Listagem 9-17 inicializa a configuração de otimizar para cargas de trabalho ad hoc e, em seguida, limpa
todo o cache do plano. Estou usando o comando DBCC apenas para fins de demonstração. É melhor usar a
remoção de cache de plano direcionado passando um identificador de plano ou remover apenas planos para um
único banco de dados usando ALTER DATABASE SCOPED CONFIGURATION CLEAR
PROCEDURE_CACHE.

EXECUTAR sp_configure 'mostrar opções avançadas', '1';


RECONFIGURAR;
VAI
EXECUTE sp_configure 'otimizar para cargas de trabalho ad hoc', 1;
RECONFIGURAR;
DBCC FREEPROCCACHE;
VAI

Listagem 9-17

282
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

A Listagem 9-18 mostra como inicializar a configuração no nível do banco de dados no Banco de Dados SQL do Azure, usando
as alterações de configuração no escopo do banco de dados.

ALTERAR CONJUNTO DE CONFIGURAÇÃO DO ESCOPO DO BANCO DE DADOS OPTIMIZE_FOR_AD_HOC_


CARGAS DE TRABALHO = ATIVADO;

ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;

Listagem 9-18

Para ver a otimização para ad hoc em ação, vamos executar uma consulta. Este usa vários literais em uma pesquisa para
encontrar endereços de e-mail que começam com "david" pertencentes a pessoas do estado de Washington.

SELECT 42 AS TheAnswer,
em.EmailAddress,
a.City FROM
Person.BusinessEntityAddress AS bea JOIN
Person.Address AS a
ON bea.AddressID = a.AddressID
JOIN Person.StateProvince AS sp
ON a.StateProvinceID = sp.StateProvinceID JOIN
Person.EmailAddress AS em ON bea.BusinessEntityID =
em.BusinessEntityID WHERE em.EmailAddress LIKE 'david%'

AND sp.StateProvinceCode = 'WA';

Listagem 9-19

A Figura 9-15 mostra o plano de execução real. Se você inspecionar as propriedades do operador SELECT , verá que o
texto da Declaração é idêntico ao texto que enviamos e não há Lista de Parâmetros. Ou seja, não ocorreu
parametrização.

283
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

Figura 9-15: Plano de execução para a consulta na Listagem 9-19.

Agora vamos ver o que está no cache do plano, consultando sys.dm_exec_cached_plans. Eu usei a consulta
na Listagem 9-2, adaptada um pouco para que ela também retorne o cp.size_in_
coluna de bytes.

Figura 9-16: Saída de sys.dm_exec_cached_plans mostrando nenhum plano de execução.

Depois de habilitar a otimização para cargas de trabalho ad hoc e executar esse ad hoc pela primeira vez, o
otimizador compila o plano, mas não o armazena no cache do plano. Há apenas um pequeno "stub" de plano (424
bytes) com um plan_handle associado.

Se você executar a Listagem 9-19 mais uma vez e consultar novamente sys.dm_exec_cached_
planos, os resultados serão diferentes. O otimizador compilou o plano novamente e, desta vez, o armazenou.

Figura 9-17: Saída de sys.dm_exec_cached_plans com um plano de execução.

Observe que o usecount não aumentou em um, porque este é efetivamente um novo plano de consulta em
cache. As execuções subsequentes da mesma consulta resultarão na contagem de execução normal, sem outras
compilações. Se executarmos a mesma consulta, mas desta vez procurando e-mails começando com "paul",
veremos uma nova entrada "stub" para essa consulta e, em seguida, um plano normal na próxima vez que o mesmo
texto for enviado.

Antes de prosseguirmos, vamos desativar a configuração para evitar confusão.

284
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

EXECUTAR sp_configure 'mostrar opções avançadas', 1;


RECONFIGURAR;
VAI
EXECUTE sp_configure 'otimizar para cargas de trabalho ad hoc', 0;
RECONFIGURAR;
VAI
EXECUTAR sp_configure 'mostrar opções avançadas', 0;
RECONFIGURAR;
VAI

Listagem 9-20

Parametrização forçada
A 'otimização para cargas de trabalho ad hoc' reduz a memória necessária no cache do plano para planos
que serão usados apenas uma vez, mas não ajuda a promover a reutilização do plano. Se o seu sistema
OLTP estiver sujeito a uma carga de trabalho pesada, incluindo consultas ad hoc, e o grande número de
compilações do plano estiver contribuindo muito para a pressão existente da CPU, talvez você precise de uma
abordagem diferente. Se você não puder reescrever as consultas para parametrizá-las, considere usar a
parametrização forçada, embora possa haver desvantagens substanciais, como discutiremos mais adiante nesta
seção.

Vimos anteriormente que o otimizador aplica parametrizações simples com muita cautela, ocasionalmente
substituindo literais por parâmetros, em planos triviais, baseados em um conjunto complexo de regras.

Se habilitarmos a parametrização forçada, o otimizador tentará substituir todos os valores literais por
um parâmetro, com as seguintes exceções importantes (entre outras, consulte https://bit.ly/2JhrIb2):

• literais na lista de seleção de qualquer instrução SELECT não são substituídos


• a parametrização não ocorre em instruções T-SQL individuais dentro de procedimentos armazenados,
gatilhos e UDFs, que obtêm planos de execução próprios
• Os argumentos padrão e escape_character de uma cláusula LIKE
• Os literais XQuery não são substituídos por parâmetros.

Normalmente, a parametrização forçada é definida no nível do banco de dados, definindo a opção


PARAMETER IZATION como FORCED, e será aplicada a todas as consultas nessa base. Você também tem
a opção de escolher configurá-lo apenas para uma única consulta usando a dica de consulta,
PARAMETERIZAÇÃO FORÇADA, mas essa dica está disponível apenas como um guia de plano, que
abordaremos mais adiante neste capítulo.

285
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

A Listagem 9-21 mostra uma consulta ad hoc simples como a que encontramos anteriormente neste capítulo, e que
não recebe parametrização simples.

' ' + Pessoa.Nome + ' ' +


SELECT ISNULL(Pessoa.Título, '') +
Pessoa.Sobrenome
DE Pessoa.Pessoa
WHERE Pessoa.BusinessEntityID = 278;

Listagem 9-21

A Figura 9-18 mostra os resultados da execução da Listagem 9-3, para ver o que está no cache do plano.

Figura 9-18: Consulta sem parametrização do cache.

Vamos agora habilitar a parametrização forçada e limpar o cache do buffer, o que acontece automaticamente
quando você altera a opção de parametrização.

ALTER DATABASE AdventureWorks2014 SET PARAMETERIZAÇÃO FORÇADA;


VAI

Listagem 9-22

Agora execute a Listagem 9-21 novamente. Se você capturar o plano real e examinar as propriedades do
operador SELECT , verá que, desta vez, elas foram parametrizadas. Vemos uma lista de parâmetros e
um StatementParameterizationType de 3, indicando parametrização forçada.

Figura 9-19: Propriedades SELECT mostrando que ocorreu a parametrização forçada.

286
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

Assim como na parametrização simples, com a parametrização forçada ainda não temos controle sobre
os nomes dos parâmetros, que são baseados apenas na ordem em que os parâmetros são criados, que por
sua vez é orientado pela ordem em que os valores literais aparecem na consulta. Fundamentalmente, também
não podemos controlar os tipos de dados escolhidos para parametrização.

A Figura 9-20 mostra o cache do plano após executar a Listagem 9-21 mais uma vez, mas com um valor
literal diferente, provando que o plano foi reutilizado.

Figura 9-20: Uma consulta parametrizada agora está no cache.

Isto é uma coisa boa? Para esta consulta, sim. O plano usa um Seek do índice clusterizado e sempre produzirá
o mesmo plano, independentemente do valor do parâmetro. No entanto, o problema de impor a parametrização
é que ela é um instrumento muito contundente. Isso forçará o otimizador a parametrizar todas as consultas em
execução no banco de dados, para melhor ou para pior. Se algumas consultas forem parametrizadas que, de
outra forma, teriam muitos planos diferentes, de acordo com o valor exato fornecido, embora você possa
reduzir as compilações, possivelmente está se encaminhando para problemas de sniffing de parâmetros ruins.

A parametrização forçada também tem limitações. E se o seu sistema OLTP estiver sujeito a muitas
pesquisas com curingas, com literais embutidos em código? Execute novamente a Listagem 9-19, que
contém exatamente essa pesquisa curinga para endereços de e-mail. Você verá que o plano de execução é
o mesmo mostrado na Figura 9-10. No entanto, o texto da consulta armazenado com o plano não é mais o
mesmo. Agora parece como abaixo (formatado para legibilidade).

SELECIONE 42 COMO A Resposta,


em.EmailAddress,
Uma cidade
FROM Person.BusinessEntityAddress AS bea
JOIN Pessoa.Endereço COMO a
ON bea.AddressID = a.AddressID
JOIN Person.StateProvince AS sp
ON a.StateProvinceID = sp.StateProvinceID
JOIN Person.EmailAddress AS em
ON bea.BusinessEntityID = em.BusinessEntityID
WHERE em.EmailAddress LIKE 'david%'
AND sp.StateProvinceCode = @0

287
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

Em vez da cadeia de dois caracteres fornecida na definição de consulta original, o parâmetro @0 é usado na comparação
com o campo StateProvinceCode. Se essa consulta for chamada novamente com um código de estado diferente de dois
ou três caracteres, o plano será reutilizado. Isso pode afetar o desempenho, positiva ou negativamente. Além disso,
como LIKE está na lista de exceções para parametrização forçada, esse plano só será reutilizado para uma pesquisa de
endereços de e-mail que comecem com 'david', em qualquer estado.

Como uma pequena nota lateral, a consulta armazenada com o plano não incluiu o terminador de instrução de ponto e
vírgula que eu tinha em minha consulta original.

Antes de prosseguir, certifique-se de redefinir a parametrização do banco de dados.

ALTER DATABASE AdventureWorks2014 SET PARAMETERIZAÇÃO SIMPLES;


VAI

Listagem 9-23

Guias de plano
A configuração de otimização para cargas de trabalho ad hoc e parametrização forçada, no nível do banco de
dados, podem ser opções úteis para corrigir problemas relacionados a cargas de trabalho de consulta ad hoc,
especialmente quando você não tem a opção de corrigir o código. No entanto, ambos são de amplo alcance em seu
impacto.

Os guias de plano oferecem uma maneira de controlar certos aspectos do comportamento do otimizador e, portanto,
"guiam" para o plano desejado, nos casos em que você não pode modificar o código ou esquema do banco de dados.
Eles nos permitem aplicar dicas de consulta válidas ao código, sem editar o código T-SQL de forma alguma. Eles estão
disponíveis em todas as edições do SQL Server, exceto a Express Edition.

Podemos criar guias de plano para procedimentos armazenados e outros objetos de banco de dados (guias de
plano de objeto) ou para instruções SQL que não fazem parte de um objeto de banco de dados (guias de plano SQL
e guias de plano modelo). Sua vantagem sobre a configuração de otimização para cargas de trabalho ad hoc
e parametrização forçada é que eles afetam apenas os objetos ou consultas específicos aos quais os aplicamos.
Oferecerei exemplos típicos de como você pode usar cada um desses tipos de guia de plano para resolver problemas
relacionados à reutilização de planos (é claro, eles também têm aplicações mais amplas).

Antes de começarmos, minhas palavras habituais de cautela: tenha o devido cuidado ao implementar guias de plano,
porque alterar a forma como o otimizador lida com uma consulta pode prejudicar seu desempenho, se usado
incorretamente. Como enfatizo fortemente no Capítulo 10, dicas e, portanto, guias de plano, podem ser

288
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

perigoso. Eles não são sugestões que o otimizador pode considerar, são comandos que o otimizador deve obedecer.
Além disso, qualquer vantagem de desempenho que um guia de plano ofereça hoje pode em breve começar a funcionar
contra você, pois o banco de dados e seus dados mudam com o tempo.

Assim como as dicas, os guias de plano devem ser o último recurso, não uma tática padrão. À medida que o código, as
estruturas ou os dados mudam, o plano forçado pode ficar abaixo do ideal, prejudicando o desempenho. O teste adequado e
a devida diligência devem ser observados antes de aplicar a imposição com guias de plano ou com o Query Store. Então,
com o tempo, você deve reavaliar os planos que estão sendo forçados dessa maneira. Por fim, os guias de plano são uma
ferramenta para lidar com alguns tipos de problemas relacionados à reutilização de planos, mas a imposição de planos por
meio do Repositório de consultas, abordada posteriormente neste capítulo, agora é um mecanismo preferencial em relação
aos guias de planos.

Você pode monitorar o sucesso ou falha de qualquer um dos guias de plano usando os Eventos Estendidos
plan_guide_successful e plan_guide_unsuccessful.

Guias de plano de modelo


Digamos que existam apenas algumas consultas ad hoc problemáticas que você gostaria que o otimizador
parametrizasse, sem afetar os comportamentos de otimização de outras consultas no banco de dados. Em outras
palavras, você gostaria de uma solução como parametrização forçada, mas localizada apenas para essas consultas de
problemas. É aqui que os guias de plano de modelo podem ser úteis.

Vamos supor que decidimos que nossa consulta da Listagem 9-17 deve ter sua PARAMETER IZATION definida como
FORCED, mas a consulta vem do código do fornecedor que não podemos editar. Podemos simplesmente criar um guia
de plano de modelo para implementar a parametrização forçada, apenas para essa consulta, em vez de alterar as
configurações em todo o banco de dados. Um guia de plano de modelo substituirá as configurações de parametrização nas
consultas.

A primeira etapa é usar o procedimento armazenado sp_get_query_template para recuperar o modelo. Usamos o texto
da consulta como entrada e as saídas, que "imitam a forma parametrizada de uma consulta que resulta do uso de
parametrização forçada", armazenamos em variáveis e passamos para o procedimento sp_create_plan_guide, para criar
o guia de plano de modelo.

O parâmetro de saída @templatetext conterá a forma parametrizada do texto da consulta, como uma string, e o parâmetro
de saída @parameters conterá uma lista separada por vírgulas de nomes de parâmetros e tipos de dados.

289
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

DECLARE @templateout NVARCHAR(MAX),


@paramsout NVARCHAR(MAX); EXEC
sys.sp_get_query_template @querytext = N'SELECT 42 AS
A resposta

A PARTIR DE

,em.EmailAddress ,e.BirthDate ,a.City


Person.Person AS p JOIN HumanResources.Employee e ON p.BusinessEntityID = e.Busines
JOIN Person.BusinessEntityAddress AS bea
ON p.BusinessEntityID = bea.BusinessEntityID JOIN
Pessoa.Endereço a
ON bea.AddressID = a.AddressID
JOIN Person.StateProvince AS sp
ON a.StateProvinceID = sp.StateProvinceID
JOIN Person.EmailAddress AS em ON
e.BusinessEntityID = em.BusinessEntityID WHERE
em.EmailAddress LIKE ''david%''
AND sp.StateProvinceCode = ''WA'';',
@templatetext = @templateout OUTPUT,
@parameters = @paramsout OUTPUT;
EXEC sys.sp_create_plan_guide
@name = N'MyTemplatePlanGuide', @stmt
= @templateout, @type = N'TEMPLATE',
@module_or_batch = NULL, @params =
@paramsout, @hints =
N'OPTION(PARAMETERIZATION FORCED)';

Listagem 9-24

Os parâmetros de entrada para sp_create_plan_guide são os seguintes: • @name


– o nome do guia de plano funcionará no contexto do banco de dados, não no servidor, o que
também significa que o guia só funciona nesse banco de dados. • @stmt – deve ser uma
correspondência exata para a consulta que o otimizador de consulta será
chamado para corresponder, embora o espaço em branco e os retornos de carro não
importem. Quando o otimizador encontrar um código que corresponda, ele procurará e
aplicará o guia de plano correto. Nesse caso, fornecemos a variável que armazena a saída @templatetext.
• @type – o tipo de guia de plano, neste caso um guia de plano modelo.

290
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

• @module_or_batch – especificaríamos o nome do objeto de destino se estivéssemos criando


um guia de plano de objeto. Caso contrário, forneceríamos NULL.
• @params – aplicável apenas a guias de plano de modelo e é uma lista separada por vírgulas de
nomes de parâmetros e tipos de dados.
• @hints – especifica quaisquer dicas que precisam ser aplicadas, neste caso
OPÇÃO (PARAMETERIZAÇÃO FORÇADA).

Execute a Listagem 9-24 e, em seguida, execute novamente a Listagem 9-19 e você verá que essa consulta
agora está sujeita à parametrização forçada, conforme indicado nas propriedades do operador SELECT . Ao
contrário de outros tipos de guias de plano, o próprio guia de plano de modelo não é identificado no plano de
execução. Você pode usar o Evento Estendido plan_guide_successful para assegurar que o guia de plano foi
aplicado.

Guias de plano SQL


Em vez de ter problemas com consultas não parametrizadas, talvez seu sistema execute muitas consultas
parametrizadas e você esteja tendo problemas de desempenho com algumas delas, devido à detecção
incorreta de parâmetros.

Na seção anterior sobre instruções Preparadas, encontramos uma consulta parametrizada em que a escolha
do plano do otimizador dependia do valor de entrada. Quando a estimativa de cardinalidade era de apenas
algumas linhas, vimos um plano simples que consiste em junções de loops aninhados (Figura 9-13).
Para linhas estimadas mais altas retornadas, vimos um plano de aparência mais complexa com um Merge Join
(Figura 9-11).

Decidimos que o plano mais simples é o melhor plano para a maioria dos valores de entrada possíveis e,
portanto, queremos aplicar a dica OPTIMIZE FOR para obter esse plano. No entanto, novamente, não
podemos adicionar uma dica porque não temos controle sobre o SQL executado. Este é um exemplo de onde
um guia de plano SQL pode ser útil.

Uma opção seria forçar o otimizador a produzir um plano para um valor específico, um que sabemos que
resulta no plano mais simples, por exemplo OPTIMIZE FOR (@ReferenceOrderID
= 41798). No entanto, e se os dados forem alterados e, de repente, esse valor de entrada retornar muitas
linhas? O plano mudará, e isso pode afetar o desempenho de outras execuções da declaração preparada.

Em vez disso, criaremos um guia de plano SQL que usa a dica OPTIMIZE FOR com um valor de
UNKNOWN para forçar um plano mais genérico no otimizador, com base em estatísticas médias, que resulta
no plano simples que queremos e é menos suscetível à instabilidade hora extra.

291
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

EXEC sys.sp_create_plan_guide @name =


N'MySQLPlanGuide', @stmt = N'SELECT
p.Name, p.ProductNumber, th.ReferenceOrderID
Production.Product AS p
Production.TransactionHistory
A PARTIR DE
AS th ON th.ProductID = p.ProductID
JUNTE WHERE th. ReferenceOrderID = @ReferenceOrderID;', @type
= N'SQL', @module_or_batch = NULL, @params =
N'@ReferenceOrderID int', @hints = N'OPTION (OPTIMIZE FOR UNKNOWN)';

Listagem 9-25

Agora, se executarmos novamente a instrução preparada na Listagem 9-10, o otimizador não


fará mais sniffing de parâmetros e chegará ao plano com o Merge Join, mas criará o plano com base
nas estatísticas médias, mostradas na Figura 9-21.

Figura 9-21: Plano de execução resultante da dica no guia do plano.

As propriedades do operador SELECT mostram que o guia de plano foi aplicado.

292
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

Figura 9-22: Propriedades SELECT mostrando o guia de plano em uso.

Isso significa que você tem um método para ver se um guia de plano foi aplicado com precisão a um
procedimento armazenado e para identificar planos em que um guia de plano afetou o otimizador ao solucionar
problemas de um banco de dados herdado.

Guias de plano de objetos

Talvez seu sistema execute muitas consultas parametrizadas, na forma de procedimento armazenado, e
novamente você está tendo problemas de desempenho com algumas delas, devido à detecção incorreta de parâmetros.
Você identificou um procedimento armazenado, dbo.uspGetManagerEmployees (que é um procedimento
armazenado interno no AdventureWorks), no qual está disposto a fazer o SQL Server compilar um plano para
cada execução, aplicando a dica RECOMPILE.
No entanto, este não é um procedimento que você pode editar. Então você decide criar um guia de plano de
objeto para aplicar a dica RECOMPILE. Só podemos usar guias de plano de objeto para consultas executadas no
contexto de procedimentos armazenados T-SQL, funções escalares definidas pelo usuário, funções definidas pelo
usuário com valor de tabela de várias instruções e gatilhos DML.

293
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

EXEC sys.sp_create_plan_guide
@name = N'MyObjectPlanGuide', @stmt =
N'WITH [EMP_cte]([BusinessEntityID],
[OrganizationNode],
[Primeiro nome, ultimo nome],
[Nível de Recursão])
-- Nome e colunas do CTE
AS
( SELECT e.[BusinessEntityID], e.[OrganizationNode], p.[FirstName],
p.[LastName], 0 -- Obter lista inicial de funcionários para gerente
n
DE [Recursos Humanos].[Funcionário] e
INNER JOIN [Pessoa].[Pessoa] p ON p.
[BusinessEntityID] = e.[BusinessEntityID]
WHERE e.[BusinessEntityID] = @BusinessEntityID UNION ALL SELECT
e.[BusinessEntityID], e.[OrganizationNode], p.[FirstName], p.[LastName],
[RecursionLevel] + 1

-- Junta membro recursivo para âncora FROM


[HumanResources].[Employee] e INNER JOIN [EMP_cte]

ON e.[OrganizationNode].GetAncestor(1) = [EMP_cte].
[OrganizationNode]
INNER JOIN [Pessoa].[Pessoa] p ON p.
[BusinessEntityID] = e.[BusinessEntityID]
)
SELECT [EMP_cte].[RecursionLevel], [EMP_cte].
[OrganizationNode].ToString() como
[OrganizationNode],
p.[FirstName] AS ''ManagerFirstName'', p.[LastName] AS
''ManagerLastName'', [EMP_cte].[BusinessEntityID],
[EMP_cte].[FirstName], [EMP_cte].[LastName] -- Externo selecione no CTE DE
[EMP_cte]

INNER JOIN [HumanResources].[Employee] e ON [EMP_cte].


[OrganizationNode].GetAncestor(1) = e.[OrganizationNode]

INNER JOIN [Pessoa].[Pessoa] p ON p.


[BusinessEntityID] = e.[BusinessEntityID]
ORDER BY [RecursionLevel], [EMP_cte].[OrganizationNode].ToString()
OPÇÃO (MAXRECURSION 25) ', @type =
N'OBJECT', @module_or_batch =
N'dbo.uspGetManagerEmployees',

294
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

@params = NULL,
@hints = N'OPTION(RECOMPILE,MAXRECURSION 25)';

Listagem 9-26

Novamente, o parâmetro @stmt deve conter texto SQL que corresponda exatamente ao que o otimizador de
consulta vê (exceto espaços em branco e retornos de carro). Lembre-se de que um procedimento pode ter mais
de uma instrução e você deseja aplicar a dica à correta dentro do procedimento.

Desta vez, o parâmetro @type é um objeto de banco de dados e no @module_or_batch


parâmetro especificamos o nome do objeto de destino.

Para o parâmetro @hints, aplicamos a dica RECOMPILE, mas observe que essa consulta já tinha uma
dica, MAX RECURSION. Essa dica também tinha que fazer parte do meu @stmt para corresponder ao que
estava dentro do procedimento armazenado. O guia de plano substitui o OPTION existente, portanto, se
precisarmos que ele seja levado adiante, devemos adicioná-lo ao guia de plano.

Deste ponto em diante, sem fazer uma única alteração na definição real do procedimento armazenado, quando o
executamos, o otimizador recompilará o plano para a consulta especificada todas as vezes e o otimizará para o
valor específico fornecido. Observe que você não pode alterar um procedimento armazenado que tenha um guia
de plano.

Novamente, você pode identificar que um guia foi usado observando o operador SELECT do plano de execução
resultante.

Visualizando, validando, desabilitando e removendo guias de plano


Para ver uma lista de guias de plano no banco de dados, basta SELECIONAR na exibição de gerenciamento
dinâmico, sys.plan_guides.

SELECIONAR *
A PARTIR DE
sys.plan_guides;

Listagem 9-27

Depois de aplicar atualizações cumulativas, atualizar sua instância do SQL Server ou até mesmo implantar
alterações em seu banco de dados, é uma boa ideia garantir que seus guias de plano, se houver, estejam intactos.
Você pode validar os guias de plano usando fn_validate_plan_guide.

295
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

SELECIONE pg.plan_guide_id,
pg.nome,
fvpg.message,
fvpg.severidade,
fvpg.state
DE sys.plan_guides AS pág
OUTER APPLY sys.fn_validate_plan_guide(pg.plan_guide_id) AS
fvpg;

Listagem 9-28

O valor que está sendo transmitido é o plan_guide_id, recuperado da exibição do sistema sys.plan_guides.
Se o guia do plano for válido, nada será devolvido. Se o guia do plano for inválido, você receberá o
primeiro erro encontrado pelo processo de validação. Esta consulta, então, listará todos os guias de plano
e mostrará qualquer um que tenha erros.

Além do procedimento para criar guias de plano, um segundo, sp_control_plan_


guia, permite descartar, desabilitar (o que salva a definição, mas impede o SQL Server de usá-la)
ou habilitar um guia de plano específico; ou elimine, desabilite ou habilite todos os guias de plano no
banco de dados.

Basta executar o procedimento sp_control_plan_guide, alterando o parâmetro @operation


adequadamente. A Listagem 9-29 removerá todos os guias de plano criados neste capítulo.

EXEC sys.sp_control_plan_guide @operation = N'DROP ALL', @name =


N'*';

Listagem 9-29

Forçar plano
Pode haver situações em que adicionar dicas usando guias de plano não produza resultados
consistentes. Embora as dicas ditem como o otimizador lida com certos aspectos de uma consulta (como
ditar o uso de um operador de junção), às vezes elas ainda permitem que o otimizador escolha entre
vários planos candidatos, dos quais alguns são bons e outros ruins. Você não pode controlar qual deles
é escolhido.

Nesses casos, em que você não pode tocar no código e deseja "forçar" o otimizador para escolher o
plano desejado, você pode usar o forçamento de plano. Mostrarei como usar um guia de plano para
forçar o uso de seu plano para uma consulta, aplicando a dica de consulta USE PLAN. Eu vou

296
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

em seguida, mostre uma abordagem alternativa para planejar o forçamento usando o Query Store (um tópico
que abordaremos em detalhes no Capítulo 16). Como você verá, é muito mais fácil usar o forçamento de plano
no Query Store do que implementar um guia de plano.

Tal como acontece com dicas e guias de plano, e por todas as razões discutidas anteriormente, a imposição
de plano deve ser uma tentativa final de resolver um problema que de outra forma seria insolúvel. À medida
que os dados e as estatísticas mudam, ou novos índices são adicionados, os guias de plano podem ficar
desatualizados e exatamente o que economizou tanto tempo de processamento ontem, custará cada vez mais
amanhã.

Usando guias de plano para forçar o plano


A dica de consulta USE PLAN, introduzida no SQL Server 2005, permite que você chegue o mais próximo
possível de obter controle total sobre um plano de execução de consulta. Essa dica permite que você pegue um
plano de execução, capturado como XML e, em seguida, use esse plano em uma determinada consulta desse
ponto em diante. Isso não impede o otimizador de fazer seu trabalho. Você ainda obterá otimização completa
dependendo da consulta, mas a otimização é usada apenas para verificar se o plano forçado será válido para a
consulta.

Com guias de plano, você não pode forçar um plano em:

• consultas INSERT, UPDATE, DELETE ou MERGE


• Consultas que usam cursores diferentes de static, fast_forward, forward_only
ou insensível.

Embora você possa simplesmente anexar um plano XML diretamente à consulta em questão, os planos de
execução XML são muito grandes. Se o seu plano anexado exceder 8 K em tamanho, o SQL Server não poderá mais
armazenar em cache a consulta, porque excede o limite de cache literal de string de 8 K. Por esta razão, você deve
empregar USE PLAN, dentro de um guia de plano, para que a consulta em questão seja armazenada em cache de
forma adequada, melhorando o desempenho. Isso também significa que você evita consultas de mil linhas,
melhorando a legibilidade e a capacidade de manutenção do código, e evita ter que implantar e reimplantar a consulta
em seu sistema de produção, se quiser adicionar ou remover um plano.

A Listagem 9-30 mostra um procedimento armazenado simples CreditInfoBySalesPerson, para relatar


algumas informações da tabela SalesOrderHeader.

CRIAR PROCEDIMENTO Sales.CreditInfoBySalesPerson (@SalesPersonID INT)


COMO

SELECT soh.AccountNumber,
soh.CreditCardApprovalCode,
soh.CreditCardID,

297
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

soh.OnlineOrderFlag
FROM Sales.SalesOrderHeader AS soh
WHERE soh.SalesPersonID = @SalesPersonID;

Listagem 9-30

Quando o procedimento é executado usando o valor para @SalesPersonID = 277, uma Verificação de Índice Clusterizado
resulta.

Figura 9-23: Plano de execução com uma varredura para um grande conjunto de dados.

Se removermos o plano do cache e alterarmos o valor para 285, veremos uma Busca de Índice com uma Pesquisa de Chave.

Figura 9-24: Plano de execução com uma pesquisa de busca e chave para um conjunto de dados menor.

Em situações como essa, geralmente você pode optar por recompilar, usando a dica RECOMPILE, mas vamos supor que isso
não seja aceitável, neste caso. A próxima opção válida é adicionar um guia de plano que use a dica OPTIMIZE FOR, conforme
descrito anteriormente. O Clustered Index Scan tem a vantagem de desempenho previsível e consistente, enquanto o plano com
Index Seek e Key Lookup provavelmente terá padrões de desempenho mais erráticos.

No entanto, seu teste sugere que, para a maioria dos valores de SalesPersonID, o Index Seek
com um Key Lookup é muito mais rápido do que o Clustered Index Scan e, em vez de usar um guia de plano e uma
dica OPTIMIZE FOR, você forçará o otimizador a sempre usar seu plano preferido.

298
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

Primeiro, precisamos criar um plano XML que se comporte da maneira que queremos. Fazemos isso retirando
o texto SQL do procedimento armazenado e modificando-o para se comportar da maneira correta. Isso resulta
no plano desejado, que capturamos envolvendo-o em STATISTICS XML, que gerará um plano de execução
real em XML. Você também pode usar um plano gráfico e clicar com o botão direito do mouse para capturar o
XML.

SET STATISTICS XML ON; GO


SELECT soh.AccountNumber,
soh.CreditCardApprovalCode,
soh.CreditCardID, soh.OnlineOrderFlag
FROM Sales.SalesOrderHeader AS
soh WHERE soh.SalesPersonID =
285; GO SET STATISTICS XML OFF; VAI

Listagem 9-31

Essa consulta simples gera um plano XML de 117 linhas, que não mostrarei aqui. Com o plano XML
em mãos, criaremos um guia de plano para aplicá-lo ao procedimento armazenado. Você pode
simplesmente clicar com o botão direito do mouse no link XML Showplan , selecionar Copiar e colá-lo
como o valor para o parâmetro @hints.

EXEC sys.sp_create_plan_guide
@name = N'UsePlanPlanGuide', @stmt
= N'SELECT soh.AccountNumber,
soh.CreditCardApprovalCode,
soh.CreditCardID, soh.OnlineOrderFlag
FROM Sales.SalesOrderHeader AS soh
WHERE soh.SalesPersonID = @SalesPersonID;',
@type = N' OBJECT', @module_or_batch =
N'Sales.CreditInfoBySalesPerson', @params = NULL,
@hints = N'<ShowPlanXML xmlns="http://sche...

Listagem 9-32

299
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

Se fornecermos um plano XML válido para @hints, sp_create_plan_guide interpretará isso automaticamente como uma dica
USE PLAN. Agora, executamos a consulta usando o valor que gera o plano não preferencial.

EXEC Sales.CreditInfoBySalesPerson @SalesPersonID = 277;

Listagem 9-33

No entanto, ainda obtemos o plano de execução que desejamos, conforme mostrado na Figura 9-25.

Figura 9-25: Plano de execução usando o Seek por causa do guia de plano.

Os canais de transferência de dados mais largos entre os operadores na Figura 9-25, comparados com a Figura 9-24, nos
informam que mais dados estão sendo movidos pelo plano, conforme esperado. Você também pode inspecionar as propriedades
do operador SELECT para verificar se o guia de plano foi usado.

Usando o Query Store para forçar o plano


Se você estiver trabalhando no Banco de Dados SQL do Azure ou no SQL Server 2016 e posterior, uma maneira mais
fácil de resolver o mesmo problema é usar a imposição de plano no Repositório de Consultas. Estou supondo que você tenha
o Query Store habilitado. Se não, vá para o Capítulo 16 para saber como.

Execute a consulta na Listagem 9-34.

300
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

SELECT Object_Name(qsq.object_id) AS ObjectName,


Cast(qsp.query_plan AS XML) AS xmlplan, qsq.query_id, qsp.plan_id
DE sys.query_store_query AS qsq
JOIN sys.query_store_plan AS qsp
ON qsp.query_id = qsq.query_id
WHERE qsq.object_id = Object_Id('Vendas.CreditInfoBySalesPerson');

Listagem 9-34

Você deverá ver três planos de execução, todos com o mesmo query_id, mas valores diferentes para
plan_id. Os dois primeiros são os planos para as duas execuções iniciais do procedimento armazenado,
com valores @SalesPersonID de 277 e 285, e o terceiro é tecnicamente um plano diferente porque agora é
um plano forçado.

Figura 9-26: Três planos no Query Store.

Se tivéssemos editado o texto da consulta diretamente, para adicionar a dica, o query_id também teria
sido diferente. No entanto, neste caso, usamos um guia de plano para que o texto da consulta ainda fosse
exatamente o mesmo.

Digamos que desta vez queremos forçar o plano Clustered Index Scan para este procedimento (Figura
9-23), então podemos extrair um plano diretamente do Query Store e colocá-lo no cache do plano. No meu
caso, plan_id 5111 é o que eu quero.

EXEC sys.sp_query_store_force_plan @query_id = 5004, @plan_id =


5111;

Listagem 9-35

Execute CreditInfoBySalesPerson com um valor de parâmetro de 285 e você verá o plano Clustered
Index Scan em vez do plano Index Seek and Key Lookup . E lembre-se, a menos que você o tenha
descartado, o guia UsePlanPlanGuide, forçando o último plano, ainda está em vigor. A imposição do plano do
Repositório de Consultas terá precedência sobre um guia de plano.

301
Machine Translated by Google

Capítulo 9: Explorando a reutilização do plano

Para desforçar o plano, execute a Listagem 9-36.

EXEC sp_query_store_unforce_plan @query_id = 5004, @plan_id = 5111;

Listagem 9-36

Você também pode querer executar a Listagem 9-29 mais uma vez, se ainda tiver o forçamento de plano com um guia de
plano em vigor.

Novamente, o forçamento de plano é um método rápido, embora temporário, para lidar com o sniffing de parâmetros
incorretos. Eu chamo a correção temporária porque, como acontece com qualquer uma das outras correções de detecção
de parâmetros ruins, você vai querer reavaliar ao longo do tempo à medida que os dados, seus sistemas e seu código mudam.

Resumo
A criação de planos de execução é uma operação cara para o SQL Server. Por causa disso, você deseja reutilizar os
planos com a maior frequência possível e de todas as maneiras possíveis. Usar consultas parametrizadas, sejam
procedimentos armazenados ou instruções preparadas, é uma ótima maneira de fazer isso.
Outros métodos de controle de uso e reutilização do plano, como parametrização forçada e otimização para cargas de
trabalho ad hoc, também podem ajudar a reduzir a carga colocada no servidor pelo processo de otimização.

Usando guias de plano e imposição de plano, você pode tirar o controle direto do otimizador e tentar obter um melhor
desempenho para suas consultas. No entanto, ao assumir o controle do otimizador, você pode introduzir problemas tão
grandes quanto aqueles que você está tentando resolver. Seja muito criterioso no uso de alguns dos métodos descritos neste
capítulo. Não se apresse e teste tudo o que você faz em seus sistemas. Você também precisará testar novamente regularmente
seus sistemas onde quer que tenha assumido o controle direto usando guias de plano. Use as informações que você coletou
nos outros capítulos deste livro para ter certeza de que as escolhas que você está fazendo são as corretas.

302
Machine Translated by Google

Capítulo 10: Controlando os planos de


execução com dicas

O otimizador de consultas acerta na maioria das vezes, mas ocasionalmente escolhe um plano que não
é o melhor possível. Conforme discutido no Capítulo 8, o otimizador baseia suas escolhas de plano em
estimativas de seletividade e cardinalidade derivadas de estatísticas. Se uma coluna tiver uma distribuição
particularmente "dentada", mesmo as estatísticas que são tão boas e tão atualizadas quanto o SQL Server
pode torná-las não podem descrevê-la com precisão. Às vezes, nossas consultas usam predicados
complexos que são difíceis de estimar ou que forçam o otimizador a usar uma estimativa de seletividade
codificada. Esses problemas podem fazer com que o otimizador erre na escolha do plano, resultando em
desempenho de consulta abaixo do ideal.

Nesses casos, podemos decidir forçar a mão do otimizador, aplicando dicas que informam como acessar
determinadas tabelas, ou qual estratégia de junção usar, ou como ele deve otimizar todo um conjunto de
operações para uma determinada consulta. Isso, é claro, resultará em um plano diferente daquele que o
otimizador teria escolhido se tivesse a mão livre.

Descreverei as dicas de consulta, junção e tabela que afetam diretamente a escolha do plano de execução.
Não abordarei dicas que afetem a estratégia de execução em vez de compilar a consulta (como dicas de
bloqueio) ou qualquer outra que tenha impacto mínimo na escolha do plano. Também explicarei por que é
uma boa ideia, em geral, ser extremamente cauteloso ao aplicar dicas às suas consultas e indicarei os perigos
específicos associados a certas dicas.

Os perigos de usar dicas


Embora você possa encontrar situações em que uma dica realmente ajude no desempenho, você
deve usá-las com moderação, porque as dicas podem ser perigosas. Até o nome deles é enganoso;
dicas não são sugestões que o otimizador pode considerar, são mandamentos que o otimizador deve
seguir. Mesmo se você fornecer uma dica com a qual é tecnicamente impossível para o otimizador cumprir,
ele ainda tentará aplicar a dica e gerará um erro. Você verá um exemplo disso mais tarde, quando
discutirmos a dica INDEX().

Embora as dicas permitam controlar o comportamento do otimizador, isso não significa que suas escolhas
são necessariamente melhores do que as escolhas do otimizador. Se você estiver colocando dicas na
maioria de suas consultas e procedimentos armazenados, então você está fazendo algo errado. Sim, a dica certa

303
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

na consulta certa pode melhorar o desempenho da consulta. No entanto, exatamente a mesma dica usada em
outra consulta pode criar mais problemas do que resolver, diminuindo radicalmente sua consulta e levando a
bloqueios e tempos limite severos em seu aplicativo. Mesmo uma dica que é "boa" agora pode se tornar muito ruim
com o tempo, porque remove a capacidade subsequente do otimizador de fazer uma escolha de plano melhor, em
resposta a mudanças na distribuição de dados ou em resposta a uma atualização para uma nova versão do SQL
Server ou a aplicação de um novo service pack.

Nas próximas seções, descreverei as várias dicas que podemos usar e os problemas que esperamos resolver
aplicando essa dica. Você verá exemplos em que uma dica melhora o desempenho ou altera o comportamento
de maneira positiva e também alguns em que uma dica prejudica o desempenho. Novamente, este não é um
capítulo sobre dicas em si, mas sim seu efeito nos planos de execução. Para obter mais detalhes sobre dicas,
consulte a documentação da Microsoft (http://bit.ly/2pt7UF2).

Para qualquer dica, aplique-a apenas após testes copiosos e com documentação completa. Você precisa tornar o mais
fácil possível para que outras pessoas encontrem onde as dicas são usadas, para entender a intenção da dica e,
portanto, agendar testes regulares para verificar se seu uso ainda é válido, pois o sistema e seus dados mudam com o
tempo .

Dicas de consulta

As dicas de consulta assumem o controle de uma consulta inteira e podem afetar todos os operadores dentro do
plano de execução. Podemos usar dicas de consulta para forçar o uso de um operador específico para todas as
agregações em uma consulta ou para todas as junções. Podemos usá-los para instruir o otimizador a otimizar uma
consulta para um valor de parâmetro definido ou para compilar um novo plano em cada execução dessa consulta,
para controlar o uso de paralelismo para essa consulta e muito mais. Algumas dicas de consulta são úteis
ocasionalmente, enquanto algumas são para circunstâncias raras. Como acontece com todas as dicas, o uso imprudente
de dicas de consulta pode causar mais problemas do que soluções!

Especificamos dicas de consulta na cláusula OPTION. A Listagem 10-1 mostra a sintaxe básica.

...
SELECIONAR OPÇÃO (<dica>,<dica>...);

Listagem 10-1

304
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Não podemos aplicar dicas de consulta a instruções de manipulação de dados INSERT, exceto como parte de uma
operação SELECT associada, e não podemos usar dicas de consulta em subconsultas, pois a dica deve ser aplicada a
toda a consulta.

HASH | GRUPO DE PEDIDO


As dicas HASH GROUP e ORDER GROUP se aplicam a todas as agregações na consulta causadas por GROUP BY ou
DISTINCT. Geralmente, o otimizador escolherá o mais apropriado dos dois mecanismos de agregação disponíveis, Hash
Match (que é baseado em hash) ou Stream Aggregate (que é baseado em ordem). A dica HASH GROUP força-o a usar
a primeira e a dica ORDER GROUP, a última.

Na Listagem 10-2, temos uma consulta GROUP BY simples que retorna uma contagem do número de ocorrências
de cada valor distinto na coluna Suffix da tabela Person.

SELECT p.Sufixo,
COUNT(*) AS SuffixUsageCount
DE Pessoa.Pessoa AS p
GROUP BY p.Sufixo;

Listagem 10-2

Vamos supor que você, como DBA, mantenha uma loja sofisticada onde a força de vendas envia muitas consultas em
relação a um conjunto de dados em constante mudança. Um dos aplicativos de vendas frequentemente chama a
consulta na Listagem 10-2 e seu trabalho é fazer com que essa consulta seja executada o mais rápido possível.

A primeira coisa que você fará, é claro, é examinar o plano de execução, conforme mostrado na Figura 10-1.

Figura 10-1: Plano de execução não forçado usando uma correspondência de hash para agregação.

Como você pode ver, o otimizador optou por usar hash para esta consulta. Os dados "não ordenados" do Clustered
Index Scan são agrupados no operador Hash Match (Aggregate) .
Esse operador cria uma tabela de hash, criando entradas para cada um dos valores distintos nos dados fornecidos
pelo Clustered Index Scan e mantém uma contagem de cada um desses valores.

305
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Como ponto de referência, no meu sistema e na minha versão do AdventureWorks, a varredura na tabela
Person causou 3.819 leituras, o plano teve um custo estimado de 2,99727 e a consulta foi executada em
cerca de 9,7 ms.

Embora não seja a operação mais cara do plano (que é o Clustered Index Scan), você pode ter lido que
o Hash Match pode causar problemas devido à sobrecarga de construir e preencher uma tabela na
memória e porque esta é uma operação de "bloqueio" .
Portanto, vamos ver o que acontece se forçarmos o otimizador a usar um Stream Aggregate
em vez disso, adicionando a dica ORDER GROUP à consulta.

SELECT p.Sufixo,
COUNT (p. Sufixo) AS SuffixUsageCount
DE Pessoa.Pessoa AS p
Agrupar por p.Sufixo
OPÇÃO (GRUPO DE ORDEM);

Listagem 10-3

A Figura 10-2 mostra o novo plano.

Figura 10-2: Plano de execução forçado a usar o operador Stream Aggregate.

Como a agregação de fluxo requer dados classificados (consulte o Capítulo 5) e como não há índice que
o SQL Server possa usar para produzir diretamente as linhas ordenadas pelo sufixo, o otimizador
introduziu um operador Sort para impor a ordenação necessária e o custo estimado do plano saltou 39%
para 4,17893, sendo a origem do aumento de custo a operação Sort . Como resultado, essa consulta agora
é executada em 18 ms, em vez dos 9,7 ms originais, um aumento de 100%.

O problema mais amplo com essa dica, como com todas as dicas, é que ela força um determinado
comportamento, independentemente de alterações na estrutura do banco de dados, como adição ou
remoção de índices, ou nos dados. Em vez de adicionar a dica, é muito melhor descobrir por que o
otimizador não usa agregação de fluxo e, em seguida, corrigir a causa raiz. Por exemplo, se apropriado
para a carga de trabalho de consulta, você pode considerar adicionar um novo índice não clusterizado ou
modificar um índice existente.

306
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

MERGE | HASH | CONCAT UNIÃO

Essas dicas de consulta afetam como o otimizador lida com as operações UNION em suas consultas, instruindo o
otimizador a usar mesclagem, hashing ou concatenação dos conjuntos de dados.
Se uma operação UNION estiver causando problemas de desempenho, você pode ficar tentado a usar essas dicas
para orientar o comportamento do otimizador. Conforme discutido no Capítulo 4, o otimizador nunca usará um
operador Hash Match para uma concatenação UNION ALL e, portanto, a dica HASH UNION não funciona para consultas
UNION ALL.

A consulta de exemplo na Listagem 10-4 não está sendo executada com rapidez suficiente para atender às
demandas do aplicativo.

SELECIONE pm1.Nome,
pm1.ModifiedDate
FROM Production.ProductModel AS pm1
UNIÃO
SELECT p.Nome,
p.ModifiedDate
DA Produção.Produto AS p;

Listagem 10-4

Quando uma consulta for identificada como lenta, é hora de examinar o plano de execução, conforme visto na Figura
10-3.

Figura 10-3: Um plano de execução para uma operação UNION usando concatenação.

O operador Concatenação simplesmente concatena as 128 linhas da entrada superior com as 504 linhas da parte
inferior e, no contexto do plano, é muito barato. O operador Sort , especificamente um Distinct Sort (consulte o
Capítulo 5), está no plano para remover duplicatas, conforme exigido pela cláusula UNION, e é relativamente caro. A
consulta levou cerca de 121 ms para ser executada
com 29 leituras.

307
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Talvez forçar o uso de um operador de junção para implementar a cláusula UNION, em vez de
concatenação, possa permitir que o otimizador remova o caro operador Sort e melhore o desempenho?
Como primeiro teste, você aplica a dica MERGE UNION.

SELECIONE pm1.Nome,
pm1.ModifiedDate
FROM Production.ProductModel AS pm1
UNIÃO
SELECT p.Nome,
p.ModifiedDate
DA Produção.Produto AS p
OPÇÃO (UNIÇÃO DE FUSÃO);

Listagem 10-5

O plano confirma que você forçou a operação UNION a usar o Merge Join (Union)
em vez do operador de concatenação .

Figura 10-4: Forçando o plano de execução a usar um Merge Join para UNION.

Agora que estamos unindo em vez de concatenar as linhas, não vemos mais o Distinct Sort. No
entanto, como o Merge Join só funciona com feeds de dados classificados, também forçamos o
otimizador a usar dois operadores Sort para classificar cada uma das entradas. O tempo de execução
subiu para 193ms de 121ms e as leituras foram para 41 de 29. Claramente, isso não funcionou.

E se você tentasse a dica HASH UNION? Observe que o uso dessa dica só funcionará se a
entrada da sonda (inferior) tiver a garantia de não ter duplicatas, como é verdade aqui.

SELECIONE pm1.Nome,
pm1.ModifiedDate
FROM Production.ProductModel AS pm1
UNIÃO

308
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

SELECT p.Nome,
p.ModifiedDate
DA Produção.Produto AS p
OPÇÃO (HASH UNION);

Listagem 10-6

A Figura 10-5 mostra o novo plano de execução, com as operações Sort eliminadas, embora, se a entrada inferior
tivesse duplicatas, o otimizador precisaria adicionar um Sort (Distinct Sort) ou outro operador à entrada para removê-
las. Você pode verificar isso removendo a coluna Nome da Listagem 10-6.

Figura 10-5: Plano de execução forçado a usar um operador Hash Match Union.

Alcançamos nosso objetivo inicial de eliminar o operador Sort pós-união sem introduzir nenhum novo operador
Sort . Acontece que, neste caso, usar um Hash Match para realizar a operação UNION é mais barato do que
realizar uma Concatenação seguida de uma Ordenação Distinta, e o tempo de execução diminuiu de 121ms em
média para 99ms, enquanto as leituras permaneceram as mesmas . Claro, é possível que com tabelas maiores ou
diferentes a dinâmica possa mudar.

CIRCUITO | MERGE | HASH JOIN


Essas dicas de consulta fazem com que todas as operações de junção na consulta, incluindo as semijunções
usadas para preencher as cláusulas EXISTS ou IN, usem o método fornecido pela dica. No entanto, observe que,
se também aplicarmos uma dica de junção (abordada posteriormente) em uma junção específica, a dica de junção
mais granular terá precedência sobre a dica de consulta geral.

309
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Digamos que nosso sistema esteja sofrendo de E/S de disco ruim, portanto, precisamos reduzir o número
de leituras que nossas consultas geram. Ao coletar dados de Eventos Estendidos e Monitor de Desempenho,
identificamos a consulta na Listagem 10-7 como uma que precisa de alguns ajustes.

SELECIONE pm.Nome,
pm.CatalogDescription,
p.Nome AS ProductName,
i. Diagrama
FROM Production.ProductModel AS pm
LEFT JOIN Produção.Produto AS p
ATIVADO pm.ProductModelID = p.ProductModelID
LEFT JOIN Produção.ProdutoModeloIlustração AS pmi
ON p.ProductModelID = pmi.ProductModelID
LEFT JOIN Produção.Ilustração AS i
ON pmi.IllustrationID = i.IllustrationID
WHERE pm.Name LIKE '%Mountain%'
ENCOMENDAR POR pm.Nome;

Listagem 10-7

A Figura 10-6 mostra o plano

Figura 10-6: Uma combinação de loops aninhados e junções de correspondência de hash.

O predicado de consulta, WHERE pm.name LIKE '%Mountain%', não é SARGable, um termo usado
para predicados que não podem ser usados pelo otimizador em um Index Seek e, portanto, o
operador Clustered Index Scan na tabela ProductModel faz sentido. A consulta não possui filtro na
tabela Produto, portanto, a verificação é a única opção. O otimizador usa um operador Hash Match para
unir as tabelas Product e ProductModel , representando 39% do custo estimado
eledo
executa
plano. aEm
classificação
seguida,
necessária que, como o otimizador estima apenas cerca de 99 linhas correspondentes, deve ser barata.
Em seguida, ele usa loops aninhados
joins para construir o resto do conjunto de dados. O otimizador opta por varrer as tabelas ProductMod
elIllustration e Illustration em vez de procurá-las, provavelmente porque ambas são tão pequenas que as
estimativas de custo são muito pequenas para fazer uma diferença significativa no custo total da consulta.

310
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Em meus testes, essa consulta foi executada em cerca de 74 ms, exigindo 485 leituras lógicas, conforme medido
usando Eventos Estendidos (consulte o Capítulo 2, Listagem 2-6).

Novamente, digamos que você tenha lido que as junções de Hash Match incorrem na sobrecarga de criar uma
tabela de trabalho na memória que é propensa a derramar para tempdb. Talvez seja mais barato se forçarmos o uso de
junções de loops aninhados , adicionando a dica LOOP JOIN ao final da consulta?

…OPÇÃO ( LOOP JOIN );

Listagem 10-8

A Figura 10-7 mostra o novo plano.

Figura 10-7: Forçando o plano de execução a usar apenas junções de loops aninhados.

Como esperado, forçamos o otimizador a usar junções de loops aninhados por toda parte. Como resultado, ele moveu a
operação Sort diretamente após a verificação da tabela ProductModel, o que pode ser feito porque uma junção de loops
aninhados sempre preservará a ordem da entrada externa, então agora classificará apenas cerca de 40 linhas (número
real é 37). Além disso, deveríamos ter eliminado a necessidade de tabelas de trabalho na memória. Mas reduziu o I/O?

Infelizmente não. A consulta agora executa 1250 leituras lógicas e foi executada em cerca de 73 ms. Isso se deve
ao aumento das leituras lógicas na tabela Produto. Graças a nós forçarmos o uso de junções de loops aninhados ,
essa tabela agora é verificada 37 vezes, uma vez para cada linha retornada pelo nosso operador Sort . No lado positivo,
se você verificar a propriedade MemoryGrantInfo do operador Select , para a Figura 10-7, verá que a consulta tem
uma concessão de memória significativamente menor em comparação com o plano original, o que pode ser considerado se
este fosse um consulta executada com frequência.

311
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

E se modificarmos a consulta para usar a dica MERGE JOIN?

OPÇÃO ( MERGE JOIN );

Listagem 10-9

A Figura 10-8 mostra o novo plano.

Figura 10-8: Um plano de execução que contém apenas Merge Joins.

O plano tem uma forma diferente e parece mais complicado principalmente porque, infelizmente, agora vemos
três operadores Sort em vez de um. A coluna Classificar no Nome agora é a operação final, antes de retornar
os resultados. Os dois novos operadores Sort são necessários porque, conforme discutido no Capítulo 4, os
dados em cada entrada devem ser ordenados na coluna de junção, e o fluxo de dados da tabela Produto e o
que emerge da segunda junção de mesclagem não estão em a ordem necessária.

Conseguimos reduzir as leituras lógicas? Na verdade, sim, esse plano realiza apenas 116 leituras
lógicas. No entanto, em meus testes, o desempenho não melhorou (cerca de 83ms em meus testes). O
primeiro problema é a sobrecarga extra das operações de classificação; a concessão de memória é quase
o dobro da consulta original. O segundo problema é que a junção de mesclagem mais à direita é uma junção
de muitos para muitos, que requer a criação de uma tabela de trabalho em tempdb e é muito menos eficiente
(consulte o Capítulo 4, Listagem 4-3 e discussão subsequente).

Dado que dissemos que estávamos preocupados com a sobrecarga das tabelas de trabalho, dificilmente
tentaríamos a opção final, a dica HASH JOIN, mas vamos ver o que isso pode fazer.

OPÇÃO ( HASH JOIN );

Listagem 10-10

312
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

A Figura 10-9 mostra o novo plano.

Figura 10-9: Forçando o plano a usar Hash Joins.

Agora vemos três junções de Hash Match e voltamos para apenas um Sort (no nome), mas acabou no lado
esquerdo. Este é o único lugar em que o otimizador pode colocá-lo com segurança, já que as junções de
Hash Match não são garantidas para preservar a ordem da entrada do probe (se fossem, a classificação
poderia ir diretamente após a varredura de ProductModel).

Como ele funciona? Bem, reduzimos as leituras lógicas para 97, o melhor até agora, mas a consulta é
executada quase ao mesmo tempo que a consulta original. Se estivermos vendo muita contenção de E/S, isso
pode ser uma vitória possível, mas você precisaria testar isso em um ambiente com carga adicional para
entender se há problemas de contenção. Além disso, aumentamos significativamente o custo da memória; a
concessão de memória é de até cerca de 6080 KB, devido à sobrecarga de valores de hash em todas as
tabelas e à criação de tabelas de hash para as entradas de compilação.

No geral, nossos esforços renderam recompensas mínimas, e se você optou por usar uma dessas dicas
dependeria dos pontos de contenção em seu sistema. Mais significativamente, todos os nossos esforços com
dicas ignoraram o maior problema com esta consulta, que é o uso do LIKE '%Mountain%' na cláusula WHERE.
Este é um operador que só pode ser resolvido por varreduras na tabela, e são essas varreduras que são
nosso principal problema. A melhor solução para esta consulta poderia ser modificar a estrutura do banco de
dados para que a necessidade da consulta LIKE, usando curingas, seja eliminada. Quando não for possível
modificar o código ou a estrutura, talvez seja necessário recorrer a dicas de consulta para tentar obter melhorias
onde puder.

313
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

RÁPIDO n

Vamos supor por um momento que estamos menos preocupados com o desempenho geral do banco de dados,
geralmente uma proposta muito ruim, do que com o desempenho percebido do aplicativo. Os usuários gostariam de
um retorno imediato dos dados para a tela, mesmo que não seja o conjunto de resultados completo, e mesmo que eles
acabem esperando mais tempo pelo conjunto de resultados completo. Essa pode ser uma maneira útil de colocar um
pouco de informação na frente das pessoas rapidamente, para que elas possam decidir se é importante e seguir em
frente ou aguardar o restante dos dados.

A dica FAST n fornece essa capacidade fazendo com que o otimizador se concentre em encontrar o plano de
execução que retornará as primeiras "n" linhas o mais rápido possível, onde "n" é um valor inteiro positivo. Considere
a consulta e o plano de execução a seguir.

SELECT soh.SalesOrderNumber,
soh.OrderData,
soh.DueDate,
sod.CarrierTrackingNumber,
sod.OrderQty
FROM Sales.SalesOrderDetail AS sod
JOIN Sales.SalesOrderHeader AS soh
ON sod.SalesOrderID = soh.SalesOrderID
ORDEM POR soh.DueDate DESC;

Listagem 10-11

A Figura 10-10 mostra o plano. O custo estimado da subárvore deste plano é 11,4, portanto, se o limite de
custo para a configuração de paralelismo (consulte o Capítulo 11) for 11,4 ou superior, você verá a versão paralelizada
desse plano.

Figura 10-10: Um plano de execução otimizado para retornar todos os dados rapidamente.

314
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Não vou explicar esse plano em detalhes, exceto para apontar o aviso visível no operador SELECT.
Se você observar a propriedade Warnings do operador SELECT , encontrará o seguinte:

Conversão de tipo na expressão (CONVERT(nvarchar(23),[soh].


[SalesOrderID],0)) pode afetar "CardinalityEstimate" na escolha do plano de consulta

Isso é causado por uma coluna calculada na tabela SalesOrderHeader. Este é um exemplo de um aviso
falso. Isso não afeta nossa consulta de forma alguma porque não estamos nos referindo a essa coluna em
nenhuma cláusula de filtragem.

Esta consulta tem um desempenho adequado considerando o fato de que ela está selecionando todos os
dados das tabelas sem nenhum tipo de operação de filtragem, mas vamos tentar obter algumas linhas, mas
não todas, mais rapidamente desta consulta adicionando a dica FAST n para retornar a primeira 10 linhas o
mais rápido possível.

OPÇÃO ( RÁPIDO 10 );

Listagem 10-12

Figura 10-11: Um plano de execução otimizado para retornar apenas 10 linhas.

Agora, o otimizador escolhe um operador de loops aninhados para realizar a junção, em vez de uma
junção de mesclagem. Esse plano retorna as primeiras linhas muito rápido, mas o restante do processamento
foi um pouco mais lento, o que talvez seja esperado, pois o otimizador concentra seus esforços em obter
apenas as primeiras dez linhas o mais rápido possível. A maneira como isso funciona, internamente, é que o
otimizador trata essa consulta como se ela tivesse uma cláusula TOP (10) e só retornasse 10 linhas. Isso muda
completamente as escolhas do plano de execução; o plano que você obtém geralmente será o mesmo que o
plano para uma consulta que usa TOP, mas sem os operadores que implementam a cláusula TOP.

315
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

O custo total estimado para a consulta original foi de 11,3573. A dica reduziu esse custo para 2,72567. Embora
isso pareça ótimo, lembre-se de que é o custo estimado apenas para as primeiras 10 linhas. É também por
isso que o plano da Figura 10-11 mostra algumas estimativas de linhas "ruins". Por exemplo, se você verificar
as propriedades do operador Sort , verá que o otimizador estimou que ele retornaria 2,6 linhas (o número real de
linhas era 31465).

Fizemos a escolha de que não nos importamos com o impacto geral no desempenho do sistema, apenas
queremos ver as primeiras 10 linhas muito rapidamente. No entanto, não podemos ignorar o fato de que o número
de leituras lógicas aumenta drasticamente, de 1.935 para a consulta não sugerida para 106.505 para a consulta
sugerida. Dependendo da carga em seu sistema e da contenção em seu disco, obter uma aparência responsiva em
seu aplicativo pode afetar seriamente o sistema geral.

FORÇAR ORDEM

Mais uma vez, nossas ferramentas de monitoramento identificaram uma consulta com desempenho insatisfatório. É
uma consulta longa com um número maior de tabelas sendo unidas, conforme mostrado na Listagem 10-13, o que
pode ser uma preocupação, pois quanto mais tabelas estiverem envolvidas, mais difícil será o trabalho do otimizador.

Normalmente, o otimizador determinará a ordem em que as junções ocorrem, reorganizando-as como achar
melhor. No entanto, o otimizador pode fazer escolhas incorretas quando as estatísticas não estão atualizadas,
quando a distribuição de dados está abaixo do ideal ou se a consulta tem um alto grau de complexidade, com muitas
junções. No último caso, o otimizador pode até atingir o tempo limite ao tentar reorganizar as tabelas porque há
muitas delas para tentar lidar.

Usando a dica FORCE ORDER, você pode fazer com que o otimizador use a ordem das junções conforme
você as definiu na consulta. Essa pode ser uma opção se você tiver certeza de que sua ordem de junção é melhor
do que a fornecida pelo otimizador, se você estiver enfrentando tempos limite no processo de otimização ou se vir
muitas compilações ou recompilações de uma consulta e o desempenho do sistema for sofrimento como resultado
(embora o teste esteja, como sempre, em ordem).

SELECT pc.Name AS ProductCategoryName,


ps.Name AS ProductSubCategoryName,
p.Nome AS ProductName,
pdr.Descrição,
pm.Name AS ProductModelName,
c.Name AS CultureName,
d.FileName,
em.Quantidade,
pr.Classificação,

316
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

pr.Comentários
DE Production.Product AS p LEFT JOIN
Production.ProductModel AS pm
ON p.ProductModelID = pm.ProductModelID LEFT JOIN
Produção.ProductSubcategory AS ps
ON p.ProductSubcategoryID = ps.ProductSubcategoryID
LEFT JOIN Production.ProductInventory AS pri ON p.ProductID
= pri.ProductID LEFT JOIN Production.ProductReview AS
pr
ON p.ProductID = pr.ProductID LEFT JOIN
Production.ProductDocument AS pd ON p.ProductID =
pd.ProductID LEFT JOIN Production.Document AS d

ON pd.DocumentNode = d.DocumentNode LEFT


JOIN Production.ProductCategory AS pc
ON ps.ProductCategoryID = pc.ProductCategoryID LEFT JOIN
Production.ProductModelProductDescriptionCulture AS pmdc

ON pm.ProductModelID = pmdc.ProductModelID
LEFT JOIN Production.ProductDescription AS pdr ON
pmpdc.ProductDescriptionID = pdr.ProductDescriptionID
LEFT JOIN Produção.Cultura AS c
ON c.CultureID = pmdc.CultureID;

Listagem 10-13

Com base em seu conhecimento dos dados, você tem certeza de que colocou as junções na ordem
correta. A Figura 10-12 mostra o plano de execução atual.

Figura 10-12: Plano de execução grande com mais tabelas.

317
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Esse plano é grande demais para ser revisto nesta página do livro. A imagem na Figura 10-12 dá uma boa ideia da
estrutura geral e da forma do plano de execução. A Figura 10-13 mostra uma vista explodida da parte inferior direita da
planta, mostrando apenas algumas das tabelas e a ordem em que estão sendo unidas.

Figura 10-13: Subconjunto do plano de execução na Figura 10-12 mostrando a ordem de junção da tabela.

Seguindo o fluxo de dados, vemos primeiro a junção de Hash Match entre ProductModel e Product. Esses dados
formam a entrada inferior para uma junção de Hash Match ao ProductSubcate gory e esse fluxo de dados combinado
forma a entrada inferior para a junção de Hash Match ao Product Inventory e assim por diante. No entanto, na ordem
de execução, o otimizador começa na outra extremidade, com Culture, depois ProductDescription, depois Product-
ModelProduct DescriptionCulture e assim por diante.

Se você verificar as propriedades do operador SELECT , verá que o otimizador expirou ao gerar este plano de
execução.

Figura 10-14: Propriedade SELECT mostrando o motivo da rescisão antecipada.

Com um número maior de tabelas e um tempo limite no otimizador, há uma boa chance de que nem todas as
permutações possíveis da ordem de junção tenham sido tentadas. Se tivéssemos esgotado outras tentativas de
ajustar essa consulta, poderíamos tentar obter o controle do otimizador usando uma dica de consulta. Faça a mesma
consulta e aplique a dica de consulta FORCE ORDER.

318
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

OPÇÃO (FORÇAR ORDEM);

Listagem 10-14

Isso resulta no plano mostrado na Figura 10-15.

Figura 10-15: Uma nova forma de plano de execução devido à dica FORCE ORDER.

Você pode dizer, apenas comparando as formas do plano na Figura 10-12 com o da Figura 10-15, que
ocorreu uma mudança substancial. O otimizador agora está acessando as tabelas exatamente na ordem
especificada pela consulta. Novamente, ampliaremos o conjunto de operadores no lado direito do plano, para
que você possa ver como a ordem de associação mudou.

Figura 10-16: Subconjunto da Figura 10-15 mostrando uma ordem de tabela diferente nas junções.

Agora a ordem de junção é da tabela Product, seguida pelo ProductModel, exatamente como especificado
na consulta. Esses dados formam a entrada principal para uma categoria Merge Join to ProductSub, que
forma a entrada principal para Merge Join to ProductInventory e assim por diante.
Essa ordem força o otimizador a fazer mais operações de classificação , e o tempo de execução passou de
149ms na primeira consulta para 166ms na segunda. Embora seja possível obter controle direto sobre o
otimizador para obter resultados positivos, esse não é um desses casos.

MAXDOP
Neste exemplo, temos um daqueles problemas desagradáveis em que uma consulta que às vezes
funciona bem, às vezes é incrivelmente lenta. Investigamos o problema, usando Extended Events ou o Query
Store para capturar o plano de execução de uma consulta, ao longo do tempo, com vários parâmetros.
Finalmente chegamos a dois planos de execução. A Figura 10-17 mostra o plano de execução que resulta em
melhor desempenho em meu sistema.

319
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Figura 10-17: Um plano de execução serial que é executado rapidamente.

A Figura 10-18 mostra o plano de execução mais lento (modifiquei esta imagem para facilitar a leitura).

Figura 10-18: Um plano de execução paralela que, neste caso, não é tão rápido.

Este é um exemplo de onde o otimizador estimou que o custo de execução do plano de forma serial pode exceder o
'limite de custo para paralelismo' sp_
configure e, assim, produz um plano paralelo, onde o trabalho necessário para executar a consulta é dividido entre
várias CPUs (consulte o Capítulo 11 para obter mais detalhes). Idealmente, isso deve ajudar no desempenho do seu
sistema, mas parece estar prejudicando neste caso específico.

Claro, a primeira pergunta a fazer aqui é por que temos dois planos com dois custos diferentes.
O que causou a nova compilação em primeiro lugar e por que os custos são diferentes? Se esta fosse uma consulta
parametrizada, então o sniffing de parâmetros poderia ser um provável culpado (veja o Capítulo 8), e nós
investigaríamos essa possibilidade primeiro. No entanto, neste caso estamos lidando com uma consulta simples e, para
esta discussão, decidimos resolver o problema da maneira "fácil", com uma dica.

Podemos controlar o paralelismo definindo o valor Max Degree of Parallelism no nível do servidor. Você também
pode controlar essa configuração no nível do banco de dados, e isso geralmente é considerado a melhor
abordagem. Um sistema configurado corretamente se beneficiará da execução paralela, portanto, você não deve
simplesmente desativá-lo. Também presumiremos que você ajustou o valor do limite de custo para paralelismo, em
seu servidor, para ter certeza de que apenas consultas de alto custo estão passando por paralelismo. (Uma forte
recomendação: não deixe o valor padrão de 5; para detalhes, veja esta postagem no blog: http://bit.ly/2DM92sc.)

No entanto, tendo feito esse trabalho, você ainda tem os valores discrepantes ocasionais em que o mecanismo de
execução opta por usar o plano paralelo. É para casos como esse que a dica MAXDOP se torna útil, pois controla o
uso de paralelismo em uma consulta individual, em vez de trabalhar usando a configuração de nível máximo de
paralelismo em todo o servidor.

Por exemplo, podemos suprimir completamente o paralelismo para essa consulta definindo MAXDOP como 1.
Mais comumente, o usaríamos para definir MAXDOP para um valor maior que 1, mas menor que o número de
processadores, para garantir que um consulta não monopoliza todos os recursos.

320
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Este exemplo é um pouco artificial porque, como parte da consulta, vou redefinir o limite de custo para
paralelismo do meu sistema para um valor baixo, para permitir que essa consulta seja executada em paralelo.

--habilitar opções avançadas EXEC


sys.sp_configure 'mostrar opções avançadas', 1 GO RECONFIGURE
WITH OVERRIDE GO --altere o limite de custo para 1 EXEC sp_configure
'limite de custo para paralelismo', 1; VAI RECONFIGURAR COM
OVERRIDE; GO --Executa a consulta que irá paralelamente SELECT
wo.DueDate,

MIN(wo.OrderQty) AS MinOrderQty,
MIN(wo.StockedQty) AS MinStockedQty,
MIN(wo.ScrappedQty) AS MinScrappedQty,
MAX(wo.OrderQty) AS MaxOrderQty, MAX(wo.StockedQty)
AS MaxStockedQty, MAX(wo.ScrappedQty ) ) AS
MaxScrappedQty DE Production.WorkOrder AS wo
GRUPO POR wo.DueDate ORDER POR wo.DueDate; GO --redefinir
o limite de custo para o valor padrão --se o limite de custo estiver
definido para um valor diferente, altere o 5 EXEC sys.sp_configure
'limite de custo para paralelismo', 5; VAI RECONFIGURAR COM
OVERRIDE; GO --disable advanced options EXEC sys.sp_configure
'show advanced options', 0 GO RECONFIGURE WITH OVERRIDE GO

Listagem 10-15

Isso resultará em um plano de execução que aproveita ao máximo o processamento paralelo, conforme
mostrado na Figura 10-18.

321
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Vamos agora modificar a consulta para incluir a dica MAXDOP.

OPÇÃO ( MAXDOP 1 );

Listagem 10-16

O uso da dica faz com que o novo plano de execução use um único processador, portanto, nenhum paralelismo
ocorre. Adicione a dica ao final da consulta na Listagem 10-15 e execute novamente o código.
O plano será o mesmo da Figura 10-17.

Geralmente, você espera que o desempenho de determinados operadores, como o Sort de nossa cláusula
ORDER BY na Listagem 10-15, se beneficie muito do paralelismo, pois reduz o custo da CPU e o tempo de
execução. Equilibrar esses tipos de economia é a sobrecarga extra associada aos operadores de paralelismo que
levam os dados de um único fluxo para um conjunto de fluxos paralelos e, em seguida, reúnem tudo novamente.
No meu sistema, parece que esses custos extras superaram as economias. No entanto, com um limite de custo
configurado corretamente para
configuração de paralelismo, você esperaria que a maioria das consultas que cruzam esse limite se beneficiassem
da execução paralela.

OTIMIZAR PARA
Você pode usar a dica OPTIMIZE FOR em qualquer situação em que queira tentar controlar como o otimizador lida
com valores de parâmetro. Digamos que você tenha identificado uma consulta que será executada em uma
velocidade adequada por horas ou dias e, de repente, ela terá um desempenho horrível.
Com muita investigação e experimentação, você descobre que os parâmetros fornecidos pelo aplicativo para
executar o procedimento ou consulta parametrizada geralmente resultam em um plano de execução que funciona
muito bem. Às vezes, porém, um determinado valor ou subconjunto de valores fornecidos aos parâmetros após um
evento de recompilação resulta em um plano de execução com desempenho extremamente ruim. Esta é uma
instância do problema de sniffing de parâmetros ruins , conforme discutido no Capítulo 8.

Quando você está atingindo uma situação de sniffing de parâmetro ruim, você pode usar o OPTIMIZE FOR
dica, que instrui o otimizador a otimizar a consulta para o valor que você fornece, em vez de um valor de parâmetro
rastreado. A partir do SQL Server 2008, também podemos usar a dica OPTI MIZE FOR com um valor UNKNOWN
para forçar um plano mais genérico no otimizador, em vez de um plano específico para um valor específico.

Podemos demonstrar a utilidade dessa dica com um conjunto muito simples de consultas.

SELECT EndereçoID,
Endereço Linha 1,
Endereço linha 2,

322
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

City,
StateProvinceID,
PostalCode,
SpatialLocation,
rowguid, ModifiedDate
FROM Person.Address
WHERE CityAddressID,
= 'Mentor'; SELECT
AddressLine1, AddressLine2,
Cidade, StateProvinceID,
PostalCode,
SpatialLocation,
rowguid, ModifiedDate
FROM Person.Address
WHERE City = 'London';

Listagem 10-17

Vamos executá-los ao mesmo tempo e obter dois planos de execução diferentes.

Figura 10-19: Dois planos de execução diferentes para dois valores diferentes.

323
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Cada consulta está retornando os dados da tabela de maneira ideal para o valor passado a ela, com base nos
índices e nas estatísticas da tabela. O primeiro plano de execução, para a primeira consulta, em que Cidade = 'Mentor'
varre a tabela de endereços para encontrar valores correspondentes. Em seguida, ele deve executar uma operação de
pesquisa de chave para obter o restante dos dados. Os dados são unidos por meio da operação de loops aninhados .
O valor de London é muito menos seletivo, então o otimizador decide realizar uma varredura apenas do índice
clusterizado, que você pode ver no segundo plano de execução na Figura 10-19.

Se essa consulta estivesse em um procedimento armazenado, que foi executado primeiro com um valor de Mentor,
então na próxima vez que a executarmos com um valor de London, o plano seria reutilizado (a menos que tenha sido
recompilado por algum motivo) e nós ' d provavelmente verá muitas pesquisas importantes e desempenho muito ruim.

Podemos considerar adicionar uma dica de consulta OPTIMIZE FOR (@City = 'London'). Embora isso possa
parecer uma opção sensata neste caso, o problema mais geral com o OPTIMIZE
FOR <value> dica, é que é suscetível a "ficar ruim", pois os dados na tabela mudam com o tempo.

Vamos agora ver o que acontece se usarmos variáveis locais em nosso T-SQL, conforme mostrado
na Listagem 10-18.

DECLARE @City NVARCHAR(30)


SET @Cidade = 'Mentor'
SELECT EndereçoID,
Endereço Linha 1,
Endereço linha 2,
Cidade,
StateProvinceID,
Código postal,
Localização espacial,
desculpe,
Data modificada
A PARTIR DE Pessoa.Endereço
ONDE Cidade = @Cidade;
SET @Cidade = 'Londres'
SELECT EndereçoID,
Endereço Linha 1,
Endereço linha 2,
Cidade,
StateProvinceID,
Código postal,
Localização espacial,

324
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

desculpe,
Data modificada
DE Pessoa.Endereço
ONDE Cidade = @Cidade;

Listagem 10-18

Agora, vemos o mesmo plano, com uma varredura de índice clusterizado, para ambas as consultas.

Figura 10-20: Planos de execução idênticos para consultas usando uma variável local.

Conforme descrito no Capítulo 8, o otimizador não pode farejar o valor fornecido, quando usamos variáveis locais, a menos
que a recompilação em nível de instrução ocorra devido a uma dica OPTION (RECOM PILE) (abordada posteriormente). Ele
otimiza para a distribuição média, usando o valor de densidade, para chegar a uma estimativa de cardinalidade (é a razão
entre o número de linhas na tabela e o número de valores distintos). Se soubermos que o plano resultante será bom o suficiente
para a maioria das execuções, podemos considerar o uso da dica OPTIMIZE FOR UNKNOWN para forçar o otimizador a
produzir esse plano genérico. A Listagem 10-19 mostra um exemplo (simplesmente movi a consulta para um procedimento
armazenado).

325
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

CRIAR OU ALTERAR PROCEDIMENTO dbo.AddressByCity @City NVARCHAR(30)


COMO

SELECT EndereçoID,
Endereço Linha 1,
Endereço linha 2,
Cidade,
StateProvinceID,
Código postal,
Localização espacial,
desculpe,
Data modificada
DE Pessoa.Endereço
ONDE Cidade = @Cidade
OPÇÃO (OTIMIZAR PARA DESCONHECIDO);
VAI
EXEC dbo.AddressByCity @Cidade = N'Mentor';

Listagem 10-19

Mesmo que Mentor seja uma cidade incomum e, portanto, nosso índice não clusterizado seja seletivo
para esse predicado, ainda vemos o plano "genérico".

Figura 10-21: O plano uma vez que a dica OPTIMIZE FOR foi aplicada.

O uso da dica OPTIMIZE FOR requer conhecimento profundo dos dados subjacentes.
Escolher o valor errado para OPTIMIZE FOR não só não ajuda no desempenho, mas pode ter um
impacto negativo muito sério. Também é muito importante manter a dica e adaptá-la conforme necessário,
à medida que os dados mudam com o tempo.

No exemplo acima, havia apenas uma única variável, portanto, era necessária apenas uma única
dica. Se você precisar controlar o valor usado para otimização de mais de uma única variável em uma
consulta, poderá definir quantas dicas forem necessárias. A Listagem 10-20 mostra um exemplo da sintaxe
necessária.

326
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

CRIAR OU ALTERAR PROCEDIMENTO dbo.AddressDetails


@Cidade NVARCHAR(30),
@PostalCode NVARCHAR(15),
@AddressLine2 NVARCHAR(60) NULL
COMO

SELECT a.AddressLine1,
a.AddressLine2,
a.Localização Espacial
DE Pessoa.Endereço AS a
ONDE a.Cidade = @Cidade
E a.PostalCode = @PostalCode
E ( a.AddressLine2 = @AddressLine2
OU @AddressLine2 É NULO)
OPÇÃO (OPTIMIZE FOR (@City = 'London', @PostalCode = 'W1Y 3RA'));

Listagem 10-20

A dica OPTIMIZE FOR é uma das poucas que uso regularmente, embora ainda não com frequência.
Mesmo assim, recomendo fortemente que você tenha cuidado e faça muitos testes antes de aplicar a dica
OPTIMIZE FOR. À medida que os dados mudam com o tempo, você precisará reavaliar se a escolha que fez
ainda é a correta. Na minha experiência, o OPTIMIZE FOR
A dica UKNOWN geralmente é mais estável do que otimizar para um valor específico, devido a essas
alterações de dados.

RECOMPILAR
Discutimos o uso da dica RECOMPILE no Capítulo 8, como uma cura comum para a detecção de parâmetros
incorretos ao usar procedimentos armazenados ou outras formas de SQL parametrizado, como instruções
preparadas. Aplicamos a dica a qualquer uma das consultas individuais dentro do procedimento e isso forçará
o SQL Server a recompilar o plano para essa consulta sempre. A nova compilação otimizará o plano para os
valores atuais de todas as variáveis e parâmetros usados na consulta (em vez de reutilizar o plano para um
valor previamente rastreado).

A dica de consulta RECOMPILE foi introduzida no SQL Server 2005 junto com recompilações de nível
de instrução. Para procedimentos armazenados e outros módulos de código, todas as instruções,
incluindo aquela com OPTION(RECOMPILE) ainda estarão no cache do plano, mas o plano para a
instrução OPTION(RECOMPILE) ainda será recompilado para cada execução, o que significa que o
plano não é reutilizado de qualquer forma.

327
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Quando usamos a dica para consultas ad hoc, o otimizador marca o plano criado para que ele não seja
armazenado no cache. Discutimos os problemas que as consultas ad hoc podem causar, como sobrecarga
de cache, no Capítulo 9. Se o problema for causado por falta de parametrização, a correção mais comum é
ativar a configuração Otimizar para cargas de trabalho ad hoc . No entanto, se o seu sistema executa
muitas consultas ad hoc parametrizadas e você está tendo problemas de desempenho com sniffing de
parâmetros incorretos, então você pode optar por fazer o SQL Server compilar um plano para cada
execução, aplicando a dica RECOMPILE .

Considere o par de consultas na Listagem 10-21.

SELECT soh.SalesOrderNumber ,
soh.OrderDate soh.SubTotal
,
soh.TotalDue ,

FROM Sales.SalesOrderHeader soh


WHERE soh.VendasPessoaID = 279;
VAI
SELECT soh.SalesOrderNumber ,
soh.OrderDate soh.SubTotal
,
soh.TotalDue ,

FROM Sales.SalesOrderHeader soh


WHERE soh.VendasPessoaID = 280;

Listagem 10-21

Isso resulta no conjunto incompatível de planos de consulta na Figura 10.22, demonstrando mais uma vez
o "ponto de inflexão" do otimizador entre a escolha de um plano com busca e pesquisas versus a varredura
do índice clusterizado (conforme discutido em detalhes no Capítulo 8).

328
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Figura 10-22: Os planos de execução mudam radicalmente quando recompilados.

Se você examinar a propriedade Parameter List de qualquer operador SELECT , parece que ambas as
consultas passaram pela Parametrização Simples (abordada no Capítulo 9). No entanto, o valor da
propriedade StatementParameterizationType , mais abaixo, nos diz que, de fato, eles não foram
parametrizados.

Figura 10-23: Uma tentativa fracassada de Parametrização Simples.

329
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Se essa consulta for executada como uma instrução preparada, veremos um comportamento diferente. O uso
de sp_prepare sempre causa otimização para valores desconhecidos (consulte o Capítulo 9) e, portanto, o
otimizador usará o gráfico de densidade para chegar a uma estimativa de cardinalidade e gerar um plano
apropriado, que será reutilizado para execuções subsequentes.

DECLARE @IDValue INT;


DECLARE @MaxID INT = 280;
DECLARE @PreparedStatement INT; SELECT
@IDValue = 279; EXEC sp_prepare
@PreparedStatement OUTPUT, N'@SalesPersonID INT',
N'SELECT soh.SalesPersonID,
soh.SalesOrderNumber,
soh.OrderDate,
soh.SubTotal,
soh.TotalDue
DE Sales.SalesOrderHeader soh
ONDE soh.SalesPersonID = @SalesPersonID'; WHILE @IDValue <=
@MaxID BEGIN

EXEC sp_execute @PreparedStatement, @IDValue;


SELECT @IDValue = @IDValue + 1;
FIM;
EXEC sp_unprepare @PreparedStatement;

Listagem 10-22

Se você consultar o cache do plano (como mostrado no Capítulo 9) ou o repositório de consultas (consulte
o Capítulo 16), verá um único plano, o plano de varredura de índice clusterizado, usado duas vezes. Isso é
o que você verá, independentemente de executar usando o valor de 280 primeiro em vez de 279, porque o
otimizador não está fazendo sniffing de parâmetros, está otimizando para um valor desconhecido.

Se essa falta de sniffing de parâmetro estiver causando problemas de desempenho para uma das consultas e,
portanto, você preferir otimizar para variáveis rastreadas, considere simplesmente adicionar OPTION
(RECOMPILE) ao final da instrução preparada.


EXEC sp_prepare @PreparedStatement OUTPUT,
N'@SalesPersonID INT', N'SELECT
soh.SalesPersonID, soh.SalesOrderNumber,
soh.OrderDate,
soh.SubTotal,
soh.TotalDue
A PARTIR DE Sales.SalesOrderHeader soh

330
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

ONDE soh.SalesPersonID = @SalesPersonID


OPÇÃO (RECOMPILAR)';

Listagem 10-23

Se você executar a Listagem 10-23 e capturar os planos no SSMS, verá os dois planos diferentes novamente, mas se
verificar o cache do plano, verá que nenhum deles está armazenado em cache.

EXPANDIR VISUALIZAÇÕES

A dica de consulta EXPAND VIEWS elimina o uso de visualizações indexadas ou materializadas em uma consulta
e força o otimizador a ir diretamente às tabelas para os dados. O otimizador substitui a exibição indexada
referenciada pela definição da exibição (em outras palavras, a consulta usada para definir a exibição) como
normalmente faz com uma exibição padrão; mas quando o EXPANDIR
Se a dica VIEWS for usada, ela não tentará corresponder as consultas expandidas com exibições indexadas
utilizáveis. Esse comportamento pode ser substituído em uma base de exibição por exibição, adicionando o WITH
(NOEXPAND) para qualquer exibição indexada na consulta. A correspondência de exibição indexada é apenas
Enterprise, portanto, essa dica não tem efeito em um sistema Standard.

Em alguns casos, o plano gerado pela referência à exibição indexada tem um desempenho pior do que aquele que
usa a definição de exibição. Na maioria dos casos, o inverso é verdadeiro. Teste esta dica para garantir que seu uso
não afete negativamente o desempenho.

Usando uma das exibições indexadas fornecidas com o AdventureWorks2014, podemos executar a seguinte
consulta simples.

SELECT vspcr.StateProvinceCode,
vspcr.StateProvinceName,
vspcr.CountryRegionName
FROM Person.vStateProvinceCountryRegion AS vspcr;

Listagem 10-24

A Figura 10-24 mostra o plano de execução resultante.

331
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Figura 10-24: Plano de execução usando uma visualização indexada.

Uma exibição é alterada para uma exibição indexada criando um índice clusterizado nela, que armazena os dados
definidos pela consulta na exibição. Este plano de execução faz todo o sentido, pois os dados necessários para
satisfazer a consulta estão disponíveis na visualização indexada. As coisas mudam, como vemos na Figura 10-25,
se adicionarmos a dica de consulta OPTION (EXPAND VIEWS).

Figura 10-25: Definição de exibição expandida devido à dica de consulta.

Agora não estamos mais verificando a exibição indexada. Dentro do processo de compilação (antes que o otimizador
seja invocado), a visualização foi expandida em sua definição e, portanto, o efeito da dica é que a fase de otimização
de correspondência de visualização é ignorada. Como resultado, vemos a Verificação de Índice Clusterizado nas
tabelas Person.CountryRegion e Person.StateProvnce. Eles são então unidos usando uma junção de mesclagem,
depois que os dados no fluxo StateProvce são executados por meio de uma operação de classificação . A primeira
consulta foi executada em cerca de 54 ms, mas a segunda em cerca de 189 ms, portanto , estamos falando de uma
diminuição substancial no desempenho para usar a dica nessa situação.

IGNORE_NONCLUSTERED_COLUMNSTORE_INDEX
Conforme discutido no Capítulo 8, o otimizador pode optar por usar um índice columnstore, quando apropriado.
Os índices Columnstore são extremamente eficientes ao auxiliar consultas de agregação, mas muito menos eficientes
para consultas de pesquisa de ponto tradicionais. Tal como acontece com todas as outras escolhas feitas pelo
otimizador, a escolha de um índice columnstore nem sempre pode ser apropriada.

332
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Você pode usar essa dica de consulta para garantir que qualquer índice columnstore não clusterizado
existente seja ignorado para a consulta inteira. Se a tabela em questão tiver um índice columnstore clusterizado,
essa dica não afetará seu uso no plano de execução.

Dicas de associação

Uma dica de junção fornece um meio de forçar o SQL Server a usar um dos três métodos de junção
padrão que detalhamos no Capítulo 4, mas para uma operação de junção específica em vez de todas as
operações de junção, como vimos quando aplicamos as dicas de consulta anteriormente.

Ao incluir uma das dicas de junção em seu T-SQL, você potencialmente substituirá a escolha do
otimizador do método de junção mais eficiente. Além disso, assim que você força uma junção específica, você
também está forçando a ordem de junção, efetivamente o mesmo que usar OPTION (FORCE
ORDEM). Em geral, isso não é uma boa ideia e, se você não for cuidadoso, poderá prejudicar seriamente o
desempenho.

A aplicação da dica de junção se aplica a qualquer consulta (SELECT, INSERT ou DELETE) onde as junções
podem ser aplicadas. As dicas de junção são especificadas como parte da cláusula JOIN entre duas entradas
(como tabelas). Você pode usar as dicas de junção LOOP, HASH ou MERGE da mesma forma. O comportamento
principal não mudará. Você apenas obterá uma junção diferente dependendo da dica que usar.
Vale a pena notar que você não pode forçar um Adaptive Join usando dicas, no momento da escrita.

Há um quarto método de junção, a junção remota , que é usado ao lidar com dados de um servidor remoto. A
dica de junção REMOTE força a operação de junção de sua máquina local para o servidor remoto. Isso não afeta
os planos de execução, portanto, não detalharemos essa funcionalidade aqui.

Como todas as dicas de junção funcionam basicamente da mesma forma, vou demonstrar apenas a dica de
junção HASH, para forçar o uso de um operador Hash Join . Reutilizaremos a consulta simples de uma consulta
anterior (Listagem 10-7) que lista Modelos de Produto, Produtos e Ilustrações.

SELECIONE pm.Nome,
pm.CatalogDescription,
p.Nome AS ProductName,
i. Diagrama
FROM Production.ProductModel AS pm
LEFT JOIN Produção.Produto AS p
ATIVADO pm.ProductModelID = p.ProductModelID
LEFT JOIN Produção.ProdutoModeloIlustração AS pmi

333
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

ON p.ProductModelID = pmi.ProductModelID
LEFT JOIN Produção.Ilustração AS i
ON pmi.IllustrationID = i.IllustrationID WHERE pm.Name LIKE
'%Mountain%'
ENCOMENDAR POR pm.Nome;

Listagem 10-25

Mais uma vez, obteremos o plano de execução mostrado na Figura 10-26.

Figura 10-26: Um plano de execução com junções escolhidas pelo otimizador.

Conforme discutido anteriormente, esse plano (não vou descrevê-lo novamente) envolve 485 leituras lógicas e a consulta
foi executada em cerca de 74ms.

A entrada superior para a junção final de loops aninhados retorna 455 linhas, o que significa que a varredura de índice
agrupado na tabela de ilustração é executada 455 vezes. O que acontece se decidirmos que somos mais inteligentes que
o otimizador e que ele realmente deveria estar usando uma junção de correspondência de hash em vez da junção de loops
aninhados ? Podemos forçar o problema adicionando a dica HASH à condição de junção entre Illustration e
ProductModelIllustration.

SELECT pm.Name,
pm.CatalogDescription, p.Name AS
ProductName, i.Diagram FROM
Production.ProductModel AS pm

LEFT JOIN Production.Product AS p ON pm.ProductModelID


= p.ProductModelID LEFT JOIN Production.ProductModelIllustration
AS pmi
ATIVADO pm.ProductModelID = pmi.ProductModelID
LEFT HASH JOIN Produção . Ilustração AS i
ON pmi.IllustrationID = i.IllustrationID WHERE pm.Name LIKE
'%Mountain%'
ENCOMENDAR POR pm.Nome;

Listagem 10-26

Se executarmos essa nova consulta, veremos o plano mostrado na Figura 10-27.

334
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Figura 10-27: O novo plano com uma junção forçada de loops aninhados.

Com certeza, onde anteriormente víamos um operador de loops aninhados , agora vemos o operador Hash
Match . No entanto, o resto do plano também mudou de forma. O otimizador decidiu que a maneira mais eficiente de lidar
com o Hash Match (que ele não tem escolha a não ser implementar devido à nossa dica) é alterar as outras junções para
Merge. Isso adiciona o requisito de Classificar os dados da tabela Produto.

Curiosamente, neste caso, caímos para 34 leituras lógicas e o tempo de execução cai, um pouco, para 74,1ms em
média. É perfeitamente possível que, eliminando os loops, tenhamos um desempenho superior. A diferença real entre 77
e 74 é pequena, mas as leituras que vão de 485 a 34 são uma economia substancial. Testes adicionais em um sistema
sob carga seriam necessários para determinar se, com certeza, essa dica resultou em desempenho superior.

Dicas de tabela

As dicas de tabela permitem controlar como o otimizador "usa" uma tabela ao gerar um plano de execução para a consulta
à qual a dica de tabela é aplicada. Por exemplo, você pode forçar o uso de uma Verificação de Tabela para essa consulta
ou especificar qual índice deseja que o otimizador use.

Assim como as dicas de consulta e junção, o uso de uma dica de tabela contorna os processos normais do
otimizador e pode levar a sérios problemas de desempenho. Além disso, como as dicas de tabela podem afetar as
estratégias de bloqueio, elas podem afetar a integridade dos dados, levando a dados incorretos ou perdidos.
Use dicas de tabela com moderação e criteriosamente!

A maioria das dicas de tabela se preocupa principalmente com as estratégias de bloqueio. Como eles não afetam os planos
de execução, não os cobriremos. As dicas de tabela abordadas abaixo têm um impacto direto nos planos de execução. Para
obter uma lista completa de dicas de tabela, consulte os Manuais Online.

A sintaxe correta é usar a palavra-chave WITH e, em seguida, listar as dicas dentro de um conjunto de parênteses. A
Listagem 10-27 mostra um exemplo de aplicação de dicas de tabela quando o nome da tabela segue diretamente a
cláusula FROM, mas elas também podem ser usadas quando o nome da tabela segue uma palavra-chave JOIN ou APPLY.

335
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

FROM TableName WITH (dica, dica,…)

Listagem 10-27

A palavra-chave WITH não é necessária em todos os casos, nem as vírgulas são necessárias em todos os casos,
mas, em vez de tentar adivinhar ou lembrar quais dicas são exceções, todas as dicas podem ser colocadas dentro da
cláusula WITH. Como prática recomendada, separe as dicas com vírgulas para garantir um comportamento consistente e
compatibilidade futura. Mesmo com as dicas que não exigem o WITH
palavra-chave, ela deve ser fornecida se mais de uma dica for aplicada a uma determinada tabela.

NOEXPAND
Quando uma ou mais exibições indexadas são referenciadas em uma consulta, o uso da propriedade NOEXPAND
A dica de tabela impedirá a expansão da visualização, aproximadamente o oposto da dica EXPAND VIEW que usamos
anteriormente. A dica de consulta afeta todas as exibições na consulta. A dica de tabela impedirá que a exibição indexada
à qual ela se aplica seja "expandida" em sua definição de exibição subjacente.
O principal uso dessa dica é obter exibições indexadas a serem usadas dentro dos planos em sistemas Standard Edition,
porque eles não usarão a exibição materializada de outra forma.

As edições SQL Server Enterprise e Developer usam os índices em uma exibição indexada se o otimizador determinar
que o índice é o melhor para a consulta. Isso é correspondência de exibição indexada e requer as seguintes configurações
para a conexão:

• ANSI_NULL definido como Ativado

• ANSI_WARNINGS definido como Ativado

• CONCAT_NULL_YIELDS_NULL definido como Ativado

• ANSI_PADDING definido como Ligado


• ARITHABORT definido como Ligado

• QUOTED_IDENTIFIER definido como Ativado

• NUMERIC_ROUNDABORT definido como Desligado.

O uso da dica NOEXPAND força o otimizador a usar um dos índices da exibição indexada. No Capítulo 7 (Listagem 7-11),
usamos uma consulta que fazia referência a uma das exibições indexadas, vStateProvinceCountryRegion, em
AdventureWorks2014. Durante o processo de compilação, a exibição indexada foi substituída por sua definição e, em
seguida, o otimizador não desfez isso durante a correspondência de exibição, e vimos um plano de execução que apresentava
uma junção de três tabelas. Por meio do uso da dica de tabela NOEXPAND, na Listagem 10-28, alteramos esse
comportamento.

336
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

SELECIONE a.Cidade,
v.StateProvinceName,
v.CountryRegionName
DE Pessoa.Endereço AS a
JOIN Person.vStateProvinceCountryRegion AS v WITH (NOEXPAND)
ON a.StateProvinceID = v.StateProvinceID
ONDE a.AddressID = 22701;

Listagem 10-28

Agora, em vez de uma junção de três tabelas, obtemos o plano de execução na Figura 10-28.

Figura 10-28: Uma execução menor devido ao uso da dica NOEXPAND.

Agora, não estamos apenas usando o índice clusterizado definido na exibição, mas também estamos vendo
um aumento de desempenho, embora muito pequeno, de 189ms para 162ms em média no meu sistema. As
leituras caíram de 6 para 4. Nessa situação, eliminar a sobrecarga da junção extra resultou em melhor
desempenho. Isso nem sempre será o caso, então você deve testar o uso de dicas com muito cuidado.

ÍNDICE()
A dica de tabela INDEX() permite especificar o índice a ser usado ao acessar uma tabela.
A sintaxe suporta dois métodos, ou quatro se você incluir o WITH (INDEX = (nome ou
number)), embora essa sintaxe não suporte vários índices, portanto, geralmente não é usada.

337
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Podemos especificar o índice a ser usado por seu número ou nome. Os índices são numerados na tabela
sys.indexes. Você terá que procurar qualquer índice lá. Os números 0 e 1 causam comportamentos
diferentes. 0 força uma varredura do índice clusterizado ou do heap, enquanto 1 força uma varredura ou
uma busca em um índice clusterizado e produz um erro em um heap. A sintaxe é a seguinte.

…FROM dbo.TableName WITH (INDEX(2))…

Listagem 10-29

Alternativamente, podemos simplesmente nos referir ao índice pelo nome, o que eu recomendo, porque
a ordem em que os índices são aplicados a uma tabela pode mudar, então você não pode garantir o valor
para o número do índice.

…FROM dbo.TableName WITH (INDEX (IndexName))…

Listagem 10-30

Você só pode ter uma única dica INDEX() para uma determinada tabela, mas pode definir vários
índices dentro dessa dica. Isso é aplicável quando você está tentando realizar junções de índice para
recuperar dados, forçando uma interseção entre todos os índices na tabela, ou seja, forçando o otimizador
a usar todos os índices listados, na ordem listada.

…FROM TableName WITH (INDEX (IndexName1,IndexName2))…

Listagem 10-31

Isso não faz com que o otimizador escolha apenas entre os índices mencionados, mas o força a usar
todos eles, na ordem especificada. Dentro da lista de índices separados por vírgulas, você pode combinar
o número do índice e os formatos do nome do índice. Para uma demonstração rápida, examine o plano
para a consulta a seguir.

CREATE TABLE dbo.IndexSample (ID INT NOT NULL IDENTITY(1, 1),


Coluna A INT,
Coluna B INT,
Coluna C INT,
CONSTRAINT IndexSamplePK
CHAVE PRIMÁRIA
(
EU IRIA

));

338
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

CREATE INDEX FirstIndex ON dbo.IndexSample (ColumnA); CREATE INDEX


SecondIndex ON dbo.IndexSample (ColumnB); CREATE INDEX ThirdIndex ON
dbo.IndexSample (ColumnC); SELECT isa.ID, isa.ColumnA, isa.ColumnB,
isa.ColumnC

FROM dbo.IndexSample AS isa WITH (INDEX(FirstIndex, SecondIndex, ThirdIndex)); DROP


TABLE dbo.IndexSample;

Listagem 10-32

Agora, vamos fazer uma consulta simples que lista o departamento, o cargo e o nome do funcionário.

SELECT de.Name,
e.JobTitle,
p.LastName + ', ' + p.Nome
DE Recursos Humanos.Departamento AS de
JOIN HumanResources.EmployeeDepartmentHistory AS edh ON
de.DepartmentID = edh.DepartmentID
JUNTE -SE A HumanResources.Funcionário AS e
ON edh.BusinessEntityID = e.BusinessEntityID
JUNTE -SE Pessoa.Pessoa AS p
ON e.BusinessEntityID = p.BusinessEntityID WHERE de.Nome
LIKE 'P%';

Listagem 10-33

Obtemos um plano de execução razoavelmente simples, conforme mostrado na Figura 10-29.

Figura 10-29: Plano de execução usando índices escolhidos pelo otimizador.

339
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Vemos uma série de operadores Index Seek e Clustered Index Seek , unidos por operadores de
loops aninhados . Suponha que estamos convencidos de que podemos obter um melhor
desempenho se pudermos eliminar o Index Seek na tabela HumanResources.Department e, em vez
disso, usar o índice clusterizado dessa tabela, PK_Department_DepartmentID. Podemos fazer isso
usando a dica INDEX(), conforme mostrado na Listagem 10-34.

SELECT de.Nome,
e.JobTitle,
p.Sobrenome + ', ' + p.Nome
FROM HumanResources.Department AS de WITH (INDEX(PK_Department_
DepartamentoID))
JOIN HumanResources.EmployeeDepartmentHistory AS edh
IS de.DepartmentID = edh.DepartmentID
JUNTE -SE A HumanResources.Funcionário AS e
ON edh.BusinessEntityID = e.BusinessEntityID
JUNTE -SE Pessoa.Pessoa AS p
ON e.BusinessEntityID = p.BusinessEntityID
WHERE de.Name LIKE 'P%';

Listagem 10-34

A Figura 10-30 mostra o plano de execução resultante.

Figura 10-30: Um plano de execução com opções de índice forçadas.

Depois que a dica é adicionada, podemos ver um Clustered Index Scan de um índice substituindo o
Index Seek do outro índice, assim como dissemos ao otimizador para fazer, embora não tenhamos
especificado busca ou varredura, por meio do uso do dica de mesa. Essa alteração resulta em uma
pequena melhoria no desempenho da consulta, com o tempo de execução chegando a 103ms em
oposição a 217ms sem a dica. Curiosamente, o número de leituras para a consulta geral permaneceu
consistente em 1042, independentemente do índice usado.

340
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

FORCESEK/FORCESCAN
Como vimos ao longo deste capítulo, é possível fazer algumas escolhas para o otimizador, o que pode
prejudicar ou melhorar o desempenho. Uma área com a qual muitas pessoas se preocupam é o uso de
índices. Ver uma varredura de índice leva muitas pessoas a querer forçar uma busca de índice em seu
lugar, trabalhando com a suposição de que as buscas são sempre melhores que as varreduras.
No entanto, isso nem sempre é o caso.

No entanto, podemos usar as dicas de tabela FORCESEEK ou FORCESCAN para forçar o tipo de operador
especificado, sem forçar o índice usado. É como o inverso de uma dica de índice, que força o índice, mas
permite que o otimizador escolha entre varredura ou busca.

Vamos pegar a consulta na Listagem 10-35 como exemplo.

SELECT p.Name AS ComponentName,


p2.Name AS AssemblyName,
bom.InícioData,
bom.EndDate
DE Production.BillOfMaterials AS bom
JUNTE -SE à Produção.Produto AS p
ON p.ProductID = bom.ComponentID
JUNTE -SE à Produção.Produto AS p2
ON p2.ProductID = bom.ProductAssemblyID;

Listagem 10-35

Como você provavelmente pode adivinhar olhando para a consulta, sem uma cláusula WHERE para fornecer
qualquer tipo de filtragem, varreduras foram usadas para recuperar os dados das tabelas em questão. Você
pode ver isso no plano de execução mostrado na Figura 10-31.

Figura 10-31: Um plano de execução usando varreduras devido à falta de uma cláusula WHERE.

341
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

Este é um comportamento completamente normal, considerando a consulta em questão. No entanto,


as pessoas realmente gostam de ver essas buscas. Observando o plano, você pode ver que o custo
estimado mais alto de qualquer uma das operações de índice é a verificação na tabela BillOfMaterials.
Vamos ver se forçar uma operação de busca melhorará o desempenho.

SELECT p.Name AS ComponentName,


p2.Name AS AssemblyName,
bom.InícioData,
bom.EndDate
FROM Production.BillOfMaterials AS nascido COM (FORCESEEK)
JUNTE -SE à Produção.Produto AS p
ON p.ProductID = bom.ComponentID
JUNTE -SE à Produção.Produto AS p2
ON p2.ProductID = bom.ProductAssemblyID;

Listagem 10-36

Tirando as opções de varredura do otimizador, ele é forçado a usar uma operação de busca e isso também
força outras mudanças no plano de execução, como você pode ver na Figura 10-32.

Figura 10-32: Plano de execução forçando uma operação Seek através da dica de tabela.

A varredura da tabela BillOfMaterials foi substituída por uma busca. Além disso, o operador Hash Match
foi substituído por um Nested Loops. A questão não é quais mudanças ocorreram no plano, no entanto.
A questão é, o que aconteceu com o desempenho. O tempo de execução passou de cerca de 145ms em
média para cerca de 290ms. As leituras saltaram de 34 para 1160. Não só a consulta foi mais lenta por
causa da busca e da junção de loops, mas o número de leituras significa que haverá um aumento
acentuado na contenção de recursos em um sistema sob carga.

342
Machine Translated by Google

Capítulo 10: Controlando os planos de execução com dicas

O operador FORCESCAN pode ser usado para fazer o caminho inverso, mudando uma busca para uma
varredura. Qualquer uma dessas dicas de tabela pode ser útil, dependendo das circunstâncias. No entanto,
você deve ter extremo cuidado no uso de todas as dicas de tabela, consulta e junção.

Resumo
Embora o otimizador tome decisões muito boas na maioria das vezes, às vezes ele pode fazer escolhas
menos do que ideais. Assumir o controle das consultas usando dicas de tabela, junção e consulta, quando
apropriado, geralmente pode ser a escolha certa. No entanto, lembre-se de que os dados em seu banco
de dados estão mudando constantemente. Quaisquer escolhas que você forçar no otimizador por meio de
dicas hoje, para alcançar qualquer melhoria que você espera, podem se tornar uma grande dor no futuro.

Se você decidir usar dicas, teste-as antes de aplicá-las e lembre-se de documentar seu uso de alguma
maneira para poder voltar e testá-las periodicamente à medida que seu banco de dados cresce e muda. À
medida que a Microsoft lança patches e service packs, o comportamento do otimizador pode mudar.
Certifique-se de testar novamente todas as consultas usando dicas após uma atualização para seu servidor.
Demonstrei intencionalmente casos em que as dicas de consulta prejudicam e ajudam, pois isso simplesmente
reflete a realidade. As dicas prejudicam mais o desempenho do que o ajudam. O uso dessas dicas deve ser
um último recurso, não um método padrão de operação.

343
Machine Translated by Google

Capítulo 11: Paralelismo em Execução


Planos
O SQL Server pode tirar proveito dos vários processadores de um servidor, distribuindo o processamento de
determinadas operações pelas CPUs disponíveis para ele. Em primeiro lugar, muitas pequenas consultas podem ser
executadas ao mesmo tempo, cada uma em seu próprio encadeamento. Essas consultas terão apenas planos de execução normais.
Em segundo lugar, uma única consulta pode ser executada em vários threads. Este último caso, a execução
paralela de uma única consulta, resultará em um plano de execução diferente, e essas diferenças são nosso foco neste
capítulo.

Essencialmente, quando o otimizador detecta que seu custo estimado para um plano excede o "limite de custo",
além do qual a paralelização da consulta beneficiará o desempenho, ele produz uma versão paralela do plano. O
trabalho realizado por qualquer operador "paralelizado" no plano paralelo pode ser distribuído em várias CPUs, com
o objetivo de que, dividindo o trabalho em partes menores, a operação geral seja mais rápida.

Para consultas em grande escala e para consultas usando índices columnstore, o paralelismo de consulta é
extremamente desejável para o desempenho. Para consultas menores no estilo OLTP, pode causar mais
problemas do que soluções. Ao entender como ler planos paralelizados, você começará a entender como isso
afeta o custo geral do plano, quais operadoras se beneficiam mais e onde a sobrecarga adicional do paralelismo
pode entrar em jogo.

Este capítulo se concentra nos detalhes da execução paralela de um único plano e apenas nos planos que usam o
modelo tradicional de execução em modo de linha, em que os operadores passam os dados linha por linha. Conforme
mencionado brevemente no Capítulo 8, os índices columnstore oferecem suporte a um novo tipo de modelo de execução
de consulta, chamado modo de lote, em que os operadores passam lotes de linhas em vez de linhas únicas. O Capítulo
12 abordará o modo de lote em detalhes, incluindo planos de execução paralela que usam índices columnstore.

Controlando a execução de consultas paralelas


O SQL Server tem duas opções de configuração em toda a instância que determinam se, ou quando, o otimizador
pode gerar planos de execução paralela e também controlar a execução paralela de consultas pelo mecanismo. O
grau máximo de paralelismo (às vezes uso MAXDOP
para brevidade) determina o número máximo de processadores que o SQL Server

344
Machine Translated by Google

Capítulo 11: Paralelismo em Planos de Execução

mecanismo de execução pode usar ao executar uma consulta paralela e o limite de custo para
configuração de paralelismo, que especifica o limite, ou custo mínimo, no qual o SQL Server cria e
executa planos paralelos; o custo a ser medido neste caso é o custo estimado do plano de execução.

Obviamente, a execução de consultas paralelas exige que o SQL Server tenha acesso a mais de
um processador. No momento da compilação, se o otimizador determinar que apenas um processador
está disponível ou que MAXDOP está definido como 1, ele não produzirá planos paralelos. Caso contrário,
o otimizador selecionará um plano da maneira usual e, se o custo estimado desse plano exceder o limite de
custo para o valor de paralelismo, ele produzirá uma versão paralela do plano.

Em tempo de execução, o mecanismo de execução determina em quantos processadores paralelizar a


consulta, até o valor máximo definido pela configuração MAXDOP no nível da instância ou pelo uso da dica
de consulta MAXDOP (consulte o Capítulo 10). Além disso, o mecanismo deve verificar com o SO para
determinar se threads suficientes (uma construção do sistema operacional que permite várias operações
simultâneas) estão disponíveis para uso, antes de iniciar um processo paralelo. Os planos elegíveis para
paralelismo podem não ser paralelos. Se o mecanismo de execução decidir que, mesmo que um plano se
qualifique para execução paralela, não há recursos suficientes para suportá-lo, ele simplesmente eliminará o
paralelismo e executará uma versão serial do plano (o Repositório de consultas é o único local em que você
verá ambas as versões do plano).

Grau máximo de paralelismo


Por padrão, MAXDOP é definido como 0 (zero), o que significa que o SQL Server pode usar todos os
processadores disponíveis para executar uma consulta. Se desejar suprimir a execução paralela, defina
esta opção com um valor de 1. Se desejar especificar o número de processadores a serem usados para
uma execução de consulta, defina um valor maior que 1 e até 64.

Sem uma medição completa e provas testadas de que o paralelismo de consulta sempre causará
problemas, recomendo deixar o paralelismo ativado para a maioria dos sistemas. No entanto, também
recomendo que você não deixe MAXDOP definido como zero. Em vez disso, você desejará defini-lo como
um valor maior que 1, mas menor que o número total de processadores disponíveis, para evitar que uma
consulta cara e paralelizada bloqueie outras consultas, "embaraçando" todos os processadores disponíveis.

Uma recomendação muito geral é definir esse valor para metade do número de núcleos físicos em
sua máquina, mas isso não abrange todas as sutilezas e nuances deste tópico.
Determinar uma configuração precisa para MAXDOP requer conhecimento preciso de seu sistema
operacional, seu hardware, se seu sistema é virtualizado e o tipo de carga de trabalho que

345
Machine Translated by Google

Capítulo 11: Paralelismo em Planos de Execução

seu sistema funciona. A Microsoft oferece algumas recomendações sobre como determinar a
configuração correta de MAXDOP para seu sistema: https://bit.ly/2uwvUeI. Paul Randal e a equipe
SQLskills também fornecem algumas recomendações muito detalhadas e perfuram mitos comuns sobre
o tema: https://bit.ly/2GwQ9Pu. Entre esses dois recursos, você deve ser capaz de determinar a resposta
certa para o seu sistema.

Você pode consultar a configuração atual e determinar a configuração dessa opção por meio dos
scripts a seguir mostrados na Listagem 11-1.

EXEC sys.sp_configure @configname = 'mostrar opções avançadas',


@configvalue = 1;
VAI
RECONFIGURAR COM OVERRIDE;
VAI
--mostra o valor atual
EXEC sys.sp_configure @configname = 'grau máximo de paralelismo'
--alterar valor
EXEC sys.sp_configure @configname = 'grau máximo de paralelismo',
@configvalue = 4;
VAI
RECONFIGURAR COM OVERRIDE;
VAI
EXEC sys.sp_configure @configname = 'mostrar opções avançadas',
@configvalue = 0;
VAI
RECONFIGURAR COM OVERRIDE;
VAI

Listagem 11-1

A primeira instrução ativa as opções avançadas, necessárias para acessar o grau de


paralelismo. O sistema é então reconfigurado, necessário para realmente ativar a nova configuração.
Em seguida, consultamos a configuração passando o primeiro valor do parâmetro e não o segundo
para o procedimento do sistema, sys.sp_configure.

Figura 11-1: O grau máximo de paralelismo definido para o valor padrão de 0.

346
Machine Translated by Google

Capítulo 11: Paralelismo em Planos de Execução

O run_value mostra a configuração atual que, neste caso, é o valor padrão de 0. Para alterar o valor
chamamos sys.sp_configure, passando dois valores, a configuração que desejamos alterar, grau
máximo de paralelismo e o valor que desejamos para alterá-lo para, 4. O script redefine a exibição de
opções avançadas.

Se fôssemos consultar a configuração novamente após executar o script, veríamos o run_value


mudou para 4.

Limite de custo para paralelismo


O otimizador atribui custos estimados aos operadores dentro do plano de execução. Esses custos,
em um ponto no passado, representavam uma estimativa do número de segundos que cada operação
levaria. Hoje, simplesmente pense no custo como apenas isso, unidades de custo estimado. Os valores
acumulados de cada um dos custos atribuídos às operadoras é o custo estimado do próprio plano.
Se esse custo estimado for maior que o limite de custo para paralelismo, essa operação poderá ser
executada como uma operação paralela.

O valor padrão para o limite de custo para paralelismo é 5. Esse provavelmente era um bom valor padrão
em 1998, quando foi estabelecido pela primeira vez para o SQL Server 7. O número e a potência dos
processadores e o tipo de processadores mudaram radicalmente desde então, e aconselho vivamente a
alterar esse valor para algo muito superior. Minha recomendação aproximada seria 25 ou mais para um
sistema de relatórios ou data warehouse e 50 para um sistema OLTP. Faço essas escolhas porque, em
geral, é mais provável que você veja movimentação de dados em grande escala em sistemas de relatórios,
onde um plano paralelo tem mais probabilidade de beneficiar o processamento de consultas. Um sistema
OLTP geralmente lida apenas com conjuntos de dados menores e, portanto, deve usar seus processadores
para muitas consultas, não para uma única consulta.

Independentemente disso, você não deve deixar o limite de custo para paralelismo no valor padrão, e a
Listagem 11-2 mostra como alterá-lo, usando a mesma função sys.sp_configure como anteriormente.

EXEC sys.sp_configure @configname = 'mostrar opções avançadas',


@configvalue = 1;
VAI
RECONFIGURAR COM OVERRIDE;
VAI
EXEC sys.sp_configure @configname = 'limite de custo para paralelismo',

@configvalue = 50;
VAI

347
Machine Translated by Google

Capítulo 11: Paralelismo em Planos de Execução

RECONFIGURAR COM OVERRIDE;


VAI
EXEC sys.sp_configure @configname = 'mostrar opções avançadas', @configvalue = 0;

VAI
RECONFIGURAR COM OVERRIDE;
VAI

Listagem 11-2

Bloqueadores de execução de consultas paralelas

Algumas instruções de código podem forçar todo o plano a ser serial, independentemente de suas configurações
para seu MAXDOP ou limite de custo:

• Funções escalares usando T-SQL •


CLR multi-instrução, com valor de tabela ou funções definidas pelo usuário que acessam dados •

Algumas funções internas no SQL Server, como ERROR_NUMBER(),


IDENT_CURRENT(), @@TRANCOUNT e outros
• Acessando tabelas do sistema •
Cursores dinâmicos.

Existem também algumas funções e objetos T-SQL que levam a partes de um plano em execução no modo
serial (esta lista pode variar dependendo da versão do SQL Server):
• CTEs recursivos
• TOPO

• Funções de paginação como ROW_NUMBER •


Varreduras para trás

• Funções multi-instrução, com valor de tabela e definidas pelo usuário.


• Agregados escalares globais.

As partes de qualquer instrução T-SQL usando esses objetos e funções impedirão a execução paralela dentro
do plano para as partes do plano que satisfaçam essas funções.

348
Machine Translated by Google

Capítulo 11: Paralelismo em Planos de Execução

Execução de consulta paralela

Quando o otimizador determina que uma consulta pode se beneficiar da paralelização, ele cria uma
versão do plano otimizada para execução paralela. Neste plano paralelo, você verá todos os operadores
familiares que viu anteriormente no livro, exceto com o ícone amarelo de "seta dupla", indicando que o
trabalho realizado pelo operador será dividido entre os processadores. Com efeito, esses operadores
fazem o mesmo trabalho que em um plano serial, mas com menos dados. Você também verá operadores
extras, que tratam da distribuição de dados entre os encadeamentos. Nos planos, eles são chamados de
operadores de Paralelismo , mas geralmente são chamados de operadores do Exchange . Eles fazem o
trabalho de "empacotamento" de particionar a carga de trabalho em vários fluxos de dados, passando-os
por vários operadores paralelos e reunindo todos os fluxos novamente. Você pode ver um exemplo disso na
Figura 11-2.

Figura 11-2: Exemplos de operadores paralelos em um plano de execução.

A maioria dos operadores não está ciente do paralelismo; eles apenas fazem seu trabalho normal em
quaisquer dados que obtenham; a única diferença é que eles processarão apenas uma parte das linhas,
em vez de todas elas, como fariam em um plano serial. Na verdade, varreduras e buscas, quando usadas
para retornar intervalos de linhas consecutivas, são os únicos operadores que alteram seu comportamento
entre planos paralelos e seriais, e discutiremos isso com mais detalhes em breve.

349
Machine Translated by Google

Capítulo 11: Paralelismo em Planos de Execução

Examinando um plano de execução paralela

Começaremos com uma consulta de agregação, do tipo que você pode encontrar em um data warehouse. Se o
conjunto de dados em que esta consulta opera for muito grande, ele poderá se beneficiar do paralelismo.

SELECT assim.ProductID,
COUNT(*) AS Order_Count
DE Sales.SalesOrderDetail so
ONDE so.ModifiedDate >= '20140301'
AND so.ModifiedDate < DATEADD(mm, 3, '20140301')
GROUP BY so.ProductID
ORDER BY so.ProductID;

Listagem 11-3

A Figura 11-3 mostra o plano de execução estimado, que parece direto.

Figura 11-3: Um plano que está sendo executado em série.

Não há nada neste plano que não tenhamos visto antes. Um ponto interessante é que o otimizador decidiu
usar o operador Hash Match , e então Ordenar os dados agregados, ao invés da alternativa, que seria Ordenar
os dados emergentes da varredura em SalesOr derDetail por ProductID e então usar o operador Stream
Aggregate . A razão é que o custo extra de classificar cerca de 24 mil linhas no último caso, em vez de 178 no
primeiro, superou qualquer economia de usar o operador de agregação mais barato.

Vamos ver o que acontece se o otimizador decidir produzir uma versão paralelizada de seu plano. Neste exemplo
simples, o custo total do plano é de apenas 1,3 (você pode ver isso na propriedade Estimated Subtree Cost do
operador SELECT , portanto, precisarei reduzir artificialmente o limite de custo para paralelismo para 1.

EXEC sys.sp_configure @configname = 'mostrar opções avançadas',


@configvalue = 1;
VAI
RECONFIGURAR COM OVERRIDE;
VAI

350
Machine Translated by Google

Capítulo 11: Paralelismo em Planos de Execução

EXEC sys.sp_configure @configname = 'limite de custo para paralelismo',

@configvalue = 1;

VAI RECONFIGURAR COM OVERRIDE;


GO SET STATISTICS XML ON; SELECT
assim.ProductID,

COUNT(*) AS Order_Count FROM


Sales.SalesOrderDetail AS so WHERE
so.ModifiedDate >= '3 de março de 2014'
E então.ModifiedDate < DATEADD(mm,
3,
'1 de março de 2014')
GROUP BY so.ProductID ORDER
BY so.ProductID; SET STATISTICS
XML OFF; VAI

EXEC sys.sp_configure @configname = 'limite de custo para paralelismo',

@configvalue = 5; --seu valor vai aqui


VAI
RECONFIGURAR COM OVERRIDE;
VAI
EXEC sys.sp_configure @configname = 'mostrar opções avançadas', @configvalue = 0;

VAI
RECONFIGURAR COM OVERRIDE;
VAI

Listagem 11-4

A Figura 11-4 mostra o plano de execução.

Figura 11-4: Um plano que foi para execução paralela.

Vamos começar à esquerda, com o operador SELECT . Se você olhar para sua folha de Propriedades ,
você pode ver a propriedade Grau de Paralelismo , que neste caso é 4, indicando que a execução desta
consulta foi dividida entre cada um dos quatro processadores disponíveis. Se houvesse carga excessiva
no sistema no momento da execução, o plano poderia não ter sido paralelo ou,

351
Machine Translated by Google

Capítulo 11: Paralelismo em Planos de Execução

pode ter usado menos processadores. A propriedade Grau de Paralelismo é uma métrica de desempenho,
capturada em tempo de execução e exibida com um plano real e, portanto, refletirá com precisão o
paralelismo usado em tempo de execução.

Figura 11-5: Propriedades do operador SELECT mostrando o Grau de Paralelismo.

Observando o plano de execução gráfica, começaremos da direita e seguiremos o fluxo de


dados. Primeiro, encontramos um operador Clustered Index Scan . A Figura 11-6 mostra parte
de sua folha de Propriedades .

Figura 11-6: Propriedades do Clustered Index Scan mostrando artefatos paralelos.

352
Machine Translated by Google

Capítulo 11: Paralelismo em Planos de Execução

A propriedade Parallel é definida como True. Mais interessante é que o Número de Execuções
valor indica que este operador foi chamado 4 vezes, uma vez para cada thread. No topo da planilha, você
pode ver que 23883 linhas corresponderam ao nosso predicado em ModifiedDate, e podemos ver como
essas linhas foram distribuídas em quatro threads, no meu caso de maneira bastante desigual.

Scans e buscas estão entre os poucos operadores que mudam seu comportamento entre planos paralelos
e seriais. Nos planos paralelos, as linhas são fornecidas para cada thread de trabalho usando um sistema
baseado em demanda, onde o operador solicita linhas de um recurso do Storage Engine chamado Parallel
Page Supplier, que responde a cada solicitação fornecendo um lote de linhas para qualquer thread que
solicite mais trabalho (esse recurso não faz parte do processador de consultas, portanto não aparece no
plano).

Os dados passam para um operador Hash Match , que está realizando uma contagem agregada para
cada valor ProductID, conforme definido pela cláusula GROUP BY dentro do T-SQL, mas apenas para
cada linha em seu encadeamento (o Hash Match não reconhece o paralelismo ). O resultado será uma
linha para cada valor ProductID que aparece em um encadeamento (mais sua contagem associada). É
provável que haja outras linhas para o mesmo ProductID nas outras threads, então as agregações
resultantes não são os valores finais, razão pela qual, nos planos de execução mostrados na Figura
11-4, a operação lógica realizada pelo Hash A correspondência é listada como um portão Agregado
Parcial, embora em todos os outros aspectos o operador funcione da mesma maneira que uma
Correspondência de Hash (Agregado).

Se você inspecionar o operador Properties of the Hash Match (Partial Aggregate) (Figura 11-7), verá
que ele foi chamado 4 vezes e, novamente, verá a distribuição das linhas parcialmente agregadas entre
os encadeamentos.

Lembre-se de que você pode, e provavelmente verá, diferentes contagens de linhas neste estágio do
plano, dependendo do grau de paralelismo que você vê em seus testes e de como as linhas são
distribuídas entre esses encadeamentos. Existem 178 valores de ProductID distintos nos dados
selecionados. Se todas as linhas de cada ProductID terminassem no mesmo encadeamento, você veria o
total mínimo teórico de 178 linhas, porque a agregação parcial já seria a agregação final. O número
máximo teórico de linhas ocorre quando cada valor ProductID ocorre em cada encadeamento. Se houver
4 threads, como no meu caso, o máximo teórico é 4*178 = 712 linhas. Eu vejo 470 linhas, bem entre o
mínimo e o máximo teóricos.

353
Machine Translated by Google

Capítulo 11: Paralelismo em Planos de Execução

Figura 11-7: Propriedades do Hash Match mostrando artefatos paralelos.

As linhas passam para um operador de Paralelismo (geralmente chamado, lembre-se, de operador


Exchange), que implementa a operação Repartição de Fluxos . Você pode pensar nesse operador,
geralmente, como responsável por rotear linhas para o thread certo. Às vezes, isso é feito apenas para
equilibrar os fluxos, tentando garantir que uma quantidade aproximadamente igual de trabalho seja
executada por cada fluxo. Outras vezes, sua principal função é garantir que todas as linhas que precisam
ser processadas por uma única instância de um operador estejam no mesmo thread. Este é um exemplo
deste último; o operador é usado para garantir que as colunas com ProductID correspondentes
os valores estão todos no mesmo encadeamento, para que a agregação global final possa ser executada.
Podemos ver que nas propriedades o tipo de particionamento é Hash (há outros tipos de particionamento
também, como Round Robin e Broadcast) e a coluna de partição é ProductID.

354
Machine Translated by Google

Capítulo 11: Paralelismo em Planos de Execução

Figura 11-8: Propriedades da Coluna de Partição da operação Repartição de Fluxos.

A Figura 11-9 mostra os resultados desse "reencaminhamento" nas propriedades do operador.

Figura 11-9: Linhas reorganizadas dentro das roscas.

355
Machine Translated by Google

Capítulo 11: Paralelismo em Planos de Execução

Você pode ver a propriedade Número real de linhas e como os encadeamentos foram reorganizados
com uma distribuição aproximadamente uniforme de linhas. Uma distribuição mais uniforme de dados
entre os encadeamentos foi um efeito colateral feliz neste caso. No entanto, se os valores ProductID não
tivessem sido distribuídos igualmente no algoritmo de hash usado, isso poderia facilmente ter adicionado
mais distorção.

Conceitualmente, você pode imaginar o plano, até este ponto, parecido com a Figura 11-10. Novamente, a
contagem exata de linhas será diferente para você, mas demonstra a execução da consulta em vários threads
e a distribuição e, em seguida, o reparticionamento das linhas nesses threads.

Figura 11-10: Um exemplo imaginário do que está acontecendo na execução paralela.

Após a agregação parcial e o reparticionamento, todas as linhas de um determinado valor ProductID


estarão no mesmo thread, o que significa que cada um dos quatro threads terá até quatro linhas por
ProductID. As linhas precisam ser agregadas novamente para concluir a "agregação local-global".

Agora que o número de linhas foi reduzido substancialmente pela agregação parcial, o otimizador estima
que é mais barato classificar os dados na ordem correta para que um operador Stream Aggregate
possa fazer a agregação final, em vez de usar outra Hash Match. Um operador Sort é aquele que se
beneficia muito da paralelização e geralmente mostra uma redução significativa no custo total, em
comparação com o Sort serial equivalente.

356
Machine Translated by Google

Capítulo 11: Paralelismo em Planos de Execução

O próximo operador é outro operador de Paralelismo , realizando a operação Gather Streams . A função desse
operador é um pouco autoexplicativa, pois reúne os fluxos novamente, para apresentar os dados como um único conjunto
de dados para a consulta ou operador que o chama. A saída desse operador agora é um único thread de dados. A
propriedade muito importante que chamarei aqui é a propriedade Order By , conforme mostrado na Figura 11-11.

Figura 11-11: Propriedades do operador Paralelismo mostrando a propriedade Order By.

No operador de Paralelismo anterior (Repartition Streams), essa propriedade estava ausente, significando que
ela apenas lia cada um dos threads de entrada e enviava pacotes de linhas para cada thread de saída assim que
pudesse. Não é garantido que a ordem de entrada dos dados seja preservada.

No entanto, se a ordem for preservada, você verá um comportamento diferente. Se os dados em cada thread já
estiverem na ordem correta, um operador de troca que preserva a ordem aguardará que os dados estejam disponíveis
em todas as entradas e os mesclará em um único fluxo que ainda esteja na ordem correta. Isso significa que um
Exchange que preserva a ordem pode ser um pouco mais lento do que um que não preserva a ordem. No entanto,
como a ordenação paralelizada é tão eficiente, o otimizador geralmente favorece um plano com uma ordenação
paralela e um Paralelismo que preserva a ordem
operador, sobre um plano com um operador de Paralelismo que não preserva a ordem e uma classificação serial de
todos os dados.

Deste ponto em diante, o plano é apenas um plano normal, "serial", trabalhando em um único thread de dados, que passa
ao lado do operador Compute Scalar , que converte a coluna agregada em um int. Isso implica que internamente,
durante as fases de agregação do plano, esse valor era grande, mas não está claro. Por fim, os dados são retornados
por meio do operador SELECT .

357
Machine Translated by Google

Capítulo 11: Paralelismo em Planos de Execução

Os planos paralelos são bons ou ruins?


O paralelismo tem um custo. É preciso tempo e poder de processamento para dividir uma operação em vários segmentos,
coordenar a execução de cada um desses segmentos e, em seguida, reunir todos os dados novamente. Se apenas
algumas linhas estiverem envolvidas, esse custo superará em muito os benefícios de colocar várias CPUs para trabalhar na
consulta.

No entanto, dada a rapidez com que os custos do operador aumentam e o custo de determinados operadores em
particular (como Sorts), com o número de linhas a serem processadas, é provável que o paralelismo faça muito sentido
para qualquer operação de processamento intensivo de longa duração , consultas de grande volume, incluindo a maioria
das consultas que usam índices columnstore. Você verá esse tipo de atividade principalmente em sistemas de relatórios,
warehouse ou business intelligence.

Em um sistema OLTP, onde a maioria das transações são pequenas e rápidas, o paralelismo às vezes pode fazer com
que uma consulta seja executada mais lentamente do que seria executada com um plano serial. Algumas vezes, isso
pode fazer com que a consulta paralelizada seja executada um pouco mais rápido, mas os recursos extras usados fazem com
que as consultas em todas as outras conexões sejam executadas mais lentamente, reduzindo o desempenho geral do sistema.
Na maioria das vezes, o otimizador faz um bom trabalho ao evitar essas situações, mas às vezes pode fazer escolhas ruins.
No entanto, mesmo em sistemas OLTP, alguns planos, como para consultas de relatórios, ainda se beneficiarão do
paralelismo. O fator de condução geral aqui são os custos estimados desses planos, e é por isso que definir o limite de custo
para o paralelismo
configuração torna-se tão importante.

Não existe uma regra rígida para determinar quando o paralelismo pode ser útil ou quando será mais caro. A melhor
abordagem é observar os tempos de execução e os estados de espera das consultas que usam paralelismo, bem
como a carga de trabalho geral, usando métricas como "solicitações por segundo". Se o sistema lidar com um nível
especialmente alto de solicitações simultâneas, permitir que a consulta de um usuário paralelize e ocupe todas as CPUs
disponíveis provavelmente causará problemas de bloqueio. Quando necessário, altere as configurações do sistema para
aumentar o limite de custo e o MAXDOP ou use a dica de consulta MAXDOP em casos individuais.

Tudo se resume a testes para ver se você está se beneficiando dos processos paralelos, e os tempos de execução das
consultas geralmente são o indicador mais seguro disso. Se o tempo diminuir com MAXDOP definido como 1 durante um
teste, isso é uma indicação de que o plano paralelo está prejudicando você, mas isso não significa que você deva desabilitar
o paralelismo completamente. Você precisa passar pelo processo de escolha das configurações apropriadas para o Grau
Máximo de Paralelismo e Custo
Limiar para Paralelismo e, em seguida, medir o desempenho e os comportamentos do sistema com a execução
de planos paralelos.

358
Machine Translated by Google

Capítulo 11: Paralelismo em Planos de Execução

Resumo
O capítulo explicou o básico de como você pode ler um plano de execução paralela.
O paralelismo não altera fundamentalmente o que você faz ao ler os planos de execução, apenas
requer conhecimento e compreensão adicionais de alguns novos operadores de paralelismo e o impacto
potencial em outros operadores no plano, para que você possa começar a ver quais tipos de consultas
realmente benefício e identificar os casos em que a sobrecarga adicional do paralelismo se torna
significativa.

A execução paralela de consultas pode melhorar o desempenho. Também pode prejudicar o desempenho.
Você precisa garantir que você configurou seu sistema corretamente, tanto o Grau Máximo de
Paralelismo e o limite de custo para o paralelismo. Com esses valores definidos corretamente, você
deve se beneficiar muito limitando a execução de consultas paralelas àquelas que realmente precisam.

359
Machine Translated by Google

Capítulo 12: Processamento em modo de lote


Introduzido com o índice columnstore em 2012, o processamento em modo de lote é uma nova maneira de o
mecanismo de consulta processar consultas, permitindo que ele passe lotes de linhas entre operadores, em vez
de linhas individuais, o que pode melhorar radicalmente o desempenho em algumas situações.

Para muitas consultas que usam índices columnstore, a execução paralela é desejável para o desempenho.
Como resultado, o processamento em lote tende a ser discutido em conjunto com o processamento paralelo,
mas, na verdade, a execução paralela não é necessária para todos os tipos de processamento em modo
lote, e o modo em lote está disponível em execução não paralela no SQL Server 2016 e posterior, como bem
como no Banco de Dados SQL do Azure.

Em outras partes do livro, como quando discutimos Adaptive Joins no Capítulo 4, você viu algumas evidências
do processamento de modo de linha ou lote nas propriedades dos operadores, mas aqui vamos discutir em
detalhes o que é e como funciona, as características dos planos de execução para consultas que utilizam o
modo batch e, por fim, algumas de suas limitações.

No momento da escrita, apenas tabelas com índices columnstore suportam esse novo modelo de execução
em modo batch e, portanto, este capítulo discutirá apenas os planos de execução para consultas que acessam
tabelas com um índice columnstore e executam em modo batch. No entanto, a Microsoft anunciou recentemente
que uma próxima versão do SQL Server também introduzirá o modo em lote para consultas de armazenamento
de linhas, portanto, esse tipo de processamento será expandido.

Processamento em modo de lote definido


O mecanismo de processamento tradicional, modo de linha, foi descrito ao longo do livro.
Um operador solicitará linhas do operador anterior, processará cada linha que receber e, em seguida, passará
essa linha para o próximo operador enquanto solicita linhas (ou, no caso de um operador de bloqueio, solicita
todas as linhas de entrada uma a uma e em seguida, retorne todas as linhas de resultado uma a uma). Essa
negociação de solicitação constante é uma operação cara no SQL Server. Ele pode, e faz, desacelerar as coisas,
especialmente quando começamos a lidar com conjuntos de dados muito grandes.

O processamento em lote reduz a frequência do processo de negociação, aumentando assim o desempenho.


Em vez de passar as linhas individuais, os operadores passam as linhas em lotes, geralmente lotes de 900
linhas, e então apenas os lotes são negociados. Portanto, se assumirmos 9.000 linhas movendo-se entre
operadores, em vez de 9.000 negociações para mover as linhas, você verá 10 negociações (9.000 linhas / 900 =
10 lotes), reduzindo radicalmente a sobrecarga de processamento dos dados.

360
Machine Translated by Google

Capítulo 12: Processamento em modo de lote

O tamanho do modo de lote nem sempre será de 900 linhas; esse é o valor fornecido pela Microsoft como
orientação. No entanto, pode variar, pois depende muito do número e do tamanho das colunas que passam
pela consulta. Você verá alguns exemplos em que o tamanho do lote é menor que 900, mas ainda não vi um
caso em que haja mais de 900 linhas em um lote, embora não tenha visto evidências de que 900 seja um
máximo difícil e você pode ver comportamentos diferentes, dependendo da versão do SQL Server.

Planejar consultas que são executadas no modo de lote


Conforme discutido no Capítulo 8, os índices columnstore são projetados para melhorar as cargas de
trabalho que envolvem uma combinação de tabelas muito grandes (milhões de linhas) e consultas de análise,
relatórios e agregação que operam em todas as linhas ou em grandes seleções. É para esses tipos de
consultas que a execução em modo de lote pode realmente melhorar o desempenho, em vez de consultas OLTP
típicas que processam linhas únicas ou pequenas coleções de linhas.

Vamos nos concentrar em como o processamento de modo de linha e lote aparece nos planos de execução e
como você pode determinar qual modo de processamento você está vendo, novamente com base nas informações
fornecidas por meio do plano de execução.

Para começar com o modo de lote e demonstrar as alterações resultantes no comportamento nos planos de
execução, precisaremos criar um índice columnstore em uma tabela bem grande. Felizmente, Adam Machanic
postou um script que pode criar algumas tabelas grandes no Adventure Works apenas para esse tipo de teste.
Você pode baixar o script em http://bit.ly/2mNBIhg.

Com as tabelas maiores no lugar, a Listagem 12-1 cria um índice columnstore não clusterizado na tabela
bigTransactionHistory.

CRIAR ÍNDICE DE COLUMNSTORE NÃO CLUSTERED TransactionHistoryCS


ON dbo.bigTransactionHistory
(
ID do produto,
Data da transação,
Quantidade,
Custo real,
ID da transação
);

Listagem 12-1

361
Machine Translated by Google

Capítulo 12: Processamento em modo de lote

Começaremos com uma consulta simples que agrupa as informações para análise, conforme mostrado na
Listagem 12-2.

SELECT th.ProductID,
AVG(th.Custo Real),
MAX(th.Custo Real),
MIN(th.Custo Real)
DE dbo.bigTransactionHistory AS th
GROUP BY th.ProductID;

Listagem 12-2

A Figura 12-1 mostra o plano de execução real.

Figura 12-1: O operador Columnstore Index Scan.

No meu sistema, o nível de compatibilidade do banco de dados é 140 e o limite de custo para
paralelismo é 50 (o custo estimado do plano serial é pouco menos de 25; veja a propriedade
Estimated Subtree Cost do operador SELECT ). Se sua configuração de nível de compatibilidade for
diferente ou sua configuração de limite de custo estiver abaixo de 25, você poderá ver uma versão
paralelizada do plano.

Seguindo o fluxo de dados da direita para a esquerda, o primeiro operador é o Columnstore Index
Scan (descrito no Capítulo 8). Não há nada na Figura 12-1 que indique visualmente se esse operador
está usando o modo de lote ou o modo de linha, mas a planilha Propriedades, mostrada na Figura 12-2,
revela as informações pertinentes.

362
Machine Translated by Google

Capítulo 12: Processamento em modo de lote

Figura 12-2: Modo de lote nas propriedades do operador Columnstore Index Scan.

Como você pode ver na Figura 12-2, há um modo de execução estimado e real que designará um
operador como executando em modo batch ou não. Este operador foi estimado para usar o modo de lote
e, em seguida, quando a consulta foi executada, o modo de lote foi usado. Antes do SQL Server 2016,
com um plano não paralelo como este, o modo de lote não estava disponível para planos seriais como
este (mais sobre isso em breve).

Esse operador escaneou toda a tabela (mais de 31 milhões de linhas) e retornou ao operador
Hash Match (Aggregate) 29.666.619 linhas, em 32.990 lotes. Os 1.596.982 restantes foram agregados
localmente (devido ao pushdown agregado, conforme descrito no Capítulo 8); os resultados dessa
agregação local foram injetados diretamente nos resultados do operador Hash Match (Aggregate) . A
agregação estava no ProductID. A Figura 12-3 mostra a dica de ferramenta para o Hash Match, que
também revelará se os operadores no plano usaram o processamento em modo de lote.

363
Machine Translated by Google

Capítulo 12: Processamento em modo de lote

Figura 12-3: Parte da dica de ferramenta do operador Hash Match.

O operador Hash Match também usou o processamento em modo batch. Recebeu quase 30 milhões de linhas em 32.990
lotes e, após a agregação, retornou 25.200 linhas em 28 lotes. Portanto, havia cerca de 899 linhas por lote entrando e 846
saindo, ambas próximas ao valor de 900 declarado anteriormente.

Modo em lote anterior ao SQL Server 2016


Se alterarmos o nível de compatibilidade do banco de dados do valor 140 do SQL Server 2017 para o valor 120 do SQL
Server 2014, isso poderá alterar o comportamento de nossas operações de modo de lote, porque menos operações
suportavam o modo de lote em versões anteriores do SQL Server .

ALTER DATABASE AdventureWorks2014


SET COMPATIBILITY_LEVEL = 120;

Listagem 12-3

Agora, quando executarmos novamente a consulta da Listagem 12-2, obteremos um plano de execução paralelizado,
conforme mostrado na Figura 12-5.

Figura 12-4: Um plano de execução paralela em relação a um índice columnstore.

364
Machine Translated by Google

Capítulo 12: Processamento em modo de lote

A principal diferença visível entre o plano da Figura 12-4 e o da Figura 12-1 é a adição do operador Parallelism
(Gather Streams) que puxa a execução paralela de volta para um único fluxo de dados.

Antes do SQL Server 2016, era muito comum que as consultas nunca entrassem no modo de lote, a
menos que fossem caras o suficiente para serem executadas em paralelo, porque era um requisito do
processamento do modo de lote que o plano fosse paralelo. Nesse caso, como o modo de lote não estava
disponível para o plano serial, o custo desse plano serial era alto o suficiente para que o limite de custo para
o paralelismo foi ultrapassado, e o plano foi paralelo. Se quiser verificar isso, você pode adicionar a dica OPTION
(MAXDOP 1) à Listagem 12.2 e capturar o plano real, e você verá que o custo do plano serial agora é de
aproximadamente 150. Você também notará que o otimizador não escolhe mais o índice columnstore, e isso
porque o custo estimado de usá-lo em um plano serial é ainda maior (aproximadamente 200). Você pode verificar
isso adicionando uma dica de índice (consulte o Capítulo 10) para forçar o uso do índice columnstore.

De maneira mais geral, dependendo da consulta, você também poderá ver custos diferentes em diferentes
versões do SQL Server e modos de compatibilidade, devido a alterações nas opções que o otimizador de
consulta pode usar e no mecanismo de estimativa de cardinalidade em uso.

No SQL Server 2012 e 2014 (e níveis de compatibilidade correspondentes), uma consulta abaixo do
limite para paralelismo nunca usaria o modo de lote. Nessas versões anteriores do SQL Server, se você
quisesse ver o modo de lote em suas consultas, precisaria diminuir o limite de custo para paralelismo. Se
isso não fosse viável, você seria forçado a modificar a consulta para adicionar um sinalizador de rastreamento
não documentado, 8649, que reduz artificialmente o limite de custo para paralelismo para zero, garantindo que
qualquer consulta seja executada em paralelo. A Listagem 12-4 mostra como usar a dica QUERYTRACEON
8649 para forçar a execução paralela.

SELECT th.ProductID,
AVG(th.Custo Real),
MAX(th.Custo Real),
MIN(th.Custo Real)
DE dbo.bigTransactionHistory AS th
Agrupar por th.ProductID
OPÇÃO(QUERYTRACEON 8649);

Listagem 12-4

Antes de continuarmos, vamos alterar o modo de compatibilidade de volta para 140.

365
Machine Translated by Google

Capítulo 12: Processamento em modo de lote

ALTER DATABASE AdventureWorks2014


SET COMPATIBILITY_LEVEL = 140;

Listagem 12-5

Misturando índices columnstore e rowstore


A seção anterior mostrou como os planos de execução expuseram o processamento em modo de
lote. Sua próxima pergunta pode ser: o que acontece quando você mistura índices columnstore e rowstore
na mesma consulta? Na verdade, o modo em lote funciona independentemente do tipo de índice usado
para ler as linhas. O único requisito para o modo de lote é que pelo menos uma das tabelas na consulta
tenha um índice columnstore (mesmo que não seja útil para a consulta). Contanto que isso seja verdade,
você poderá ver planos com alguns operadores usando o modo de linha e alguns usando o processamento
do modo de lote, dependendo dos operadores envolvidos. Vamos ver um exemplo que une dados de
rowstore e columnstore.

SELECT bp.Nome,
AVG(th.Custo Real),
MAX(th.Custo Real),
MIN(th.Custo Real)
DE dbo.bigTransactionHistory AS th
JOIN dbo.bigProduct AS bp
ON bp.ProductID = th.ProductID
GROUP BY bp.Nome;

Listagem 12-6

A Figura 12-5 mostra o plano de execução resultante (se seu limite de custo para paralelismo for 26
ou mais).

Figura 12-5: Um plano de execução combinando dados rowstore e columnstore.

366
Machine Translated by Google

Capítulo 12: Processamento em modo de lote

Mais uma vez, se você inspecionar as propriedades do Columnstore Index Scan, verá que ele está usando a execução
em modo de lote e que novamente usa um aprimoramento de agregação inicial chamado empilhamento agregado, em
que parte (ou às vezes toda) da agregação é feito pela própria varredura, à medida que os dados são lidos. Fazer isso
reduz o número de linhas retornadas ao Hash Match
cerca de 1,5 milhão.

Os dados passam para um operador Hash Match (Aggregate) que, novamente, está usando a execução em modo
de lote. Você pode se surpreender ao ver não um, mas dois operadores de agregação neste plano, para o que é uma
consulta relativamente simples. Este é outro exemplo do otimizador optando por usar agregação local e global.
Também vimos uma agregação "local-global" no Capítulo 11 (Listagem 11-4), como parte de um plano paralelo de
modo de linha. Nesse caso, o operador Hash Mash foi claramente marcado como (Partial Aggregate), porque estava
trabalhando apenas nos dados desse segmento, mas se comportou da mesma maneira que um operador de agregação
normal.

Aqui, as propriedades Defined Values e Hash Key Build da Hash Match (Aggregate)
oferecer algumas informações sobre o que está acontecendo. A Figura 12-6 mostra os Valores Definidos.

Figura 12-6: Valores definidos mostrando agregação parcial.

Você pode ver que um valor chamado [partialagg1005] é criado, consistindo na agregação de várias colunas no
conjunto de dados. A agregação está sendo realizada no ProductID
coluna, conforme mostrado na propriedade Hash Keys Build .

367
Machine Translated by Google

Capítulo 12: Processamento em modo de lote

Figura 12-7: Agregação local baseada em ProductID.

Com base na saída do índice Columnstore Index Scan , o otimizador decidiu que uma agregação inicial em
ProductID fará a agregação posterior, pelo nome do produto
como definimos no T-SQL, mais eficiente. Consequentemente, o número de linhas retornado para a
operação de junção subsequente é reduzido de aproximadamente 30 milhões de linhas (da tabela base) para
apenas 25.200 (o número de valores ProductID distintos), conforme mostrado pela propriedade Número real
de linhas .

Esse fluxo de dados é unido a linhas na tabela bigProduct, com base nos valores de ProductID
correspondentes. Ele usa um Adaptive Join (veja o Capítulo 4), novamente executando em modo batch.
O Tipo de Junção Real usado é Correspondência de Hash, com a Varredura de Índice Agrupado como
a entrada mais baixa (escolhida porque o número de linhas retornadas excede o valor da propriedade Linhas
de Limite Adaptativo ). O Clustered Index Scan usou o processamento de modo de linha.

Neste ponto, os valores da coluna Nome do produto estão disponíveis e, após um modo de lote Classificar
operador, vemos o operador Stream Aggregate , que usa as agregações parciais para realizar a
agregação "global" final em Name.

Figura 12-8: Agregação global para valores finais.

O operador Stream Aggregate usou o processamento em modo de linha, pois não oferece suporte
ao modo em lote.

368
Machine Translated by Google

Capítulo 12: Processamento em modo de lote

Concessão de memória adaptável no modo de lote

Finalmente, para processamento em modo batch, vamos ver mais uma consulta, um procedimento armazenado como
mostrado na Listagem 12-7.

CRIAR OU ALTERAR PROCEDIMENTO dbo.CostCheck (@Cost MONEY)


AS
SELECT p.Nome,
AVG(th.Quantity)
FROM dbo.bigTransactionHistory AS th JOIN
dbo.bigProduct AS p ON p.ProductID =
th.ProductID
WHERE th.ActualCost = @Cost GROUP
BY p.Name;

Listagem 12-7

A Listagem 12-8 mostra como podemos executar o procedimento CostCheck.

EXEC dbo.CostCheck @Cost = 0;

Listagem 12-8

A Figura 12-9 mostra o plano de execução.

Figura 12-9: Plano de execução com um indicador de Aviso.

Você verá o aviso no operador SELECT do plano. Embora possamos ver o aviso na dica de ferramenta, ele mostrará
apenas o primeiro aviso. Se houver mais de um aviso, é melhor usar as propriedades. A Figura 12-10 mostra a seção
Avisos das propriedades do SELECT.

369
Machine Translated by Google

Capítulo 12: Processamento em modo de lote

Figura 12-10: Propriedades de MemoryGrantWarning.

O texto completo do aviso é o seguinte:

A concessão de memória de consulta detectou "ExcessiveGrant", o que pode afetar a confiabilidade.


Tamanho da concessão: inicial 80840 KB, final 80840 KB, usado 3192 KB.

A estimativa inicial nas linhas do índice columnstore foi de 12,3 milhões, mas a real foi de apenas 10,8
milhões. A partir daí, o operador Hash Match estimou que a agregação, com base nas estatísticas
amostradas, retornaria 25.200 linhas. A Hash Match (Aggregate), neste exemplo, usa uma tabela de hash
otimizada para agregação, pois armazena GROUP BY
valores e resultados de agregação intermediários, em vez de armazenar todas as linhas de entrada
inalteradas, como fazem outros operadores de correspondência de hash . Isso significa que sua concessão
de memória é baseada no número estimado de linhas produzidas (25200), não lidas. No entanto, produziu
apenas 10 mil linhas. Essa superestimação também afeta a concessão de memória para o Adaptive Join
subsequente que, até o final de sua fase de construção, exigirá memória ao mesmo tempo que o Hash Match
e a concessão de memória para o Sort.

Em suma, essas contagens de linhas superestimadas significaram que uma quantidade maior de
memória foi solicitada, 80.840, do que foi consumida, 3.192. superestimações não explicam por que
a estimativa de concessão de memória é tão grande neste caso.

A partir do SQL Server 2017 e do Banco de Dados SQL do Azure, o mecanismo de consulta agora pode
ajustar a concessão de memória para execuções subsequentes, para cima ou para baixo, com base nos
valores das execuções anteriores da consulta. Resumindo, se reexecutarmos a consulta, a alocação de
memória, durante o processamento em modo batch, se ajustará instantaneamente. Vamos dar um exemplo.
Supondo que acabei de executar a Listagem 12-8, vou executar o procedimento armazenado novamente,
fornecendo um valor diferente para o parâmetro @Cost.

370
Machine Translated by Google

Capítulo 12: Processamento em modo de lote

EXEC dbo.CostCheck @Cost = 15,035;

Listagem 12-9

Esta consulta tem um conjunto de resultados semelhante. A execução disso resultará na reutilização do plano
de execução já em cache. No entanto, como estamos fazendo o processamento em lote, a concessão de
memória pode ser ajustada em execuções subsequentes com base em processos semelhantes que permitem a
junção adaptativa. O plano agora se parece com o mostrado na Figura 12-11.

Figura 12-11: Plano de execução sem aviso.

O aviso foi removido, embora o plano não tenha sido recompilado, as estatísticas não tenham sido ajustadas
ou qualquer outro processo que normalmente resultaria em uma alteração na alocação de memória. A concessão
de memória foi ajustada rapidamente, como podemos ver nas propriedades do operador SELECT na Figura 12-12.
Também expandi a propriedade Parameter List , para verificar se o otimizador reutilizou o plano compilado para um
Cost zero .

Figura 12-12: Propriedades mostrando a memória ajustada.

371
Machine Translated by Google

Capítulo 12: Processamento em modo de lote

A memória adaptativa pode funcionar em qualquer direção, alocações de memória sub ou supercalculadas,
para ajustar a memória durante execuções subsequentes de alocações semelhantes.
No entanto, isso pode levar a thrashing se uma consulta tiver muitos tipos diferentes de alocações, de modo
que, em algum momento, automaticamente, a memória adaptativa será desativada. Isso é rastreado por plano.
Ele pode ser desativado para uma consulta e ainda funciona para outras consultas e será ativado novamente
toda vez que o plano de uma consulta for recompilado.

Você não pode dizer diretamente de um único plano se a memória adaptativa foi desativada para esse
plano. Você teria que configurar o monitoramento por meio de eventos estendidos para observar esse
comportamento. Se você suspeitar que isso aconteceu, você pode comparar os valores da alocação de
memória de uma execução para a seguinte. Se eles não estiverem mudando, mesmo que a consulta sofra
derramamentos ou grande superalocação, a concessão de memória adaptável foi desabilitada.

Embora a memória adaptativa esteja disponível atualmente apenas com processamento em lote, a Microsoft
afirmou que habilitará o processamento de memória adaptativa em modo de linha em algum momento no
futuro.

Perda de processamento em modo de lote

O SQL Server 2017, ao lidar com índices columnstore, tem uma tendência muito forte para usar o
processamento em modo de lote para todas, ou pelo menos parte, de qualquer consulta executada no
índice columnstore. Se você estiver trabalhando no SQL Server 2014 ou 2016, verá que algumas das
seguintes operações não serão executadas no modo de lote:
• UNIÃO TODOS

• JUNÇÃO EXTERNA

• EM/EXISTE ou NÃO EM/NÃO EXISTE


• OU em ONDE

• Agregação sem GROUP BY


• SOBRE

Você precisará verificar o plano de execução real, porque ele mostrará se os operadores dentro do plano
usaram o modo de lote ou se foram para o modo de linha. No entanto, ao testar tudo isso no SQL Server
2017, o plano sempre foi, no todo ou em parte, para o processamento em modo batch.

372
Machine Translated by Google

Capítulo 12: Processamento em modo de lote

Resumo
O novo modo de execução em lote significa que o mecanismo de consulta pode passar grandes grupos de linhas de
uma só vez, em vez de mover os dados linha por linha. Além disso, existem algumas otimizações de desempenho
específicas que só estão disponíveis no modo de lote.

Por enquanto, o modo de lote vem com certas pré-condições. Atualmente, ele só funciona com consultas em tabelas que
possuem um índice columnstore, mas isso mudará no futuro. Nas versões mais antigas do SQL Server, o modo de lote é
suportado por um conjunto relativamente limitado de operadores.

O modo de lote pode oferecer enormes benefícios de desempenho ao processar grandes conjuntos de dados, mas as
consultas que realizam pesquisas de ponto e varreduras de intervalo limitado ainda são melhores no modo de linha.

373
Machine Translated by Google

Capítulo 13: O XML dos Planos de Execução


Por trás de cada um dos planos de execução que examinamos até este ponto do livro está o XML. Um "plano
XML" não é diferente de um plano gráfico; ele contém as mesmas informações que você pode encontrar nos operadores
e propriedades de um plano gráfico. XML é apenas um formato diferente para visualizar o mesmo plano. Se salvarmos
um plano, ele será salvo em seu formato XML nativo, o que facilita o compartilhamento com outras pessoas.

Eu imagino que muito poucas pessoas prefeririam ler planos de execução no formato XML bruto, em vez de
gráfico. Além disso, tendo o XML recebido apenas uma menção nos doze capítulos anteriores deste livro, deve ficar
claro que você não precisa ler XML para entender os planos de execução. No entanto, existem alguns casos em que
o acesso a ele será útil, que destacarei, e depois discutiremos a principal razão pela qual você pode querer usar os
dados XML brutos: programabilidade. Você pode executar consultas XQuery T-SQL em arquivos XML e planos XML.
Na verdade, isso nos dá um meio direto de consultar os planos no Plan Cache.

Um breve tour pelo XML por trás de um plano


A maneira mais fácil de visualizar o XML de qualquer plano no SSMS é simplesmente clicar com o botão direito do
mouse em qualquer plano gráfico e selecionar Mostrar XML do plano de execução no menu de contexto.

Se necessário, você pode capturar o plano XML programaticamente, encapsulando o lote nos comandos SET
SHOWPLAN_XML ON/OFF, para o plano estimado, ou SET STATIS TICS XML ON/OFF para o plano real (mais
sobre isso posteriormente).

O XML para um plano estimado


Exiba o plano estimado para a consulta na Listagem 13-1, que recupera alguns detalhes de clientes no estado de
Nova York.

SELECT c.CustomerID, a.City, s.Name, st.Name


DE Vendas.Cliente AS c
JOIN Sales.Store AS s
ON c.StoreID = s.BusinessEntityID
JUNTE -SE a Vendas.VendasTerritório AS st

374
Machine Translated by Google

Capítulo 13: O XML dos Planos de Execução

ON c.TerritoryID = st.TerritoryID
JOIN Person.BusinessEntityAddress AS bea
ON c.CustomerID = bea.BusinessEntityID
JOIN Pessoa.Endereço COMO a
ON bea.AddressID = a.AddressID
JOIN Person.StateProvince AS sp
ON a.StateProvinceID = sp.StateProvinceID
WHERE st.Name = 'Nordeste' AND sp.Name = 'New York';
VAI

Listagem 13-1

A Figura 13-1 mostra o plano gráfico usual.

Figura 13-1: Plano de execução para consulta de clientes do estado de Nova York.

Clique com o botão direito do mouse em qualquer área de espaço em branco do plano e escolha Mostrar XML do
plano de execução para obter o XML por trás desse plano estimado. Os resultados, mesmo para nossa consulta
simples, são muito grandes para serem exibidos aqui, e a Figura 13-2 mostra apenas a seção de abertura. O
conteúdo geralmente é adicionado em novas versões do SQL Server, e a ordem dos atributos e, às vezes, dos
elementos pode diferir entre as versões, portanto, não se preocupe se parecer diferente em seu sistema.

375
Machine Translated by Google

Capítulo 13: O XML dos Planos de Execução

Figura 13-2: O XML de um plano de execução.

Logo no início, temos a definição do esquema. O XML possui uma estrutura padrão,
composta por elementos e atributos, conforme definido e publicado pela Microsoft. Uma
revisão de alguns dos elementos e atributos comuns e o esquema completo está disponível
em https://bit.ly/2BU9Yhf .
Listados primeiro estão os elementos BatchSequence, Batch e Statements. Neste
exemplo, estamos analisando apenas um único lote e uma única instrução, portanto,
nada mais é exibido.

376
Machine Translated by Google

Capítulo 13: O XML dos Planos de Execução

Em seguida, como parte do elemento StmtSimple, vemos o texto da consulta seguido de uma lista de
atributos da própria instrução. Depois disso, o elemento StatementSetOptions mostra as opções em
nível de banco de dados que estavam em vigor. A Listagem 13-2 mostra StmtSimple e
StatementSetOptions para o plano estimado.

<StmtSimple StatementText="SELECT c.CustomerID, a.City, s.Name,


st.Name FROM Sales.Customer AS c JOIN
Sales.Store AS s ON c.StoreID =
s.BusinessEntityID
JUNTE-SE a Vendas.VendasTerritório AS st
…etc…
WHERE st.Name = 'Nordeste' AND sp.Name =
'Nova york'"
StatementId="1" StatementCompId="1"
TipoDeInstrução="SELECT"
StatementSqlHandle="0x0900A7CAC098F11600D1596466
7F6395453000000 …"
DatabaseContextSettingsId="3" ParentObjectId="0"
StatementParameterizationType="0"
RetrievedFromCache="true"
StatementSubTreeCost="1.04758" StatementEstRows="1"
SecurityPolicyApplied="false" StatementOptmLevel="FULL"
QueryHash="0x6F422E0A48C0E2DA" QueryPlanHash="0xBF47C49
83DC8361D"
StatementOptmEarlyAbortReason="TimeOut"
CardinalityEstimationModelVersion="140">
<StatementSetOptions QUOTED_IDENTIFIER="true" ARITHABORT="true"
CONCAT_NULL_YIELDS_NULL="true" ANSI_
NULL="verdadeiro"
ANSI_PADDING="true" ANSI_WARNINGS="true"
NUMERIC_ROUNDABORT="falso">
</StatementSetOptions>

Listagem 13-2

Em seguida, está o elemento QueryPlan, que mostra algumas propriedades no nível do plano e do
otimizador (o elemento OptimizerStatsUsage é recolhido).

<QueryPlan NonParallelPlanReason="CouldNotGenerateValidParallelPl an"

CachedPlanSize="104" CompileTime="10" CompileCPU="10"


CompileMemory="1160">
<MemoryGrantInfo SerialRequiredMemory="2048"

377
Machine Translated by Google

Capítulo 13: O XML dos Planos de Execução

SerialDesiredMemory="2632" />
<OptimizerHardwareDependentProperties EstimatedDisponívelMemoryGra
nt="157286"
EstimatedPagesCached="19660"
EstimadoDisponívelGrauOfP
alternativo="2"
MaxCompileMemory="1475656"
/>
<OptimizerStatsUsage>…</OptimizerStatsUsage>

Listagem 13-3

Coletivamente, as Listagens 13-2 e 13-3 mostram as mesmas informações disponíveis para nós observando as
propriedades do primeiro operador, neste caso um SELECT, no plano gráfico. Você pode ver informações como
o CompileTime, o CachedPlanSize e o Statement OptmEarlyAbortReason. Eles são convertidos em Tempo de
compilação, Tamanho do plano em cache e Motivo para término antecipado da otimização quando você
está analisando o plano gráfico. Como sempre, alguns dos valores em seu XML (para custos estimados e
contagens de linhas, por exemplo) podem diferir daqueles mostrados aqui.

Dentro do elemento QueryPlan há uma hierarquia aninhada de elementos RelOp, cada um


descrevendo um operador no plano e suas propriedades. Os elementos RelOp são listados na ordem em
que são chamados, semelhante à leitura de um plano gráfico da esquerda para a direita. Loops aninhados

operador, seguido por outro Nested Loops, com um NodeId de "1" e, em seguida, um Index Seek
na tabela SalesTerritory e assim por diante.

Os dados XML são mais difíceis de receber, todos de uma vez, do que os planos de execução gráfica,
mas você pode expandir e recolher elementos usando os nódulos "+" e "–" no lado esquerdo e, ao fazê-lo,
a hierarquia do plano fica um pouco mais claro. No entanto, encontrar operadores específicos no XML não
é fácil, principalmente para planos complexos. Se você conhece o NodeId do operador (do plano gráfico),
pode fazer um Ctrl-F para NodeID="xx."

A Listagem 13-4 mostra as propriedades da primeira junção de loops aninhados (reformatada um pouco
para legibilidade).

<RelOp NodeId="0" PhysicalOp=" Loops Aninhados" LogicalOp="Inner Join"


EstimateRows="1"
EstimateIO="0" EstimateCPU="5.9304e-005" AvgRowSize="149"
EstimatedTotalSubtreeCost="1.04758" Parallel="0"

378
Machine Translated by Google

Capítulo 13: O XML dos Planos de Execução

EstimateRebinds="0"
EstimateRewinds="0" EstimatedExecutionMode="Row">

Listagem 13-4

Depois disso, vemos um elemento aninhado, OutputList, mostrando os dados retornados por esse operador (reformatei-o
e reduzi os níveis de aninhamento para facilitar a leitura). Este operador, como seria de esperar, retorna valores para
todas as colunas solicitadas na lista SELECT de nossa consulta.

<Lista de Saídas>
<ColumnReference Database="[AdventureWorks2016]" Schema="[Vendas]"
Tabela="[Cliente]" Alias="[c]"
Column="CustomerID" />
<ColumnReference Database="[AdventureWorks2016]" Schema="[Vendas]
" Table="[Store]" Alias="[s]" Column="Name" />
<ColumnReference Database="[AdventureWorks2016]" Schema="[Vendas]"
Table="[SalesTerritory]" Alias="[st]"
Coluna="Nome" />
<ColumnReference Database="[AdventureWorks2016]"
Schema="[Pessoa]"
Tabela="[Endereço]" Alias="[a]" Coluna="Cidade" />
</OutputList>

Listagem 13-5

Para planos complexos, acho que essa é uma maneira relativamente fácil de ver todas as colunas e seus atributos
retornados.

Em seguida, vemos o elemento NestedLoops, que contém elementos para propriedades específicas desse operador,
conforme mostrado na Listagem 13-6. Nesse caso, podemos ver que esse operador resolve a condição de junção
usando OuterReferences (consulte o Capítulo 4 para obter uma descrição completa).
Abaixo disso, incluí a versão recolhida para as duas entradas do primeiro operador, sendo a entrada externa outro
NestedLoops (com NodeId="1") e a entrada interna um Clustered Index Seek (NodeId="14"), que é o último operador
chamado neste plano.

Os valores da coluna StoreID retornados pela entrada externa são enviados para a entrada interna, onde são usados para
executar uma operação de busca na tabela Store para retornar o nome
coluna para linhas correspondentes.

379
Machine Translated by Google

Capítulo 13: O XML dos Planos de Execução

<NestedLoops Optimized="0">
<OuterReferences>
<ColumnReference Database="[AdventureWorks2016]"
Schema="[Vendas]"
Tabela="[Cliente]" Alias="[c]"
Column="StoreID">
</ColumnReference>
</OuterReferences>
<RelOp AvgRowSize="101" EstimateCPU="0.00059304" EstimateIO="0"
EstimateRebinds="0" EstimateRewinds="0"
EstimatedExecutionMode="Linha"
EstimateRows="14.1876" LogicalOp="Inner Join" NodeId="1"
Paralelo="falso"
PhysicalOp="Loops Aninhados" EstimatedTotalSubtreeCo
st="1.03974">…</RelOp>
<RelOp AvgRowSize="61" EstimateCPU="0.0001581"
EstimativaIO="0.003125"
EstimateRebinds="1.78078" EstimateRewinds="11.4068"
EstimatedExecutionMode="Linha" EstimateRows="1"
EstimatedRowsRead="1"
LogicalOp="Busca de Índice Agrupado" NodeId="14"
Paralelo="falso"
PhysicalOp="Busca de Índice Agrupado" EstimatedTotalSubtreeC
ost="0.00778646"
TableCardinality="701">…</RelOp>

Listagem 13-6

Por outro lado, dentro do elemento equivalente para o segundo Nested Loops (NodeId="1"), quando
você expandir a primeira entrada novamente, verá que esse operador resolve a condição de junção
para a tabela Store, usando uma propriedade Predicate. Não mostrei todo o predicado mas,
resumindo, este operador recebe os valores TerritoryID e Name do Index Seek
na tabela SalesTerritory e unirá esses dados com os da entrada inferior, retornando apenas as
linhas que tenham valores correspondentes para TerritoryID na tabela Customer.

<NestedLoops Optimized="0">
<Predicado>
<Operador Escalar
ScalarString="[AdventureWorks2016].[Vendas].
[VendasTerritório].[TerritórioID]
como [st].[TerritoryID]=
[AdventureWorks2016].[Vendas].[Cliente].
[TerritórioID]

380
Machine Translated by Google

Capítulo 13: O XML dos Planos de Execução

como [c].[TerritoryID]">
…etc…
</Predicado>

Listagem 13-7

O XML para um plano real


Se você retornar à Listagem 13-1 e desta vez executá-la e capturar o plano real, verá todas as mesmas
informações do XML para o plano estimado, além de alguns novos elementos, ou seja, aqueles que
são preenchidos apenas em tempo de execução , em vez de tempo de compilação.

A Listagem 13-8 compara o conteúdo do elemento QueryPlan para o plano estimado (mostrado
primeiro) e depois o plano real. Você pode ver que o último contém informações adicionais,
incluindo DegreeOfParallelism (mais sobre paralelismo no Capítulo 11), MemoryGrant (que é a
quantidade de memória necessária para a execução da consulta) e algumas propriedades adicionais
dentro do elemento MemoryGrantInfo.

<QueryPlan
NonParallelPlanReason="CouldNotGenerateValidParallelPlan"
CachedPlanSize="104" CompileTime="9" CompileCPU="9"
CompileMemory="1160">
<MemoryGrantInfo SerialRequiredMemory="2048"
SerialDesiredMemory="2632">
<QueryPlan
GrauDoParalelismo="0"
NonParallelPlanReason="CouldNotGenerateValidParallelPlan"
MemoryGrant="2632"
CachedPlanSize="104" CompileTime="9" CompileCPU="9"
CompileMemory="1160">
<MemoryGrantInfo SerialRequiredMemory="2048"
SerialDesiredMemory="2632"
RequiredMemory="2048" DesiredMemory="2632" RequestedMemory="2632"
GrantWaitTime="0" GrantedMemory="2632" MaxUsedMemory="640"
MaxQueryMemory="576112" />

Listagem 13-8

Outra grande diferença é que, no XML de um plano real, cada operador tem um elemento
RunTimeInformation, mostrando o encadeamento, as linhas reais e o número de execuções
desse operador junto com informações adicionais.

381
Machine Translated by Google

Capítulo 13: O XML dos Planos de Execução

<RelOp AvgRowSize="149" EstimateCPU="5.9304E-05" EstimateIO="0"


EstimateRebinds="0" EstimateRewinds="0"
EstimatedExecutionMode="Linha"
EstimateRows="1" LogicalOp="Inner Join" NodeId="0"
Paralelo="falso"
PhysicalOp=" Loops Aninhados" EstimatedTotalSubtreeCo
st="1.04758">
<Lista de Saídas>
…etc…
</OutputList>
<RunTimeInformation>
<RunTimeCountersPerThread Thread="0" ActualRows="1" Lotes="0"
ActualEndOfScans="1"
ActualExecutions="1"
ActualExecutionMode="Linha"
ActualElapsedms="6" ActualCPUms="6"
/>
</RunTimeInformation>

Listagem 13-9

Salvando e Compartilhando com Segurança Planos de Execução

Embora você possa gerar um plano de execução diretamente em seu formato XML nativo, você só
pode salvá-lo a partir da representação gráfica. Se tentarmos salvar em XML diretamente da janela de
resultados, obteremos apenas o que está sendo exibido na janela de resultados. Outra opção é usar um
script do PowerShell, ou similar, para gerar saída de XML para um arquivo .sqlplan .

Basta clicar com o botão direito do mouse no plano gráfico e selecionar Salvar plano de execução
como… para salvá-lo como um arquivo .sqlplan . Este arquivo XML, como vimos, fornece todas as
informações do plano, incluindo todos os imóveis. Este pode ser um recurso muito útil. Por exemplo,
podemos coletar vários planos em formato XML, salvá-los em arquivo e abri-los em formato gráfico fácil
de visualizar (e comparar). Isso também é útil para aplicativos de terceiros (abordados brevemente no
Capítulo 17).

Contudo, uma palavra de cautela; como vimos anteriormente, o XML do plano de execução armazena os
valores da consulta e do parâmetro. Essas informações podem incluir informações proprietárias ou de
identificação pessoal. Tenha cuidado ao compartilhar um plano de execução publicamente.

382
Machine Translated by Google

Capítulo 13: O XML dos Planos de Execução

Quando você realmente precisará do plano XML


Como você pode ver, enquanto todas as informações estão lá, ler as plantas diretamente pelo XML não é
tão fácil quanto ler a planta gráfica e as folhas de propriedades de cada operador.
No entanto, existem alguns casos específicos em que você precisará do XML, e eu os revisarei
brevemente aqui (pode haver outros!).

Use o plano XML para forçar o plano


No Capítulo 9, discutimos o forçamento de plano, usando um guia de plano para aplicar o USE PLAN
dica de consulta. Aqui, você precisa fornecer o XML do plano para o parâmetro @hints do
procedimento armazenado do sistema sp_create_plan_guide, ao criar o guia do plano.

Para fazer isso, primeiro você precisará capturar o plano programaticamente. Essa consulta extrai
algumas informações da tabela Purchasing.PurchaseOrderHeader e filtra os dados na ShipDate.

SET STATISTICS XML ON;


SELECT poh.PurchaseOrderID,
poh.EnvioData,
poh.ShipMethodID
FROM Purchasing.PurchaseOrderHeader AS poh
WHERE poh.ShipDate ENTRE '20140103' E '20140303';
VAI
SET STATISTICS XML OFF;

Listagem 13-10

A Figura 13-3 mostra o resultado, no modo de grade padrão.

Figura 13-3: Um link clicável para o plano XML no SSMS.

Se você tiver os resultados da consulta sendo exibidos no modo de texto, verá parte da string XML, mas
ela não será clicável e, dependendo das configurações do SSMS, pode não estar completa.

383
Machine Translated by Google

Capítulo 13: O XML dos Planos de Execução

No modo de grade, clicar neste link abre o plano de execução como um plano gráfico. No entanto, em vez
disso, se você estiver forçando o plano, basta clicar com o botão direito do mouse no link, copiá-lo e colá-lo no @
parâmetro hints ao criar o guia de plano.

Propriedades do primeiro operador ao capturar planos usando


Eventos estendidos
Ao capturar um plano usando Eventos Estendidos, você não verá o primeiro operador, SELECT,
INSERT, UPDATE ou DELETE no plano gráfico, então você não terá acesso a todos os metadados úteis que
ele oculta, exceto alternando para a representação XML, onde parte dela ainda está armazenada. Isso ocorre
porque o XML para os planos capturados usando Eventos Estendidos (e Eventos de Rastreamento) difere de
todas as outras fontes de planos de execução (SSMS, cache de planos e Repositório de Consultas).

A Listagem 13-11 mostra a seção relevante do XML, para um plano capturado usando Extended Events
(você verá como fazer isso no Capítulo 15), entre o elemento Statement e o primeiro elemento RelOp.

<StmtSimple StatementSubTreeCost="1.04758" StatementEstRows="1"


SecurityPolicyApplied="false" StatementOptmLevel="FULL"
QueryHash="0x6F422E0A48C0E2DA" QueryPlanHash="0xBF47C49
83DC8361D"
StatementOptmEarlyAbortReason="TimeOut"
CardinalityEstimationModelVersion="140">
<QueryPlan DegreeOfParallelism="0" MemoryGrant="2632"
NonParallelPlanReason="CouldNotGenerateValidParallelPl
um"
CachedPlanSize="104" CompileTime="10" CompileCPU="10"
CompileMemory="1160">
<MemoryGrantInfo SerialRequiredMemory="2048"
SerialDesiredMemory="2632"
RequiredMemory="2048" DesiredMemory="2632"
RequestedMemory="2632" GrantWaitTime="0"
GrantedMemory="2632"
MaxUsedMemory="640" MaxQueryMemory="573840">
</MemoryGrantInfo>

384
Machine Translated by Google

Capítulo 13: O XML dos Planos de Execução

<OptimizerHardwareDependentProperties EstimatedDisponívelMemoryG
desabafo="157286"

EstimatedPagesCached="19660"
EstimadoDisponívelGrauO
fParalelismo="2"

MaxCompileMemory="1475656">
</OptimizerHardwareDependentProperties>
<OptimizerStatsUsage>…</OptimizerStatsUsage>

Listagem 13-11

É um conjunto reduzido de informações e não tenho uma história completa da Microsoft sobre por que isso
acontece. O código para capturar os planos parece ter vindo originalmente de Trace Events e foi duplicado em
Extended Events. No entanto, o que resta ainda é útil e está disponível apenas no XML.

Pré-SQL Server 2012: detalhes completos do "índice ausente"


Como vimos anteriormente no livro, muitas vezes, você verá uma mensagem na parte superior de um plano
dizendo que há um índice ausente que "reduzirá o custo" de uma operadora em alguma porcentagem.
Antes do SQL Server 2012, se houvesse mais de um índice ausente, apenas um estaria visível na dica de
índice ausente no plano gráfico. Portanto, se você ainda estiver trabalhando em versões anteriores do SQL
Server, o XML é o único local em que você encontrará a lista completa.

Além disso, usar o plano de execução vincula diretamente as informações de índice ausentes à própria consulta.
Usando apenas os DMVs fornecidos pela Microsoft, você não verá qual consulta se beneficiará do índice
sugerido.

Se você abrir o XML para o plano de execução real da Listagem 13-10, notará um elemento próximo ao topo
rotulado como MissingIndexes, que lista tabelas e colunas onde o otimizador reconhece que, potencialmente, se
tivesse um índice, poderia resultar em um melhor plano de execução e melhor desempenho.

385
Machine Translated by Google

Capítulo 13: O XML dos Planos de Execução

<MissingIndexes>
<MissingIndexGroup Impact="83.5833">
<MissingIndex Database="[AdventureWorks2016]"
Schema="[Compra]" Table="[PurchaseOrderHeader]">
<ColumnGroup Usage="INEQUALITY">
<Column Name="[ShipDate]" ColumnId="8" />
</ColumnGroup>
<ColumnGroup Usage="INCLUDE">
<Column Name="[ShipMethodID]" ColumnId="6" />
</ColumnGroup>
</MissingIndex>
</MissingIndexGroup>
</MissingIndexes>

Listagem 13-12

Embora as informações sobre índices ausentes às vezes possam ser úteis, elas são tão boas quanto
as estatísticas disponíveis e, às vezes, podem não ser confiáveis. Também não considera o custo
adicional de manutenção do índice. Sempre coloque testes apropriados antes de agir de acordo com
essas sugestões.

Consultando o cache do plano


No restante deste capítulo, vamos nos concentrar na razão primordial pela qual é muito útil ter o
XML bruto por trás de um plano: especificamente para consultá-lo, usando XQuery. Podemos
executar consultas XQuery no arquivo .sqlplan, ou nos planos de execução armazenados em
colunas XML nas tabelas, ou diretamente no XML que existe no cache do plano ou no Query Store no
SQL Server.

Esta seção apresenta apenas alguns dos conceitos básicos para escrever XQuery e alguns exemplos
úteis para você começar, porque um tutorial aprofundado está muito além do escopo deste livro.
Para isso, recomendo XML e JSON Recipes for SQL Server de Alex Grinberg.

386
Machine Translated by Google

Capítulo 13: O XML dos Planos de Execução

Por que consultar o XML dos planos?

Conforme discutido no Capítulo 9, vários DMOs, como sys.dm_exec_query_stats e sys.dm_exec_cached_plans


armazenam o plan_handle para um plano, que podemos fornecer à função sys.dm_exec_query_plan, para
retornar o plano de execução em formato XML, bem como para o sys.dm_exec_sql_text para retornar o texto
SQL. Todas as consultas armazenadas no Query Store também estão no mesmo formato XML, embora
armazenadas por padrão como texto, que você deve CAST para XML.

Podemos então usar XQuery para retornar os elementos, propriedades e valor dentro do plano XML, muitos dos
quais discutimos anteriormente neste capítulo. Por que isso é útil?

Em primeiro lugar, vamos supor que temos muitos planos que precisamos examinar. Milhares, ou mais.
Em vez de tentar percorrer esses planos, um de cada vez, procurando algum padrão comum, podemos
escrever consultas que pesquisam elementos ou termos específicos dentro do plano XML, como "Motivo para
rescisão antecipada", e assim rastrear problemas dentro de todo o conjunto de planos.

Em segundo lugar, como sabemos, os DMOs e o Query Store contêm muitas outras informações úteis, como
estatísticas de execução para as consultas que usaram os planos em cache. Isso significa, por exemplo, que
podemos consultar o cache do plano ou o Query Store, para todos os planos com recomendações de índice
ausentes e as instruções SQL associadas, juntamente com estatísticas de execução apropriadas, para que
possamos escolher a estratégia de índice correta para a carga de trabalho, em vez de do que consulta por
consulta. O XML é o único lugar onde você pode recuperar certas informações, como informações de índice
ausentes correlacionadas à sua consulta, portanto, a capacidade de recuperar informações do XML pode tornar o
uso de XQuery útil.

Finalmente, às vezes um plano é muito grande e torna-se um pouco mais fácil pesquisar o XML do plano
para determinados valores e propriedades, em vez de percorrer as propriedades do operador individual nos
planos gráficos. Abordaremos essa ideia mais no Capítulo 14.

Antes de começarmos, porém, uma nota de cautela: consultas XML são inerentemente caras, e consultas
em XML podem afetar seriamente o desempenho no servidor, principalmente devido à memória que o XQuery
consome. Sempre aplique a devida diligência ao executar esses tipos de consultas e tente minimizar a sobrecarga
causada pelo XQuery, aplicando alguns critérios de filtragem às suas consultas, por exemplo, restringindo os
resultados a um único banco de dados, para limitar a quantidade de dados acessados.

Melhor ainda, poderíamos exportar os planos XML, e potencialmente também as estatísticas de tempo de
execução, para uma tabela em um servidor diferente e, em seguida, executar o XQuery nele, para evitar colocar
muita carga diretamente em uma máquina de produção.

387
Machine Translated by Google

Capítulo 13: O XML dos Planos de Execução

Consulte o XML do plano para operadores específicos

A listagem 13-13, dada apenas como um exemplo do que é possível, retorna os três principais
operadores da consulta chamada com mais frequência no cache do plano, supondo que essa consulta
tenha um plano em cache, com base no custo total estimado de cada operador.

Ele ilustra como podemos construir consultas no cache do plano, mas eu hesitaria antes de executar essa
consulta em um sistema de produção se esse sistema já estivesse sob estresse.

COM Top1Query
AS (SELECIONAR TOPO (1)
dest.texto,
deqp.query_plan
FROM sys.dm_exec_query_stats AS deqs
CROSS APPLY sys.dm_exec_sql_text(deqs.sql_handle) AS dest
CROSS APPLY sys.dm_exec_query_plan(deqs.plan_handle) AS
deqp
ORDER BY deqs.execution_count DESC)
SELECIONE OS 3 PRINCIPAIS

tq.texto,
RelOp.op.value('@PhysicalOp', 'varchar(50)') AS PhysicalOp,
RelOp.op.value('@EstimateCPU', 'float') + RelOp.op.value('@
EstimateIO', 'float') AS EstimatedCost
DE Top1Query AS tq
CROSS APPLY tq.query_plan.nodes('declarar elemento padrão
namespace "http://schemas.microsoft.com/sqlserver/2004/07/
plano de exibição";
//RelOp') RelOp(op)
ORDEM POR Custo Estimado DESC;

Listagem 13-13

A lógica básica é bastante fácil de seguir. Primeiro, defino uma expressão de tabela comum (CTE),
Top1Query, que retorna o texto SQL e o plano para a consulta executada com mais frequência
atualmente no cache, conforme definido pelo execution_count.

Em seguida, pule para a cláusula FROM da segunda consulta, o "membro recursivo", que faz
referência ao CTE. Para cada linha em nossa CTE Top1Query, (neste caso há apenas uma linha), o
CROSS APPLY avaliará a subconsulta, que neste caso usa o .nodes
método para "fragmentar" o XML do plano, armazenado na coluna query_plan de sys.dm_
exec_query_plan, expondo o XML como se fosse uma tabela. Vale a pena notar que a consulta usa a
soma de EstimatedCPU e EstimatedIO para chegar a um EstimatedCost

388
Machine Translated by Google

Capítulo 13: O XML dos Planos de Execução

valor para cada operador. Normalmente, mas nem sempre, isso corresponderá exatamente ao valor exibido para o
Custo estimado do operador nas propriedades do plano gráfico. Para alguns operadores, outros fatores (como
concessões de memória) são considerados como parte do Custo Estimado do Operador
valor.

Feito isso, a lista SELECT da segunda consulta aproveita os métodos disponíveis dentro do XQuery, nesta
instância .value. Definimos o caminho para o local dentro do XML do qual desejamos recuperar informações, como a
propriedade @PhysicalOp.

Os resultados do meu sistema são mostrados na Figura 13-4.

Figura 13-4: As três operadoras com maior custo estimado para o


consulta executada com mais frequência.

Consultando o XML para informações de índice ausentes


Vejamos mais um exemplo. Você já viu as informações de índice ausente que estavam presentes no plano de execução
(consulte a Listagem 13-12). Existem Exibições de gerenciamento dinâmico de índice ausente que mostram todos os
possíveis índices ausentes sugeridos encontrados pelo otimizador.
No entanto, esses DMVs não possuem nenhum mecanismo para correlacionar as informações com as consultas
envolvidas. Se quisermos ver as informações de índice ausentes e também a quais consultas elas podem estar
relacionadas, podemos usar a consulta na Listagem 13-14.

COM XMLNAMESPACES
(
PADRÃO 'http://schemas.microsoft.com/sqlserver/2004/07/
showplan'
)
SELECT deqp.query_plan.value(N'(//MissingIndex/@Database)[1]',
'NVARCHAR(256)')
AS DatabaseName,
dest.text AS QueryText,
deqs.total_elapsed_time,
deqs.last_execution_time,

389
Machine Translated by Google

Capítulo 13: O XML dos Planos de Execução

deqs.execution_count,
deqs.total_logical_writes,
deqs.total_logical_reads,
deqs.min_elapsed_time,
deqs.max_elapsed_time,
deqp.query_plan,
deqp.query_plan.value(N'(//MissingIndex/@Table)[1]',
'NVARCHAR(256)')
AS TableName,
deqp.query_plan.value(N'(//MissingIndex/@Schema)[1]',
'NVARCHAR(256)')
AS SchemaName,
deqp.query_plan.value(N'(//MissingIndexGroup/@Impact)[1]', 'DECIMAL(6,4)')

AS ProjectedImpact,
ColumnGroup.value('./@Usage', 'NVARCHAR(256)') AS
ColumnGroupUso,
ColumnGroupColumn.value('./@Name', 'NVARCHAR(256)') AS ColumnName
FROM sys.dm_exec_query_stats AS deqs CROSS APPLY
sys.dm_exec_query_plan(deqs.plan_handle) AS deqp CROSS APPLY
sys.dm_exec_sql_text(deqs.sql_handle) AS dest CROSS APPLY deqp.query_plan.nodes('//
MissingIndexes/

MissingIndexGroup/MissingIndex/ColumnGroup') AS t1(ColumnGroup)
CROSS APPLY t1.ColumnGroup.nodes('./Column') AS
t2(ColumnGroupColumn);

Listagem 13-14

Nos resultados mostrados na Figura 13-5, executei uma versão ligeiramente modificada da Listagem 13-14
para filtrar os resultados para mostrar apenas informações sobre o banco de dados AdventureWorks2014 e
limitar o número de colunas para facilitar a leitura.

Figura 13-5: Sugestões de índice ausentes para o AdventureWorks 2014.

390
Machine Translated by Google

Capítulo 13: O XML dos Planos de Execução

A consulta em sua forma atual retorna várias linhas para as mesmas sugestões de índice ausentes, portanto, nas
linhas 2 a 4, você vê a única sugestão de índice ausente para a consulta da Listagem 13-13. A linha 1 mostra uma
sugestão adicional para um procedimento armazenado que pode precisar de um índice criado nele.

As informações TableName e ColumnName são autoexplicativas. O uso de ColumnGroup está sugerindo onde a
coluna deve ser adicionada ao índice. Um valor EQUALITY ou INEQUALITY está sugerindo que a coluna em
questão seja adicionada à chave do índice. Um valor INCLUDE está sugerindo adicionar essa coluna à cláusula
INCLUDE da instrução de criação de índice. Cada índice sugerido nesta consulta está associado ao QueryText
relevante.

A consulta usa o método .nodes, para o qual fornecemos o caminho para o ColumnGroup
elemento no plano XML armazenado em cache:

'//MissingIndexes/MissingIndexGroup/MissingIndex/ColumnGroup'

Os valores passados para .node aqui garantem que apenas as informações desse caminho completo sejam usadas
para executar o restante das funções .value, que retornam informações sobre o índice, especificamente as
informações TableName, ColumName e ColumnGroupUsage. Com isso, você pode apenas consultar o caminho //
MissingIndexGroup/ e fornecer um valor de propriedade como @Schema para chegar aos dados.

Essa é uma maneira útil de filtrar ou classificar as consultas atualmente em cache que têm sugestões de índice
ausentes, para localizar consultas que precisam de ajuste rápido. No entanto, lembre-se de que nem todas as
consultas de problemas têm índices ausentes e nem todas as consultas com índices ausentes são consultas de
problemas. Por fim, nem todas as consultas com problemas têm garantia de estar no cache quando você executa
a Listagem 13-14.

Muito poucas pessoas vão sentar e escrever suas próprias consultas XQuery para recuperar dados de planos
de execução. Em vez disso, você pode fazer uma consulta como a Listagem 13-14 e depois ajustar para seus
próprios propósitos. A única parte difícil é descobrir como obter o caminho correto. Isso é feito melhor simplesmente
olhando para o XML e percorrendo a árvore para chegar aos valores corretos.

391
Machine Translated by Google

Capítulo 13: O XML dos Planos de Execução

Resumo
Os dados fornecidos nos planos XML estão completos e o arquivo XML é fácil de compartilhar com
outras pessoas. No entanto, ler um plano XML não é uma tarefa fácil e, a menos que você seja o
tipo de profissional de dados que precisa conhecer todos os detalhes internos (a maioria dos quais
estão disponíveis através das propriedades de um plano gráfico), não é aquele que você irá gastar
tempo dominando.

Muito melhor ler os planos em forma gráfica e, se necessário, gastar tempo aprendendo como usar
XQuery para acessar os dados nesses planos programaticamente e, assim, começar a automatizar
o acesso aos seus planos em alguns casos, como a consulta Missing Index mostrada em este
capítulo.

392
Machine Translated by Google

Capítulo 14: Planos para tipos de dados


especiais e cursores
Alguns dos tipos de dados introduzidos no SQL Server ao longo dos anos têm uma funcionalidade bem
diferente do conjunto padrão de números, strings e datas que representam a maioria dos dados com os quais
trabalhamos. Esses tipos de dados têm funcionalidade e indexação especiais que afetam como funcionam e,
quando nossas consultas, procedimentos e funções funcionam com esses tipos de dados, as diferenças podem
aparecer nos planos de execução.

Passaremos grande parte do capítulo examinando planos para consultas que usam XML, já que esse é o tipo de
dados "especial" que a maioria de nós já encontrou em algum momento. Examinaremos os planos que convertem
dados de XML para relacional (OPENXML), de relacional para XML (FOR XML) e aqueles que consultam dados XML
usando XQuery. Não vamos nos aprofundar em nenhum detalhe de ajuste, mas deixarei você saber onde no plano
você pode procurar pistas, se uma consulta que usa XML estiver tendo um desempenho insatisfatório.

O SQL Server 2016 adicionou suporte para JavaScript Object Notation (JSON). Ele não fornece nenhum tipo
de dados específico de JSON (ele armazena dados JSON em um tipo NVARVAR) e, consequentemente, nenhum
dos tipos de métodos disponíveis para o tipo de dados XML. No entanto, ele fornece vários elementos importantes
da linguagem T-SQL para consultar JSON, e veremos como isso afeta os planos de execução.

Também veremos brevemente os planos para consultas que usam o tipo de dados HIERARCHYID. Em seguida,
examinaremos os planos de consultas que acessam dados espaciais , embora apenas suas características básicas,
porque mesmo consultas espaciais bastante simples podem ter planos impressionantemente complexos.

A parte final do capítulo examina os planos para cursores. Eles não se encaixam perfeitamente na categoria de tipo de
dados especial; você não pode armazenar um cursor em uma coluna e, portanto, não é, estritamente, um tipo de
dados, embora a Microsoft use "cursor" como o tipo de dados para uma variável ou parâmetro de saída que faz
referência a um cursor. De qualquer forma, os cursores são certamente especiais, pois são uma construção de
programação que nos permite processar os resultados da consulta uma linha por vez, em vez de da maneira normal e
esperada, baseada em conjuntos. Isso, é claro, afetará o plano de execução, e nem sempre de uma maneira boa.

393
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

XML
XML é um tipo de dados padrão em muitos aplicativos e, às vezes, leva ao armazenamento de XML em bancos
de dados SQL Server, usando o tipo de dados XML. No entanto, se nosso banco de dados simplesmente aceita
entrada XML e a armazena em uma coluna ou variável XML, ou lê uma coluna ou variável XML e a retorna no
formato XML, então, no nível do plano de execução, isso não é diferente de armazenar e recuperar dados de
qualquer outro tipo.

O XML se torna relevante para os planos de execução se consultarmos os dados XML usando XQuery, ou se
uma consulta usar a cláusula FOR XML para converter dados relacionais em XML ou o provedor de conjunto de
linhas OPENXML para passar de XML para relacional.

Esses métodos de acesso e manipulação de XML são muito úteis, mas têm um custo.
A manipulação de XML usa uma combinação de expressões T-SQL e XQuery, e problemas nas partes T-SQL e
XQuery podem afetar o desempenho. Além disso, o analisador XML, que é necessário para manipular XML, usa
ciclos de memória e CPU que você normalmente teria disponível apenas para T-SQL.

No geral, há motivos para ser criterioso no uso e na aplicação de XML em bancos de dados SQL
Server.

Planos para consultas que convertem dados relacionais em XML


(PARA XML)
Usando a cláusula FOR XML em nossas consultas T-SQL, podemos transformar dados relacionais em formato
XML, geralmente para saída para um cliente, mas às vezes para que possamos armazená-los em uma variável
ou coluna XML. Podemos usar a cláusula FOR XML em qualquer um dos quatro modos a seguir, AUTO, RAW,
PATH ou EXPLICIT. Os três primeiros podem ser usados da mesma maneira, criarão um formato diferente de saída
XML da mesma consulta e o plano de execução será o mesmo em cada caso. O quarto modo permite definir
explicitamente, na própria consulta, a forma da árvore XML resultante. Isso requer uma reescrita de consulta e,
portanto, resulta em um plano de execução diferente.

Planos para consultas FOR XML básicas


A Listagem 14-1 mostra uma consulta padrão que produz uma lista de lojas e a pessoa de contato dessa loja.

394
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

SELECT s.Name AS StoreName,


bec.PersonID,
bec.ContactTypeID FROM
Sales.Store AS s
JOIN Person.BusinessEntityContact AS bec
ON s.BusinessEntityID = bec.BusinessEntityID ORDER BY
s.Nome;

Listagem 14-1

O plano resultante é muito simples e não precisa de explicação nesta fase do livro.

Figura 14-1: Plano de execução tradicional como em outras partes do livro.

Para ver o impacto no plano de conversão da saída relacional para um formato XML, simplesmente
adicionamos a cláusula FOR XML à Listagem 14-1.

SELECT s.Name AS StoreName,


bec.PersonID,
bec.ContactTypeID FROM
Sales.Store AS s
JOIN Person.BusinessEntityContact AS bec
ON s.BusinessEntityID = bec.BusinessEntityID ORDER BY
s.Name FOR XML AUTO;

Listagem 14-2

Nesse caso, usei o modo AUTO, mas, independentemente de usar isso, RAW ou PATH, o plano em
cada caso é como mostrado na Figura 14-2.

395
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Figura 14-2: Um plano de execução mostrando a saída para XML por meio do
Operador XML SELECT.

A única diferença visível é que o operador SELECT é substituído por um XML SELECT
operador, e de fato esta é realmente a única diferença. Os planos para a consulta com saída relacional
e para consultas FOR XML com AUTO, RAW ou PATH parecem ser idênticos em todos os aspectos.
No entanto, cada um dos três modos FOR XML produz uma saída XML diferente da mesma consulta,
conforme mostrado abaixo, para a primeira linha do conjunto de resultados, em cada caso.

-- AUTOMÁTICO:

<s StoreName="Uma Associação de Bicicleta">


<bec PersonID="2050" ContactTypeID="11" />
</s>
-- CRU:
<row StoreName="Uma Associação de Bicicleta" PersonID="2050"
ContactTypeID="11" />
-- CAMINHO
<linha>
<StoreName>Uma associação de bicicletas </StoreName>
<PersonID>2050</PersonID>
<ContactTypeID>11</ContactTypeID>
</row>

Cada um desses modos básicos de FOR XML retorna texto formatado como XML. Se quisermos
que os dados sejam retornados no formato XML nativo (como um tipo de dados XML), precisamos
usar a diretiva TYPE. Se você não usar a diretiva TYPE, embora possa parecer XML para você e para
mim, para SQL Server e SSMS, é apenas uma string.

396
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Retornando XML como tipo de dados XML

Uma extensão do modo XML AUTO permite especificar a diretiva TYPE, para gerar os resultados da
consulta como o tipo de dados XML, não simplesmente como texto no formato XML. O tipo
A diretiva é relevante principalmente se você usar subconsultas com FOR XML. A consulta na Listagem
14-3 retorna os mesmos dados da anterior, mas em uma estrutura diferente. Estamos usando a subconsulta
para fazer XML usando TYPE e, em seguida, combinando isso com dados da consulta externa, que é saída
como texto formatado em XML.

SELECT s.Name AS StoreName,


( SELECT bec.BusinessEntityID,
bec.ContactTypeID
FROM Person.BusinessEntityContact AS bec
WHERE bec.BusinessEntityID = s.BusinessEntityID
PARA XML AUTO, TIPO, ELEMENTOS) COMO contato
DE Sales.Store AS s
ORDEM POR s.Nome
PARA XML AUTO;

Listagem 14-3

A Figura 14-3 mostra dois conjuntos de resultados, o primeiro para a consulta conforme escrito na Listagem
14-3 e o segundo para a mesma consulta, mas sem a diretiva TYPE.

Figura 14-3: Saída de FOR XML AUTO, com e sem a diretiva TYPE.

Observe que, no último caso, os colchetes angulares na subconsulta são convertidos em &gt; e &lt; porque
a subconsulta é considerada texto a ser convertido em XML. No primeiro caso, é formatado como XML.

A Figura 14-4 mostra o plano de execução resultante para a Listagem 14-3 (com a diretiva TYPE).

397
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Figura 14-4: Plano de execução para XML AUTO.

Primeiro, vale a pena notar que essa consulta agora causa 1.515 leituras lógicas, cerca de 10 vezes
mais do que a consulta na Listagem 14-2. Isso ocorre porque o otimizador usa uma junção de loops
aninhados aos dados da consulta e da subconsulta e como a entrada externa produz 701 linhas.

A entrada externa retorna as colunas BusinessEntityID e Nome, classificadas por Nome. Os valores
BusinessEntityID são enviados para a entrada interna e vemos 701 buscas, para as linhas correspondentes,
retornando as colunas BusinessEntityID e ContactTypeID.
Vemos então o operador UDX , que neste caso converte cada linha que emerge do Index Seek para
o formato XML.

A Figura 14-5 mostra a janela Propriedades para o operador UDX . A propriedade Name tem o valor FOR
XML, que nos diz que está convertendo dados relacionais em XML. A propriedade Colunas UDX Usadas
mostra quais dados de entrada ela processa. E a lista de saída
contém o nome interno dos dados XML criados, neste caso Expr1002, que consiste nas duas colunas
BusinessEntityID e ContactTypeID da tabela Business EntityContact.

Figura 14-5: Propriedades do operador UDX.

398
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

O operador UDX é frequentemente visto em planos que executam operações XPath e XQuery e, portanto, o
veremos novamente mais adiante neste capítulo.

Por fim, vemos o operador Compute Scalar , que por algum motivo mal definido atribui o valor de Expr1002 a
Expr1004, depois passa Expr1004 para seu pai.

Planos para o modo explícito para consultas XML


O modo XML EXPLICIT existe para as ocasiões em que precisamos exercer um controle muito preciso sobre o
formato do XML gerado pela consulta. A desvantagem é que o conjunto de linhas que nossa consulta produz deve
obedecer a certas regras de formatação. Se você tentar executar a Listagem 14-2 usando o modo EXPLICIT de
FOR XML, verá um erro informando que o formato do seu conjunto de resultados está incorreto.

Msg 6803, Nível 16, Estado 1, Linha 46


FOR XML EXPLICIT requer que a primeira coluna contenha números inteiros
positivos que representam IDs de tags XML.

Portanto, cabe a nós escrever a consulta para que o conjunto de linhas esteja no formato correto, dependendo
da estrutura necessária da saída XML. O modo EXPLICIT é usado para criar XML muito específico, misturando e
combinando propriedades e elementos de qualquer maneira que você escolher com base no que você definir na
consulta. A Listagem 14-4 mostra um exemplo simples.

SELECIONE 1 AS Tag,
NULL AS Pai,
s.Name AS [Store!1!StoreName],
NULL AS [BECContact!2!PersonID],
NULL AS [BECContact!2!ContactTypeID]
A PARTIR DE Vendas.Lojas _
JUNTE Pessoa.BusinessEntityContact AS bec
ON s.BusinessEntityID = bec.BusinessEntityID
UNIÃO TODOS
SELECT 2 AS Tag,
1 AS Pai,
s.Name AS StoreName,
bec.PersonID,
bec.ContactTypeID

399
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

A PARTIR DE Vendas.Lojas _
JUNTE Pessoa.BusinessEntityContact AS bec
ON s.BusinessEntityID = bec.BusinessEntityID
ENCOMENDAR POR [Store!1!StoreName],
[BECContato!2!ID da pessoa]
POR XML EXPLÍCITO;

Listagem 14-4

A Figura 14-6 mostra o plano de execução real para essa consulta, que é um pouco mais
complexa.

Figura 14-6: Plano de execução mostrando como o XML EXPLICIT funciona.

Para construir a hierarquia do XML, tivemos que usar a cláusula UNION ALL em T-SQL, entre duas cópias
quase idênticas da mesma consulta. A execução dupla dessa ramificação a torna cerca de duas vezes mais
cara que o plano para a consulta na Listagem 14-2. Isso não é um resultado direto do uso de FOR XML
EXPLICIT, mas é um resultado indireto dos requisitos que a opção coloca em como escrevemos a consulta.

Assim, enquanto você obtém mais controle sobre a saída XML, isso tem o custo de sobrecarga
adicional, devido à necessidade da cláusula UNION ALL e das regras de formatação explícitas.
Isso leva a um desempenho reduzido devido ao aumento do número de consultas necessárias para reunir
os dados.

Novamente, se você simplesmente executar novamente a consulta sem a cláusula FOR XML
EXPLICIT, a única diferença no plano será um operador XML Select em vez de um Select. Apenas o
formato dos resultados é diferente. Com FOR XML EXPLICIT você obtém XML; sem ele, você obtém um
conjunto de resultados formatado de forma estranha, pois a estrutura que você definiu na consulta UNION
não é naturalmente aninhada, como o XML o faz.

400
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Planos para consultas que convertem XML em dados relacionais


(OPENXML)
Podemos usar OPENXML em nossas consultas T-SQL para "fragmentar" XML em um formato relacional, na
maioria das vezes para obter dados do formato XML e alterá-los para armazenamento estruturado em um banco
de dados normalizado.

O OPENXML pega um documento XML, armazenado em uma variável nvarchar, e o converte em uma "visualização
de conjunto de linhas" desse documento, que pode ser tratada como se fosse uma tabela normal. Por conjunto de
linhas, queremos dizer uma visualização tradicional dos dados em formato tabular, como se estivesse sendo
consultado em uma tabela. Podemos usar OPENXML como fonte de dados em qualquer consulta. Ele pode
substituir uma tabela ou exibição em uma instrução SELECT ou na cláusula FROM de instruções de modificação,
mas não pode ser o destino de INSERT, UPDATE, DELETE ou MERGE.

Para demonstrar isso, precisamos de um documento XML. Eu tive que quebrar elementos em linhas para
apresentar o documento de forma legível.

<RAIZ>
<Moeda CurrencyCode="UTE"
CurrencyName=" Câmbio Transacional Universal ">
<CurrencyRate FromCurrencyCode="USD" ToCurrencyCode="UTE"
CurrencyRateDate="2007/1/1" AverageRate="553"
EndOfDateRate= ."558" />
<CurrencyRate FromCurrencyCode="USD" ToCurrencyCode="UTE"
CurrencyRateDate="2017/6/1/" AverageRate="928"
EndOfDateRate= "1.057" />
</Moeda>
</ROOT>

Listagem 14-5

Neste exemplo, estamos criando uma nova moeda, a Universal Transactional Exchange, também conhecida
como UTE. Precisamos de taxas de câmbio para converter a UTE para USD.
Vamos pegar todos esses dados e inseri-los, em lote, em nosso banco de dados, direto do XML. A Listagem
14-6 mostra o script.

401
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

INICIAR TRAN;
DECLARE @iDoc COMO INTEIRO;
DECLARE @Xml AS NVARCHAR(MAX); SET
@Xml = '<ROOT> <Currency
CurrencyCode="UTE" CurrencyName="Universal Transactional Exchange">
<CurrencyRate FromCurrencyCode="USD" ToCurrencyCode="UTE"

CurrencyRateDate="2007/1/1" AverageRate="553"
EndOfDayRate= ."558" />
<CurrencyRate FromCurrencyCode="USD" ToCurrencyCode="UTE"
CurrencyRateDate="2007/6/1" AverageRate="928"
EndOfDayRate= "1.057" /> </
Currency> </ROOT>'; EXEC
sys.sp_xml_preparedocument @iDoc
OUTPUT, @Xml; INSERIR EM Vendas.Moeda

(Código da moeda,
Nome,
ModifiedDate )

SELECT CurrencyCode,
Nome da moeda,
GETDATA()
FROM OPENXML (@iDoc, 'ROOT/Currency',1)
WITH (CurrencyCode NCHAR(3), CurrencyName NVARCHAR(50));
INSERT INTO Sales.CurrencyRate
(CurrencyRateDate,
FromCurrencyCode,
ToCurrencyCode, AverageRate,
EndOfDayRate , ModifiedDate )

SELECT CurrencyRateDate,
FromCurrencyCode,
ToCurrencyCode,
Taxa média,
Taxa de fim de dia,
GETDATA()
DE OPENXML(@iDoc , 'RAIZ/Moeda/Taxa de Moeda',2)
COM (CurrencyRateDate DATETIME '@CurrencyRateDate',
FromCurrencyCode NCHAR(3) '@FromCurrencyCode',

402
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

ToCurrencyCode NCHAR(3) '@ToCurrencyCode',


AverageRate MONEY '@AverageRate',
EndOfDayRate MONEY '@EndOfDayRate');
EXEC sys.sp_xml_removedocument
@iDoc;
ROLLBACK TRAN;

Listagem 14-6

A partir desta consulta, obtemos dois planos de execução reais, um para cada INSERT. O primeiro INSERTO
é contra a tabela de moedas, conforme mostrado na Figura 14-7.

Figura 14-7: Plano de execução do INSERT em relação à tabela Moeda.

Uma varredura rápida do plano revela um único novo operador, o Remote Scan. Todos os OPENXML
o processamento de instruções é tratado dentro desse operador Remote Scan . Este operador representa
a abertura de um objeto remoto, ou seja, uma DLL ou algum processo externo, como um objeto CLR, dentro
do SQL Server, que pegará o XML e o converterá em um formato dentro da memória que se pareça com o
mecanismo de consulta como linhas normais de dados. Como o Remote Scan não faz parte do próprio
mecanismo de consulta, o otimizador representa a chamada, no plano, como um único ícone.

O único lugar onde podemos realmente ver a evidência do XML é na Lista de Saída para o Remote
Scan. Na Figura 14-8, podemos ver a instrução OPENXML chamada de tabela e as propriedades
selecionadas dos dados XML listados como colunas.

Figura 14-8: Propriedades do operador OPENXML.

403
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

A partir daí, é uma consulta direta com os dados sendo classificados primeiro para inserção no índice
clusterizado e, em seguida, classificados uma segunda vez para adição ao outro índice na tabela.

O principal ponto a ser observado é que o Optimizer usa uma estimativa fixa de 10.000 linhas retornadas para
o Remote Scan, o que explica por que ele decide classificar as linhas primeiro, para tornar a inserção nos
índices mais eficiente, embora neste caso isso seja desnecessário, pois apenas retornar 1 linha. Essa estimativa
fixa afeta outras escolhas do operador que o otimizador faz e, portanto, pode afetar o desempenho.

Também vale a pena notar os diferentes tamanhos de seta que entram e saem do Compute Scalar, que são o
resultado de uma estimativa ruim. Um Compute Scalar nunca realmente faz seu próprio trabalho, então ele só
apresenta contagens de linhas estimadas mesmo em um plano real. O tamanho da seta de entrada reflete as
contagens de linhas reais (1 linha) e a seta de saída reflete a estimativa (10.000 linhas).

O segundo plano de execução descreve o INSERT na tabela CurrencyRate.

Figura 14-9: Plano de execução da tabela CurrencyRate.

Essa consulta é a mais complicada das duas por causa das etapas extras necessárias para a manutenção
da integridade referencial (consulte o Capítulo 6) entre as tabelas Moeda e Taxa de Moeda. Há duas
verificações feitas para isso por causa das colunas FromCurrency e ToCurrency No entanto, ainda não
vemos ícones específicos de XML, já que todo o trabalho XML está oculto por trás da operação Remote
Scan . Nesse caso, vemos duas comparações com a tabela pai, por meio das operações Merge Join . Os
dados são classificados, primeiro por FromCurren cyCode e depois por ToCurrencyCode, para que os dados
sejam usados em um Merge Join, o operador escolhido pelo otimizador porque estimou que 10.000 linhas
seriam retornadas pelo Remote Scan.

Como você pode ver, é fácil trazer dados XML para o banco de dados para uso em nossas consultas ou
para inclusão em nosso banco de dados. No entanto, muito trabalho é feito nos bastidores para fazer isso,
e pouco desse trabalho é visível no plano de execução. Primeiro, o SQL Server precisa chamar a função
sp_xml_preparedocument, que analisa o texto XML usando o analisador MSXML. No entanto, não vemos nada
desse trabalho no plano. Em seguida, ele precisa transformar o documento analisado em um conjunto de linhas,
mas esse trabalho está "oculto" e representado pelo Remote Scan
operador. No entanto, vemos que a contagem de linhas estimada para OPENXML é fixada em 10.000 linhas,
o que pode afetar o desempenho da consulta. Se isso estiver causando problemas de desempenho para você,

404
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

você deve se concentrar em outros mecanismos de manipulação de dados, como carregar primeiro em uma
tabela temporária para obter estatísticas para um plano de execução com melhor desempenho.

Uma ressalva que vale a pena mencionar é que a análise de XML usa muita memória. Você deve
planejar abrir o XML, obter os dados e, em seguida, fechar e desalocar o XML o mais rápido possível.
Isso reduzirá a quantidade de tempo que a memória é alocada em seu sistema.

Planos para consultar XML usando XQuery


A verdadeira força de consultar XML no SQL Server é por meio de XQuery. Examinaremos os planos de
execução para alguns exemplos simples de XQuery, para que você possa começar a ver como a incorporação
de expressões XQuery em nossas consultas T-SQL pode afetar esses planos. No entanto, não podemos cobrir
toda a amplitude e profundidade dos padrões de plano de execução que você pode ver com XQuery (e nem posso
ensinar XQuery; isso exigiria um livro inteiro próprio). Para uma introdução completa, leia este white paper oferecido
pela Microsoft em http://bit.ly/1UH6KfP.

O objetivo de ver como consultar XML, especificamente o XML dentro de planos de execução, é poder pesquisar
valores em muitos planos, em vez de navegar nos próprios planos. Isso pode ser usado em planos em cache,
planos no Repositório de Consultas e planos que são arquivos. Há exemplos de como fazer isso na seção
Consultando o Cache do Plano do Capítulo 13.

Efetivamente, usar XQuery significa uma linguagem de consulta completamente nova para aprender, além do
T-SQL. O tipo de dados XML é o mecanismo usado para fornecer a funcionalidade XQuery por meio do sistema
SQL Server. Quando você deseja consultar o tipo de dados XML, existem cinco métodos básicos:

• .query() – usado para consultar o tipo de dados XML e retornar o tipo de dados XML
• .value() – usado para consultar o tipo de dados XML e retornar um valor escalar não XML
• .nodes() – um método para dinamizar dados XML em linhas
• .exist() – consulta o tipo de dados XML e retorna um bit para indicar se o conjunto de resultados está
vazio ou não, assim como a palavra-chave EXISTS no T-SQL
• .modify() – um método para inserir, atualizar e excluir trechos XML dentro do conjunto de dados XML.

Geralmente, o otimizador parece implementar esses métodos usando dois operadores específicos,
Table-Valued Function (XML Reader), com ou sem filtro XPath, e UDX, combinados em diferentes padrões.

405
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

As várias opções para executar uma consulta em XML, incluindo o uso de FLWOR
(For, Let, Where, Order By e Return) nas consultas, todas afetam os planos de execução. Vou
cobrir apenas dois exemplos, para familiarizá-lo com os conceitos e apresentá-lo ao tipo de planos
de execução que você pode esperar ver. Está fora do escopo deste livro cobrir esse tópico com a
profundidade que seria necessária para demonstrar todos os aspectos dos planos que essa
linguagem gera.

Planos para consultas que usam o método .exist


A coluna Currículo da tabela JobCandidate no AdventureWorks é um tipo de dados XML. Se precisarmos
consultar os currículos de todos os funcionários para descobrir quais das pessoas contratadas já foram
gerentes de vendas, precisaremos usar o método .exist em nossa expressão XQuery, para que nossa
consulta retorne apenas uma linha se o elemento JobTitle do documento contém o texto "Gerente de
Vendas".

SELECT p.LastName, p.FirstName, e.HireDate, e.JobTitle


DE Pessoa.Pessoa AS p
INNER JOIN HumanResources.Employee AS e
ON p.BusinessEntityID = e.BusinessEntityID
INNER JOIN HumanResources.JobCandidate AS jc
ON e.BusinessEntityID = jc.BusinessEntityID
AND jc.Resume.exist(
'declara namespace
res="http://schemas.microsoft.com/sqlserver/2004/07/
aventura-trabalhos/Retomar";
/res:Resume/res:Emprego/res:Emp.JobTitle[contém
(.,"Gerente de Vendas")]') = 1;

Listagem 14-7

A Figura 14-10 mostra o plano de execução real para esta consulta.

406
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Figura 14-10: Plano de execução do método .exist XQuery.

Seguindo o fluxo de dados, da direita para a esquerda, vemos um plano de execução normal. Uma
Verificação de Índice Clusterizado na tabela JobCandidate seguida por um Filtro que garante que
o campo Currículo não seja nulo. Uma junção de loops aninhados combina esses dados da tabela
JobCandidate filtrada com os dados retornados da tabela Employee, filtrando-nos para
duas filas.

Em seguida, outro operador Nested Loops é usado para combinar dados de um novo operador, um
operador Table Valued Function , com o subtítulo "Leitor XML com filtro XPath", que representa como
dados relacionais a saída do XQuery. A função que desempenha não é diferente da operação Remote
Scan da consulta OPENXML. No entanto, a Função com Valor de Tabela, diferentemente do Remote
Scan no exemplo anterior, faz parte do mecanismo de consulta e é representada por um ícone distinto. Ao
contrário de uma função com valor de tabela de várias instruções, as funções com valor de tabela usadas
pelo XQuery não têm um plano que podemos acessar por meio do cache ou do armazenamento de
consultas ou pela captura de um plano estimado. Sua execução é puramente interna.

As propriedades da Table Valued Function mostram que o operador foi executado duas vezes e quatro
linhas foram retornadas.

407
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Figura 14-11: Propriedades da Função de Valor da Tabela mostrando a operação XML.

Essas linhas são passadas para um operador Filter . Dois valores são definidos pela Table
Valued Function, value e lvalue. Não está completamente claro como isso funciona, mas o operador
Filter determina se a consulta XPath que definimos é igual a 1 e é NOT NULL (e o NOT NULL
check não é necessário, mas está lá). Isso resulta em uma única linha para saída para o
operador de loops aninhados . A partir daí, é um plano de execução típico, recuperando dados do Contact
tabela e combinando-a com o restante dos dados já reunidos.

Planos para consultas que usam o método .query


O método .query retorna XML. Usamos isso se, em vez de simplesmente filtrar com base no
XML, quisermos retornar alguns ou todos os XML que estamos consultando. Em nosso exemplo,
consultaremos dados demográficos para encontrar lojas com mais de 20.000 pés quadrados.
Temos que definir a estrutura XML a ser retornada e, para isso, a consulta utiliza as expressões
FLWOR do XQuery : For, Let Where, Order, Return.

Neste exemplo, precisamos gerar uma lista de lojas gerenciadas por um determinado vendedor.
Especificamente, queremos analisar qualquer demografia de lojas gerenciadas por esse
vendedor que tenha mais de 20.000 pés quadrados, onde essas lojas registraram qualquer
informação demográfica. Também listaremos as lojas que não o possuem. As informações
demográficas são dados semiestruturados, portanto, são armazenadas em XML no banco de dados.
Para filtrar o XML diretamente, usaremos o método .query. A Listagem 14-8 mostra nosso exemplo
de consulta e plano de execução.

408
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

SELECT s.Nome,
s.Demographics.query
('
declare namespace ss="http://schemas.microsoft.com/
sqlserver/2004/07/adventure-works/StoreSurvey";
por $s em /ss:StoreSurvey
onde ss:StoreSurvey/ss:SquareFeet > 20000
retornar $ s
') AS Demografia
DE Sales.Store AS s
WHERE s.SalesPersonID = 279;

Listagem 14-8

A Figura 14-12 mostra o plano.

Figura 14-12: Plano de execução completo para o método .query XQuery.

O T-SQL consiste em duas consultas:

• uma consulta T-SQL regular na tabela Store para retornar as linhas onde o
SalesPersonId = 279

• uma expressão XQuery que usa o método .query para retornar os dados em que a metragem quadrada da
loja era superior a 20.000.

Dito dessa forma, parece simples, mas é necessário muito mais trabalho em torno dessas duas consultas para chegar
a um conjunto de resultados.

Vamos dividir esse plano de execução em três partes, cada uma com responsabilidades separadas: uma para a
parte relacional da consulta, a segunda para ler e filtrar os dados XML de acordo com a expressão XQuery e a
terceira para pegar os dados e convertê-lo de volta em XML adequado.

409
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Primeiro, a Figura 14-13 mostra a parte superior esquerda do plano, que contém as partes padrão da
consulta que está recuperando informações da tabela Store.

Figura 14-13: Explosão do plano mostrando o acesso tradicional a dados.

O fluxo de dados começa com uma Verificação de Índice Clusterizado na tabela Sales, filtrada pelo
SalesPersonId. Os dados retornados são alimentados na metade superior de um loop aninhado, junção
externa esquerda. Esse operador de loops aninhados chama sua entrada inferior (a seção do plano na
Figura 14-13) para cada linha, empurrando os dados (valores das colunas BusinessEntityID e
Demographics) da entrada superior para a entrada inferior, conforme visto na Propriedade de referências
externas . O resultado dessa entrada inferior é então combinado com os dados lidos da tabela Stores e
retornado ao cliente.

Indo para a direita para encontrar o segundo fluxo de dados para a junção, encontramos três operadores
de busca de índice clusterizado familiares , mas desta vez, porém, eles estão acessando um índice
clusterizado XML. A Figura 14-14 mostra uma ampliação dessa parte do plano.

Figura 14-14: Explosão do plano mostrando o uso do índice XML para a instrução XQuery.

Os dados no tipo de dados XML são armazenados separadamente do restante da tabela e há um índice
XML disponível. As três buscas e a forma como são combinadas são um artefato de como os dados XML
são codificados em índices XML, e não vou me aprofundar nisso. O operador Clustered Index Seek no
canto superior direito recupera esses dados, usando os valores empurrados dos loops aninhados discutidos
anteriormente.

410
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Figura 14-15: Propriedades da Busca de Índice mostrando o acesso a dados XML.

Você pode ver na Figura 14-15 que a busca está ocorrendo em PXML_Store_Demographics, retornando
as 80 linhas do índice que correspondem à coluna BusinessEntityId da tabela de armazenamento. Você
também pode ver a saída das colunas dos nós de índice XML. Essas informações permitem que você
entenda melhor como o SQL Server está recuperando o XML do índice em questão.

O operador Filter implementa a parte WHERE da expressão FLWOR na expressão XQuery. Seu
predicado mostra que testa uma coluna chamada "valor", extraída do XML, contra o valor 20.000, pois
estamos retornando apenas lojas com metragem quadrada maior que esse valor. Isso ilustra que nem
toda lógica FLWOR é inserida em um operador especial relacionado a XML, como vimos em exemplos
anteriores. Partes da expressão XQuery são

411
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

avaliados como se fossem expressões relacionais. Aqui, o mecanismo extrai dados do XML, tornando-
o relacional, opera nele usando os operadores normais e depois os coloca de volta no formato XML.

O resultado deste fragmento do plano são dados extraídos da coluna XML e manipulados de
acordo com a expressão XQuery, mas apresentados como um conjunto de linhas, ou seja,
em formato relacional.

A terceira parte do plano faz a conversão para XML. Você pode ver esta seção ampliada na Figura 14-16.

Figura 14-16: Explosão do plano mostrando a conversão para XML.

O Compute Scalar faz algum trabalho de preparação para o operador UDX , que converte as
informações de dados recuperadas por meio das operações definidas acima de volta para o formato
XML. Essa, na verdade, é a parte final da parte do plano relacionada ao XML. O operador Filter usa
uma propriedade Startup Expression Predicate para suprimir a execução de toda a subárvore deste
plano para quaisquer linhas com um valor NULL na coluna XML (ou seja, para os dados Demográficos),
evitando perda desnecessária de desempenho.

Tudo isso é combinado com as linhas originais retornadas da tabela Store por meio do operador
Nested Loops na Figura 14-13.

Quando usar XQuery


Esses exemplos mostram que todos os operadores familiares e alguns operadores novos são
combinados para implementar XQuery, mas que uma cobertura completa está além do escopo deste
livro. XQuery pode substituir o FOR XML, mas você pode ver alguma degradação no desempenho.

Você também pode usar XQuery no lugar de OPENXML. A funcionalidade fornecida pelo XQuery
vai além do que é possível no OPENXML. Combinar isso com o T-SQL será uma combinação poderosa
quando você precisar manipular dados XML no SQL Server. Como em todo o resto, teste a solução com
todas as ferramentas possíveis para garantir que você esteja usando a solução ideal para sua situação.

412
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Notação de Objeto JavaScript


JavaScript Object Notation (JSON), um formato de arquivo de padrão aberto usando texto legível por
humanos, é suportado pelo SQL Server e Banco de Dados SQL do Azure a partir do SQL Server 2016.
Existem mecanismos de armazenamento e recuperação incorporados ao SQL Server para lidar com dados JSON .
Não vamos explorar todas essas informações aqui. Veremos um exemplo de consulta JSON, pois resulta
em diferenças nos planos de execução gerados, portanto, você precisa saber o que procurar. Para um
exame mais detalhado da funcionalidade JSON completa no SQL Server, consulte a documentação da
Microsoft: https://bit.ly/2qCP8Mx.

Infelizmente, a versão atual do AdventureWorks não tem nenhum dado JSON, então devemos primeiro
construir alguns.

SELECT p.BusinessEntityID,
p.Título,
p.Nome,
p.Sobrenome,
( SELECT p2.FirstName AS "person.name,"
p2.LastName AS "pessoa.sobrenome,"
p2.Título,
p2.BusinessEntityID
DE Pessoa.Pessoa AS p2
ONDE p.BusinessEntityID = p2.BusinessEntityID
PARA CAMINHO JSON) AS JsonData
INTO dbo.PersonJson
DE Pessoa.Pessoa AS p;

Listagem 14-9

Essa consulta move os dados para uma tabela chamada dbo.PersonJson. Incluí os dados regulares e os
dados JSON, apenas para que você possa ver a conversão se executar consultas nela. Isso está usando o
comando JSON PATH para chegar a dados JSON definidos, semelhante a como usaríamos o comando
XML PATH.

Isso não apenas carregará dados na tabela e converterá alguns deles em JSON, mas também podemos
examinar o plano de execução dessa consulta para ver a formatação JSON em ação.

413
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Figura 14-17: Plano de execução mostrando o operador UDX para JSON PATH.

Essa consulta processa 19.000 linhas, além de convertê-las em dados JSON, portanto, é um plano de alto
custo, o que explica por que o otimizador o paralelizou.

Há apenas dois pontos principais de nota. Primeiro, um Spool de Índice foi usado para garantir que a
Verificação de Índice Agrupado não fosse usada repetidamente. Você pode verificar isso observando
os valores de Contagem de Execução em ambos os operadores Clustered Index Scan , que têm um valor
de 1. O próprio Spool de Índice tem um valor de 19.972, uma vez para cada linha. Em seguida, o operador UDX .

Nesse caso, o operador UDX está atendendo às necessidades da operação JSON PATH. Podemos
validar isso olhando para as propriedades. O valor do nome é PARA JSON. Esse é o único indicador
que temos do que está ocorrendo dentro deste operador. Ele gera uma expressão, Expr1005, mas
não há outras definições fornecidas. Você pode ver tudo isso na Figura 14-18.

Figura 14-18: FOR JSON expresso no operador UDX.

414
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

O operador Compute Scalar executa algum tipo de conversão em Expr1005 para criar Expr1007 e, em seguida,
o operador Table Insert insere as linhas na coluna JsonData.

Figura 14-19: Operador escalar executando uma operação final para criar dados JSON.

Não podemos ver nenhuma das operações JSON em funcionamento com base nas propriedades dos operadores.
Só sabemos que um operador é FOR JSON e o outro faz algum tipo de conversão.
Nada mais é claro.

Também podemos ver evidências de consultas JSON em funcionamento. A Listagem 14-10 mostra como podemos
recuperar os dados JSON da tabela.

SELECT oj.FirstName,
oj.LastName,
oj.Título
FROM dbo.PersonJson AS pj
APLICAÇÃO CRUZADA

OPENJSON(pj.JsonData,
N'$')
WITH (Nome VARCHAR(50) N'$.pessoa.nome',
Sobrenome VARCHAR(50) N'$.pessoa.sobrenome',
Título VARCHAR(8) N'$.Title',
BusinessEntityID INT N'$.BusinessEntityID') AS oj
WHERE oj.BusinessEntityID = 42;

Listagem 14-10

Podemos simplesmente consultar esses dados das colunas dentro da tabela, mas o objetivo aqui é mostrar
OPENJSON no trabalho, então usamos isso. O plano de execução resultante é bastante interessante.

415
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Figura 14-20: Execução para consulta OPEN JSON.

O operador Table Scan é usado porque a tabela em questão, dbo.PersonJson, não possui índice,
portanto, não há outra maneira de recuperar os dados. Uma junção de loops aninhados une os dados da
tabela aos dados produzidos por chamadas para uma função, Função com valor de tabela
(OPENJSON_EXPLICIT). A escolha da junção de loops aninhados pode parecer surpreendente, uma vez
que o Table Scan retorna uma estimativa (e real) de 19.972 linhas, o que significa 19.972 execuções de
uma função com valor de tabela relativamente cara. A razão é simplesmente porque o otimizador não tem
escolha neste caso. Essa consulta usa um CROSS APPLY e a entrada interna produz linhas diferentes para
cada linha da entrada externa. A única maneira de o otimizador implementar isso nas versões atuais é com um
loop aninhado.

O otimizador usa uma estimativa fixa de 50 linhas retornadas, por execução da função com valor de tabela
JSON. Na verdade, ele retorna uma linha por execução. Essas linhas não representam uma linha de tabela,
mas uma nova linha de dados, com os dados JSON selecionados extraídos como uma coluna relacional no
conjunto de linhas. O operador Filter elimina todas as linhas, exceto aquelas que correspondem ao nosso WHERE
valor da cláusula na coluna BusinessEntityID de 42. Em outras palavras, ele fragmenta todo o JSON em
todas as linhas antes de aplicar o filtro quando, é claro, o que preferimos fazer é empurrar o predicado para
baixo e destruir apenas as linhas necessárias !

Se abrirmos as propriedades da Table Valued Function, podemos ver algumas das atividades JSON
em ação. Primeiro, na parte inferior das propriedades, vemos os valores da Lista de Parâmetros , conforme
mostrado na Figura 14-21.

416
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Figura 14-21: Valores de parâmetro para a função de valor da tabela OPENJSON.

Na parte superior, estamos passando a string JSON completa. Em seguida, passamos a operação de caminho e cada
um dos valores que estamos recuperando. Não podemos ver como esses valores de parâmetro são usados
internamente, mas podemos ver que as definições são muito claras.

Você também pode ver os Valores Definidos da função, conforme mostrado na Figura 14-22.

417
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Figura 14-22: Valores definidos na função de valor da tabela OPEN JSON.

Esses são os aliases definidos na cláusula WITH do comando OPEN JSON na Listagem 14-10. Esses
também são os nomes das colunas usadas na Lista de Saída da operação.

Não há outras indicações de exatamente como os dados JSON são convertidos no SQL Server além
dessas dicas que você pode ver no plano de execução. Com essas informações, você pode observar os
efeitos do JSON em suas consultas. Nesse caso, discutimos várias causas de preocupação: a necessidade
de usar uma junção de loops aninhados ineficiente para o CROSS APPLY, a estimativa fixa de 50 linhas
retornadas pela função com valor de tabela OPEN JSON e a necessidade de fragmentar o JSON para cada
linha, antes de filtrar. Para ajudar com o último, você pode considerar usar uma coluna computada persistente
e indexá-la para os dados JSON que são usados com mais frequência em filtros.

Dados hierárquicos
O SQL Server pode armazenar dados hierárquicos usando HIERARCHYID, um tipo de dados introduzido no
SQL Server 2008 (implementado como um tipo de dados CLR). Ele não armazena dados hierárquicos
automaticamente; você deve definir esse armazenamento a partir de seus aplicativos e código T-SQL, à
medida que usa o tipo de dados. Como um tipo de dados CLR, ele vem com várias funções para recuperar
e manipular os dados. Novamente, esta seção simplesmente demonstra como as operações de dados
hierárquicos aparecem em um plano de execução; não é uma visão geral exaustiva do tipo de dados.

A Listagem 14-11 mostra uma lista simples de funcionários atribuídos a um determinado gerente.
Mantive intencionalmente a consulta simples para que possamos nos concentrar na atividade do
HIERARCHYID dentro do plano de execução e não precisar nos preocupar com outros problemas
relacionados à consulta.

DECLARE @ManagerID HIERARCHYID;


SELECT @ManagerID = e.OrganizationNode
FROM HumanResources.Employee AS e
WHERE e.JobTitle = 'Vice-Presidente de Engenharia';
SELECT e.BusinessEntityID, p.LastName

418
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

FROM HumanResources.Employee AS e
JUNTE -SE Pessoa.Pessoa AS p
ON e.BusinessEntityID = p.BusinessEntityID
WHERE e.OrganizationNode.IsDescendantOf(@ManagerID) = 1;

Listagem 14-11

A Figura 14-23 mostra o plano de execução.

Figura 14-23: Plano de execução para dados de hierarquia.

Como você pode ver, é um plano muito simples e limpo. O otimizador pode usar um índice na coluna
HIERARCHYID, OrganizationNode, para realizar uma Busca de Índice. Os dados então fluem para o operador Nested
Loops , que recupera os dados conforme necessário por meio de uma série de operações Clustered Index Seek na
tabela Person.Person, para recuperar os dados adicionais solicitados. O aspecto interessante desse plano é o
Predicado Seek do operador Index Seek , conforme mostrado na Figura 14-24.

Figura 14-24: Propriedades de busca de índice mostrando a filtragem de hierarquia no trabalho.

Agora você pode ver algumas das operações internas executadas pelo tipo de dados CLR. O predicado fornece
os parâmetros Start e End, ambos funcionando a partir de mecanismos dentro da operação HIERARCHYID. O
índice é apenas um índice normal, e o HIERARCHYID

419
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

coluna, OrganizationNode, é apenas uma coluna varbinary no que diz respeito ao Index Seek. O trabalho é
feito por funções internas, como o DescendantLimit que vemos nas propriedades Index Seek na Figura 14-24,
que encontra o valor varbinary apropriado.

Se eu tivesse executado a consulta e adicionado uma coluna extra à lista SELECT, como JobTitle da tabela
HumanResources.Employee, a consulta teria mudado para um Clustered Index Scan ou para Index Seek
and Key Lookup, dependendo das estimativas de custo , já que o índice em OrganizationNode não seria mais
um índice de cobertura.

Poderíamos explorar algumas outras funções com o tipo de dados HIERARCHYID, mas isso dá uma ideia
razoável de como ele se manifesta nos planos de execução, então vamos passar para uma discussão sobre outro
dos tipos de dados CLR, dados espaciais.

Dados espaciais

O tipo de dados espaciais apresenta dois tipos diferentes de armazenamento de informações. O primeiro é o
conceito de formas geométricas e o segundo são dados mapeados para uma projeção da superfície da Terra.
Há um grande número de funções e métodos associados a tipos de dados espaciais e simplesmente não temos
espaço para abordar tudo isso em detalhes neste livro. Para uma introdução detalhada aos dados espaciais,
recomendo o Pro Spatial com SQL Server 2012 (Apress) de Alastair Aitchison.

Assim como o tipo de dados HIERARCHYID, existem índices associados a dados espaciais, mas esses
índices são de natureza extremamente complexa. Ao contrário de um índice clusterizado ou não clusterizado
no SQL Server, esses índices podem (e funcionam) funcionar com funções, mas não com todas as funções. A
Listagem 14-12 mostra uma consulta que pode resultar no uso de um índice espacial, se existir, em um banco
de dados SQL Server.

DECLARE @MyLocation GEOGRAPHY = geografia::STPointFromText('POI


NT(-122,33383 47,610870)', 4326);
SELECT a.AddressLine1,
Uma cidade,
a.Código Postal,
a.Localização Espacial
DE Pessoa.Endereço AS a
WHERE @MyLocation.STDistance(a.SpatialLocation) < 1000;

Listagem 14-12

420
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Essa consulta cria uma variável GEOGRAPHY e a preenche com um ponto específico do globo, que coincide
com o Seattle Sheraton, perto de onde, na maioria dos anos, o PASS hospeda seu Summit anual. Em seguida,
ele usa o cálculo STDistance nessa variável para localizar todos os endereços no banco de dados que estão
dentro de um quilômetro (1.000 metros) desse local.

A Figura 14-25 mostra o plano que, na ausência de um índice útil, é apenas uma Varredura de Índice
Agrupado e depois um Filtro. Se revisássemos as propriedades do operador SELECT , veríamos que o custo
estimado da subárvore para o plano é 19,9.

Figura 14-25: Planeje uma consulta espacial sem índice espacial.

Vamos agora criar um índice espacial na tabela Address para nossa consulta espacial usar, conforme mostrado
na Listagem 14-13.

CRIAR ÍNDICE ESPACIAL TesteSpatial


ON Pessoa.Endereço (Localização Espacial)
USANDO GEOGRAPHY_GRID
COM (GRADES = (NÍVEL_1 = MÉDIO, NÍVEL_2 = MÉDIO, NÍVEL_3 =
MÉDIO, NÍVEL_4 = MÉDIO),
CELLS_PER_OBJECT = 16,
PAD_INDEX = DESLIGADO,
SORT_IN_TEMPDB = OFF,
DROP_EXISTING = OFF,
ALLOW_ROW_LOCKS = ATIVADO,
ALLOW_PAGE_LOCKS = ATIVADO)
EM [PRIMÁRIO];
VAI

Listagem 14-13

Execute novamente a Listagem 14-12 e você verá um plano de execução bastante grande e complicado,
quando você considerar que estamos consultando uma única tabela, embora o custo estimado do plano seja
muito menor, de 19,9 para 0,67.

421
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Figura 14-26: Plano de execução complexo usando um índice espacial para recuperar dados.

Dizer que os índices espaciais são complicados não começa a descrever o que está acontecendo. Você
pode ver que, apesar de uma simples consulta, uma tonelada de atividade está ocorrendo. Teremos que
dividir isso em pedaços menores para entender. A Figura 14-27 concentra-se nos operadores que recuperam
o conjunto inicial de dados do disco.

Figura 14-27: Explosão do plano mostrando o acesso aos dados.

A função com valor de tabela, chamada GetGeographyTessellation_VarBinary, está recuperando


informações usando um processo chamado tesselação. Consiste em blocos de informações definidos pelo
nosso raio de 1000 metros em torno de um único ponto. Você pode ver os valores dos parâmetros passados
observando as propriedades conforme mostrado na Figura 14-28.

Figura 14-28: Parâmetros para tesselação dentro da Função de Valor da Tabela.

422
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Sem entrar nos detalhes de exatamente como os dados geográficos são armazenados e recuperados, esta função
reflete as configurações do índice que criamos anteriormente e mostra, no valor final do parâmetro, como o limite
de 1000 metros está sendo fornecido à função que recupera um conjunto inicial de dados. Você tem uma ideia da
complexidade de acessar índices espaciais por causa disso. Podemos até ir mais longe. No lado esquerdo do plano
na Figura 14-27 podemos ver que os valores gerados são usados para realizar a Busca de Índice Agrupado
(Spatial) em relação ao armazenamento adicional criado como parte do índice espacial. Essa busca não é igual a
outras que vimos antes, que geralmente consistem em um simples operador de comparação, como você pode ver
observando os Predicados de Busca na Figura 14-29.

Figura 14-29: Os Predicados de Busca contra o Índice Espacial.

O número de operadores envolvidos torna este plano mais complicado. Ele reflete todo o trabalho necessário
para satisfazer um tipo de dados diferente, dados espaciais.

Para limpar, você pode descartar o índice criado anteriormente.

DROP INDEX TestSpatial ON Person.Address;

Listagem 14-14

Embora essas funções espaciais sejam complexas e exijam muito mais conhecimento para serem usadas, você
pode ver que os planos de execução ainda usam as mesmas ferramentas para entender essas operações,
embora em configurações muito complexas, dificultando a solução de problemas dessas consultas.

423
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Cursores
Os cursores, apesar de serem definidos no T-SQL, não são tipos de dados. Eles representam um tipo
diferente de comportamento de processamento. A maioria das operações em um banco de dados SQL Server
deve ser definida com base, em vez de usar o processamento processual linha por linha incorporado por cursores.
No entanto, ainda haverá ocasiões em que um cursor é a maneira mais apropriada ou mais rápida de
resolver um problema, e pode haver momentos em que você não tenha tempo para substituir o cursor por uma
solução baseada em conjunto, mas precisará investigar problemas com este código.

Embora existam alguns operadores específicos do cursor, principalmente o otimizador usa os mesmos
operadores fazendo as mesmas coisas que já vimos no restante do livro. No entanto, os operadores exibem de
forma diferente entre os planos estimados e reais.

Cursor estático
Começaremos com o tipo mais simples de cursor, um cursor estático. Isso é o mais fácil de entender porque
os dados dentro do cursor não podem ser alterados, por isso simplifica o processamento de forma bastante
radical. A Listagem 14-15 define o primeiro cursor.

DECLARE CurrencyList CURSOR STATIC FOR


SELECT c.CurrencyCode, cr.Name
DE Vendas. Moeda AS c
JOIN Sales.CountryRegionCurrency AS crc
ON crc.CurrencyCode = c.CurrencyCode
JOIN Pessoa.PaísRegião AS cr
ON cr.CountryRegionCode = crc.CountryRegionCode
WHERE c.Name LIKE '%Dólar%';
ABRIR lista de moedas;
BUSCAR PRÓXIMO DE CurrencyList;
WHILE @@Fetch_Status = 0
COMEÇAR

-- Normalmente haveria operações aqui usando dados de


cursor
BUSCAR PRÓXIMO DE CurrencyList;
FIM;
FECHAR Lista de moedas;
DEALLOCATE CurrencyList;
VAI

Listagem 14-15

424
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Na Listagem 14-15, não faço nada com o cursor. Ele não processa dados nem executa outras ações comumente
associadas a cursores. Isso é simplesmente para que possamos focar apenas nas ações do próprio cursor, dentro dos
planos de execução.

Capture o plano estimado para a Listagem 14-15.

Figura 14-30: Plano estimado para todas as declarações que definem o uso do cursor.

No plano estimado, a maioria dos operadores de cursor é representada usando um ícone de espaço reservado.
A instrução declare mostra o plano que será usado; este é o primeiro plano de execução que você vê na parte
superior da Figura 14-30 e mostra como o cursor será satisfeito, conforme definido na Listagem 14-15.

O plano para a instrução DECLARE CURSOR mostra como o cursor será preenchido e acessado com base nas outras
instruções da Listagem 14-15. Vamos nos concentrar apenas no plano principal para começar. A Figura 14-31 mostra
uma pequena parte do plano.

425
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Figura 14-31: Definição do cursor mostrando as consultas Population e Fetch.

Como você pode ver, temos um operador inicial mostrando que tipo de cursor temos, Snapshot
nesse caso. Este operador é muito parecido com o operador SELECT ; ele contém informações sobre o
cursor que estamos definindo. A Figura 14-32 mostra as propriedades desta operação, fornecendo uma
definição completa do cursor.

Figura 14-32: Propriedades do operador Snapshot cursor.

A verdadeira mágica dos cursores está nos próximos dois operadores mostrados na Figura 14-31, os
operadores Population Query e Fetch Query .

A Consulta de População representa o plano do otimizador para executar a consulta que coletará o
conjunto de dados a ser processado pelo cursor. Isso é executado quando abrimos o cursor e, em seguida,
o Fetch Query representa o plano do otimizador para buscar cada uma das linhas, e isso é executado uma
vez para cada instrução FETCH.

426
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Nesse caso, como um cursor estático não deve mostrar nenhuma alteração feita posteriormente, o OPEN
A instrução simplesmente executa a consulta e armazena os resultados em uma tabela temporária e FETCH
em seguida, recupera as linhas dessa tabela temporária. Outros tipos de cursores usam a mesma ideia
básica de combinar uma Consulta de População e uma Consulta de Busca, mas modificada para acomodar
o tipo de cursor solicitado, como veremos mais adiante.

Cada um desses operadores tem propriedades que definem a consulta, mais uma vez,
semelhante a como o operador SELECT funcionaria. A Figura 14-33 mostra as propriedades do
operador Population Query.

Figura 14-33: Propriedades do operador Population Query.

Você usaria esses dados da mesma maneira que usaria as informações no SELECT
operador. Ele fornece as informações necessárias para entender algumas das escolhas feitas pelo otimizador,
assim como em outros planos.

Com o entendimento de que existem duas consultas em funcionamento, vamos ver as definições dessas consultas
conforme expressas pelos planos de execução que definem esse cursor, começando pela primeira, a Consulta de
População. Neste caso, está realizando duas ações. Primeiro, ele está recuperando dados do disco, conforme
mostrado na Figura 14-34.

427
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Figura 14-34: Recuperação de dados para a Consulta de População do Cursor Estático.

É um plano de execução muito simples que resolve a consulta na Listagem 14-15. As partes
interessantes do plano de execução vêm após a definição do conjunto de dados, conforme mostrado na
Figura 14-35.

Figura 14-35: Criação de armazenamento temporário para o Cursor Estático.

Incluí o operador Population Query e o operador Nested Loops como marcadores para a parte interessante
das operações, para que fique mais claro exatamente onde elas estão ocorrendo.

Depois que os dados são recuperados e unidos, vemos os operadores Segment and Sequence Project
(Compute Scalar) , que vimos no Capítulo 5 ao discutir os planos para as funções do Windows. Nesse
caso, a propriedade Agrupar por do Segmento está vazia, portanto, toda a entrada é considerada um único
segmento.

O operador Sequence Project (Compute Scalar) , que é usado pelas funções de classificação, funciona
com um conjunto ordenado de dados, com marcas de segmento adicionadas pelo operador Segment . Nesse
caso, está adicionando um número de linha com base nos valores segmentados, contando de zero cada vez
que o segmento muda. Aqui, porém, há apenas um único segmento. Mais uma vez, podemos ver isso nas
propriedades mostradas na Figura 14-36.

Figura 14-36: Adicionando uma coluna row_number ao conjunto de dados.

428
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

O que isso fez foi criar uma chave primária artificial no conjunto de resultados de nossos dados para o cursor em
questão. Todos os dados são então adicionados a um índice clusterizado temporário, CWT_Prima ryKey. Tudo
isso aconteceu no tempdb, como podemos ver nas propriedades mostradas na Figura 14-37.

Figura 14-37: A localização do objeto CWT_PrimaryKey é tempdb.

Conforme observado anteriormente, o comando FETCH simplesmente recupera as linhas desse índice clusterizado.
Seu objetivo é evitar a necessidade de voltar aos dados repetidamente, por meio de uma consulta padrão,
funcionando como vimos os operadores de spool em outros planos.

Figura 14-38: Plano de execução de busca de consulta definido.

O restante dos operadores no plano estimado, na Figura 14-30, são vários processos dentro das operações do
cursor; ABRIR, BUSCAR, FECHAR e DEALLOCATE. Cada um deles é representado pelo operador Cursor catch-all,
mostrado na Figura 14-39.

Figura 14-39: Operador catch-all do cursor mostrado no comando FETCH NEXT.

Esses operadores só estarão visíveis no plano estimado. As propriedades do operador não revelam nenhuma
informação útil na maioria dos casos, pois simplesmente representam o comando do cursor em questão, como o
comando FETCH NEXT na Figura 14-39.

Também podemos capturar planos reais para um cursor. Se você fizer isso, porém, esteja pronto para lidar com o
fato de que você receberá vários planos. Nesse caso, um para a Consulta de população e um para cada linha de
dados da Consulta de busca. Será algo como a Figura 14-40.

429
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Figura 14-40: Planos reais para um cursor estático.

Como esperado, com base no que vimos nos planos estimados, os dados são recuperados e colocados em um
índice clusterizado e, em seguida, esse índice clusterizado é usado repetidamente à medida que buscamos os
dados. O único outro ponto de interesse para o plano real é como o operador SELECT foi novamente substituído,
primeiro por um operador OPEN CURSOR e depois por vários FETCH CURSOR
operadores. No entanto, as informações dentro de cada um deles são as mesmas encontradas no operador
SELECT , incluindo informações interessantes como as opções Compile Time, Query Hash e Set .

Capturar planos reais para cursores é uma operação cara e provavelmente não deve ser feita na maioria das
circunstâncias. Em vez disso, use Eventos Estendidos para capturar uma única execução de uma das consultas
ou use SET STATISTICS XML ON para uma única instrução.

Vamos ver como o comportamento dos planos muda conforme usamos diferentes tipos de cursores.

430
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Cursor de conjunto de teclas

Um cursor de conjunto de chaves recupera um conjunto de chaves para os dados em questão. Isso é muito
diferente do que vimos com o cursor Estático acima. Os cursores de conjunto de chaves não devem mostrar
novas linhas, mas devem mostrar novos dados se as atualizações simultâneas modificarem as linhas existentes.
Para conseguir isso, o Population Query armazenará os valores de chave na tabela temporária e o Fetch Query
usa esses valores de chave para recuperar os valores atuais nessas linhas. Nossa consulta agora será
semelhante à Listagem 14-16.

DECLARE O CONJUNTO DE TECLAS DE CURSOR DA LISTA DE MOEDA


POR
SELECT c.CurrencyCode,
cr.Nome
DE Vendas. Moeda AS c
JOIN Sales.CountryRegionCurrency AS crc
ON crc.CurrencyCode = c.CurrencyCode
JOIN Pessoa.PaísRegião AS cr
ON cr.CountryRegionCode = crc.CountryRegionCode
WHERE c.Name LIKE '%Dólar%';
ABRIR lista de moedas;
BUSCAR PRÓXIMO DE CurrencyList;
WHILE @@FETCH_STATUS = 0
COMEÇAR

-- Normalmente haveria operações aqui usando dados do cursor


BUSCAR PRÓXIMO DE CurrencyList;
FIM
FECHAR Lista de moedas;
DEALLOCATE CurrencyList;
VAI

Listagem 14-16

Se capturarmos um plano estimado para esse conjunto de consultas, veremos novamente um plano que define
o cursor e uma série de planos abrangentes para o restante das instruções de suporte para as operações do
cursor. Vamos nos concentrar aqui apenas na definição dos cursores. O plano completo é mostrado na Figura
14-41.

431
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Figura 14-41: Plano para definir um cursor Keyset.

Mais uma vez, o plano para a instrução DECLARE CURSOR mostra a Consulta de População e a Consulta
de Busca. As diferenças estão no comportamento fundamental. Começaremos com a parte do plano que
recupera os dados para a Consulta de população.

Figura 14-42: Recuperação de dados para a Consulta de População do cursor Keyset.

Novamente, este plano de execução não introduz nada que não tenhamos visto em outras partes do livro.
A única coisa muito importante a ser observada, porém, é que esse plano para recuperação de dados é
diferente do plano anterior para recuperação de dados com o cursor estático (Figura 14-34). A pesquisa de chave
foi adicionado porque, para suportar o cursor Keyset, ele deve recuperar todos os valores de chave.
Portanto, enquanto antes a Busca de Índice Não Clusterizado satisfazia o plano, agora temos que obter um
novo valor, um valor de verificação de chave que só pode vir da chave de índice clusterizado. Você pode ver
isso na saída de cada um dos operadores Clustered Index Seek e Clustered Index Scan , na Figura 14-43.

432
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Figura 14-43: Verifique as colunas adicionadas dos índices clusterizados.

Esse valor será usado mais tarde, como veremos. A próxima parte da Consulta de população é praticamente a
mesma de antes.

Figura 14-44: Carregando informações em um índice temporário para uso posterior.

Um índice temporário é criado para uso pela Fetch Query, cujo plano é mostrado na Figura 14-45.

Figura 14-45: Buscar Consulta para o operador Keyset.

Isso é muito mais complicado do que o cursor anterior. Isso porque, com o cursor Keyset, os dados podem mudar.
Portanto, para recuperar o conjunto de dados correto, em vez de simplesmente olhar para tudo armazenado no
índice temporário, ele leu os valores de chave da varredura de índice clusterizado em CWT_PrimaryKey e os usou
para fazer buscas de índice clusterizado nas outras tabelas. Observe também que todos estão usando uma junção
externa esquerda, porque é possível que a linha referenciada tenha sido excluída desde então.

Em seguida, vamos a cada uma dessas tabelas para recuperar os dados com base nos valores-chave armazenados.

433
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Figura 14-46: Recuperando os dados das tabelas com base nos valores-chave.

Há também uma verificação para ver se os dados foram excluídos, o que explica o operador Compute
Scalar final. O operador Nested Loops (Left Outer Join) , imediatamente à direita do Compute Scalar, está
lá para reunir os dados em preparação para a verificação.

Os planos reais são muito os mesmos de antes. Você verá uma instância do plano de execução para a
Consulta de população e, em seguida, uma série de planos para a Consulta de busca.

Cursor dinâmico
Finalmente, veremos um cursor dinâmico. Aqui, qualquer um dos dados pode ser alterado de qualquer
forma, em qualquer ponto em que acessamos o cursor. A mudança de código real é pequena, então, em
vez de repetir toda a lista de códigos, apenas mostrarei a mudança na Listagem 14-17.

DECLARE CurrencyList CURSOR DYNAMIC

Listagem 14-17

Capturar um plano estimado para este novo cursor resulta em mais uma variação dos planos de execução
que já vimos. Vou me concentrar nos detalhes do plano de execução para a definição do cursor, pois todos os
comportamentos genéricos são os mesmos.

A Figura 14-47 mostra o plano estimado.

434
Machine Translated by Google

Capítulo 14: Planos para tipos de dados especiais e cursores

Figura 14-47: Plano de execução estimado para um cursor dinâmico.

O maior ponto a ser observado aqui é que temos apenas uma Fetch Query. Não há preenchimento de
consulta para cursores dinâmicos. Os dados e a ordem dos dados podem mudar, então tudo o que podemos
fazer é executar a consulta completa, sempre. Existe um operador Compute Scalar para adicionar um valor de
ID e armazenamos as informações recuperadas em um índice clusterizado temporário. Isso nos permite mover
em várias direções dentro do cursor, não apenas para frente, mas os dados são buscados repetidamente à
medida que o cursor é executado, e é por isso que esse é o menos eficiente dos vários tipos de cursor.

Curiosamente, em algum lugar nas partes internas, existem verificações que de alguma forma impedem
o mecanismo de executar a consulta repetidamente, todas as vezes. Os detalhes não são conhecidos por
mim, mas, efetivamente, você precisa pensar nessa abordagem como se ela executasse a consulta 15 vezes.
Capturar os planos reais para este cursor mostrará apenas o mesmo plano de execução repetidamente.

Existem várias outras opções que podem afetar o comportamento do cursor, mas isso não refletirá de
nenhuma maneira nova no plano de execução. Os comportamentos que você pode esperar estão refletidos
nos exemplos fornecidos.

Resumo
A introdução desses diferentes tipos de dados, XML, Hierárquico, Espacial e JSON, expande radicalmente
o tipo de dados que podemos armazenar no SQL Server e o tipo de operações que podemos realizar
nesses dados. Cada um desses tipos é refletido de forma diferente nos planos de execução. Os cursores
também adicionam novas rugas ao que veremos nos planos de execução.
Nem os tipos de dados complexos, nem os cursores, alteram fundamentalmente o que é necessário para
entender os planos de execução. Muitos dos mesmos operadores estão em uso, embora esses tipos de
dados e cursores especiais tenham valores agregados. Você ainda precisa detalhar as propriedades e
percorrer os detalhes para entender como os planos de execução exibem os comportamentos definidos em seu
T-SQL, mesmo que seja para um cursor ou um tipo de dados especial.

435
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano


Ao longo deste livro, executamos consultas ad hoc e módulos de código de dentro do SSMS e capturamos seus planos de
execução. No Capítulo 9, também exploramos como recuperar os planos atualmente no cache de planos, consultando um
conjunto de objetos de gerenciamento dinâmico (DMOs) relacionados à execução. Essas consultas DMO nos permitiram
retornar propriedades interessantes para cada plano, como seu tamanho e o número de vezes que foi usado, além de
métricas de tempo de execução. Muitas das colunas que armazenam essas métricas são contadores e retornam uma linha
para cada instrução de consulta em cada plano. Por exemplo, cada vez que um plano armazenado em cache é executado, o
tempo necessário para executar cada consulta é adicionado ao valor do contador total_elapsed_time para essa linha. Em outras
palavras, as métricas são agregadas ao longo do tempo em que o plano está em cache.

Se você estiver usando o Query Store, poderá capturar planos e rastrear métricas de tempo de execução agregadas por
períodos ainda mais longos (conforme explorado em detalhes no Capítulo 16).

Embora essas informações sejam úteis, há momentos em que o histórico de métricas agregadas obscurece a causa de
um problema recente com uma consulta. Se uma consulta estiver sendo executada de forma irregular ou uma instância SQL
estiver apresentando problemas de desempenho apenas em momentos específicos, convém capturar os planos e as métricas
de execução associadas para cada uma das consultas em uma carga de trabalho, durante esse período. Se esse período for
por volta das 2 da manhã, provavelmente você preferirá ter uma ferramenta para capturar as informações automaticamente.

Veremos como usar duas ferramentas, Extended Events e SQL Trace, para capturar automaticamente os planos de
execução para cada consulta na carga de trabalho ou, talvez mais especificamente, para as consultas mais intensivas e de
longa duração em aquela carga de trabalho.

Por que automatizar a captura do plano?

Podem surgir situações de todos os tipos em que a captura de um plano usando SSMS, usando as informações no Query Store
ou consultando o cache do plano, não fornecerá os dados de que você precisa ou não os fornecerá com facilidade e precisão.
Por exemplo, se seus aplicativos enviarem um número muito alto de consultas ad hoc não parametrizadas, isso basicamente
inundará o cache com planos de uso único e os planos mais antigos ficarão rapidamente fora do cache, conforme discutido no
Capítulo 9. Nessa situação , você provavelmente terá o Repositório de Consultas configurado para que também não capture
todos os planos.
Portanto, os planos para uma determinada consulta que você deseja investigar não podem mais ser armazenados em cache
ou no Repositório de Consultas.

436
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

Durante o trabalho de desenvolvimento, você pode capturar os planos para sua carga de trabalho de teste
simplesmente adicionando comandos SET STATISTICS XML ao código. No entanto, isso requer alterações de código
que nem sempre são possíveis ou fáceis ao lidar com uma carga de trabalho do servidor de produção.

É nestas e outras circunstâncias que vamos recorrer a outras ferramentas para recuperar os planos de
execução.

Ferramentas para automatizar a captura de planos

Primeiro, mostrarei como usar Eventos Estendidos para capturar planos reais e, em seguida, a ferramenta que
substituiu, SQL Trace. Começando no Banco de Dados SQL do Azure e no SQL Server 2016 ou superior, você
também tem acesso ao Repositório de Consultas como meio de investigar os planos de execução, e abordaremos
esse tópico no próximo capítulo. No entanto, uma coisa que o Repositório de Consultas não oferece, como os Eventos
Estendidos e o Rastreamento SQL, são as métricas de tempo de execução detalhadas.

Minha suposição básica é que você está trabalhando no SQL Server 2012 ou superior ou no Banco de Dados SQL
do Azure. Em ambos os casos, você realmente deve usar Extended Events em vez de SQL Trace, pois é uma
ferramenta muito superior para coletar dados de diagnóstico para todos os diferentes tipos de eventos que ocorrem
em nossas instâncias e bancos de dados do SQL Server.

Todas as novas funcionalidades do SQL Server usam Eventos Estendidos como seu mecanismo de monitoramento
interno. A GUI embutida no Management Studio é atualizada regularmente e possui muitas funcionalidades para
torná-la bastante atraente, especialmente ao ajustar consultas e analisar planos de execução. A coleta de dados
de diagnóstico com Extended Events adiciona uma sobrecarga muito menor do que com o SQL Trace e, portanto,
tem um impacto muito menor no servidor sob observação, pois os eventos são capturados em um nível inferior no
sistema SQL Server. O SQL Trace, em termos gerais, funciona com o princípio de coletar todos os dados de
eventos que podem ser necessários e, em seguida, descartar os que os rastreamentos individuais não precisam.
Eventos Estendidos funciona no princípio oposto; ele coleta o mínimo de dados possível e nos permite definir com
precisão as circunstâncias sob as quais coletar dados de eventos. Por fim, os eventos do SQL Trace estão no
caminho de substituição no SQL Server, portanto, em algum momento, eles não estarão disponíveis.

Dito isso, se você ainda estiver trabalhando no SQL Server 2005, terá que usar o SQL Trace, pois os Extended
Events foram introduzidos apenas no SQL Server 2008. Se você estiver trabalhando no SQL Server 2008 ou SQL
Server 2008R2, então os Eventos Estendidos estão disponíveis, mas essas versões iniciais ofereciam um conjunto
muito menos completo de eventos, e um dos eventos ausentes em 2008 e 2008R2 é a capacidade de capturar
planos de execução. Existem outros pontos fracos também, como a ausência de uma GUI integrada ao SSMS, o
que significa que devemos analisar os dados do evento XML.

437
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

CUIDADO! Automatizando a captura de planos em servidores de produção

Com qualquer uma dessas ferramentas, você está capturando o plano armazenado em cache no mesmo thread que
executa a consulta; em outras palavras, é uma operação em processo. Além disso, os planos de execução podem ser
grandes, de modo que capturá-los usando essas ferramentas adiciona memória em processo e sobrecarga de E/S consideráveis.
Como tal, tenha cuidado ao executar sessões de Eventos Estendidos ou rastreamentos do lado do servidor que capturam o
plano em um servidor de produção. Certifique-se de adicionar filtros muito granulares a esses eventos de plano de execução,
para que você capture o plano para o menor número possível de instâncias de eventos.

Automatizando a captura de planos usando Extended Events


Com o Extended Events, podemos coletar e analisar dados de diagnóstico para vários tipos de eventos que ocorrem
em instâncias e bancos de dados do SQL Server. Por exemplo, podemos coletar dados para eventos relacionados à
instrução T-SQL ou execução de procedimento armazenado, processos bloqueados, bloqueios mortos e muito mais.
Criamos uma sessão de evento, aproximadamente o equivalente a um rastreamento no SQL Trace, ao qual
adicionamos os eventos necessários, especificamos quaisquer dados adicionais necessários que desejamos coletar como
uma ação, adicionamos os predicados de filtros que limitarão quando os dados forem coletados e quanto e, finalmente,
especifique os destinos que consumirão os dados do evento.

Por exemplo, se tivermos consultas de longa duração que consomem muitos recursos de CPU e E/S, talvez queiramos
capturar os planos para essas consultas, juntamente com um ou dois outros eventos úteis, para descobrir o motivo.
Você pode capturar estatísticas de espera para uma determinada consulta ou procedimento armazenado. Você pode
usar eventos estendidos para observar as estatísticas sendo consultadas e consumidas pelo otimizador à medida que
compila um plano. Você pode ver eventos de compilação e recompilação e pode correlacionar cada um deles com
outros para obter uma visão completa do comportamento das consultas dentro do sistema, muito além de qualquer coisa
possível antes da introdução dos Eventos Estendidos, mas tudo isso vai além o escopo deste livro.

O Extended Events fornece três eventos que capturam os planos de execução. Cada um captura o plano em um
estágio diferente do processo de otimização. A query_post_compilation_
O evento showplan é acionado apenas na compilação do plano. Na primeira vez que você chamar um procedimento
armazenado ou executar uma consulta em lote ou ad hoc, verá esse evento acionado. Se você executá-los novamente e
seus planos forem reutilizados do cache, o evento não será acionado. Esse evento também será acionado quando você
solicitar um plano estimado, supondo que não haja nenhum plano em cache para essa consulta.

O evento query_pre_execution_showplan é acionado logo antes da execução de uma consulta.


Ele mostra o plano que foi compilado ou recuperado do cache. Este é um evento muito útil quando você está
lidando com muitas recompilações e deseja ver os planos antes e depois da recompilação.

438
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

Como os dois eventos acima são acionados antes da execução da consulta, nenhum deles contém estatísticas de
tempo de execução. Se você quiser isso, precisará capturar o evento query_post_execution_showplan.
Como ele captura o plano, mais as métricas de tempo de execução, para todas as consultas que atendem aos
critérios de filtro da sua sessão de evento, também é mais caro do que capturar os eventos de pré-execução
equivalentes. Embora eu defenda seu uso, lembre-se do meu cuidado anterior: tenha cuidado com este evento e
os outros dois. Teste cuidadosamente qualquer sessão de evento que os capture, antes de executá-lo em um
ambiente de produção.

Crie uma sessão de evento usando a GUI do SSMS


As sessões de eventos são armazenadas em cada servidor e você pode encontrá-las no SSMS Object Explorer,
conforme mostrado na Figura 15-1. O AlwaysOn_health, system_health e telemetry_
xevents são sessões de eventos embutidas, e todo o resto são sessões que eu criei. As setas verdes definem as
sessões de eventos que estão atualmente em execução e os quadrados vermelhos as que estão atualmente paradas.

Figura 15-1: Uma lista de sessões de eventos estendidos.

439
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

Minha maneira preferida de criar uma nova sessão de evento é usando o T-SQL, mas às vezes é útil usar a GUI
para criar uma nova sessão rapidamente e, em seguida, fazer um script e ajustá-la, conforme necessário.
Portanto, vamos ver como criar uma sessão de eventos que capture nossos eventos relacionados ao plano de
execução, usando a caixa de diálogo Nova Sessão .

Não abordarei todos os detalhes e todas as opções disponíveis ao criar sessões de eventos. Para isso, acesse a
documentação da Microsoft (https://bit.ly/2Ee8cok). Também não abordarei o Assistente de Nova Sessão, porque ele
tem várias limitações e só pode ser usado para criar novas sessões de eventos. Se você deseja alterar um evento
existente, a caixa de diálogo para isso usa o mesmo layout e opções da caixa de diálogo Nova sessão .

Clique com o botão direito do mouse na pasta Sessões e selecione Nova sessão... no menu de contexto para abrir
a caixa de diálogo Nova sessão .

Figura 15-2: Janela Nova Sessão para Eventos Estendidos.

Esta figura mostra a página Geral da caixa de diálogo, onde damos um nome à sessão, especificamos quando
queremos que a sessão comece a ser executada e algumas outras opções. Dei um nome à nova sessão de evento,
ExecutionPlansOnAdventureWorks2014, e especifiquei que a sessão deve começar a ser executada assim que a
criarmos e que quero assistir aos dados do evento ao vivo na tela. Também ativei o rastreamento de causalidade
para esta sessão e explicarei o que isso faz, brevemente, mais adiante no capítulo.

440
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

Agora clique na próxima página, Eventos, onde podemos selecionar os eventos para a sessão.

Figura 15-3: A página Eventos de uma nova Sessão de Eventos Estendidos.

No painel esquerdo, identificamos os eventos que queremos capturar. Eu usei a biblioteca de eventos
caixa de texto para filtrar nomes de eventos que contenham a palavra showplan. Existem quatro deles e,
na Figura 15-3, já usei a seta '>' para selecionar os três eventos que desejo.

Eu destaquei o evento query_post_compilation_showplan e ele mostra uma descrição para esse


evento no painel abaixo, junto com um aviso de que você pode ver problemas de desempenho capturando
esse evento.

Também quero capturar um outro evento, não relacionado diretamente aos planos de execução, sql_batch_
concluído, que é acionado quando um lote T-SQL termina de ser executado e fornece métricas de
desempenho úteis dessa consulta. Muitas vezes, também é útil adicionar o comando sql_statement_
recompile, que é acionado quando ocorre uma recompilação em nível de instrução, para qualquer tipo de
lote, e fornece campos de evento úteis que revelam a causa da recompilação e a identidade do banco de
dados e do objeto em que ocorreu.

441
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

Tendo selecionado todos os quatro eventos, clique no botão Configurar no canto superior direito. Isso muda
nossa visão, mas ainda estamos na mesma página de eventos , e é aqui que podemos controlar o comportamento
de nossas sessões de eventos.

Na guia Filtro (predicado) , podemos definir predicados para nossa sessão de evento que definirão as
circunstâncias em que desejamos que o evento seja acionado totalmente e para coletar esses dados. Neste
exemplo, queremos coletar os dados do evento apenas se o evento for acionado no banco de dados Adventure
Works2014 e apenas para uma consulta que acesse a tabela Person.Person, conforme mostrado na Figura 15-4.

Figura 15-4: Configurando os eventos selecionados em uma nova sessão de Eventos Estendidos.

Usei o mouse e a tecla Shift para selecionar todos os quatro eventos e, em seguida, adicionei os dois filtros a
todos os quatro eventos. Para limitar a coleta de dados de eventos ao banco de dados AdventureWorks2014,
precisamos criar um predicado no campo global sqlserver.database_name.
O operador necessário é o comparador textual equal_i_sql_unicode_string, para comparar o database_name
para o evento gerado com a string 'Adventure Works2014'. O mecanismo de eventos só acionará o evento
totalmente e coletará os dados se eles corresponderem. Para restringir ainda mais a coleta de dados, adiciono
o operador And e um segundo predicado no campo global sqlserver.sql_text, selecionando o like_i_sql_unicode_

comparador de strings, para usar o comando LIKE e o valor %Person.Person%.

Dessa forma, apesar do plano de consulta de Eventos Estendidos ser caro, garanti que estou capturando
apenas um conjunto muito limitado desses eventos.

Embora não façamos isso aqui, podemos usar as outras duas guias para controlar os dados que queremos
que a sessão do evento colete. Na aba Event Fields , podemos ver as colunas de dados do evento que
definem o payload base para o evento, ou seja, elas sempre serão capturadas quando o evento for disparado,
além de quaisquer colunas de dados do evento que sejam configuráveis.

442
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

Na guia Campos Globais (Ações) , podemos especificar quaisquer dados adicionais que desejamos adicionar à carga
útil do evento, como uma "ação". Nenhum campo global é coletado por padrão, em contraste com o SQL Trace, onde
cada evento coleta esses dados quando o evento é acionado, mesmo que não faça parte da definição do arquivo de
rastreamento. Por exemplo, se quisermos coletar o texto SQL exato em um evento que ainda não coleta essas
informações, adicionaremos o campo global sql_text à sessão do evento, explicitamente, como uma ação. As ações
adicionam uma sobrecarga adicional, portanto, escolha quando e como usá-las com cuidado.

Em seguida, clique na página Armazenamento de dados à esquerda, onde podemos especificar um ou mais destinos
para coletar os dados do evento.

Figura 15-5: A página Armazenamento de Dados de uma nova sessão de Eventos Estendidos.

Eu usei o destino event_file, que é simplesmente armazenamento de arquivo simples, semelhante ao arquivo de
rastreamento do lado do servidor. É o destino mais usado para sessões de eventos padrão e geralmente tem um
desempenho melhor do que as outras opções. Você pode definir as propriedades do arquivo na janela inferior.
Exceto para definir a localização precisa do arquivo no servidor, aceitei todos os padrões nesta instância.

443
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

Há uma página avançada final , onde podemos definir uma variedade de opções de sessão
avançadas, relacionadas à configuração do buffer de memória para eventos, frequência de envio para
o destino e retenção de eventos no destino. Não vamos cobrir isso aqui.

Com isso, você pode clicar no botão OK para criar a nova sessão do evento. Se você fez como
eu, especificando que a sessão deve iniciar e mostrar a janela Live Data , você não apenas verá uma
nova sessão, mas uma nova janela será aberta no SSMS. Chegaremos a isso em apenas um minuto.

Criar uma sessão de evento em T-SQL


Como afirmei anteriormente, geralmente prefiro criar sessões de eventos em T-SQL. É simples e claro e
facilita a migração de sessões entre diferentes servidores. Nesse caso, vou simplesmente mostrar o T-
SQL para a sessão de evento ExecutionPlansOnAdventureWorks2014 que acabamos de criar (basta
clicar com o botão direito do mouse na sessão de evento no SSMS Object Explorer e usar Script Session
As…).

CRIAR SESSÃO DE EVENTO ExecutionPlansOnAdventureWorks2014


NO SERVIDOR
ADICIONAR EVENTO sqlserver.query_post_compilation_showplan
(ONDE ( sqlserver.database_name = N'AdventureWorks2014'
E sqlserver.like_i_sql_unicode_string(sqlserver.
sql_text, N'%Pessoa.Pessoa%'))),
ADICIONAR EVENTO sqlserver.query_post_execution_showplan
(ONDE ( sqlserver.database_name = N'AdventureWorks2014'
E sqlserver.like_i_sql_unicode_string(sqlserver.
sql_text, N'%Pessoa.Pessoa%'))),
ADICIONAR EVENTO sqlserver.query_pre_execution_showplan
(ONDE ( sqlserver.database_name = N'AdventureWorks2014'
E sqlserver.like_i_sql_unicode_string(sqlserver.
sql_text, N'%Pessoa.Pessoa%'))),
ADICIONAR EVENTO sqlserver.sql_batch_completed
(ONDE ( sqlserver.database_name = N'AdventureWorks2014'
E sqlserver.like_i_sql_unicode_string(sqlserver.
sql_text, N'%Pessoa.Pessoa%')))
ADICIONAR TARGET package0.event_file
(SET nome do arquivo = N'C:\PerfData\ExecutionPlansOnAdventureWor
ks2014.xel')

444
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

COM (MAX_MEMORY = 4096 KB,


EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS,
MAX_DISPATCH_LATENCY = 30 SEGUNDOS,
MAX_EVENT_SIZE = 0 KB,
MEMORY_PARTITION_MODE = NONE,
TRACK_CAUSALITY = ATIVADO,
STARTUP_STATE = OFF)
VAI

Listagem 15-1

Cada um dos eventos do plano de execução usa o mesmo predicado ou definições de filtro, como
podemos ver na cláusula WHERE para cada evento. O código é direto e você pode ver todas as escolhas
que fizemos na GUI refletidas nas instruções T-SQL.

Visualizando os dados do evento

Se você seguiu exatamente, você tem uma sessão em execução e a janela Live Data Viewer aberta.
Caso contrário, você precisará clicar com o botão direito do mouse em uma sessão e selecionar Iniciar na opção de menu, clicar com

o botão direito do mouse novamente e selecionar Assistir dados ao vivo.

Agora, execute a consulta mostrada na Listagem 15-2. Para garantir que capturamos todos os três eventos
do plano de execução, a seção de abertura do código pega o plan_handle de um plano em cache para uma
consulta que contém o texto %Person.Person% e o usa para remover esses planos do cache. Feito isso,
executamos a consulta que fará com que os eventos sejam acionados.

USE AdventureWorks2014;
VAI
DECLARE @PlanHandle VARBINARY(64);
SELECT @PlanHandle = deqs.plan_handle
FROM sys.dm_exec_query_stats AS deqs
CROSS APPLY sys.dm_exec_sql_text(deqs.sql_handle) AS dest
WHERE dest.text LIKE '%Person.Person%';
SE @PlanHandle NÃO FOR NULL
COMEÇAR

DBCC FREEPROCCACHE(@PlanHandle);
FIM;
VAI

445
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

SELECT p.Sobrenome + ', ' p.Título , + p.FirstName ,

pp.Número de telefone
Pessoa.Pessoa AS p
A PARTIR DE

INSCREVA- SE Pessoa.PessoaTelefone AS pp
ON pp.BusinessEntityID = p.BusinessEntityID
JOIN Person.PhoneNumberType AS pnt
ON pnt.PhoneNumberTypeID = pp.PhoneNumberTypeID
WHERE pnt.Name = 'Célula'
AND p.LastName = 'Dempsey';
VAI

Listagem 15-2

Eu executei a consulta DMO em um lote e a consulta real em um segundo lote, e ambas no contexto do
AdventureWorks, então você verá todos os quatro eventos dispararem duas vezes. A Figura 15-6 mostra apenas
os quatro relativos à execução do segundo lote.

Figura 15-6: Eventos no visualizador do Live Data mostrando os eventos que capturamos.

Se você executar novamente apenas o lote de consultas, verá apenas três eventos; você não verá um post_
evento de compilação, pois a consulta não será compilada novamente. Clique em qualquer um dos *_
showplan na grade superior para ver o plano de consulta associado exibido graficamente na guia Plano de consulta .

Não vamos explorar esse plano em detalhes, exceto para observar que o primeiro operador não é o operador
SELECT , como vimos ao longo do livro. Em vez disso, o primeiro operador para planos capturados usando
Extended Events é o primeiro operador do plano conforme definido pelo NodeID
valor. Por algum motivo conhecido apenas pela Microsoft, algumas das propriedades normalmente exibidas para o
primeiro operador não são exibidas em Eventos Estendidos. Conforme explicado no Capítulo 13, você ainda pode
encontrar essas informações no XML do plano simplesmente clicando com o botão direito do mouse no plano gráfico,
selecionando Mostrar XML do plano de execução e procurando no elemento StmtSimple o plano.

446
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

Figura 15-7: Um plano de execução capturado por Extended Events.

A guia Detalhes de cada evento revela algumas informações que podem ser úteis para seus
esforços de ajuste de consulta. Por exemplo, a Figura 15-8 mostra o painel Detalhes para nosso
primeiro evento, query_post_compilation_showplan.

Figura 15-8: detalhes do query_post_compilation_showplan.

447
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

O primeiro conjunto de campos, com nomes começando com attach_, é adicionado às sessões
de eventos para as quais TRACK_CAUSALITY está definido como ON, como é para esta
sessão. Isso significa que um conjunto de eventos vinculados terá um ID comum e uma
sequência. Você pode ver que, em nossa sequência, este é o primeiro evento. Isso é útil se você
deseja agrupar todas as atividades para um determinado conjunto de eventos, definido pelo valor
attach_activity_id.guid, e ordenar esses eventos na ordem exata em que ocorreram no SQL Server,
conforme mostrado pelo attach_activity_id.seq valor. Em um sistema de teste como o meu, isso pode
não importar porque sou o único executando consultas. No entanto, ao capturar eventos como esse
em um sistema de produção, mesmo eventos bem filtrados, você pode ver consultas adicionais e
conjuntos de eventos nos quais não tem interesse. Alternativamente, você pode ver vários eventos
interessantes, mas entrelaçados porque foram executados ao mesmo tempo e, nesses casos, os
valores activity_id podem ajudá-lo a descobrir quais pertencem um ao outro.

As informações interessantes estão mais abaixo. Por exemplo, o campo de duração mostra o
tempo que levou para compilar esse plano, que foi de 4.192 microssegundos na minha máquina.
Você também pode ver que o número estimado de linhas retornadas foi 1. Também temos o plan_handle
e sql_handle que pode ser usado para recuperar este plano e o código T-SQL do cache, se necessário.
A coluna showplan_xml tem o plano como XML. A coluna object_name descreve essa consulta como
SQL dinâmico. Isso é preciso para o tipo de consulta que estou executando neste caso, que é apenas
uma instrução T-SQL, não uma instrução preparada ou procedimento armazenado.
Ao obter planos para procedimentos armazenados ou outros objetos, você poderá ver seus nomes
de objetos, bem como o object_type.

O próximo evento é query_pre_execution_showplan que mostra informações semelhantes, mas a


carga útil base para este evento não inclui alguns dos campos de evento que vimos para o evento
anterior, como plan_handle e sql_handle.

O terceiro evento na sequência é query_post_execution_showplan com os detalhes mostrados na


Figura 15-9.

448
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

Figura 15-9: detalhes do query_post_execution_showplan.

Como você pode ver, não há muitas informações adicionais sobre o plano aqui. O detalhe está no
próprio plano. É importante ressaltar que o plano capturado por este evento possui informações de tempo
de execução. Clique na guia Query Plan e examine o operador Properties for the Nested Loops , e você
verá que temos contadores de tempo de execução reais para o número de linhas e número de execuções,
bem como valores estimados.

449
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

Figura 15-10: Propriedades para o operador Nested Loops mostrando as métricas de tempo de execução.

Garantir sessões de eventos "leves" ao capturar o plano


O aspecto mais importante de tudo isso é que você tem um plano de execução e o capturou de forma automatizada.
Basta lembrar que a captura de planos usando eventos estendidos é uma operação de alto custo. Você só deve executar
a sessão do evento por um tempo limitado. Ele deve capturar apenas exatamente os dados de que você precisa e nada
mais. Muito raramente você desejaria executar uma sessão de evento que capturasse todos os três eventos de plano de
exibição , como fiz na Listagem 15-1. Em vez disso, basta escolher um; Eu geralmente uso o evento
query_post_execution_showplan. Além disso, defina filtros, como eu fiz, para controlar estritamente as circunstâncias em
que o evento é totalmente acionado, o que limitará o número de eventos para os quais a sessão do evento coleta os
dados do evento.

A Listagem 15-3 oferece um exemplo mais realista do tipo de sessão de evento que você pode usar para capturar
planos específicos ao ajustar a consulta.

450
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

CRIAR SESSÃO DE EVENTO ExecPlansAndWaits


NO SERVIDOR
ADICIONAR EVENTO sqlos.wait_completed
(ONDE ( (sqlserver.database_name = N'AdventureWorks2014')
E (sqlserver.like_i_sql_unicode_string(sqlserver.
sql_text, N'%ProductTransferByReference%')))),
ADICIONAR EVENTO sqlserver.query_post_execution_showplan
(ONDE ( (sqlserver.database_name = N'AdventureWorks2014')
E (sqlserver.like_i_sql_unicode_string(sqlserver.
sql_text, N'%ProductTransferByReference%')))),
ADICIONAR EVENTO sqlserver.rpc_completed
(ONDE ( (sqlserver.database_name = N'AdventureWorks2014')
E (sqlserver.like_i_sql_unicode_string(sqlserver.
sql_text, N'%ProductTransferByReference%')))),
ADICIONAR EVENTO sqlserver.rpc_starting
(ONDE ( (sqlserver.database_name = N'AdventureWorks2014')
E (sqlserver.like_i_sql_unicode_string(sqlserver.
sql_text, N'%ProductTransferByReference%'))))
ADICIONAR TARGET package0.event_file
(SET nome do arquivo = N'C:\PerfData\ExecPlansAndWaits.xel')
COM (TRACK_CAUSALITY = ON)
VAI

Listagem 15-3

Ele captura os eventos rpc_starting e rpc_completed, que são acionados quando um procedimento armazenado
inicia e conclui a execução, respectivamente; wait_completed, que é acionado para qualquer espera que tenha
ocorrido durante a execução; e query_post_execution_showplan, para capturar o plano, uma vez que a consulta foi
executada.

Eu filtrei esses eventos por banco de dados e por nome de procedimento e adicionei rastreamento de causalidade.
Com isso, pude ver quando o procedimento começou a ser executado, incluindo os valores dos parâmetros,
cada espera conforme ele é concluído, e a ordem em que eles foram concluídos e a conclusão do procedimento
junto com o plano de execução. Isso seria praticamente tudo o que você precisa para solucionar problemas de
desempenho em uma consulta específica.

Inicie isso em um sistema de produção, capture alguns minutos de execuções ou o que for apropriado para seu
sistema e, em seguida, desligue-o novamente. A carga será a mínima possível enquanto ainda captura dados úteis
que ajudarão a orientar suas escolhas de ajuste de consulta.

451
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

Automatizando a captura do plano usando o SQL Trace

Conforme discutido no início do capítulo, se você estiver executando o SQL Server 2008/R2 ou inferior, talvez seja
necessário usar Trace Events.

Podemos usar o SQL Profiler para definir um rastreamento do lado do servidor para capturar planos de execução
XML, à medida que as consultas estão sendo executadas. Podemos então examinar os planos coletados, começando
com as consultas com os custos mais altos, e procurar possíveis possibilidades de otimização, como índices que
podem permitir que o otimizador realize busca de índice em vez de operações de varredura para consultas frequentes
que acessam tabelas grandes, ou por investigar o SQL que o acompanha para encontrar a causa de avisos
específicos nos planos, como classificações que se espalham para o disco.

CUIDADO! Nunca use a GUI do Profiler para capturar dados de eventos

Vou mostrar como configurar um rastreamento do lado do servidor; nunca use o Profiler para capturar dados de
eventos diretamente. A GUI do Profiler usa um mecanismo de armazenamento em cache diferente que pode ter um
impacto profundamente negativo no servidor que é o destino da coleta de eventos. Você pode usar a GUI para gerar um
script de rastreamento, mas deve executá-lo independentemente como um rastreamento do lado do servidor, salvando
os dados em um arquivo.

O princípio básico do SQL Trace é capturar dados sobre eventos conforme eles ocorrem no mecanismo
do SQL Server, como a execução de T-SQL ou um procedimento armazenado. No entanto, capturar eventos
de rastreamento é muito caro, especialmente quando comparado aos Eventos Estendidos.
Muitos dos eventos têm uma carga útil padrão muito mais pesada, qualquer dado que não seja realmente
necessário é simplesmente descartado. Além disso, os mecanismos de filtragem em eventos de rastreamento são
altamente ineficientes. Conforme discutido anteriormente, desaconselho o uso de eventos SQL Trace se você puder
usar eventos estendidos.

Trace eventos para planos de execução


Há muitos eventos de rastreamento que capturarão um plano de execução. Os mais usados são os seguintes:

• Showplan XML – o evento é acionado a cada execução de uma consulta e captura o plano de execução
em tempo de compilação, da mesma forma que o query_pre_execution_
showplan event em Extended Events. Este é provavelmente o evento preferível se você precisar
minimizar o impacto no sistema. Os outros devem ser evitados por causa da carga que colocam no
sistema ou porque não retornam dados utilizáveis para nossos propósitos.

452
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

• Showplan XML para Query Compile – como o Showplan XML acima, mas só é acionado em uma
compilação de uma consulta, como o query_post_compilation_
showplan event em Extended Events.
• Estatísticas de desempenho – podem ser usadas para rastrear quando os planos de execução são
adicionados ou removidos do cache.

• Perfil de Estatísticas XML do Showplan - este evento irá gerar a execução real
plano para cada consulta, depois de executada. Embora este seja o que você provavelmente desejará
mais usar, também é o mais caro para capturar.

Você deve ser extremamente cauteloso ao executar rastreamentos que capturam qualquer um desses eventos
em uma máquina de produção, pois isso pode causar um impacto significativo no desempenho. O mecanismo
de filtragem do SQL Trace é muito menos eficiente do que para eventos estendidos. Mesmo se filtrarmos no
banco de dados e no texto SQL, como fizemos anteriormente para nossas sessões de eventos, o rastreamento
SQL ainda dispara o evento totalmente para cada banco de dados e para qualquer texto SQL e aplica o filtro
apenas no ponto em que o rastreamento individual consome o evento. Além de coletar os planos de execução,
esses eventos também coletarão vários campos globais por padrão, quer você os queira ou não.

Execute rastreamentos pelo menor tempo possível. Se puder, você absolutamente deve substituir o SQL Trace
por Extended Events.

Criando um rastreamento XML do Showplan usando o Profiler

O evento SQL Server Profiler Showplan XML captura o plano de execução XML criado pelo otimizador de consulta
e, portanto, não inclui métricas de tempo de execução. Para capturar um rastreamento básico do Profiler, mostrando
os planos de execução estimados, inicie o Profiler no menu Ferramentas no SSMS, crie um novo rastreamento e
conecte-se à sua instância do SQL Server. Por padrão, apenas uma pessoa conectada como sa ou um membro do
grupo SYSADMIN pode criar e executar um rastreamento do Profiler. Para que outros usuários criem um rastreamento,
eles devem receber a permissão ALTER TRACE.

Na guia Geral , altere o modelo para em branco, dê um nome ao rastreamento e, em seguida, alterne para a
guia Seleção de eventos e certifique-se de que as colunas Mostrar todos os eventos e Mostrar todos
caixas de seleção são selecionadas. O evento Showplan XML está localizado dentro do Performance
seção, então clique no sinal de mais (+) para expandir essa seleção. Clique na caixa de seleção para o evento
Showplan XML .

Embora você possa capturar o evento Showplan XML sozinho no Profiler, geralmente é mais útil se,
como fiz com a sessão de eventos estendidos, você o captura junto com alguns outros eventos básicos, como
RPC:Completed (na classe de eventos Stored Procedures ) e SQL:BatchCompleted ( classe de evento TSQL ).

453
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

Esses eventos extras fornecem informações adicionais para ajudar a contextualizar o plano XML.
Por exemplo, podemos ver quais parâmetros foram passados para um procedimento armazenado no qual
estamos interessados.

Não entrarei em detalhes de quais campos de dados escolher para cada evento, mas, se você estiver executando
o rastreamento em um ambiente compartilhado, poderá adicionar o campo database_name e filtrá-lo (usando
Column Filters…) para que você veja apenas os eventos nos quais está interessado.

Desmarque "Mostrar todos os eventos" e "Mostrar todas as colunas" quando terminar. A tela de seleção de
eventos deve se parecer com a Figura 15-11.

Figura 15-11: Rastreamento definido no Profiler.

Com o Showplan XML ou qualquer outro evento XML selecionado, uma terceira guia aparece, chamada
Events Extraction Settings. Nesta guia, podemos optar por enviar, para um arquivo separado para uso
posterior, uma cópia do XML conforme ele é capturado. Não só podemos definir o arquivo, como também
podemos determinar se todo o XML irá para um único arquivo ou uma série de arquivos, exclusivo para cada
plano de execução.

454
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

Figura 15-12: Configurando a extração do plano de execução.

Apenas para fins de teste, para provar que o rastreamento funciona corretamente, e nunca em um sistema de
produção, clique no botão Executar para iniciar o rastreamento. Execute novamente o código da Listagem 15-2 e você
deverá ver os eventos capturados, conforme mostrado na Figura 15-13.

Figura 15-13: Saída do Trace Event com um plano de execução em exibição.

455
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

Pare a execução do rastreamento. Nos dados de eventos coletados, cliquei no evento Showplan XML . No painel inferior,
você pode ver o plano de execução gráfica. Observe que o plano capturado novamente não possui o operador SELECT .

Você não pode acessar as propriedades do operador nesta janela; você precisará navegar no XML do plano, disponível na coluna
TextData , ou exportá-lo para um arquivo clicando com o botão direito do mouse na linha e selecionando Extrair Dados do Evento.
No entanto, neste caso, já temos os planos em arquivos devido às Configurações de Extração de Eventos, mostradas na Figura
15-12.

Criando um rastreamento do lado do servidor

Conforme observado anteriormente, se estivermos usando o SQL Trace, queremos executar rastreamentos do lado do
servidor, salvando os resultados em um arquivo. Uma maneira rápida de fazer o script de uma definição de arquivo de
rastreamento é iniciar e parar imediatamente a execução do rastreamento, no Profiler, e clicar em Arquivo | Exportação |
Definição de rastreamento de script | Para SQL Servers 2005–2017….

A Listagem 15-4 mostra uma extração truncada do arquivo de rastreamento salvo.

EXEC @rc = sp_trace_create @TraceID SAÍDA,


0,
N'InsertFileNameHere', @maxfilesize,

NULO;
SE (@rc != 0)
Erro de GOTO ;
-- Arquivo e Tabela do lado do cliente não podem ser scriptados -- Defina os
eventos DECLARE @on BIT; SET @on = 1; EXEC sp_trace_setevent
@TraceID, 122, 1, @on; EXEC sp_trace_setevent @TraceID, 122, 9, @on;
EXEC sp_trace_setevent @TraceID, 122, 2, @on;


-- Definir os filtros
DECLARE @intfilter INT;
DECLARE @bigintfilter BIGINT;
-- Defina o status do rastreamento para iniciar
EXEC sp_trace_setstatus @TraceID, 1;

456
Machine Translated by Google

Capítulo 15: Automatizando a captura do plano

-- exibe id de rastreamento para referências futuras


SELECT @TraceID AS TraceID;
GOTO terminar;
erro:
SELECT @rc AS ErrorCode;
Finalizar:
VAI

Listagem 15-4

Sim, esse script longo é aproximadamente equivalente ao das Listagens 15-1 ou 15-3, apenas muito menos claro
e muito mais prolixo. Siga as instruções nos comentários para usar isso em seu
servidores próprios.

Resumo
Automatizar a captura de planos permitirá que você direcione consultas ou planos que talvez você não consiga
obter por meios mais tradicionais. Isso será extremamente útil quando você quiser o plano de execução e um número
correlacionado de outros eventos, como estatísticas de espera ou eventos de recompilação. Tente não usar eventos
de rastreamento para fazer isso, porque eles colocam uma carga muito alta no sistema. Em vez disso, sempre que
possível, use Eventos Estendidos. Apenas lembre-se de que os Eventos Estendidos, embora tenham um custo
muito baixo em termos de sobrecarga no sistema, especialmente em comparação com os Eventos de rastreamento,
não são gratuitos, portanto, você deve filtrar cuidadosamente os eventos capturados.

457
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Introduzido no Banco de Dados SQL do Azure em 2015 e na versão em caixa com o SQL Server 2016, o Repositório
de Consultas é um novo mecanismo para monitorar as métricas de desempenho de consultas no nível do banco de
dados. Além de capturar o desempenho da consulta, o Repositório de Consultas também retém planos de execução,
incluindo várias versões de planos para uma determinada consulta se as estatísticas ou configurações dessa consulta
puderem resultar em diferentes planos de execução. Este capítulo abordará o Repositório de Consultas no que se
refere diretamente aos planos de execução e ao controle do plano de execução; não é uma documentação completa
sobre todo o comportamento em torno do Repositório de Consultas.

Comportamento do repositório de consultas

O objetivo do Query Store é capturar as informações sem interferir nas operações normais de seu banco de
dados e servidor. Com essa intenção, então, as informações que o Query Store captura são inicialmente gravadas
de forma assíncrona na memória. O Repositório de Consultas tem então um processo secundário que liberará as
informações da memória para o disco, novamente de forma assíncrona. O Repositório de Consultas não interfere
diretamente no processo de otimização de consultas. Em vez disso, assim que um plano de execução for gerado pelo
processo de otimização, o Repositório de Consultas capturará esse plano ao mesmo tempo em que ele é gravado no
cache.

Alguns planos não são gravados no cache. Por exemplo, uma consulta ad hoc com uma dica RECOMPILE gerará
um plano, mas esse plano não será armazenado em cache. No entanto, todos os planos, por padrão, são capturados
pelo Repositório de Consultas no momento em que seriam gravados no cache.

Depois que uma consulta é executada, outro processo assíncrono captura informações de tempo de execução
sobre o comportamento dessa consulta, por quanto tempo ela foi executada, quanta memória ela usou etc. em um
processo assíncrono, assim como os planos.

Todas essas informações são armazenadas nas tabelas do sistema para cada banco de dados no qual você habilita
o Repositório de Consultas. Por padrão, o Repositório de Consultas não está habilitado no SQL Server 2016, mas está
habilitado por padrão no Banco de Dados SQL do Azure. Você pode controlar se o Repositório de Consultas está
habilitado ou desabilitado, mas não pode alterar onde as informações coletadas são colocadas, pois estão dentro das
tabelas do sistema, portanto, sempre estarão no grupo de arquivos Primário.

458
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

O princípio organizador do Query Store é a consulta. Não procedimentos armazenados e nem lotes, mas
consultas individuais. Para cada consulta, um ou mais planos de execução também serão armazenados.
Existem várias opções em relação ao comportamento do Query Store e as consultas que ele captura, duração
da retenção, etc. Nada disso é diretamente aplicável ao comportamento dos planos de execução no Query
Store, então não vou abordá-los aqui .

As informações sobre os planos de execução são armazenadas em uma tabela no Query Store, conforme
mostrado na Figura 16-1.

Figura 16-1: Planos de execução no Query Store.

O próprio plano é armazenado na coluna query_plan como um tipo de dados NVARCHAR(MAX).


Além disso, há uma grande quantidade de metadados sobre o plano armazenados como várias outras
colunas na exibição do catálogo. Os dados são armazenados como texto, NVARCHAR, embora seja um
plano de execução XML, pois há um limite nos níveis de aninhamento de XML no SQL Server. Armazenar o
plano como texto evita esse problema. Se você deseja recuperar o plano da visualização do catálogo e
visualizá-lo graficamente, deve-se CAST como XML (supondo que esteja abaixo do limite de profundidade de
aninhamento de XML) ou exportá-lo para um arquivo .showplan.

459
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Como existem algumas opções que afetam a retenção e a captura do plano no Repositório de consultas, quero
falar sobre elas, para que você possa ter certeza de capturar ou não os planos corretos para suas consultas.

Opções do repositório de consultas

Por padrão, você pode capturar até 200 planos diferentes para cada consulta. Isso deve ser suficiente para
quase qualquer consulta que eu tenha ouvido falar. É possível, embora eu ainda não tenha visto, que esse valor
seja muito alto para um sistema e você queira ajustá-lo para baixo. Também é possível para um determinado
sistema que esse valor seja muito baixo e precise aumentar. O método para ajustar as configurações do
Repositório de Consultas é usar o comando ALTER DATABASE conforme mostrado na Listagem 16-1.

ALTER DATABASE AdventureWorks2014 SET QUERY_STORE (MAX_PLANS_PER_


CONSULTA = 20);

Listagem 16-1

Nesse exemplo, altero os planos para cada consulta do padrão de 200 para 20. Deixe-me repetir, não estou
recomendando essa alteração. É apenas um exemplo. Os valores padrão devem funcionar bem na maioria dos
casos. Existem alguns padrões que você pode querer considerar ajustar.

A primeira opção de Repositório de Consultas que será significativa para planos de execução e
captura de planos é o Modo de Captura de Consultas. Por padrão, isso é definido como TODOS
no SQL Server 2016–2017 e AUTO no Banco de Dados SQL do Azure. Existem três configurações:

TUDO
Captura todos os planos para todas as consultas no banco de dados para o qual você habilitou o
Repositório de Consultas.

AUTO Captura planos com base em dois critérios. Qualquer uma das consultas com um tempo de
execução e tempo de compilação significativo, em testes, maior que um segundo de tempo de
execução, mas isso é controlado pela Microsoft. Como alternativa, uma consulta deve ser chamada
pelo menos três vezes antes que o plano seja capturado.

NENHUM
Deixa o Repositório de Consultas habilitado no banco de dados, mas para de capturar informações
em novas consultas, enquanto continua a capturar métricas de tempo de execução em consultas
existentes.

460
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Se você tiver um banco de dados no qual tenha ativado Otimizar para cargas de trabalho ad hoc, uma
configuração que garanta que uma consulta seja executada duas vezes antes que o plano seja carregado no
cache, pode ser uma boa ideia alterar o modo de captura para AUTO. Isso ajudará a reduzir o espaço desperdiçado
no conjunto de dados do Repositório de Consultas. Para fazer essa alteração, use o comando ALTER DATABASE novamente

ALTER DATABASE AdventureWorks2014 SET QUERY_STORE (QUERY_CAPTURE_


MODO = AUTO);

Listagem 16-2

Ter o Repositório de Consultas definido como NONE significa que nenhum plano adicional para qualquer
consulta será capturado (conforme observado acima). No entanto, ele continuará capturando as métricas de
tempo de execução de execução para os planos e consultas que já capturou. Isso pode ser útil em algumas
circunstâncias em que você só se preocupa com um conjunto limitado de consultas.

Outra configuração que você pode querer controlar é a limpeza automática das informações no Repositório de
Consultas. Por padrão, ele mantém 367 dias de dados, ano bissexto mais um dia. Isso pode ser demais, ou não
o suficiente. Você pode ajustá-lo usando as mesmas funções acima. Por padrão, o Repositório de Consultas
também limpará os dados assim que esse limite for atingido. Você pode querer desativá-lo, dependendo das
suas circunstâncias.

Além de usar o T-SQL para controlar o Query Store, você pode usar a GUI do Management Studio. Eu prefiro
T-SQL porque permite a automação do processamento. Para acessar as configurações da GUI, clique com o
botão direito do mouse em um banco de dados e selecione Propriedades no menu de contexto. Haverá uma nova
página listada, Query Store, e ela contém as informações básicas sobre o Query Store no banco de dados em
questão, conforme mostrado na Figura 16-2.

Você não pode controlar todas as configurações desta GUI, então você precisará usar o comando ALTER
DATA BASE para algumas configurações. Por exemplo, o número máximo de planos por consulta que
demonstramos na Listagem 16-1 não pode ser ajustado na GUI. O relatório da GUI sobre o uso do disco é útil,
mas se você realmente precisar monitorá-lo, novamente, você desejará configurar consultas para recuperar
essas informações.

461
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Figura 16-2: GUI do SSMS para gerenciar o Query Store.

Recuperando Planos do Repositório de Consultas


Recuperar planos de execução é simples. Existem relatórios predefinidos integrados ao Management Studio
e disponíveis no banco de dados. Você também pode usar T-SQL para recuperar os planos de execução das
exibições de catálogo expostas para as informações do Repositório de Consultas. Começaremos com a exibição
básica de um relatório do Repositório de Consultas e, em seguida, focaremos no uso das exibições de catálogo
para recuperar planos de execução usando T-SQL.

Relatórios SSMS
O SSMS fornece vários relatórios integrados, alguns dos quais podem ajudá-lo a encontrar consultas de
problemas e seus planos. Não posso abordar esses relatórios em detalhes, mas descreverei o básico do que
eles oferecem e, em seguida, focarei no uso de um dos relatórios disponíveis para o Repositório de consultas.

462
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Visão geral dos relatórios do repositório de consultas

Se você expandir seu banco de dados no Pesquisador de Objetos, verá uma pasta marcada como Repositório de Consultas.
Expanda essa pasta e você deverá ver os relatórios mostrados na Figura 16-3.

Figura 16-3: Relatórios do Repositório de Consultas no SSMS.

Cada um desses relatórios traz informações diferentes com base em sua estrutura. A maioria dos relatórios tem um layout
muito semelhante. A exceção é o consumo geral de recursos
relatório, que mostra um conjunto de dados muito diferente dos demais. A abertura desse relatório mostra as consultas
classificadas por consumo de recursos ao longo do tempo, com base nos dados de tempo de execução de execução no
Repositório de Consultas.

463
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Figura 16-4: Relatório de consumo geral de recursos do Query Store.

Este relatório é útil para identificar consultas que estão usando mais recursos. Clicar em qualquer consulta abre
a janela Principais consultas que consomem recursos , que veremos em detalhes abaixo.

Os outros relatórios são estruturados como o relatório Principais consultas de consumo de recursos ,
portanto, não passaremos por todas as suas funções. No entanto, vamos descrever onde cada relatório pode ser usado.

464
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Relatório Utilidade

Consultas regredidas Quando o comportamento de tempo de execução de


uma consulta é alterado ao mesmo tempo que o plano de
execução é alterado, pode-se dizer que a consulta regrediu
devido à alteração do plano de execução. Isso pode vir de
um mau sniffing de parâmetros, uma mudança no processo
de otimização ou outros. Este relatório ajudará você a
identificar as consultas nas quais se concentrar.

Consumo geral de recursos Exibido acima na Figura 16-4, este relatório divide as
consultas pelos recursos que elas consomem ao longo do
tempo. É útil ao trabalhar para identificar qual consulta está
causando um problema específico com memória, E/S ou
CPU.

Principais consultas que consomem recursos Isso será abordado em detalhes a seguir. É

simplesmente uma versão focada do relatório de


consumo geral de recursos com uma única métrica sendo
exibida.

Consultas com planos forçados Quando você optar por forçar um plano, detalhado abaixo,
este relatório mostrará quais consultas atualmente possuem
planos que estão sendo forçados.

Consultas com alta variação São consultas que, com base em uma determinada
métrica, estão passando por mais mudanças de
comportamento do que outros planos. Isso pode ser usado
em conjunto com o relatório de consultas regredidas.

Consultas rastreadas Você pode marcar uma consulta para rastreamento por
meio do Repositório de Consultas. As consultas rastreadas
serão então expostas neste relatório.

Cada relatório exibe conjuntos exclusivos de dados com base nas informações capturadas pelo Repositório
de Consultas, mas, exceto o relatório de Consumo Geral de Recursos , todos eles se comportam
aproximadamente da mesma maneira.

465
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

O relatório Principais consultas que consomem recursos

Vamos nos concentrar no relatório Principais consultas de consumo de recursos porque é provável que seja usado
regularmente na maioria dos sistemas. Se você acabou de habilitar o Repositório de Consultas, deverá executar
algumas consultas para ver alguns dados no relatório. Clicar duas vezes no relatório irá abri-lo.

Figura 16-5: Relatório de consultas de consumo de recursos principais para o repositório de consultas.

O relatório está dividido em três seções. No canto superior esquerdo há uma lista de consultas classificadas por
várias métricas. O padrão é Duração. Você pode usar a lista suspensa para escolher entre CPU e outras medidas
fornecidas pelo Repositório de Consultas. Você também pode escolher a Estatística a ser medida. O padrão aqui é
Total. Eles preencherão o gráfico, mostrando as consultas mais problemáticas ao considerar a Duração total. À direita
está uma segunda seção mostrando vários tempos de execução. Cada círculo representa, não uma execução individual,
mas

466
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

tempos de execução agregados em um intervalo de tempo. Pode haver mais de um plano. A seleção
de qualquer um desses planos altera o terceiro painel do relatório, na parte inferior, para uma
representação gráfica do plano de execução em questão. Esse plano gráfico funciona exatamente como
qualquer outro plano gráfico com o qual trabalhamos ao longo do livro.

Em suma, este relatório reúne a consulta, uma agregação de suas métricas de desempenho e o
plano de execução associado a essas métricas. Você pode ajustar os relatórios e modificá-los de
gráficos para mostrar grades de dados. Basta clicar nos botões no canto superior direito da primeira
janela do relatório.

Uma funcionalidade adicional é especialmente interessante do ponto de vista do Plano de Execução.


Quando você tem mais de um plano disponível, como no exemplo da Figura 16-5, você pode
selecionar dois desses planos, usando a tecla SHIFT para selecionar um segundo plano. Com dois
planos selecionados, um dos botões na barra de ferramentas, mostrado na Figura 16-6, permite
comparar os planos.

Figura 16-6: Botão Comparar Planos de Execução.

Clicar nesse botão abre a janela Comparar Plano de Execução (abordada com mais detalhes no
Capítulo 17). Você pode ver os dois planos do exemplo acima na Figura 16-7.

467
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Figura 16-7: Planos de execução comparados com o relatório do Query Store.

A funcionalidade é descrita em detalhes no Capítulo 17. As partes comuns da planta são realçadas em
vários tons de cor (neste caso, rosa). As diferenças nas propriedades são exibidas usando o símbolo
"diferente de". Você pode explorar e expor informações sobre as diferenças e semelhanças entre os planos.

Além disso, há apenas uma outra funcionalidade diretamente aplicável aos planos de execução e a
abordaremos um pouco mais adiante neste capítulo.

Recuperar planos do Repositório de Consultas usando T-SQL

Obter informações sobre o plano de consulta das tabelas de sistema do Repositório de Consultas é
bastante simples. Existem apenas algumas visualizações de catálogo (como você lê uma tabela do sistema)
fornecendo as informações, que são diretamente aplicáveis aos próprios planos:

• query_store_plan – a visão que contém o próprio plano de execução junto com informações
sobre o plano, como query_plan_hash, nível de compatibilidade e se um plano é trivial (tudo
conforme mostrado na Figura 16-1).

468
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

• query_store_query – a exibição que identifica cada consulta, mas não o texto da consulta, que é
armazenado separadamente e inclui informações como o último tempo de compilação, o tipo de
parametrização, o hash da consulta e muito mais. Embora o texto e o contexto sejam armazenados
separadamente, eles são como uma consulta é identificada.
• query_context_settings – define metadados sobre a consulta, como configurações ANSI, se uma
consulta é para replicação e seu idioma.
• query_store_query_text – esta visão define o texto real da consulta.
Embora existam três outras exibições de catálogo do Repositório de Consultas, elas são muito focadas
no desempenho da consulta, portanto não as abordarei diretamente neste livro.

Consultar para recuperar o plano é basicamente uma questão de juntar as exibições de catálogo apropriadas
para recuperar as informações nas quais você está mais interessado. Você pode simplesmente consultar o sys.
query_store_plan, mas você não terá nenhum contexto para esse plano, como o texto da consulta ou o
procedimento armazenado de onde vem. A Listagem 16-3 demonstra um bom uso das tabelas para recuperar um
plano de execução.

SELECT qsq.query_id,
qsqt.query_sql_text,
CAST(qsp.query_plan AS XML),
qcs.set_options
DE sys.query_store_query AS qsq
JOIN sys.query_store_query_text AS qsqt
ON qsqt.query_text_id = qsq.query_text_id
JOIN sys.query_store_plan AS qsp
ON qsp.query_id = qsq.query_id
JOIN sys.query_context_settings AS qcs
ON qcs.context_settings_id = qsq.context_settings_id
WHERE qsq.object_id = OBJECT_ID('dbo.AddressByCity');

Listagem 16-3

Supondo que você tenha executado pelo menos uma vez um procedimento armazenado chamado
dbo.AddressBy City, você obterá informações de volta. Eu incluí o query_context_settings
sob a suposição de que, se uma consulta for executada usando configurações diferentes, você poderá vê-la mais
de uma vez. Para que os resultados contenham um plano de execução clicável, optei por CAST o plano como
XML. Os resultados dessa consulta seriam semelhantes à Figura 16-8.

Figura 16-8: Resultados da consulta nas tabelas de sistema do Query Store.

469
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Essa consulta retorna o plano de execução como uma coluna clicável e mostra o query_id.
Recuperar informações adicionais sobre o plano é apenas uma questão de adicionar colunas a essa consulta.
Um ponto digno de nota é o texto da consulta, conforme mostrado aqui. A Listagem 16-4 mostra o texto
completo dessa coluna.

(@City nvarchar(30))SELECT a.AddressID ,a.AddressLine1 ,a.AddressLine2 ,a.City ,sp.


[Name] AS StateProvinceName ,a.PostalCode FROM Person.Address AS a JOIN
Pessoa.StateProvince AS sp ON a .StateProvinceID = sp.StateProvinceID ONDE a.Cidade = @

Cidade

Listagem 16-4

Esta é uma consulta que contém um parâmetro conforme definido pelo procedimento armazenado de onde
a consulta vem:

CREATE OR ALTER PROC dbo.AddressByCity @City NVARCHAR(30)


COMO

SELECIONE a.IDEndereço,
a.Linha de Endereço1,
a.AddressLine2,
Uma cidade,
sp.Name AS StateProvinceName,
a.Código postal
DE Pessoa.Endereço AS a
JOIN Person.StateProvince AS sp
ON a.StateProvinceID = sp.StateProvinceID
WHERE a.Cidade = @Cidade;

Listagem 16-5

Observe a alteração no texto da consulta. No Query Store, a definição do parâmetro, @City, é incluída
com o texto da consulta na frente da instrução (@City nvar char(30)). Esse mesmo texto não está incluído
no texto da consulta do procedimento armazenado conforme mostrado na Listagem 16-5. Esse capricho no
funcionamento do Repositório de Consultas pode dificultar o rastreamento de consultas individuais nas exibições
de catálogo.

470
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Existe uma função, sys.fn_stmt_sql_handle_from_sql_stmt, que o ajudará a resolver uma consulta parametrizada
simples ou forçada do Query Store. Essa função não funciona com procedimentos armazenados, no entanto. Lá,
você seria forçado a usar o operador LIKE para recuperar as informações. Você pode usar o object_id, mas terá
que lidar com quantas instruções estiverem contidas no procedimento. Para encontrar instruções individuais, você
será forçado a usar as funções listadas abaixo.

Vejamos um exemplo disso em ação, fazendo uma consulta muito simples como a Listagem 16-6.

SELECT bom.BillOfMaterialsID,
bom.InícioData,
bom.EndDate
DE Production.BillOfMaterials AS bom
WHERE bom.BillOfMaterialsID = 2363;

Listagem 16-6

A consulta na Listagem 16-6 resultará em um plano de consulta que usa parametrização simples para
garantir o potencial de reutilização do plano. Isso significa que o valor, 2363, é substituído por um parâmetro,
@1, dentro do plano armazenado em cache. Se executássemos uma consulta como a Listagem 16-7, não
veríamos nenhum dado.

SELECT qsqt.query_text_id
DE sys.query_store_query_text AS qsqt
WHERE qsqt.query_sql_text = 'SELECT bom.BillOfMaterialsID,
bom.InícioData,
bom.EndDate
DE Production.BillOfMaterials AS bom
WHERE bom.BillOfMaterialsID = 2363;';''

Listagem 16-7

Os resultados são um conjunto vazio completo porque o Query Store não tem o T-SQL original que
passamos. Em vez disso, ele tem o novo texto que define o parâmetro. É aqui que a função
sys.fn_stmt_sql_handle_from_sql_stmt entra em ação. Modificaremos nossa consulta nas exibições de catálogo
do Repositório de Consultas para filtrar a consulta em questão.

471
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

SELECT qsqt.query_text_id
DE sys.query_store_query_text AS qsqt
JOIN sys.query_store_query AS qsq
ATIVADO qsq.query_text_id = qsqt.query_text_id
APLICAÇÃO CRUZADA sys.fn_stmt_sql_handle_from_sql_stmt(
'SELECT bom.BillOfMaterialsID,
bom.InícioData,
bom.EndDate
DE Production.BillOfMaterials AS bom
WHERE bom.BillOfMaterialsID = 2363;',
qsq.query_parameterization_type) AS fsshfss
WHERE fsshfss.statement_sql_handle = qsqt.statement_sql_handle;''

Listagem 16-8

Para trabalhar com sys.fn_stmt_sql_handle_from_sql_stmt, você deve fornecer dois valores. A primeira é a
consulta na qual você está interessado. No nosso caso, essa é a consulta da Listagem 16-6. A segunda contém o
tipo de parametrização. Felizmente, essas informações são armazenadas diretamente na tabela
sys.query_store_query, para que possamos recuperá-las.
Com esses valores fornecidos, obteremos a consulta necessária no conjunto de resultados.

Planos de controle usando forçar plano


Um dos aspectos mais importantes do Repositório de Consultas, em relação aos planos de execução, é a
capacidade de escolher um plano de execução para uma determinada consulta e, em seguida, usar a imposição de
plano no Repositório de Consultas para forçar o otimizador a usar esse plano. É muito mais fácil usar o forçamento de
plano no Query Store do que implementar um guia de plano (consulte o Capítulo 9). Se você tiver um guia de plano
existente para uma consulta e também forçar um plano, talvez um plano diferente, usando o Repositório de Consultas,
a imposição do plano Repositório de Consultas terá precedência. Se você estiver no Banco de Dados SQL do Azure
ou usando o SQL Server 2016 ou superior e precisar forçar o otimizador a usar um plano de execução, o método
preferencial é usar o forçamento de plano por meio do Repositório de Consultas em vez de guias de plano.

O Repositório de Consultas foi projetado para coletar dados usando um processo assíncrono. O forçamento do
plano é a única exceção a esse processo. Neste caso, quando você define um plano como um plano forçado,
independente do que aconteça com o plano em cache, compila ou recompila, reinicializa o servidor, até mesmo
backup e restauração do banco de dados, esse plano será forçado. Para forçar um plano, o plano deve ser válido
para a consulta e estrutura conforme definido atualmente; alterações na indexação, por exemplo, podem significar
que um plano não é mais válido para uma consulta.

472
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

As informações de que um plano é forçado são gravadas nas tabelas de sistema do Repositório de
Consultas e armazenadas no banco de dados. Com o Repositório de Consultas habilitado, e se o plano for
um plano válido, se for forçado, esse é o plano de execução que será usado. Há uma situação relativamente
obscura em que um plano "moralmente equivalente", um plano idêntico em todos os fundamentos essenciais,
mas não necessariamente perfeitamente idêntico, pode ser usado em vez do plano preciso que você definiu.
No entanto, isso não é comum.

O forçamento de planos é uma faca de dois gumes que pode ajudar ou prejudicar dependendo de como é
implementado e mantido. Eu recomendo o uso extremamente criterioso de forçar planos e aconselho você a
descobrir um cronograma para revisar os planos que foram forçados. Isso não é algo que você define uma vez
e esquece.

Dito isso, existem várias situações em que você pode considerar o uso de forçar plano, uma das quais é a
clássica situação de "sniffing de parâmetros que deu errado", que encontramos várias vezes anteriormente
no livro. No entanto, outro bom caso de uso é corrigir problemas de "regressão de plano", onde alguma
mudança no sistema significa que o otimizador gera um novo plano, que não funciona tão bem quanto o plano
antigo. A regressão do plano pode ocorrer após, por exemplo, atualizar de uma versão do SQL Server anterior
a 2014 que usava o antigo mecanismo de estimativa de cardinalidade ou aplicar atualizações cumulativas ou
hot fixes que introduzem alterações no otimizador de consulta. Há um relatório específico disponível para
consultas regredidas. Durante as atualizações ou ao aplicar uma CU, é uma boa ideia executar o Repositório de
Consultas antes de alterar o nível de compatibilidade durante uma atualização ou aplicar a CU nessa situação.

Como forçar um plano


Demonstrarei o básico de como forçar um plano, usando o caso de "farejamento de parâmetro ruim" como
exemplo.

Execute o procedimento armazenado dbo.AddressByCity, passando-lhe um valor de 'London'.

EXEC dbo.AddressByCity @Cidade = N'Londres';

Listagem 16-9

Vejamos o plano de execução.

473
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Figura 16-9: Primeiro plano de execução do procedimento armazenado.

Em seguida, devemos garantir que o plano de execução do procedimento armazenado dbo.AddressByCity


seja removido do cache.

DECLARE @PlanHandle VARBINARY(64); SELECT


@PlanHandle = deqs.plan_handle FROM
sys.dm_exec_query_stats AS deqs CROSS APPLY
sys.dm_exec_sql_text(deqs.sql_handle) AS dest
WHERE dest.objectid = OBJECT_ID('dbo.AddressByCity'); SE @PlanHandle
NÃO É NULL BEGIN; DBCC FREEPROCCACHE(@PlanHandle); FIM;

VAI

Listagem 16-10

Se executarmos a consulta novamente, mas desta vez passarmos o valor de 'Mentor', veremos um plano de
execução completamente diferente.

EXEC dbo.AddressByCity @Cidade = N'Mentor';

Listagem 16-11

474
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Figura 16-10: Segundo plano de execução do procedimento armazenado.

Este é um caso clássico de sniffing de parâmetros que deu errado. Cada plano funciona muito bem para
as contagens de linhas estimadas, que são maiores para 'London' e menores para 'Mentor', mas surgem
problemas quando uma consulta que retorna muitas linhas usa o plano otimizado para retornar conjuntos de
dados menores. Em algumas circunstâncias, esse tipo de comportamento leva a problemas de desempenho.
De volta ao Capítulo 10, abordamos exatamente o mesmo problema aplicando o OPTIMIZE
PARA dica de consulta.

Digamos que um desses planos leve a um desempenho mais consistente e previsível em uma faixa de
valores de parâmetros do que o outro. Gostaríamos de usar o Query Store para forçar o otimizador a
sempre usar esse plano.

O T-SQL para forçar um plano requer que primeiro obtenhamos o query_id e o plan_id. Isso significa que
temos que rastrear essas informações das tabelas do Repositório de Consultas.

SELECT qsq.query_id,
qsp.plan_id,
CAST(qsp.query_plan AS XML)
DE sys.query_store_query AS qsq
JOIN sys.query_store_plan AS qsp
ON qsp.query_id = qsq.query_id
WHERE qsq.object_id = OBJECT_ID('dbo.AddressByCity');

Listagem 16-12

475
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Isso retornará as informações que precisamos junto com o plano de execução para que possamos determinar qual
plano queremos. Olhe para os planos para determinar o que você deseja forçar. Implementar a imposição do plano
é, então, extremamente simples.

EXEC sys.sp_query_store_force_plan 214, 248;

Listagem 16-13

Agora, se eu remover este plano do cache, usando a Listagem 16-9 novamente, independentemente do valor
passado para o procedimento armazenado dbo.AddressByCity, o plano gerado sempre será o plano que eu
escolhi. As informações dentro do plano e o comportamento do plano serão as mesmas de qualquer outro plano
de execução dentro do sistema, com algumas exceções.
Primeiro, o plano definido será sempre o plano retornado (exceto quando for um plano moralmente equivalente ou
um plano inválido) até que deixemos de forçar o plano ou desabilitemos o Repositório de Consultas. Segundo, um
marcador foi adicionado às propriedades do plano de execução para que possamos ver que é um plano forçado.

Figura 16-11: Use as propriedades do plano do operador SELECT.

No primeiro operador, neste caso o operador SELECT , uma nova propriedade será adicionada aos planos
que forem forçados, Use plan. Se esse valor for definido como True, esse plano será um plano de execução
forçada.

Você pode recuperar informações sobre planos que são forçados consultando diretamente o Repositório de Consultas.

SELECT qsq.query_id,
qsp.plan_id,
CAST(qsp.query_plan AS XML)
DE sys.query_store_query AS qsq
JOIN sys.query_store_plan AS qsp
ON qsp.query_id = qsq.query_id
WHERE qsp.is_forced_plan = 1;

Listagem 16-14

476
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Com essas informações, você pode, se desejar, desforçar um plano usando outro comando.

EXEC sys.sp_query_store_unforce_plan 214, 248;

Listagem 16-15

Isso deixará de forçar o plano de execução do Repositório de Consultas e todos os outros comportamentos
retornarão ao normal.

Você também pode usar a GUI para forçar e cancelar a força de planos. Se você observar o relatório da
Figura 16-4, mostrado novamente na Figura 16-11, poderá ver, no lado direito, dois botões, Force Plan e
Unforce Plan.

Figura 16-12: Plano forçado nos relatórios do Query Store.

Você pode clicar em um plano no painel superior direito e selecionar Forçar plano para forçar o plano da
mesma forma que teria usado o T-SQL para fazer isso. Desforçar o plano é tão simples. Se um plano for
forçado, você poderá ver uma marca de seleção na lista do plano à direita e em qualquer lugar que o plano esteja
visível. Escolhendo forçar ou cancelar a força de um plano do relatório, você será solicitado a verificar se tem
certeza.

477
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Apenas lembre-se de que forçar um plano pode ser uma boa opção para lidar com regressões de planos.
No entanto, essa escolha deve ser revisada regularmente para ver se a situação mudou de alguma forma que
sugira que a remoção do plano forçado seja a escolha preferida.

Forçamento de plano automatizado

Introduzido no SQL Server 2017 e a base do ajuste automático no Banco de Dados SQL do Azure, o
Repositório de Consultas pode ser usado para identificar e corrigir automaticamente a regressão do plano. Ele
é chamado de ajuste automático, mas entenda, é apenas usar o bom plano mais recente que é executado
consistentemente melhor do que outros planos no Repositório de Consultas. Não está ajustando o banco de
dados em termos de atualização de estatísticas, adição, remoção ou modificação de índices ou, o mais
importante, alterando o código. No entanto, para muitas situações, isso pode ser suficiente para lidar
automaticamente com problemas de desempenho.

A sintonia automática está desabilitada por padrão. Para habilitá-lo, primeiro você deve ter o Repositório de
Consultas habilitado e coletando dados. Então, é um comando simples para habilitar o ajuste automatizado.

ALTER DATABASE CURRENT SET AUTOMATIC_TUNING(FORCE_LAST_GOOD_PLAN =


SOBRE);

Listagem 16-16

O mecanismo de banco de dados realmente monitorará o desempenho das consultas usando as


informações coletadas no Repositório de Consultas. Quando uma mudança de plano claramente causa
problemas de desempenho, uma regressão, o mecanismo pode habilitar automaticamente o último plano
bom. Esse pode não ser o melhor plano possível, dependendo das circunstâncias, mas será um plano melhor
do que o que está atualmente em uso. No entanto, o mecanismo também verificará automaticamente se o
desempenho melhorou ou piorou. Se estiver degradado, a imposição do plano será revogada e o plano será
recompilado na próxima chamada.

Você pode ver imediatamente, mesmo sem habilitar o ajuste automático, se uma potencial oportunidade de
ajuste automático estiver disponível. Um novo DMV, sys.dm_db_tuning_recommendations, está disponível
para mostrar essas recomendações. A Figura 16-13 mostra todas as colunas retornadas do DMV.

478
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Figura 16-13: DMV para recomendações de ajuste automático.

Embora todas as colunas possam ser importantes dependendo da situação, as mais interessantes são o
tipo, motivo, estado e detalhes. O restante dos dados é amplamente informativo. No entanto, não
podemos apenas consultar esses dados diretamente. Os dados nas colunas de estado e detalhes são
armazenados como JSON. A Listagem 16-17 mostra como separar essas informações.

SELECT ddtr.reason,
ddtr.score,
pfd.query_id,
JSON_VALUE(ddtr.state,
'$.currentValue') AS CurrentState FROM
sys.dm_db_tuning_recommendations AS ddtr
APLICAÇÃO CRUZADA

OPENJSON(ddtr.details,
'$.planForceDetails')
WITH (query_id INT '$.queryId') AS pfd;

Listagem 16-17

479
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Essa consulta reunirá alguns dos dados interessantes do DMV. No entanto, para realmente colocar
esses dados para funcionar com as informações do Query Store para entender melhor o que está
acontecendo, teremos que expandir um pouco as consultas JSON. A Listagem 16-18 combina os
dados da DMV sys.dm_db_tuning_recommendations com as exibições de catálogo do Repositório
de Consultas.

WITH DbTuneRec
AS (SELECT ddtr.reason,
ddtr.score,
pfd.query_id,
pfd.regressedPlanId,
pfd.recommendedPlanId,
JSON_VALUE(ddtr.state,
'$.currentValue') AS CurrentState,
JSON_VALUE(ddtr.state, '$.reason ') AS CurrentStateReason,
JSON_VALUE(ddtr.details,
'$.implementationDetails.script') AS

ImplementaçãoScript FROM
sys.dm_db_tuning_recommendations AS ddtr
CROSS APPLY
OPENJSON(ddtr.details,
'$.planForceDetails')
WITH (query_id INT '$.queryId',
regressedPlanId INT '$.regressedPlanId', recomendadoPlanId
INT '$.recommendedPlanId') AS pfd)
SELECT qsq.query_id,
dtr.reason,
dtr.score,
dtr.CurrentState,
dtr.CurrentStateReason,
qsqt.query_sql_text,
CAST(rp.query_plan AS XML) AS RegressedPlan,
CAST(sp.query_plan AS XML) AS SuggestedPlan,
dtr.ImplementationScript
DE DbTuneRec AS dtr
JOIN sys.query_store_plan AS rp
ON rp.query_id = dtr.query_id AND
rp.plan_id = dtr.regressedPlanId JOIN
sys.query_store_plan AS sp
ON sp.query_id = dtr.query_id E sp.plan_id
= dtr.recommendedPlanId JOIN sys.query_store_query
AS qsq

480
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

ON qsq.query_id = rp.query_id
JOIN sys.query_store_query_text AS qsqt
ON qsqt.query_text_id = qsq.query_text_id;

Listagem 16-18

Esta consulta mostrará o motivo da recomendação e a pontuação (um valor de impacto estimado de 0 a
100), o estado atual e o motivo, a consulta, os dois planos em questão e, por fim, o script para implementar a
alteração sugerida. Você pode usar essa consulta quando o Repositório de Consultas estiver habilitado (e
você estiver no SQL Server 2017 e superior) para encontrar possíveis candidatos de forçar o plano; ou você
pode habilitar o forçamento automático de plano e, em seguida, essa consulta provavelmente encontrará
consultas que já possuem um plano forçado por esse recurso.

Você pode ver a saída do meu sistema na Figura 16-18.

Figura 16-14: Uma oportunidade sugerida de ajuste automático.

Eu tenho um procedimento armazenado simples, dbo.ProductTransactionHistoryByRefer ence, que


gera cinco planos de execução diferentes quando você o executa em toda a lista de valores ReferenceID
(usei um script do PowerShell).

CRIAR OU ALTERAR PROC dbo.ProductTransactionHistoryByReference (@


ReferenceOrderID INT)
COMO

COMEÇAR

SELECT p.Nome,
p.ProdutoNúmero,
th.ReferenceOrderID
DA Produção.Produto AS p
JOIN Production.TransactionHistory AS th
ON th.ProductID = p.ProductID
WHERE th.ReferenceOrderID = @ReferenceOrderID;
FIM;

Listagem 16-19

Um desses planos é muito mais lento que os outros. Com os planos sendo recompilados regularmente, é
inevitável que o plano mais lento cause problemas. Em algum momento, o mecanismo identificará esses
problemas e criará um plano forçado. Posso aproveitar o relatório de planos forçados para ver o plano.

481
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Figura 16-15: Relatório de planos forçados do Query Store mostrando o ajuste automático.

Você pode ver que há uma marca de seleção no ID do plano 7, o plano que está destacado e visível.
Isso significa que o sistema forçou esse plano. Posso verificar isso voltando para sys.dm_
db_tuning_recommendations e examinando colunas adicionais.

SELECT ddtr.reason,
ddtr.valid_since,
ddtr.last_refresh,
ddtr.execute_action_initiated_by FROM
sys.dm_db_tuning_recommendations AS ddtr;
Listagem 16-20

482
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Isso nos permitirá saber, não apenas um processo de ajuste sugerido, mas quando foi iniciado e por
quem. A saída do sistema tem a aparência mostrada na Figura 16-16.

Figura 16-16: Saída do DMV sys.dm_db_tuning_recommendations.

Você pode ver que a ação foi executada pelo sistema. Esta é uma evidência direta de que o sistema
decidiu forçar este plano de execução.

Com o tempo, o sistema continua a medir o desempenho das consultas. No meu exemplo acima,
ocasionalmente, por meio de medições, decidirá que forçar esse plano de fato prejudica o desempenho.
Nesse caso, você verá que a imposição do plano será removida e, se observar as colunas Revert_*
disponíveis em sys.dm_db_tuning_recommendations, verá que elas serão preenchidas. O fato de que o
plano foi forçado e, mais importante, por que, não será removido de sys.dm_db_tuning_recommendations,
a menos que você remova os dados do Repositório de Consultas (mais sobre isso na próxima seção).

Por fim, você pode decidir remover manualmente o forçamento do plano. Você pode usar o botão no
relatório, visível na Figura 16-15 e outros relatórios neste capítulo, ou usar o comando T-SQL mostrado
na Listagem 16-13. Nesse caso, o execute_action_initiated_by
coluna (Listagem 16-20) mostrará Usuário em vez de sistema.

Se você decidir substituir o ajuste automático, essa consulta não será forçada automaticamente
novamente, independentemente do comportamento. Suas escolhas têm precedência sobre a automação.
A exceção a isso surgirá se você remover os dados do Repositório de Consultas. Isso resultará no retorno
ao plano forçado novamente porque sua substituição não pode sobreviver à perda de dados. Sempre que
você substituir o comportamento do ajuste automático, ele impedirá qualquer manipulação automática
adicional dos planos, ligado ou desligado.

Remover planos do repositório de consultas


Se você desabilitar o Repositório de Consultas, ele deixará todas as informações no lugar. Se
você deseja remover cada bit de informação do Repositório de Consultas, você pode emitir o comando
na Listagem 16-21.

ALTER DATABASE AdventureWorks2014 SET QUERY_STORE CLEAR;

Listagem 16-21

483
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

No entanto, isso é pesado, a menos que sua intenção seja, por exemplo, remover dados de produção de um
banco de dados antes de usar esse banco de dados em um ambiente de desenvolvimento. Se você quisesse
remover apenas uma consulta específica e todas as suas informações associadas, incluindo todos os planos de
execução, você poderia usar a Listagem 16-22.

EXEC sys.sp_query_store_remove_query
@query_id = 214;

Listagem 16-22

Se eu tivesse recuperado o query_id usando outra consulta, como uma da Listagem 16-3, eu poderia usar o valor para
executar essa consulta. Ele remove a consulta, todos os planos capturados e todas as estatísticas de tempo de
execução registradas. Isso impediria até mesmo o forçamento do plano porque a consulta foi removida e as
informações não estão mais armazenadas no banco de dados.

Você também pode segmentar apenas planos para remoção. Se recuperássemos o plan_id usando a Listagem 16-10,
poderíamos remover um plano do Repositório de Consultas usando a Listagem 16-23.

EXEC sys.sp_query_store_remove_plan @plan_id = 248;

Listagem 16-23

Isso deixará a consulta intacta, bem como quaisquer outros planos associados a essa consulta. Ele removerá o
plano de execução definido pelo plan_id. Se esse plano estiver associado ao plan_
forcing, o forçamento de plano será interrompido porque o plano não está mais no banco de dados.

Uma coisa importante a ser lembrada sobre as informações do Query Store é que elas são armazenadas
com o banco de dados, dentro das tabelas do sistema. Isso significa que é feito backup com o banco de dados. Se
você fizer backup de um banco de dados de produção e, em seguida, restaurá-lo em um sistema que não seja de
produção, todas as informações do repositório de consultas irão com ele. Isso inclui qualquer texto armazenado
com a consulta, como critérios de filtragem ou valores de parâmetro de tempo de compilação. Se estiver trabalhando
com dados com acesso limitado, como dados de saúde, você precisa levar em consideração o Repositório de
Consultas ao remover informações confidenciais de um banco de dados antes de fornecê-las a pessoas não autorizadas.
Use o mecanismo de remoção apropriado acima para garantir a proteção adequada de seus dados.

484
Machine Translated by Google

Capítulo 16: O Repositório de Consultas

Resumo
O Repositório de Consultas apresenta muitas informações úteis para ajustes de desempenho de consultas
e planos de execução. Ele persiste essas informações com o banco de dados, o que permite que você
faça todos os tipos de solução de problemas e ajuste de desempenho offline do seu sistema de produção.
O forçamento de plano significa que você não precisa se preocupar com certos tipos de regressões de
plano no futuro, pois pode desfazê-las facilmente e evitar que aconteçam novamente. No entanto, não se
esqueça de que os dados e as estatísticas mudam com o tempo, portanto, o plano perfeito para forçar hoje,
pode não ser o plano perfeito amanhã.

485
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para Exploração


Planos de Execução
Aprender o que compõe um plano de execução e entender como ler as propriedades e os operadores
é uma parte fundamental para aumentar seu conhecimento sobre como escrever consultas T-SQL
eficientes e aprimorar suas habilidades para ajustar aquelas que estão causando problemas.
No entanto, como você viu em alguns dos capítulos anteriores, certos planos são mais difíceis de
navegar e leva tempo para reunir todos os detalhes de cada operador e suas várias propriedades, para
descobrir exatamente como o SQL Server escolheu executar uma consulta, e por que, e que ajuda você
pode oferecer ao otimizador para chegar a um plano melhor, se necessário. Nesses casos, não é uma má
ideia obter um pouco de ajuda extra e, neste capítulo, abordarei as ferramentas de gerenciamento do SQL
Server que uso quando preciso de um pouco de orientação extra na leitura e compreensão de um plano.
Também mencionarei brevemente algumas das ferramentas de terceiros que considero úteis ao tentar
navegar em planos mais complexos.

A pergunta
A verdadeira força dessas ferramentas está na ajuda extra que oferecem na leitura e compreensão de
planos mais complexos, muitas vezes com centenas de operadores, em vez de apenas um punhado. No
entanto, seria difícil demonstrar esses planos facilmente dentro dos limites de um livro.
Portanto, optei por usar uma consulta relativamente simples e um plano direto, embora com alguns
problemas inerentes. Usarei a mesma consulta por toda parte, mostrada na Listagem 17-1.

SELECT soh.OrderDate,
soh.Status
sod.CarrierTrackingNumber,
sod.OrderQty,
p.Nome
FROM Sales.SalesOrderHeader AS soh
JOIN Sales.SalesOrderDetail AS sod
ON sod.SalesOrderID = soh.SalesOrderID
JUNTE -SE à Produção.Produto AS p
ON p.ProductID = sod.ProductID
ONDE sod.PedidoQtd * 2 > 60
AND sod.ProductID = 867;

Listagem 17-1

486
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

Essa consulta se beneficiaria de um pequeno ajuste e um novo índice. Primeiro, o cálculo na coluna
OrderQty é desnecessário. Em seguida, não há índice para dar suporte aos critérios de filtro na cláusula
WHERE. A Figura 17-1 mostra o plano de execução resultante no SSMS.

Figura 17-1: Plano de execução no SSMS para a consulta problemática.

Você pode ver que a verificação da chave primária da tabela SalesOrderDetail é estimada como o operador
mais caro. Há uma sugestão para um possível índice mostrado nas informações de índice ausente na parte
superior da tela:

CRIAR ÍNDICE NÃO CLUSTERADO [<Nome do índice ausente, sysname,>]


ATIVADO [Vendas].[Detalhe do pedido de vendas] ([ID do produto])
INCLUIR ([SalesOrderID],[CarrierTrackingNumber],[OrderQty])

Dada a natureza simples dessa consulta, provavelmente já temos informações suficientes disponíveis
para que possamos começar a ajustar a consulta. No entanto, agora vamos usá-lo para explorar os
benefícios adicionais de nossas ferramentas.

487
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

As ferramentas do SQL Server Management Studio 17

Após muitos anos de melhorias relativamente modestas nas informações disponíveis com planos de execução, a
versão mais recente, SSMS 17, deu alguns passos maiores para aumentar a visibilidade de informações importantes
nos planos, permitindo comparar essas informações entre planos e muito mais.

Antes do lançamento do SQL Server 2017, foi anunciado que o SSMS se tornaria um software autônomo, instalado
e mantido separadamente do mecanismo do SQL Server. Isso separou o SSMS do ciclo de lançamento mais longo e
mais lento de Service Packs e Atualizações Cumulativas e permitiu que a equipe do SSMS introduzisse aprimoramentos
em um ritmo mais rápido do que estávamos acostumados, incluindo vários em suporte aos planos de execução.

Ainda é uma ferramenta gratuita e você pode baixá-la da Microsoft (http://bit.ly/2kDEQrk). Você pode instalá-lo lado a lado
com as versões existentes do SSMS. A versão atual (no momento da redação deste artigo) dá suporte ao SQL Server 2008–
2017, bem como ao Banco de Dados SQL do Azure, e tem algum suporte limitado para o Azure SQL Data Warehouse.

Estamos explorando planos usando SSMS ao longo do livro, então vou abordar apenas a nova funcionalidade que foi
explicitamente introduzida para ajudá-lo a entender os planos de execução.

Clique com o botão direito do mouse em um plano de execução no SSMS 17 e você verá um menu de contexto listando
três novas funcionalidades: Compare Showplan, Analyze Actual Execution Plan e Find Node.

Figura 17-2: Menu de contexto mostrando as opções de menu mais recentes relacionadas aos planos de execução.

488
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

Analisar o Plano de Execução Real


Selecione Analyze Actual Execution Plan no menu e ele abrirá um novo painel na parte inferior da
janela de consulta, conforme mostrado na Figura 17-3.

Figura 17-3: Análise do plano de execução com uma única consulta para o lote.

Com um único lote de instruções, como o exemplo da Listagem 17-1, você verá apenas uma única consulta.
Se você tiver várias instruções em seu lote, verá várias consultas. Para ter uma das consultas analisadas,
basta selecionar essa consulta usando os botões de opção. Em seguida, clique na guia Cenários , onde
cada cenário mostra detalhes sobre uma categoria de possíveis problemas encontrados nos planos.

Figura 17-4: A guia Cenários da Análise do Plano de Execução com problemas sugeridos.

De acordo com a Microsoft, os cenários apresentados para uma consulta fornecerão diferentes
mecanismos de análise para orientá-lo em planos problemáticos. No momento da redação, eles definiram
apenas um cenário, Estimativa de cardinalidade imprecisa. Essa é uma boa escolha, pois é um problema
comum em um ambiente estável e um problema muito sério durante as atualizações, especialmente ao passar
de servidores anteriores ao SQL Server 2014 para servidores mais recentes ao SQL Server 2014 (onde o
novo mecanismo de estimativa de cardinalidade foi introduzido).

489
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

Para o cenário Estimativa de cardinalidade imprecisa , as informações são divididas em duas


partes. À esquerda está uma lista de operadores onde as estimativas de cardinalidade diferem
significativamente entre estimada e real. Você recebe informações sobre as diferenças, em uma grade
organizada. Isso mostra os valores reais e estimados para cada operador, o nó envolvido e a diferença
percentual. Se você verificar as propriedades do operador Clustered Index Seek (destacado na Figura
17-4) no plano gráfico, verá que o número real de linhas é 6 e o número estimado de linhas é 1, mas
a análise do plano de execução contabiliza com precisão pelo fato de que o Número Estimado de
Execuções é 69,4177, dando um número total estimado de linhas retornadas de 69,4177.

À direita, você encontrará uma explicação de uma ou mais razões possíveis pelas quais a
estimativa de cardinalidade pode ser diferente. Isso fornece orientação sobre como resolver o problema
e possivelmente melhorar o desempenho da consulta, embora nunca assuma que essa orientação seja
100% precisa. Sempre valide-o em seu sistema antes de implementar o conselho.

A seleção de qualquer um dos nós também atualizará qual nó é selecionado no próprio plano de
execução e atualizará a orientação para que reflita o nó selecionado. Na Figura 17-5, selecionei o
terceiro nó na lista, uma das junções de loops aninhados .

Figura 17-5: Selecionar diferentes sugestões também altera os nós no plano.

490
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

Embora essa funcionalidade seja limitada no momento, sei que haverá mais aprimoramentos, que devem
aprofundar sua compreensão de possíveis problemas com suas consultas e estruturas de dados, conforme exposto por
meio dos planos de execução no SSMS.

Comparar plano de exibição

Os recursos Compare Showplan nos permitem, talvez sem surpresa, comparar dois planos de execução
diferentes para semelhanças e diferenças. Você pode comparar dois planos reais, dois planos estimados ou
um plano real com um plano estimado; qualquer combinação funcionará. Você também pode comparar planos
entre diferentes versões do SQL Server, diferentes níveis de patch e assim por diante. Se você tiver dois planos
válidos e pelo menos um deles armazenado como um arquivo, poderá compará-los.

Para testá-lo, usaremos a consulta na Listagem 17-2, que é semelhante à Listagem 17-1, pois faz referência às
mesmas tabelas e colunas, mas com uma cláusula WHERE diferente.

SELECT soh.OrderDate,
soh.Status
sod.CarrierTrackingNumber,
sod.OrderQty,
p.Nome
FROM Sales.SalesOrderHeader AS soh
JOIN Sales.SalesOrderDetail AS sod
ON sod.SalesOrderID = soh.SalesOrderID
JUNTE -SE à Produção.Produto AS p
ON p.ProductID = sod.ProductID
ONDE sod.ProductID = 897;

Listagem 17-2

Execute a Listagem 17-1, capture o plano real, use Salvar Plano de Execução como…, para salvá-lo como um
arquivo .sqlplan e, em seguida, capture o plano real para a Listagem 17-2. Clique com o botão direito do mouse e
selecione Comparar Showplans no menu de contexto, que abrirá uma janela do Explorador de Arquivos.
Localize e selecione seu arquivo de plano de exibição salvo e você deverá ver uma comparação de plano de exibição
janela que se parece com a Figura 17-6.

491
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

Figura 17-6: Comparação do plano de exibição incluindo os planos, propriedades e opções de declaração.

O plano superior é aquele a partir do qual iniciamos a comparação (Listagem 17-2). Abaixo dos planos,
você verá a guia Análise do plano de execução , que vimos anteriormente, mas agora com uma guia
adicional, Opções de declaração. A Figura 17-7 mostra uma ampliação desta área.

Figura 17-7: Guia Opções de Instrução da janela Análise do Plano de Execução.

492
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

Por padrão, a caixa de seleção Realçar operações semelhantes está ativada e a caixa abaixo destaca
áreas de funcionalidade semelhante dentro do plano. Nesse caso, você pode ver duas áreas semelhantes,
destacadas em rosa e verde. Se as operadoras conectadas diretamente forem semelhantes em cada plano,
elas serão agrupadas. No nosso caso, duas operadoras são semelhantes, mas em partes diferentes de cada plano.
Além disso, por padrão, a comparação de planos ignora nomes de banco de dados. Você pode não ver
nenhuma semelhança ou pode ver vários conjuntos de semelhanças, caso em que cada "área semelhante"
terá uma cor diferente.

À direita dos planos gráficos estão as janelas Propriedades de cada plano, com o plano superior à
esquerda, que você pode usar para comparar os valores das propriedades entre os planos.
Na Figura 17-6, destaquei o operador SELECT em ambos os planos, e Compare Showplan está
destacando com o sinal de "não igual" os valores de propriedade que não correspondem, conforme
mostrado na Figura 17-8.

Figura 17-8: Comparação de propriedades entre dois planos.

Além disso, você pode ver que existem algumas propriedades visíveis em um plano que não existem no
outro. Nesse caso, apenas o plano da Listagem 17-2 mostra uma propriedade MissingIndexes .

Se você selecionar o operador destacado em rosa na Figura 17-6, o Clustered Index Seek
na tabela Produto, você pode ver que quase todos os valores de propriedade entre esses dois operadores
em dois planos são idênticos.

493
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

Figura 17-9: Uma operadora muito semelhante entre os dois planos.

Até mesmo os valores do Custo Estimado da Operadora são os mesmos, mas é destacado como
diferente porque o custo da operadora como porcentagem de todo o plano é diferente em cada caso. A
outra diferença destacada está na propriedade Seek Predicates . No meu caso, isso ocorre
simplesmente porque eu forcei a parametrização (veja o Capítulo 9) em operação para esta consulta, e
o otimizador usou diferentes nomes de parâmetros durante o processo de parametrização forçada.
Sem isso, as diferenças serão simplesmente os diferentes valores literais usados, em cada caso.

494
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

Podemos alterar o comportamento de comparação do Compare Showplan ativando a caixa de seleção


Realçar operadores que não correspondem a segmentos semelhantes mostrada na Figura 17-7, em vez
de, ou além da caixa de seleção Realçar operações semelhantes . Optei pelo primeiro, e a Figura 17-10
mostra que os operadores não correspondentes agora estão destacados em amarelo.

Figura 17-10: Os operadores não correspondentes agora são destacados.

Eu uso essa funcionalidade o tempo todo ao ajustar consultas porque, embora às vezes haja diferenças
gritantes entre os planos, geralmente elas são muito mais sutis, mas com implicações significativas de
desempenho. Esse recurso ajuda a identificar essas pequenas diferenças mais rapidamente,
especialmente ao comparar dois planos de execução em grande escala quase idênticos.

495
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

Localizar nó
Clique com o botão direito do mouse em um plano gráfico e escolha Find Node, e uma pequena janela será aberta no
canto superior direito do plano de execução. Listada na lista suspensa à esquerda está uma grande lista de propriedades,
conforme mostrado na Figura 17-11.

Figura 17-11: Lista suspensa do recurso Find Node, com todas as propriedades do plano.

496
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

Selecione uma propriedade, por exemplo ActualRows, depois selecione um operador de comparação, "igual"
para pesquisas numéricas ou "contém" para pesquisas de texto e o valor que deseja pesquisar.

Figura 17-12: A lista de propriedades de comparação.

Para pesquisas de texto não há necessidade de curingas; ele pressupõe que você deseja ver
correspondências semelhantes e exatas. Se você pesquisar em ActualRows = 6 e clicar nas setas para a
esquerda ou para a direita, poderá pesquisar os planos, na ordem NodeId, por operadores que retornam 6 linhas.

Figura 17-13: Encontrando o primeiro operador que corresponde aos critérios de pesquisa Find Node.

Embora você realmente não precise Find Node para planos de execução pequenos, torna-se uma grande ajuda
ao lidar com planos maiores, tornando muito mais fácil, por exemplo, encontrar o operador com o ParentNodeID
que corresponde ao NodeID de um operador Table Spool , ou para encontrar todas as referências a um nome de
coluna.

497
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

Planos de execução ao vivo

Um plano de execução ao vivo é aquele que expõe estatísticas de tempo de execução por operador, em tempo
real, à medida que a consulta é executada. Você poderá ver a execução da consulta em ação e visualizar as
estatísticas por operador, à medida que a execução avança e os dados fluem de um operador para o próximo.
Isso é útil se, por exemplo, você precisar entender como os dados se movem pelo plano para uma consulta de
execução muito longa. Um plano de execução ao vivo também mostrará o progresso estimado da consulta, o
que pode ser útil se você precisar decidir se deseja eliminar a consulta.

O SQL Server 2014 foi a primeira versão a apresentar uma maneira de acompanhar o progresso de uma consulta
de longa duração. Você pode consultar o sys.dm_exec_query_profiles Dynamic Management View (DMV) de outra
conexão. No entanto, veio com uma sobrecarga bastante alta, pois os dados só eram capturados se você
executasse a consulta com a opção de incluir o plano de execução real ativado.

Versões subsequentes do SQL Server (e Service Pack 2 para SQL Server 2014) introduziram maneiras de menor
sobrecarga para visualizar as estatísticas de tempo de execução em andamento, sem a necessidade de capturar
o plano real, por meio de um novo evento estendido (query_thread_profile) ou habilitando o Trace Sinalizador
7412. Ativar o sinalizador de rastreamento nos permite usar uma nova infraestrutura leve de criação de perfil de
estatísticas de execução de consulta, que reduz drasticamente a sobrecarga de capturar as estatísticas de
execução de consulta em andamento.

Usar o sinalizador de rastreamento é o método de menor custo dos três, seguido pelo uso do evento estendido
(que habilita o sinalizador de rastreamento automaticamente) e capturar o plano real é a opção mais cara.
Cuidado, porém: mesmo se você estiver usando o sinalizador de rastreamento, baixo custo não significa nenhum
custo. Você ainda deve testar isso cuidadosamente antes de habilitá-lo em seus sistemas de produção.
Há sobrecarga associada à captura das métricas de tempo de execução.

Vamos ver tudo isso em ação. Para fazer isso, apresentaremos uma nova consulta, na Listagem 17-3.

SELECIONAR *
DE sys.objects AS o,
sys.columns AS c;

Listagem 17-3

Essa consulta viola várias regras, muitas das quais mantivemos ao longo deste livro. No entanto, leva cerca
de 40 segundos para ser executado no meu sistema, por isso é um bom teste para todas as outras funções que
veremos nos planos de execução ao vivo.

498
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

Estatísticas ao vivo por operador usando sys.dm_exec_query_profiles

A DMV sys.dm_exec_query_profiles mostra o número de linhas processadas por operadores individuais em uma
consulta em execução no momento, permitindo que você veja o status da consulta em execução e compare os valores
estimados de contagem de linhas com os valores reais.

Se estiver testando isso no SQL Server 2014, mas antes do SQL Server 2014 SP2, você precisará executar a Listagem
17-3 usando qualquer uma das opções que incluem o plano de execução real, seja no SSMS ou usando um dos comandos
SET ou capturando o query_post_execution_
evento showplan (consulte o Capítulo 15).

O SQL Server 2016 introduziu estatísticas de execução ao vivo no SSMS e adicionou aos Eventos Estendidos o novo
evento de categoria "depuração" chamado query_thread_profile. O SQL Server 2016 SP1 introduziu o sinalizador de
rastreamento 7412. Tanto o evento estendido quanto o sinalizador de rastreamento foram retroajustados no SQL Server
2014 SP2.

Portanto, no SQL Server 2014 SP2 ou no SQL Server 2016 SP1 e posterior, a melhor maneira é habilitar primeiro o
sinalizador de rastreamento 7412, conforme mostrado na listagem 17-4.

DBCC TRACEON (7412, -1);

Listagem 17-4

Agora, comece a executar a Listagem 17-3 e, em outra sessão, execute a seguinte consulta no DMV
sys.dm_exec_query_profiles. Observe que estou eliminando a sessão atual da consulta porque, caso contrário, ela aparecerá
nos resultados.

SELECT deqp.session_id,
deqp.node_id,
deqp.physical_operator_name,
deqp.estimate_row_count,
deqp.row_count
FROM sys.dm_exec_query_profiles AS deqp
WHERE deqp.session_id <> @@SPID
ORDER BY deqp.node_id ASC;

Listagem 17-5

O DMV retorna muito mais informações do que solicitei aqui (consulte a documentação da Microsoft para obter uma
descrição completa: https://bit.ly/2JKYe5s), e você pode combinar esse DMV com outros para retornar ainda mais
informações. A Figura 17-14 mostra um subconjunto dos resultados.

499
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

Figura 17-14: Resultados da consulta em relação a sys.dm_exec_query_profiles.

Você pode ver os nós e seus nomes junto com a contagem_linha_estimativa, que mostra o número total estimado
de linhas a serem processadas, que você pode comparar com o número real de linhas processadas atualmente,
na coluna contagem_linha. Você pode ver imediatamente que o nó com um valor de ID de 1, o operador Nested
Loops , tem um número estimado de linhas de 6.264.966 e só processou 94.465. Isso nos permite saber que,
sem dúvida, a consulta ainda está sendo processada e tem um longo caminho a percorrer para chegar ao número
estimado de linhas. Obviamente, se as estimativas de contagem de linhas do otimizador forem imprecisas,
row_count e estimado_row_count poderão não corresponder. No entanto, isso fornece uma maneira de rastrear o
status de execução atual de uma consulta e quanto ela foi processada com êxito.

Se você consultar o DMV novamente enquanto a consulta ainda estiver em execução, poderá ver as alterações
nos dados.

Figura 17-15: Alterações nas informações de sys.dm_exec_query_profiles.

500
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

Como você pode ver, mais linhas foram processadas por vários operadores, mas a execução ainda não foi
concluída. Ao executar a consulta após a conclusão da consulta de longa duração, você não verá um
conjunto completo de contagens de linhas. Em vez disso, você não verá nada, pois não há sessões ativas.

Usando o evento estendido query_thread_profile


Se você quiser ver apenas as informações completas, você pode capturar o query_thread_
evento estendido de perfil, que é acionado para cada operador de plano de consulta e thread de execução,
no final da execução da consulta. É um evento de canal "Debug", portanto, você precisará habilitar esse
canal na GUI do SSMS para eventos estendidos para ver o evento.

Capturando os dados do evento, você verá as estatísticas de execução de cada operador dentro de um
determinado plano de execução. Como dito anteriormente, este é um evento de depuração, portanto, deve-
se ter cuidado ao usá-lo. No entanto, a Microsoft documentou seu uso, portanto, não tenho problemas em
compartilhar isso com você. Para adicionar o evento através do T-SQL, basta adicionar o evento. Para
adicionar o evento por meio da GUI, você precisará clicar no menu suspenso do Canal e selecionar Depurar.
A Figura 17-16 mostra as informações para o operador Nested Loops (NodeId=1) que vimos anteriormente.

Figura 17-16: Saída do evento estendido query_thread_profile.

501
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

Você pode ver que o número estimado de linhas é 6.264.966, como antes. O número real de linhas mostra o valor
completo da execução até a conclusão de 1.273.188. Portanto, nesse caso, a contagem real de linhas é
significativamente menor que a contagem estimada de linhas. Você também obtém informações adicionais
interessantes, como total_time_us e cpu_time_us, que podem ser úteis para ajuste de desempenho.

Planos de execução ao vivo no SSMS


Todas as formas anteriores de ver as informações de tempo de execução "ao vivo" são úteis. Você pode até criar
uma ferramenta que consulte constantemente essas fontes, para mostrar uma visualização ao vivo de um plano de
execução, enquanto ele está sendo executado. No entanto, não precisamos porque, a partir do SQL Server 2016,
esse recurso já está incluído no SSMS. Observe novamente que isso funcionará apenas em versões do SQL Server
que podem mostrar as métricas de consulta ao vivo que capturamos nas seções acima.

A Figura 17-17 mostra o ícone Incluir estatísticas de consulta ao vivo no SSMS (a seta vermelha é toda
minha). Este ícone funciona como um botão de alternância, assim como o botão Incluir Plano de Execução
Real à sua esquerda.

Figura 17-17: A dica de ferramenta e o ícone para Incluir estatísticas de consulta ao vivo.

Se você habilitar Incluir estatísticas de consulta em tempo real e, em seguida, executar a consulta, poderá
capturar um plano de execução em tempo real e visualizar as estatísticas de execução do plano enquanto a consulta
ainda estiver em execução; desligue-o e não o fará (a menos que use o Activity Monitor, como demonstrarei em
breve). Como estamos capturando o plano, não precisamos executar o query_thread_
evento estendido de perfil ou ter o sinalizador de rastreamento 7412 ativado para usar esse recurso. Observe
que habilitar o sinalizador de rastreamento não torna mais leve o uso desse recurso do SSMS; você ainda está
pagando o custo de capturar o plano.

502
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

A Figura 17-18 mostra o plano de execução ao vivo para nossa consulta de longa duração.

Figura 17-18: Um subconjunto de um plano de execução ao vivo em ação.

É claro que mostrar resultados em tempo real e em constante mudança em um quadro estático, dentro de um livro,
não tem o mesmo impacto. As únicas indicações imediatas de que você não está apenas olhando para outro plano
de execução são o progresso estimado da consulta no canto superior esquerdo (atualmente em 12%), as linhas
tracejadas em vez de linhas sólidas entre os operadores e a contagem de linhas com porcentagem concluída
abaixo os operadores. Se você estiver visualizando um plano de execução ao vivo no SSMS, verá as linhas
tracejadas se movendo, indicando a movimentação de dados, e as contagens de linhas subindo à medida que os
dados são processados por um operador. Isso continua até que a consulta conclua a execução, momento em que
você está apenas olhando para um plano de execução regular.

Você também pode observar as propriedades de qualquer um dos operadores durante a execução da consulta.
Lá você verá um conjunto normal de propriedades. No entanto, as propriedades associadas a um plano de
execução real, como a contagem real de linhas, serão alteradas no tempo com o plano, fornecendo indicações
sobre o andamento da consulta, em tempo real.

Visualizando o plano de execução ao vivo no Activity Monitor

Com o Trace Flag 7412 habilitado, ou se você estiver capturando o evento query_thread_profile, outras ferramentas
podem oferecer a exibição de um plano de execução ao vivo, a qualquer momento durante a execução de uma
consulta, sem a necessidade de capturar um plano real.

Assim, podemos usar o Activity Monitor no SSMS para ver as consultas que estão consumindo ativamente uma
grande quantidade de recursos, conforme mostrado na Figura 17-19.

503
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

Figura 17-19: Monitor de atividade mostrando uma consulta cara ativa.

A consulta mostrada no relatório Active Expensive Queries é da Listagem 17-3. Se eu clicar com o botão
direito nessa consulta enquanto ela estiver no estado ativo, verei uma opção de menu, como na Figura 17-20.

Figura 17-20: Um menu de contexto mostrando uma opção para um plano de execução ao vivo.

Se eu selecionar Show Live Execution Plan, serei levado a uma janela como na Figura 17-18. O
comportamento a partir de então é o mesmo.

Os planos de execução ao vivo são úteis se você tiver consultas de execução muito longa e desejar desenvolver
uma compreensão mais direta de como os dados se movem dentro dos operadores. As informações contidas nos
planos de execução ao vivo, bem como os DMVs e eventos estendidos associados, podem ajudá-lo a decidir
quando reverter uma transação ou tomar outros tipos de decisões, com base em quão longe e quão rápido o
processamento foi em uma consulta .

504
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

Eles sofrem de dois problemas. Primeiro, eles são dependentes dos valores estimados. Se estiverem desativadas,
as informações também estarão dentro do plano de execução ao vivo. Em segundo lugar, capturar as informações
para um plano de execução ao vivo, mesmo as opções leves do Trace Flag 7412 ou o evento query_thread_profile,
pode ser muito caro para alguns sistemas. Tenha cuidado ao implementar essa funcionalidade fascinante e útil.

Outras Ferramentas do Plano de Execução

Embora eu tenha decidido que estava fora do escopo abranger ferramentas de terceiros, mencionarei aqui as
que usei, pessoalmente, e que não apenas exibem os planos, mas também oferecem funcionalidades adicionais
que ajudarão você a entendê-las. Essa não é uma lista completa; eles são apenas os que eu usei até agora, e
minhas desculpas se eu deixei de fora seu software favorito.

Explorador de planos

Talvez a ferramenta mais conhecida para navegar nos planos de execução seja o Plan Explorer da SentryOne
(sentryone.com). É um aplicativo completo e autônomo que oferece muitas visualizações e layouts diferentes de
um plano. Ele também executa algumas análises inteligentes dos valores de propriedade, estatísticas de índice
e estatísticas de tempo de execução, para ajudá-lo a ler até mesmo planos de grande escala e identificar
possíveis causas de desempenho abaixo do ideal.

Compreensão

Supratimas é uma ferramenta baseada em navegador da web, disponível gratuitamente online em


supratimas.com. Você pode simplesmente "arrastar e soltar" seu texto de consulta, ou arquivo .sqlplan , e ele exibirá
o plano gráfico e destacará visualmente valores de propriedade importantes e os operadores estimados como os
mais caros. Ele também possui um plug-in SSMS que é gratuito quando suportado por anúncios, ou você pode
comprá-lo.

Pacote de Ferramentas SSMS - Analisador de Plano de Execução

O SSMS Tools Pack (ssmstoolspack.com), escrito por Mladen Prajdiÿ, é uma coleção de complementos para o SQL
Server Management Studio que fornece uma série de funcionalidades adicionais para ajudar a tornar o SSMS um
local mais amigável para trabalhar, incluindo um Execution Plan Analyzer.

505
Machine Translated by Google

Capítulo 17: Ferramentas SSMS para explorar planos de execução

Essa ferramenta funciona diretamente na janela de consulta do SSMS. Ele oferece uma variedade de visões
diferentes dos operadores "caros" no plano, e o analisador destacará problemas potenciais, como uma
grande incompatibilidade entre contagens de linhas estimadas e reais, e sugerirá possíveis cursos de ação.

Ferramentas de monitoramento de desempenho do SQL Server

Não abordarei nenhuma das ferramentas de monitoramento e ajuste de desempenho de


terceiros que capturam o plano de execução como parte de seu conjunto de dados de diagnóstico,
como o que uso, Redgate SQL Monitor. Essas ferramentas não tentam melhorar sua compreensão dos
planos, em vez de apenas apresentá-los. Dito isso, uma ferramenta como o SQL Monitor é valiosa
precisamente porque captura o plano de cada consulta, dentro do contexto de todos os outros dados
úteis de uso de recursos e métricas de desempenho, coletados no momento da execução da consulta.

Resumo
O SSMS 17 nos forneceu muito mais ajuda do que antes para entender os planos de execução e as
diferenças entre os planos. Além disso, existem algumas ferramentas de terceiros que são úteis, especialmente
ao tentar abrir e navegar em planos muito grandes, para identificar possíveis problemas.

Cada uma dessas ferramentas traz diferentes pontos fortes para a mesa, mas nenhuma delas substitui
seu conhecimento de como os planos de execução são gerados por meio do otimizador de consultas e
como lê-los e entendê-los. Em vez disso, eles apenas adicionam ao seu conhecimento, habilidade e eficiência.

506
Machine Translated by Google

Índice
UMA B

Activity Monitor 503–505 Adaptive Modo de lote

Join 30. Consulte também Operadores Ad hoc concessão de memória adaptável 369–372

queries 260–273 misturando índices columnstore e rowstore 366–368

uso excessivo de 264-266

Empurrão agregado. Consulte Índices Columnstore Agregando planejar consultas em 361–372

e Classificando Dados 129–158. Veja também Operadores; antes do SQL Server 2016 364–366

Consulte também Consultas com ORDER BY Agregando dados as operações não serão executadas no modo de lote 372

140–152 processamento 360–372

e bobinas 148–152 processamento definido 360–361

filtrando com HAVING 146–148 versus modo de linha 247

Agregado de correspondência de hash 143–146


C
Agregado de fluxo 140–143

Algebrizador 28, 43 Estimativa de cardinalidade 33, 223, 231

ANSI 75 Índices Columnstore 241–247

Automatizando a captura do plano 436–457. Veja também Planos empilhamento agregado 245 para
de execução: capturando
uma consulta de agregação 242–244
razões para 436-437
empilhamento de predicado 246–247
ferramentas para 437-457
Operadores comuns 57–59
usando Eventos Estendidos 438–451
Expressão de Tabela Comum (CTE). Consulte Escalar de
garantindo sessões de eventos leves 450–451 usando a
computação T-SQL. Ver Operadores
GUI do SSMS 439–444 usando T-SQL 444–445 exibindo
Concatenação 126–128. Veja também Operadores
dados de eventos 445–450
Restrições 29

Converter dados relacionais em XML

usando SQL Trace 452–457


PARA XML. Veja os planos XML
rastreamento do lado do servidor 456–457 Converter XML em dados relacionais

Rastreamento XML do plano de execução 453–456


OPENXML. Veja os planos XML
rastrear eventos 452–453

507
Machine Translated by Google

Índice

Cursores 424–435 para DELETEs 171–177

dinâmico 434-435 plano por índice 173–177

conjunto de chaves 431–434 plano simples 171-172

estático 424-430 para INSERÇÕES 159–168

formatos 39–41
D
para MERGEs 177–183

Fluxo de dados 66 para ATUALIZAÇÕES 168–171

Setas de fluxo de dados. Veja Planos gráficos: compo planos gráficos 41


entes introdução a 26-55

Operadores de leitura de dados 80–97 planejar envelhecimento 35

Resolução de nomes adiada 29 mapa de cache 35–38

Gráfico de densidade 226-227 rotatividade de cache, evitando 37


Tabelas derivadas
limpando manualmente 36–37

usando APLICAR 195–199 recuperando planos de 41

Desabilitando o Repositório de Consultas 483 recompilação do plano 38

Objetos de Gerenciamento Dinâmico (DMOs) 259–260 planos de leitura 57–79

SQL dinâmico 264 qual direção? 60–61

salvando e compartilhando 55, 382


E
planos de texto 40

Número estimado e real de linhas 104–105 Custos estimados planos triviais 31

do operador 222–223. Consulte também Planos gráficos: Planos XML 40–41, 374–391

componentes Custo total estimado da consulta. Veja Planos Eventos Estendidos 64, 76, 78–79, 196, 265

gráficos: componentes e propriedades do primeiro operador 384–385

Planos de execução F
capturando 46–47
Primeiro operador 63–64
captura automática 436–457
Parametrização forçada. Consulte Plano de reutilização
estimado e real 41–42, 65 diferença entre
Forçando planos. Consulte Plano forçando Cumprindo
42–44 explorando. Consulte
comandos JOIN 100–110
Ferramentas para explorar planos de execução para
Funções 212–220
modificações de dados 159–184
escalar 212–215

508
Machine Translated by Google

Índice

tabela avaliada em 216-220 dicas de consulta 304–333

em linha (iTVF) 216 EXPANDIR VISUALIZAÇÕES 331–332

declaração múltipla 216 RÁPIDO n 314-316

ORDEM DE FORÇA 316-319


G
GRUPO DE HASH 305–306

ObterPróximo 61 IGNORE_NONCLUSTERED_COLUMN

LOJA_INDEX 332–333
Planos gráficos. Veja também Planos de execução: planos

gráficos JUNTE
LOOP, MERGE, HASH 309–313
capturando 44–46
MAXDOP 319–322
componentes 47-55
OTIMIZE PARA 322–327
setas de fluxo de dados 49–50
GRUPO DE PEDIDO 305-306
custos estimados do operador 50 custo
RECOMPILAR 327–331
total estimado da consulta 51
UNIÃO
propriedades do operador 51–53
MERGE, HASH, CONCAT 307–309
operadores 48–49
dicas de tabela 335–343
Dicas de ferramentas 53–55
BUSCA DE FORÇA/FORCESCAN 341–343
economizando 55
ÍNDICE() 337–340

H NOEXPAND 336–337

Histograma 227-230
Índice de hash 248
EU

não para um intervalo de valores 253–254

Hash 111–124
Incluir Estatísticas do Cliente 78
tabela de hash 111
Mapa de alocação de índice (IAM) 131
Hash Match 30, 116. Veja também Operadores
Varreduras de índice 81–86. Veja também Operadores
Montes 94–97 quando usar 86
Dados hierárquicos 418–420
Índice procura 87-94. Consulte também o uso do
Dicas 303–343
Índice de Operadores 221–256 e a seletividade 223–
perigos ao usar 303–304 dicas de
230
junção 333–335
columnstore. Ver índices Columnstore

cobrindo índices 230–231 com

otimização de memória 248–256

509
Machine Translated by Google

Índice

possíveis problemas 231–241 Mensagem de índice ausente 66

índices padrão 221–222 Misturando índices columnstore e rowstore.

agrupado 221 Veja o modo de lote

não agrupado 221 Vários planos de execução 187

Execução intercalada 32
N
J Loops aninhados 30

JavaScript Object Notation (JSON) 413–418 Unindo


O
dados 99–128. Consulte também Operadores Join

operadores. Consulte Operadores outros usos de 124– Planos de objetos 259

126 Mapeamento Objeto-Relacional (ORM) 264

gerando consultas ad hoc 282


K Custo do operador 65–66

Pesquisa de chave 91–94 Propriedades do operador. Veja Planos gráficos: compo


entes

eu Operadores. Veja também Planos gráficos: componentes

Junção Adaptativa 58, 120–124


Processo Lazywriter 35
Aplicar 195
Planos de execução ao vivo. Consulte Ferramentas para
Afirmação 59, 167-168
explorar planos de execução no Activity Monitor 503–505
Bitmap 59
no SSMS 502–503
Inserção de Índice Agrupado 165–167

Varredura de Índice Agrupado 82–84


Operações de junção lógica 100
Busca de Índice Agrupado 87–89
M Atualização de Índice Agrupado 170–171

Recolher 59
MAXDOP. Consulte Dicas: dicas de consulta; Veja também
Paral lelism: controlando a execução de consultas paralelas Calcular escalar 59, 113–114

MemoryGrantWarning 370 Concatenação 58, 126-128

Limite de memória excedido 73 Condicional 215

Tabelas e índices com otimização de memória 248–252 Varredura Constante 58, 162–165

Mesclar 30 Operadores de cursor 58

Classificação Distinta 135


Detalhes de índice ausentes. Veja os planos XML

510
Machine Translated by Google

Índice

Filtro 59 Carretéis 59

Partida de Hash 58, 111–124 e agregações 148–152

Correspondência de Hash (Agregado) 58 Agregado de fluxo 58

Hash Match junta-se 109–110 streaming versus bloqueio 62–63

considerações de desempenho 111-112 Interruptor 58

Varredura de Índice 85 Exclusão de Tabela/Índice 59

Propriedade encomendada 131 Inserção de Tabela/Índice 59

Busca de Índice 58 Mesclagem de Tabela/Índice 59

Busca de Índice (não agrupado) 89–91 Varredura de Tabela/Índice 58

INSERIR 161–168 Atualização de Tabela/Índice 59

Key LookUp 91–94 Varredura de Tabela 94–95

elementos de linguagem 58 Carretel de Mesa (Carretel Ansioso) 169

Pesquisa 58 59 melhores

Mesclar Junção 58, 115–120 Classificação N superior 133–134

considerações de desempenho 117-120 Agregado de Janela 58

Loops aninhados 58, 92, 102–109 Carretel de Janela 58

número estimado e real de linhas Otimizador 32–38

104–105 seleção de índice 222–231

Referências externas 106–107 custos estimados e estatísticas 222-223

Religar e rebobinar 107–109


nível de otimização 71–73

Paralelismo 59
Propriedade ordenada 192

operadores físicos 57 Outras ferramentas de plano de execução 505–506

Pesquisa RID 96–97 Relatório de consumo geral de recursos 465

Segmento 58, 153

SELECIONE 54
P
Sequência 58, 174
Paralelismo 344-358

Projeto de Sequência 59
controlando a execução de consultas paralelas 344–348
Projeto de Sequência (Compute Scalar) 154
bloqueadores 348
Classificar 58
limite de custo 347-348

Divisão 59
grau máximo de paralelismo (MAXDOP)

345–347

511
Machine Translated by Google

Índice

planos paralelos: custo-benefício 358 Guias de plano 288–296

execução de consulta paralela 349–358 objeto 293-295

examinando um plano 350-357 SQL 291–293

Parametrização modelo 289–291

e procedimentos armazenados 278–280 visualização, validação, desativação, remoção 295–


296
para reutilização do plano 273–281

Valor Compilado do Parâmetro 189 Recompilação do plano. Veja Planos de execução:

recompilação do plano
Texto Parametrizado 161
Planejar a reutilização 258–302
Lista de Parâmetros 73–74, 189
e consultas ad hoc. Consulte consultas ad hoc
Valor de tempo de execução do parâmetro 189
parametrização forçada 285–288
declarações preparadas 274-278
otimizar para cargas de trabalho ad hoc 282–285
parametrização simples 73, 266-273
parametrizando consultas 273–281
para planos triviais 266-273
problemas com o código de terceiros 281–302
inseguro 270–273
problemas com consultas parametrizadas 281
Sniffing de parâmetros
Planos para tipos de dados especiais e cursores 393–435.
problemas com 236-241
Veja também planos XML
Estatísticas de Desempenho 453
Dados hierárquicos (HIERARCHYID). Veja Hierar dados chicais
Permissões

necessário para visualizar os planos de execução 39


dados espaciais. Ver dados espaciais
Planeje o cache. Veja também Planos de execução: cache do plano
PowerShell 382
consultando 258–260
Empurrão de predicado. Ver índices Columnstore
com XQuery 386–391
Cache de procedimento 54
Explorador de planos. Consulte Ferramentas para explorar planos
Perfilador 78
de execução
Propriedades 68–76
Plano forçando 296-302, 472-483. Consulte também Repositório
tamanho do plano em cache 70
de consultas

CardinalidadeEstimationModelVersão 70
automatizando 478–483
Compile CPU 70
usando guias de plano 297–300
CompileMemória 70
usando o Repositório de Consultas 300–302
CompileTime 70
usando o plano XML 383–384
Lista de Saída 163

Texto Parametrizado 161

512
Machine Translated by Google

Índice

Predicado 90 controlando

Janela de propriedades 69 T-SQL ou Management Studio 461

QueryTimeStats 71 ativado por padrão? 458

Recuperado do Cache 71 forçando um plano com 281

Buscar Predicados 90 opções 460–462

Com Pilha 204 plano forçando 472-483

automatizando 478–483
Q removendo planos de 483-484

Relatório de consultas com planos forçados 465 recuperar planos usando T-SQL 468–472

Relatório de consultas com alta variação 465 recuperando planos 462-472

Consultas com ORDER BY 129–139 Relatórios SSMS 462–468

Operações de classificação 130–131 Relatório de consultas de maior consumo de recursos


466–468
Consulta. Consulte Procedimentos armazenados; Consulte
evento estendido query_thread_profile 501
também T-SQL ad hoc. Consulte Consultas ad hoc

vinculando 28 compilação e execução 27–32


R

mecanismo de execução 31– Índice de intervalo 248

32 otimização 29–32. Veja também Otimizador Lendo um índice 80–94

com base no custo total 29–30 Planos de leitura 76–79

analisando 28 Operadores de leitura 67

Subconsultas 191–194 Religar 107

QueryHash 75 Redgate SQL Monitor 506

Query Interop 251 Relatório de consultas regredidas

Otimizador de consultas. Consulte 465 Result Showplan 68 Planos

Parametrização da consulta do otimizador reutilizáveis. Consulte Planejar

37 QueryPlanHash 75 reutilização Rebobinar 107

Árvore do processador de consultas 29 Pesquisa RID. Veja o modo de

Repositório de consultas 458–


Linha de Operadores 242

484 comportamento 458–460

configurações do modo de captura 460

513
Machine Translated by Google

Índice

S Estatísticas 33–34, 222–223

gráfico de densidade 33

Procure operadores. Ver Operadores


cabeçalho 33

não no índice columnstore 246


histograma 33

Seletividade
problemas com 232-236

e índices 223–230 Cabeçalho de estatísticas 226

estimativa 223, 231


ESTATÍSTICAS IO 77

Comandos SET 77–78


ESTATÍSTICAS IO/HORA 77

Opções SET 75
Procedimentos armazenados 29, 185–190

Showplan XML 452, 453 Showplan


e consultas parametrizadas 278–280

XML para compilação de consulta 453 Showplan XML


compilado nativamente 254–256

Statistics Profile 453 Classificando e agregando dados


Subconsultas 191–194

129–158. Veja também Operadores; Veja também Consultas


Supratimas. Consulte Ferramentas para explorar planos de
com ORDER BY Distinct Sort 135
execução

sys.dm_exec_query_profiles DMV 499

caro Classificações 132–133

Classificar avisos 136–139


T
Classificação N superior 133–134
Varredura de Tabela. Consulte Planos
Dados espaciais 420–435
de texto de operadores. Veja Planos de execução: planos
Spools. Veja também Operadores
de texto Tempo limite 73
Carretel de Índice 148, 150–152
Ferramentas para explorar planos de execução 486–506
Carretel de contagem de linhas 148
em SSMS 17 488-505
Carretel de Mesa 148, 149–150
Monitor de Atividade 503–505
Carretel de Mesa (Carretel Ansioso) 169
Analisar o Plano de Execução Real 489–491
Carretel de Janela 148
Compare o plano de exibição 491–495
Planos SQL 259
Localizar Nó 496–497
SQL Server 2014 372
planos de execução ao vivo 498–505
SQL Server 2016 372
Explorador de Plano 505
SQL Server Management Studio (SSMS)
Pacote de Ferramentas SSMS 505

ferramentas 488–505
Entendendo 505
Ferramentas de monitoramento de desempenho do SQL Server
Dicas de ferramentas 53–55

506 relatórios SSMS. Ver Repositório de Consultas

514
Machine Translated by Google

Índice

Relatório 465 das Principais Consultas de Consumo de Recursos. Consulte X

também Repositório de Consultas

Rastrear Eventos 78 Planos XML 374–391, 394–412. Consulte também Planos de


execução: planos XML e eventos estendidos 384–385
Relatório de consultas rastreadas 465

Planos triviais. Ver planos de execução


e detalhes de índice ausentes 385–386
parametrização simples para 266–273
converter dados relacionais para XML
T-SQL
PARA XML 394–400
Expressão de Tabela Comum (CTE) 199–205
converter XML em dados relacionais
Instruções T-SQL comuns 185–220
OPENXML 401–405
recuperar planos do Repositório de Consultas com 468–472
Explica como 399-400
DENTRO

para planos reais 381–382 para

planos estimados 374–381 para


Operador UDX 399, 414
plano forçando 383–384 consulta
DENTRO

usando XQuery 386–391, 405–412

.exist() 405
Visualizações 206–212

para informações de índice ausentes 389–391 para


indexado 208–212
operadores específicos 388–389 .modify()
padrão 206-208
405 .nodes() 405
Dentro

.query() 405, 408–412 .value()


Avisos 64–65
405
Classificar avisos 136–139
retornando XML como tipo de dados XML
Funções da janela 152–158
XML AUTO 397–399
DENSE_RANK 152
salvando e compartilhando
NTIL 152
382 XQuery. Veja também planos XML
MAIS DE 152
quando usar 412
CLASSIFICAÇÃO 152

ROW_NUMBER 152

515

Você também pode gostar