SELECT とクエリの基礎
SELECT、WHERE、ORDER BY
SELECT は1つ以上のテーブルから行を取得します。パフォーマンスと明確さのため * ではなく列を明示的に指定してください(スキーマ変更がアプリを壊しません)。WHERE はグループ化の前に行をフィルタします。ORDER BY は結果をソートします(ASC がデフォルト、DESC が降順)。LIMIT/OFFSET はページネーションを実装します — 大規模データセットにはキーセットページネーション(WHERE id > last_id)を優先します。
-- basic query: select specific columns
SELECT id, name, email
FROM users
WHERE age >= 18 AND status = 'active'
ORDER BY name ASC, created_at DESC
LIMIT 10 OFFSET 0;
-- select all columns (avoid in production)
SELECT * FROM products;
-- column aliases with AS
SELECT name AS product_name, price * 1.1 AS price_with_tax
FROM products;DISTINCT とエイリアス
DISTINCT は結果セットから重複行を削除します。個々の列ではなく行全体に作用します — SELECT DISTINCT city, country は一意の city+country ペアを返します。テーブルエイリアス(u、o)はクエリを短縮し、テーブルを自己結合する場合に必要です。列エイリアスは可読性のために出力列をリネームします。
-- unique values only
SELECT DISTINCT country FROM users;
SELECT DISTINCT city, country FROM users; -- unique combos
-- table aliases (essential for joins)
SELECT u.name, o.total
FROM users AS u
JOIN orders AS o ON u.id = o.user_id;
-- column alias (AS is optional)
SELECT name product_name, COUNT(*) count
FROM products
GROUP BY name;フィルタリング:BETWEEN、IN、IS NULL
BETWEEN は両端を含みます。IN はリストまたはサブクエリの任意の値にマッチします。NULL には IS NULL / IS NOT NULL が必要です(= NULL は使用不可)。NOT IN とサブクエリに注意してください — サブクエリが NULL を返す場合、NOT IN は行を全く返しません。NOT EXISTS を使用してください。NULL を正しく処理し、多くの場合より高速です。
SELECT * FROM products
WHERE price BETWEEN 10 AND 100 -- inclusive range
AND category IN ('tech', 'home') -- match any value
AND stock IS NOT NULL -- exclude NULLs
AND discount IS NULL; -- only NULLs
-- NOT IN with NULL caveat: returns nothing if subquery has NULL!
SELECT * FROM users
WHERE id NOT IN (SELECT user_id FROM orders); -- risky if NULLs
-- safer: NOT EXISTS
SELECT * FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id
);LIKE とパターンマッチング
LIKE は %(ゼロ以上の文字)と _(正確に1文字)をワイルドカードとして使用します。LIKE は MySQL 以外のほとんどのデータベースで大文字小文字を区別します(MySQL はデフォルトで大文字小文字を区別しません)。PostgreSQL では大文字小文字を区別しないマッチングに ILIKE を使用します。複雑なパターンには正規表現を使用します(PostgreSQL では ~、MySQL では REGEXP)。先頭の % 付き LIKE はインデックスを使用できません — パフォーマンスのためにフルテキスト検索を検討してください。
-- LIKE: basic pattern matching
SELECT * FROM users WHERE name LIKE 'A%'; -- starts with A
SELECT * FROM users WHERE name LIKE '%son'; -- ends with son
SELECT * FROM users WHERE name LIKE '%a%'; -- contains a
SELECT * FROM users WHERE name LIKE '_a%'; -- second char is a
-- ILIKE (PostgreSQL): case-insensitive
SELECT * FROM users WHERE name ILIKE 'a%';
-- SIMILAR TO (PostgreSQL): regex-like
SELECT * FROM users WHERE name SIMILAR TO '[AB]%';
-- full regex (PostgreSQL)
SELECT * FROM users WHERE name ~ '^A[a-z]+$';CASE 式
CASE は SQL の if-then-else で、行ごとに評価されます。SELECT、WHERE、ORDER BY、HAVING に現れます。'ピボット'パターン(CASE の SUM)は行を列に変換します — レポートに便利です。CASE は WHEN がマッチせず ELSE がない場合 NULL を返します。予測可能な結果のために常に ELSE を含めてください。
-- conditional logic in queries
SELECT
name,
price,
CASE
WHEN price < 10 THEN 'cheap'
WHEN price < 50 THEN 'moderate'
WHEN price < 100 THEN 'expensive'
ELSE 'luxury'
END AS price_category
FROM products;
-- CASE in aggregation (pivot table)
SELECT
category,
COUNT(*) AS total,
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) AS active_count,
SUM(CASE WHEN status = 'inactive' THEN 1 ELSE 0 END) AS inactive_count
FROM products
GROUP BY category;JOIN
INNER JOIN
INNER JOIN は両方のテーブルでマッチする行のみを返します。JOIN は INNER JOIN の短縮形です。マルチテーブルクエリでは、テーブルを段階的に結合します。ON は結合条件を指定し、USING(column) は両方のテーブルが同じ列名を持つ場合の短縮形です。内部結合は両側から非マッチング行を除外します。
-- only matching rows from both tables
SELECT u.name, o.total, o.created_at
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.total > 100
ORDER BY o.total DESC;
-- multiple joins
SELECT u.name, o.total, p.product_name
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id;
-- USING (when column names match)
SELECT * FROM users
JOIN profiles ON users.id = profiles.user_id;LEFT JOIN(LEFT OUTER JOIN)
LEFT JOIN は左テーブルのすべての行を返し、非マッチング右行には NULL を入れます。これは「すべて含める」クエリに不可欠です。アンチジョインパターン(WHERE right.id IS NULL)は右にマッチがない左テーブルの行を見つけます — 「注文していないユーザー」に便利です。COUNT(right.id) は非 NULL 値をカウントするため、注文のないユーザーには 0 を返します。
-- all users, with their orders (NULL if no orders)
SELECT u.name, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
ORDER BY u.name;
-- find users with NO orders (anti-join pattern)
SELECT u.name
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;
-- count orders per user (including zero)
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;RIGHT と FULL OUTER JOIN
RIGHT JOIN はすべての右テーブル行を返します。テーブルを入れ替えて LEFT JOIN を使用するのと同等です(より読みやすい)。FULL OUTER JOIN は両方のテーブルのすべての行を返し、マッチがない場合は NULL を入れます — データ照合に便利です。MySQL は FULL OUTER JOIN を直接サポートしません。LEFT JOIN UNION RIGHT JOIN でエミュレートします。
-- RIGHT JOIN: all rows from right table
SELECT u.name, o.total
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;
-- returns all orders, even orphaned ones (user_id = NULL)
-- FULL OUTER JOIN: all rows from both tables
SELECT u.name, o.total
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id;
-- returns all users AND all orders, matching where possible
-- Note: RIGHT JOIN is rarely used (just swap tables and use LEFT)
-- FULL OUTER JOIN is useful for finding mismatches between tables
SELECT u.name, o.total
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id
WHERE u.id IS NULL OR o.user_id IS NULL;CROSS JOIN と自己結合
CROSS JOIN はデカルト積を生成します — A のすべての行と B のすべての行をペアにします。組み合わせの生成(サイズ × 色)に使用します。自己結合(テーブルを自身に結合)は階層データ(従業員-管理者)、重複の発見、同じテーブル内の行の比較によく使用されます。自己結合では常にテーブルエイリアスを使用して2つの「コピー」を区別します。
-- CROSS JOIN: Cartesian product (every combination)
SELECT s.size, c.color
FROM sizes s
CROSS JOIN colors c;
-- produces all size+color combinations
-- Self join: join a table to itself
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
-- self join for hierarchical data
SELECT c.name AS child, p.name AS parent
FROM categories c
JOIN categories p ON c.parent_id = p.id;
-- self join to find duplicates
SELECT a.id, a.email, b.id AS dup_id
FROM users a
JOIN users b ON a.email = b.email AND a.id < b.id;NATURAL JOIN と JOIN タイプのまとめ
NATURAL JOIN は同じ名前の列で自動的に結合します — 便利ですが、スキーマ変更が結合動作を黙って変更する可能性があるため危険です。本番では避けてください。LATERAL 結合はサブクエリが外部クエリの列を参照できるようにします — 「グループごとのトップ N」クエリに強力です。カンマ構文(FROM a, b)は CROSS JOIN と同等です。
-- NATURAL JOIN: joins on all matching column names
-- (rarely recommended — implicit, fragile)
SELECT * FROM users NATURAL JOIN profiles;
-- joins on any column that exists in BOTH tables
-- Summary of join types:
-- INNER JOIN : matching rows only
-- LEFT JOIN : all left + matching right
-- RIGHT JOIN : all right + matching left
-- FULL JOIN : all from both sides
-- CROSS JOIN : Cartesian product
-- SELF JOIN : table joined to itself
-- LATERAL JOIN (PostgreSQL): subquery can reference outer query
SELECT u.name, recent.*
FROM users u,
LATERAL (
SELECT * FROM orders o
WHERE o.user_id = u.id
ORDER BY o.created_at DESC
LIMIT 3
) recent;GROUP BY と集計
GROUP BY と HAVING
GROUP BY は行をグループに折りたたみ、グループごとに1行にします。集計関数(COUNT、SUM、AVG、MIN、MAX)は各グループで動作します。WHERE はグループ化の前に個々の行をフィルタし、HAVING は集計後にグループをフィルタします。SELECT の非集計列は GROUP BY に現れなければなりません(標準 SQL)。MySQL は寛容ですが予測不能です — すべての非集計列を GROUP BY に含めてください。
-- aggregate per group
SELECT category, COUNT(*) AS cnt, AVG(price) AS avg_price
FROM products
GROUP BY category
HAVING COUNT(*) > 5
ORDER BY cnt DESC;
-- HAVING filters groups (after aggregation)
-- WHERE filters rows (before aggregation)
SELECT dept, AVG(salary) AS avg_sal
FROM employees
WHERE status = 'active' -- filter rows first
GROUP BY dept
HAVING AVG(salary) > 50000; -- then filter groups集計関数
COUNT(*) は NULL を含むすべての行をカウントし、COUNT(column) は非 NULL 値のみをカウントします。COUNT(DISTINCT col) は一意の値をカウントします。SUM/AVG は NULL を無視します。AVG = SUM/COUNT(非 NULL)なので、NULL は平均に影響します。STRING_AGG(PostgreSQL)/ GROUP_CONCAT(MySQL)はグループごとに文字列を連結します。BOOL_OR/BOOL_AND はいずれか/すべての値が true の場合 true を返します。
SELECT
COUNT(*) AS total_rows, -- counts all rows
COUNT(email) AS emails_filled, -- counts non-NULL emails
COUNT(DISTINCT country) AS countries,
SUM(amount) AS total_revenue,
AVG(amount) AS avg_order,
MIN(amount) AS smallest_order,
MAX(amount) AS largest_order,
-- string aggregation (PostgreSQL)
STRING_AGG(name, ', ') AS all_names,
-- boolean aggregation
BOOL_OR(is_active) AS any_active,
BOOL_AND(is_active) AS all_active
FROM orders;複数列の GROUP BY
複数列でグループ化するとグループの階層が作成されます。WITH ROLLUP は小計と総計行を追加します(グループ化列に NULL)。GROUPING SETS はどのグループ化組み合わせが必要かを正確に指定できます — ROLLUP より柔軟です。CUBE はすべての可能なグループ化組み合わせを生成します。これらはレポートと OLAP クエリに不可欠です。
-- multi-level grouping
SELECT
EXTRACT(YEAR FROM created_at) AS yr,
EXTRACT(MONTH FROM created_at) AS mon,
category,
COUNT(*) AS cnt,
SUM(total) AS revenue
FROM orders
GROUP BY yr, mon, category
ORDER BY yr DESC, mon DESC, cnt DESC;
-- GROUP BY with ROLLUP (subtotals + grand total)
SELECT category, COUNT(*) AS cnt
FROM products
GROUP BY category WITH ROLLUP;
-- last row has NULL category = grand total
-- GROUPING SETS (PostgreSQL): specify multiple groupings
SELECT category, status, COUNT(*)
FROM products
GROUP BY GROUPING SETS ((category, status), (category), ());HAVING と WHERE
主な区別:WHERE は集計前に個々の行をフィルタし(SUM、COUNT などを使用不可)、HAVING は集計後にグループをフィルタします(集計関数を使用可)。WHERE で早期にデータを削減し(より良いパフォーマンス)、次に HAVING で集計結果をフィルタします。両方が同じクエリに現れます — 最初に WHERE、次に GROUP BY、次に HAVING。
-- WHERE: filters rows BEFORE grouping
-- Cannot use aggregates
SELECT category, COUNT(*) AS cnt
FROM products
WHERE price > 10 -- OK: filter on raw column
GROUP BY category;
-- HAVING: filters groups AFTER grouping
-- Can use aggregates
SELECT category, COUNT(*) AS cnt
FROM products
GROUP BY category
HAVING COUNT(*) > 5 -- OK: filter on aggregate
AND AVG(price) > 20; -- OK: multiple aggregate filters
-- combining both
SELECT category, COUNT(*) AS cnt
FROM products
WHERE price > 10 -- filter rows
GROUP BY category -- group
HAVING COUNT(*) > 5; -- filter groups日付/時刻の集計
日付切り捨ては時系列レポートに不可欠です。DATE(col) は日付のみを抽出し、EXTRACT/TIME_PART は特定のコンポーネント(年、月、時間)を取得します。TO_CHAR はグループ化と表示のために日付をフォーマットします。時系列分析には、タイムスタンプ型を保持する DATE_TRUNC('month', col) を検討してください。大きなテーブルのパフォーマンスのために日付列にインデックスを付けます。
-- group by date parts
SELECT
DATE(created_at) AS order_date,
COUNT(*) AS orders,
SUM(total) AS revenue
FROM orders
GROUP BY DATE(created_at)
ORDER BY order_date DESC;
-- group by hour
SELECT
EXTRACT(HOUR FROM created_at) AS hr,
COUNT(*) AS cnt
FROM orders
WHERE created_at >= CURRENT_DATE
GROUP BY hr
ORDER BY hr;
-- monthly revenue trend
SELECT
TO_CHAR(created_at, 'YYYY-MM') AS month,
SUM(total) AS revenue,
COUNT(*) AS orders,
AVG(total) AS avg_order
FROM orders
WHERE created_at >= '2024-01-01'
GROUP BY month
ORDER BY month;サブクエリと CTE
スカラーと列サブクエリ
スカラーサブクエリは単一値を返し、値が期待されるどこでも使用できます。列サブクエリは1列を返し、IN、ANY、ALL で使用されます。SELECT のサブクエリ(相関)は外部行ごとに1回実行されます — 大規模データセットで遅くなる可能性があります。より良いパフォーマンスのために GROUP BY 付き JOIN として書き直すことを検討してください。
-- scalar subquery (returns single value)
SELECT name, age
FROM users
WHERE age > (SELECT AVG(age) FROM users);
-- column subquery (returns one column, multiple rows)
SELECT name
FROM users
WHERE id IN (SELECT user_id FROM orders WHERE total > 100);
-- subquery in SELECT
SELECT
u.name,
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_count
FROM users u;相関サブクエリと EXISTS
相関サブクエリは外部クエリを参照し、外部行ごとに1回実行されます — 潜在的に遅いです。EXISTS/NOT EXISTS はショートサーキットするため(最初のマッチで停止)効率的です。NOT EXISTS は「マッチする行のない行」を見つける推奨方法です — NULL を正しく処理し、多くの場合 NOT IN より高速です。データベースは相関サブクエリを結合に最適化する場合があります。
-- correlated: subquery references outer query
SELECT u.name
FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id
AND o.total > 1000
);
-- NOT EXISTS: users without any orders
SELECT u.name
FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id
);
-- correlated subquery in SELECT (runs per row)
SELECT
u.name,
(SELECT MAX(o.total)
FROM orders o
WHERE o.user_id = u.id) AS max_order
FROM users u;共通テーブル式(CTE)
CTE(WITH 節)は複雑なクエリを読みやすくする名前付き一時結果セットを作成します。サブクエリとは異なり、CTE は複数回参照でき、上から下に読めます。ほとんどのデータベースで、CTE はインライン化されます(最適化はクエリレベルで行われます)。PostgreSQL 12+ は MATERIALIZED/NOT MATERIALIZED ヒントをサポートします。CTE は再帰クエリにも必要です。
-- CTE: named temporary result set
WITH active_users AS (
SELECT id, name FROM users WHERE status = 'active'
),
user_orders AS (
SELECT user_id, COUNT(*) AS cnt, SUM(total) AS revenue
FROM orders
GROUP BY user_id
)
SELECT au.name, uo.cnt, uo.revenue
FROM active_users au
LEFT JOIN user_orders uo ON au.id = uo.user_id
ORDER BY uo.revenue DESC NULLS LAST;
-- CTEs improve readability for complex queries
-- They are NOT materialized (just syntactic sugar) in most DBs
-- (PostgreSQL 12+ can materialize with MATERIALIZED keyword)再帰 CTE
再帰 CTE は自身を参照し、ツリー/グラフトラバーサルとシーケンス生成を可能にします。構造:ベースケース UNION ALL 再帰ケース。再帰ケースは CTE を参照し、終了しなければなりません(無限ループを防ぐため WHERE を追加)。一般的な用途:組織図、カテゴリツリー、依存関係グラフ、日付シーケンス。各データベースで構文が少し異なります — DBMS ドキュメントを確認してください。
-- hierarchical data: org chart
WITH RECURSIVE org_tree AS (
-- base case: top-level managers
SELECT id, name, manager_id, 1 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- recursive case: direct reports
SELECT e.id, e.name, e.manager_id, ot.level + 1
FROM employees e
JOIN org_tree ot ON e.manager_id = ot.id
)
SELECT level, name FROM org_tree ORDER BY level, name;
-- generate a series of dates
WITH RECURSIVE dates AS (
SELECT DATE '2024-01-01' AS d
UNION ALL
SELECT d + 1 FROM dates WHERE d < '2024-01-31'
)
SELECT d FROM dates;
-- factorial
WITH RECURSIVE fact(n, result) AS (
SELECT 1, 1
UNION ALL
SELECT n + 1, result * (n + 1) FROM fact WHERE n < 10
)
SELECT * FROM fact;サブクエリ演算子:ANY、ALL
ANY と ALL は値をサブクエリ結果セットと比較します。> ANY は「少なくとも1つより大きい」を意味します。> ALL は「すべてより大きい」を意味します。= ANY は IN と同等です。<> ALL は NOT IN と同等ですが NULL をより安全に処理します。これらの演算子は IN/EXISTS より使用頻度は低いですが、特定のクエリをより自然に表現できます。
-- ANY: greater than ANY of the values (= at least one)
SELECT * FROM products
WHERE price > ANY (
SELECT price FROM products WHERE category = 'tech'
);
-- true if price exceeds at least one tech product's price
-- ALL: greater than ALL values (= every one)
SELECT * FROM products
WHERE price > ALL (
SELECT price FROM products WHERE category = 'tech'
);
-- true if price exceeds every tech product's price
-- = ANY is equivalent to IN
SELECT * FROM users
WHERE id = ANY (SELECT user_id FROM orders);
-- <> ALL is equivalent to NOT IN (but NULL-safe)
SELECT * FROM users
WHERE id <> ALL (SELECT user_id FROM orders WHERE total < 0);ウィンドウ関数
ROW_NUMBER、RANK、DENSE_RANK
ROW_NUMBER は一意の連番(1、2、3...)を割り当てます。RANK は同順位に同じランクを与えますが後続の番号をスキップします(1、1、3)。DENSE_RANK はスキップせずに同順位に同じランクを与えます(1、1、2)。PARTITION BY は行をグループに分割し、関数はパーティションごとにリセットします。「グループごとのトップ N」パターン(ROW_NUMBER + WHERE rn <= N)は分析で非常に一般的です。
SELECT
name,
salary,
dept,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,
RANK() OVER (ORDER BY salary DESC) AS rank,
DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank
FROM employees;
-- top 3 earners per department
SELECT * FROM (
SELECT
name,
dept,
salary,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM employees
) ranked
WHERE rn <= 3;LAG と LEAD
LAG は前の行の値にアクセスし、LEAD は次の行の値にアクセスします。両方ともオプションのオフセット(デフォルト 1)とデフォルト値(デフォルト NULL)を受け入れます。時系列分析に不可欠です:日々の変化、移動比較、ギャップ検出。NULLIF はパーセンテージ計算でのゼロ除算を防ぎます。決定的な結果のために OVER 節で常に ORDER BY を指定してください。
-- compare each row to previous/next
SELECT
date,
revenue,
LAG(revenue) OVER (ORDER BY date) AS prev_day,
LEAD(revenue) OVER (ORDER BY date) AS next_day,
revenue - LAG(revenue) OVER (ORDER BY date) AS daily_change,
ROUND(
(revenue - LAG(revenue) OVER (ORDER BY date)) * 100.0
/ NULLIF(LAG(revenue) OVER (ORDER BY date), 0),
2
) AS pct_change
FROM daily_sales
ORDER BY date;
-- LAG with offset and default
SELECT
date,
revenue,
LAG(revenue, 7) OVER (ORDER BY date) AS revenue_7_days_ago
FROM daily_sales;累計合計と移動平均
ウィンドウフレームは関数が動作する行を定義します。ROWS BETWEEN は物理行オフセットを使用し、RANGE は論理値範囲を使用します(日付ギャップに適しています)。UNBOUNDED PRECEDING は「最初から」を意味します。累計合計(累積 SUM)と移動平均が最も一般的な分析パターンです。フレームなしでは、集計ウィンドウ関数はデフォルトを使用します:RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。