在現代資料工程領域,半結構化資料的處理能力已成為雲端資料倉儲平台的核心競爭力。Snowflake 透過表格函式(Table Functions)提供了強大而靈活的資料處理能力,讓開發者能夠直接在 SQL 查詢中操作複雜的資料結構。不同於傳統純量函式只能回傳單一數值,表格函式能夠回傳完整的資料集,這個特性使得 SQL 的表達能力大幅提升,特別是在處理 JSON、XML 等半結構化資料時展現出顯著優勢。

對於台灣的資料工程師而言,掌握 Snowflake 表格函式不僅能提升資料處理效率,更能在面對日益複雜的資料整合需求時游刃有餘。本文將深入探討 flatten()、query_history() 與 result_scan() 等核心表格函式的實務應用,並透過完整的程式碼範例展示如何在真實場景中運用這些工具。從 JSON 資料的展開與查詢,到查詢歷史的追蹤與效能監控,再到 VARIANT 資料型別的儲存策略,我們將系統化地解析表格函式的設計原理與最佳實踐。

表格函式的核心概念與運作機制

表格函式在 Snowflake 資料處理架構中扮演著關鍵角色,它們突破了傳統 SQL 函式的限制,提供了更接近程式語言的表達能力。理解表格函式的運作機制是有效運用這項技術的基礎。傳統的純量函式(Scalar Functions)接受一個或多個輸入值並回傳單一結果,舉例來說 UPPER() 函式接受字串並回傳大寫版本。相對地,表格函式接受輸入後回傳整個資料集,這個資料集可以像一般表格一樣在 FROM 子句中使用,與其他表格進行 JOIN 或進行進一步的查詢處理。

表格函式的這種特性使其特別適合處理一對多的資料轉換場景。當你需要將單一欄位中的複雜資料結構(如 JSON 陣列)展開為多筆記錄時,表格函式就是最佳選擇。Snowflake 的查詢最佳化器能夠有效處理包含表格函式的查詢計畫,透過謂詞下推(Predicate Pushdown)與平行處理等技術,確保即使在處理大量資料時也能維持高效能。

在 Snowflake 的實作中,表格函式與 LATERAL JOIN 語法緊密結合。當你在 FROM 子句中使用 TABLE() 關鍵字呼叫表格函式時,Snowflake 會將其視為一個內嵌的表格來源。這種設計允許表格函式存取同一查詢中其他表格的欄位值,實現動態的資料轉換。舉例來說,你可以針對主表格的每一筆記錄呼叫表格函式,將該記錄中的 JSON 欄位展開,然後與其他欄位組合產生最終結果集。

表格函式的型別系統也值得注意。Snowflake 的表格函式可以接受各種資料型別作為輸入,包括標準的數值、字串型別,以及特殊的 VARIANT、OBJECT、ARRAY 等半結構化資料型別。函式的回傳型別定義了輸出表格的結構,包括欄位名稱與資料型別。這種強型別的設計在提供彈性的同時,也確保了查詢的正確性與可預測性。

效能考量是使用表格函式時的重要議題。雖然表格函式提供了強大的資料處理能力,但不當使用可能導致效能問題。舉例來說,在大型表格上對每筆記錄呼叫複雜的表格函式可能產生大量的中間結果,消耗大量運算資源。因此在設計查詢時,應該考慮是否能透過篩選條件減少需要處理的資料量,或者是否能將部分處理邏輯移至其他階段執行。

@startuml
!define DISABLE_LINK
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

package "Snowflake 表格函式架構" {
  
  component "SQL 查詢解析器" as parser
  component "查詢最佳化器" as optimizer
  component "執行引擎" as executor
  
  package "表格函式層" {
    component "內建表格函式" as builtin
    component "使用者自訂函式" as udf
  }
  
  package "資料處理層" {
    component "純量函式引擎" as scalar
    component "表格函式引擎" as table
    component "VARIANT 處理器" as variant
  }
  
  database "資料儲存層" as storage {
    component "結構化資料" as structured
    component "半結構化資料" as semi
  }
}

actor "查詢請求" as query

query --> parser : 提交 SQL 查詢
parser --> optimizer : 產生查詢計畫
optimizer --> builtin : 識別表格函式
optimizer --> udf : 識別自訂函式

builtin --> table : 執行內建函式
udf --> table : 執行自訂邏輯

table --> variant : 處理 VARIANT 資料
table --> scalar : 呼叫純量函式

variant --> semi : 存取半結構化資料
scalar --> structured : 存取結構化資料

table --> executor : 回傳結果集
executor --> query : 輸出查詢結果

