Nosql精粹读书笔记(一)——聚合数据模型

聚合数据模型

数据模型是认知和操作数据时所用的模型。对于使用数据库的人来说, 数据模型描述了我们如何同数据库中的数据打交道。 它与存储模型不同, 后者描述了数据库内部存储及操作数据的机制。

大家日常所说的“ 数据模型” 一词, 一般指应用程序的特定数据所具备的模型。 开发者可能会指着一张数据库的“ 实体 - 关系图 ”( entity-relationship diagram), 把这个包含客户、 订单、 产品等信息的东西叫做他们的数据模型。然而本书的“ 数据模型” 通常表示数据库组织数据的方式, 它的正式名称是“ 元模型”( metamodel)。

NoSQL 技术与传统的关系型数据库相比, 一个最明显的转变就是抛弃了关系模型。 每种 NoSQL 解决方案的模型都不同, 本书把 NoSQL 生态系统中广泛使用的模型分为四类 :“ 键值”、“ 文档”、“ 列族” 和“ 图”。 前三类数据模型有一个共同特征, 我们称其为“ 面向聚合”( aggregate orientation)。

聚合

关系模型把待存储的信息分隔成元组( 行)。 元组是种受限的数据结构 : 它只能包含一系列的值, 因此不能在元组中嵌套另一个元组, 也不能包含由值或元组所组成的列表。 这种简单的数据结构支撑着关系模型 : 所有操作都必须以元组为目标, 而且其返回值也必须是元组。

面向聚合所用的方式与之不同, 我们通常操作数据时所用的单元, 其结构都比元组集合复杂得多。 如果能够以这种复杂的结构来存放列表或嵌套其他记录结构就好了。大家在后面的章节中将会看到,“键值数据库”、“ 文档数据库”、“ 列族数据库” 都使用这种更为复杂的记录。 然而, 没有公认的术语来称呼这种复杂的记录, 在本书中, 把它叫做“聚合”( aggregate)。

聚合是“ 领域驱动设计”[ Evans] 中的术语。 在领域驱动设计中, 我们想把一组相互关联的对象视为一个整体单元来操作, 而这个单元就叫聚合。 在涉及数据操作与一致性管理时, 更是如此。 一般情况下, 我们通过原子操作( atomic operation) 更新聚合的值, 并且在与数据存储通信时, 也以聚合为单位。 这个定义也非常符合“ 键值数据库”、“ 文档数据库” 和“ 列族数据库” 的工作方式。 因为用聚合为单位来复制和分片显得比较自然, 所以在集群中操作数据库时, 还是使用聚合比较简单一些。 此外,由于程序员经常通过聚合结构来操作数据, 故而采用聚合也能让其工作更为轻松。

关系模型与聚合模型示例



现在我们再来看看, 如果用面向聚合的思路来做, 那么数据模型会是什么样子

这次也要用一些范例数据, 我们使用 JSON 格式来表示, 因为它是 NoSQL 领域中常用的数据格式。

面向聚合的影响

关系型数据库的数据模型中, 没有“ 聚合” 这一概念, 因此我们称之为“ 聚合无知”( aggregate-ignorant)。 NoSQL 领域中的“ 图数据库” 也是聚合无知的。 这一特征并不是坏事。 聚合的边界一般都很难正确划分出来, 当不同场景要使用同一份数据时,更是如此。

选用面向聚合模型的决定性因素, 就在于它非常适合在集群上运行。 大家应该还记得, 这正是 NoSQL 崛起的杀手锏。在集群上运行时, 我们需要把采集数据时所需的节点数降至最小。如果在数据库中明确包含聚合结构, 那么它就可以根据这一重要信息, 知道哪些数据需要一起操作了, 而且这些数据应该放在同一个节点中。

聚合对于事务处理有一个重要影响。 通常情况下, 面向聚合的数据库确实不支持跨越多个聚合的ACID 事务。 取而代之的是, 它每次只能在一个聚合结构上执行原子操作。 也就是说,如果我们想以原子方式操作多个聚合, 那么就必须自己组织应用程序的代码。

键值数据模型与文档数据模型

键值数据库的聚合不透明 , 只包含一些没有太多意义的大块信息 ; 与此相反, 在文档数据库的聚合中, 可以看到其结构。 不透明的优势在于, 聚合中可以存储任意数据。文档数据库则要限制其中存放的内容, 它定义了其允许的结构与数据类型, 而这样做的好处是, 能够更加灵活地访问数据。

在键值数据库中, 要访问聚合内容, 只能通过键来查找。 而使用文档数据库时,则可以用聚合中的字段查询。 我们可以只获取一部分聚合, 而不用获取全部内容, 此外, 数据库还可以按照聚合内容创建索引 。

列族存储

理解列族模型的最好方式也许就是将其视为两级聚合结构( two-level aggregate
structure)。 与“ 键值存储” 相同,第一个键通常代表行标识符, 可以用它来获取想要的聚合。 列族结构与“ 键值存储” 的区别在于, 其“ 行聚合”( row aggregate) 本身又是一个映射, 其中包含一些更为详细的值。 这些“ 二级值”( second-level value)就叫做“ 列”。 与整体访问某行数据一样, 我们也可以操作特定的列。

列族数据库将列组织为列族。 每一列都必须是某个列族的一部分, 而且访问数据的单元也得是列。 这样设计的前提是, 某个列族中的数据经常需要一起访问。

于是, 我们也得出了两种数据组织方式。

  • 面向行( row-oriented): 每一行都是一个聚合( 例如 ID 为 1234 的顾客就是一个聚合), 该聚合内部存有一些包含有用数据块( 客户信息、 订单记录) 的列族。
  • 面向列( column-oriented): 每个列族都定义了一种记录类型( 例如客户信息),其中每行都表示一条记录 。 你可以将数据库中的大“ 行” 理解为列族中每一个短行记录的串接。

