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?