query: represent bindings as alists (O(1) extension) - major join speedup
ober
6702e43a64e26008a832d52e576fdf764ac4db17
--- a/lib/jerboa-db/query/engine.ss +++ b/lib/jerboa-db/query/engine.ss @@ -64,15 +64,17 @@ ;; overall for mixed workloads. Vector-indexed bindings would be ideal (O(1) ;; both ways) but require query-compile-time var-index assignment. - (def (make-empty-bindings) (make-hashtable symbol-hash eq?)) + ;; A binding-set is an immutable alist ((var . val) ...). Extension is O(1) + ;; cons (no whole-map copy) — the dominant cost in a deep join that builds many + ;; intermediate bindings — and lookup is assq over the (few) query variables. + ;; Unification only adds *new* vars, so no key ever appears twice. + (def (make-empty-bindings) '()) (def (binding-ref bindings var) - (hashtable-ref bindings var #f)) + (let ([p (assq var bindings)]) (and p (cdr p)))) (def (binding-set bindings var val) - (let ([new-ht (hashtable-copy bindings #t)]) - (hashtable-set! new-ht var val) - new-ht)) + (cons (cons var val) bindings)) (def (resolve-in-bindings bindings x) (if (logic-var? x) @@ -83,17 +85,10 @@ ;; Strip dead variables from bindings after each clause to reduce allocation. (def (project-bindings bindings live-vars n-live) - ;; Return a hashtable containing only keys in live-vars. - ;; Fast path: if count matches, bindings are already minimal — return as-is. - ;; Uses hashtable-size (O(1)) instead of hashtable-keys (allocates vector). - (if (= (hashtable-size bindings) n-live) - bindings ;; fast path: already minimal, no allocation - (let ([new-ht (make-hashtable symbol-hash eq?)]) - (for-each (lambda (v) - (let ([val (hashtable-ref bindings v #f)]) - (when val (hashtable-set! new-ht v val)))) - live-vars) - new-ht))) + ;; Keep only entries whose var is live. Fast path: already minimal. + (if (= (length bindings) n-live) + bindings + (filter (lambda (p) (memq (car p) live-vars)) bindings))) (def (project-bindings-list bindings-list live-vars) ;; Project every binding in the list to live-vars. @@ -568,13 +563,12 @@ (let* ([join-vars (cadr clause)] [sub-clauses (cddr clause)] ;; Build restricted bindings containing ONLY the join vars - [restricted (let ([new-ht (make-hashtable symbol-hash eq?)]) - (for-each - (lambda (v) - (let ([val (binding-ref bindings v)]) - (when val (hashtable-set! new-ht v val)))) - join-vars) - new-ht)] + [restricted (let loop ([vs join-vars] [acc '()]) + (if (null? vs) + acc + (let ([val (binding-ref bindings (car vs))]) + (loop (cdr vs) + (if val (cons (cons (car vs) val) acc) acc)))))] ;; Evaluate sub-clauses from the restricted binding set only [results (evaluate-where-clauses db sub-clauses (list restricted) schema rules-ht)]) @@ -953,12 +947,8 @@ ;; ---- Hash-join execution ---- (def (merge-bindings b1 b2) - ;; Merge two binding hashtables; combined keys from both. - ;; Values in b2 override b1 on conflict (shouldn't conflict on non-join vars). - (let ([new-ht (hashtable-copy b1 #t)]) - (let-values ([(keys vals) (hashtable-entries b2)]) - (vector-for-each (lambda (k v) (hashtable-set! new-ht k v)) keys vals)) - new-ht)) + ;; Combine two binding alists; b2 wins on conflict. Dedup so no var repeats. + (append b2 (filter (lambda (p) (not (assq (car p) b2))) b1))) (def (evaluate-hash-join db ca cb join-vars bindings-list schema) ;; Step 1: evaluate ca from current bindings → side-A @@ -1405,7 +1395,7 @@ ;; Reorder clauses for optimal execution ;; input-bindings is now a list of binding-sets [bound-at-start (if (null? input-bindings) '() - (vector->list (hashtable-keys (car input-bindings))))] + (map car (car input-bindings)))] [db-stats (db-value-stats db)] [card-fn (make-card-fn db)] [ordered-clauses (reorder-clauses where-clauses bound-at-start schema db-stats card-fn)]