SELECT
CURRENT_VERSION() AS version, -- Snowflakeのバージョン
CURRENT_USER() AS me, -- ログインしている自分
CURRENT_ROLE() AS my_role, -- 今の権限(ロール)
CURRENT_WAREHOUSE() AS my_wh, -- 今使っている計算機
CURRENT_TIMESTAMP() AS now;
実行結果(例)
VERSION ME MY_ROLE MY_WH NOW
9.x.x UEKI ACCOUNTADMIN COMPUTE_WH 2026-08-14 09:12:33.412 +0900
① 今あるウェアハウスの一覧と、その自動停止時間を確認してください。② 最初から用意されている COMPUTE_WH の自動停止も60秒に変更してください。
解答を見る
answer.sql
SHOW WAREHOUSES; -- 結果の auto_suspend 列(秒)を見る
USE ROLE SYSADMIN;
ALTER WAREHOUSE COMPUTE_WH SET AUTO_SUSPEND = 60;
-- 今すぐ止めたいとき
ALTER WAREHOUSE COMPUTE_WH SUSPEND;
※ SHOW 系のコマンドは、直後に SELECT * FROM TABLE(RESULT_SCAN(LAST_QUERY_ID())) と書くと、結果をSQLで絞り込めます。
USE WAREHOUSE LEARN_WH;
SELECT
C_CUSTKEY AS 顧客番号,
C_NAME AS 顧客名,
C_NATIONKEY AS 国コード,
C_ACCTBAL AS 残高
FROM SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.CUSTOMER
WHERE C_ACCTBAL > 9000 -- 残高が9000より大きい行だけ
ORDER BY C_ACCTBAL DESC -- 残高の大きい順(DESC=降順)
LIMIT 10;
AS は列に別名を付ける書き方です。日本語の見出しにしたいときは AS "売上金額" のように二重引用符で囲みます。
WHERE ── 行をしぼる書き方
書き方
意味
例
=<>
等しい/等しくない
WHERE status = '完了'
>>=
より大きい/以上
WHERE amount >= 10000
BETWEEN
範囲(両端を含む)
WHERE d BETWEEN '2026-01-01' AND '2026-03-31'
IN
いずれかに一致
WHERE pref IN ('東京都','大阪府')
LIKE
あいまい検索(%=任意の文字列)
WHERE name LIKE '田中%'
ILIKE
大文字小文字を区別しないLIKE
WHERE mail ILIKE '%@Example.com'
IS NULL
値が入っていない
WHERE memo IS NULL
ANDORNOT
条件の組み合わせ
WHERE a = 1 AND (b = 2 OR c = 3)
NULLは「不明」であって、0でも空文字でもありませんWHERE memo = NULL と書いても、1行も返りません(「不明と等しいか」は判定できないため)。必ず IS NULL / IS NOT NULL を使います。計算でも、100 + NULL の答えは NULL です。これはSQL初学者が最も多く踏む落とし穴です。
null_handling.sql
SELECT
COALESCE(memo, '(記載なし)') AS memo, -- NULLなら代わりの値を返す
IFNULL(point, 0) + 10 AS point, -- COALESCEの2引数版
NVL2(memo, '有', '無') AS 有無 -- NULLでない/NULLで出し分け
FROM (SELECT NULL AS memo, NULL AS point);
実行結果
MEMO POINT 有無
(記載なし) 10 無
データ型を知っておく
型
入るもの
実務での使い方
NUMBER(38,0)
整数(INT / INTEGER も同じもの)
件数、ID
NUMBER(12,2)
小数点以下2桁までの数
金額はこれ。FLOATは誤差が出るので避ける
FLOAT
浮動小数点数
科学計算、割合。金額には使わない
VARCHAR
文字列(最大16MB)
名前、コード。文字数指定は任意
DATE
日付
売上日、締め日
TIMESTAMP_NTZ
日時(タイムゾーンなし)
既定はこれ。ログの時刻
TIMESTAMP_TZ / LTZ
タイムゾーン付きの日時
海外拠点をまたぐとき
BOOLEAN
TRUE / FALSE
フラグ
VARIANT
JSONなど何でも
STEP 7で詳しく
cast.sql
SELECT
'1234'::NUMBER AS 文字を数値に, -- :: が型変換の書き方
CAST('2026-08-14' AS DATE) AS 文字を日付に, -- 標準SQLの書き方(同じ意味)
TRY_CAST('あ' AS NUMBER) AS 失敗しても止めない, -- 変換できなければNULL
ROUND(1234.567, 1) AS 四捨五入,
TO_CHAR(1234567, '999,999,999') AS カンマ区切り;
SELECT O_ORDERKEY, O_ORDERDATE, O_TOTALPRICE, O_ORDERSTATUS
FROM SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.ORDERS
WHERE O_ORDERSTATUS = 'F'
AND O_TOTALPRICE > 300000
ORDER BY O_TOTALPRICE DESC
LIMIT 5;
SELECT
COUNT(*) AS 行数, -- 行の数(NULLも数える)
COUNT(O_COMMENT) AS コメント有り, -- NULLは数えない
COUNT(DISTINCT O_CUSTKEY) AS 顧客数, -- 重複を除いた数
SUM(O_TOTALPRICE) AS 合計金額,
AVG(O_TOTALPRICE) AS 平均金額,
MIN(O_ORDERDATE) AS 最初の注文日,
MAX(O_ORDERDATE) AS 最後の注文日,
MEDIAN(O_TOTALPRICE) AS 中央値
FROM SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.ORDERS;
平均だけを見ない 実務では、平均は少数の極端な値に引っぱられます。MEDIAN(中央値)や PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY 金額)(上位10%の境目)も一緒に見ると、データの姿を見誤りません。
GROUP BY ── 「〜ごと」の集計
「顧客ごと」「月ごと」のように単位を決めて集計するのが GROUP BY です。SELECTに書いた列のうち、集計関数で包まれていない列は、すべてGROUP BYに書くのがルールです。
group_by.sql
SELECT
O_ORDERSTATUS AS 状態,
COUNT(*) AS 件数,
SUM(O_TOTALPRICE) AS 合計金額
FROM SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.ORDERS
WHERE O_ORDERDATE >= '1998-01-01' -- 集計する前に行をしぼる
GROUP BY O_ORDERSTATUS -- 状態ごとにまとめる
HAVING COUNT(*) > 1000 -- まとめた後の結果をしぼる
ORDER BY 合計金額 DESC;
実行結果(例)
状態 件数 合計金額
O 70000 10800000000.0
F 69000 10600000000.0
P 3900 600000000.0
WHERE と HAVING の違いWHERE は集計する前に行をしぼり、HAVING は集計した後の結果をしぼります。「1998年以降のデータで(WHERE)、件数が1000件を超えるグループだけ(HAVING)」という関係です。処理の順番は FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT で、これを覚えると多くの疑問が解けます。
Snowflakeの便利機能:GROUP BY ALL 列が増えるとGROUP BYの書き写しが面倒になります。Snowflakeでは GROUP BY ALL と書くだけで、「集計関数で包まれていない列すべて」を自動的に指定してくれます。列の追加漏れによるエラーが消えるので、実務では多用されます。
CASE ── 条件で値を振り分ける
case_when.sql
SELECT
CASE
WHEN O_TOTALPRICE >= 300000 THEN '大口'
WHEN O_TOTALPRICE >= 100000 THEN '中口'
ELSE '小口'
END AS 区分,
COUNT(*) AS 件数,
-- 条件に合う行だけを数える書き方(実務で頻出)
COUNT(CASE WHEN O_ORDERSTATUS = 'F' THEN 1 END) AS 完了件数,
SUM(IFF(O_ORDERSTATUS = 'F', O_TOTALPRICE, 0)) AS 完了金額
FROM SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.ORDERS
GROUP BY ALL
ORDER BY 件数 DESC;
IFF(条件, 真のとき, 偽のとき) は、Excelの IF 関数とまったく同じ働きをする、CASEの短縮形です。
日付の扱い
実務の集計は、9割が「日付ごと」「月ごと」です。日付関数は最優先で覚えてください。
date_functions.sql
SELECT
CURRENT_DATE() AS 今日,
DATE_TRUNC('MONTH', CURRENT_DATE()) AS 今月の1日, -- 月初に切り下げ
LAST_DAY(CURRENT_DATE()) AS 月末,
DATEADD('DAY', -7, CURRENT_DATE()) AS 7日前,
DATEDIFF('DAY', '2026-01-01', CURRENT_DATE()) AS 経過日数,
TO_CHAR(CURRENT_DATE(), 'YYYY-MM') AS 年月,
YEAR(CURRENT_DATE()) AS 年,
DAYNAME(CURRENT_DATE()) AS 曜日;
SELECT
DATE_TRUNC('MONTH', O_ORDERDATE) AS 年月,
COUNT(*) AS 件数,
ROUND(SUM(O_TOTALPRICE)) AS 売上
FROM SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.ORDERS
WHERE O_ORDERDATE >= '1998-01-01'
GROUP BY ALL
ORDER BY 年月;
-- 方法1: 自分のユーザー既定を日本時間にする(おすすめ)
ALTER USER CURRENT_USER SET TIMEZONE = 'Asia/Tokyo';
-- 方法2: その場だけ変換する
SELECT CONVERT_TIMEZONE('Asia/Tokyo', CURRENT_TIMESTAMP()) AS 日本時間;
-- アカウント全体を変えたいとき(ACCOUNTADMINが必要。影響が大きいので合意のうえで)
-- ALTER ACCOUNT SET TIMEZONE = 'Asia/Tokyo';
SELECT
C_NATIONKEY AS 国コード,
COUNT(*) AS 顧客数,
ROUND(AVG(C_ACCTBAL), 2) AS 平均残高,
COUNT(CASE WHEN C_ACCTBAL < 0 THEN 1 END) AS マイナス顧客数
FROM SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.CUSTOMER
GROUP BY ALL
ORDER BY 顧客数 DESC;
※ COUNT(CASE WHEN 条件 THEN 1 END) は「条件に合う行だけを数える」定型句です。ELSEを書かなければ、条件外はNULLになり数えられません。
STEP 4基礎
結合とCTE ── 複数の表を組み合わせる
目安 1〜2週間
このステップの到達点 ── JOINで表をつなぎ、長いSQLを WITH で読みやすく分割できる。ここがSQLの山場です。越えれば、実務のSQLの大半が読めるようになります。
SELECT
o.O_ORDERKEY AS 注文番号,
o.O_ORDERDATE AS 注文日,
c.C_NAME AS 顧客名,
n.N_NAME AS 国名,
o.O_TOTALPRICE AS 金額
FROM SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.ORDERS AS o
JOIN SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.CUSTOMER AS c
ON o.O_CUSTKEY = c.C_CUSTKEY -- 注文の顧客番号 = 顧客の顧客番号
JOIN SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.NATION AS n
ON c.C_NATIONKEY = n.N_NATIONKEY -- さらに国マスタもつなぐ
WHERE o.O_ORDERDATE >= '1998-07-01'
ORDER BY o.O_TOTALPRICE DESC
LIMIT 10;
-- つなぐ相手(マスタ)に、キーの重複が無いかを確認する
SELECT C_CUSTKEY, COUNT(*) AS 件数
FROM SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.CUSTOMER
GROUP BY C_CUSTKEY
HAVING COUNT(*) > 1; -- 0行なら重複なし=安心してJOINできる
LEFT JOIN と「つながらなかった行」
left_join.sql
-- 注文はすべて残しつつ、マスタに無い顧客番号を洗い出す
SELECT
o.O_ORDERKEY,
o.O_CUSTKEY,
c.C_NAME
FROM SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.ORDERS AS o
LEFT JOIN SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.CUSTOMER AS c
ON o.O_CUSTKEY = c.C_CUSTKEY
WHERE c.C_CUSTKEY IS NULL -- つながらなかった行だけ=マスタ漏れ
LIMIT 20;
この「LEFT JOIN したうえで IS NULL で絞る」型は、データの取りこぼしを見つける点検SQLとして現場で頻繁に使います。
WITH 月別 AS ( -- ① まず月ごとに集計
SELECT
DATE_TRUNC('MONTH', O_ORDERDATE) AS 年月,
SUM(O_TOTALPRICE) AS 売上
FROM SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.ORDERS
WHERE O_ORDERDATE >= '1998-01-01'
GROUP BY ALL
),
平均 AS ( -- ② ①を使って平均を出す
SELECT AVG(売上) AS 月平均 FROM 月別
)
SELECT -- ③ ①と②を組み合わせる
m.年月,
ROUND(m.売上) AS 売上,
ROUND(a.月平均) AS 月平均,
ROUND(m.売上 / a.月平均 * 100, 1) AS 平均比パーセント
FROM 月別 AS m
CROSS JOIN 平均 AS a
ORDER BY m.年月;
SELECT '東日本' AS 地域, O_ORDERKEY, O_TOTALPRICE
FROM SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.ORDERS WHERE O_ORDERKEY < 100
UNION ALL -- 重複を除かない(速い。ふつうはこちら)
SELECT '西日本' AS 地域, O_ORDERKEY, O_TOTALPRICE
FROM SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.ORDERS WHERE O_ORDERKEY BETWEEN 100 AND 200;
-- UNION(ALL無し)は重複行を除くが、その分だけ遅い
WITH 注文 AS (
SELECT c.C_NATIONKEY, COUNT(DISTINCT c.C_CUSTKEY) AS 顧客数, SUM(o.O_TOTALPRICE) AS 売上
FROM SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.CUSTOMER AS c
LEFT JOIN SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.ORDERS AS o
ON c.C_CUSTKEY = o.O_CUSTKEY
GROUP BY ALL
)
SELECT
n.N_NAME AS 国名,
t.顧客数,
ROUND(COALESCE(t.売上, 0)) AS 売上
FROM SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.NATION AS n
LEFT JOIN 注文 AS t ON n.N_NATIONKEY = t.C_NATIONKEY
ORDER BY 売上 DESC;
CREATE OR REPLACE と CREATE IF NOT EXISTS 前者は既存のテーブルを中身ごと作り直します(学習中は便利、本番では危険)。後者は既にあれば何もしません。本番の運用スクリプトでは CREATE TABLE IF NOT EXISTS か、既存の定義を活かして変更できる CREATE OR ALTER TABLE を使い分けます。
1行ずつのINSERTは実務では使わない Snowflakeは大量データをまとめて扱うのが得意で、逆に1行ずつの書き込みは苦手です(1件ごとにコストがかかります)。実務では、次のSTEP 6で学ぶ COPY INTO でファイルから一括投入するのが基本です。ここでのINSERTは、あくまで練習用データの用意です。
SELECTの結果からテーブルを作る(CTAS)
ctas.sql
-- 集計結果をそのままテーブルにする。列の型は自動で決まる
CREATE OR REPLACE TABLE MONTHLY_SALES AS
SELECT
DATE_TRUNC('MONTH', ORDER_DATE) AS 年月,
COUNT(*) AS 件数,
SUM(AMOUNT) AS 売上
FROM ORDERS
WHERE STATUS <> 'CANCEL'
GROUP BY ALL;
SELECT * FROM MONTHLY_SALES ORDER BY 年月;
-- 更新(WHEREを忘れると全行が書き換わる。必ず先にSELECTで確認)
UPDATE ORDERS SET STATUS = 'PAID' WHERE ORDER_ID = 1004;
-- 削除
DELETE FROM ORDERS WHERE STATUS = 'CANCEL';
-- 中身だけ全部消す(テーブルの定義は残る)
-- TRUNCATE TABLE ORDERS;
-- テーブルごと消す
-- DROP TABLE ORDERS;
UPDATE / DELETE を書くときの手順 ① まず SELECT * FROM ... WHERE 同じ条件 を実行して、対象行を目で確認する ② 件数が想定どおりなら、SELECT * の部分だけを UPDATE ... SET に書き換える。この順番を守るだけで、事故はほぼ防げます。なお、間違えて消してもSTEP 9のTime Travelで戻せます。あわてないでください。
-- 今日届いた差分データ(本来はファイルから取り込む)
CREATE OR REPLACE TEMPORARY TABLE ORDERS_DELTA AS
SELECT * FROM ORDERS WHERE FALSE; -- 同じ形の空テーブルを作る小技
INSERT INTO ORDERS_DELTA VALUES
(1008, 1, '2026-08-09', 'WEB', 65000, 'PAID'), -- 既存 → 更新される
(1009, 3, '2026-08-13', 'WEB', 138000, 'OPEN'); -- 新規 → 追加される
MERGE INTO ORDERS AS t -- 対象(target)
USING ORDERS_DELTA AS s -- 差分(source)
ON t.ORDER_ID = s.ORDER_ID -- 突き合わせるキー
WHEN MATCHED THEN UPDATE SET
t.STATUS = s.STATUS,
t.AMOUNT = s.AMOUNT
WHEN NOT MATCHED THEN INSERT
(ORDER_ID, CUSTOMER_ID, ORDER_DATE, CHANNEL, AMOUNT, STATUS)
VALUES (s.ORDER_ID, s.CUSTOMER_ID, s.ORDER_DATE, s.CHANNEL, s.AMOUNT, s.STATUS);
実行結果
number of rows inserted number of rows updated
1 1
CREATE OR REPLACE TABLE PRODUCTS (
PRODUCT_ID NUMBER(8,0) NOT NULL,
PRODUCT_NAME VARCHAR(100) NOT NULL,
CATEGORY VARCHAR(30),
UNIT_PRICE NUMBER(10,2) NOT NULL
);
INSERT INTO PRODUCTS VALUES
(1, 'ノートPC', '電子機器', 128000),
(2, 'モニター', '電子機器', 32000),
(3, 'デスク', '家具', 45000),
(4, 'チェア', '家具', 28000),
(5, 'ボールペン', '文具', 180);
SELECT CATEGORY AS カテゴリ, COUNT(*) AS 品目数, AVG(UNIT_PRICE) AS 平均単価
FROM PRODUCTS
GROUP BY ALL
ORDER BY 平均単価 DESC;
STEP 6応用
データを取り込む ── ステージとCOPY INTO
目安 2週間
このステップの到達点 ── 手元のCSVをSnowflakeに取り込める。ステージ・ファイルフォーマット・COPY INTO の関係を説明でき、取り込みエラーを自分で調べて直せる。実務でいちばん相談される作業がこれです。
-- ③ ステージの中身を確認
LIST @MY_STAGE;
-- ④ 取り込む前に、中身を目で見る(これができるのがSnowflakeの強み)
SELECT $1, $2, $3, $4, $5, $6 -- $1 は1列目という意味
FROM @MY_STAGE/orders_202608.csv
(FILE_FORMAT => CSV_JP)
LIMIT 5;
-- ⑤ 本番の取り込み前に、検証だけ行う(1行も入らない)
COPY INTO ORDERS
FROM @MY_STAGE/orders_202608.csv
FILE_FORMAT = (FORMAT_NAME = CSV_JP)
VALIDATION_MODE = 'RETURN_ERRORS';
-- ⑥ 取り込む
COPY INTO ORDERS
FROM @MY_STAGE/orders_202608.csv
FILE_FORMAT = (FORMAT_NAME = CSV_JP)
ON_ERROR = 'CONTINUE'; -- エラー行は飛ばして続行
実行結果(例)
file status rows_parsed rows_loaded errors_seen
orders_202608.csv.gz LOADED 12043 12040 3
ON_ERROR の選び方
指定
動き
使う場面
ABORT_STATEMENT(既定)
1行でもエラーがあれば全体を中止
会計データなど、1行の欠けも許されないとき
CONTINUE
エラー行だけ飛ばして続行
ログなど、多少の欠損を許せるとき
SKIP_FILE
そのファイルごと飛ばす
ファイル単位で品質を管理するとき
check_copy_error.sql
-- どの行が、なぜ弾かれたのかを調べる
SELECT *
FROM TABLE(VALIDATE(ORDERS, JOB_ID => '_last'));
-- 過去の取り込み履歴(いつ・何件・エラー数)
SELECT FILE_NAME, ROW_COUNT, ROW_PARSED, ERROR_COUNT, STATUS, LAST_LOAD_TIME
FROM TABLE(INFORMATION_SCHEMA.COPY_HISTORY(
TABLE_NAME => 'ORDERS',
START_TIME => DATEADD('DAY', -7, CURRENT_TIMESTAMP())
))
ORDER BY LAST_LOAD_TIME DESC;
同じファイルは二重に取り込まれないCOPY INTO は、一度読み込んだファイル名を64日間おぼえていて、同じファイルは自動的にスキップします(ロードメタデータ)。二重計上を防いでくれる親切な仕組みですが、「直したCSVを同じ名前で入れ直したのに反映されない」という混乱の原因にもなります。その場合は FORCE = TRUE を付けます。
列の構造が分からないCSVを取り込む
infer_schema.sql
-- ファイルから列名と型を自動で推測させ、その形のテーブルを作る
CREATE OR REPLACE TABLE ORDERS_RAW
USING TEMPLATE (
SELECT ARRAY_AGG(OBJECT_CONSTRUCT(*))
FROM TABLE(INFER_SCHEMA(
LOCATION => '@MY_STAGE/orders_202608.csv',
FILE_FORMAT => 'CSV_JP'
))
);
-- 列の順番ではなく「見出しの名前」で対応づけて取り込む
COPY INTO ORDERS_RAW
FROM @MY_STAGE/orders_202608.csv
FILE_FORMAT = (FORMAT_NAME = CSV_JP)
MATCH_BY_COLUMN_NAME = 'CASE_INSENSITIVE';
COPY INTO @MY_STAGE/export/monthly_
FROM (SELECT * FROM MONTHLY_SALES ORDER BY 年月)
FILE_FORMAT = (TYPE = 'CSV' COMPRESSION = 'GZIP' HEADER = TRUE)
SINGLE = FALSE
MAX_FILE_SIZE = 100000000
OVERWRITE = TRUE;
LIST @MY_STAGE/export/;
-- GET でパソコンに落とす(Snowflake CLIから)
実践課題
手元にある実際のCSV(売上でも家計簿でも構いません)を1つ選び、① Snowsightの画面から取り込む ② 取り込んだテーブルの行数と、各列のNULL件数を数える ③ 1つでも取り込みエラーが出たら、その原因を COPY_HISTORY で確認する、までをやってみてください。この一連の流れが、データ基盤の入り口の仕事そのものです。
PARSE_JSON は、文字列をJSONとして解釈してVARIANTに変換する関数です。ファイルから取り込む場合は、TYPE = 'JSON' のファイルフォーマットで COPY INTO すれば同じ形になります。
中の値を取り出す
variant_extract.sql
SELECT
PAYLOAD:action::STRING AS 行動, -- コロンで階層をたどる
PAYLOAD:user.name::STRING AS 名前, -- 入れ子はドットでつなぐ
PAYLOAD:user.id::NUMBER AS ユーザーID,
PAYLOAD:amount::NUMBER AS 金額,
PAYLOAD:ts::TIMESTAMP_NTZ AS 時刻,
PAYLOAD:items[0].sku::STRING AS 最初の商品, -- 配列は0から数える
ARRAY_SIZE(PAYLOAD:items) AS 商品数
FROM EVENTS;
SELECT
e.EVENT_ID,
e.PAYLOAD:user.name::STRING AS 名前,
f.INDEX AS 何番目,
f.VALUE:sku::STRING AS 商品コード,
f.VALUE:qty::NUMBER AS 数量,
f.VALUE:price::NUMBER AS 単価,
f.VALUE:qty::NUMBER * f.VALUE:price::NUMBER AS 小計
FROM EVENTS AS e,
LATERAL FLATTEN(INPUT => e.PAYLOAD:items) AS f;
SELECT
f.VALUE:sku::STRING AS 商品コード,
SUM(f.VALUE:qty::NUMBER) AS 合計数量,
SUM(f.VALUE:qty::NUMBER * f.VALUE:price::NUMBER) AS 合計金額
FROM EVENTS AS e,
LATERAL FLATTEN(INPUT => e.PAYLOAD:items) AS f
GROUP BY ALL
ORDER BY 合計金額 DESC;
GROUP BY は行をまとめて減らします。一方 ウィンドウ関数は、行を減らさずに、各行の隣に集計値を並べます。「明細を残したまま、その顧客の合計も横に出したい」というときに使います。書き方はいつもこの形です。
ウィンドウ関数の形
関数() OVER (
PARTITION BY 区切る列 -- 何ごとに計算するか(省略可=全体)
ORDER BY 並べる列 -- どの順で見るか
)
window_basic.sql
SELECT
ORDER_ID, CUSTOMER_ID, ORDER_DATE, AMOUNT,
-- 顧客ごとの合計を、明細の横に並べる
SUM(AMOUNT) OVER (PARTITION BY CUSTOMER_ID) AS 顧客合計,
-- 顧客ごとの、注文順の通し番号
ROW_NUMBER() OVER (PARTITION BY CUSTOMER_ID ORDER BY ORDER_DATE) AS 何回目,
-- 全体での金額順位
RANK() OVER (ORDER BY AMOUNT DESC) AS 金額順位,
-- その顧客の前回の注文金額
LAG(AMOUNT) OVER (PARTITION BY CUSTOMER_ID ORDER BY ORDER_DATE) AS 前回金額
FROM ORDERS
ORDER BY CUSTOMER_ID, ORDER_DATE;
-- 顧客ごとの最新の注文だけを1行ずつ取り出す
SELECT ORDER_ID, CUSTOMER_ID, ORDER_DATE, AMOUNT
FROM ORDERS
QUALIFY ROW_NUMBER() OVER (PARTITION BY CUSTOMER_ID ORDER BY ORDER_DATE DESC) = 1
ORDER BY CUSTOMER_ID;
-- 重複行の除去(同じ注文番号が複数あるとき、いちばん新しい1行を残す)
SELECT *
FROM ORDERS
QUALIFY ROW_NUMBER() OVER (PARTITION BY ORDER_ID ORDER BY ORDER_DATE DESC) = 1;
取り込み直したデータの重複を消す定型句 「同じキーで複数回取り込んでしまった」ときの後始末は、この QUALIFY ROW_NUMBER() ... = 1 が定番です。処理の順番は WHERE → GROUP BY → HAVING → QUALIFY で、QUALIFYはウィンドウ計算の後に効きます。
前月比と累計
trend.sql
WITH 月別 AS (
SELECT DATE_TRUNC('MONTH', O_ORDERDATE) AS 年月, SUM(O_TOTALPRICE) AS 売上
FROM SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.ORDERS
WHERE O_ORDERDATE >= '1997-01-01'
GROUP BY ALL
)
SELECT
年月,
ROUND(売上) AS 売上,
ROUND(LAG(売上) OVER (ORDER BY 年月)) AS 前月,
ROUND((売上 / LAG(売上) OVER (ORDER BY 年月) - 1) * 100, 1) AS 前月比パーセント,
ROUND(SUM(売上) OVER (ORDER BY 年月)) AS 累計,
-- 3か月移動平均(自分を含む直近3行の平均)
ROUND(AVG(売上) OVER (ORDER BY 年月 ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)) AS 移動平均3か月
FROM 月別
ORDER BY 年月;
前年同月比を出したいときLAG(売上, 12) OVER (ORDER BY 年月) のように、第2引数で「何行前か」を指定します。ただし、途中に売上ゼロの月があって行そのものが無いと、12行前が前年同月とは限りません。実務では日付マスタ(カレンダーテーブル)と LEFT JOIN して、欠けた月を埋めてから計算するのが安全です。
WITH 顧客別 AS (
SELECT
c.C_NATIONKEY AS 国コード,
c.C_NAME AS 顧客名,
SUM(o.O_TOTALPRICE) AS 売上
FROM SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.ORDERS AS o
JOIN SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.CUSTOMER AS c
ON o.O_CUSTKEY = c.C_CUSTKEY
GROUP BY ALL
)
SELECT
国コード, 顧客名, ROUND(売上) AS 売上,
RANK() OVER (PARTITION BY 国コード ORDER BY 売上 DESC) AS 順位
FROM 顧客別
QUALIFY 順位 <= 3
ORDER BY 国コード, 順位;
実務での使い道 ① 大きな更新の前に CLONE で退避しておく(数秒で終わる保険)② 本番と同じデータで開発環境を作る ③ 月末時点のスナップショットを残す。「危ない作業の前にクローン」は、Snowflake実務者の基本動作です。
結果キャッシュ ── 2回目がタダになる
cache.sql
-- 同じSQLを2回実行してみる。2回目は一瞬で終わり、ウェアハウスすら起動しない
SELECT COUNT(*) FROM SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.LINEITEM;
-- 検証のためキャッシュを無効にする(性能を測るときだけ)
ALTER SESSION SET USE_CACHED_RESULT = FALSE;
Snowflakeでは、データをコピーせずに他社・他部門のアカウントへ共有できます(Secure Data Sharing)。相手はそのデータを自分のアカウントのテーブルのように読めますが、実体は1つのままです。また、Marketplaceでは天気・人流・為替などの外部データを、その場で自分のアカウントに追加して結合できます。「データを送る」から「データを見せる」への転換が、Snowflakeの思想です。
練習問題
① CUSTOMERS テーブルをクローンして CUSTOMERS_BK を作る ② 元のテーブルで UPDATE を実行して1行書き換える ③ Time Travelを使って、書き換える前の値を確認する、をやってみてください。
解答を見る
answer.sql
CREATE OR REPLACE TABLE CUSTOMERS_BK CLONE CUSTOMERS;
UPDATE CUSTOMERS SET PREFECTURE = '神奈川県' WHERE CUSTOMER_ID = 1;
-- 変更前と変更後を並べて比べる
SELECT '変更後' AS 状態, PREFECTURE FROM CUSTOMERS WHERE CUSTOMER_ID = 1
UNION ALL
SELECT '変更前', PREFECTURE FROM CUSTOMERS AT(OFFSET => -60) WHERE CUSTOMER_ID = 1;
USE SCHEMA LEARN_DB.SALES;
-- ふつうのビュー(実データを持たない。常に最新)
CREATE OR REPLACE VIEW V_MONTHLY_SALES AS
SELECT
DATE_TRUNC('MONTH', o.ORDER_DATE) AS 年月,
c.PREFECTURE AS 都道府県,
COUNT(*) AS 件数,
SUM(o.AMOUNT) AS 売上
FROM ORDERS AS o
JOIN CUSTOMERS AS c USING (CUSTOMER_ID)
WHERE o.STATUS <> 'CANCEL'
GROUP BY ALL;
SELECT * FROM V_MONTHLY_SALES ORDER BY 年月, 売上 DESC;
種類
実体
使いどころ
VIEW
持たない(毎回計算)
基本。定義を共有したいとき
SECURE VIEW
持たない+定義を隠す
他社へのデータ共有、行レベルの制限
MATERIALIZED VIEW
持つ(自動で更新される)
1つのテーブルへの重い集計を繰り返すとき。Enterprise以上。制約が多い
DYNAMIC TABLE
持つ(指定した遅延内で自動更新)
いまの主流。JOINを含む加工でも使える
タスク ── 決まった時刻にSQLを実行する
task.sql
CREATE OR REPLACE TASK T_REFRESH_MONTHLY
SCHEDULE = 'USING CRON 0 6 * * * Asia/Tokyo' -- 毎朝6時(日本時間)
USER_TASK_MANAGED_INITIAL_WAREHOUSE_SIZE = 'XSMALL' -- サーバーレスで動かす
AS
CREATE OR REPLACE TABLE MONTHLY_SALES AS
SELECT * FROM V_MONTHLY_SALES;
-- 作った直後のタスクは停止状態。明示的に開始する(忘れがち)
ALTER TASK T_REFRESH_MONTHLY RESUME;
-- 手で1回動かして試す
EXECUTE TASK T_REFRESH_MONTHLY;
-- 実行結果の確認
SELECT NAME, STATE, SCHEDULED_TIME, COMPLETED_TIME, ERROR_MESSAGE
FROM TABLE(INFORMATION_SCHEMA.TASK_HISTORY())
ORDER BY SCHEDULED_TIME DESC
LIMIT 10;
CREATE OR REPLACE TABLE ORDERS_MART LIKE ORDERS; -- 同じ形の空テーブル
CREATE OR REPLACE TASK T_SYNC_ORDERS
SCHEDULE = '5 MINUTE'
WHEN SYSTEM$STREAM_HAS_DATA('S_ORDERS') -- 差分があるときだけ動く=無駄な課金なし
AS
MERGE INTO ORDERS_MART AS t
USING (SELECT * FROM S_ORDERS WHERE METADATA$ACTION = 'INSERT') AS s
ON t.ORDER_ID = s.ORDER_ID
WHEN MATCHED THEN UPDATE SET t.STATUS = s.STATUS, t.AMOUNT = s.AMOUNT
WHEN NOT MATCHED THEN INSERT (ORDER_ID, CUSTOMER_ID, ORDER_DATE, CHANNEL, AMOUNT, STATUS)
VALUES (s.ORDER_ID, s.CUSTOMER_ID, s.ORDER_DATE, s.CHANNEL, s.AMOUNT, s.STATUS);
ALTER TASK T_SYNC_ORDERS RESUME;
WHEN SYSTEM$STREAM_HAS_DATA() を付けると、差分が無いときはタスクがスキップされ、ウェアハウスが起動しないので課金もされません。5分おきのタスクでも安心して回せます。
CREATE OR REPLACE DYNAMIC TABLE DT_MONTHLY_SALES
TARGET_LAG = '10 minutes' -- 元データから最大10分遅れまで許す
WAREHOUSE = LEARN_WH
AS
SELECT
DATE_TRUNC('MONTH', o.ORDER_DATE) AS 年月,
c.PREFECTURE AS 都道府県,
COUNT(*) AS 件数,
SUM(o.AMOUNT) AS 売上
FROM ORDERS AS o
JOIN CUSTOMERS AS c USING (CUSTOMER_ID)
WHERE o.STATUS <> 'CANCEL'
GROUP BY ALL;
SELECT * FROM DT_MONTHLY_SALES; -- ふつうのテーブルとして読める
SHOW DYNAMIC TABLES; -- 更新状況の確認
USE ROLE SECURITYADMIN;
CREATE ROLE IF NOT EXISTS SALES_READ; -- アクセスロール
CREATE ROLE IF NOT EXISTS ANALYST; -- 機能ロール
-- 参照に必要な3点セット(どれか1つ欠けても読めない)
GRANT USAGE ON DATABASE LEARN_DB TO ROLE SALES_READ;
GRANT USAGE ON SCHEMA LEARN_DB.SALES TO ROLE SALES_READ;
GRANT SELECT ON ALL TABLES IN SCHEMA LEARN_DB.SALES TO ROLE SALES_READ;
-- これから作られるテーブルにも自動で権限を付ける(重要)
GRANT SELECT ON FUTURE TABLES IN SCHEMA LEARN_DB.SALES TO ROLE SALES_READ;
GRANT SELECT ON FUTURE VIEWS IN SCHEMA LEARN_DB.SALES TO ROLE SALES_READ;
-- 計算機を使う権限も必要
GRANT USAGE ON WAREHOUSE LEARN_WH TO ROLE SALES_READ;
-- 機能ロールに束ねて、人に渡す
GRANT ROLE SALES_READ TO ROLE ANALYST;
GRANT ROLE ANALYST TO USER "SATO";
-- 管理者から見えるように、SYSADMINの下にもぶら下げる(推奨)
GRANT ROLE ANALYST TO ROLE SYSADMIN;
「テーブルは見えるのに中身が読めない」の正体 Snowflakeの参照権限は、データベースのUSAGE + スキーマのUSAGE + テーブルのSELECTの3点セットです。どれか1つでも欠けると読めません。また、ON ALL TABLES は実行した時点のテーブルにしか効かないため、明日作るテーブルには ON FUTURE TABLES が必要です。この2点で、権限のトラブルのほとんどが説明できます。
check_grants.sql
SHOW GRANTS TO ROLE ANALYST; -- そのロールが持つ権限
SHOW GRANTS ON TABLE ORDERS; -- そのテーブルに付いている権限
SHOW GRANTS TO USER "SATO"; -- その人が持つロール
ユーザーと認証
users.sql
USE ROLE USERADMIN;
CREATE USER IF NOT EXISTS SATO
LOGIN_NAME = 'sato'
DEFAULT_ROLE = ANALYST
DEFAULT_WAREHOUSE = LEARN_WH
MUST_CHANGE_PASSWORD = TRUE;
-- プログラムから接続する用途では、パスワードではなく鍵を使う
-- ALTER USER BATCH_USER SET RSA_PUBLIC_KEY = 'MIIBIj...';
USE ROLE ACCOUNTADMIN;
CREATE OR REPLACE RESOURCE MONITOR RM_LEARN
WITH CREDIT_QUOTA = 50 -- 月に50クレジットまで
FREQUENCY = MONTHLY
START_TIMESTAMP = IMMEDIATELY
TRIGGERS
ON 75 PERCENT DO NOTIFY -- 75%で通知
ON 90 PERCENT DO NOTIFY
ON 100 PERCENT DO SUSPEND -- 到達で新規クエリを止める
ON 110 PERCENT DO SUSPEND_IMMEDIATE; -- 実行中のものも止める
ALTER WAREHOUSE LEARN_WH SET RESOURCE_MONITOR = RM_LEARN;
cost_check.sql
USE ROLE ACCOUNTADMIN;
-- ウェアハウス別の、直近30日のクレジット消費
SELECT
WAREHOUSE_NAME,
ROUND(SUM(CREDITS_USED), 2) AS クレジット
FROM SNOWFLAKE.ACCOUNT_USAGE.WAREHOUSE_METERING_HISTORY
WHERE START_TIME >= DATEADD('DAY', -30, CURRENT_TIMESTAMP())
GROUP BY ALL
ORDER BY クレジット DESC;
-- 重いクエリの犯人探し(誰の、どのSQLが時間を使ったか)
SELECT
USER_NAME, WAREHOUSE_NAME,
ROUND(TOTAL_ELAPSED_TIME / 1000, 1) AS 秒,
ROUND(BYTES_SCANNED / POWER(1024, 3), 2) AS 読んだGB,
LEFT(QUERY_TEXT, 80) AS SQL冒頭
FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
WHERE START_TIME >= DATEADD('DAY', -7, CURRENT_TIMESTAMP())
AND TOTAL_ELAPSED_TIME > 60000 -- 60秒以上かかったもの
ORDER BY TOTAL_ELAPSED_TIME DESC
LIMIT 20;
import os
import snowflake.connector
from dotenv import load_dotenv
load_dotenv() # .env からアカウント情報を読み込む
conn = snowflake.connector.connect(
account=os.environ["SF_ACCOUNT"],
user=os.environ["SF_USER"],
password=os.environ["SF_PASSWORD"],
warehouse=os.environ["SF_WAREHOUSE"],
database=os.environ["SF_DATABASE"],
schema=os.environ["SF_SCHEMA"],
)
sql = """
SELECT c.PREFECTURE AS 都道府県, SUM(o.AMOUNT) AS 売上
FROM ORDERS AS o
JOIN CUSTOMERS AS c USING (CUSTOMER_ID)
WHERE o.ORDER_DATE >= %s -- 値は必ずこの形で渡す(文字列連結しない)
GROUP BY ALL
ORDER BY 売上 DESC
"""
try:
cur = conn.cursor()
cur.execute(sql, ("2026-06-01",))
df = cur.fetch_pandas_all() # 結果をそのままpandasのDataFrameで受け取る
print(df)
df.to_excel("売上レポート.xlsx", index=False)
finally:
conn.close() # 接続は必ず閉じる
Pythonから接続し、① 月別売上を取得して DataFrame にする ② Excelファイルとして保存する ③ そのスクリプトを毎朝実行できる形(引数で対象月を渡せる形)に整える、までを作ってください。Python入門ページのSTEP 9・10と組み合わせると、そのまま業務で使える道具になります。
USE ROLE SYSADMIN;
CREATE DATABASE IF NOT EXISTS PROD_DB; -- 本番(人手で触らない)
CREATE DATABASE IF NOT EXISTS DEV_DB; -- 開発
-- 本番と同じデータで検証したいとき(数秒・追加費用ほぼゼロ)
CREATE OR REPLACE DATABASE DEV_DB CLONE PROD_DB;
-- 本番への変更は、SQLファイルにして流す(画面で手打ちしない)
USE ROLE ACCOUNTADMIN;
CREATE OR REPLACE API INTEGRATION GIT_API
API_PROVIDER = GIT_HTTPS_API
API_ALLOWED_PREFIXES = ('https://github.com/my-org')
ENABLED = TRUE;
CREATE OR REPLACE GIT REPOSITORY SQL_REPO
API_INTEGRATION = GIT_API
ORIGIN = 'https://github.com/my-org/snowflake-sql.git';
ALTER GIT REPOSITORY SQL_REPO FETCH;
-- リポジトリ内のSQLファイルをそのまま実行する
EXECUTE IMMEDIATE FROM @SQL_REPO/branches/main/deploy/create_marts.sql;
冪等に書く 何度実行しても同じ結果になるスクリプトを「冪等」といいます。CREATE OR REPLACE/CREATE IF NOT EXISTS/CREATE OR ALTER と MERGE を使えば、途中で失敗して流し直しても壊れません。本番用のSQLは、必ず2回実行してみて確認します。
Snowflake Scripting ── 手続きを書く
procedure.sql
CREATE OR REPLACE PROCEDURE SP_DAILY_LOAD(TARGET_DATE DATE)
RETURNS STRING
LANGUAGE SQL
AS
$$
DECLARE
row_count INTEGER;
BEGIN
DELETE FROM MART_SALES WHERE 売上日 = :TARGET_DATE; -- やり直せるように先に消す
INSERT INTO MART_SALES
SELECT ORDER_DATE, SUM(AMOUNT)
FROM ORDERS
WHERE ORDER_DATE = :TARGET_DATE
GROUP BY ORDER_DATE;
row_count := SQLROWCOUNT;
RETURN :TARGET_DATE::STRING || ' を ' || :row_count || ' 件で更新しました';
EXCEPTION
WHEN OTHER THEN
RETURN '失敗: ' || SQLERRM;
END;
$$;
CALL SP_DAILY_LOAD('2026-08-14');
-- ① 主キーの重複がないか
SELECT 'ORDER_IDの重複' AS 検査, COUNT(*) AS 件数
FROM (SELECT ORDER_ID FROM ORDERS GROUP BY ORDER_ID HAVING COUNT(*) > 1)
UNION ALL
-- ② 必須項目のNULL
SELECT 'CUSTOMER_IDがNULL', COUNT(*) FROM ORDERS WHERE CUSTOMER_ID IS NULL
UNION ALL
-- ③ マスタに存在しない顧客
SELECT 'マスタ未登録の顧客', COUNT(*)
FROM ORDERS AS o LEFT JOIN CUSTOMERS AS c USING (CUSTOMER_ID)
WHERE c.CUSTOMER_ID IS NULL
UNION ALL
-- ④ 金額のマイナスや異常値
SELECT '金額が0以下', COUNT(*) FROM ORDERS WHERE AMOUNT <= 0
UNION ALL
-- ⑤ データが今日も届いているか(届いていなければ0件になる)
SELECT '本日の取り込み件数', COUNT(*) FROM ORDERS WHERE ORDER_DATE = CURRENT_DATE();