返回文章列表

PL/SQL 與 Snowflake 進階實戰:從迴圈控制到 JSON 操作的完整技術指南

深入探討 PL/SQL 程式設計的核心技術與 Snowflake 雲端資料倉儲的進階應用,從迴圈控制、例外處理機制到預存程式設計,再到 JSON 資料操作與查詢效能分析,提供完整的實戰程式碼範例與技術解析,協助開發者掌握企業級資料庫應用開發的關鍵技能。

資料庫技術 SQL 程式設計 雲端資料倉儲

在企業級資料庫應用開發的領域中,PL/SQL 作為 Oracle 資料庫的程序化擴充語言,提供了遠超過標準 SQL 的程式設計能力。透過變數宣告、流程控制、例外處理與模組化設計,PL/SQL 讓開發者能夠在資料庫層級實作複雜的業務邏輯,避免應用程式與資料庫之間頻繁的網路往返,大幅提升系統效能。

從技術演進的角度來看,現代資料庫系統已經不再是單純的資料儲存工具,而是承載著關鍵業務邏輯的運算平台。PL/SQL 的迴圈控制機制讓開發者能夠高效處理批次資料,例外處理機制確保了程式在面對異常狀況時的穩健性,而預存程式與函式則提供了程式碼重用與維護性的保障。這些特性在金融交易、電信計費、供應鏈管理等對效能與可靠性要求極高的場景中,扮演著不可或缺的角色。

與此同時,雲端資料倉儲技術的興起為資料處理帶來了新的典範。Snowflake 作為雲端原生資料倉儲的代表,不僅提供了強大的 SQL 查詢能力,更整合了 JSON 等半結構化資料的原生支援。在現代資料應用中,結構化資料與半結構化資料的界線日益模糊,能夠靈活處理多種資料格式成為了資料平台的核心競爭力。Snowflake 的表格函式、JSON 操作函式與查詢最佳化機制,為開發者提供了處理複雜資料分析任務的強大工具集。

PL/SQL 迴圈控制機制的深入解析

迴圈控制是程序化程式設計的基礎,它讓程式能夠重複執行特定的操作,處理集合資料或實作複雜的演算法邏輯。在 PL/SQL 中,迴圈機制提供了多種形式,每種形式都有其適用的場景與特性。

計數迴圈是最直觀的迴圈形式,它透過指定起始值、結束值與步進值來控制迴圈的執行次數。這種迴圈特別適合需要執行固定次數操作的場景,例如批次產生測試資料、執行週期性的資料清理任務或實作簡單的數值計算。

在實際應用中,計數迴圈常常需要配合條件判斷來實作更靈活的控制邏輯。Break 陳述式提供了在迴圈執行過程中提前跳出的能力,讓程式能夠在滿足特定條件時立即終止迴圈,避免不必要的運算資源消耗。

-- 計數迴圈的基本結構與條件控制
-- 這個範例展示如何使用計數迴圈配合條件判斷
BEGIN
  -- FOR 迴圈從 1 迭代到 100
  -- 迴圈變數 n 會在每次迭代時自動遞增
  FOR n IN 1..100 LOOP
    -- 條件判斷:當 n 大於 60 時執行跳出邏輯
    -- 這種模式常用於在大量資料處理中設定處理上限
    IF (n > 60) THEN
      -- BREAK 陳述式立即終止迴圈
      -- 程式流程會跳轉到 END LOOP 之後的陳述式
      BREAK;
    END IF;
    
    -- 在這個區段可以執行實際的業務邏輯
    -- 例如:資料處理、數值計算或狀態更新
    -- 每次迭代時 n 的值都可以被使用
    
    -- 範例:可以在這裡插入資料到表格
    -- INSERT INTO batch_process(seq_num, process_time)
    -- VALUES (n, SYSDATE);
    
  END LOOP;
  
  -- 迴圈結束後可以執行後續處理
  -- 例如:提交交易、記錄日誌或觸發後續流程
  
END;
/

這段程式碼展示了計數迴圈的基本結構。迴圈變數 n 會從 1 開始,每次迭代後自動遞增,直到達到 100 或遇到 BREAK 陳述式。在這個範例中,當 n 超過 60 時,條件判斷會觸發 BREAK 陳述式,立即終止迴圈執行。這種模式在實務中非常常見,例如在批次處理中設定處理記錄數的上限,或在搜尋演算法中找到目標後立即停止。

除了計數迴圈,遊標迴圈是 PL/SQL 中另一個極為重要的迴圈類型。遊標提供了一種機制,讓程式能夠逐行處理查詢結果集,這在需要對每筆資料執行複雜邏輯時特別有用。

從技術實作的角度來看,遊標本質上是一個指向查詢結果集的指標。當查詢執行時,資料庫引擎會建立一個結果集,遊標則提供了逐行存取這個結果集的介面。遊標迴圈封裝了遊標的開啟、提取與關閉操作,讓開發者能夠以簡潔的語法處理複雜的資料遍歷邏輯。