@enduml

這個元件圖展現了 Snowflake 表格函式在整體查詢處理架構中的位置。當使用者提交包含表格函式的 SQL 查詢時,查詢解析器首先將其轉換為內部表示,查詢最佳化器識別表格函式呼叫並產生最佳化的執行計畫。表格函式引擎負責實際執行函式邏輯,可能涉及 VARIANT 處理器來處理半結構化資料,或呼叫純量函式進行輔助計算。整個流程展現了表格函式如何整合在 Snowflake 的查詢處理管線中。

flatten() 函式的深度應用與 JSON 資料處理

flatten() 函式是 Snowflake 處理半結構化資料最核心的工具之一,它能夠將巢狀的資料結構展開為平面的表格形式。在現代應用系統中,JSON 格式廣泛用於資料交換與儲存,但其階層式結構往往不利於直接進行 SQL 查詢與分析。flatten() 函式正是為了解決這個問題而設計,它能夠遍歷 JSON 物件或陣列,將每個元素轉換為獨立的資料列。

flatten() 函式的基本語法相當直觀,主要接受兩個參數。INPUT 參數指定要展開的資料來源,通常是一個 VARIANT 型別的欄位或表達式。PATH 參數則是選擇性的,用於指定要展開的特定路徑,這在處理深度巢狀的 JSON 結構時特別有用。函式會回傳一個包含多個欄位的表格,其中最重要的是 VALUE 欄位,它包含了展開後的每個元素值。

在實務應用中,flatten() 經常與 PARSE_JSON() 函式搭配使用。PARSE_JSON() 負責將 JSON 字串轉換為 Snowflake 的 VARIANT 資料型別,然後 flatten() 將其展開為可查詢的格式。這種組合在處理從外部系統匯入的 JSON 資料時特別常見。舉例來說,當你從 REST API 接收到包含客戶訂單的 JSON 回應時,可以先用 PARSE_JSON() 解析,再用 flatten() 展開訂單項目陣列,最後透過標準 SQL 進行分析。

以下範例展示了如何使用 flatten() 處理包含員工資訊的 JSON 資料:

SELECT
    value:empid::NUMBER AS employee_id,
    value:name::VARCHAR AS employee_name,
    value:department::VARCHAR AS department,
    value:salary::NUMBER AS salary
FROM
    TABLE(FLATTEN(INPUT => PARSE_JSON(
        '{
            "employees": [
                {
                    "empid": 1001, 
                    "name": "陳大明",
                    "department": "工程部",
                    "salary": 85000
                },
                {
                    "empid": 1002, 
                    "name": "林小華",
                    "department": "行銷部",
                    "salary": 72000
                },
                {
                    "empid": 1003, 
                    "name": "王志明",
                    "department": "財務部",
                    "salary": 78000
                },
                {
                    "empid": 1004, 
                    "name": "張美玲",
                    "department": "人資部",
                    "salary": 68000
                },
                {
                    "empid": 1005, 
                    "name": "劉建國",
                    "department": "工程部",
                    "salary": 92000
                }
            ]
        }'
    ), PATH => 'employees'));

這個查詢展示了 flatten() 的典型使用模式。PARSE_JSON() 首先將 JSON 字串轉換為 VARIANT 型別,PATH 參數指定展開 employees 陣列,VALUE 欄位透過冒號語法存取 JSON 物件的屬性,雙冒號運算子則將 VARIANT 值轉換為適當的 SQL 資料型別。查詢結果會是一個包含五筆記錄的表格,每位員工佔據一列,所有資料都以標準 SQL 型別呈現。

flatten() 函式的進階應用包括處理多層巢狀結構。當 JSON 包含巢狀陣列或物件時,可以多次呼叫 flatten() 或使用遞迴展開模式。RECURSIVE 參數允許函式遞迴地展開所有層級的巢狀結構,這在處理不確定深度的 JSON 資料時特別有用。舉例來說,組織架構資料可能包含任意層級的部門階層,使用遞迴展開可以將所有部門與員工資訊完全攤平。

效能最佳化是使用 flatten() 時需要特別注意的議題。當處理大型 JSON 陣列時,展開操作可能產生大量資料列,影響查詢效能與資源消耗。因此在設計資料模型時,應該考慮是否需要將所有資料都儲存為 JSON 格式,或者將常用的欄位提取為獨立的欄位以提升查詢效率。另外,適當的篩選條件可以減少需要展開的資料量,舉例來說可以先篩選特定的 JSON 文件,再對篩選結果進行展開操作。

