【问题标题】:Order a query by the sum of two "has_many" sub-tables?按两个“has_many”子表的总和排序查询?
【发布时间】:2012-12-16 01:51:45
【问题描述】:

在我的应用程序中,Invoice has_many item_numbersInvoice has_many payments。每张发票都有一个余额,它是 ItemNumber 金额属性的总和,减去 Payment amount 属性的总和。

在发票模型中余额很容易计算,但我正在尝试编写一个按余额对发票进行排序的查询,事实证明这在 ActiveRecord/SQL 中更难做到。

我已成功通过以下查询订购了 item_numbers 总数的发票(感谢 Daniel Rikowski):

Invoice.where(user_id: 1, deleted: false, status: 'Sent')
       .joins(:item_numbers)
       .select('invoices.*, sum(item_numbers.amount)')
       .group('invoices.id')
       .order('sum(item_numbers.amount) asc')
       .limit(20)

我已尝试通过以下方式将其扩展为按余额排序;

Invoice.where(user_id: 1, deleted: false, status: 'Sent')
       .joins(:item_numbers)
       .joins("FULL OUTER JOIN payments ON payments.invoice_id = invoices.id")
       .select("invoices.*, sum(item_numbers.amount_with_gst) - COALESCE(sum(payments.amount), 0)")
       .group("invoices.id")
       .order("sum(item_numbers.amount_with_gst) - COALESCE(sum(payments.amount), 0) #{dir}")/

这个查询有两个问题。首先,它非常丑陋,其次,它不起作用。我在付款表上使用了完整的外部联接,因为并非所有发票都有付款,如果我只使用 joins(:payments) 任何没有付款的发票,都会从结果中排除。 COALESCE 用于处理空金额。

查询接近,但假设有 3 个 item_numbers 和 1 个付款(非常典型的场景),付款金额将被减去 3 倍,导致余额远低于实际金额(通常为负余额)。

我可能很清楚我的深度。我在这个查询中付出了很多努力(大约 4 小时的阅读和失败的尝试)并且不能完全确定它。我的数据库是 PostgreSQL。

【问题讨论】:

    标签: sql ruby-on-rails postgresql activerecord join


    【解决方案1】:

    不确定 AR 语法,但正确的查询应该是:

    SELECT i.*, COALESCE(n.total, 0) - COALESCE(p.total, 0) AS balance
    FROM   invoices i
    LEFT   JOIN (
        SELECT invoice_id, sum(amount) AS total
        FROM   payments
        GROUP  BY invoice_id 
        ) p ON p.invoice_id = i.id
    LEFT   JOIN (
        SELECT invoice_id, sum(amount_with_gst) AS total
        FROM   item_numbers
        GROUP  BY invoice_id 
        ) n ON n.invoice_id = i.id
    WHERE  i.user_id = 1
    AND    i.deleted = false
    AND    i.status = 'Sent'
    ORDER  BY balance;
    

    如果将两个has_many 表连接到基表,这些行会相互相乘,从而导致完全任意的结果。您可以通过聚合总计之前加入基表来解决这个问题。

    另外,我在您的查询中没有看到item_numbers 的连接条件。这将导致 cross join - 除了非常错误之外,还非常昂贵。 (或者 AR 是否足够聪明,可以自动从外键关系导出连接条件?如果是,为什么要在第二个表上使用连接条件?)假设item_numbers 有一个 invoice_id 列比如payments,我修改了。

    【讨论】:

    • 非常感谢。如果我将它放入 Invoice.find_by_sql() 中,您的答案确实有效。 item_numbers 上没有连接条件,因为 AR 在这种情况下会进行内部连接 ​​(Invoice.joins(:item_numbers) => SELECT "invoices".* FROM "invoices" INNER JOIN "item_numbers" ON "item_numbers"."invoice_id" = “发票”。“身份证”)。我选择了另一个答案,因为它包含 ActiveRecord 解决方案,这是我在代码中使用的。但是,您的回答很有教育意义。
    【解决方案2】:

    您的问题是由列相乘引起的。想象一下,有一个 Payment 和三个 Item_numbers 属于 Invoice。常规连接的结果是这样的:

    |发票.id | item_number.amount |付款金额 | | 1 | 4 | 5 | | 1 | 7 | 5 | | 1 | 2 | 5 |

    因此,sum(payment.amount) 将返回 15 而不是 5。要获得正确的总和,您必须直接获取总和:

    Invoice.select('invoices.id, (SELECT SUM(item_numbers.amount) from item_numbers WHERE item_numbers.invoice_id = invoices.id) - (SELECT COALESCE(SUM(payments.amount),0) from payments WHERE payments.invoice_id = invoices.id) AS balance').group('invoices.id')
    

    【讨论】:

    • 这非常有效,而且还具有让我很容易理解的额外优势。我什至修改了一些现有的搜索查询,以相同的方式进行排序。 .group('invoices.id') 似乎不再需要了(大概是因为没有加入)。谢谢。
    • @brad:请注意 - 对于结果中的多行 - 相关子查询通常比 (LEFT) JOIN 慢得多,就像我演示的那样。有些人甚至称其为反模式。使用EXPLAIN ANALYZE 测试差异。
    • 感谢您的提醒。我会再做一些调查。我的数据库很小,我的查询也很小,所以它不应该对我造成太大影响(现在......)。
    猜你喜欢
    • 1970-01-01
    • 2021-03-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-18
    相关资源
    最近更新 更多