-- 遊標迴圈的進階應用
-- 這個範例展示如何使用遊標處理查詢結果並實作條件控制
DECLARE
  -- 宣告遊標變數
  -- CURSOR FOR 語法定義了遊標要執行的 SELECT 查詢
  -- 這個遊標會從 NATION 表格中提取所有國家名稱
  v_cur CURSOR FOR 
    SELECT n_name 
    FROM nation
    ORDER BY n_name; -- 按照國家名稱排序,確保處理順序的可預測性
    
  -- 可以宣告額外的變數用於計數或狀態追蹤
  v_counter INTEGER := 0;
  v_target_found BOOLEAN := FALSE;
  
BEGIN
  -- FOR IN 遊標迴圈
  -- rec 是記錄變數,會自動接收遊標的每一筆資料
  -- 遊標的開啟、提取與關閉都由系統自動處理
  FOR rec IN v_cur LOOP
    -- 增加處理計數器
    -- 這在需要追蹤處理進度時很有用
    v_counter := v_counter + 1;
    
    -- 條件判斷:尋找特定的國家名稱
    -- 這種模式常用於在大量資料中搜尋特定記錄
    IF (rec.n_name = 'EGYPT') THEN
      -- 設定找到標記
      v_target_found := TRUE;
      
      -- 可以在這裡執行找到目標後的處理邏輯
      -- 例如:記錄日誌、更新狀態或觸發其他流程
      DBMS_OUTPUT.PUT_LINE('找到目標國家: ' || rec.n_name);
      DBMS_OUTPUT.PUT_LINE('處理記錄數: ' || v_counter);
      
      -- 找到目標後立即跳出迴圈
      -- 避免處理剩餘的不必要資料
      BREAK;
    END IF;
    
    -- 如果沒有找到目標,繼續處理當前記錄
    -- 可以在這裡執行對每筆資料的通用處理邏輯
    -- 例如:資料轉換、驗證或彙總計算
    
  END LOOP;
  
  -- 迴圈結束後的處理
  -- 可以根據是否找到目標執行不同的邏輯
  IF v_target_found THEN
    DBMS_OUTPUT.PUT_LINE('成功找到目標並完成處理');
  ELSE
    DBMS_OUTPUT.PUT_LINE('未找到目標,已處理所有 ' || v_counter || ' 筆記錄');
  END IF;
  
END;
/

這個範例展示了遊標迴圈在實務中的應用模式。遊標 v_cur 定義了要處理的資料集,FOR IN 迴圈則提供了簡潔的遍歷語法。記錄變數 rec 會在每次迭代時自動填充當前行的資料,開發者可以直接存取查詢選取的欄位。條件判斷與 BREAK 陳述式的組合,讓程式能夠在找到特定記錄後立即停止處理,這在處理大量資料時能夠顯著提升效能。

在選擇使用計數迴圈或遊標迴圈時,需要考慮實際的業務需求與資料特性。計數迴圈適合固定次數的重複操作,例如產生序列資料或執行固定次數的嘗試。遊標迴圈則適合處理查詢結果集,特別是當需要對每筆資料執行複雜邏輯時。在某些場景中,這兩種迴圈可以組合使用,例如在遊標迴圈內部使用計數迴圈來處理巢狀結構的資料。

從效能的角度來看,遊標迴圈需要特別注意記憶體使用與網路開銷。當處理大量資料時,建議使用 BULK COLLECT 與 FORALL 等批次處理機制,能夠大幅降低上下文切換的開銷。這些進階技巧在處理數十萬甚至數百萬筆記錄時,能夠帶來數倍甚至數十倍的效能提升。

例外處理機制的設計與實作

在企業級資料庫應用中,例外處理機制是確保系統穩健性的關鍵要素。資料庫操作可能因為各種原因而失敗,包括資料完整性約束違反、資源不足、網路中斷或業務規則驗證失敗。沒有適當的例外處理,這些錯誤可能導致交易不一致、資料損壞或系統崩潰。

PL/SQL 的例外處理機制提供了結構化的錯誤管理框架。透過宣告例外、觸發例外、捕捉例外與處理例外的完整流程,開發者能夠建構健壯且易於維護的資料庫程式。例外處理不僅能夠優雅地處理錯誤狀況,更重要的是能夠確保資源的正確釋放與交易的原子性。

在 PL/SQL 中,例外可以分為預定義例外與使用者自訂例外。預定義例外由 Oracle 系統自動觸發,對應常見的資料庫錯誤,如 NO_DATA_FOUND、TOO_MANY_ROWS 或 DUP_VAL_ON_INDEX。使用者自訂例外則讓開發者能夠定義業務相關的錯誤狀況,實作更精確的錯誤處理邏輯。

-- 完整的例外處理機制範例
-- 展示例外的宣告、觸發、捕捉與重新拋出
DECLARE
  -- 宣告使用者自訂例外
  -- EXCEPTION 關鍵字定義一個例外變數
  -- 使用 PRAGMA EXCEPTION_INIT 將例外與特定錯誤碼關聯
  -- -20200 是使用者自訂錯誤碼的範圍(-20000 到 -20999)
  e_my_exception EXCEPTION;
  PRAGMA EXCEPTION_INIT(e_my_exception, -20200);
  
  -- 宣告其他可能需要的變數
  v_error_message VARCHAR2(200);
  v_error_code NUMBER;
  
