Данная статья является заключительной частью цикла работ, посвященных изучению понятия индекса графа. Для ее понимания требуются знания из двух предыдущих работ (Индекс графа. Часть 1. Логическая архитектура (гипердеревья, фундаментальная группа)., Индекс графа. Часть 2. Физическая архитектура.).
Протестируем алгоритм поиска маршрутов на больших объемах. Создадим такое же дерево как и при тестировании традиционного способа поиска маршрутов. То есть построим тернарное дерево, состоящее из 18 уровней. Для решения задачи выполним такой скрипт:
Код работал около 12 часов, объем данных в таблице составил 52 Гб. Запустим такой запрос:
Код работает 40 миллисекунд, возвращая маршрут из 37 вершин! Проверим теперь как работает процедура в ситуации, когда есть несколько соединений, выполняющие поиск маршрутов в большом графе. Запустим следующий код одновременно в 10 соединениях:
Протестируем алгоритм поиска маршрутов на больших объемах. Создадим такое же дерево как и при тестировании традиционного способа поиска маршрутов. То есть построим тернарное дерево, состоящее из 18 уровней. Для решения задачи выполним такой скрипт:
declare @maxLevel int = 18, @curLevel int = 1, @num int
while @curLevel <= @maxLevel
begin
if @curLevel = 0
begin
insert into dbo.HyperTrees ( LevelId, VerParId, IsRoot, VerRootId, EdgeId, VerStartEdgeId, VerEndEdgeId )
values ( 1, null, 1, null, null, null, null )
end
set @num = power ( 3, @curLevel )
insert into dbo.HyperTrees with ( tablock ) ( LevelId, VerParId, IsRoot, VerRootId, EdgeId, VerStartEdgeId, VerEndEdgeId )
select 1,
(
( data.num + ( @num - 3 ) / 2 ) - 1
) / 3,
0, null, null, null, null
from
(
select top ( power ( 3, @curLevel ) ) row_number () over ( order by ( select 1 ) ) as num
from
master.dbo.spt_values val1
cross join
master.dbo.spt_values val2
cross join
master.dbo.spt_values val3
) data
set @curLevel += 1
endКод работал около 12 часов, объем данных в таблице составил 52 Гб. Запустим такой запрос:
declare @dt1 datetime2 = sysdatetime ()
exec dbo.FindPath 200000000, 400000000
declare @dt2 datetime2 = sysdatetime ()
select datediff ( [ms], @dt1, @dt2 )Код работает 40 миллисекунд, возвращая маршрут из 37 вершин! Проверим теперь как работает процедура в ситуации, когда есть несколько соединений, выполняющие поиск маршрутов в большом графе. Запустим следующий код одновременно в 10 соединениях:
waitfor time '21:40:00'
declare @dt1 datetime2 = sysdatetime ()
exec dbo.FindPath 200000000, 400000000
declare @dt2 datetime2 = sysdatetime ()
select datediff ( [ms], @dt1, @dt2 )
Время работы кода в миллисекундах во всех этих соединениях:
Проверим удаление ребер. Попробуем удалить ребро, которое находится близко к листовому уровню дерева:
- 1) 561; 2) 550; 3) 600; 4) 86; 5) 415; 6) 451; 7) 559; 8) 430; 9) 402; 10) 584.
Проверим удаление ребер. Попробуем удалить ребро, которое находится близко к листовому уровню дерева:
begin tran
declare @dt1 datetime2 = sysdatetime ()
exec dbo.DeleteEdge 400000000, 134666666
declare @dt2 datetime2 = sysdatetime ()
select datediff ( [ms], @dt1, @dt2 )
rollback tran
Такой код работает на горячем кэше всего 6 миллисекунд ()на холодном кэше время выполнения 0.9 секунды. Если предпринять попытку удаления ребра на уровне, близком к корню, то время выполнения может увеличиться, так как процедуре потребуется проверять наличие ребер соединяющих вершины их двух больших поддеревьев. Чтобы этого избежать, можно в процедуре удаления ребра по другому искать внешнее ребро, соединяющее два дерева. Вместо соединения вершин на таблицу с внешними ребрами, можно найти корни для концов внешних ребер и сравнить их с корнями тех деревьев, для которых мы ищем ребро.
Посмотрим как можно построить индекс графа. Допустим имеется таблица dbo.Edges, которая рассматривалась в первой части. Требуется наполнить таблицы dbo.HyperTrees, dbo.ExtEdges, dbo.EdgesRel данными, которые бы составляли индекс графа, который хранится в dbo.Edges. Мы напишем хранимую процедуру, которая построит индекс графа. Идея алгоритма процедуры состоит в том, чтобы организовать цикл по компонентам связности графа. На каждой итерации необходимо выбрать произвольную вершину и рассматривать ее как корень. Построить дерево и добавить в таблицу внешних ребер все ребра, не входящие в построенное дерево. Данная процедура является одновременно и процедурой создания и процедурой перестройки индекса графа.
Процедура реализует описанный выше алгоритм. При вставке данных в промежуточную таблицу необходимо делать группировку по добавляемым вершинам, поскольку несколько маршрутов из выбранного корня могут вести к одной вершине. При наполнении временной таблицы #HyperTrees_Tmp делается merge для того, чтобы в предложении output была возможность запомнить и новое идентификационное значение и идентификатор вершины из таблицы dbo.Edges. Подробнее об это можно прочитать в статье этого блога: Интересное применение оператора merge и функции output.
В самом начале процедуры включается 610-ый флаг трассировки, который появился в MS SQL 2008. Он задает минимальное протоколирование при вставке. То есть при аллокации новой страницы, в журнале транзакций записывается только ее параметры, но не сами строки. Поскольку в конце делается вставка в таблицу dbo.HyperTrees, которая предварительно очищается, и модель восстановления для базы данных Graphs является простой, то включение этого флага значительно повысит производительность на больших объемах.
Последняя вставка добавляет в таблицу dbo.ExtEdges внешние ребра, которые не входят в связное поддерево. Как я упоминал в первой части, вместе с таблицей dbo.Edges можно хранить таблицу для изолированных вершин. Тогда в процедуре необходимо IndexGraphsRebuild необходимо в конце делать дополнительную вставку этих вершин в dbo.HyperTress. Поле VerParId инициализировалось бы для них пустым значением.
При работе данной процедуры, необходимо чтобы никто не добавлял и не удалял вершин и ребер, то есть чтобы не было обновлений в таблице dbo.Edges.
Посмотрим как можно построить индекс графа. Допустим имеется таблица dbo.Edges, которая рассматривалась в первой части. Требуется наполнить таблицы dbo.HyperTrees, dbo.ExtEdges, dbo.EdgesRel данными, которые бы составляли индекс графа, который хранится в dbo.Edges. Мы напишем хранимую процедуру, которая построит индекс графа. Идея алгоритма процедуры состоит в том, чтобы организовать цикл по компонентам связности графа. На каждой итерации необходимо выбрать произвольную вершину и рассматривать ее как корень. Построить дерево и добавить в таблицу внешних ребер все ребра, не входящие в построенное дерево. Данная процедура является одновременно и процедурой создания и процедурой перестройки индекса графа.
if
object_id ( N'dbo.IndexGraphsRebuild', N'P' ) is null
exec ( N'create proc
dbo.IndexGraphsRebuild as return' )
go
alter
proc dbo.IndexGraphsRebuild
as
begin
set nocount, xact_abort on
declare @VerId int, @LevCount int, @Component int = 0, @LevelTree int, @ErrMsg varchar ( max ), @NewVerId int
set identity_insert Graphs.dbo.HyperTrees on
begin try
dbcc
traceon ( 610 )
if objecT_id ( N'tempdb..#EdgesRel', N'U' ) is not null
drop
table #EdgesRel
create
table #EdgesRel
(
LevelTree int not null,
Component int not null,
VerIniId int not null,
VerId int not null,
primary
key clustered ( Component asc, LevelTree asc, VerIniId asc, VerId asc ) on [PRIMARY]
) on [PRIMARY]
if object_id ( N'tempdb..#HyperTrees_Tmp', N'U' ) is not null
drop
table #HyperTrees_Tmp
create
table #HyperTrees_Tmp
(
LevelTree int not null,
Component int not null,
LevelId int not null,
VerId int
identity ( 1, 1 ) not null,
VerParId int not null,
IsRoot int not null,
constraint
PK_Tmp primary key clustered ( Component asc, LevelTree asc, VerId asc ) on [PRIMARY],
constraint
AK_Tmp_Component unique
nonclustered ( Component asc, VerId asc ) on [PRIMARY],
constraint
AK_Tmp_VerId unique
nonclustered ( VerId, VerParId asc ) on [PRIMARY],
constraint
AK_Tmp_VerParId unique
nonclustered ( VerParId, VerId asc ) on [PRIMARY]
) on [PRIMARY]
select
top 1 @VerId = StartId
from
dbo.Edges
while
@VerId is not null
begin
set
@VerId = (
select top 1 edge.StartId
from dbo.Edges edge
where not exists
(
select VerId
from #HyperTrees_Tmp tr
where tr.VerId = edge.StartId
)
)
if
@VerId is null
begin
break
end
else
begin
set @LevelTree = 1
set @Component += 1
insert into #HyperTrees_Tmp
(
LevelTree,
Component,
LevelId,
VerParId,
IsRoot
)
values
(
@LevelTree,
@Component,
1,
null,
1
)
set @NewVerId = scope_identity ()
insert into #EdgesRel
(
LevelTree,
Component,
VerIniId,
VerId
)
values
(
@LevelTree,
@Component,
@NewVerId,
@VerId
)
end
set
@LevCount = 1
while
@LevCount <>
0
begin
set @LevelTree += 1
merge #HyperTrees_Tmp
with ( tablock ) as trg
using
(
select @LevelTree LevelTree, @Component Component, 1 LevelId, max ( tr.VerId ) VerParId, 0 IsRoot, edge.EndId
from
#HyperTrees_Tmp tr
inner join
#EdgesRel rel on
tr.Component = rel.Component and
tr.LevelTree = rel.LevelTree and
tr.VerId = rel.VerIniId
inner join
dbo.Edges edge on rel.VerId = edge.StartId
where
tr.LevelTree = @LevelTree and
tr.Component = @Component and
not exists
(
select *
from #HyperTrees_Tmp trIn
where
trIn.Component = @Component and
trIn.VerId = edge.EndId
)
group by edge.EndId
) as src on 1 = 0
when not matched then insert
(
LevelTree,
Component,
LevelId,
VerParId,
IsRoot
)
values
(
src.LevelTree,
src.Component,
src.LevelId,
src.VerParId,
src.IsRoot
)
output
src.LevelTree,
src.Component,
src.EndId,
inserted.VerId
into #EdgesRel
(
LevelTree,
Component,
VerIniId,
VerId
)
;
set @LevCount = @@rowcount
merge #HyperTrees_Tmp
with ( tablock ) as trg
using
(
select @LevelTree LevelTree, @Component Component, 1 LevelId, max ( tr.VerId ) VerParId, 0 IsRoot, edge.StartId
from
#HyperTrees_Tmp tr
inner join
#EdgesRel rel on
tr.Component = rel.Component and
tr.LevelTree = rel.LevelTree and
tr.VerId = rel.VerIniId
inner join
dbo.Edges edge on rel.VerId = edge.EndId
where
tr.LevelTree = @LevelTree and
tr.Component = @Component and
not exists
(
select *
from #HyperTrees_Tmp trIn
where
trIn.Component = @Component and
trIn.VerId = edge.StartId
)
group by edge.EndId
) as src on 1 = 0
when not matched then insert
(
LevelTree,
Component,
LevelId,
VerParId,
IsRoot
)
values
(
src.LevelTree,
src.Component,
src.LevelId,
src.VerParId,
src.IsRoot
)
output
src.LevelTree,
src.Component,
src.StartId,
inserted.VerId
into #EdgesRel
(
LevelTree,
Component,
VerIniId,
VerId
)
;
set @LevCount += @@rowcount
end
end
begin tran
truncate table Graphs.dbo.HyperTrees
truncate table Graphs.dbo.ExtEdge
truncate table Graphs.dbo.EdgesRel
insert into Graphs.dbo.EdgesRel with ( tablock )
(
VerIniId,
VerId
)
select
VerIniId,
VerId
from #EdgesRel
insert into Graphs.dbo.HyperTrees with ( tablock )
(
LevelId,
VerId,
VerParId,
IsRoot
)
select
LevelId,
VerId,
VerParId,
IsRoot
from
#HyperTrees_Tmp
insert
into Graphs.dbo.ExtEdges with ( tablock ) ( StartId, EndId )
select edge.StartId, edge.EndId
from
dbo.Edges edge
inner join
Graphs.dbo.EdgesRel relst on edge.StartId = relst.VerIniId
inner join
Graphs.dbo.EdgesRel relen on edge.EndId = relen.VerIniId
left outer join
Graphs.dbo.HyperTrees st on
relst.VerId = st.VerId and
relen.VerId = st.VerParId
left outer join
Graphs.dbo.HyperTrees en on
relst.VerId = en.VerParId and
relst.VerId = en.VerId
where st.VerId is null and en.VerId is null
commit
tran
end try
begin catch
set
@ErrMsg = 'Ошибка. Строка:
' + isnull ( cast ( error_line () as varchar ( 100 ) ), 'неизвестно' ) +
'. Номер: ' + isnull ( cast ( error_number () as varchar ( 100 ) ), 'неизвестно' ) +
'. Описание: ' + isnull ( error_message (), 'неизвестно' )
end catch
set identity_insert Graphs.dbo.HyperTrees off
if isnull ( @ErrMsg, '' ) <> ''
begin
; throw 50000, @ErrMsg, 1
end
end
goПроцедура реализует описанный выше алгоритм. При вставке данных в промежуточную таблицу необходимо делать группировку по добавляемым вершинам, поскольку несколько маршрутов из выбранного корня могут вести к одной вершине. При наполнении временной таблицы #HyperTrees_Tmp делается merge для того, чтобы в предложении output была возможность запомнить и новое идентификационное значение и идентификатор вершины из таблицы dbo.Edges. Подробнее об это можно прочитать в статье этого блога: Интересное применение оператора merge и функции output.
В самом начале процедуры включается 610-ый флаг трассировки, который появился в MS SQL 2008. Он задает минимальное протоколирование при вставке. То есть при аллокации новой страницы, в журнале транзакций записывается только ее параметры, но не сами строки. Поскольку в конце делается вставка в таблицу dbo.HyperTrees, которая предварительно очищается, и модель восстановления для базы данных Graphs является простой, то включение этого флага значительно повысит производительность на больших объемах.
Последняя вставка добавляет в таблицу dbo.ExtEdges внешние ребра, которые не входят в связное поддерево. Как я упоминал в первой части, вместе с таблицей dbo.Edges можно хранить таблицу для изолированных вершин. Тогда в процедуре необходимо IndexGraphsRebuild необходимо в конце делать дополнительную вставку этих вершин в dbo.HyperTress. Поле VerParId инициализировалось бы для них пустым значением.
При работе данной процедуры, необходимо чтобы никто не добавлял и не удалял вершин и ребер, то есть чтобы не было обновлений в таблице dbo.Edges.
Комментариев нет:
Отправить комментарий