DeciDB Documentation

Complete reference for writing optimization queries with the DECIDE clause.

Syntax Overview

DeciDB adds a DECIDE clause to standard SQL. You declare decision variables, define constraints with SUCH THAT, and optionally set an objective with MAXIMIZE or MINIMIZE. DeciDB solves the optimization problem and returns every candidate row with the optimal variable values filled in.

Complete Syntax

SQL
-- Single-block order: the whole clause sits after WHERE
SELECT select_list
FROM table_expression
[WHERE filter_conditions]
DECIDE [scalar] [Table.]variable(INT | BOOL | REAL) [, ...]
SUCH THAT
    constraint [AND constraint ...]   -- constraints may use WHEN, PER
[MAXIMIZE | MINIMIZE] objective        -- reducers / POWER / bilinear / bare scalar

-- Split order: the declaration sits between SELECT and FROM
SELECT select_list
DECIDE [scalar] [Table.]variable(INT | BOOL | REAL) [, ...]
FROM table_expression
[JOIN ...]
[WHERE filter_conditions]
SUCH THAT
    constraint [AND constraint ...]
[MAXIMIZE | MINIMIZE] objective

Both orders parse to the same plan. Put the declaration in one position or the other, never both. A DECIDE with no SUCH THAT, or a SUCH THAT with no DECIDE, is a parser error.

Execution Order

DeciDB processes your query in this order:

  1. 1
    FROM + WHERE — Standard SQL filtering produces candidate rows
  2. 2
    DECIDE — Creates the decision variables, one per candidate row unless the declaration says otherwise
  3. 3
    SUCH THAT + Objective — Constraints and objective are sent to the solver
  4. 4
    SELECT — Returns all candidate rows with their optimal variable values

Decision variables are available in SUCH THAT, MAXIMIZE/MINIMIZE, and SELECT.

The DECIDE Clause

Use DECIDE to declare one or more decision variables. An unqualified declaration creates one instance per candidate row; table-qualified and scalar declarations use the other scopes described below. The solver assigns optimal values to these variables.

SQL
DECIDE variable_name(TYPE) [, variable_name2(TYPE) ...]

Variable Types

The type is required and goes in parentheses after the variable name. There are exactly three: x(BOOL), x(INT), and x(REAL). Omitting it is a parser error:

Error
DECIDE variable "x" needs a type; write x(INT), x(BOOL) or x(REAL)

BOOLEAN — select or skip

The variable takes the value 0 or 1. Use this when each row is either included or excluded.

SQL
DECIDE x(BOOL)

INTEGER — choose a quantity

The variable starts with a non-negative integer domain (0, 1, 2, 3, ...), but an explicit negative lower bound makes it signed. Use this when you need to decide a whole-numbered quantity.

SQL
DECIDE quantity(INT)

REAL — continuous quantity

The variable starts with a non-negative continuous domain, but an explicit negative lower bound makes it signed. Use this for decisions like dollar amounts, percentages, or flows. Note that <, >, and <> on REAL variables are rejected — use <= / >= instead.

SQL
DECIDE amount(REAL)

Signed Variables

INT and REAL decisions start at 0. That is a default, not a floor. A variable becomes signed as soon as the query gives it an explicit negative lower bound: x >= -K, x BETWEEN -K AND K, or a negative value in an IN domain. A variable the query never lowers stays non-negative.

SQL
DECIDE adj(REAL) ... SUCH THAT adj BETWEEN -10 AND 10   -- adj ranges over -10 .. 10
DECIDE d(INT)    ... SUCH THAT d >= -5                  -- d is at least -5
DECIDE s(INT)    ... SUCH THAT s IN (-5, 0, 5)          -- s is one of -5, 0, 5

There is no fully unbounded domain. A signed variable always has a finite lower bound.

Multiple Variables

Declare multiple variables separated by commas. In the unqualified form below, each row gets its own instance of every variable; scopes may also be mixed in one declaration list.

SQL
-- Two variables per row: include or not, and how many
DECIDE x(BOOL), quantity(INT)

Variable Scopes

How you spell the declaration decides how many values the solver assigns.