BEGIN
  -- 主要程式邏輯區段
  -- 在這裡執行正常的業務處理
  
  -- 模擬業務規則驗證
  -- 在實際應用中,這裡會是複雜的業務邏輯判斷
  DECLARE
    v_balance NUMBER := -100; -- 假設餘額為負值
  BEGIN
    -- 業務規則:餘額不能為負
    IF v_balance < 0 THEN
      -- 使用 RAISE_APPLICATION_ERROR 觸發自訂例外
      -- 第一個參數是錯誤碼(-20000 到 -20999)
      -- 第二個參數是錯誤訊息
      RAISE_APPLICATION_ERROR(-20200, 
        '業務規則違反:帳戶餘額不能為負值,當前餘額: ' || v_balance);
    END IF;
  END;
  
  -- 如果沒有觸發例外,繼續執行正常流程
  DBMS_OUTPUT.PUT_LINE('業務處理成功完成');
  
EXCEPTION
  -- 例外處理區段
  -- 這個區段會捕捉在 BEGIN 區段中觸發的例外
  
  -- 捕捉特定的使用者自訂例外
  WHEN e_my_exception THEN
    -- 記錄錯誤資訊
    -- 在實務中通常會寫入錯誤日誌表格
    v_error_code := SQLCODE;
    v_error_message := SQLERRM;
    
    DBMS_OUTPUT.PUT_LINE('捕捉到自訂例外');
    DBMS_OUTPUT.PUT_LINE('錯誤碼: ' || v_error_code);
    DBMS_OUTPUT.PUT_LINE('錯誤訊息: ' || v_error_message);
    
    -- 可以在這裡執行清理操作
    -- 例如:回滾部分交易、釋放資源或發送告警
    
    -- 重新拋出例外
    -- RAISE 不帶參數時會重新拋出當前捕捉的例外
    -- 這允許外層程式碼繼續處理這個例外
    RAISE;
    
  -- 捕捉預定義例外
  WHEN NO_DATA_FOUND THEN
    DBMS_OUTPUT.PUT_LINE('查詢未返回任何資料');
    -- 根據業務需求決定是否重新拋出
    
  WHEN TOO_MANY_ROWS THEN
    DBMS_OUTPUT.PUT_LINE('查詢返回多筆資料,預期只有一筆');
    RAISE; -- 重新拋出,讓外層處理
    
  -- 捕捉所有其他例外
  WHEN OTHERS THEN
    -- OTHERS 必須是最後一個例外處理器
    v_error_code := SQLCODE;
    v_error_message := SQLERRM;
    
    DBMS_OUTPUT.PUT_LINE('發生未預期的錯誤');
    DBMS_OUTPUT.PUT_LINE('錯誤碼: ' || v_error_code);
    DBMS_OUTPUT.PUT_LINE('錯誤訊息: ' || v_error_message);
    
    -- 在捕捉到未預期錯誤時,通常需要記錄詳細資訊
    -- 並重新拋出例外,避免隱藏問題
    RAISE;
    
END;
/

這個完整的例外處理範例展示了從例外宣告到處理的完整流程。使用者自訂例外讓開發者能夠為業務規則驗證定義明確的錯誤類型。RAISE_APPLICATION_ERROR 函式提供了觸發自訂例外的標準方式,能夠設定錯誤碼與錯誤訊息。在例外處理區段,不同類型的例外可以被分別捕捉與處理,讓程式能夠根據錯誤類型執行不同的恢復邏輯。

例外的重新拋出是一個重要的設計模式。在某些情況下,程式需要在捕捉例外後執行部分清理操作,但仍然希望讓外層程式碼知道錯誤的發生。RAISE 陳述式不帶參數時,會重新拋出當前捕捉的例外,保留原始的錯誤資訊與堆疊追蹤,這對於錯誤診斷非常重要。

在設計例外處理策略時,需要考慮錯誤的嚴重程度與恢復可能性。對於可恢復的錯誤,如暫時性的資源不足,程式可能需要實作重試邏輯。對於不可恢復的錯誤,如資料完整性約束違反,應該確保交易完整回滾並記錄詳細的錯誤資訊。良好的例外處理設計能夠在錯誤發生時提供清晰的診斷資訊,大幅縮短問題排查的時間。

預存程式的設計模式與最佳實踐

預存程式是 PL/SQL 程式設計中實現程式碼模組化與重用的核心機制。相較於在應用程式層級實作業務邏輯,將邏輯封裝在預存程式中能夠帶來多方面的優勢,包括效能提升、網路開銷降低、安全性增強與維護性改善。

從架構設計的角度來看,預存程式提供了一個清晰的抽象層,將資料存取邏輯與業務邏輯從應用程式中分離。應用程式只需要呼叫預存程式並傳遞參數,而不需要關心底層的 SQL 操作細節。這種分離不僅簡化了應用程式的複雜度,更重要的是讓資料庫管理員能夠獨立最佳化資料存取路徑,而不影響應用程式程式碼。

在效能方面,預存程式的優勢主要來自兩個方面。首先,預存程式在首次執行時會被編譯並快取在資料庫中,後續呼叫可以直接使用編譯後的執行計畫,避免了重複的語法分析與最佳化開銷。其次,預存程式能夠在資料庫伺服器端執行複雜的資料處理邏輯,大幅減少應用程式與資料庫之間的網路往返次數。

