MySQL - 我什么时候不应该加入表格?值的组合爆炸

MySQL - When shouldn't I Join tables? Combinatorial Explosion of values

我正在处理一个名为 classicmodels 的数据库,我发现它位于:https://www.mysqltutorial.org/mysql-sample-database.aspx/

我意识到当我在 'payments' 和 'orders' table 之间执行 Inner Join 时,发生了 'cartesian explosion'。我知道这两个 table 不是要合并的。但是,我想知道是否可以仅通过查看关系模式来识别它,或者我是否应该一个一个地检查 tables。

例如,customer number '141'在'orders table'中出现了26次,这是我用下面的代码发现的:

SELECT
    customerNumber,
    COUNT(customerNumber) 
FROM
    orders
WHERE customerNumber=141
GROUP BY customerNumber;

并且相同的客户编号 (141) 在付款中出现 13 次 table:

SELECT
    customerNumber,
    COUNT(customerNumber)
FROM
    payments
WHERE customerNumber=141
GROUP BY customerNumber;

最后,我在 'payments' 和 'orders' table 之间执行了一个内部连接,并且只选择了客户编号为 '141' 的行。 MySQL 返回了 338 rows,这是 26*13 的结果。因此,我的查询是将 'customer n°' 在 'orders' table 中出现的次数乘以它在 'payments'.

中出现的次数
SELECT
    o.customernumber,
    py.amount
FROM
    customers c
        JOIN
    orders o ON c.customerNumber=o.customerNumber
        JOIN
    payments py ON c.customerNumber=py.customerNumber
WHERE o.customernumber=141;

我的问题如下:

1 ) 有没有办法查看关系模式并确定是否可以执行 Join(不产生组合爆炸)?或者我应该通过table检查table以了解它们之间的关系?

这是 'MySQL Tutorial' 网站提供的数据库关系模式:

感谢您的关注!

这称为“组合爆炸”,它发生在一个 table 中的每行连接到其他 table 中的多行时。

(这不是“高估”或任何类型的估计。它在应该只计算一次数据项时多次计算数据项。)

在一对多关系中汇总数据是一个臭名昭著的陷阱。在您的示例中,每个客户可能没有订单、一个订单或多个订单。独立地,他们可能没有付款,一次或多次。

诀窍是这样的:使用子查询,这样您使用 GROUP BY 的顶级查询就可以避免连续加入一对多关系。在您向我们展示的查询中,这正在发生。

您可以使用此子查询获得每个客户只有一行的结果集。 (试一试。)

                    SELECT customernumber, 
                           SUM(amount) amount
                      FROM payments 
                  GROUP BY customernumber

同样你可以用这个

获得每个客户的所有订单的价值
                    SELECT c.customernumber, 
                           SUM(od.qytOrdered * od.priceEach) amount
                      FROM orders o
                      JOIN orderdetails od ON o.orderNumber = od.orderNumber
                     GROUP BY c.customernumber

这个JOIN不会在你面前爆炸,因为客户可以有多个订单,每个订单可以有多个细节。所以这是一个严格的分层汇总。

现在,我们可以在主查询中使用这些子查询。

SELECT c.customernumber, p.payments, o.orders 
  FROM customers c
  LEFT JOIN (
                    SELECT c.customernumber, 
                           SUM(od.qytOrdered * od.priceEach) orders
                      FROM orders o
                      JOIN orderdetails od ON o.orderNumber = od.orderNumber
                     GROUP BY c.customernumber
            ) o ON c.customernumber = o.customernumber
  LEFT JOIN (
                    SELECT customernumber, 
                           SUM() payment
                      FROM payments 
                  GROUP BY customernumber
            ) p on c.customernumber = p.customernumber

实用技巧:

  1. 子查询是一个 table(一个虚拟的 table),可以在您提到 table 或视图的任何地方使用。
  2. 此查询中的 GROUP BY 内容分别出现在两个子查询中,因此没有组合爆炸。
  3. 顶层 JOIN 中的所有三个参与者每个 customernumber 都有一行或零行。
  4. LEFT JOIN 在那里,所以我们仍然可以看到没有订单或没有付款的客户(对企业来说很重要)。对于普通的内部 JOIN,行必须匹配 ON 条件的两侧,否则它们会从结果集中被忽略。

专业提示 仔细格式化您的 SQL 查询:它们真的很冗长。 Adm. Grace Hopper 会感到自豪。这意味着它们变得相当长且嵌套,将 Structured 置于结构化查询语言中。如果您或任何人将来要对它们进行推理,我们必须能够轻松掌握结构。

专业提示 2 设计这个数据库的数据工程师在思考和记录方面做得非常好。渴望达到这种质量水平。 (在现实世界中很少到达。)

