【问题标题】:Optimize solution for categories tree search优化分类树搜索解决方案
【发布时间】:2010-08-15 00:46:25
【问题描述】:

我正在创建某种拍卖应用程序,我必须确定解决此问题的最优化方式。我使用 BL Toolkit 作为我的 OR 映射器(它有很好的 Linq 支持)和 ASP.NET MVC 2。

背景


我有多个动态创建的 Category 对象,它们作为此类的表示保存在我的数据库中:

class Category
{
    public int Id { get; set; }
    public int ParentId { get; set; }
    public string Name { get; set; }
}

现在每个Category 对象都可以关联多个InformatonClass 对象,这些对象代表该类别中的单个信息,例如价格或颜色。这些类也由管理员动态创建并存储在数据库中。有一组特定的类别。代表它的类如下所示:

class InformationClass
{
    public int Id { get; set; }
    public InformationDataType InformationDataType { get; set; }
    public string Name { get; set; }
    public string Label { get; set; }
}

现在我有了第三个表,表示它们之间的连接,如下所示:

class CategoryInformation
{
    public int InformationClassId { get; set; }
    public int AuctionCategoryId { get; set; }
}

问题


现在的问题是我需要继承子类别中的所有类别InformationClass。例如,每个产品都会有一个价格,所以我只需要将此 InformationClass 添加到我的根类别中。频率信息可以添加到基本 CPU 类别中,并且应该在派生自 CPU 类别的 AMD 和 Intel 类别中可用。

我必须知道在我的应用程序中哪些InformationClass 对象经常与指定的Category 相关。

所以这是我的问题。对于这个问题,最优化的解决方案是什么?我有一些想法,但我无法决定。

  1. 将所有类别从数据库加载到Application 表并每次都从该位置获取它们 - 只要类别不会经常更改,它将减少数据库请求的数量,但仍需要使用 Linq-to 进行树搜索-对象
  2. 发明(我不知道是否可能)一些花哨的 Linq 查询,它可以树搜索并获取所有信息类 ID,而不会对数据库造成太大压力。
  3. 还有其他好主意吗?

我将不胜感激每一个答案和想法。谢谢大家的建议。

【问题讨论】:

  • @lukasz 什么是拍卖申请
  • 你知道,打赌...比如 eBay 之类的...问题也可能与任何具有与其相关联的特定信息的类别的商店有关。

标签: c# linq optimization asp.net-mvc-2 business-logic-toolkit


【解决方案1】:

听起来像是我曾经在博客上写过的一个想法的案例:

基本思路是这样的:除了Category 表之外,您还有一个CategoryTC 表,其中包含父子关系的传递闭包。它允许您快速有效地检索特定类别的所有祖先或后代类别的列表。这篇博文解释了如何在每次创建、删除新类别或更改父子关系(每次最多两次查询)时使传递闭包保持最新。

这篇文章使用 SQL 来表达这个想法,但我相信你可以将它翻译成 LINQ。

您没有在问题中指定InformationClass 表如何链接到Category 表,所以我必须假设您有一个看起来像这样的CategoryInformation 表:

class CategoryInformation
{
    public int CategoryId { get; set; }
    public int InformationClassId { get; set; }
}

然后,您可以使用以下方法获取与特定类别关联的所有信息类:

var categoryId = ...;
var infoClasses = db.CategoryInformation
    .Where(cinf => db.CategoryTC.Where(tc => tc.Descendant == categoryId)
                                .Any(tc => tc.Ancestor == cinf.CategoryId))
    .Select(cinf => db.InformationClass
                      .FirstOrDefault(ic => ic.Id == cinf.InformationClassId));

这有意义吗?有任何问题,欢迎提问。

【讨论】:

  • 看起来不错,但我发现他有问题。当我将拥有更多嵌套的类别时,TC 记录的数量将是巨大的。也不是比把所有类别都记下来效率低吗?我的意思是-它们不会经常更改,这是一个重要的假设-您的解决方案似乎很好,而不是一般用途..
  • @ŁukaszW.pl — ① 不,TC 表很小。每行只有两个整数!即使对于 一百万 个父/子链接,也只有 8 MB,可能比在 Category 表中添加一个新的字符串列还小。 — ② 如果您的所有类别都适合内存,并且值得这样做,因为您一直在查询它们,那么 SQL Server 已经这样做了。为什么要保留一个副本,迫使您担心在它过时等情况下必须更新它,而 SQL Server 已经为您这样做了?
  • (我刚刚注意到“2”小节的博文中的一个错误。已修复。)
