如果您是一个 SQL 用户,您可能首先学到的一些 SQL 概念是连接和聚合函数(如 COUNT
和 SUM)
。那就是这两个概念如何相互作用相互影响呢,值得注意的是有时会产生不正确的结果。
在本文中,我们将讨论“ fanout”的概念,以及为什么它对 SQL 编写者很重要。
让我们从一个简单的示例开始,我们将把两个表连接在一起。我们的第一个表格将显示我们客户的姓名和每个客户访问我们网站的次数:
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表,我们需要仔细考虑如何使用聚合函数如COUNT
和SUM
.
让我们再次单独考虑一下客户表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
次访问。 Amelia
的2
次访问被加了两次。
在扇出和非扇出两种情况下,仍然需要考虑聚合函数的精度问题。然而,在我们将要看到的问题类型上有一个细微的差别。
No fanout:可以信任主表上的聚合函数,但不一定信任联接表上的聚合函数。
fanout:不一定要信任主表或联接表上的聚合函数。
回顾之前的示例,扇出的原因: customer 表只有三行,但是连接的表有四行。由于我们处于扇出状态,我们不能相信聚合函数将在主表(customer)上工作。正如我们前面看到的,SUM (visits)
将给我们一个10的值,尽管实际上只有8次访问。
为了说明这一点,让我们看看以前的例子。
在我们正在查看的示例中,主表(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,这是收集到的正确数额。
如果我们想避免扇出,理解这三种不同类型的连接是很重要的。
如果主表中的一行只与联接表中的一行匹配,那么就是一对一联接。这种类型的连接不会导致扇出,聚合函数将是准确的,无论您在哪里使用它们。
例如: 假设您有一个人员表和一个 DNA 表。因为只有一个人可以与一个 DNA 记录匹配,所以这是一对一的连接。
如果主表中的许多行与联接表中的同一行匹配,那么就是多对一联接。这种类型的连接也不会导致扇出,聚合函数在主表上至少是准确的。
例如: 假设您有一个 person 表和一个 state-of-residence表。因为许多人可以生活在一个状态,所以这是一个多对一的连接。
如果主表中的一行可以与联接表中的多行匹配,那么就是一对多联接。这种类型的连接可能导致扇出,聚合函数在任何地方都不一定准确。
例如: 假设您有一个 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
都可以正常工作。
如果将 SUM
和 COUNT
这样的聚合函数用于已联接的表,则可能出现错误行为。
如果连接具有一对一的关系,那么聚合函数就可以很好地工作。
如果连接具有多对一关系,则聚合函数将在主表上工作,但可能不在已连接的表上工作。
如果连接具有一对多关系,则聚合函数可能无法在任何地方工作。
如果需要联接具有一对多关系的表,并且希望使用聚合函数,请首先尝试对联接的表进行分组GROUP
,以便它们与主表具有一对一关系。