目次
フォーマット/可読性
- フィールドを区切るときは先頭カンマを使う
- WHERE句でダミー値を使う
- 適切にコードをインデントする
- 複雑なクエリを書くときはCTEを検討する
便利な機能
:: 演算子を使ってデータ型を変換する
- アンチ結合を活用する
- ウィンドウ関数のフィルタリングに
QUALIFY を使う
- 列位置で
GROUP BY を使える
避けるべき落とし穴
NULL 値と一緒に NOT IN を使う際は注意する
- 計算済みフィールドの名前を変えて曖昧さを防ぐ
- 各列がどのテーブルに属するかを明示する
- 実行順序を理解する
- コードにコメントを付ける
- ドキュメント全体を読む
フォーマット/可読性
フィールドを区切るときは先頭カンマを使う
SELECT 句でフィールドを区切る際に先頭カンマを使うと、新しい列を明確に区別できる
- 先頭カンマは、カンマの抜け漏れを簡単に確認できる視覚的な手がかりになる
SELECT
employee_id,
employee_name,
job,
salary
FROM employees;
WHERE句でダミー値を使う
- WHERE句でダミー値を使うことで、条件を動的に追加・削除できる
SELECT *
FROM employees
WHERE 1=1 -- ダミー値
AND job IN ('Clerk', 'Manager')
AND dept_no != 5;
適切にコードをインデントする
- コードをインデントすると可読性が上がり、同僚や将来の自分にとって理解しやすくなる
-- 悪い例:
SELECT
timeslot_date,
timeslot_channel,
overnight_fta_share,
IFF(DATEDIFF(DAY, timeslot_date, CURRENT_DATE()) > 7,
LAG(overnight_fta_share, 1) OVER (PARTITION BY timeslot_date, timeslot_channel ORDER BY timeslot_activity),
NULL) AS C7_fta_share,
IFF(DATEDIFF(DAY, timeslot_date, CURRENT_DATE()) >= 29,
LAG(overnight_fta_share, 2) OVER (PARTITION BY timeslot_date, timeslot_channel ORDER BY timeslot_activity),
NULL) AS C28_fta_share
FROM timeslot_data;
-- 良い例:
SELECT
timeslot_date,
timeslot_channel,
overnight_fta_share,
IFF(DATEDIFF(DAY, timeslot_date, CURRENT_DATE()) > 7,
LAG(overnight_fta_share, 1) OVER (PARTITION BY timeslot_date, timeslot_channel ORDER BY timeslot_activity),
NULL) AS C7_fta_share,
IFF(DATEDIFF(DAY, timeslot_date, CURRENT_DATE()) >= 29,
LAG(overnight_fta_share, 2) OVER (PARTITION BY timeslot_date, timeslot_channel ORDER BY timeslot_activity),
NULL) AS C28_fta_share
FROM timeslot_data;
複雑なクエリを書くときはCTEを検討する
- インラインビューをネストする代わりに共通テーブル式(CTE)を使うことで、コードの可読性と整理しやすさを高められる
-- インラインビューを使用:
SELECT
vhs.movie,
vhs.vhs_revenue,
cs.cinema_revenue
FROM
(SELECT
movie_id,
SUM(ticket_sales) AS cinema_revenue
FROM tickets
GROUP BY movie_id) AS cs
INNER JOIN
(SELECT
movie,
movie_id,
SUM(revenue) AS vhs_revenue
FROM blockbuster
GROUP BY movie, movie_id) AS vhs
ON cs.movie_id = vhs.movie_id;
-- CTEを使用:
WITH cinema_sales AS (
SELECT
movie_id,
SUM(ticket_sales) AS cinema_revenue
FROM tickets
GROUP BY movie_id
),
vhs_sales AS (
SELECT
movie,
movie_id,
SUM(revenue) AS vhs_revenue
FROM blockbuster
GROUP BY movie, movie_id
)
SELECT
vhs.movie,
vhs.vhs_revenue,
cs.cinema_revenue
FROM cinema_sales AS cs
INNER JOIN vhs_sales AS vhs
ON cs.movie_id = vhs.movie_id;
便利な機能
:: 演算子を使ってデータ型を変換する
- 一部のRDBMSでは、
:: 演算子を使って値を別のデータ型に変換できる
SELECT CAST('5' AS INTEGER); -- CAST関数を使用
SELECT '5'::INTEGER; -- :: 構文を使用
アンチ結合を活用する
- アンチ結合は、片方のテーブルにしか存在しない行を返したいときにとても便利
- サブクエリも使えるが、一般的にはアンチ結合のほうが速い
-- アンチ結合:
SELECT
video_content.*
FROM video_content
LEFT JOIN archive
ON video_content.series_id = archive.series_id
WHERE archive.series_id IS NULL;
-- サブクエリ:
SELECT
*
FROM video_content
WHERE series_id NOT IN (SELECT DISTINCT series_id FROM archive);
-- 相関サブクエリ:
SELECT
*
FROM video_content
WHERE NOT EXISTS (
SELECT 1
FROM archive a
WHERE a.series_id = vc.series_id
);
-- EXCEPT:
SELECT series_id
FROM video_content
EXCEPT
SELECT series_id
FROM archive;
ウィンドウ関数のフィルタリングに QUALIFY を使う
QUALIFY を使うと、ウィンドウ関数の結果をフィルタリングできる
- コード行数を減らすのに便利
-- QUALIFYを使用:
SELECT
product,
market,
SUM(revenue) AS market_revenue
FROM sales
GROUP BY product, market
QUALIFY DENSE_RANK() OVER (PARTITION BY product ORDER BY SUM(revenue) DESC) <= 10
ORDER BY product, market_revenue;
-- QUALIFYなし:
SELECT
product,
market,
market_revenue
FROM (
SELECT
product,
market,
SUM(revenue) AS market_revenue,
DENSE_RANK() OVER (PARTITION BY product ORDER BY SUM(revenue) DESC) AS market_rank
FROM sales
GROUP BY product, market
)
WHERE market_rank <= 10
ORDER BY product, market_revenue;
列位置で GROUP BY を使える
- 列名の代わりに列位置を使って
GROUP BY や ORDER BY を書ける
- 一時的なクエリでは便利だが、本番コードでは常に列名を参照すべき
SELECT
dept_no,
SUM(salary) AS dept_salary
FROM employees
GROUP BY 1 -- dept_noはSELECT句の1番目の列
ORDER BY 2 DESC;
避けるべき落とし穴
NULL 値と一緒に NOT IN を使う際は注意する
NOT IN は NULL 値があると正しく動作しない
- 代わりに
NOT EXISTS を使うべき
INSERT INTO departments (id)
VALUES (1), (2), (NULL);
-- NULL値のため動作しない
SELECT *
FROM employees
WHERE department_id NOT IN (SELECT DISTINCT id FROM departments);
-- 解決方法
SELECT *
FROM employees e
WHERE NOT EXISTS (
SELECT 1
FROM departments d
WHERE d.id = e.department_id
);
計算済みフィールドの名前を変えて曖昧さを防ぐ
- 計算済みフィールドに既存の列名を付けると、予期しない動作が起こることがある
INSERT INTO products (product, revenue)
VALUES ('Shark', 100), ('Robot', 150), ('Alien', 90);
-- ウィンドウ関数が 'Robot' 製品を1位にランク付けする
SELECT
product,
CASE product WHEN 'Robot' THEN 0 ELSE revenue END AS revenue,
RANK() OVER (ORDER BY revenue DESC)
FROM products;
各列がどのテーブルに属するかを明示する
- 複雑なクエリでは、各列がどのテーブルに属するかを明示すると問題を追いやすい
SELECT
vc.video_id,
vc.series_name,
metadata.season,
metadata.episode_number
FROM video_content AS vc
INNER JOIN video_metadata AS metadata
ON vc.video_id = metadata.video_id;
実行順序を理解する
- SQLを学ぶ人にとって最も重要なアドバイスは、実行順序を理解すること
- 実行順序を理解すると、クエリの書き方が大きく変わる
コードにコメントを付ける
- コードを書くときは、理由を説明するコメントを付けるべき
- 同僚や将来の自分が感謝するはず
SELECT
video_content.*
FROM video_content
LEFT JOIN archive -- 新しいCMSはアーカイブ動画形式を処理できない
ON video_content.series_id = archive.series_id
WHERE archive.series_id IS NULL;
ドキュメント全体を読む
- ドキュメントを一通り読むと、思わぬ問題を避けられる
- 読むのに数分しかかからず、予期しない問題の解決に役立つ
-- ドキュメントをもっと読んでいればNULL問題を解決できた
SELECT COALESCE(GREATEST(signup_date, consumption_date), signup_date, consumption_date);
-- GREATEST_IGNORE_NULLS関数を使える
SELECT GREATEST_IGNORE_NULLS(signup_date, consumption_date);
GN⁺のまとめ
- この記事は、SQLをより効率的かつ読みやすく書くためのさまざまなヒントとテクニックを紹介している
- SQLを初めて学ぶ人にも、経験のあるデータアナリストにも役立つ情報が多い
- 特に複雑なクエリを書くときのCTE活用、アンチ結合、
QUALIFY の利用などは実務で大いに役立つはず
- SQLの実行順序を理解し、コードにコメントを付け、ドキュメントを丁寧に読む習慣を付けることが重要
- 似た機能を持つ別のツールとしては、PostgreSQL、MySQL、Oracle などがある
2件のコメント
先頭カンマがこの投稿ではすべて末尾カンマで書かれていますね。原文では先頭カンマで入力されています。
Hacker Newsの意見
DBサーバーをよく理解し、クエリプランを頻繁に確認する必要性
EXISTSがINより速いことが多いNOT EXISTSとEXCEPTはNULL値の処理で異なる動作をするUNION ALLがORより速い場合があるJOINによってフィルタリング順序を強制できる複雑なストアドプロシージャ作業のコツ
コードの可読性に関する意見
SQLのFROM-firstとパイピング構文の使用提案
Anti Joinに関するヒント
EXISTSの使用を推奨。条件ベースのサブクエリで行の存在有無を確認する際に有利SELECT文で先頭カンマを使う利点MSSQLでコメントを使う際は
--ではなく/* */の使用を推奨ウィンドウ関数の使用を推奨
WHERE句で1=1を使うことへの論争AI2sqlの紹介