总结

键值数据模型将聚合看作不透明的整体,这意味着只能根据键来查出整个聚合, 而不能仅仅查询或获取其中的一部分。

文档模型的聚合对数据库透明, 于是就可以只查询并获取其中一部分数据了, 不过, 由于文档没有模式, 因此在想优化存储并获取聚合中的部分内容时, 数据库不太好调整文档结构。

列族模型把聚合分为列族, 让数据库将其视为行聚合内的一个数据单元。 此类聚合的结构有某种限制, 但是数据库可利用此种结构的优点来提高其易访问性。

MongoDB权威指南笔记

特点

MongoDB是它是一个面向集合的,模式自由的文档型数据库。那么什么是文档性数据库呢?它与其他的数据库的差别在哪里?

  1. 面向集合的存储( Collenction-Orented):适合存储对象及JSON形式的数据。

    意思是数据被分组存储在数据集中, 被称为一个集合( Collenction)。每个集合在数据库中都有一个唯一的标识名 ,并且可以包含无限数目的文档 。集合的概念类似关系型数据库( RDBMS)里的表( table), 不同的是它不需要定义任何模式( schema)。

  2. 模式自由(( schema-free)

    意味着对于存储在 MongoDB 数据库中的文件,我们不需要知道它的任何结构定义。提了这么多次”无模式”或”模式自由 “,它到是个什么概念呢?例如,下面两个记录可以存在于同一个集合里面:
    {“welcome” : “Beijing”}
    {“age” : 25}

  3. 文档型

    意思是我们存储的数据是键-值对的集合,键是字符串,值可以是数据类型集合里的任意类型,包括数组和文档 . 我们把这个数据格式称作 “ BSON ” 即 “ Binary Serialized dOcument Notation.”

适用场景

网站数据: MongoDB 非常适合实时的插入,更新与查询,并具备网站实时数据存储所需的复制及高度伸缩性

缓存:由于性能很高, MongoDB 也适合作为信息基础设施的缓存层。在系统重启之后,由 MongoDB 搭建的持久化缓存层可以避免下层的数据源过载

大尺寸,低价值的数据:使用传统的关系型数据库存储一些数据时可能会比较昂贵,在此之前,很多时候程序员往往会选择传统的文件进行存储

高伸缩性的场景: MongoDB 非常适合由数十或数百台服务器组成的数据库。 MongoDB的路线图中已经包含对 MapReduce 引擎的内置支持

用于对象及 JSON 数据的存储: MongoDB 的 BSON 数据格式非常适合文档化格式的存储及查询

数据逻辑结构

MongoDB 的文档( document), 相当于关系数据库中的一行记录。
多个文档组成一个集合( collection), 相当于关系数据库的表。
多个集合( collection), 逻辑上组织在一起,就是数据库( database)。
一个 MongoDB 实例支持多个数据库( database)。

文档(document)、集合(collection)、数据库(database)的层次结构如下图:

性能篇

MongoDB 提供了多样性的索引支持,索引信息被保存在 system.indexes 中,且默认总是为_id
创建索引 ,它的索引使用基本和 MySQL 等关系型数据库一样。

基础索引

在字段 age 上创建索引, 1(升序);-1(降序)
db.t3.ensureIndex({age:1})

文档索引

db.factories.insert( { name: "wwl", addr: { city: "Beijing", state: "BJ" } } );
db.factories.ensureIndex( { addr : 1 } );
//下面这个查询将会用到我们刚刚建立的索引
db.factories.find( { addr: { city: "Beijing", state: "BJ" } } );
//但是下面这个查询将不会用到索引,因为查询的顺序跟索引建立的顺序不一样
db.factories.find( { addr: { state: "BJ" , city: "Beijing"} } );

组合索引

db.factories.ensureIndex( { "addr.city" : 1, "addr.state" : 1 } );
// 下面的查询都用到了这个索引
db.factories.find( { "addr.city" : "Beijing", "addr.state" : "BJ" } );
db.factories.find( { "addr.city" : "Beijing" } );
db.factories.find().sort( { "addr.city" : 1, "addr.state" : 1 } );
db.factories.find().sort( { "addr.city" : 1 } )

架构篇

MongoDB 支持在多个机器中通过异步复制达到故障转移和实现冗余。多机器中同一时刻只有一台是用于写操作。正是由于这个情况,为 MongoDB 提供了数据一致性的保障。担当Primary 角色的机器能把读操作分发给 slave。

复制集

MongoDB 高可用可用分两种:

  • Master-Slave 主从复制:

    只需要在某一个服务启动时加上–master 参数,而另一个服务加上–slave 与–source 参数,即可实现同步。 MongoDB 的最新版本已不再推荐此方案。

  • Replica Sets 复制集:

    MongoDB 在 1.6 版本对开发了新功能 replica set,这比之前的 replication 功能要强大一些,增加了故障自动切换和自动修复成员节点,各个 DB 之间数据完全一致,大大降低了维护成功。 auto shard 已经明确说明不支持 replication paris,建议使用 replica set, replica set故障切换完全自动。

Sharding 分片

这是一种将海量的数据水平扩展的数据库集群系统,数据分表存储在 sharding 的各个节点上,使用者通过简单的配置就可以很方便地构建一个分布式 MongoDB 集群。

要构建一个 MongoDB Sharding Cluster,需要三种角色:

  • Shard Server

    即存储实际数据的分片,每个 Shard 可以是一个 mongod 实例,也可以是一组 mongod 实例构成的 Replica Set。为了实现每个 Shard 内部的 auto-failover, MongoDB 官方建议每个 Shard为一组 Replica Set。

  • Config Server

    为了将一个特定的 collection 存储在多个 shard 中,需要为该 collection 指定一个shard key,例如{age: 1} ,shard key 可以决定该条记录属于哪个 chunk。 Config Servers 就是用来存储:所有 shard 节点的配置信息、每个 chunk 的 shard key 范围、 chunk 在各 shard 的分布情况、该集群中所有 DB 和 collection 的 sharding 配置信息。

  • Route Process

    这是一个前端路由,客户端由此接入,然后询问 Config Servers 需要到哪个 Shard 上查询或保存记录,再连接相应的 Shard 进行操作,最后将结果返回给客户端。客户端只需要将原本发给 mongod 的查询或更新请求原封不动地发给 Routing Process,而不必关心所操作的记录存储在哪个 Shard 上。

Replica Sets + Sharding

MongoDB Auto-Sharding 解决了海量存储和动态扩容的问题,但离实际生产环境所需的高可
靠、高可用还有些距离,所以有了 ” Replica Sets + Sharding”的解决方案:

  • Shard:

    使用 Replica Sets,确保每个数据节点都具有备份、自动容错转移、自动恢复能力。

  • Config:

    使用 3 个配置服务器,确保元数据完整性

  • Route:

    使用 3 个路由进程,实现负载平衡,提高客户端接入性能

mondrian 源码解读(九)-聚集层

包mondrian.rolap.agg,管理聚合缓存,这些缓存中包含着各单元格值。
RolapStar中含有aggregation(聚合),一个aggregation是针对一组columns的,该聚合可以包含多个segment,同一个aggregation中的每个segment都将覆盖到相同的列集合;每个segment表达了一组cell值,这些cell自然是由具体的列值和一个必备度量值(如下面的unit sales)来限定,如:(Unit sales, Gender = 'F', State in {'CA','OR'}, Marital Status = anything),由于其中的列值可能会取多个,因此最终表达的cell值也可能是多个。RolapStar中有一个aggregations,是一个map对象,通过request的constrainedColumnsBitKey来索引一个aggregation。

聚合装载过程

实际中无论是底层的单元值还是聚合后的单元值都是放在聚合对象aggregation中的。以aggregation.load(colums,measures,predicates,pinnedSegments)为入口:

参数中的除了measure不一样外,其限定的列(colums)及列值(prediactes)都是一致的。因此转换成对对若干segment[]的求值: Segment.load(segment[],….),在该方法内部:

  • 首先根据segments中的信息生成sql查询语句,有两个不同的生成类:AggQuerySpec和SegmentArrayQuerySpec,前者用于找到聚合表情况下的sql语句生成,后者用于基于原始表的sql语句生成。具体可以参见它们的generateSqlQuery()方法,这里注意对以distinct count有不同的生成方法。Sql生成的核心类是sqlQuery,类似于交换系统中的QuerySqlFactory类。注意:聚合操作如avg、sum等都最终还是利用sql语句实现的,并非mondiran自己实现这些聚合功能。
  • 利用jdbc,执行sql语句,获取到jdbc 结果集。参见mondrian.rolap.RolapUtil.executeQuery()方法。
  • 解析结果集,将结果集中的数据填充到rows[][]二维数值中,并且把各列的值也填充好。如图:
    结果集每条记录的值如宁波市、G010….,前面两个是维度列值,后面几个是度量值。

    各列的值(其中第0项值为:[宁波市]):
  • 决定采用稀疏性(sparse)还是稠密性(dense)SegmentDataSet存储(如果是稠密的,就用数组存储,如果是稀疏的,则用Map存储);并创建该空的DataSet对象。每个segment关联一个DataSet对象;但其稀疏性还是稠密性都是一致的。注意dataset中单元值的个数可能是1个或多个,是由各限定列的指定值个数乘积,若所有限定列都取单值,则显然最终决定一个唯一的单元。
  • 将上述的rows中间集转换到SegmentDataSets集中。最后再分拣给每个segment,确保每个segment的setData(SegmentDataSet)被调用。

segment详解

我们看Segment里面都放了什么:

protected final Column[] columns; //约束列
public final MeasureColumn measure;//这个segment是针对哪个度量的。
private RolapModel model; //对模型的引用
public final StarColumnPredicate[] predicates; //Segment存在哪些断言

从这里我们可以看出,Segment就是对某个Cube的断面做的定义。那它的单元值是存在哪里呢?
就是我们在上面讲的到SegmentDataSet,由它来存储单元值。那么如何关联Segment与SegmentDataSet呢?这里我们有要讲到SegmentWithData。我们看他的定义:

public class SegmentWithData extends Segment{
    final SegmentAxis[] axes;//一组维度的约束
    private final SegmentDataset data;//数据存储
...
    //此方法判断SegmentWithData中是否存在以keys为维度的单元值
    public Object getCellValue(Object[] keys) {
        assert keys.length == axes.length;
        int missed = 0;
        CellKey cellKey = CellKey.Generator.newCellKey(axes.length);
        for (int i = 0; i < keys.length; i++) {
            Comparable key = (Comparable) keys[i];
            int offset = axes[i].getOffset(key);
            if (offset < 0) {
                if (axes[i].wouldContain(key)) {
                    // see whether this segment should contain this value
                    missed++;
                    continue;
                } else {
                    // this value should not appear in this segment; we
                    // should be looking in a different segment
                    return null;
                }
            }
            cellKey.setAxis(i, offset);
        }
        if (isExcluded(keys)) {
            // this value should not appear in this segment; we
            // should be looking in a different segment
            return null;
        }
        if (missed > 0) {
            // the value should be in this segment, but isn't, because one
            // or more of its keys does have any values
            return FunUtil.nullValue;
        } else {
            //cellkey是对一组维度的值得定义
            Object o = data.getObject(cellKey);
            if (o == null) {
                o = FunUtil.nullValue;
            }
            return o;
        }
    }
}    

在这里,SegmentWithData包装了Segment与它单元值之间的关系,通过SegmentAxis来判断当前比较的值是否相同。

下图是有两个限定列的两个segment的描述(注:其中roadid列虽然指定了8个候选值,但由于使用了空行/列过滤,最后只剩下两个路线有值,故最后segment结果集的单元数也只有两个,对应于G010和G318的):


其中第二个对应的dataset为:
[317.769, 120.604]
对应的透视界面为(参见其中的“观测里程”度量值,与上面的dataset一致):

再譬如有三个限定列的segment描述,它们位于另一个aggreation对象中:(其中timeId列的any代表所以可能的时间值,共有2003~2005三个年,所以最终该segment共有3个cell值)

对应的dataset为:
[129.910, 129.909, 57.950]
对应的透视界面为(显然该aggreation还有另外一个segment,其中的roadId对应于G010—宁波梁辉):

再譬如维度中有多个层次的情况时,一个维度会对应多个列:

Query query = connection.parseQuery(
    "SELECT" +
    " {[Time].[1997]," +
    " [Time].[1997].Children} ON COLUMNS," +
    " {[Customer].[USA]," +
    " [Customer].[USA].[OR]," +
    " [Customer].[USA].[WA]} ON ROWS" +
    "FROM [Sales]");
Result result = connection.execute(query);

该语句执行后产生的segment分别为(除了第一个外,其他segment都会包含多个cell,因为它们的限定列中含有多值的情况):

Segment YN#1    Year Nation Unit Sales
                1997 USA    xxx
Predicates: Year=1997, Nation=USA

Segment YNS#1    Year Nation State Unit Sales
                1997 USA    OR    xxx
                1997 USA    WA    xxx
Predicates: Year=1997, Nation=USA, State={OR, WA}

Segment YQN#1    Year Quarter Nation Unit Sales
                1997 Q1      USA    xxx
                1997 Q2      USA    xxx
Predicates: Year=1997, Quarter=any, Nation=USA

Segment YQNS#1    Year Quarter Nation State Unit Sales
                1997 Q1      USA    OR    xxx
                1997 Q1      USA    WA    xxx
                1997 Q2      USA    OR    xxx
                1997 Q2      USA    WA    xxx
Predicates: Year=1997, Quarter=any, Nation=USA, State={OR, WA}

mondrian 源码解读(八)-计算结果

上一节我们了解了member的查询过程。下面,我们将进入mdx的执行过程。

首先,我们需要了解3个重要的类,结果集RolapResult,求值上下文RolapEvaluator与单元格读取CellReader。这3个类的关系是首先由在RolapResult初始化构造函数中传入一个Query,由Query生成一个RolapEvaluator,然后由Query中的Calc根据RolapEvaluator来获取轴上的成员。获取到所有的成员之后,RolapEvaluator根据成员调用CellReader获取到当前值。

RolapResult

RolapResult是一个运行中的请求的结果集。

Mondiran的执行结果由RolapResult类表单,由于mdx查询语句本身就包含on rows(行轴上)、on columns(列轴上)和where部分(切片轴上),结果集中相对应的为ROlapAxis对象,这其中有个sliceAxis对象。因此结果集是由若干ROlapAxis对象和一个RolapCell组构成的。每个axis对象又由若干Position对象组成,每个Position对象又可能由若干member组成(注意一个postion会横跨多个维度的成员)。注意ROlapAxis是抽象类,实际的对象类可能随着不同的轴是不同的。如图:

图中,column轴上两个position(每个position含有一个成员),分别是:

 [[Measures].[YJD]]
[[Measures].[GCLC]]

Row轴上有三个position(每个position含有二个成员),分别是:

[[dimLX].[All dimLXs], [dimTime].[All dimTimes]]
[[dimLX].[All dimLXs].[宁波—梁辉], [dimTime].[All dimTimes]]
[[dimLX].[All dimLXs].[同江-三亚], [dimTime].[All dimTimes]]

切片轴上则有一个position:

[[dimStation].[All dimStations].[宁波市]]

单元值们则放置在RolapResult中的cellInfos对象里,属CellInfoContainer接口,其中存放着CellInfo,并通过Cellkey进行索引。

CellKey:用于在maps里访问cellinfo时使用的键值,根据cell的位置来决定键值。CellKey共有四个默认实现,及zero、one、two、three和many版的实现,分别对应着轴的个数。这些类中关键的属性便是存储各轴的位置值。

CellInfo、CellInfoContainer:内部类。CellInfo包含了一个cell所需要的所有信息(最关键的包含value值和一些formatter设置);最终将作为构造ROlapCell对象的参数。CellInfoContainer显然是cellInfo的容器,并使用CellKey来索引。

ROlapCell:最终返回给jpivot的cell单元值。

RolapEvaluator

RolapEvaluator即在多维环境中计算表达式。
该类中维护一个很重要的对象,即currentMembers,该上下文对象针对每个维度都包含了一个成员;通过setContext方法用来设置当前维度,以开始计算当前维度组合下的表达式值。

该类有一个方法:
public final Object evaluateCurrent()
该方法就是对单元格的求值,在该方法中,规定了solver order的求解顺序。

CellReader

CellReader即单元格读取。

Cells会被求值多次。第一次时, Evaluator使用FastBatchingCellReader来求值。当一个单元被求值时,evaluateCurrent()被调用。此时FastBatchingCellReader并没有被调用,而是为那个cell记录了一个 CellRequest并且return (not throw) an exception。在所有的cells都有了对应的CellRequests之后, Aggregation会生成 SQL,以一个单独的sql请求来载入所有的cells。然后由AggregatingCellReader 重新计算cells,从缓存中返回cells值。

FastBatchingCellReader

主要方法,Object get(Evaluator evaluator)

  1. 首先根据当前的上下文环境(即一组members)创建cellRequest,cellRequest中包含了所有必要的从star中取值的信息。该组members的交集便是要求值的单元格,其中切片轴上的成员和其他轴上的成员完全同等对待;其中度量轴上的成员要求上StoredMeasure(非计算成员CaculatedMember);度量值上的成员位于第一个。通过调用request的addConstrainedColumn()方法把各member对应的column和value(属StarColumnPredicate)值加至到request中.
  2. 调用AggregationManager.getCellFromCache(request,pinnedSegments)方法从缓存中获取cell值。首先根据request中的列组索引标识从缓存中获取aggreation缓存对象,如果为空说明缓存还未建立则直接返回null,如果有值则调用aggregation.getCellValue(measure,colValueKeys)方法获取缓存的cell值;getCellValue内部首先会根据measure查找匹配的segment,然后调用segment.getCellValue(keys)从segment的dataset缓存集中查找相应的cell值。
  3. 如果getCellFromCache返回为null则调用recordCellRequest()记录需求。这些cell request会被组织成多个cell request batch,以便将来聚合层进行批读取以提高效率。关于batch的详细讨论参见下面Batch类章节。
  4. 上层会在适当的时候调用batchCellReading.loadAggregations()以实际读取这些cell值,前提是batches对象中已有cellRequest了。每个batch的读取参见batch. loadAggregation()方法,最终调用聚合层的方法,参见aggreation.load(….)。

FastBatchingCellReader.Batch类

每个batch对应与一组特定的columns环境下的cell求取(具有相同的列和列值(列值是具体的值,不会是“all”值));从batch的属性可以看出batch包含了哪些上下文:

  1. RolapStar.Column[],这个指明了基于哪些列(也即基于哪些维度,包括切片维度)进行读取;
  2. Set[],保存了每列的限定值,对于一列而言,限定值可能会有多个(毕竟是批处理,一次请求多个);
  3. MeasureList,指明求取哪些度量值上的cell(度量值本质是度量维上的限定值)。
  4. BitKey,该batch的唯一索引。
    如图所示的一个mdx查询结果界面:

此时会产生两个batch,每个batch最终可能会产生若干segment,segment是cells的集合,

  1. 一个batch是(其中“当量数/适应交通量=拥挤度”,拥挤度是计算成员),最终产生3个segment,每个segment只有一个cell:

    (地市=’宁波市’,measure=’观察里程’)

    (地市=’宁波市’,measure=’当量数’)
    

    (地市=’宁波市’,measure=’适应交通量’)

  2. 另一个batch是(其中的G310等是路线代码,最终过滤掉空值后就剩下两个了) ,最终产生3个segment,每个segment有多个cell:

    (地市=’宁波市’,roadId in (G310,G322,G210,S321….),measure=’观察里程’)
    (地市=’宁波市’,roadId in (G310,G322,G210,S321….),measure=’当量数’) 
    (地市=’宁波市’,roadId in (G310,G322,G210,S321….),measure=’适应交通量’)
    

此次:mdx的执行过程就分析完了。
ps:最后这一点来源于网络,由于从文档上无法得知作者来源,如有侵权还望见谅。

Oracle rollup、cube、grouping sets

Oracle的group by除了基本用法以外,还有3种扩展用法,分别是rollup、cube、grouping sets。

1 rollup

假设有一个表test,有A、B、C、D、E5列。
如果使用group by rollup(A,B,C),首先会对(A、B、C)进行GROUP BY,然后对(A、B)进行GROUP BY,然后是(A)进行GROUP BY,最后对全表进行GROUP BY操作。roll up的意思是“卷起”,这也可以帮助我们理解group by rollup就是对选择的列从右到左以一次少一列的方式进行grouping直到所有列都去掉后的grouping(也就是全表grouping),对于n个参数的rollup,有n+1次的grouping。以下2个sql的结果集是一样的:

Select A,B,C,sum(E) from test group by rollup(A,B,C)

Select A,B,C,sum(E) from test group by A,B,C union all Select A,B,null,sum(E) from test group by A,B union all Select A,null,null,sum(E) from test group by A union all Select null,null,null,sum(E) from test

2 cube

cube的意思是立方,对cube的每个参数,都可以理解为取值为参与grouping和不参与grouping两个值的一个维度,然后所有维度取值组合的集合就是grouping的集合,对于n个参数的cube,有2^n次的grouping。如果使用group by cube(A,B,C),,则首先会对(A、B、C)进行GROUP BY,然后依次是(A、B),(A、C),(A),(B、C),(B),(C),最后对全表进行GROUP BY操作,一共是2^3=8次grouping。同rollup一样,也可以用基本的group by加上结果集的union all写出一个与group by cube结果集相同的sql:
Select A,B,C,sum(E) from test group by cube(A,B,C);

Select A,B,C,sum(E) from test group by A,B,C union all Select A,B,null,sum(E) from test group by A,B union all Select A,null,C,sum(E) from test group by A,C union all Select A,null,null,sum(E) from test group by A union all Select null,B,C,sum(E) from test group by B,C union all Select null,B,null,sum(E) from test group by B union all Select null,null,C,sum(E) from test group by C union all Select null,null,null,sum(E) from test;

3 grouping sets

grouping sets就是对参数中的每个参数做grouping,也就是有几个参数做几次grouping,例如使用group by grouping sets(A,B,C),则对(A),(B),(C)进行group by,如果使用group by grouping sets((A,B),C),则对(A,B),(C)进行group by。甚至grouping by grouping set(A,A)都是语法允许的,也就是对(A)进行2次group by,grouping sets的参数允许重复

4 总结

  • rollup (N+1个分组方案)
  • cube (2^N个分组方案)
  • grouping sets (自定义罗列出分组方案)

5 注意点

5.1 机制不同

在rollup和cube的说明中分别给出了用基本group by加结果集union all给出了结果集相同的sql,但这只是为了理解的方便而给出的sql,并不说明rollup和cube与基本group by加结果集union all等价。实际上两者的内部机制是安全不一样的,前者除了写法简洁以外,运行时不需多次扫描表,效率远比后者高。

5.2 集合可运算

3种扩展用法的参数可以是源表中的某一个具体的列,也可以是若干列经过计算而形成的一个新列(比如说A+B,A||B),也可以是这两种列的一个集合(例如(A+B,C)),对于grouping set更是特殊,可以是空集合(),表示对全表进行group by。

5.3 group by 与 rollup, cube组合使用

Group by的基本用法以及这3种扩展用法可以组合使用,也就是说可以出现group by A,rollup(A,B)这样的用法,oracle将对出现在group by中的每种用法的grouping列集合做笛卡尔积然后对其中的每一个元素做group by。这话说起来挺绕口,举例说明吧,group by A, rollup(A,B),基本用法的grouping集合是(A),rollup(A,B)的grouping集合是((A,B),(A),()),两个集合的笛卡尔积集合是((A,A,B),(A,A),(A)),所以会首先对(A,A,B)做group by,然后对(A,A)做group by,最后对(A)做group by。实际上对(A,A,B)做group by和对(A,B)做group by两者是完全等价的(group by A,A,B结果和group by A,B完全一样),同理对(A,A)做group by和对(A)做group by也是等价的。简化后的结果就是首先对(A,B)做group by,然后对(A)做group by,最后再对(A)做group by。下面给出两个等价的sql以便理解:
Select A,B,sum(E) from test1 group by A, rollup(A,B);

Select A,B,sum(E) from test1 group by A,B Union all Select A,null,sum(E) from test1 group by A Union all Select A,null,sum(E) from test1 group by A;

6 grouping()、grouping_id()、group_id()

6.1 grouping()

参数只有一个,而且必须为group by中出现的某一列,表示结果集的一行是否对该列做了grouping。对于对该列做了grouping的行而言,grouping()=0,反之为1;

6.2 grouping_id()

参数可以是多个,但必须为group by中出现的列。Grouping_id()的返回值其实就是参数中的每列的grouping()值的二进制向量,例如如果grouping(A)=1,grouping(B)=0,则grouping_id(A,B)的返回值就是二进制的10,转成10进制就是2。

6.3 group_id()

无参数。见上面的说明3),group by对某些列的集合会进行重复的grouping,而实际上绝大多数情况下对结果集中的这些重复行是不需要的,那就必须有办法剔出这些重复grouping的行。当结果集中有n条重复grouping而形成的行时,每行的group_id()分别是0,1,…,n,这样我们在条件中加入一个group_id()<1就可以剔出这些重复grouping的行了。

