[Tutorial] Optional relationships

A relationship line usually tells you how many rows can match: one, or many. It should also tell you whether a match is required. That second part is optionality, and DBML marks it with a ?.

Cardinality and optionality

  • Cardinality: when a row matches, how many can it match? One, or many.
  • Optionality: does a row have to match at all? Or is zero allowed?

Most diagrams only answer the first. “One customer has many orders” leaves out whether a customer must have at least one order. Usually they don’t. Someone who signed up yesterday and bought nothing is still a customer.

Diagram notation

Each end of the line has its own symbol:

At the line end Symbol Means
bar ──┤ exactly one
circle ──○ zero or one
crow’s foot ──< one or many
circle + crow’s foot ─○< zero or many

The circle means zero is allowed. The table at that end is optional.

DBML syntax

One rule: put ? on the side of the operator that is optional.

  • >? - the right side is optional
  • ?> - the left side is optional
  • ?>? - neither side is required
  • Works with all four operators: <, >, -, <>

Example schema

Below is a small store. Each relationship is optional in a different way. Drag the tables around. To see a table’s sample rows, hover its header and click the records icon.

Table orders {
  id int [pk]
  customer_id int [ref: ?> customers.id] // a customer may have no orders
  coupon_id int [ref: ?>? coupons.id]    // an order may have no coupon
  created_at varchar
}

Table order_items {
  order_id int [ref: > orders.id]        // every order has at least one item
  product_id int [ref: ?> products.id]   // a product may never be ordered
  quantity int
}

Ref: customers.id -? customer_profiles.customer_id // a customer may have no profile

Each ? above says something different. Open the data samples and you can see all four rules at work:

Relationship Operator What is optional In the data
customers - orders ?> a customer may have zero orders Aisha Khan (3) has never ordered
orders - coupons ?>? an order may have no coupon, and a coupon may go unused orders 102 and 103 show (null); FREESHIP (51) is unused
customers - customer_profiles -? a customer may have no profile Diego (2) has an account, no profile
products - order_items ?> a product may appear in zero orders Standing Desk (4) has never been ordered

Leaving ? off is a claim too. order_id int [ref: > orders.id] says every order has at least one item.

Practical impact

  • It tells you which join to write. An optional side means LEFT JOIN. Use an inner join across an optional relationship and you drop rows: count customers by joining through orders, and everyone who never ordered vanishes.

  • It tells you which columns can be null. Downstream code has to handle the missing value.

  • It records a real decision. “Can a product exist without ever being ordered?” is a modeling choice. Written as ?, it lives in the schema instead of in someone’s head.

Further reading

IMO, you had this correct with the original implementation that derived the optionality based on the nullability of the referencing attribute:
Upgrade for Database Relationships: Zero-to-One/Many Relationships, Colors and more! - News from dbx - dbdiagram Community

Now, it’s possible to specify optionality that is incompatible with the nullability, as the current “Sample Diagram” exhibits. Respectfully, this is a step backward.

Thanks for pushing back on this, and for linking the original release.

You’re right that nullability-derived optionality was correct as far as it went, and we haven’t discarded it. Three reasons we moved optionality into the relationship notation:

Nullability can only ever describe one side. A nullable FK says “this row may have no parent”. There is no column anywhere to hang “a parent may have no children” on, so that half of the relationship was simply not expressible. ? on either side of the operator lets you state both, which is what crow’s-foot notation has always been able to say and DBML could not.

Ref should stay conceptual. Deriving a modeling fact (optional participation) from a storage detail (a nullable column) mixes abstraction levels. Optionality is frequently enforced by application logic, check constraints, or conditional rules that don’t map 1:1 to nullability, so the column is not a reliable source of truth for relationship semantics.

It stays opt-in. Leave ? off and your diagram behaves exactly as before. Nothing forces you to annotate, so a simple ERD stays simple.

On the incompatibility you spotted: that should not pass silently, and we agree. Nullability still carries meaning, and we surface a conflict between the ref operator and the column constraint as a warning rather than accept it quietly.

Full reasoning, including the cardinality table for all eight combinations, is in the design thread: Detect the cardinality of unique foreign key relationships · Issue #802 · holistics/dbml · GitHub