| name | sqlite |
| description | SQLite database commands, queries, and embedded database patterns. Use when user mentions "sqlite", "sqlite3", "embedded database", "local database", "lightweight db", "sqlite schema", "sqlite backup", "mobile database", or working with .db/.sqlite files. |
SQLite
sqlite3 CLI Basics
sqlite3 myapp.db
sqlite3 :memory:
sqlite3 myapp.db ".tables"
sqlite3 myapp.db < schema.sql
Common dot-commands inside the shell:
.tables
.schema
.schema users
.headers on
.mode column
.width 20 30 10
.databases
.indexes users
.quit
Create Tables, Insert, Update, Delete
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
total REAL NOT NULL,
status TEXT DEFAULT 'pending',
created_at TEXT DEFAULT (datetime('now'))
);
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
INSERT INTO orders (user_id, total, status)
VALUES (1, 49.99, 'completed'), (1, 25.00, 'pending'), (2, 120.00, 'completed');
UPDATE users SET email = 'newalice@example.com' WHERE id = 1;
DELETE FROM orders WHERE status = 'pending' AND created_at < datetime('now', '-30 days');
RETURNING clause available in 3.35.0+. Check with sqlite3 --version.
Queries
Joins, Subqueries, GROUP BY
SELECT o.id, u.name, o.total
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.status = 'completed';
SELECT u.name, COALESCE(SUM(o.total), 0) AS lifetime_value
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.name;
SELECT * FROM users
WHERE id IN (
SELECT user_id FROM orders GROUP BY user_id HAVING SUM(total) > 100
);
SELECT status, COUNT(*) AS cnt, ROUND(AVG(total), 2) AS avg_total
FROM orders GROUP BY status HAVING cnt > 1 ORDER BY avg_total DESC;
CTEs and Window Functions
WITH monthly AS (
SELECT strftime('%Y-%m', created_at) AS month, SUM(total) AS revenue
FROM orders WHERE status = 'completed' GROUP BY 1
)
SELECT month, revenue,
revenue - LAG(revenue) OVER (ORDER BY month) AS change
FROM monthly;
SELECT user_id, total,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY total DESC) AS rn,
SUM(total) OVER (PARTITION BY user_id) AS user_total
FROM orders;
Output Modes
sqlite3 -header -csv myapp.db "SELECT * FROM users;" > users.csv
sqlite3 -json myapp.db "SELECT * FROM users;"
sqlite3 -header -column myapp.db "SELECT * FROM users;"
Inside the shell:
.mode csv
.mode json
.mode column
.mode table
.mode line
.separator "\t"
Import and Export CSV
sqlite3 -header -csv myapp.db "SELECT * FROM users;" > users.csv
sqlite3 myapp.db <<'EOF'
.mode csv
.import users.csv users
EOF
For large imports, wrap in a transaction for speed:
BEGIN;
.mode csv
.import large_file.csv target_table
COMMIT;
Indexes and EXPLAIN QUERY PLAN
CREATE INDEX idx_orders_user ON orders(user_id);
CREATE INDEX idx_orders_status_date ON orders(status, created_at);
CREATE UNIQUE INDEX idx_users_email ON users(email);
EXPLAIN QUERY PLAN SELECT * FROM orders WHERE user_id = 1;
Key things in the output:
- SCAN TABLE -- no usable index, reads every row
- SEARCH TABLE ... USING INDEX -- index is being used
- USING COVERING INDEX -- query answered entirely from index
- USE TEMP B-TREE -- sort or GROUP BY without index
WAL Mode and Concurrency
PRAGMA journal_mode=WAL;
PRAGMA busy_timeout=5000;
- One writer at a time, but readers are not blocked
- Creates
-wal and -shm files alongside the database
- Force checkpoint:
PRAGMA wal_checkpoint(TRUNCATE);
- Set once; persists across connections
Backup
.backup main backup.db
.dump
.dump users
sqlite3 myapp.db .dump > full_dump.sql
sqlite3 restored.db < full_dump.sql
VACUUM rebuilds the database file, reclaiming space:
VACUUM;
VACUUM INTO 'compact.db';
JSON Support
Available in SQLite 3.38+ (built-in) or via the JSON1 extension:
CREATE TABLE events (id INTEGER PRIMARY KEY, data TEXT NOT NULL);
INSERT INTO events (data)
VALUES ('{"type":"click","page":"/home","tags":["mobile","v2"]}');
SELECT json_extract(data, '$.type') AS event_type FROM events;
SELECT data->>'$.page' AS page FROM events;
SELECT e.id, j.value AS tag
FROM events e, json_each(json_extract(e.data, '$.tags')) j;
UPDATE events SET data = json_set(data, '$.processed', true) WHERE id = 1;
SELECT json_object('id', id, 'name', name) FROM users;
SELECT json_group_array(json_object('id', id, 'name', name)) FROM users;
Full-Text Search (FTS5)
CREATE VIRTUAL TABLE articles_fts USING fts5(title, body, content=articles, content_rowid=id);
INSERT INTO articles_fts(rowid, title, body) SELECT id, title, body FROM articles;
SELECT *, rank FROM articles_fts WHERE articles_fts MATCH 'database AND performance' ORDER BY rank;
SELECT highlight(articles_fts, 1, '<b>', '</b>') FROM articles_fts WHERE articles_fts MATCH 'sqlite';
CREATE TRIGGER articles_ai AFTER INSERT ON articles BEGIN
INSERT INTO articles_fts(rowid, title, body) VALUES (new.id, new.title, new.body);
END;
CREATE TRIGGER articles_ad AFTER DELETE ON articles BEGIN
INSERT INTO articles_fts(articles_fts, rowid, title, body)
VALUES ('delete', old.id, old.title, old.body);
END;
Date and Time Functions
SQLite stores dates as TEXT, REAL, or INTEGER. Built-in functions handle ISO-8601 strings:
SELECT datetime('now');
SELECT date('now', '-7 days');
SELECT strftime('%Y-%m', 'now');
SELECT strftime('%s', 'now');
SELECT datetime('2025-01-15', '+3 months', '-1 day');
SELECT julianday('now') - julianday(created_at) AS days_old FROM users;
Attach Multiple Databases
ATTACH DATABASE 'archive.db' AS archive;
SELECT * FROM main.users u JOIN archive.orders o ON o.user_id = u.id;
INSERT INTO archive.orders SELECT * FROM main.orders WHERE created_at < '2024-01-01';
DELETE FROM main.orders WHERE created_at < '2024-01-01';
DETACH DATABASE archive;
Pragmas
PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
PRAGMA cache_size=-64000;
PRAGMA mmap_size=268435456;
PRAGMA temp_store=MEMORY;
PRAGMA foreign_keys=ON;
PRAGMA integrity_check;
PRAGMA table_info(users);
PRAGMA index_list(users);
PRAGMA compile_options;
Common Patterns
Config Key-Value Store
CREATE TABLE config (
key TEXT PRIMARY KEY, value TEXT,
updated_at TEXT DEFAULT (datetime('now'))
);
INSERT OR REPLACE INTO config (key, value) VALUES ('theme', 'dark');
SELECT value FROM config WHERE key = 'theme';
Local Cache with Expiry
CREATE TABLE cache (key TEXT PRIMARY KEY, value TEXT, expires_at TEXT);
INSERT OR REPLACE INTO cache (key, value, expires_at)
VALUES ('api:/users', '{"data":[...]}', datetime('now', '+1 hour'));
SELECT value FROM cache WHERE key = 'api:/users' AND expires_at > datetime('now');
DELETE FROM cache WHERE expires_at <= datetime('now');
CLI Tool Storage
CREATE TABLE history (
id INTEGER PRIMARY KEY, command TEXT NOT NULL,
args TEXT, exit_code INTEGER,
ran_at TEXT DEFAULT (datetime('now'))
);
SELECT command, args, ran_at FROM history ORDER BY ran_at DESC LIMIT 20;
SELECT command, COUNT(*) AS cnt FROM history GROUP BY command ORDER BY cnt DESC;
Test Fixtures
sqlite3 test.db < schema.sql && sqlite3 test.db < seed.sql
BEGIN;
INSERT INTO users (id, name, email) VALUES
(1, 'Test User', 'test@example.com'),
(2, 'Admin', 'admin@example.com');
INSERT INTO orders (user_id, total, status) VALUES
(1, 99.99, 'completed'), (2, 50.00, 'pending');
COMMIT;