7 示例

7.1 建表与数据

SQL> create table test(department_id number, a varchar2(20), b varchar2(20));

Table created

SQL> insert into test values(10, 'A', 'B');

1 row inserted

SQL> commit;

Commit complete

7.2 查询语句

select department_id, a, b, grouping(department_id), grouping(a), grouping(b) from test group by rollup(department_id, a, b) order by 4, 5, 6; select department_id, a, b, grouping(department_id), grouping(a), grouping(b) from test group by cube(department_id, a, b) order by 4, 5, 6;

mondrian 源码解读(番外篇)-Calc层次结构

接口层

首先我们看Calc的接口。
他下面还有11个其他类型的计算器接口,分别是:

  • VoidCalc 接口 void evaluateVoid(Evaluator evaluator)
  • MemberCalc 接口 Member evaluateMember(Evaluator evaluator)
  • LevelCalc 接口 Level evaluateLevel(Evaluator evaluator)
  • DateTimeCalc 接口 Date evaluateDateTime(Evaluator evaluator)
  • DimensionCalc 接口 Dimension evaluateDimension(Evaluator evaluator)
  • HierarchyCalc 接口 Hierarchy evaluateHierarchy(Evaluator evaluator)
  • DoubleCalc 接口 double evaluateDouble(Evaluator evaluator)
  • BooleanCalc 接口 boolean evaluateBoolean(Evaluator evaluator)
  • TupleCalc 接口 Member[] evaluateTuple(Evaluator evaluator)
  • StringCalc 接口 String evaluateString(Evaluator evaluator)
  • IntegerCalc 接口 int evaluateInteger(Evaluator evaluator)

