批量随机键值的查询优化

一、   问题描述键值查询是很常见的查询场景,在数据表上建有索引后,即使表中数据记录数巨大(几亿甚至几十亿行),用键值查询出单条记录也会很快,因为建立索引后的复杂度只有 O(log2N), 10 亿行数据大概只要比较 30 几次(10 亿约等于 2^30),在现代计算机上是个毫秒级别的事务。不过,如果需要查询的键值很多,比如多达几千甚至几万的时候,如果每次都独立查找,那读取和比较也会累积到几万甚至几十万次,时间延迟由此也会涨到几十秒甚至几十分钟的级别,简单地使用数据库索引对于用户体验必然是难以容忍的了。本文将讨论大批量键值查询的性能优化方法,示例代码将用集算器 SPL 完成。SPL 代码简洁易懂,且提供了丰富的算法支持,非常适合实现这种高性能计算。二、   单一整数键值我们先考虑最简单的单一整数键值情况,即用作键的字段只有一个,且为整数。2.1 存储与大多数分析场景不同,键值查找更适合使用行式存储而非列式存储。因为行存是按记录,一条条连续存储的。而列存是按列存储的,对整条记录来说并不连续,读取单条记录时,需要从更多的数据块中读取数据,访问量反而更多。对于超大规模数据下的随机批量键值查找,不适合对数据进行压缩。因为命中的结果分布有可能很分散,大概率出现解压一个数据块仅能获取一条记录的情况,浪费很多解压缩时间,使得查询效率变低。2.2 索引通常我们会针对查找键建立索引,这样指定键值可以迅速定位,而不必遍历数据。我们知道,索引的本质也是排序,利用键值有序可以快速用二分法找到指定记录。那么,如果数据本身对键值就有序,是不是可以不建索引了?键值有序的数据本身往往过大,不能放在内存中,即使利用二分法查找时,也多次读取外存用以对比。建立索引后,可以预加载索引到内存,在内存中对索引查找,相比多次读取外存效率高很多。所以,即使数据有序,再建立索引对于高速查找仍然有意义。反过来,建立了索引后,还要求数据对键值有序吗?对于键值无序并建有索引时,读取数据时需要再按物理位置排序, 因为外存数据是按块存储的, 在大批量查找时,同一块有可能涉及多条找到的记录,而如果按键值次序去读取时,物理次序如果键值次序不同,就可能发生同一块被读多次的情况,性能损失严重,所以一定要在读取前将欲读数据按物理位置排序。而取出的数据还要再按原顺序排序,来回就多两个排序步骤。读取的数据量大的时候会有些影响效率,若数据有序存放则可以省去这两次排序的开销。如果每次读取的数据量不大时,额外排序损失的性能也很少,可以不必按键值有序。2.3 索引缓存当数据量很大时,索引本身也会很大,无法全部放入内存,这样每一次的查找都需要从硬盘上读取索引。如果对索引再建立外层索引(可能有多级),再在进行批量查找之前,尽可能多地预先加载多级索引缓存,减少重复读取索引的次数,就可以提升查询效率。2.4 查找查找时,将待查找的键值集有序排序,当查完一个键值后,由于键值是有序(升序)的,下一个键值肯定不小于上一个键值,这样就不会重复读取比上一个索引区间更小的索引块,少走回头路。2.5 举例字段名称类型是否主键说明idlong是从 1 开始自增data1string需要获取的数据 1data2int需要获取的数据 2使用 SPL 创建一个满足以上数据结构的行存组表:AB1=file("single.ctx")2=A1.create@r(#id,data1,data2)3for 600=to((A3-1)*1000000+1,A3*1000000).new(~:id,rands("qwertyuiopasdfghjklzxcvbnm",rand(20)+180):data1,rand(60000)+1:data2)4=A2.append(B3.cursor())5>A2.close()用键值 id,建立索引:A1=file("single.ctx")2=A1.open()3=A2.index(id_idx;id;)利用索引进行批量键值查找:A1=10000.(rand(600000000)+1)2=file("single.ctx").open()3=A2.index@3(id_idx)4=A2.icursor(A1.contain(id),id_idx).fetch()A1:这里待查找键值没有排序,因为 A4 的 T.icursor 中的 A.contain 会自动对 A 进行排序。A3:其中 @3 的意思是将 id 键的多级索引的更多级缓存预先加载进内存。经过 index@3 预处理,第一遍查询时间就能达到索引被缓存后才能达到的极限值。另外,还有 index@2,相比 @3 缓存的索引层级更少,占用内存也更小。三、   复杂键值3.1    非整数键值非整数键值,比如车牌号,通常是由字符与数字组合成的字符串。因为字符串比较要比整数的比较复杂得多,比较速度也更慢。所以可以把这种有规律或者可枚举的字符串转为简单的整数类型,提升性能。3.2    多字段键值多字段键值,可以直接利用多个字段键进行查找,但集合的存储和比较都要慢一些。为了获取高性能,更常用的办法是把多字段键拼成单字段键。对于有规律或者可枚举的串,可以将串数字化后,与其他数字化后的键值合并为一个新的整数键值。3.3    举例字段名称类型是否主键说明pnumstring车牌号pdatedate日期datastring需要获取的数据其中 pnum 和 pdate 两个字段作为联合主键确定一条记录。3.3.1     多字段键直接查找使用 SPL,按以上数据结构,创建组表(和单键值类似,多键值也需要确保键值有序,可以使用cs.sortx()函数排序,具体方法详见函数参考。):AB1=file("multi.ctx")2=A1.create@r(#pnum,#pdate,data)3for 600=to(1000000). new(rands("QWERTYUIOPASDFGHJKLZXCVBNM",1)+rands("1234567890",6):pnum,date("2007-01-01")+rand(3000):pdate,rands("qwertyuiopasdfghjklzxcvbnm",rand(20)+180):data)4=A2.append(B3.cursor())5>A2.close()多字段键组表建立索引时需要包含多个维。例如,以上数据结构中的 pnum 和 pdate 两个维:A1=file("multi.ctx")2=A1.open()3=A2.index(p_idx;pnum,pdate)多字段键组表查询时可以使用 n 元组。例如,以上数据结构中的 [pnum,pdate]:A1=file("multi.ctx")2=A1.open()3=[["R263547",2010-06-09],["A762573",2008-03-22]]4=A2.icursor(;A3.contain([pnum,pdate]),p_idx).fetch()3.3.2     数字化合并键值用以上的数据结构做个例子,来说明如何进行数字化合并键值。pnum是个串,一共 7 位,第一位是一个随机大写英文字母,后六位都是数字,可以将字母用 ACS 码值表示,与后六位数字拼接后转为整数。pdate 是一个日期类型字段,可以将 2007-01-01 定为起始日期,pdate 与起始日期间隔的天数可以作为该字段的数字化后的值。pnum中大写英文字母的 ACS 码是两位数,与后面的 6 位数加起来是 8 位数,而日期 pdate 的跨度范围为 10 年,换算成天是个 4 位数,将数字化后的 pnum*10000+pdate,就得到了一个新的 12 位的数字化字段,pid。A1=file("multi.ctx").open().cursor()2=A1.run(pnum=int(asc(pnum)/right(pnum,6)),pdate=interval@d(date("2007-01-01"),pdate))3=A1.new(pnum*10000+pdate:pid,data)4=A3.sortx(pid)5=file("conbi.ctx").create@r(#pid,data)6=A5.append(A4)7>A5.close()这样就得到了一个单字段键的组表“conbi.ctx”,其数据结构如下:字段名称类型是否主键说明pidlong是包含 pnum 和 pdate 信息的唯一主键datastring需要获取的数据然后按照 "单一整数键值" 中的做法就可以处理了。四、   并行技术4.1    数据拆分多线程并行,是把数据分成 N 份,用 N 个线程查询。但如果只是随意地将数据分成 N 份,很可能无法真正地提高性能。因为将要查询的键值集是未知的,所以理论上也无法确保希望查找的数据能够均匀分布在每一份中。比较好的处理方式是先观察键值集的特征,从而尽可能地进行数据的均匀拆分。如果键值数据有比较明显的业务特征,我们可以考虑按照实际业务场景使用日期、部门之类的字段来对数据进行拆分。如:将属于部门 A 的 1000 条记录均分在 10 份数据中,每份数据就有 100 条记录。在利用多线程查询属于部门 A 的记录时,每个线程就会从各自对应的数据中取数相应的这 100 条记录了。对于没有什么特征的数据,尤其是随机数,可以考虑按整数的尾数取余数来分成 N 份数据。这样可以尽可能地确保每份数据中数据量的均匀分配。4.2    举例字段名称类型是否主键说明idlong是从 1 开始自增datastring需要获取的数据基于以上数据结构,数据拆分以及建立组表:A1=file("single.ctx").open().cursor()2=N.(file("id_"+string(~-1)+".ctx").create@r(#id,data))3=N.(eval("channel(A1).select(id%N=="+string(~-1)+").attach(A2("+string(~)+").append(~.cursor()))"))4for A1,500000A2:使用循环函数,创建名为“键值名 _ 键值取 N 的余数.ctx”的组表文件,其结构同为 (#id,data)。A3:用循环函数将游标数据分别追加到 N 个原组表上。此处 N 为参数,假设 N=4,当循环第一次时,拼出的 eval 函数参数为:channel(A1).select(id%4==0).attach(A2(1).append(~.cursor()))。意思是对游标 A1 创建管道,将管道中记录按键值 id 取 4 的余数,将余数值等于 0 的记录过滤出来。attach 是对当前管道的附加运算,表示取和当前余数值对应的原组表,将当前管道中筛选过滤出的记录,以游标记录的方式追加到 A2(1),即第 1 个组表。多线程并行创建索引:AB1fork   directory@p("id*.ctx")=file(A1).open().index(id_idx;id;)A1:使用 fork 执行多线程时,需要注意环境中的并行限制数是否设置合理。多线程并行查询:AB1=10000.(rand(600000000)+1)2=A1.group(~%N)3fork N.(~-1),A2=A3(2)4=file("id_"/A3(1)/".ctx").open().icursor(;B3.contain(id),id_idx)5return B4.fetch()6=A3.conj()7=A6.fetch()需要注意的是,将待查找的批量键值用和拆分组表数据同样的规则拆分后,在各自对应的组表中进行查找。并且取数动作应当在多线程并行内完成,而不是将各自的游标返回后再取数。五、   数据追加对于大数据来说,新数据的追加是不可避免的。数据追加通常要考虑原数据与追加数据的关系,较简单的情况是连续有序的数据,直接在原数据尾部追加新数据,对追加后的数据,还需要更新索引。如果新数据与原数据,整体不是连续有序,就需要在追加数据时做归并运算,并随后更新索引。尤其是当原数据变得很大时,原数据与新数据的频繁归并、每次追加后更新索引,都会带来极高的时间成本和磁盘损耗问题。这种情况可以建立一份临时数据区来存放每次追加的新数据,当这个临时数据区达到一定规模(在数据增量较为稳定的情况下也可以按时间来决定,比如每个月初),就将临时数据区(小数据)与原数据(大数据)进行一次归并,并更新索引。查找时可以将大小两份数据在逻辑上合并为一份数据后进行查找。无序的数据追加时,因为索引就是排序的,要重建索引也会面临以上有序数据追加时的问题,还是需要大小两份数据的办法,等积累多了再重建索引。5.1 举例字段名称类型是否主键说明idlong是从 1 开始自增datastring需要获取的数据SPL提供了补文件的功能,可以方便地应对复杂的数据追加场景。单个文件时,利用补文件追加数据:AB1=file("add.txt").cursor@t()2if day(now())==1=file("single.ctx").reset()3=file("single.ctx").open().append@a(A1)A1:add.txt 是新增数据(这里假设数据来自文本文件,实际也可能来自数据库等其他数据来源),与已有文件结构相同,均为 (#id,data),其中 id 有序。A2:判断为当前系统时间是否为月初。B2:若是月初,f.reset 函数将补文件有序归并到主文件,同时清空补文件,并更新索引。A3:每天对补文件进行归并运算,append 的 @a 代表归并追加到补文件上,若没有补文件则自动创建,并更新索引。多个文件时,利用补文件追加数据:AB1=file("add.txt").cursor@t()=directory@p("id*.ctx").(file(~))2if day(now())==1=B1.(~.reset())3=N.(eval("channel(A1).select(id%N=="+string(~-1)+").attach(B1("+string(~)+").open().append([~.cursor()]))"))4for A1,500000查找时,代码不变。open 函数,当存在补文件时,会连带补文件的数据一同查找。六、   索引冗余同一份数据不能在遍历运算(通常为列存)和随机取数(通常为行存)这两方面都达到最优性能。如果这份数据还想用于遍历运算,可以把数据冗余多份,以空间换时间。6.1    举例字段名称类型是否主键说明idlong是从 1 开始自增data1string需要获取的数据 1………num1int需要聚合的字段data2int需要获取的数据 2odatedate日期字段………对需要遍历的数据,通常采用列存。SPL 中创建列存组表,只需要将 f.create@r 的 @r 去掉,就是以列存方式创建组表。对于列存组表,在建立索引时,还可以建立带值索引,把需要获取的数据复制进索引,基于以上的数据结构:A1=file("multi.ctx")2=A1.open()3=A2.index(id_data_idx;id;data1,data2)对于多字段键的场景,组表建索引时,对应参数部分也有类似的写法:index(ids_data_idx;id1,id2,…,idN;data1,data2,…,dataN)。SPL的带值索引的查询与不带值索引使用一样。需要注意的是,使用带值索引,读取数据时将不再访问原数据文件,取数速度会更快。但是另一方面,因为在索引中做了冗余,索引文件也会较大。

(0)

相关推荐

  • Redis_数据类型

    Redis支持的键值数据类型如下: 字符串类型 散列类型 列表类型 集合类型 有序集合类型  一.字符串类型 字符串类型是Redis中最基本的数据类型,它能存储任何形式的字符串,包括二进制数据.一个字 ...

  • Mysql :(一)

    顶哥说sql语句其实不难,尤其对于那些英语稍好一些的人来说! 最最最重要的就是搞清楚sql语句的执行顺序!!!数据就像沙子, 语句就是筛子, 沙子按照顺序经过给定的筛子,留下来的就是你要的!!!并不是 ...

  • Mysql中的索引

    Mysql中的索引

  • 什么是MongoDB?简介、架构、功能和示例 | MongoDB中文社区

    什么是MongoDB? 什么是MongoDB?MongoDB是一个面向文档的NoSQL数据库,用于大容量数据存储.MongoDB是2000年代中期出现的一个数据库,属于NoSQL数据库. 在这个教学大 ...

  • MySQL之Json类型

    MySQL之Json类型

  • 教大家如何使用KD-X1遥控智能卡如何改键值及场强

    各位锁匠朋友们,大家好!今天教大家如何使用KD-X1遥控智能卡如何改键值及场强.KD-X1这个设备很多人手里都有,是一个还不错的设备. 操作视频

  • python3 测试时候如何批量随机生成伪数据?(faker模块)

    前言 在测试的过程中,我们经常需要造一些测试数据,比如姓名,手机号,身份证,地址,以及公司信息等测试数据. 就拿姓名来说,我们平常想到的姓名就是张三,李四,王五这些简单的名字. 如果领导让我们想一百个 ...

  • 【转】编程的朋友们用的着的键盘键值汇总

    键盘键值汇总 功能键键值 ESC键: (27) 回车键: (13) TAB键: (9) CapsLock键: (20) Shift键: ($10) Ctrl键: (17) Alt键: (18) 空格键 ...

  • Js~对键值对操作

    键值对主要是面向对象语言里的字典,或者叫哈希表,它通过键(key)可以直接访问到值(value),所以它查找的时间复杂度是O(1),即一次查找即可找到目标:在.net里有Dictionary,而在ja ...

  • 随机取值(抽签,抽奖)

    前言 面对不会的选择题,我们一般会采取连蒙带猜的方法,这个也仅限于马上交卷,没有时间的情况,如果有时间,还是要认真推敲一下的,比如大家总结了一套公式就很不错,正确率也是蛮高的. 三长一短就选短, 三短 ...

  • 编程语言php中数组键值分发为变量名及值的函数extract详解

    PHP extract() 函数的作用是将数组键值分发为变量及值的函数extract详解. extract(array,extract_rules,prefix) array必需. 规定要使用的输入. ...

  • 浅谈随机梯度下降&小批量梯度下降

    机器学习三要素 上次的报告中,我们介绍了一种用于求解模型参数的迭代算法--梯度下降法.首先需要明确一点,即"梯度下降算法"在一个完整的统计学习流程中,属于什么?根据<统计学习 ...

  • 批量输入随机数和随机字母

    明天就是国庆长假了,小编祝各位亲们国庆节快乐,中秋节和家人大团圆. 今日分享如下: 工作中有时候需要在单元格区域随机输入数字或字母,如果一个个手工输入,效率很低.怎样批量输入随机数呢?请看下文. 一. ...

  • 一个【Tab】键就能让所有的目录页批量对齐?也太太太太方便了吧!

    作者:小包 策划:视频小分队 编辑:亦染 在做目录页的时候,不知道小伙伴有没有碰到过一个难题?   那就是目录页码的总是对不齐!   这个真的是让人抓狂,还好!我有对齐的万能工具:尺子.   但是,如 ...