Tech Waves

produced by Hakuhodo DY ONE

本ブログは、株式会社Hakuhodo DY ONEの開発チームによるエンジニアブログです。
それぞれのメンバーが業務を通して得た技術情報や、各種セミナーの参加レポート、またその他トピックについて情報発信を行っています。

Snowflakeで雪だるま作ってみた

Snowflakeで雪だるま作ってみた

こんにちは、片岡です。

私は普段、Treasure AI(旧称:Treasure Data)を活用したCDP構築/運用保守業務を担当しているのですが、最近「Snowflakeが便利」「これからはSnowflake」という声が聞こえてきます。データ基盤の第一候補として名前が挙がることも増えてきたのに、正直私自身は「名前は知っているけれど、実際に何ができるかは分からない」という状態で、このままではまずいぞ、と感じていました。

そこで今回、社内の検証環境を使ってSnowflakeを実際に触ってみることにしました。題材は「雪だるま」です。「Snow(雪)+flake(結晶)」というサービス名の縁もありますし、何より部位ごとに違う機能を試せば、Snowflakeの主要機能を一通りなぞれるはず、というのが狙いです。

この記事では、その試行錯誤の記録をお届けします。

 


1. そもそもSnowflakeってなに?

ざっくり言うと、Snowflakeはクラウド上で動くデータウェアハウス(DWH)です。膨大なデータを溜めて、SQLで集計・分析するための基盤、というのが本来の得意分野になります。

ただ、実際に触ってみて驚いたのですが、いまのSnowflakeは「データを置く箱」の枠を超えていて、下の4つが同一画面(Snowsight)上ですべて動きます

  • SQLはもちろん、Pythonも同じ画面で書けて動く
  • LLM(大規模言語モデル)をSQL関数として呼び出せる
  • Streamlitという対話アプリまで、Snowflake内で公開できる
  • スケジューラや変更検知まで内蔵されていて、外部ツールなしで自動化が組める

というように、"分析からAI・アプリ・自動化まで一気通貫"のプラットフォームになっていました。本記事では上の4つを雪だるまの部位ごとに順番に試していきます。

 


2. 「雪だるまをどう作るか」を考えた

社内でSnowflake有識者に相談してみたところ、「ヒートマップで雪だるまっぽい濃淡を出せばいいんじゃない?」というアイデアをもらいました。なるほど、と思ったものの、これだと"データを作った"だけで、Python・LLM・アプリまで揃っているというSnowflakeらしさが伝わりにくい気がして、しっくり来ませんでした。

そのため、部位ごとに違うSnowflake機能を割り当てて組み立てることにしました。

配役は以下の通りです。

部位 使うSnowflake機能
🎩 帽子 SQL基礎(GENERATOR関数)
🧠 頭 Snowpark for Python
🧣 マフラー Cortex AI(LLM呼び出し)
🫙 パーツ管理 VARIANT型(半構造化データ)
🖼️ 展示 Streamlit in Snowflake
⏰ 時間経過 Streams & Tasks

これなら雪だるまを完成させる頃には、Snowflakeの主要機能をひと通りなぞれているはずです。

まずは、この検証で使うワークスペース(データベース・スキーマ・ウェアハウス)から用意していきます。

 


準備:ワークスペースを作る

Snowsight(SnowflakeのWeb画面)にログインし、次のSQLで一気に作ってしまいます。

-- 検証用のデータベースとスキーマを作る
CREATE DATABASE IF NOT EXISTS SNOWMAN_LAB;
USE DATABASE SNOWMAN_LAB;
CREATE SCHEMA IF NOT EXISTS BUILD;
USE SCHEMA BUILD;

-- コンピュートリソース(小さいので十分)
CREATE WAREHOUSE IF NOT EXISTS SNOWMAN_WH
    WAREHOUSE_SIZE = 'XSMALL'
    AUTO_SUSPEND = 60
    AUTO_RESUME = TRUE;
USE WAREHOUSE SNOWMAN_WH;

AUTO_SUSPEND = 60を入れておくと、60秒使わないと自動で止まってくれるので、無駄なクレジット消費を防げます。地味に安心できるポイントでした。

