はじめに ― MySQL/PostgreSQL から来ると Snowflake の SELECT は少し違います
私が Snowflake で SELECT 文を書き始めて、すぐに気づくのは MySQL / PostgreSQL とは少し違う書き味です。SELECT * EXCLUDE と QUALIFY を覚えてから、これまでサブクエリで囲っていたクエリの半分が要らなくなりました。データエンジニア歴6年で日々 SQL を書いている私から見ても、Snowflake SELECT は「他DBの方言」というより「他DBで何度も繰り返し書いていた定型処理を一行で済ませるための新しい構文」を持っています。
この記事は、Snowflake の SELECT 文を「初めて書く方」と「他DB(MySQL / PostgreSQL / BigQuery)から来た方」両方を想定して書いています。基本構文・他DB比較・実務パターン10選・パフォーマンス・落とし穴・FAQまで、検索意図ごとに節を分けてあります。

結論早見表 ― Snowflakeでしか書けない/書きにくい SELECT 構文
先に結論からお見せします。下表の構文を覚えるだけで、他DBから移ってきた SQL が驚くほど短くなります。
| 機能名 | Snowflakeの構文サンプル | 他DB(PostgreSQL等)で書くなら |
|---|---|---|
| SELECT * EXCLUDE | SELECT * EXCLUDE (password) FROM users; |
必要列を全列挙して書く |
| SELECT * RENAME | SELECT * RENAME (id AS user_id) FROM users; |
全列挙 + AS で改名 |
| QUALIFY | QUALIFY ROW_NUMBER() OVER(...) = 1 |
サブクエリで囲って WHERE |
| GROUP BY ALL | GROUP BY ALL |
非集計列を全列挙 |
| SAMPLE | FROM orders SAMPLE (1000 ROWS) |
ORDER BY RANDOM() LIMIT n |
| TOP n | SELECT TOP 10 * FROM t |
LIMIT 10(MySQL/PG) |
| 半構造化のドット記法 | v:user.name::string |
JSON関数のネスト |
| LATERAL FLATTEN | , LATERAL FLATTEN(input => v:items) |
jsonb_array_elements など |
| IDENTIFIER | FROM IDENTIFIER($tbl) |
動的SQL を別途生成 |
| RESULT_SCAN | FROM TABLE(RESULT_SCAN(LAST_QUERY_ID())) |
一時テーブル経由 |
SELECT文の基本構文(他DB経験者向け)
Snowflake の SELECT は ANSI SQL に準拠しつつ、QUALIFY と TOP / LIMIT 併存 が独自です。まずは構文の骨格と評価順序を確認します。
-- 完全形 (Snowflake SELECT 構文の骨格)
USE DATABASE ANALYTICS;
USE SCHEMA PUBLIC;
SELECT TOP 100 -- (a) 取得列
customer_id,
SUM(amount) AS total_amount
FROM orders -- (b) 取得元
WHERE order_date >= '2026-01-01' -- (c) 行フィルタ
GROUP BY customer_id -- (d) 集計単位
HAVING SUM(amount) > 10000 -- (e) 集計後フィルタ
QUALIFY ROW_NUMBER() OVER( -- (f) ウィンドウ関数のフィルタ
PARTITION BY customer_id
ORDER BY total_amount DESC
) = 1
ORDER BY total_amount DESC -- (g) 並び替え
LIMIT 10; -- (h) 件数制限
意味的な評価順序は次のとおりです。書く順番(SELECT が先頭)と評価順序(FROM が最初)が違う点は、他DBと同じです。
| 順序 | 句 | 役割 |
|---|---|---|
| 1 | FROM / JOIN | ソーステーブルを決める |
| 2 | WHERE | 行レベルでフィルタ |
| 3 | GROUP BY | グループ化 |
| 4 | HAVING | 集計後の条件 |
| 5 | SELECT | 列を選択 / 計算 |
| 6 | QUALIFY | ウィンドウ関数の結果でフィルタ |
| 7 | ORDER BY | 並び替え |
| 8 | LIMIT / TOP | 件数制限 |
テーブル参照は orders のような短い名前のほか、ANALYTICS.PUBLIC.ORDERS という完全修飾名でも書けます。階層の意味は データベース・スキーマ・テーブルの階層解説 を参照してください。スクリプトや dbt のような自動化では完全修飾名のほうが事故が減ります。
他DBにない便利機能 6選
ここからが本題です。MySQL / PostgreSQL から移ってきた方が「これは便利」と感じる機能を厳選しました。
① SELECT * EXCLUDE / RENAME ― 必要なところだけ除外
-- パスワード列だけ除外して全部取りたい
SELECT * EXCLUDE (password_hash, salt)
FROM ANALYTICS.PUBLIC.USERS
LIMIT 3;
-- 期待出力 (列イメージ)
-- USER_ID | EMAIL | NAME | CREATED_AT
-- 1 | a@example.com | Alice | 2026-04-01 ...
-- 2 | b@example.com | Bob | 2026-04-02 ...
PostgreSQL でこれを書こうとすると、列を全部 information_schema.columns から取り出すか、ハードコーディングで全列挙する必要があります。10列のテーブルなら 10行、50列なら 50行。Snowflake の EXCLUDE は 1行で済みます。RENAME も同系統で、SELECT * RENAME (id AS user_id) のように特定列だけ別名にできます。
② QUALIFY ― ウィンドウ関数の結果でそのままフィルタ
-- 顧客ごとに最新の注文を1件
SELECT customer_id, order_id, order_date, amount
FROM ANALYTICS.PUBLIC.ORDERS
QUALIFY ROW_NUMBER() OVER(
PARTITION BY customer_id
ORDER BY order_date DESC
) = 1;
-- 期待出力
-- CUSTOMER_ID | ORDER_ID | ORDER_DATE | AMOUNT
-- 101 | 9001 | 2026-05-08 | 12000
-- 102 | 9002 | 2026-05-09 | 3200
PostgreSQL だと、まず ROW_NUMBER() を付けたサブクエリを CTE で作り、外側で WHERE rn = 1 でフィルタする2段構えになります。Snowflake の QUALIFY は WHERE がウィンドウ関数版になっただけ、と覚えると一発です。サブクエリ1段ぶん(おおむね 4〜6行)が消えます。
③ GROUP BY ALL ― 非集計列を自動推測
SELECT region,
product_category,
DATE_TRUNC('month', order_date) AS month,
SUM(amount) AS total
FROM ANALYTICS.PUBLIC.ORDERS
GROUP BY ALL;
GROUP BY ALL は SELECT 句から集計関数以外の列を Snowflake が自動的にグループ化キーとみなしてくれます。PostgreSQL なら GROUP BY region, product_category, DATE_TRUNC('month', order_date) と3つ並べる必要がありますが、Snowflake ならキーワード1つで済みます。私が SELECT 文を書く際は、探索クエリでまず GROUP BY ALL を使い、最終的にビュー化するときに明示形へ書き直す、という流れにしています。
④ SAMPLE ― 行サンプリングを構文として持つ
-- 1000行だけ抽出
SELECT * FROM ANALYTICS.PUBLIC.ORDERS SAMPLE (1000 ROWS);
-- ブロックサンプリングで 1% を高速取得
SELECT * FROM ANALYTICS.PUBLIC.ORDERS SAMPLE BLOCK (1);
PostgreSQL で同じことをすると ORDER BY RANDOM() LIMIT 1000 となり、テーブル全行をソートする最悪のクエリになります。Snowflake の SAMPLE BLOCK はマイクロパーティション単位での抽出なので、巨大テーブルの初動探索に向いています。
⑤ TOP n と LIMIT n の併存
SELECT TOP 5 * FROM ORDERS ORDER BY amount DESC;
-- これは LIMIT 5 と等価
SELECT * FROM ORDERS ORDER BY amount DESC LIMIT 5;
SQL Server から来た方は TOP、MySQL / PostgreSQL から来た方は LIMIT をそのまま使えます。両方とも合法です。ただし両方を同じクエリ内に書くとエラーになるので、チームでどちらを使うか統一する運用が現実的です。
⑥ 半構造化データのドット記法と FLATTEN
-- VARIANT 列 v に {"user": {"name": "Alice"}, "items": [{"sku":"A"},{"sku":"B"}]}
SELECT v:user.name::string AS user_name,
f.value:sku::string AS sku
FROM ANALYTICS.PUBLIC.EVENTS,
LATERAL FLATTEN(input => v:items) f;
-- 期待出力
-- USER_NAME | SKU
-- Alice | A
-- Alice | B
PostgreSQL で同じことを書くと jsonb_extract_path_text と jsonb_array_elements をネストし、行数も型キャストも増えます。Snowflake の v:user.name::string はパス指定 + 型キャストが1行で済むのが魅力です。

