Причудливая плотность приводит к выборочной статистике

8

NC-индекс получает совершенно другое статистическое распределение при оценке с выборкой по сравнению с fullscan; образец, имеющий странный вектор плотности. Это приводит к плохим планам выполнения.


У меня есть таблица из ~ 27M строк, с ненулевым FK-столбцом, поддерживаемым некластеризованным индексом. Таблица сгруппирована по первичному ключу. Обе колонки varchar.

Обновление статистики полного сканирования для нашей FK-колонки дает нормально выглядящий вектор плотности:

All density Average Length  Columns
6,181983E-08    45,99747    INSTANCEELEMENTID
3,615442E-08    95,26874    INSTANCEELEMENTID, ID

То есть мы должны прочитать около 1,7 строки для каждого отдельного INSTANCELEMENTIDэлемента, с которым мы соединяемся.

Типичный бин из гистограммы выглядит так:

RANGE_HI_KEY    RANGE_ROWS  EQ_ROWS DISTINCT_RANGE_ROWS AVG_RANGE_ROWS
FOOBAR          133053      10      71366               1,679318

Тем не менее, если мы делаем выборочное обновление (используя номер выборки по умолчанию, равный 230 тыс. Строк для этой таблицы), все превращается в странное:

4,773657E-06    45,99596    INSTANCEELEMENTID
3,702179E-08    95,30183    INSTANCEELEMENTID, ID

Плотность INSTANCEELEMENTIDтеперь на два порядка больше. (Однако плотность для обоих столбцов была оценена как вполне приемлемая величина).

Типичный бин из гистограммы теперь выглядит следующим образом;

RANGE_HI_KEY    RANGE_ROWS  EQ_ROWS     DISTINCT_RANGE_ROWS AVG_RANGE_ROWS
FOOBAR          143870,4    766,2573    1247                115,3596
ZOTZOT          131560,7    1           969                 135,7092

который является совершенно другим распределением. Обратите внимание, что INSTANCEELEMENTIDнаибольшее число ассоциированных IDs имеет 12, наиболее распространенное число равно 1. Также очень странно, что некоторые ячейки получают EQ_ROWS = 1, это происходит примерно с 10% корзин.

Там нет "неудачного" розыгрыша странных строк, которые могли бы способствовать этому.

Я правильно читаю гистограмму? Не похоже ли, что выборка каким-то образом неправильно масштабировала EQ_ROWS, DISTINCT_RANGE_ROWS и AVG_RANGE_ROWS?

Таблица, насколько я могу судить, не косая. Я пытался эмулировать сэмплер, оценивая значения самостоятельно tablesample. Обычный подсчет этих результатов дает результаты, которые согласуются с полной версией сканирования, а не сэмплером.

Кроме того, я не смог воспроизвести это поведение на кластерных индексах.


Я сузил это, чтобы воспроизвести:

CREATE TABLE F_VAL (
    id varchar(100) primary key,
    num_l_val int not null
)

set nocount on

declare @rowlimit integer = 20000000;

Стол должен быть достаточно большим, чтобы это можно было наблюдать. Я видел это с uniqueidentiferи , varchar(100)но не int.

declare @i integer = 1;

declare @r float = rand()

while @i < @rowlimit
begin
set @r = rand()
insert f_val (id,num_l_val)
values (
   cast(@i as varchar(100)) + REPLICATE('f', 40 - len(@i)),
   case when @r > 0.8 then 4 when @r > 0.5 then 3 when @r > 0.4 then 2 else 1 end
)
  set @i = @i + 1

end

create table k_val (
 id int identity primary key,
 f_val varchar(100) not null,
)

insert into k_val(f_val)
select id from F_VAL
union all select id from f_val where num_l_val - 1 = 1
union all select id from f_val where num_l_val - 2 = 1
union all select id from f_val where num_l_val - 3 = 1
order by id

create nonclustered index IX_K_VAL_F_VAL  ON K_VAL (F_VAL)

update statistics K_VAL(IX_K_VAL_F_VAL) 
dbcc show_statistics (k_val,IX_k_VAL_F_VAL)

