A zero-dependency .NET library and command-line tool for reading and writing QlikView / Qlik Sense QVD files, based on my reverse engineering of the undocumented binary format.
QVD is Qlik's proprietary columnar storage format. There is no official specification and no official way to read one outside a Qlik product. This project covers the whole format:
- All five symbol types — integers, doubles, strings, and both dual values (number + display text in one value, e.g. a date serial together with its formatted text), which many QVD readers skip or flatten.
- NULL handling via the format's index bias mechanism.
- Bit-packed records — rows are stored as little-endian bit-stuffed indexes into per-field symbol tables, at arbitrary bit widths (a year column takes 6 bits per row, a boolean 1 bit).
- A writer, not just a reader —
QvdWriterproduces QVDs that QlikView itself loads (verified in QlikView desktop), including duals and NULLs, with automatic symbol deduplication and minimal bit widths. As far as I can tell it is the only QVD writer in the .NET ecosystem.
The full reverse-engineered format description lives in docs/qvd-file-format.md.
This project has a sequel: vortex-dotnet, a .NET reader for the Vortex columnar format, with a benchmark that puts QVD, Parquet and Vortex side by side on ten million rows of energy data.
using Qvd;
var reader = QvdReader.Open("sales.qvd");
Console.WriteLine($"{reader.TableName}: {reader.RecordCount} records");
// Records decode to typed values, not strings
foreach (QvdValue[] record in reader.ReadRecords())
{
QvdValue amount = record[reader.GetFieldIndex("Amount")];
if (!amount.IsNull)
{
Console.WriteLine($"{amount.Number} (shown as '{amount.DisplayText}')");
}
}
// Column-oriented access: a field's distinct values without touching the rows
IReadOnlyList<QvdValue> countries = reader.GetSymbolTable("Country");
// Or export everything
using var csv = new StreamWriter("sales.csv");
QvdCsv.Write(reader, csv);QvdValue models Qlik's dual semantics directly:
| Kind | Number |
Text |
On disk |
|---|---|---|---|
Integer |
✔ | — | 0x01 + int32 |
Double |
✔ | — | 0x02 + float64 |
Text |
— | ✔ | 0x04 + UTF-8, NUL-terminated |
DualInteger |
✔ | ✔ | 0x05 + int32 + text |
DualDouble |
✔ | ✔ | 0x06 + float64 + text |
Null |
— | — | not in the symbol table (bias) |
Symbol tables are decoded lazily per field and records on demand, so peeking at a few rows of a large file only pays for what you touch.
using Qvd;
var writer = new QvdWriter("Sales", "Country", "Amount", "Updated");
writer.AddRecord(
QvdValue.FromText("Denmark"),
QvdValue.FromDouble(1234.56),
QvdValue.FromDateTime(new DateTime(2026, 7, 3, 14, 30, 0))); // dual: serial + text
writer.AddRecord(
QvdValue.FromText("Norway"),
QvdValue.Null, // NULL (bias-encoded)
QvdValue.FromDualInteger(2026, "Y2026")); // dual: int + text
writer.Save("sales.qvd");The writer deduplicates values into symbol tables, computes the narrowest possible bit width per field (a constant column costs zero bits per row), and encodes NULLs with the same bias convention QlikView uses.
$ qvdtool info sales.qvd
File: sales.qvd
Table: Sales
Records: 56,758
Fields: 9
Record: 30 bytes, bit-packed
FIELD TYPE SYMBOLS BITS NULLS TAGS
year INTEGER 54 6 $numeric $integer
quarter INTEGER 4 2 $numeric $integer
country ASCII 83 7 $text
mtons REAL 9,741 14 yes $numeric
...
$ qvdtool head sales.qvd -n 5 # first rows as a table
$ qvdtool csv sales.qvd -o sales.csv # full export
$ qvdtool symbols sales.qvd country # a field's distinct values
dotnet build
dotnet test
Requires the .NET 8 SDK. No runtime dependencies. No binary fixtures are checked in: reader tests decode files produced by an independent low-level byte encoder, and writer tests round-trip through the reader — so the two implementations cross-check each other.
- Reads standard uncompressed QVDs (the only kind QlikView and Qlik Sense write). A non-empty
Compressionheader is rejected explicitly rather than misread. - The file is buffered in memory, so files are limited to ~2 GB.
- The writer produces standard uncompressed QVDs following the same layout QlikView writes; every write is round-trip tested against the reader, and generated files have been verified to load cleanly in QlikView desktop (duals, dates and NULLs intact).
QVD files start with a plain XML header that freely gives away table and field metadata — including each field's bit offset, bit width, index bias, and symbol-table extents. The binary section behind it was worked out by inspecting files in a hex editor and cross-checking decoded output against the source tables in QlikView: first the symbol tables with their per-value type bytes, then the bit-stuffed record section, and finally the bias mechanism Qlik uses to represent NULLs as negative indexes. Details in docs/qvd-file-format.md.
- Bondski.QvdLib — .NET QVD reader
- devinsmith/qvdreader — C++ QVD reader
- PyQvd — Python QVD reader/writer
MIT — David Linton