Stage 4 — VDBE
vdbe.c · vdbeaux.c · vdbeapi.c — Virtual Database Engine (register-based bytecode VM)
Architecture of the VDBE

The VDBE (Virtual DataBase Engine) is SQLite's bytecode interpreter. It is a register-based virtual machine: instructions operate on numbered memory registers (Mem objects) rather than a stack. A prepared statement is essentially a Vdbe struct containing:

  • aOp[] — the opcode array (each Op has opcode + P1/P2/P3/P4 operands)
  • aMem[] — the register bank (each Mem holds one SQL value)
  • apCsr[] — array of open B-tree cursors
  • db — back-pointer to the database connection
Register vs stack VM: Most interpreters (JVM, CPython) use a stack. SQLite uses registers because the code generator can assign optimal registers at compile time, avoiding push/pop overhead in the inner loop.
sqlite3_step and the Execution Loop
Call chain: sqlite3_step → sqlite3Step → sqlite3VdbeExec
/* vdbeapi.c:913 — public API */
int sqlite3_step(sqlite3_stmt *pStmt){
  Vdbe *v = (Vdbe*)pStmt;
  int rc;
  rc = sqlite3Step(v);           /* handles schema-change reprepare */
  return rc;
}

/* vdbeapi.c:771 — internal wrapper */
static int sqlite3Step(Vdbe *p){
  sqlite3 *db = p->db;
  int rc;
  ...
  db->nVdbeActive++;
  if( p->pc<=0 ){
    /* First call: begin transaction if needed */
    sqlite3VdbeEnter(p);
  }
  rc = sqlite3VdbeExec(p);       /* ← interpreter loop */
  ...
  if( rc==SQLITE_SCHEMA ){
    /* schema changed: reprepare and retry */
    sqlite3_reset((sqlite3_stmt*)p);
    rc = sqlite3Step(p);
  }
  return rc;
}
vdbe.c:881 — sqlite3VdbeExec: the interpreter loop (simplified)
int sqlite3VdbeExec(Vdbe *p){
  Op *aOp = p->aOp;
  Op *pOp = aOp;          /* current instruction pointer */
  Mem *aMem = p->aMem;    /* register bank */
  int rc = SQLITE_OK;

  for(;;){                /* ── main interpreter loop ── */
    assert( pOp >= aOp && pOp < &aOp[p->nOp] );
    switch( pOp->opcode ){

      case OP_Goto: {
        pOp = &aOp[pOp->p2 - 1];   /* jump to address P2 */
        break;
      }

      case OP_Integer: {
        /* P2 = destination register, P1 = integer value */
        pOut = &aMem[pOp->p2];
        pOut->u.i = pOp->p1;
        pOut->flags = MEM_Int;
        break;
      }

      case OP_Column: {
        /* Extract column P2 from cursor P1 into register P3 */
        VdbeCursor *pCur = p->apCsr[pOp->p1];
        sqlite3VdbeSerialGet(...);  /* decode from B-tree record format */
        break;
      }

      case OP_ResultRow: {
        /* Return SQLITE_ROW to the caller */
        rc = SQLITE_ROW;
        p->pc = (int)(pOp - aOp) + 1;  /* save next PC */
        goto vdbe_return;
      }

      case OP_Halt: {
        if( pOp->p1==SQLITE_OK ) rc = SQLITE_DONE;
        else rc = pOp->p1;
        goto vdbe_return;
      }

      /* ... ~150 more opcodes ... */
    }
    pOp++;                /* advance to next instruction */
  }
vdbe_return:
  return rc;
}
Mem Register — the SQL Value Type

Each register in aMem[] is a Mem struct that can hold any SQL value type. The flags field is a bitmask that indicates which union field is valid.

/* vdbeInt.h — Mem (simplified) */
struct Mem {
  sqlite3 *db;       /* database connection (for malloc) */
  char *z;           /* string or blob pointer */
  double r;          /* floating point value */
  union {
    i64  i;          /* integer value */
    int  nZero;      /* extra zero bytes for blob */
    ...
  } u;
  u16 flags;         /* MEM_Null | MEM_Int | MEM_Real | MEM_Str | MEM_Blob */
  u8  enc;           /* encoding: SQLITE_UTF8, SQLITE_UTF16LE, ... */
  int n;             /* bytes in z[], not counting NUL terminator */
  ...
};
FlagMeaningActive field
MEM_NullSQL NULLnone
MEM_Int64-bit integeru.i
MEM_RealIEEE 754 doubler
MEM_Strtext stringz, n
MEM_Blobbinary dataz, n
Key Opcodes Reference
OpcodeP1P2P3Action
OP_OpenReadcursorroot pageOpen B-tree cursor for reading
OP_OpenWritecursorroot pageOpen B-tree cursor for reading/writing
OP_Rewindcursorjump-if-emptyMove cursor to first entry
OP_Nextcursorloop-back-addrAdvance cursor; jump if not done
OP_Prevcursorloop-back-addrMove cursor backward
OP_SeekGEcursorjump-addrkey-regSeek to first key ≥ P3
OP_Columncursorcol-idxdest-regExtract column from current row
OP_Rowidcursordest-regRead rowid of current row
OP_MakeRecordstart-regcountdest-regEncode registers into B-tree record
OP_Insertcursordata-regkey-regInsert record into B-tree
OP_DeletecursorflagsDelete row at cursor
OP_ResultRowstart-regcountYield one output row → SQLITE_ROW
OP_Integervaluedest-regLoad integer constant into register
OP_String8dest-regLoad string constant (P4) into register
OP_Addreg-Areg-Bdest-regr[P3] = r[P1] + r[P2]
OP_Eq / Nereg-Ajump-addrreg-BCompare and jump conditionally
OP_HaltrcStop execution; return P1 as result code
OP_GotoaddrUnconditional jump to P2
OP_Transactiondb-idxwrite?Begin transaction on database P1

The full opcode list is generated from vdbe.c by mkopcodeh.tcl into opcodes.h.

Result Extraction (vdbeapi.c)

When sqlite3VdbeExec() returns SQLITE_ROW, the caller calls sqlite3_column_*() to extract values from the output registers. These functions are thin wrappers around Mem accessors.

/* Application code pattern */
sqlite3_stmt *stmt;
sqlite3_prepare_v2(db, "SELECT id, name FROM users", -1, &stmt, 0);

while( sqlite3_step(stmt) == SQLITE_ROW ){
  int   id   = sqlite3_column_int(stmt, 0);   /* reads aMem[0].u.i */
  const char *name = sqlite3_column_text(stmt, 1); /* reads aMem[1].z */
  printf("%d: %s\n", id, name);
}
sqlite3_finalize(stmt);
Next Stage

When the VDBE executes OP_OpenRead, OP_Next, OP_Insert, etc., it calls into the B-tree engine to move cursors and read/write row data.