最近在項目中使用了開源OLAP引擎——Mondrian實現一個多維分析系統,在項目后期系統優化階段使用了Mondrian中的聚合表機制。這里結合Mondrian官方資料和個人使用經驗,對Mondrian中聚合表的概念、應用場景、如何使用、注意事項等內容做一個總結。
1. OLAP相關概念
Mondrian是一個基於Java語言的開源OLAP引擎,它通過MDX語句執行查詢,從關系型數據庫RDBMS中讀取數據,以多維度的形式展示查詢結果。
Mondrian通過Schema來定義一個多維數據庫,它是一個邏輯概念上的模型,其中包含Cube(立方體)、Dimension(維度)、Hierarchy(層次)、Level(級別)、Measure(度量),這些被映射到數據庫物理模型。Mondrian中Schema是以XML文件的形式定義的。
- Cube(立方體)是一系列Dimension和Measure的集合區域,它們共用一個事實表。
- Dimension(維度)是一個Hierarchy的集合,維度一般有其相對應的維度表,它由Hierarchy(層次)組成,而Hierarchy(層次)又是由組成Level(級別)的。
- Hierarchy(層次)是指定維度的層級關系的,如果沒有指定,默認Hierarchy里面裝的是來自立方體中的真實表。
- Level(級別)是Hierarchy的組成部分,使用它可以構成一個結構樹,Level的先后順序決定了Level在結構樹上的位置,最頂層的 Level 位於樹的第一級,依次類推。
- Measure(度量)是我們要進行度量計算的數值,支持的操作有sum、count、avg、distinct-count、max、min等。
概括總結一下:在多維分析中,關注的內容通常被稱為度量(Measure),而把限制條件稱為維度(Dimension)。多維分析就是對同時滿足多種限制條件的所有度量值做匯總統計。包含度量值的表被稱為事實表(Fact Table),描述維度具體信息的表被稱為維表(Dimension Table),同時有一點需要注意:並不是所有的維度都要有維表,對於取值簡單的維度,可以直接使用事實表中的一列作為維度展示。
下面是Mondrian中一個簡單的Schema文件:

<Schema>
<Cube name="Sales">
<Table name="sales_fact_1997"/>
<Dimension name="Gender" foreignKey="customer_id">
<Hierarchy hasAll="true" allMemberName="All Genders" primaryKey="customer_id">
<Table name="customer"/>
<Level name="Gender" column="gender" uniqueMembers="true"/>
</Hierarchy>
</Dimension>
<Dimension name="Time" foreignKey="time_id">
<Hierarchy hasAll="false" primaryKey="time_id">
<Table name="time_by_day"/>
<Level name="Year" column="the_year" type="Numeric" uniqueMembers="true"/>
<Level name="Quarter" column="quarter" uniqueMembers="false"/>
<Level name="Month" column="month_of_year" type="Numeric" uniqueMembers="false"/>
</Hierarchy>
</Dimension>
<Measure name="Unit Sales" column="unit_sales" aggregator="sum" formatString="#,###"/>
<Measure name="Store Sales" column="store_sales" aggregator="sum" formatString="#,###.##"/>
<Measure name="Store Cost" column="store_cost" aggregator="sum" formatString="#,###.00"/>
<CalculatedMember name="Profit" dimension="Measures" formula="[Measures].[Store Sales] - [Measures].[Store Cost]">
<CalculatedMemberProperty name="FORMAT_STRING" value="$#,##0.00"/>
</CalculatedMember>
</Cube>
</Schema>
其中包含一個名為“Sales”的Cube,立方體中有兩個維度:“Gender”和“Time”,兩個度量值:“Unit Sales”和“Store Sales”。有關Mondrian的Schema文件的具體編寫規則,可以參考官方文檔:如何編寫Schema。
2. 什么是聚合表
下圖描述了一個數據庫的結構。該數據庫中共有五張表,分別是Sales表,Customer表,Time表,Product表和Mfr表。這個數據庫的作用是存儲每一筆交易:包括這筆交易發生在什么時間,交易的產品類型,進行交易的客戶信息,交易方式,交易了多少件產品以及成交金額是多少。
星型模型中有一張事實表(Sales),兩個度量列(units和dollars),四個維度表(Product, Mfr, Customer, Time)。在這個星型模型的最頂層,我們創建了以下多維模型:
- [Sales]立方體包含[Unit sales]和[Dollar sales]兩個度量值;
- [Product]維度包含[All Products],[Manufacturer],[Brand],[Prodid]四個級別;
- [Time]維度包含[All Time],[Year],[Quarter],[Month],[Day]五個級別;
- [Customer]維度包含[All Customers],[State],[City],[Custid]四個級別;
- [Payment Method]維度包含[All Payment Methods],[Payment Method]兩個級別。
其中,大部分維度都有一個對應的維度表,除了兩個地方:[Product]
維度是一個雪花維度,它會把
Product
和
Mfr
兩張表展開;
[Payment Method]
維度是一個退化的維度,直接使用事實表中的
payment
列作為維度屬性,因此不需要一個單獨的維表。
假設現在我們要對交易做一些統計,例如,某一件特定產品在某一個時間段內以某種特定方式總共賣出多少件或多少錢,這時成交產品數和成交金額是我們最終關注的內容,其他的因素例如時間、產品、方式等都只是對我們最終關注內容進行統計的限制條件。
在上面的例子中,限制條件有時間、產品類型、用戶類型和交易方式,有時我們並不需要同時使用所有的限制條件,例如,當我們只想知道指定產品的成交總金額時,那么除了產品類型之外其他三個限制條件都是多余的,而在查詢時,需要在整個事實表中執行查詢,找出產品類型為指定類型的所有產品然后再做統計,為了提高查詢效率,我們可以新建一張表,這張表按照產品類型把事實表中的行合並到一起,合並的方式是拋棄其他維,把度量值按特定的方式(max,min,sum,count或avg)整合到一起。這種表被叫做聚合表(Aggregate Table)。
3. 聚合表的應用場景
事實表中的行構成了一個集合,每一維(或若干維)按照其取值的不同可以將事實表這個全集划分成若干個不相交的子集。聚合表所做的工作實際上就是把划分出的子集歸為數據庫表中的一行,這樣做一方面可以減少數據庫表的行數,另一方面也省去了查詢時所需要做的一些統計工作,從而提高查詢時的效率。
4. 如何在Mondrian中使用聚合表
在Mondrian應用中加入聚合表需要進行以下工作:
4.1. 定義聚合表
在Mondrian中,一張事實表可以有多張聚合表,但每個聚合表只對應一個事實表。目前Mondrian中支持兩種聚合表:lost dimension和collapsed dimension。
1. lost dimension
lost dimension表示有維度完全消失的聚合表,舉個例子,例如一個包含有時間、地域、產品三個維度,以及度量值sales的立方體,那么如果有一個聚合表不包含維度,那么就被稱為lost dimension,這里度量sales會被聚合為所有地域下的值。一個聚合表可以把所有維度都消失掉,這個聚合表將只包含一行記錄,代表所有時間、地域、產品維度下的sales總和。
fact table
time_id
product_id
location_id
measure
lost (location_id) dimension table
time_id
product_id
measure (aggregated over location)
fact_count
fully lost dimension table
measure (aggregated over everything)
fact_count
其中,聚合表中的fact_count列是一個附加列,表示事實表中有多少行記錄被聚合到了聚合表中的這一行。
2. collapsed dimension
collapsed dimension表示有維度被退化的聚合表,所謂退化是指某個維度在聚合表中只包含了這個維度的若干級別(Level)。舉個例子,時間維度下包含了day,month,quarter,year級別,而在聚合表中退化成了只包含month這個級別,那么聚合表中不會包含time_id列,而是包含month,quarter和year列。當MDX查詢語句可以用到這個聚合表時,就不再查詢時間維度的維表,而是直接通過聚合表查詢所有有關時間的信息(month,quarter和year)。
time dimension table
time_id
day
month
quarter
year
fact table
time_id
measure
collapsed dimension table
month
quarter
year
measure (aggregated to month level)
fact_count
4.2. 數據庫中創建聚合表
在創建聚合表時,只對聚合表的表名稱和列名稱有所要求。聚合表的名稱以它所對應的事實表的名稱為后綴。聚合表的名稱由三部分組成:
- agg_[第二部分]_[對應的事實表的名字]
其中,第二部分原則上的要求是至少包含一個字符,可以以字母、數字或下划線,但通常會用第二部分說明聚合表的類型並且對聚合表進行編號。例如,事實表的名稱是customer,那么下面這些都是合法的、對應於該事實表的聚合表名:
- agg_01_sales
- agg_02_sales
- agg_l_01_sales
- agg_l_02_sales
- agg_c_01_sales
- agg_lc_01_sales
通常,我們會使用類似后面四個這樣的聚合表名,在聚合表名的第二部分,首先是l或c或lc(分別表示包含lost dimension,collapsed dimension或者同時包含兩者的聚合表),然后是一個下划線,接着后面是聚合表的數字編號。
在給聚合表的列命名時,只要使聚合表中的列名稱和類型與事實表或維表中對應列的名稱一致即可。除此之外,在聚合表中必須新加一列,這一列的名稱會由Schema中的<AggFactCount>標簽所指定(下面會有詳細說明),這一列的作用是統計聚合表中一行聚合了事實表中的行的數目。
另外,聚合表還可以增加一些度量值,增加的度量值所在列的名字由度量方法(sum, max, min, avg)加下滑線再加對應的事實表中的列名字組成。例如,在上圖中的事實表有一個名為units的度量值,在聚合表中如果我們想對這個值求和,那么聚合表中保存對units求和結果的列的名字就可以被命名為sum_units。更具體的內容可以參考:聚合表與事實表的表名和列名匹配規則。
聚集表必須被構建,一般來說,聚合表示非實時的,它們需要被重新構建,例如每天凌晨重新構建一次,供第二天分析。
下面是個簡單的例子,這里有一張sales_fact_1997事實表:
sales_fact_1997
product_id
time_id
customer_id
promotion_id
store_id
store_sales
store_cost
unit_sales
首先我們構建一個時間維度消失了的lost dimension聚合表:

