This page explains how Datajure queries work. The snippets use the prices dataset from the quick start; the examples show their output. Every option is covered in the README, and the API docs are on cljdoc.

The dt Function

Every query is a call to dt with a dataset and keyword arguments:

(require '[datajure.core :refer [dt dt1 nrow asc desc]])

(dt prices
    :where #dt/e (> :price 100)
    :by [:sym]
    :agg {:n nrow :avg-price #dt/e (mn :price)})
KeywordPurpose
:whereKeep the rows where an expression is true
:setAdd or replace columns, keeping every row
:aggCollapse rows into one row per group (or one row in total)
:byGroup for :set or :agg
:within-orderSort rows within each group before :set or :agg runs
:selectKeep, drop or rename columns
:order-bySort the result
:takeKeep the first n rows, or the last n if negative

dt is a function, not a macro, and always returns a dataset. dt1 takes the same arguments and returns the single result row as a map, or throws if the result is not exactly one row:

(dt1 prices :agg {:n nrow :avg-price #dt/e (mn :price)})
;; => {:n 8, :avg-price 151.5}

A query can also be one map, which makes it plain data that can be stored, merged or built by a program:

(dt prices {:where [:> :price 100] :by [:sym] :agg {:n [:nrow]}})

Evaluation Order

Whatever order the keywords are written in, dt evaluates them in this order:

  1. :where filters rows.
  2. :set or :agg derives or aggregates (:by and :within-order apply here).
  3. :select keeps the listed columns.
  4. :order-by sorts the result.
  5. :take limits the number of rows.

:set and :agg cannot be used in the same call; thread several dt calls with -> instead. An unknown keyword is an error with a suggestion (:wehre -> :where), not silently ignored.

:by with :set or :agg

:by combines with :set and :agg to give four kinds of operation:

Without :byWith :by
:setDerive columns over the whole datasetDerive columns within each group (window functions run per group)
:aggOne summary rowOne row per group
;; one row per symbol
(dt prices :by [:sym] :agg {:n nrow :avg-price #dt/e (mn :price)})

;; every row kept, return computed within each symbol in date order
(dt prices
    :by [:sym]
    :within-order [(asc :date)]
    :set {:ret #dt/e (- (win/ratio :price) 1)})

A :set query keeps the input row order. :within-order only sets the order in which window functions walk each group; use :order-by to sort the output.

Columns Built in Order

:set takes a map or a sequence of pairs. In a map, every expression sees only the input columns, and referring to a sibling is an error. In a sequence of pairs, each expression sees the columns derived before it:

(dt prices :set [[:value #dt/e (* :price :volume)]
                 [:big?  #dt/e (> :value 200000)]])

Expressions

#dt/e and Plain Functions

#dt/e is a reader tag that turns a form into an expression: bare keywords refer to columns, and the expression runs on whole columns at once. Plain Clojure functions also work and are called once per row with a row map:

(dt prices :where #dt/e (> :price 150))     ;; expression, vectorized
(dt prices :where #(> (:price %) 150))      ;; plain function, per row
#dt/ePlain function
Column access:price(:price %)
SpeedVectorizedOne call per row in :where and :set
Missing valuesHandled by the rules belowUp to the function (see pass-nil)
CheckingUnknown columns and operations are reported before running, with suggestionsErrors at run time

Prefer #dt/e, and use plain functions for logic that has no vectorized form, such as Java interop. In :agg, a plain function receives the whole group dataset rather than a row.

Operations

Inside #dt/e:

KindOperations
Arithmetic and math+ - * /, sq, sqrt, pow, log, exp, log1p, expm1, abs, signum, floor, ceil, round
Comparison and logic= < <= > >=, and, or, not, in (set membership), between?
Conditionals and bindingsif, cond, let, when-finite
Missing valuescoalesce, coalesce-finite, div0
Aggregationsmn (mean), sm (sum), md (median), qnt (quantile), sd, mx, mi, ct (non-missing count), nuniq, first-val, last-val, wavg, wsum, cov, corr, tstat, nrow
Calendar partsyear, month, day, dow (1 = Monday), quarter
Window functionswin/rank, win/lag, win/lead, win/cumsum, win/ratio, win/delta, win/mavg, win/mdev, win/ema, win/fills, win/scan, and more
Across columns in a rowrow/sum, row/mean, row/min, row/max, row/count-nil, row/any-nil?
Column transformsstat/standardize, stat/demean, stat/winsorize, stat/trim, stat/rescale
Binningxbar (fixed width), cut (equal-count quantile bins)
(dt prices :set {:size #dt/e (cond (> :price 200) "large"
                                   (> :price 100) "medium"
                                   :else          "small")})

(dt prices :where #dt/e (and (in :sym #{"AAPL" "MSFT"})
                             (between? :price 100 205)))

Aggregations also have full names (mean, sum, median, …), and datajure.concise exports short aliases such as N for nrow.

Missing Values

Datajure handles missing values (nil) with a few rules rather than one blanket guarantee:

(dt prices :set {:volume #dt/e (coalesce :volume 0)})
(dt prices :by [:sym] :within-order [(asc :date)]
    :set {:volume #dt/e (win/fills :volume)})

Runtime Values and Data-Forms

#dt/e is applied when the code is read, so it cannot see a local variable. For values known only at run time, write the expression as a data-form: a vector headed by an operation keyword, with keywords as columns and anything else as a literal:

(let [ticker "MSFT" lo 199]
  (dt prices :where [:and [:= :sym ticker] [:> :price lo]]))

Data-forms cover everything #dt/e does, including windows ([:win/lag :price 1]) and conditionals ([:cond … :else …]), and run on the same vectorized path. They make it easy to generate queries:

(dt prices :by [:sym]
    :agg (into {} (for [c [:price :volume]]
                    [(keyword (str "max-" (name c))) [:mx c]])))

To use a keyword as a value rather than a column, wrap it in lit: #dt/e (= :status (lit :active)), or [:lit status] in a data-form.

Reusable Expressions

An #dt/e expression is a value. Store it in a var and use it in several queries or inside other expressions:

(def value #dt/e (* :price :volume))

(dt prices :set {:value value})
(dt prices :by [:sym] :agg {:avg-value #dt/e (mn value)})

This works for window functions too, which build a shared vocabulary of returns and other time-series measures:

(def ret     #dt/e (- (win/ratio :price) 1))
(def log-ret #dt/e (log (+ 1 ret)))

(dt prices :by [:sym] :within-order [(asc :date)]
    :set {:ret ret :log-ret log-ret})

Selecting Columns

:select accepts several forms:

(dt prices :select [:sym :price])               ;; these columns
(dt prices :select [:not :volume])              ;; all but these
(dt prices :select #"p.*")                      ;; names matching a regex
(dt prices :select :type/numerical)             ;; numeric columns (dates count as numeric)
(dt prices :select {:sym :ticker :price :close}) ;; select and rename

rename renames columns without dropping any: (rename prices {:sym :ticker}).

Grouping

:by takes column keywords, and also:

(require '[datajure.core :refer [xbar]])

(dt prices :by [{:dow #dt/e (dow :date)}] :agg {:avg-price #dt/e (mn :price)})
(dt prices :by [(xbar :price 50)] :agg {:n nrow})

For several passes over the same groups, prepare-grouping computes the grouping once and can be passed as :by in each pass.

Joins

datajure.join/join covers regular joins (:inner, :left, :right, :outer), filtering joins (:semi, :anti), :cross, as-of joins (:asof) and window joins (:window). :validate checks key cardinality, and :report true prints how many rows matched:

(require '[datajure.join :refer [join]])

(join prices sectors :on :sym :how :left :validate :m:1)

In an as-of join, the last key in :on is matched to the latest right row at or before the left row's value, and the other keys must match exactly. :direction (:backward, :forward, :nearest) and :tolerance control the match:

(join trades quotes :on [:sym :time] :how :asof)

See joins, as-of joins and window joins in the README for all options.

Reshaping

datajure.reshape has melt (wide to long), cast (long to wide) and tsfill, which fills gaps in panel data so that each group has a row for every period:

(require '[datajure.reshape :as reshape])

(reshape/cast prices {:id [:date] :from :sym :value :price})
(reshape/melt prices {:id [:date :sym] :measure [:price :volume]})

Other Namespaces

NamespaceContents
datajure.ioread, write and read-seq for CSV, TSV, JSON, JSON Lines, Nippy, Parquet, Arrow and Excel
datajure.utildescribe, clean-column-names, distinct-rows, mark-duplicates, coerce-columns, blank->nil, parse-numeric
datajure.indexindex-by and lookup for repeated lookups by key
datajure.conciseShort aliases (mn, sm, N, …) for use outside #dt/e
datajure.clay, datajure.clerkNotebook viewers
datajure.nreplnREPL middleware that binds *dt* to the last result

Errors

Errors are ex-info exceptions whose data has a :dt/error key for handling them in code. Many suggest a fix, such as the closest column name for a misspelled column or the intended keyword for a misspelled dt keyword:

(dt prices :set {:value #dt/e (* :prcie :volume)})
;; Unknown column(s) #{:prcie} in :set :value expression
;; Did you mean: :price (edit distance 1)
;; Available: :date :price :sym :volume

See the examples for more.


Copyright © 2026 Datajure
Powered by Cryogen