Serverless, self-contained relational database engine stored in a single file, ideal for embedded applications, local storage, prototyping, and datasets up to several hundred gigabytes.

Table of Contents#

  1. Overview
  2. Installation
  3. Database Basics
  4. Schema Management
  5. Data Manipulation
  6. Transactions and Savepoints
  7. WAL Mode
  8. Concurrency and Locking
  9. Pragma Directives
  10. Indexing and Query Optimization
  11. Multi-Database with ATTACH
  12. Backup Strategies
  13. Performance Tuning
  14. Troubleshooting
  15. See Also
  16. Sources

1. Overview#

SQLite is a C library that implements a complete, ACID-compliant SQL database engine in a single file. Unlike client-server databases, SQLite reads and writes directly to ordinary disk files. A complete database (tables, indexes, triggers, views) is contained in one cross-platform file.

Key characteristics:

  • Zero configuration - no server process, no setup, no administration
  • Self-contained - single library with no external dependencies
  • ACID compliant - atomic, consistent, isolated, durable transactions
  • Cross-platform - database files are portable between 32-bit and 64-bit, big-endian and little-endian systems
  • Size limits - maximum database size of 281 TB; practical limit depends on filesystem
  • Public domain - no license restrictions

When to Use SQLite#

Good FitPoor Fit
Embedded/mobile applicationsHigh-concurrency write workloads
Desktop applicationsMulti-server web applications
Testing and prototypingDatasets requiring fine-grained access control
Data analysis and ETLWrite-heavy workloads with many concurrent writers
Configuration/cache storageApplications needing client-server architecture
Single-user web applicationsReplication or clustering requirements

2. Installation#

2.1 Linux#

# Debian/Ubuntu
sudo apt install -y sqlite3

# RHEL/Rocky/Alma
sudo dnf install -y sqlite

# Arch Linux
sudo pacman -S sqlite

2.2 Verify#

sqlite3 --version

3. Database Basics#

3.1 Creating and Opening Databases#

# Create or open a database (creates file if it doesn't exist)
sqlite3 <database_name>.db

# Open in read-only mode
sqlite3 -readonly <database_name>.db

# Execute a command and exit
sqlite3 <database_name>.db "SELECT * FROM users;"

3.2 Shell Commands (Dot Commands)#

CommandDescription
.helpShow all dot commands
.databasesList attached databases
.tablesList tables
.schema <table>Show CREATE statement
.headers onShow column headers in output
.mode columnColumnar output format
.mode csvCSV output format
.mode jsonJSON output format
.output <file>Redirect output to file
.import <file> <table>Import CSV into table
.dumpExport entire database as SQL
.dump <table>Export single table as SQL
.read <file>Execute SQL from file
.quitExit the shell
.timer onShow query execution time
.eqp onShow query plan for every query

3.3 Deleting a Database#

SQLite databases are ordinary files. Delete them with your filesystem tools:

rm <database_name>.db
# Also remove WAL and SHM files if they exist
rm -f <database_name>.db-wal <database_name>.db-shm

4. Schema Management#

4.1 Creating Tables#

CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT NOT NULL UNIQUE,
    created_at TEXT DEFAULT (datetime('now')),
    status TEXT DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'banned'))
);

-- Without AUTOINCREMENT (reuses deleted rowids, faster)
CREATE TABLE logs (
    id INTEGER PRIMARY KEY,
    message TEXT NOT NULL,
    level TEXT NOT NULL,
    timestamp TEXT DEFAULT (datetime('now'))
);

4.2 Modifying Tables#

SQLite has limited ALTER TABLE support:

-- Add a column
ALTER TABLE users ADD COLUMN phone TEXT;

-- Rename a column (SQLite 3.25+)
ALTER TABLE users RENAME COLUMN phone TO phone_number;

-- Rename a table
ALTER TABLE users RENAME TO app_users;

-- Drop a column (SQLite 3.35+)
ALTER TABLE users DROP COLUMN phone_number;

For operations not supported by ALTER TABLE (changing column types, adding constraints), use the rebuild pattern:

BEGIN TRANSACTION;
CREATE TABLE users_new (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT NOT NULL UNIQUE,
    age INTEGER CHECK (age > 0)
);
INSERT INTO users_new (id, name, email) SELECT id, name, email FROM users;
DROP TABLE users;
ALTER TABLE users_new RENAME TO users;
COMMIT;

4.3 Deleting Tables#

DROP TABLE IF EXISTS users;

4.4 Views#

CREATE VIEW active_users AS
SELECT id, name, email FROM users WHERE status = 'active';

DROP VIEW IF EXISTS active_users;

5. Data Manipulation#