作成できたら、いまのセッションが本当にこの設定になっているかを1本のSELECTで確認しておきます。

SELECT CURRENT_DATABASE() AS DB, CURRENT_SCHEMA() AS SCHEMA,
       CURRENT_WAREHOUSE() AS WH, CURRENT_ROLE() AS ROLE;

SNOWMAN_LAB / BUILD / SNOWMAN_WH / SYSADMIN の4つが揃っていれば準備完了です。以降の章はこのコンテキストを前提に進めます。ではウォームアップに、SQLだけで帽子を描くところから始めます。

 


3. 🎩 帽子を作る ── SQLの GENERATOR でシルクハット

帽子(シルクハット)を描くのに使うのは、GENERATORという、指定した数だけ行を生成できるSnowflakeの関数です。テストデータ作成でよく使われる定番機能のようなので、これを流用してASCIIアートを組み立てます。

-- シルクハットを4行のテキストで描画
WITH hat AS (
    SELECT
        seq4() AS row_no,
        CASE
            WHEN seq4() < 3 THEN REPEAT(' ', 3) || REPEAT('█', 7) || REPEAT(' ', 3)
            ELSE REPEAT('█', 13)
        END AS shape
    FROM TABLE(GENERATOR(ROWCOUNT => 4))
)
SELECT shape AS "🎩帽子" FROM hat ORDER BY row_no;

GENERATOR(ROWCOUNT => 4)で4行分の空の行を生成し、seq4()で0,1,2,3…の連番を振ります。あとはCASEで「上3行は帽子の筒」「4行目はつば」を振り分けるだけです。

普段はシステム同士を繋いで元データを取ってくる仕事が中心なので、DWHの中でExcelアートみたいなお絵描きまで出来てしまうのは意外と新鮮で、地味に楽しい発見でした。次は同じSnowflakeの画面でPythonを動かしてみます。

 


4. 🧠 頭を作る ── Snowparkで円を描いてテーブル化

次は頭(丸い顔)です。ここではSnowpark for Pythonを使って、SQLのすぐ隣でPythonのコードを走らせます。

Snowsight上でNotebookを新規作成し、まずSQLセルで今回のロールとコンテキストを合わせておきます。

USE ROLE SYSADMIN;
USE DATABASE SNOWMAN_LAB;
USE SCHEMA BUILD;

続いて、Pythonセルを追加して、頭のASCIIアートを描くコードを貼ります。

from snowflake.snowpark.context import get_active_session
session = get_active_session()

# 円形の頭をASCIIで描画
size = 11
radius = 5
center = size // 2
lines = []
for y in range(size):
    line = ""
    for x in range(size):
        dx, dy = x - center, y - center
        if dx*dx + dy*dy <= radius*radius:
            line += "●"
        else:
            line += "・"
    lines.append(line)

# データフレームとしてテーブルに保存
head_df = session.create_dataframe(
    [(i, l) for i, l in enumerate(lines)],
    schema=["row_no", "shape"]
)
head_df.write.save_as_table("SNOWMAN_HEAD", mode="overwrite")

for l in lines:
    print(l)

get_active_session()でSnowflakeとのセッションを取得したら、あとは普通のPython。pandasに慣れている方なら、ほとんど同じ感覚で書けると思います。save_as_tableで計算結果をそのままテーブルに書き出せるのが便利で、「Pythonの処理結果をSQLで参照する」がシームレスに繋がっていることが分かりました。

SQLもPythonも動く、と分かったところで、次はLLMにマフラーの色を提案してもらうことにします。

 


5. 🧣 マフラーの色をLLMに決めてもらう ── Cortex AI

雪だるまと言えばマフラー。色を自分で決めるのも味気ないので、ここではLLMに任せることにしました。

驚いたのが、SnowflakeではSNOWFLAKE.CORTEX.COMPLETEというSQL関数をSELECTするだけでLLMを呼べる、ということでした。外部APIキーは不要で、必要なロール権限(CORTEX_USERなど)が付いていればSELECTだけで呼び出せます

