SQLリファレンス - よく使うクエリの構文と実例まとめ
SQLの実務でよく使うクエリをまとめたリファレンス。MySQL / PostgreSQL / SQLite で共通して使える構文を中心に掲載している。
SELECT の基本
-- 全列を取得
SELECT * FROM users;
-- 特定列を取得
SELECT id, name, email FROM users;
-- 条件を指定
SELECT * FROM users WHERE age >= 20;
-- 並び替え(昇順 ASC / 降順 DESC)
SELECT * FROM users ORDER BY age DESC;
-- 取得件数の制限
SELECT * FROM users LIMIT 10;
-- ページネーション(11件目から10件取得)
SELECT * FROM users LIMIT 10 OFFSET 10;
-- 重複を除外
SELECT DISTINCT country FROM users;
WHERE 句の条件
-- 比較演算子
WHERE age = 20
WHERE age != 20
WHERE age > 20
WHERE age >= 20
-- 範囲
WHERE age BETWEEN 20 AND 30
-- リストに含まれる
WHERE status IN ('active', 'pending')
WHERE status NOT IN ('deleted')
-- NULL チェック
WHERE deleted_at IS NULL
WHERE deleted_at IS NOT NULL
-- 文字列の部分一致(LIKE)
WHERE name LIKE 'A%' -- "A" で始まる
WHERE name LIKE '%田' -- "田" で終わる
WHERE name LIKE '%山%' -- "山" を含む
-- 複数条件
WHERE age >= 20 AND status = 'active'
WHERE age < 18 OR age >= 65
WHERE NOT (status = 'deleted')
JOIN(テーブル結合)
サンプルテーブル:
-- users テーブル
id | name | dept_id
1 | 田中 | 1
2 | 佐藤 | 2
3 | 鈴木 | NULL
-- departments テーブル
id | name
1 | 開発部
2 | 営業部
3 | 総務部
INNER JOIN(内部結合)
両方のテーブルに一致するレコードのみ取得。
SELECT u.name, d.name AS dept_name
FROM users u
INNER JOIN departments d ON u.dept_id = d.id;
-- 結果
name | dept_name
田中 | 開発部
佐藤 | 営業部
-- ※ dept_id が NULL の鈴木と、dept_id に対応しない部署は除外される
LEFT JOIN(左外部結合)
左テーブルの全レコードを取得し、右テーブルに一致しない場合はNULLを補完。
SELECT u.name, d.name AS dept_name
FROM users u
LEFT JOIN departments d ON u.dept_id = d.id;
-- 結果
name | dept_name
田中 | 開発部
佐藤 | 営業部
鈴木 | NULL -- dept_id がNULLでも含まれる
JOIN の種類まとめ
| 種類 | 説明 |
|---|---|
INNER JOIN | 両テーブルに一致するレコードのみ |
LEFT JOIN | 左テーブル全件 + 右テーブルは一致分のみ |
RIGHT JOIN | 右テーブル全件 + 左テーブルは一致分のみ |
FULL OUTER JOIN | 両テーブルの全件(SQLite非対応) |
CROSS JOIN | 全組み合わせ(直積) |
実務では INNER JOIN と LEFT JOIN が大半を占める。
複数テーブルの結合
SELECT
o.id AS order_id,
u.name AS user_name,
p.name AS product_name,
o.quantity
FROM orders o
INNER JOIN users u ON o.user_id = u.id
INNER JOIN products p ON o.product_id = p.id
WHERE o.status = 'completed';
集計関数
SELECT COUNT(*) FROM users; -- 件数
SELECT COUNT(email) FROM users; -- NULLを除いた件数
SELECT SUM(price) FROM orders; -- 合計
SELECT AVG(price) FROM orders; -- 平均
SELECT MAX(price) FROM orders; -- 最大値
SELECT MIN(price) FROM orders; -- 最小値
GROUP BY(グループ集計)
-- 部署ごとのユーザー数
SELECT dept_id, COUNT(*) AS user_count
FROM users
GROUP BY dept_id;
-- 月ごとの売上合計
SELECT
DATE_FORMAT(ordered_at, '%Y-%m') AS month, -- MySQL
SUM(total_price) AS monthly_sales
FROM orders
GROUP BY month
ORDER BY month;
HAVING(集計後の絞り込み)
WHERE は集計前、HAVING は集計後に条件を適用する。
-- 注文が3件以上あるユーザー
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 3;
サブクエリ
-- 平均より高い商品
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);
-- 注文があるユーザーのみ取得
SELECT name FROM users
WHERE id IN (SELECT DISTINCT user_id FROM orders);
-- 注文がないユーザーを取得
SELECT name FROM users
WHERE id NOT IN (SELECT DISTINCT user_id FROM orders WHERE user_id IS NOT NULL);
EXISTS
サブクエリに結果が存在するか確認する。IN より大量データでの性能が良い場合が多い。
SELECT name FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id
);
FROM 句のサブクエリ(派生テーブル)
SELECT dept_name, avg_age
FROM (
SELECT d.name AS dept_name, AVG(u.age) AS avg_age
FROM users u
INNER JOIN departments d ON u.dept_id = d.id
GROUP BY d.id, d.name
) AS dept_stats
WHERE avg_age >= 30;
WITH 句(CTE: 共通テーブル式)
複雑なサブクエリを名前付きで定義し、可読性を高める。
WITH active_users AS (
SELECT * FROM users WHERE status = 'active'
),
user_orders AS (
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
)
SELECT
u.name,
COALESCE(o.order_count, 0) AS order_count
FROM active_users u
LEFT JOIN user_orders o ON u.id = o.user_id
ORDER BY order_count DESC;
ウィンドウ関数
集計しながら元の行を保持する。GROUP BY と違い行が減らない。
-- 各行に部署内の平均給与を付与
SELECT
name,
dept_id,
salary,
AVG(salary) OVER (PARTITION BY dept_id) AS dept_avg_salary
FROM employees;
-- 売上ランキング(同点は同順位)
SELECT
name,
sales,
RANK() OVER (ORDER BY sales DESC) AS rank
FROM employees;
-- 累積売上
SELECT
ordered_at,
total_price,
SUM(total_price) OVER (ORDER BY ordered_at) AS cumulative_sales
FROM orders;
-- 直前行との差分
SELECT
date,
daily_sales,
daily_sales - LAG(daily_sales) OVER (ORDER BY date) AS diff
FROM daily_reports;
| 関数 | 説明 |
|---|---|
ROW_NUMBER() | 連番(同値でも異なる番号) |
RANK() | ランク(同値は同順位、次は飛ぶ) |
DENSE_RANK() | ランク(同値は同順位、次は飛ばない) |
LAG(col, n) | n行前の値 |
LEAD(col, n) | n行後の値 |
FIRST_VALUE(col) | ウィンドウ内の最初の値 |
LAST_VALUE(col) | ウィンドウ内の最後の値 |
INSERT / UPDATE / DELETE
-- INSERT
INSERT INTO users (name, email, age) VALUES ('田中', 'tanaka@example.com', 30);
-- 複数行を一度に挿入
INSERT INTO users (name, email) VALUES
('佐藤', 'sato@example.com'),
('鈴木', 'suzuki@example.com');
-- UPDATE
UPDATE users SET age = 31 WHERE id = 1;
UPDATE users SET status = 'inactive', updated_at = NOW() WHERE last_login < '2023-01-01';
-- DELETE
DELETE FROM users WHERE id = 1;
DELETE FROM users WHERE status = 'deleted' AND created_at < '2023-01-01';
UPDATE と DELETE は WHERE を付け忘れると全件が対象になる。実行前に SELECT で対象を確認する。
DDL(テーブル定義)
-- テーブル作成
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT, -- MySQL
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
age INT,
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- カラム追加
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- カラム変更
ALTER TABLE users MODIFY COLUMN phone VARCHAR(30); -- MySQL
ALTER TABLE users ALTER COLUMN phone TYPE VARCHAR(30); -- PostgreSQL
-- カラム削除
ALTER TABLE users DROP COLUMN phone;
-- インデックス追加
CREATE INDEX idx_users_email ON users (email);
CREATE INDEX idx_orders_user_id ON orders (user_id);
-- テーブル削除
DROP TABLE users;
DROP TABLE IF EXISTS users; -- 存在する場合のみ削除
トランザクション
複数の操作をひとまとまりとして扱い、全て成功か全て失敗かを保証する。
BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
-- 問題がなければコミット
COMMIT;
-- エラーが起きた場合はロールバック
ROLLBACK;
よく使うSELECT クエリパターン
最新レコードを取得(ユーザーごとの最終ログイン)
SELECT u.id, u.name, l.logged_in_at
FROM users u
LEFT JOIN (
SELECT user_id, MAX(logged_in_at) AS logged_in_at
FROM login_logs
GROUP BY user_id
) l ON u.id = l.user_id;
重複レコードの検出
SELECT email, COUNT(*) AS cnt
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
ランキング上位N件を各グループから取得
-- 各部署の給与トップ3
SELECT *
FROM (
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn
FROM employees
) ranked
WHERE rn <= 3;
参考リンク
- SQL Standard (ISO/IEC 9075) - SQL国際標準仕様
- MySQL 8.0 リファレンスマニュアル - MySQL公式(日本語)
- PostgreSQL 16 ドキュメント - PostgreSQL公式