5.1 Inserting Data#

-- Single row
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');

-- Multiple rows
INSERT INTO users (name, email) VALUES
    ('Bob', 'bob@example.com'),
    ('Carol', 'carol@example.com'),
    ('Dave', 'dave@example.com');

-- Insert or replace on conflict
INSERT OR REPLACE INTO users (id, name, email)
VALUES (1, 'Alice Updated', 'alice@example.com');

-- Upsert (SQLite 3.24+)
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')
ON CONFLICT(email) DO UPDATE SET name = excluded.name;

5.2 Querying Data#

-- Basic select
SELECT * FROM users;

-- With conditions and ordering
SELECT name, email FROM users
WHERE status = 'active'
ORDER BY name ASC
LIMIT 10 OFFSET 20;

-- Aggregation
SELECT status, COUNT(*) as count
FROM users
GROUP BY status
HAVING count > 5;

-- Window functions (SQLite 3.25+)
SELECT name, email,
       ROW_NUMBER() OVER (ORDER BY created_at) as row_num
FROM users;

-- JSON functions (SQLite 3.38+)
SELECT json_extract(metadata, '$.role') as role
FROM users
WHERE json_extract(metadata, '$.active') = 1;

-- Common Table Expressions
WITH recent AS (
    SELECT * FROM users WHERE created_at > datetime('now', '-7 days')
)
SELECT * FROM recent WHERE status = 'active';

5.3 Updating Data#

UPDATE users SET status = 'inactive'
WHERE created_at < datetime('now', '-1 year');

5.4 Deleting Data#

DELETE FROM users WHERE status = 'banned';

-- Delete all rows (faster than DELETE without WHERE)
DELETE FROM users;

6. Transactions and Savepoints#

6.1 Basic Transactions#

BEGIN TRANSACTION;
INSERT INTO users (name, email) VALUES ('Eve', 'eve@example.com');
INSERT INTO logs (message, level) VALUES ('User created: Eve', 'INFO');
COMMIT;

-- Or rollback on error
BEGIN TRANSACTION;
INSERT INTO users (name, email) VALUES ('Eve', 'eve@example.com');
-- Something went wrong
ROLLBACK;

6.2 Transaction Types#

-- DEFERRED (default): acquires lock only when needed
BEGIN DEFERRED TRANSACTION;

-- IMMEDIATE: acquires RESERVED lock immediately (prevents other writers)
BEGIN IMMEDIATE TRANSACTION;

-- EXCLUSIVE: acquires EXCLUSIVE lock immediately (prevents all other access)
BEGIN EXCLUSIVE TRANSACTION;

For write transactions, prefer BEGIN IMMEDIATE to avoid SQLITE_BUSY errors from lock escalation:

BEGIN IMMEDIATE;
UPDATE users SET status = 'inactive' WHERE id = 42;
COMMIT;

6.3 Savepoints#

Savepoints create nested transaction points within a transaction:

BEGIN TRANSACTION;

INSERT INTO users (name, email) VALUES ('Frank', 'frank@example.com');

SAVEPOINT before_logs;
INSERT INTO logs (message, level) VALUES ('Test entry', 'DEBUG');
-- Undo just the log entry
ROLLBACK TO SAVEPOINT before_logs;

-- Frank's insert is still staged
COMMIT;

7. WAL Mode#

Write-Ahead Logging (WAL) mode significantly improves concurrency by allowing readers and a single writer to operate simultaneously without blocking each other.

7.1 Enable WAL Mode#

PRAGMA journal_mode=WAL;

This is persistent; the database stays in WAL mode until explicitly changed back.

7.2 WAL vs Rollback Journal#

FeatureRollback Journal (default)WAL Mode
Concurrent readers during writeNo (blocked)Yes
Write performanceGoodBetter (sequential writes)
Read performance during writesBlockedUnblocked
File count1 (.db)3 (.db, .db-wal, .db-shm)
Network filesystem supportYesNo (requires shared memory)
Crash recoverySlowerFaster

7.3 WAL Checkpointing#

The WAL file must be periodically checkpointed (flushed to the main database):

-- Automatic checkpointing threshold (default 1000 pages)
PRAGMA wal_autocheckpoint=1000;

-- Manual checkpoint
PRAGMA wal_checkpoint(PASSIVE);    -- Non-blocking, checkpoint what's possible
PRAGMA wal_checkpoint(FULL);       -- Block until all frames checkpointed
PRAGMA wal_checkpoint(TRUNCATE);   -- Full + truncate WAL file to zero bytes

