【SQL】徹底解析 GROUP BY 語句的用法!靈活地彙總資料
不論是網站訪問分析、電商平台的銷售分析,還是應用程式的用戶行為追蹤,「彙總資料」是處理資料時不可或缺的一環。你是否覺得,如果能夠快速取得像是「依類別統計商品數量」、「依都道府縣統計用戶數量」、「每月銷售總額」等資訊,會非常方便呢?SQL 的 GROUP BY 語句正是實現這些需求的利器。
GROUP BY 是一個強大的工具,可以根據特定欄位(欄)值,將資料表中的資料分組,並對每個群組套用彙總函數(例如 COUNT、SUM、AVG 等)。雖然一開始看起來有些困難,但只要掌握用法,你的資料分析能力將會大幅提升。
本文將以初學者、希望成為網頁創作者的讀者為對象,從 GROUP BY 的基本用法開始,延伸到 HAVING 語句與多欄位分組等進階技巧,搭配大量「可直接複製使用的程式碼」進行詳細解說。現在就一起體驗「將資料分組並進行彙總」的樂趣吧!
準備:建立分析用的範例資料
學習 SQL 最有效的方法就是實際動手操作。本篇文章將使用名為 products 的資料表,模擬一間虛構的小型電商平台的商品清單。以下 SQL 指令將建立該資料表並插入範例資料。文中所有 SQL 操作皆以此表為基礎。
-- 若資料表已存在則刪除(可重複執行)
DROP TABLE IF EXISTS products;
-- 建立 products 資料表
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
category TEXT NOT NULL,
prefecture TEXT NOT NULL,
price INTEGER NOT NULL,
stock_quantity INTEGER NOT NULL
);
-- 插入資料
INSERT INTO products (id, name, category, prefecture, price, stock_quantity) VALUES
(1, '超棒的 T 恤', '服飾', '東京', 3000, 50),
(2, '極佳牛仔褲', '服飾', '岡山', 12000, 30),
(3, '魔法鍵盤', '電腦周邊', '東京', 8500, 20),
(4, '會發光的滑鼠', '電腦周邊', '神奈川', 4500, 45),
(5, '匠人菜刀', '廚房用品', '新潟', 9800, 15),
(6, '究極平底鍋', '廚房用品', '新潟', 6200, 25),
(7, '初學者 SQL', '書籍', '東京', 2800, 100),
(8, '網頁設計入門', '書籍', '東京', 3200, 80),
(9, '舒適運動鞋', '服飾', '東京', 7800, 60);
這個資料表包含商品 ID、商品名稱、類別、產地(都道府縣)、價格與庫存數量等資訊。特別設計了重複的資料內容,例如「服飾」類別有 3 項、「東京」產地有 4 項,方便用來實作 GROUP BY 彙總操作。
【基础】使用 `GROUP BY` 统计每个类别的商品数量
首先来看 GROUP BY 最基本的用法。“每个商品类别下有多少种商品?”我们将使用 GROUP BY 按照 category 列进行分组,并用常见的 COUNT() 函数统计每个分组的行数。
SELECT
category,
COUNT(*) AS product_count
FROM
products
GROUP BY
category;
执行结果:
category | product_count
-------------|---------------
PC周边设备 | 2
服饰 | 3
厨房用品 | 2
书籍 | 2
成功获取了每个类别的商品数量!SQL 引擎在内部会首先根据 category 列的值(如 '服饰'、'PC周边设备' 等)将行进行分组,然后对每个分组执行 COUNT(*)。同时,通过 AS product_count 为结果列指定了别名,这是一个重点。
【进阶1】结合 `SUM`、`AVG` 等其他聚合函数使用
GROUP BY 的强大之处在于它不仅能和 COUNT() 一起使用,还能灵活搭配 SUM()(总和)、AVG()(平均)、MAX()(最大值)、MIN()(最小值)等聚合函数。
接下来我们来计算“每个类别的平均价格”。
SELECT
category,
AVG(price) AS average_price
FROM
products
GROUP BY
category;
执行结果:
category | average_price
-------------|---------------
PC周边设备 | 6500
服饰 | 7600
厨房用品 | 8000
书籍 | 3000
只需更换聚合函数,就能分析每个分组的不同方面。例如想知道“每个类别的总库存数量”,只需使用 SUM(stock_quantity)。
SELECT
category,
SUM(stock_quantity) AS total_stock
FROM
products
GROUP BY
category;
执行结果:
category | total_stock
-------------|-------------
PC周边设备 | 65
服饰 | 140
厨房用品 | 40
书籍 | 180
这样,我们可以一目了然地看出“书籍”类别的库存最多,“厨房用品”最少。
【进阶2】使用 `HAVING` 子句筛选聚合结果
在使用 GROUP BY 进行聚合之后,常常会遇到“希望进一步通过条件筛选结果”的情况。例如,“只显示商品数量在3个以上的分类”。
此时需要注意,普通的 WHERE 子句是用来在聚合之前筛选单条记录,因此不能用于像 COUNT(*) 这样的聚合结果。
`HAVING` 子句的作用就是在聚合之后对分组结果添加条件。
SELECT
category,
COUNT(*) AS product_count
FROM
products
GROUP BY
category
HAVING
COUNT(*) >= 3;
执行结果:
category | product_count
---------|---------------
服饰 | 3
由于添加了 HAVING COUNT(*) >= 3 条件,最终结果只返回了商品数量为3的“服饰”分类。
`WHERE` 是在分组之前使用,`HAVING` 是在分组之后使用。务必记住这一区别。
【进阶3】按多列进行分组
GROUP BY 还可以通过多个列的组合来进行更细致的分组。例如,如果你想了解“每个分类与产地组合下的商品数量”,就可以轻松实现。
只需在 GROUP BY 子句中使用逗号分隔多个列名即可。
SELECT
category,
prefecture,
COUNT(*) AS product_count
FROM
products
GROUP BY
category, prefecture;
执行结果:
category | prefecture | product_count
-------------|------------|---------------
PC周边设备 | 神奈川 | 1
PC周边设备 | 东京 | 1
服饰 | 冈山 | 1
服饰 | 东京 | 2
厨房用品 | 新潟 | 2
书籍 | 东京 | 2
可以看到,“来自冈山的服饰”与“来自东京的服饰”被视为不同的分组。通过这种方式可以进行更深入的分析,例如“东京的服饰商品有2种”。
[实践环节] 在浏览器中自由执行 SQL!
理论够了!现在是你亲自编写 SQL、运行并验证结果的时间。
请完整复制以下 HTML 代码,将其保存为类似 sql_groupby_test.html 的文件,并使用 Web 浏览器打开。您将获得一个内置 products 表的专属 SQL 执行环境。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SQL GROUP BY 执行环境</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.7; color: #333; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
h1 { color: #2c3e50; }
textarea { width: 100%; height: 180px; font-family: "SF Mono", "Consolas", monospace; font-size: 16px; padding: 12px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; margin-bottom: 1rem; }
button { background-color: #3498db; color: white; border: none; padding: 12px 22px; font-size: 16px; border-radius: 6px; cursor: pointer; transition: background-color 0.2s; }
button:hover { background-color: #2980b9; }
button:disabled { background-color: #bdc3c7; cursor: not-allowed; }
#result-container { margin-top: 2rem; border: 1px solid #ddd; padding: 1rem; border-radius: 6px; background: #fdfdfd; min-height: 50px; }
#error-message { color: #e74c3c; font-weight: bold; }
table { border-collapse: collapse; width: 100%; margin-top: 1rem; }
th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
th { background-color: #f2f2f2; }
tr:nth-child(even) { background-color: #f9f9f9; }
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/sql-wasm.js"></script>
</head>
<body>
<h1>在浏览器中尝试 SQL!</h1>
<p>在下方文本区域输入 SQL 语句,然后点击“Execute”按钮。试试本文介绍的各种 SQL 查询吧!</p>
<textarea id="sql-input">-- 按省/都道府县显示平均价格和最高价格
SELECT
prefecture,
AVG(price) AS "Average Price",
MAX(price) AS "Max Price"
FROM
products
GROUP BY
prefecture;</textarea>
<button id="execute-btn">Execute</button>
<div id="result-container">
<p id="error-message"></p>
<div id="result-output"></div>
</div>
<script>
const sqlInput = document.getElementById('sql-input');
const executeBtn = document.getElementById('execute-btn');
const errorMsg = document.getElementById('error-message');
const resultOutput = document.getElementById('result-output');
let db;
async function initDb() {
executeBtn.disabled = true;
executeBtn.textContent = '初始化数据库中…';
try {
const SQL = await initSqlJs({
locateFile: file => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/${file}`
});
db = new SQL.Database();
const setupSql = `
DROP TABLE IF EXISTS products;
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
category TEXT NOT NULL,
prefecture TEXT NOT NULL,
price INTEGER NOT NULL,
stock_quantity INTEGER NOT NULL
);
INSERT INTO products (id, name, category, prefecture, price, stock_quantity) VALUES
(1, '超赞T恤', '服装', '东京', 3000, 50),
(2, '最佳牛仔裤', '服装', '冈山', 12000, 30),
(3, '魔法键盘', '电脑配件', '东京', 8500, 20),
(4, '发光鼠标', '电脑配件', '神奈川', 4500, 45),
(5, '匠人菜刀', '厨房用品', '新潟', 9800, 15),
(6, '终极煎锅', '厨房用品', '新潟', 6200, 25),
(7, 'SQL入门', '书籍', '东京', 2800, 100),
(8, '网页设计基础', '书籍', '东京', 3200, 80),
(9, '舒适运动鞋', '服装', '东京', 7800, 60);
`;
db.run(setupSql);
executeBtn.disabled = false;
executeBtn.textContent = 'Execute';
resultOutput.innerHTML = '<p>准备就绪!欢迎随意尝试 SQL。</p>';
} catch (err) {
errorMsg.textContent = '数据库初始化失败: ' + err.message;
console.error(err);
}
}
function executeSql() {
if (!db) return;
const sql = sqlInput.value;
errorMsg.textContent = '';
resultOutput.innerHTML = '';
try {
const results = db.exec(sql);
if (results.length === 0) {
resultOutput.innerHTML = '<p>查询执行成功,但未返回结果(例如:INSERT、UPDATE)。</p>';
return;
}
results.forEach(result => {
const table = document.createElement('table');
const thead = document.createElement('thead');
const tbody = document.createElement('tbody');
const headerRow = document.createElement('tr');
result.columns.forEach(colName => {
const th = document.createElement('th');
th.textContent = colName;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
result.values.forEach(row => {
const bodyRow = document.createElement('tr');
row.forEach(cellValue => {
const td = document.createElement('td');
td.textContent = cellValue === null ? 'NULL' : (typeof cellValue === 'number' ? cellValue.toLocaleString() : cellValue);
bodyRow.appendChild(td);
});
tbody.appendChild(bodyRow);
});
table.appendChild(thead);
table.appendChild(tbody);
resultOutput.appendChild(table);
});
} catch (err) {
errorMsg.textContent = 'SQL 错误: ' + err.message;
console.error(err);
}
}
executeBtn.addEventListener('click', executeSql);
initDb();
</script>
</body>
</html>
【挑战题】
- 尝试显示每个都道府县(
prefecture)的商品数量。 - 筛选价格在 5,000 日元以上的商品后(提示:使用
WHERE子句),按类别统计商品数量。 - 使用
HAVING子句找出库存量(stock_quantity)少于 30 件的类别。
初学者常犯的错误:`SELECT` 子句的规则
使用 GROUP BY 时,初学者最容易犯的错误是:“SELECT 子句中只能写出现在 GROUP BY 中的列,或是包裹在聚合函数中的列”。
例如,以下 SQL 会报错:
-- 这是会报错的示例
SELECT
category,
name -- ← 既没有在 GROUP BY 中,也没有使用聚合函数
FROM
products
GROUP BY
category;
为什么会报错?其实是很合理的。GROUP BY category 将“服装”类别合并成一行时,name 列中可能包含“超棒T恤”“最佳牛仔裤”“舒适运动鞋”这三项,SQL 引擎不知道该显示哪一个,因此返回错误。
使用 GROUP BY 后,每一行代表原始多行数据的一个组合。因此 SELECT 中只能包含分组列(如 category),或整个组的聚合结果(如 COUNT(*)、SUM(price) 等)。
相关技巧:使用 `ORDER BY` 排序结果
你可能经常希望将 GROUP BY 的结果按某种顺序排列。这时可以使用 ORDER BY 子句,它应写在 GROUP BY 和 HAVING 之后。
例如,将“每个类别的商品数量”按数量从多到少(降序)排列:
SELECT
category,
COUNT(*) AS product_count
FROM
products
GROUP BY
category
ORDER BY
product_count DESC;
执行结果:
category | product_count
-----------|---------------
服装 | 3
电脑配件 | 2
厨房用品 | 2
图书 | 2
使用 ORDER BY product_count DESC,可以按 product_count 列从大到小排序。这样就能一眼看出哪个类别的商品最多。
总结
本文介绍了用于灵活汇总数据的强大工具——GROUP BY 子句。
- 基本: 使用
GROUP BY 列名可对数据进行分组。 - 聚合: 搭配
COUNT、SUM、AVG等聚合函数提取每组的特征。 - 筛选: 对分组结果设置条件时使用
HAVING子句。 - 多列分组: 可使用
GROUP BY 列1, 列2进行更精细的分组。 - 规则:
SELECT中只能使用GROUP BY中的列或聚合函数的结果。
掌握 GROUP BY 后,不仅能查询数据,还能洞察隐藏在数据中的趋势和含义,迈入“数据分析”的领域。请务必在本文的体验环节尝试多种聚合操作,亲身感受其强大功能。它将为你的 Web 创作技能增添一项“数据汇总能力”。