抽象层

AbstractCal实现了Calc接口。
在它下面有10种不同的抽象计算器,分别实现了上述的11中接口类型。
注意AbstractVoidCalc并没有继承。

  • GenericCal 抽象类 实现了所有的Calc下层接口,除了Calc的Object evaluate(Evaluator evaluator)接口
  • AbstractMemberCalc 抽象类 需实现Member evaluateMember(Evaluator evaluator)
  • AbstractLevelCalc 抽象类 需实现Level evaluateLevel(Evaluator evaluator)
  • AbstractDimensionCalc 抽象类 需实现Dimension evaluateDimension(Evaluator evaluator)
  • AbstractHierarchyCalc 抽象类 需实现Hierarchy evaluateHierarchy(Evaluator evaluator)
  • AbstractDoubleCalc 抽象类 需实现double evaluateDouble(Evaluator evaluator)
  • AbstractBooleanCalc 抽象类 需实现boolean evaluateBoolean(Evaluator evaluator)
  • AbstractTupleCalc 抽象类 需实现Member[] evaluateTuple(Evaluator evaluator)
  • AbstractStringCalc 抽象类 需实现String evaluateString(Evaluator evaluator)
  • AbstractIntegerCalc 抽象类 需实现int evaluateInteger(Evaluator evaluator)

