Я видел такую же проблему плотности в некоторых некластеризованных индексах в крупнейших базах данных, к которым у меня есть доступ. Сначала я начну с нескольких наблюдений о гистограммах и расчетах плотности:
- 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;
Объект статистики пока не интересен. Плотность для FK
1.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
обновление может завершиться быстрее, чем некоторые выборочные обновления.
tablesample