# Virtual & Inferred Relations

> Document foreign keys your database never declared. Draw virtual relations by hand, let Schemity infer them from column names, and show, hide or remove them in one menu.

Source: https://schemity.com/doc/virtual-relations/

A **virtual relation** documents a dependency the database does not declare. It is drawn dashed on the canvas, saved in the diagram's JSON, and never written to the database: no migration, SQL export, or DBML export ever turns it into a real foreign key. An **inferred** virtual relation is one Schemity read from column names, so a schema whose foreign keys were never created opens with its edges already drawn.

This is for the schemas where the real dependencies live only in naming: a dbt-built warehouse, a Rails or Django database whose keys were never declared, or a database whose keys were dropped for load performance. Without it, such a schema opens as hundreds of tables with no lines between them.

## How do I draw a virtual relation?

Draw a relationship between two entities as usual (see [Relationships](https://schemity.com/doc/relationships/)), then switch the relation dialog from **Relation** to **Virtual relation**. The question changes: instead of naming a new foreign key column, you pick which columns **already carry** the dependency, one on each entity.

A virtual relation adds a foreign key to the diagram and nothing else, so no column is created and the child entity does not grow. The dialog leaves out what a virtual relation cannot honestly offer:

- **No ON DELETE or ON UPDATE.** Nothing enforces a virtual relation, so a referential action would claim behavior the database never performs.
- **No N:N.** Many-to-many would build a junction table, which is schema, not documentation.
- **Cardinality is derived** from the child column's unique constraints rather than asked for.

The dialog remembers which mode you used last in that diagram, so documenting a whole warehouse does not mean re-picking **Virtual relation** every time.

## How does Schemity infer relations from column names?

When a diagram loads - on connect, refresh, plain file open, and SQL import - Schemity scans every column for a name that points at another entity and adds a virtual relation for each match. It never runs while you type, and you can turn it off (see below).

The matcher splits a column name into words rather than matching strings, so it follows the naming convention your codebase already uses:

- `articles.user_profile_id`, `userProfileId`, and `userProfileID` all resolve to `user_profiles` (or `userProfiles`).
- `created_by_user_id` falls back through shorter word runs until it reaches `users`.
- The longest match wins, so `tenant_history_id` resolves to `tenant_history`, never to `tenants`.
- `superuser_id` does **not** match `users`, because `superuser` is one word.

The referenced column resolves in order: a single-column primary key, then a lone single-column unique constraint, then a column named for the suffix (`id`). The second and third rungs matter because a `CREATE TABLE AS SELECT` table has no primary key at all.

## How do I turn off inferred virtual relations?

Inference is on by default. A schema that already declares its foreign keys can still get many inferred ones: Invoice Ninja declares 138 foreign keys, and inference adds 132 more for columns such as `user_id`, `vendor_id` and `project_id` that its Laravel migrations left undeclared. To stop that:

- **For a connection** - untick **Infer virtual relations from column names** in the connection's settings, under **Connect to a database**. Load and Refresh & merge then add none.
- **For one import** - untick the same checkbox at the bottom of the [Import SQL](https://schemity.com/doc/reverse-engineer-database/) preview. It starts from the connection's setting and applies to that import only.

Turning inference off adds nothing new and removes nothing: virtual relations already on the diagram stay until you remove them.

## When does Schemity not infer a relation?

A candidate is skipped when:

- the column is already part of a foreign key, declared by the database or drawn by you;
- either end is a database view;
- two entities normalize to the same name, so the match is ambiguous;
- no referenced column resolves;
- the two columns' types disagree.

Warehouse suffixes such as `_key` and `_sk` are deliberately not matched, because `sort_key` and `api_key` have the same shape and would produce confident nonsense. Composite keys are not inferred either: nothing in a naming convention says which columns group together.

## How do I tell an inferred virtual relation from one I drew?

After a load that inferred anything, a notification gives the count of virtual relations inferred from column names and says they are drawn dashed and are not declared in the database. The cardinality dialog of an inferred virtual relation says it was inferred from column names and is not declared in the database, and the [Context Map](https://schemity.com/doc/context-map/)'s dependency drawer tags each row behind an arrow as **Virtual** or **Inferred**. The arrows themselves count both kinds, because an architectural dependency is real whether or not the database enforces it.

## How do I hide virtual relations?

Open the **Virtual relations** menu in the control bar and untick **Show virtual relations**. The number beside it counts every virtual relation in the diagram, drawn or inferred.

While they are hidden:

- the canvas shows only the relations the database declares, and the footer reads **Relations: 138 (+132 virtual hidden)**;
- PNG, JPEG and SVG exports leave them out too, so an image shows the declared schema;
- text exports (SQL, DBML, Mermaid and the rest), [Schema Lint](https://schemity.com/doc/schema-lint/), impact analysis, Route Relations and AI agents still see every relation, because hiding changes only what is drawn.

Hiding is a view setting for that diagram on your machine. It is not saved in the diagram file, adds no undo step and leaves nothing to save, so someone opening the same file from Git still sees the virtual relations.

## How do I remove inferred virtual relations?

- **One at a time** - delete the relation like any other. The deletion is permanent: the pair is recorded in the diagram file, so the next load does not bring it back. Renaming the column clears that record, because the dismissal was a judgement about that column.
- **All at once** - choose **Remove inferred virtual relations** from the **Virtual relations** menu in the control bar. The number beside it is how many it removes. It removes only the inferred ones: virtual relations you drew stay. Each removal is recorded the same way, so none comes back on the next load.

Deleting a virtual relation never deletes its columns. It did not create them, and they may already belong to a primary key or a real foreign key.

## What happens to virtual relations when the database changes?

They survive every rewrite that could quietly drop them. A [re-sync](https://schemity.com/doc/resync-database/) against the live database restores saved virtual relations onto the freshly read entities, with their waypoints and cardinality. Renaming an entity or a column, pasting, and duplicating all carry them along. A saved virtual relation whose columns no longer exist is not restored, and one whose columns a real foreign key now covers is dropped, so the declared edge replaces it rather than doubling it.

## How do lint and AI agents treat virtual relations?

[Schema Lint](https://schemity.com/doc/schema-lint/) separates shape from enforcement. Junction table detection counts virtual keys as ordinary edges, which is what lets it recognize junction tables on a warehouse where every relation is virtual. The not-null cycle rule ignores them, since a cycle only blocks inserts if the database checks the keys in it. A type mismatch is still reported, because mismatched types stop a join from using an index whether or not a constraint is declared.

An AI agent connected over [MCP](https://schemity.com/doc/ai-assisted-design/) is told which relations are virtual and which were inferred, so an undeclared dependency is weighed as the guess it is.

## Next

Check the schema for problems with [Schema Lint](https://schemity.com/doc/schema-lint/).
