Целочисленные, символьные и GUID-ключи в MS SQL

| рубрика: Испытания | автор: st
Метки:

Сравнение производительности некоторых SQL-операций при использовании целочисленных, символьных и GUID-ключей. Тесты проводились на Microsoft SQL Server 2000 и 2005.

Обновление от 2012-11-01. Более развернутый тест для SQL Server 2012 с учетом параметров хранения.

Содержание теста

Тест создает в базе данных 6 таблиц. Таблицы Т1 и Т11 имеют целочисленные первичные ключи типа int, Т1 связана с Т11 в отношении "один-ко-многим". Имеются аналогичные таблицы Т2 и Т21 с символьными ключами типа char(15). В каждой таблице имеется индекс на первичный ключ. Таблицы Т11 и Т21 имеют также индексы на внешние ключи. Для GUID-ключей были добавлены таблицы T3 и T31. Этот тест проводился только на MS SQL 2005.

В тесте определятся параметры-переменные:

Параметр Нанзначение
@ParentCount определяет количество записей в главной таблице
@ChildCount определяет количество записей в связанной таблице на каждую запись в главной
@StagesCount количество циклов повторения этапа выполнения запросов для получения усредненных результатов
@MaxNum максимальное значение для диапазона значений поля Num (0, @MaxNum]

В первой части теста выполняется создание и заполнение таблиц данными. Ключи генерируются последовательным образом, однако для символьных полей порядковый номер добавляемой записи преобразуется в "NKEY00000...", где N - порядковый номер, KEY0000 - символьное выражение, дополняющее длину преобразованного в строку номера до 15 (Это выполняет процедура GetCharKey).

Для полей Num в таблицах T1 и Т2 генерируются случайные значения в диапазоне (0, @MaxNum]. Значение @MaxNum следует выбирать меньшим в разы, чем количество записей в главной таблице.

Поля Title всех таблиц заполняются фиксированными значениями вида: "'Title T: KEY'", где Т - название таблицы, KEY - значение ее ключа, преобразованного в строку. Имеется возможность заполнять строки случайным образом, однако это резко увеличивает время, требуемое для заливки первоначальных данных. Тем не менее, для включения этого режима достаточно удалить комментарии в тексте вызов процедуры GetRandomString и установить комментарии на строку присвоения переменной @Title фиксированного значения.

Во второй части теста производятся запросы к обеим таблицам, вида:

SELECT @j = count(*)
FROM T1 JOIN T11 ON T1.T1ID = T11.T1ID
WHERE T1.Num = @k

Значение @k выбирается случайным образом из диапазона (0, @MaxNum]. Запрос представляет собой подсчет количества записей, попавших в запрос после эквисоединения двух таблиц по ключу с условием фильтрации по случайно выбираемым значением поля Num. При первоначально выбранных значениях @MaxNum меньших, чем @ParentsCount обеспечивается выборка более чем одной записи из главной таблицы и их соединение с записями из связанной таблицы. Поле, по которому происходит фильтрация также имеет индекс, избирательность которого напрямую зависит от соотношения @ParentsCount к @MaxNum. К каждой групп таблиц производится @ParentsCount запросов.

В третьей части теста производятся простые поисковые запросы в связанных таблицах по ключу, значение которого выбирается случайным образом из диапазона (0..@ParentsCount]. К каждой групп таблиц также производится @ParentsCount запросов.

Исходный текст теста

Текст SQL-скрипта теста находится в конце страницы.

Условия проведения теста

В разное время для проведения теста использовались две конфигурации.

Конфигурация 1: MS SQL Server 2000 Evaluation, установленный на ПК Pentium-II-300, RAM 256, HDD 20 Gb UltraATA-33. Операционная система: Windows NT Workstation 4 SP5.

Конфигурация 2: MS SQL Server 2005 Developer, установленный на ПК AMD Athlon 2400, RAM 1 Gb, HDD WesternDigital 240 Gb Serial ATA 300. Операционная система: Windows XP Pro SP2.

