【问题标题】:对层次较深的数据进行分组时如何处理 SQL 聚合函数
【发布时间】:2021-12-27 15:49:56
【问题描述】:

鉴于以下情况:

我有一个包含 5 个表的数据库:

  1. 货币(iso_number,iso_code),
  2. 产品(ID、名称、当前价格),
  3. 销售(id、time_of_sale、currency_items_sold_in),
  4. sale_lines(id、sale_id、product_id、price_paid、数量),
  5. cash_transactions(id、sale_id、receed_currency_id、converted_currency_id、receed_amount、converted_amount)

该设置允许存储客户最初提供的货币种类,内部兑换的货币以及原始金额和兑换(转换)的金额。

我希望能够找到符合特定条件(时间段、卖家、商店)等的所有销售((为简单起见,省略)。

对于所有这些销售,我将加入相关数据,即 sale_lines 和 cash_transactions。现在 sale_lines 上的货币始终与相关销售上的货币相匹配。 但是,对于 cash_transactions,received_amount/received_currency 可能与销售时的货币不同。尽管converted_currency/converted_amount 存储在cash_transaction 行中,但它应该跟随销售。

当我尝试对某些字段执行 SUM 时出现问题,当您开始加入一对多关系然后执行像 SUM 这样的聚合函数时,即使您在幕后指定了正确的 GROUP BY,SQL Server 仍然会对如果我们不使用 GROUP BY,则显示数据所需的重复行。

这里也描述了这个问题: https://wikido.isoftdata.com/index.php/The_GROUPing_pitfall

按照上面文章中的解决方案,在我的情况下,我应该将每次销售的汇总结果加入到外部查询中。

但是当 sale_lines 货币与销售匹配时,我该怎么办,但 cash_transactions 货币可能与销售不同。

我尝试创建以下 SQL Fiddle,它插入一些测试数据并突出显示问题:http://sqlfiddle.com/#!17/54a7b/15

在小提琴中,我创建了 2 个销售,其中的商品以丹麦克朗(208)和 752(瑞典克朗)出售。 第一次销售有2条销售线,2笔现金交易,第一笔交易直接是DKK => DKK,第二笔交易是SEK => DKK。

第二次销售也有2条销售线,2笔现金交易,第一笔交易是NOK => DKK,第二笔交易直接是DKK => DKK。

在小提琴的最后一个查询中,可以观察到 total_received_amount 是伪造的,因为它是 DKK、SEK 和 NOK 的混合,并没有提供太多价值。

我想要关于如何正确获取数据的建议,我不在乎我是否必须在服务器端 (PHP) 上执行额外的“逻辑”以便对一些数据进行重复数据删除,只要总和是正确的。

非常感谢任何建议。

来自 FIDDLE 的 DDL

CREATE TABLE currency (
  iso_number CHARACTER VARYING(3) PRIMARY KEY,
  iso_code CHARACTER VARYING(3)
);

INSERT INTO currency(iso_number, iso_code) VALUES ('208','DKK'), ('752','SEK'), ('572','NOK');

CREATE TABLE product (
  id SERIAL PRIMARY KEY,
  name CHARACTER VARYING(12),
  current_price INTEGER
);

INSERT INTO product(id,name,current_price) VALUES (1,'icecream',200), (2,'sunglasses',300);

CREATE TABLE sale (
  id SERIAL PRIMARY KEY,
  time_of_sale TIMESTAMP,
  currency_items_sold_in CHARACTER VARYING(3)
);

INSERT INTO sale(id, time_of_sale, currency_items_sold_in) 
VALUES 
(1, CURRENT_TIMESTAMP, '208'),
(2, CURRENT_TIMESTAMP, '752')
;

CREATE TABLE sale_lines (
  id SERIAL PRIMARY KEY,
  sale_id INTEGER,
  product_id INTEGER,
  price_paid INTEGER,
  quantity FLOAT
);

INSERT INTO sale_lines(id, sale_id, product_id, price_paid, quantity)
VALUES 
(1, 1, 1, 200, 1.0),
(2, 1, 2, 300, 1.0),

(3, 2, 1, 100, 1.0),
(4, 2, 1, 100, 1.0)
;
        


CREATE TABLE cash_transactions (
  id SERIAL PRIMARY KEY,
  sale_id INTEGER,
  received_currency_id CHARACTER VARYING(3),
  converted_currency_id CHARACTER VARYING(3),
  received_amount INTEGER,
  converted_amount INTEGER
);


INSERT INTO cash_transactions(id, sale_id, received_currency_id, converted_currency_id, received_amount, converted_amount)
VALUES
(1, 1, '208', '208', 200, 200),
(2, 1, '752', '208', 400, 300),

(3, 2, '572', '208', 150, 100),
(4, 2, '208', '208', 100, 100)
;

来自 FIDDLE 的查询

--SELECT * FROM currency;
--SELECT * FROM product;
--SELECT * FROM sale;
--SELECT * FROM sale_lines;
--SELECT * FROM cash_transactions;


--- Showing the sales with duplicated lines to 
--- fit joined data for OneToMany SaleLines, and OneToMany cash transactions.
SELECT *
FROM sale s
LEFT JOIN sale_lines sl ON sl.sale_id = s.id
LEFT JOIN cash_transactions ct ON ct.sale_id = s.id;



--- Grouping the data by important identifier "currency_items_sold_in".
--- The SUM of sl.price_paid is wrong as it SUMS the duplicated lines as well.
SELECT 
  s.currency_items_sold_in, 
  SUM(sl.price_paid) as "price_paid"
FROM sale s
LEFT JOIN sale_lines sl ON sl.sale_id = s.id
LEFT JOIN cash_transactions ct ON ct.sale_id = s.id
GROUP BY s.currency_items_sold_in;

--- To solve this the SUM can be joined via the "Monkey-Poop" method.
--- Here the problem arises, the SUMS for cash_transaction.received_amount and cash_transaction.converted_amount cannot be relied upon
--- As those fields themselves rely on cash_transaction.received_currency_id and cash_transaction.converted_currency_id
SELECT 
  s.currency_items_sold_in, 
  SUM(sale_line_aggregates.price_paid) as "total_price_paid",
  SUM(cash_transaction_aggregates.converted_amount) as "total_converted_amount",
  SUM(cash_transaction_aggregates.received_amount) as "total_received_amount"
FROM sale s
LEFT JOIN (
  SELECT 
    sale_id,
    SUM(price_paid) AS price_paid
  FROM sale_lines
  GROUP BY sale_id
) AS sale_line_aggregates ON sale_line_aggregates.sale_id = s.id

LEFT JOIN (
  SELECT
    sale_id,
    SUM(converted_amount) as converted_amount,
    SUM(received_amount) as received_amount
  FROM cash_transactions
  GROUP BY sale_id
) AS cash_transaction_aggregates ON cash_transaction_aggregates.sale_id = s.id
GROUP BY s.currency_items_sold_in;

【问题讨论】:

    标签: php sql postgresql aggregate aggregate-functions


    【解决方案1】:

    您可以计算子查询中按货币分组的每个金额。 然后加入他们的货币。

    使用 CTE,您可以确保每个子查询使用相同的销售额。

    WITH CTE_SALE AS (
      SELECT
       id as sale_id, 
       currency_items_sold_in AS iso_number
      FROM sale
    )
    SELECT curr.iso_code AS currency
    , COALESCE(line.price_paid, 0)  as total_price_paid
    , COALESCE(received.amount, 0)  as total_received_amount
    , COALESCE(converted.amount, 0) as total_converted_amount
    FROM currency AS curr
    LEFT JOIN (
      SELECT s.iso_number
      , SUM(sl.price_paid) AS price_paid
      FROM sale_lines sl
      JOIN CTE_SALE s ON s.sale_id = sl.sale_id
      GROUP BY s.iso_number
    ) AS line 
      ON line.iso_number = curr.iso_number
    LEFT JOIN (
      SELECT tr.received_currency_id as iso_number
      , SUM(tr.received_amount) AS amount
      FROM cash_transactions tr
      JOIN CTE_SALE s ON s.sale_id = tr.sale_id
      GROUP BY tr.received_currency_id
    ) AS received
      ON received.iso_number = curr.iso_number
    LEFT JOIN (
      SELECT tr.converted_currency_id as iso_number
      , SUM(tr.converted_amount) AS amount
      FROM cash_transactions AS tr
      JOIN CTE_SALE s ON s.sale_id = tr.sale_id
      GROUP BY tr.converted_currency_id
    ) AS converted
      ON converted.iso_number = curr.iso_number;
    
    货币 | total_price_paid | total_received_amount | total_converted_amount :------- | ---------------: | --------------------: | ---------------------: 丹麦克朗 | 500 | 300 | 700 瑞典克朗 | 200 | 400 | 0 挪威克朗 | 0 | 150 | 0

    db小提琴here

    【讨论】:

    • 我相信这可以解决问题,非常巧妙。我很惊讶一旦给出直接答案,它看起来是多么“简单”,但我想这就是这类问题的本质。我相信我刚刚从中了解到 CTE 何时有用。并给了我优化其他代码的想法。非常感谢!
    • @Velreine 谢谢。好吧,一旦我发现货币是真正的猴子而数量是大便,这很容易。 ;) 但是 CTE 就像子查询的模板。因此,数据库可能决定每次使用它时都运行相同的 CTE。所以明智地使用。如果速度太慢,临时表可能是一个有效的替代方案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-01-20
    • 2010-09-08
    • 2021-12-12
    • 1970-01-01
    • 1970-01-01
    • 2020-06-29
    相关资源
    最近更新 更多