B-деревья, базы данных, последовательные и случайные вставки и скорость. Рандом побеждает

ИЗМЕНИТЬ

@Remus исправил мой тестовый образец. Вы можете увидеть исправленную версию в его ответе ниже.

Я принял предложение заменить INT на DECIMAL(29,0) и получил следующие результаты:

Десятичный: 2133
GUID: 1836

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

Несмотря на объяснения, указывающие на то, что случайные вставки медленнее, чем последовательные, эти тесты показывают, что они явно быстрее. Объяснения, которые я получаю, не согласуются с тестами. Поэтому мой вопрос по-прежнему сосредоточен на b-деревьях, последовательных вставках и скорости.

...

Я знаю по опыту, что b-деревья имеют ужасную производительность, когда данные добавляются к ним последовательно (независимо от направления). Однако когда данные добавляются случайным образом, достигается наилучшая производительность.

Это легко продемонстрировать с помощью RB-Tree. При последовательной записи выполняется максимальное количество балансировок дерева.

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

Это разожгло мое любопытство.

Если это так, то можно сделать вывод, что запись последовательных идентификаторов (например, в IDENTITY(1,1)) приведет к многократной перебалансировке дерева. Я видел многие сообщения, выступающие против GUID, поскольку "это вызовет случайную запись". Я никогда не использую GUID, но меня поразило, что это "плохое" замечание на самом деле было хорошим замечанием.

Поэтому я решил проверить это. Вот мой код:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[T1](
    [ID] [int] NOT NULL
 CONSTRAINT [T1_1] PRIMARY KEY CLUSTERED ([ID] ASC) 
)
GO

CREATE TABLE [dbo].[T2](
    [ID] [uniqueidentifier] NOT NULL
 CONSTRAINT [T2_1] PRIMARY KEY CLUSTERED ([ID] ASC)
)

GO

declare @i int, @t1 datetime, @t2 datetime, @t3 datetime, @c char(300)

set @t1 = GETDATE()
set @i = 1

while @i < 2000 begin
    insert into T2 values (NEWID(), @c)
    set @i = @i + 1
end

set @t2 = GETDATE()
WAITFOR delay '0:0:10'
set @t3 = GETDATE()
set @i = 1

while @i < 2000 begin
    insert into T1 values (@i, @c)
    set @i = @i + 1
end

select DATEDIFF(ms, @t1, @t2) AS [Int], DATEDIFF(ms, @t3, getdate()) AS [GUID]

drop table T1
drop table T2

Обратите внимание, что я не вычитаю время на создание GUID ни на значительно больший размер строки. Результаты на моей машине были следующими:

Целое: 17 340 мс GUID: 6 746 мс

Это означает, что в этом тесте случайные вставки 16 байтов были почти в 3 раза быстрее, чем последовательные вставки 4 байтов.

Кто-нибудь хочет это прокомментировать?

Пс. Я понимаю, что это не вопрос. Это приглашение к обсуждению, и это имеет отношение к изучению оптимального программирования.


person IamIC    schedule 04.01.2011    source источник
comment
Добавьте столбец char(3000) или столбец char(500), чтобы уменьшить плотность строк на странице. Что происходит при запуске 2-й раз? Должен ли БД расти? Затем добавьте некластеризованный индекс в столбец char (если ‹ 900). Я бы предпочел что-то более связанное с реальной жизнью...   -  person gbn    schedule 04.01.2011
comment
Я сделал это (символ (3000)). Результаты: Интеллект: 7 406; GUID: 22 286. Char(300): Int: 6630, GUID: 5816. Почему?   -  person IamIC    schedule 04.01.2011


Ответы (3)


переверните операцию, и int будет быстрее ... учли ли вы рост журнала и файла данных? Запускать каждый отдельно

declare @i int, @t1 datetime, @t2 datetime

set @t1 = GETDATE()
set @i = 1

while @i < 10000 begin
    insert into T2 values (NEWID())
    set @i = @i + 1
END


set @t2 = GETDATE()
set @i = 1

