我开始重写现有的应用程序。当前性能瓶颈之一是将用户生成的Employees列表保存到静态列表中,以便他们可以在以后返回并查看相同的员工列表。
以下是我正在寻找的功能性的简单示例。生成的列表将通过比示例中更复杂的查询。
场景: 用户在夜班时搜索所有员工,并希望保存此列表以便稍后加载。他们希望列表始终返回结果,因为这是他们第一次运行搜索。即如果新员工被添加到夜班,他们在上拉时不应出现在列表中。
我尝试过:
目前,将结果列表中的所有ID存储为字符串数组,然后使用这些ID重建查询的解决方案非常糟糕。这是非常低效的并且导致具有太多参数的大列表的问题。
我还玩过从已保存的ID数组中构建表然后加入,但是,这在大型列表(20,000多名员工)上非常慢,并且经常导致超时。
我目前的想法是为每个列表创建一个新表,然后在该表和Employee表上调用JOIN。但是,如果100个用户各自保存10个大型列表(20,000多名员工),它很快就会成为存储和表管理的噩梦。
我认为这是一个解决方案相当普遍的问题。但是,我还没有找到任何关于如何存储静态列表的示例或最佳实践(我可能正在寻找错误的东西)。有没有人对如何最好地处理这种用例场景有任何一般概念?
更新 我想我以前尝试过以下设置,但是由于某种原因它不起作用,这是几年前的事了;但回顾它似乎最有意义。我认为我遇到的问题是NHibernate在Linq中遇到子选择问题。但是,这不再是一种限制。
我想我有一个StaticSavedLists
的表和索引表链接Person(前一个例子中的Employee),它将List链接到Employee中的多对多映射。
c#中的类看起来像这样:
public class StaticSavedList : BaseModel
{
public string Name { get; set; }
public IList<StaticSavedListPersonIdx> PersonsIdx { get; set; } //Has many persons
}
public class StaticSavedListPersonIdx : BaseModel
{
public StaticSavedList StaticSavedList { get; set; }
public Person Person { get; set; }
}
答案 0 :(得分:2)
您可能需要有1个表格,其中包含搜索的“标题”详细信息,其中包含搜索的ID,然后是包含每个搜索条目的第二个表。然后你只需要以某种方式获取ID(可能是通过用户ID和转移日期)并使用它来加入结果和员工表。
以下是架构的外观
ShiftSearches
SearchID int (PK)
ShiftDate datetime
SearchResult所
SearchID int (PK)
EmployeeID int (PK)
员工
EmployeeID int (PK)
FirstName varchar
etc ...
可能的LINQ查询
DateTime shiftDate = new DateTime(2014,11,26);
int searchId = db.ShiftSearches.Single(s => s.ShiftDate == shiftDate).SearchID;
var results = from r in db.SearchResults where r.SearchID == searchId
join e in Employees on r.EmployeeID equals e.EmployeeID
select e;
这样你只需要一张桌子,它非常薄,所以不应占用太多空间 - 因为你只是存储了所需的搜索和员工ID,你无论如何都无法真正获得更小的数据。
您发布的类结构几乎与此概念相符。
答案 1 :(得分:1)
在所有情况下,实体框架可能不是适当的技术选择,并且一次处理20k行的批次可能是这些情况之一。
但是,我相信您的数据模型设计是一个很好的设计。下面,在Sql Only上,可以看出包含(ListId, EmployeeId)
对的60k行可以在一秒钟内插入到一个明智的聚类ListSearchEmployee
表中,随后,一个20k行的列表可以是从冷启动开始,在1.8秒内连接回Employee
行。
性能瓶颈更可能是原始用户搜索 - 可能这可能是针对您的Employees +相关表执行的几乎任意查询,这对于所有排列都很难索引。
一些性能建议(对于List save + refetch):
SqlBulkCopy
将EmployeeId批量转储到List表SqlReader
来获取用户界面的列表数据(虽然我猜它会被分页? - 如果是这样,DbSet.SqlQuery
AsNoTracking()
关闭就足够了。ListSearchEmployee
表经常需要重新索引,因为它有大量的流失。-- Sample data setup - not considered in the timing
CREATE TABLE Employee
(
EmployeeID INT identity(1,1),
Name NVARCHAR(100),
SomeOtherFieldToLessenTheDensityOfEmployee CHAR(500),
PRIMARY KEY(EmployeeID)
);
CREATE TABLE ListSearch
(
ListSearchID INT IDENTITY(1,1) PRIMARY KEY
-- Other fields you may want to identify the search, e.g. date, which user, which filters etc
)
CREATE TABLE ListSearchEmployee
(
ListSearchID INT,
EmployeeID INT, -- Don't bother Foreign Keying for performance
PRIMARY KEY CLUSTERED (ListSearchID, EmployeeID)
);
-- Insert 1M Employees
WITH cteData AS
(
SELECT top 1000000 sc1.name, ROW_NUMBER() OVER (ORDER BY sc1.object_id) AS rn
FROM sys.columns sc1 CROSS JOIN sys.columns sc2 CROSS JOIN sys.columns sc3
)
INSERT INTO Employee(Name)
SELECT name + CAST(rn AS VARCHAR)
FROM cteData;
-- Timing : 0.972 seconds on SQLExpress 2012 on an i3
-- Inserting 3 x 20 k lists of pseudo random employees (but not contigious on the EmployeeId Cluster)
WITH cteData AS
(
SELECT top 20000 1 as listid, ROW_NUMBER() OVER (ORDER BY sc1.object_id) * 50 AS empid
FROM sys.columns sc1 CROSS JOIN sys.columns sc2
UNION ALL
SELECT top 20000 2 as listid, ROW_NUMBER() OVER (ORDER BY sc1.object_id) * 30 AS empid
FROM sys.columns sc1 CROSS JOIN sys.columns sc2
UNION ALL
SELECT top 20000 3 as listid, ROW_NUMBER() OVER (ORDER BY sc1.object_id) * 41 AS empid
FROM sys.columns sc1 CROSS JOIN sys.columns sc2
)
INSERT INTO ListSearchEmployee(ListSearchID, EmployeeID)
SELECT listid, empid
FROM cteData;
DBCC DROPCLEANBUFFERS;
-- Timing : 1.751 seconds on SQLExpress 2012 on an i3
-- Joining 20k rows
SELECT *
FROM ListSearchEmployee el INNER JOIN Employee e on el.EmployeeID = e.EmployeeID
WHERE el.ListSearchID = 2