-- 簡單的純量函式預存程式
-- 這個範例展示如何封裝常用的數學計算
CREATE OR REPLACE PROCEDURE rectangle_area(
  -- 輸入參數:矩形的寬度
  -- NUMBER 類型可以處理整數與浮點數
  p_width IN NUMBER,
  
  -- 輸入參數:矩形的長度
  p_length IN NUMBER,
  
  -- 輸出參數:計算得到的面積
  -- OUT 關鍵字表示這是輸出參數
  p_area OUT NUMBER
)
-- LANGUAGE SQL 指定預存程式使用 SQL 語言實作
-- 也可以使用 LANGUAGE PLSQL 來實作更複雜的邏輯
AS
BEGIN
  -- 執行面積計算
  -- 簡單的乘法運算
  p_area := p_width * p_length;
  
  -- 可以在這裡添加驗證邏輯
  -- 例如:檢查輸入值是否為正數
  IF p_width <= 0 OR p_length <= 0 THEN
    RAISE_APPLICATION_ERROR(-20001, 
      '矩形的寬度與長度必須為正數');
  END IF;
  
  -- 可以記錄計算日誌
  -- INSERT INTO calculation_log(calc_type, input1, input2, result, calc_time)
  -- VALUES ('AREA', p_width, p_length, p_area, SYSDATE);
  
END rectangle_area;
/

-- 呼叫預存程式的範例
DECLARE
  v_width NUMBER := 10;
  v_length NUMBER := 20;
  v_result NUMBER;
BEGIN
  -- 呼叫預存程式
  rectangle_area(v_width, v_length, v_result);
  
  -- 使用回傳結果
  DBMS_OUTPUT.PUT_LINE('矩形面積: ' || v_result);
END;
/

這個簡單的預存程式範例展示了基本的參數傳遞與計算邏輯。輸入參數使用 IN 關鍵字標記,表示這些參數的值由呼叫者提供。輸出參數使用 OUT 關鍵字標記,預存程式會透過這些參數將結果傳回給呼叫者。這種參數傳遞機制讓預存程式能夠回傳多個值,比起函式只能回傳單一值更加靈活。

對於需要回傳資料集的場景,PL/SQL 提供了回傳表格的預存程式設計模式。這種模式特別適合實作複雜的查詢邏輯,將資料檢索封裝在預存程式中。

-- 回傳表格的預存程式
-- 這個範例展示如何根據條件篩選資料並回傳結果集
CREATE OR REPLACE PROCEDURE get_parts_by_type(
  -- 輸入參數:零件類型的篩選條件
  p_type_name IN VARCHAR2,
  
  -- 輸出參數:結果集遊標
  -- SYS_REFCURSOR 是 Oracle 提供的內建遊標類型
  p_result OUT SYS_REFCURSOR
)
AS
BEGIN
  -- 開啟遊標並指定查詢
  -- 查詢會從 PART 表格中篩選符合條件的記錄
  OPEN p_result FOR
    SELECT 
      p_partkey,  -- 零件主鍵
      p_name,     -- 零件名稱
      p_type,     -- 零件類型
      p_size,     -- 零件尺寸
      p_brand     -- 零件品牌
    FROM part
    WHERE p_type = p_type_name
    ORDER BY p_partkey; -- 按照主鍵排序,確保結果的一致性
    
  -- 遊標會保持開啟狀態
  -- 呼叫者負責提取資料並關閉遊標
  
  -- 可以在這裡添加審計日誌
  -- INSERT INTO query_log(proc_name, param_value, exec_time)
  -- VALUES ('get_parts_by_type', p_type_name, SYSDATE);
  
END get_parts_by_type;
/

-- 呼叫回傳表格的預存程式
DECLARE
  -- 宣告遊標變數接收結果
  v_cursor SYS_REFCURSOR;
  
  -- 宣告記錄變數儲存每一筆資料
  v_partkey NUMBER;
  v_name VARCHAR2(100);
  v_type VARCHAR2(50);
  v_size NUMBER;
  v_brand VARCHAR2(50);
  
BEGIN
  -- 呼叫預存程式
  get_parts_by_type('ECONOMY POLISHED STEEL', v_cursor);
  
  -- 迴圈提取遊標資料
  LOOP
    FETCH v_cursor INTO v_partkey, v_name, v_type, v_size, v_brand;
    EXIT WHEN v_cursor%NOTFOUND; -- 沒有更多資料時跳出迴圈
    
    -- 處理每一筆資料
    DBMS_OUTPUT.PUT_LINE('零件: ' || v_partkey || ' - ' || v_name);
  END LOOP;
  
  -- 關閉遊標釋放資源
  CLOSE v_cursor;
  
END;
/

這個範例展示了如何設計回傳資料集的預存程式。SYS_REFCURSOR 類型提供了一種標準化的方式來回傳查詢結果,呼叫者可以像處理普通遊標一樣提取資料。這種設計模式在實務中非常常見,特別是在需要根據複雜條件篩選資料時,將查詢邏輯封裝在預存程式中能夠大幅簡化應用程式的程式碼。

在設計預存程式時,參數命名規範是一個重要的考量。建議使用有意義的參數名稱,並加上前綴來區分參數類型,例如使用 p_ 前綴表示參數、v_ 前綴表示局部變數。清晰的命名規範能夠大幅提升程式碼的可讀性與可維護性。

使用者定義函式的實作與應用

