如何在表达式中多次使用 COUNT() 实现 OVER?

Posted

技术标签:

【中文标题】如何在表达式中多次使用 COUNT() 实现 OVER?【英文标题】:How do I implement OVER using COUNT() multiple times in an expression? 【发布时间】:2020-11-02 19:53:17 【问题描述】:

我有一个关于我正在写的查询的问题,以解决来自LeetCode 的问题。问题来了:

广告

+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| ad_id         | int     |
| user_id       | int     |
| action        | enum    |
+---------------+---------+

(ad_id, user_id) 是这个表的主键。

此表的每一行都包含广告的 ID、用户的 ID 和 该用户对该广告采取的行动。动作栏是 一个 ENUM 类型('Clicked', 'Viewed', 'Ignored')。

一家公司正在投放广告并希望计算 每个广告。

使用点击率 (CTR) 衡量广告的效果 其中:

CTR = 0 如果没有广告点击,则广告点击次数/(广告点击次数 + 广告浏览量)否则

编写一个 SQL 查询来查找每个广告的 ctr。

将 ctr 舍入到小数点后 2 位。按 ctr in 对结果表进行排序 降序排列,如果出现平局,则按 ad_id 升序排列。

查询结果格式如下例:

广告表:

+-------+---------+---------+
| ad_id | user_id | action  |
+-------+---------+---------+
| 1     | 1       | Clicked |
| 2     | 2       | Clicked |
| 3     | 3       | Viewed  |
| 5     | 5       | Ignored |
| 1     | 7       | Ignored |
| 2     | 7       | Viewed  |
| 3     | 5       | Clicked |
| 1     | 4       | Viewed  |
| 2     | 11      | Viewed  |
| 1     | 2       | Clicked |
+-------+---------+---------+

这是一个fiddle,其中包含示例数据和我尝试的解决方案。尝试的解决方案转载如下:

SELECT DISTINCT t.ad_id, ROUND(
    IF(
        COUNT(c.ad_id) OVER (PARTITION BY t.ad_id) = 0, 
        0,
      COUNT(c.ad_id) OVER (PARTITION BY t.ad_id) * 100 / ( COUNT(c.ad_id) OVER (PARTITION BY t.ad_id) + COUNT(v.ad_id) OVER (PARTITION BY t.ad_id) )
    ), 2) as ctr
FROM Ads as t
LEFT JOIN Ads as c ON c.ad_id=t.ad_id AND c.user_id=t.user_id AND c.action='Clicked'
LEFT JOIN Ads as v ON v.ad_id=t.ad_id AND v.user_id=t.user_id AND v.action='Viewed'
GROUP BY t.ad_id, c.ad_id, v.ad_id
ORDER BY ctr DESC, t.ad_id

此查询的结果:

ad_id   ctr
1   50.00
2   50.00
3   50.00
5   0.00

正确的结果应该是:

ad_id ctr
1, 66.67
3, 50.00
2, 33.33
5, 0.00

通过查看示例数据,我的猜测是 COUNT() 实际上并没有像我预期的那样按 t.ad_id 进行分区。 50% CTR 结果可以通过我的 CTR 计算来解释,计算计算中的所有“点击”和所有“查看”实例。 (另一方面,删除 CTR 计算中的 OVER 语句 - 只是计算,而不是条件 - 不会产生上述结果,正如我的假设所暗示的那样。所以我不确定这一点。)

我使用 OVER 的方式有问题吗?我的逻辑有问题吗?

另外,我有一个额外的问题:我在这里选择使用 JOIN,因为我假设 JOIN 比使用子查询更快。这是一个公平的假设吗?我正在为 Data Analyst 1 面试而学习 - 你认为面试官是否会关心我是否使用 JOIN 与子查询?

编辑:感谢 forpas 的解释,我能够想出一个比我原来的更简单的解决方案。我认为 forpas 在下面的回答中的解决方案可能仍然更可取,因为它明确处理表中的 NULL。

SELECT ad_id, ROUND(IF(
    SUM(action='Clicked') = 0,
    0,
    SUM(action='Clicked') * 100 / ( SUM(action='Clicked') + SUM(action='Viewed'))
), 2) as ctr
FROM Ads
GROUP BY ad_id
ORDER BY ctr DESC, ad_id

【问题讨论】:

【参考方案1】:

您可以使用条件聚合来做到这一点:

SELECT ad_id,
  ROUND(100 * COALESCE(SUM(action = 'Clicked') / SUM(action IN ('Clicked', 'Viewed')), 0), 2) ctr
FROM Ads
GROUP BY ad_id
ORDER BY ctr DESC, ad_id;

您可以使用SUM() 窗口函数获得相同的结果,但我不认为这对性能或可读性更好:

SELECT DISTINCT ad_id,
  ROUND(
    100 * 
    COALESCE(
      SUM(action = 'Clicked') OVER (PARTITION BY ad_id) / 
      SUM(action IN ('Clicked', 'Viewed')) OVER (PARTITION BY ad_id)
      , 0
    )
    , 2
  ) ctr
FROM Ads
ORDER BY ctr DESC, ad_id;

请参阅demo。 结果:

> ad_id |   ctr
> ----: | ----:
>     1 | 66.67
>     3 | 50.00
>     2 | 33.33
>     5 |  0.00

【讨论】:

感谢您的回答!很高兴了解 COALESCE 功能,我会检查一下。您对为什么我写的 CTR 表达式不能按预期工作有任何想法吗? 你把事情复杂化了。在此处查看:dbfiddle.uk/… 您的联接的结果集是什么。 感谢您的链接。我认识到我的解决方案可能不是最简单的。我真的很想知道如何让它发挥作用,主要是因为我看到了一个场景,在这种场景中,理解我的方法为什么不起作用可以在面试场景中真正帮助我。大多数情况下,如果我在类似的问题上犯了类似的错误。我真的很感激能深入了解为什么我的特定语法不会导致正确的计算。你有什么想法吗? 您的查询的问题是您使用了 GROUP BY。如果你删除它,你会得到正确的结果:dbfiddle.uk/… 但正如我所说,这不是要走的路。 呵呵,解决了。你知道为什么 GROUP BY 会这样改变结果吗? GROUP BY 是否改变了表格的内容?我以为它只是改变了行的顺序。我认为这不会影响 JOINed 表中的 COUNT()【参考方案2】:
SELECT t1.ad_id,
(CASE 
 WHEN t2.clicked/t1.total IS NULL THEN 0 
 ELSE round((t2.clicked/t1.total)*100,2) END) as ctr 
FROM
(SELECT ad_id,SUM(CASE WHEN action IN ('Viewed','Clicked') THEN 1 ELSE 0 END) as total
FROM Ads
GROUP BY 1
)t1
LEFT JOIN 
(SELECT ad_id,SUM(CASE WHEN action IN('Clicked') THEN 1 ELSE 0 END) as clicked
FROM Ads
GROUP BY 1)t2
ON t1.ad_id = t2.ad_id
ORDER BY 2 DESC, ad_id;

【讨论】:

以上是关于如何在表达式中多次使用 COUNT() 实现 OVER?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用SQL语句,实现多条件分组统计

如何在 Pyspark 中基于正则表达式条件验证(和删除)列,而无需多次扫描和改组?

技术博客

如何使用正则表达式进行多次替换?

如何使用正则表达式组查找多次出现?

辖郭腻胚呻鬓巍凛棺髓dpj9ov