通用层

GenericCal实现了这11个接口并继层了AbstractCal,在它下面还有几个实现类

  • AbstractVoidCalc 具体类,不返回任何值,这个类貌似应该继承AbstractCal即可,没必要继承GenericCal。
  • ConstantCalc 具体类 求值结果与原参数相同。
  • TupleValueCalc 具体类 在当前元组上下文中求cell值。
  • ValueCalc 具体类 直接在当前上下文中求Cell值
  • MemberArrayValueCalc 在多个成员上下文中求Cell值
  • CacheCalc 具体类 在缓存中获取Cell值
  • MemberValueCalc 具体类 在当前成员上下文中求Cell值

集合层

还有一种特殊类型IterCalc接口也继承了Calc。
ListCalc接口也继承IterCalc.
他们有4个类

  • AbstractIterCalc实现了IterCalc,并继承了AbstractCal。
  • AbstractListCalc实现了ListCalc,并继承了AbstractCal。
  • GenericIterCalc实现了ListCalc和IterCalc,并继承了AbstractCal。
  • IterableCalc继承了AbstractListCalc

使用

那么问题来了,有了这么多求值器,我应该在程序中使用哪种呢?下面将对如何选择各个层做分析。

  • 如果是求一般的成员,维度,数值,级别等等。那么,使用抽象层即可。
  • 如果是求集(Set)类型的,那么使用集合层。
  • 如果是求Cell值或者其他操作,那么使用通用层。

