訂單報表原本對得上,加入商品數量與付款金額後,營業額卻突然變多。這時先別改訂單資料,也別急著在SUM裡加DISTINCT。常見原因是同一筆訂單有多件商品、也有多筆付款;兩張明細表直接JOIN,會讓彼此的資料重複出現。
修正的關鍵是讓每張明細表先變成「一筆訂單一列」,再連回訂單表。下面用三筆虛構訂單比較原始總額、錯誤查詢與修正查詢,連沒有商品或付款的訂單也一起保留。
先算出三張表各自的答案
這組範例只放在自有測試資料庫。第一、第二筆訂單刻意使用相同金額120,第三筆為80;這樣才能看出按金額去重會漏算。items代表商品明細,qty是數量;payments代表付款紀錄,其amount與訂單表的amount是不同統計。
CREATE TABLE orders (
id INT PRIMARY KEY,
amount DECIMAL(10,2) NOT NULL
);
CREATE TABLE items (
id INT PRIMARY KEY,
order_id INT NOT NULL,
qty INT NOT NULL
);
CREATE TABLE payments (
id INT PRIMARY KEY,
order_id INT NOT NULL,
amount DECIMAL(10,2) NOT NULL
);
INSERT INTO orders VALUES
(1,120), (2,120), (3,80);
INSERT INTO items VALUES
(11,1,2), (12,1,1), (21,2,1);
INSERT INTO payments VALUES
(31,1,60), (32,1,60), (33,2,120);
單看orders,三筆訂單總額是320;items的商品總數是4;payments的付款總額是240。第三筆訂單還沒有商品或付款明細,因此不能把訂單總額320與已付款240當成同一個數字。真實網站還要定義取消、退款與付款狀態,這裡先把連接造成的重複算清楚。
SELECT COUNT(*) AS orders_n,
SUM(amount) AS order_total
FROM orders;
SELECT SUM(qty) AS qty_total FROM items;
SELECT SUM(amount) AS paid_total FROM payments;
兩張明細一起JOIN,第一筆訂單變成四列
把訂單、商品與付款直接連起來,看起來只是多加一張表,但第一筆訂單的兩列商品會分別配到兩列付款,形成2×2的四種組合。這四列都有訂單金額120;商品數量及付款金額也各自出現了兩次。
SELECT o.id AS order_id,
i.id AS item_id,
p.id AS payment_id,
o.amount, i.qty,
p.amount AS paid
FROM orders AS o
LEFT JOIN items AS i ON i.order_id = o.id
LEFT JOIN payments AS p ON p.order_id = o.id
ORDER BY o.id, i.id, p.id;
第一筆訂單的組合依序是商品11/付款31、商品11/付款32、商品12/付款31、商品12/付款32。第二筆只有一列商品與一列付款,因此只出現一列。第三筆沒有明細,LEFT JOIN仍留下它,右側欄位為NULL。整份結果共六列,已不是三筆訂單各一列。
先攤開明細ID,比只盯著最後的SUM容易找出問題。訂單1沒有多賣四次,付款31也沒有多收一次;重複的是查詢結果。若你要的報表每列代表一筆訂單,就不能直接把這六列當成六筆訂單計算。
SELECT COUNT(*) AS joined_rows,
SUM(o.amount) AS order_total,
SUM(i.qty) AS qty_total,
SUM(p.amount) AS paid_total
FROM orders AS o
LEFT JOIN items AS i ON i.order_id = o.id
LEFT JOIN payments AS p ON p.order_id = o.id;
這段得到六列、訂單總額680、商品總數7、付款總額360。第一筆訂單的120被加了四次;兩筆60的付款也各被加了兩次。即使最後加上GROUP BY o.id,只是把四列收進同一組,組內的重複金額仍會被加總。
SUM(DISTINCT 金額)會把不同訂單一起去重
MySQL的SUM(DISTINCT expr)依表達式的值去重,不會替你辨認訂單ID。本例訂單1與訂單2都付出一個120的訂單金額;它們是不同訂單,卻會被當成同一個金額值。把上一段的SUM(o.amount)改成下式,結果只有200:
SUM(DISTINCT o.amount) AS order_total
120只算一次,再加80,得到200,並不是應有的320。同樣地,訂單1的兩筆付款都是60;若對付款金額做DISTINCT,也會漏掉一筆真實付款。只用金額彼此不同的測試資料,這種錯誤可能暫時看不出來。
先按訂單ID彙總,再接回訂單表
商品表先依order_id合計qty,付款表先依order_id合計amount。兩個子查詢各自保證同一訂單只有一列,再LEFT JOIN回orders,商品與付款就不會互相放大。子查詢後的別名i、p也要保留,讓外層能指出欄位來自哪一份結果。
SELECT o.id, o.amount,
COALESCE(i.qty_total, 0) AS qty_total,
COALESCE(p.paid_total, 0) AS paid_total
FROM orders AS o
LEFT JOIN (
SELECT order_id, SUM(qty) AS qty_total
FROM items
GROUP BY order_id
) AS i ON i.order_id = o.id
LEFT JOIN (
SELECT order_id, SUM(amount) AS paid_total
FROM payments
GROUP BY order_id
) AS p ON p.order_id = o.id
ORDER BY o.id;
結果回到三列:訂單1為120、數量3、付款120;訂單2為120、數量1、付款120;訂單3為80、數量0、付款0。這裡的COALESCE把沒有對應彙總列的NULL顯示成0,意思是這份報表沒有找到明細,不是替原始資料補值。如果你的業務要把「尚未付款」與「付款紀錄金額為0」分開顯示,應保留額外狀態或筆數。
要整份報表的總計,可以對同一個連接結果加總。orders仍是一筆訂單一列,所以訂單金額各算一次;兩張明細已各自彙總,也不會被另一張明細重複展開。
SELECT COUNT(*) AS orders_n,
SUM(o.amount) AS order_total,
SUM(COALESCE(i.qty_total, 0)) AS qty_total,
SUM(COALESCE(p.paid_total, 0)) AS paid_total
FROM orders AS o
LEFT JOIN (
SELECT order_id, SUM(qty) AS qty_total
FROM items GROUP BY order_id
) AS i ON i.order_id = o.id
LEFT JOIN (
SELECT order_id, SUM(amount) AS paid_total
FROM payments GROUP BY order_id
) AS p ON p.order_id = o.id;
本例得到三筆訂單、320、4、240,與三張表各自的答案一致。若外層篩選後一筆訂單都沒有,SUM仍可能回傳NULL;前面的COALESCE處理的是每列明細缺值,不能保證空集合的總計自動變0。只有在產品定義空報表應顯示0時,才再包COALESCE(SUM(…), 0)。
套到網站報表時,篩選範圍也要對齊
報表若只算成功付款,把付款狀態條件放進付款彙總的子查詢;若要某段日期的付款,日期條件也在那裡定義。外層按訂單建立日期篩選,回答的是該段期間建立的訂單;付款按入帳日期篩選,回答的是該段期間收到的款項。兩者可以同時顯示,但欄位名稱要讓使用者分得清楚。
分組鍵必須能識別你要連接的單位。本例訂單ID全表唯一,所以使用order_id;若正式資料要連同商店ID才能唯一識別訂單,就要在子查詢分組與JOIN條件裡一起保留。只按一個可能重複的訂單編號分組,會先把不同訂單混在一起,後面再正確JOIN也救不回來。
修完後,用同金額的兩筆訂單、一筆含多商品與多付款的訂單,以及沒有明細的訂單各查一次。確認結果每列代表的單位、各表單獨總計與報表總計,再處理資料量與索引。這組小型案例能說明計算邏輯,不能替你的正式資料證明查詢效能。
如果你遇到的是沒有付款的訂單整列消失,另看LEFT JOIN右表條件放ON還是WHERE;那是篩選位置的問題,與本篇兩張明細互相放大的問題不同。
範例於2026年10月5日使用MySQL 8.4.9與虛構資料執行;金額欄位使用DECIMAL(10,2)。

評論0