Я бы сказал, корень всех зол начинается здесь:
where acl.ToIds().Contains(r.PersonOrGroupID)
acl.ToIds().Contains(...)
- это выражение, которое не может быть разрешено на стороне сервера, поэтому запрос visible
должен быть разрешен (очень неэффективно) на стороне клиента, и, что еще хуже, результат должен быть возвращен клиенту и затем, когда он повторяется, необходимо отправлять на сервер отдельные запросы для каждого видимого назначения (поля назначений, роли и заметки). Если бы у меня все было по-другому, я бы создал хранимую процедуру, которая принимает список ACL как Табличный параметр и выполняет все соединения / фильтрацию на стороне сервера.
Я начну с этой схемы:
create table Appointments (
AppointmentID int not null identity(1,1),
Start DateTime not null,
[End] DateTime not null,
Location varchar(100),
constraint PKAppointments
primary key nonclustered (AppointmentID));
create table AppointmentRoles (
AppointmentID int not null,
PersonOrGroupID int not null,
Role int not null,
constraint PKAppointmentRoles
primary key (PersonOrGroupID, AppointmentID),
constraint FKAppointmentRolesAppointmentID
foreign key (AppointmentID)
references Appointments(AppointmentID));
create table AppointmentNotes (
AppointmentID int not null,
NoteId int not null,
Note varchar(max),
constraint PKAppointmentNotes
primary key (AppointmentID, NoteId),
constraint FKAppointmentNotesAppointmentID
foreign key (AppointmentID)
references Appointments(AppointmentID));
go
create clustered index cdxAppointmentStart on Appointments (Start, [End]);
go
И получить назначения для произвольного ACL следующим образом:
create type AccessControlList as table
(PersonOrGroupID int not null);
go
create procedure usp_getAppointmentsForACL
@acl AccessControlList readonly,
@start datetime,
@end datetime
as
begin
set nocount on;
select a.AppointmentID
, a.Location
, r.Role
, n.NoteID
, n.Note
from @acl l
join AppointmentRoles r on l.PersonOrGroupID = r.PersonOrGroupID
join Appointments a on r.AppointmentID = a.AppointmentID
join AppointmentNotes n on n.AppointmentID = a.AppointMentID
where a.Start >= @start
and a.[End] <= @end;
end
go
Давайте попробуем это на 1M встречах. Сначала заполните таблицы (это займет около 4-5 минут):
set nocount on;
declare @i int = 0;
begin transaction;
while @i < 1000000
begin
declare @start datetime, @end datetime;
set @start = dateadd(hour, rand()*10000-5000, getdate());
set @end = dateadd(hour, rand()*100, @start)
insert into Appointments (Start, [End], Location)
values (@start, @end, replicate('X', rand()*100));
declare @appointmentID int = scope_identity();
declare @atendees int = rand() * 10.00 + 1.00;
while @atendees > 0
begin
insert into AppointmentRoles (AppointmentID, PersonOrGroupID, Role)
values (@appointmentID, @atendees*100 + rand()*100, rand()*10);
set @atendees -= 1;
end
declare @notes int = rand()*3.00;
while @notes > 0
begin
insert into AppointmentNotes (AppointmentID, NoteID, Note)
values (@appointmentID, @notes, replicate ('Y', rand()*1000));
set @notes -= 1;
end
set @i += 1;
if @i % 10000 = 0
begin
commit;
raiserror (N'Added %i appointments...', 0, 1, @i);
begin transaction;
end
end
commit;
go
Итак, давайте посмотрим сегодня назначения на несколько человек:
set statistics time on;
set statistics io on;
declare @acl AccessControlList;
insert into @acl (PersonOrGroupID) values (102),(111),(131);
exec usp_getAppointmentsForACL @acl, '20100730', '20100731';
Table 'AppointmentNotes'. Scan count 8, logical reads 39, 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.
Table 'Appointments'. Scan count 1, logical reads 9829, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'AppointmentRoles'. Scan count 3, logical reads 96, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table '#25869641'. Scan count 1, logical reads 1, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 63 ms, elapsed time = 1294 ms.
SQL Server Execution Times:
CPU time = 63 ms, elapsed time = 1294 ms.
1,2 секунды (в холодном кеше, в теплом кеше - 224 мс). Хм, это не очень хорошо. Проблема в том, что 9829 страниц попали в таблицу встреч. Чтобы улучшить это, мы хотели бы иметь оба критерия фильтрации (acl и date) одновременно. Возможно индексированное представление?
create view vwAppointmentAndRoles
with schemabinding
as
select r.PersonOrGroupID, a.AppointmentID, a.Start, a.[End]
from dbo.AppointmentRoles r
join dbo.Appointments a on r.AppointmentID = a.AppointmentID;
go
create unique clustered index cdxVwAppointmentAndRoles on vwAppointmentAndRoles (PersonOrGroupID, Start, [End]);
go
alter procedure usp_getAppointmentsForACL
@acl AccessControlList readonly,
@start datetime,
@end datetime
as
begin
set nocount on;
select ar.AppointmentID
, a.Location
, r.Role
, n.NoteID
, n.Note
from @acl l
join vwAppointmentAndRoles ar with (noexpand) on l.PersonOrGroupID = ar.PersonOrGroupID
join AppointmentNotes n on n.AppointmentID = ar.AppointMentID
join Appointments a on ar.AppointmentID = a.AppointmentID
join AppointmentRoles r
on ar.AppointmentID = r.AppointmentID
and ar.PersonOrGroupID = r.PersonOrGroupID
where ar.Start >= @start
and ar.Start <= @end
and ar.[End] <= @end;
end
go
Мы также можем изменить кластеризованный индекс на Назначениях на более полезный AppointmentID:
drop index cdxAppointmentStart on Appointments;
create clustered index cdxAppointmentAppointmentID on Appointments (AppointmentID);
go
Возвращает встречи в том же списке @acl для того же диапазона дат в 77 мс (в теплом кэше).
Теперь, конечно, фактическая схема, которую вы должны использовать, зависит от гораздо большего числа факторов, которые не принимаются во внимание. Но я надеюсь, что это дало вам представление о правильных действиях, которые необходимо предпринять сейчас, чтобы добиться достойных результатов. Добавление табличного параметра в контекст выполнения клиента и его передачу в процедуру, а также интеграция с LINQ оставлены читателю в качестве упражнения.