skip to content

The relational model defines a relation's body as an unordered set of tuples over a heading of named attributes. Why is neither row order nor column position part of a relation, and what goes wrong in production when application code assumes rows come back in the order they were stored?

level: seniorimportance: should knowfreq 44%

answer

  1. body is a set: no first row, no row number
  2. heading is a set: attributes by name, not position
  3. order is a property of the query result, not the table
  4. row movement, space reuse, parallel scan, plan change all reorder
  5. total order = meaningful attributes + unique tiebreaker

basics

~20 s

Both the heading and the body are sets, and sets have no order, so position is not part of the value. Engines exploit that freedom: storage reorganisation, updates relocating rows, parallel scans and plan changes all reorder results, so code assuming stored order breaks intermittently with no code change to blame.

solid answer

~60 s

A relation is a heading (a **set** of named attributes) and a body (a **set** of tuples). Sets are unordered, so there is no first tuple and no third column: attributes are addressed by name, tuples only by their values. Ordering is a property of a *query result*, produced when the query explicitly asks for one - never a property of the stored relation. That is not pedantry, it is the licence the engine needs. Rows move when updated, space from deleted rows is reused, background reorganisation and compaction rewrite pages, parallel scans interleave workers, and the optimiser may switch between a scan, an index traversal or a hash-based operator at any time. Each of those silently changes the order rows arrive in. So the failure mode is nasty: code relying on insertion order works in test and for months in production, then returns different results after a bulk update, a data reload, a new index or a statistics refresh - with no deployment to blame. The fix is to make the ordering explicit and total, ordering on real attributes with a deterministic tiebreaker, and to reference attributes by name rather than by position.

code

text · 6 lines
text
Plan A (after statistics refresh)      Plan B (previous plan)
  Seq Scan on events                     Index Scan using events_created_idx
    -> rows in physical page order         -> rows in index key order

Same set of tuples. Different sequence. Neither plan is wrong;
the query never asked for an ordering.

go deeper

for a junior

Say the body is a set, so there is no inherent row order, and that any order you need must be asked for explicitly in the query.

for a middle

Explain why the engine reorders in practice - row relocation, space reuse, plan changes - and that an ordering must be total to be deterministic.

for a senior

Diagnose the production shape: intermittent breakage with no deploy, pagination skips, wrong 'latest' row, and prescribe explicit total ordering plus stored sequence attributes.

for a principal

Treat unorderedness as a deliberate contract that buys implementation freedom, and treat every leaked ordering assumption as an unwritten interface constraining future storage and execution choices.

## Why the model refuses to define order A relation has a heading and a body, and both are sets. Because a set has no order, three familiar notions simply do not exist in the model: - there is no first, last or n-th tuple; - there is no column 1 or column 3, only attributes addressed by name; - there is no identity for a tuple beyond its values, which is exactly why keys have to be declared rather than assumed. This is a deliberate omission, not an oversight. By withholding order from the logical value, the model frees every implementation to store and retrieve data however it likes. Anything the model guarantees is a promise the engine must keep forever; anything it withholds is room the engine can use to go faster. ## What the engine does with that freedom In a running system the physical arrangement of rows is in constant motion: - an update may not fit in place, so the row is written elsewhere and the original slot freed; - space from deleted rows is reused by later inserts, so new data can appear physically 'before' old data; - background maintenance - reorganisation, compaction, defragmentation, table rewrites during schema changes - relocates rows wholesale; - the optimiser may satisfy the same query by a sequential scan today and an index traversal tomorrow, purely because statistics or data volume changed, and the two produce entirely different orders; - parallel execution splits a scan across workers whose output interleaves nondeterministically; - hash-based joins and aggregations emit rows in hash-bucket order, effectively arbitrary and sensitive to memory settings and spilling. None of these is a bug. All of them are permitted precisely because the model never promised order. ## How the assumption fails in production The symptoms are recognisable, and they are why this is a senior-level question. **Intermittent, deploy-free breakage.** A report that always listed the newest entry first starts listing something else after a bulk load. Nobody changed the code. The plan changed, or the table was rewritten. **Pagination that skips and repeats.** If pages are cut out of a result whose order is not total, rows can appear on two pages or on none. This also happens with an ordering that is specified but not unique: ties may break differently between executions, so the boundary between pages shifts. **Wrong 'first' or 'latest' rows.** Taking one row from an unordered result to represent 'the current record' returns an arbitrary member of the set. It is right most of the time by luck, which is worse than being wrong reliably, because the failures are rare and data-dependent. **Positional coupling to attributes.** Consumers that read result columns by index, or that depend on the expansion order of a wildcard select, break when an attribute is added or the table is rewritten. The model's name-addressed attributes are the reason position was never safe to depend on. ## What to do instead Make ordering explicit in the query and make it **total**: order by attributes that genuinely encode the sequence you mean, and append a unique tiebreaker so no two tuples compare equal. If sequence matters to the business - the order lines were entered, the version history of a record - it must be an attribute you store, not something inferred from physical placement. A monotonically increasing surrogate key is an approximation only, and a weak one: values can be skipped, and concurrent transactions commit out of allocation order, so a higher key does not reliably mean 'later'. A timestamp needs a tiebreaker of its own. For stable paging, prefer continuing from the last seen key under a fixed total ordering rather than counting positions into the result, since positional offsets assume a stable arrangement between requests that no engine promises. And reference attributes by name everywhere, in queries and in result handling. ## The framing that lands in an interview Say it as a contract: a relation is a set, so ordering is not part of the data - it is something a query produces on request. Anything your code believes about order that the query did not state is an accidental dependency on today's physical layout, and physical layout is exactly what the engine reserves the right to change.

  • A query already specifies an ordering, yet paginated results still repeat and skip rows. What is the likely cause?
    The specified ordering is probably not total: several tuples compare equal on the ordering attributes, and ties may break differently between executions, so the page boundary lands in a different place each time. Adding a unique tiebreaker such as the key makes the order deterministic. Counting positions into the result compounds this, because it also assumes the underlying data has not changed between page requests.
  • Is an auto-incrementing surrogate key a reliable proxy for insertion order?
    Not reliably. Values are typically allocated before commit, so a transaction holding a lower key can commit after one holding a higher key, meaning key order and visibility order differ. Gaps appear from rolled-back transactions, and caching or multi-node allocation can interleave ranges. If the business cares about sequence, store the event time or an explicit sequence attribute rather than inferring it.

A relation is a bag of marbles, not a row of them on a shelf. If you want them in a particular sequence you must state the arranging rule each time you take them out; whatever order they happened to fall into the bag is not a fact about the bag.

saying these in an interview costs you the question

  • Claiming rows are returned in insertion order unless something changes
  • Believing an index or a key gives the table an inherent order
  • Treating an ordering on a non-unique attribute as deterministic
  • Reading result columns by position and assuming that position is stable
  • Saying 'it has always worked' as evidence that ordering is guaranteed

context