SQL 扇出的问题(SQL fanouts)

如果您是一个 SQL 用户,您可能首先学到的一些 SQL 概念是连接和聚合函数(如 COUNTSUM)。那就是这两个概念如何相互作用相互影响呢,值得注意的是有时会产生不正确的结果。
在本文中,我们将讨论“ fanout”的概念,以及为什么它对 SQL 编写者很重要。

从Join开始

让我们从一个简单的示例开始,我们将把两个表连接在一起。我们的第一个表格将显示我们客户的姓名和每个客户访问我们网站的次数:

customer

customer_id first_name last_name visits
1 Amelia Earhart 2
2 Charles Lindbergh 2
3 Wilbur Wright 4

我们的第二张表将包括那些客户下的所有订单。您可以看到,每个订单都通过客户 ID 链接到放置订单的客户。

order

order_id amount customer_id
1 25.00 1
2 50.00 1
3 75.00 2
4 100.00 3

在 SQL 中将这些表连接在一起非常简单:

SELECT    *
FROM      customer
LEFT JOIN order
ON        customer.customer_id = order.customer_id
customer_id first_name last_name visits order_id amount customer_id
1 Amelia Earhart 2 1 25.00 1
1 Amelia Earhart 2 2 50.00 1
2 Charles Lindbergh 2 3 75.00 2
3 Wilbur Wright 4 4 100.00 3

聚合函数出问题

现在,我们已经有了一个join表,我们需要仔细考虑如何使用聚合函数如COUNTSUM.

单个表上的聚合函数

让我们再次单独考虑一下客户表customer。如果我们想知道客户的总数,我们可以执行这样一个简单的查询:

SELECT COUNT(*)
FROM   customer

SQL 将按如下方式计算表中的行数:
customer

count customer_id first_name last_name visits
1 1 Amelia Earhart 2
2 2 Charles Lindbergh 2
3 3 Wilbur Wright 4

我们会得到一个计数3,这是正确的。

或者,如果我们想知道客户访问的总次数,我们可以执行另一个类似这样的简单查询:

SELECT SUM(visits)
FROM   customer

SQL 将表中的访问次数加起来如下:
customer

customer_id first_name last_name visits
1 Amelia Earhart 2
2 Charles Lindbergh +2
3 Wilbur Wright +4
SUM 8

我们会得到8的结果,这也是正确的。

合并表上的聚合函数

目前为止,一切顺利。然而,如果我们尝试在任何一个联接的表上使用相同的聚合函数,我们将开始看到不正确的结果。

对合并表进行基本计数,我们将不再得到正确的客户数量:

SELECT    COUNT(*)
FROM      customer
LEFT JOIN order
ON        customer.customer_id = order.customer_id

SQL 将按如下方式计算表中的行数:

count customer_id first_name last_name visits order_id amount customer_id
1 1 Amelia Earhart 2 1 25.00 1
2 1 Amelia Earhart 2 2 50.00 1
3 2 Charles Lindbergh 2 3 75.00 2
4 3 Wilbur Wright 4 4 100.00 3

我们将得到4的结果,即使实际上只有3个客户。你可以看到 Amelia 被数了两次。

同样,如果我们试图将访问次数相加,我们将不再得到正确的结果:

SELECT    SUM(visits)
FROM      customer
LEFT JOIN order
ON        customer.customer_id = order.customer_id

SQL 将表中的访问次数加起来如下:

customer_id first_name last_name visits order_id amount customer_id
1 Amelia Earhart 2 1 25.00 1
1 Amelia Earhart +2 2 50.00 1
2 Charles Lindbergh +2 3 75.00 2
3 Wilbur Wright +4 4 100.00 3
SUM 10

我们得到的结果是10,即使只有8次访问。 Amelia2次访问被加了两次。

Fanouts, schmanouts, who cares?

在扇出和非扇出两种情况下,仍然需要考虑聚合函数的精度问题。然而,在我们将要看到的问题类型上有一个细微的差别。

  • No fanout:可以信任主表上的聚合函数,但不一定信任联接表上的聚合函数。

  • fanout:不一定要信任主表或联接表上的聚合函数。
    回顾之前的示例,扇出的原因: customer 表只有三行,但是连接的表有四行。由于我们处于扇出状态,我们不能相信聚合函数将在主表(customer)上工作。正如我们前面看到的,SUM (visits)将给我们一个10的值,尽管实际上只有8次访问。

为了说明这一点,让我们看看以前的例子。

No fanout example

在我们正在查看的示例中,主表(customer)只有三行。“主表”是 SQL 查询的 FROM 子句中的表。join之后,我们现在有4行。由于联接的表比主表具有更多的行,因此我们认为发生了扇出fanout
为了避免扇出,我们可以按照相反的顺序写入连接:

SELECT    *
FROM      order
LEFT JOIN customer
ON        order.customer_id =
          customer.customer_id