flatten() 函式還支援 OUTER 參數,控制當路徑不存在時的行為。預設情況下 flatten() 採用內部展開模式,如果指定的路徑在某些文件中不存在,這些文件不會出現在結果中。將 OUTER 設為 TRUE 可以改為外部展開模式,即使路徑不存在也會保留原始記錄,VALUE 欄位會是 NULL。這個特性在處理結構不一致的 JSON 資料時特別有用,能夠確保所有原始記錄都被保留。

@startuml
!define DISABLE_LINK
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

start

:接收 JSON 資料;

:使用 PARSE_JSON 轉換;

partition "flatten 處理流程" {
  :指定 INPUT 參數;
  
  if (是否指定 PATH?) then (是)
    :定位到指定路徑;
  else (否)
    :使用根層級資料;
  endif
  
  if (資料型別?) then (陣列)
    :遍歷陣列元素;
    
    fork
      :提取元素 1;
    fork again
      :提取元素 2;
    fork again
      :提取元素 N;
    end fork
    
  else (物件)
    :遍歷物件屬性;
    
    fork
      :提取屬性 1;
    fork again
      :提取屬性 2;
    fork again
      :提取屬性 N;
    end fork
  endif
  
  if (RECURSIVE 模式?) then (是)
    :遞迴展開巢狀結構;
  else (否)
    :僅展開當前層級;
  endif
  
  :產生結果集;
}

:透過 VALUE 欄位存取資料;

:使用型別轉換提取欄位;

:與其他表格進行 JOIN;

:執行彙總與分析;

stop

@enduml

這個活動圖完整展現了 flatten() 函式的處理流程。從接收 JSON 資料開始,經過 PARSE_JSON 轉換為 VARIANT 型別,然後 flatten() 根據參數設定進行展開處理。函式會判斷資料型別是陣列還是物件,採用對應的遍歷策略。如果啟用遞迴模式,會繼續展開巢狀結構。最終產生的結果集可以透過 VALUE 欄位存取,進行型別轉換與進一步的 SQL 操作。

query_history() 與 result_scan() 的效能監控應用

在企業級資料倉儲環境中,查詢效能監控與問題診斷是確保系統穩定運作的關鍵工作。Snowflake 透過 INFORMATION_SCHEMA.QUERY_HISTORY() 表格函式提供了完整的查詢歷史追蹤能力,讓資料工程師能夠深入分析查詢行為、識別效能瓶頸並最佳化系統配置。這個函式回傳的不僅僅是查詢文字,還包括執行時間、資源消耗、錯誤訊息等豐富的中繼資料,是效能調校不可或缺的工具。

QUERY_HISTORY() 函式接受多個選擇性參數來篩選查詢記錄。RESULT_LIMIT 參數控制回傳的最大記錄數,在快速查看最近查詢時特別有用。END_TIME_RANGE_START 與 END_TIME_RANGE_END 參數允許指定時間範圍,這在分析特定時段的查詢模式時很重要。USER_NAME 參數可以篩選特定使用者的查詢,WAREHOUSE_NAME 則篩選特定虛擬倉儲的查詢,這些參數可以組合使用以實現精確的查詢篩選。

以下範例展示如何使用 QUERY_HISTORY() 函式分析最近的查詢執行狀況:

SELECT
    query_id,
    query_text,
    database_name,
    schema_name,
    query_type,
    warehouse_name,
    warehouse_size,
    execution_status,
    error_code,
    error_message,
    start_time,
    end_time,
    total_elapsed_time / 1000 AS execution_seconds,
    bytes_scanned,
    bytes_written,
    rows_produced,
    compilation_time,
    execution_time,
    queued_provisioning_time,
    queued_repair_time,
    queued_overload_time
FROM
    TABLE(INFORMATION_SCHEMA.QUERY_HISTORY(
        RESULT_LIMIT => 100,
        END_TIME_RANGE_START => DATEADD(hour, -24, CURRENT_TIMESTAMP())
    ))
WHERE
    execution_status = 'SUCCESS'
    AND query_type IN ('SELECT', 'INSERT', 'UPDATE')
ORDER BY
    total_elapsed_time DESC;

這個查詢展示了效能分析的典型場景。透過篩選過去 24 小時內成功執行的查詢,並按執行時間降序排列,可以快速識別出最耗時的查詢。查詢結果包含了多個關鍵指標,TOTAL_ELAPSED_TIME 顯示總執行時間,BYTES_SCANNED 顯示掃描的資料量,ROWS_PRODUCED 顯示產生的記錄數。這些指標的組合分析可以揭示查詢的效能特徵,舉例來說高掃描量但低產出量可能表示缺少適當的篩選條件。