while @i < 10000 begin
    insert into T1 values (@i)
    set @i = @i + 1
end



select DATEDIFF(ms, @t1, @t2) AS [UID], DATEDIFF(ms, @t2, getdate()) AS [Int]

проблема с UUID заключается в том, что при кластеризации по ним и без использования NEWSEQUENTIALID() они вызывают разрывы страниц и фрагментацию таблицы.

теперь попробуй вот так и увидишь, что это почти то же самое

declare @i int, @t1 datetime, @t2 datetime

set @t1 = GETDATE()
set @i = 1

while @i < 10000 begin
    insert into T2 values (NEWID())
    set @i = @i + 1
END
select DATEDIFF(ms, @t1, getdate()) 

set @t1 = GETDATE()
set @i = 1

while @i < 10000 begin
    insert into T1 values (@i)
    set @i = @i + 1
end



select DATEDIFF(ms, @t1, getdate())

И наоборот

declare @i int, @t1 datetime, @t2 datetime



set @t1 = GETDATE()
set @i = 1

while @i < 10000 begin
    insert into T1 values (@i)
    set @i = @i + 1
end

set @t1 = GETDATE()
set @i = 1

while @i < 10000 begin
    insert into T2 values (NEWID())
    set @i = @i + 1
END
select DATEDIFF(ms, @t1, getdate())
person SQLMenace    schedule 04.01.2011
comment
Я попробовал, и вы правы. Однако я бы никогда не использовал UID в качестве ключа. Мой вопрос действительно о переупорядочении b-дерева. - person IamIC; 04.01.2011
comment
проблема с UUID заключается в том, что при кластеризации по ним и без использования NEWSEQUENTIALID() они вызывают разрывы страниц и фрагментацию таблицы - это из-за случайности вставок? - person IamIC; 04.01.2011
comment
да, это правильно, так как ему нужно освободить место на странице, а затем он выполняет разделение с помощью NEWSEQUENTIALID(), этого не происходит - person SQLMenace; 04.01.2011
comment
Конечно, если бы у кого-то был большой varchar в качестве PK, и значения были несколько случайными, это могло бы быть еще хуже. - person IamIC; 04.01.2011

Вы не измеряете скорость INSERT. Вы измеряете производительность сброса журнала. Поскольку вы совершаете коммит после каждого INSERT, все эти тесты сидят без дела в ожидании коммита, чтобы закрепить журнал. Это вряд ли имеет отношение к производительности INSERT. И, пожалуйста, не публикуйте измерения «производительности», когда SET NOCOUNT равен OFF...

Итак, давайте попробуем это без ненужной болтовни между сервером и клиентом, с данными правильного размера, пакетными фиксациями и предварительно выращенными базами данных:

:setvar dbname testdb
:setvar testsize 1000000
:setvar batchsize 1000

use master;
go

if db_id('$(dbname)') is not null
begin
    drop database [$(dbname)];
end
go

create database [$(dbname)] 
    on (name='test_data', filename='c:\temp\test_data.mdf', size=10gb)
    log on (name='test_log', filename='c:\temp\test_log.ldf', size=100mb);
go

use [$(dbname)];
go  

CREATE TABLE [dbo].[T1](
    [ID] [int] NOT NULL
 CONSTRAINT [T1_1] PRIMARY KEY CLUSTERED ([ID] ASC) 
)
GO

CREATE TABLE [dbo].[T2](
    [ID] [uniqueidentifier] NOT NULL
 CONSTRAINT [T2_1] PRIMARY KEY CLUSTERED ([ID] ASC)
)
GO

set nocount on;
go

declare @i int, @t1 datetime, @t2 datetime

set @t1 = GETDATE()
set @i = 1

begin transaction;
while @i < $(testsize) begin
    insert into T1 values (@i)
    set @i = @i + 1
    if @i % $(batchsize) = 0
    begin
        commit;
        begin transaction;
    end
end
commit

set @t2 = GETDATE()
set @i = 1
begin transaction
while @i < $(testsize) begin
    insert into T2 values (NEWID())
    set @i = @i + 1
    if @i % $(batchsize) = 0
    begin
        commit;
        begin transaction;
    end