Spelling Scope How many values
x(INT) row-scoped (default) one per result row
T.x(INT) table-scoped one per distinct row of T
scalar x(INT) query-wide exactly one, for the whole query

Table-Scoped Variables

By default, decision variables are row-scoped — one variable per result row. Prefix the variable with a table alias to make it table-scoped: one variable per unique entity in the source table, shared across every result row originating from that entity. This reduces the solver variable count from the size of the join result to the number of distinct entities.

SQL
-- One decision per nurse, even though each nurse appears in many shift rows
SELECT n.name, s.shift_date, keepN
FROM nurses n
JOIN shifts s ON n.id = s.nurse_id
DECIDE n.keepN(BOOL)
SUCH THAT SUM(keepN * s.hours) <= 100
MAXIMIZE SUM(keepN * n.skill_score);

The qualifier (n.) must match a table or alias in the FROM clause. SUM still sums over result rows, so an entity that appears in k joined rows contributes its variable k times. To count each entity once instead, qualify the reducer — see Reducers.

The entity is identified by all columns of the source table. Two rows of nurses share a variable only when every column matches. There is no syntax for picking a subset of columns as the key.

Query-Wide Variables (scalar)

Write scalar before the name to declare one decision shared by the entire query, however many rows the query returns. Use it when the thing you are deciding is not attached to any row — a single threshold, a shared ceiling, a fleet-wide budget.

SQL
-- Spread the shipments so the busiest route is as light as possible
SELECT r.regionID, ship, max_load
FROM routes r
DECIDE ship(INT), scalar max_load(INT)
SUCH THAT SUM(ship) >= 12 AND ship <= max_load
MINIMIZE max_load;
  • • The assigned value repeats on every output row.
  • • A query-wide decision may be the bound of an aggregate: SUM(ship) <= max_load.
  • • It may not be table-qualified.
Error — DECIDE scalar t.m(INT)
scalar DECIDE variable "m" cannot name a table; write scalar m(INT),
or drop scalar for one decision per t row

Using Variables in SELECT

Reference decision variables in SELECT to see their optimal values in the output. You can also use them in expressions.

SQL
SELECT id, price, x, (x * price) AS cost
FROM Products
DECIDE x(BOOL)
SUCH THAT SUM(x * price) <= 1000
MAXIMIZE SUM(x * profit);

This returns every product row. The x column shows 1 for selected products and 0 for skipped ones. The cost column shows the price for selected items and 0 otherwise.

The SUCH THAT Clause

Use SUCH THAT to define constraints that the solution must satisfy. Constraints are separated by AND. Linear and supported degree-two quadratic or bilinear constraints are accepted; solver-specific limits are listed below.

Row-Level Constraints

A constraint without SUM() applies independently to each row. Use this to bound individual variable values.

SQL
SUCH THAT
    x >= 0
    AND x <= 5

Each row's x is independently constrained between 0 and 5.

Aggregate Constraints

Wrap an expression in SUM() to constrain the total across all rows. This is how you express global limits like total weight or budget.

SQL
DECIDE x(BOOL)
SUCH THAT
    SUM(x * weight) <= 100
    AND SUM(x * volume) <= 50
    AND SUM(x) >= 5

The total weight of selected items must be at most 100, total volume at most 50, and at least 5 items must be selected.

Comparison Operators

Operator Example Result
= SUM(x) = 3 Exactly 3 items selected
<> x <> 0 Variable is not zero
<, <= SUM(x * weight) <= 50 Total weight at most 50
>, >= SUM(x) >= 3 At least 3 items selected

The strict operators (<, >, <>) require the left-hand side to be integer-valued — every term must involve only INT / BOOL variables with integer coefficients. Constraints involving REAL variables must use <= or >= instead.

Which Side the Decision Goes On

Either side of the comparison may carry the decision. SUM(x) <= 10 and 10 >= SUM(x) are the same constraint, and a reducer may appear on both sides at once.

SQL
-- The bound may be written first
SUCH THAT 5 >= x AND 10 >= SUM(x)

-- Reducers on both sides
SUCH THAT SUM(x * v) <= SUM(y * v)

