導讀:
StarRocks 4.0 已正式發佈!這一版本帶來了多項關鍵升級。本篇聚焦 JSON 查詢性能的系統性提升——通過全新的 FlatJSON 列式存儲與執行優化機制,StarRocks 4.0 讓 JSON 在實時分析場景中具備接近原生列存的性能。
無論是日誌、埋點還是 IoT 數據,用户都無需額外 ETL,即可直接對 JSON 進行高性能查詢分析,真正讓“靈活的數據結構”與“高效的分析能力”兼得。
在實時分析場景中,日誌、點擊流、埋點、用户畫像等數據幾乎無處不在。這些數據通常以 JSON 格式存儲——它靈活、通用、無需建模,尤其適合快速變化的業務場景:字段可以隨時新增或刪除,系統之間也能無障礙傳輸。
正因如此,JSON 成為互聯網業務中最常見的數據格式。然而,當這種靈活的數據進入數據庫,情況就截然不同了。
在日誌分析或行為分析場景中,即便 SQL 寫得沒問題,查詢仍可能遲遲跑不出來。
SELECT
get_json_string(event, '$.type') AS event_type,
COUNT(DISTINCT user_id)
FROM events_log
WHERE
get_json_string(event, '$.region') = 'US' AND
to_datetime(get_json_int(dt, '$.event_ts')) BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY event_type;
這條簡單的 SQL 為什麼這麼慢?因為在數據庫眼裏,JSON 就是一塊黑盒:
- 存儲層需要將每一行的完整 JSON 讀入內存;
- 即便 SQL 只訪問其中少數字段,也必須讀取整個對象;
- 過濾條件無法利用索引,只能全表掃描;
- 基於字符串的計算代價高,無法使用字典編碼等優化手段。
看似只是過濾 region='US'、分組 event_type 並做一次去重計數,但執行時間可能長達幾十秒。CPU 飆升、延遲高企——這並非 SQL 寫得複雜,而是因為 JSON 最初並非為分析而設計的。
當 JSON 成為分析的基礎時,它的優勢很快就會變成劣勢:
- 存儲臃腫:字段名和值反覆存儲,空間佔用大,壓縮效果差;
- 查詢開銷大:字段讀取操作需要遍歷和搜索 JSON,CPU 消耗高
- Schema 變化快:字段隨時增減,歷史數據不一致,建模困難;
- 類型不統一:同一字段在不同記錄裏類型不同,增加查詢複雜度;
- 字段數量多:單條 JSON 常常包含上百字段,但查詢通常只關心少數字段。
數據庫的幾種應對方案
多年來,工程師一直在嘗試各種解決思路,大體可以分為三類:
- 二進制序列化:典型代表是 PostgreSQL 的 JSONB。這種方式在寫入時就解析 JSON 字符串,並將其序列化為二進制格式,以減少查詢階段的字符串解析開銷。這類方案能夠一定程度上優化性能,但依然不適合 OLAP 場景,StarRocks 的早期版本也採用了類似思路。
- 用户手工抽取:傳統數倉更傾向於讓用户在 ETL 階段手動將 JSON 字段展開成獨立列。這種做法能獲得接近列存的性能,但代價是維護複雜。在 StarRocks 中,用户也可以通過 Generated Column(生成列) 來實現類似能力,比如:
ALTER TABLE tbl ADD COLUMN json_event_type STRING AS get_json_string(event, '$.event_type') - 自動列化存儲:理想的方向是讓系統自動識別並抽取常用字段,實現“對用户無感知”的列式化存儲。
不過,這種方案的實現極具挑戰——系統需要智能地處理異構 schema、 schema 演進等複雜問題。因此,各數據庫產品在這一方向的優化深度差異很大。
從 Binary JSON 到接近列存的 JSON
為了解決這些問題,StarRocks 在 2.3 版本支持了 Binary JSON,即以二進制序列化格式來存儲 JSON,規避了每次解析字符串的開銷,並且搜索字段時能夠應用二分查找來降低 CPU 開銷。但是即便如此,總體仍然是非常低效,以文首查詢為例:
- 系統無法利用索引過濾,需要讀取所有 JSON 數據,帶來巨大的 I/O 放大
- 每一行都要多次
get_json_*,解析region、event_type、user_id - 過濾條件
region='US'需要逐字符比較,CPU 熱點函數可能 80-90% 都耗在解析和比較上 - 聚合階段又要基於字符串做哈希和去重,無法利用字典編碼進行優化
哪怕只有幾千萬行數據,這樣的查詢依舊可能需要幾十秒。
JSON 能力的持續演進
在持續優化 JSON 查詢性能的過程中,StarRocks 逐步實現了從“字符串存儲”到“列式存儲”的演進:
- 2.3 之前:不支持原生 JSON,僅能以字符串形式存儲;
- 2.3 版本:引入 JSON 類型,採用二進制序列化格式,並補充豐富的 JSON 查詢函數;
- 3.3 版本:推出 FlatJSON 列式存儲,自動將 JSON 中的高頻字段列化,使存儲層性能首次接近列存;
- 4.0 版本:在執行層加入 索引、全局字典、延遲物化 等優化,使 JSON 查詢性能接近原生列存字段。
性能差異直觀對比
在深入技術細節之前,可以先直觀感受不同方案帶來的性能差異。以文首相同的查詢為例,不同版本的 JSON 實現具有顯著的性能差異:
接下來,本文將從技術原理角度展開介紹 FlatJSON 如何實現“既靈活又高效”的存儲與查詢能力。
FlatJSON 列式存儲
要理解 FlatJSON 的設計原理,首先需要回顧 StarRocks 的基礎存儲結構:
- Segment 是 StarRocks 的最小文件存儲單元,通常大小約為 1GB,每次導入數據至少會生成一個 Segment 文件;
- Segment 採用列式存儲格式,每個列(Column)獨立存儲為多個 Page,Page 層面支持 Encoding + Compression;
- Segment 內部支持多種索引:默認會創建輕量級索引(如 ZoneMap、SortKey Index),同時支持按需構建複雜索引(如 Bitmap Index、Bloomfilter Index);
- 數據在寫入時會根據用户指定的 ORDER BY 進行排序。
基於這一架構,FlatJSON 在數據導入階段進行列化存儲:
- 掃描 JSON 鍵集合,統計字段頻率,識別出“熱字段”(例如每條記錄都包含 "region" 字段);
- 自動推斷字段類型——能以數值存儲的字段絕不存為字符串,對於多類型字段選擇可兼容的公共類型;
- 將識別出的字段單獨存儲為列式格式(如 INT、STRING、DOUBLE 等);
- 對低頻或不固定字段,統一寫入一個“冗餘列”,以 JSON 類型存儲,作為兜底方案。
經過這一過程,原本需要層層解析的 JSON 數據,在物理存儲上被轉化為一張“半結構化表”。
在 Segment 文件內部,這些字段已經與普通列無異——可被按需讀取,也能創建索引。
為什麼 FlatJSON 更快?
- 列存壓縮更高效:低基數字段(如 region)可使用字典編碼,減少存儲空間;
- 消除冗餘存儲:無需重複存儲 JSON key;
- I/O 成本更低:查詢只需讀取被列化的字段;
- 免解析執行:查詢階段不再解析 JSON 字符串,直接讀取 Segment 文件中的列數據
SELECT
get_json_string(event, '$.type') AS event_type,
COUNT(DISTINCT user_id)
FROM events_log
WHERE
to_datetime(get_json_int(dt, '$.event_ts'))
BETWEEN '2024-01-01' AND '2024-12-31'
AND get_json_string(event, '$.region') = 'US'
GROUP BY event_type;
回到文首的示例查詢:
- 存儲引擎只需讀取 $.type、$.event_ts、$.region 這幾個被列化的字段,無需再加載完整的 JSON 數據;
- 對於表達式 get\_json\_string(event, '$.type'),系統不再解析 JSON 字符串,而是直接讀取 Segment 文件中的對應列。
得益於這種列式化設計,JSON 性能有了質的飛躍!
FlatJSON 列式查詢
在存儲實現列化之後,執行層的優化同樣關鍵。以下將介紹 FlatJSON 在執行階段的四項核心技術,以及它們如何進一步提升查詢性能。
- 索引(Index):把大海撈針變成定向查找
為避免全表掃描,FlatJSON 支持在已列化字段(如 region、event\_time、event\_type)上創建合適的索引,例如 ZoneMap。ZoneMap 會記錄每個 Page 的最小值和最大值,在查詢時可據此快速判斷哪些 Page 可能命中過濾條件,從而跳過無關數據。
性能提升的核心來自於掃描量的降低:
- 通過利用過濾條件的選擇度,系統可以直接跳過無關的數據塊,將 I/O 成本從“全表掃描”降至“部分數據讀取”。這種方式在謂詞選擇度較高的場景(例如 region='US' AND dt BETWEEN ...)中效果最為顯著;當分區鍵或排序鍵與查詢條件對齊時,收益會進一步提升。
- 若查詢條件選擇度較低,或使用模糊匹配(如 LIKE '%xx'),索引過濾的效果會受到限制。
- 但在多數典型分析場景中,通過 ZoneMap 過濾,I/O 開銷仍能減少一個數量級,從而顯著提升整體查詢性能。
- 高效解碼(Dictionary Decoding)
對於低基數字符串字段,Segment 在寫入時會自動進行字典編碼。以 region 字段為例,系統會將其拆分為兩部分存儲:字典和字典碼,從而消除重複字符串。
在查詢執行階段,StarRocks 會對字典編碼數據進行謂詞改寫:例如 region='US' 會被改寫為 region=1,然後利用索引直接過濾對應的數據塊。這種方式避免了頻繁的字符串比較,計算更輕量,內存與 I/O 效率均明顯提升。
在完成過濾後,系統再將字典碼翻譯回實際字符串,用於計算。
- 延遲物化(Late Materialization)
在傳統的執行路徑中,數據庫通常會在過濾之前就將整行數據解碼。這意味着,即使最終被過濾掉的記錄,也已經付出了完整的解析與對象創建成本。
延遲物化的思路正好相反:在初始階段僅使用輕量級的數據結構(例如行號)來表示記錄,在完成所有過濾操作之後,再根據篩選結果去讀取真正需要的列。以 region='US' 為例,StarRocks 會先讀取並過濾 region 字段,僅記錄滿足條件的行號。隨後再按照這些行號讀取 user_id 字段,從而避免無謂的數據加載。
延遲物化的核心在於延後不必要的計算與解碼:它減少了 I/O 讀量,提高了緩存命中率,並讓 CPU 資源集中用於真正需要的數據。
因此,在低選擇度的查詢中,這一機制能顯著降低無效計算成本。
- 全局字典(Global Dictionary):把字符串降維成整數計算
在 StarRocks 中,字符串一直是影響查詢性能的關鍵因素。無論是過濾還是聚合,都需要進行逐字符比較或字符串哈希,不僅消耗大量 CPU,還容易破壞緩存局部性。為此,StarRocks 在 Segment 局部字典的基礎上引入了 全局字典,通過彙總各節點的局部字典,構建出統一的全局映射表。
有了全局字典,字典編碼的使用範圍不再侷限於單個 Segment,而可擴展到 聚合、排序、關聯等計算場景。原本昂貴的字符串運算被“降維”為整數運算:CPU 只需執行輕量的整型比較或哈希操作,緩存命中率顯著提升,哈希表也更緊湊、更高效,聚合時的衝突率大幅降低。
例如,原本的 GROUP BY region 操作需要在哈希表中頻繁進行字符串查找和更新;使用全局字典後,執行計劃可改寫為 GROUP BY region_code,以整數完成聚合,僅在最終輸出階段再將整數解碼為字符串,從而顯著減少計算開銷。
- 小結對比
回到文首的示例,可以直觀看出兩種執行方式的差異:
- 傳統 JSON 查詢:讀取完整 JSON → 路徑解析 → 類型轉換 → 執行過濾與聚合;
- FlatJSON 查詢:直接命中列存字段 → 索引與字典過濾 → 全局字典計算 → 延遲解碼。
相比傳統方式,FlatJSON 在執行路徑上大幅減少了解析、比較和全量掃描等開銷。
在實際測試中,對 10 億行 JSON 數據執行相同的聚合查詢,傳統方式可能耗時約 30 秒,
而基於 FlatJSON 的查詢僅需 約 500 毫秒,I/O 與 CPU 成本均降低了數個數量級。
業務場景的真實收益
在真實的業務場景中,FlatJSON 的價值不僅體現在性能提升上,更體現在對靈活性與易用性的平衡。
- 埋點日誌分析
痛點:實時看板需要對最新埋點數據進行聚合和分組,通常涉及數十億行 JSON。傳統模式下,查詢需要逐行解析 JSON,返回結果往往需要數十秒甚至更久;同時,埋點字段變化頻繁,新字段出現時必須修改 ETL 流程。
FlatJSON 方案:在數據導入階段,系統會自動將常用字段(如 user\_id、event\_time、region、event_type 等)拍平成列存格式。新字段出現時,不會影響現有查詢,用户仍可通過 JSON 函數直接訪問,系統會根據訪問頻率判斷是否列化。
收益:查詢延遲從數十秒降至數百毫秒;同時兼容頻繁變化的埋點 schema,分析團隊無需因字段變更頻繁修改表結構或 ETL 流程。
- 電商報表生成
痛點:電商訂單數據通常以 JSON 形式存儲,包含 SKU、價格、促銷、地域、配送等上百個字段。傳統方案依賴複雜的 ETL 流程來抽取字段,開發與維護成本高;當字段新增或類型不一致(例如 price 既可能是字符串也可能是數值)時,整個數據流水線容易出錯。
FlatJSON 方案:在導入階段,FlatJSON 自動識別並列化高頻字段,同時保留完整的 Binary JSON 結構。即使 schema 動態變化或字段類型異構,查詢仍可正常執行,常用字段的訪問性能依舊接近原生列存。
收益:報表生成時間從分鐘級縮短至秒級,開發和運維團隊無需頻繁調整 ETL 流程,分析團隊能更快速地響應業務變化。
- IoT 實時監控
痛點:IoT 設備上報的數據通常以 JSON 格式存儲,包含數十個指標,不同型號設備間的字段差異顯著(異構 schema 十分常見)。傳統模式下,數據庫需要完整解析 JSON;若字段缺失或類型不一致,查詢容易退化甚至報錯。
FlatJSON 方案:FlatJSON 會在導入階段自動將高頻指標(如温度、濕度等)列化存儲,其餘字段保留在 Binary JSON 中。查詢時系統能自動跳過不存在的字段,避免執行錯誤;對於類型不一致的字段,執行層可進行統一處理或延遲物化。
收益:支持千萬級設備數據的實時聚合,能夠在秒級生成監控報表。同時,IoT 場景中的動態與異構 schema 由系統自動處理,顯著減少人工清洗和建模成本。
FlatJSON 不僅解決了性能問題,也讓動態 schema 與異構 schema 成為可控的系統成本。對開發者而言,不必再因字段新增、缺失或類型不一致而焦慮;對分析師而言,依舊可以通過 SQL 自由查詢,而無需擔心底層數據結構的頻繁變化。
如何使用 FlatJSON
FlatJSON 的啓用方式非常簡單,使用方法與標準 JSON 類型幾乎相同。
-- Minimal table with a JSON column and FlatJSON enabled
CREATE TABLE events_log (
dt DATE,
event_id BIGINT,
event JSON
)
DUPLICATE KEY(`dt`, event_id)
PARTITION BY date_trunc('DAY', dt)
DISTRIBUTED BY HASH(dt, event_id)
PROPERTIES (
"flat_json.enable" = "true", -- enable FlatJSON for this table
"flat_json.null.factor" = "0.3" -- optional: skip extracting too-sparse fields
);
可以通過表屬性來開啓或關閉 FlatJSON;新導入的 JSON 數據會自動被列化存儲。
此外,還可以通過參數 flat_json.null.factor 設置閾值,以避免抽取過於稀疏的字段。
插入幾行示例數據
INSERT INTO events_log VALUES
('2025-09-01', 1001, PARSE_JSON('{
"user_id": 12345, "region": "US", "event_type": "click", "ts": 1710000000
}')),
('2025-09-01', 1002, PARSE_JSON('{
"user_id": 54321, "region": "CA", "event_type": "purchase", "ts": 1710000300,
"experiment_flag": "A" -- rare field, remains in JSON fallback
}'));
像平常一樣使用 JSON 函數進行查詢。
-- Filter + group on fields that FlatJSON likely extracted (high-occurrence)
SELECT
get_json_string(event, '$.event_type') AS event_type,
COUNT(*) AS cnt
FROM events_log
WHERE
get_json_string(event, '$.region') = 'US'
AND get_json_int(event, '$.ts') BETWEEN 1710000000 AND 1710003600
GROUP BY event_type;
總結
FlatJSON 為 JSON 在數據庫中的處理提供了一種工程化的解決路徑。在存儲層,FlatJSON 通過自動列化高頻字段,提高壓縮率並顯著降低掃描成本;在執行層,則結合索引、全局字典和延遲物化等優化技術,減少了解析與解碼開銷。實際測試表明,在相同 SQL 下,查詢延遲可從數十秒下降至亞秒級。
對於用户來説,無需再在“靈活性”和“性能”之間反覆取捨。埋點、日誌、IoT 等動態 schema 的數據可以直接寫入 StarRocks,既能支持快速變化的業務需求,又能提供穩定可預期的查詢性能,從而降低建模和 ETL 成本,讓團隊更專注於業務分析本身。