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
-- 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
FROM + WHERE — Standard SQL filtering produces candidate rows
-
2
DECIDE — Creates the decision variables, one per candidate row unless the declaration says otherwise
-
3
SUCH THAT + Objective — Constraints and objective are sent to the solver
-
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.
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:
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.
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.
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.
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.
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.
-- 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.
-- 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.
-- 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.
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.
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.
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.
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.
-- 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.
-- 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).
-- 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.
-- 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.
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.
-- 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.
-- 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.
-- 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.
-- 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.
SUCH THAT CAST(x AS INTEGER) <= 3DECIDE 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.
-- 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.
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:
-- 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
x * 5
x * price
SUM(x * price + y * cost)
Quadratic / bilinear
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
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.
-- 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
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.
-- 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-widescalardecision, or an additive combination of those terms. A bare row- or table-scoped decision is ambiguous across rows and is rejected.COUNTis 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 viax * 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:
-- 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.
-- 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.
-- 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:
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:
SUCH THAT COUNT(x) <= 2COUNT over a DECIDE variable is degenerate: decision variables are never
null, so COUNT(x) always equals the row count. Did you mean SUM(x)?
-- 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.
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.
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:
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
WHEREdecide 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 distinctDrows, not the number of result rows. - •
MINandMAXare 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.
'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.
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:
SUCH THAT w * SUM(x) <= 10DECIDE 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 |
-- 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.
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:
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:
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
sto 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.
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);
- •
EXPLAINrenders the logical or physical plan selected by DuckDB'sexplain_outputsetting and does not run the solver. - •
EXPLAIN ANALYZEexecutes 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.
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 |
|---|---|---|
state | VARCHAR | infeasible, unbounded, or feasible |
clause | VARCHAR | the clause as you wrote it, or the runaway decision's name |
suggested_change | VARCHAR | the smallest edit that addresses this finding |
amount | DOUBLE | how far a bound moves, escaping instances, or the achievable objective |
total | BIGINT | denominator for an unbounded escape count; otherwise NULL |
scope | VARCHAR | row or entity for an unbounded escape count; otherwise NULL |
edit_source | VARCHAR | what kind of finding this row is (below) |
group | VARCHAR | the PER key, or the categorical slice a decision escapes on |
row | BIGINT | the 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_literal | a literal you wrote, loosened in place |
virtual_offset | a synthetic offset over a data-backed bound (x <= col + delta) |
expanded_row | one emitted row's own overshoot |
expanded_group | one PER group's own overshoot |
remove_only | a <> that cannot be loosened, only deleted |
unreachable_bound | a bound no assignment can reach |
rigid_conflict | loosening the clauses you wrote cannot restore feasibility |
runaway_+inf / runaway_-inf | a decision growing without bound, and which way |
achievable_objective | what the objective reaches once the edits are applied |
unbounded_after_fix | the repaired problem has no finite optimum |
undiagnosed | the state is known but no cause could be named |
Because it is a relation, you can select from it and filter it:
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.
┌─────────┬─────────┬──────────────────┬────────┬───┬─────────────┬─────────┬───────┐
│ state │ clause │ suggested_change │ amount │ … │ edit_source │ group │ row │
├─────────┼─────────┼──────────────────┼────────┼───┼─────────────┼─────────┼───────┤
│ feasible │ NULL │ NULL │ NULL │ … │ NULL │ NULL │ NULL │
└─────────┴─────────┴──────────────────┴────────┴───┴─────────────┴─────────┴───────┘
A Query That Could Not Be Satisfied
DIAGNOSE SELECT id, x FROM t
DECIDE x(INT)
SUCH THAT SUM(x) <= 3 AND SUM(x) >= 10
MAXIMIZE SUM(x);
│ 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
DECIDEoperator. It may be the outer query's clause or the sole operator inside an ordinary subquery. - • A plan containing no
DECIDEoperator, 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.
DIAGNOSEexplains the outcome of a solve.
DIAGNOSE SELECT id FROM tDIAGNOSE 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_scope | query | one edit per clause you wrote; expanded gives one per emitted row or group, and fills group and row |
diagnose_decide_escape_rate | 0.8 | report a category once this share of its rows run away |
diagnose_decide_categorical_ratio | 0.1 | treat a column as a category when its distinct values are at most this share of the rows |
diagnose_decide_min_categories | 20 | a 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
SELECT * FROM Products
DECIDE x(BOOL)
SUCH THAT SUM(x * weight) <= 50
MAXIMIZE SUM(x * value);
Better — filtered
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.
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.
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);