-- A plain data column as the bound of a grouped constraint
SUCH THAT SUM(ship) <= stock PER depotID

-- A data-only aggregate as the bound
SUCH THAT SUM(x * val) <= SUM(val)

A side carrying no decision must still come down to one value per group. For <= and < that is the smallest value it takes in the group; for >= and >, the largest. = rejects a bound that varies within a group. <> keeps every value the bound takes as its own exclusion.

BETWEEN and IN

Use BETWEEN to constrain a variable to a range, or IN to restrict it to specific discrete values. IN on an aggregate, such as SUM(x) IN (...), is not supported.

SQL
-- x must be between 1 and 10 (inclusive)
SUCH THAT x BETWEEN 1 AND 10

-- x must be one of these values
SUCH THAT x IN (1, 2, 3)

Conditional Constraints (WHEN)

Append WHEN condition to make a constraint apply only to rows (or groups) for which the condition holds. WHEN works as a postfix on both per-row and aggregate constraints, and the condition may reference table columns or constants (but not decision variables).

SQL
-- Per-row: x must be at most 1 only for premium items
SUCH THAT x <= 1 WHEN tier = 'premium'

-- Aggregate: sum only over rows where category = 'A'
SUCH THAT SUM(x * weight) <= 50 WHEN category = 'A'

Aggregate-Local WHEN

A WHEN can attach to a single aggregate inside a larger expression. It filters only that aggregate; the other terms keep their own rows.

SQL
-- Morning hours and evening hours together at most 40
SUCH THAT
    SUM(x * h) WHEN (shift = 'morning') + SUM(x * h) WHEN (shift = 'evening') <= 40

-- In an objective, a single simple comparison needs no parentheses
MAXIMIZE SUM(x * profit) WHEN category = 'electronics'

In a constraint the condition must be parenthesised, so that the trailing comparison stays the bound. An objective has no trailing bound, so a single simple comparison may omit them. Parenthesise any condition containing AND, OR, NOT, or arithmetic.

Error — unparenthesised condition in a constraint
syntax error at or near "="
Hint: wrap the WHEN condition in parentheses — e.g. WHEN (a = b),
WHEN (NOT flag), or WHEN (a + b > 5).

Do not mix an aggregate-local WHEN with a WHEN on the whole constraint or objective. Use one or the other.

Grouped Constraints (PER)

Append PER column (or PER (col1, col2, ...)) to an aggregate constraint to emit one constraint per distinct value (or combination) of those columns. Empty groups (filtered out by WHEN) are skipped.

A PER column may be written bare or qualified — PER empID, PER e.empID and PER employees.empID all mean the same thing. Use the qualifier when a join makes the bare name ambiguous. Mixed forms such as PER (empID, dept.id) are accepted. Rows where a PER column is NULL are left out of every group.

SQL
-- One assignment per worker
SUCH THAT SUM(x) <= 1 PER worker_id

-- At most 2 items per (sector, region) pair
SUCH THAT SUM(x) <= 2 PER (sector, region)

MIN and MAX in Constraints

MIN(expr) and MAX(expr) over a linear expression are supported, in both directions and with =.

A MIN or MAX term may also sit inside a larger sum, as in SUM(x) + MAX(x * v) <= 40. In that composed form the outer comparison may be <, <=, >, >= or BETWEEN low AND high for a genuine range. Direct equality on the composed expression is not supported in the current implementation.

SQL
-- No row exceeds capacity
SUCH THAT MAX(x * weight) <= 50

-- At least one row must reach the target
SUCH THAT MAX(x * value) >= 100

AVG and ABS

AVG(expr) and ABS(linear_expr) are both supported in constraints. AVG averages over the rows the constraint covers, which under WHEN or PER is the rows left in the group.

SQL
-- Average cost across selected items at most 25
SUCH THAT AVG(x * cost) <= 25

-- Deviation from target within tolerance
SUCH THAT ABS(x - target) <= 5

Scalar Subqueries