7.4 WAL Mode Limitations#

  • Does not work on network filesystems (NFS, SMB) because it requires shared memory (mmap)
  • Creates two additional files (.db-wal and .db-shm) alongside the database
  • Long-running read transactions can prevent WAL checkpointing, causing the WAL file to grow

8. Concurrency and Locking#

8.1 Lock States#

SQLite uses a file-level locking system with five states:

Lock StateDescription
UNLOCKEDNo lock held; default state
SHAREDReading; multiple readers allowed simultaneously
RESERVEDPreparing to write; only one RESERVED lock at a time; readers still allowed
PENDINGWaiting for SHARED locks to clear before writing
EXCLUSIVEWriting; no other connections can read or write

8.2 Busy Timeout#

When a connection encounters a locked database, it can wait instead of failing immediately:

-- Wait up to 5 seconds for locks to clear
PRAGMA busy_timeout=5000;

In application code, always set a busy timeout:

sqlite3 mydb.db "PRAGMA busy_timeout=5000;"

8.3 Concurrency Best Practices#

  • Enable WAL mode for all multi-connection use cases
  • Use BEGIN IMMEDIATE for write transactions to fail fast on contention
  • Keep transactions short; commit as soon as possible
  • Set busy_timeout to a reasonable value (1000-30000 ms)
  • For highest write throughput, funnel all writes through a single connection
  • Avoid BEGIN EXCLUSIVE unless absolutely necessary

9. Pragma Directives#

Pragmas are SQLite-specific commands that control database behavior:

9.1 Essential Pragmas#

-- Performance
PRAGMA journal_mode=WAL;           -- Enable WAL mode
PRAGMA synchronous=NORMAL;         -- NORMAL is safe with WAL; FULL is safest
PRAGMA cache_size=-64000;          -- 64 MB page cache (negative = KB)
PRAGMA mmap_size=268435456;        -- 256 MB memory-mapped I/O
PRAGMA temp_store=MEMORY;          -- Store temporary tables in memory

-- Integrity
PRAGMA foreign_keys=ON;            -- Enforce foreign key constraints (off by default)
PRAGMA integrity_check;            -- Full database integrity check
PRAGMA quick_check;                -- Faster, less thorough check

-- Information
PRAGMA table_info(<table>);        -- Column information
PRAGMA index_list(<table>);        -- Indexes on a table
PRAGMA compile_options;            -- SQLite compile-time options
PRAGMA database_list;              -- Attached databases

Apply these at connection open (they do not persist across connections except journal_mode):

PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
PRAGMA busy_timeout=5000;
PRAGMA cache_size=-64000;
PRAGMA foreign_keys=ON;
PRAGMA temp_store=MEMORY;
PRAGMA mmap_size=268435456;

9.3 Synchronous Modes#

ModeSafetyPerformanceNotes
OFFRisk of corruption on crashFastestNever use in production
NORMALSafe with WAL modeFastRecommended with WAL
FULLSafe in all modesSlowerDefault; safest choice
EXTRAMaximum safetySlowestRarely needed

10. Indexing and Query Optimization#

10.1 Creating Indexes#

-- Single column index
CREATE INDEX idx_users_email ON users(email);

-- Compound index
CREATE INDEX idx_logs_level_time ON logs(level, timestamp);

-- Unique index
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);

-- Partial index (SQLite 3.8.0+)
CREATE INDEX idx_active_users ON users(name) WHERE status = 'active';

-- Expression index (SQLite 3.9.0+)
CREATE INDEX idx_users_lower_name ON users(lower(name));

10.2 Query Plan Analysis#

-- Show the query plan
EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = 'alice@example.com';

-- Full bytecode explanation
EXPLAIN SELECT * FROM users WHERE email = 'alice@example.com';

-- Enable automatic query plan display
.eqp on

Look for SCAN (full table scan, usually bad) vs SEARCH (index lookup, usually good).

10.3 Analyzing Statistics#

-- Update query planner statistics
ANALYZE;

-- Analyze a specific table
ANALYZE users;

-- View statistics
SELECT * FROM sqlite_stat1;

10.4 Index Best Practices#

  • Index columns used in WHERE, JOIN, and ORDER BY clauses
  • Compound indexes should list equality columns first, then range columns
  • Avoid over-indexing; each index costs write performance and storage
  • Use partial indexes for queries that always filter on the same condition
  • Run ANALYZE after significant data changes

11. Multi-Database with ATTACH#

SQLite can attach multiple database files to a single connection, enabling cross-database queries:

-- Attach another database
ATTACH DATABASE '/path/to/other.db' AS other;

-- Query across databases
SELECT u.name, o.order_date
FROM main.users u
JOIN other.orders o ON u.id = o.user_id;

-- List attached databases
PRAGMA database_list;

