4.星型模式与雪花模式

我们之前看到的在一个事实表上创建数据立方,在事实表上或与事实表join联接的表上创建维度。这是最常见的映射方法,称之为 星型模式。

但一个维度可能是基于一个以上的表格,指定了定义好的与事实表的路径路径。这种维度称之为雪花模式,并使用<Join>操作进行定义。举个例子:

<Cube name="Sales">

...

<Dimension name="Product" foreignKey="product_id">

<Hierarchy hasAll="true" primaryKey="product_id" primaryKeyTable="product">

<Join leftKey="product_class_key" rightAlias="product_class" rightKey="product_class_id">

<Table name="product"/>

<Join leftKey="product_type_id" rightKey="product_type_id">

<Table name="product_class"/>

<Table name="product_type"/>

</Join>

</Join>

<!-- Level declarations ... -->

</Hierarchy>

</Dimension>

</Cube>


这里定义了一个’product’维度,包含了3个表格。事实表联接’product’表(通过外键product_id),’product’表联接了表’product_class’表(通过外键product_class_id),’product_class’表联接了表’product_type’表(通过外键product_type_id)。我们请求一个嵌套在<Join>元素中的<Join>元素,因为<Join>需要两个操作参数,参数的类型可以是表格,join,或者query查询。


这种表格的安排似乎很复杂,简单的规则是根据表格包含的行数多少来排序。’product’表拥有最多的行,因此它首先联接事实表,并第一个显示;’product_class’行数较少,然后最少的是’product_type’,放在雪花型的最外端。


需要注意的是,外层的<Join>元素有一个rightAlias属性,这是必须要有的,因为当前联接的右边的部分包含一个以上的表。这种情况下,leftAlias属性不需要,因为product表中的leftKey列不会出现冲突。


4.1 共享的维度

当针对一个join生成SQL时,Mondiran需要知道join哪个列。如果联接到一个join,那么我们需要告诉Mondrian那个列属于join联接中的哪个表(通常会是join中的第一个表)。


由于共享的维度不属于某个cube数据立方,我们必须为其指定一个显性的表格(或其他数据源)。当我们在某个特定的cube中使用这个共享维度时,我们需要指定foreign key(外键)。下面的例子展示了 Store Type维度通过外键 sales_fact_1997.store_id 联接到了 Sales数据立方,并通过外键 warehouse.warehouse_store_id  联接到了Warehouse 数据立方。


<Dimension name="Store Type">

<Hierarchy hasAll="true" primaryKey="store_id">

<Table name="store"/>

<Level name="Store Type" column="store_type" uniqueMembers="true"/>

</Hierarchy>

</Dimension>


<Cube name="Sales">

<Table name="sales_fact_1997"/>

...

<DimensionUsage name="Store Type" source="Store Type" foreignKey="store_id"/>

</Cube>


<Cube name="Warehouse">

<Table name="warehouse"/>

...

<DimensionUsage name="Store Type" source="Store Type" foreignKey="warehouse_store_id"/>

</Cube>


4.2 Join 优化

schema模式文件中映射的表格会告诉Mondrian如何去获取数据,但Mondrian足够智能,不需要按行读取模式文件。在生成查询的过程中,Mondrian会应用许多优化策略:

(1)TODO: 描述大批量维度支持

(2)如果一个维度(更准确地说,所获取的维度的层级)在事实表中,Mondrian就不会进行join操作。

(3)如果两个维度通过一样的join路径接入同一张表,那么Mondrian仅仅会join这两个表一次。举个例子,[Gender]和[Age]维度都属于customers表中的字段,通过sales_1997.cust_id = customers.cust_id进行连接。


5 高级逻辑结构

5.1 虚拟数据立方

一个虚拟数据立方包含两个或更多的普通数据立方。虚拟数据立方在<VirtualCube>元素中定义。

<VirtualCube name="Warehouse and Sales">

<CubeUsages>

<CubeUsage cubeName="Sales" ignoreUnrelatedDimensions="true"/>

<CubeUsage cubeName="Warehouse"/>

</CubeUsages>

<VirtualCubeDimension cubeName="Sales" name="Customers"/>

<VirtualCubeDimension cubeName="Sales" name="Education Level"/>

<VirtualCubeDimension cubeName="Sales" name="Gender"/>

<VirtualCubeDimension cubeName="Sales" name="Marital Status"/>

<VirtualCubeDimension name="Product"/>

<VirtualCubeDimension cubeName="Sales" name="Promotion Media"/>

<VirtualCubeDimension cubeName="Sales" name="Promotions"/>

<VirtualCubeDimension name="Store"/>

<VirtualCubeDimension name="Time"/>

<VirtualCubeDimension cubeName="Sales" name="Yearly Income"/>