使用者定義函式在功能上與預存程式類似,但有一個關鍵差異,函式必須回傳一個值,且可以直接在 SQL 陳述式中使用。這個特性讓函式成為擴充 SQL 功能的強大工具,開發者可以建立自訂的計算邏輯、資料轉換規則或複雜的業務規則驗證。

純量函式回傳單一值,可以在 SELECT 子句、WHERE 條件或任何需要運算式的地方使用。這種函式特別適合封裝可重用的計算邏輯,例如特殊的數學公式、字串處理規則或日期計算。

-- 純量函式的實作範例
-- 這個函式計算圓形的面積
CREATE OR REPLACE FUNCTION fn_circle_area(
  -- 輸入參數:圓的半徑
  p_radius IN NUMBER
)
-- 函式的回傳值類型
RETURN NUMBER
-- 指定函式使用 SQL 語言實作
-- 這允許函式被內嵌到 SQL 查詢中
AS
BEGIN
  -- 檢查輸入值的有效性
  -- 半徑必須為非負數
  IF p_radius < 0 THEN
    RAISE_APPLICATION_ERROR(-20002, 
      '半徑不能為負數,輸入值: ' || p_radius);
  END IF;
  
  -- 計算圓面積
  -- 使用公式: Area = π × r²
  -- POWER 函式計算平方
  RETURN 3.14159265359 * POWER(p_radius, 2);
  
  -- 也可以使用 Oracle 內建的 PI 常數(如果可用)
  -- RETURN ACOS(-1) * POWER(p_radius, 2);
  
END fn_circle_area;
/

-- 在 SQL 查詢中使用函式
-- 函式可以像內建函式一樣使用
SELECT 
  fn_circle_area(5) AS small_circle,
  fn_circle_area(10) AS medium_circle,
  fn_circle_area(15) AS large_circle
FROM DUAL;

-- 在複雜查詢中使用函式
SELECT 
  product_id,
  radius,
  fn_circle_area(radius) AS calculated_area,
  stored_area,
  -- 計算面積差異百分比
  ABS(fn_circle_area(radius) - stored_area) / stored_area * 100 AS area_diff_pct
FROM circular_products
WHERE ABS(fn_circle_area(radius) - stored_area) > 0.01;

這個純量函式範例展示了如何將數學計算封裝為可重用的函式。函式可以在任何 SQL 陳述式中使用,就像使用內建函式一樣自然。輸入驗證確保了函式的健壯性,錯誤處理機制讓函式在遇到無效輸入時能夠提供清晰的錯誤訊息。

表格函式是另一種重要的函式類型,它回傳一個資料集而非單一值。表格函式在 FROM 子句中使用,可以像查詢普通表格一樣查詢函式的回傳結果。這種函式特別適合實作複雜的資料轉換邏輯或動態產生資料集。

-- 表格函式的實作範例
-- 這個函式根據零件類型篩選資料並回傳結果集
CREATE OR REPLACE FUNCTION fn_get_parts_by_type(
  -- 輸入參數:零件類型篩選條件
  p_type_name IN VARCHAR2
)
-- 函式回傳一個表格
-- 定義回傳表格的結構
RETURN TABLE (
  p_partkey NUMBER,
  p_name VARCHAR2(100),
  p_type VARCHAR2(50),
  p_size NUMBER,
  p_retailprice NUMBER
)
AS
BEGIN
  -- 使用 RETURN QUERY 回傳查詢結果
  -- 這個語法讓函式可以直接回傳 SELECT 的結果
  RETURN QUERY
    SELECT 
      p.p_partkey,
      p.p_name,
      p.p_type,
      p.p_size,
      p.p_retailprice
    FROM part p
    WHERE p.p_type = p_type_name
    -- 可以添加額外的過濾條件
    AND p.p_size > 0
    -- 按照零售價格排序
    ORDER BY p.p_retailprice DESC;
    
  -- 如果沒有找到符合條件的資料
  -- 函式會回傳空的結果集
  -- 這是正常行為,不需要觸發例外
  
END fn_get_parts_by_type;
/

-- 在 SQL 查詢中使用表格函式
-- TABLE 關鍵字告訴資料庫這是一個表格函式
SELECT *
FROM TABLE(fn_get_parts_by_type('STANDARD POLISHED STEEL'))
WHERE p_size >= 10;

-- 將表格函式與其他表格 JOIN
SELECT 
  f.p_partkey,
  f.p_name,
  s.s_name AS supplier_name,
  ps.ps_supplycost
FROM TABLE(fn_get_parts_by_type('ECONOMY BRUSHED COPPER')) f
JOIN partsupp ps ON f.p_partkey = ps.ps_partkey
JOIN supplier s ON ps.ps_suppkey = s.s_suppkey
WHERE ps.ps_availqty > 100
ORDER BY ps.ps_supplycost;

表格函式提供了強大的資料處理能力,它可以封裝複雜的查詢邏輯,包括多表 JOIN、子查詢與聚合計算。函式的回傳結果可以像普通表格一樣參與 JOIN、WHERE 過濾與排序操作,這讓函式成為建構可重用查詢元件的理想選擇。

在效能方面,函式的使用需要特別注意。當函式在查詢中對大量資料列執行時,可能會產生顯著的效能開銷。對於需要在大型表格上使用的函式,建議考慮使用確定性函式與結果快取機制。Oracle 提供了 DETERMINISTIC 關鍵字,標記函式對相同輸入始終回傳相同結果,這允許資料庫引擎快取函式的計算結果。

