【问题标题】:How to group multiple subqueries in the same output row如何在同一输出行中对多个子查询进行分组
【发布时间】:2016-03-11 15:56:59
【问题描述】:

我正在尝试从两个已经存在的表中提取交易详情:

  1. transactions,包含收到的总金额,

  2. bills,交易中收到的每张钞票都有一行,并包含钞票的面额。

两者都使用共同的session id 进行索引。 [更正: 只有transactions 表在session id 上被索引。]

我已加入表格并进行了子查询,以计算每笔交易中每张钞票面额的数量(有多少 10、20 等)。我想为每笔交易获取一条记录,所有计数都在同一行。

我做到了这个查询:

SELECT
t.session,
to_char(t.amount::numeric, '"$"9990D99') AS "USD",
(select count(b.denom) where b.denom = '50' ) AS "50",
(select count(b.denom) where b.denom = '20') AS "20",
(select count(b.denom) where b.denom = '10') AS "10",
(select count(b.denom) where b.denom = '5') AS "5",
(select count(b.denom) where b.denom = '1') AS "1"

FROM transactions AS t JOIN bills AS b USING (session)
GROUP BY
t.session, t.amount, b.denom
ORDER BY 
t.session,
b.denom ASC;

...它正确地给了我钞票的数量,但每个面额都有一行:

   session    |    USD    | 50 | 20 | 10 | 5 | 1
--------------+-----------+----+----+----+---+----
 c64af32f1815 | $  135.00 |    |    |    | 1 |
 c64af32f1815 | $  135.00 |    |    |  1 |   |
 c64af32f1815 | $  135.00 |    |  6 |    |   |
 643e096b6542 | $  175.00 |    |    |    |   | 10
 643e096b6542 | $  175.00 |    |    |    | 1 |
 643e096b6542 | $  175.00 |    |  8 |    |   |
 ce7d2c647eff | $  200.00 |  4 |    |    |   |

我想要的是这样,每笔交易一行:

   session    |    USD    | 50 | 20 | 10 | 5 | 1
--------------+-----------+----+----+----+---+----
 c64af32f1815 | $  135.00 |    |  6 |  1 | 1 |
 643e096b6542 | $  175.00 |    |  8 |    | 1 | 10
 ce7d2c647eff | $  200.00 |  4 |    |    |   |

我需要了解什么才能修复此查询?

修改后的查询(遵循@erwin 的建议以避免子查询):

SELECT
t.session,
to_char(t.amount::numeric, '"$"9990D99') AS "USD",
COUNT(NULLIF(b.denom = '100', FALSE)) AS "100",
COUNT(NULLIF(b.denom = '50', FALSE)) AS "50",
COUNT(NULLIF(b.denom = '20', FALSE)) AS "20",
COUNT(NULLIF(b.denom = '10', FALSE)) AS "10",
COUNT(NULLIF(b.denom = '5', FALSE)) AS "5",
COUNT(NULLIF(b.denom = '1', FALSE)) AS "1"

FROM transactions AS t JOIN bills AS b USING (session)
GROUP BY
t.session, t.amount, b.denom
ORDER BY 
t.session,
b.denom ASC;

此查询仍会为每个聚合(计数)函数调用生成一行输出。

【问题讨论】:

  • 谢谢大家的建议。其中两个答案解决了手头的直接问题,并且似乎是类似的解决方案。 @erwin 的建议允许我将子查询层的数量从 2 减少到 1,但是如果没有子查询,我仍然无法实现我想要的。在选择一个作为答案之前,我想更深入地探索这种可能性。而且我仍然想了解为什么结果不是我的预期......为什么每个聚合函数调用都会在输出中生成一个完整的新行(即使我删除了第二级子查询)?
  • 表定义应该在问题中 - 理想情况下是完整的CREATE TABLE 脚本。我们需要查看数据类型和约束。还有你的 Postgres 版本。 numeric 的演员提出了问题。您的示例中存在不一致之处:您说ORDER BY t.session ...,但所需的结果显示了不同的排序顺序。我已经强调过,你不能在你的GROUP BY 中包含b.denom,这是剩下的主要问题。
  • 这里的上下文是我试图从一个不是我要更改的数据库中提取报告。对查询和输出进行了轻微编辑/简化,以专注于手头的问题——不一致仅由此产生。强制转换为数字是不必要的,并且是复制/粘贴错误。 (感谢您指出这一点。)
  • 好的。所有三个答案都给出了我想要的结果。 (我的一些困惑最终是自己造成的。)我选择@erwin's 作为消除子查询的最佳选择。感谢大家的帮助。

标签: sql postgresql aggregate


【解决方案1】:

不要使用相关子查询。那是低效的。
并且不要GROUP BY 子句中包含b.denom。这是你的主要错误。

Postgres 9.4+

在 Postgres 9.4 或更高版本中使用专用聚合 FILTER 功能:

SELECT t.session
     , to_char(t.amount::numeric, '"$"9990D99') AS "USD"
     , count(*) FILTER (WHERE b.denom = '50')   AS "50"  -- !
     , count(*) FILTER (WHERE b.denom = '20')   AS "20"  -- !
     , ...
FROM   ...
GROUP  BY t.session, t.amount  -- !
ORDER  BY ...

解释和更多链接:

Postgres 9.3-

对于旧版本(Postgres 9.3 或更早版本),有多种(不太优雅的)替代方案:

SELECT t.session
     , to_char(t.amount::numeric, '"$"9990D99') AS "USD"  -- why cast to numeric?
     , count(b.denom = '100' OR NULL) AS "100"  -- bad column name
     , count(b.denom =  '50' OR NULL) AS "50"
     , count(b.denom =  '20' OR NULL) AS "20"
     , count(b.denom =  '10' OR NULL) AS "10"
     , count(b.denom =   '5' OR NULL) AS "5"
     , count(b.denom =   '1' OR NULL) AS "1"
FROM   transactions t
JOIN   bills b USING (session)
GROUP  BY t.session, t.amount
ORDER  BY t.session;

清理后:

SELECT t.session, to_char(t.amount, '"$"9990D99') AS usd
     , d100, d50, d20, d10, d5, d1
FROM   transactions t
LEFT   JOIN (
   SELECT session
        , nullif(count(denom = 100 OR NULL), 0) AS d100
        , nullif(count(denom =  50 OR NULL), 0) AS d50
        , nullif(count(denom =  20 OR NULL), 0) AS d20
        , nullif(count(denom =  10 OR NULL), 0) AS d10
        , nullif(count(denom =   5 OR NULL), 0) AS d5
        , nullif(count(denom =   1 OR NULL), 0) AS d1
   FROM   bills
   GROUP  BY 1
   ) b USING (session)
ORDER  BY session;

发生了什么变化?

  • 如果可以避免,不要使用非法标识符,那么你也不需要双引号。

  • 假设integer 代表bills.denom,所以我们也不需要在常量周围加上单引号。

  • 您似乎希望在没有找到账单的列中出现NULL。将结果包装在NULLIF()

  • 由于您检索整个表,因此在您加入之前进行聚合会更快。

SQL Fiddle.

更多技巧和解释:

更快的选择:crosstab()

为了获得最佳性能,请使用带有crosstab() 的实际交叉表。您需要安装附加模块tablefunc如果您不熟悉,请先阅读本文

SELECT t.session, to_char(t.amount, '"$"9990D99') AS usd
     , d100, d50, d20, d10, d5, d1
FROM   transactions t
LEFT   JOIN crosstab(
     $$SELECT session, denom, count(*)::int
       FROM   bills
       GROUP  BY 1,2
       ORDER  BY 1$$
   , $$SELECT unnest ('{100,50,20,10,5,1}'::int[])$$
   ) AS ct (session text, d100 int, d50 int, d20 int, d10 int, d5 int, d1 int)
     USING (session);

【讨论】:

  • 当前运行的是 9.3.11 版本,所以 9.4 / FILTER 建议要等到我们可以升级到 9.4 之后。 FILTER 看起来是一个更优雅的解决方案,在处理子查询后非常有吸引力。
  • 在 9.3 版中:删除单个 SELECT 并将其替换为 SUM((b.denom = '100')::int) AS "100" 显然是朝着正确方向迈出的一步。但这仍然使每个教派都各行其道。如果我从 GROUP BY 子句中删除 b.denom,我会得到 ERROR: column "b.denom" must appear in the GROUP BY clause...。是否仍然需要一个子查询来将行减少到每个事务一个?还是我错过了什么?
  • 是的。 “清理后”查询按我的预期工作。感谢您推动我超越多个子查询。
【解决方案2】:

你可以添加查询全局吗:

SELECT
session,
"USD",
sum("50") AS "50",
sum("20") AS "20",
sum("10") AS "10",
sum("5") AS "5",
sum("1") AS "1"

from (SELECT
t.session,
to_char(t.amount::numeric, '"$"9990D99') AS "USD",
(select count(b.denom) where b.denom = '50' ) AS "50",
(select count(b.denom) where b.denom = '20') AS "20",
(select count(b.denom) where b.denom = '10') AS "10",
(select count(b.denom) where b.denom = '5') AS "5",
(select count(b.denom) where b.denom = '1') AS "1"

FROM transactions AS t JOIN bills AS b USING (session)
GROUP BY
t.session, t.amount, b.denom
ORDER BY 
t.session,
b.denom ASC)youre_query
GROUP BY session,"USD"

【讨论】:

  • 是的,这确实产生了我想要的结果。但我不明白“查询全局”或“全局查询”指的是什么。你能解释一下这个术语或解释的链接吗?
  • 表示查询你的查询结果
【解决方案3】:

我想你快到了。您需要在查询后进行另一个分组。例如:

WITH q1 as (SELECT
    t.session,
    to_char(t.amount::numeric, '"$"9990D99') AS "USD",
    (select count(b.denom) where b.denom = '50' ) AS "50",
    (select count(b.denom) where b.denom = '20') AS "20",
    (select count(b.denom) where b.denom = '10') AS "10",
    (select count(b.denom) where b.denom = '5') AS "5",
    (select count(b.denom) where b.denom = '1') AS "1"

    FROM transactions AS t JOIN bills AS b USING (session)
    GROUP BY t.session, t.amount, b.denom)

SELECT session, "USD", SUM("50") AS "50", SUM("20") AS "20", SUM("10") AS "10",
       SUM("5") AS "5", SUM("1") AS "1"
FROM q1
GROUP BY session, "USD"

【讨论】:

  • 我尝试按照您的建议修改查询,但它没有将 USD 识别为第二个 SELECT 中的有效列名。我收到了ERROR: column "usd" does not exist。如果我改为使用 t.amount 或 trnasactions.amount 来引用它,我会收到ERROR: missing FROM-clause entry for table "t"。所以,无法完成这项工作。
  • 您是否尝试用“USD”修改美元的两种出现?我编辑了我的答案。
  • 是的。使用双引号中的“USD”,这会产生我想要的结果。
猜你喜欢
  • 1970-01-01
  • 2022-11-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-06
相关资源
最近更新 更多