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,判空后取默认值
如果是大量反复取属性,比如排序,性能可能造成瓶颈
如果有这种情况,可以在返回的空表中缓存属性

参考博文

Lua配置表存储优化方案Lua 性能剖析