A subquery on the right-hand side of a constraint can compute a bound from another table. Both uncorrelated and correlated subqueries are accepted: correlated subqueries are decorrelated into joins so they produce a per-row value. An ordinary RHS subquery cannot reference a decision variable declared by the enclosing query.

SQL
-- Uncorrelated: a single scalar bound shared by all rows
SUCH THAT
    SUM(x) <= (SELECT COUNT(*) FROM Drivers)

-- Correlated: per-row bound pulled from another table
SUCH THAT
    x <= (SELECT budget FROM Depts WHERE Depts.id = Items.dept_id)

For aggregate constraints, the subquery RHS must evaluate to the same scalar for all rows in the aggregate. A scalar RHS subquery may contain its own DECIDE clause; that inner decision query is solved first and supplies the outer bound. It cannot reference decision variables declared by the enclosing query.

Casts

A cast over a decision is rejected inside SUCH THAT and the objective. This covers CAST, TRY_CAST and ::, whatever the target type. Cast the data or the bound instead.

Error — SUCH THAT CAST(x AS INTEGER) <= 3
DECIDE does not allow casts over decision expressions: 'CAST(x AS INTEGER)'.
Decision values are modeled directly as DOUBLE. Remove the cast; casts over
data-only expressions are allowed.
SQL — the accepted forms
-- Cast the bound
SUCH THAT x <= CAST(3.7 AS INTEGER)

-- Cast the data
MAXIMIZE SUM(x * CAST(price AS BIGINT))

-- Ordinary SQL casts in the SELECT list work as usual
SELECT id, CAST(x AS INTEGER) AS chosen FROM ...

A cast DuckDB inserts on its own, to reconcile SQL types, is not affected by this rule.

NULLs

A NULL in any number the solver reads — a coefficient, a bound, anything inside a SUM — stops the query. It is never treated as zero, because a missing weight and a weight of zero are different problems and only you know which one you meant.

Error
DECIDE: column "weight" is NULL. Impute it with COALESCE(weight, 0)
or filter those rows out with a WHERE clause.

Both fixes work, and the COALESCE can go inside the SUM:

SQL
-- Treat a missing weight as zero
SUCH THAT SUM(x * COALESCE(weight, 0)) <= 50

-- Or drop those rows before deciding
WHERE weight IS NOT NULL

A NULL in a WHEN condition or PER key excludes that row from the filter or group. A NULL coefficient or bound still raises an error even when the clause also uses WHEN or PER.

Expression Shapes

Terms in constraints (and objectives) can be linear, quadratic, or bilinear. Total degree must be at most 2 — products of three or more decision variables are rejected.

Always linear

SQL
x * 5
x * price
SUM(x * price + y * cost)

Quadratic / bilinear

SQL
POWER(x - target, 2)
(x - target) ** 2
x * y

Solver coverage. Both Gurobi and HiGHS support continuous convex quadratic objectives with a non-singular quadratic matrix, and both handle bilinear products where one factor is BOOL through linearization. Coupled or rank-deficient quadratic groups, non-convex quadratic objectives (MAXIMIZE SUM(POWER(expr, 2))), mixed-integer quadratic objectives, general bilinear products of non-Boolean variables, and quadratic constraints are Gurobi only.

Still invalid

SQL
SIN(x)              -- non-polynomial functions
x * x * x           -- total degree > 2
POWER(x, 3)         -- only exponent 2 is supported

Quadratic Constraints (QCQP)

Constraints whose left-hand side contains a POWER(linear_expr, 2), expr ** 2, or (expr)*(expr) term — or a bilinear product of non-Boolean variables — are supported with Gurobi via GRBaddqconstr. HiGHS rejects these with a clear error.

SQL
-- Per-row: distance from target within a radius
SUCH THAT POWER(x - target, 2) <= 9

-- Aggregate: total variance budget
SUCH THAT SUM(POWER(x - target, 2)) <= 1000

Full Example

SQL
SELECT id, value, weight, x
FROM Items
DECIDE x(BOOL)
SUCH THAT
    SUM(x * weight) <= 100
    AND SUM(x * volume) <= 50
    AND SUM(x) >= 5
    AND SUM(x) <= (SELECT capacity FROM Config WHERE name = 'max_items')
