10

我正在开发一个店面 Web 应用程序。当潜在客户在网站上查看产品时,我想自动从数据库中推荐一组相似的产品(而不是需要人工明确输入产品相似性数据/映射)。

事实上,仔细想想,大多数店面数据库已经有大量的相似性数据可用。在我的情况下Products可能是:

  • 映射到Manufacturer(aka Brand),
  • 映射到一个或多个Categories,并且
  • 映射到一个或多个Tags(又名Keywords)。

店面数据模型片段

通过计算产品与所有其他产品之间共享属性的数量,您可以计算“相似度得分”,以将其他产品与客户正在查看的产品进行比较。这是我最初的原型实现:

;WITH ProductsRelatedByTags (ProductId, NumberOfRelations)
AS
(
    SELECT  t2.ProductId, COUNT(t2.TagId)
    FROM    ProductTagMappings AS t1 INNER JOIN
                ProductTagMappings AS t2 ON t1.TagId = t2.TagId AND t2.ProductId != t1.ProductId
    WHERE   t1.ProductId = '22D6059C-D981-4A97-8F7B-A25A0138B3F4'
    GROUP BY t2.ProductId
), ProductsRelatedByCategories (ProductId, NumberOfRelations)
AS
(
    SELECT  t2.ProductId, COUNT(t2.CategoryId)
    FROM    ProductCategoryMappings AS t1 INNER JOIN
                ProductCategoryMappings AS t2 ON t1.CategoryId = t2.CategoryId AND t2.ProductId != t1.ProductId
    WHERE   t1.ProductId = '22D6059C-D981-4A97-8F7B-A25A0138B3F4'
    GROUP BY t2.ProductId
)
SELECT  prbt.ProductId AS ProductId
        ,IsNull(prbt.NumberOfRelations, 0) AS TagsInCommon
        ,IsNull(prbc.NumberOfRelations, 0) AS CategoriesInCommon
        ,CASE WHEN SimilarProduct.ManufacturerId = SourceProduct.ManufacturerId THEN 1 ELSE 0 END as SameManufacturer
        ,CASE WHEN SimilarProduct.ManufacturerId = SourceProduct.ManufacturerId 
            THEN IsNull(prbt.NumberOfRelations, 0) + IsNull(prbc.NumberOfRelations, 0) + 1
            ELSE IsNull(prbt.NumberOfRelations, 0) + IsNull(prbc.NumberOfRelations, 0)
        END as SimilarityScore
FROM    Products AS SourceProduct, 
        Products AS SimilarProduct INNER JOIN
        ProductsRelatedByTags prbt ON prbt.ProductId = SimilarProduct.Id FULL OUTER JOIN
        ProductsRelatedByCategories prbc ON prbt.ProductId = prbc.ProductId
WHERE SourceProduct.Id = '22D6059C-D981-4A97-8F7B-A25A0138B3F4'

这导致这样的数据:

ProductId                            TagsInCommon CategoriesInCommon SameManufacturer SimilarityScore
------------------------------------ ------------ ------------------ ---------------- ---------------
6416C19D-BA4F-4AE6-AB75-A25A0138B3A5 1            0                  0                1
77B2ECC0-E2EB-4C1B-A1E1-A25A0138BA19 1            0                  0                1
2D83276E-40CC-44D0-9DDF-A25A0138BE14 2            1                  1                4
E036BFE0-BBB5-450C-858C-A25A0138C21C 3            0                  0                3

我不是 SQL 性能专家,所以我有以下问题要问你 SQL 专家:

  • 这会是索引视图(用于持久性)的良好候选者,还是会增加源数据更改的过多成本?在这种情况下,我将使它成为一个存储过程,它可以更新SimilarProductMappings任何给定产品的物理表。
4

2 回答 2

2

你问了很多问题。我将尝试解决每一个问题,而不涉及太多细节。

  1. CTE 与派生表是语法糖。在性能方面没有区别。使用它们的唯一好处是您可以重复使用它们,而不是再次复制/粘贴/键入派生表。但是,在这种情况下,您不会重复使用它们,所以这取决于您。

  2. 索引视图:请记住,视图上的索引就像表上的索引一样,几乎没有例外。将其想象为正在为您的特定查询/视图创建另一个表并存储在磁盘上以便更快地检索。当基础数据发生变化时,必须更新这些索引。是的,这会产生巨大的资源影响。一般来说,我宁愿看到有人编写一个使用基表上的索引的查询,如果他们需要更多索引来实现特定目的,那么请详细查看,而不是在具有多个表的视图上整体查看。这更容易维护,也更容易弄清楚为什么你的 CRUD 花费的时间比预期的要长。索引视图没有任何问题。但,在这样的应用程序数据库模型上添加它时要非常小心,因为更新/插入/删除的表很复杂。索引视图的大多数更合适的用途是在报告数据仓库中。无论如何,不​​要在不了解 CRUD(创建、读取、更新、删除)操作对表的作用的情况下将索引放在视图上。在 CRM 或应用程序支持类型的数据库中,我会在很大程度上远离它们,除非有静态需求并且它不会真正影响性能。

