前言
过去近七年在网易杭研一直从事数据库相关的开发工作,主要是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对应的数据载体叫做数据仓库,称之为仓库个人认为挺贴切的。因为它不是数据的生产者,其中的数据都是从其他地方搬运过来的,而搬运和清洗的过程就是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):
即维的位置的互换,就像是二维表的行列转换,如图中通过旋转实现产品维和地域维的互换。
与上面几种操作不同,旋转并未减少或增加要分析的样本。而是根据不同的目的,改变了分析的角度,比如本来将产品作为观察角度,将地域和时间作为参照,分析不同产品在销售情况。通过旋转,转而分析江浙沪三个不同地区的产品销售情况。
以上仅简单介绍了数仓领域基础的知识和概念。下一篇重点分析现实中数仓的类型及其代表产品,并介绍的数仓产品会用到的核心技术。
注:在调研过程中,看过不少数仓的基础文章,逐渐形成了对数仓的认识,在将其转化为自己的描述过程中,发现了一个博客(http://webdataanalysis.net),有论述有实践,在实践中不断加深自己的认识。虽然其中的文章都是很多年前的,但个人觉得对学习入门数仓很有帮助,所以文中很多内容和图片都参考了该博客。
引用:
- http://webdataanalysis.net/web-data-warehouse/
- https://en.wikipedia.org/wiki/Online_analytical_processing
未完待续,TO BE CONTINUE!