はじめに
データ処理案件でSnowflakeを使うことになった。
MySQLやPostgreSQLは普段から触っているが、Snowflakeは名前を聞いたことがある程度で中身を知らなかった。「クラウドデータウェアハウス」という言葉の意味から整理した。
RDBMSとの概念の違いを理解するまでに時間がかかったので、同じような立場の人の参考になればと思って書く。
データウェアハウスとは何か
まず前提の整理から。
OLTP(Online Transaction Processing)
→ MySQLやPostgreSQLが得意な領域
→ 日常的なトランザクション処理
→ INSERT/UPDATE/DELETEが多い
→ 少数レコードを高速に読み書き
OLAP(Online Analytical Processing)
→ Snowflakeが得意な領域
→ データ分析・集計処理
→ SELECT(集計)がほとんど
→ 大量レコードを高速に集計
PHPのバックエンドで使うMySQLはOLTP用途。ユーザーの操作ひとつひとつをリアルタイムで処理するのが得意。
SnowflakeはOLAP用途。「先月の売上を地域ごとに集計して前年比を出す」のような大量データの分析クエリを高速に処理するのが得意。
Snowflakeのアーキテクチャ
通常のRDBMSと根本的に違う部分がここ。
通常のRDBMS
サーバー(1台または数台)
├── CPU(処理)
└── ストレージ(データ)
CPUとストレージが同じサーバーに同居している
→ スケールアップ(サーバーを強化)が必要
→ リソースを使っていなくても課金
Snowflakeのアーキテクチャ
ストレージ層(S3などのオブジェクトストレージ)
↓ 分離
コンピュート層(仮想ウェアハウス)
↓ 分離
クラウドサービス層(メタデータ・認証・最適化)
ストレージとコンピュートが完全に分離されているのがSnowflakeの最大の特徴。
メリット:
・使ったコンピュートリソースだけ課金(停止すれば課金ゼロ)
・ストレージとコンピュートを独立してスケール可能
・同じデータに対して複数のウェアハウスから同時アクセス可能
MySQLはサーバーを止めてもストレージの課金は続くが、Snowflakeはウェアハウスを停止すればコンピュート課金が止まる。
仮想ウェアハウス — Snowflake独自の概念
Snowflakeでクエリを実行する「コンピュートリソースの塊」が仮想ウェアハウス。
-- ウェアハウスの作成
CREATE WAREHOUSE my_warehouse
WAREHOUSE_SIZE = 'X-SMALL'
AUTO_SUSPEND = 60 -- 60秒アイドルで自動停止
AUTO_RESUME = TRUE -- クエリが来たら自動起動
COMMENT = '開発用ウェアハウス';
-- ウェアハウスの一覧
SHOW WAREHOUSES;
-- ウェアハウスを手動で停止
ALTER WAREHOUSE my_warehouse SUSPEND;
-- ウェアハウスを手動で起動
ALTER WAREHOUSE my_warehouse RESUME;
ウェアハウスのサイズ
| サイズ | クレジット/時間 | 用途の目安 |
|---|---|---|
| X-SMALL | 1 | 開発・小規模クエリ |
| SMALL | 2 | 中規模クエリ |
| MEDIUM | 4 | 大規模集計 |
| LARGE | 8 | 重い集計・ML |
| X-LARGE | 16 | 超大規模処理 |
クレジットはSnowflakeの課金単位。1クレジットの価格は契約プランによる(おおよそ$2〜$4程度)。
AUTO_SUSPENDを設定しておかないとウェアハウスが起動しっぱなしになって課金が膨らむ。開発環境では60秒、本番でも必要最小限に設定するのが基本。
Snowflakeの階層構造
Organization(組織)
└── Account(アカウント)
├── Database(データベース)
│ └── Schema(スキーマ)
│ └── Table / View / Stage ...
├── Warehouse(仮想ウェアハウス)
├── Role(ロール)
└── User(ユーザー)
MySQLとの違いとしてSchemaがある点。MySQLはDatabase > Tableの2層だが、SnowflakeはDatabase > Schema > Tableの3層構造。
-- データベースの作成
CREATE DATABASE my_database;
-- スキーマの作成
CREATE SCHEMA my_database.my_schema;
-- テーブルの作成
CREATE TABLE my_database.my_schema.users (
id NUMBER AUTOINCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
created_at TIMESTAMP_NTZ DEFAULT CURRENT_TIMESTAMP()
);
-- 使用するDBとSchemaを設定(以降のクエリで省略できる)
USE DATABASE my_database;
USE SCHEMA my_schema;
Snowflakeのデータ型
MySQLとの対応を整理した。
| MySQL | Snowflake | 備考 |
|---|---|---|
INT |
NUMBER / INTEGER
|
NUMBERはスケール指定可 |
VARCHAR(n) |
VARCHAR(n) |
同じ |
TEXT |
TEXT / VARCHAR(16777216)
|
最大16MB |
FLOAT |
FLOAT / DOUBLE
|
同じ |
BOOLEAN |
BOOLEAN |
同じ |
DATE |
DATE |
同じ |
DATETIME |
TIMESTAMP_NTZ |
タイムゾーンなし |
TIMESTAMP |
TIMESTAMP_TZ |
タイムゾーンあり |
JSON |
VARIANT |
Snowflake独自 |
VARIANT型はJSON・XML・Avroなど半構造化データを格納できるSnowflake独自の型。
-- VARIANT型の使い方
CREATE TABLE events (
id NUMBER AUTOINCREMENT PRIMARY KEY,
event_data VARIANT, -- JSONを格納
created_at TIMESTAMP_NTZ DEFAULT CURRENT_TIMESTAMP()
);
-- JSONを挿入
INSERT INTO events (event_data)
SELECT PARSE_JSON('{"user_id": 1, "action": "click", "page": "/top"}');
-- JSON内のフィールドにアクセス
SELECT
event_data:user_id::NUMBER AS user_id,
event_data:action::VARCHAR AS action,
event_data:page::VARCHAR AS page
FROM events;
JSONをカラムとして格納してそのまま検索できるのはRDBMSにはない機能。ログデータや半構造化データを扱うときに便利。
クエリの基本
基本的なSQLは同じように書ける。
-- 基本的なSELECT
SELECT
name,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
WHERE created_at >= DATEADD(month, -3, CURRENT_TIMESTAMP())
GROUP BY name
HAVING COUNT(*) >= 5
ORDER BY total_amount DESC
LIMIT 100;
Snowflake固有の便利な関数
-- 日付操作
DATEADD(day, 7, CURRENT_DATE()) -- 7日後
DATEDIFF(day, start_date, end_date) -- 日数差
DATE_TRUNC('month', created_at) -- 月初に切り捨て
-- 文字列
SPLIT_PART('a,b,c', ',', 1) -- 'a'(1始まり)
IFF(condition, true_val, false_val) -- 三項演算子
-- ウィンドウ関数(分析で頻出)
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC)
LAG(amount, 1) OVER (ORDER BY month) -- 前の行の値
LEAD(amount, 1) OVER (ORDER BY month) -- 次の行の値
-- 配列・オブジェクト操作
ARRAY_AGG(tag_name) -- 値を配列に集約
OBJECT_CONSTRUCT('key', value) -- オブジェクト生成
FLATTEN(input => array_column) -- 配列を行に展開
Time Travel — 過去のデータを参照する
Snowflake独自の機能で、過去のある時点のデータを参照できる。
-- 1時間前のデータを参照
SELECT * FROM orders
AT (OFFSET => -3600);
-- 特定の時刻のデータを参照
SELECT * FROM orders
AT (TIMESTAMP => '2024-04-01 00:00:00'::TIMESTAMP);
-- 誤って削除したテーブルを復元
UNDROP TABLE orders;
-- 誤って削除したデータを復元
CREATE TABLE orders_backup AS
SELECT * FROM orders
BEFORE (STATEMENT => '<statement_id>');
デフォルトで1日(Enterpriseは90日)のTime Travelが使える。誤ってデータを削除してもUNDROPで復元できるのはRDBMSにはない機能。本番でDELETE FROMを誤実行したとき、これで助かった経験がある。
Zero-Copy Cloning
テーブルやデータベースをコピーできる機能。コピーといっても内部ではデータをコピーせずメタデータだけを複製するので一瞬で終わる。
-- テーブルをクローン(一瞬で完了、ストレージは共有)
CREATE TABLE orders_test CLONE orders;
-- データベース全体をクローン
CREATE DATABASE mydb_staging CLONE mydb_production;
-- 過去の状態をクローン
CREATE TABLE orders_yesterday CLONE orders
BEFORE (OFFSET => -86400);
本番DBをステージング環境にコピーするのに数時間かかっていた作業が、Snowflakeではほぼ瞬時に完了する。開発環境の構築に使える。
Snowflakeのコスト管理
最初にハマりやすいのがコスト。
-- クレジット使用量の確認
SELECT
warehouse_name,
SUM(credits_used) AS total_credits,
SUM(credits_used_cloud_services) AS cloud_credits
FROM snowflake.account_usage.warehouse_metering_history
WHERE start_time >= DATEADD(day, -30, CURRENT_TIMESTAMP())
GROUP BY warehouse_name
ORDER BY total_credits DESC;
-- クエリ実行時間の確認
SELECT
query_text,
total_elapsed_time / 1000 AS seconds,
credits_used_cloud_services,
warehouse_name
FROM snowflake.account_usage.query_history
WHERE start_time >= DATEADD(day, -1, CURRENT_TIMESTAMP())
ORDER BY total_elapsed_time DESC
LIMIT 20;
コスト管理の基本ルール:
-- ① ウェアハウスにAUTO_SUSPENDを必ず設定する
ALTER WAREHOUSE my_warehouse
SET AUTO_SUSPEND = 60;
-- ② 開発時はX-SMALLで十分
ALTER WAREHOUSE my_warehouse
SET WAREHOUSE_SIZE = 'X-SMALL';
-- ③ 不要なウェアハウスは停止
ALTER WAREHOUSE my_warehouse SUSPEND;
-- ④ リソースモニターで上限を設定
CREATE RESOURCE MONITOR my_monitor
WITH CREDIT_QUOTA = 100 -- 月100クレジットまで
TRIGGERS ON 80 PERCENT DO NOTIFY
ON 100 PERCENT DO SUSPEND;
MySQLからの移行で気をつけた点
大文字小文字
-- MySQLはデフォルトで大文字小文字を区別しない
SELECT * FROM Users; -- usersテーブルを参照できる
-- Snowflakeはデフォルトで識別子を大文字に変換する
CREATE TABLE users (...);
SELECT * FROM USERS; -- OK(大文字に変換されて同じテーブルを参照)
SELECT * FROM "users"; -- NG(ダブルクォートは大文字変換されない)
テーブル名・カラム名はすべて大文字で定義するか、
ダブルクォートを使わない命名規則を統一する。
NULL と空文字
-- MySQLは空文字とNULLを区別する
-- Snowflakeも同様だが挙動が微妙に違う
-- EQUAL_NULL で NULL同士を比較できる
SELECT EQUAL_NULL(NULL, NULL); -- TRUE
SELECT NULL = NULL; -- NULL(通常のSQL)
AUTO_INCREMENT
-- MySQL
id INT AUTO_INCREMENT PRIMARY KEY
-- Snowflake
id NUMBER AUTOINCREMENT PRIMARY KEY
-- または
id NUMBER DEFAULT mysequence.nextval
Snowflakeの料金モデルまとめ
課金対象:
1. コンピュート(仮想ウェアハウス)
→ 起動中のみ課金
→ サイズとアクティブ時間に比例
2. ストレージ
→ 圧縮後のデータ量に課金
→ 約$23/TB/月程度(契約による)
3. データ転送
→ リージョン間のデータ移動
→ 同一リージョン内は無料
開発中はウェアハウスを使っていない時間が長いので、AUTO_SUSPENDを短く設定しておけばコストを最小限に抑えられる。
まとめ
- Snowflakeはコンピュートとストレージが分離したクラウドDWH
- 仮想ウェアハウスがコンピュートリソースの単位。停止すれば課金なし
- Time TravelとZero-Copy Cloningはミス対応・環境構築に使える
- VARIANT型で半構造化データ(JSON)をSQLで操作できる
- AUTO_SUSPENDを必ず設定してコストを管理する
MySQLやPostgreSQLとはアーキテクチャの発想が根本的に違うので、最初に概念を整理してから使い始めると迷いが少なかった。