id | Col1 | Col2 | Col3 |
---|---|---|---|
Id1 | Value1_1 | Value1_2 | Value1_3 |
Id2 | Value2_1 | Value2_2 | Value2_3 |
Id3 | Value3_1 | Value3_2 | Value3_3 |
以一个M行N列的表格为例
以行存储
存储结构
Dictionary<id, dictionary<col_name, value>>
第一层: key:行id,value:行数据
第二层: key: 列名称 value: 该行该列值
占用空间
在lua中,空表占用56字节,数据分list部分和hash部分
list部分每个数据占16字节
hash部分每个数据占32字节
lua中table的list和hash部分实际空间大小和分配是个复杂的问题
一般来说list部分大小为2的幂,填充率不低于1/2,
不在list部分的放入hash部分
hash部分大小原则同list
一个M行N列的表格
以行存储,一个M大小hash表,M个N大小的hash表
占用大小,(56 + 32M) + M * (56 + 32N)
数据读取
以读取一行配置,使用5个列数据为例
根据id获得table(执行1次)
根据col_name从table中获取数据(执行5次)
以列存储
存储结构
Dictionary<id, index>
Key:行id,value: 行索引
Dictionary<col_name, list>
Key:列名称 value:该列值组成的数组,按行索引排序
占用空间
以列存储,一个M大小的hash表,1个N大小的hash表,N个M大小的list表
占用大小, 56 + 32M + 56 + 32N + N*(56+16M)
数据读取
根据id获得index索引,生成闭包,返回带元方法的空表 (执行1次)
读取属性时触发__index,根据col_name获得数据列,根据index获取数据(执行5次)
function GetConfig(id)
local index = id_tab[id]
return setmetatable({},{__index = function (col_name)
return data[index][col_name]
end
})
end
比较
占用空间
以行存储-以列存储= 16MN + 56M – 88N – 56
一般行数M远大于列数N,故列存储占用空间小很多
数据读取
以行存储简单直接,没有GC
以列存储需要创建空表,原表,闭包,索引次数也更多
项目看重内存占用,故选取列存储
优化
默认值
在很多时候,同列数据会有重复值
如果出现次数超过一半,就成为该列默认值
去掉与默认值相同的行索引,list表转换为hash表
hash元素占用空间是list两倍,故一半是个分割点
当然GetConfig也就更复杂一点
重复表
项目中很多列都是数组甚至二维数组,即lua的表
如果每个值都指向各自的表,白白浪费空间
只有同列才最可能出现重复值,这也是列存储思路的优势,当然行存储也可以这么做
将出现次数超过某个值(与表行数相关)的值提取成local变量,可大幅节省空间
文件结构
local dup_col2_val1 = {...}
return {
id = {[101]=2, [104]=3, [105]=4, [203]=5, [204]=6, [302]=7, [320]=8},
col1 = {{...}, [6] = {...}},
col2 = {null, {...}, dup_col2_val1, dup_col2_val1, {...}, {...}, dup_col2_val1, {...} },
...
}
- col1有默认值,只有id为204的行值不同
- col2中dup_col2_val1出现3次,不能成为默认值,但是可以提取为重复表
其他
local a = {1,2,3,4,5}
local b = {[1]=1, [2]=2, [3]=3, [4]=4, [5]=5}
a是长度8的list表
b却是大小8的hash表
不知是我测试错误还是另有原因
每次读取属性都要触发__index
有默认值时大概率索引index返回null,判空后取默认值
如果是大量反复取属性,比如排序,性能可能造成瓶颈
如果有这种情况,可以在返回的空表中缓存属性