Skip to content

Commit 58e2e2b

Browse files
committed
[docs] Add notes on DB representation
Also describe how key size limits work.
1 parent 669f4c7 commit 58e2e2b

2 files changed

Lines changed: 154 additions & 0 deletions

File tree

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
---
2+
id: db
3+
title: Database Representation
4+
sidebar_label: Database Representation
5+
---
6+
7+
Glean can use any embeddable key/value store, as long as it provides the following:
8+
9+
* multiple tables (in RocksDB this is "column families", in LMDB it is DBs within an "environment")
10+
* table scanning, i.e. seek to the first key with a given prefix and iterate through subsequent keys in lexicographic order, both forwards and backwards
11+
* concurrent reading and writing, and multiple concurrent readers. Multi-threaded writing is not required.
12+
* saving and restoring the whole DB, while the DB is open for reading. This operation should be *fast*, not much more expensive than just copying the files. This is used by Glean to support backup and restore, and automatic fetching of DBs from remote storage.
13+
* (nice to have) read-only access.
14+
* (nice to have) data compression.
15+
16+
Glean currently supports two DB backends:
17+
18+
* [RocksDB](https://github.com/facebook/rocksdb/)
19+
* [LMDB](https://www.symas.com/mdb)
20+
21+
The rest of this page goes into more detail about exactly how Glean uses the key/value store to store its data.
22+
23+
# Facts
24+
25+
A stored fact comprises
26+
27+
* A **Fact ID**
28+
29+
* A **Predicate ID**. Each stored predicate in the schema has a unique ID. These are assigned by Glean when the DB is created, and stored in the DB itself along with the full schema source.
30+
31+
* A **Key** and a **Value**, binary serialized representations of the fact's key and value.
32+
33+
# DB Tables
34+
35+
The important DB tables are
36+
37+
* **entities**, which maps fact IDs to (key,value) pairs. The encoding of fact IDs is designed so that iterating in lexicographic order returns facts in numeric order.
38+
* **keys**, which maps fact keys to fact IDs.
39+
40+
There are various other tables:
41+
42+
* **admin**, stores various admin values about the DB, such as the DB version and the next unused fact ID.
43+
* **meta**, arbitrary key/value store, used for example to store the schema.
44+
* **stats**, keeps track of the amount of data stored for each predicate
45+
46+
And a number of tables used to keep track of fact ownership, which are described in [Incrementality](incrementality.md).
47+
48+
# Key sizes
49+
50+
Key/value stores typically have a limit on the size of a key. In RocksDB this is very large, so we don't worry about it. For LMDB the limit is much smaller - currently about 2kB - so we have to consider what to do in our *keys* table when the key is larger than the limit.
51+
52+
Let's look at a small example, suppose we have the following facts (ignoring predicate IDs for simplicity):
53+
54+
```
55+
Fact ID Key
56+
57+
1 abc
58+
2 abcd
59+
3 abcde
60+
4 abcdef
61+
5 abcdefg
62+
6 abcdefgh
63+
```
64+
65+
With RocksDB, our *keys* and *entities* tables would look like this:
66+
67+
```
68+
Keys table
69+
70+
abc -> 1
71+
abcd -> 2
72+
abcde -> 3
73+
abcdef -> 4
74+
abcdefg -> 5
75+
abcdefgh -> 6
76+
77+
Entities table
78+
79+
1 -> abc
80+
2 -> abcd
81+
3 -> abcde
82+
4 -> abcdef
83+
5 -> abcdefg
84+
6 -> abcdefgh
85+
```
86+
87+
Note that for each fact, the key is stored twice: once in the *keys* table and once in the *entities* table (although the *value* is stored only once). So this representation is rather wasteful of space, particularly for facts that have large keys.
88+
89+
Suppose the key size is restricted. We'll use a max key size of 4 for this example. Then our tables will look like this:
90+
91+
```
92+
Keys table
93+
94+
abc -> 1
95+
abcd -> 2 -.
96+
abcd -> 3 |
97+
abcd -> 4 | sorted by fact ID
98+
abcd -> 5 |
99+
abcd -> 6 -`
100+
101+
Entities
102+
103+
1 -> abc
104+
2 -> abcd
105+
3 -> abcde
106+
4 -> abcdef
107+
5 -> abcdefg
108+
6 -> abcdefgh
109+
```
110+
111+
For this representation to work, the DB must support storing multiple entries with the same key (LMDB does, given a particular flag). If the DB doesn't support this, then you can append the fact ID to the key to distinguish facts with the same key prefix.
112+
113+
Note that while small keys are stored twice, for larger keys only the prefix up to the DB's max key size is duplicated. This goes a long way towards addressing the earlier space wastage, so we might want to adopt this representation *even when the DB supports larger keys*.
114+
115+
Let's look at the operations we need to support and see how they map to this representation:
116+
117+
## Lookup by key
118+
119+
Glean needs to be able to retrieve a particular fact ID given its full key: `idByKey(key)`.
120+
121+
If the key we're looking up is larger than the max key size, then we'll need to do a linear search of all the keys with the same prefix, and for each entry in the keys table we look up the fact ID in the *entities* table to find the full key, and do a comparison. So this ends up being O(n). However, the claim here is that `n` is usually 1, or at least very small:
122+
123+
Claim:
124+
* `key[0..maxkey-1]` often uniquely determines the rest of the key, or
125+
narrows it down to a small number.
126+
* looking up by full key is uncommon during querying, it mostly happens during writing
127+
128+
What do we typically use large keys for?
129+
130+
* `src.FileLines` - basically `(src.File, [nat])`, so usually the first field uniquely determines the fact.
131+
* `src.File : string`
132+
* looking up pathnames could exceed the key size, pathnames sometimes get long
133+
* but this probably isn't a big issue in general.
134+
135+
## Iterators
136+
137+
An iterator works by starting from a key prefix: `seek(prefix)`
138+
139+
If `prefix` is larger than the max key size, then the iterator will traverse all the entries with the same `prefix[0..maxkey-1]`. For each entry we lookup the full key in the *entities* table, and check to see whether the key matches `prefix`, ignoring those entries that don't match.
140+
141+
Again, this case will typically be rare because key prefixes are usually small.
142+
143+
The other thing to consider here is that the iterator will not necessarily return facts in lexicographic order. Glean used to rely on this property, but we removed that requirement in order to support limited key sizes; see [this PR](https://github.com/facebookincubator/Glean/pull/647/).
144+
145+
Iterators for a stacked DB use an "append iterator" which iterates through the base DB in the stack first and then the stacked DB, so the iterator will first return all the facts from the base DB that match the prefix, followed by facts from the stacked DB.
146+
147+
## Restarting a fact iterator
148+
149+
For resuming queries, we need to be able to save an iterator and restart it when the query is resumed.
150+
151+
A saved iterator is `(Pid, Fact ID)`. This uniquely determines where to restart from, and we can find the start point fast by searching for the pair `(key, fact ID)` in the keys table. LMDB supports this operation with good performance, because it keeps multiple entries with the same key sorted by value and we can seek by both key and value. For a DB that doesn't support this, you could append the fact ID to the key as described above.
152+
153+
For a stacked DB, we know whether the fact ID is in the lower or the upper DB because we know the fact ID boundary between the two DBs.

glean/website/sidebars.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ module.exports = {
9898
'Developers': [
9999
{
100100
'Implementation Notes': [
101+
'implementation/db',
101102
'implementation/incrementality',
102103
],
103104
},

0 commit comments

Comments
 (0)