CREATE TABLE agg_l_05_sales_fact_1997 (
product_id INTEGER NOT NULL,
customer_id INTEGER NOT NULL,
promotion_id INTEGER NOT NULL,
store_id INTEGER NOT NULL,
store_sales DECIMAL(10,4) NOT NULL,
store_cost DECIMAL(10,4) NOT NULL,
unit_sales DECIMAL(10,4) NOT NULL,
fact_count INTEGER NOT NULL);
CREATE INDEX i_sls_97_cust_id ON agg_l_05_sales_fact_1997 (customer_id);
CREATE INDEX i_sls_97_prod_id ON agg_l_05_sales_fact_1997 (product_id);
CREATE INDEX i_sls_97_promo_id ON agg_l_05_sales_fact_1997 (promotion_id);
CREATE INDEX i_sls_97_store_id ON agg_l_05_sales_fact_1997 (store_id);
INSERT INTO agg_l_05_sales_fact_1997 (
product_id,
customer_id,
promotion_id,
store_id,
store_sales,
store_cost,
unit_sales,
fact_count)
SELECT
product_id,
customer_id,
promotion_id,
store_id,
SUM(store_sales) AS store_sales,
SUM(store_cost) AS store_cost,
SUM(unit_sales) AS unit_sales,
COUNT(*) AS fact_count
FROM
sales_fact_1997
GROUP BY
product_id,
customer_id,
promotion_id,
store_id;
接下來構建一個collapsed dimension聚合表,其中時間維度退化為月級別:

