Целочисленные, символьные и GUID-ключи в MS SQL
Сравнение производительности некоторых 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