QVD-файлы — что внутри, часть 3
- вторник, 25 июня 2019 г. в 00:18:18
В первой статье о структуре 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 байт, где
Таблица строк начинается со смещения "Offset" (тэг метаданных) относительно начала бинарной части файла.
Информация о таблице строк (длина, размер строки, смещение) хранится в общей части метаданных.
Все строки таблицы строк имеют одинаковый формат и представляют из себя конкатенацию "чисел без знака". Длина числа — минимально достаточна для представления конкретного поля: длина зависит от количества уникальных значений конкретного поля.
Для полей с одним значением (как я уже писал) эта длина будет равна нулю (это значение одинаково в каждой строке исходной таблицы и хранится в соответствующей таблице символов).
Для полей с двумя значениями эта длина будет равна единице (возможные значения индекса в таблице символов — 0 и 1), и так далее.
Поскольку совокупная длина строки таблицы строк должна быть кратна байту, длина "последнего символа" выравнивается до границы байта (увидим ниже, когда будем разбирать нашу табличку).
Информация о формате каждого поля хранится в разделе метаданных, посвященных этому полю (остановимся чуть подробнее ниже), длина битового представления поля хранится в тэге "BitWidth".
Как хранить отсутствующие значения? Воздерживаясь от рассуждений на тему "почему", отвечу так: насколько я понял, NULL значениям соответствует следующая комбинация
Соответственно, все остальные индексы в колонке, имеющей NULL значения, увеличены на 2 — увидим на нашем примере чуть ниже.
Порядок следования полей в строке таблицы строк соответствует битовому смещению поля, которое хранится в тэге "BitOffset" раздела метаданных, относящихся к данному полю.
Разберем наш пример (см. метаданные в части первой этой серии).
Поле "ID"
Поле "NAME"
Давайте посмотрим на реальные "нолики и единички" — я буду приводить фрагменты QVD файла в виде двоичного представления "в шестнадцатеричном формате" (так компактнее).
Сначала вся бинарная часть целиком (выделена розовым, метаданные обрезаны — уж больно их много...)
Достаточно компактно, согласитесь. Давайте вглядимся повнимательнее — сразу после метаданных расположены таблицы символов (метаданные, кстати, в данном файле окончились переводом строки и нулевым байтом — технически такое бывает, нулевые байты после метаданных нужно пропускать...).
Первая таблица символов выделена на рисунке ниже.
Видим:
Первое уникальное значение поля "ID" это
Остальные три уникальных значения имеют тип 5 (целое число со строкой) — значения "124", "-2" и "1" (легко видеть по строкам).
На рисунке ниже выделил вторую таблицу символов (для поля "NAME")
Первое уникальное значение поля "NAME" — тип "4" (первый выделенный байт) — строка, заканчивающаяся нулем.
Остальные четыре уникальных значения — также строки "12/31/2018", "Vaysa", "John" и "None".
Теперь — таблица строк (выделил на рисунке ниже)
Как и ожидалось — 5 байт (5 строк по одному байту).
Первая строка (соответствующая строке 123.12, "Pete" нашей таблички)
Значение строки — байт "02" (бинарно 000000010).
Разделим его (вспоминаем описание выше)
Вторая строка (124,12/31/2018) в таблице строк
Значение — байт "0B" (бинарно 00001011)
Ну и так далее, давайте посмотрим быстренько на последнюю строку — там у нас было ,"None" (т.е. NULL и строка "None"):
Значение — байт "20" (бинарно 0010000)
ВАЖНО не могу найти пример, подтверждающий это, но мне попадались файлы, которые содержали итоговый индекс -1 для NULL значений. Поэтому в своих программах я считаю NULL-ами все поля, итоговый индекс которых отрицателен.
В завершение разбора формата QVD кратко остановлюсь на важных нюансах — длинные строки в таблице строк хранят поля в порядке "справа — налево", где самым правым будет поле с нулевым битовым смещением (как я и описывал выше). НО порядок байтов — обратный, т.е. первый байт будет самым правым (и будет содержать "правое" поле — поле с нулевым битовым смещением), последний — первым (т.е. содержать самое "левое" поле — поле с максимальным битовым смещением).
Нужно привести пример, но не перегрузить деталями. Давайте рассмотрим такую табличку (привожу фрагмент — чтобы получить длинные строки в таблице строк необходимо увеличить количество уникальных значений).
tab2:
LOAD * INLINE [
ID, VAL, NAME, PHONE, SINGLE
1, 100001, "Pete1", "1234567890", "single value"
2, 200002, "Pete2", "2234567890", "single value"
...
];
В кратком виде информация о полях (выжимка из метаданных):
Таблица строк состоит из строк длиной 3 байта, соответственно, в строке таблицы строк данные о полях логически разложатся так:
Логическая последовательность преобразуется в физическую перестановкой байт в обратном порядке, т.е.
Посмотрим на примерах, вот как выглядит первая строка таблицы строк (выделена розовым)
Значения полей
Т.е первая строка содержит первые символы из соотвествующих таблиц символов.
Вообще удобно начинать разбор именно с первой строки — она, как правило, содержит нули в качестве индекса (так уж строится QVD файл, что в таблицу символов первыми попадают значения из первой строки).
Давайте для закрепления посмотрим еще на вторую строку
Значения полей
Т.е вторая строка содержит вторые символы из соотвествующих таблиц символов.
Немного поделюсь опытом — как я технически "читал" QVD.
Первая версия была написана на питоне (я ее облагорожу и выложу в github).
Достаточно быстро выяснились основные проблемы:
Часть из этих проблем может быть решена сменой языка (с питона на Си, например). Часть потребовала каких-то дополнительных действий.
Текущая достаточно быстрая реализация выглядит так — общая логика реализована на питоне, а наиболее критичные операции вынесены в отдельные Си программы, запускаемые параллельно.
Коротко
По производительности цифр давать не хочу — они потребуют привязки к железу, на качественном уровне получается скопировать QVD файл в ORC таблицу примерно со скоростью копирования данных по сети. Или, другими словами, взять данные из QVD вполне реально (на бытовом уровне).
Я также реализовал логику создания QVD файлов — она достаточно быстро работает на питоне (видимо, я еще не дошел до больших объемов — нет потребности. Дойду — перепишу аналогично "читающему" варианту).
Что дальше:
Давно я ходил вокруг да около QVD файлов, казалось, что "там все сложно". Оказалось, что сложно, но не очень, хорошим толчком послужил github, который я упомянул в первой части (своего рода катализатор). Дальше уже было дело техники. Себе и всем на заметку (еще одно подтверждение) — в программировании все можно сделать, вопрос во времени и мотивации.
Надеюсь, не очень утомил подробностями, готов ответить на вопросы (в комментариях или любым другим способом). Если будет продолжение — обязательно напишу.