モニター画面に表示されたカラフルなプログラミングコードのクローズアップ。

開発者のためのSQLチートシート:2026年版 基礎から高度なクエリまで

📷 Myburgh Roux / Pexels

開発者のためのSQLチートシート:2026年版 基礎から高度なクエリまで

SELECT、JOIN、サブクエリ、ウィンドウ関数、CTE、インデックス、最適化のヒントを網羅した包括的なSQLリファレンスガイドです。

2026年3月19日9分で読了

SQLは2026年においても、開発者が持つことのできる最も重要なスキルの一つです。REST APIの構築、アナリティクスパイプラインの操作、本番データのデバッグなど、SQLの読み書きは避けて通れません。このチートシートは基礎を超え、シンプルなSELECT文から高度なウィンドウ関数やクエリ最適化戦略まで、あらゆることをカバーする実践的なリファレンスを提供します。

基本的なSELECTクエリ

すべてのSQLの旅はSELECT文から始まります。これはすべてのデータ取得操作の基盤です。

カラムの選択

-- Select specific columns
SELECT first_name, last_name, email
FROM users;

-- Select all columns (avoid in production code)
SELECT *
FROM users;

-- Aliasing columns for clarity
SELECT
  first_name AS "First Name",
  last_name AS "Last Name",
  created_at AS "Registration Date"
FROM users;

-- Selecting distinct values
SELECT DISTINCT department
FROM employees;

WHEREによるフィルタリング

WHERE句を使用すると、条件に基づいて行をフィルタリングできます。正しいクエリを書くためには、演算子の優先順位とNULLの処理を理解することが重要です。

-- Basic comparison
SELECT * FROM products
WHERE price > 50.00;

-- Multiple conditions with AND / OR
SELECT * FROM orders
WHERE status = 'shipped'
  AND total_amount > 100
  AND created_at >= '2026-01-01';

-- IN operator for multiple values
SELECT * FROM users
WHERE country IN ('US', 'CA', 'GB', 'DE');

-- BETWEEN for ranges (inclusive)
SELECT * FROM events
WHERE event_date BETWEEN '2026-01-01' AND '2026-12-31';

-- Pattern matching with LIKE
SELECT * FROM products
WHERE name LIKE '%wireless%';  -- contains 'wireless'

-- Handling NULLs (= does NOT work with NULL)
SELECT * FROM users
WHERE phone IS NULL;

SELECT * FROM users
WHERE phone IS NOT NULL;

よくある間違いは、WHERE column IS NULLではなくWHERE column = NULLと書くことです。後者は常にUNKNOWNと評価され、行を返しません。

ORDER BYによるソート

-- Single column sort
SELECT * FROM products
ORDER BY price DESC;

-- Multiple column sort
SELECT * FROM employees
ORDER BY department ASC, salary DESC;

-- Sort by column position (less readable, but valid)
SELECT first_name, last_name, hire_date
FROM employees
ORDER BY 3 DESC;

-- NULLS FIRST / NULLS LAST (PostgreSQL, Oracle)
SELECT * FROM tasks
ORDER BY due_date ASC NULLS LAST;

結果の制限

-- PostgreSQL / MySQL
SELECT * FROM logs
ORDER BY created_at DESC
LIMIT 100;

-- With offset for pagination
SELECT * FROM products
ORDER BY id
LIMIT 20 OFFSET 40;  -- Page 3 (20 items per page)

-- SQL Server
SELECT TOP 100 * FROM logs
ORDER BY created_at DESC;

-- Standard SQL (FETCH FIRST)
SELECT * FROM logs
ORDER BY created_at DESC
FETCH FIRST 100 ROWS ONLY;

集約関数とGROUP BY

集約関数は複数の行を単一の要約値に集約します。ほぼ常にGROUP BYと一緒に使用されます。

-- Common aggregate functions
SELECT
  department,
  COUNT(*) AS employee_count,
  AVG(salary) AS avg_salary,
  MIN(salary) AS min_salary,
  MAX(salary) AS max_salary,
  SUM(salary) AS total_payroll
FROM employees
GROUP BY department;

-- Filtering groups with HAVING
SELECT
  category,
  COUNT(*) AS product_count,
  AVG(price) AS avg_price
FROM products
GROUP BY category
HAVING COUNT(*) > 10
ORDER BY avg_price DESC;

-- COUNT variations
SELECT
  COUNT(*) AS total_rows,         -- counts all rows including NULLs
  COUNT(email) AS has_email,      -- counts non-NULL emails
  COUNT(DISTINCT city) AS cities  -- counts unique cities
FROM users;

