在第一篇有关QVD文件结构的文章中 ,我足够详细地描述了元数据的一般结构和细节, 第二篇有关列(字符)的存储。 在本文中,我将描述用于存储有关字符串的信息的格式,进行总结,并讨论计划和成就。
因此(记住)QVD文件与关系表相对应,在QVD文件中,该表存储在两个间接连接的部分中:
字符表(我的术语)包含源表中每一列的唯一值。 我在第二篇文章中谈到了它们。
行表包含源表的行,每行存储相应符号表中该行的列(字段)值的索引。 这篇文章就是关于这个的。
以我们的盘子为例(请记住-从第一部分开始)
SET NULLINTERPRET =<sym>; tab1: LOAD * INLINE [ ID, NAME 123.12,"Pete" 124,12/31/2018 -2,"Vasya" 1,"John" <sym>,"None" ];
在我们的QVD文件的行表中,此标签将对应5行-始终完全匹配:表中有多少行,QVD文件的行表中有多少行。
行表中的一行由非负整数组成,这些数字中的每一个都是相应符号表的索引。 从逻辑上讲,一切都很简单,仍然需要澄清一些细微差别并举一个例子(拆卸-正如我们的铭牌在QVD中所示)。
行表由K * N字节组成,其中
- K-源表中的行数(“ NoOfRecords”元数据标记的值)
- N-符号表行的字节长度(元数据标签“ RecordByteSize”的值)
线表以相对于文件二进制部分开头的偏移量“ Offset”(元数据标记)开头。
有关行表的信息(长度,行大小,偏移量)存储在元数据的常规部分中。
行表的所有行都具有相同的格式,并且是“无符号数字”的串联。 该数字的长度足以代表一个特定字段:该长度取决于特定字段的唯一值的数量。
对于具有一个值的字段(如我已经写过的),该长度将为零(该值在源表的每一行中都是相同的,并存储在相应的符号表中)。
对于具有两个值的字段,此长度将等于1(符号表中可能的索引值为0和1),依此类推。
由于行表的行的总长度应为字节的倍数,因此“最后一个字符”的长度与字节边界对齐(请参阅下文,当我们分析面板时)。
有关每个字段格式的信息存储在专用于该字段的元数据部分中(我们将在下面进行详细介绍),该字段的位表示形式的长度存储在“ BitWidth”标签中。
存储NULL值
如何存储缺失值? 不讨论为什么的话题,我将以这种方式回答:据我了解,以下组合对应于NULL值
- 相应字段的“ Bias”标记取值为“ -2”(总而言之,我遇到了此标记的两个可能的值-“ 0”和“ -2”)
- 该字段为NULL的行的字段索引为0
因此,该列中具有NULL值的所有其他索引都增加2-在我们的示例中将看到更低一些。
行中字段的顺序
行表的行中字段的顺序与字段的位偏移相对应,该位偏移存储在与此字段相关的元数据部分的“ BitOffset”标签中。
让我们分析一下示例(请参阅本系列第一部分中的元数据)。
ID栏
- 位偏移量0-该字段将是“最右边”
- 位长度3-该字段将在行表的一行中占据3位
- 偏差为“ -2”-该字段具有NULL值,所有索引均增加2
栏位“ NAME”
- 位偏移3-该字段位于ID字段的左侧3位
- 位长度5-该字段将在行表的行中占据5位(与字节边界对齐)
- 偏差为“ 0”-该字段没有NULL值,所有索引均为“诚实”
介绍我们的铭牌。
让我们看一下真正的“零和一”-我将以二进制形式“以十六进制格式”(如此紧凑)给出QVD文件的片段。
首先,整个二进制部分(粉红色突出显示,元数据被截断-伤害了很多...)

足够紧凑,同意。 让我们仔细看看-在元数据之后有符号表(顺便说一下,元数据在此文件中以换行符结尾,并且为零字节-从技术上讲,这种情况发生在元数据后需要跳过零字节...)。
下图中突出显示了第一个符号表。

