0
# Key Encoding and Subspaces
1
2
FoundationDB stores data as ordered key-value pairs where keys are byte sequences. To work effectively with structured data, FoundationDB provides tuple encoding and subspace utilities that allow you to create hierarchical, typed keys while maintaining sort order and efficient range queries.
3
4
## Core Concepts
5
6
- **Tuple Encoding**: Convert structured data (strings, numbers, nested tuples) into sortable byte sequences
7
- **Subspace**: Key prefix management for organizing hierarchical data
8
- **Directory Layer**: Named subspaces with automatic prefix allocation
9
- **Sort Order**: Encoded tuples maintain logical sort order
10
- **Type Preservation**: Round-trip encoding preserves data types
11
12
## Capabilities
13
14
### Tuple Encoding Across Languages
15
16
**Python API:**
17
18
```python { .api }
19
import fdb.tuple as tuple
20
21
def pack(items: Tuple[Any, ...]) -> bytes:
22
"""
23
Encode tuple to bytes for use as key.
24
25
Args:
26
items: Tuple of values to encode (str, int, float, bytes, tuple, None)
27
28
Returns:
29
Encoded bytes suitable for use as FoundationDB key
30
"""
31
32
def unpack(data: bytes) -> Tuple[Any, ...]:
33
"""
34
Decode bytes back to tuple.
35
36
Args:
37
data: Encoded tuple bytes
38
39
Returns:
40
Decoded tuple with original types preserved
41
"""
42
43
def range(prefix: Tuple[Any, ...]) -> Tuple[bytes, bytes]:
44
"""
45
Create key range for all tuples with given prefix.
46
47
Args:
48
prefix: Tuple prefix for range query
49
50
Returns:
51
(begin_key, end_key) tuple for range operations
52
"""
53
```
54
55
**Java API:**
56
57
```java { .api }
58
public class Tuple {
59
public static Tuple from(Object... items);
60
61
public byte[] pack();
62
public static Tuple fromBytes(byte[] data);
63
64
public Tuple add(Object item);
65
public Object get(int index);
66
public int size();
67
68
public Range range();
69
public static Range range(Tuple prefix);
70
}
71
```
72
73
**Go API:**
74
75
```go { .api }
76
// Package: fdb/tuple
77
78
type Tuple []TupleElement
79
80
func (t Tuple) Pack() []byte
81
func Unpack(b []byte) Tuple
82
func (t Tuple) FDBKey() Key
83
func (t Tuple) FDBRangeKeys() (Key, Key)
84
```
85
86
**Ruby API:**
87
88
```ruby { .api }
89
module FDB::Tuple
90
def self.pack(items)
91
def self.unpack(data)
92
def self.range(prefix)
93
end
94
```
95
96
### Supported Data Types
97
98
Tuple encoding supports the following types across all language bindings:
99
100
| Type | Python Example | Java Example | Go Example | Ruby Example | Sort Order |
101
|------|----------------|--------------|------------|--------------|------------|
102
| **Null** | `None` | `null` | `nil` | `nil` | First |
103
| **Bytes** | `b"data"` | `"data".getBytes()` | `[]byte("data")` | `"data".bytes` | Lexicographic |
104
| **String** | `"text"` | `"text"` | `"text"` | `"text"` | Lexicographic |
105
| **Integer** | `42`, `-7` | `42L`, `-7L` | `int64(42)` | `42` | Numeric |
106
| **Float** | `3.14`, `-2.5` | `3.14d` | `float64(3.14)` | `3.14` | Numeric |
107
| **Boolean** | `True`, `False` | `true`, `false` | `true`, `false` | `true`, `false` | false < true |
108
| **Tuple** | `(1, "a")` | `Tuple.from(1, "a")` | `tuple.Tuple{1, "a"}` | `[1, "a"]` | Element-wise |
109
| **UUID** | `uuid.UUID(...)` | `UUID.randomUUID()` | Custom | Custom | Lexicographic |
110
111
### Basic Tuple Encoding Examples
112
113
**Python:**
114
115
```python
116
import fdb.tuple as tuple
117
118
# Encode various data types
119
user_key = tuple.pack(("users", "user123", "profile"))
120
# Result: b'\x02users\x00\x02user123\x00\x02profile\x00'
121
122
score_key = tuple.pack(("game", "chess", 1500, "player456"))
123
timestamp_key = tuple.pack(("events", 1634567890.123, "login"))
124
125
# Hierarchical keys maintain sort order
126
keys = [
127
tuple.pack(("users", "alice")),
128
tuple.pack(("users", "alice", "email")),
129
tuple.pack(("users", "alice", "name")),
130
tuple.pack(("users", "bob")),
131
tuple.pack(("users", "bob", "email"))
132
]
133
# Keys are naturally sorted: alice < alice/email < alice/name < bob < bob/email
134
135
# Decode back to tuple
136
decoded = tuple.unpack(user_key)
137
print(decoded) # ('users', 'user123', 'profile')
138
139
# Create ranges for prefix queries
140
user_range = tuple.range(("users", "alice"))
141
begin_key, end_key = user_range
142
# Finds all keys starting with ("users", "alice")
143
```
144
145
**Java:**
146
147
```java
148
import com.apple.foundationdb.tuple.Tuple;
149
150
// Encode structured keys
151
byte[] userKey = Tuple.from("users", "user123", "profile").pack();
152
byte[] scoreKey = Tuple.from("game", "chess", 1500L, "player456").pack();
153
154
// Hierarchical organization
155
Tuple userPrefix = Tuple.from("users", "alice");
156
Tuple emailKey = userPrefix.add("email");
157
Tuple nameKey = userPrefix.add("name");
158
159
// Range operations
160
Range userRange = Tuple.from("users", "alice").range();
161
162
// Decode
163
Tuple decoded = Tuple.fromBytes(userKey);
164
String table = (String) decoded.get(0); // "users"
165
String userId = (String) decoded.get(1); // "user123"
166
String field = (String) decoded.get(2); // "profile"
167
```
168
169
**Go:**
170
171
```go
172
import "github.com/apple/foundationdb/bindings/go/src/fdb/tuple"
173
174
// Encode keys
175
userKey := tuple.Tuple{"users", "user123", "profile"}.Pack()
176
scoreKey := tuple.Tuple{"game", "chess", int64(1500), "player456"}.Pack()
177
178
// Hierarchical keys
179
userPrefix := tuple.Tuple{"users", "alice"}
180
emailKey := append(userPrefix, "email").Pack()
181
nameKey := append(userPrefix, "name").Pack()
182
183
// Range operations
184
beginKey, endKey := userPrefix.FDBRangeKeys()
185
186
// Decode
187
decoded := tuple.Unpack(userKey)
188
table := decoded[0].(string) // "users"
189
userID := decoded[1].(string) // "user123"
190
field := decoded[2].(string) // "profile"
191
```
192
193
**Ruby:**
194
195
```ruby
196
require 'fdb/tuple'
197
198
# Encode keys
199
user_key = FDB::Tuple.pack(['users', 'user123', 'profile'])
200
score_key = FDB::Tuple.pack(['game', 'chess', 1500, 'player456'])
201
202
# Hierarchical organization
203
user_prefix = ['users', 'alice']
204
email_key = FDB::Tuple.pack(user_prefix + ['email'])
205
name_key = FDB::Tuple.pack(user_prefix + ['name'])
206
207
# Range operations
208
begin_key, end_key = FDB::Tuple.range(['users', 'alice'])
209
210
# Decode
211
decoded = FDB::Tuple.unpack(user_key)
212
table, user_id, field = decoded # ['users', 'user123', 'profile']
213
```
214
215
### Subspace Operations
216
217
Subspaces provide key prefix management for organizing hierarchical data:
218
219
**Python:**
220
221
```python { .api }
222
class Subspace:
223
def __init__(self, prefixTuple: Tuple[Any, ...] = tuple(), rawPrefix: bytes = b''):
224
"""Create subspace with tuple or raw prefix."""
225
226
def pack(self, t: Tuple[Any, ...] = tuple()) -> bytes:
227
"""Pack tuple with subspace prefix."""
228
229
def unpack(self, key: bytes) -> Tuple[Any, ...]:
230
"""Unpack key and remove subspace prefix."""
231
232
def range(self, t: Tuple[Any, ...] = tuple()) -> Tuple[bytes, bytes]:
233
"""Create range within this subspace."""
234
235
def contains(self, key: bytes) -> bool:
236
"""Check if key belongs to this subspace."""
237
238
def subspace(self, t: Tuple[Any, ...]) -> Subspace:
239
"""Create child subspace with extended prefix."""
240
```
241
242
**Subspace Usage Examples:**
243
244
```python
245
import fdb.subspace
246
247
# Create application subspaces
248
app_space = fdb.Subspace(("myapp",))
249
user_space = app_space.subspace(("users",))
250
session_space = app_space.subspace(("sessions",))
251
252
# Work with individual users
253
alice_space = user_space.subspace(("alice",))
254
255
# Generate keys within subspaces
256
profile_key = alice_space.pack(("profile",))
257
settings_key = alice_space.pack(("settings",))
258
preferences_key = alice_space.pack(("preferences", "theme"))
259
260
# Range queries within subspaces
261
@fdb.transactional
262
def get_user_data(tr, user_id):
263
user = user_space.subspace((user_id,))
264
begin, end = user.range()
265
266
data = {}
267
for kv in tr.get_range(begin, end):
268
field_tuple = user.unpack(kv.key)
269
field_name = field_tuple[0] if field_tuple else "unknown"
270
data[field_name] = kv.value.decode()
271
272
return data
273
274
# Check key membership
275
assert alice_space.contains(profile_key)
276
assert not session_space.contains(profile_key)
277
```
278
279
### Complex Data Modeling Patterns
280
281
**User Profile with Nested Data:**
282
283
```python
284
class UserProfileManager:
285
def __init__(self, database):
286
self.db = database
287
self.users = fdb.Subspace(("users",))
288
289
@fdb.transactional
290
def create_user(self, tr, user_id, profile_data):
291
user = self.users.subspace((user_id,))
292
293
# Basic profile
294
tr.set(user.pack(("email",)), profile_data["email"].encode())
295
tr.set(user.pack(("name",)), profile_data["name"].encode())
296
tr.set(user.pack(("created",)), str(time.time()).encode())
297
298
# Preferences with nested structure
299
prefs = profile_data.get("preferences", {})
300
for category, settings in prefs.items():
301
if isinstance(settings, dict):
302
for key, value in settings.items():
303
pref_key = user.pack(("preferences", category, key))
304
tr.set(pref_key, json.dumps(value).encode())
305
else:
306
pref_key = user.pack(("preferences", category))
307
tr.set(pref_key, json.dumps(settings).encode())
308
309
# Tags with individual keys for efficient queries
310
tags = profile_data.get("tags", [])
311
for tag in tags:
312
tag_key = user.pack(("tags", tag))
313
tr.set(tag_key, b"") # Existence key
314
315
@fdb.transactional
316
def get_user_preferences(self, tr, user_id):
317
user = self.users.subspace((user_id,))
318
pref_prefix = user.subspace(("preferences",))
319
begin, end = pref_prefix.range()
320
321
preferences = {}
322
for kv in tr.get_range(begin, end):
323
pref_path = pref_prefix.unpack(kv.key)
324
325
# Reconstruct nested structure
326
current = preferences
327
for part in pref_path[:-1]:
328
if part not in current:
329
current[part] = {}
330
current = current[part]
331
332
current[pref_path[-1]] = json.loads(kv.value.decode())
333
334
return preferences
335
336
@fdb.transactional
337
def find_users_with_tag(self, tr, tag):
338
# Efficient tag-based queries
339
results = []
340
341
begin, end = self.users.range()
342
for kv in tr.get_range(begin, end):
343
user_key_parts = self.users.unpack(kv.key)
344
if len(user_key_parts) >= 3 and user_key_parts[1] == "tags" and user_key_parts[2] == tag:
345
user_id = user_key_parts[0]
346
results.append(user_id)
347
348
return list(set(results)) # Remove duplicates
349
```
350
351
**Time-Series Data with Efficient Queries:**
352
353
```java
354
public class TimeSeriesStore {
355
private final Database db;
356
private final Subspace metricsSpace;
357
358
public TimeSeriesStore(Database db) {
359
this.db = db;
360
this.metricsSpace = new Subspace(Tuple.from("metrics"));
361
}
362
363
public void recordMetric(String metricName, long timestamp, double value, Map<String, String> tags) {
364
db.run(tr -> {
365
// Primary key: metric name, timestamp, then tag hash for uniqueness
366
String tagHash = computeTagHash(tags);
367
Tuple keyTuple = Tuple.from(metricName, timestamp, tagHash);
368
byte[] key = metricsSpace.pack(keyTuple);
369
370
// Store value and tags as JSON
371
Map<String, Object> data = new HashMap<>();
372
data.put("value", value);
373
data.put("tags", tags);
374
data.put("timestamp", timestamp);
375
376
String jsonData = new Gson().toJson(data);
377
tr.set(key, jsonData.getBytes());
378
379
// Secondary indexes for efficient tag queries
380
for (Map.Entry<String, String> tag : tags.entrySet()) {
381
Tuple indexKey = Tuple.from("by_tag", tag.getKey(), tag.getValue(), timestamp, metricName, tagHash);
382
tr.set(metricsSpace.pack(indexKey), key);
383
}
384
385
return null;
386
});
387
}
388
389
public List<DataPoint> queryRange(String metricName, long startTime, long endTime) {
390
return db.read(tr -> {
391
List<DataPoint> results = new ArrayList<>();
392
393
Tuple beginTuple = Tuple.from(metricName, startTime);
394
Tuple endTuple = Tuple.from(metricName, endTime + 1); // Exclusive end
395
396
Range queryRange = new Range(
397
metricsSpace.pack(beginTuple),
398
metricsSpace.pack(endTuple)
399
);
400
401
for (KeyValue kv : tr.getRange(queryRange)) {
402
String jsonData = new String(kv.getValue());
403
DataPoint point = new Gson().fromJson(jsonData, DataPoint.class);
404
results.add(point);
405
}
406
407
return results;
408
});
409
}
410
411
public List<DataPoint> queryByTag(String tagKey, String tagValue, long startTime, long endTime) {
412
return db.read(tr -> {
413
List<DataPoint> results = new ArrayList<>();
414
415
Tuple beginTuple = Tuple.from("by_tag", tagKey, tagValue, startTime);
416
Tuple endTuple = Tuple.from("by_tag", tagKey, tagValue, endTime + 1);
417
418
Range indexRange = new Range(
419
metricsSpace.pack(beginTuple),
420
metricsSpace.pack(endTuple)
421
);
422
423
for (KeyValue indexKv : tr.getRange(indexRange)) {
424
// Get actual data using the stored key
425
byte[] dataKey = indexKv.getValue();
426
byte[] data = tr.get(dataKey).join();
427
428
if (data != null) {
429
String jsonData = new String(data);
430
DataPoint point = new Gson().fromJson(jsonData, DataPoint.class);
431
results.add(point);
432
}
433
}
434
435
return results;
436
});
437
}
438
439
private String computeTagHash(Map<String, String> tags) {
440
// Create deterministic hash of sorted tags
441
return tags.entrySet().stream()
442
.sorted(Map.Entry.comparingByKey())
443
.map(e -> e.getKey() + "=" + e.getValue())
444
.collect(Collectors.joining(","))
445
.hashCode() + "";
446
}
447
448
public static class DataPoint {
449
public double value;
450
public long timestamp;
451
public Map<String, String> tags;
452
}
453
}
454
```
455
456
### Advanced Subspace Patterns
457
458
**Multi-Level Hierarchy with Go:**
459
460
```go
461
type DocumentStore struct {
462
db fdb.Database
463
docsSpace subspace.Subspace
464
indexSpace subspace.Subspace
465
}
466
467
func NewDocumentStore(db fdb.Database) *DocumentStore {
468
root := subspace.Sub(tuple.Tuple{"documents"}.Pack())
469
return &DocumentStore{
470
db: db,
471
docsSpace: root.Sub(tuple.Tuple{"data"}),
472
indexSpace: root.Sub(tuple.Tuple{"indexes"}),
473
}
474
}
475
476
func (ds *DocumentStore) StoreDocument(collection, docID string, doc map[string]interface{}) error {
477
_, err := ds.db.Transact(func(tr fdb.Transaction) (interface{}, error) {
478
// Store document data
479
docSpace := ds.docsSpace.Sub(tuple.Tuple{collection, docID})
480
481
for field, value := range doc {
482
key := docSpace.Pack(tuple.Tuple{field})
483
jsonValue, _ := json.Marshal(value)
484
tr.Set(key, jsonValue)
485
}
486
487
// Build indexes
488
for field, value := range doc {
489
if ds.shouldIndex(field) {
490
indexKey := ds.indexSpace.Pack(tuple.Tuple{collection, field, value, docID})
491
tr.Set(indexKey, []byte{})
492
}
493
}
494
495
return nil, nil
496
})
497
498
return err
499
}
500
501
func (ds *DocumentStore) QueryByField(collection, field string, value interface{}) ([]string, error) {
502
result, err := ds.db.ReadTransact(func(tr fdb.ReadTransaction) (interface{}, error) {
503
var docIDs []string
504
505
indexPrefix := ds.indexSpace.Sub(tuple.Tuple{collection, field, value})
506
begin, end := indexPrefix.FDBRangeKeys()
507
508
ri := tr.GetRange(fdb.Range{begin, end}, fdb.RangeOptions{}).Iterator()
509
for ri.Advance() {
510
kv, err := ri.Get()
511
if err != nil {
512
return nil, err
513
}
514
515
// Extract document ID from index key
516
keyParts := ds.indexSpace.Unpack(kv.Key)
517
if len(keyParts) >= 4 {
518
docID := keyParts[3].(string)
519
docIDs = append(docIDs, docID)
520
}
521
}
522
523
return docIDs, nil
524
})
525
526
if err != nil {
527
return nil, err
528
}
529
530
return result.([]string), nil
531
}
532
533
func (ds *DocumentStore) shouldIndex(field string) bool {
534
// Define which fields should be indexed
535
indexedFields := map[string]bool{
536
"status": true,
537
"category": true,
538
"author": true,
539
"published": true,
540
}
541
return indexedFields[field]
542
}
543
```
544
545
### Performance Considerations
546
547
**Efficient Key Design:**
548
549
```ruby
550
class EfficientKeyDesign
551
def initialize(database)
552
@db = database
553
@metrics = FDB::Subspace.new(['metrics'])
554
end
555
556
# Good: Use tuple encoding for structured keys
557
def store_metric_good(metric_name, timestamp, value, host)
558
key = @metrics.pack([metric_name, timestamp, host])
559
560
FDB.transactional do |tr|
561
tr[key] = value.to_s
562
end
563
end
564
565
# Bad: String concatenation loses sort order and type info
566
def store_metric_bad(metric_name, timestamp, value, host)
567
key = "#{metric_name}:#{timestamp}:#{host}"
568
569
FDB.transactional do |tr|
570
tr[key] = value.to_s
571
end
572
end
573
574
# Good: Hierarchical subspaces for range queries
575
def query_metric_range_good(metric_name, start_time, end_time)
576
metric_space = @metrics.subspace([metric_name])
577
578
FDB.transactional do |tr|
579
results = []
580
581
tr.get_range(
582
metric_space.pack([start_time]),
583
metric_space.pack([end_time + 1]) # Exclusive end
584
).each do |kv|
585
timestamp, host = metric_space.unpack(kv.key)
586
results << {
587
timestamp: timestamp,
588
host: host,
589
value: kv.value.to_f
590
}
591
end
592
593
results
594
end
595
end
596
597
# Good: Batch operations for better performance
598
def store_metrics_batch(metrics_data)
599
FDB.transactional do |tr|
600
metrics_data.each do |data|
601
key = @metrics.pack([data[:name], data[:timestamp], data[:host]])
602
tr[key] = data[:value].to_s
603
end
604
end
605
end
606
end
607
```
608
609
Key encoding and subspaces are fundamental to efficient FoundationDB usage, providing type-safe, sortable keys that enable complex queries while maintaining high performance across all language bindings.