重要な違い:WHEREは集約前に個々の行をフィルタリングし、HAVINGは集約後にグループをフィルタリングします。WHERE句で集約関数を参照することはできません。

JOIN:テーブルの結合

JOINはSQLが真に強力になる部分です。リレーショナルデータを扱うためには、さまざまなタイプを理解することが不可欠です。

INNER JOIN

両方のテーブルで一致する値を持つ行のみを返します。

SELECT
  o.id AS order_id,
  o.order_date,
  c.name AS customer_name,
  c.email
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id;

LEFT JOIN (LEFT OUTER JOIN)

左テーブルのすべての行と、右テーブルの一致する行を返します。一致しない右側のカラムはNULLを返します。

-- Find all customers and their orders (including customers with no orders)
SELECT
  c.name,
  c.email,
  o.id AS order_id,
  o.total_amount
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id;

-- Find customers who have never placed an order
SELECT c.name, c.email
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.id IS NULL;

RIGHT JOINとFULL OUTER JOIN

-- RIGHT JOIN: all rows from the right table
SELECT
  e.name AS employee,
  d.name AS department
FROM employees e
RIGHT JOIN departments d ON e.department_id = d.id;

-- FULL OUTER JOIN: all rows from both tables
SELECT
  s.name AS student,
  c.title AS course
FROM students s
FULL OUTER JOIN enrollments e ON s.id = e.student_id
FULL OUTER JOIN courses c ON e.course_id = c.id;

CROSS JOIN

2つのテーブルのデカルト積を生成します。最初のテーブルのすべての行が2番目のテーブルのすべての行と組み合わされます。

-- Generate all possible size-color combinations
SELECT
  s.size_name,
  c.color_name
FROM sizes s
CROSS JOIN colors c;

Self JOIN

自分自身と結合するテーブルで、階層データや比較データに便利です。

-- Find employees and their managers
SELECT
  e.name AS employee,
  m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

サブクエリ

サブクエリは他のクエリの中にネストされたクエリです。SELECT、FROM、WHERE、HAVING句に出現できます。

WHEREでのサブクエリ

-- Find products priced above the average
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);

-- Find customers who placed orders in the last 30 days
SELECT name, email
FROM customers
WHERE id IN (
  SELECT DISTINCT customer_id
  FROM orders
  WHERE order_date >= CURRENT_DATE - INTERVAL '30 days'
);

相関サブクエリ

相関サブクエリは外部クエリのカラムを参照し、外部行ごとに1回実行されます。強力ですが、大規模なデータセットではJOINより遅くなる可能性があります。

-- Find employees who earn more than their department average
SELECT e.name, e.salary, e.department_id
FROM employees e
WHERE e.salary > (
  SELECT AVG(e2.salary)
  FROM employees e2
  WHERE e2.department_id = e.department_id
);

-- EXISTS check
SELECT c.name
FROM customers c
WHERE EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.customer_id = c.id
    AND o.total_amount > 1000
);

FROMでのサブクエリ(派生テーブル)

SELECT
  dept_stats.department,
  dept_stats.avg_salary,
  dept_stats.employee_count
FROM (
  SELECT
    department,
    AVG(salary) AS avg_salary,
    COUNT(*) AS employee_count
  FROM employees
  GROUP BY department
) AS dept_stats
WHERE dept_stats.employee_count > 5
ORDER BY dept_stats.avg_salary DESC;

CTE(Common Table Expressions)

CTEはモジュール式で読みやすいSQLを書く方法を提供します。単一のクエリの実行期間中に存在する一時的な名前付き結果セットを定義します。

基本的なCTE

WITH active_customers AS (
  SELECT
    c.id,
    c.name,
    c.email,
    COUNT(o.id) AS order_count,
    SUM(o.total_amount) AS total_spent
  FROM customers c
  JOIN orders o ON c.id = o.customer_id
  WHERE o.order_date >= '2026-01-01'
  GROUP BY c.id, c.name, c.email
)
SELECT *
FROM active_customers
WHERE total_spent > 500
ORDER BY total_spent DESC;

複数のCTE

WITH
monthly_revenue AS (
  SELECT
    DATE_TRUNC('month', order_date) AS month,
    SUM(total_amount) AS revenue
  FROM orders
  WHERE order_date >= '2025-01-01'
  GROUP BY DATE_TRUNC('month', order_date)
),
revenue_with_growth AS (
  SELECT
    month,
    revenue,
    LAG(revenue) OVER (ORDER BY month) AS prev_month_revenue,
    ROUND(
      (revenue - LAG(revenue) OVER (ORDER BY month))
      / LAG(revenue) OVER (ORDER BY month) * 100, 2
    ) AS growth_pct
  FROM monthly_revenue
)
SELECT *
FROM revenue_with_growth
ORDER BY month;