Snowflake 資料倉儲的進階查詢技術

Snowflake 作為現代雲端資料倉儲的代表,提供了豐富的 SQL 功能與效能最佳化機制。相較於傳統的關聯式資料庫,Snowflake 的架構設計專注於大規模資料分析場景,支援彈性的運算資源擴展與高效的查詢並行處理。

在 Snowflake 中,表格函式的使用模式與 Oracle 類似,但語法上有一些差異。Snowflake 的 TABLE 函式語法讓查詢能夠直接使用函式回傳的結果集,這在實作動態查詢邏輯時特別有用。

-- 在 Snowflake 中使用表格函式
-- 這個查詢展示如何呼叫自訂的表格函式並處理結果
SELECT *
FROM TABLE(fn_get_parts_by_type('SMALL PLATED NICKEL'))
-- 回傳結果包含符合條件的所有零件資訊
-- 每一列代表一個零件記錄
-- 包含零件主鍵(P_PARTKEY)與零件名稱(P_NAME)等欄位
ORDER BY p_partkey;

-- 查詢說明:
-- fn_get_parts_by_type 是一個使用者自訂的表格函式
-- 它接受零件類型作為輸入參數
-- 回傳所有符合該類型的零件資料
-- TABLE 關鍵字將函式的回傳值轉換為可查詢的結果集

這個簡單的範例展示了表格函式在 Snowflake 中的基本用法。函式封裝了資料篩選邏輯,讓查詢程式碼更加簡潔。當需要修改篩選邏輯時,只需要更新函式定義,而不需要修改所有使用該邏輯的查詢。

Snowflake 支援更複雜的查詢模式,包括在 CROSS JOIN 中使用表格函式。這種模式在需要為每筆輸入資料呼叫函式時特別有用,例如動態展開資料或實作參數化查詢。

-- 使用子查詢與 CROSS JOIN 的進階查詢模式
-- 這個查詢展示如何為多個不同的參數值呼叫表格函式
SELECT 
  gpt.p_partkey,
  gpt.p_name,
  gpt.p_type,
  p.p_type AS original_type
FROM 
  -- 子查詢:取得所有符合模式的不重複零件類型
  (
    SELECT DISTINCT p_type
    FROM part
    -- LIKE 運算子支援模式匹配
    -- % 萬用字元匹配任意字元序列
    WHERE p_type LIKE 'STANDARD % STEEL'
  ) p
  -- CROSS JOIN 為每個類型值呼叫函式
  -- 這會產生笛卡兒積,但在這個場景中是預期行為
  CROSS JOIN TABLE(fn_get_parts_by_type(p.p_type)) AS gpt
-- 結果集包含所有符合模式的類型及其對應的零件資料
ORDER BY 
  p.p_type,
  gpt.p_partkey;

-- 查詢邏輯說明:
-- 1. 內層子查詢找出所有符合 'STANDARD % STEEL' 模式的零件類型
-- 2. 對每個找到的類型,呼叫 fn_get_parts_by_type 函式
-- 3. CROSS JOIN 將子查詢的每一列與函式回傳的結果組合
-- 4. 最終結果包含所有類型及其對應的零件詳細資訊

-- 效能考量:
-- 這種查詢模式可能產生大量的函式呼叫
-- 當類型數量很多時,建議評估查詢效能
-- 考慮使用 JOIN 替代方案或最佳化函式內部邏輯

這個進階查詢範例展示了如何組合子查詢、CROSS JOIN 與表格函式來實作複雜的資料處理邏輯。子查詢首先篩選出符合特定模式的所有類型值,然後 CROSS JOIN 為每個類型值呼叫表格函式,將結果組合成一個完整的資料集。這種模式在需要為動態參數集合執行相同操作時非常有用。

在實務應用中,這種查詢模式需要謹慎使用。當參數值數量很大時,CROSS JOIN 會導致大量的函式呼叫,可能影響查詢效能。在這種情況下,建議評估是否能夠透過調整資料模型或使用其他查詢技術來達到相同目的。

Snowflake 的查詢歷史與效能分析

在資料倉儲環境中,查詢效能分析是持續最佳化的基礎。Snowflake 提供了完整的查詢歷史記錄功能,讓開發者與資料庫管理員能夠追蹤每一個查詢的執行狀況,包括執行時間、資源消耗與錯誤訊息。

查詢歷史記錄透過 INFORMATION_SCHEMA.QUERY_HISTORY 表格函式提供,這個函式回傳最近執行的查詢清單,包含豐富的後設資料資訊。透過分析這些資訊,可以識別效能瓶頸、追蹤問題查詢或監控系統使用狀況。

-- 執行一個簡單的測試查詢
-- 這個查詢會被記錄在查詢歷史中
SELECT 23 * 75 AS result;
-- 結果: 1725