mondrian 源码解读(番外篇)-MDX函数的执行过程详解

MDX函数的执行过程详解

笔者注:由于将来公司可能会有对函数的二次开发需求,所以为公司写了函数的执行过程,以供将来使用。

一,Query对象的创建

在Query对象中,最主要的是resolve()方法,这里涉及到了2个非常重要的功能。

  • 第一,轴(查询轴和切片轴)上表达式(Exp)的转换。

    主要是将表达式从UnresolvedFunCall转换为ResolvedFunCall。

  • 第二,轴上的计算器(Calc)的创建。

    主要是针对各个轴如何创建计算器。

下面是的resolve()方法解释:

 public void resolve() {
    final Validator validator = createValidator(); //1
    resolve(validator); // resolve self and children ,2
    // Create a dummy result so we can use its evaluator
    final Evaluator evaluator = Util.createEvaluator(this); //3
    ExpCompiler compiler =
            createCompiler(
                    evaluator, validator, Collections.singletonList(resultStyle)); //4
    compile(compiler);  //5
}
  1. 第1行创建校验器(Validator)。
  2. 第2行根据Validator转换所有的UnresolvedFunCall到ResolvedFunCall。
  3. 第3行针对当前Query创建求值器(Evaluator)。
  4. 第4行根据求值器,校验器创建表达式编译器(ExpCompiler)。
  5. 第5行根据表达式编译器编译所有的轴,并创建轴上的计算器。