Под SQL Server в обоих случаях выделялось не более 192 Мб физической памяти. Размер тестовой БД - 500 Мб (исключает динамическое увеличение), журнала - 50 Мб, Размер временной БД (tempdb) - 300 Мб.

Параметры:

Параметр Значение - прогон 1 Прогон 2 Прогон 3
@ParentsCount 10 000 10 000 10 000
@ChildsCount 30 40 50
@StagesCount 10 10 10
@MaxNum 1 000 1 000 1 000

Таким образом, на каждом прогоне в группу таблиц заносится: 10 000 записей в главную и 300 000 (400 000 и 500 000) - в связанную. Выполняется 10 циклов выполнения запросов, по 10 000 к каждой группе таблиц в цикле (всего 100 000 запросов к каждой группе). Так как соотношение @ParentsCount к @MaxNum составляет на всех прогонах 10, то в среднем за каждый запрос с эквисоединением выбирается 10*@ChildsCount записей (300, 400 и 500, соответственно).

Результаты

Наиболее интересны несколько выявленных наблюдений:

Наиболее интересны несколько выявленных наблюдений:

  • Поиск по ключу символьного поля производится примерно так же быстро или даже быстрее, чем по целочисленному
  • MS SQL 2005 производит соединение по целочисленным ключам примерно в 2 раза быстрее своего предшественника (относительно скорости соединения по символьным ключам)
  • При использование GUID-ключей быстродействие сравнимо с таковым при целочисленных ключах
  • Мощность компьютера возросла: в 8 раз по частоте процессора, в 10 раз по частоте шины контролера диска (ОЗУ в 4 раза можно не учитывать, т.к. объемы данных не изменились). При этом скорость обработки запросов выросла только в 5-6 раз.
Тест и этап / Результаты Таблицы с целочисленными ключами, мсек Таблицы с символьными ключами, мсек Таблицы с GUID-ключами, мсек Соотношение времени (2):(3):(4), %
(1) (2) (3) (4) (5)
Прогон 1 Прогон 2 Прогон 3 (1) Прогон 1 Прогон 2 Прогон 3 Прогон 1 Прогон 2 Прогон 3 Прогон 1 Прогон 2 Прогон 3
MS SQL 2000 (конфигурация 1)
Заполнение данными 321003 396870 504393 355280 459153 665026 н/д н/д н/д 90 : 100 86 : 100 76 : 100
Выборка эквисоединением 7123 9555 10352 10042 13147 14743 н/д н/д н/д 70 : 100 73 : 100 70 : 100
Поиск по ключу 1475 1921 1899 1298 1546 1414 н/д н/д н/д 100 : 88 100 : 80 100 : 75
MS SQL 2005 (конфигурация 2)
Заполнение данными 64080 86670 100860 70233 102923 125313 61623 96703 107343 91 : 100 : 88 84 : 100 : 94 80 : 100 : 86
Выборка эквисоединением 821 996 1445 2312 2860 4388 1053 1135 1274 36 : 100 : 46 35 : 100 : 40 33 : 100 : 29
Поиск по ключу 244 265 263 241 198 213 214 180 202 100 : 99 : 88 100 : 75 : 68 100 : 81 : 77
С некластерными индексами (конфигурация 2)
Заполнение данными 67313 90470 108280 75906 113436 124986 72483 98063 133280 89 : 100 : 95 80 : 100 : 86 81 : 94 : 100
Выборка эквисоединением 1397 1882 3554 2199 2744 6086 970 1169 1855 64 : 100 : 44 69 : 100 : 43 58 : 100 : 30
Поиск по ключу 244 293 274 197 237 239 191 199 152 100 : 81 : 78 100 : 81 : 68 100 : 87 : 55

Примечания:

  • для конфигурации с некластерными индексами доступная физическая память для SQL Server была увеличена до 256 Мб

Сергей Тарасов, июнь 2001. С дополнениями, февраль-апрель 2007