我们看到:
ID字段的第一个唯一值是
- 类型“ 6”(分配的第一个字节)是带有字符串的浮点数(请参阅第二篇文章)
- 在第一个字节之后,接下来的8个字节是二进制表示的浮点数
- 在它们之后是字符串表示形式-非常方便(无需记住-数字是多少),以零字节结尾
其余三个唯一值的类型为5(带字符串的整数)-值分别为“ 124”,“-2”和“ 1”(很容易看到)。
在下图中,我突出显示了第二个符号表(用于“ NAME”字段)

“ NAME”字段的第一个唯一值是类型“ 4”(分配的第一个字节)-以零结尾的字符串。
其他四个唯一值也是字符串“ 12/31/2018”,“ Vaysa”,“ John”和“ None”。
现在-行表(在下图中突出显示)

如预期的那样-5个字节(5行乘1字节)。
第一行 (对应于板的“ Pete”行123.12)
字符串值是字节“ 02”(二进制000000010)。
分开(记住上面的描述)
- 正确的3位(二进制010,我们认为是2)-这是“ ID”字段的符号表的索引
- 我们的字段“ ID”包含NULL,因此索引增加了2,即 结果索引为0,对应于字符“ 123.12”。
- 接下来的5位(二进制和十进制0)是“ NAME”字段的符号表中的索引,它不包含NULL,因此这是符号表中的“ Pete”索引。
行表中的第二行 (124.12 / 31/2018)
值-字节“ 0B”(二进制00001011)
- 正确的3位(二进制011,我们认为是3)-这是“ ID”字段的符号表中的索引
- 我们的字段“ ID”包含NULL,因此索引增加了2,即 结果索引为1,对应于符号“ 124”。
- 接下来的5位(二进制和十进制1)是“名称”字段的符号表中的索引,它不包含NULL,因此这是符号表中的索引“ 12/31/2018”。
等等,让我们快速看一下最后一行 -到了那行,它是“ None”(即NULL和字符串“ None”):
该值是字节“ 20”(二进制0010000)
- 右3位(二进制和十进制0)-这是“ ID”字段的符号表的索引
- 我们的字段“ ID”包含NULL,因此索引增加了2,即 最终索引为-2,对应于NULL值。
- 接下来的5位(二进制100,十进制4)是“名称”字段的符号表中的索引,它不包含NULL,因此这是符号表中的索引“无”。
重要信息我找不到一个证实这一点的示例,但是我遇到了一些文件,这些文件的NULL值最终索引为-1。 因此,在我的程序中,我将最终索引为负的所有字段都视为NULL。
行表中较长的行
在QVD格式解析结束时,我将简要介绍重要的细微差别-行表中的长行以从右到左的顺序存储字段,其中零位偏移量的字段将是最右边的字段(如上所述)。 但是字节顺序是相反的,即 第一个字节将是最右边的一个字节(并将包含“ right”字段-零位偏移量的字段),最后一个字节将是第一个字节(也就是说,包含最多的“ left”字段-具最大位偏移量的字段)。
应该给出一个例子,但不要过多地详述。 让我们看一下这样的标签(我引用一个片段-为了使行表中的行较长,您需要增加唯一值的数量)。
tab2: LOAD * INLINE [ ID, VAL, NAME, PHONE, SINGLE 1, 100001, "Pete1", "1234567890", "single value" 2, 200002, "Pete2", "2234567890", "single value" ... ];
有关字段的简要信息(从元数据中挤出):
- ID:宽度8位,位偏移-0,偏置-0
- VAL:宽度5位,位偏移-8,偏置-0
- 名称:宽度6位,位偏移-18,偏置-0
- 电话:宽度5位,位偏移-13,偏置-0
- SINGLE:宽度0位(具有一个值)
该行表分别由长度为3个字节的字符串组成,在该行表的行中,有关字段的数据将在逻辑上进行如下分解:
- 前6位-栏位“ NAME”
- 接下来的5位-字段“ PHONE”
- 然后5位-字段“ VAL”
- 后8位-ID字段
逻辑序列以相反的顺序转换为物理字节,即
- “ ID”字段完全占据了第一个字节(按逻辑顺序是最后一个字节)
- “ VAL”字段占据第二个字节的低5位
- 字段“ PHONE”占据第二个字节的高3位,第三个字节的低2位
- 栏位“ NAME”占据第三个字节的高6位
让我们看一下示例,这是行表的第一行的样子(以粉色突出显示)