QUERY_HISTORY() 函式回傳的欄位中,特別值得注意的是各種排隊時間指標。QUEUED_PROVISIONING_TIME 顯示等待虛擬倉儲啟動的時間,QUEUED_REPAIR_TIME 顯示等待叢集修復的時間,QUEUED_OVERLOAD_TIME 則顯示因倉儲過載而排隊的時間。這些指標對於容量規劃與資源配置決策非常重要。如果發現大量查詢有高排隊時間,可能需要調整虛擬倉儲的大小或增加倉儲數量。

COMPILATION_TIME 與 EXECUTION_TIME 的比例分析也能提供有價值的洞察。如果編譯時間佔總執行時間的比例過高,可能表示查詢複雜度過高或缺少結果快取。Snowflake 會快取查詢的編譯結果與執行結果,相同或類似的查詢可以直接使用快取避免重複計算。因此在設計查詢時應該考慮是否能利用參數化查詢模式來提高快取命中率。

RESULT_SCAN() 函式則提供了檢索歷史查詢結果集的能力。當你透過 QUERY_HISTORY() 找到感興趣的查詢後,可以使用該查詢的 QUERY_ID 來取得其執行結果。這個功能在多種場景下都很有用,舉例來說當你想要複製某次成功查詢的結果,或者需要比較不同時間點執行相同查詢的結果差異時。需要注意的是,Snowflake 只會保留查詢結果一段時間,通常是 24 小時,超過這個時間的查詢結果將無法透過 RESULT_SCAN() 取得。

-- 首先找出特定查詢的 ID
SELECT
    query_id,
    SUBSTR(query_text, 1, 50) AS partial_query,
    start_time,
    total_elapsed_time / 1000 AS seconds
FROM
    TABLE(INFORMATION_SCHEMA.QUERY_HISTORY(
        RESULT_LIMIT => 20
    ))
WHERE
    query_text ILIKE '%SELECT * FROM sales_summary%'
ORDER BY
    start_time DESC;

-- 使用找到的 QUERY_ID 檢索結果
SELECT *
FROM TABLE(RESULT_SCAN('01b23456-789a-bcde-f012-3456789abcde'))
LIMIT 100;

這個範例展示了 QUERY_HISTORY() 與 RESULT_SCAN() 的協同使用模式。第一個查詢搜尋包含特定文字的歷史查詢,ILIKE 運算子提供不區分大小寫的模式比對。找到目標查詢的 QUERY_ID 後,第二個查詢使用 RESULT_SCAN() 檢索其執行結果。這種模式在除錯與問題重現時特別有用,可以快速回到之前的查詢狀態進行分析。

效能監控的進階應用包括建立自動化的監控儀表板與告警系統。透過定期執行 QUERY_HISTORY() 查詢並將結果儲存到監控表格中,可以追蹤查詢效能的長期趨勢。舉例來說,可以計算每日的平均查詢時間、資源消耗總量、失敗查詢數量等指標,並與歷史基準線比較以偵測異常。當某些指標超過閾值時,可以觸發告警通知相關人員進行處理。

QUERY_HISTORY() 還可以用於審計與合規目的。透過記錄誰在什麼時間執行了什麼查詢,可以追蹤資料存取模式,確保符合資料治理政策。舉例來說在金融或醫療等受監管產業,需要詳細記錄所有對敏感資料的存取,QUERY_HISTORY() 提供的完整查詢日誌正是滿足這類需求的理想工具。

@startuml
!define DISABLE_LINK
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

start

:使用者提交查詢;

:Snowflake 執行引擎處理;

partition "查詢生命週期追蹤" {
  :記錄查詢開始時間;
  
  :分配虛擬倉儲資源;
  
  if (倉儲需要啟動?) then (是)
    :記錄 provisioning 時間;
  endif
  
  if (需要排隊等待?) then (是)
    :記錄 queued 時間;
  endif
  
  :編譯查詢計畫;
  
  :記錄 compilation 時間;
  
  :執行查詢;
  
  :記錄 execution 時間;
  
  :產生結果集;
  
  :記錄查詢結束時間;
}

:儲存查詢中繼資料;

:儲存結果集快取;

note right
  查詢中繼資料包含:
  - 查詢 ID 與文字
  - 執行狀態與錯誤
  - 時間與資源指標
  - 倉儲與使用者資訊
end note