set nocount on
go

-- таблицы с целочисленными ключами
create table T1
(
    T1ID int not null,
    Num int not null,
    Title varchar(100) null
    constraint PK_T1 primary key (T1ID),
)
go
create index IX_T1 on T1(Num)
go

create table T11
(
    T11ID int not null,
    T1ID int not null,
    Title varchar(100) null,
    constraint PK_T11 primary key (T11ID),
    foreign key(T1ID) references T1(T1ID)
)
go
create index IX_T11 on T11(T1ID)
go

-- таблицы с символьными ключами
create table T2
(
    T2ID char(15) not null,
    Num int not null,
    Title varchar(100) null
    constraint PK_T2 primary key (T2ID),
)
go
create index IX_T2 on T2(Num)
go

create table T21
(
    T21ID char(15) not null,
    T2ID char(15) not null,
    Title varchar(100) null,
    constraint PK_T21 primary key (T21ID),
    foreign key(T2ID) references T2(T2ID)
)
go
create index IX_T21 on T21(T2ID)
go

-- служебные процедуры и функции
create procedure GetRandomString
    @Length int = 0,
    @Result varchar(255) output
as
begin
    if @Length > 255
        set @Length = 255

    declare @i int
    declare @ch char(1)

    set @i = 0
    set @Result = ''
    while (@i < @Length) begin
        set @ch = char(convert(int, (rand() * (128 - 33)) ) + 33)
        set @Result = @Result + @ch
        set @i = @i + 1
    end
end
go


create procedure GetCharKey
    @Key int,
    @Result char(15) output
as
begin
    declare @s varchar(15)
    declare @i int
    set @s = cast(@Key as varchar(15))
    set @i = 15 - len(@s)
    set @Result = @s + 'KEY' + replicate('0', @i - 3)
end
go

-- заполнение таблиц данными
declare @ParentCount    int     -- общее количество записей в главной таблице
declare @ChildCount int         -- количество дочерних записей для каждой родительской
declare @StagesCount int        -- оличество циклов повторения этапа выполнения запросов для получения усредненных результатов
declare @MaxNum int                 -- максимальное значение для диапазона значений поля Num (0, @MaxNum]
declare @i int, @j int, @k int, @Stage int
declare @Title varchar(100)
declare @ParentKey char(15), @ChildKey char(15)
declare @StartTime datetime
declare @Time1 int, @Time2 int

set @ParentCount = 10000
set @MaxNum = @ParentCount / 10
set @ChildCount = 30

-- таблицы с целочисленными ключами
set @i = 1
set @k = 1
select @StartTime = getdate()
while (@i <= @ParentCount) begin
-- вызов формирования строки из случайного набора символов закомментарен для быстроты прохождения этапа заливки данных
-- вместо этого используем строку, включающую ключи и фиксированный текст
--  exec GetRandomString 100, @Title output
    exec GetCharKey @i, @ParentKey output  -- для уравнивания количества требуемых операция при заливке
    set @Title = 'Title T1: ' + cast(@i as varchar)
    insert into T1 (T1ID, Num, Title) values (@i, cast((rand() * @MaxNum) as int)+ 1, @Title)
    set @j = 1
    while (@j <= @ChildCount) begin
--      exec GetRandomString 100, @Title output
        set @Title = 'Title T11: ' + cast(@k as varchar) + ' ' + cast(@i as varchar)
        exec GetCharKey @k, @ChildKey output
        insert into T11 (T11ID, T1ID, Title) values (@k, @i, @Title)
        set @j = @j + 1
        set @k = @k + 1
    end
    set @i = @i + 1
end
print N'Закачка данных (таблицы с целочисленными ключами) завершена за ' + cast(datediff(ms, @StartTime, getdate()) as varchar) + N' мсек'


