我们的日历应用程序将约会域表示为:
预约
AppointmentRole
约会与AppointmentRoles有1对多的关系。每个AppointmentRole代表一个特定角色的人或团体(例如,下车,接送,参加......)。
这种关系有两个目的:
还有第三个表来跟踪与约会相关的注释/评论。这与预约的一对多关系有很多方面:
AppointmentNote
要显示约会日历,我们目前使用的是......
List<IAppointment> GetAppointments(IAccess acl, DateTime start, DateTime end, ...
{
// Retrieve distinct appointments that are visible to the acl
var visible = (from appt in dc.Appointments
where !(appt.StartDateTime >= end || appt.EndDateTime <= start)
join role in
(from r in dc.Roles
where acl.ToIds().Contains(r.PersonOrGroupID)
select new { r.AppointmentID })
on appt.ID equals role.AppointmentID
select new
{
...
}).Distinct();
...
可见 Linq表达式选择给定访问控制列表可以看到的不同约会。
下面,我们将可见加入/加入角色和备注,以便选择参与约会的所有人员和群组预约说明。
...
// Join/into to get all appointment roles and notes
var q = from appt in visible
orderby appt.StartDateTime, ...
join r in dc.Roles
on appt.ID equals r.AppointmentID
into roles
join note in dc.AppointmentNotes
on appt.ID equals note.AppointmentID
into notes
select new { Appointment = appt, Roles = roles, Notes = notes };
最后,我们枚举查询,希望Linq-To-Sql将生成一个非常优化的查询(没有如下所述的运气)......
// Marshal the anonymous type into an IAppointment
// IAppointment has a Roles and Notes collection
var result = new List<IAppointment>();
foreach (var record in q)
{
IAppointment a = new Appointment();
a.StartDateTime = record.StartDateTime;
...
a.Roles = Marshal(record.Roles);
a.Notes = Marshal(record.Notes);
result.Add(a);
}
Linq-to-Sql生成的查询非常繁琐。它生成单个查询以确定可见约会。但随后它会在每次迭代时生成三个查询:一个用于获取约会字段,另一个用于获取角色,第三个用于获取注释。 where子句始终是可见的约会ID。
因此,我们正在重构GetAppointments,并认为我们可以从SO社区的专业知识中受益。
我们希望将所有内容都移到T-SQL存储过程中,这样我们就可以获得更多控制权。你能否分享一下如何解决这个问题?对数据模型,T-SQL和Linq-to-SQL修改的更改都是公平的游戏。我们还想了解索引方面的建议。我们正在使用MS-SqlServer 2008和.NET 4.0。
答案 0 :(得分:3)
我想说所有邪恶的根源都从这里开始:
where acl.ToIds().Contains(r.PersonOrGroupID)
acl.ToIds().Contains(...)
是一个无法在服务器端解析的表达式,因此必须在客户端解析visible
查询(非常有效),更糟糕的是,结果必须是保留客户端,然后,当迭代时,必须将不同的查询发送到服务器以进行每个可见的约会(约会字段,角色和注释)。如果我按照自己的方式处理,我会创建一个存储过程,将ACL列表作为Table Valued Parameter接受,并在服务器端进行所有加入/过滤。
我从这个架构开始:
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 和日期)。也许是一个索引视图?
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
我们还可以将Appointments上的聚集索引更改为可能更有用的AppointmentID:
drop index cdxAppointmentStart on Appointments;
create clustered index cdxAppointmentAppointmentID on Appointments (AppointmentID);
go
这将在77毫秒内(在热缓存上)返回相同日期范围的同一@acl列表中的约会。
现在,当然,您应该使用的实际架构取决于更多未考虑的因素。但是我希望这能给你一些关于现在采取适当行动来获得体面表现的想法。将表值参数添加到客户端执行上下文并将其传递给过程以及LINQ集成,这仍然是读者的练习。
答案 1 :(得分:2)
如果我理解正确,Appointment
的{{1}}集合和Roles
集合。如果是这种情况(并且您在设计器中对此进行了正确建模),则Notes
类中包含Roles
和Notes
个属性。当您更改Appointment
查询的投影(select
)时,请选择q
本身,您可以帮助LINQ to SQL为您获取以下集合。在这种情况下,您应该按如下方式编写查询:
Appointment
在此之后,您可以使用var q =
from appt in visible
...
select appt;
的{{1}}属性为您预取子集合,如下所示:
LoadOptions
然而,一个问题是我认为DataContext
仅限于加载单个子集合,而不是两个。
您可以通过在两个查询中写出来解决此问题。第一个查询是您获取约会并使用using (var db = new AppointmentContext())
{
db.LoadOptions.LoadWith<Appointment>(a => a.Roles);
// Do the rest here
}
来获取所有LoadWith
。然后使用第二个查询(在新的LoadWith
中)并使用Roles
获取所有DataContext
。)
答案 2 :(得分:1)
where !(appt.StartDateTime >= end || appt.EndDateTime <= start)
这可能是一个非常好的AND标准。
where appt.StartDateTime < end && start < appt.EndDateTime
acl.ToIds().
将其从查询中拉出来,要求数据库执行操作没有任何意义。
List<int> POGIDs = acl.ToIds();
join role in
您希望将角色用作过滤器。如果你在哪里,而不是加入,你不必在以后区别。
尝试使用和不使用DataLoadOptions。如果没有DataLoadOptions的查询是好的,那么还有另一种(更多手动)方式来加载相关的行。
DataLoadOptions myOptions = new DataLoadOptions();
myOptions.LoadWith<Appointment>(appt => appt.Roles);
myOptions.LoadWith<Appointment>(appt => appt.Notes);
dc.LoadOptions = myOptions;
List<int> POGIDs = acl.ToIds();
IQueryable<Roles> roleQuery = dc.Roles
.Where(r => POGIDs.Contains(r.PersonOrGroupId));
IQueryable<Appointment> visible =
dc.Appointments
.Where(appt => appt.StartDateTime < end && start < appt.EndDateTime)
.Where(appt => appt.Roles.Any(r => roleQuery.Contains(r));
IQueryable<Appointment> q =
visible.OrderBy(appt => appt.StartDateTime);
List<Appointment> rows = q.ToList();
这是获取相关数据的“更多手动”方式。注意:当apptIds或POGID包含超过2100个整数时,此技术会中断。还有办法解决这个问题......
List<int> POGIDs = acl.ToIds();
List<Role> visibleRoles = dc.Roles
.Where(r => POGIDs.Contains(r.PersonOrGroupId)
.ToList()
List<int> apptIds = visibleRoles.Select(r => r.AppointmentId).ToList();
List<Appointment> appointments = dc.Appointments
.Where(appt => appt.StartDateTime < end && start < appt.EndDate)
.Where(appt => apptIds.Contains(appt.Id))
.OrderBy(appt => appt.StartDateTime)
.ToList();
ILookup<int, Roles> appointmentRoles = dc.Roles
.Where(r => apptIds.Contains(r.AppointmentId))
.ToLookup(r => r.AppointmentId);
ILookup<int, Notes> appointmentNotes = dc.AppointmentNotes
.Where(n => apptIds.Contains(n.AppointmentId));
.ToLookup(n => n.AppointmentId);
foreach(Appointment record in appointments)
{
int key = record.AppointmentId;
List<Roles> theRoles = appointmentRoles[key].ToList();
List<Notes> theNotes = appointmentNotes[key].ToList();
}
此样式突出显示需要索引的位置:
Roles.PersonOrGroupId
Appointments.AppointmentId (should be PK already)
Roles.AppointmentId
Notes.AppointmentId