partition "查詢歷史分析" {
  :呼叫 QUERY_HISTORY;
  
  :篩選目標查詢;
  
  :分析效能指標;
  
  if (需要查看結果?) then (是)
    :使用 RESULT_SCAN;
    
    if (結果快取存在?) then (是)
      :回傳快取結果;
    else (否)
      :回傳無法取得訊息;
    endif
  endif
}

:產出效能分析報告;

stop

@enduml

這個活動圖展現了從查詢執行到歷史分析的完整流程。當使用者提交查詢時,Snowflake 開始追蹤整個執行生命週期,記錄各個階段的時間與資源消耗。查詢完成後,所有中繼資料與結果集都會被儲存。後續透過 QUERY_HISTORY() 可以檢索這些中繼資料進行分析,如果需要查看實際結果則使用 RESULT_SCAN()。整個流程展現了 Snowflake 如何提供完整的查詢可觀測性。

VARIANT 資料型別與 JSON 儲存策略

VARIANT 資料型別是 Snowflake 處理半結構化資料的核心機制,它提供了儲存與查詢 JSON、XML、Avro 等格式資料的統一介面。不同於傳統關聯式資料庫需要預先定義嚴格的結構描述,VARIANT 允許儲存任意結構的資料,這種彈性在處理來自不同來源、結構可能變化的資料時特別有價值。理解 VARIANT 的內部運作機制與最佳使用模式,是充分發揮 Snowflake 半結構化資料處理能力的關鍵。

VARIANT 資料型別在底層採用了高度最佳化的二進位表示法。當你將 JSON 字串轉換為 VARIANT 時,Snowflake 不是簡單地儲存原始文字,而是解析 JSON 結構並建立內部的樹狀表示。這種表示法支援高效的路徑查詢與欄位提取,避免了每次查詢都需要重新解析 JSON 字串的開銷。同時 VARIANT 也整合了 Snowflake 的壓縮與編碼技術,能夠有效減少儲存空間的使用。

在建立包含 VARIANT 欄位的表格時,需要考慮資料的存取模式。雖然 VARIANT 提供了極大的彈性,但這種彈性也帶來了查詢效能的考量。如果某些 JSON 欄位會被頻繁查詢或用於篩選條件,應該考慮將這些欄位提取為獨立的欄位,而不是每次都從 VARIANT 中提取。Snowflake 支援建立包含實體化欄位與 VARIANT 欄位的混合表格,這種設計可以平衡彈性與效能。

-- 建立包含 VARIANT 欄位的表格
CREATE TABLE product_catalog (
    product_id NUMBER PRIMARY KEY,
    product_name VARCHAR(200),
    category VARCHAR(100),
    base_price NUMBER(10,2),
    attributes VARIANT,
    created_at TIMESTAMP_NTZ DEFAULT CURRENT_TIMESTAMP()
);