-- таблицы с символьными ключами
set @i = 1
set @k = 1
select @StartTime = getdate()
while (@i <= @ParentCount) begin
--  exec GetRandomString 100, @Title output
    set @Title = 'Title T2: ' + cast(@i as varchar)
    exec GetCharKey @i, @ParentKey output
    insert into T2 (T2ID, Num, Title) values (@ParentKey, cast((rand() * @MaxNum) as int)+ 1, @Title)
    set @j = 1
    while (@j <= @ChildCount) begin
--      exec GetRandomString 100, @Title output
        set @Title = 'Title T21: ' + cast(@k as varchar) + ' ' + cast(@i as varchar)
        exec GetCharKey @k, @ChildKey output
        insert into T21 (T21ID, T2ID, Title) values (@ChildKey, @ParentKey, @Title)
        set @j = @j + 1
        set @k = @k + 1
    end
    set @i = @i + 1
end
print N'Закачка данных (таблицы с символьными ключами) завершена за ' + cast(datediff(ms, @StartTime, getdate()) as varchar) + N' мсек'


set @StagesCount = 10
set @Stage = 1
set @Time1 = 0
set @Time2 = 0
while (@Stage <= @StagesCount) begin
    -- выполнение запросов со связью по ключу
    set @i = 1
    set @j = 1
    select @StartTime = getdate()
    while (@i <= @ParentCount) begin
        set @k = cast((@MaxNum * rand()) as int) + 1
        select @j = count(*)
            from T1 join T11 on T1.T1ID = T11.T1ID
            where T1.Num = @k
        set @i = @i + 1
    end
    set @Time1 = @Time1 + datediff(ms, @StartTime, getdate())

    set @i = 1
    select @StartTime = getdate()
    while (@i <= @ParentCount) begin
        set @k = cast((@MaxNum * rand()) as int) + 1
        select @j = count(*)
            from T2 join T21 on T2.T2ID = T21.T2ID
            where T2.Num = @k
        set @i = @i + 1
    end
    set @Time2 = @Time2 + datediff(ms, @StartTime, getdate())
    set @Stage = @Stage + 1
end

print N'Проведено циклов: ' + cast(@StagesCount as varchar)
print N'Запросы выполнены (таблицы с целочисленными ключами) за ' + cast((@Time1/@StagesCount) as varchar) + N' мсек'
print N'Запросы выполнены (таблицы с символьными ключами) за ' + cast((@Time2/@StagesCount) as varchar) + N' мсек'

set @Stage = 1
set @Time1 = 0
set @Time2 = 0
while (@Stage <= @StagesCount) begin
    -- выполнение запросов со связью по ключу
    set @i = 1
    set @j = 1
    select @StartTime = getdate()
    while (@i <= @ParentCount) begin
        set @k = cast((@ChildCount * rand()) as int) + 1
        exec GetCharKey @k, @ParentKey output
        select @j = count(*)
            from T11
            where T11.T1ID = @k
        set @i = @i + 1
    end
    set @Time1 = @Time1 + datediff(ms, @StartTime, getdate())

    set @i = 1
    select @StartTime = getdate()
    while (@i <= @ParentCount) begin
        set @k = cast((@ChildCount * rand()) as int) + 1
        exec GetCharKey @k, @ParentKey output
        select @j = count(*)
            from T21
            where T21.T21ID = @ParentKey
        set @i = @i + 1
    end
    set @Time2 = @Time2 + datediff(ms, @StartTime, getdate())
    set @Stage = @Stage + 1
end

print N'Проведено циклов: ' + cast(@StagesCount as varchar)
print N'Запросы выполнены (таблицы с целочисленными ключами) за ' + cast((@Time1/@StagesCount) as varchar) + N' мсек'
print N'Запросы выполнены (таблицы с символьными ключами) за ' + cast((@Time2/@StagesCount) as varchar) + N' мсек'
go

-- очистка БД после теста
drop table T11
drop table T1
drop table T21
drop table T2

drop procedure GetRandomString
drop procedure GetCharKey
go

set nocount off
go

blog comments powered by Disqus