datalevin 2026-09-30

Running into a possibly rule-related performance oddity, and I'm not sure if its a bug or just a skill issue. My data-model is similar to examples I've used in the past: I have a collection of entities (companies, for this example) that I'm tracking relationships between (the :rel entities), which are queried using as rules (defined in-thread)

(def schema
  {:company/name {:db/valueType :db.type/string, :db/unique :db.unique/identity}
   :rel/from {:db/valueType :db.type/ref}
   :rel/to {:db/valueType :db.type/ref}
   :rel/type {:db/valueType :db.type/keyword}})
I'm making a query for connections (`alias`, in this case) to a specific entity id:
(d/q
    '[:find ?c2
      :in $ % ?c1
      :where
      (alias ?c1 ?c2)]
    (d/db core/conn) query/base-rules 17108)
^The above query executes almost instantly, however, if I add on an arbitrary match on an attribute...
(d/q ;; slow
    '[:find ?c2
      :in $ % ?c1
      :where
      (alias ?c1 ?c2)
      [?c2 :company/name]]
    (d/db core/conn) query/base-rules 17108)
The query takes long enough to spawn a little loading bar in Emacs; in the explain, it looks like the [?c2 :company/name] may have been resolved first (both queries return the same values) There might be some interaction with magic-rewrites happening? Running the above query with *magic-rewrite?* set to false returns instantly again:
(binding [datalevin.rules/*magic-rewrite?* false]
    (d/q
      '[:find ?c2
        :in $ % ?c1
          :where
          (alias ?c1 ?c2)
          [?c2 :company/name]]
      (d/db core/conn) query/base-rules 17108))
It seems like these slowdowns aren't consistent between rules however, I have a rule alias-net, which is the same as alias but also includes an [(identity ?a) ?b] clause, and it has the same fast performance regardless of the *magic-rewrite?* variable. LASTLY (I'm sorry that this is a lot): it looks like that of the queries slowed by the magic-rewrite, that they are slowed down more when more unrelated :rel's are added to the DB. The reproduction for this one is a little more involved (and, forgive me, partially slop-generated), because it only becomes apparent at scale, I will include it in-thread If this is a known possibility, is there a general guideline of when magic-rewrites should be enabled or not? Or should they always remain enabled?

Reproduction:

(require '[datalevin.core :as d]
  '[datalevin.rules :as rules]
  '[datalevin.query.cache :as qcache]
  '[datalevin.util])

(def schema
  {:company/name {:db/valueType :db.type/string, :db/unique :db.unique/identity}
   :rel/from {:db/valueType :db.type/ref}
   :rel/to {:db/valueType :db.type/ref}
   :rel/type {:db/valueType :db.type/keyword}})

(def rules
  '[[(rel ?type ?from ?to)
     [?r :rel/from ?from]
     [?r :rel/type ?type]
     [?r :rel/to ?to]]

    [(bidirectional-rel ?type ?a ?b) (rel ?type ?a ?b)]
    [(bidirectional-rel ?type ?a ?b) (rel ?type ?b ?a)]

    [(transitive-rel ?type ?a ?b)
     (bidirectional-rel ?type ?a ?b)]
    [(transitive-rel ?type ?a ?b)
     (bidirectional-rel ?type ?a ?c)
     (transitive-rel ?type ?c ?b)]

    [(alias ?a ?b)
     (transitive-rel :alias ?a ?b)
     [(!= ?a ?b)]]

    [(alias-net ?a ?b) [(identity ?a) ?b]]
    [(alias-net ?a ?b) (alias ?a ?b)]])

;;; Seed data

(defn seed-db
  "Creates 2000 companies, with 20 groups of aliases of 2-6 companies, and creates `n-other` random :rel entities. Returns an eid from each group of aliases."
  [n-other]
  (let [n-companies 2000
        rnd (java.util.Random. 42)
        dir (str (System/getProperty "java.io.tmpdir") "/dl-magic-repro-" (random-uuid))
        conn (d/create-conn dir schema)
        cid #(- (inc %))
        groups (->> (range 20) (map #(+ 2 (mod % 5))) (reductions + 0) (partition 2 1))]
    (d/transact! conn (mapv #(hash-map :db/id (cid %), :company/name (str "company-" %)) (range n-companies)))
    (d/transact! conn (vec (for [[from to] groups, i (range from (dec to))]
                             {:rel/type :alias
                              :rel/from [:company/name (str "company-" i)]
                              :rel/to [:company/name (str "company-" (inc i))]})))
    (when (pos? n-other)
      (d/transact! conn (vec (repeatedly n-other
                               #(hash-map :rel/type :redirect
                                  :rel/from [:company/name (str "company-" (.nextInt rnd n-companies))]
                                  :rel/to [:company/name (str "company-" (.nextInt rnd n-companies))])))))
    [conn dir (vec (range (ffirst groups) (second (last groups))))]))

;;; Timing

(defn median-ms [f xs]
  (let [ts (vec (sort (for [x xs]
                        (let [t (System/nanoTime)] (f x) (/ (- (System/nanoTime) t) 1e6)))))]
    (nth ts (quot (count ts) 2))))

(defn q [magic? query db & args]
  (binding [rules/*magic-rewrite?* magic?
            qcache/*cache?* false]
    (set (apply d/q query db rules args))))

(def transitive-q '[:find [?x ...] :in $ % ?c :where (transitive-rel :alias ?c ?x)])
(def alias-q '[:find [?x ...] :in $ % ?c :where (alias ?c ?x)])
(def alias-net-q '[:find [?x ...] :in $ % ?c :where (alias-net ?c ?x) [(!= ?c ?x)]])
(def alias+attr-q '[:find [?x ...] :in $ % ?c :where (alias ?c ?x) [?x :company/name]])

(def cases
  [["(transitive-rel :alias ?c ?x)" true transitive-q]
   ["(transitive-rel :alias ?c ?x)" false transitive-q]
   ["(alias ?c ?x)" true alias-q]
   ["(alias ?c ?x)" false alias-q]
   ["(alias-net ?c ?x) [(!= ?c ?x)]" true alias-net-q]
   ["(alias ?c ?x) [?x :company/name]" true alias+attr-q]
   ["(alias ?c ?x) [?x :company/name]" false alias+attr-q]])

;; Testing against a db with only the alias rels, and against a db with 2000 additional random rels:
(let [dbs (mapv seed-db [0 2000])
      eids (fn [conn aliased] (mapv #(d/entid (d/db conn) [:company/name (str "company-" %)]) aliased))]
  (doseq [[conn _ aliased] dbs, [_ magic? query] cases, c (take 3 (eids conn aliased))]
    (q magic? query (d/db conn) c))

  (println "Median ms per query, over 10 companies that have aliases (query cache off):\n")
  (println (format "%-36s %-10s %14s %16s" "where" "magic" "0 other rels" "2000 other rels"))
  (doseq [[label magic? query] cases]
    (println (format "%-36s %-10s %14.1f %16.1f" label (if magic? "on" "off")
               (double (median-ms #(q magic? query (d/db (first (dbs 0))) %) (take 10 (eids (first (dbs 0)) (last (dbs 0))))))
               (double (median-ms #(q magic? query (d/db (first (dbs 1))) %) (take 10 (eids (first (dbs 1)) (last (dbs 1)))))))))

  (println "\nAll queries equivalent?:"
    (every? true?
      (for [[conn _ aliased] dbs, c (take 10 (eids conn aliased))]
        (let [db (d/db conn)
              expected (q false transitive-q db c)
              aliases (disj expected c)]
          (and (= expected (q true transitive-q db c))
            (= aliases (q true alias-q db c) (q false alias-q db c) (q true alias-net-q db c)
              (q true alias+attr-q db c) (q false alias+attr-q db c)))))))

  (doseq [[conn dir] dbs] (d/close conn) (datalevin.util/delete-files dir)))

Running the above produces the following measurements for me:

Median ms per query, over 10 companies that have aliases (query cache off):

where                                magic        0 other rels  2000 other rels
(transitive-rel :alias ?c ?x)        on                   60.6            172.8
(transitive-rel :alias ?c ?x)        off                   4.5              4.1
(alias ?c ?x)                        on                   56.3            169.1
(alias ?c ?x)                        off                   4.5              4.2
(alias-net ?c ?x) [(!= ?c ?x)]       on                    4.3              4.4
(alias ?c ?x) [?x :company/name]     on                  805.0            642.3
(alias ?c ?x) [?x :company/name]     off                  10.1             13.1

All queries equivalent?: true