前言
過去近七年在網易杭研一直從事數據庫相關的開發工作,主要是MySQL和MongoDB這兩種數據庫,去年開始涉及圖數據庫Neo4J。上述幾種,都可認為是OLTP類數據處理,由於工作需要,需要調研學習OLAP技術和相關系統,本文開始逐步進行第一輪總結,很多東西還只是片面理解,權當做個筆記。
對事物的認識總是螺旋式的,先有個大致的認識,再逐漸豐富其血肉。這個過程,會有片面性、也可能否定之前的理解,但只要一直用心用腦,總會不斷進步的。當然,多參考優秀的資料,會大大縮短過程中花費的時間。
基礎問答
什么是OLAP,其與OLTP有什么區別?
如果展開來說,這個問題估計可以寫好幾篇文章,這里簡單談談個人的理解。
OLTP是Online transaction processing的英文縮寫,指在線/聯機事務處理,這么說其實還是比抽象的。OLTP典型的應用領域包括銀行、證劵等金融行業,電子商務系統等,在此舉最經典的銀行例子,我們在招商銀行APP上查詢賬戶余額、收支信息和轉賬記錄,在ATM機上存錢,取錢,將招行賬號的錢轉到工行賬號上。這些都是典型的OLTP類操作,這些操作都比較簡單,主要是對數據庫中的數據進行增刪改查。操作主體一般是產品的用戶。
OLAP是Online analytical processing的英文縮寫,指聯機分析處理。從字面上我們能看出是做分析類操作。通過分析數據庫中的數據來得出一些結論性的東西。比如給老總們看的報表,用於進行市場開拓的用戶行為統計,不同維度的匯總分析結果等等。操作主體一般是運營、銷售和市場等團隊人員而不是用戶。
單次OLTP處理的數據量比較小,所涉及的表非常有限,一般僅一兩張表。而OLAP是為了從大量的數據中找出某種規律性的東西,經常用到count()、sum()和avg()等聚合方法,用於了解現狀並為將來的計划/決策提供數據支撐,所以對多張表的數據進行連接匯總非常普遍。
為了表示跟OLTP的數據庫(database)在數據量和復雜度上的不同,一般稱OLAP的操作對象為數據倉庫(data warehouse),簡稱數倉。數據庫倉庫中的數據,往往來源於多個數據庫,以及相應的業務日志。
下表是對OLTP和OLAP的簡單總結。
網易杭研OLTP數據庫團隊為業界培養了多位數據庫資深專家,在線數倉團隊也同樣如此,在Impala、Kudu等技術上有深厚的積累,是網易猛獁、有數等網易大數據產品的核心基礎設施。
MySQL等OLTP數據庫能處理OLAP業務嗎?
MySQL是當前最流行的開源數據庫,一般作為OLTP數據庫使用。在MySQL上也能執行一些OLAP操作,但這不是MySQL擅長的領域。雖然OLTP和OLAP都是通過SQL來執行,但SQL語句只是描述了我想要什么,而並沒有說明應該怎么做(不考慮hint等),即確定最優的執行計划。由於OLTP操作比較簡單,所涉及的表也少,因此不需要相應的數據庫具有強大的執行優化能力,比如說MySQL在查詢優化這塊就比較弱,但這其實沒有給它的大規模普及使用造成多大傷害。
當然,MySQL也在快速進步,尤其是最新的8.0版本,在查詢優化模塊添加了很多眾望所歸的功能特性,包括窗口函數,通用表達式和更強大的Join能力等。
而OLAP類操作不一樣,沒有強大的執行計划產生和優化能力,執行這類操作肯定不會有多高的效率,甚至會寸步難行。當然,如果總數據量較小,SQL也相對簡單,那MySQL也是能夠應付的。在MySQL高可用實例的從庫做些報表類查詢也有不少案例。
OLAP的查詢跟OLTP查詢具體有那些不一樣?
上文簡要提及,OLTP查詢一般僅涉及單表,點查為主,返回的是記錄本身或該記錄的多個列。即使是范圍查詢,基本上也會通過limit來限制返回的記錄數。
而OLAP則不同,表中單條記錄本身並不是查詢所關心的,比較典型的特點包括有聚合類算子、涉及多表Join,查詢所用謂語/條件沒有索引,玩玩不是返回記錄。由於這些操作都非常耗計算資源,而且數據倉庫相比數據庫在數據量上大很多,因此,OLAP類查詢經常表現為cpu-bound而不是io-bound。
這樣說可能還不夠直觀,下面換種形式。OLTP和OLAP發展到現在已經比較成熟,業界也有些公認的benchmark來進行性能評估。我們可以通過這些benchmark中的對應sql來了解兩位服務的典型查詢語句。 對於OLTP來說,有sysbench和tpcc測試套件,對於OLAP來說,有tpch和tpcds 2種。這里分別例舉sysbench oltp和tpcds的sql作為參考。
sysbench oltp查詢
可以從sysbench的lua腳本中獲取都有哪些查詢類型。如下所示:
local stmt_defs = {
point_selects = {
"SELECT c FROM sbtest%u WHERE id=?",
t.INT},
simple_ranges = {
"SELECT c FROM sbtest%u WHERE id BETWEEN ? AND ?",
t.INT, t.INT},
sum_ranges = {
"SELECT SUM(k) FROM sbtest%u WHERE id BETWEEN ? AND ?",
t.INT, t.INT},
order_ranges = {
"SELECT c FROM sbtest%u WHERE id BETWEEN ? AND ? ORDER BY c",
t.INT, t.INT},
distinct_ranges = {
"SELECT DISTINCT c FROM sbtest%u WHERE id BETWEEN ? AND ? ORDER BY c",
t.INT, t.INT},
對應到測試時,就是下面的樣子。
SELECT c FROM sbtest10 WHERE id=4352
SELECT c FROM sbtest10 WHERE id BETWEEN 5046 AND 5046+99 ORDER BY c
SELECT c FROM sbtest3 WHERE id BETWEEN 4983 AND 4983+99
SELECT SUM(K) FROM sbtest1 WHERE id BETWEEN 4981 AND 4981+99
SELECT DISTINCT c FROM sbtest3 WHERE id BETWEEN 4989 AND 4989+99 ORDER BY c
感興趣的同學可以查看github上sysbench代碼。上述sql均位於oltp_common.lua中。
https://github.com/akopytov/sysbench/blob/master/src/lua/oltp_common.lua
同樣的,我們也可以從github上找到tpcc的查詢sql。如下:
https://github.com/Percona-Lab/sysbench-tpcc/blob/master/tpcc_run.lua
例子如下:
-- SELECT c_id
-- FROM customer
-- WHERE c_w_id = :c_w_id
-- AND c_d_id = :c_d_id
-- AND c_last = :c_last
-- ORDER BY c_first;
-- SELECT c_balance, c_first, c_middle, c_last
-- FROM customer
-- WHERE c_w_id = :c_w_id
-- AND c_d_id = :c_d_id
-- AND c_id = :c_id;
-- SELECT c_discount, c_last, c_credit, w_tax
-- FROM customer, warehouse
-- WHERE w_id = :w_id
-- AND c_w_id = w_id
-- AND c_d_id = :d_id
-- AND c_id = :c_id;
相對來說,tpcc的查詢比oltp查詢更復雜些。包含了2表join操作。
tpcds查詢
下面在看看復雜的tpcds查詢是怎么樣的。tpcds一共99個query,下面舉例。
-- query68
SELECT
"c_last_name"
, "c_first_name"
, "ca_city"
, "bought_city"
, "ss_ticket_number"
, "extended_price"
, "extended_tax"
, "list_price"
FROM
(
SELECT
"ss_ticket_number"
, "ss_customer_sk"
, "ca_city" "bought_city"
, "sum"("ss_ext_sales_price") "extended_price"
, "sum"("ss_ext_list_price") "list_price"
, "sum"("ss_ext_tax") "extended_tax"
FROM
${database}.${schema}.store_sales
, ${database}.${schema}.date_dim
, ${database}.${schema}.store
, ${database}.${schema}.household_demographics
, ${database}.${schema}.customer_address
WHERE ("store_sales"."ss_sold_date_sk" = "date_dim"."d_date_sk")
AND ("store_sales"."ss_store_sk" = "store"."s_store_sk")
AND ("store_sales"."ss_hdemo_sk" = "household_demographics"."hd_demo_sk")
AND ("store_sales"."ss_addr_sk" = "customer_address"."ca_address_sk")
AND ("date_dim"."d_dom" BETWEEN 1 AND 2)
AND (("household_demographics"."hd_dep_count" = 4)
OR ("household_demographics"."hd_vehicle_count" = 3))
AND ("date_dim"."d_year" IN (1999 , (1999 + 1) , (1999 + 2)))
AND ("store"."s_city" IN ('Midway' , 'Fairview'))
GROUP BY "ss_ticket_number", "ss_customer_sk", "ss_addr_sk", "ca_city"
) dn
, ${database}.${schema}.customer
, ${database}.${schema}.customer_address current_addr
WHERE ("ss_customer_sk" = "c_customer_sk")
AND ("customer"."c_current_addr_sk" = "current_addr"."ca_address_sk")
AND ("current_addr"."ca_city" <> "bought_city")
ORDER BY "c_last_name" ASC, "ss_ticket_number" ASC
LIMIT 100
--query53
SELECT *
FROM
(
SELECT
"i_manufact_id"
, "sum"("ss_sales_price") "sum_sales"
, "avg"("sum"("ss_sales_price")) OVER (PARTITION BY "i_manufact_id") "avg_quarterly_sales"
FROM
${database}.${schema}.item
, ${database}.${schema}.store_sales
, ${database}.${schema}.date_dim
, ${database}.${schema}.store
WHERE ("ss_item_sk" = "i_item_sk")
AND ("ss_sold_date_sk" = "d_date_sk")
AND ("ss_store_sk" = "s_store_sk")
AND ("d_month_seq" IN (1200 , (1200 + 1) , (1200 + 2) , (1200 + 3) , (1200 + 4) , (1200 + 5) , (1200 + 6) , (1200 + 7) , (1200 + 8) , (1200 + 9) , (1200 + 10) , (1200 + 11)))
AND ((("i_category" IN ('Books ' , 'Children ' , 'Electronics '))
AND ("i_class" IN ('personal ' , 'portable ' , 'reference ' , 'self-help '))
AND ("i_brand" IN ('scholaramalgamalg #14 ' , 'scholaramalgamalg #7 ' , 'exportiunivamalg #9 ' , 'scholaramalgamalg #9 ')))
OR (("i_category" IN ('Women ' , 'Music ' , 'Men '))
AND ("i_class" IN ('accessories ' , 'classical ' , 'fragrances ' , 'pants '))
AND ("i_brand" IN ('amalgimporto #1 ' , 'edu packscholar #1 ' , 'exportiimporto #1 ' , 'importoamalg #1 '))))
GROUP BY "i_manufact_id", "d_qoy"
) tmp1
WHERE ((CASE WHEN ("avg_quarterly_sales" > 0) THEN ("abs"((CAST("sum_sales" AS DECIMAL(38,4)) - "avg_quarterly_sales")) / "avg_quarterly_sales") ELSE null END) > DECIMAL '0.1')
ORDER BY "avg_quarterly_sales" ASC, "sum_sales" ASC, "i_manufact_id" ASC
LIMIT 100
--query59
WITH
wss AS (
SELECT
"d_week_seq"
, "ss_store_sk"
, "sum"((CASE WHEN ("d_day_name" = 'Sunday ') THEN "ss_sales_price" ELSE null END)) "sun_sales"
, "sum"((CASE WHEN ("d_day_name" = 'Monday ') THEN "ss_sales_price" ELSE null END)) "mon_sales"
, "sum"((CASE WHEN ("d_day_name" = 'Tuesday ') THEN "ss_sales_price" ELSE null END)) "tue_sales"
, "sum"((CASE WHEN ("d_day_name" = 'Wednesday') THEN "ss_sales_price" ELSE null END)) "wed_sales"
, "sum"((CASE WHEN ("d_day_name" = 'Thursday ') THEN "ss_sales_price" ELSE null END)) "thu_sales"
, "sum"((CASE WHEN ("d_day_name" = 'Friday ') THEN "ss_sales_price" ELSE null END)) "fri_sales"
, "sum"((CASE WHEN ("d_day_name" = 'Saturday ') THEN "ss_sales_price" ELSE null END)) "sat_sales"
FROM
${database}.${schema}.store_sales
, ${database}.${schema}.date_dim
WHERE ("d_date_sk" = "ss_sold_date_sk")
GROUP BY "d_week_seq", "ss_store_sk"
)
SELECT
"s_store_name1"
, "s_store_id1"
, "d_week_seq1"
, ("sun_sales1" / "sun_sales2")
, ("mon_sales1" / "mon_sales2")
, ("tue_sales1" / "tue_sales2")
, ("wed_sales1" / "wed_sales2")
, ("thu_sales1" / "thu_sales2")
, ("fri_sales1" / "fri_sales2")
, ("sat_sales1" / "sat_sales2")
FROM
(
SELECT
"s_store_name" "s_store_name1"
, "wss"."d_week_seq" "d_week_seq1"
, "s_store_id" "s_store_id1"
, "sun_sales" "sun_sales1"
, "mon_sales" "mon_sales1"
, "tue_sales" "tue_sales1"
, "wed_sales" "wed_sales1"
, "thu_sales" "thu_sales1"
, "fri_sales" "fri_sales1"
, "sat_sales" "sat_sales1"
FROM
wss
, ${database}.${schema}.store
, ${database}.${schema}.date_dim d
WHERE ("d"."d_week_seq" = "wss"."d_week_seq")
AND ("ss_store_sk" = "s_store_sk")
AND ("d_month_seq" BETWEEN 1212 AND (1212 + 11))
) y
, (
SELECT
"s_store_name" "s_store_name2"
, "wss"."d_week_seq" "d_week_seq2"
, "s_store_id" "s_store_id2"
, "sun_sales" "sun_sales2"
, "mon_sales" "mon_sales2"
, "tue_sales" "tue_sales2"
, "wed_sales" "wed_sales2"
, "thu_sales" "thu_sales2"
, "fri_sales" "fri_sales2"
, "sat_sales" "sat_sales2"
FROM
wss
, ${database}.${schema}.store
, ${database}.${schema}.date_dim d
WHERE ("d"."d_week_seq" = "wss"."d_week_seq")
AND ("ss_store_sk" = "s_store_sk")
AND ("d_month_seq" BETWEEN (1212 + 12) AND (1212 + 23))
) x
WHERE ("s_store_id1" = "s_store_id2")
AND ("d_week_seq1" = ("d_week_seq2" - 52))
ORDER BY "s_store_name1" ASC, "s_store_id1" ASC, "d_week_seq1" ASC
LIMIT 100
很顯然,tpcds的查詢復雜度相比oltp和tpcc高非常多。
是否有可能將OLAP和OLTP統一起來?
目前有個趨勢是將OLTP和OLAP相融合,在同一個系統中同時提供TP和AP 2種服務,即HTAP產品,國內的數據庫創業公司PingCAP的TiDB即是其中的佼佼者。
但由於兩者服務類型相差甚大,完全融合是很難的,如何解決AP業務對要求更高實時性和穩定性的TP業務帶來影響,如何同時提供2種服務且2種服務與業界其他系統相比具備足夠競爭力,這些都是很大的挑戰。
在目前的HTAP系統中,一般通過存儲層的數據多副本來進行針對AP和TP業務的不同方式的優化,使用多個副本來以行存方式更好滿足TP業務,通過增加一個副本來以列存方式為AP業務提供服務。
在存儲系統上,配置獨立的計算/查詢系統,分別滿足TP和AP不同的要求。比如TP系統很重要的一個特點就是事務的ACID,而AP系統更加關心分布式並行查詢能力。
TP和AP融合不是本系列文章關注焦點,因此下面我們聚焦到OLAP/數倉上來。
數倉有哪些基礎知識和概念?
OLAP的查詢語句比OLTP更復雜,顯然是因為兩則操作的數據集和目的都是不一樣的。數據庫模型是2維的關系-實體模型。而數倉則是多維立方體模型。相對來說,給數倉建模的難度更高。為此,有必要再介紹下輸出基礎知識和一些重要概念。
先來看看這張圖,基於該圖,介紹下數倉的數據來源,作用和存在方式。
說說數倉中數據的前世今生?
數倉中的數據從何而來?
OLAP對應的數據載體叫做數據倉庫,稱之為倉庫個人認為挺貼切的。因為它不是數據的生產者,其中的二手QQ拍賣地圖數據都是從其他地方搬運過來的,而搬運和清洗的過程就是ETL流程(Extract-Transform-Load,即數據抽取、轉換和加載),在此不展開。
(圖片來源)
那么這些數據從何而來,表現形式如何呢? 歸納起來大體有3種:
- 結構化數據:一般來自於數據庫,比如MySQL等關系型數據庫的表中保存的記錄(rows)。即承擔OLTP功能的數據載體。這類數據最好處理,因為數據表達方式作為規范,約束性最好;
- 半結構化數據:該部分數據來源較多,包括用戶行為日志(如app的頁面訪問記錄)、平台或管理服務日志(tomcat、mysql等服務日志)等等,也包括存儲於MongoDB等NoSQL數據庫中的記錄(Docs等)。這些數據一般以Json或XML等形式存在,在ETL時難度較大。
- 非結構化數據:包括圖片、音頻、視頻和網頁等,這些數據非常復雜,信息量也很大,一般不會直接抽取出來直接保存到數倉中,而是記錄他們的元數據信息(metadata),舉圖片為例,可能保存該圖片的產生時間、格式、大小等等,至於圖片本身,一般通過url鏈接保存在對象或文件存儲系統中。
數倉的作用有哪些?
數據倉庫大致可以分為以下一些作用:
- - 進行交互式/即席查詢(ad-hoc);
- - 用於報表類查詢(BI Reporting);
- - 進行數據分析類查詢(Data Analytics);
- - 用於數據挖掘類查詢(Data Mining);
在數倉倉庫之前可以部署至少如上所述4類數據應用。
數據在數倉中是如何組織的?
簡單介紹了數倉的數據來源,數倉中數據所能發揮的作用后,接下來聊聊這些通過不同方式進來的數據,如何存在於數倉當中的。相應地引入多維數據模型和數據立方體(data cube)概念。數倉中數據的存在方式跟數倉索要發揮的作用息息相關,即該數倉要承載什么樣的業務模型。
基於業務模型設計對應的數據倉庫的數據模型,進而針對性實現不同的ETL操作將外部數據經過不同程度的過濾、聚合等處理之后引入到數倉之中。
什么是多維數據模型?
抽象的概念光通過文字描述是無法在大腦中具象化的,這是因為大自然存在的都是具體的事物,抽象的東西是竟然我們加工所得。為了更加清晰的進行說明,需要將抽象概念重新具體化。下面就通過例子來說明與數倉多維數據模型相關的概念,以便大家更好得建立初步的認識。
上圖所示即為一個采用簡單星型模型組織起來的多維數據模型,用來存儲商品銷售情況。在這張圖中的6個表又可分為2種類型,分別是最中間的事實表,和圍繞其展開的維度表。
什么是事實表?
事實表(Fact Table)用來記錄具體事件,包含了每個事件的具體要素,以及具體發生的事情。事實表是主干,簡明扼要得介紹一個事實。例子中就通過一條事實表記錄說明了某個地方(地域ID)的某人(用戶ID)在某個時間(時間ID)通過某種方式(支付ID)買了某產品(產品ID)。
什么是維度表?
維度表(Dimension Table )是依賴事實表而存在的,“皮之不存,毛將焉附”,沒有事實表數據,維度表也就沒有存在的意義。每個維度表都是對事實表中的每個列/字段進行展開描述。
比如事實表中的用戶ID,就可以進一步展開成一張維度表,記錄該用戶ID實體的用戶名、聯系信息、地址信息、年齡、性別和注冊方式等等;
一般來說,對於數倉,事實表的增刪改操作相比維度表更為頻繁,模型建立后,維度表中的數據保持相對穩定。試想,商品銷售行為是一直在發生的,而用戶注冊和產品更新不總是隨時有的。再說到地域和支付方式,那就更少變化了。
通過事實表和維度表組織起來的數倉多維數據模型,相比原本分散在數據庫等各處的數據,能夠有更有目的更高效的查詢效率,比如可以查詢匯總地域維度中某個省的商品銷售情況,也可以通過時間維度分析每個季度的某類商品銷售趨勢。將多個維度表跟事實表進行不同程度的連接,可以展開得到各種各樣的分析結果,滿足商品運營等數據使用者的不同需求。
基於數據模型及操作又可以引入數據立方體概念及對其的常見操作。
什么是數據立方體?
中國作為信息技術領域的后起之秀,我們現在介紹的這些概念都源於英文。數據立方體就是從英文“Data Cube”而來。下圖就是一個商品銷售模型的數據立方體。
(圖片來源)
其實我們也可以叫它”數據魔方體“,因為立方體是三維的,而多維數據模型並不僅僅三維,雖然受圖形化展示限制,一般僅展示其三個維度。而”魔方“一詞,則凸現出了其變化性,通過對其進行不同的操作,讓數據呈現出千變萬化的結果。
上圖來源於參考資料,比較好展示了多維模型,從大立方體上可以看到商品類型、季度和地區三個維度。但對於每個維度又是一個小立方體,比如第一季度浙江的書籍銷售情況就是左下角的小立方體。在這個小立方體中,根據需要,我們還可以按照書籍類型,從季度拆分為月度,浙江拆出各地級市。
上面的拆分例子正是基於立方體的場景操作之一,下面進一步介紹。
數據立方體有哪些常見操作?
在進行OLAP查詢時,基於數據立方體的多維分析操作包括:鑽取(Drill-down)、上卷(Roll-up)、切片(Slice)、切塊(Dice)以及旋轉(Pivot),接下來以上面的數據立方體為例來逐一解釋下:
鑽取(Drill-down):
該操作我們上面簡單舉過例子,從鑽取這個名字,就可以知道,這是往更細粒度深挖。從上一個層次到下一層,即深入該層內部。比如書籍中可以分計算機、數理化、文史地等,二季度又可分為4、5、6三個月,浙江省又可以分為杭、甬、溫等地級市的銷售數據。
上卷(Roll-up):
與鑽取往深度挖相反,上卷顧名思義,即從細粒度數據向上層聚合,如將江蘇省、上海市和浙江省的銷售數據進行匯總來查看江浙滬地區的銷售數據,將2010年的四個季度匯總成2010年的總數據;將電子產品、日用品和書籍匯總成實體商品,與服務相對應。
上面的鑽取和上卷通過攤薄和加厚來改變維度的粒度。接下來介紹的切片和切塊相似,是對維度進行篩選,獲取其中一部分相同的樣本。
切片(Slice):
如左圖所示,切片就是選擇維中特定的值進行分析,比如只選擇電子產品的銷售數據,或2010年第二季度的數據,或浙江一個省粒度進行分析。
切塊(Dice):
如右圖所示,切塊是選擇維中特定區間的數據或者某批特定值進行分析,比如選擇2010年第一季度到2010年第二季度的銷售數據,或者是電子產品和日用品的銷售數據。
與切片不同的是,切塊的粒度更大,會選擇一個維度中某個區間或范圍的值,而不僅僅是某個值。
旋轉(Pivot):
即維的位置的互換,就像是二維表的行列轉換,如圖中通過旋轉實現產品維和地域維的互換。
與上面幾種操作不同,旋轉並未減少或增加要分析的樣本。而是根據不同的目的,改變了分析的角度,比如本來將產品作為觀察角度,將地域和時間作為參照,分析不同產品在銷售情況。通過旋轉,轉而分析江浙滬三個不同地區的產品銷售情況。
以上僅簡單介紹了數倉領域最最基礎的知識和概念。下一篇重點分析現實中數倉的類型及其代表產品,並介紹優秀的數倉產品會用到的核心技術。
注:在調研過程中,看過不少數倉的基礎文章,逐漸形成了對數倉的認識,在將其轉化為自己的描述過程中,有論述有實踐,在實踐中不斷加深自己的認識。雖然其中的文章都是很多年前的,但個人覺得對學習入門數倉很有幫助,所以文中很多內容和圖片都參考了該博客。