SQLのMIN関数は、指定した列から最小値を取得する集約関数です。売上の最低額、最も古い日付、最安値の商品など、データ分析では欠かせない機能です。
この記事では、MIN関数の基本構文からGROUP BYやHAVINGとの組み合わせ、NULLの扱い、サブクエリで該当レコード全体を取得する方法、さらにRDBMS別の違いやパフォーマンスのコツまで、実務で使えるパターンを網羅的に解説します。
この記事で学べること
- MIN関数の基本構文と数値・日付・文字列での使い方
- GROUP BYでグループ単位の最小値を取得する方法
- HAVING句で集計結果を絞り込む方法
- NULLの扱いとCOALESCEによる対策
- サブクエリ・ウィンドウ関数で最小値のレコード全体を取得する方法
- MIN関数 vs ORDER BY + LIMIT の違い
- CASE WHEN + MINで条件付き最小値を取得する方法
- RDBMS別の違いとパフォーマンスのコツ
サンプルデータ
この記事では、以下のemployeesテーブルを使って解説します。
| id |
name |
department |
salary |
hire_date |
| 1 |
田中太郎 |
営業 |
350000 |
2020-04-01 |
| 2 |
鈴木花子 |
開発 |
420000 |
2019-07-15 |
| 3 |
佐藤一郎 |
営業 |
300000 |
2021-01-10 |
| 4 |
高橋美咲 |
開発 |
380000 |
2022-03-20 |
| 5 |
山田健太 |
人事 |
NULL |
2023-06-01 |
| 6 |
伊藤恵 |
人事 |
320000 |
2020-09-15 |
| 7 |
渡辺大輔 |
開発 |
450000 |
2018-11-01 |
| 8 |
中村由美 |
営業 |
280000 |
2023-02-14 |
サンプルデータの作成SQL(クリックで展開)
CREATE TABLE + INSERT
CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(50),
department VARCHAR(20),
salary INT,
hire_date DATE
);
INSERT INTO employees (id, name, department, salary, hire_date) VALUES
(1, '田中太郎', '営業', 350000, '2020-04-01'),
(2, '鈴木花子', '開発', 420000, '2019-07-15'),
(3, '佐藤一郎', '営業', 300000, '2021-01-10'),
(4, '高橋美咲', '開発', 380000, '2022-03-20'),
(5, '山田健太', '人事', NULL, '2023-06-01'),
(6, '伊藤恵', '人事', 320000, '2020-09-15'),
(7, '渡辺大輔', '開発', 450000, '2018-11-01'),
(8, '中村由美', '営業', 280000, '2023-02-14');
MIN関数の基本構文
MIN関数は、指定した列の中から最も小さい値を1つ返す集約関数(集計関数)です。数値・日付・文字列のいずれにも使えます。
基本構文
SELECT MIN(列名)
FROM テーブル名;
MIN関数の特徴
- NULL値は自動的に無視される(計算に含まれない)
- 数値・日付・文字列すべてのデータ型に対応
- GROUP BYと組み合わせてグループ単位の最小値を取得可能
- DISTINCTを指定しても結果は変わらない(最小値は1つだけ)
基本的な使用例
数値の最小値を取得
全社員の中で最も低い給与を取得します。
SQL
SELECT MIN(salary) AS min_salary
FROM employees;
id=5の山田健太はsalaryがNULLですが、MIN関数はNULLを自動的にスキップして280000を返します。
日付の最小値(最古の日付)を取得
最も早い入社日を取得します。日付型の列では、最も古い日付が最小値になります。
SQL
SELECT MIN(hire_date) AS earliest_hire
FROM employees;
文字列の最小値を取得
文字列型では辞書順(照合順序)で最も先頭の値が返されます。
SQL
SELECT MIN(department) AS first_dept
FROM employees;
注意:文字列のMINは照合順序(COLLATION)に依存します。日本語の場合、使用するDBの照合順序(utf8mb4_general_ci等)によって結果が変わることがあります。
WHERE句との組み合わせ
WHERE句で条件を絞ったうえでMIN関数を適用できます。
開発部のみの最低給与
SELECT MIN(salary) AS min_salary
FROM employees
WHERE department = '開発';
開発部の3人(鈴木:420000、高橋:380000、渡辺:450000)のうち最小値の380000が返されます。
2021年以降に入社した社員の最低給与
SELECT MIN(salary) AS min_salary
FROM employees
WHERE hire_date >= '2021-01-01';
GROUP BYとの組み合わせ
GROUP BY句と組み合わせると、グループ単位の最小値を取得できます。
部署ごとの最低給与
SQL
SELECT department,
MIN(salary) AS min_salary
FROM employees
GROUP BY department;
| department |
min_salary |
| 営業 |
280000 |
| 開発 |
380000 |
| 人事 |
320000 |
人事部の山田健太(salary=NULL)は無視され、伊藤恵の320000が人事部の最小値となります。
複数の集約関数を同時に使用
MIN関数は他の集約関数と一緒に使えます。
部署別の給与統計
SELECT department,
MIN(salary) AS min_salary,
MAX(salary) AS max_salary,
AVG(salary) AS avg_salary,
COUNT(salary) AS cnt
FROM employees
GROUP BY department;
| department |
min_salary |
max_salary |
avg_salary |
cnt |
| 営業 |
280000 |
350000 |
310000 |
3 |
| 開発 |
380000 |
450000 |
416667 |
3 |
| 人事 |
320000 |
320000 |
320000 |
1 |
HAVINGで集計結果を絞り込む
HAVING句を使えば、GROUP BYの集計結果にさらに条件を指定できます。
最低給与が30万以上の部署のみ
SELECT department,
MIN(salary) AS min_salary
FROM employees
GROUP BY department
HAVING MIN(salary) >= 300000;
| department |
min_salary |
| 開発 |
380000 |
| 人事 |
320000 |
営業部はMIN(salary)=280000で条件を満たさないため除外されます。
注意:WHERE句は集約前のフィルタ、HAVING句は集約後のフィルタです。MIN関数の結果で絞り込むにはHAVINGを使います。WHEREにMIN関数を書くとエラーになります。
NULLの扱い
MIN関数とNULLの関係は実務でよくある落とし穴です。
| ケース |
結果 |
説明 |
| NULLが混在 |
NULLを無視して最小値を返す |
NULL以外の値で計算 |
| 全てNULL |
NULLを返す |
比較対象がない |
| 行が0件 |
NULLを返す |
対象レコードなし |
NULLを0として扱いたい場合
NULLを特定の値に変換してから比較するには、COALESCE関数を使います。
NULLを0に変換してMIN
SELECT MIN(COALESCE(salary, 0)) AS min_salary
FROM employees;
山田健太のNULLが0に変換され、最小値は0になります。
結果がNULLの場合にデフォルト値を返す
対象が0件の場合にデフォルト値を返す
SELECT COALESCE(MIN(salary), 0) AS min_salary
FROM employees
WHERE department = '経理'; -- 該当なし
COALESCE(MIN(...), 0)とすることで、対象データが0件でもNULLではなく0を返せます。アプリケーション側でNULLチェックが不要になります。
最小値のレコード全体を取得する方法
MIN関数は最小値だけを返します。「最も給与が低い社員の名前や部署も知りたい」場合は、サブクエリやJOINを使います。
方法1:サブクエリを使う
WHERE句のサブクエリ
SELECT id, name, department, salary
FROM employees
WHERE salary = (SELECT MIN(salary) FROM employees);
| id |
name |
department |
salary |
| 8 |
中村由美 |
営業 |
280000 |
方法2:ORDER BY + LIMIT(MySQL / PostgreSQL)
ORDER BY + LIMIT
SELECT id, name, department, salary
FROM employees
WHERE salary IS NOT NULL
ORDER BY salary ASC
LIMIT 1;
方法3:ウィンドウ関数を使う
最小値が同じ値の行が複数ある場合にすべて取得したいときに便利です。
ウィンドウ関数
SELECT id, name, department, salary
FROM (
SELECT id, name, department, salary,
RANK() OVER (ORDER BY salary ASC) AS rnk
FROM employees
WHERE salary IS NOT NULL
) sub
WHERE rnk = 1;
| 方法 |
同値複数行 |
パフォーマンス |
適した場面 |
| サブクエリ(WHERE = MIN) |
全て取得 |
良好 |
汎用的・標準SQL |
| ORDER BY + LIMIT |
1件のみ |
最速 |
1件だけ必要な場合 |
| ウィンドウ関数(RANK) |
全て取得 |
中程度 |
グループ別の最小値レコード |
グループごとの最小値レコードを取得
「各部署で最も給与が低い社員」のように、グループ単位で最小値のレコード全体を取得するパターンです。
相関サブクエリを使う方法
部署別に最低給与の社員を取得
SELECT e.id, e.name, e.department, e.salary
FROM employees e
WHERE e.salary = (
SELECT MIN(e2.salary)
FROM employees e2
WHERE e2.department = e.department
);
| id |
name |
department |
salary |
| 8 |
中村由美 |
営業 |
280000 |
| 4 |
高橋美咲 |
開発 |
380000 |
| 6 |
伊藤恵 |
人事 |
320000 |
ウィンドウ関数+PARTITION BYを使う方法
PARTITION BYで部署別の最小値レコードを取得
SELECT id, name, department, salary
FROM (
SELECT id, name, department, salary,
ROW_NUMBER() OVER (
PARTITION BY department
ORDER BY salary ASC
) AS rn
FROM employees
WHERE salary IS NOT NULL
) sub
WHERE rn = 1;
ポイント:ROW_NUMBERは同値でも1件だけ、RANKは同値を全て取得します。要件に合わせて使い分けましょう。
MIN関数 vs ORDER BY + LIMIT の違い
「ORDER BY + LIMITでも最小値は取れるのでは?」というのはよくある疑問です。
MIN関数
-- 最小値のみ取得
SELECT MIN(salary) FROM employees;
ORDER BY + LIMIT
-- レコード全体を取得
SELECT * FROM employees
ORDER BY salary ASC
LIMIT 1;
| 比較項目 |
MIN関数 |
ORDER BY + LIMIT |
| 取得できる情報 |
値のみ |
レコード全体 |
| NULLの扱い |
自動で無視 |
先頭に来る場合あり |
| GROUP BYとの併用 |
可能 |
不可 |
| 同値の複数行 |
1つの値のみ |
1行のみ |
| SQL標準 |
標準SQL |
RDBMS依存 |
| 適した場面 |
集計・統計 |
詳細レコード取得 |
注意:ORDER BY + LIMITはNULLの並び順がRDBMSによって異なります。MySQLとSQL ServerではNULLが先頭、PostgreSQLでは末尾になります。NULLを含む列で使う場合はWHERE salary IS NOT NULLを追加しましょう。
RDBMS別の違い
MIN関数自体はSQL標準ですが、取得件数の制限構文がRDBMSごとに異なります。
| RDBMS |
最小値レコード取得 |
NULLの並び順 |
| MySQL |
ORDER BY col LIMIT 1 |
先頭(NULLS FIRST) |
| PostgreSQL |
ORDER BY col LIMIT 1 |
末尾(NULLS LAST) |
| Oracle |
FETCH FIRST 1 ROW ONLY |
末尾(NULLS LAST) |
| SQL Server |
TOP 1 |
先頭(NULLS FIRST) |
| SQLite |
ORDER BY col LIMIT 1 |
先頭(NULLS FIRST) |
Oracle / SQL Serverの記述例を見る
Oracle(12c以降)
SELECT * FROM employees
ORDER BY salary ASC NULLS LAST
FETCH FIRST 1 ROW ONLY;
SQL Server
SELECT TOP 1 * FROM employees
WHERE salary IS NOT NULL
ORDER BY salary ASC;
MINをウィンドウ関数として使う
MIN関数にOVER句を付けると、行を集約せずに各行に最小値を付加できます。
全体の最小給与を各行に表示
SELECT name, department, salary,
MIN(salary) OVER () AS overall_min,
MIN(salary) OVER (PARTITION BY department) AS dept_min
FROM employees
WHERE salary IS NOT NULL;
| name |
department |
salary |
overall_min |
dept_min |
| 田中太郎 |
営業 |
350000 |
280000 |
280000 |
| 佐藤一郎 |
営業 |
300000 |
280000 |
280000 |
| 中村由美 |
営業 |
280000 |
280000 |
280000 |
| 鈴木花子 |
開発 |
420000 |
280000 |
380000 |
| 高橋美咲 |
開発 |
380000 |
280000 |
380000 |
| 渡辺大輔 |
開発 |
450000 |
280000 |
380000 |
| 伊藤恵 |
人事 |
320000 |
280000 |
320000 |
OVER ()で全体の最小値、OVER (PARTITION BY department)で部署別の最小値を各行に付加しています。GROUP BYと違い、元のレコードはそのまま保持されます。
最小値との差分を計算する
ウィンドウ関数のMINを使えば、各社員の給与が部署内の最低給与からどれだけ高いかを計算できます。
最小値との差分
SELECT name, department, salary,
salary - MIN(salary) OVER (PARTITION BY department) AS diff_from_min
FROM employees
WHERE salary IS NOT NULL;
CASE WHEN + MIN で条件付き最小値を取得
CASE WHENと組み合わせると、条件に合致するデータだけの最小値を1つのクエリで取得できます。
部署ごとの条件付き最小値
各部署の最小給与を横並びで取得
SELECT
MIN(CASE WHEN department = '営業' THEN salary END) AS sales_min,
MIN(CASE WHEN department = '開発' THEN salary END) AS dev_min,
MIN(CASE WHEN department = '人事' THEN salary END) AS hr_min
FROM employees;
| sales_min |
dev_min |
hr_min |
| 280000 |
380000 |
320000 |
GROUP BYを使わずに部署別の最小値を1行で横並びに取得できます。クロス集計やレポート作成で便利なテクニックです。
年度ごとの最低給与を横並びで取得
入社年度ごとの最低給与
SELECT
MIN(CASE WHEN YEAR(hire_date) <= 2020 THEN salary END) AS min_2020,
MIN(CASE WHEN YEAR(hire_date) >= 2021 THEN salary END) AS min_2021_later
FROM employees;
| min_2020 |
min_2021_later |
| 320000 |
280000 |
ポイント:CASE WHENの条件に一致しない行はNULLになり、MINはNULLを無視するため、条件付きの集計が正しく動作します。CASE文の詳しい使い方はこちら
実務でよく使うパターン
シナリオ1:商品テーブルから最安値を取得
カテゴリ別の最安値
SELECT category,
MIN(price) AS lowest_price,
MAX(price) AS highest_price,
MAX(price) - MIN(price) AS price_range
FROM products
WHERE is_active = 1
GROUP BY category;
シナリオ2:注文履歴から初回注文日を取得
顧客別の初回注文日
SELECT customer_id,
MIN(order_date) AS first_order,
MAX(order_date) AS last_order,
COUNT(*) AS total_orders
FROM orders
GROUP BY customer_id;
シナリオ3:ログテーブルから最古のエラー発生日時を取得
エラー種別ごとの初回発生日時
SELECT error_code,
MIN(occurred_at) AS first_occurred,
MAX(occurred_at) AS last_occurred,
COUNT(*) AS occurrence_count
FROM error_logs
WHERE occurred_at >= '2024-01-01'
GROUP BY error_code
HAVING COUNT(*) >= 5
ORDER BY first_occurred;
シナリオ4:在庫テーブルから最低在庫数の商品を通知
在庫が全体の最小値と一致する商品
SELECT product_name, stock_quantity, warehouse
FROM inventory
WHERE stock_quantity = (
SELECT MIN(stock_quantity)
FROM inventory
WHERE stock_quantity > 0
);
シナリオ5:JOINと組み合わせて最安値の仕入先を取得
商品ごとの最安仕入先
SELECT p.product_name, s.supplier_name, ps.unit_price
FROM product_suppliers ps
JOIN products p ON ps.product_id = p.id
JOIN suppliers s ON ps.supplier_id = s.id
WHERE ps.unit_price = (
SELECT MIN(ps2.unit_price)
FROM product_suppliers ps2
WHERE ps2.product_id = ps.product_id
);
パフォーマンスのコツ
| 対策 |
効果 |
説明 |
| インデックスを作成 |
大幅に高速化 |
B-Treeインデックスの最左端を読むだけで完了 |
| WHEREで絞り込み |
スキャン範囲を削減 |
不要なデータを事前に除外 |
| GROUP BY列にインデックス |
グループ化が高速 |
複合インデックス(group_col, min_col)が最適 |
| 不要なJOINを避ける |
処理量を削減 |
MINだけ必要ならサブクエリで取得 |
インデックス作成例
-- 単一列のMINを高速化
CREATE INDEX idx_salary ON employees (salary);
-- GROUP BY + MINを高速化(複合インデックス)
CREATE INDEX idx_dept_salary ON employees (department, salary);
ポイント:適切なインデックスがあれば、MIN関数はテーブル全体をスキャンせずにインデックスの先頭1件を読むだけで完了します(Index Only Scan)。大量データでもミリ秒単位で結果を返せます。
よくあるエラーと対処法
| エラー |
原因 |
対処法 |
not a single-group group function |
GROUP BY なしで集約関数と非集約列を同時にSELECT |
GROUP BY を追加 or サブクエリに変更 |
aggregate functions are not allowed in WHERE |
WHERE句でMIN関数を使用 |
HAVING句に変更 or サブクエリを使用 |
| 結果がNULL |
対象行が0件 or 全てNULL |
COALESCEでデフォルト値を設定 |
| 文字列の並び順が想定外 |
照合順序の違い |
COLLATEを明示的に指定 |
エラー例と修正
NG: GROUP BY なしで非集約列をSELECT
-- エラーになる
SELECT name, MIN(salary)
FROM employees;
OK: サブクエリで解決
-- サブクエリを使って正しく取得
SELECT name, salary
FROM employees
WHERE salary = (SELECT MIN(salary) FROM employees);
NG: WHERE句でMIN関数を使用
-- エラーになる
SELECT department, MIN(salary)
FROM employees
WHERE MIN(salary) >= 300000
GROUP BY department;
OK: HAVING句に変更
-- HAVING句を使えばOK
SELECT department, MIN(salary)
FROM employees
GROUP BY department
HAVING MIN(salary) >= 300000;
集約関数まとめ
MIN関数は5つの集約関数のうちの1つです。それぞれの違いを確認しましょう。
| 関数 |
機能 |
NULLの扱い |
対応型 |
記事リンク |
| MIN |
最小値 |
無視 |
数値・日付・文字列 |
この記事 |
| MAX |
最大値 |
無視 |
数値・日付・文字列 |
MAX関数 |
| SUM |
合計 |
無視 |
数値のみ |
SUM関数 |
| AVG |
平均値 |
無視 |
数値のみ |
AVG関数 |
| COUNT |
件数 |
COUNT(*)は含む COUNT(列)は無視 |
全型 |
COUNT関数 |
まとめ
| 項目 |
内容 |
| 基本構文 |
SELECT MIN(列名) FROM テーブル名 |
| 対応データ型 |
数値・日付・文字列すべて |
| NULLの扱い |
自動的に無視(全てNULL or 0件でNULLを返す) |
| グループ別取得 |
GROUP BY句と併用 |
| 集計結果の絞り込み |
HAVING句(WHERE句ではエラー) |
| レコード全体の取得 |
サブクエリ / ORDER BY + LIMIT / ウィンドウ関数 |
| ウィンドウ関数 |
MIN() OVER () で行を集約せずに最小値を付加 |
| 高速化 |
対象列にインデックスを作成 |
MIN関数はシンプルな関数ですが、サブクエリやウィンドウ関数と組み合わせることで実務の多くの場面で活躍します。まずは基本のGROUP BYとの組み合わせを押さえ、NULLの挙動を理解しておけば、困ることはありません。
関連記事