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 JOINLEFT 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';

UPDATEDELETEWHERE を付け忘れると全件が対象になる。実行前に 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;

参考リンク