Порядок сортировки кластеризованного индекса SQL Server 2008+
влияет ли порядок сортировки кластеризованного индекса SQL Server 2008+ на производительность вставки?
тип данных в конкретном случае составляет integer
и вставленные значения возрастают (Identity
). Поэтому порядок сортировки индекса будет противоположен порядку сортировки вставляемых значений.
Я предполагаю, что это повлияет, но я не знаю, может быть, SQL Server имеет некоторые оптимизации для этого случая или это внутренний формат хранения данных равнодушен к этому.
обратите внимание, что вопрос о INSERT
производительность, а не SELECT
.
обновление
Чтобы быть более ясным в вопросе: что происходит, когда значения, которые будут вставлены (integer
) в обратном порядке (ASC
) для упорядочения кластеризованного индекса (DESC
)?
4 ответов
есть разница. Вставка из порядка кластеров вызывает массовую фрагментацию.
при выполнении следующего кода кластеризованный индекс DESC генерирует дополнительные операции обновления на уровне NONLEAF.
CREATE TABLE dbo.TEST_ASC(ID INT IDENTITY(1,1)
,RandNo FLOAT
);
GO
CREATE CLUSTERED INDEX cidx ON dbo.TEST_ASC(ID ASC);
GO
CREATE TABLE dbo.TEST_DESC(ID INT IDENTITY(1,1)
,RandNo FLOAT
);
GO
CREATE CLUSTERED INDEX cidx ON dbo.TEST_DESC(ID DESC);
GO
INSERT INTO dbo.TEST_ASC VALUES(RAND());
GO 100000
INSERT INTO dbo.TEST_DESC VALUES(RAND());
GO 100000
два оператора Insert производят точно такой же план выполнения, но при просмотре операционной статистики различия отображаются против [nonleaf_update_count].
SELECT
OBJECT_NAME(object_id)
,*
FROM sys.dm_db_index_operational_stats(DB_ID(),OBJECT_ID('TEST_ASC'),null,null)
UNION
SELECT
OBJECT_NAME(object_id)
,*
FROM sys.dm_db_index_operational_stats(DB_ID(),OBJECT_ID('TEST_DESC'),null,null)
экстра-под клобуком-деятельность продолжается, когда SQL работает с индексом DESC, который работает против идентификатора. Это происходит потому, что таблица DESC становится фрагментированной (строки вставляются в начале страницы) и дополнительные обновления происходят для поддержания структуры B-дерева.
наиболее заметным в этом примере является то, что кластеризованный индекс DESC становится более 99% фрагментированным. это воссоздание того же плохого поведения, что и использование случайного GUID для кластеризованного индекса. Приведенный ниже код демонстрирует фрагментация.
SELECT
OBJECT_NAME(object_id)
,*
FROM sys.dm_db_index_physical_stats (DB_ID(), OBJECT_ID('dbo.TEST_ASC'), NULL, NULL ,NULL)
UNION
SELECT
OBJECT_NAME(object_id)
,*
FROM sys.dm_db_index_physical_stats (DB_ID(), OBJECT_ID('dbo.TEST_DESC'), NULL, NULL ,NULL)
обновление:
В некоторых тестовых средах я также вижу, что таблица DESC подвержена большему количеству ожиданий с увеличением [page_io_latch_wait_count] и [page_io_latch_wait_in_ms]
обновление:
возникла дискуссия о том, что является точкой нисходящего индекса, когда SQL может выполнять обратное сканирование. Пожалуйста, прочитайте эту статью о ограничения назад Сканы.
порядок значений, вставленных в кластеризованный индекс, наиболее определенно влияет на производительность индекса, потенциально создавая много фрагментации, а также влияет на производительность самой вставки.
я построил испытательный стенд, чтобы посмотреть, что произойдет:
USE tempdb;
CREATE TABLE dbo.TestSort
(
Sorted INT NOT NULL
CONSTRAINT PK_TestSort
PRIMARY KEY CLUSTERED
, SomeData VARCHAR(2048) NOT NULL
);
INSERT INTO dbo.TestSort (Sorted, SomeData)
VALUES (1797604285, CRYPT_GEN_RANDOM(1024))
, (1530768597, CRYPT_GEN_RANDOM(1024))
, (1274169954, CRYPT_GEN_RANDOM(1024))
, (-1972758125, CRYPT_GEN_RANDOM(1024))
, (1768931454, CRYPT_GEN_RANDOM(1024))
, (-1180422587, CRYPT_GEN_RANDOM(1024))
, (-1373873804, CRYPT_GEN_RANDOM(1024))
, (293442810, CRYPT_GEN_RANDOM(1024))
, (-2126229859, CRYPT_GEN_RANDOM(1024))
, (715871545, CRYPT_GEN_RANDOM(1024))
, (-1163940131, CRYPT_GEN_RANDOM(1024))
, (566332020, CRYPT_GEN_RANDOM(1024))
, (1880249597, CRYPT_GEN_RANDOM(1024))
, (-1213257849, CRYPT_GEN_RANDOM(1024))
, (-155893134, CRYPT_GEN_RANDOM(1024))
, (976883931, CRYPT_GEN_RANDOM(1024))
, (-1424958821, CRYPT_GEN_RANDOM(1024))
, (-279093766, CRYPT_GEN_RANDOM(1024))
, (-903956376, CRYPT_GEN_RANDOM(1024))
, (181119720, CRYPT_GEN_RANDOM(1024))
, (-422397654, CRYPT_GEN_RANDOM(1024))
, (-560438983, CRYPT_GEN_RANDOM(1024))
, (968519165, CRYPT_GEN_RANDOM(1024))
, (1820871210, CRYPT_GEN_RANDOM(1024))
, (-1348787729, CRYPT_GEN_RANDOM(1024))
, (-1869809700, CRYPT_GEN_RANDOM(1024))
, (423340320, CRYPT_GEN_RANDOM(1024))
, (125852107, CRYPT_GEN_RANDOM(1024))
, (-1690550622, CRYPT_GEN_RANDOM(1024))
, (570776311, CRYPT_GEN_RANDOM(1024))
, (2120766755, CRYPT_GEN_RANDOM(1024))
, (1123596784, CRYPT_GEN_RANDOM(1024))
, (496886282, CRYPT_GEN_RANDOM(1024))
, (-571192016, CRYPT_GEN_RANDOM(1024))
, (1036877128, CRYPT_GEN_RANDOM(1024))
, (1518056151, CRYPT_GEN_RANDOM(1024))
, (1617326587, CRYPT_GEN_RANDOM(1024))
, (410892484, CRYPT_GEN_RANDOM(1024))
, (1826927956, CRYPT_GEN_RANDOM(1024))
, (-1898916773, CRYPT_GEN_RANDOM(1024))
, (245592851, CRYPT_GEN_RANDOM(1024))
, (1826773413, CRYPT_GEN_RANDOM(1024))
, (1451000899, CRYPT_GEN_RANDOM(1024))
, (1234288293, CRYPT_GEN_RANDOM(1024))
, (1433618321, CRYPT_GEN_RANDOM(1024))
, (-1584291587, CRYPT_GEN_RANDOM(1024))
, (-554159323, CRYPT_GEN_RANDOM(1024))
, (-1478814392, CRYPT_GEN_RANDOM(1024))
, (1326124163, CRYPT_GEN_RANDOM(1024))
, (701812459, CRYPT_GEN_RANDOM(1024));
первый столбец является первичным ключом, и, как вы можете видеть, значения перечислены в случайном порядке(ish). Перечисление значений в случайном порядке должно сделать SQL Server либо:
- сортировка данных, предварительно вставить
- не сортировать данные, что приводит к фрагментированной таблице.
на CRYPT_GEN_RANDOM()
функция используется для генерации 1024 байт случайных данных в строке, чтобы позволить этой таблице потреблять несколько страниц, что в свою очередь позволяет нам видеть эффекты фрагментированных вставок.
после запуска вышеуказанной вставки вы можете проверить фрагментацию следующим образом:
SELECT *
FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID('TestSort'), 1, 0, 'SAMPLED') ips;
запуск это на моем экземпляре SQL Server 2012 Developer Edition показывает среднюю фрагментацию 90%, что указывает на то, что SQL Server не сортировал во время вставки.
мораль этой особенности история, скорее всего, будет, "когда сомневаетесь, сортируйте, если это будет полезно". Сказав это, добавив и ORDER BY
предложение инструкции insert не гарантирует, что вставки будут происходить в этом порядке. Рассмотрим, что происходит, если вставка идет параллельно, в качестве примера.
On непроизводственные системы флаг трассировки 2332 можно использовать в инструкции insert для" принудительной " сортировки входных данных SQL Server перед их вставкой. @PaulWhite есть интересная статья, оптимизация запросов T-SQL, которые изменяют данные покрытие этого и других деталей. Знайте, что флаг трассировки не поддерживается и не должен использоваться в производственных системах, так как это может привести к аннулированию гарантии. В непроизводственной системе, для собственного образования, можно попробовать добавление этого в конец INSERT
о себе:
OPTION (QUERYTRACEON 2332);
как только вы добавите это к вставке, взгляните на план, вы увидите явный вид:
было бы здорово, если бы Microsoft сделала это поддерживаемым флагом трассировки.
Пол Уайт дал мне понять этот SQL Server тут автоматически ввести оператор сортировки в план, когда он думает, что будет полезный. Для примера запроса выше, если я запускаю insert с 250 элементами в values
предложения, не реализуется автоматически. Однако при 251 элементе SQL Server автоматически сортирует значения перед вставкой. Почему срез 250/251 строк остается для меня загадкой, кроме того, что он кажется жестко закодированным. Если я уменьшу размер данных, вставленных в SomeData
столбец до одного байта, отсечение еще 250/251 строк, даже если размер таблицы в обоих достаточно одной странице. Интересно, глядя на вставку с SET STATISTICS IO, TIME ON;
показывает вставки с одним байтом SomeData
значение занимает в два раза больше времени при сортировке.
без сортировки (т. е. вставлено 250 строк):
SQL Server parse and compile time: CPU time = 0 ms, elapsed time = 0 ms. SQL Server parse and compile time: CPU time = 16 ms, elapsed time = 16 ms. SQL Server parse and compile time: CPU time = 0 ms, elapsed time = 0 ms. Table 'TestSort'. Scan count 0, logical reads 501, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. (250 row(s) affected) (1 row(s) affected) SQL Server Execution Times: CPU time = 0 ms, elapsed time = 11 ms.
с сортировкой (т. е. вставлено 251 строка):
SQL Server parse and compile time: CPU time = 0 ms, elapsed time = 0 ms. SQL Server parse and compile time: CPU time = 15 ms, elapsed time = 17 ms. SQL Server parse and compile time: CPU time = 0 ms, elapsed time = 0 ms. Table 'TestSort'. Scan count 0, logical reads 503, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. (251 row(s) affected) (1 row(s) affected) SQL Server Execution Times: CPU time = 16 ms, elapsed time = 21 ms.
как только вы начнете увеличивать размер строки, сортированная версия, безусловно, станет более эффективной. При вставке 4096 байт в SomeData
, отсортированные вставить почти в два раза так же быстро на моей испытательной установке, как несортированная вставка.
в качестве примечания, если вам интересно, я создал VALUES (...)
предложение, использующее этот T-SQL:
;WITH s AS (
SELECT v.Item
FROM (VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9)) v(Item)
)
, v AS (
SELECT Num = CONVERT(int, CRYPT_GEN_RANDOM(10), 0)
)
, o AS (
SELECT v.Num
, rn = ROW_NUMBER() OVER (PARTITION BY v.Num ORDER BY NEWID())
FROM s s1
CROSS JOIN s s2
CROSS JOIN s s3
CROSS JOIN v
)
SELECT TOP(50) ', ('
+ REPLACE(CONVERT(varchar(11), o.Num), '*', '0')
+ ', CRYPT_GEN_RANDOM(1024))'
FROM o
WHERE rn = 1
ORDER BY NEWID();
это генерирует 1000 случайных значений, выбирая только верхние 50 строк с уникальными значениями в первом столбце. Я скопировал и вставил вывод в INSERT
заявление выше.
пока данные упорядочиваются кластеризованным индексом (независимо от того, по возрастанию или по убыванию), то не должно быть никакого влияния на производительность вставки. Причиной этого является то, что SQL не заботится о физическом порядке строк на странице для кластеризованного индекса. Порядок строк хранится в так называемом" массиве смещения записи", который является единственным, который необходимо переписать для новой строки (что в любом случае было бы сделано независимо от порядка). Этот фактические строки данных будут просто записываться один за другим.
на уровне журнала транзакций записи должны быть идентичными независимо от направления, чтобы это не создавало дополнительного влияния на производительность. Обычно журнал транзакций генерирует большинство проблем с производительностью, но в этом случае их не будет.
вы можете найти хорошее объяснение физической структуре страницы / строки здесь https://www.simple-talk.com/sql/database-administration/sql-server-storage-internals-101/ .
таким образом, в основном, пока ваши вставки не будут генерировать разбиения страниц (и если данные поступают в порядке кластеризованного индекса независимо от порядка, он не будет), ваши вставки будут иметь незначительное влияние на производительность вставки.
на основе приведенного ниже кода вставка данных в столбец идентификаторов с отсортированным кластеризованным индексом является более ресурсоемкой, если выбранные данные упорядочены в противоположном направлении от отсортированного кластеризованного индекса.
в этом примере логические чтения почти удвоены.
после 10 запусков отсортированное по возрастанию логическое считывает среднее 2284 и отсортированное по убыванию логическое считывает среднее 4301.
--Drop Table Destination;
Create Table Destination (MyId INT IDENTITY(1,1))
Create Clustered Index ClIndex On Destination(MyId ASC)
set identity_insert destination on
Insert into Destination (MyId)
SELECT TOP (1000) n = ROW_NUMBER() OVER (ORDER BY [object_id])
FROM sys.all_objects
ORDER BY n
set identity_insert destination on
Insert into Destination (MyId)
SELECT TOP (1000) n = ROW_NUMBER() OVER (ORDER BY [object_id])
FROM sys.all_objects
ORDER BY n desc;
подробнее о логических чтениях, если вы заинтересованный: https://www.brentozar.com/archive/2012/06/tsql-measure-performance-improvements/