-- マフラーの色を提案してもらう + 気持ちを俳句にしてもらう(1回のSELECTで両方)
SELECT SNOWFLAKE.CORTEX.COMPLETE(
    'llama3.1-70b',
    '雪だるまが巻くマフラーの色を1つ提案してください。50字以内で理由も。'
) AS "マフラー提案",
SNOWFLAKE.CORTEX.COMPLETE(
    'llama3.1-70b',
    '寒い夜に立っている雪だるまの気持ちを、五・七・五の俳句で表現してください。'
) AS "雪だるまの俳句";

 

第1引数がモデル名、第2引数がプロンプト。それだけです。返ってきた結果はこちら。

マフラー提案 雪だるまの俳句
赤です。雪だるまは白なので、赤いマフラーが映えます。赤は元気なイメージがあるので、雪だるまの可愛さを引き立てます。 雪だるまの 寒い夜に立つ私 冬の悲しみ

マフラーは「赤」、理由は「白い雪だるまに映えるから」と、まっとうな配色提案が返ってきました。俳句のほうは、五・七・五の音数はちょっと怪しいものの、"冬の悲しみ"で締める文学的なフィニッシュが個人的にツボでした。

「集計した結果に、そのままLLMのコメントを添える」みたいな使い方も、SELECTの延長で書けそうです。データとAIが同じ場所にあることの利点を実感しました。

 


6. 🫙 パーツをJSONで管理する ── VARIANT型

ここまでで帽子・頭・マフラーができました。ただ、パーツによって持ちたい属性が違います(帽子には「種類」、マフラーには「長さ」、鼻には「素材」…)。

普通のRDBだと「属性ごとに列を作る」or「別テーブルに逃がす」で悩みそうな場面ですが、SnowflakeにはVARIANTという半構造化データ(=JSONっぽいやつ)を直接列に入れられる型があります。試してみます。

-- パーツテーブル作成
CREATE OR REPLACE TABLE snowman_parts (
    part_id INT AUTOINCREMENT,
    part_name VARCHAR,
    attributes VARIANT
);

-- 各パーツを挿入(属性はJSONで)
INSERT INTO snowman_parts (part_name, attributes)
SELECT '頭',       PARSE_JSON('{"直径_cm": 30, "色": "白", "素材": "雪"}')       UNION ALL
SELECT '帽子',     PARSE_JSON('{"種類": "シルクハット", "色": "黒"}')            UNION ALL
SELECT 'マフラー', PARSE_JSON('{"色": "赤", "素材": "ウール", "長さ_cm": 80}')    UNION ALL
SELECT '胴体',     PARSE_JSON('{"直径_cm": 45, "色": "白", "ボタン数": 3}')       UNION ALL
SELECT '目',       PARSE_JSON('{"個数": 2, "素材": "石炭"}')                     UNION ALL
SELECT '鼻',       PARSE_JSON('{"素材": "にんじん", "長さ_cm": 8}');

-- 属性を取り出す
SELECT
    part_name AS "パーツ",
    attributes:"色"::STRING       AS "色",
    attributes:"素材"::STRING     AS "素材",
    attributes:"長さ_cm"::NUMBER  AS "長さcm"
FROM snowman_parts
ORDER BY part_id;

見どころはattributes:"色"::STRINGのような記法で、JSONの中のキーを列のように扱えるところです。パーツごとに属性が違っても、テーブルはひとつで済みます。日本語のキー名やエイリアスにはダブルクォート必須です。

実行すると、こんな一覧が返ってきます。

パーツ 素材 長さcm
null
帽子 null null
マフラー ウール 80
胴体 null null
null 石炭 null
null にんじん 8

 

パーツによって持っていない属性は自動でnullが並ぶだけで、テーブル定義側は何も変えていません。

「後で新しい属性を追加したくなっても、テーブル定義を変えなくていい」というのは、実運用でもかなり効きそうな体験でした。

パーツの管理まで揃ったので、次はいよいよ完成した雪だるまをSnowflakeの中で"展示"してみます。

 


7. 🖼️ 完成した雪だるまを飾る ── Streamlit in Snowflake

ここで登場するのがStreamlit in Snowflake。Snowflakeの中で、Pythonの対話アプリをそのまま公開できる仕組みです。