end
commit

select DATEDIFF(ms, @t1, @t2) AS [Int], DATEDIFF(ms, @t2, getdate()) AS [UID]

drop table T1
drop table T2

INTS: 18 с
GUID: 23 с

КЭД

person Remus Rusanu    schedule 04.01.2011
comment
@ Ремус, я не устанавливал SET NOCOUNT OFF. Но почему это повлияет на этот тест? - person IamIC; 05.01.2011
comment
@IanC по умолчанию NOCOUNT выключен, вы должны включить его явно - person SQLMenace; 05.01.2011
comment
SET NOCOUNT OFF (по умолчанию) отправляет после каждой вставки число строк обратно клиенту (1 rows inserted). Таким образом, сервер теперь должен ждать, пока клиент обработает эти сообщения. - person Remus Rusanu; 05.01.2011
comment
Спасибо. Я не видел :setvar раньше. Это дает мне синтаксическую ошибку. Мне нужно его во что-то завернуть? - person IamIC; 05.01.2011
comment
Вам необходимо включить расширения SQLCMD в SSMS, см. msdn.microsoft.com/en- нас/библиотека/ms174187.aspx - person Remus Rusanu; 05.01.2011
comment
Инструменты\Параметры\Проверка выполнения запроса By default, open new queries in SQLCMD mode - person Remus Rusanu; 05.01.2011
comment
Также убедитесь, что мгновенная инициализация файлов включена, не нужно ждать обнуления 10-гигабайтного файла... см. msdn.microsoft.com/en-us/library/ms175935.aspx - person Remus Rusanu; 05.01.2011
comment
@ Ремус Я долго ждал :-) У тебя наверняка есть быстрое оборудование! Примерно в 12 раз больше моего. - person IamIC; 05.01.2011
comment
Я запускаю их на ноутбуке (и, как и на большинстве ноутбуков, на нем действительно включено кэширование записи на диск, правда) ... Тем не менее, похоже, что у вас очень медленное оборудование :) - person Remus Rusanu; 05.01.2011
comment
@ Ремус, я только что понял, что просчитался. Это происходит в 5:30 утра. Я также на ноутбуке, 2 ГГц двухъядерный, 5400 об / мин ... ничего плохого или особенного. Я использую SQL Server 2008 Express, поэтому я изменил размер на 4 ГБ и количество на 100 тыс. У меня 2s и 2.7s. - person IamIC; 05.01.2011
comment
@ Ремус, хорошо, ты доказал свою точку зрения. Однако чего я не понимаю, так это того, что с последовательным int должно быть много переупорядочений дерева, что привело бы к тяжелому дисковому вводу-выводу. Этот вопрос остается в силе. - person IamIC; 05.01.2011
comment
Да, о переупорядочении дерева: его нет. В обоих случаях. B-деревья самобалансируются, и никогда не происходит переупорядочения или реорганизации дерева. Единственные «накладные расходы» связаны с разделением страниц. Последовательные вставки вызывают меньше разбиений страниц, чем случайные вставки. В этих двух тестах идентификаторы GUID работают медленнее из-за двух факторов: большего размера ключа (всего больше страниц) и случайных вставок (вызывающих разбиение страниц). Если вместо NEWID использовать NEWSEQUENTIALGUID, то случайные вставки исключаются, и в моих тестах это приводит к времени примерно ~ 21 с, разница с INT 18 с из-за более широкого ключа, 16 против 4 байта. - person Remus Rusanu; 05.01.2011
comment
Хорошо, пожалуйста, потерпите меня здесь. Если страница в b-дереве заполняется, конечно, ее нужно разделить и обновить все соответствующие указатели, точно так же, как это было бы в ОЗУ в C? Иначе как она может оставаться сбалансированной? - person IamIC; 05.01.2011
comment
Давайте посмотрим, умещается ли он в 600 символов: как случайная вставка, так и последовательная вставка может привести к разделению страницы. Для последовательной вставки потребуется новая страница для нового ключа, но никакие ключи не должны перемещаться. Это делает эту операцию дешевой: выделить новую страницу, изменить предыдущую последнюю страницу next 'указатель', чтобы он указывал на новую страницу, готово. Для случайной вставки разделение страницы будет иметь клавиши перемещения. Так намного дороже: выделить новую страницу, скопировать примерно половину строк со старой страницы на новую, затем исправить ссылку next на предыдущей странице и ссылку prev на следующей странице . Больше работы, больше времени. - person Remus Rusanu; 05.01.2011
comment
Как последовательные, так и случайные вставки вызывают разделение страниц, а также должны вставлять ключ на следующем, верхнем уровне. Но эта вставка в точности описана выше: последовательная вставка конечной страницы вызовет последовательную вставку верхней страницы, а случайная вставка вызовет случайную вставку. Применяются те же соображения стоимости. Таким образом, вы правы в том, что заполнение страницы должно быть разделено, а все указатели обновлены и т. д. Но работа, проделанная для случайного разделения страницы, значительно больше, чем разделение страницы, вызванное последовательной вставкой. В основном потому, что в последовательном случае не нужно перемещать строки. - person Remus Rusanu; 05.01.2011
comment
Кстати, из интереса добавление заполнения char(12) в таблицу int, что делает размер строки двух таблиц равным (+/- накладные расходы), превращает таблицы, и GUID выигрывает 2160 против 1786 мс. - person IamIC; 05.01.2011
comment
С INT+padding GUID имеют несправедливое преимущество: у них нет столбцов в строках, а у int есть столбец. Чтобы быть справедливым, используйте DECIMAL(29,0), который будет храниться в 17 байтах (msdn.microsoft.com/en-us/library/ms187746.aspx). - person Remus Rusanu; 05.01.2011
comment
@Remus Я принял ваше предложение заменить INT на DECIMAL (29,0), и результаты были следующими: Decimal: 2133, GUID: 1836. Случайные вставки по-прежнему выигрывают, даже с немного большей строкой. Ваш комментарий Давайте посмотрим... похоже, он описывает связанный список, а не дерево. - person IamIC; 05.01.2011

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

