MochiuWiki : SUSE, EC, PCB
検索
個人用ツール
ログイン
Toggle dark mode
名前空間
ページ
議論
表示
閲覧
ソースを閲覧
履歴を表示
MySQL - GROUP BYのソースを表示
提供: MochiuWiki : SUSE, EC, PCB
←
MySQL - GROUP BY
あなたには「このページの編集」を行う権限がありません。理由は以下の通りです:
この操作は、次のグループのいずれかに属する利用者のみが実行できます:
管理者
、new-group。
このページのソースの閲覧やコピーができます。
== 概要 == <code>GROUP BY</code> 句は、指定した列の値が同じ行をグループ化し、各グループごとに集約関数を適用するために使用される。<br> <br> <code>GROUP BY</code> 句を使用することにより、部署別の平均給与、カテゴリ別の売上合計、日別の注文数等、データを集約して分析することができる。<br> 集約関数 (<code>COUNT</code>、<code>SUM</code>、<code>AVG</code>、<code>MAX</code>、<code>MIN</code> 等) と組み合わせて使用することが一般的である。<br> <br> <code>GROUP BY</code> 句は、<code>WHERE</code> 句の後、<code>HAVING</code> 句の前に記述される。<br> <code>WHERE</code> 句がグループ化前の個別行に対する条件指定であるのに対し、<code>HAVING</code> 句はグループ化後の集計結果に対する条件指定である。<br> <br> MySQL 8.0以降では、<code>GROUP BY</code> の挙動にいくつかの重要な変更がある。<br> MySQL 5.7以前では <code>GROUP BY</code> による暗黙的なソートが行われていたが、MySQL 8.0以降ではこの挙動が廃止され、明示的に <code>ORDER BY</code> を指定する必要がある。<br> また、<code>WITH ROLLUP</code> 修飾子により小計や総計行を生成できるようになり、<code>GROUPING()</code> 関数でスーパーアグリゲート行を識別できる。<br> <br> パフォーマンスの観点では、<code>GROUP BY</code> 列にインデックスを作成することで、ルースインデックススキャンやタイトインデックススキャンといった効率的な実行戦略が使用される。<br> インデックスがない場合は一時テーブルと <code>filesort</code> が使用されるため、パフォーマンスが低下する可能性がある。<br> <br><br> == 基本構文 == ==== GROUP BY句の基本構文 ==== GROUP BY句の基本構文は以下の通りである。<br> <br> <syntaxhighlight lang="mysql"> # 基本構文 SELECT column1, aggregate_function(column2) FROM table_name GROUP BY column1; # 複数列でのグループ化 SELECT column1, column2, aggregate_function(column3) FROM table_name GROUP BY column1, column2; </syntaxhighlight> <br> GROUP BY句の実行順序は以下の通りである。<br> <br> <syntaxhighlight lang="mysql"> # 実行順序 FROM table_name # 1. テーブルから行を取得 WHERE condition # 2. グループ化前のフィルタリング GROUP BY column1 # 3. グループ化 HAVING condition # 4. グループ化後のフィルタリング ORDER BY column1 # 5. 並び替え LIMIT n; # 6. 行数制限 </syntaxhighlight> <br> 単一列でのグループ化の例を以下に示す。<br> <br> <syntaxhighlight lang="mysql"> # 部署別の従業員数を取得 SELECT department_id, COUNT(*) AS employee_count FROM employees GROUP BY department_id; # カテゴリ別の商品数を取得 SELECT category_id, COUNT(*) AS product_count FROM products GROUP BY category_id; # ステータス別の注文数を取得 SELECT status, COUNT(*) AS order_count FROM orders GROUP BY status; </syntaxhighlight> <br> 複数列でのグループ化の例を以下に示す。<br> <br> <syntaxhighlight lang="mysql"> # 部署と役職の組み合わせごとの従業員数 SELECT department_id, job_title, COUNT(*) AS employee_count FROM employees GROUP BY department_id, job_title; # 年と月の組み合わせごとの売上合計 SELECT YEAR(order_date) AS year, MONTH(order_date) AS month, SUM(amount) AS total_sales FROM orders GROUP BY YEAR(order_date), MONTH(order_date); # カテゴリとサプライヤーの組み合わせごとの商品数 SELECT category_id, supplier_id, COUNT(*) AS product_count FROM products GROUP BY category_id, supplier_id; </syntaxhighlight> <br> ==== WHERE句との組み合わせ ==== <code>WHERE</code> 句と <code>GROUP BY</code> 句を組み合わせることで、グループ化前に行をフィルタリングできる。<br> <br> <syntaxhighlight lang="mysql"> # WHERE句でフィルタリングしてからグループ化 SELECT department_id, COUNT(*) AS employee_count FROM employees WHERE hire_date >= '2024-01-01' GROUP BY department_id; # 特定のステータスの注文のみを集計 SELECT user_id, COUNT(*) AS completed_order_count FROM orders WHERE status = 'completed' GROUP BY user_id; # 価格が1000円以上の商品のカテゴリ別平均価格 SELECT category_id, AVG(price) AS avg_price FROM products WHERE price >= 1000 GROUP BY category_id; </syntaxhighlight> <br> 実行順序の重要性を理解する例を以下に示す。<br> <br> <syntaxhighlight lang="mysql"> # 実行順序 : WHERE → GROUP BY → HAVING SELECT department_id, AVG(salary) AS avg_salary FROM employees WHERE hire_date >= '2024-01-01' # 1. 2024年以降に雇用された従業員のみを対象 GROUP BY department_id # 2. 部署ごとにグループ化 HAVING AVG(salary) > 50000; # 3. 平均給与が50000以上の部署のみを抽出 </syntaxhighlight> <br> ==== 式によるグループ化 ==== 列名だけでなく、関数や式の結果でグループ化することも可能である。<br> <br> <syntaxhighlight lang="mysql"> # YEAR()関数による年別グループ化 SELECT YEAR(order_date) AS year, COUNT(*) AS order_count FROM orders GROUP BY YEAR(order_date); # DATE_FORMAT()関数による月別グループ化 SELECT DATE_FORMAT(order_date, '%Y-%m') AS month, SUM(amount) AS total_sales FROM orders GROUP BY DATE_FORMAT(order_date, '%Y-%m'); # 四半期ごとのグループ化 SELECT YEAR(order_date) AS year, QUARTER(order_date) AS quarter, SUM(amount) AS total_sales FROM orders GROUP BY YEAR(order_date), QUARTER(order_date); # 曜日別のグループ化 SELECT DAYNAME(order_date) AS day_of_week, COUNT(*) AS order_count FROM orders GROUP BY DAYNAME(order_date); # 価格帯別のグループ化 SELECT FLOOR(price / 1000) * 1000 AS price_range, COUNT(*) AS product_count FROM products GROUP BY FLOOR(price / 1000); </syntaxhighlight> <br> <code>EXTRACT()</code> 関数を使用した日時のグループ化も可能である。<br> <br> <syntaxhighlight lang="mysql"> # EXTRACT()関数による年別グループ化 SELECT EXTRACT(YEAR FROM order_date) AS year, COUNT(*) AS order_count FROM orders GROUP BY EXTRACT(YEAR FROM order_date); # 年と月の抽出 SELECT EXTRACT(YEAR FROM order_date) AS year, EXTRACT(MONTH FROM order_date) AS month, SUM(amount) AS total_sales FROM orders GROUP BY EXTRACT(YEAR FROM order_date), EXTRACT(MONTH FROM order_date); </syntaxhighlight> <br><br> == 集約関数 == GROUP BY句と組み合わせて使用される集約関数を以下に示す。<br> <br> ==== COUNT()関数 ==== <code>COUNT()</code> 関数は、グループ内の行数をカウントする。<br> <br> <syntaxhighlight lang="mysql"> # COUNT(*): 全行数をカウント (NULL含む) SELECT department_id, COUNT(*) AS total_employees FROM employees GROUP BY department_id; # COUNT(column): 指定列の非NULL値の数をカウント SELECT department_id, COUNT(manager_id) AS employees_with_manager FROM employees GROUP BY department_id; # COUNT(DISTINCT column): 重複を除外してカウント SELECT department_id, COUNT(DISTINCT job_title) AS unique_job_titles FROM employees GROUP BY department_id; # 複数のCOUNTを組み合わせる SELECT department_id, COUNT(*) AS total_employees, COUNT(manager_id) AS with_manager, COUNT(*) - COUNT(manager_id) AS without_manager FROM employees GROUP BY department_id; </syntaxhighlight> <br> <code>COUNT(*)</code> と <code>COUNT(column)</code> の違いに注意が必要である。<br> <br> <syntaxhighlight lang="mysql"> # COUNT(*) はNULLを含む全行をカウント SELECT status, COUNT(*) AS total_count FROM orders GROUP BY status; # COUNT(column) は非NULL値のみをカウント SELECT status, COUNT(shipped_date) AS shipped_count FROM orders GROUP BY status; # shipped_dateがNULLの行は除外される </syntaxhighlight> <br> ==== SUM()関数とAVG()関数 ==== <code>SUM()</code> 関数は合計値を、<code>AVG()</code> 関数は平均値を計算する。<br> どちらもNULL値は計算から除外される。<br> <br> <syntaxhighlight lang="mysql"> # SUM() : 合計値を計算 SELECT department_id, SUM(salary) AS total_salary FROM employees GROUP BY department_id; # AVG() : 平均値を計算 SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id; # SUMとAVGを組み合わせる SELECT category_id, SUM(amount) AS total_sales, AVG(amount) AS avg_sales, COUNT(*) AS order_count FROM order_items GROUP BY category_id; # 複数列の集計 SELECT user_id, SUM(amount) AS total_spent, AVG(amount) AS avg_order_value, COUNT(*) AS order_count FROM orders GROUP BY user_id; </syntaxhighlight> <br> NULL値の扱いを理解する例を以下に示す。<br> <br> <syntaxhighlight lang="mysql"> # AVG()はNULL値を除外して計算 SELECT department_id, AVG(bonus) AS avg_bonus, COUNT(*) AS total_employees, COUNT(bonus) AS employees_with_bonus FROM employees GROUP BY department_id; # bonusがNULLの従業員は平均計算から除外される # NULL値を0として扱う場合 SELECT department_id, AVG(IFNULL(bonus, 0)) AS avg_bonus_including_zero FROM employees GROUP BY department_id; </syntaxhighlight> <br> ==== MAX()関数とMIN()関数 ==== <code>MAX()</code> 関数は最大値を、<code>MIN()</code> 関数は最小値を取得する。<br> <br> <syntaxhighlight lang="mysql"> # MAX(): 最大値を取得 SELECT department_id, MAX(salary) AS max_salary FROM employees GROUP BY department_id; # MIN(): 最小値を取得 SELECT department_id, MIN(salary) AS min_salary FROM employees GROUP BY department_id; # MAXとMINを組み合わせる SELECT category_id, MAX(price) AS max_price, MIN(price) AS min_price, MAX(price) - MIN(price) AS price_range FROM products GROUP BY category_id; # 日付の最大値と最小値 SELECT user_id, MIN(order_date) AS first_order_date, MAX(order_date) AS last_order_date FROM orders GROUP BY user_id; </syntaxhighlight> <br> 文字列や日付にも使用可能である。<br> <br> <syntaxhighlight lang="mysql"> # 文字列の最大値と最小値 (辞書順) SELECT department_id, MIN(name) AS first_name_alphabetically, MAX(name) AS last_name_alphabetically FROM employees GROUP BY department_id; # 日付の範囲 SELECT YEAR(order_date) AS year, MIN(order_date) AS earliest_order, MAX(order_date) AS latest_order FROM orders GROUP BY YEAR(order_date); </syntaxhighlight> <br> ==== GROUP_CONCAT()関数 ==== <code>GROUP_CONCAT()</code> 関数は、グループ内の値を文字列として連結する。<br> <br> <syntaxhighlight lang="mysql"> # 基本的なGROUP_CONCAT SELECT department_id, GROUP_CONCAT(name) AS employee_names FROM employees GROUP BY department_id; # SEPARATOR句で区切り文字を指定 SELECT department_id, GROUP_CONCAT(name SEPARATOR ', ') AS employee_names FROM employees GROUP BY department_id; # ORDER BY句で並び順を指定 SELECT department_id, GROUP_CONCAT(name ORDER BY name ASC SEPARATOR ', ') AS employee_names FROM employees GROUP BY department_id; # DISTINCTで重複を排除 SELECT user_id, GROUP_CONCAT(DISTINCT product_name ORDER BY product_name SEPARATOR ', ') AS products FROM order_items JOIN products ON order_items.product_id = products.id GROUP BY user_id; </syntaxhighlight> <br> <u><code>GROUP_CONCAT()</code> のデフォルト最大長は1024文字である。</u><br> <u>これは、<code>group_concat_max_len</code> システム変数で変更可能である。</u><br> <br> <syntaxhighlight lang="mysql"> # 現在のgroup_concat_max_lenを確認 SELECT @@group_concat_max_len; # group_concat_max_lenを変更 SET SESSION group_concat_max_len = 10000; # またはクエリごとに設定 SET @old_max_len = @@group_concat_max_len; SET SESSION group_concat_max_len = 10000; SELECT department_id, GROUP_CONCAT(name SEPARATOR ', ') AS employee_names FROM employees GROUP BY department_id; SET SESSION group_concat_max_len = @old_max_len; </syntaxhighlight> <br> 複数列の連結も可能である。<br> <br> <syntaxhighlight lang="mysql"> # 複数列を連結 SELECT department_id, GROUP_CONCAT(CONCAT(name, ' (', job_title, ')') ORDER BY name SEPARATOR ', ') AS employee_info FROM employees GROUP BY department_id; # 列名と値を組み合わせる SELECT order_id, GROUP_CONCAT(CONCAT(product_name, ': ', quantity, '個') SEPARATOR ', ') AS order_details FROM order_items JOIN products ON order_items.product_id = products.id GROUP BY order_id; </syntaxhighlight> <br> ==== JSON_ARRAYAGG()関数 と JSON_OBJECTAGG()関数 ==== MySQL 5.7.22以降では、<code>JSON_ARRAYAGG()</code> と <code>JSON_OBJECTAGG()</code> 関数が使用できる。<br> これらはJSON形式でグループ内の値を集約する。<br> <br> <syntaxhighlight lang="mysql"> # JSON_ARRAYAGG(): JSON配列を生成 SELECT department_id, JSON_ARRAYAGG(name) AS employee_names FROM employees GROUP BY department_id; # 結果例: {"department_id": 1, "employee_names": ["Alice", "Bob", "Charlie"]} # JSON_OBJECTAGG(): JSONオブジェクトを生成 SELECT department_id, JSON_OBJECTAGG(id, name) AS employee_map FROM employees GROUP BY department_id; # 結果例: {"department_id": 1, "employee_map": {"1": "Alice", "2": "Bob"}} # JSON_ARRAYAGG()とORDER BYの組み合わせ SELECT department_id, JSON_ARRAYAGG(name ORDER BY name ASC) AS employee_names_sorted FROM employees GROUP BY department_id; # JSON_ARRAYAGG()で構造化データを生成 SELECT department_id, JSON_ARRAYAGG(JSON_OBJECT('id', id, 'name', name, 'salary', salary)) AS employees FROM employees GROUP BY department_id; </syntaxhighlight> <br> <code>JSON_ARRAYAGG()</code> は、NULL値も含めて配列に追加する。<br> <br> <syntaxhighlight lang="mysql"> # NULL値の扱い SELECT department_id, JSON_ARRAYAGG(manager_id) AS manager_ids FROM employees GROUP BY department_id; # NULLも配列に含まれる: [1, 2, null, 3] # NULL値を除外する場合 SELECT department_id, JSON_ARRAYAGG(manager_id) AS manager_ids FROM employees WHERE manager_id IS NOT NULL GROUP BY department_id; </syntaxhighlight> <br><br> == HAVING句 == <code>HAVING</code> 句は、<code>GROUP BY</code> 句でグループ化された後の結果に対して条件を指定する。<br> <br> ==== HAVING句の基本 ==== <code>HAVING</code> 句は、集約関数を使用した条件指定に使用される。<br> <br> <syntaxhighlight lang="mysql"> # 基本的なHAVING句 SELECT department_id, COUNT(*) AS employee_count FROM employees GROUP BY department_id HAVING COUNT(*) > 5; # 平均給与が50000以上の部署 SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id HAVING AVG(salary) >= 50000; # 合計売上が100000以上のカテゴリ SELECT category_id, SUM(amount) AS total_sales FROM order_items GROUP BY category_id HAVING SUM(amount) >= 100000; </syntaxhighlight> <br> 複数の条件を組み合わせることも可能である。<br> <br> <syntaxhighlight lang="mysql"> # AND条件 SELECT department_id, AVG(salary) AS avg_salary, COUNT(*) AS employee_count FROM employees GROUP BY department_id HAVING AVG(salary) >= 50000 AND COUNT(*) > 10; # OR条件 SELECT category_id, SUM(amount) AS total_sales FROM order_items GROUP BY category_id HAVING SUM(amount) >= 100000 OR COUNT(*) > 100; # 複雑な条件 SELECT user_id, COUNT(*) AS order_count, SUM(amount) AS total_spent FROM orders GROUP BY user_id HAVING COUNT(*) >= 5 AND SUM(amount) > 50000; </syntaxhighlight> <br> ==== WHEREとHAVINGの違い ==== <code>WHERE</code> 句と <code>HAVING</code> 句の違いを理解することが重要である。<br> <br> <center> {| class="wikitable" |+ WHEREとHAVINGの違い ! 項目 !! WHERE句 !! HAVING句 |- | 適用タイミング || グループ化前 || グループ化後 |- | 対象 || 個別の行 || グループ化された結果 |- | 集約関数 || 使用不可 || 使用可能 |- | 実行順序 || GROUP BYの前 || GROUP BYの後 |} </center> <br> 具体的な違いを以下の例で示す。<br> <br> <syntaxhighlight lang="mysql"> # WHERE句 : グループ化前の個別行をフィルタリング SELECT department_id, COUNT(*) AS employee_count FROM employees WHERE salary > 50000 # 給与が50000以上の従業員のみを対象 GROUP BY department_id; # HAVING句 : グループ化後の集計結果をフィルタリング SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id HAVING AVG(salary) > 50000; # 部署の平均給与が50000以上のグループのみを抽出 # WHEREとHAVINGを組み合わせる SELECT department_id, COUNT(*) AS employee_count, AVG(salary) AS avg_salary FROM employees WHERE hire_date >= '2024-01-01' # 1. 2024年以降に雇用された従業員のみを対象 GROUP BY department_id # 2. 部署ごとにグループ化 HAVING COUNT(*) > 5; # 3. 従業員数が5人以上の部署のみを抽出 </syntaxhighlight> <br> <code>WHERE</code> 句では集約関数を使用できない。<br> <br> <syntaxhighlight lang="mysql"> # エラーになる例: WHERE句で集約関数を使用 SELECT department_id, COUNT(*) AS employee_count FROM employees WHERE COUNT(*) > 5 # エラー: Invalid use of group function GROUP BY department_id; # 正しい形式: HAVING句で集約関数を使用 SELECT department_id, COUNT(*) AS employee_count FROM employees GROUP BY department_id HAVING COUNT(*) > 5; </syntaxhighlight> <br> ==== 集約関数を使用した条件指定 ==== <code>HAVING</code> 句では、様々な集約関数を条件として使用できる。<br> <br> <syntaxhighlight lang="mysql"> # COUNT()を使用 SELECT user_id, COUNT(*) AS order_count FROM orders GROUP BY user_id HAVING COUNT(*) >= 10; # SUM()を使用 SELECT category_id, SUM(amount) AS total_sales FROM order_items GROUP BY category_id HAVING SUM(amount) > 100000; # AVG()を使用 SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id HAVING AVG(salary) BETWEEN 50000 AND 80000; # MAX()とMIN()を使用 SELECT category_id, MAX(price) - MIN(price) AS price_range FROM products GROUP BY category_id HAVING MAX(price) - MIN(price) > 10000; # 複数の集約関数を組み合わせる SELECT user_id, COUNT(*) AS order_count, SUM(amount) AS total_spent, AVG(amount) AS avg_order_value FROM orders GROUP BY user_id HAVING COUNT(*) >= 5 AND AVG(amount) > 5000; </syntaxhighlight> <br><br> == MySQL 8.0以降の変更点 == MySQL 8.0以降では、GROUP BY の挙動にいくつかの重要な変更がある。<br> <br> ==== GROUP BYの暗黙ソート廃止 ==== MySQL 5.7以前では、GROUP BYを使用すると自動的に結果がソートされていた。<br> <u>MySQL 8.0以降では、この暗黙的なソートが廃止され、明示的に <code>ORDER BY</code> を指定する必要がある。</u><br> <br> <syntaxhighlight lang="mysql"> # MySQL 5.7以前 : GROUP BYで自動的にソートされる SELECT department_id, COUNT(*) AS employee_count FROM employees GROUP BY department_id; # 結果は暗黙的にdepartment_id順にソートされる # MySQL 8.0以降 : 明示的にORDER BYを指定する必要がある SELECT department_id, COUNT(*) AS employee_count FROM employees GROUP BY department_id ORDER BY department_id; # 集計結果でソートする場合 SELECT department_id, COUNT(*) AS employee_count FROM employees GROUP BY department_id ORDER BY employee_count DESC; </syntaxhighlight> <br> この変更により、不要なソート処理が削減され、パフォーマンスが向上する可能性がある。<br> <br> <syntaxhighlight lang="mysql"> # ソートが不要な場合は、ORDER BYを省略することでパフォーマンス向上 SELECT category_id, COUNT(*) AS product_count FROM products GROUP BY category_id; # 結果の順序は保証されないが、ソート処理が不要 # 必要な場合のみORDER BYを明示 SELECT category_id, COUNT(*) AS product_count FROM products GROUP BY category_id ORDER BY product_count DESC; </syntaxhighlight> <br> ==== ROLLUP修飾子 ==== <code>WITH ROLLUP</code> 修飾子を使用すると、小計と総計を含む結果セットを生成できる。<br> <br> <syntaxhighlight lang="mysql"> # 基本的なROLLUP SELECT department_id, COUNT(*) AS employee_count FROM employees GROUP BY department_id WITH ROLLUP; # 結果には各部署の従業員数と、全体の合計行が含まれる # 複数列でのROLLUP SELECT department_id, job_title, COUNT(*) AS employee_count FROM employees GROUP BY department_id, job_title WITH ROLLUP; # 部署と役職の組み合わせ、部署ごとの小計、全体の総計が生成される # SUMとROLLUPの組み合わせ SELECT category_id, SUM(amount) AS total_sales FROM order_items GROUP BY category_id WITH ROLLUP; # 各カテゴリの売上と、全カテゴリの売上合計が生成される </syntaxhighlight> <br> <code>ROLLUP</code> は階層的な集計を行う。<br> <br> <syntaxhighlight lang="mysql"> # 年と月での階層的集計 SELECT YEAR(order_date) AS year, MONTH(order_date) AS month, SUM(amount) AS total_sales FROM orders GROUP BY year, month WITH ROLLUP; # 結果: # - 各年月の売上 # - 各年の小計 (月がNULL) # - 全体の総計 (年と月がNULL) # 部署、役職、従業員の階層的カウント SELECT department_id, job_title, name, COUNT(*) AS count FROM employees GROUP BY department_id, job_title, name WITH ROLLUP; </syntaxhighlight> <br> ==== GROUPING()関数 ==== <code>GROUPING()</code> 関数は、<code>ROLLUP</code> で生成されたスーパーアグリゲート行を識別する。<br> スーパーアグリゲート行の場合は 1 を、通常行の場合は 0 を返す。<br> <br> <syntaxhighlight lang="mysql"> # GROUPING()関数の基本的な使用 SELECT department_id, COUNT(*) AS employee_count, GROUPING(department_id) AS is_rollup FROM employees GROUP BY department_id WITH ROLLUP; # is_rollupが1の行は総計行 # 複数列でのGROUPING() SELECT YEAR(order_date) AS year, MONTH(order_date) AS month, SUM(amount) AS total_sales, GROUPING(YEAR(order_date)) AS year_rollup, GROUPING(MONTH(order_date)) AS month_rollup FROM orders GROUP BY year, month WITH ROLLUP; # GROUPING()を使用してラベルを追加 SELECT CASE WHEN GROUPING(department_id) = 1 THEN '全部署合計' ELSE CAST(department_id AS CHAR) END AS department, COUNT(*) AS employee_count FROM employees GROUP BY department_id WITH ROLLUP; </syntaxhighlight> <br> <code>GROUPING()</code> は複数列の集計レベルを識別するのに便利である。<br> <br> <syntaxhighlight lang="mysql"> # 集計レベルを識別 SELECT department_id, job_title, COUNT(*) AS employee_count, CASE WHEN GROUPING(department_id) = 1 THEN '総計' WHEN GROUPING(job_title) = 1 THEN '部署別小計' ELSE '詳細' END AS level FROM employees GROUP BY department_id, job_title WITH ROLLUP; </syntaxhighlight> <br> ==== ROLLUPとORDER BYの併用 ==== MySQL 8.0.12以降では、<code>ROLLUP</code> と <code>ORDER BY</code> を併用できる。<br> <br> <syntaxhighlight lang="mysql"> # ROLLUPとORDER BYの併用 (MySQL 8.0.12以降) SELECT department_id, COUNT(*) AS employee_count FROM employees GROUP BY department_id WITH ROLLUP ORDER BY employee_count DESC; # DISTINCTとROLLUPの併用も可能 SELECT DISTINCT department_id, job_title, COUNT(*) AS employee_count FROM employees GROUP BY department_id, job_title WITH ROLLUP; </syntaxhighlight> <br> ==== ONLY_FULL_GROUP_BY SQLモード ==== <code>ONLY_FULL_GROUP_BY</code> SQLモードは、<code>SELECT</code> 句、<code>HAVING</code> 句、<code>ORDER BY</code> 句に記述できる非集計列を制限する。<br> <br> このモードが有効な場合、非集計列はGROUP BY句に含まれている、または、関数従属している必要がある。<br> <br> <syntaxhighlight lang="mysql"> # ONLY_FULL_GROUP_BYモードの確認 SELECT @@sql_mode; # エラーになる例 (ONLY_FULL_GROUP_BY有効時) SELECT department_id, name, COUNT(*) AS employee_count FROM employees GROUP BY department_id; # エラー : nameはGROUP BYに含まれておらず、集約関数でもない # 正しい形式1: nameをGROUP BYに追加 SELECT department_id, name, COUNT(*) AS employee_count FROM employees GROUP BY department_id, name; # 正しい形式2 : nameを集約関数で囲む SELECT department_id, GROUP_CONCAT(name) AS employee_names, COUNT(*) AS employee_count FROM employees GROUP BY department_id; # 正しい形式3 : nameを削除 SELECT department_id, COUNT(*) AS employee_count FROM employees GROUP BY department_id; </syntaxhighlight> <br> 関数従属が成立する場合は、GROUP BYに含まれていない列も使用できる。<br> <br> <syntaxhighlight lang="mysql"> # 主キーでグループ化した場合、同じテーブルの他の列も使用可能 SELECT id, name, email FROM users GROUP BY id; # idが主キーの場合、nameとemailは関数従属しているため使用可能 </syntaxhighlight> <br><br> == パフォーマンス == GROUP BYのパフォーマンスは、インデックスの有無と使用方法によって大きく異なる。<br> <br> ==== ルースインデックススキャン ==== ルースインデックススキャンは、最も効率的なGROUP BYの実行戦略である。<br> 各グループの最初のエントリのみをインデックスから読み取る。<br> <br> ルースインデックススキャンが使用される条件は以下の通りである。<br> <br> * 単一テーブルのクエリ * <code>GROUP BY</code> 列がインデックスの左端から連続している * 集約関数は <code>MIN()</code> または <code>MAX()</code> のみ (または集約関数なし) <br> <syntaxhighlight lang="mysql"> # インデックス: (department_id, hire_date) # ルースインデックススキャンが使用される例 SELECT department_id, MIN(hire_date) AS earliest_hire FROM employees GROUP BY department_id; # EXPLAINで確認 EXPLAIN SELECT department_id, MIN(hire_date) AS earliest_hire FROM employees GROUP BY department_id; # Extra: Using index for group-by (scanning) # ルースインデックススキャンが使用される条件 SELECT department_id, MAX(salary) AS max_salary FROM employees GROUP BY department_id; </syntaxhighlight> <br> ルースインデックススキャンが使用されない例を以下に示す。<br> <br> <syntaxhighlight lang="mysql"> # インデックス: (department_id, hire_date) # GROUP BY列がインデックスの途中から始まる (使用されない) SELECT hire_date, COUNT(*) AS employee_count FROM employees GROUP BY hire_date; # MIN/MAX以外の集約関数 (使用されない) SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id; # 複数テーブルのJOIN (使用されない) SELECT d.department_name, COUNT(*) AS employee_count FROM employees e JOIN departments d ON e.department_id = d.id GROUP BY d.department_name; </syntaxhighlight> <br> ==== タイトインデックススキャン ==== タイトインデックススキャンは、インデックスの全エントリを読み取るが、テーブル本体へのアクセスは不要である。<br> ルースインデックススキャンより遅いが、インデックスなしよりは高速である。<br> <br> <syntaxhighlight lang="mysql"> # インデックス: (department_id, salary) # タイトインデックススキャンが使用される例 SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id; # インデックスの全エントリを読み取る # EXPLAINで確認 EXPLAIN SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id; # Extra: Using index </syntaxhighlight> <br> ==== インデックスなしの場合 ==== インデックスがない場合、一時テーブルと <code>filesort</code> が使用される。<br> これは最も遅い実行戦略である。<br> <br> <syntaxhighlight lang="mysql"> # インデックスがない列でGROUP BY SELECT job_title, COUNT(*) AS employee_count FROM employees GROUP BY job_title; # EXPLAINで確認 EXPLAIN SELECT job_title, COUNT(*) AS employee_count FROM employees GROUP BY job_title; # Extra: Using temporary; Using filesort </syntaxhighlight> <br> ==== 最適化の指針 ==== GROUP BYのパフォーマンスを向上させるための指針を以下に示す。<br> <br> * <code>GROUP BY</code> 列にインデックスを作成する *: 最も基本的で効果的な最適化 *: <br> * 列の順序をインデックスと一致させる *: <code>GROUP BY col1, col2</code> の場合、インデックスも (col1, col2) の順 *: <br> * カバリングインデックスを使用する *: <code>SELECT</code> 句と <code>GROUP BY</code> 句の全列を含むインデックスを作成 *: <br> * 不要な <code>ORDER BY</code> を削除する *: MySQL 8.0以降では暗黙的なソートが廃止されているため、不要な <code>ORDER BY</code> を削除 <br> <syntaxhighlight lang="mysql"> # インデックスの作成例 CREATE INDEX idx_department_id ON employees(department_id); # 複数列のインデックス CREATE INDEX idx_dept_job ON employees(department_id, job_title); # カバリングインデックス CREATE INDEX idx_dept_salary ON employees(department_id, salary); # このクエリはカバリングインデックスを使用 SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id; # テーブル本体へのアクセスが不要 </syntaxhighlight> <br> インデックスの順序が重要である。<br> <br> <syntaxhighlight lang="mysql"> # インデックス: (department_id, job_title) # インデックスが使用される (左端から一致) SELECT department_id, COUNT(*) AS employee_count FROM employees GROUP BY department_id; # インデックスが使用される (左端から一致) SELECT department_id, job_title, COUNT(*) AS employee_count FROM employees GROUP BY department_id, job_title; # インデックスが使用されない (途中から始まる) SELECT job_title, COUNT(*) AS employee_count FROM employees GROUP BY job_title; </syntaxhighlight> <br> 実行計画の確認には、<code>EXPLAIN</code> を使用する。<br> <br> <syntaxhighlight lang="mysql"> # 実行計画を確認 EXPLAIN SELECT department_id, COUNT(*) AS employee_count FROM employees GROUP BY department_id; # MySQL 8.0.18以降では、EXPLAIN ANALYZEで実際の実行時間も確認可能 EXPLAIN ANALYZE SELECT department_id, COUNT(*) AS employee_count FROM employees GROUP BY department_id; # Extraフィールドで最適化状況を確認 # - "Using index for group-by": ルースインデックススキャン # - "Using index": タイトインデックススキャン # - "Using temporary; Using filesort": 一時テーブルとソート </syntaxhighlight> <br><br> == JOINとの組み合わせ == GROUP BYは、<code>JOIN</code> と組み合わせることにより、複数テーブルのデータを集約できる。<br> <br> ==== INNER JOINとGROUP BY ==== <code>INNER JOIN</code> でテーブルを結合し、<code>GROUP BY</code> で集約する。<br> <br> <syntaxhighlight lang="mysql"> # 部署別の従業員数と部署名を取得 SELECT d.department_name, COUNT(*) AS employee_count FROM employees e INNER JOIN departments d ON e.department_id = d.id GROUP BY d.department_name; # カテゴリ別の商品数とカテゴリ名を取得 SELECT c.category_name, COUNT(*) AS product_count FROM products p INNER JOIN categories c ON p.category_id = c.id GROUP BY c.category_name; # ユーザ別の注文数と合計金額を取得 SELECT u.name, COUNT(o.id) AS order_count, SUM(o.amount) AS total_spent FROM users u INNER JOIN orders o ON u.id = o.user_id GROUP BY u.name; </syntaxhighlight> <br> 複数のテーブルを結合して集約する例を以下に示す。<br> <br> <syntaxhighlight lang="mysql"> # 注文、注文明細、商品を結合して集計 SELECT p.product_name, COUNT(DISTINCT o.id) AS order_count, SUM(oi.quantity) AS total_quantity, SUM(oi.amount) AS total_sales FROM orders o INNER JOIN order_items oi ON o.id = oi.order_id INNER JOIN products p ON oi.product_id = p.id GROUP BY p.product_name; # 部署、従業員、プロジェクトを結合 SELECT d.department_name, COUNT(DISTINCT e.id) AS employee_count, COUNT(DISTINCT p.id) AS project_count FROM departments d INNER JOIN employees e ON d.id = e.department_id INNER JOIN project_assignments pa ON e.id = pa.employee_id INNER JOIN projects p ON pa.project_id = p.id GROUP BY d.department_name; </syntaxhighlight> <br> ==== LEFT JOINとGROUP BY ==== <code>LEFT JOIN</code> を使用すると、マッチしない行も含めて集計できる。<br> <br> <syntaxhighlight lang="mysql"> # 全ユーザの注文数を取得 (注文がないユーザも含む) SELECT u.name, COUNT(o.id) AS order_count FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.name; # 注文がないユーザはorder_countが0になる # 全部署の従業員数を取得 (従業員がいない部署も含む) SELECT d.department_name, COUNT(e.id) AS employee_count FROM departments d LEFT JOIN employees e ON d.id = e.department_id GROUP BY d.department_name; # 全カテゴリの商品数と合計売上を取得 (売上がないカテゴリも含む) SELECT c.category_name, COUNT(p.id) AS product_count, IFNULL(SUM(oi.amount), 0) AS total_sales FROM categories c LEFT JOIN products p ON c.id = p.category_id LEFT JOIN order_items oi ON p.id = oi.product_id GROUP BY c.category_name; </syntaxhighlight> <br> <u><code>COUNT(*)</code> と <code>COUNT(column)</code> の違いに注意が必要である。</u><br> <br> <syntaxhighlight lang="mysql"> # COUNT(*)は全行をカウント (NULLも含む) SELECT u.name, COUNT(*) AS row_count, COUNT(o.id) AS order_count FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.name; # 注文がないユーザ: row_count=1, order_count=0 # LEFT JOINではCOUNT(joined_table.column)を使用すべき SELECT d.department_name, COUNT(e.id) AS employee_count FROM departments d LEFT JOIN employees e ON d.id = e.department_id GROUP BY d.department_name; </syntaxhighlight> <br> ==== 部署別集計の実用例 ==== 実用的な部署別集計の例を以下に示す。<br> <br> <syntaxhighlight lang="mysql"> # 部署別の従業員数、平均給与、最高給与、最低給与 SELECT d.department_name, COUNT(e.id) AS employee_count, ROUND(AVG(e.salary), 2) AS avg_salary, MAX(e.salary) AS max_salary, MIN(e.salary) AS min_salary FROM departments d LEFT JOIN employees e ON d.id = e.department_id GROUP BY d.department_name ORDER BY employee_count DESC; # 部署別の役職ごとの従業員数 SELECT d.department_name, e.job_title, COUNT(*) AS employee_count FROM departments d INNER JOIN employees e ON d.id = e.department_id GROUP BY d.department_name, e.job_title ORDER BY d.department_name, employee_count DESC; # 部署別の年別採用人数 SELECT d.department_name, YEAR(e.hire_date) AS hire_year, COUNT(*) AS hire_count FROM departments d INNER JOIN employees e ON d.id = e.department_id GROUP BY d.department_name, hire_year ORDER BY d.department_name, hire_year; </syntaxhighlight> <br><br> == サンプルクエリ == ==== 日別・月別集計 ==== 以下の例では、日付データを集計している。<br> <br> <syntaxhighlight lang="mysql"> # 日別の注文数と売上合計 SELECT DATE(order_date) AS order_day, COUNT(*) AS order_count, SUM(amount) AS total_sales FROM orders GROUP BY DATE(order_date) ORDER BY order_day DESC; # 月別の売上集計 SELECT DATE_FORMAT(order_date, '%Y-%m') AS month, COUNT(*) AS order_count, SUM(amount) AS total_sales, AVG(amount) AS avg_order_value FROM orders GROUP BY DATE_FORMAT(order_date, '%Y-%m') ORDER BY month DESC; # 年別の売上推移 SELECT YEAR(order_date) AS year, COUNT(*) AS order_count, SUM(amount) AS total_sales FROM orders GROUP BY YEAR(order_date) ORDER BY year; # 年月の階層的集計 SELECT YEAR(order_date) AS year, MONTH(order_date) AS month, COUNT(*) AS order_count, SUM(amount) AS total_sales FROM orders GROUP BY YEAR(order_date), MONTH(order_date) ORDER BY year DESC, month DESC; # 曜日別の注文傾向 SELECT DAYNAME(order_date) AS day_of_week, COUNT(*) AS order_count, AVG(amount) AS avg_order_value FROM orders GROUP BY DAYNAME(order_date) ORDER BY FIELD(day_of_week, 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'); </syntaxhighlight> <br> ==== GROUP_CONCATでのリスト生成 ==== <code>GROUP_CONCAT()</code> 関数を使用して、グループ内の値をリストとして表示する。<br> <br> <syntaxhighlight lang="mysql"> # 部署ごとの従業員名リスト SELECT d.department_name, GROUP_CONCAT(e.name ORDER BY e.name SEPARATOR ', ') AS employee_list FROM departments d INNER JOIN employees e ON d.id = e.department_id GROUP BY d.department_name; # ユーザごとの注文商品リスト SELECT u.name, GROUP_CONCAT(DISTINCT p.product_name ORDER BY p.product_name SEPARATOR ', ') AS products_ordered FROM users u INNER JOIN orders o ON u.id = o.user_id INNER JOIN order_items oi ON o.id = oi.order_id INNER JOIN products p ON oi.product_id = p.id GROUP BY u.name; # カテゴリごとの商品名と価格のリスト SELECT c.category_name, GROUP_CONCAT(CONCAT(p.product_name, ' (', p.price, '円)') ORDER BY p.price DESC SEPARATOR ' | ') AS products FROM categories c INNER JOIN products p ON c.id = p.category_id GROUP BY c.category_name; # 部署ごとの従業員IDリスト SELECT d.department_name, GROUP_CONCAT(e.id ORDER BY e.id SEPARATOR ',') AS employee_ids FROM departments d INNER JOIN employees e ON d.id = e.department_id GROUP BY d.department_name; </syntaxhighlight> <br> ==== JSON_ARRAYAGGでのJSON配列生成 ==== <code>JSON_ARRAYAGG()</code> 関数を使用して、グループ内の値をJSON配列として表示する。<br> <br> <syntaxhighlight lang="mysql"> # 部署ごとの従業員名JSON配列 SELECT d.department_name, JSON_ARRAYAGG(e.name) AS employees FROM departments d INNER JOIN employees e ON d.id = e.department_id GROUP BY d.department_name; # ユーザごとの注文情報JSON配列 SELECT u.name, JSON_ARRAYAGG( JSON_OBJECT( 'order_id', o.id, 'order_date', o.order_date, 'amount', o.amount ) ) AS orders FROM users u INNER JOIN orders o ON u.id = o.user_id GROUP BY u.name; # カテゴリごとの商品情報JSON配列 SELECT c.category_name, JSON_ARRAYAGG( JSON_OBJECT( 'product_name', p.product_name, 'price', p.price, 'stock', p.stock_quantity ) ORDER BY p.price DESC ) AS products FROM categories c INNER JOIN products p ON c.id = p.category_id GROUP BY c.category_name; </syntaxhighlight> <br><br> == 関連項目 == * [[MySQL - SELECT]] * [[MySQL - JOIN]] * [[MySQL - LIMIT句]] * [[MySQL - 副問い合わせ]] <br><br> {{#seo: |title={{PAGENAME}} : Exploring Electronics and SUSE Linux | MochiuWiki |keywords=MochiuWiki,Mochiu,Wiki,Mochiu Wiki,Electric Circuit,Electric,pcb,Mathematics,AVR,TI,STMicro,AVR,ATmega,MSP430,STM,Arduino,Xilinx,FPGA,Verilog,HDL,PinePhone,Pine Phone,Raspberry,Raspberry Pi,C,C++,C#,Qt,Qml,MFC,Shell,Bash,Zsh,Fish,SUSE,SLE,Suse Enterprise,Suse Linux,openSUSE,open SUSE,Leap,Linux,uCLnux,電気回路,電子回路,基板,プリント基板 |description={{PAGENAME}} - 電子回路とSUSE Linuxに関する情報 | This page is {{PAGENAME}} in our wiki about electronic circuits and SUSE Linux |image=/resources/assets/MochiuLogo_Single_Blue.png }} __FORCETOC__ [[カテゴリ:MySQL]]
MySQL - GROUP BY
に戻る。
案内
メインページ
最近の更新
おまかせ表示
MediaWiki についてのヘルプ
ツール
リンク元
関連ページの更新状況
特別ページ
ページ情報
We ask for
Donations
Collapse