第8章: アンチパターン13選 ─ 症状 → 根本原因 → 脱出法
第7章が「ベストプラクティス」なら、本章はその裏返し ─ 「初導入で踏みやすい代表的アンチパターン13個」を集中的に扱う。Snowflakeで月末請求書に驚く理由のほとんどは、この章のどれかに該当する。
各パターンを「症状 / 根本原因 / 脱出法」の3段で整理する。第7章で挙げたチェックリストを、実例から逆算する形で読むと記憶に残りやすい。
全体マップ
mindmap
root((13<br/>アンチパターン))
コスト系
1 XL常時起動
2 全WH混載
4 不要MV乱立
5 Clustering Key過剰
9 Time Travel最大値
10 大テーブルでDELETE/UPDATE
11 Resource Monitor未割当
12 Serverless Task無制御
13 Statement timeout未設定
クエリ品質系
3 SELECT *濫用
ガバナンス系
6 権限直付け
7 データシェア過剰権限
Cortex系
8 行ごとCortex呼び出し
順に見ていく。
#1. XL/2XL Warehouse を常時起動
症状
月末の請求書を見て「先月の3倍」と気づく。原因不明、Snowflake のサポートに問い合わせる。
根本原因
AUTO_SUSPEND = NULL(停止しない設定)になっている- もしくは
AUTO_SUSPEND = 600以上で「ほとんど停止しない」状態 - WH のサイズが用途に対して大きすぎる(XL を「念のため」常時起動)
XL は 16 credit/h × 24h × 30日 = 11,520 credit / 月。1 credit $2 換算で $23,040。1 WHでこの規模。
脱出法
-- ✅ 即修正:auto-suspend を60秒に
ALTER WAREHOUSE etl_wh SET
AUTO_SUSPEND = 60
AUTO_RESUME = TRUE;
-- ✅ 用途別にサイズを下げて検証
ALTER WAREHOUSE etl_wh SET WAREHOUSE_SIZE = 'LARGE';
-- → クエリ時間とコストの比率を見て、最適点を探す
-- ✅ Resource Monitor で月次クォータを設定
CREATE RESOURCE MONITOR etl_monthly
WITH CREDIT_QUOTA = 1500
FREQUENCY = MONTHLY
TRIGGERS ON 80 PERCENT DO NOTIFY
ON 100 PERCENT DO SUSPEND_IMMEDIATE;
ALTER WAREHOUSE etl_wh SET RESOURCE_MONITOR = etl_monthly;
#2. 1つの Warehouse に全ワークロードを混載
症状
ETL が走るとBIダッシュボードが固まる。ML 訓練を始めると Cortex が遅くなる。「Snowflakeって遅いね」という会話が社内で出始める。
根本原因
WH を1つしか作っていない。ETL / BI / ML / Cortex / アドホックがすべて同じ WH に積まれている。
第2章で見た「3層分離」の最大の利点を捨てている状態。
脱出法
-- ✅ 用途別に WH を分離(コストは増えないことに注意)
CREATE WAREHOUSE etl_wh WAREHOUSE_SIZE = 'LARGE' AUTO_SUSPEND = 60;
CREATE WAREHOUSE bi_wh WAREHOUSE_SIZE = 'MEDIUM' AUTO_SUSPEND = 600
MIN_CLUSTER_COUNT = 1 MAX_CLUSTER_COUNT = 5;
CREATE WAREHOUSE ml_wh WAREHOUSE_SIZE = 'X-LARGE' AUTO_SUSPEND = 60;
CREATE WAREHOUSE cortex_wh WAREHOUSE_SIZE = 'SMALL' AUTO_SUSPEND = 60;
CREATE WAREHOUSE adhoc_wh WAREHOUSE_SIZE = 'X-SMALL' AUTO_SUSPEND = 60;
「WH を増やすとコストが増える」と思いがちだが、秒課金なので使った時間しか課金されない。むしろ用途に合ったサイズで動くようになり、合計コストは下がる傾向。
#3. SELECT * の濫用
症状
- スキャン量が想定の10倍
- Result Cache が効かない(カラム追加で即無効化)
- BIツール経由のクエリが遅い
根本原因
- 「全カラム必要かもしれない」で
SELECT * - BIツールがデフォルトで
SELECT *系のクエリを生成 - カラム追加時にキャッシュが破壊される
Snowflake はカラム単位で micro-partition を読む(カラムナ)。必要なカラムだけ指定すれば、不要列を物理的に読まない。
脱出法
-- ❌ 危険
SELECT * FROM orders WHERE order_date >= '2026-04-01';
-- ✅ 必要列だけ
SELECT order_id, customer_id, order_date, total_amount
FROM orders
WHERE order_date >= '2026-04-01';
-- ✅ 中間 view / mart で「必要列だけのワイドビュー」を作る
CREATE VIEW orders_for_bi AS
SELECT order_id, customer_id, order_date, total_amount, status
FROM orders;
BIツール側でも「explicit column selection」を有効化する(Tableau、Looker、Power BI で設定可能)。
#4. 不要な Materialized View の乱立
症状
- バックグラウンドの reclustering / refresh credit が膨らんでいる
- 「速くしたいから」と何でも MV にした結果、メンテコストの方が大きい
根本原因
- 「MV = 速い」という単純な理解
- 実際は MV は単一テーブル・限定SQLしか対応せず、refresh credit を継続消費
- Dynamic Tables(GA 2024-Q4)の存在を知らない
脱出法
-- ❌ MV をたくさん作る
CREATE MATERIALIZED VIEW customer_stats_mv AS
SELECT customer_id, COUNT(*) FROM orders GROUP BY 1;
-- ✅ Dynamic Tables に統合(JOIN や複雑SQLも書ける)
CREATE OR REPLACE DYNAMIC TABLE customer_stats
TARGET_LAG = '5 minutes'
WAREHOUSE = mart_wh
AS
SELECT
c.customer_id,
c.name,
c.segment,
COUNT(o.order_id) AS total_orders,
SUM(o.amount) AS total_revenue
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY 1, 2, 3;
新規開発では Dynamic Tables 優先。MV は「単一テーブルの単純な集約 + 高頻度参照」に限定。
#5. Clustering Key の過剰指定
症状
- Auto-clustering の serverless credit が膨大
- データ書き込みのレイテンシが上がる
- 期待した検索高速化が見られない
根本原因
- 「全テーブルにとりあえず CLUSTER BY」
- カーディナリティの高すぎる列を指定
- 5カラム以上指定(メンテナンスコストが急増)
脱出法
-- ❌ 中小テーブルや低使用頻度テーブルに付ける
ALTER TABLE small_lookup CLUSTER BY (id);
-- ❌ カラム数が多すぎる
ALTER TABLE orders CLUSTER BY (col1, col2, col3, col4, col5, col6);
-- ✅ 1TB+ のテーブルに、3-4カラム以下、低カーディナリティ → 高カーディナリティの順
ALTER TABLE orders CLUSTER BY (order_date, customer_segment, customer_id);
-- ✅ 不要な Clustering Key を外す
ALTER TABLE small_lookup DROP CLUSTERING KEY;
判断基準:
-- 該当テーブルの clustering depth と reclustering cost を確認
SELECT
TABLE_NAME,
AUTO_RECLUSTERING_CREDIT_USED,
AUTO_RECLUSTERING_BYTES_RECLUSTERED
FROM SNOWFLAKE.ACCOUNT_USAGE.AUTOMATIC_CLUSTERING_HISTORY
WHERE START_TIME >= DATEADD(day, -30, CURRENT_TIMESTAMP())
ORDER BY AUTO_RECLUSTERING_CREDIT_USED DESC;
ROIを必ず計算 ─ 「reclustering credit < クエリ高速化で節約された credit」になっているか。
#6. ユーザーへの権限直付け
症状
- 異動・退職のたびに権限管理が崩壊
- 誰がどこに何の権限を持っているか分からない
- 監査時に大量の権限調査依頼
根本原因
第7章で書いた Functional Role + Access Role 二段構成を組まずに、ユーザーに直接 GRANT してしまう。RDB(Oracle / SQL Server)出身者がよくやる。
-- ❌ ユーザーに直接権限
GRANT SELECT ON sales_db.public.orders TO USER alice;
GRANT INSERT ON sales_db.public.orders TO USER alice;
-- → alice が異動したら全部洗い直し
脱出法
-- ✅ 二段構成
-- (1) Access Role
CREATE ROLE db_sales_read;
GRANT USAGE ON DATABASE sales_db TO ROLE db_sales_read;
GRANT USAGE ON ALL SCHEMAS IN DATABASE sales_db TO ROLE db_sales_read;
GRANT SELECT ON ALL TABLES IN SCHEMA sales_db.public TO ROLE db_sales_read;
GRANT SELECT ON FUTURE TABLES IN SCHEMA sales_db.public TO ROLE db_sales_read;
-- (2) Functional Role
CREATE ROLE analyst;
GRANT ROLE db_sales_read TO ROLE analyst;
-- (3) ユーザーには Functional Role のみ
GRANT ROLE analyst TO USER alice;
異動 = Functional Role を入れ替えるだけ。退職 = Functional Role を全外しするだけ。
#7. Data Sharing で過剰権限を付与
症状
共有相手のアカウントから「想定外のテーブルにもアクセスできていた」と指摘される。
根本原因
GRANT SELECT ON ALL TABLESで共有- スキーマ全体を共有してしまう
- Reader Account ではなく通常 Account に共有
脱出法
-- ❌ 過剰
CREATE SHARE my_share;
GRANT USAGE ON DATABASE prod_db TO SHARE my_share;
GRANT SELECT ON ALL TABLES IN SCHEMA prod_db.public TO SHARE my_share;
-- ✅ 必要なテーブル / view だけ
CREATE SHARE my_share;
GRANT USAGE ON DATABASE prod_db TO SHARE my_share;
GRANT USAGE ON SCHEMA prod_db.shared TO SHARE my_share;
GRANT SELECT ON VIEW prod_db.shared.customer_summary TO SHARE my_share;
GRANT SELECT ON VIEW prod_db.shared.order_summary TO SHARE my_share;
-- ✅ Reader Account を発行(Snowflakeアカウントを持たない相手向け)
CREATE MANAGED ACCOUNT external_partner
ADMIN_NAME = 'partner_admin'
ADMIN_PASSWORD = '...'
TYPE = READER;
「view 経由で共有 + 必要列だけ」が原則。Row Access Policy / Masking Policy も併用すれば、テナント別の行フィルタリングも可能。
#8. Cortex 関数を毎行で呼び出す
症状
- Cortex を導入したら月末請求書に $5,000+ の Cortex 課金
- 11.8億行を処理した実例で1クエリ $5,000
根本原因
- UDF的な感覚で
SELECT *, CORTEX.SUMMARIZE(text) FROM big_table - バッチ処理 / 増分処理を考えていない
- 大型モデル(Claude-4-opus級 = 12 credits / M tokens)をいきなり使う
脱出法
-- ❌ 全行に対して大型モデルで Cortex 呼び出し
SELECT
id,
SNOWFLAKE.CORTEX.COMPLETE('claude-4-opus', 'Summarize: ' || comment) AS summary
FROM all_comments; -- 11.8億行 → $5,000
-- ✅ Step 1: COUNT_TOKENS で見積もる
SELECT
AVG(SNOWFLAKE.CORTEX.COUNT_TOKENS('mistral-large2', comment)) AS avg_tokens,
COUNT(*) AS rows
FROM all_comments;
-- ✅ Step 2: 軽量モデル + 少量サンプルで品質確認
WITH sample AS (
SELECT id, comment FROM all_comments
WHERE LENGTH(comment) > 100
LIMIT 1000
)
SELECT id, SNOWFLAKE.CORTEX.SUMMARIZE(comment) FROM sample;
-- ✅ Step 3: Dynamic Tables で増分処理(2025-09 GA)
CREATE OR REPLACE DYNAMIC TABLE comment_summaries
TARGET_LAG = '1 hour'
WAREHOUSE = cortex_wh
AS
SELECT
id,
comment,
SNOWFLAKE.CORTEX.SUMMARIZE(comment) AS summary
FROM all_comments
WHERE LENGTH(comment) > 100; -- 短文はskip
新規 / 変更分だけ Cortex を呼ぶことで、初回の高コストを継続的なコストにせず済む。
#9. Time Travel を「念のため」最大値に
症状
ストレージ課金が実体データの数倍に膨らんでいる。
根本原因
-- ❌ Enterprise+ で全テーブルにこれを設定
ALTER TABLE big_table SET DATA_RETENTION_TIME_IN_DAYS = 90;
-- 大きなテーブル + 高頻度UPDATE = 過去90日分の全変更がストレージとして残る
Time Travel + Fail-safe(Time Travel 後7日)を全テーブルに最大値で設定すると、ストレージは簡単に5-10倍に膨らむ。
脱出法
-- ✅ デフォルトは1日、重要DBのみ7-14日
ALTER TABLE small_dimension SET DATA_RETENTION_TIME_IN_DAYS = 1;
ALTER TABLE big_facts SET DATA_RETENTION_TIME_IN_DAYS = 1;
ALTER TABLE critical_audit_log SET DATA_RETENTION_TIME_IN_DAYS = 14;
-- ✅ 一時的なテーブルは Transient で(Fail-safe なし、Time Travel 0-1日)
CREATE TRANSIENT TABLE staging_orders ( ... );
-- ✅ 完全一時的なら Temporary(セッション内のみ、ストレージ課金なし)
CREATE TEMPORARY TABLE tmp_calc ( ... );
ストレージ使用量を確認:
SELECT
TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME,
ACTIVE_BYTES / POW(1024, 3) AS active_gb,
TIME_TRAVEL_BYTES / POW(1024, 3) AS time_travel_gb,
FAILSAFE_BYTES / POW(1024, 3) AS failsafe_gb
FROM SNOWFLAKE.ACCOUNT_USAGE.TABLE_STORAGE_METRICS
WHERE DELETED = FALSE
ORDER BY TIME_TRAVEL_BYTES + FAILSAFE_BYTES DESC
LIMIT 20;
#10. 大テーブルでの DELETE / UPDATE
症状
- 数十億行のテーブルで
DELETE WHERE date < ...を流す → 30分以上、credit 大量消費 - micro-partition が増えてストレージも膨らむ
- Clustering Key の reclustering credit がさらに膨らむ
根本原因
OLTPの感覚で DELETE / UPDATE を使う。Snowflake の micro-partition は不変だから、UPDATE/DELETE は実質「該当 micro-partition を新しいバージョンに書き換える」操作。
脱出法
-- ❌ OLTP的発想
DELETE FROM orders WHERE order_date < DATEADD(year, -3, CURRENT_DATE());
-- ✅ 論理削除フラグ + Insert-only
ALTER TABLE orders ADD COLUMN is_deleted BOOLEAN DEFAULT FALSE;
ALTER TABLE orders ADD COLUMN deleted_at TIMESTAMP;
UPDATE orders
SET is_deleted = TRUE, deleted_at = CURRENT_TIMESTAMP()
WHERE order_date < DATEADD(year, -3, CURRENT_DATE());
-- BI / アプリは is_deleted = FALSE をフィルタ
CREATE VIEW orders_active AS
SELECT * FROM orders WHERE is_deleted = FALSE;
-- ✅ 大規模な物理削除が必要なら、CREATE TABLE AS で再作成
CREATE OR REPLACE TABLE orders_new AS
SELECT * FROM orders WHERE order_date >= DATEADD(year, -3, CURRENT_DATE());
-- 入れ替え(Atomic Swap)
ALTER TABLE orders SWAP WITH orders_new;
DROP TABLE orders_new; -- 古い方を削除
大規模テーブルの再構築は CREATE TABLE AS + SWAP が定石。
#11. Resource Monitor を作って WH に割り当てない
症状
請求書を見て初めて「Resource Monitor が機能していない」ことに気づく。
根本原因
作っただけで安心して、WH への割り当てを忘れる。
-- ❌ これだけだと無効
CREATE RESOURCE MONITOR my_quota
WITH CREDIT_QUOTA = 1000
FREQUENCY = MONTHLY
TRIGGERS ON 100 PERCENT DO SUSPEND;
脱出法
-- ✅ 作成 + 割り当て
CREATE RESOURCE MONITOR my_quota
WITH CREDIT_QUOTA = 1000
FREQUENCY = MONTHLY
TRIGGERS
ON 80 PERCENT DO NOTIFY
ON 95 PERCENT DO NOTIFY
ON 100 PERCENT DO SUSPEND_IMMEDIATE;
-- アカウント全体に適用
ALTER ACCOUNT SET RESOURCE_MONITOR = my_quota;
-- もしくは特定 WH に
ALTER WAREHOUSE etl_wh SET RESOURCE_MONITOR = my_quota;
-- 設定確認
SHOW RESOURCE MONITORS;
SELECT * FROM SNOWFLAKE.ACCOUNT_USAGE.WAREHOUSE_METERING_HISTORY
WHERE WAREHOUSE_NAME = 'ETL_WH'
AND START_TIME >= DATEADD(day, -7, CURRENT_TIMESTAMP());
Serverless系(Cortex / Snowpipe / auto-clustering)は WH-attached モニタでカバーされない。Budget で補完する。
#12. Serverless Task の無制御スケジュール
症状
- Serverless Task の credit 単価は通常 WH の 1.5x
- スケジュールを多重に組んで、想定の3倍消費
根本原因
- Task を多数定義し、それぞれが Serverless Compute Pool で動く
- 重複実行・ロングラン・スキップ条件未設定
- TARGET_COMPLETION_INTERVAL を指定していない
脱出法
-- ❌ Serverless で重複実行・条件なし
CREATE TASK refresh_x WAREHOUSE = NULL -- Serverless
SCHEDULE = '5 MINUTE'
AS CALL refresh_proc();
-- ✅ 通常 WH で実行(コスト1/1.5)
CREATE TASK refresh_x WAREHOUSE = etl_wh
SCHEDULE = '5 MINUTE'
AS CALL refresh_proc();
-- ✅ Serverless が必要なら TARGET_COMPLETION_INTERVAL 指定
CREATE TASK refresh_x WAREHOUSE = NULL
SCHEDULE = '5 MINUTE'
TARGET_COMPLETION_INTERVAL = '4 MINUTE'
AS CALL refresh_proc();
-- ✅ 変更があるときだけ実行
CREATE TASK process_stream WAREHOUSE = etl_wh
SCHEDULE = '5 MINUTE'
WHEN SYSTEM$STREAM_HAS_DATA('orders_stream')
AS MERGE INTO ...;
-- ✅ Tasks Flex で柔軟性とコスト両立(2025〜)
CREATE TASK my_flex_task WAREHOUSE = NULL
SCHEDULE = '5 MINUTE'
TASK_TYPE = 'FLEX'
AS ...;
#13. Statement Timeout 未設定で 48時間 runaway
症状
- 1つのクエリが48時間動き続ける
- VWH が auto-suspend 効かず credit 大量消費
根本原因
Snowflake のデフォルト STATEMENT_TIMEOUT_IN_SECONDS は 172800秒(48時間)。間違ったクエリが意図せず長時間動き続ける。
脱出法
-- ✅ アカウント全体のデフォルトを下げる
ALTER ACCOUNT SET STATEMENT_TIMEOUT_IN_SECONDS = 3600; -- 1時間
-- ✅ ロール別に設定
ALTER ROLE analyst SET STATEMENT_TIMEOUT_IN_SECONDS = 600; -- 10分
-- ✅ WH 別に設定
ALTER WAREHOUSE adhoc_wh SET STATEMENT_TIMEOUT_IN_SECONDS = 1800; -- 30分
-- ✅ STATEMENT_QUEUED_TIMEOUT も併設
ALTER ACCOUNT SET STATEMENT_QUEUED_TIMEOUT_IN_SECONDS = 300; -- キュー5分でタイムアウト
「長時間クエリが許される文脈(バッチ ETL)と許されない文脈(アドホック)を WH 単位で分ける」のが現実的。
まとめ表
| # | アンチパターン | 症状 | 根本原因 | 脱出法 |
|---|---|---|---|---|
| 1 | XL/2XL 常時起動 | 月末巨額請求 | auto-suspend=Never / 600+ | auto-suspend=60秒、Resource Monitor紐付け |
| 2 | 全ワークロード混載 | クエリ詰まり | 1WHにETL/BI/ML集中 | 用途別WH分離 |
| 3 | SELECT * 濫用 | スキャン量増・キャッシュ無効 | カラム特定の手間 | 必要列明示、view経由 |
| 4 | 不要MV乱立 | refreshコスト | ”速ければ良い”発想 | Dynamic Tables統合 |
| 5 | Clustering Key過剰 | reclustering credit | 全テーブル付与 | 1TB超 + 高頻度フィルタのみ、3-4カラム以下 |
| 6 | 権限直付け | 異動でカオス | Functional Role省略 | Functional + Access Role二段構成 |
| 7 | データシェア過剰権限 | 情報漏洩リスク | ALL PRIVILEGES | view + 必要列のみ + Reader Account |
| 8 | Cortex行ごと呼び出し | LLMコスト爆発 | UDF感覚で毎行 | バッチ + 軽量モデル + Dynamic Tables |
| 9 | Time Travel 最大値 | ストレージ膨張 | ”念のため最大” | デフォルト1日、重要DBのみ7-14日 |
| 10 | 大テーブルDELETE/UPDATE | micro-partition増 | OLTP的発想 | INSERT-only、論理削除、SWAP |
| 11 | Resource Monitor未割当 | ガードレール無効 | 作って終わり | WHにASSIGN必須 |
| 12 | Serverless Task無制御 | 1.5x単価で計上 | スケジュール多重 | TARGET_COMPLETION_INTERVAL、Tasks Flex |
| 13 | Statement timeout未設定 | 48h runaway | デフォルト依存 | アカウント / Role / WH 別設定 |
効いている根本原理
| アンチパターン | 違反原理 |
|---|---|
| #1, #2, #11, #12, #13 | 原理2(Pricing) ─ Consumption-based pricing の管理失敗 |
| #3, #4 | 原理1(3層分離) + 原理2 ─ Cache・MV / Dynamic Tables の使い方 |
| #5, #10 | 原理3(コピーを作らない) ─ micro-partition の不変性を無視 |
| #6, #7 | RBAC ガバナンス(4原理外、独立した重要事項) |
| #8 | 原理4(SQL→AI/Apps) ─ Cortex のコスト最適化 |
| #9 | 原理3 ─ Time Travel の保持コスト |
第7章(ベストプラクティス)と本章(アンチパターン)をセットで読んで初めて、Snowflake運用の「べき/べからず」が立体的に見える。
次章では、ここまでの知識を最初の30日にどう落とすかを実践ガイドとして整理する。