-- 從查詢歷史中尋找剛才執行的查詢
-- INFORMATION_SCHEMA.QUERY_HISTORY 提供查詢歷史資訊
SELECT 
  query_id,                          -- 查詢的唯一識別碼
  query_text,                        -- 完整的查詢文字
  user_name,                         -- 執行查詢的使用者
  warehouse_name,                    -- 使用的虛擬倉儲
  warehouse_size,                    -- 虛擬倉儲的大小
  execution_status,                  -- 執行狀態(SUCCESS, FAILED 等)
  execution_time,                    -- 執行時間(毫秒)
  total_elapsed_time,                -- 總耗時(毫秒)
  bytes_scanned,                     -- 掃描的資料量(位元組)
  rows_produced,                     -- 產生的資料列數
  compilation_time,                  -- 編譯時間(毫秒)
  start_time,                        -- 查詢開始時間
  end_time                           -- 查詢結束時間
FROM TABLE(INFORMATION_SCHEMA.QUERY_HISTORY(
  -- result_limit 參數限制回傳的記錄數
  -- 10 表示只回傳最近的 10 筆查詢
  RESULT_LIMIT => 10
))
-- 使用 WHERE 子句篩選特定的查詢
-- LIKE 運算子支援模糊匹配
WHERE query_text LIKE '%75%'
-- 按照開始時間排序,最近的查詢排在前面
ORDER BY start_time DESC;

-- 查詢結果說明:
-- query_id 可用於進一步的查詢分析
-- 例如查看查詢的詳細執行計畫
-- 或在 Snowflake Web UI 中定位特定查詢

-- 效能分析要點:
-- execution_time: 實際執行時間,用於評估查詢效率
-- bytes_scanned: 掃描的資料量,越小表示越有效率
-- compilation_time: 編譯時間,反映查詢複雜度

這個查詢範例展示了如何使用 INFORMATION_SCHEMA.QUERY_HISTORY 追蹤特定的查詢。透過篩選查詢文字,可以快速找到目標查詢的 query_id,這個 ID 可以用於進一步的分析,例如查看詳細的執行計畫或在 Snowflake Web UI 中查看視覺化的查詢統計資訊。

在實務中,查詢歷史分析通常用於識別效能問題。透過比較相似查詢的執行時間與資源消耗,可以評估索引、分區或查詢重寫的效果。定期檢查慢查詢清單,能夠及早發現效能退化的趨勢,在問題影響使用者之前採取最佳化措施。

-- 進階的查詢歷史分析範例
-- 這個查詢找出最消耗資源的查詢
SELECT 
  query_id,
  SUBSTR(query_text, 1, 100) AS query_preview,
  user_name,
  warehouse_name,
  execution_time / 1000.0 AS execution_seconds,
  bytes_scanned / (1024.0 * 1024.0 * 1024.0) AS gb_scanned,
  rows_produced,
  -- 計算每秒處理的資料列數
  CASE 
    WHEN execution_time > 0 
    THEN rows_produced / (execution_time / 1000.0)
    ELSE 0
  END AS rows_per_second,
  start_time
FROM TABLE(INFORMATION_SCHEMA.QUERY_HISTORY(
  -- 查看最近 1000 筆查詢
  RESULT_LIMIT => 1000,
  -- 可以指定時間範圍
  -- END_TIME_RANGE_START => DATEADD(hour, -24, CURRENT_TIMESTAMP()),
  -- END_TIME_RANGE_END => CURRENT_TIMESTAMP()
))
-- 只看成功執行的查詢
WHERE execution_status = 'SUCCESS'
  -- 排除元資料查詢
  AND query_type != 'SHOW'
  AND query_type != 'DESCRIBE'
  -- 只看執行時間超過 10 秒的查詢
  AND execution_time > 10000
-- 按照執行時間降序排列
ORDER BY execution_time DESC
-- 只看前 20 個最慢的查詢
LIMIT 20;

-- 分析說明:
-- 這個查詢幫助識別效能瓶頸
-- execution_seconds: 轉換為秒單位更易閱讀
-- gb_scanned: 轉換為 GB 單位評估 I/O 負載
-- rows_per_second: 評估查詢處理效率的指標

這個進階分析範例展示了如何識別系統中最消耗資源的查詢。透過計算執行時間、掃描資料量與處理速率等指標,可以建立效能基準線,並追蹤最佳化工作的成效。定期執行這類分析,結合業務知識判斷哪些慢查詢值得優先最佳化,能夠有效提升系統的整體效能。

JSON 資料操作與半結構化資料處理

在現代資料應用中,JSON 已成為資料交換的標準格式。Snowflake 原生支援 JSON 資料類型,提供了豐富的函式來處理半結構化資料。這種支援讓開發者能夠在同一個查詢中混合處理結構化與半結構化資料,大幅簡化了資料整合的複雜度。

Snowflake 的 OBJECT_CONSTRUCT 與 ARRAY_AGG 函式提供了將查詢結果轉換為 JSON 格式的能力。這在需要產生 API 回應、匯出資料或實作資料交換介面時特別有用。

-- 將查詢結果轉換為 JSON 格式
-- 這個範例展示如何建構巢狀的 JSON 結構
SELECT 
  -- OBJECT_CONSTRUCT 建立 JSON 物件
  -- 第一個參數是鍵名,第二個參數是值
  OBJECT_CONSTRUCT(
    'Parts',
    -- ARRAY_AGG 將多筆記錄聚合為 JSON 陣列
    ARRAY_AGG(
      -- 為每筆記錄建立 JSON 物件
      OBJECT_CONSTRUCT(
        'Partkey', p_partkey,
        'Name', p_name,
        'Brand', p_brand,
        'Size', p_size,
        'RetailPrice', p_retailprice
      )
    )
  ) AS part_doc
