Cheat sheet

SQLite cheat sheet

SQLite quirks: dynamic typing, single-file databases, no information_schema, JSON via the JSON1 extension, ATTACH DATABASE, and the . meta-commands you'll use in the CLI.

The mental model #

sql
-- Each .db file is one database. No server.
-- Dynamic typing: column types are *hints* — SQLite stores actual values as one of:
--   NULL, INTEGER, REAL, TEXT, BLOB.
-- INTEGER PRIMARY KEY is an alias for the rowid (no separate column).

CREATE TABLE users (
  id    INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT NOT NULL UNIQUE,
  data  ANY                         -- type names are hints; STRICT tables (3.37+) enforce them
);

-- Strict tables (3.37+) — actually enforce declared types.
CREATE TABLE strict_users (id INTEGER, email TEXT) STRICT;

CLI meta-commands (sqlite3 shell) #

sql
.databases                    -- list attached databases
.tables                       -- list tables in current db
.tables pattern               -- tables matching LIKE pattern
.schema table_name            -- CREATE TABLE statement
.indexes                      -- list indexes
.indexes table_name           -- indexes for a table
.headers on                   -- show column names in output
.mode column                  -- aligned columns (also: csv, tabs, json, html, line)
.timer on                     -- show query times
.read script.sql              -- execute SQL from file
.dump                         -- export all tables as SQL
.backup file.db               -- copy current db
.import file.csv table_name   -- bulk load CSV
.changes on                   -- show row counts after DML
.quit                         -- exit

-- Get help.
.help

Schema introspection (no information_schema) #

sql
-- List tables.
SELECT name FROM sqlite_master WHERE type = 'table';
SELECT name FROM sqlite_schema WHERE type = 'table';   -- alias from 3.33+

-- Get a table's columns.
PRAGMA table_info('orders');                            -- name, type, notnull, dflt_value, pk

-- Get a table's foreign keys.
PRAGMA foreign_key_list('orders');

-- Get a table's indexes.
PRAGMA index_list('orders');
PRAGMA index_info('idx_name');

-- Get the original CREATE statement.
SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'orders';

Date and time #

sql
-- SQLite has no DATE / TIMESTAMP types — stores as TEXT, INTEGER, or REAL.
-- Use ISO 8601 text by default: '2026-04-26' or '2026-04-26 14:30:00'.

date('now')                              -- '2026-04-26' (UTC)
datetime('now')                          -- '2026-04-26 14:30:00' (UTC)
datetime('now', 'localtime')             -- session-local
strftime('%Y-%m-%d', 'now')              -- format

-- Modifiers ("date math" via the second argument).
date('now', '+7 days')
date('now', '-1 month')
date('now', 'start of month')             -- first of month
date('now', 'start of month', '+1 month', '-1 day')   -- last of month
date('now', 'weekday 1')                  -- next Monday

-- Difference in days.
julianday(end) - julianday(start)         -- as REAL

-- Extract a part.
strftime('%Y', d)                         -- year as text
CAST(strftime('%Y', d) AS INTEGER)        -- year as integer

JSON (JSON1 extension, included by default since 3.38) #

sql
json_extract(payload, '$.user_id')        -- scalar
json_extract(payload, '$.items[0]')       -- array index

-- Path operators (3.38+).
payload -> '$.user_id'                    -- returns JSON
payload ->> '$.user_id'                   -- returns scalar text

-- Modify.
json_set(payload, '$.user.plan', 'pro')
json_remove(payload, '$.tmp_field')

-- Build.
json_object('id', id, 'email', email)
json_array(1, 2, 3)
json_group_array(name)                    -- aggregate to array
json_group_object(key, value)             -- aggregate to object

-- Iterate.
SELECT j.value FROM events, json_each(payload, '$.items') AS j;

ATTACH DATABASE: cross-file queries #

sql
-- Open another .db file alongside the current connection.
ATTACH DATABASE 'archive.db' AS archive;

-- Now you can query across files.
SELECT * FROM main.orders
UNION ALL
SELECT * FROM archive.orders;

-- Detach when done.
DETACH DATABASE archive;

Common patterns #

sql
-- Upsert.
INSERT INTO users (id, email) VALUES (1, 'a@x.com')
ON CONFLICT (id) DO UPDATE SET email = excluded.email;