order_id amount customer_id customer_id first_name last_name visits
1 25.00 1 1 Amelia Earhart 2
2 50.00 1 1 Amelia Earhart 2
3 75.00 2 2 Charles Lindbergh 2
4 100.00 3 3 Wilbur Wright 4

目光敏锐的观察者会认识到,除了列的顺序之外,这与我们的其他联接表完全相同。这是正确的,这也是为什么大多数 SQL 编写者认为我们两个不同的连接完全相同的原因。

但是,当当我们反转 join 时,我们没有扇出。我们最初的表(订单order)有四行,我们的联接表也有四行,所以没有扇出。这里有一个关键点: 为了避免扇出,可以从最细粒度的表开始连接join。

如果没有扇出,我们可以相信聚合函数将在主表(order)上工作。例如,SUM (amount)将给我们一个值250.00,这是收集到的正确数额。

友好的join和不友好的join

如果我们想避免扇出,理解这三种不同类型的连接是很重要的。

One-to-one (友好)

如果主表中的一行只与联接表中的一行匹配,那么就是一对一联接。这种类型的连接不会导致扇出,聚合函数将是准确的,无论您在哪里使用它们。

例如: 假设您有一个人员表和一个 DNA 表。因为只有一个人可以与一个 DNA 记录匹配,所以这是一对一的连接。

Many-to-one (友好)

如果主表中的许多行与联接表中的同一行匹配,那么就是多对一联接。这种类型的连接也不会导致扇出,聚合函数在主表上至少是准确的。

例如: 假设您有一个 person 表和一个 state-of-residence表。因为许多人可以生活在一个状态,所以这是一个多对一的连接。

One-to-many(不友好)

如果主表中的一行可以与联接表中的多行匹配,那么就是一对多联接。这种类型的连接可能导致扇出,聚合函数在任何地方都不一定准确。

例如: 假设您有一个 person 表和一个 children 表。由于一个人可以有多个孩子,所以这是一对多的连接。

检查扇出的风险

检查扇出的第一个(也是首选)方法是了解正在发生的连接的类型。一对一和多对一的连接永远不会导致扇出。然而,如果你知道你是在一对多的情况下,那么总是会有风险扇出。即使扇出尚未发生,如果添加新行,将来也会有风险。

检查扇出的第二种方法是在连接之前和之后查询 COUNT。查询应该是这样的:

SELECT COUNT(*)
FROM   my_primary_table

SELECT    COUNT(*)
FROM      my_primary_table
LEFT JOIN my_joined_table
ON        my_primary_table.my_column_1 = my_joined_table.my_column_2

如果两个查询之间的计数增加,我们就知道发生了扇出。因为我们正在寻找增加,所以第二个查询使用 LEFT JOIN 非常重要。我们不希望仅仅因为主表中的一行在联接表中没有对应的行而人为地减少报告的行数。

不幸的是,这种方法不能告诉您是否存在未来扇出的风险。要了解这一点,您需要了解正在发生的连接的类型。

将数据分组为一对一联接

在本文的开头,我们从客户customer表开始,left join order表,然后在提取总客户和总访问量时发现了一些不好的行为。

为了以安全的方式将这些数据连接在一起,我们可能采取的一种方法是按 customer_ id 对订单order表进行分组。在这样做的时候,我们将创建一个单独的行来与客户中的每一行进行匹配,并以一对一的关系结束。执行此分组的 SQL 可以写为:

SELECT   customer_id,
         SUM(amount) AS total_amount
FROM     order
GROUP BY customer_id

得到的表格如下:

customer_id total_amount
1 75.00
2 75.00
3 105.00

如果将这个结果集与 customer 表进行比较,就可以看到它的连接非常好。要在 SQL 中实际执行连接,您需要使用一个子查询:

SELECT *
FROM   customer
LEFT JOIN
(
  SELECT   customer_id,
           SUM(amount) AS total_amount
  FROM     order
  GROUP BY customer_id
)
AS customer_totals
ON customer.customer_id = customer_totals.customer_id
customer_id first_name last_name visits customer_id total_amount
1 Amelia Earhart 2 1 75.00
2 Charles Lindbergh 2 2 75.00
3 Wilbur Wright 4 3 105.00

现在,我们想要使用的任何聚合函数计算(例如访问时的 SUM 或访问 total_amount都可以正常工作。

总结

  • 如果将 SUMCOUNT 这样的聚合函数用于已联接的表,则可能出现错误行为。

  • 如果连接具有一对一的关系,那么聚合函数就可以很好地工作。

  • 如果连接具有多对一关系,则聚合函数将在主表上工作,但可能不在已连接的表上工作。

  • 如果连接具有一对多关系,则聚合函数可能无法在任何地方工作。

  • 如果需要联接具有一对多关系的表,并且希望使用聚合函数,请首先尝试对联接的表进行分组GROUP,以便它们与主表具有一对一关系。

你可能感兴趣的:(sql,数据库)