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

Using subqueries to sum an associated model's column returns the same amount for all parents

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

发票有 statusamount_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 列中为所有不同的用户返回相同的金额,而不是为每个用户返回正确的金额。

如有任何帮助,我们将不胜感激。

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

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

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

具有横向连接的解决方案:

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 和 window 函数的解决方案:

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

您可以使用标量子查询

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;

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

  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