【解决方案2】:

过去(SQLServer 2005 之前和 LINQ 之前)在处理这种结构时(或更一般的有向无环图,使用联结表实现,以便项目可以有多个“父项”) ,我要么通过将整个图形加载到内存中,要么通过在数据库中创建一个以祖先与后代关系缓存的 tigger-updated 查找表来完成此操作。

两者都有优势,哪一个胜出取决于更新频率、父子关系之外的对象的复杂性以及更新频率。通常,加载到内存中可以更快地进行单个查找,但是对于大图,由于每个 Web 服务器中使用的内存量(此处为“每个”,因为 Webfarm 情况是具有缓存在内存中的项目会带来额外的问题),这意味着您必须非常小心如何保持同步以抵消这种影响。

现在可用的第三种选择是使用递归 CTE 进行祖先查找:

CREATE VIEW [dbo].[vwCategoryAncestry]
AS
WITH recurseCategoryParentage (ancestorID, descendantID)
AS
(
    SELECT parentID, id
    FROM Categories
    WHERE parentID IS NOT NULL

    UNION ALL

    SELECT ancestorID, id
    FROM recurseCategoryParentage
        INNER JOIN Categories ON parentID = descendantID
)
SELECT DISTINCT ancestorID, descendantID
FROM recurseCategoryParentage

假设根类别由 null parentID 指示。

(我们使用 UNION ALL 是因为无论如何我们都将在之后选择 DISTINCT,这样我们就有一个 DISTINCT 操作而不是重复它)。

这允许我们执行查找表方法,而无需该非规范化表的冗余。效率权衡明显不同,通常比使用 table 差,但不多(select 轻微命中,insert 和 delete 略有增益,空间增益可忽略不计),但正确性保证更大。

我忽略了 LINQ 适合于此的问题,因为无论以何种方式查询,权衡都是相同的。 LINQ 可以更好地处理具有单独主键的“表”,因此我们可以将 select 子句更改为 SELECT DISTINCT (cast(ancestorID as bigint) * 0x100000000 + descendantID) as id, ancestorID, descendantID 并将其定义为 [Column] 属性中的主键。当然,所有列都应指示为 DB 生成的。


编辑。更多关于所涉及的权衡取舍。

将 CTE 方法与数据库中维护的查找进行比较:

专业 CTE:

  1. CTE代码很简单,上面的视图就是你需要的所有额外的DB代码,需要的C#是一样的。
  2. DB 代码都在一个地方,而不是同时存在一个表和另一个表上的触发器。
  3. 插入和删除更快;这不会影响它们,而触发器会影响它们。
  4. 虽然在语义上是递归的,但它是查询规划器能够理解和处理的一种方式,因此它通常(对于任何深度)仅在两个索引扫描(可能是集群)中实现,两个轻量级假脱机,一个连接和一个不同的排序,而不是您可能想象的许多扫描。因此,虽然扫描肯定比简单的表查找更重,但它远没有人们一开始想象的那么糟糕。事实上,即使是这两个索引扫描的性质(同一张表,不同的行),它也比你在阅读时想象的要便宜。
  5. 如果以后的经验证明这是可行的方法,则很容易将其替换为表查找。
  6. 就其本质而言,查找表将使数据库非规范化。撇开纯度问题不谈,所涉及的“难闻气味”意味着必须向任何新开发者解释和证明这一点,因为在此之前它可能只是“看起来不对”,他们的直觉会让他们疯狂追逐试图移除它。