-- INSERT OR IGNORE / REPLACE.
INSERT OR IGNORE INTO users (id, email) VALUES (1, 'a@x.com');
INSERT OR REPLACE INTO users (id, email) VALUES (1, 'a@x.com');

-- LIMIT / OFFSET.
SELECT * FROM users LIMIT 20 OFFSET 40;

-- Window functions (3.25+).
SELECT id, total,
       SUM(total) OVER (ORDER BY id) AS running_total
FROM orders;

-- RETURNING (3.35+).
INSERT INTO users (email) VALUES ('a@x.com') RETURNING *;
DELETE FROM users WHERE id = 1 RETURNING email;

PRAGMAs (the SQLite-specific config) #

sql
-- Foreign keys are OFF by default in SQLite!
PRAGMA foreign_keys = ON;                 -- enable per connection

-- Other useful pragmas.
PRAGMA journal_mode = WAL;                -- write-ahead log; better concurrency
PRAGMA synchronous = NORMAL;              -- balance durability and speed
PRAGMA temp_store = MEMORY;               -- temp tables in RAM
PRAGMA cache_size = -64000;               -- 64 MB cache
PRAGMA integrity_check;                   -- verify db is not corrupt
PRAGMA quick_check;                       -- faster integrity check

SQLite gotchas #

  • No native DATE / TIMESTAMP types. Store ISO text, use date() / datetime() / strftime() / julianday() for operations. Or use INTEGER for Unix timestamps.
  • Foreign keys disabled by default, so set PRAGMA foreign_keys = ON per connection.
  • Single writer at a time: SQLite allows many readers but only one writer. Use WAL mode (PRAGMA journal_mode = WAL) for better concurrency.
  • Type affinity, not enforcement. CREATE TABLE t (id INTEGER) will accept INSERT INTO t VALUES ('hello') unless the table is declared STRICT. Use STRICT tables for production schemas.
  • ALTER TABLE is limited. You can rename tables/columns and add columns, but dropping or modifying is the rebuild-and-copy dance on older versions. 3.35+ supports DROP COLUMN.
  • No information_schema, so use sqlite_master and PRAGMA commands.

FAQ #

Are foreign keys enforced in SQLite?

Off by default. You must enable them per connection: PRAGMA foreign_keys = ON. Once enabled, FK constraints work as expected. Many SQLite users are surprised to find their FK declarations were never actually enforced, so check this on any project.

How does SQLite handle dates and timestamps?

SQLite has no native DATE / TIMESTAMP type; values are stored as TEXT (ISO 8601), INTEGER (Unix epoch), or REAL (Julian day). Use date(), datetime(), julianday(), and strftime() to manipulate. ISO 8601 text ('2026-04-26 14:30:00') is the safest convention.

How do I list all tables in SQLite?

SELECT name FROM sqlite_master WHERE type = 'table', since SQLite has no information_schema. From the CLI: .tables. To see a table's columns: PRAGMA table_info('orders') or .schema orders.

Can SQLite handle multiple writers?

Limited. SQLite serializes writers (one writer at a time). Reads are concurrent. Use WAL mode (PRAGMA journal_mode = WAL) for much better concurrency: readers and writers don't block each other in many cases. SQLite is excellent for small services with low write contention; not for high-write multi-tenant apps.

What's the difference between TEXT and STRICT typing in SQLite?

By default, SQLite's column types are hints, so INTEGER columns will accept 'hello'. STRICT tables (3.37+) actually enforce types: CREATE TABLE t (id INTEGER, name TEXT) STRICT. Use STRICT for production schemas to catch type bugs at insert time.

How do I do an upsert in SQLite?

INSERT INTO t (id, value) VALUES (1, 'x') ON CONFLICT (id) DO UPDATE SET value = excluded.value uses Postgres-compatible syntax (3.24+). Or INSERT OR REPLACE INTO t ... (legacy form), but that does a DELETE + INSERT which fires triggers and changes the rowid.

Tip

SQLite is the right database far more often than people assume. A single-file embedded database with full SQL, ACID, and zero ops is the right choice for: desktop apps, mobile apps, small services with low write concurrency, local dev, prototypes, and edge deployments. It scales to millions of rows on commodity hardware. Reach for Postgres / MySQL when you need real multi-writer concurrency, advanced indexing, or a network protocol, but don't reach for them by default.