CREATE TABLE agg_c_14_sales_fact_1997 (
product_id INTEGER NOT NULL,
customer_id INTEGER NOT NULL,
promotion_id INTEGER NOT NULL,
store_id INTEGER NOT NULL,
month_of_year SMALLINT(6) NOT NULL,
quarter VARCHAR(30) NOT NULL,
the_year SMALLINT(6) NOT NULL,
store_sales DECIMAL(10,4) NOT NULL,
store_cost DECIMAL(10,4) NOT NULL,
unit_sales DECIMAL(10,4) NOT NULL,
fact_count INTEGER NOT NULL);
CREATE INDEX i_sls_97_cust_id ON agg_c_14_sales_fact_1997 (customer_id);
CREATE INDEX i_sls_97_prod_id ON agg_c_14_sales_fact_1997 (product_id);
CREATE INDEX i_sls_97_promo_id ON agg_c_14_sales_fact_1997 (promotion_id);
CREATE INDEX i_sls_97_store_id ON agg_c_14_sales_fact_1997 (store_id);
INSERT INTO agg_c_14_sales_fact_1997 (
product_id,
customer_id,
promotion_id,
store_id,
month_of_year,
quarter,
the_year,
store_sales,
store_cost,
unit_sales,
fact_count)
SELECT
BASE.product_id,
BASE.customer_id,
BASE.promotion_id,
BASE.store_id,
DIM.month_of_year,
DIM.quarter,
DIM.the_year,
SUM(BASE.store_sales) AS store_sales,
SUM(BASE.store_cost) AS store_cost,
SUM(BASE.unit_sales) AS unit_sales,
COUNT(*) AS fact_count
FROM
sales_fact_1997 AS BASE, time_by_day AS DIM
WHERE
BASE.time_id = DIM.time_id
GROUP BY
BASE.product_id,
BASE.customer_id,
BASE.promotion_id,
BASE.store_id,
DIM.month_of_year,
DIM.quarter,
DIM.the_year;
4.3. 在Schema中聲明聚合表
在Schema中聲明聚合表時,需要把聲明內容放到<table>標簽中。聲明聚合表時常用的標簽及其含義如下:
<AggName> 和一個聚合表的聲明相關的內容都放在這個標簽內,並且通過這個標簽的name屬性,可以把這部分聲明與數據庫中的一個聚合表對應起來。例如,數據庫中有一個聚合表的名字為:agg_l_01_sales,那么在Schema中可以這樣聲明這個聚合表:
<AggName name="agg_l_01_sales"> ... </AggName>
其中...表示聲明的其他部分,這部分由下面的一個或若干個標簽組成,下面的標簽都在<AggName>中使用,並且它們是平級的,不會相互出現在其他標簽內。
<AggFactCount> 通過這個標簽的column屬性可以指定一個聚合表中用來統計每一行聚合了事實表中多少行的列的名字,例如:
<AggFactCount name="fact_count" />
表示在這個聚合表中用一個名為fact_count的列來統計聚合表的一行聚合了事實表的多少行。
<AggForeignKey> 這個標簽用來把事實表中的一個外鍵同聚合表中含義相同的標簽匹配起來,例如:
<AggForeignKey factColumn="product_id" aggColumn="product_id" />
表示在事實表中有一個外鍵product_id,而在該事實表所對應的聚合表中與它功能相同(是同一張維表的主鍵)的外鍵名字是 product_id。其中factColumn指定事實表中外鍵的名字,aggColumn指定聚合表中相匹配的外鍵的名字。
<AggLevel> 如果聚合表中的維不是一個外鍵,那么需要用這個標簽來聲明聚合表中的這一維。這里舉兩個例子來說明它的用法:
當聚合表中的這一維也是事實表中的一維時(例如上圖中payment那一列),可以這樣寫:
<AggLevel name="[Payment Method].[Payment Method]" column="payment"/>
其中name屬性由兩部分組成,首先是事實表的這一維在Schema中聲明時的維的名稱(由<Dimension>標簽的name屬性所指定),然后加上一個.最后再加上這一維的層次結構(Hierarchy)的名字(由<Dimension>標簽內的<Hierarchy>標簽的name屬性所指定)即可。而column屬性則是聚合表中這一列的名字,此處標簽只指定聚合表中列的名字而 沒有指定事實表中相對應列的名字是因為Mondrian會根據列名字匹配規則自動在事實表中查找相匹配的列。
當聚合表中的這一維是維表中的一維時(例如上圖中month那一列),與上一種情況寫法完全相同即可,並不因為聚合表中這一列對應的是維表中的列而有所不同:
<AggLevel name="[Time].[Month]" column="month"/>
<AggMeasure> 用來聲明聚合表中度量值和事實表中度量值的匹配關系,例如:
<AggMeasure name="[Measures].[Dollar Sales]" column="sum_dollars"/>
其中的name屬性的寫法是[Measures].后面跟上度量值在Schema中聲明時所使用的名字,它由<Measure>標簽中的name屬性所指定。而column的值是聚合表中一列的名字。
5. 在Mondrian中使用聚合表的注意事項
5.1. 在什么情況下Mondrian會使用聚合表
當需要查詢的度量值的維是一張聚合表所包含的維的子集時,這張聚合表就可能會被使用。這里說可能被使用是因為其他聚合表可能也滿足使用條件,這時 Mondrian會首先選擇滿足條件且維數與行數之乘積最少的聚合表,如果有多張滿足條件的聚合表維數相同,Mondrian會選擇一個行數最少的聚合 表。如果沒有聚合表滿足條件,Mondrian會從事實表中進行查詢。詳細內容參考Mondrian配置屬性:mondrian.rolap.aggregates.ChooseByVolume
5.2. Mondrian的聚合表與事實表數據同步的問題
一般來說,事實表中的數據是靜態不變的,目前,Mondrian並不提供聚合表和事實表同步的機制,聚合表的數據需要自己批量導入后計算生成。
也就是說,當事實表被修改時,Mondrian不會對聚合表做相應的更改,Mondrian不提供根據事實表向聚合表中導入數據和同步數據的功能。因此,如果自己的應用場景中事實表中數據是動態變化的,就需要自己考慮如何做到事實表和聚合表的同步更新。
6. Mondrian中聚合表的例子
6.1. 第一個例子
建立一個聚合表Agg_1,結構如下圖所示:
其中,
- Time維度被退化,只提取year、quarter列,忽略month和day列;
- Product相關的兩個維度也在聚合表中被退化;
- Customer維度消失掉了;
- 對於事實表中的每個度量列(
units
,dollars
),
聚合表中可以有一個或多個聚合列(sum units
,min units
,max units
,sum dollars
); - 同時聚合表中還有個度量列row count,表示出現的次數。
聚合表Agg_1對應的Schema聲明如下:

