【问题标题】: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 列中为所有不同的用户返回相同的金额,而不是为每个用户返回正确的金额。

任何帮助将不胜感激。

【问题讨论】:

    标签: sql ruby-on-rails database postgresql performance


    【解决方案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
      

      【讨论】:

        猜你喜欢
        • 2021-09-14
        • 1970-01-01
        • 1970-01-01
        • 2020-09-15
        • 2021-07-08
        • 1970-01-01
        • 2018-09-13
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多