update statistics K_VAL(IX_K_VAL_F_VAL) WITH FULLSCAN
dbcc show_statistics (k_val,IX_k_VAL_F_VAL)

Сравните две статистики; тот, что с выборкой, теперь представляет общий вектор плотности, а ячейки гистограммы отключены. Обратите внимание, что таблица не перекошена.

Использование в intкачестве типа данных не вызывает этого, разве SQL Server не проверяет всю точку данных при использовании varchar?

Стоит отметить, что проблема, кажется, масштабируется, помогает увеличение частоты дискретизации.

Пол Уайт 9
источник

Ответы:

3

Я видел такую ​​же проблему плотности в некоторых некластеризованных индексах в крупнейших базах данных, к которым у меня есть доступ. Сначала я начну с нескольких наблюдений о гистограммах и расчетах плотности:

  • SQL Server может использовать первичный ключ в таблице, чтобы сделать вывод о плотности обоих столбцов. Это означает, что плотность, включающая столбцы PK, обычно будет очень точной.
  • Расчет плотности для первого столбца в статистике согласуется с гистограммой. Если гистограмма плохо моделирует данные, плотность может быть отключена.
  • Для создания гистограммы StatManфункция делает выводы об отсутствующих данных. Поведение может меняться в зависимости от типа данных столбца.

Для одного взгляда на проблему, предположим, что вы отобрали 100 строк из таблицы 10000 строк и получили 100 различных значений. Можно предположить, что остальные данные в таблице состоят из 10000 уникальных значений. Другое предположение состоит в том, что существует 100 различных значений, но каждое из значений повторяется 100 раз. Второе предположение может показаться вам необоснованным, с чем я согласен. Однако как вы уравновешиваете два подхода, когда выборочные данные возвращаются неравномерно? Для этой цели Microsoft разработала некоторый набор алгоритмов StatMan. Алгоритмы могут работать не для всех нарушений данных и для всех уровней выборки.

Давайте рассмотрим относительно простой пример. Я собираюсь использовать VARCHARстолбцы, как в вашей таблице, чтобы увидеть то же поведение. Однако я просто добавлю одно искаженное значение в таблицу. Я тестирую против SQL Server 2016 SP1. Начните с 100 тыс. Строк с 100 тыс. Уникальных значений для FKстолбца:

DROP TABLE IF EXISTS X_STATS_SMALL;

CREATE TABLE X_STATS_SMALL (
ID VARCHAR(10) NOT NULL, 
FK VARCHAR(10) NOT NULL,
PADDING VARCHAR(900) NOT NULL,
PRIMARY KEY (ID)
);
-- insert 100k rows
INSERT INTO X_STATS_SMALL WITH (TABLOCK)
SELECT N, N, REPLICATE('Z', 900)
FROM dbo.GetNums(100000);

CREATE INDEX IX_X_STATS_SMALL ON X_STATS_SMALL (FK);

-- get sampled stats
UPDATE STATISTICS X_STATS_SMALL IX_X_STATS_SMALL;

Вот несколько примеров из статистики:

╔═════════════╦════════════════╦═════════╗
 All density  Average Length  Columns 
╠═════════════╬════════════════╬═════════╣
 1.00001E-05  4.888205        FK      
 1.00001E-05  9.77641         FK, ID  
╚═════════════╩════════════════╩═════════╝

╔══════════════╦════════════╦═════════╦═════════════════════╦════════════════╗
 RANGE_HI_KEY  RANGE_ROWS  EQ_ROWS  DISTINCT_RANGE_ROWS  AVG_RANGE_ROWS 
╠══════════════╬════════════╬═════════╬═════════════════════╬════════════════╣
 1005          0           1        0                    1              
 10648         665.0898    1        664                  1.002173       
 10968         431.6008    1        432                  1              
 11182         290.0924    1        290                  1              
 1207          445.7517    1        446                  1              
 ...           ...         ...      ...                  ...            
 99989         318.3941    1        318                  1              
