GROUP BY と集計で合計・件数を出す
行を眺めるだけでなく、「カテゴリ別の売上」「顧客ごとの注文数」を一発で出します。products · customers · orders を用意し、COUNT / SUM / AVG と GROUP BY、あとで効いてくる HAVING まで進めます。
目次
準備
cd $DB_SQL/PostgreSQL_setupdocker compose up -ddocker compose exec db psql -U practice -d practice_dbCREATE TABLE IF NOT EXISTS products ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL, price INT NOT NULL);CREATE TABLE IF NOT EXISTS customers ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE);CREATE TABLE IF NOT EXISTS orders ( id SERIAL PRIMARY KEY, customer_id INT NOT NULL REFERENCES customers(id), product_id INT NOT NULL REFERENCES products(id), qty INT NOT NULL CHECK (qty > 0), ordered_at DATE NOT NULL DEFAULT CURRENT_DATE);TRUNCATE orders, customers, products RESTART IDENTITY;INSERT INTO products (name, category, price) VALUES ('ノート A5', 'stationery', 300), ('ボールペン', 'stationery', 120), ('USB-C ケーブル', 'gadget', 980), ('ワイヤレスマウス', 'gadget', 2500);INSERT INTO customers (name, email) VALUES ('山田', 'yamada@example.com'), ('佐藤', 'sato@example.com'), ('鈴木', 'suzuki@example.com');INSERT INTO orders (customer_id, product_id, qty, ordered_at) VALUES (1, 1, 2, '2026-08-01'), (1, 3, 1, '2026-08-02'), (2, 2, 5, '2026-08-03'), (2, 4, 1, '2026-08-03');集計関数だけ使う
表全体に対して:
SELECT COUNT(*) AS order_count FROM orders;SELECT SUM(qty) AS total_qty FROM orders;SELECT AVG(price)::NUMERIC(10, 2) AS avg_price FROM products;SELECT MIN(price) AS cheapest, MAX(price) AS priciest FROM products;COUNT(*) は行数、COUNT(列) は NULL 以外の件数、という違いだけ覚えておくと後で楽です。
GROUP BY
カテゴリごとの商品数と平均価格:
SELECT category, COUNT(*) AS item_count, ROUND(AVG(price), 1) AS avg_priceFROM productsGROUP BY categoryORDER BY category;SELECT に書く非集計の列は、基本的に GROUP BY にも入れます。入れ忘れると PostgreSQL はエラーにします(これがむしろ親切です)。
JOIN してから集計
顧客ごとの注文件数と購入個数:
SELECT c.name, COUNT(o.id) AS orders, COALESCE(SUM(o.qty), 0) AS total_qtyFROM customers cLEFT JOIN orders o ON o.customer_id = c.idGROUP BY c.id, c.nameORDER BY c.id;鈴木さんは注文 0 件です。LEFT JOIN + COUNT(o.id) にしているので、顧客行は残しつつ件数が 0 になります。COUNT(*) にすると、注文が無くても 1 と数えやすいので注意です。LEFT JOIN 後の数え方は、一度やると忘れにくいです。
商品別の売上(qty × price):
SELECT p.name, SUM(o.qty) AS sold_qty, SUM(o.qty * p.price) AS salesFROM orders oJOIN products p ON o.product_id = p.idGROUP BY p.id, p.nameORDER BY sales DESC;HAVING
WHERE は集計前の行に効き、HAVING は集計後のグループに効きます。
売上が 1000 以上の商品だけ:
SELECT p.name, SUM(o.qty * p.price) AS salesFROM orders oJOIN products p ON o.product_id = p.idGROUP BY p.id, p.nameHAVING SUM(o.qty * p.price) >= 1000ORDER BY sales DESC;「先に絞ってから集めたい」なら WHERE、「集めた結果で絞りたい」なら HAVING、と切り分けると迷いにくいです。
よくあるつまずき
| 症状 | 対処 |
|---|---|
must appear in the GROUP BY clause |
SELECT の素の列を GROUP BY に足す |
| 鈴木が消える | JOIN が INNER。顧客軸なら LEFT JOIN |
HAVING と WHERE の違いが曖昧 |
集計前 / 後で使い分ける。同じ条件を両方に書かない |
| 小数が長い | ROUND や ::NUMERIC(10,2) で表示を整える |
ワンライナー例:
docker compose exec db psql -U practice -d practice_db -c \ "SELECT category, COUNT(*) FROM products GROUP BY category;"WHERE と HAVING の前後関係が腹落ちすれば、集計の基本は押さえられています。