mondrian 源码解读(七)-读取member

在前面几节中我们了解了cube的创建和query的创建。有了这2样东西后,我们就可以开始执行mdx语句了。在第二小节中,我们了解了mdx的执行顺序。

  1. 先执行from,即表示从哪个cube中获取结果。
  2. 再执行where,with和axis,即获取member阶段。
  3. 有了所有依赖的成员后,最后执行Cell的值。

cube的创建已经在前一节中讲过了,接着是member获取阶段,由于这一阶段分为3部分,内容比较多。所以下面我们先将member是如何读取的,后几节再介绍它的执行过程。

MemberSource

所有对成员的读取操作都是通过MemberSource来完成的。
MemberSource基本功能就是是读取层次的成员操作。
我们看下它的基本接口:

public interface MemberSource {

    RolapHierarchy getHierarchy();

    boolean setCache(MemberCache cache);

    List<RolapMember> getMembers();

    List<RolapMember> getRootMembers();

    void getMemberChildren(
        RolapMember parentMember,
        List<RolapMember> children);

    void getMemberChildren(
        List<RolapMember> parentMembers,
        List<RolapMember> children);

    int getMemberCount();

    RolapMember lookupMember(
        List<Id.Segment> uniqueNameParts,
        boolean failIfNotFound);
}    

从上面我们可以看到,有3个最基本的接口,分别是获取层次,即当前的MemberSource属于哪个层次,设置缓存对象(为了从缓存中读取member),和获得当前层次的成员。这就是MemberSource的核心功能。

MemberReader

我们先看MemberReader的层次图。

从上图中我们看到,MemberReader是继承的MemberSource,所以MemberReader具有MemberSource所有的功能。那么它有哪些功能呢?
我们看看它的接口:

interface MemberReader extends MemberSource {

    RolapMember getLeadMember(RolapMember member, int n);


    List<RolapMember> getMembersInLevel(
        RolapLevel level);