<VirtualCubeDimension cubeName="Warehouse" name="Warehouse"/>

<VirtualCubeMeasure cubeName="Sales" name="[Measures].[Sales Count]"/>

<VirtualCubeMeasure cubeName="Sales" name="[Measures].[Store Cost]"/>

<VirtualCubeMeasure cubeName="Sales" name="[Measures].[Store Sales]"/>

<VirtualCubeMeasure cubeName="Sales" name="[Measures].[Unit Sales]"/>

<VirtualCubeMeasure cubeName="Sales" name="[Measures].[Profit Growth]"/>

<VirtualCubeMeasure cubeName="Warehouse" name="[Measures].[Store Invoice]"/>

<VirtualCubeMeasure cubeName="Warehouse" name="[Measures].[Supply Time]"/>

<VirtualCubeMeasure cubeName="Warehouse" name="[Measures].[Units Ordered]"/>

<VirtualCubeMeasure cubeName="Warehouse" name="[Measures].[Units Shipped]"/>

<VirtualCubeMeasure cubeName="Warehouse" name="[Measures].[Warehouse Cost]"/>

<VirtualCubeMeasure cubeName="Warehouse" name="[Measures].[Warehouse Profit]"/>

<VirtualCubeMeasure cubeName="Warehouse" name="[Measures].[Warehouse Sales]"/>

<VirtualCubeMeasure cubeName="Warehouse" name="[Measures].[Average Warehouse Sale]"/>

<CalculatedMember name="Profit Per Unit Shipped" dimension="Measures">

<Formula>[Measures].[Profit] / [Measures].[Units Shipped]</Formula>

</CalculatedMember>

</VirtualCube>


其中<CubeUsages> 元素时可选的,它指明了导入到虚拟数据立方中的cubes数据立方。目前,我们可以从基础数据立方中定义一个虚拟数据立方度量以及相似的引入,而不用通过定义这个基础数据立方的CubeUsage。


虚拟数据立方频繁用于现实应用中,当事实表有不同的粒度(比如一个维度是在day层级,另一个维度在month层级时),或者事实表的不同维度(比如一个在Product,Time,Customer上,另一个在Product,Time,Warehouse上),并且希望将数据结果呈现给不知道数据结构情况的最终客户时。


任意的公共维度——即共享维度(被所有数据立方使用)是自动同步的,比如,[Time]和[Product]是共享维度,那么如果当前使用的是[Time].[1997].[Q2][Product].[Beer].[Miller Lite],那么其他数据立方的度量会关联到当前上下文中。


只属于某个数据立方的维度称之为非一致性维度,[Gender]维度就是一个非一致性维度,它在Sales立方中,但不在Warehouse立方中。如果存在[Gender].[F][Time].[1997].[Q1],它会去从[Sales]立方中的[Unit Sales]度量中获取值,却不会从[Warehouse]立方中的[Units Ordered]度量中去获取。此时在[Gender].[F]的值中,[Units Ordered]度量是NULL。


5.2 父-子层次结构

传统的层次结构有一个固定的层级集合,和依附这些层级的成员。比如,Product层级中,任何Product Name层级的成员都有一个父层级在Brand Name层级中,而这个父层级又有一个父层级在Product Subcategory层级中。这种结构特性在处理现实中的数据时,有时会显得过于僵化。


parent-child 层次结构只有一个level层级(不包括特殊的’all’层级),但任何成员都可以拥有同层级的成员作为其父层级。下面是一个典型的Employee的层次结构:

<Dimension name="Employees" foreignKey="employee_id">

<Hierarchy hasAll="true" allMemberName="All Employees" primaryKey="employee_id">

<Table name="employee"/>

<Level name="Employee Id" uniqueMembers="true" type="Numeric" column="employee_id" nameColumn="full_name" parentColumn="supervisor_id" nullParentValue="0">

<Property name="Marital Status" column="marital_status"/>

<Property name="Position Title" column="position_title"/>

<Property name="Gender" column="gender"/>

<Property name="Salary" column="salary"/>

<Property name="Education Level" column="education_level"/>

<Property name="Management Role" column="management_role"/>

</Level>

</Hierarchy>

</Dimension>


其中重要的属性是:parentColumn 和 nullParentValue:

parentColumn属性是一个列的名字,它将一个成员链接到它的父成员中,在本例这种情况下,它就是一个外键列,指向employee的supervisor。

nullParentValue属性的值能代表一个element元素是否有parent。默认值是 null,但由于很多数据库中没有null值的索引,因此schema模式设计者通常使用空字符串,0,或者 -1 代替 null值。


5.2.1 调整parent-child结构

在上述定义的parent-child结构中存在一个严重的问题,比如employee表中包含下面的数据:












如果我们想为Bill做所有的工资预算,我们需要对Eric和 Carla(Bill的下属)还有Mark的工资加总。通常Mondrian生成一个sql的Group by的语句来计算总和,却没有(普遍有效)一个sql结构用于Hierarchies层次结构中。所以,Mondrian默认为每一个supervisor生成一个SQL语句,并获取和汇总supervisor的直接汇报者。

但这种方法有两个缺点,其一,如果一个层次结构中包含了上百个成员时,执行效率不高;其二,由于Mondrian在生成SQL时使用了distinct-count聚合,我们不能定义一个distinct-count度量在任何包含parent-child层次结构的数据立方cube中。


这个问题该如何解决?答案就是:强化数据,因此Mondrian能使用标准SQL时也可以获取它需要的信息。针对这个目的,Mondrian支持一种称之为“closure table 闭合表”的机制。


5.2.2 closure tables闭合表

闭合表就是一个sql表,里面包含了employee与supervisor的对照关系信息和关系深度信息。“关系深度”列不是必须的,但它可以有利于表的统计计算。























在XML文件中,<Closure>元素将level层级映射到<Table>闭合表中。

<Dimension name="Employees" foreignKey="employee_id">

<Hierarchy hasAll="true" allMemberName="All Employees" primaryKey="employee_id">

<Table name="employee"/>

<Level name="Employee Id" uniqueMembers="true" type="Numeric"

column="employee_id" nameColumn="full_name" parentColumn="supervisor_id" nullParentValue="0">

<Closure parentColumn="supervisor_id" childColumn="employee_id">

<Table name="employee_closure"/>

</Closure>

<Property name="Marital Status" column="marital_status"/>

<Property name="Position Title" column="position_title"/>

<Property name="Gender" column="gender"/>

<Property name="Salary" column="salary"/>

<Property name="Education Level" column="education_level"/>

<Property name="Management Role" column="management_role"/>

</Level>

</Hierarchy>

</Dimension>


闭合表使得在单纯的SQL中可以进行汇总计算。尽管这种方法在查询中引入了额外的表,但数据库优化器很擅长处理join连接。建议申明supervisor_id和employee_id为NOT NULL,并建立索引:

CREATE UNIQUE INDEX employee_closure_pk ON employee_closure (

   supervisor_id,

   employee_id);

CREATE INDEX employee_closure_emp ON employee_closure (

   employee_id);


5.3 成员属性Member properties

成员属性定义在<Level>元素的<Property>元素内部,比如:

<Level name="MyLevel" column="LevelColumn" uniqueMembers="true">

<Property name="MyProp" column="PropColumn"  formatter="com.example.MyPropertyFormatter"/>

<Level/>

formatter属性用于定义成员的格式。

