iT邦幫忙

2025 iThome 鐵人賽

DAY 9
2

原本我們的即時數據儀表板就像一個簡潔的咖啡店櫃台 - 只顯示今天賣了多少杯咖啡(orders)。老闆每天早上都會滿意地看著螢幕上跳動的數字,點點頭說:「嗯,生意不錯!」
但是有一天,老闆突然皺起眉頭:「等等,我想知道這些訂單裡到底賣了什麼?是拿鐵多還是美式多?大杯的比較受歡迎還是小杯的?」
這就是我們面臨的挑戰 - 從簡單的訂單數量,進化到需要訂單明細(orders_detail)的複雜分析。

重要提醒
本文所有程式碼皆為教學與概念演示用的 Pseudo Code,可以幫助你理解並解決實際場景的問題,但這裡的程式碼不能直接使用,主要目的是用來講解 Stream Processing 的設計思路與核心概念。閱讀時建議把重點放在理解整體架構和設計邏輯上,程式碼細節部分可以輕鬆看過就好。

Lambda 架構的三層設計

還記得我們之前討論的 Lambda 架構嗎?

  • Batch Layer:處理歷史全量數據,保證準確性
  • Speed Layer(加速層):處理即時數據流,保證低延遲
  • Serving Layer:合併兩層結果,對外提供查詢服務

我們之前的文章重點在加速層如何快速處理即時數據,現在面臨的問題是:當老闆需要更複雜的分析時,該在哪一層實現JOIN?

設計抉擇:加速層的純粹性

想像一下高速公路的收費站 - 如果每輛車都要停下來做複雜的檢查和登記,整個交通就會大塞車。加速層就像這個收費站,它的任務很單純:
關鍵原則:加速層不做 JOIN!
加速層只負責:簡單清洗 → 快速寫入 → 完成!

老闆打開儀表板,點擊「查看訂單詳情」。這時候,魔法發生在 Serving Layer:

SELECT
    o.order_id,
    o.customer_id,
    o.order_time,
    o.total_amount,
    od.product_name,
    od.quantity,
    od.unit_price,
    SUM(od.quantity * od.unit_price) as line_total
FROM
    orders AS o
JOIN
    orders_detail AS od ON o.order_id = od.order_id  -- JOIN 操作在這裡執行
GROUP BY
    o.order_id, od.product_name
ORDER BY
    o.order_time DESC;

真正的挑戰:OLAP 報表的複雜邏輯

這個架構設計看似簡單,但真正的考驗在於:你的 SQL 功力夠強嗎?

當老闆的需求越來越複雜時,你就會發現這條路有多崎嶇:

場景升級:老闆的進階需求

老闆說:「我要看每個產品在不同時段的銷售趨勢,還要按客戶類型分群!」

複雜 JOIN 查詢拆解

-- 複雜的多表 JOIN + 分析查詢
WITH hourly_product_sales AS (
SELECT
    od.product_name,
    DATE_FORMAT(o.order_time, '%H') as hour_of_day,
    c.customer_type,
    SUM(od.quantity) as qty_sold,
    SUM(od.quantity * od.unit_price) as revenue,
    COUNT(DISTINCT o.customer_id) as unique_customers
FROM
    orders o
JOIN
    orders_detail od ON o.order_id = od.order_id
JOIN
    customers c ON o.customer_id = c.customer_id
WHERE
    o.order_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY
    od.product_name, hour_of_day, c.customer_type
),
product_rankings AS (
SELECT
    product_name,
    hour_of_day,
    customer_type,
    qty_sold,
    revenue,
    unique_customers,
    ROW_NUMBER() OVER (
    PARTITION BY hour_of_day, customer_type
    ORDER BY revenue DESC
    ) as sales_rank
FROM hourly_product_sales
)
SELECT
    pr.product_name,
    pr.hour_of_day,
    pr.customer_type,
    pr.revenue,
    pr.sales_rank,
    ROUND(pr.revenue / SUM(pr.revenue) OVER (
    PARTITION BY pr.hour_of_day, pr.customer_type
    ) * 100, 2) as revenue_percentage
FROM
    product_rankings pr
WHERE
    pr.sales_rank <= 5
ORDER BY
    pr.hour_of_day,
    pr.customer_type,
    pr.sales_rank;

這還只是開胃菜!真正的 OLAP 報表通常包含:

  • 多層巢狀子查詢
  • 複雜的視窗函數
  • 交叉報表和樞紐分析
  • 同期比較和趨勢分析

選擇這個架構,你需要掌握:

效能調優

  • 分區表設計
  • 複合索引策略
  • 查詢執行計畫分析

總結:初期團隊的常見選擇

這個故事反映了很多團隊在實作 Lambda 架構時的常見做法:在加速層保持簡單,把複雜邏輯推遲到資料庫層處理

這種「先存後 JOIN」的策略是一把雙面劍:

優勢:

  • 加速層保持極簡,處理速度快
  • 開發門檻低,快速上線
  • 彈性擴展,各層級獨立運作

真實挑戰:

  • 對 SQL 技能要求極高
  • 複雜 OLAP 報表開發困難
  • 查詢效能調優需要深厚功力

現在老闆能看到複雜的分析結果,但背後是工程師在深夜與複雜SQL搏鬥的身影…

Day 10 預告:把 Join 搬到 Streaming

在效能調優的過程中,你很可能會發現 - - JOIN 是最消耗資料庫效能的操作之一。
數據倉庫領域有個常見解法:大寬表(Wide Table),用預先展開的方式,把查詢需要的欄位全部攤平成單張表,避免查詢時再 JOIN。
這種思路不只在離線數倉可行,在即時數據處理中也能用。如果我們能在 Streaming 流程中提前把多張表 Join 起來,直接生成即時大寬表,那麼下游查詢就能變得更快、更輕量,減少 OLAP 查詢的壓力。
下一篇,我們就來看看,如何在 Streaming 中設計即時大寬表,減少 Serving Layer 的 JOIN 開銷,並分析它帶來的好處與取捨。


上一篇
【知其然,更知其所以然】Day 8: Checkpoint 與進度保存
下一篇
【知其然,更知其所以然】Day 10:Join 太慢?Streaming 幫你提前攤平
系列文
「知其然,更知其所以然:什麼是 Real-time (Streaming) Pipeline?從造輪子到 Flink 與 RisingWave」15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言