【问题标题】:Find similar entities in EAV model在 EAV 模型中查找相似实体
【发布时间】:2012-12-17 07:10:19
【问题描述】:

我需要一个在我的 EAV 表中返回“相似”产品的查询:

1) 至少有一个相似的属性

2) 没有与产品不同的属性

例如

ProductID Attribute Value
1         Prop1      1
1         Prop2      2
2         Prop1      1
3         Prop1      1
3         Prop2      3

在此示例中,搜索与产品 id 1(Prop1:1 和 Prop2:2)类似的产品。产品 2 会被退回,因为 Prop1 是 1,但是产品 3 不行,因为 Prop2 不同。等等。

每个产品都有可变数量的属性,因此不能为每个属性加入表格。目前我正在连接道具列表以构建动态 SQL “where”,但我找不到一个好的(快速?)SQL 语句来执行此操作。

也许我在这个问题上花了太多时间,但我无法摆脱我错过了一个明显的方法来解决这个问题的感觉......

【问题讨论】:

  • 我不明白你的例子。为什么 productid=2 匹配,为什么 productid=3 不匹配?您的业​​务规则到底是什么?
  • @BobDuell:属性相同的地方,值必须相同。虽然 ProductID 1 和 3 共享 Prop1 并且具有相同的值,但它们也共享 Prop2 但值不同,因此 3 被拒绝为与 1 不相似。相比之下,ProductID 1 和 2 共享 Prop1 并且具有相同的值,因此它们没事。我认为如果 ProductID 4 具有值为 1 的 Prop1 和值为 2 的 Prop3,它将与 ProductID 1 匹配。
  • 正是乔纳森所说的。我需要获得具有我正在寻找的一种或多种道具的产品。没关系,如果他们有其他道具,或者缺少道具,只要它没有我正在寻找的道具但价值错误
  • 这种事情变得一团糟。看看How to select similar sets in SQL,尽量不要太恐慌。
  • 这个问题及其答案很好地说明了查询 EAV 数据模型的困难。如果只是简单地搜索重复项,那么解决方案(以及数据库需要完成的工作)将不会容易得多!也许被 EAV 的“优雅”和“灵活”所吸引的读者可能会看到这一点并重新考虑。

标签: sql entity-attribute-value


【解决方案1】:

当遇到这类问题时,我使用 TDQD — 测试驱动的查询设计。

请注意,如果你给你的桌子起个名字,它会对每个人都有帮助!

通过 1

一个或多个属性与产品 1 相同的产品列表

SELECT a.ProductID, COUNT(*) AS matches
  FROM EAV_Table AS a
  JOIN EAV_Table AS b
    ON a.Attribute = b.Attribute AND a.value = b.value
 WHERE a.ProductID != 1
   AND b.ProductID  = 1
 GROUP BY a.ProductID

这显然不会列出任何计数为 0 的产品,这很好。

一个或多个属性与产品 1 不匹配的产品列表

SELECT c.ProductID, COUNT(*) AS matches
  FROM EAV_Table AS c
  JOIN EAV_Table AS d
    ON c.Attribute = d.Attribute AND c.value != d.value
 WHERE c.ProductID != 1
   AND d.ProductID  = 1
 GROUP BY c.ProductID

这也不会列出计数为 0 的产品,这更令人讨厌。

结果 - 通过 1

我们需要第一个查询中未在第二个查询中列出的所有产品。这可以通过 NOT EXISTS 和相关子查询来表示:

SELECT a.ProductID, COUNT(*) AS matches
  FROM EAV_Table AS a
  JOIN EAV_Table AS b
    ON a.Attribute = b.Attribute AND a.value = b.value
 WHERE a.ProductID != 1
   AND b.ProductID  = 1
   AND NOT EXISTS
       (SELECT c.ProductID
          FROM EAV_Table AS c
          JOIN EAV_Table AS d
            ON c.Attribute = d.Attribute AND c.value != d.value
         WHERE c.ProductID != 1
           AND d.ProductID  = 1
           AND c.ProductID = a.ProductID
       )
 GROUP BY a.ProductID

这太丑了。它有效,但它很丑。

测试数据

CREATE TABLE eav_table
(
    productid INTEGER NOT NULL,
    attribute CHAR(5) NOT NULL,
    value INTEGER NOT NULL,
    PRIMARY KEY(productid, attribute, value)
);

INSERT INTO eav_table VALUES(1, "Prop1", 1);
INSERT INTO eav_table VALUES(1, "Prop2", 2);
INSERT INTO eav_table VALUES(2, "Prop1", 1);
INSERT INTO eav_table VALUES(3, "Prop1", 1);
INSERT INTO eav_table VALUES(3, "Prop2", 3);
INSERT INTO eav_table VALUES(4, "Prop1", 1);
INSERT INTO eav_table VALUES(4, "Prop3", 1);

第一季度结果

2    1
3    1
4    1

第二季度结果

3    1

第三季度结果

2    1
4    1

这些是我生成的计数;更精致的演绎会删除它们。


通过 2

如果可以管理,一个更好的最终查询会将列出所有产品 ID 的表连接起来,该表列出了至少一个与产品 ID 1 相同的匹配属性/值对与列出所有具有与产品 ID 1 的分歧为零。

