Chapter 03 · SQLite Schema Objects, ROWID, Keys, and Table Design
CREATE TABLE in SQLite: Schema, sqlite_schema, and Introspection
Turn relational table definitions into SQLite schema objects, then verify what the engine actually stored instead of trusting DDL by appearance alone.
Learning outcomes
In the prerequisite SQL and modeling courses, a table is a
relation-shaped structure with named columns, keys, and
constraints. SQLite implements those ideas through concrete
schema objects stored inside each database. The important habit
for this chapter is therefore: declare, then inspect.
Do not assume the engine created exactly what you intended
merely because a CREATE TABLE statement returned no
error.
Create ordinary, temporary, conditional, and schema-qualified SQLite tables with deliberate names.
Explain what sqlite_schema stores and why
sqlite_master still appears in older material.
Compare shell-level .schema output with engine
metadata from sqlite_schema and PRAGMAs.
Use PRAGMA table_info,
table_xinfo, and table_list for
different inspection questions.
Demonstrate why IF NOT EXISTS is an
error-suppression tool, not schema verification.
Start from the relational idea, then ask SQLite where it lives
A logical model says that FieldNotes needs objects such as
sites, devices, and maintenance notes. DDL turns that model into
database objects. In SQLite, the database named
main is normally the file opened by the connection.
A table created without a schema qualifier and without
TEMP is created in main.
CREATE TABLE site_probe ( site_id INTEGER PRIMARY KEY, site_code TEXT NOT NULL, site_name TEXT NOT NULL);
This statement declares a table; it does not insert any rows. The durable state change is the schema object itself. Chapter 4 will examine SQLite type behavior in depth; here, declared types are used only to make the schema readable and intentional.
| Relational term | SQLite implementation here | Inspection question |
|---|---|---|
| Table |
A schema object created by CREATE TABLE
|
Does the object exist in the intended schema? |
| Column | A named field with a declared type and optional constraints | What type/default/not-null metadata did SQLite record? |
| Primary key | A uniqueness/identity rule whose physical meaning depends on table form | Is it an INTEGER PRIMARY KEY alias, an index-backed key, or a WITHOUT ROWID key? |
| Schema | A namespace/database attached to one connection |
Is the object in main, temp, or
an attached database?
|
CREATE TABLE can target main, temp, or an attached schema
SQLite allows a schema name before the table name. The built-in
names are main for the primary database and
temp for the connection's temporary schema.
Additional names appear after ATTACH DATABASE,
which Lesson 4 introduces.
CREATE TABLE main.main_example ( id INTEGER PRIMARY KEY, note TEXT);CREATE TEMP TABLE scratch_example ( label TEXT);-- Equivalent placement for a temporary object:CREATE TABLE temp.scratch_example_2 ( label TEXT);
TEMP and TEMPORARY are synonyms here.
If TEMP is present, the object belongs to the
temp schema. Do not combine TEMP with
an unrelated schema name such as main.some_table;
SQLite rejects that contradictory request.
Names beginning with sqlite_ are reserved for
SQLite internal objects. Use application-owned prefixes or
ordinary domain names instead of trying to create objects such
as sqlite_notes.
sqlite_schema is the database's schema catalog
Every SQLite database has a schema table conventionally exposed
as sqlite_schema. It contains one row for each
table, index, view, and trigger in that schema, apart from the
schema table itself. Older applications and tutorials often use
sqlite_master; SQLite still accepts that historical
alias for compatibility.
SELECT type, name, tbl_name, rootpage, sqlFROM main.sqlite_schemaWHERE name NOT LIKE 'sqlite_%'ORDER BY type, name;
The sql column stores normalized CREATE text for
user-defined objects. It is useful evidence, but not an excuse
to edit the catalog directly. SQLite maintains this table as DDL
executes. Direct schema-table manipulation is an expert-only
recovery/migration technique with corruption risk and is outside
this beginner workflow.
| Column | Meaning for inspection |
|---|---|
type |
Object category: table, index, view, or trigger. |
name |
Object name. SQLite-created automatic indexes often begin
with sqlite_autoindex_.
|
tbl_name |
Associated table/view name; especially useful for indexes and triggers. |
rootpage |
Root b-tree page number for tables/indexes; storage internals are deferred to Chapter 11. |
sql |
CREATE text for the object, normally normalized by SQLite; internal autoindexes can have NULL SQL. |
.schema and PRAGMAs answer different inspection questions
.schema is a sqlite3 shell command. It
asks the database for schema information and renders DDL-like
output for a human. PRAGMAs are SQLite statements that return
structured metadata as rows, which makes them easier to query,
compare, or use from application code.
.schema site_probePRAGMA main.table_info('site_probe');PRAGMA main.table_xinfo('site_probe');PRAGMA main.table_list('site_probe');
table_info returns normal columns and their
declared type, not-null flag, default, and primary-key position.
table_xinfo adds generated and hidden columns when
those exist. table_list reports table-level facts
such as schema name, column count, whether the table is
WITHOUT ROWID, and whether it is
STRICT.
The labs target SQLite 3.53.4.
PRAGMA table_list exists in modern SQLite
(3.37.0+). If a bundled runtime is older, use
sqlite_version() to record that fact rather than
assuming a missing PRAGMA means your SQL is wrong.
IF NOT EXISTS prevents one error; it does not compare schemas
IF NOT EXISTS means “if an object with this name
already satisfies the command's existence check, do nothing
rather than raise the usual creation error.” It does
not mean “compare the existing table to this
declaration and migrate differences.” That distinction is
critical in installers and migrations.
DROP TABLE IF EXISTS drift_demo;CREATE TABLE drift_demo ( id INTEGER PRIMARY KEY, message TEXT);-- No error, but this does NOT add created_at:CREATE TABLE IF NOT EXISTS drift_demo ( id INTEGER PRIMARY KEY, message TEXT, created_at TEXT NOT NULL);PRAGMA table_info('drift_demo');SELECT sqlFROM sqlite_schemaWHERE type='table' AND name='drift_demo';
The expected metadata still contains only id and
message. SQLite produced that result because the
second CREATE became a no-op after it found the existing object.
Safe migration code needs explicit versioning and verification;
Chapter 17 will build that discipline.
Schema-inspection lab: declared DDL versus engine metadata
Create a disposable file named chapter03_schema.db.
Use a fresh file so results are reproducible and do not collide
with the Chapter 1–2 database.
sqlite3 chapter03_schema.dbCREATE TABLE main.device_model ( model_code TEXT PRIMARY KEY, model_name TEXT NOT NULL, vendor TEXT, active INTEGER NOT NULL DEFAULT 1);CREATE TEMP TABLE inspection_notes ( question TEXT, answer TEXT);.schema device_modelSELECT type, name, tbl_name, sqlFROM main.sqlite_schemaWHERE name='device_model';PRAGMA main.table_info('device_model');PRAGMA main.table_xinfo('device_model');PRAGMA main.table_list('device_model');SELECT type, name, sqlFROM temp.sqlite_schemaWHERE name='inspection_notes';
Expected durable state: main.device_model is stored
in chapter03_schema.db. Expected transient state:
temp.inspection_notes exists only for this
connection. The catalog query and PRAGMAs should agree on the
table name and declared columns even though they expose
different metadata shapes.
Inspection checkpoint
Answer by citing the evidence you would inspect.
-
Why can a successful
CREATE TABLE IF NOT EXISTSstill leave the wrong schema? -
When would
table_xinforeveal more thantable_info? - Where do you query metadata for a TEMP table?
-
Why is
.schemauseful but insufficient for programmatic verification? -
What should you record if
PRAGMA table_listis unavailable?
Review the answers
IF NOT EXISTS can be a no-op;
table_xinfo includes generated/hidden columns
omitted by table_info;
TEMP objects have their own temp.sqlite_schema;
.schema is shell-rendered text rather than a
structured application interface;
and an unavailable modern PRAGMA should trigger a version
check such as SELECT sqlite_version().
Failure patterns and safe corrections
| Failure | Diagnosis | Safe correction |
|---|---|---|
| “The installer ran, so the table must be current.” | Only creation success was checked. | Compare expected migration/version state and inspect actual metadata. |
| TEMP table seems missing after reconnect. |
It belonged to the closed connection's
temp schema.
|
Use main for durable objects; use TEMP only
for intentionally transient work.
|
| Created the object in the wrong database. | An unqualified name resolved to a different schema than intended. |
Use main.table or an attached schema
qualifier when ambiguity matters.
|
| A generated/hidden column seems absent. | Only table_info was inspected. |
Use table_xinfo when complete column metadata
matters.
|
Tempted to UPDATE sqlite_schema. |
Catalog text looks editable. | Use supported DDL/migration techniques; direct catalog writes can corrupt a database. |
Summary and bridge to ROWID
SQLite schema design begins with a logical model but must end
with inspection of the concrete objects the engine created. You
can now distinguish main from temp,
read sqlite_schema, use structured PRAGMAs, and
explain why existence checks are weaker than schema
verification. The next lesson asks a deeper question: when
SQLite creates an ordinary table, what key actually organizes
its rows?