SELECT 詳細
射影(列選択)/ 選択(WHERE)/ 並び替え / 限定 / DISTINCT / 演算子 / 関数 / NULL / CASE まで。
基本
SELECT id, email FROM users;
SELECT * FROM users;
* は便利だが本番コードでは避けるのが流儀(列順変更で挙動変わる、転送量増、不要列)。
列のエイリアス
SELECT
id AS user_id,
name "Display Name",
email
FROM users;
計算列
SELECT
id,
price,
price * 1.10 AS price_with_tax,
upper(email) AS email_upper
FROM products;
WHERE
-- 等価
SELECT * FROM posts WHERE status = 'published';
-- 比較
SELECT * FROM posts WHERE views > 100;
SELECT * FROM posts WHERE views BETWEEN 100 AND 1000;
-- IN
SELECT * FROM posts WHERE status IN ('draft', 'published');
SELECT * FROM posts WHERE status NOT IN ('archived');
-- LIKE
SELECT * FROM users WHERE email LIKE '%@example.com';
SELECT * FROM users WHERE email ILIKE 'ALICE%'; -- 大小無視 (Postgres)
-- NULL
SELECT * FROM posts WHERE published_at IS NULL;
SELECT * FROM posts WHERE published_at IS NOT NULL;
-- 真偽
SELECT * FROM users WHERE is_active; -- = TRUE
SELECT * FROM users WHERE NOT is_active;
論理演算子
SELECT * FROM posts
WHERE status = 'published'
AND views >= 100
AND (created_at >= '2026-01-01' OR is_featured);
AND が OR より優先。混在時は括弧で明示するのが安全。
パターンマッチ
- % — 0 文字以上
- _ — 任意の 1 文字
- エスケープは
\またはESCAPE '#'
正規表現(Postgres)
SELECT * FROM users WHERE email ~ '^[a-z]+@'; -- 大小区別
SELECT * FROM users WHERE email ~* '@example\.com$'; -- 大小無視
SELECT * FROM users WHERE email !~ '^admin'; -- マッチしない
ORDER BY
SELECT * FROM posts ORDER BY created_at DESC;
SELECT * FROM posts ORDER BY status, created_at DESC;
SELECT * FROM posts ORDER BY views DESC NULLS LAST;
SELECT * FROM posts ORDER BY 2, 3; -- 列番号(推奨されない)
LIMIT / OFFSET
SELECT * FROM posts ORDER BY created_at DESC LIMIT 10;
SELECT * FROM posts ORDER BY id LIMIT 10 OFFSET 20;
-- 標準 SQL
SELECT * FROM posts ORDER BY id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
カーソル式ページング(推奨)
-- 前回最後の id を記録しておき
SELECT * FROM posts WHERE id < $last_id ORDER BY id DESC LIMIT 20;
DISTINCT
-- 重複を排除
SELECT DISTINCT category FROM posts;
-- 複数列の組み合わせで一意
SELECT DISTINCT category, status FROM posts;
-- DISTINCT ON(Postgres)— 各グループの 1 件
SELECT DISTINCT ON (user_id) user_id, title, created_at
FROM posts
ORDER BY user_id, created_at DESC;
-- ↑ 各ユーザの最新投稿を 1 件だけ
CASE
SELECT
id,
title,
CASE
WHEN views > 1000 THEN 'hot'
WHEN views > 100 THEN 'warm'
ELSE 'cold'
END AS popularity
FROM posts;
-- 短縮形(同じ列を比較)
SELECT
CASE status
WHEN 'draft' THEN '下書き'
WHEN 'published' THEN '公開中'
ELSE '不明'
END AS status_jp
FROM posts;
NULL の扱い
- NULL は「不明」。
NULL = NULLは NULL (TRUE ではない) - 判定は
IS NULL/IS NOT NULL - NULL を含む計算はすべて NULLに
- 文字列連結も同じ(
'a' || NULL = NULL)
COALESCE / NULLIF
-- 最初の非 NULL を返す
SELECT COALESCE(nickname, name, email) FROM users;
-- 値が等しければ NULL に
SELECT NULLIF(value, 0) FROM data;
-- 0 で割らないように
SELECT total / NULLIF(count, 0);
主要な文字列関数
UPPER(s) LOWER(s) INITCAP(s)
LENGTH(s) CHAR_LENGTH(s)
TRIM(s) LTRIM(s) RTRIM(s)
LEFT(s, n) RIGHT(s, n)
SUBSTRING(s, 1, 10)
REPLACE(s, 'old', 'new')
CONCAT(a, b, c) -- a || b || c も同じ(Postgres)
POSITION('@' IN email)
SPLIT_PART('a,b,c', ',', 2) -- 'b'
REGEXP_REPLACE(s, 'pat', 'rep')
主要な数値関数
ABS(x) ROUND(x, 2) CEIL(x) FLOOR(x) TRUNC(x)
MOD(a, b) POWER(x, n) SQRT(x)
GREATEST(a, b, c) LEAST(a, b, c)
RANDOM() -- 0〜1
FLOOR(RANDOM() * 100) -- 0〜99 整数
主要な日付・時刻関数(Postgres)
NOW() -- 現在
CURRENT_DATE
CURRENT_TIMESTAMP
AGE(timestamp1, timestamp2)
DATE_TRUNC('month', created_at)
DATE_PART('year', created_at)
EXTRACT(EPOCH FROM created_at)
created_at + INTERVAL '7 days'
TO_CHAR(created_at, 'YYYY-MM-DD')
TO_TIMESTAMP('2026-01-01', 'YYYY-MM-DD')
典型的なクエリ
-- 過去 7 日
SELECT * FROM posts WHERE created_at >= NOW() - INTERVAL '7 days';
-- 月初・月末
SELECT date_trunc('month', NOW()) AS month_start;
SELECT date_trunc('month', NOW()) + INTERVAL '1 month' - INTERVAL '1 day' AS month_end;
-- 月別集計
SELECT date_trunc('month', created_at) AS month, count(*)
FROM posts GROUP BY 1 ORDER BY 1;
型キャスト
SELECT '42'::INT; -- Postgres
SELECT CAST('42' AS INT); -- 標準
-- TEXT に
SELECT id::TEXT FROM posts;
-- 危険:文字列 → 数値で型エラー
SELECT 'abc'::INT; -- エラー
EXISTS / IN / NOT EXISTS
-- 「投稿を持つユーザ」
SELECT * FROM users
WHERE EXISTS (SELECT 1 FROM posts WHERE posts.user_id = users.id);
-- IN(同じ意味)
SELECT * FROM users
WHERE id IN (SELECT user_id FROM posts);
-- NOT EXISTS(投稿を持たないユーザ)
SELECT * FROM users
WHERE NOT EXISTS (SELECT 1 FROM posts WHERE posts.user_id = users.id);
EXISTS は1 件見つかった時点で停止するので一般に IN より速い。
サブクエリ(スカラー)
SELECT
id,
title,
(SELECT count(*) FROM comments WHERE comments.post_id = posts.id) AS comment_count
FROM posts;
ANY / ALL / SOME
-- 「他より高い」
SELECT * FROM products WHERE price > ALL (SELECT price FROM old_products);
-- 「いずれかと等しい」
SELECT * FROM users WHERE id = ANY (SELECT user_id FROM banned);
UNION / INTERSECT / EXCEPT
-- 重複排除
SELECT id FROM users
UNION
SELECT id FROM admins;
-- 重複そのまま(速い、推奨)
SELECT id FROM users
UNION ALL
SELECT id FROM admins;
-- 共通
SELECT id FROM users INTERSECT SELECT id FROM admins;
-- 差分
SELECT id FROM users EXCEPT SELECT id FROM admins;
RETURNING(Postgres)
INSERT INTO posts (title) VALUES ('hello') RETURNING id, created_at;
UPDATE posts SET views = views + 1 WHERE id = 1 RETURNING views;
DELETE FROM posts WHERE created_at < '2020-01-01' RETURNING id;
VALUES
-- インラインの一時データ
SELECT * FROM (VALUES (1, 'a'), (2, 'b'), (3, 'c')) AS t(id, name);
-- LEFT JOIN と組み合わせて差分検出など
FILTER 句
-- 集計関数に条件を付ける(Postgres)
SELECT
count(*) AS total,
count(*) FILTER (WHERE status = 'published') AS published,
count(*) FILTER (WHERE status = 'draft') AS draft
FROM posts;
テーブル間の比較(差分)
-- 実テーブルにあるが期待リストに無い
SELECT id FROM actual
EXCEPT
SELECT id FROM expected;
-- 逆向き
SELECT id FROM expected
EXCEPT
SELECT id FROM actual;
NULL を含むソート
SELECT * FROM posts ORDER BY published_at NULLS LAST;
SELECT * FROM posts ORDER BY published_at DESC NULLS FIRST;
FOR UPDATE / SHARE(行ロック)
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- 他のトランザクションがこの行を更新できなくなる
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
失敗パターン
| NG | 原因 |
|---|---|
WHERE name = NULL | 常に NULL = false 扱い。IS NULL を使う |
SELECT * を本番コードに | 列追加で挙動変化 |
OR が多くて遅い | UNION ALL に書き換え |
| OFFSET 100000 が遅い | カーソル式ページング |
| 大文字小文字が一致しない | ILIKE / LOWER + 関数式 INDEX |
| NULL 行が混入 | COALESCE で初期値を入れる |
原則
SELECT * を避け、必要な列だけ書く。WHERE 句はインデックスを意識。 複雑なロジックは CTE / サブクエリで段階的に書くと読みやすい。