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 より優先。混在時は括弧で明示するのが安全。

パターンマッチ

正規表現(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 の扱い

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 / サブクエリで段階的に書くと読みやすい。