MAXIMIZE SUM(x * value);

Select at least 5 items without exceeding weight, volume, or capacity limits. Maximize total value.

The Objective Clause

Use MAXIMIZE or MINIMIZE to tell the solver what to optimize. Row- and table-scoped decisions belong inside supported reducers; a query-wide scalar decision may also appear as a bare term. SUM, AVG, MIN, and MAX are supported, along with quadratic (POWER) and bilinear (x * y) shapes.

SQL
-- Maximize total value of selected items
MAXIMIZE SUM(x * value)

-- Minimize total cost
MINIMIZE SUM(x * cost)

-- Complex coefficients work too
MAXIMIZE SUM(x * (revenue - cost) * discount_factor)

-- Multiple variables in one objective
MAXIMIZE SUM(x * profit_a + y * profit_b)

-- A query-wide decision contributes once, without a reducer
MINIMIZE max_load - SUM(ship)

Requirements

  • • The objective must use a supported aggregate (SUM, AVG, MIN, MAX), a bare query-wide scalar decision, or an additive combination of those terms. A bare row- or table-scoped decision is ambiguous across rows and is rejected. COUNT is rejected: "[MAXIMIZE|MINIMIZE] clause does not support function 'count', only SUM, AVG, MIN, or MAX is allowed."
  • • Must involve at least one decision variable.
  • • Each term has total degree at most 2 in decision variables (linear, quadratic via POWER(expr, 2), or bilinear via x * y).
  • • Constant terms are ignored (they don't affect which solution is optimal).

Quadratic Objectives (QP)

SUM(POWER(linear_expr, 2)) turns the objective into a quadratic program. MINIMIZE with a positive coefficient is convex; HiGHS handles it when all decisions are continuous and the quadratic matrix is non-singular, while Gurobi also handles mixed-integer and coupled/rank-deficient forms. MAXIMIZE with a positive coefficient is non-convex and requires Gurobi (which handles it via NonConvex=2). Three equivalent syntactic forms are accepted:

SQL
-- Least-squares-style: pull every x toward a target
MINIMIZE SUM(POWER(x - target, 2))
MINIMIZE SUM((x - target) ** 2)
MINIMIZE SUM((x - target) * (x - target))

-- Negation gives a concave QP (same backend limits under MAXIMIZE)
MAXIMIZE SUM(-POWER(x - target, 2))

Bilinear Objectives

Products of two different decision variables (x * y) are allowed in objectives. When one factor is BOOL, both solvers handle the product, provided the other factor has a finite upper bound — either one you write (x <= K) or one implied by a constraint such as SUM(x) <= K. General bilinear products between non-Boolean variables are Gurobi only.

SQL
-- Boolean * Integer: maximize selected-revenue, where x is binary
MAXIMIZE SUM(x * quantity * unit_price)

PER on Objectives

The objective can carry a PER grouping by nesting an inner aggregate inside an outer one. The inner aggregate runs per group; the outer aggregates the per-group values into a single scalar to optimize.

SQL
-- Minimize the worst region's loss (maximin over regions)
MAXIMIZE MIN(SUM(x * profit)) PER region

Mixed Quadratic + Linear

Linear terms and a single quadratic group can sit in the same objective. The two forms below are equivalent:

SQL
MINIMIZE SUM(POWER(x - target, 2) + c * x)
MINIMIZE SUM(POWER(x - target, 2)) + SUM(c * x)

COUNT Workaround

To maximize the number of selected items, use SUM(x) with a boolean variable instead of COUNT. The same applies in SUCH THAT:

Error — SUCH THAT COUNT(x) <= 2
COUNT over a DECIDE variable is degenerate: decision variables are never
null, so COUNT(x) always equals the row count. Did you mean SUM(x)?
SQL
-- Want to select as many items as possible?
DECIDE x(BOOL)
SUCH THAT SUM(x * weight) <= 100
MAXIMIZE SUM(x);

Since x is 0 or 1, SUM(x) counts how many rows are selected.

Feasibility Queries (No Objective)

You can omit the objective entirely. In that case, the solver finds any feasible assignment that satisfies all constraints.

SQL
SELECT id, x
FROM Items
DECIDE x(BOOL)
SUCH THAT
    SUM(x * weight) <= 50
    AND SUM(x) = 3;

Returns any combination of exactly 3 items whose total weight is at most 50.

Reducers

A reducer is an aggregate over decisions — SUM, AVG, MIN or MAX. Everything in this section applies in both SUCH THAT and the objective.

Relation-Qualified Reducers

A join repeats a table's rows once per match, so a plain SUM over a table-scoped decision counts that decision once per joined row. Name a relation in front of the body to count it once per row of that relation instead.

SQL
agg(Rel[, Rel, ...]: expr)   -- agg is SUM, AVG, MIN or MAX

A depot serving three routes is charged its opening cost once, not three times:

SQL
SELECT routeID, D.depotID, open, ship
FROM Depots D JOIN Routes T USING (depotID)
DECIDE D.open(BOOL), T.ship(INT)
SUCH THAT ship <= capacity * open AND SUM(ship) >= 100
MINIMIZE SUM(unit_cost * ship) + SUM(D: opening_cost * open)
ORDER BY routeID;
  • • Identity is the row, not the value. Two depots with the same opening cost are still two terms. This is not SUM(DISTINCT ...).
  • • Only surviving rows count. The join and WHERE decide which rows contribute; a depot filtered out contributes nothing.
  • • Everything inside must come from a named relation. A query-wide (scalar) decision is the one exception.
  • • AVG(D: ...) divides by the number of distinct D rows, not the number of result rows.
  • • MIN and MAX are unaffected: the qualifier is accepted and changes nothing.
  • • Naming several relations widens the identity to their combination — SUM(D, T: ...) collapses a row only when it repeats on both. The order of the names does not matter.
Error — a column from outside the qualifier
'unit_cost' does not come from D, so SUM(D: ...) cannot use it; keep only
those relations' columns inside the qualified reducer and sum the rest
separately

Scaling a Reducer

A reducer may be multiplied or divided by a factor, on either side. The factor must be one value for the whole query: a literal, an expression over literals, or an uncorrelated scalar subquery.

SQL
SUCH THAT 2 * SUM(x * p) <= 40
SUCH THAT 2 * MAX(x * v) <= 12
MINIMIZE SUM(x * p) / 2

A per-row column is not a valid factor. Move it inside the aggregate:

Error — SUCH THAT w * SUM(x) <= 10
DECIDE constraint: 'w' varies per row, so it cannot multiply SUM(x).
Move it inside the aggregate, e.g. SUM(x * w).

norm(expr, p)

norm reduces a decision expression to a single penalty term, for regularized objectives such as lasso and ridge. It works in objectives and in constraints, and composes with WHEN and PER. You supply the weight.

Form Means
norm(e, 1) SUM(ABS(e)) — L1, leans toward sparse answers
norm(e, 2) SUM(POWER(e, 2)) — squared L2, or ridge
norm(e, 'inf') MAX(ABS(e)) — the worst single deviation
norm(e, 0[, M]) the exact count of nonzeros
SQL
-- Stay close to a baseline while keeping cost down
MINIMIZE SUM(cost * x) + 0.5 * norm(x - base, 1)

-- Touch at most two rows
SUCH THAT norm(x, 0) <= 2

norm(e, 0) counts a value as nonzero once it reaches a tolerance, 1e-4 by default. Lower it for small-magnitude data, raise it to treat larger residuals as zero. The floor is 1e-5, below which the solver's own tolerance would swallow it.

SQL
SET decide_l0_tolerance = 1e-5;

Solver Selection

DeciDB ships with two solvers and picks one at runtime. Gurobi is used automatically when its runtime library and license are usable; it is faster on these workloads and covers more of the table below. HiGHS is bundled, needs no setup, and handles the model classes it supports when Gurobi is unavailable. A Gurobi-only query is rejected if Gurobi cannot be loaded.

Feature Gurobi HiGHS
LP / MILP (linear) Yes Yes
Convex QP (MINIMIZE SUM(POWER(...))) Yes (incl. MIQP) Yes (continuous, non-singular Q only)
Non-convex QP (MAXIMIZE SUM(POWER(...))) Yes Rejected
Bilinear, Boolean × anything Yes Yes
Bilinear, general (Real×Real, Int×Int) Yes Rejected
Quadratic constraints (QCQP) Yes Rejected

Solver Outcomes

A solve ends in one of four ways:

Optimal

A solution was found. The query returns all candidate rows with optimal variable values.

Infeasible

No assignment satisfies every constraint. The query returns an error:

Error
DECIDE optimization is infeasible. Prefix the query with DIAGNOSE
to see which clause to change.

See DIAGNOSE for which clause to change and by how much.

Unbounded

The objective can grow without limit, because nothing bounds it. The query returns an error:

Error
DECIDE optimization is unbounded. Prefix the query with DIAGNOSE
to see which decision needs a bound.

See DIAGNOSE for which decision needs a bound.

Out of time, or interrupted

The solve can hit its time limit, or you can stop it with Ctrl-C. Either way the query prints a checkpoint report: the best answer found so far and how much better the answer could still get.

  • • At a terminal, after the time limit, it offers to keep going. Press Enter to continue solving, or s to stop and take the best answer so far.
  • • In a script or a pipe there is nobody to answer that offer, so the query returns an error instead.
  • • After Ctrl-C you get the best answer found so far, marked as not proven best.

EXPLAIN

Prefix a decision query with EXPLAIN to inspect its plan without solving it. The DECIDE node shows the declared variables, objective and constraints. When rewriting changes a constraint, the plan groups the written clause with its canonical and solver-ready forms.

SQL
EXPLAIN SELECT id, x
FROM (VALUES (1, 2), (2, 3)) AS items(id, weight)
DECIDE x(BOOL)
SUCH THAT SUM(x * weight) <= 3
MAXIMIZE SUM(x);
  • • EXPLAIN renders the logical or physical plan selected by DuckDB's explain_output setting and does not run the solver.
  • • EXPLAIN ANALYZE executes the decision query and adds actual row counts and timing.
  • • EXPLAIN (FORMAT JSON) returns the same DECIDE structure as JSON.

DIAGNOSE

DIAGNOSE is a prefix on a SELECT whose complete plan contains exactly one DECIDE operator. It runs the query and reports on the run instead of returning the query's rows — the same relationship EXPLAIN ANALYZE has to an ordinary query. Diagnostics run only under the prefix.

SQL
DIAGNOSE SELECT id, x FROM t
DECIDE x(INT)
SUCH THAT x <= 5 AND x >= 8
MAXIMIZE SUM(x);

The Result Is a Relation

DIAGNOSE returns nine columns, one row per finding.

Column Type What it holds
stateVARCHARinfeasible, unbounded, or feasible
clauseVARCHARthe clause as you wrote it, or the runaway decision's name
suggested_changeVARCHARthe smallest edit that addresses this finding
amountDOUBLEhow far a bound moves, escaping instances, or the achievable objective
totalBIGINTdenominator for an unbounded escape count; otherwise NULL
scopeVARCHARrow or entity for an unbounded escape count; otherwise NULL
edit_sourceVARCHARwhat kind of finding this row is (below)
groupVARCHARthe PER key, or the categorical slice a decision escapes on
rowBIGINTthe emitted row this finding covers, under the expanded slack scope

edit_source is the finding's kind, and the column to filter on.

Value Meaning
source_literala literal you wrote, loosened in place
virtual_offseta synthetic offset over a data-backed bound (x <= col + delta)
expanded_rowone emitted row's own overshoot
expanded_groupone PER group's own overshoot
remove_onlya <> that cannot be loosened, only deleted
unreachable_bounda bound no assignment can reach
rigid_conflictloosening the clauses you wrote cannot restore feasibility
runaway_+inf / runaway_-infa decision growing without bound, and which way
achievable_objectivewhat the objective reaches once the edits are applied
unbounded_after_fixthe repaired problem has no finite optimum
undiagnosedthe state is known but no cause could be named

Because it is a relation, you can select from it and filter it:

SQL
SELECT clause, suggested_change
FROM (DIAGNOSE SELECT ... DECIDE ...)
WHERE amount > 1000;

A Query That Worked

One row, everything but state NULL. There is no separate output for a query that succeeded.

Output
┌─────────┬─────────┬──────────────────┬────────┬───┬─────────────┬─────────┬───────┐
│  state   │ clause  │ suggested_change │ amount │ … │ edit_source │  group  │  row  │
├─────────┼─────────┼──────────────────┼────────┼───┼─────────────┼─────────┼───────┤
│ feasible │ NULL    │ NULL             │   NULL │ … │ NULL        │ NULL    │  NULL │
└─────────┴─────────┴──────────────────┴────────┴───┴─────────────┴─────────┴───────┘

A Query That Could Not Be Satisfied

SQL
DIAGNOSE SELECT id, x FROM t
DECIDE x(INT)
SUCH THAT SUM(x) <= 3 AND SUM(x) >= 10
MAXIMIZE SUM(x);
Output
│ infeasible │ SUM(x) <= 3 │ SUM(x) <= 10 │  7.0 │ source_literal       │
│ infeasible │ NULL        │ NULL         │ 10.0 │ achievable_objective │

Change SUM(x) <= 3 to SUM(x) <= 10 — a move of 7 — and the objective reaches 10.

Restrictions

  • • The plan must contain exactly one DECIDE operator. It may be the outer query's clause or the sole operator inside an ordinary subquery.
  • • A plan containing no DECIDE operator, or several outer, nested or sibling operators, is rejected with the number found.
  • • There are no options. DIAGNOSE (VERBOSE) ... is a syntax error.
  • • A query that fails before it can be solved — a syntax error, a semantic error, a model the host's solver refuses — still raises under the prefix. DIAGNOSE explains the outcome of a solve.
Error — DIAGNOSE SELECT id FROM t
DIAGNOSE needs a query with a DECIDE clause — it reports on an optimization
run, and this query has no optimization to run. Use EXPLAIN ANALYZE for a
plain SQL query.

Settings

These tune the engine once DIAGNOSE has started it. None of them starts or suppresses it.

Setting Default What it changes
diagnose_decide_infeasible_slack_scopequeryone edit per clause you wrote; expanded gives one per emitted row or group, and fills group and row
diagnose_decide_escape_rate0.8report a category once this share of its rows run away
diagnose_decide_categorical_ratio0.1treat a column as a category when its distinct values are at most this share of the rows
diagnose_decide_min_categories20a floor on that cap, so small tables still qualify

Best Practices

Filter with WHERE first

The solver runs on every candidate row. Fewer rows means faster solving. Use WHERE to exclude rows that can't possibly be in the optimal solution.

Slow — all rows

SQL
SELECT * FROM Products
DECIDE x(BOOL)
SUCH THAT SUM(x * weight) <= 50
MAXIMIZE SUM(x * value);

Better — filtered

SQL
SELECT * FROM Products
WHERE category = 'electronics'
  AND in_stock = true
DECIDE x(BOOL)
SUCH THAT SUM(x * weight) <= 50
MAXIMIZE SUM(x * value);

Prefer BOOLEAN over INTEGER

Boolean variables (0/1) solve faster than integer variables. Use BOOL whenever your decision is "include or not." Only use INT when you need quantities.

Build incrementally

Start with one constraint and verify the result makes sense. Add constraints one at a time. If the query returns an error, the most recently added constraint may be making the problem infeasible — try relaxing it.

SQL — debugging infeasibility
DECIDE x(BOOL)
SUCH THAT
    SUM(x * weight) <= 50  -- try increasing this
    AND SUM(x) >= 10       -- or lowering this

JOINs work normally

You can use JOIN before DECIDE. The decision variables operate on the joined result set.

SQL
SELECT o.id, p.name, x
FROM Orders o
JOIN Products p ON o.product_id = p.id
DECIDE x(BOOL)
SUCH THAT SUM(x * p.weight) <= 100
MAXIMIZE SUM(x * p.profit);