╚══════════════╩════════════╩═════════╩═════════════════════╩════════════════╝

Для равномерно распределенных данных с одним уникальным значением в строке мы получаем точную плотность, даже со VARCHARстолбцом гистограммы и размером выборки 14294 строк.

Теперь давайте добавим перекошенное значение и снова обновим статистику:

-- add 70k rows with a FK value of '35000'
INSERT INTO X_STATS_SMALL WITH (TABLOCK)
SELECT N + 100000 , '35000',  REPLICATE('Z', 900)
FROM dbo.GetNums(70000);

UPDATE STATISTICS X_STATS_SMALL IX_X_STATS_SMALL;

При размере выборки 17010 строк плотность первого столбца меньше, чем должна быть:

╔══════════════╦════════════════╦═════════╗
 All density   Average Length  Columns 
╠══════════════╬════════════════╬═════════╣
 6.811061E-05  4.935802        FK      
 5.882353E-06  10.28007        FK, ID  
╚══════════════╩════════════════╩═════════╝

╔══════════════╦════════════╦══════════╦═════════════════════╦════════════════╗
 RANGE_HI_KEY  RANGE_ROWS  EQ_ROWS   DISTINCT_RANGE_ROWS  AVG_RANGE_ROWS 
╠══════════════╬════════════╬══════════╬═════════════════════╬════════════════╣
 10039         0           1         0                    1              
 10978         956.9945    1         138                  6.954391       
 11472         621.0283    1         89                   6.941863       
 1179          315.6046    1         46                   6.907561       
 11909         91.62713    1         14                   6.74198        
 ...           ...         ...       ...                  ...            
 35000         376.6893    69195.05  54                   6.918834       
 ...           ...         ...       ...                  ...            
 99966         325.7854    1         47                   6.909731       
╚══════════════╩════════════╩══════════╩═════════════════════╩════════════════╝

Удивительно, что AVG_RANGE_ROWSэто довольно равномерно для всех шагов в пределах 6,9, даже для блоков ключей, для которых образец не смог найти повторяющиеся значения. Я не знаю, почему это так. Наиболее вероятное объяснение состоит в том, что алгоритм, используемый для угадывания пропущенных страниц, не справляется с этим распределением данных и размером выборки.

Как указывалось ранее, можно рассчитать плотность для столбца FK с помощью гистограммы. Сумма DISTINCT_RANGE_ROWSзначений для всех шагов равна 14497. Существует 179 шагов гистограммы, поэтому плотность должна быть около 1 / (179 + 14497) = 0,00006813845, что довольно близко к сообщенному значению.

Тестирование с использованием таблицы большего размера может показать, как проблема может усугубляться по мере увеличения таблицы. На этот раз мы начнем с 1 M строк:

DROP TABLE IF EXISTS X_STATS_LARGE;

CREATE TABLE X_STATS_LARGE (
ID VARCHAR(10) NOT NULL,
FK VARCHAR(10) NOT NULL,
PADDING VARCHAR(900) NOT NULL,
PRIMARY KEY (ID));

INSERT INTO X_STATS_LARGE WITH (TABLOCK)
SELECT N, N, REPLICATE('Z', 900)
FROM dbo.Getnums(1000000);

CREATE INDEX IX_X_STATS_LARGE ON X_STATS_LARGE (FK);

-- get sampled stats
UPDATE STATISTICS X_STATS_LARGE IX_X_STATS_LARGE;

Объект статистики пока не интересен. Плотность для FK1.025289E-06, которая близка к точной (1.0E-06).

Теперь давайте добавим перекошенное значение и снова обновим статистику:

INSERT INTO X_STATS_LARGE WITH (TABLOCK)
SELECT N + 1000000 , '350000',  REPLICATE('Z', 900)
FROM dbo.Getnums(700000);

UPDATE STATISTICS X_STATS_LARGE IX_X_STATS_LARGE;

При размере выборки 45627 строк плотность первого столбца хуже, чем раньше:

╔══════════════╦════════════════╦═════════╗
 All density   Average Length  Columns 