一旦属性在模式文件中定义好了,就可以在MDX语句中通过  member.Properties(“propertyName") 函数来使用这个属性。比如:

SELECT {[Store Sales]} ON COLUMNS,

  TopCount(Filter([Store].[Store Name].Members,

                  [Store].CurrentMember.Properties("Store Type") = "Supermarket"),

           10,

           [Store Sales]) ON ROWS

FROM [Sales]


Mondrian会尽可能地推导出成员表达式。如果属性名是一个字符串常量,那么这个属性的类型是来自于属性定义中的type属(String,Numeric,Boolean)性。如果属性的名字是一个表达式(比如:CurrentMember.Properties("Store " + "Type")),Mondrian会返回一个无类型的值。


5.4 计算成员 Calculated members

如果需要生成一个度量,这个度量不是来源于事实表中的列,而是来自于MDX表达式,一种方法是使用WITH MEMBER 语句,比如:

WITH MEMBER [Measures].[Profit] AS '[Measures].[Store Sales]-[Measures].[Store Cost]',

   FORMAT_STRING = '$#,###'

SELECT {[Measures].[Store Sales], [Measures].[Profit]} ON COLUMNS,

  {[Product].Children} ON ROWS

FROM [Sales]

WHERE [Time].[1997]


但是如果不想每次使用时都要在MDX中写一遍的话,可以在模式文件中定义这个计算成员,作为数据立方定义中的一部分:

<CalculatedMember name="Profit" dimension="Measures">

<Formula>[Measures].[Store Sales] - [Measures].[Store Cost]</Formula>

<CalculatedMemberProperty name="FORMAT_STRING" value="$#,##0.00"/>

</CalculatedMember>

其中FORMAT_STRING 的值还可以是表达式:

<CalculatedMemberProperty name="FORMAT_STRING" expression="Iif(Value < 0, '|($#,##0.00)|style=red', ‘|$#,##0.00|style=green')"/>


5.5 Named集合

WITH SET语句能在MDX语句中申明一个集合的表达式,这个集合能在query中使用。比如:

WITH SET [Top Sellers] AS 

    'TopCount([Warehouse].[Warehouse Name].MEMBERS, 5, [Measures].[Warehouse Sales])'

SELECT 

    {[Measures].[Warehouse Sales]} ON COLUMNS,

    {[Top Sellers]} ON ROWS

FROM [Warehouse]

WHERE [Time].[Year].[1997]


类似于WITH MEMBER语句,WITH SET语句也可以在模式文件中使用<NamedSet>元素定义:

<Cube name="Warehouse">
...
<NamedSet name="Top Sellers">
<Formula>TopCount([Warehouse].[Warehouse Name].MEMBERS, 5, [Measures].[Warehouse Sales])</Formula>
</NamedSet>
</Cube>

在MDX中的使用如下:

SELECT 
    {[Measures].[Warehouse Sales]} ON COLUMNS,
    {[Top Sellers]} ON ROWS
FROM [Warehouse]
WHERE [Time].[Year].[1997]

6.插件程序


6.1用户自定义方法

方法中必须要有public类型的构造函数,并implements mondrian.spi.UserDefinedFunction.

如:

public class PlusOneUdf implements UserDefinedFunction {


然后在模式文件中申明:

<Schema>
...
<UserDefinedFunction name="PlusOne" className="com.example.PlusOneUdf"/>
</Schema>

在MDX文件中使用:

WITH MEMBER [Measures].[Unit Sales Plus One] 
    AS 'PlusOne([Measures].[Unit Sales])'
SELECT
    {[Measures].[Unit Sales]} ON COLUMNS,
    {[Gender].MEMBERS} ON ROWS
FROM [Sales]


7.国际化


8. 聚合表 Aggregate tables

当事实表中包含大量的行时,比如百万级或以上的行数,聚合表是提高Mondrian效率的一种方法。一个聚合表本质上是对事实表中的数据进行预先计算汇总。

下面是一个聚合表的简单实例:

<Cube name="Sales">
<Table name="sales_fact_1997">
<AggName name="agg_c_special_sales_fact_1997">
<AggFactCount column="FACT_COUNT"/>
<AggMeasure name="[Measures].[Store Cost]" column="STORE_COST_SUM"/>
<AggMeasure name="[Measures].[Store Sales]" column="STORE_SALES_SUM"/>
<AggLevel name="[Product].[Product Family]" column="PRODUCT_FAMILY"/>
<AggLevel name="[Time].[Quarter]" column="TIME_QUARTER"/>
<AggLevel name="[Time].[Year]" column="TIME_YEAR"/>
<AggLevel name="[Time].[Quarter]" column="TIME_QUARTER"/>
<AggLevel name="[Time].[Month]" column="TIME_MONTH"/>
</AggName>
</Table>
<!-- Rest of the cube definition -->
</Cube>



这个表中没有显示出来的<AggForeignKey>元素,可以让我们在不用引入维度表中的列到聚合表中的前提下,直接引用维度表。在“aggregate tables guide 聚合表使用指导”中有描述。

实际使用过程中,一个基于非常大的事实表的数据立方cube,可能会有多个聚合表。在模式定义的XML文件中,不方便对每张聚合表进行显式地申明,而且这里有更好的方法。下面的例子中,Mondrian通过模式匹配来申明聚合表:

<Cube name="Sales">
<Table name="sales_fact_1997">
<AggPattern pattern="agg_.*_sales_fact_1997">
<AggFactCount column="FACT_COUNT"/>
<AggMeasure name="[Measures].[Store Cost]" column="STORE_COST_SUM"/>
<AggMeasure name="[Measures].[Store Sales]" column="STORE_SALES_SUM"/>
<AggLevel name="[Product].[Product Family]" column="PRODUCT_FAMILY"/>
<AggLevel name="[Time].[Quarter]" column="TIME_QUARTER"/>
<AggLevel name="[Time].[Year]" column="TIME_YEAR"/>
<AggLevel name="[Time].[Quarter]" column="TIME_QUARTER"/>
<AggLevel name="[Time].[Month]" column="TIME_MONTH"/>
<AggExclude name="agg_c_14_sales_fact_1997"/>
<AggExclude name="agg_lc_100_sales_fact_1997"/>
</AggPattern>
</Table>
</Cube>



这个定义告诉Mondrian将所有能匹配上agg_.*_sales_fact_1997 的表作为聚合表,除了agg_c_14_sales_fact_1997 和 agg_lc_100_sales_fact_1997 。Mondrian使用相应的规则来推导这些表中的列所具有的角色,所以依靠严格的命名习惯是很重要的。命名习惯也在“aggregate tables guide ”中做了说明。

针对使用指导的相关建议,在“choosing aggregate tables ”做了说明。


9. 数据获取控制(数据阅读的权限控制)