{"id":2487,"date":"2026-07-22T15:13:23","date_gmt":"2026-07-22T15:13:23","guid":{"rendered":"https:\/\/www.tiagoneves.net\/blog\/?p=2487"},"modified":"2026-07-22T15:14:40","modified_gmt":"2026-07-22T15:14:40","slug":"estatisticas-no-sql-server-por-que-seus-planos-de-execucao-comecam-aqui","status":"publish","type":"post","link":"https:\/\/www.tiagoneves.net\/blog\/estatisticas-no-sql-server-por-que-seus-planos-de-execucao-comecam-aqui\/","title":{"rendered":"Estat\u00edsticas no SQL Server: por que seus planos de execu\u00e7\u00e3o come\u00e7am aqui"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><em>Post 01 de 03 da s\u00e9rie <strong>Estat\u00edsticas: SQL Server \u00d7 PostgreSQL<\/strong><\/em><\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-vivid-cyan-blue-color\">O sintoma que ningu\u00e9m questiona<\/mark><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Toda madrugada, \u00e0s 02h00, uma rotina de UPDATE STATISTICS entra em execu\u00e7\u00e3o em um ambiente que administro. A tabela principal tem cerca de <strong>4 bilh\u00f5es de linhas e 2 TB<\/strong>, particionada por per\u00edodo. A rotina varria, a cada execu\u00e7\u00e3o, <strong>2,8 bilh\u00f5es de linhas<\/strong> para atualizar estat\u00edsticas de dados que em sua esmagadora maioria n\u00e3o mudaram nada desde a noite anterior.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Voce pode se perguntar o Por qu\u00ea? porque em uma tabela particionada por data, a escrita acontece quase toda na <strong>\u00faltima parti\u00e7\u00e3o<\/strong>. As parti\u00e7\u00f5es hist\u00f3ricas s\u00e3o, na pr\u00e1tica, read-only. Mas a rotina n\u00e3o sabia disso, ela atualizava a estat\u00edstica da tabela inteira, toda noite, consumindo horas de janela de manuten\u00e7\u00e3o, I\/O e CPU para reler bilh\u00f5es de linhas congeladas.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Esse \u00e9 o retrato de muitos ambientes que avalio a rotina de estat\u00edsticas foi herdada, roda porque &#8220;sempre rodou assim, desde antes de eu entrar na empresa&#8221;, e ningu\u00e9m parou para perguntar <strong>o que exatamente ela est\u00e1 atualizando e se precisava<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Para responder essa pergunta, precisamos voltar um passo e entender o que \u00e9, afinal, uma estat\u00edstica?<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-vivid-cyan-blue-color\">O que \u00e9 uma estat\u00edstica (de verdade)<\/mark><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Estat\u00edstica n\u00e3o \u00e9 um n\u00famero m\u00e1gico. Segundo a <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/statistics\/statistics\" target=\"_blank\" rel=\"noopener\">documenta\u00e7\u00e3o oficial da Microsoft<\/a>, estat\u00edsticas de otimiza\u00e7\u00e3o de consulta s\u00e3o <strong>BLOBs (binary large objects)<\/strong> que armazenam informa\u00e7\u00e3o sobre a distribui\u00e7\u00e3o de valores em uma ou mais colunas de uma tabela ou view indexada. O otimizador usa esses objetos para estimar a <strong>cardinalidade<\/strong>, o n\u00famero de linhas, de cada etapa da consulta, e \u00e9 essa estimativa que decide, por exemplo, entre um Index Seek e um Index Scan, entre um Nested Loops e um Hash Match.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Kalen Delaney e coautores, no livroe <em>Microsoft SQL Server Internals<\/em> (Microsoft Press), resumem bem a ess\u00eancia do problema: o otimizador \u00e9 baseado em <strong>custo<\/strong>, e o custo \u00e9 calculado sobre <strong>estimativas,<\/strong> ele nunca olha os dados reais no momento da compila\u00e7\u00e3o, apenas os valores estat\u00edstico que existe deles. Se os valores est\u00e3o velhos ou mal amostrado, o otimizador toma uma decis\u00e3o racional sobre uma premissa errada.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Fisicamente, esse objeto \u00e9 composto por tr\u00eas partes:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>1. O histograma.<\/strong> Uma representa\u00e7\u00e3o da distribui\u00e7\u00e3o de valores da <strong>coluna l\u00edder<\/strong> da estat\u00edstica, com no m\u00e1ximo <strong>200 steps<\/strong>. Cada step guarda um valor de fronteira (<code>RANGE_HI_KEY<\/code>), quantas linhas t\u00eam exatamente aquele valor (<code>EQ_ROWS<\/code>), quantas caem dentro do intervalo (<code>RANGE_ROWS<\/code>) e quantos valores distintos existem (<code>DISTINCT_RANGE_ROWS<\/code>).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pare um segundo nesse n\u00famero: <strong>200 steps<\/strong>. N\u00e3o importa se a tabela tem 10 mil ou 4 bilh\u00f5es de linhas o otimizador enxerga a distribui\u00e7\u00e3o dos seus dados resumida em, no m\u00e1ximo, 200 faixas. Em tabelas gigantes, cada step pode representar milh\u00f5es de linhas, e valores &#8220;escondidos&#8221; dentro de um range podem ter estimativas bem distantes da realidade.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Um detalhe de internals que vale conhecer, o histograma n\u00e3o nasce de um simples &#8220;divide em 200 peda\u00e7os&#8221;. A documenta\u00e7\u00e3o da Microsoft descreve a constru\u00e7\u00e3o em <strong>tr\u00eas etapas<\/strong>, sobre o conjunto ordenado de valores da coluna:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Inicializa\u00e7\u00e3o:<\/strong> os primeiros valores da sequ\u00eancia ordenada s\u00e3o processados e at\u00e9 200 valores de <code>RANGE_HI_KEY<\/code> s\u00e3o coletados;<\/li>\n\n\n\n<li><strong>Scan com merge de buckets:<\/strong> cada valor seguinte \u00e9 adicionado ao \u00faltimo range ou vira um novo range no fim e, quando um novo range nasce, um par de ranges vizinhos \u00e9 <strong>colapsado<\/strong> em um s\u00f3, escolhido de forma a minimizar a perda de informa\u00e7\u00e3o. \u00c9 o chamado algoritmo de <em>maximum difference<\/em>, minimizar o n\u00famero de steps maximizando a diferen\u00e7a entre os valores de fronteira;<\/li>\n\n\n\n<li><strong>Consolida\u00e7\u00e3o:<\/strong> ranges adicionais podem ser colapsados se isso n\u00e3o perder informa\u00e7\u00e3o relevante, por isso um histograma pode terminar com <em>menos<\/em> de 200 steps mesmo em colunas com milhares de valores distintos.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-vivid-red-color\"><strong>Importante:<\/strong><\/mark> o histograma \u00e9 constru\u00eddo <strong>somente sobre a primeira coluna<\/strong> (a coluna principal) do conjunto de chaves da estat\u00edstica. As demais colunas de uma estat\u00edstica multi-coluna s\u00f3 contribuem para o density vector, o que explica por que a ordem das colunas em \u00edndices e estat\u00edsticas importa tanto para a qualidade das estimativas.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>2. O density vector:<\/strong> A medida de seletividade m\u00e9dia das colunas, calculada como <strong>1 \/ n\u00famero de valores distintos<\/strong>, quanto menor a density, mais seletiva a coluna (pense numa coluna de CPF, onde cada valor identifica uma \u00fanica pessoa, versus uma coluna de cidade, onde o mesmo valor se repete milhares de vezes). \u00c9 ele que o otimizador usa quando n\u00e3o consegue usar o histograma, por exemplo, em predicados com vari\u00e1veis locais ou em colunas secund\u00e1rias de estat\u00edsticas multi-coluna.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">E h\u00e1 uma sutileza que derruba muita gente: o density vector guarda um density para <strong>cada prefixo<\/strong> das colunas da estat\u00edstica. Uma estat\u00edstica em <code>(CustomerId, ItemId, Price)<\/code> tem densities para <code>(CustomerId)<\/code>, <code>(CustomerId, ItemId)<\/code> e <code>(CustomerId, ItemId, Price)<\/code>, mas <strong>n\u00e3o<\/strong> para <code>(CustomerId, Price)<\/code>. Se a sua consulta filtra por <code>CustomerId<\/code> e <code>Price<\/code> pulando a coluna do meio, aquela correla\u00e7\u00e3o simplesmente n\u00e3o existe para o otimizador.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">E o que acontece, ent\u00e3o, quando a consulta filtra s\u00f3 por <code>CustomerId<\/code> e <code>Price<\/code>? O otimizador <strong>n\u00e3o desiste<\/strong>, ele estima cada predicado isoladamente (o <code>CustomerId<\/code> pelo histograma dessa estat\u00edstica, j\u00e1 que \u00e9 a coluna l\u00edder; o <code>Price<\/code> por alguma outra estat\u00edstica que tenha <code>Price<\/code> como l\u00edder, tipicamente uma <code>_WA_Sys_<\/code> auto-criada) e depois <strong>combina as duas seletividades por uma f\u00f3rmula<\/strong>. E \u00e9 aqui que a vers\u00e3o do Cardinality Estimator muda tudo:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Legacy CE:<\/strong> assume <strong>independ\u00eancia total<\/strong> entre os predicados e simplesmente multiplica as seletividades: <code>S1 \u00d7 S2<\/code>. Se 1% das linhas s\u00e3o do cliente e 1% t\u00eam aquele pre\u00e7o, a estimativa vira 0,01% \u00f3timo se as colunas forem de fato independentes, desastre se forem correlacionadas;<\/li>\n\n\n\n<li><strong>New CE (2014+):<\/strong> assume <strong>correla\u00e7\u00e3o parcial<\/strong> e usa o chamado <em>exponential backoff<\/em>, ordena os predicados do mais seletivo para o menos seletivo e amortece os seguintes <code>S1 \u00d7 S2^(1\/2) \u00d7 S3^(1\/4) \u00d7 S4^(1\/8)<\/code>. A estimativa fica maior (mais conservadora) que a do Legacy.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Repare que <strong>nenhuma das duas f\u00f3rmulas conhece a correla\u00e7\u00e3o real<\/strong> entre <code>CustomerId<\/code> e <code>Price<\/code>, ambas s\u00e3o chutes matem\u00e1ticos. Quando as colunas s\u00e3o fortemente correlacionadas (cada cliente compra numa faixa de pre\u00e7o pr\u00f3pria, por exemplo), as duas subestimam o Legacy mais agressivamente. Subestimativa vira Nested Loops onde deveria ser Hash Match, memory grant curto, spill em <code>tempdb<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A solu\u00e7\u00e3o, quando essa combina\u00e7\u00e3o de filtros \u00e9 frequente e a estimativa comprovadamente erra, \u00e9 dar ao otimizador a correla\u00e7\u00e3o que falta, criar uma estat\u00edstica (ou \u00edndice) <strong>com o prefixo certo<\/strong> <code>CREATE STATISTICS St_Customer_Price ON dbo.Pedido (CustomerId, Price)<\/code>. Agora <code>(CustomerId, Price)<\/code> \u00e9 um prefixo v\u00e1lido, a density existe, e a estimativa passa a refletir a correla\u00e7\u00e3o real.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>3. Os metadados de amostragem.<\/strong> Principalmente <code>rows<\/code> \u00d7 <code>rows_sampled<\/code>: quantas linhas a tabela tinha no momento da atualiza\u00e7\u00e3o e quantas foram efetivamente lidas para construir o histograma. Guarde essa dupla ela volta daqui a pouco.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-vivid-cyan-blue-color\">Como ler tudo isso<\/mark><\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>DBCC SHOW_STATISTICS ('dbo.Cliente', 'IDX_Cliente_Data');<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">O resultado vem em tr\u00eas blocos, que correspondem exatamente \u00e0s tr\u00eas partes acima:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Header:<\/strong> <code>Updated<\/code> (\u00faltima atualiza\u00e7\u00e3o), <code>Rows<\/code>, <code>Rows Sampled<\/code>, <code>Steps<\/code>;<\/li>\n\n\n\n<li><strong>Density vector:<\/strong> <code>All density<\/code> por combina\u00e7\u00e3o de colunas;<\/li>\n\n\n\n<li><strong>Histograma:<\/strong> os steps com <code>RANGE_HI_KEY<\/code>, <code>EQ_ROWS<\/code>, <code>RANGE_ROWS<\/code>, <code>AVG_RANGE_ROWS<\/code>.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Em vers\u00f5es mais recentes (SQL 2016 SP1 CU2+), d\u00e1 para consultar o mesmo conte\u00fado via DMFs, o que facilita automatizar an\u00e1lises:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SELECT *\nFROM sys.dm_db_stats_properties(OBJECT_ID('dbo.Cliente'), 1);\n\nSELECT *\nFROM sys.dm_db_stats_histogram(OBJECT_ID('dbo.Cliente'), 1);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Quando o otimizador erra uma estimativa em tr\u00eas ordens de grandeza, ele n\u00e3o est\u00e1 &#8220;com bug&#8221;. Ele est\u00e1 lendo exatamente o que est\u00e1 gravado nesse objeto. Plano de execu\u00e7\u00e3o ruim, na maioria das vezes, \u00e9 sintoma e a causa mora aqui.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-vivid-cyan-blue-color\">De onde nascem as estat\u00edsticas: AUTO_CREATE_STATISTICS<\/mark><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Antes de falar de atualiza\u00e7\u00e3o, vale responder a pergunta anterior: quem cria esses objetos? Segundo a documenta\u00e7\u00e3o, as estat\u00edsticas nascem por dois caminhos autom\u00e1ticos:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>1. Junto com os \u00edndices.<\/strong> Todo \u00edndice que voc\u00ea cria carrega uma estat\u00edstica sobre suas colunas de chave, com o mesmo nome do \u00edndice. Se o \u00edndice \u00e9 filtrado, a estat\u00edstica nasce filtrada sobre o mesmo subconjunto. S\u00e3o as &#8220;estat\u00edsticas de \u00edndice&#8221; aquelas da Classe 2, que mais adiante veremos que s\u00f3 viram incrementais via rebuild.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>2. Pelo <code>AUTO_CREATE_STATISTICS<\/code>.<\/strong> Com a op\u00e7\u00e3o ligada (padr\u00e3o), quando uma consulta filtra por uma coluna que <strong>n\u00e3o tem histograma em nenhuma estat\u00edstica existente<\/strong>, o otimizador cria na hora uma estat\u00edstica de coluna \u00fanica para melhorar a pr\u00f3pria estimativa. S\u00e3o as famosas <code>_WA_Sys_*<\/code> que povoam qualquer base em produ\u00e7\u00e3o. Tr\u00eas limites importantes que a documenta\u00e7\u00e3o deixa claros: a op\u00e7\u00e3o cria <strong>apenas estat\u00edsticas de coluna \u00fanica<\/strong>, sempre sobre a <strong>tabela inteira<\/strong> (nunca filtradas), e <strong>n\u00e3o tem rela\u00e7\u00e3o<\/strong> com as estat\u00edsticas de \u00edndice deslig\u00e1-la n\u00e3o afeta \u00edndices.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Para inventariar o que o otimizador j\u00e1 criou sozinho no seu banco:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SELECT OBJECT_NAME(s.object_id) AS Tabela,\n       COL_NAME(sc.object_id, sc.column_id) AS Coluna,\n       s.name AS Estatistica,\n       STATS_DATE(s.object_id, s.stats_id) AS UltimaAtualizacao\nFROM sys.stats AS s\nJOIN sys.stats_columns AS sc\n    ON s.stats_id = sc.stats_id AND s.object_id = sc.object_id\nWHERE s.name LIKE '_WA%'\nORDER BY Tabela, Coluna;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Esse invent\u00e1rio conta uma hist\u00f3ria de cada <code>_WA_Sys_*<\/code> \u00e9 uma coluna que alguma consulta filtrou sem ter \u00edndice ou estat\u00edstica de apoio. Um volume grande delas numa mesma tabela \u00e9 pista de investiga\u00e7\u00e3o \u00e0s vezes de \u00edndice faltando, \u00e0s vezes de predicado que nem deveria existir.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A recomenda\u00e7\u00e3o oficial \u00e9 direta: mantenha <code>AUTO_CREATE_STATISTICS<\/code> ligado <strong>mesmo quando<\/strong> voc\u00ea cria estat\u00edsticas manualmente via <code>CREATE STATISTICS<\/code> os dois mecanismos se complementam. As cria\u00e7\u00f5es manuais entram em cena nos casos que o autom\u00e1tico n\u00e3o cobre, estat\u00edsticas <strong>multi-coluna<\/strong> para predicados com colunas correlacionadas que n\u00e3o est\u00e3o no mesmo \u00edndice, e estat\u00edsticas <strong>filtradas<\/strong> para subconjuntos com distribui\u00e7\u00e3o pr\u00f3pria.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-vivid-red-color\">Duas notas de vers\u00e3o para fechar:<\/mark><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Desde o <strong>SQL Server 2022<\/strong>, existe ainda o <code>AUTO_DROP<\/code> (habilitado por padr\u00e3o em bases novas e migradas): estat\u00edsticas criadas manualmente passam a se comportar como as auto-created diante de mudan\u00e7as de schema em vez de <strong>bloquear<\/strong> um <code>ALTER TABLE<\/code>, s\u00e3o descartadas e recriadas depois. Quem j\u00e1 teve deploy travado por estat\u00edstica criada por ferramenta de terceiro sabe a dor que isso resolve.<\/li>\n\n\n\n<li>E adiantando um gancho, no n\u00edvel do banco d\u00e1 para fazer as futuras auto-created j\u00e1 nascerem <strong>incrementais<\/strong> em tabelas particionadas voltamos a isso na se\u00e7\u00e3o de convers\u00e3o.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-vivid-cyan-blue-color\">Quando a estat\u00edstica se atualiza sozinha<\/mark><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Com <code>AUTO_UPDATE_STATISTICS = ON<\/code> (o padr\u00e3o), o SQL Server marca a estat\u00edstica como desatualizada quando o contador de modifica\u00e7\u00f5es da coluna l\u00edder (<code>modification_counter<\/code>) cruza um threshold e a atualiza\u00e7\u00e3o acontece na <strong>pr\u00f3xima consulta que precisar dela<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>O detalhe \u00e9 <em>qual<\/em> threshold:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Regra antiga (at\u00e9 2014, sem trace flag):<\/strong> para tabelas com mais de 500 linhas, <code>500 + (0,20 \u00d7 n)<\/code> ou seja, 500 modifica\u00e7\u00f5es <strong>+ 20% das linhas<\/strong> da tabela. Em uma tabela de 100 mil linhas, isso significa ~20.500 modifica\u00e7\u00f5es. Razo\u00e1vel. J\u00e1 em uma tabela de <strong>4 bilh\u00f5es de linhas<\/strong>, significa <strong>800 milh\u00f5es de modifica\u00e7\u00f5es<\/strong> antes de qualquer auto update. Na pr\u00e1tica: nunca.<\/li>\n\n\n\n<li><strong>Regra din\u00e2mica (2016+ com compatibility level 130+, ou TF 2371 no 2008 R2 SP1\u20132014):<\/strong> a f\u00f3rmula documentada \u00e9 <code>MIN( 500 + (0,20 \u00d7 n), SQRT(1.000 \u00d7 n) )<\/code> o menor entre a regra antiga e a raiz quadrada. Para tabelas pequenas, nada muda (a regra dos 20% ainda vence); \u00e9 a partir de ~25 mil linhas que a raiz quadrada assume. Para os mesmos 4 bilh\u00f5es de linhas, o gatilho cai para cerca de <strong>2 milh\u00f5es de modifica\u00e7\u00f5es<\/strong>. Ordens de grandeza mais sensato. A pr\u00f3pria Microsoft cita, como cen\u00e1rio t\u00edpico para habilitar a TF 2371 em vers\u00f5es antigas, exatamente o caso de quem depende de job noturno porque o auto update nunca dispara.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Um detalhe pouco conhecido da documenta\u00e7\u00e3o: a <strong>marca\u00e7\u00e3o<\/strong> de estat\u00edstica desatualizada pelo contador de modifica\u00e7\u00f5es acontece <strong>mesmo com <code>AUTO_UPDATE_STATISTICS = OFF<\/code><\/strong>. Com a op\u00e7\u00e3o desligada, o SQL Server continua sabendo que a estat\u00edstica est\u00e1 velha ele s\u00f3 n\u00e3o faz nada a respeito, e os planos seguem usando o objeto desatualizado. Por isso a recomenda\u00e7\u00e3o oficial \u00e9 manter a op\u00e7\u00e3o ligada, com a rotina manual atuando como complemento, n\u00e3o como substituto.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Se voc\u00ea administra bases grandes em compatibility level antigo e nunca habilitou a TF 2371, \u00e9 bem prov\u00e1vel que suas estat\u00edsticas de tabelas grandes s\u00f3 sejam atualizadas quando a sua rotina manual roda. O auto update, para elas, \u00e9 apenas decorativo.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Sync ou async?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Por padr\u00e3o, o auto update \u00e9 <strong>s\u00edncrono,<\/strong> a consulta que disparou a atualiza\u00e7\u00e3o <strong>espera<\/strong> ela terminar para compilar o plano. Em estat\u00edstica pequena, impercept\u00edvel. Em tabela grande, \u00e9 aquela query que &#8220;\u00e0s vezes demora do nada&#8221;.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>AUTO_UPDATE_STATISTICS_ASYNC = ON<\/code> inverte a l\u00f3gica: a consulta compila com a estat\u00edstica velha e a atualiza\u00e7\u00e3o roda em background. A documenta\u00e7\u00e3o recomenda o modo ass\u00edncrono justamente para os cen\u00e1rios cl\u00e1ssicos de OLTP, aplica\u00e7\u00f5es que executam as mesmas consultas com frequ\u00eancia e precisam de tempo de resposta <strong>previs\u00edvel<\/strong>, e aplica\u00e7\u00f5es que j\u00e1 sofreram timeout de cliente por consultas esperando atualiza\u00e7\u00e3o de estat\u00edstica. Voc\u00ea troca &#8220;uma consulta aleat\u00f3ria paga o pato&#8221; por &#8220;o plano fica levemente defasado por alguns instantes&#8221;.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Mas o async tem um efeito colateral de concorr\u00eancia que pouca gente conhece, a atualiza\u00e7\u00e3o em background precisa de um <strong>lock de modifica\u00e7\u00e3o de schema (Sch-M)<\/strong> no metadado da estat\u00edstica para gravar o resultado. Em workloads com compila\u00e7\u00f5es muito frequentes, essa disputa de locks pode gerar bloqueio, a sess\u00e3o de background segurando (ou esperando) o Sch-M enquanto outras sess\u00f5es precisam do Sch-S para compilar. A partir do <strong>SQL Server 2022<\/strong> (e no Azure SQL), existe a resposta para isso: a configura\u00e7\u00e3o de escopo de banco <code>ASYNC_STATS_UPDATE_WAIT_AT_LOW_PRIORITY<\/code>, que coloca a grava\u00e7\u00e3o da estat\u00edstica numa fila de baixa prioridade o background s\u00f3 adquire o lock quando ningu\u00e9m mais est\u00e1 segurando o metadado, e as demais sess\u00f5es seguem compilando com a estat\u00edstica existente:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ALTER DATABASE SCOPED CONFIGURATION\nSET ASYNC_STATS_UPDATE_WAIT_AT_LOW_PRIORITY = ON;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-vivid-red-color\"><em>Se voc\u00ea est\u00e1 em SQL Server 2022 com async habilitado em base de alto volume, essa configura\u00e7\u00e3o merece entrar no seu checklist.<\/em><\/mark><\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>A pegadinha do sample autom\u00e1tico<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Aqui est\u00e1 o ponto que mais engana em VLDB quando o auto update dispara (ou quando voc\u00ea roda <code>UPDATE STATISTICS<\/code> sem especificar sample), o SQL Server <strong>decide sozinho a taxa de amostragem<\/strong> e ela <strong>cai<\/strong> conforme a tabela cresce. Em tabelas de bilh\u00f5es de linhas, o sample autom\u00e1tico pode ficar <strong>abaixo de 1%<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Volte \u00e0 dupla <code>rows<\/code> \u00d7 <code>rows_sampled<\/code> do header do <code>DBCC SHOW_STATISTICS<\/code>. Se voc\u00ea v\u00ea 4 bilh\u00f5es em <code>rows<\/code> e 30 milh\u00f5es em <code>rows_sampled<\/code>, seu histograma de 200 steps foi constru\u00eddo lendo menos de 1% dos dados. Para distribui\u00e7\u00f5es uniformes, funciona. Para dados enviesados e dados reais quase sempre s\u00e3o \u00e9 receita para estimativa ruim. E lembre da nota da documenta\u00e7\u00e3o: quando o histograma nasce de amostra, os valores de <code>EQ_ROWS<\/code>, <code>RANGE_ROWS<\/code> e afins s\u00e3o <strong>estimativas<\/strong> por isso voc\u00ea v\u00ea n\u00fameros quebrados no histograma.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Duas armadilhas correlatas, direto da documenta\u00e7\u00e3o, que merecem destaque em ambiente particionado:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Rebuild de \u00edndice particionado n\u00e3o faz FULLSCAN de estat\u00edstica.<\/strong> Desde o SQL Server 2014, criar ou reconstruir um \u00edndice <strong>particionado<\/strong> n\u00e3o varre todas as linhas para a estat\u00edstica, usa o algoritmo de sample padr\u00e3o. Ou seja, aquele <code>ALTER INDEX ... REBUILD<\/code> que voc\u00ea contava como &#8220;de brinde atualiza a estat\u00edstica com fullscan&#8221; n\u00e3o entrega isso em \u00edndice particionado. Se precisar de fullscan, \u00e9 <code>UPDATE STATISTICS ... WITH FULLSCAN<\/code> expl\u00edcito.<\/li>\n\n\n\n<li><strong><code>PERSIST_SAMPLE_PERCENT<\/code> (2016 SP1 CU4+):<\/strong> quando voc\u00ea calibra um sample decente para uma estat\u00edstica cr\u00edtica, o pr\u00f3ximo auto update joga sua calibragem fora e volta ao sample autom\u00e1tico. Essa op\u00e7\u00e3o resolve o percentual definido fica <strong>persistido<\/strong> para as atualiza\u00e7\u00f5es seguintes que n\u00e3o especificarem sample:<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code>UPDATE STATISTICS dbo.Cliente (St_Cliente_DataMovimento)\nWITH SAMPLE 10 PERCENT, PERSIST_SAMPLE_PERCENT = ON;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A conclus\u00e3o parcial \u00e9 inc\u00f4moda: em tabelas gigantes, o auto update chega tarde (ou nunca) <strong>e<\/strong>, quando chega, amostra pouco. Por isso a rotina manual de estat\u00edsticas existe e \u00e9 indispens\u00e1vel em VLDB. O problema \u00e9 <em>como<\/em> ela costuma ser escrita e \u00e9 a\u00ed que o particionamento entra.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-vivid-cyan-blue-color\">Particionamento: N parti\u00e7\u00f5es, UMA estat\u00edstica<\/mark><\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Aqui mora um mal-entendido, mais comum que encontro em ambientes particionados.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Quando voc\u00ea particiona uma tabela, o SQL Server divide o <strong>armazenamento<\/strong> em N parti\u00e7\u00f5es cada uma com seus segmentos de dados, aloca\u00e7\u00e3o e possibilidade de manuten\u00e7\u00e3o individual de \u00edndice. \u00c9 natural assumir que as estat\u00edsticas seguem a mesma l\u00f3gica.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">E a resposta \u00e9: <strong>N\u00e3o seguem.<\/strong> Por padr\u00e3o, a estat\u00edstica de uma tabela particionada \u00e9 <strong>uma s\u00f3, agregada, no n\u00edvel da tabela<\/strong>. Um \u00fanico histograma de 200 steps descrevendo 4 bilh\u00f5es de linhas espalhadas por dezenas de parti\u00e7\u00f5es.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As consequ\u00eancias pr\u00e1ticas:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Manuten\u00e7\u00e3o tudo-ou-nada:<\/strong> para atualizar a estat\u00edstica, o SQL Server precisa reler a tabela <em>inteira<\/em> (ou uma amostra dela inteira). N\u00e3o existe, no modelo padr\u00e3o, &#8220;atualiza s\u00f3 a parti\u00e7\u00e3o de julho&#8221;. Foi exatamente isso que gerou a varredura de 2,8 bilh\u00f5es de linhas da abertura: 99% do esfor\u00e7o relendo parti\u00e7\u00f5es que n\u00e3o mudaram um byte.<\/li>\n\n\n\n<li><strong>200 steps para tudo:<\/strong> a parti\u00e7\u00e3o corrente justamente a mais consultada e a mais vol\u00e1til divide os mesmos 200 steps com todo o hist\u00f3rico.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">\u00c9 para atacar o problema n\u00ba 1 que existe o <code>INCREMENTAL = ON<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>INCREMENTAL = ON: o que ele realmente faz<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>UPDATE STATISTICS dbo.Cliente(St_Cliente_DataMovimento)<br>WITH FULLSCAN, INCREMENTAL = ON;<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Com estat\u00edsticas incrementais, o SQL Server passa a manter, internamente, <strong>uma estrutura de estat\u00edstica por parti\u00e7\u00e3o<\/strong> e <strong>mescla<\/strong> essas estruturas para produzir a estat\u00edstica final da tabela. A partir da\u00ed, quando os dados de <em>uma<\/em> parti\u00e7\u00e3o mudam, voc\u00ea pode atualizar <strong>s\u00f3 ela<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>-- Atualiza apenas a parti\u00e7\u00e3o corrente (ex.: parti\u00e7\u00e3o 87)\nUPDATE STATISTICS dbo.Cliente(St_Cliente_DataMovimento)\nWITH RESAMPLE ON PARTITIONS (87);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">O SQL Server rel\u00ea apenas a parti\u00e7\u00e3o indicada, reconstr\u00f3i a estat\u00edstica daquela fatia e refaz a mesclagem. No caso da abertura deste post, isso transformou uma varredura noturna de 2,8 bilh\u00f5es de linhas em uma leitura da ordem de <strong>dezenas de milh\u00f5es<\/strong> s\u00f3 a parti\u00e7\u00e3o ativa. A janela de manuten\u00e7\u00e3o agradece; o I\/O do storage tamb\u00e9m.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">De quebra, o threshold de auto update passa a ser avaliado <strong>por parti\u00e7\u00e3o<\/strong>, o que faz o gatilho da parti\u00e7\u00e3o quente disparar com muito mais sensibilidade do que o threshold calculado sobre a tabela inteira.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>A ressalva que quase ningu\u00e9m explica<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Agora, o ponto mais importante deste post e o motivo de muita frustra\u00e7\u00e3o com o recurso:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>INCREMENTAL = ON<\/code> barateia a MANUTEN\u00c7\u00c3O. Ele N\u00c3O muda o que o otimizador enxerga.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">O otimizador de consultas <strong>continua lendo o histograma mesclado, no n\u00edvel da tabela, com os mesmos 200 steps<\/strong>. N\u00e3o existe &#8220;histograma por parti\u00e7\u00e3o&#8221; na hora de estimar cardinalidade mesmo que sua consulta tenha elimina\u00e7\u00e3o de parti\u00e7\u00e3o perfeita e toque uma \u00fanica parti\u00e7\u00e3o, a estimativa vem do histograma agregado.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ou seja: se o seu problema \u00e9 <strong>custo e dura\u00e7\u00e3o da rotina de manuten\u00e7\u00e3o<\/strong>, estat\u00edsticas incrementais s\u00e3o a solu\u00e7\u00e3o. Se o seu problema \u00e9 <strong>qualidade de estimativa<\/strong> (aqueles 200 steps espremendo 4 bilh\u00f5es de linhas), incremental n\u00e3o resolve para isso, o caminho envolve estat\u00edsticas filtradas, revis\u00e3o de sampling (<code>FULLSCAN<\/code>\/<code>PERSIST_SAMPLE_PERCENT<\/code>) e, em \u00faltimo caso, redesenho.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Saber separar esses dois problemas \u00e9 o que evita implantar o recurso com a expectativa errada<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-vivid-cyan-blue-color\">As tr\u00eas classes de estat\u00edsticas (e o custo de convers\u00e3o de cada uma)<\/mark><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Na hora de implantar incremental em um ambiente que j\u00e1 existe, descobri na pr\u00e1tica que as estat\u00edsticas se dividem em tr\u00eas classes com custos de convers\u00e3o completamente diferentes:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Classe 1 &#8211; Estat\u00edsticas standalone e auto-created (<code>_WA_Sys_*<\/code>): convers\u00e3o barata.<\/strong> S\u00e3o as estat\u00edsticas criadas via <code>CREATE STATISTICS<\/code> ou automaticamente pelo <code>AUTO_CREATE_STATISTICS<\/code>. A convers\u00e3o \u00e9 um <code>UPDATE STATISTICS ... WITH FULLSCAN, INCREMENTAL = ON<\/code> custa uma atualiza\u00e7\u00e3o completa (uma \u00fanica vez), e a partir da\u00ed toda a manuten\u00e7\u00e3o vira incremental. Em paralelo, vale habilitar no n\u00edvel do banco para que as futuras auto-created j\u00e1 nas\u00e7am incrementais:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ALTER DATABASE &#91;SeuBanco]\nSET AUTO_CREATE_STATISTICS ON (INCREMENTAL = ON);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Classe 2 &#8211; Estat\u00edsticas de \u00edndice: s\u00f3 via REBUILD.<\/strong> A estat\u00edstica que nasce junto com um \u00edndice n\u00e3o aceita convers\u00e3o via <code>UPDATE STATISTICS<\/code>. O \u00fanico caminho \u00e9 reconstruir o \u00edndice com a op\u00e7\u00e3o:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ALTER INDEX IDX_Cliente_Data ON dbo.Cliente\nREBUILD WITH (STATISTICS_INCREMENTAL = ON);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Em um \u00edndice de 2 TB, isso n\u00e3o \u00e9 uma decis\u00e3o de rotina de estat\u00edsticas \u00e9 uma decis\u00e3o de <strong>projeto<\/strong>, com janela, log, espa\u00e7o e impacto pr\u00f3prios. No ambiente do caso real, a convers\u00e3o dessas estat\u00edsticas foi deliberadamente <strong>adiada<\/strong> para o projeto de migra\u00e7\u00e3o da base, quando os \u00edndices ser\u00e3o reconstru\u00eddos de qualquer forma. N\u00e3o force um rebuild de terabytes s\u00f3 para converter estat\u00edstica: encaixe a convers\u00e3o em um momento em que o rebuild j\u00e1 vai acontecer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Classe 3 &#8211; Estat\u00edsticas de tabelas n\u00e3o particionadas (ou de \u00edndices n\u00e3o alinhados \u00e0 parti\u00e7\u00e3o): fora do jogo.<\/strong> <code>INCREMENTAL = ON<\/code> s\u00f3 existe para objetos alinhados ao esquema de particionamento. A documenta\u00e7\u00e3o lista explicitamente os casos <strong>n\u00e3o suportados<\/strong>, se a estat\u00edstica cair em um deles, a op\u00e7\u00e3o \u00e9 ignorada e o SQL Server apenas gera um warning:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Estat\u00edsticas de \u00edndices <strong>n\u00e3o alinhados<\/strong> ao particionamento da tabela base;<\/li>\n\n\n\n<li>Estat\u00edsticas de <strong>\u00edndices filtrados<\/strong>;<\/li>\n\n\n\n<li>Estat\u00edsticas sobre <strong>views<\/strong>;<\/li>\n\n\n\n<li>Estat\u00edsticas de \u00edndices <strong>espaciais ou XML<\/strong>;<\/li>\n\n\n\n<li>Estat\u00edsticas em bases <strong>read-only<\/strong> e em secund\u00e1rias leg\u00edveis do Always On;<\/li>\n\n\n\n<li>Estat\u00edsticas de tabelas internas.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Para tudo isso, a manuten\u00e7\u00e3o continua no modelo tradicional e a sua rotina precisa saber diferenciar.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-vivid-cyan-blue-color\">Montando a rotina: dirigida por modifica\u00e7\u00e3o, por parti\u00e7\u00e3o<\/mark><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Com as estat\u00edsticas convertidas, a rotina deixa de ser &#8220;atualiza tudo toda noite&#8221; e passa a ser <strong>dirigida por evid\u00eancia<\/strong>: atualize apenas as parti\u00e7\u00f5es cujo contador de modifica\u00e7\u00f5es justifica. A DMF que viabiliza isso \u00e9 a <code>sys.dm_db_incremental_stats_properties<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SELECT\n    OBJECT_NAME(s.object_id)      AS Tabela,\n    s.name                        AS Estatistica,\n    isp.partition_number          AS Particao,\n    isp.rows                      AS Linhas,\n    isp.rows_sampled              AS LinhasAmostradas,\n    isp.modification_counter      AS Modificacoes,\n    isp.last_updated              AS UltimaAtualizacao\nFROM sys.stats AS s\nCROSS APPLY sys.dm_db_incremental_stats_properties(s.object_id, s.stats_id) AS isp\nWHERE s.object_id = OBJECT_ID('dbo.Cliente')\n  AND isp.modification_counter > 0\nORDER BY isp.modification_counter DESC;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A partir da\u00ed, a l\u00f3gica da rotina fica simples de descrever:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Varra as estat\u00edsticas incrementais e colete o <code>modification_counter<\/code> <strong>por parti\u00e7\u00e3o<\/strong>;<\/li>\n\n\n\n<li>Aplique o seu crit\u00e9rio de corte (percentual sobre <code>rows<\/code> da parti\u00e7\u00e3o, ou um valor absoluto calibrado pelo throughput do ambiente);<\/li>\n\n\n\n<li>Gere dinamicamente os comandos <code>UPDATE STATISTICS ... WITH RESAMPLE ON PARTITIONS (n)<\/code> s\u00f3 para as parti\u00e7\u00f5es que cruzaram o corte;<\/li>\n\n\n\n<li>Para as estat\u00edsticas n\u00e3o incrementais (classe 3), mantenha o fluxo tradicional baseado em <code>sys.dm_db_stats_properties<\/code>.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">O resultado \u00e9 uma rotina que trabalha proporcionalmente ao que <strong>mudou<\/strong>, n\u00e3o ao que <strong>existe<\/strong>. Em uma tabela onde 99% do dado \u00e9 hist\u00f3rico congelado, essa diferen\u00e7a \u00e9 a diferen\u00e7a entre horas e minutos.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-vivid-red-color\">B\u00f4nus: descobrindo quais estat\u00edsticas o plano realmente usou<\/mark><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Para fechar o ciclo diagn\u00f3stico, o plano de execu\u00e7\u00e3o conta quais estat\u00edsticas o otimizador carregou na compila\u00e7\u00e3o: o elemento <strong><code>OptimizerStatsUsage<\/code><\/strong> do XML do plano lista, para cada estat\u00edstica, o nome, o <code>ModificationCount<\/code> no momento da compila\u00e7\u00e3o, o <code>SamplingPercent<\/code> e o <code>LastUpdate<\/code>. No SSMS, basta selecionar o operador raiz do plano e expandir <em>OptimizerStatsUsage<\/em> na janela de propriedades.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u00c9 a ponte perfeita entre &#8220;essa query est\u00e1 lenta&#8221; e &#8220;essa estat\u00edstica estava velha\/mal amostrada quando o plano nasceu&#8221;: um <code>ModificationCount<\/code> alto em rela\u00e7\u00e3o ao tamanho da tabela, ou um <code>SamplingPercent<\/code> baixo em coluna enviesada, apontam o culpado sem achismo. E o inverso tamb\u00e9m informa: se a coluna do seu filtro <strong>n\u00e3o aparece<\/strong> na lista, o otimizador n\u00e3o encontrou estat\u00edstica relevante para ela candidata a <code>CREATE STATISTICS<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-vivid-cyan-blue-color\">A armadilha final: ascending key + Legacy CE<\/mark><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Um alerta antes de encerrar, porque ele derruba at\u00e9 quem fez tudo certo at\u00e9 aqui.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Tabelas particionadas por data quase sempre t\u00eam o padr\u00e3o <strong>ascending key<\/strong>: os novos valores (a data de hoje, o pr\u00f3ximo ID) entram sempre <strong>acima<\/strong> do maior valor registrado no histograma. Entre uma atualiza\u00e7\u00e3o de estat\u00edstica e outra, existe uma faixa de valores rec\u00e9m-inseridos que o histograma <strong>ainda n\u00e3o conhece<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">O que o otimizador estima para um predicado que cai nessa faixa depende do <strong>Cardinality Estimator<\/strong>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Legacy CE (compatibility level \u2264 110, ou for\u00e7ado via hint\/TF 9481):<\/strong> estima <strong>1 linha<\/strong>. A consulta &#8220;me traga as posi\u00e7\u00f5es de hoje&#8221; justamente as mais comuns executada nas aplica\u00e7\u00f5es recebe estimativa de 1 linha, ganha um plano de Nested Loops com lookup, e executa isso contra milh\u00f5es de linhas reais. Desastre cl\u00e1ssico. (No Legacy CE, as trace flags 2389\/2390 existem exatamente para remendar esse cen\u00e1rio, marcando a coluna como ascending.)<\/li>\n\n\n\n<li><strong>New CE (2014+, compatibility level \u2265 120):<\/strong> assume que existem linhas al\u00e9m do \u00faltimo step do histograma e usa uma extrapola\u00e7\u00e3o baseada no <code>modification_counter<\/code>. Imperfeito, mas ordens de grandeza melhor para esse padr\u00e3o.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">A li\u00e7\u00e3o que amarra o post inteiro: <strong>estat\u00edstica perfeita n\u00e3o salva plano ruim se o modelo de estimativa n\u00e3o souber us\u00e1-la<\/strong>. Rotina incremental impec\u00e1vel, sample generoso, tudo em dia e ainda assim, no Legacy CE, a consulta mais importante do sistema pode estar estimando 1 linha. Estat\u00edstica, threshold, sampling e Cardinality Estimator formam um sistema; otimizar uma pe\u00e7a ignorando as outras \u00e9 enxugar gelo.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-vivid-cyan-blue-color\">Fechando &#8211; e o que vem no pr\u00f3ximo post<\/mark><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-vivid-red-color\">Recapitulando o caminho:<\/mark><\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Estat\u00edstica \u00e9 <strong>histograma (\u2264 200 steps) + density vector + metadados de amostragem<\/strong> e o plano de execu\u00e7\u00e3o nasce dali;<\/li>\n\n\n\n<li>Elas nascem por dois caminhos: junto com os <strong>\u00edndices<\/strong> e via <strong><code>AUTO_CREATE_STATISTICS<\/code><\/strong> (as <code>_WA_Sys_*<\/code>) coluna \u00fanica, tabela inteira, e um invent\u00e1rio que conta hist\u00f3ria sobre o seu workload;<\/li>\n\n\n\n<li>Em tabelas grandes, o auto update <strong>chega tarde<\/strong> (threshold) e <strong>amostra pouco<\/strong> (sample autom\u00e1tico) rotina manual \u00e9 obrigat\u00f3ria em VLDB;<\/li>\n\n\n\n<li>Tabela particionada tem N parti\u00e7\u00f5es, mas <strong>uma estat\u00edstica agregada<\/strong> e \u00e9 isso que faz rotinas ing\u00eanuas varrerem bilh\u00f5es de linhas paradas;<\/li>\n\n\n\n<li><code>INCREMENTAL = ON<\/code> mant\u00e9m estat\u00edstica <strong>por parti\u00e7\u00e3o<\/strong> e mescla barateia (e muito) a manuten\u00e7\u00e3o, mas o otimizador <strong>continua lendo o histograma mesclado<\/strong>;<\/li>\n\n\n\n<li>A convers\u00e3o tem tr\u00eas custos: <strong>barata<\/strong> (standalone\/auto-created), <strong>cara<\/strong> (stats de \u00edndice, s\u00f3 via rebuild encaixe em um projeto) e <strong>imposs\u00edvel<\/strong> (n\u00e3o particionadas\/filtradas);<\/li>\n\n\n\n<li>A rotina moderna \u00e9 dirigida por <code>modification_counter<\/code> <strong>por parti\u00e7\u00e3o<\/strong>, via <code>sys.dm_db_incremental_stats_properties<\/code>;<\/li>\n\n\n\n<li>E nada disso salva voc\u00ea do padr\u00e3o <strong>ascending key<\/strong> rodando em Legacy CE.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">No <strong>post 02<\/strong>, atravessamos a ponte: como o <strong>PostgreSQL<\/strong> resolve exatamente os mesmos problemas <code>ANALYZE<\/code>, <code>default_statistics_target<\/code> (o &#8220;200 steps&#8221; de l\u00e1, s\u00f3 que configur\u00e1vel), <code>pg_stats<\/code>, autovacuum\/autoanalyze e seus thresholds. E com uma invers\u00e3o curiosa: no PostgreSQL, cada parti\u00e7\u00e3o <strong>tem<\/strong> estat\u00edstica pr\u00f3pria de verdade\u2026 mas o autovacuum <strong>n\u00e3o roda ANALYZE na tabela pai<\/strong> o &#8220;esquecido&#8221; da hist\u00f3ria \u00e9 outro. Quem trabalha com os dois engines vai gostar do espelho.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">At\u00e9 l\u00e1!<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-vivid-cyan-blue-color\">Refer\u00eancias e leitura recomendada<\/mark><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Microsoft Learn &#8211; Statistics (SQL Server):<\/strong><br><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/statistics\/statistics?view=sql-server-ver17\" target=\"_blank\" rel=\"noopener\">learn.microsoft.com\/en-us\/sql\/relational-databases\/statistics\/statistics<\/a> &#8211; a p\u00e1gina oficial sobre estat\u00edsticas, base das defini\u00e7\u00f5es de histograma, density vector, thresholds de auto update e op\u00e7\u00f5es <code>INCREMENTAL<\/code>\/<code>ASYNC<\/code> discutidas neste post;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Microsoft Learn <\/strong><br><strong><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/database-console-commands\/dbcc-show-statistics-transact-sql\" target=\"_blank\" rel=\"noopener\">DBCC SHOW_STATISTICS<\/a><\/strong>, <strong><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/statements\/update-statistics-transact-sql\" target=\"_blank\" rel=\"noopener\">UPDATE STATISTICS<\/a><\/strong>, <strong><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/system-dynamic-management-views\/sys-dm-db-stats-properties-transact-sql\" target=\"_blank\" rel=\"noopener\">sys.dm_db_stats_properties<\/a><\/strong> e <strong><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/system-dynamic-management-views\/sys-dm-db-incremental-stats-properties-transact-sql\" target=\"_blank\" rel=\"noopener\">sys.dm_db_incremental_stats_properties<\/a><\/strong> &#8211; refer\u00eancias de sintaxe e metadados usados nos exemplos; <strong>Delaney, K.; Freeman, C.; et al. <\/strong><br><br><strong><em>Microsoft SQL Server 2012 Internals<\/em> (Microsoft Press, 2013)<\/strong> &#8211; leitura obrigat\u00f3ria para entender como o otimizador baseado em custo consome estat\u00edsticas, o papel do Cardinality Estimator e o armazenamento interno desses objetos. Boa parte da intui\u00e7\u00e3o deste post sobre &#8220;estimativa versus realidade&#8221; vem da forma como o livro ensina a raciocinar sobre o otimizador.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Post 01 de 03 da s\u00e9rie Estat\u00edsticas: SQL Server \u00d7 PostgreSQL O sintoma que ningu\u00e9m questiona Toda madrugada, \u00e0s 02h00, uma rotina de UPDATE STATISTICS entra em execu\u00e7\u00e3o em um ambiente que administro. A tabela principal tem cerca de 4 bilh\u00f5es de linhas e 2 TB, particionada por per\u00edodo. A rotina varria, a cada execu\u00e7\u00e3o,&#8230;<\/p>\n","protected":false},"author":1,"featured_media":2492,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rop_custom_images_group":[],"rop_custom_messages_group":[],"rop_publish_now":"initial","rop_publish_now_accounts":{"twitter_91251433_91251433":""},"rop_publish_now_history":[],"rop_publish_now_status":"pending","_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"Toda madrugada, uma rotina varria 2,8 bilh\u00f5es de linhas para atualizar estat\u00edsticas de dados que n\u00e3o mudaram. Neste primeiro post da s\u00e9rie SQL Server \u00d7 PostgreSQL, mergulho no objeto que decide todos os seus planos de execu\u00e7\u00e3o: histograma, density vector, thresholds de auto update e o que o INCREMENTAL = ON realmente faz (e o que n\u00e3o faz) em tabelas particionadas.","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2},"jetpack_post_was_ever_published":false,"_wpscppro_dont_share_socialmedia":false,"_wpscppro_custom_social_share_image":0,"_facebook_share_type":"default","_twitter_share_type":"default","_linkedin_share_type":"default","_pinterest_share_type":"default","_linkedin_share_type_page":"","_instagram_share_type":"default","_medium_share_type":"default","_threads_share_type":"default","_google_business_share_type":"default","_bluesky_share_type":"default","_selected_social_profile":[],"_wpsp_enable_custom_social_template":false,"_wpsp_social_scheduling":{"enabled":false,"datetime":null,"platforms":[],"status":"template_only","dateOption":"today","timeOption":"now","customDays":"","customHours":"","customDate":"","customTime":"","schedulingType":"absolute"},"_wpsp_active_default_template":true},"categories":[20,239],"tags":[657,701,693,697,699,695,267,35,661,662],"class_list":["post-2487","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-sqlserver-geral","category-tuning","tag-cardinality-estimator","tag-dbcc-show_statistics","tag-estatisticas","tag-estatisticas-incrementais","tag-otimizador-de-consultas","tag-particionamento","tag-performance-tuning","tag-sql-server","tag-update-statistics","tag-vldb"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/www.tiagoneves.net\/blog\/wp-content\/uploads\/2026\/07\/featured-post1-estatisticas-sqlserver.png?fit=1200%2C630&ssl=1","jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/p6eIyh-E7","jetpack-related-posts":[{"id":2431,"url":"https:\/\/www.tiagoneves.net\/blog\/atualizacao-estatisticas-sql-server-vldb\/","url_meta":{"origin":2487,"position":0},"title":"Estat\u00edsticas SQL Server em VLDB: Por Que Atualizar Pode Ser Melhor Que Rebuild de \u00cdndices","author":"tiagoneves","date":"1 de junho de 2026","format":false,"excerpt":"Descubra por que a atualiza\u00e7\u00e3o de estat\u00edsticas SQL Server pode gerar mais performance que rebuild de \u00edndices em ambientes VLDB e janelas curtas de manuten\u00e7\u00e3o.","rel":"","context":"Em &quot;Tuning&quot;","block_context":{"text":"Tuning","link":"https:\/\/www.tiagoneves.net\/blog\/category\/tuning\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.tiagoneves.net\/blog\/wp-content\/uploads\/2026\/06\/StatisticsXRebuild.png?resize=350%2C200","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/www.tiagoneves.net\/blog\/wp-content\/uploads\/2026\/06\/StatisticsXRebuild.png?resize=350%2C200 1x, https:\/\/i0.wp.com\/www.tiagoneves.net\/blog\/wp-content\/uploads\/2026\/06\/StatisticsXRebuild.png?resize=525%2C300 1.5x, https:\/\/i0.wp.com\/www.tiagoneves.net\/blog\/wp-content\/uploads\/2026\/06\/StatisticsXRebuild.png?resize=700%2C400 2x"},"classes":[]},{"id":1495,"url":"https:\/\/www.tiagoneves.net\/blog\/dicas-de-como-realizar-um-tuning-no-sql-server\/","url_meta":{"origin":2487,"position":1},"title":"Dicas de como realizar um tuning no SQL Server","author":"tiagoneves","date":"9 de maio de 2019","format":false,"excerpt":"Ol\u00e1 pessoal tudo certo? No post de hoje, eu quero compartilhar com voc\u00eas algumas dicas de como iniciar um tuning em alguma rotina, seja stored procedure, function ou query adhoc. Quando vamos iniciar um trabalho de tuning, uma das primeiras informa\u00e7\u00f5es que precisamos \u00e9 visualizar o plano de execu\u00e7\u00e3o da\u2026","rel":"","context":"Em &quot;Casos do dia-a-dia&quot;","block_context":{"text":"Casos do dia-a-dia","link":"https:\/\/www.tiagoneves.net\/blog\/category\/casos-do-dia-a-dia\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.tiagoneves.net\/blog\/wp-content\/uploads\/2019\/05\/word-image-3-1.png?fit=352%2C278&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":134,"url":"https:\/\/www.tiagoneves.net\/blog\/microsoft-anuncia-o-sql-server-2016-parte-2\/","url_meta":{"origin":2487,"position":2},"title":"Microsoft anuncia o SQL Server 2016 &#8211; Parte 2","author":"tiagoneves","date":"28 de maio de 2015","format":false,"excerpt":"Fala pessoal, tudo certo? Hoje daremos continua\u00e7\u00e3o ao post do SQL Server 2016, aproveitando que hoje a Microsoft disponibilizou para download o preview (clique aqui para fazer o download). High Availability O AlwaysOn j\u00e1 est\u00e1 dispon\u00edvel desde o SQL Server 2012, mas na vers\u00e3o do SQL Server 2016 houve algumas\u2026","rel":"","context":"Em &quot;Microsoft&quot;","block_context":{"text":"Microsoft","link":"https:\/\/www.tiagoneves.net\/blog\/category\/microsoft\/"},"img":{"alt_text":"sql-server-vnext","src":"https:\/\/i0.wp.com\/www.tiagoneves.net\/blog\/wp-content\/uploads\/2015\/05\/sql-server-vnext-e1432175774977.jpg?resize=350%2C200","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/www.tiagoneves.net\/blog\/wp-content\/uploads\/2015\/05\/sql-server-vnext-e1432175774977.jpg?resize=350%2C200 1x, https:\/\/i0.wp.com\/www.tiagoneves.net\/blog\/wp-content\/uploads\/2015\/05\/sql-server-vnext-e1432175774977.jpg?resize=525%2C300 1.5x"},"classes":[]},{"id":778,"url":"https:\/\/www.tiagoneves.net\/blog\/microsoft-libera-o-service-pack-1-e-preview-do-sql-on-linux\/","url_meta":{"origin":2487,"position":3},"title":"Microsoft libera o Service Pack 1 e Preview do SQL on Linux","author":"tiagoneves","date":"23 de novembro de 2016","format":false,"excerpt":"Ol\u00e1 pessoal, tudo certo? Na ultima semana tivemos algumas novidades anunciadas pela Microsoft. A primeira delas foi o lan\u00e7amento do Service Pack 1 do SQL Server 2016, n\u00e3o me lembro de tantas novidades em um service pack. Deu a louca na Microsoft, agora praticamente todos os recursos que s\u00f3 estavam\u2026","rel":"","context":"Em &quot;Microsoft&quot;","block_context":{"text":"Microsoft","link":"https:\/\/www.tiagoneves.net\/blog\/category\/microsoft\/"},"img":{"alt_text":"LinuxLove","src":"https:\/\/i0.wp.com\/msdnshared.blob.core.windows.net\/media\/2016\/11\/LinuxLove.png?resize=350%2C200&ssl=1","width":350,"height":200},"classes":[]},{"id":2216,"url":"https:\/\/www.tiagoneves.net\/blog\/como-tirei-a-certificacao-mcse-data-management-and-analytics\/","url_meta":{"origin":2487,"position":4},"title":"Como tirei a certifica\u00e7\u00e3o MCSE &#8211; Data Management and Analytics","author":"tiagoneves","date":"18 de setembro de 2019","format":false,"excerpt":"Ol\u00e1 pessoal tudo certo? No post de hoje eu vou comentar um pouco sobre provas de certifica\u00e7\u00e3o, especialmente sobre as provas 70-764, 70-765 e 70-465. Quem me acompanha nas redes sociais (LinkedIn \/ Twitter \/ Facebook) viu que nas \u00faltimas semanas eu recebi o t\u00edtulo de MCSE (Microsoft Certified Solutions\u2026","rel":"","context":"Em &quot;Certifica\u00e7\u00e3o&quot;","block_context":{"text":"Certifica\u00e7\u00e3o","link":"https:\/\/www.tiagoneves.net\/blog\/category\/certificacao\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.tiagoneves.net\/blog\/wp-content\/uploads\/2019\/09\/MCSE-Data-Management-and-Analytics2019.png?fit=600%2C600&ssl=1&resize=350%2C200","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/www.tiagoneves.net\/blog\/wp-content\/uploads\/2019\/09\/MCSE-Data-Management-and-Analytics2019.png?fit=600%2C600&ssl=1&resize=350%2C200 1x, https:\/\/i0.wp.com\/www.tiagoneves.net\/blog\/wp-content\/uploads\/2019\/09\/MCSE-Data-Management-and-Analytics2019.png?fit=600%2C600&ssl=1&resize=525%2C300 1.5x"},"classes":[]},{"id":2463,"url":"https:\/\/www.tiagoneves.net\/blog\/conversao-implicita-no-postgresql-por-que-o-comportamento-e-diferente-do-sql-server-e-onde-a-performance-realmente-morre\/","url_meta":{"origin":2487,"position":5},"title":"Convers\u00e3o impl\u00edcita no PostgreSQL: por que o comportamento \u00e9 diferente do SQL Server (e onde a performance realmente morre)","author":"tiagoneves","date":"14 de julho de 2026","format":false,"excerpt":"O resumo (excerpt) que sugeri no kit SEO: O blog agora tamb\u00e9m fala PostgreSQL! No primeiro post da s\u00e9rie SQL Server \u00d7 PostgreSQL, desmontamos um mito sobre casts, mostramos onde os \u00edndices morrem de verdade (com EXPLAIN ANALYZE em 100 mil linhas) e apresentamos a arma secreta do PG: o\u2026","rel":"","context":"Em &quot;PostgreSQL&quot;","block_context":{"text":"PostgreSQL","link":"https:\/\/www.tiagoneves.net\/blog\/category\/postgresql\/"},"img":{"alt_text":"Convers\u00e3o impl\u00edcita: duas filosofias, dois caminhos \u2014 SQL Server \u00d7 PostgreSQL","src":"https:\/\/i0.wp.com\/www.tiagoneves.net\/blog\/wp-content\/uploads\/2026\/07\/og-conversao-implicita-postgresql-1200x630-1.png?fit=1200%2C630&ssl=1&resize=350%2C200","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/www.tiagoneves.net\/blog\/wp-content\/uploads\/2026\/07\/og-conversao-implicita-postgresql-1200x630-1.png?fit=1200%2C630&ssl=1&resize=350%2C200 1x, https:\/\/i0.wp.com\/www.tiagoneves.net\/blog\/wp-content\/uploads\/2026\/07\/og-conversao-implicita-postgresql-1200x630-1.png?fit=1200%2C630&ssl=1&resize=525%2C300 1.5x, https:\/\/i0.wp.com\/www.tiagoneves.net\/blog\/wp-content\/uploads\/2026\/07\/og-conversao-implicita-postgresql-1200x630-1.png?fit=1200%2C630&ssl=1&resize=700%2C400 2x, https:\/\/i0.wp.com\/www.tiagoneves.net\/blog\/wp-content\/uploads\/2026\/07\/og-conversao-implicita-postgresql-1200x630-1.png?fit=1200%2C630&ssl=1&resize=1050%2C600 3x"},"classes":[]}],"jetpack_likes_enabled":true,"_links":{"self":[{"href":"https:\/\/www.tiagoneves.net\/blog\/wp-json\/wp\/v2\/posts\/2487","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.tiagoneves.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.tiagoneves.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.tiagoneves.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tiagoneves.net\/blog\/wp-json\/wp\/v2\/comments?post=2487"}],"version-history":[{"count":4,"href":"https:\/\/www.tiagoneves.net\/blog\/wp-json\/wp\/v2\/posts\/2487\/revisions"}],"predecessor-version":[{"id":2491,"href":"https:\/\/www.tiagoneves.net\/blog\/wp-json\/wp\/v2\/posts\/2487\/revisions\/2491"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.tiagoneves.net\/blog\/wp-json\/wp\/v2\/media\/2492"}],"wp:attachment":[{"href":"https:\/\/www.tiagoneves.net\/blog\/wp-json\/wp\/v2\/media?parent=2487"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.tiagoneves.net\/blog\/wp-json\/wp\/v2\/categories?post=2487"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.tiagoneves.net\/blog\/wp-json\/wp\/v2\/tags?post=2487"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}