╠══════════════╬════════════════╬═════════╣
 2.60051E-05   5.93563         FK      
 5.932542E-07  12.28485        FK, ID  
╚══════════════╩════════════════╩═════════╝

╔══════════════╦════════════╦═════════╦═════════════════════╦════════════════╗
 RANGE_HI_KEY  RANGE_ROWS  EQ_ROWS  DISTINCT_RANGE_ROWS  AVG_RANGE_ROWS 
╠══════════════╬════════════╬═════════╬═════════════════════╬════════════════╣
 100023        0           1        0                    1              
 107142        8008.354    1        306                  26.17787       
 110529        4361.357    1        168                  26.02392       
 114558        3722.193    1        143                  26.01217       
 116696        2556.658    1        98                   25.97568       
 ...           ...         ...      ...                  ...            
 350000        5000.522    700435   192                  26.03268       
 ...           ...         ...      ...                  ...            
 999956        2406.266    1        93                   25.96841       
╚══════════════╩════════════╩═════════╩═════════════════════╩════════════════╝

AVG_RANGE_ROWSдо 26. Интересно, что если я изменю размер выборки на 170100 строк (в 10 раз больше, чем в другой таблице), то среднее значение AVG_RANGE_ROWSснова будет около 6,9. Когда ваша таблица становится больше, SQL Server будет выбирать меньший размер выборки, что означает, что ему нужно угадывать больший процент страниц в таблице. Это может преувеличивать статистические проблемы для некоторых видов перекоса данных.

В заключение важно помнить, что SQL Server не рассчитывает плотность следующим образом:

SELECT COUNT(DISTINCT FK) * 1700000. / COUNT(*) -- 1071198.9 distinct values for one run
FROM X_STATS_LARGE TABLESAMPLE (45627 ROWS);

Что для некоторых распределений данных будет очень точным. Вместо этого он использует недокументированные алгоритмы . В своем вопросе вы сказали, что ваши данные не были искажены, но INSTANCEELEMENTIDзначение с наибольшим количеством связанных идентификаторов имеет 12, а наиболее распространенное число равно 1. Для целей используемых алгоритмов Statmanэто может быть искажено.

В этот момент вы ничего не можете с этим поделать, кроме сбора статистики с более высокой частотой дискретизации. Одна общая стратегия - собирать статистику с FULLSCANи NORECOMPUTE. Вы можете обновить статистику с заданием на любом интервале, который имеет смысл для вашей скорости изменения данных. По моему опыту, FULLSCANобновление не так плохо, как думает большинство людей, особенно в отношении индекса. SQL Server может просто сканировать весь индекс, а не всю таблицу (как это было бы для таблицы хранилища строк по отношению к неиндексированному столбцу). Кроме того, в SQL Serer 2014 FULLSCANпараллельно выполняются только обновления статистики, поэтому FULLSCANобновление может завершиться быстрее, чем некоторые выборочные обновления.

Джо Оббиш
источник
Спасибо за ответ, Джо! Это похоже на ошибку или пробел в функции; помните, что такое поведение не происходит, когда вы используете значения на основе INT. На INT система работает намного лучше, и вы получаете оценку статистического распределения, которое намного лучше приближается к реальному распределению. В то время как StatMan явно делает некоторые сглаживания / эвристики; Я бы сказал, что довольно неприятно, что вы можете получить намного лучшие результаты самостоятельно, вычисляя гистограмму напрямую, по-прежнему используя те же исходные данные, что и при использованииtablesample
@JohanBenumEvensberget IMO Не так уж и разумно вести себя по-разному для столбцов INT. С INT у вас есть гораздо более ограниченный домен для пропущенных значений. Для строк это может быть что угодно, вплоть до ограничения длины. Это может сбивать с толку, когда мы не получаем хорошую гистограмму, но большую часть времени она работает довольно хорошо. Поскольку код является секретным, мы не можем точно сказать, работает ли он должным образом или нет. Вы можете сделать здесь сообщение, если считаете, что эта проблема должна решаться MS: connect.microsoft.com/SQLServer/Feedback
Джо Оббиш,