python

QVD-файлы — что внутри, часть 2

  • пятница, 14 июня 2019 г. в 00:17:17
https://habr.com/ru/company/alfastrah/blog/455940/
  • Блог компании АльфаСтрахование (Сервис будущего в настоящем)
  • Python
  • Data Mining
  • Big Data


В первой статье о структуре QVD файла я описал общую структуру и достаточно подробно остановился на метаданных. В этой статье я опишу формат хранения информации о колонках, поделюсь своим опытом трактовки этих данных.


Итак (вспоминаем) QVD файл соответствует реляционной таблице, которая, как известно состоит из строк. Каждая строка таблицы в свою очередь состоит из колонок (или полей), причем строки имеют одинаковую структуру, которая может быть описана, например, SQL оператором (create table).


В QVD файле таблица хранится в виде двух косвенно связанных частей:


Таблицы символов (термин мой) содержат уникальные значения каждой колонки исходной таблицы. Именно о них пойдет речь ниже.


Таблица строк содержит строки исходной таблицы, каждая строка хранит индексы значений колонки (поля) строки в соответствующей таблице символов. О таблице строк более подробно я расскажу в третьей части этой серии.


На примере нашей таблички (помните — из первой части)


SET NULLINTERPRET =<sym>;
tab1:
LOAD * INLINE [
    ID, NAME
    123.12,"Pete"
    124,12/31/2018
    -2,"Vasya"   
    1,"John"
    <sym>,"None"
];

В этой табличке:


  • 5 строк
  • поле “ID” имеет 4 уникальных значения (NULL не считается значением, более подробно о нем — в третьей части)
  • поле “NAME” имеет 5 уникальных значений
  • первая строка в таблице строк будет содержать индексы 0 и 0, соответствующие значениям 123.12 и “Pete” соответственно

Специальные случаи


Как правило, для всех полей таблицы в QVD файле создаются таблицы символов. Но есть нюансы.


Если поле имеет одно значение, то это значение, как правило, хранится в таблице символов (в этом случае таблица символов будет содержать одну запись). А в таблице строк поле будет отсутствовать (ибо итак понятно, каким должно быть значение этого поля в каждой строке...)


Если поле не имеет значений вовсе (всегда содержит NULL), на него не создается таблица символов.


Эти специальные случаи будут описаны в третьей части, когда мы дойдем до строк и алгоритма их воссоздания.


Хранение таблиц символов в файле


Каждая таблица символов хранится в QVD файле в виде бинарного блока, его смещение (относительно начала бинарного блока) содержится в поле Offset раздела метаданных этого поля, длина (в байтах) — в поле Length метаданных.


Таким образом, первая таблица символов всегда будет имет смещение 0.


Таблицы символов следуют одна за другой и никак друг от друга не отделяются.


Структура таблицы символов


Таблица символов содержит значения поля, которые идут друг за другом без разделителей, каждое значение представлено следующим образом:


  • один байт кодирует тип поля (опишу ниже)
  • далее идет опциональное бинарное значение (зависит от типа поля)
  • за ним идет опциональное строковое значение (зависит от типа поля)

Строки хранятся “как есть” (в кодировке, указанной в метаданных), строка оканчивается нулевым байтом. Строка может иметь нулевую длину, т.е. состоять только из нулевого байта.


Бинарные значения хранятся по правилам той архитектуры, где был сгенерирован QVD файл (из моего опыта их можно просто читать как бинарные значения с оглядкой на "endian-ность").


Типы полей


Все многообразие типов данных QVD свел с трем базовым


  • (1) целое число (4 байта)
  • (2) плавающее число (8 байт)
  • (4) строка, заканчивающаяся нулем

Существуют еще комбинированные типы


  • (5) целое число, после которого идет его строковое представление (4 байта плюс строка с нулевым байтом)
  • (6) плавающее число, после которого идет его строковое представление (8 байт плюс строка с нулевым байтом)

В скобках я привел числовые значения “типов” (первый байт значения поля в таблице символов).


Пытливый ум спросит — “где же тройка?”. Это не ко мне, вопросов у меня тоже много, от комментариев здесь, как сказал герой Хабенского в известном фильме “воздержусь…”.


