优化日历应用的查询和/或数据模型

时间:2010-08-01 19:14:48

标签: c# sql sql-server linq-to-sql database-schema

我们的日历应用程序将约会域表示为:

预约

      
  • ID(PK)
  •   
  • 的startDateTime
  •   
  • EndDateTime
  •   
  • ...

AppointmentRole

      
  • 约会ID(FK)
  •   
  • PersonOrGroupID(FK)/ *加入此人/群组,超出此问题的范围* /
  •   
  • 作用
  •   
  • ...

约会与AppointmentRoles有1对多的关系。每个AppointmentRole代表一个特定角色的人或团体(例如,下车,接送,参加......)。

这种关系有两个目的:

  1. 它定义了一个访问控制列表 - 经过身份验证的主体只能在其访问控制列表与关联的人员或组匹配时查看约会
  2. 它记录了谁参加了这个任命以及扮演什么角色。
  3. 还有第三个表来跟踪与约会相关的注释/评论。这与预约的一对多关系有很多方面:

    AppointmentNote

        
    • 约会ID(FK)
    •   
    • ...

    要显示约会日历,我们目前使用的是......

    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。

3 个答案:

答案 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类中包含RolesNotes个属性。当您更改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