-- 插入包含不同結構的產品資料
INSERT INTO product_catalog (product_id, product_name, category, base_price, attributes)
VALUES
    (1001, '智慧型手機 Pro', '電子產品', 25000,
     PARSE_JSON('{
         "brand": "TechCorp",
         "model": "X-2024",
         "specs": {
             "screen_size": "6.7吋",
             "ram": "12GB",
             "storage": "256GB",
             "camera": "108MP"
         },
         "colors": ["黑色", "銀色", "金色"]
     }')),
    (1002, '無線耳機 Elite', '電子產品', 4500,
     PARSE_JSON('{
         "brand": "AudioMax",
         "model": "Elite-500",
         "specs": {
             "battery_life": "30小時",
             "noise_cancellation": true,
             "bluetooth_version": "5.3"
         },
         "colors": ["黑色", "白色"]
     }')),
    (1003, '運動智慧手錶', '穿戴裝置', 8900,
     PARSE_JSON('{
         "brand": "FitTrack",
         "model": "Sport-X",
         "specs": {
             "display": "AMOLED",
             "waterproof": "5ATM",
             "heart_rate_monitor": true,
             "gps": true
         },
         "colors": ["黑色", "藍色", "紅色"],
         "compatible_os": ["iOS", "Android"]
     }'));

-- 查詢 VARIANT 欄位中的資料
SELECT
    product_id,
    product_name,
    attributes:brand::VARCHAR AS brand,
    attributes:specs:ram::VARCHAR AS ram,
    attributes:specs:storage::VARCHAR AS storage
FROM
    product_catalog
WHERE
    category = '電子產品'
    AND attributes:specs:ram IS NOT NULL;

-- 展開 VARIANT 中的陣列
SELECT
    p.product_id,
    p.product_name,
    f.value::VARCHAR AS available_color
FROM
    product_catalog p,
    LATERAL FLATTEN(INPUT => p.attributes:colors) f
WHERE
    p.category = '電子產品';

這個範例展示了 VARIANT 的典型使用模式。表格結構結合了固定欄位與彈性的 VARIANT 欄位,固定欄位儲存所有產品共有的屬性如名稱、類別、價格,VARIANT 欄位則儲存各產品特有的屬性。這種設計允許不同類別的產品有不同的屬性結構,同時保持查詢的彈性。冒號語法用於存取 VARIANT 中的欄位,雙冒號進行型別轉換,LATERAL FLATTEN 展開陣列元素。

VARIANT 欄位的查詢最佳化是一個重要議題。Snowflake 會自動收集 VARIANT 欄位的統計資訊,包括常見的路徑與值分布,這些資訊用於查詢最佳化。然而相較於固定型別的欄位,VARIANT 的查詢效能仍然會有些許差距。因此在設計資料模型時,應該根據存取模式做出權衡,頻繁查詢的欄位建議提取為固定欄位,偶爾存取的欄位則適合保留在 VARIANT 中。

TRY_PARSE_JSON() 函式在資料載入階段扮演重要角色。不同於 PARSE_JSON() 遇到無效 JSON 時會拋出錯誤,TRY_PARSE_JSON() 會回傳 NULL,這在處理可能包含格式錯誤資料的匯入流程中特別有用。透過在載入時使用 TRY_PARSE_JSON(),可以避免因個別記錄的錯誤而中斷整個載入作業,錯誤記錄可以被標記並單獨處理。

-- 驗證 JSON 格式並安全載入
CREATE TABLE json_import_staging (
    raw_data VARCHAR,
    parsed_data VARIANT,
    is_valid BOOLEAN,
    load_timestamp TIMESTAMP_NTZ DEFAULT CURRENT_TIMESTAMP()
);

INSERT INTO json_import_staging (raw_data, parsed_data, is_valid)
SELECT
    raw_json,
    TRY_PARSE_JSON(raw_json) AS parsed,
    TRY_PARSE_JSON(raw_json) IS NOT NULL AS valid
FROM
    external_json_source;

-- 分析無效的 JSON 記錄
SELECT
    raw_data,
    load_timestamp
FROM
    json_import_staging
WHERE
    is_valid = FALSE;

-- 處理有效的記錄
INSERT INTO product_catalog (product_id, product_name, category, base_price, attributes)
SELECT
    parsed_data:id::NUMBER,
    parsed_data:name::VARCHAR,
    parsed_data:category::VARCHAR,
    parsed_data:price::NUMBER,
    parsed_data:attributes
FROM
    json_import_staging
WHERE
    is_valid = TRUE;

這個載入流程展示了如何使用 TRY_PARSE_JSON() 實現容錯的資料匯入。原始 JSON 字串與解析後的 VARIANT 都被保留在暫存表格中,同時記錄解析是否成功。這種設計允許分別處理有效與無效的記錄,有效記錄可以繼續進行資料轉換與載入,無效記錄則可以被檢視與修正後重新處理。

VARIANT 資料型別的儲存限制需要特別注意。單一 VARIANT 值的最大大小為 16MB,這個限制涵蓋了壓縮後的資料大小。對於非常大的 JSON 文件,可能需要考慮分割儲存或使用其他策略。另外 VARIANT 中的日期時間值會被儲存為字串,如果需要進行日期時間運算,應該在查詢時明確轉換為 TIMESTAMP 型別,或者考慮將日期時間欄位提取為固定欄位。

效能最佳化的另一個面向是善用 Snowflake 的自動叢集與分割功能。雖然 VARIANT 欄位本身不能直接用於叢集鍵,但可以使用從 VARIANT 中提取的固定欄位作為叢集鍵。舉例來說如果經常根據產品類別查詢,可以將類別提取為固定欄位並設為叢集鍵,這樣可以大幅減少查詢需要掃描的資料量。

@startuml
!define DISABLE_LINK
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

package "VARIANT 資料處理架構" {
  
  component "JSON 字串輸入" as input
  component "PARSE_JSON 解析器" as parser
  component "TRY_PARSE_JSON 容錯解析器" as try_parser
  
  package "VARIANT 儲存層" {
    component "二進位樹狀結構" as tree
    component "壓縮編碼引擎" as compress
    component "統計資訊收集" as stats
  }
  
  package "查詢處理層" {
    component "路徑查詢引擎" as path
    component "型別轉換器" as cast
    component "FLATTEN 處理器" as flatten_proc
  }
  
  package "最佳化層" {
    component "查詢最佳化器" as opt
    component "統計資訊使用" as stats_use
    component "謂詞下推" as pushdown
  }
  
  database "實體儲存" as storage
}

input --> parser : 正常解析
input --> try_parser : 容錯解析

parser --> tree : 建立樹狀結構
try_parser --> tree : 建立樹狀結構
try_parser --> storage : NULL 值處理

tree --> compress : 壓縮最佳化
tree --> stats : 收集統計資訊

compress --> storage : 儲存二進位資料

storage --> path : 路徑查詢請求
path --> cast : 提取欄位值
path --> flatten_proc : 展開陣列

cast --> opt : 提供查詢結果
flatten_proc --> opt : 提供展開結果

stats --> stats_use : 提供統計資訊
stats_use --> pushdown : 最佳化查詢計畫

@enduml

這個元件圖展現了 VARIANT 資料型別的完整處理架構。JSON 字串透過解析器轉換為內部的樹狀結構,經過壓縮編碼後儲存。查詢時路徑引擎負責提取欄位值,型別轉換器確保資料以正確型別回傳,FLATTEN 處理器則負責展開陣列結構。最佳化層利用收集的統計資訊進行查詢計畫最佳化,包括謂詞下推等技術。整個架構展現了 Snowflake 如何在提供彈性的同時維持查詢效能。

表格函式的進階應用與最佳實踐

表格函式在實務應用中的場景遠比基本用法更加豐富多元。結合 Snowflake 的其他功能如 Common Table Expressions (CTEs)、視圖與 User-Defined Functions (UDFs),表格函式能夠構建出強大而優雅的資料處理管線。理解這些進階模式不僅能提升開發效率,更能充分發揮 Snowflake 平台的潛力。

階層式資料的處理是表格函式的重要應用場景。許多業務資料天生具有階層結構,舉例來說組織架構、產品分類、地理區域等。透過遞迴使用 flatten() 函式,可以將任意深度的階層結構完全攤平。以下範例展示如何處理包含多層部門結構的組織資料:

-- 建立包含階層結構的組織資料
CREATE TABLE organization_hierarchy (
    org_id NUMBER PRIMARY KEY,
    org_data VARIANT
);

INSERT INTO organization_hierarchy VALUES
(1, PARSE_JSON('{
    "company": "科技創新股份有限公司",
    "departments": [
        {
            "dept_id": "D001",
            "dept_name": "研發部",
            "manager": "張志明",
            "teams": [
                {
                    "team_id": "T001",
                    "team_name": "前端團隊",
                    "members": [
                        {"emp_id": "E001", "name": "陳小華", "position": "資深工程師"},
                        {"emp_id": "E002", "name": "林美玲", "position": "工程師"}
                    ]
                },
                {
                    "team_id": "T002",
                    "team_name": "後端團隊",
                    "members": [
                        {"emp_id": "E003", "name": "王大明", "position": "架構師"},
                        {"emp_id": "E004", "name": "李小芬", "position": "資深工程師"}
                    ]
                }
            ]
        },
        {
            "dept_id": "D002",
            "dept_name": "行銷部",
            "manager": "劉建國",
            "teams": [
                {
                    "team_id": "T003",
                    "team_name": "數位行銷",
                    "members": [
                        {"emp_id": "E005", "name": "周雅婷", "position": "行銷經理"},
                        {"emp_id": "E006", "name": "吳志豪", "position": "行銷專員"}
                    ]
                }
            ]
        }
    ]
}'));

-- 多層展開取得所有員工資訊
WITH departments AS (
    SELECT
        org_id,
        dept.value:dept_id::VARCHAR AS dept_id,
        dept.value:dept_name::VARCHAR AS dept_name,
        dept.value:manager::VARCHAR AS manager,
        dept.value:teams AS teams
    FROM
        organization_hierarchy,
        LATERAL FLATTEN(INPUT => org_data:departments) dept
),
teams AS (
    SELECT
        d.org_id,
        d.dept_id,
        d.dept_name,
        d.manager,
        team.value:team_id::VARCHAR AS team_id,
        team.value:team_name::VARCHAR AS team_name,
        team.value:members AS members
    FROM
        departments d,
        LATERAL FLATTEN(INPUT => d.teams) team
),
employees AS (
    SELECT
        t.org_id,
        t.dept_id,
        t.dept_name,
        t.manager AS dept_manager,
        t.team_id,
        t.team_name,
        member.value:emp_id::VARCHAR AS emp_id,
        member.value:name::VARCHAR AS emp_name,
        member.value:position::VARCHAR AS position
    FROM
        teams t,
        LATERAL FLATTEN(INPUT => t.members) member
)
SELECT
    dept_name AS 部門,
    team_name AS 團隊,
    emp_name AS 員工姓名,
    position AS 職位,
    dept_manager AS 部門主管
FROM
    employees
ORDER BY
    dept_id, team_id, emp_id;

這個範例展示了階層式展開的典型模式。透過串聯多個 CTE,每一層都使用 flatten() 展開下一層級的陣列結構。第一個 CTE 展開部門層級,第二個 CTE 展開團隊層級,第三個 CTE 展開員工層級。最終查詢可以取得完全攤平的員工清單,同時保留了所有層級的上下文資訊。這種模式在處理複雜的階層資料時特別有用。

表格函式與視圖的結合可以封裝複雜的資料轉換邏輯,提供簡潔的查詢介面。當某個 flatten() 操作會被頻繁使用時,可以建立視圖將其封裝起來,使用者只需查詢視圖而不需要每次都撰寫複雜的 flatten() 語法。這種做法特別適合建立資料服務層,為下游應用提供標準化的資料存取介面。

-- 建立視圖封裝 flatten 邏輯
CREATE VIEW employee_details_view AS
WITH departments AS (
    SELECT
        org_id,
        dept.value:dept_id::VARCHAR AS dept_id,
        dept.value:dept_name::VARCHAR AS dept_name,
        dept.value:manager::VARCHAR AS manager,
        dept.value:teams AS teams
    FROM
        organization_hierarchy,
        LATERAL FLATTEN(INPUT => org_data:departments) dept
),
teams AS (
    SELECT
        d.org_id,
        d.dept_id,
        d.dept_name,
        d.manager,
        team.value:team_id::VARCHAR AS team_id,
        team.value:team_name::VARCHAR AS team_name,
        team.value:members AS members
    FROM
        departments d,
        LATERAL FLATTEN(INPUT => d.teams) team
)
SELECT
    t.org_id,
    t.dept_id,
    t.dept_name,
    t.manager AS dept_manager,
    t.team_id,
    t.team_name,
    member.value:emp_id::VARCHAR AS emp_id,
    member.value:name::VARCHAR AS emp_name,
    member.value:position::VARCHAR AS position
FROM
    teams t,
    LATERAL FLATTEN(INPUT => t.members) member;

-- 使用視圖進行簡單查詢
SELECT
    dept_name,
    COUNT(*) AS employee_count
FROM
    employee_details_view
GROUP BY
    dept_name;

效能最佳化在使用表格函式時至關重要。雖然 Snowflake 的查詢最佳化器已經相當智慧,但在處理大量資料時仍需要注意一些最佳實踐。首先是盡可能在展開之前進行篩選,減少需要處理的資料量。如果只需要特定部門的員工資訊,應該在展開部門陣列之前就先篩選出目標部門。其次是注意 JOIN 的順序,先進行能夠大幅減少資料量的 JOIN,再進行 flatten() 操作。

資料品質驗證是另一個重要議題。當處理來自外部系統的 JSON 資料時,無法保證所有文件都具有一致的結構。在展開資料之前,應該先驗證必要欄位的存在性,避免在展開過程中產生大量 NULL 值或錯誤。IS_NULL_VALUE() 與 TYPEOF() 等函式可以用來檢查 VARIANT 欄位的內容型別與有效性。

-- 資料品質檢查與驗證
SELECT
    org_id,
    org_data,
    CASE
        WHEN org_data:company IS NULL THEN '缺少公司名稱'
        WHEN org_data:departments IS NULL THEN '缺少部門資料'
        WHEN TYPEOF(org_data:departments) != 'ARRAY' THEN '部門資料格式錯誤'
        WHEN ARRAY_SIZE(org_data:departments) = 0 THEN '部門陣列為空'
        ELSE '資料正常'
    END AS data_quality_check
FROM
    organization_hierarchy;

對於台灣的資料工程師而言,掌握這些進階技巧能夠大幅提升工作效率。在處理來自不同系統的異質資料時,表格函式提供了統一而強大的處理介面。透過組合使用 flatten()、CTEs、視圖與其他 SQL 功能,可以建構出既靈活又高效的資料處理管線。重點在於理解每個工具的特性與適用場景,根據實際需求選擇最適當的解決方案,而不是盲目追求最新或最複雜的技術。持續學習與實踐,並將經驗轉化為可重用的模式與最佳實踐,才能真正掌握 Snowflake 表格函式的精髓。