実務でよく使う SELECT パターン10選
データ分析のよく使うパターンを、SQL とハマりポイント込みで整理しました。テーブル名は ANALYTICS.PUBLIC 配下の架空サンプルです。
1. 顧客ごとに最新レコードを1件
SELECT customer_id, order_id, order_date, amount
FROM ANALYTICS.PUBLIC.ORDERS
QUALIFY ROW_NUMBER() OVER(
PARTITION BY customer_id ORDER BY order_date DESC, order_id DESC
) = 1;
ハマりポイント: 同じ order_date が複数あるとき、ORDER BY のタイブレーカー(ここでは order_id DESC)を入れないと結果が毎回変わります。
2. グループごとの上位N件
SELECT region, customer_id, lifetime_amount
FROM ANALYTICS.PUBLIC.CUSTOMER_LTV
QUALIFY RANK() OVER(PARTITION BY region ORDER BY lifetime_amount DESC) <= 3;
ハマりポイント: RANK は同点を許容するので結果が3行を超えることがあります。厳密にN件にしたいときは ROW_NUMBER を選びます。
3. JSONカラムから特定キーを抽出
SELECT event_id,
payload:user_id::number AS user_id,
payload:device.os::string AS device_os
FROM ANALYTICS.PUBLIC.EVENTS
WHERE payload:event_type::string = 'login';
ハマりポイント: 型キャストを忘れると VARIANT のまま比較されて結合キーが噛み合いません。::string や ::number を必ず付けます。
4. 日次集計のロールアップ
SELECT DATE_TRUNC('day', order_date) AS day,
COUNT(*) AS order_count,
SUM(amount) AS total_amount,
AVG(amount) AS avg_amount
FROM ANALYTICS.PUBLIC.ORDERS
WHERE order_date >= DATEADD(day, -30, CURRENT_DATE)
GROUP BY ALL
ORDER BY day;
ハマりポイント: order_date が TIMESTAMP の場合、DATE_TRUNC しないと「同じ日付」がミリ秒単位でバラけて1日1行になりません。
5. 期間集計を週/月で切り替え
SET grain = 'month'; -- 'day' / 'week' / 'month' を切り替え
SELECT DATE_TRUNC($grain, order_date) AS bucket,
SUM(amount) AS total
FROM ANALYTICS.PUBLIC.ORDERS
GROUP BY ALL
ORDER BY bucket;
ハマりポイント: 週次は曜日始まりが Snowflake のセッションパラメータ WEEK_START に依存します。レポート定義では明示的に設定しておくと結果のブレを防げます。
6. SELECT 結果を別テーブルに INSERT
INSERT INTO ANALYTICS.MART.DAILY_SALES (sales_date, total_amount)
SELECT DATE_TRUNC('day', order_date) AS sales_date,
SUM(amount) AS total_amount
FROM ANALYTICS.PUBLIC.ORDERS
WHERE order_date >= DATEADD(day, -1, CURRENT_DATE)
GROUP BY ALL;
ハマりポイント: INSERT 先テーブルの列数と SELECT の列数が完全一致しないとエラーになります。列リストを必ず明示する習慣にしておくと事故が減ります。
7. 直前の結果を再利用 ― RESULT_SCAN
-- 1) 重い集計を1回だけ実行
SELECT region, SUM(amount) AS total
FROM ANALYTICS.PUBLIC.ORDERS GROUP BY ALL;
-- 2) 直前のクエリ結果を再加工
SELECT * FROM TABLE(RESULT_SCAN(LAST_QUERY_ID()))
WHERE total > 100000;
ハマりポイント: RESULT_SCAN はリザルトキャッシュの保持期限(24時間)を超えると失敗します。長時間後に再走するパイプラインには向きません。
8. 動的なテーブル名 ― IDENTIFIER
SET tbl = 'ANALYTICS.PUBLIC.ORDERS';
SELECT COUNT(*) FROM IDENTIFIER($tbl);
ハマりポイント: SQL インジェクションを防ぐため、$tbl の中身は外部入力をそのまま入れず、ホワイトリストで検証してから渡します。
9. 重複行の検出
SELECT customer_id, order_id, COUNT(*) AS dup_count
FROM ANALYTICS.PUBLIC.ORDERS
GROUP BY ALL
HAVING COUNT(*) > 1;
ハマりポイント: NULL を含むキーは GROUP BY で「同じ NULL 同士」とみなされます。意図せず重複扱いされるので、検出後の処理で NULL を別管理する設計にしておくと安全です。
10. 配列/オブジェクトの展開 ― LATERAL FLATTEN
SELECT e.event_id,
f.index AS item_index,
f.value:sku::string AS sku,
f.value:qty::number AS qty
FROM ANALYTICS.PUBLIC.EVENTS e,
LATERAL FLATTEN(input => e.payload:items) f;
ハマりポイント: FLATTEN は対象が NULL や空配列の行を結果から落とします。残したい場合は OUTER => TRUE を付けます。
SELECT のパフォーマンスとコスト
同じ SELECT でも、書き方とウェアハウスのサイズで料金は数倍変わります。私が普段 SQL を書く際は、いきなり大きなクエリを投げるのではなく、次の3つを必ず確認しています。
クエリプロファイルでボトルネックを見る
Snowsight の Query History から該当クエリを開き、Query Profile タブを確認します。「TableScan の Bytes Scanned」「Bytes Spilled to Local/Remote Storage」「Pruning の効き具合」が要点です。詳しい読み方は Snowflakeクエリプロファイル入門 で扱っています。
リザルトキャッシュは24時間
同じクエリを24時間以内に再実行するとウェアハウスは起動せず、ストレージ料金も計算料金も発生しません。Snowflakeの3つのキャッシュの違い でリザルト/ウェアハウス/メタデータの3層を整理しました。これを意識すると、ダッシュボードの裏で同じ SELECT が何度も走っているムダに気づけます。
マイクロパーティションプルーニング
Snowflake はデータを 16MB 程度のマイクロパーティションに自動分割しています。WHERE order_date >= '2026-05-01' のような条件は、各パーティションの min/max メタデータを使ってスキャン対象を絞り込みます。これが「プルーニング」です。WHERE TO_CHAR(order_date) = '2026-05-01' のように関数で包むとプルーニングが効かなくなる罠があります。詳しくは クラスタリングキー入門 と クエリ最適化ベストプラクティス10選 を参照してください。
SELECT * の本番運用での問題
本番運用で SELECT * を書くと、列が追加されたときに後段の BI ツールやアプリ側で予期せぬ挙動が起きます。さらに、Snowflake は列指向ストレージなので、不要な列を読むとそのぶんだけスキャン量(=料金)が増えます。私はビューや本番クエリではほぼ SELECT * を書きません。詳しくは「落とし穴」の節で扱います。コスト面の感覚は ウェアハウス使用状況の可視化とコスト最適化 も合わせて読むと掴めます。