在这种特殊情况下,您的行为应该取决于数据库支持的会计风格,这似乎不是“未清项目”风格的会计,即当订单为 1000 时,不需要以 1000 美元作为付款。这在大多数消费者体验中可能 ps 不寻常,因为您会非常熟悉从亚马逊订购的开放式商品 - 您购买了 500 美元的电视和 500 美元的游戏机,订单是一千美元,你付钱,付款与订单不符。但是,如果您使用信用卡为该订单付款,您也熟悉“余额远期”会计,因为您在一个月内每天都进行类似的购买,然后您从银行收到一份声明,说您欠 31000,并且您支付了一大笔钱,甚至不必是 31k。您不需要在月底向银行支付 31 笔金额为 1000 的款项。您的银行将其分配给帐户中最旧的项目(如果它们很好,或者最新的项目如果它们不好)并且最终可能会向您收取未付款交易的利息

1 ) Is there a way to look at the relational schema and identify if a Join can be executed

是的,您可以通过查看架构来判断 - 客户有很多订单,客户进行了很多付款,但是订单和付款之间根本没有关系 tables 所以我们可以看到没有尝试直接将付款附加到订单。您可以看到 customer 是 payment 和 order 的父级 table,因此与他们每个人都有关系,但他们之间没有关系。如果你有 Person, Car 和 Address tables,一个人一生有很多地址,也有很多车,但这并不意味着车和地址之间有关系

在这种情况下,加入 payments to customers to orders 根本没有意义,因为它们之间没有那种关系。如果您想进行这样的连接而不遭受笛卡尔爆炸,那么您绝对必须对一侧或另一侧(或两侧)求和,以确保您的连接是 1:1 和 1:M(或 1:1 和 1:1)。您不能安排一对 1:M.

的联接

回到 car/person/address 示例以进行任何有意义的连接,您必须在问题中构建更多信息并安排连接以创建答案。 Perhaps 问题是“他们住的时候拥有什么车”——这将 Person:Address 关系扁平化为 1:1 但留下 Person:Car 为 1:M 所以他们在那所房子里可能拥有很多汽车。如果“最新”有明显的赢家,“他们住在...时拥有的最新汽车是什么”可能是 1:1 双方(尽管如果他们购买了两辆同时生产的汽车...)

您在订单案例中对哪一方求和取决于您想知道什么,但在这种情况下,我会说您通常想知道“哪些订单尚未付款”,这就是所有付款的总和并滚动汇总所有订单,然后查看滚动总和超过支付总和的点。这些是未支付的订单

再次查看您的 数据库 图表(在您的问题的第一次迭代中出现的图表)。看到 tables 之间的线在一端有 3 个倾斜的腿 - 这是许多端。您可以从图中的任何 table 开始,然后通过沿着关系移动来加入其他 table。如果你从多端到一端,假设你在开始时选择了一行 table (一个订单)你总是可以走到任何其他 table多->一个方向,而不是增加你的行数。如果你走另一条路,你可能会增加你的行数。如果您分开并走两种都增加行数的方式,您会得到笛卡尔爆炸。当然,你也不必只加入关系线,但这超出了问题的范围

ps:这在 db 图上比问题中的 ERD 更容易看到,因为数据库只关心外键列。 ERD 表示客户有零笔或一笔付款,并带有特定的支票号码,但数据库只会关注“客户 ID 在客户 table 中出现一次,在付款中出现多次 table”因为只有部分支付的复合主键键控给客户 table。换句话说,ERD 也关注业务逻辑关系,但数据库图纯粹是 table 的关系,它们不一定对齐。出于这个原因,在四处寻找连接策略时,数据库图可能更容易阅读

看到 Caius Jard 和 O.Jones 的回答(请检查他们的回复),帮助我澄清了这个疑问后,我决定创建一个 table 来识别哪些客户付款对于他们下的所有订单,哪些没有。这创造了加入 'orders'、'orderdetails'、'payments' 和 'customers' table 的相关原因,因为某些订单可能已被取消或仍可能 'On Hold',我们可以在'status'中看到它们对应的'orders'中的table。此外,这使我们能够在不生成 'combinatorial explosion'.

的情况下执行此连接

我是通过使用 CASE statement 来做到这一点的,它在 py.amountamount_in_orders 匹配、不匹配或它们为 NULL(没有下订单的客户或付款):

SELECT
    c.customerNumber, 
    py.amount, 
    amount_in_orders,
    CASE
        WHEN py.amount=amount_in_orders THEN 'Match'
        WHEN py.amount IS NULL AND amount_in_orders IS NULL THEN 'NULL'
        ELSE 'Don''t Match'
    END AS Match
FROM
    customers c

    LEFT JOIN(
        SELECT
            o.customerNumber, SUM(od.quantityOrdered*od.priceEach) AS amount_in_orders
        FROM
            orders o
            JOIN orderdetails od ON o.orderNumber=od.orderNumber
        GROUP BY o.customerNumber
    ) o ON c.customerNumber=o.customerNumber

    LEFT JOIN(
        SELECT customernumber, SUM(amount) AS amount
        FROM payments
        GROUP BY customerNumber
    ) py ON c.customerNumber=py.customerNumber
ORDER BY py.amount DESC;

查询返回了 122 行。下面的图像是生成的输出的一部分,因此您可以想象发生了什么:

例如,我们可以看到编号为“141”、“124”、“119”和“496”的客户并没有为他们下的所有订单付款。也许他们中的一些人取消了或者他们只是还没有支付。

这张图片显示了一些为 NULL 的列(不是全部):