Что может стать более серьезной проблемой, так это конкуренция за этот единственный блок, содержащий все новые записи. В Oracle есть функция хранения байтов ключа в обратном порядке для распространения новых записей по всем блокам: http://oracletoday.blogspot.com/2006/09/there-is-option-to-create-index.html Не знаю о других базах данных .

person Jens Schauder    schedule 04.01.2011
comment
Я недавно хотел кое-что проверить. Я создал таблицу с двумя столбцами int, 1-й PK IDENTITY(1,1), а второй имел свой собственный индекс. Потребовалось чуть более 24 часов, чтобы вставить 16 миллионов записей... это ‹ 200 в секунду. Это ужасное выступление и стало причиной написания этого поста. - person IamIC; 04.01.2011
comment
возможно, Oracle делает это, чтобы избежать проблемы, о которой я упоминаю. - person IamIC; 04.01.2011
comment
Конфликт защелки при вставке последней страницы будет виден только в высокопроизводительных системах (16–32 ЦП и выше). См. sqlcat.com/technicalnotes/archive/2009/09/22/ - person Remus Rusanu; 05.01.2011
comment
@IanC: 200 вставок в секунду, скорее всего, из-за отсутствия пакетной фиксации. Обычная подсистема ввода-вывода может обрабатывать около 200-350 сбросов журнала в секунду. Вы должны выполнять пакетную фиксацию для достижения какой-либо производительности в ETL. Обычные нагрузки OLTP выигрывают от множества одновременных пользователей, а стоимость очистки журнала амортизируется по многим одновременным запросам, поэтому это не такая уж большая проблема. - person Remus Rusanu; 05.01.2011
comment
@ Ремус Я видел это из твоего кода, спасибо. Я почти никогда не запускаю такие вставки, поэтому я никогда не знал о пользе того, что вы говорите. Но это отмечено сейчас. - person IamIC; 05.01.2011