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 (eachOphas opcode + P1/P2/P3/P4 operands)aMem[]— the register bank (eachMemholds one SQL value)apCsr[]— array of open B-tree cursorsdb— back-pointer to the database connection
sqlite3_step and the Execution Loop
/* 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;
}
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 */
...
};
| Flag | Meaning | Active field |
|---|---|---|
MEM_Null | SQL NULL | none |
MEM_Int | 64-bit integer | u.i |
MEM_Real | IEEE 754 double | r |
MEM_Str | text string | z, n |
MEM_Blob | binary data | z, n |
Key Opcodes Reference
| Opcode | P1 | P2 | P3 | Action |
|---|---|---|---|---|
OP_OpenRead | cursor | root page | — | Open B-tree cursor for reading |
OP_OpenWrite | cursor | root page | — | Open B-tree cursor for reading/writing |
OP_Rewind | cursor | jump-if-empty | — | Move cursor to first entry |
OP_Next | cursor | loop-back-addr | — | Advance cursor; jump if not done |
OP_Prev | cursor | loop-back-addr | — | Move cursor backward |
OP_SeekGE | cursor | jump-addr | key-reg | Seek to first key ≥ P3 |
OP_Column | cursor | col-idx | dest-reg | Extract column from current row |
OP_Rowid | cursor | — | dest-reg | Read rowid of current row |
OP_MakeRecord | start-reg | count | dest-reg | Encode registers into B-tree record |
OP_Insert | cursor | data-reg | key-reg | Insert record into B-tree |
OP_Delete | cursor | flags | — | Delete row at cursor |
OP_ResultRow | start-reg | count | — | Yield one output row → SQLITE_ROW |
OP_Integer | value | dest-reg | — | Load integer constant into register |
OP_String8 | — | dest-reg | — | Load string constant (P4) into register |
OP_Add | reg-A | reg-B | dest-reg | r[P3] = r[P1] + r[P2] |
OP_Eq / Ne | reg-A | jump-addr | reg-B | Compare and jump conditionally |
OP_Halt | rc | — | — | Stop execution; return P1 as result code |
OP_Goto | — | addr | — | Unconditional jump to P2 |
OP_Transaction | db-idx | write? | — | 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.