FROM part
WHERE p_mfgr = 'Manufacturer#1'
  AND p_type = 'ECONOMY POLISHED STEEL'
-- 限制結果數量避免產生過大的 JSON
LIMIT 100;

-- 結果格式說明:
-- 回傳單一列,包含一個 JSON 物件
-- JSON 結構:
-- {
--   "Parts": [
--     {
--       "Partkey": 95608,
--       "Name": "royal thistle floral frosted midnight",
--       "Brand": "Brand#12",
--       "Size": 25,
--       "RetailPrice": 1403.42
--     },
--     ...
--   ]
-- }

-- 應用場景:
-- 1. 產生 RESTful API 回應
-- 2. 資料匯出與交換
-- 3. 建立文件資料庫的輸入資料

這個 JSON 轉換範例展示了如何將關聯式資料轉換為階層式的 JSON 結構。OBJECT_CONSTRUCT 函式接受成對的鍵值參數,建立 JSON 物件。ARRAY_AGG 聚合函式將多筆記錄的 JSON 物件組合成陣列。這兩個函式的組合讓開發者能夠建構任意複雜的 JSON 結構,滿足各種資料格式需求。

反向操作,即將 JSON 資料匯入表格,也是常見的需求。Snowflake 的 PARSE_JSON 函式能夠解析 JSON 字串,結合 INSERT 陳述式可以將 JSON 資料儲存到表格中。

-- 建立用於儲存 JSON 資料的表格
CREATE TABLE IF NOT EXISTS my_docs (
  doc_id INTEGER AUTOINCREMENT,
  doc_data VARIANT,           -- VARIANT 類型可儲存任意 JSON 結構
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 將 JSON 資料插入表格
INSERT INTO my_docs (doc_data)
SELECT PARSE_JSON(
  -- JSON 字串包含零件資料
  -- 在實務中通常從檔案或 API 回應讀取
  '{
    "Parts": [
      {
        "Brand": "Brand#12",
        "Name": "royal thistle floral frosted midnight",
        "Partkey": 95608,
        "Size": 25,
        "RetailPrice": 1403.42
      },
      {
        "Brand": "Brand#11",
        "Name": "azure honeydew grey aquamarine black",
        "Partkey": 100308,
        "Size": 31,
        "RetailPrice": 1533.71
      },
      {
        "Brand": "Brand#13",
        "Name": "steel lemon tomato brown blush",
        "Partkey": 103808,
        "Size": 49,
        "RetailPrice": 1709.60
      }
    ],
    "Manufacturer": "Manufacturer#1",
    "Type": "ECONOMY POLISHED STEEL",
    "ImportDate": "2025-11-20"
  }'
) AS parsed_json;

-- 查詢 JSON 資料
-- Snowflake 支援使用點記號法存取 JSON 欄位
SELECT 
  doc_id,
  doc_data:Manufacturer::STRING AS manufacturer,
  doc_data:Type::STRING AS part_type,
  doc_data:ImportDate::STRING AS import_date,
  -- 存取陣列元素
  parts.value:Partkey::INTEGER AS partkey,
  parts.value:Name::STRING AS part_name,
  parts.value:Brand::STRING AS brand,
  parts.value:RetailPrice::FLOAT AS retail_price
FROM my_docs,
  -- LATERAL FLATTEN 展開 JSON 陣列
  LATERAL FLATTEN(input => doc_data:Parts) parts
WHERE doc_data:Manufacturer::STRING = 'Manufacturer#1'
ORDER BY parts.value:RetailPrice::FLOAT DESC;

-- 查詢說明:
-- :: 運算子將 VARIANT 值轉換為特定類型
-- LATERAL FLATTEN 將 JSON 陣列展開為多筆記錄
-- 每個陣列元素產生一列結果
-- 這讓 JSON 資料能像關聯式資料一樣查詢

這個完整的 JSON 處理範例展示了從資料匯入到查詢的完整流程。VARIANT 資料類型是 Snowflake 處理半結構化資料的核心,它能夠儲存任意複雜的 JSON、XML 或 Avro 資料。點記號法與型別轉換運算子讓查詢 JSON 資料變得直觀,LATERAL FLATTEN 則提供了處理巢狀結構與陣列的強大能力。

在實務應用中,JSON 資料處理常見於整合異質資料源、處理 API 回應或儲存彈性結構的資料。Snowflake 的半結構化資料支援讓開發者能夠在資料倉儲中統一處理多種格式的資料,避免複雜的 ETL 轉換流程,加速資料分析的價值實現。

PL/SQL 與 Snowflake 代表了資料庫程式設計領域的不同面向。PL/SQL 提供了傳統關聯式資料庫的強大程序化能力,適合建構複雜的交易處理系統與業務邏輯。Snowflake 則專注於大規模資料分析場景,提供彈性的運算資源與豐富的資料格式支援。掌握這兩個平台的核心技術,能夠讓開發者在不同的應用場景中選擇最適合的工具,建構高效能且可維護的資料庫應用系統。從迴圈控制到例外處理,從預存程式到 JSON 操作,這些技術的深入理解與靈活應用,是成為資料庫專家的必經之路。