1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
//! Lightweight cardinality stats used by the cost model.
//!
//! Label and type counts are exact, derived in O(labels + types) from
//! the existing `nodes_by_label` / `relationships_by_type` maps. The
//! per-(label / type, property) distinct counts are estimates from the
//! counting multi-resolution bitmaps in `memory::distinct_stats`, kept
//! for every property key on every write (deletes included) and
//! rebuilt on snapshot load / WAL replay. No separate ANALYZE phase, no
//! background sampling, and no dependence on which hash property indexes
//! happen to be active.
use std::collections::{hash_map::DefaultHasher, BTreeMap, BTreeSet};
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use super::index_catalog::{StoredIndexEntity, StoredIndexKind};
/// `(label or relationship type, property key) → estimated distinct
/// values`.
pub type DistinctValues = BTreeMap<(String, String), usize>;
/// Snapshot of graph cardinality. Populated by the storage backend
/// (see [`super::InMemoryGraph::stats`]).
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct GraphStats {
/// Total live node count.
pub node_count: usize,
/// Total live relationship count.
pub relationship_count: usize,
/// Per-label node count. `nodes_by_label[label].len()`.
pub nodes_by_label: BTreeMap<String, usize>,
/// Per-rel-type relationship count. `relationships_by_type[type].len()`.
pub relationships_by_type: BTreeMap<String, usize>,
/// Per-(label, property) approximate distinct value count, for
/// every property key that a live node with that label carries,
/// indexed or not. Estimated from a distinct-value sketch kept on
/// every write (see `memory::distinct_stats`): a function of the live
/// data alone, so a restarted process, the writer and a process with
/// a different lookup history get the same numbers. Absent only for
/// keys no node of the label has (or whose values the hash index
/// can't key: NaN, durations, points, vectors). Shared: the store
/// reuses one map while the estimates don't change.
pub node_distinct_values: Arc<DistinctValues>,
/// Per-(relationship type, property) distinct value count; as above.
pub relationship_distinct_values: Arc<DistinctValues>,
/// Online catalog-backed range indexes by `(label_or_type, property)`.
pub node_range_indexes: BTreeSet<(String, String)>,
pub relationship_range_indexes: BTreeSet<(String, String)>,
/// Online catalog-backed text indexes by `(label_or_type, property)`.
pub node_text_indexes: BTreeSet<(String, String)>,
pub relationship_text_indexes: BTreeSet<(String, String)>,
/// Online catalog-backed point indexes by `(label_or_type, property)`.
pub node_point_indexes: BTreeSet<(String, String)>,
pub relationship_point_indexes: BTreeSet<(String, String)>,
/// Online catalog-backed vector indexes by `(label_or_type, property)`.
/// Tracked alongside the other index scopes so the optimizer / planner
/// can see them in stats fingerprints, though kNN currently goes
/// through a flat per-query scan rather than a dedicated structure.
pub node_vector_indexes: BTreeSet<(String, String)>,
pub relationship_vector_indexes: BTreeSet<(String, String)>,
/// List elements held by the RANGE index scopes whose property holds
/// lists, by `(label_or_type, property)`: what an element seek
/// (`$x IN n.list`) is costed from. Exact counts, kept on every write.
pub node_list_elements: BTreeMap<(String, String), ListElementStats>,
pub relationship_list_elements: BTreeMap<(String, String), ListElementStats>,
}
/// List elements in one RANGE index scope (see
/// [`GraphStats::node_list_elements`]).
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ListElementStats {
/// Distinct elements across every list.
pub distinct: usize,
/// `(element, entity)` pairs: each list counts each distinct element
/// it holds once.
pub postings: usize,
}
impl ListElementStats {
/// Entities one element seek finds, on average.
pub fn per_element(&self) -> u64 {
(self.postings as u64).div_ceil(self.distinct.max(1) as u64)
}
}
/// Builds a [`GraphStats::plan_signature`]: a hash of every input the
/// planner reads from the stats, with cardinalities coarsened to
/// power-of-two buckets. Backends feed it the same items `GraphStats`
/// holds, in a deterministic order, without building the maps.
pub struct PlanSignature {
hasher: DefaultHasher,
}
impl Default for PlanSignature {
fn default() -> Self {
Self::new()
}
}
impl PlanSignature {
pub fn new() -> Self {
Self {
hasher: DefaultHasher::new(),
}
}
/// Bucket of a cardinality: 0 for 0, else its bit length (1, 2-3,
/// 4-7, ...). Cost estimates within one bucket are within a factor
/// of two of each other.
pub fn bucket(count: usize) -> u32 {
usize::BITS - count.leading_zeros()
}
pub fn totals(&mut self, nodes: usize, relationships: usize) {
(0u8, Self::bucket(nodes), Self::bucket(relationships)).hash(&mut self.hasher);
}
/// A label present in the stats (even with no nodes left: an absent
/// label and an empty one estimate differently).
pub fn label(&mut self, label: &str, nodes: usize) {
(1u8, label, Self::bucket(nodes)).hash(&mut self.hasher);
}
pub fn relationship_type(&mut self, rel_type: &str, relationships: usize) {
(2u8, rel_type, Self::bucket(relationships)).hash(&mut self.hasher);
}
pub fn node_distinct(&mut self, label: &str, property: &str, distinct: usize) {
(3u8, label, property, Self::bucket(distinct)).hash(&mut self.hasher);
}
pub fn relationship_distinct(&mut self, rel_type: &str, property: &str, distinct: usize) {
(4u8, rel_type, property, Self::bucket(distinct)).hash(&mut self.hasher);
}
/// An online range / text / point / vector index scope, as the
/// `*_indexes` sets of [`GraphStats`] hold them.
pub fn index(
&mut self,
entity: StoredIndexEntity,
kind: StoredIndexKind,
label: &str,
property: &str,
) {
(5u8, entity, kind.as_str(), label, property).hash(&mut self.hasher);
}
/// A RANGE index scope holding list elements, by the entities one
/// element seek finds on average.
pub fn list_elements(
&mut self,
entity: StoredIndexEntity,
label: &str,
property: &str,
stats: ListElementStats,
) {
(
6u8,
entity,
label,
property,
Self::bucket(stats.per_element() as usize),
)
.hash(&mut self.hasher);
}
pub fn finish(&self) -> u64 {
self.hasher.finish()
}
}
impl GraphStats {
/// Selectivity of an equality predicate `label:prop = value`.
/// Returns `Some(rows)` when we have enough info to answer; `None`
/// when the optimizer should fall back to its conservative default.
pub fn estimate_node_property_equality(&self, label: &str, property: &str) -> Option<u64> {
let total = self.nodes_by_label.get(label).copied()? as u64;
let distinct = self
.node_distinct_values
.get(&(label.to_string(), property.to_string()))
.copied()
.unwrap_or(1)
.max(1) as u64;
// Uniform-distribution heuristic: each value owns
// ⌈total / distinct⌉ rows.
Some(total.div_ceil(distinct))
}
/// Rows an element seek `$x IN label.property` finds, from the RANGE
/// index on the pair: the average entities per element, or 0 when
/// the indexed values hold no list element. `None` without the index.
pub fn estimate_node_list_element(&self, label: &str, property: &str) -> Option<u64> {
let scope = (label.to_owned(), property.to_owned());
if !self.node_range_indexes.contains(&scope) {
return None;
}
Some(
self.node_list_elements
.get(&scope)
.map_or(0, ListElementStats::per_element),
)
}
/// Mirror of [`Self::estimate_node_list_element`] for relationships.
pub fn estimate_relationship_list_element(
&self,
rel_type: &str,
property: &str,
) -> Option<u64> {
let scope = (rel_type.to_owned(), property.to_owned());
if !self.relationship_range_indexes.contains(&scope) {
return None;
}
Some(
self.relationship_list_elements
.get(&scope)
.map_or(0, ListElementStats::per_element),
)
}
pub fn label_count(&self, label: &str) -> Option<u64> {
self.nodes_by_label.get(label).copied().map(|c| c as u64)
}
pub fn relationship_type_count(&self, rel_type: &str) -> Option<u64> {
self.relationships_by_type
.get(rel_type)
.copied()
.map(|c| c as u64)
}
pub fn fingerprint(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.node_count.hash(&mut hasher);
self.relationship_count.hash(&mut hasher);
self.nodes_by_label.hash(&mut hasher);
self.relationships_by_type.hash(&mut hasher);
self.node_distinct_values.hash(&mut hasher);
self.relationship_distinct_values.hash(&mut hasher);
self.node_range_indexes.hash(&mut hasher);
self.relationship_range_indexes.hash(&mut hasher);
self.node_text_indexes.hash(&mut hasher);
self.relationship_text_indexes.hash(&mut hasher);
self.node_point_indexes.hash(&mut hasher);
self.relationship_point_indexes.hash(&mut hasher);
self.node_vector_indexes.hash(&mut hasher);
self.relationship_vector_indexes.hash(&mut hasher);
self.node_list_elements.hash(&mut hasher);
self.relationship_list_elements.hash(&mut hasher);
hasher.finish()
}
/// Hash of what the planner reads from these stats, with every
/// cardinality coarsened to a power-of-two bucket (see
/// [`PlanSignature`]). Two stats with the same signature have the
/// same labels, types, counted keys and online index scopes, and
/// counts within a factor of two: a plan made for one is a sound
/// and reasonable plan for the other. Plans never depend on stats
/// for correctness, only for cost.
pub fn plan_signature(&self) -> u64 {
let mut sig = PlanSignature::new();
sig.totals(self.node_count, self.relationship_count);
for (label, count) in &self.nodes_by_label {
sig.label(label, *count);
}
for (rel_type, count) in &self.relationships_by_type {
sig.relationship_type(rel_type, *count);
}
for ((label, property), distinct) in self.node_distinct_values.iter() {
sig.node_distinct(label, property, *distinct);
}
for ((rel_type, property), distinct) in self.relationship_distinct_values.iter() {
sig.relationship_distinct(rel_type, property, *distinct);
}
let scopes = [
(
StoredIndexEntity::Node,
StoredIndexKind::Range,
&self.node_range_indexes,
),
(
StoredIndexEntity::Node,
StoredIndexKind::Text,
&self.node_text_indexes,
),
(
StoredIndexEntity::Node,
StoredIndexKind::Point,
&self.node_point_indexes,
),
(
StoredIndexEntity::Node,
StoredIndexKind::Vector,
&self.node_vector_indexes,
),
(
StoredIndexEntity::Relationship,
StoredIndexKind::Range,
&self.relationship_range_indexes,
),
(
StoredIndexEntity::Relationship,
StoredIndexKind::Text,
&self.relationship_text_indexes,
),
(
StoredIndexEntity::Relationship,
StoredIndexKind::Point,
&self.relationship_point_indexes,
),
(
StoredIndexEntity::Relationship,
StoredIndexKind::Vector,
&self.relationship_vector_indexes,
),
];
for (entity, kind, set) in scopes {
for (label, property) in set {
sig.index(entity, kind, label, property);
}
}
for (entity, elements) in [
(StoredIndexEntity::Node, &self.node_list_elements),
(
StoredIndexEntity::Relationship,
&self.relationship_list_elements,
),
] {
for ((label, property), stats) in elements {
sig.list_elements(entity, label, property, *stats);
}
}
sig.finish()
}
pub fn has_node_range_index(&self, label: &str, property: &str) -> bool {
self.node_range_indexes
.contains(&(label.to_owned(), property.to_owned()))
}
pub fn has_node_text_index(&self, label: &str, property: &str) -> bool {
self.node_text_indexes
.contains(&(label.to_owned(), property.to_owned()))
}
pub fn has_node_point_index(&self, label: &str, property: &str) -> bool {
self.node_point_indexes
.contains(&(label.to_owned(), property.to_owned()))
}
pub fn has_relationship_range_index(&self, rel_type: &str, property: &str) -> bool {
self.relationship_range_indexes
.contains(&(rel_type.to_owned(), property.to_owned()))
}
pub fn has_relationship_text_index(&self, rel_type: &str, property: &str) -> bool {
self.relationship_text_indexes
.contains(&(rel_type.to_owned(), property.to_owned()))
}
pub fn has_relationship_point_index(&self, rel_type: &str, property: &str) -> bool {
self.relationship_point_indexes
.contains(&(rel_type.to_owned(), property.to_owned()))
}
pub fn has_node_vector_index(&self, label: &str, property: &str) -> bool {
self.node_vector_indexes
.contains(&(label.to_owned(), property.to_owned()))
}
pub fn has_relationship_vector_index(&self, rel_type: &str, property: &str) -> bool {
self.relationship_vector_indexes
.contains(&(rel_type.to_owned(), property.to_owned()))
}
}