一个或多个属性与产品 1 相同的产品列表

第一个查询与 Pass 1 中的第一个查询相同,只是我们将删除结果集中的计数。

SELECT a.ProductID
  FROM EAV_Table AS a
  JOIN EAV_Table AS b
    ON a.Attribute = b.Attribute AND a.value = b.value
 WHERE a.ProductID != 1
   AND b.ProductID  = 1
 GROUP BY a.ProductID

通常,选择列表中的 GROUP BY 子句或 DISTINCT 是必需的(尽管示例数据并未正式要求它)。

与产品 1 不匹配的零属性产品列表

我们将利用COUNT(column) 仅计算非空值这一事实,并使用左外连接。

SELECT c.ProductID
  FROM      EAV_Table AS c
  LEFT JOIN EAV_Table AS d
    ON c.Attribute = d.Attribute
   AND c.Value != d.Value
   AND c.ProductID != 1
   AND d.ProductID  = 1
 GROUP BY c.ProductID
HAVING COUNT(d.Value) == 0;

请注意,WHERE 子句已合并到 ON 子句中;这实际上是相当重要的。

结果 - 通过 2

我们将上面的两个查询构建为连接以生成最终结果的子查询:

SELECT f.ProductID
  FROM (SELECT a.ProductID
          FROM EAV_Table AS a
          JOIN EAV_Table AS b
            ON a.Attribute = b.Attribute AND a.value = b.value
         WHERE a.ProductID != 1
           AND b.ProductID  = 1
         GROUP BY a.ProductID
       ) AS e
  JOIN (SELECT c.ProductID
          FROM      EAV_Table AS c
          LEFT JOIN EAV_Table AS d
            ON c.Attribute = d.Attribute
           AND c.Value != d.Value
           AND c.ProductID != 1
           AND d.ProductID  = 1
         GROUP BY c.ProductID
        HAVING COUNT(D.Value) = 0
       ) AS f
    ON e.ProductID = f.ProductID

这会在样本数据上产生答案 2 和 4。

请注意,此练习的一部分是学习不要对您提出的第一个答案感到满意。请注意,最好在完整大小的数据集上对解决方案进行基准测试,而不是在表中只有 7 行的测试数据集上进行基准测试。

【讨论】:

  • 漂亮!它看起来类似于我开始构建的查询,但与你的实际工作有显着差异!
【解决方案2】:

如果我正确理解了您的问题,我认为这应该可以解决问题。 Here is the fiddle;

DECLARE @pId INT = 1

SELECT A.pid
FROM (
        SELECT pid, count(*) total
        FROM t
        WHERE pid <> @pId
        GROUP BY pid 
     ) A JOIN 
     (
        SELECT pid, count(*) matches
        FROM t 
        WHERE pid<>@pId and att + ':' + convert(varchar(12), val) in (
             SELECT att + ':' + convert(varchar(12), val) FROM t
             WHERE pid=@pId)
        GROUP BY pid
     ) B ON A.pid = B.pid

WHERE total = matches

注意:根据 cmets 使用附加数据进行编辑

【讨论】:

  • 不会拒绝 3 吧?
  • @JonathanLeffler 查看小提琴,确实如此。答案是2
  • 它会拒绝 3,但只会导致'它定义了两个道具,而不是因为这些道具不匹配..尝试将产品 3 的 prop2 设置为 '2',它仍然会(错误地)拒绝它:-(
  • @Radu094 这意味着还要检查您在 OP 中未提及的值,对吗?不确定
  • @Radu094,现在看看,虽然有点丑。
【解决方案3】:

为了完整起见,使用 CTE。 (注意:这将找到所有双胞胎,而不仅仅是 productId = 1 的双胞胎)

DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp ;
SET search_path=tmp;

CREATE TABLE eav
        ( zentity INTEGER NOT NULL
        , zattribute varchar NOT NULL
        , zvalue INTEGER
        , PRIMARY KEY (zentity,zattribute)
        );
INSERT INTO eav(zentity, zattribute, zvalue) VALUES
 (1, 'Prop1',1) ,(1, 'Prop2',2)
,(2, 'Prop1',1)
,(3, 'Prop1',1) ,(3, 'Prop2',3)
,(4, 'Prop1',1) ,(4, 'Prop3',3) -- added by Jonathan L.
        ;

        -- CTE: pair of entities that have an 
        -- {attribute,value} in common
WITH pair AS (
        SELECT a.zentity AS one
                , b.zentity AS two
                , a. zattribute AS att
        FROM eav a
        JOIN eav b ON a.zentity <> b.zentity  -- tie-breaker
                AND a.zattribute = b.zattribute
                AND a.zvalue = b.zvalue
        )
SELECT pp.one, pp.two, pp.att
FROM pair pp
        -- The Other entity (two) may not have extra attributes
        -- NOTE: this NOT EXISTS could be repeated for pair.one, to also
        -- suppress the one.* products that have an extra attribute
WHERE NOT EXISTS (
        SELECT * FROM eav nx
        WHERE nx.zentity = pp.two
        AND nx.zattribute <> pp.att
        )
ORDER BY pp.one, pp.two, pp.att
        ;

顺便说一句:真正的根本问题是“关系划分”。也许更新的 SQL 标准应该为它引入一个运算符?

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-04-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多