    void getMemberRange(
        RolapLevel level,
        RolapMember startMember,
        RolapMember endMember,
        List<RolapMember> list);

    int compare(
        RolapMember m1,
        RolapMember m2,
        boolean siblingsAreEqual);

    Map<? extends Member, Access> getMemberChildren(
        RolapMember member,
        List<RolapMember> children,
        MemberChildrenConstraint constraint);

    Map<? extends Member, Access> getMemberChildren(
        List<RolapMember> parentMembers,
        List<RolapMember> children,
        MemberChildrenConstraint constraint);

    List<RolapMember> getMembersInLevel(
        RolapLevel level,
        TupleConstraint constraint);

    int getLevelMemberCount(RolapLevel level);

    MemberBuilder getMemberBuilder();

    RolapMember getDefaultMember();

    RolapMember getMemberParent(RolapMember member);

    RolapMember substitute(RolapMember member);

    RolapMember desubstitute(RolapMember member);

    RolapMember getMemberByKey(
        RolapLevel level,
        List<Comparable> keyValues);
}

从上面大家发现没,MemberReader更细化功能,它可以获取到Level上的成员,也可以获取成员的父成员或者成员的子成员,并且在获取成员的时候可以添加约束条件。有了这些功能,我们就可以任意的获取成员了,所以MemberReader是成员获取的主接口。

具体实现

SmartMemberReader

SmartMemberReader实现了MemberReader接口,它实现了维度成员及其子成员的缓存,如果有一个成员位于缓存中,则还会有一个其子成员的列表。它同时缓存了level下的成员们。该类主要的成员有:

  • source:MemberReader,用于实际从数据库中读取维度成员值。
  • mapMemberToChildren:map,实现成员及其子成员的映射, key为RolapMember,value为List
  • mapKeyToMember: map ,实现所有成员的缓存,其中的key为MmberKey
  • mapLevelToMembers: map,实现级别及其所有成员的映射, key为RolapLevel,value为List

SqlMemberSource

SqlMemberSource是最终更数据库打交道的类,它会针对要查询对Level生成一个sql,该sql最终通过数据库执行,将查询结果返回后拼装成member。需要注意的一点是,如果该member有parentmember,必须要生成其对应的parentmember。生成过程即先递归的在level中查找其上一级level的member是否缓存,如果没有缓存,则将其加入查询列表中,直到上一级为空或缓存了member。找到所有要查询的level后,最后一次性生成sql向数据库查询数据。

SmartMemberReader的source其实为mondrian.rolap.SqlMemberSource类,该类中反过来又存储了SmartMemberReader对象,作为其cache成员属性。成员读取过程:

  1. smartMemberReader.getMemberChildren(parentMembers,children,constrain);
  2. 最终通过source.getMemberChildren()…,其中反过来会把找到的children赋予mapKeyToMember。
  3. 最终除了将结果返回在children输出参数中,同时也对mapMemberToChildren赋值了。

mondrian 源码解读(六)-创建CUBE

在第4节中,构建query的时候,有一个非常重要的东西被我忽略了,那就是构建cube Util.lookupCube(statement.getSchemaReader(), cube, true),下面我们来谈谈cube是如何构建的。

Cube是什么?

在第2节中,我们已经谈论了cube,即维度和度量的组合。

RolapCube

在理解了Cube后,我们就可以理解RolapCube了。根据字面意思,我们也可以理解为关系型联机分析处理cube。我们看看它里面主要是实体对象:

//不解释
protected Dimension[] dimensions;

//
private final RolapHierarchy measuresHierarchy;

/**
 * List of calculated members.
 * 定义在schema中的计算成员,注意公式是如何转换的呢?
 */
private final List<Formula> calculatedMemberList = new ArrayList<Formula>();

private RolapStar star; //关联的星型模型

//在Evaluator中用于计算当前成员是否为空,如果当前度量成员中有count聚合,即为当前成员,如果没有找到,创建一个虚拟的count聚合为测量的成员。
RolapBaseCubeMeasure factCountMeasure;

calculatedMemberList这里谈一下。如何将schema中定义的计算成员转换成已近解析好的Formula?
这里其实很简单,通过构造一个特定的mdx:
with member [Measures].[aa] as 'expression1' , [Measures].[bb] as 'expression2'select from cube
然后使用parser解析器解析mdx后返回一个query,query里面的计算成员即为此计算成员。

RolapStar是什么?

我们可以将它理解为一个关联了物理表之间的关系的对象。通过它来生成特定的sql,查询需要的数据。
具体实体对象大家可以打开看看,这里不做详细的解释了。

private DataSource dataSource;
private final Table factTable;
private final List<Column> columnList = new ArrayList<Column>();

Table对象..星型模型
public static class Table {
    private final RolapStar star;
    private final MondrianDef.Relation relation;
    private final List<Column> columnList;
    private final Table parent;
    private List<Table> children;
    private final Condition joinCondition;
    private final String alias;
}

源码修改

cube的创建是根据schema中定义的属性配置来创建cube,这部分没有细看,如何创建其实我们并不关系,理解cube的数据结构就行了。

我们在修改mondrian源码时,将cube创建这块全部去掉了,取而代之的使用了自己的cube创建方式。由于我们底层的存储结构不依赖与xml,而是我们自己定义好的实体间的关系,所以我们将我们的实体转换成了cube。

mondrian 源码解读(五)-访问者模式

在上一节中,我们看到在resolve()方法中使用了大量的访问者模式,所以下面我们总结下:

什么是访问者模式

我发现还是GOF书中定义的最好,就拿过来了。

意图

表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提
下定义作用于这些元素的新操作。

结构图