Working with q files¶
This page explains how to persist a q value in q's own on-disk format: writing and reading it with save, load, set, and get. It also covers appending to a file with hopen, and when a single file is the right choice.
Alongside the text and binary formats, q has a native on-disk format. It adds a small header recording the data type, attributes, and length, which is what lets q map a file back into memory as a typed object rather than parse it again. A directory of such files is a kdb database.
You can save any q object to disk — a vector, a dictionary, a nested list, a function, a table. A table saved this way is called a flat-file table (also called basic or non-splayed), to distinguish it from the splayed and partitioned forms. Every keyword below writes it, so each one preserves the type, length, and any attributes along with the data.
The file header¶
The header is 16 bytes for most types, and its size does not depend on the vector's length or attribute. Save a million longs:
q)a: til 1000000
q)save `a
`:a
hcount shows the file is 16 bytes larger than the 8 million bytes of data:
q)hcount `:a
8000016
Those 16 bytes are what a raw binary write omits. Writing the same vector with 1: gives exactly the data and nothing else:
q)`:raw 1: a
`:raw
q)hcount `:raw
8000000
Enumerated columns get a bigger header
An enumerated vector's header also has to record the name of the domain it points to, so it grows to 4096 bytes regardless of the domain name's length:
q)sym:`a`b`c
q)s:`sym$1000000?sym
q)save `s
`:s
q)`:raw 1: s
`:raw
q)hcount `:s
8004096
q)hcount `:raw
8000000
The header is what makes a file readable
get needs the header to know what it is reading. A file written with 1: carries no type information, so get cannot interpret it at all — see writing bytes.
save and load¶
Keywords save and load let you serialize and write any q object to a file of the same name, by default in the working directory. That includes tables, making this the shortest way to persist one.
q)cities:([]city:`Tokyo`Delhi`Shanghai;pop:37435191 29399141 26317104)
q)key `:. / nothing in working directory
`symbol$()
q)save `cities
`:cities
q)key `:. / file in working directory
,`cities
q)delete cities from `. / delete from memory
`.
q)cities
'cities
[0] cities
^
q)load `cities / load from filesystem
`cities
q)cities
city pop
-----------------
Tokyo 37435191
Delhi 29399141
Shanghai 26317104
Because save takes the file name from the variable name, you cannot choose what the file is called. To control the file name and location as well, use set.
Save vectors and dictionaries¶
save works on any global, so the same keyword persists a vector or a dictionary just as it does a table, and get reads either back:
q)v: til 5
q)save `v
`:v
q)get `:v
0 1 2 3 4
q)d: `a`b!1 2
q)save `d
`:d
q)get `:d
a| 1
b| 2
q loads objects like these from a database root as variables, and you can query them alongside the tables. See a database is more than tables.
Tip
By specifying the extension (for example .csv, .xls), you can also export the table in another format.
q)save `cities.csv
`:cities.csv
q)read0 `:cities.csv
"city,pop"
"Tokyo,37435191"
"Delhi,29399141"
"Shanghai,26317104"
No extension means the q format instead. How to work with files covers the recognized extensions, and the rest of q's text and binary file handling.
save and load cannot set the file name, location, or on-disk layout. For those options, you need the keywords used to define them.
set and get¶
Keywords set and get differ from save and load:
setis a binary function; its left argument says where in the filesystem to write.getreturns the object's value rather than the name of the variable it has been assigned to.
Notice the similarity of reading a value from memory to reading it from the filesystem.
q)get `:cities / from filesystem
city pop
-----------------
Tokyo 37435191
Delhi 29399141
Shanghai 26317104
q)get `cities / from memory
city pop
-----------------
Tokyo 37435191
Delhi 29399141
Shanghai 26317104
q)`:foo/bar/bigcities set cities
`:foo/bar/bigcities
q)get `:foo/bar/bigcities
city pop
-----------------
Tokyo 37435191
Delhi 29399141
Shanghai 26317104
As that last example shows, set creates any missing directories in the path, and overwrites an existing target rather than complaining.
value has an overload that does the same job as get, so value `:file and get `:file are interchangeable on a data file.
A trailing slash changes what you get
A trailing slash on the handle writes a directory of column files — a splayed table — rather than a single file:
q)`:file set cities / one file
`:file
q)`:dir/ set cities / a directory, one file per column
`:dir/
The slash matters on reading too, where it selects deferred rather than immediate memory mapping.
Loading a directory of files¶
load on a directory of individual object files gives a dictionary keyed by file name:
q)`:t/a set 1 2 3
`:t/a
q)`:t/b set 4 5 6
`:t/b
q)load `:t
`t
q)t
a| 1 2 3
b| 4 5 6
Instead, rload reads a directory as a splayed table, using its .d file to recover the columns; it's the counterpart to rsave:
q)`:t/ set ([] a: 1 2 3; b: 4 5 6)
`:t/
q)rload `t
`t
q)t
a b
---
1 4
2 5
3 6
To load a whole database rather than one object, use \l.
Appending with hopen¶
Open a q data file with hopen and the handle appends typed values. The file stays a valid q file, and get reads back the extended list:
q)`:L set 10 20 30
`:L
q)h:hopen `:L
q)h[42]
7i
q)h 100 200
7i
q)hclose h
q)get `:L
10 20 30 42 100 200
This is the difference from opening a file that has no q header, where the same handle appends raw bytes and get cannot read the result at all. Which behavior you get depends on the file, not on how you open it.
Use cases¶
Serialization as a flat-file table suits a table that is:
- Small relative to memory
- Read frequently
- Mostly read in full, rather than a few columns at a time
Splay a table too large for that, and partition one that grows over time. See splayed tables and partitioned tables.
Mapped lists¶
A mapped list (anymap, type 77h) is a nested list stored so that its inner vectors stay mapped on disk instead of being copied to the heap. This type supersedes the legacy mapped nested types (77h+t) by dropping the requirement that all elements share a type:
q)a: get`:a set (1 2 3;"cde")
q)b: get`:b set ("abc";"def")
q)77 77h~(type a;type b)
1b
Elements can be of any type, including lists, dictionaries, and tables:
q)a: get`:a set ((1 2;3 4);`time`price`vol!(.z.p;1.;100i);([]a:1 2;b:("ab";"cd")))
q)77 0h~(type a;type first a)
1b
Write `:a 1: x rather than set to let mapped lists nest inside one another. Whatever the depth, all vectors in the structure stay mapped, so q can work with them without copying to the heap — note that used does not move when you extract a column:
q)a: get`:a 1: ((1 2;3 4);([]time:1000?.z.p;price:1000?100.);([]time:1000?.z.p;price:1000?200))
q)77 77h~(type a;type first a)
1b
q).Q.w[]`used`mmap
336736 40432
q)p: exec price from a[1]
q).Q.w[]`used`mmap
336736 40432
Saving compound data creates companion files beside the main one:
file#holds the flat, concatenated bytes of every vector nested inside the structure — the actual payload, as opposed to the shape recorded infileitself — and q always writes it. It stays mapped as long as anything still references a mapped object inside it.file##holds the enumeration domain, and appears only when the data contains symbols. q enumerates symbol vectors and atoms against it automatically. On access it de-enumerates them, and so always copies.
q)`:file set ((`a`b;`b`c);0 1) / the symbols cause file## to be written too
`:file
q stores a nested column of a splayed table the same way.
A column of same-shaped dictionaries is not a nested column
q collapses a list of dictionaries that all share the same keys into a table. Writing such a column hits set's table overload rather than its mapped-list handling, so it lands on disk as a flat-file table, not as an anymap column with #/## companions.
Immediate and deferred memory mapping¶
When the target is a directory, a trailing slash on the handle decides how q maps it:
- Immediate (no trailing slash): q memory-maps the directory's contents at load time.
- Deferred (trailing slash): q maps nothing at load time, and maps and unmaps each file as you access it.
To see the difference, splay a table to disk:
q)`:/tmp/data/tab/ set ([] 100000?100; 100000?1000f)
`:/tmp/data/tab/
Three tools show what happens:
.Q.wfor memory statistics, of whichmmapis the mapped total..Q.s1for the object's structure as a string.\tto time a repeated query; higher is slower.
Load without the trailing slash and q maps everything up front, so queries then run against memory:
q)`used`mmap#.Q.w[]
used| 371616
mmap| 0
q)t:get`:/tmp/data/tab
q)`used`mmap#.Q.w[]
used| 373360
mmap| 1600032
q)\t:100 select from t
0
q)\t:100 select from t
0
q)`used`mmap#.Q.w[]
used| 373360
mmap| 1600032
q).Q.s1 t
"+`x`x1!(81 96 32 46 99 88 61 54 31 6 25 49 61 76 30 10 37 90 92..."
With the trailing slash, mmap stays at zero and each query pays to map the columns it touches. .Q.s1 shows why: the table holds the path, not the data:
q)`used`mmap#.Q.w[]
used| 371616
mmap| 0
q)t:get`:/tmp/data/tab/
q)`used`mmap#.Q.w[]
used| 373344
mmap| 0
q)\t:100 select from t
11
q)\t:100 select from t
10
q)`used`mmap#.Q.w[]
used| 373344
mmap| 0
q).Q.s1 t
"+`x`x1!`:/tmp/data/tab/"
Deferred mode therefore holds less memory over the life of the session, at the cost of mapping on every access. Note that used barely moves in either case: neither mode copies file contents onto the heap.
The trailing slash is significant on write too
It decides whether you get a single file or a directory of column files — see set and get and splayed tables.
Next steps¶
- Database overview — what a database holds, and the flat, splayed, partitioned, and segmented forms a table can take. You persist and query vectors, dictionaries, and nested lists the same way.
- Splayed tables, partitioned tables, and segmented databases — a directory of column files, and the forms that scale beyond one.
- Loading a database — loading a whole database at once, and what it costs in memory.
- Compression and encryption at rest — both configured on the same
setcall. - Text files and binary files — reading and writing the non-native formats.