使用子查询对关联模型的列求和会为所有父项返回相同的数量

Posted

技术标签:

【中文标题】使用子查询对关联模型的列求和会为所有父项返回相同的数量【英文标题】:Using subqueries to sum an associated model's column returns the same amount for all parents 【发布时间】:2022-01-12 16:00:45 【问题描述】:

给定模型UserInvoice,一个用户有很多张发票,一张发票属于一个用户。

发票有 statusamount_cents 列。

我需要编写一个查询来获取所有用户列,但还要添加以下列:

total_paid 别名列对每个用户的所有 paid 发票中的 amount_cents 求和 total_unpaid 别名列,用于汇总每个用户的所有 unpaid 发票的 amount_cents

在使用我为其分配别名的多个子查询时,我有点不知道正确的结构是什么,但我已经为任务的第一部分提出了一些非常基本的东西:

select users.*, (SELECT SUM(amount_cents) FROM invoices) as total_paid from users
join invoices on users.id = invoices.user_id
where invoices.status = 'paid'
group by users.id

我不确定我是否应该从父方或子方编写查询(我想是从父(用户)方,因为我需要的所有数据都在用户列中)但上面的查询似乎是在 total_paid 列中为所有不同的用户返回相同的金额,而不是为每个用户返回正确的金额。

任何帮助将不胜感激。

【问题讨论】:

【参考方案1】:

这可以使用子查询来完成,如下所示:

Select users.id,
       (Select Sum(amount_cents) 
        From invoices Where status = 'paid' And user_id=users.id) As total_paid,
       (Select Sum(amount_cents)
        From invoices Where status = 'unpaid' And user_id=users.id) As total_unpaid
From users
Group by users.id

【讨论】:

【参考方案2】:

语句 (SELECT SUM(amount_cents) FROM invoices) 返回所有用户的总金额,这与您想要的每个用户的金额不同:

横向连接的解决方案:

select u.*
    , paid.total as total_paid
    , unpaid.total as total_unpaid
 FROM users AS u
 LEFT JOIN LATERAL
    ( SELECT sum(amount_cents) AS total
        FROM invoices
       WHERE user_id = u.id
         AND status = 'paid'
    ) AS paid
   ON True
 LEFT JOIN LATERAL
    ( SELECT sum(amount_cents) AS total
        FROM invoices
       WHERE user_id = u.id
         AND status = 'unpaid'
    ) AS unpaid
   ON True

使用 JOIN 和窗口函数的解决方案:

SELECT u.*
     , t.total_paid
     , t.total_unpaid
  FROM users AS u
 INNER JOIN 
(
SELECT DISTINCT ON (user_id)
     , user_id
     , sum(amount_cents) FILTER (WHERE status = 'paid') OVER (PARTITION BY user_id  ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS total_paid
     , sum(amount_cents) FILTER (WHERE status = 'unpaid') OVER (PARTITION BY user_id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS total_unpaid
  FROM invoices
 ORDER BY u.user_id
) AS t
ON u.id = t.user_id

【讨论】:

谢谢。尽管我选择了另一个答案作为接受的答案,因为它更简单、更容易转换为 ActiveRecord 语句,但您的解决方案帮助我理解了这两个概念。窗口函数解决方案返回了一个语法错误,但我无法修复。【参考方案3】:

您可以使用标量子查询

select u.*, 
       (select sum(amount_cents) from invoices where user_id = u.id and status = 'paid') total_paid,
       (select sum(amount_cents) from invoices where user_id = u.id and status = 'unpaid') total_unpaid
from users u;

或横向连接可能更有效。

select u.*, l.*
from users u
left join lateral 
(
  select sum(amount_cents) filter (where status = 'paid') total_paid,
         sum(amount_cents) filter (where status = 'unpaid') total_unpaid
  from invoices where user_id = u.id
) l on true;

如果 users.id 是主键(可能是这种情况),那么事情可以简化为

select u.*, 
       sum(i.amount_cents) filter (where i.status = 'paid') total_paid,
       sum(i.amount_cents) filter (where i.status = 'unpaid') total_unpaid 
from users u
left outer join invoices i on u.id = i.user_id
group by u.id;

【讨论】:

我认为最后一个解决方案可能是最干净的解决方案,尽管我可能会将total_unpaid 更改为filter (where i.status <> 'paid'),因为问题中未指定未付款的状态。 @engineersmnky 公平点。【参考方案4】:

另一种选择是使用外部连接

  users_table = User.arel_table
  paid_invoices_table = Arel::Table.new(Invoice.arel_table.name, as: 'paid_invoices')
  unpaid_invoices_table = Arel::Table.new(Invoice.arel_table.name, as: 'unpaid_invoices')

  paid_join = Arel::Nodes::OuterJoin.new(
    paid_invoices_table,
    Arel::Nodes::On.new(
      users_table[:id].eq(paid_invoices_table[:user_id])
        .and(paid_invoices_table[:status].eq('paid'))
    )
  )

  unpaid_join = Arel::Nodes::OuterJoin.new(
    unpaid_invoices_table,
    Arel::Nodes::On.new(
      users_table[:id].eq(unpaid_invoices_table[:user_id])
        .and(unpaid_invoices_table[:status].not_eq('paid'))
    )
  )

  User.joins(paid_join,unpaid_join)
    .select(
       User.arel_table[Arel.star],
       paid_invoices_table[:amount_cents].sum.as('total_paid'), 
       unpaid_invoices_table[:amount_cents].sum.as('total_unpaid'))   
    .group(:id)

查询结果:

SELECT 
  users.*,
  SUM(paid_invoices.amount_cents) AS total_paid,
  SUM(unpaid_invoices.amount_cents) AS total_unpaid
FROM 
  users 
  LEFT OUTER JOIN invoices AS paid_invoices ON users.id = paid_invoices.user_id
    AND paid_invoices.status = 'paid' 
  LEFT OUTER JOIN invoices AS unpaid_invoices ON users.id = unpaid_invoices.user_id
    AND unpaid_invoices.status <> 'paid'
GROUP BY 
  users.id

【讨论】:

以上是关于使用子查询对关联模型的列求和会为所有父项返回相同的数量的主要内容,如果未能解决你的问题,请参考以下文章

sql查询优化总结ing

SQL Server 递归自联接

sql中in和exist语句的区别?

MySQL子查询

使用子查询添加具有不同 where 子句的列

SQL 不能使用从 group by 中的子查询返回的列