SPL 虚表的数据类型优化
物理上存储数据的表(简称物理表)往往会采用一些有利于提升性能、减小空间占用的存储机制。
但是,这些机制往往会降低数据可读性,且增加使用的复杂度。比如用整数代替枚举值,或者用整型字段的二进制位存储布尔值。整数、二进制位没有字符串和布尔型那么容易读懂,而且我们还要为这些物理值编写类型转换、二进制计算等代码。
SPL虚表是定义在物理表上的逻辑表,我们可以在虚表中定义更方便使用的逻辑字段。这样的逻辑字段称为伪字段,而物理表中真实存在的字段称为真字段。
虚表通过伪字段和真字段的映射,将特殊的存储结构封装起来,实现了物理表存储机制的透明化。这样既有利于减小磁盘占用、提高性能;又能方便人们读懂数据,降低使用复杂度。
下面我们详细介绍虚表中不同类型的伪字段用法。
枚举维度
在实际应用中,很多枚举类型字段的取值范围是字符串集合。物理表中一般会用整数来代替字符串。整数比字符串占用空间小、运算速度快,但却无法像字符串那样容易被人读懂和使用。
如果将字符串和数值一起存储下来可以兼顾性能和易用性。但这样做会增加生成数据的时间、占用更多的磁盘空间。有些情况下,还有可能出现字符串和数值不一致的错误。
例如交易明细这个物理表,数据结构大概是下图这样:
detail 表的枚举字段 device 中存储的是整数 1、2、3,分别对应三个设备类型 computer、smart phone、pad。现在要完成数据过滤计算,条件是本交易所使用的设备类型是智能手机或者平板电脑。再按照交易日期(年月)、设备类型等分组求总金额和笔数。结果集要以方便阅读的字符串方式展现。
这种情况可以使用虚表的枚举维度,用伪字段定义数值和字符串的对应关系。
定义枚举维度后,计算表达式中就能直接使用字符串,计算结果也能得到字符串。而物理表还可以继续存储数值以达到高性能目标。
下图中,虚表 p_detail 就定义了一个伪字段 deviceString 用于枚举维度的透明化:
物理表 detail 没有 deviceString 字段,需要依据枚举集合 list 转换得来。本例中 list 为 ["computer","smart phone","pad"],其中的三个枚举值成员依次对应自然数 1、2、3。
SPL 使用包含多个属性的结构来定义虚表,每个属性都有固定的名称。本例的虚表 p_detail 定义中要包含物理表名称、枚举维度 enum 名称,以及 list 集合,大致是下图这样:
属性 file 是虚表对应物理表的名称,这里是组表 "detail.ctx";column 是特殊字段的集合,其中每个成员对应一个特殊字段。name 是真字段名称 device,enum 是伪字段名称 deviceString,list 枚举值取值集合是 ["computer","smart phone","pad"]。
SPL 定义虚表 p_detail 的代码大致是这样:
A |
|
1 |
=[{file:"detail.ctx", column:[ {name:"device",enum:"deviceString",list:["computer","smart phone","pad"]} ] }] |
2 |
>p_detail=pseudo(A1) |
A1 准备一个虚表的结构,包含一个枚举类型的伪字段 deviceString。
A2 生成虚表 p_detail。
生成虚表之后,可以用游标 p_detail.cursor(id,ddate,device,deviceString,amt,bools).fetch(6) 取出虚表部分数据,大致是下图这样:
计算时将 p_detail 看作是一个简单的普通表,直接使用 deviceString 字段来做条件过滤和分组,并在结果集中显示字符串:
… |
|
3 |
=p_detail.select(deviceString=="pad" || deviceString=="smart phone") |
4 |
=A3.groups(month@y(ddate):dmonth,deviceString;sum(amt),count(~)) |
A3:过滤条件中的字符串 smart phone 会被自动转换为 2;pad 会被转换为 3,然后才会用来过滤物理表 detail。
A4 计算结果集中的 device 字段值 2、3 则分别会被转换为 "smart phone","pad",如下图:
虚表可以更新 update 或者追加 append,实际上改变的是对应的物理表。
包括枚举维度的虚表,追加和修改数据时只需要有伪字段值就可以了,SPL 会自动转换为真字段。
例如上面的例子,向 p_detail 中追加数据时,待追加的 new.btx 中只要有 deviceString 就可以了,SPL 会自动转换生成 device 字段。代码大致是这样的:
A |
|
1 |
=[{file:"detail.ctx", column:[ {name:"device",enum:"deviceString",list:["computer","smart phone","pad"]} ] }] |
2 |
>p_detail=pseudo(A1) |
3 |
=file("new.btx").cursor@b(id,ddate,deviceString,amt) |
4 |
=p_edtail.append(A3) |
A3:新数据中只有 deviceString,没有 device。
A4:向虚表对应的组表追加数据,这时不要求字段顺序必须和原组表一致。
假设要按照 modify.btx 中的主键和数据更新 p_detail,且按照 delete.btx 中的主键删除 p_detail,代码是这样的:
A |
|
1 |
=[{file:"detail.ctx", column:[ {name:"device",enum:"deviceString",list:["computer","smart phone","pad"]} ] }] |
2 |
>p_detail=pseudo(A1) |
3 |
=file("modify.btx").import@b(id,deviceString) |
4 |
=file("delete.btx").import@b(id) |
5 |
=p_edtail.update(A3:A4) |
A5:SPL 组表仅支持少量的修改和删除,所以 update 的参数是序表,而不是游标。
位维度
实际业务中还有很多布尔字段,用来描述是或者否。如果我们都存储为独立字段,那么即使用二值字段(取值为 1 或 0)代替布尔字段,也要用很多个整型字段来存储。
在物理表中,我们可以用整型字段的二进制位存储布尔字段,这样,一个 16 位短整型字段就能存储 16 个布尔值,每个二进制位存储一个,可以减少存储空间占用。而且,用二进制与或运算,一次就可以完成最多 16 个布尔值计算,性能会有明显提升。
但是,这样做就要自行实现整型二进制位与布尔值的转换和计算,会增加复杂度。而且,整数二进制位没有布尔字段或者二值字段那么容易读懂。
这种情况可以使用虚表的位维度,用伪字段定义二进制位和布尔值之间的映射关系,让代码变得非常简单。
仍以交易明细表 detail 为例,bools 是整型字段,存储了两个布尔值 isOversea(是否海外购)、isCreditCard(是否信用卡支付),每个值占用 bools 字段的一个二进制位。
这时,我们要在虚表中定义伪字段,将 bools 字段和布尔维度之间的转换透明化,如下图:
相应的,虚表定义要修改成下图的样子:
column 中增加了一个特殊字段的定义,name 是物理表真实字段的名称 bools,bits 属性中是伪字段名称集合,最多可以有 16 个布尔字段名称。本例 bits 中是两个字段名,分别对应 bools 字段中的第 1、2 个二进制位。
相应的,定义虚表 p_detail 的代码也要修改:
A |
||
1 |
=[{file:"detail.ctx", column:[ {name:"device",enum:"deviceString",list:["computer","smart phone","pad"]}, {name:"bools",bits:["isOversea","isCreditCard"]} ] }] |
|
2 |
>p_detail=pseudo(A1) |
A1 增加了特殊字段 bools,属性 bits 中定义了两个伪字段名称。
此时,取出虚表 p_detail 的部分数据会变成下面这样:
计算时直接使用 isOversea、isCreditCard 字段来做条件过滤和分组,并在结果集中显示布尔值:
… |
|
3 |
=p_detail.select(isOversea && (deviceString=="pad" || deviceString=="smart phone")) |
4 |
=A3.groups(month@y(ddate):dmonth,deviceString,isCreditCard;sum(amt),count(~)) |
A3 中过滤条件中的 isOversea 会被自动转换为 bools 字段的第 1 位,用来过滤物理表 detail。
A4 计算结果集中的 bools 字段第 2 位的值,会被转换为布尔值 isCreditCard:
在对虚表追加、修改数据时,只需要有位维度的伪字段值就可以了,SPL 会自动转换为真字段。
例如向 p_detail 中追加数据时,待追加的 new.btx 中只要有 isOversea,isCreditCard 就可以了,SPL 会自动转换生成真字段 bools。代码大致是这样的:
A |
|
1 |
=[{file:"detail.ctx", column:[ {name:"device",enum:"deviceString",list:["computer","smart phone","pad"]}, {name:"bools",bits:["isOversea","isCreditCard"]} ] }] |
2 |
>p_detail=pseudo(A1) |
3 |
=file("new.btx").cursor@b(id,ddate,deviceString,amt,isOversea,isCreditCard) |
4 |
=p_edtail.append(A3) |
按照 modify.btx 中的主键和数据更新 p_detail,且按照 delete.btx 中的主键删除 p_detail,代码是这样的:
A |
|
1 |
=[{file:"detail.ctx", column:[ {name:"device",enum:"deviceString",list:["computer","smart phone","pad"]}, {name:"bools",bits:["isOversea","isCreditCard"]} ] }] |
2 |
>p_detail=pseudo(A1) |
3 |
=file("modify.btx").import@b(id,isOversea,isCreditCard) |
4 |
=file("delete.btx").import@b(id) |
5 |
=p_edtail.update(A3:A4) |
冗余字段
假设物理表 detail 增加一个记录字段 custInfo,存储这个交易的客户信息,其中有个属性字段 comment,如下图:
现在需要过滤出 custInfo.comment 字符串包含 "risk" 子串的数据。
如果过滤时直接使用 custInfo.comment,就会把整个 custInfo 都读出。即使 custInfo 的其他字段不参与计算,也要被全部读出、解析并生成记录。
为提高性能,可以在物理表中增加一个冗余字段 custInfoComment,用来存放 custInfo.comment 的值,能减少读入的数据,且可以省去生成记录的过程。
但是,这个冗余字段和记录字段的关系需要特殊说明,使用很不方便。而且,即使有说明,也无法保证应用程序员在合适的场景中都能使用冗余字段。
针对这种情况,虚表提供冗余字段机制,可以定义一个冗余字段对应的计算式exp,如果计算表达式中存在这个 exp,会自动用冗余字段代替。
图中我们定义 exp 为 custInfo.comment,SPL 会自动用 custInfoComment 字段代替计算表达式中的 custInfo.comment。
虚表定义是下面这样:
定义和使用冗余字段的代码大致是这样的:
A |
B |
|
1 |
=[{file:"detail.ctx", column:[ {name:"device",enum:"deviceString",list:["computer","smart phone","pad"]}, {name:"bools",bits:["isOversea","isCreditCard"]} {name:"custInfoComment",exp:"custInfo.comment"} ] }] |
|
2 |
>p_detail=pseudo(A1) |
|
3 |
=p_detail.select(pos(custInfo.comment,"risk") && …) |
A1:在虚表中定义了一个计算式 exp:"custInfo.comment",和对应的真实字段 custInfoComment。
A3:过滤条件中出现了这个计算式,所以并不会取出 custInfo 的记录再取其中的字段,而是被替换成真实字段 custInfoComment,用这个冗余字段的值完成接下来的计算。
在对虚表追加、修改数据时,只需要有记录字段值就可以了,SPL 会自动转换为冗余字段。
例如向 p_detail 中追加数据时,待追加的 new.btx 中只要有 custInfo 就可以了,SPL 会自动转换生成 custInfoComment。代码大致是这样的:
A |
|
1 |
=[{file:"detail.ctx", column:[ {name:"device",enum:"deviceString",list:["computer","smart phone","pad"]}, {name:"bools",bits:["isOversea","isCreditCard"]}, {name:"custInfoComment",exp:"custInfo.comment"} ] }] |
2 |
>p_detail=pseudo(A1) |
3 |
=file("new.btx").cursor@b(id,ddate,deviceString,amt,isOversea,isCreditCard,custInfo) |
4 |
=p_edtail.append(A3) |
按照 modify.btx 中的主键和数据更新 p_detail,且按照 delete.btx 中的主键删除 p_detail,代码是这样的:
A |
|
1 |
=[{file:"detail.ctx", column:[ {name:"device",enum:"deviceString",list:["computer","smart phone","pad"]}, {name:"bools",bits:["isOversea","isCreditCard"]}, {name:"custInfoComment",exp:"custInfo.comment"} ] }] |
2 |
>p_detail=pseudo(A1) |
3 |
=file("modify.btx").import@b(id,custInfo) |
4 |
=file("delete.btx").import@b(id) |
5 |
=p_edtail.update(A3:A4) |
字段别名
有些应用中,真字段在不同情况下有不同的含义。这时候可以用虚表的字段别名来表示真字段的业务含义。
以电商系统事件表 events 为例,表中每一行是一个事件。每个事件都要存储事件类型 eventType 以及这类事件对应的属性信息 eventInfo。eventInfo 是一个记录字段,又包含多个字段,用来存储多个属性。
需要注意的是:不同类型的事件有不同的属性,相同类型的事件的属性相同。也就是说,eventType 字段值决定了 eventInfo 这条记录的字段个数、名称、数据类型。
举例说明,eventType 和 eventInfo 中的各属性关系大致是下表中这样:
eventType |
eventInfo |
|
property |
data type |
|
appInstall |
browser |
string |
device |
string |
|
reward |
float |
|
appStart |
page |
string |
title |
string |
|
dt |
datetime |
|
appEnd |
page |
string |
title |
string |
|
amount |
float |
|
dt |
datetime |
|
… |
… |
从表中可以看到,事件类型 appInstall 对应三个属性,appEnd 则对应完全不同的四个属性。由于事件类型很多,如果每个属性都存一个字段,就会让物理表有太多的字段。其实每种类型事件的属性个数并不会太多。
实际上,一个事件只能属于一个事件类型。假如一个事件属于 appInstall 类型,有 browser 属性,那么它就不可能有 appStart 类型的 page 属性。同时,这两个属性又都是字符串,我们可以用一个真字段 s1 来存储这两个属性。
以此类推,可以将同样数据类型的属性都合并起来,如下图:
eventType |
eventInfo |
trueField |
|
property |
dataType |
||
appInstall |
browser |
string |
s1 |
device |
string |
s2 |
|
reward |
float |
f1 |
|
appStart |
page |
string |
s1 |
title |
string |
s2 |
|
dt |
datetime |
dt1 |
|
appEnd |
page |
string |
s1 |
title |
string |
s2 |
|
amount |
float |
f1 |
|
dt |
datetime |
dt1 |
|
… |
… |
… |
… |
如图所示,我们用真字段 s1 存储三类事件的三个字符串属性 browser、page 和 page。其他真字段 s2、f1、dt1 和 s1 类似。总体来看只需要 s1、s2、f1、dt1 四个字段就可以存储图上出现的所有属性。
这样,物理表的结构就是下图这样:
虽然这样的物理表字段总数少很多,但是字段名看不出业务含义,不太方便使用。查询起来还要根据 eventType 解释不同字段的业务含义,也增加了复杂度。
这种情况可以利用虚表的字段别名和计算式 exp 配合使用。虚表为每个真字段配置多个有业务含义的别名,就会是下图这样:
别名不能重复,所以有些别名加上了事件类型前缀,例如 appStart_page。
events 表中的 eventType 字段要用前面讲到的枚举维度,物理表存成连续自然数。
这时,虚表定义大致是这样的:
代码大致是这样的:
A |
B |
|
1 |
=[{file:"events.ctx", column:[ {name:"eventType",enum:"eventTypeString",list:["appInstall","appStart","appEnd"]}, {name:"s1",alias:["browser","appStart_page","appEnd_page"],exp:"case(eventType, 1:eventInfo.browser, 2: eventInfo.page, 3: eventInfo.page)"}, {name:"s2",alias:["device","appStart_title","appEnd_title"],exp:"case(eventType, 1:eventInfo.device,2: eventInfo.title, 3:eventInfo.title)"}, {name:"f1",alias:["reward","amount"],exp:"case(eventType,1:eventInfo.reward,3: eventInfo.amount)"}, {name:"dt1",alias:["appStart_dt","appEnd_dt"],exp:"case(eventType,2:eventInfo.dt, 3: eventInfo.dt)"} ] }] |
|
2 |
>p_events=pseudo(A1) |
|
3 |
=p_events.select(eventTypeString=="appInstall" && browser=="firefox") |
|
4 |
=A3.import(id,eventTypeString,browser,reward) |
A1:在虚表中定义了真字段 s1 的三个别名 "browser","appStart_page","appEnd_page",以及对应的计算式 exp:"case(eventType, 1:eventInfo.browser, 2: eventInfo.page, 3: eventInfo.page)"。
eventType 取值如果不是 1、2 或者 3,真字段 s1 应该为 null。所以这里不能写成 case(eventType, 1:eventInfo.browser;eventInfo.page)。
s2、f1、dt1 的写法和 s1 类似。
A3:过滤条件中出现了字段别名,SPL 并不会取出 eventInfo 的记录再取其中的字段,而是自动替换成真实字段 s1,用冗余字段的值完成后续计算。
A4:取出过滤结果。
在对虚表追加、修改数据时,只需要有类型字段和记录字段值就可以了,SPL 会自动转换为 s1、f1 等字段。
例如向 p_events 中追加数据时,待追加的 new.btx 中只要有 eventTypeString,eventInfo 就可以了,SPL 会根据这两个字段,自动生成 s1、f1 等。代码大致是这样的:
A |
|
1 |
=[{file:"events.ctx", column:[ {name:"eventType",enum:"eventTypeString",list:["appInstall","appStart","appEnd"]}, {name:"s1",alias:["browser","appStart_page","appEnd_page"],exp:"case(eventType, 1:eventInfo.browser, 2: eventInfo.page, 3: eventInfo.page)"}, {name:"s2",alias:["device","appStart_title","appEnd_title"],exp:"case(eventType, 1:eventInfo.device,2: eventInfo.title, 3:eventInfo.title)"}, {name:"f1",alias:["reward","amount"],exp:"case(eventType,1:eventInfo.reward,3: eventInfo.amount)"}, {name:"dt1",alias:["appStart_dt","appEnd_dt"],exp:"case(eventType,2:eventInfo.dt, 3: eventInfo.dt)"} ] }] |
2 |
>p_events=pseudo(A1) |
3 |
=file("new.btx").cursor@b(id,eventTypeString,eventInfo) |
4 |
=p_events.append(A3) |
按照 modify.btx 中的主键和数据(id、eventTypeString、eventInfo)更新 p_events,且按照 delete.btx 中的主键删除 p_events,代码是这样的:
A |
|
1 |
=[{file:"events.ctx", column:[ {name:"eventType",enum:"eventTypeString",list:["appInstall","appStart","appEnd"]}, {name:"s1",alias:["browser","appStart_page","appEnd_page"],exp:"case(eventType, 1:eventInfo.browser, 2: eventInfo.page, 3: eventInfo.page)"}, {name:"s2",alias:["device","appStart_title","appEnd_title"],exp:"case(eventType, 1:eventInfo.device,2: eventInfo.title, 3:eventInfo.title)"}, {name:"f1",alias:["reward","amount"],exp:"case(eventType,1:eventInfo.reward,3: eventInfo.amount)"}, {name:"dt1",alias:["appStart_dt","appEnd_dt"],exp:"case(eventType,2:eventInfo.dt, 3: eventInfo.dt)"} ] }] |
2 |
>p_events=pseudo(A1) |
3 |
=file("modify.btx").import@b(id,eventTypeString,eventInfo) |
4 |
=file("delete.btx").import@b(id) |
5 |
=p_edtail.update(A3:A4) |
回顾与总结
SPL 虚表相当于元数据,可以将物理表中比较复杂的存储机制透明化。这样,我们就可以像一个最简单的表一样使用虚表,降低了使用复杂度。同时,虚表又可以将逻辑结构上的简单计算,自动映射成高性能存储机制上的高效计算,可以保证计算性能。
在虚表中定义好枚举维度、位维度、冗余字段和字段别名后,使用者就不必再考虑这些存储和计算机制,将虚表当成简单的普通表即可。
除此之外,虚表还可以用于用户分析计算,实现两个维度字段同时有序,详情参见SPL 虚表的双维度有序机制。
英文版