阅读这篇文章:http ://technet.microsoft.com/en-us/library/ms187864(v=sql.105).aspx

请注意页面下方的 3/4,它谈到了在哪里不使用它,我认为您的案例适合您不应该使用它的 4 / 5 个场景。

  1. 关于保存连接......请记住,完整的外部连接是最严重的效率违规者之一。在我看来,您拥有它的唯一原因是因为您的 CTE 中没有包括制造商。您可以将其包含在您的 CTE 中,然后在最终查询中按 cat/tag 汇总匹配数,将其组合在一起以获得您的分数。这样,您只有两个左外连接(每个 CTE 一个),然后将两个计数相加并按同一制造商(案例声明)、productId 等进行分组。

  2. 最后......我会考虑将所有这些放在一个非规范化的表中,或者甚至是一个预先计算好的多维数据集中。让我们考虑有关您的要求的几件事:产品的相关分数是否需要实时显示?如果是,为什么?当添加/删除新产品时,这不是关键任务。任何人说它需要直播,可能并不是真的。湾。检索速度。我可以使用临时表重写您的查询,确保索引正确等,并在存储过程中提出一个相当快的查询。但是,每次页面加载时,我仍在汇总数据库中的数据,以便在我的店面各处显示。如果数据是预先计算好的,并存储在一个单独的 productIds 表和每个产品的评分表中,并由 productId 索引,那么检索将非常快。您可以每晚、每小时/任何时间在 ETL 中中继和重新加载表,而不必担心维护每次都重建的索引。当然,如果您的店面是 24/7/365,您将需要编写一些数据库端代码来担心版本控制,这样您的应用程序就不必等待数据库正在重新计算。

此外,如果没有别的,请确保至少将此信息缓存在 Web/应用程序服务器上。有一件事是肯定的,如果您使用上面的解决方案,那么您需要在您的站点中构建一些东西,这样它就不会等待数据返回并缓存它。

希望所有这些都有帮助。

于 2013-11-13T20:52:47.807 回答
1

稍微不同的方法怎么样?

;WITH ProductFindings (ProductId, NbrTags, NbrCategories)
AS
(
    SELECT  t2.ProductId, COUNT(t2.TagId), 0
    FROM    ProductTagMappings AS t1 
    INNER JOIN
            ProductTagMappings AS t2  ON t1.TagId      = t2.TagId
                                     AND t1.ProductId != t2.ProductId
    WHERE   t1.ProductId = '22D6059C-D981-4A97-8F7B-A25A0138B3F4'
    GROUP BY t2.ProductId

  UNION ALL

    SELECT  c2.ProductId, 0, COUNT(c2.CategoryId)
    FROM    ProductCategoryMappings AS c1 
    INNER JOIN
            ProductCategoryMappings AS c2  ON c1.CategoryId = c2.CategoryId
                                          AND c1.ProductId != c2.ProductId
    WHERE c1.ProductId = '22D6059C-D981-4A97-8F7B-A25A0138B3F4'
    GROUP BY c2.ProductId

), ProductTally (ProductId, TotTags, TotCategories) as
(
   SELECT ProductID, sum(NbrTags), sum(NbrCategories)
   FROM     ProductFindings
   GROUP BY ProductID
)
SELECT  Tot.ProductId      AS ProductId
       ,Tot.TotTags        AS TagsInCommon
       ,Tot.TotCategories  AS CategoriesInCommon
       ,CASE WHEN SimilarProduct.ManufacturerId = SourceProduct.ManufacturerId
               THEN 1
               ELSE 0 
         END               as SameManufacturer
       ,CASE WHEN SimilarProduct.ManufacturerId = SourceProduct.ManufacturerId 
               THEN 1
               ELSE 0
         END + Tot.TotTags + Tot.TotCategories
                           as SimilarityScore
FROM    ProductTally as Tot
INNER JOIN  Products     AS SimilarProduct   ON Tot.ProductID = SimilarProduct.Id 
INNER JOIN  Products     AS SourceProduct    ON SourceProduct.Id = '22D6059C-D981-4A97-8F7B-A25A0138B3F4'
于 2013-11-16T01:48:22.967 回答