再帰CTE

再帰CTEは、組織図、カテゴリツリー、グラフ走査などの階層データに最適です。

WITH RECURSIVE org_chart AS (
  -- Base case: top-level managers (no manager)
  SELECT id, name, manager_id, 1 AS level
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  -- Recursive case: employees with a manager already in the result
  SELECT e.id, e.name, e.manager_id, oc.level + 1
  FROM employees e
  JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT
  REPEAT('  ', level - 1) || name AS org_tree,
  level
FROM org_chart
ORDER BY level, name;

ウィンドウ関数

ウィンドウ関数は、現在の行に関連する行のセットに対して計算を実行しますが、単一の出力行に集約しません。モダンSQLで最も強力な機能の一つです。

ROW_NUMBER、RANK、DENSE_RANK

SELECT
  name,
  department,
  salary,
  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;

違い:2人の従業員がランク2で同点の場合、ROW_NUMBERは任意に2と3を割り当て、RANKは両方に2を割り当てて4にスキップし、DENSE_RANKは両方に2を割り当てて3に続けます。

パーティション付きウィンドウ関数

-- Top 3 earners per department
WITH ranked AS (
  SELECT
    name,
    department,
    salary,
    ROW_NUMBER() OVER (
      PARTITION BY department
      ORDER BY salary DESC
    ) AS dept_rank
  FROM employees
)
SELECT * FROM ranked
WHERE dept_rank <= 3;

LAGとLEAD

これらの関数を使用すると、前の行または次の行の値にアクセスできます。

SELECT
  order_date,
  total_amount,
  LAG(total_amount, 1) OVER (ORDER BY order_date) AS prev_day_amount,
  LEAD(total_amount, 1) OVER (ORDER BY order_date) AS next_day_amount,
  total_amount - LAG(total_amount, 1) OVER (ORDER BY order_date) AS daily_change
FROM daily_sales;

累積合計と移動平均

SELECT
  order_date,
  total_amount,
  SUM(total_amount) OVER (
    ORDER BY order_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS running_total,
  AVG(total_amount) OVER (
    ORDER BY order_date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ) AS seven_day_avg
FROM daily_sales;

NTILEによるバケット分割

-- Divide customers into 4 quartiles by spending
SELECT
  name,
  total_spent,
  NTILE(4) OVER (ORDER BY total_spent DESC) AS spending_quartile
FROM customer_summary;

インデックス戦略

効率的なSQLを書くことは、クエリ構文だけでなく、データベースがクエリを高速に実行できるようにすることでもあります。インデックスはそのための主要なメカニズムです。

インデックスの種類

-- B-tree index (default, good for equality and range queries)
CREATE INDEX idx_users_email ON users (email);

-- Composite index (column order matters)
CREATE INDEX idx_orders_customer_date
ON orders (customer_id, order_date DESC);

-- Unique index (also enforces uniqueness)
CREATE UNIQUE INDEX idx_users_email_unique ON users (email);

-- Partial index (PostgreSQL - index only a subset of rows)
CREATE INDEX idx_orders_pending
ON orders (created_at)
WHERE status = 'pending';

-- GIN index for full-text search (PostgreSQL)
CREATE INDEX idx_articles_search
ON articles USING GIN (to_tsvector('english', title || ' ' || body));

インデックスを作成すべきとき

インデックスは読み取りを高速化しますが、書き込みを遅くします。WHERE句、JOIN条件、ORDER BY句に頻繁に現れるカラムにインデックスを作成してください。パーシャルインデックスを使用する場合を除き、カーディナリティが非常に低いカラム(booleanカラムなど)にはインデックスを作成しないでください。

(a, b, c)の複合インデックスは、aa AND ba AND b AND cでフィルタリングするクエリに対応できますが、b単独やc単独には対応できません。カラムの順序が重要です。

EXPLAINでクエリパフォーマンスを確認する

-- PostgreSQL
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE customer_id = 42
  AND order_date >= '2026-01-01';

-- MySQL
EXPLAIN
SELECT * FROM orders
WHERE customer_id = 42
  AND order_date >= '2026-01-01';

大きなテーブルでのシーケンシャルスキャン(Seq Scan)は、インデックスが不足していることを示しています。Index ScanまたはIndex Only Scanが表示されるのが望ましいです。

クエリ最適化のヒント

1. SELECT *を避ける

必要なカラムだけを指定してください。これによりI/O、メモリ使用量が削減され、インデックスのみのスキャンが可能になる場合があります。

-- Bad
SELECT * FROM users WHERE id = 42;

-- Good
SELECT id, name, email FROM users WHERE id = 42;

2. 大きなサブクエリにはINの代わりにEXISTSを使用する

-- Slower with large subquery results
SELECT * FROM customers
WHERE id IN (SELECT customer_id FROM orders);

-- Faster: stops at first match
SELECT * FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);

3. インデックス付きカラムへの関数適用を避ける

-- Bad: index on created_at is not used
SELECT * FROM orders
WHERE YEAR(created_at) = 2026;

-- Good: index-friendly range scan
SELECT * FROM orders
WHERE created_at >= '2026-01-01'
  AND created_at < '2027-01-01';

4. 可能な場合はUNIONの代わりにUNION ALLを使用する

UNIONは重複を除去するためソートが必要ですが、UNION ALLは必要ありません。結果セットがすでに一意であることが分かっている場合は、常にUNION ALLを使用してください。

-- Slower (deduplicates)
SELECT name FROM employees
UNION
SELECT name FROM contractors;

-- Faster (no deduplication)
SELECT name FROM employees
UNION ALL
SELECT name FROM contractors;

5. 大量データにはバッチ処理を使用する

-- Instead of deleting millions of rows at once
-- Delete in batches to avoid locking the table
DELETE FROM logs
WHERE created_at < '2025-01-01'
LIMIT 10000;
-- Repeat until 0 rows affected

6. 適切なデータ型を使用する

小さなデータ型は、メモリとインデックスページにより多くの行が収まることを意味します。範囲が許す場合はBIGINTの代わりにINTEGERを使用してください。インデックス付きカラムにはTEXTよりも適切な長さのVARCHARを使用してください。

便利なSQLパターン

UPSERT (INSERT ON CONFLICT)

-- PostgreSQL
INSERT INTO user_settings (user_id, theme, language)
VALUES (42, 'dark', 'en')
ON CONFLICT (user_id)
DO UPDATE SET
  theme = EXCLUDED.theme,
  language = EXCLUDED.language;

-- MySQL
INSERT INTO user_settings (user_id, theme, language)
VALUES (42, 'dark', 'en')
ON DUPLICATE KEY UPDATE
  theme = VALUES(theme),
  language = VALUES(language);

CASEによるデータのピボット

SELECT
  product_id,
  SUM(CASE WHEN month = 1 THEN revenue ELSE 0 END) AS jan,
  SUM(CASE WHEN month = 2 THEN revenue ELSE 0 END) AS feb,
  SUM(CASE WHEN month = 3 THEN revenue ELSE 0 END) AS mar
FROM monthly_revenue
GROUP BY product_id;

日付シリーズの生成

-- PostgreSQL: generate a series of dates
SELECT generate_series(
  '2026-01-01'::date,
  '2026-12-31'::date,
  '1 day'::interval
)::date AS date;

-- Use with LEFT JOIN to fill gaps in time series data
SELECT
  d.date,
  COALESCE(s.total, 0) AS daily_total
FROM generate_series('2026-01-01'::date, '2026-03-31'::date, '1 day') AS d(date)
LEFT JOIN daily_sales s ON s.sale_date = d.date
ORDER BY d.date;

クイックリファレンス表

操作構文
行のフィルタリングWHERE condition
結果のソートORDER BY column ASC/DESC
行の制限LIMIT n または FETCH FIRST n ROWS ONLY
重複の除去SELECT DISTINCT
グループ化と集約GROUP BY ... HAVING
テーブルの結合JOIN ... ON
一時的な結果セットWITH cte AS (...)
行番号付けROW_NUMBER() OVER (...)
前の行/次の行LAG() / LEAD() OVER (...)
累積合計SUM() OVER (ORDER BY ... ROWS ...)
挿入または更新INSERT ... ON CONFLICT DO UPDATE

まとめ

SQLは広大な言語ですが、このチートシートのパターンをマスターすれば、開発者として遭遇する実際のシナリオの大部分をカバーできます。しっかりとしたSELECTとJOINの基礎から始めて、CTEとウィンドウ関数を段階的にツールキットに追加してください。特にデータが増えるにつれて、インデックスとクエリパフォーマンスを常に意識してください。

SQL Formatterを使ってクエリを整形してみてください。

このページをブックマークして、クイックリファレンスが必要なときにいつでも戻ってきてください。SQLを身につける最良の方法は、自分のデータベースに対してクエリを書いたり、SQLiteインメモリサンドボックスなどのプラットフォームを使ったり、オープンデータセットを探索したりして、定期的に練習することです。今日構築するパターンは、キャリア全体を通じて役立つでしょう。

関連記事