专业查找表:

  1. 虽然 CTE 的选择速度比人们想象的要快,但查找速度仍然更快,尤其是在用作更复杂查询的一部分时。
  2. 虽然 CTE(以及用于创建它们的 WITH 关键字)是 SQL 99 标准的一部分,但它们相对较新,一些开发人员并不了解它们(尽管我认为这个特定的 CTE 非常容易阅读,因此很重要无论如何,这是一个很好的学习示例,所以也许这实际上是专业 CTE!)
  3. 虽然 CTE 是 SQL 99 标准的一部分,但某些 SQL 数据库(包括旧版本的 SQLServer(仍在使用中))并未实施它们,这可能会影响任何移植工作。 (虽然它们受到 Oracle 和 Postgres 等的支持,所以目前这可能不是问题)。
  4. 如果以后的经验表明您应该这样做,以后用 CTE 版本替换它是相当容易的。

比较(两者)db-heavy 选项与内存缓存。

专业内存:

  1. 除非你的实现真的很糟糕,否则它会比数据库查找快得多。
  2. 这使得在此更改的背后进行一些二次优化成为可能。
  3. 如果以后的分析显示内存是可行的方法,那么从 DB 更改为内存是相当困难的。

专业查询数据库:

  1. 在内存中启动时间可能会很慢。
  2. 对数据的更改要简单得多。大部分观点都是这方面的。确实,如果您采用内存中的路线,那么如何处理使缓存信息无效的更改问题将成为项目生命周期中一个全新的持续关注点,而不是一个微不足道的问题。
  3. 如果您使用内存,您可能不得不使用此内存存储,即使是在不相关的操作中也是如此,这可能会使它与您的其余数据访问代码相匹配的地方变得复杂。李>
  4. 无需跟踪更改和缓存新鲜度。
  5. 不必确保网络农场和/或网络花园解决方案中的每个网络服务器(一定程度的成功需要这样做)都具有完全相同的新鲜度。
  6. 同样,跨机器的可扩展性程度(通过将网络服务器和数据库从属服务器的数量增加一倍可以获得接近 100% 的额外性能)更高。
  7. 在内存中,内存使用率可能会变得非常高,如果 (a) 对象的数量很高或 (b) 对象的大小(字段、尤其是字符串、集合和对象本身就有刺痛)或收藏)。可能“我们需要更大的网络服务器”内存量,这适用于场中的每台机器。 7a。随着项目的发展,这种大量的内存使用尤其会继续增长。
  8. 除非更改导致内存中存储立即刷新,否则内存中解决方案将意味着负责管理这些类别的人员使用的视图将与客户看到的不同,直到他们重新同步。
  9. 内存中重新同步可能非常昂贵。除非您非常聪明,否则它可能会导致随机(对用户)大量性能峰值。如果你很聪明,它可能会激怒其他问题(尤其是在保持不同机器的新鲜度方面)。
  10. 除非您在内存方面很聪明,否则这些尖峰可能会累积,使机器长期挂起。如果您能巧妙地避免这种情况,您可能会激怒其他问题。
  11. 非常很难从内存转移到访问数据库,如果这证明了要走的路。

这一切都不会 100% 确定地倾向于一种解决方案,我当然不会给出明确的答案,因为这样做是过早的优化。您可以做的先验是做出一个合理的决定,决定哪个可能是最佳解决方案。无论您选择哪种方式,都应该事后进行分析,尤其是。如果代码确实成为瓶颈并可能发生变化。您还应该在产品的生命周期内这样做,因为对代码的更改(修复和新功能)和对数据集的更改肯定会改变哪个选项是最佳的(实际上,它可以从一个更改为另一个,然后再更改回前一个,在整个生命周期中)。这就是为什么我在上面的优缺点列表中考虑了从一种方法转移到另一种方法的难易程度。

【讨论】:

  • 你能更简单地解释一下(我在 SQL Server 中不太擅长)为什么这个视图是一个好方法。我的意思是我必须告诉我的同事我为什么选择这个解决方案。这对我来说很好,因为它很简单,但是我必须知道为什么这种查询对于数据库来说不是太麻烦,因为在我看来递归解决方案对我来说意味着性能应该很差......当我将加载所有数据到我的应用程序我可以进行一次此操作。当我使用我的视图时,它不会总是再次执行它吗?
  • 当然。这些考虑并非微不足道,所以我在上面修改了我的答案。
猜你喜欢
  • 1970-01-01
  • 2016-04-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-05
  • 1970-01-01
相关资源
最近更新 更多