QVD文件-内部内容,第3部分

第一篇有关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是一个很好的推动力,但不是很强,我在第一部分中提到过(一种催化剂)。 那是技术问题。 我自己和每个人都注意到(再次确认)-一切都可以在编程中完成,问题是时间和动力。


希望我对细节不厌倦,可以回答问题(通过评论或其他方式)。 如果会继续下去,我一定会写。

Source: https://habr.com/ru/post/zh-CN457102/


All Articles