import streamlit as st
import pandas as pd

st.title("⛄ Snowflakeで作った雪だるま")

temp = st.slider("今日の気温 (℃)", -20, 25, -5)

# 気温で雪だるまの状態を切り替え(AA本体は付録参照)
if temp < 0:
    st.text(snowman_alive)     # 元気な雪だるまAA
    st.success(f"気温{temp}℃ - 元気に立っています!")
elif temp < 10:
    st.text(snowman_melting)   # 少し溶けかけAA
    st.warning(f"気温{temp}℃ - 少し溶けてきました…")
else:
    st.text("💧💧💧")
    st.error(f"気温{temp}℃ - 溶けてしまいました…")

# 6章で作ったパーツ一覧を表示(pandasに直書き)
st.subheader("使用したパーツ")
parts_df = pd.DataFrame([
    {"パーツ": "頭",       "色": "白", "素材": "雪",     "長さcm": None},
    {"パーツ": "帽子",     "色": "黒", "素材": None,     "長さcm": None},
    {"パーツ": "マフラー", "色": "赤", "素材": "ウール", "長さcm": 80},
    # …残りのパーツは付録のフルコード参照
])
st.dataframe(parts_df, use_container_width=True)

コード全文は末尾の付録に載せています。本当は6章で作ったsnowman_partsテーブルをそのままSELECTしたかったのですが、Streamlit in Snowflakeは公式ドキュメントによれば所有者ロール(owner's rights)で実行される仕様のところ、今回の検証環境ではPUBLICロール相当の挙動になり、SYSADMINで作ったテーブルには権限が足りず参照できませんでした(アカウント側のポリシー設定が影響していた可能性があります)。

ここは記事の主旨から少し外れるため、今回はpandasに直書きして先へ進めています。

起動すると、気温スライダーを動かすたびに雪だるまの状態が変わり、テーブルにあるパーツ一覧も同時に表示されるという、地味ながら「雪だるまが立った!」という達成感のある画面ができました。

 

スライダーを右に動かすほど雪だるまが目に見えて弱っていくようになっています。1℃を超えたあたりで頭が小さくなり、10℃を超えると水滴だけが残ります。

1度と10度の雪だるま



これで本編の雪だるまは完成です。以下はおまけとして、この雪だるまを時間の経過で変化させる仕組みも試してみます。

 


8. ⏰ おまけ:雪だるまを"日々変化"させる ── Streams & Tasks

時間経過で雪だるまが変化する仕組みは、Stream(テーブルの変更を検知)とTask(スケジューラ)を組み合わせて作ります。

  • Stream: テーブルに入った差分(新規行など)を追いかけてくれる仕組み
  • Task: 「毎分」「毎日」などの間隔で処理を回すスケジューラ

これを組み合わせると、「気温データが増えたら、自動的に雪だるまの状態を計算して履歴に追記する」というイベント駆動処理が、Snowflake内だけで組めます。

まずは元になるテーブルと、Streamおよび Task を一気に作ります。

-- 気温データテーブル
CREATE OR REPLACE TABLE daily_temperature (
    date DATE,
    temperature FLOAT
);

-- 雪だるまの状態履歴
CREATE OR REPLACE TABLE snowman_history (
    date DATE,
    size_cm FLOAT,
    condition STRING
);

-- Stream: 気温テーブルの変更を検知
CREATE OR REPLACE STREAM temp_stream ON TABLE daily_temperature;

-- Task: Streamに新データが入ったら状態を計算して履歴に追記
CREATE OR REPLACE TASK update_snowman
    WAREHOUSE = SNOWMAN_WH
    SCHEDULE = '1 MINUTE'
    WHEN SYSTEM$STREAM_HAS_DATA('temp_stream')
AS
INSERT INTO snowman_history (date, size_cm, condition)
SELECT
    date,
    CASE
        WHEN temperature < 0 THEN ROUND(30 + ABS(temperature) * 0.5, 1)
        ELSE GREATEST(0, ROUND(30 - temperature * 1.5, 1))
    END AS size_cm,
    CASE
        WHEN temperature < 0  THEN '元気'
        WHEN temperature < 5  THEN '少し溶けかけ'
        WHEN temperature < 15 THEN 'かなり溶けた'
        ELSE '消滅'
    END AS condition
FROM temp_stream;

作成自体はすんなり通ります。ここまで来たら、いよいよTaskを起動して自動発火を待つだけ…と思いきや、ここで壁にぶつかりました。

ALTER TASK update_snowman RESUME;

Cannot execute task, EXECUTE TASK privilege must be granted to owner role と表示され、Taskを起動できません。調べてみたところ、SnowflakeでTaskを起動・実行するにはEXECUTE TASKというアカウント単位の権限が必要で、これはデフォルトではACCOUNTADMINにしか付いていないとのこと。今回検証で使っているSYSADMINロールでは、Taskを作れても動かせない、ようでした。

正攻法では管理者に依頼し権限を付与してもらうところですが、今回は記事の趣旨から少しズレるため、Taskの自動発火は諦めて、Streamの動きと、Taskの中身のロジックだけは手動で確認してみます。

まず、気温データを投入します。

INSERT INTO daily_temperature VALUES
    ('2025-12-20', -5),
    ('2025-12-25', -8),
    ('2026-01-10', -2),
    ('2026-02-14', 3),
    ('2026-03-01', 12);

次に、temp_stream(Streamオブジェクト)をSELECTで覗いてみます。Streamは、対象テーブルに対して発生した変更を"仮想テーブル"として見せてくれる仕組みです。

SELECT * FROM temp_stream;

METADATA$ACTION列にすべてINSERTが並んでいます。先ほど投入した5行がすべて"変更差分"として捕まっている、という状態が可視化されました。ここまでで、Stream自体は権限問題なく動いていることが確認できます。

続いて、Taskの中身とまったく同じロジックを、その場で手動INSERTします(=Taskがやるはずだった仕事を代わりに実行)。

INSERT INTO snowman_history (date, size_cm, condition)
SELECT
    date,
    CASE
        WHEN temperature < 0 THEN ROUND(30 + ABS(temperature) * 0.5, 1)
        ELSE GREATEST(0, ROUND(30 - temperature * 1.5, 1))
    END AS size_cm,
    CASE
        WHEN temperature < 0  THEN '元気'
        WHEN temperature < 5  THEN '少し溶けかけ'
        WHEN temperature < 15 THEN 'かなり溶けた'
        ELSE '消滅'
    END AS condition
FROM temp_stream;

SELECT * FROM snowman_history ORDER BY date;

気温の低い日ほど大きく元気、暖かい日は溶けて縮んでいく、という履歴が入りました。冬本番の-8℃では34cm、春先の12℃では12cmまで縮むという物語が、SQLだけで表現できています。

なお、INSERT ... SELECT FROM temp_stream を1回実行すると、Stream側ではその差分が"消費された"扱いになり、次にSELECT * FROM temp_streamしても0行になります。ポイントは、Streamは単にSELECTしただけでは消費されず、INSERT/UPDATE/DELETEなどのDMLに組み込まれて初めてオフセットが進むという挙動になっていることでした。読み取り専用の確認と、実際の消費を分けて設計できるのは、実運用でも安心できる仕様です。

Task本体の自動化までは辿り着けませんでしたが、「変更を検知する Stream + そのロジックをスケジュールで回す Task」という設計そのものは、SQLの延長で書ききれるというのが、この章の一番の学びでした。普段ならETLツールやワークフローエンジンを別に立てたくなる処理が、SQLの延長で完結するというのが、触ってみての最大の驚きでした。

ℹ️ 権限が付与された環境であれば、ALTER TASK ... RESUMEで稼働開始 → Snowsight上で実行履歴も確認できるようです。検証用アカウントでTaskを常時稼働させる場合は、ALTER TASK update_snowman SUSPEND; で忘れずに止めておくのがおすすめです(動きっぱなしはクレジット消費の原因になります)。

 


9. やってみて思ったこと・まとめ

雪だるまを組み立てながら、Snowflakeの主要機能をひととおり触ってみた率直な感想は以下です。

  • ひとつの画面で完結するのが、想像以上に楽だった
    SQLもPythonもLLM呼び出しもアプリ公開も、すべてSnowsightの中で完結します。「あのツールに切り替えて…」の摩擦がないのは、初心者に本当に優しい構造だと感じました。
  • "データ基盤"の枠を超えていた
    ただの倉庫ではなく、AI・アプリ・自動化まで一気通貫で乗っている、というのが触ってみての実感でした。
  • 意外と、詰まらずに動いた
    ワークシートを開いてSQLを打つ、その延長線上に全機能があるので、初心者でもすんなり手が動きました。

もちろん、細かい引っかかりはありました。たとえば以下の通りです。

  • SnowsightのワークシートでCmd+Enter(またはCtrl+Enter)を押すと、他エディタの感覚だと"全部Run"されると思ってしまうのですが、実際はカーソルのある1文だけが実行される仕様でした。最初、SELECTだけが走っていて「なぜCREATEの結果が反映されないのか」と首をひねっていました。全部実行したいときはRun Allボタン(もしくは全選択→実行)を明示的に押す必要があります
  • ソースコードエディタでよく使うCmd+Shift+U(選択行のコメントアウト)のショートカットが効かないため、慣れているエディタの癖で何度か指が空振りしました
  • アカウントへの権限付与周りが少し複雑で理解するのに時間がかかりました

とはいえ、いずれも、画面のエラーメッセージやドキュメントに沿えば数分で解決できる範囲でした。

「Snowflake、名前は聞くけど何ができるのだろう?」という当初の私と同じ立ち位置の方に、この記事が「意外と楽しそう、触ってみようかな」と感じるきっかけになれば嬉しいです。もし業務での活用を検討されているようでしたら、ぜひHakuhodo DY ONEにもお気軽にご相談ください。

 


付録:第7章の完全なStreamlitコード

 
import streamlit as st
import pandas as pd

st.title("⛄ Snowflakeで作った雪だるま")

temp = st.slider("今日の気温 (℃)", -20, 25, -5)

if temp < 0:
    snowman_alive = """
        █████████
        █████████
        █████████
    █████████████████
        ●●●●●●●●●
      ●●●●●●●●●●●●●
      ●●●●○ ▲ ○●●●
      ●●●●● ▽ ●●●●
        ●●●●●●●●●
    ━━━━━━━━━━━━━━━━━
      ●●●●●●●●●●●●●
    ●●●●●●●●●●●●●●●●●
    ●●●●●● ● ●●●●●●●●
    ●●●●●● ● ●●●●●●●●
    ●●●●●● ● ●●●●●●●●
    ●●●●●●●●●●●●●●●●●
    """
    st.text(snowman_alive)
    st.success(f"気温{temp}℃ - 元気に立っています!")
elif temp < 10:
    snowman_melting = "     ●●●●●\n    ●● ○▽○ ●●\n     ●●●●●\n   ━━━━━━━━━\n    ● ● ● ●\n     ●●●●●"
    st.text(snowman_melting)
    st.warning(f"気温{temp}℃ - 少し溶けてきました…")
else:
    st.text("💧💧💧")
    st.error(f"気温{temp}℃ - 溶けてしまいました…")

st.subheader("使用したパーツ")
parts_df = pd.DataFrame([
    {"パーツ": "頭",       "色": "白", "素材": "雪",       "長さcm": None},
    {"パーツ": "帽子",     "色": "黒", "素材": None,       "長さcm": None},
    {"パーツ": "マフラー", "色": "赤", "素材": "ウール",   "長さcm": 80},
    {"パーツ": "胴体",     "色": "白", "素材": None,       "長さcm": None},
    {"パーツ": "目",       "色": None, "素材": "石炭",     "長さcm": None},
    {"パーツ": "鼻",       "色": None, "素材": "にんじん", "長さcm": 8},
])
st.dataframe(parts_df, use_container_width=True)

 

この記事を書いた人

one-ktok (id:kktok)

元JA職員のエンジニア。現在はCDP基盤構築支援に従事しております。柴犬を飼っています。