В-общем-то и все, не сложно — правда?


Два не очень приятных практических наблюдения


Одно и то же поле может иметь в таблице символов значения разных типов (целые, плавающие и строки). Я сам не верил, пока не провел серию экспериментов… Единственное, что можно “гарантировать” (с оговорокой из первой части — ничего гарантировать нельзя), — не может быть смеси “число” и “число со строкой” (либо то, либо другое). Это важно, пытливый ум поймет :-).


Значения не числовых полей (не типов 1 и 2 в нотации выше) приходится читать подряд — невозможно спозиционироваться на поле номер N… Объяснимо, но неэффективно (в плане обработки).


Рассмотрим опять нашу приведенную выше табличку, таблица символов поля ID будет выглядеть так (пишу побайтно/посимвольно):


  • число 6 (тип) + 8 байт (плавающее значение 123.12) + 7 байт (строка "123.12" с нулевым байтом)
  • число 5 (тип) + 4 байта (целое значение 124) + 4 байта (строка "124" с нулевым байтом)
  • число 5 (тип) + 4 байта (целое -2) + 3 байта (строка "-2" с нулевым байтом)
  • число 5 (тип) + 4 байта (целое 1) + 2 байта (строка "1" с нулевым байтом)

Итого 40 байт (см. предыдущую часть — метаданные, значение атрибута Length для поля ID).


Из практики


Практической задачей (одной из), как я уже писал, для меня было воссоздание таблицы по QVD файлу. Из изложенного выше следует (по крайней мере — должно следовать, я старался :-) ), что из описания колонки (метаданные плюс данные) невозможно однозначно определить тип поля (тот, который, например, писать в "create table...").


Как я упоминал в первой части — 90% полей имеют в метаданных тип UNKNOWN, тэги также не позволяют однозначно установить тип поля (не буду грузить читателя подробностями — поверьте)…


Как быть?


Я в своих работах пошел по статистическому пути — анализирую определенный процент значений колонки и по результатам делаю вывод — какой тип ей присвоить. Точность получается вполне удовлетворительной, неприятность в том, что анализировать (в общем случае) нужно все данные… В своей практике я ограничивался первыми 5-10% значений поля.


Если на этом закончить про типы данных, то пытливый ум задаст вполне резонный вопрос — упомянутый "create table" подразумевает куда как большее количество типов данных…


Скажу так: в обработанных файлах не обнаружено данных типов, отличных от вышеперечисленных. Файлы соответствовали вполне реальным таблицам реальных баз данных и содержали весь спектр типов данных (к примеру, мне попались даже блобы… Зачем они в QVD??? Лучше бы комментарии писали).


Наверное, для полноты картины с типами данных надо пояснить про даты и таймстампы (остальные типы — вопрос длины).


Даты представлены в QVD виде целого числа — количество дней от начала эпохи (эпохи клика). Специалисты по QlikView/QlikSense легко скажут — когда она началась (хотя было это 30 декабря 1899 года, не спрашивайте — почему).


Таймстампы в QVD представлены плавающим числом, содержащим дату так, как это описано чуть выше, и время в дробной части (где .0 соответствует времени "00:00:00" и .999999 соответствует времени "23:59:59" — см. более подробно, например, здесь).


Я пока глубоко в эту сторону не копал — мои таблицы, воссозданные из QVD, содержат целые и плавающие типы для полей типа "date" и "datetime". Как вариант можно воспользоваться строковым представлением — для полей этого типа всегда используется комбинированное представление (типы 5 и 6).


Последнее (про практику) — при чтении больших файлов логично создавать индексы для строковых полей, что я и делал. Это существенно сокращает время обработки в случаях, когда размер таблицы символов сильно меньше количества строк (т.е. одно значение встречается в поле исходной таблицы более одного раза).


Подытожим


В этой статье мы рассмотрели хранение уникальных значениях полей (колонок), увидели, что колонки хранятся в виде последовательности уникальных значений, поняли, что типы данных — смешаны и типов всего три (целое, плавающее и строка).


Далее нам осталось познакомиться с тем, как хранятся строки — этому будет посвящена третья, заключительная часть серии статей про структуру QVD.