-- Detach
DETACH DATABASE other;

Limitations#

  • Transactions span all attached databases (atomic across all)
  • Maximum 10 attached databases by default (compile-time configurable)
  • ATTACH is not allowed inside a transaction in WAL mode

12. Backup Strategies#

12.1 SQL Dump#

# Full dump (portable, human-readable)
sqlite3 <database>.db .dump > backup.sql

# Restore from dump
sqlite3 <database_new>.db < backup.sql

12.2 Online Backup API#

The .backup command uses SQLite's online backup API, which is safe even while the database is in use:

# Backup to file
sqlite3 <database>.db ".backup /backup/db-$(date +%Y%m%d).db"

12.3 File Copy (WAL Mode Considerations)#

In WAL mode, you must checkpoint and copy all three files:

# Checkpoint first to flush WAL to main database
sqlite3 <database>.db "PRAGMA wal_checkpoint(TRUNCATE);"

# Then copy the database file
cp <database>.db /backup/<database>-$(date +%Y%m%d).db

If you cannot checkpoint (database is busy), copy all three files atomically:

cp <database>.db <database>.db-wal <database>.db-shm /backup/

12.4 Automated Backup Script#

#!/bin/bash
# /usr/local/bin/sqlite-backup.sh
DB_PATH="<database>.db"
BACKUP_DIR="/backup/sqlite"
RETENTION_DAYS=30
DATE=$(date +%Y%m%d-%H%M%S)

mkdir -p "${BACKUP_DIR}"
sqlite3 "${DB_PATH}" ".backup ${BACKUP_DIR}/backup-${DATE}.db"

if [ $? -eq 0 ]; then
    echo "Backup successful"
    find "${BACKUP_DIR}" -name "*.db" -mtime +${RETENTION_DAYS} -delete
else
    echo "Backup FAILED" >&2
    exit 1
fi

12.5 VACUUM INTO (SQLite 3.27+)#

Creates a compacted copy of the database:

VACUUM INTO '/backup/compacted.db';

This is safe during concurrent access and produces a defragmented copy.

13. Performance Tuning#

13.1 Bulk Insert Optimization#

-- Wrap bulk inserts in a transaction (50x+ faster)
PRAGMA synchronous=OFF;  -- Only during initial load
BEGIN TRANSACTION;
INSERT INTO data VALUES (1, 'a');
INSERT INTO data VALUES (2, 'b');
-- ... thousands more
COMMIT;
PRAGMA synchronous=NORMAL;

13.2 Memory-Mapped I/O#

-- Map 256 MB of database into memory
PRAGMA mmap_size=268435456;

This allows the OS to handle caching via the page cache, which can dramatically speed up read-heavy workloads.

13.3 VACUUM#

Over time, deleted and updated rows leave free space in the database file. VACUUM reclaims this space:

-- Full vacuum (rebuilds entire database, requires 2x disk space temporarily)
VACUUM;

-- Auto-vacuum (reclaims space automatically, slight overhead)
PRAGMA auto_vacuum=INCREMENTAL;
PRAGMA incremental_vacuum(100);  -- Free up to 100 pages

13.4 Size and Speed Comparison#

ConfigurationRelative Write SpeedDurability
Default (rollback journal, FULL sync)1x (baseline)Maximum
WAL mode, NORMAL sync~5xHigh
WAL mode, NORMAL sync, large cache~10xHigh
In-transaction batch, NORMAL sync~50xHigh
OFF sync (dangerous)~100xData loss risk on crash

14. Troubleshooting#

IssueCauseSolution
database is lockedAnother connection holds an exclusive lockSet PRAGMA busy_timeout=5000; use WAL mode; keep transactions short
SQLITE_BUSY on writeConcurrent writers in rollback journal modeSwitch to WAL mode; use BEGIN IMMEDIATE
Database file keeps growingDeleted rows leave free pagesRun VACUUM; or enable auto_vacuum
Slow queries on large tablesMissing indexes or stale statisticsRun EXPLAIN QUERY PLAN; add indexes; run ANALYZE
database disk image is malformedFile corruption (crash, bad disk, NFS)Run PRAGMA integrity_check; restore from backup
WAL file growing unboundedLong-running read transaction prevents checkpointClose long-lived read connections; run PRAGMA wal_checkpoint(TRUNCATE)
Foreign keys not enforcedforeign_keys pragma defaults to OFFSet PRAGMA foreign_keys=ON at every connection open
Poor write performanceAutocommit mode (each INSERT is its own transaction)Batch writes in explicit transactions
Cannot use WAL on NFSWAL requires shared memory (mmap)Use rollback journal mode on network filesystems

See Also#

Sources#