<Cube name="Sales">
<Table name="sales">
<AggName name="agg_1">
<AggFactCount column="row count"/>
<AggMeasure name="[Measures].[Unit Sales]" column="sum units"/>
<AggMeasure name="[Measures].[Min Units]" column="min units"/>
<AggMeasure name="[Measures].[Max Units]" column="max units"/>
<AggMeasure name="[Measures].[Dollar Sales]" column="sum dollars"/>
<AggLevel name="[Time].[Year]" column="year"/>
<AggLevel name="[Time].[Quarter]" column="quarter"/>
<AggLevel name="[Product].[Mfrid]" column="mfrid"/>
<AggLevel name="[Product].[Brand]" column="brand"/>
<AggLevel name="[Product].[Prodid]" column="prodid"/>
</AggName>
</Table>
<!-- Rest of the cube definition -->
6.2. 第二個例子
建立一個聚合表Agg_2,結構如下圖所示:
其中,
- Time維度被退化為year、quarter和month級別;
- Customer維度被退化為state級別;
- Payment Method被退化為Payment Method級別;
- Product維度保持了原始的雪花模型關系。
聚合表Agg_2對應的Schema聲明如下:

<Cube name="Sales">
<Table name="sales">
<AggName name="agg_2_sales">
<AggFactCount column="row count"/>
<AggForeignKey factColumn="prodid" aggColumn="prodid"/>
<AggMeasure name="[Measures].[Unit Sales]" column="sum units"/>
<AggMeasure name="[Measures].[Min Units]" column="min units"/>
<AggMeasure name="[Measures].[Max Units]" column="max units"/>
<AggMeasure name="[Measures].[Dollar Sales]" column="sum dollars"/>
<AggLevel name="[Time].[Year]" column="year"/>
<AggLevel name="[Time].[Quarter]" column="quarter"/>
<AggLevel name="[Time].[Month]" column="month"/>
<AggLevel name="[Payment Method].[Payment Method]" column="payment"/>
<AggLevel name="[Customer].[State]" column="state"/>
</AggName>
</Table>
<Dimension name="Product">
<Hierarchy hasAll="true" primaryKey="prodid" primaryKeyTable="Product">
<Join leftKey="mfrid" rightKey="mfrid">
<Table name="Product"/>
<Table name="Mfr"/>
</Join>
<Level name="Manufacturer" table="Mfr" column="mfrid"/>
<Level name="Brand" table="Product" column="brand"/>
<Level name="Name" table="Product" column="prodid"/>
</Hierarchy>
</Dimension>
<Dimension name="Day" foreignKey="day">
<Hierarchy hasAll="true" primaryKey="day">
<Table name="Time" />
<Level name="Year" column="year" type="Numeric" uniqueMembers="true" />
<Level name="Quarter" column="quarter" uniqueMembers="false" />
<Level name="Month" column="month" type="Numeric" uniqueMembers="false" />
</Hierarchy>
</Dimension>
<Dimension name="Customer" foreignKey="custid">
<Hierarchy hasAll="true" primaryKey="custid">
<Table name="Customer" />
<Level name="City" column="city" uniqueMembers="ture" />
<Level name="State" column="state" uniqueMembers="true" />
</Hierarchy>
</Dimension>
<Dimension name="Payment method">
<Hierarchy hasAll="true">
<Level name="Payment method" column="payment" uniqueMembers="ture" />
</Hierarchy>
</Dimension>
<AggMeasure name="Unit Sales" aggregator="sum" />
<AggMeasure name="Min Units" aggregator="min" />
<AggMeasure name="Max Units" aggregator="max" />
<AggMeasure name="Dollar Sales" aggregator="sum" />
</Cube>
其中,<AggForeignKey>標簽用於聲明prodid列連接到維表的prodid列,其他的所有列仍然從Product和Mfr維表中獲取。
7. 總結
1. 使用Mondrian做大數據量(如>100W行)的OLAP分析時,考慮是否可以使用聚合表進行優化。
2. 然而Mondrian的優化方式又不限於聚合表這一種,是否要進行聚合表優化,要根據實際情況來決定。
3. Mondrian目前並不提供對聚合表的數據同步機制,如果要做實時OLAP,需要自己實現聚合表和事實表中的數據同步。
8. 參考資料
1. Mondiran在線文檔