栏位值
- ID-二进制00000000,十进制0
- VAL-二进制00010,十进制2,从偏差中减去2-得到0
- 电话-二进制00010,十进制2,从偏差中减去2-得到0
- 名称-二进制000000,十进制0
也就是说,第一行包含来自相应字符表的前几个字符。
通常,从第一行开始分析很方便-它通常包含零作为索引(QVD文件的构建方式是使第一行的值首先进入字符表)。
让我们看第二行修复

栏位值
- ID-二进制00000001,十进制1
- VAL-二进制00011,十进制3,从偏差中减去2-得到1
- 电话-二进制00011,十进制3,从偏差中减去2-得到1
- 名称-二进制000001,十进制1
即,第二行包含相应字符表中的第二个字符。
我将分享一些经验-我如何从技术上“阅读” QVD。
第一个版本是用python编写的(我将对其进行修饰并将其放在github上)。
主要问题很快变得清晰起来:
- 符号表只能“连续”读取(如果不读取所有先前字符,则无法读取符号号N)
- 实际文件不适合RAM
- 最慢的操作(使用文件除外)中的位操作(对字符串表的行进行解包)
- 性能在“宽” QVD文件上严重降低(当有许多列时)
这些问题中的一些可以通过更改语言来解决(例如,从python到C)。 部分需要采取一些其他措施。
当前相当快的实现是这样的-通用逻辑是在python中实现的,而最关键的操作是在并行运行的单独C程序中执行的。
不久
- 将符号表写入文件,另外为文本字段创建索引,从而可以读取符号号N
- 使用QVD和带有通过内存映射文件实现的符号表的文件(速度更快)
- 首先,以并行方式(对处理器数量有所限制),使用符号表(和索引)创建文件
- 然后以并行方式(具有类似的限制)读取行表的行并创建csv文件(在HDFS中)
- 最后一步是将这些文件转换为ORC表(使用Hive工具)
- C语言实现了使用符号表创建文件以及为一系列行创建CSV文件的功能
我不想提供性能数据-它们将需要绑定到硬件,从质量上讲,它证明了将QVD文件复制到ORC表中的速度与通过网络复制数据的速度差不多。 或者换句话说,从QVD中获取数据是很现实的(在家庭层面上)。
我还实现了创建QVD文件的逻辑-它在python上运行非常快(显然,我还没有达到大批量-不需要。我会到达那里-我将以与“读取”版本相同的方式重写它)。
未来计划
接下来是什么:
- 我计划将Python版本的代码放在github中(此版本将允许您“探索” QVD文件-查看元数据,读写字符,字符串。该版本尽可能简单且显然速度较慢-无需字符表文件,无需顺序读取,即可使用标准库进行处理位等)
- 我考虑为熊猫做一些事情(如read_qvd()),它限制了它在python上的运行速度,以及事实上并非每个QVD都会“适合”内存的事实,因此
- 我考虑将QVD文件作为Spark的数据源-应该不会出现“不进入内存”的问题(那里的语言-scala-与硬件更接近)
而不是后记
很长时间以来,我到处都是QVD文件,似乎“那里的一切都很复杂”。 事实证明,github是一个很好的推动力,但不是很强,我在第一部分中提到过(一种催化剂)。 那是技术问题。 我自己和每个人都注意到(再次确认)-一切都可以在编程中完成,问题是时间和动力。
希望我对细节不厌倦,可以回答问题(通过评论或其他方式)。 如果会继续下去,我一定会写。