SELECT 実行時のよくあるエラーと対処
Object does not exist or not authorized
SELECT で最も遭遇しやすいエラーがこれです。テーブル名のタイプミスもありますが、多いのは「現在のロールに USAGE / SELECT 権限が無い」「USE DATABASE / USE SCHEMA を切り替えていない」のいずれか。詳細な切り分けは Object does not exist or not authorized エラーの正体 にまとめてあります。
Statement timeout (Statement reached its statement or warehouse timeout)
SELECT が STATEMENT_TIMEOUT_IN_SECONDS を超えると強制終了されます。ウェアハウスのサイズが小さすぎる、JOIN の結合キーがミスマッチで巨大な中間結果が出ている、のどちらかが原因のことが多いです。ウェアハウス入門 でサイズの考え方を整理しています。
SQL compilation error: invalid identifier ‘X’
列名のタイポか、クォートの不一致が原因です。Snowflake は識別子をデフォルトで大文字に正規化します。"customer_id" と二重引用符で作ったテーブルは小文字のまま保存され、SELECT customer_id ではなく SELECT "customer_id" でしか参照できなくなります。
Numeric value ‘abc’ is not recognized
暗黙の型変換が失敗したときに出ます。WHERE amount = '100' のように文字列リテラルと数値列を比較したときに頻発します。TRY_TO_NUMBER を使うとエラーではなく NULL を返してくれるので、データクレンジングの前段で重宝します。
他DBユーザーが詰まる落とし穴 5つ
① 大文字正規化
Snowflake は CREATE TABLE Users と書いても内部的には USERS として保存します。MySQL の Linux 環境(case-sensitive)から来ると、SELECT * FROM Users を期待通り動かそうとしてハマります。原則「DDL では引用符で囲まない」「列名・テーブル名はすべて大文字で扱う前提」と覚えておくのが楽です。
② NULL の扱い
NULL = NULL は TRUE ではなく NULL です。WHERE 句では NULL 行は弾かれます。customer_id IS NULL、IS NOT DISTINCT FROM、NVL、COALESCE を使い分けます。COUNT(*) と COUNT(column) も挙動が違い、後者は NULL をカウントしません。
③ 二重引用符は識別子、シングルクォートは文字列
MySQL では "abc" が文字列として通る場面がありますが、Snowflake / PostgreSQL では "abc" は識別子(列名・テーブル名)です。文字列は必ずシングルクォート 'abc'。「列が見つからない」と思ったら、ほぼこの取り違えです。
④ 暗黙の型変換
Snowflake は MySQL ほど寛容ではありません。VARCHAR と NUMBER の比較や、VARIANT から数値への暗黙キャストは失敗することがあります。::number や TRY_CAST を明示するクセを付けると、後段の本番障害が減ります。
⑤ SELECT * は本番禁忌
探索クエリでは SELECT * を多用しますが、本番に乗せる SQL(ビュー、dbt model、アプリのクエリ)では列を明示するのが鉄則です。理由は、列追加時のコントラクト破壊・スキャンコスト増・PII 列の意図せぬ漏洩の3つ。SELECT * EXCLUDE (password_hash) という妥協案もありますが、列名が変わったときに気づけないリスクは残ります。
FAQ
Snowflake SELECT 文と MySQL/PostgreSQL の SELECT 文は何が一番違いますか?
QUALIFY、SELECT * EXCLUDE、GROUP BY ALL、SAMPLE、半構造化データのドット記法の5つが「他DBで何行も書いていた処理を1行に圧縮できる」という意味で大きな違いです。基本構文(SELECT/FROM/WHERE/GROUP BY/HAVING/ORDER BY/LIMIT)はほぼ同じで、上位互換に近い感覚で書けます。
QUALIFY と HAVING はどう違いますか?
HAVING は集計関数(SUM/COUNT/AVG)の結果に対するフィルタ、QUALIFY はウィンドウ関数(ROW_NUMBER/RANK/LAG など)の結果に対するフィルタです。両者は併用でき、HAVING → SELECT → QUALIFY の順に評価されます。
SELECT * EXCLUDE は本番運用で使ってもよいですか?
探索や ad-hoc 分析では便利ですが、ビューや dbt model のような長期運用クエリでは避けたほうが安全です。テーブルに列が追加されたとき、EXCLUDE の対象でない列まで自動的に流れ込み、後段で予期せぬ挙動を起こすことがあります。
LIMIT と TOP はどちらを使うべきですか?
動作は同じです。MySQL / PostgreSQL から来た方は LIMIT、SQL Server から来た方は TOP をそのまま使えます。チームで統一されていれば、どちらでも構いません。
大文字小文字の正規化を回避したいのですが?
CREATE TABLE "users" のように二重引用符で囲んで作成すれば小文字のまま保持されます。ただし、以後すべての参照で必ず引用符を付ける必要があり、運用コストが上がります。Snowflake では「すべて大文字に正規化される前提」で運用するほうが楽です。命名のコツは Snowflake命名規則ベストプラクティス に整理しています。
同じクエリを何度も投げるとお金がかかりますか?
24時間以内に同一の SELECT(同じテキスト・同じ結果セット)を投げた場合は、リザルトキャッシュからゼロコストで返ります。BIツールから同じダッシュボードを開き直すケースの多くはこれに当たります。
SELECT * を JOIN すると重くなる理由は?
列指向ストレージなので、不要な列を含めるほど読み込むファイルが増え、結果として圧縮済みデータの解凍コストとネットワーク転送コストが増えます。JOIN の中間結果も列幅ぶん大きくなるので、メモリ溢れ(Bytes Spilled)が発生しやすくなります。
QUALIFY が他DBにも欲しいのですが代替方法は?
PostgreSQL / MySQL では CTE で ROW_NUMBER を付けたサブクエリを作り、外側で WHERE rn = 1 でフィルタするのが定番です。BigQuery には Snowflake と同じ QUALIFY があります。
私の SELECT 文の書き方ルール
データエンジニアとして6年間 SQL を書いてきた経験から、自分が SELECT 文を書くときに必ず守っているルールが5つあります。所属に依存しない、汎用的なものだけ並べました。
ルール1: 本番クエリで SELECT * を書かない
探索や検証では SELECT * を頻繁に使いますが、ビュー化・パイプライン化のタイミングで必ず列を明示し直します。これはコスト削減のためでもありますが、それ以上に「列が増えたら気づける状態」を保つためです。本番障害の何割かは、知らないうちに列が増えていたところから起きます。
ルール2: 完全修飾名で書く
パイプラインや dbt のような自動化された SQL では、USE DATABASE / USE SCHEMA に依存せず ANALYTICS.PUBLIC.ORDERS の形で書きます。これだけでロール切替時の事故と「Object does not exist」系のエラーが激減します。
ルール3: ウィンドウ関数のフィルタは QUALIFY で書く
サブクエリで囲むより圧倒的に読みやすく、レビューコストが下がります。「最新1件」「上位N件」「重複検知」あたりは、もう QUALIFY 以外で書きません。
ルール4: クエリ末尾にコメントで意図を残す
Snowflake の Query History は SQL テキストで検索できます。クエリ末尾に -- daily_sales rollup for executive dashboard のように一文添えておくと、将来の自分や他の人が「このクエリは誰のために走っているのか」を即座に追えます。私は監査ログの分析でも助けられました。監査ログ入門 と組み合わせると、コスト原因の特定が速くなります。
ルール5: 遅いと感じた瞬間にクエリプロファイルを開く
感覚で「ウェアハウスを大きくしよう」と判断する前に、Query Profile を1分でも見るのが結果的に最短です。Bytes Spilled が大きいならサイズアップ、Pruning が効いていないなら WHERE 句の関数包みを疑う、のように打ち手が変わります。
最後に、これから Snowflake SELECT に本気で取り組む方への実用的なアドバイスをひとつ。迷ったらまず QUALIFY と SAMPLE から覚えると業務効率が変わります。この2つを知っているかどうかで、ad-hoc 分析の所要時間が体感で半分以下になります。
参考リンク
- Snowflake公式: SELECT
- Snowflake公式: QUALIFY
- Snowflake公式: SELECT * EXCLUDE / RENAME
- Snowflake公式: SAMPLE / TABLESAMPLE
- Snowflake公式: FLATTEN
関連記事
- Object does not exist or not authorized エラーの正体 – SELECT で最も多いエラーの最短デバッグ
- Snowflakeの3つのキャッシュの違い – SELECT が無料で返る仕組みを理解する
- Snowflakeクエリプロファイル入門 – 遅い SELECT のボトルネックを見抜く方法
- Snowflakeクエリ最適化ベストプラクティス10選 – SELECT を速く・安くする実用集
- Snowflakeクラスタリングキー入門 – 大規模テーブルの SELECT を速くする仕組み
- Snowflakeマルチクラスタウェアハウス入門 – 同時実行 SELECT を捌く仕組み
- Snowflakeウェアハウス使用状況の可視化とコスト最適化 – SELECT 由来のクレジットを抑える

