SQLite Quick Reference
Goal: Practical SQLite knowledge from basics to advanced usage patterns Level: Beginner → Advanced Language: English
Table of Contents: Introduction | CLI Essentials | Data Types | SQL Operations | Advanced Queries | Indexes & Performance | JSON Support | Full-Text Search | Python Integration | Backup & Administration | Limitations & When to Use
Introduction
SQLite is a self-contained, serverless, zero-configuration, transactional SQL database engine. It is the most widely deployed database in the world — embedded in every phone, browser, and countless applications.
Key properties:
- Serverless — no separate process, reads/writes directly to a file
- Zero-config — no setup, no administration
- Single file — entire database is one cross-platform file
- ACID compliant — full transaction support even after crashes
- Small footprint — library is ~600KB
Best for: local/embedded apps, prototyping, testing, data analysis, IoT, mobile, caching, file format replacement
Not ideal for: high-concurrency write-heavy workloads, client/server architectures, multi-TB datasets
CLI Essentials
Installation
SQLite CLI (sqlite3) is pre-installed on macOS and most Linux distributions.
# Linux (Debian/Ubuntu)
sudo apt install sqlite3
# Linux (Fedora)
sudo dnf install sqlite
# macOS (already installed, or update via Homebrew)
brew install sqlite
# Windows (download from https://sqlite.org/download.html)
# Or via scoop/choco:
scoop install sqlite
choco install sqlite
Starting the CLI
# Open or create a database
sqlite3 mydb.db
# Open in-memory database (temporary)
sqlite3 :memory:
# Open read-only
sqlite3 -readonly mydb.db
# Execute SQL directly from command line
sqlite3 mydb.db "SELECT * FROM users;"
# Execute from file
sqlite3 mydb.db < script.sql
Essential Dot Commands
-- Help
.help -- List all dot commands
.help COMMAND -- Help for specific command
-- Database info
.databases -- Show attached databases
.tables -- List all tables
.tables %pattern% -- List tables matching pattern
.schema -- Show CREATE statements for all objects
.schema tablename -- Show CREATE statement for specific table
.indexes -- List all indexes
.indexes tablename -- List indexes for specific table
-- Output formatting
.mode column -- Columnar output (readable)
.mode csv -- CSV output
.mode json -- JSON output
.mode markdown -- Markdown table output
.mode table -- ASCII table output
.mode tabs -- Tab-separated output
.mode line -- One value per line
.headers on -- Show column headers
-- File I/O
.output file.txt -- Redirect output to file
.output stdout -- Reset output to terminal
.read script.sql -- Execute SQL from file
.dump -- Dump entire database as SQL
.dump tablename -- Dump specific table
-- Import/Export
.import file.csv tablename -- Import CSV into table
.excel -- Open results in spreadsheet app
.once file.txt -- Send next command output to file
-- Session
.quit -- Exit (also .exit or Ctrl+D)
.timer on -- Show execution time for commands
.changes on -- Show number of rows changed
Recommended CLI Startup
Or create ~/.sqliterc for persistent settings:
Data Types & Type Affinity
SQLite uses dynamic typing with type affinity. Any column can hold any type, but columns have a preferred type.
Storage Classes
| Storage Class | Description | Examples |
|---|---|---|
NULL |
Missing or unknown value | NULL |
INTEGER |
Signed integer (1, 2, 3, 4, 6, or 8 bytes) | 42, -7, 0 |
REAL |
8-byte IEEE floating point | 3.14, -0.001 |
TEXT |
UTF-8 or UTF-16 string | 'hello' |
BLOB |
Binary data, stored as-is | X'48454C4C4F' |
Type Affinity Rules
SQLite maps declared types to affinities:
| Declared Type Contains | Affinity | Example Declarations |
|---|---|---|
INT |
INTEGER | INT, INTEGER, BIGINT |
CHAR, CLOB, TEXT |
TEXT | VARCHAR(255), TEXT |
BLOB or no type |
BLOB | BLOB, (none) |
REAL, FLOA, DOUB |
REAL | REAL, DOUBLE, FLOAT |
| Otherwise | NUMERIC | DECIMAL(10,2), BOOLEAN |
Boolean & Date Handling
SQLite has no native BOOLEAN or DATE types — use conventions:
-- Booleans: store as INTEGER (0 = false, 1 = true)
CREATE TABLE tasks (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
done INTEGER DEFAULT 0 CHECK (done IN (0, 1))
);
-- Dates: store as TEXT (ISO 8601) or INTEGER (Unix timestamp)
CREATE TABLE events (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
timestamp_unix INTEGER DEFAULT (unixepoch())
);
SQL Operations
CREATE TABLE
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL,
age INTEGER CHECK (age > 0),
bio TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now'))
);
-- Composite primary key
CREATE TABLE order_items (
order_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (order_id, product_id),
FOREIGN KEY (order_id) REFERENCES orders(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
-- Strict mode (SQLite 3.37+) — enforces declared types
CREATE TABLE measurements (
id INTEGER PRIMARY KEY,
value REAL NOT NULL,
label TEXT NOT NULL
) STRICT;
INSERT
-- Single row
INSERT INTO users (username, email, age) VALUES ('alice', 'alice@example.com', 30);
-- Multiple rows
INSERT INTO users (username, email, age) VALUES
('bob', 'bob@example.com', 25),
('carol', 'carol@example.com', 28);
-- Insert or ignore on conflict
INSERT OR IGNORE INTO users (username, email, age) VALUES ('alice', 'a@b.com', 31);
-- Upsert (INSERT or UPDATE on conflict)
INSERT INTO users (username, email, age) VALUES ('alice', 'new@email.com', 31)
ON CONFLICT(username) DO UPDATE SET
email = excluded.email,
age = excluded.age;
-- Insert from SELECT
INSERT INTO archived_users SELECT * FROM users WHERE age > 60;
SELECT
-- Basic query
SELECT username, email FROM users WHERE age >= 25 ORDER BY username;
-- LIMIT and OFFSET (pagination)
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 20;
-- DISTINCT
SELECT DISTINCT age FROM users;
-- Aliases
SELECT username AS name, age AS years FROM users;
-- Aggregate functions
SELECT
COUNT(*) AS total,
AVG(age) AS avg_age,
MIN(age) AS youngest,
MAX(age) AS oldest,
GROUP_CONCAT(username, ', ') AS names
FROM users;
-- GROUP BY with HAVING
SELECT age, COUNT(*) AS count
FROM users
GROUP BY age
HAVING count > 1
ORDER BY count DESC;
-- CASE expression
SELECT username,
CASE
WHEN age < 25 THEN 'junior'
WHEN age < 40 THEN 'mid'
ELSE 'senior'
END AS category
FROM users;
-- COALESCE (first non-NULL)
SELECT COALESCE(bio, 'No bio provided') AS bio FROM users;
-- LIKE and GLOB
SELECT * FROM users WHERE email LIKE '%@example.com'; -- Case-insensitive
SELECT * FROM users WHERE username GLOB '[a-c]*'; -- Case-sensitive, Unix-style
UPDATE
UPDATE users SET email = 'updated@example.com' WHERE username = 'alice';
-- Update multiple columns
UPDATE users SET age = age + 1, bio = 'Birthday!' WHERE username = 'bob';
-- Update with subquery
UPDATE orders SET status = 'archived'
WHERE user_id IN (SELECT id FROM users WHERE age > 60);
-- UPDATE with RETURNING (SQLite 3.35+)
UPDATE users SET age = age + 1 WHERE username = 'alice' RETURNING *;
DELETE
DELETE FROM users WHERE username = 'carol';
-- Delete with RETURNING (SQLite 3.35+)
DELETE FROM users WHERE age > 60 RETURNING id, username;
-- Delete all rows (faster than DELETE FROM without WHERE)
DELETE FROM users;
ALTER TABLE
-- Add column
ALTER TABLE users ADD COLUMN phone TEXT;
-- Rename column (SQLite 3.25+)
ALTER TABLE users RENAME COLUMN phone TO phone_number;
-- Rename table
ALTER TABLE users RENAME TO app_users;
-- Drop column (SQLite 3.35+)
ALTER TABLE users DROP COLUMN bio;
Advanced Queries
JOINs
-- INNER JOIN (only matching rows)
SELECT o.id, u.username, o.total
FROM orders o
INNER JOIN users u ON o.user_id = u.id;
-- LEFT JOIN (all left rows, NULLs for no match)
SELECT u.username, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id;
-- CROSS JOIN (cartesian product)
SELECT * FROM colors CROSS JOIN sizes;
-- Self-join
SELECT a.username AS user, b.username AS referrer
FROM users a
INNER JOIN users b ON a.referred_by = b.id;
Subqueries
-- Scalar subquery
SELECT username, (SELECT COUNT(*) FROM orders WHERE user_id = users.id) AS order_count
FROM users;
-- IN subquery
SELECT * FROM users
WHERE id IN (SELECT DISTINCT user_id FROM orders WHERE total > 100);
-- EXISTS
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
Common Table Expressions (CTEs)
-- Simple CTE
WITH active_users AS (
SELECT * FROM users WHERE last_login > datetime('now', '-30 days')
)
SELECT username, email FROM active_users ORDER BY username;
-- Multiple CTEs
WITH
big_orders AS (
SELECT user_id, SUM(total) AS total_spent
FROM orders GROUP BY user_id HAVING total_spent > 1000
),
user_info AS (
SELECT id, username, email FROM users
)
SELECT u.username, b.total_spent
FROM big_orders b
JOIN user_info u ON b.user_id = u.id;
-- Recursive CTE (hierarchical data)
WITH RECURSIVE category_tree(id, name, parent_id, depth) AS (
SELECT id, name, parent_id, 0
FROM categories WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, ct.depth + 1
FROM categories c
INNER JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT * FROM category_tree ORDER BY depth, name;
Window Functions (SQLite 3.25+)
-- ROW_NUMBER, RANK, DENSE_RANK
SELECT username, age,
ROW_NUMBER() OVER (ORDER BY age DESC) AS row_num,
RANK() OVER (ORDER BY age DESC) AS rank,
DENSE_RANK() OVER (ORDER BY age DESC) AS dense_rank
FROM users;
-- Partition
SELECT username, department, salary,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank,
SUM(salary) OVER (PARTITION BY department) AS dept_total
FROM employees;
-- LAG / LEAD (access previous/next row)
SELECT date, revenue,
revenue - LAG(revenue) OVER (ORDER BY date) AS daily_change,
LEAD(revenue) OVER (ORDER BY date) AS next_day
FROM daily_sales;
-- Running total
SELECT date, amount,
SUM(amount) OVER (ORDER BY date ROWS UNBOUNDED PRECEDING) AS running_total
FROM transactions;
-- Moving average
SELECT date, value,
AVG(value) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS moving_avg_7d
FROM metrics;
Indexes & Performance
Creating Indexes
-- Basic index
CREATE INDEX idx_users_email ON users(email);
-- Unique index
CREATE UNIQUE INDEX idx_users_username ON users(username);
-- Composite index (column order matters — leftmost prefix rule)
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);
-- Partial index (only index rows matching condition)
CREATE INDEX idx_active_users ON users(email) WHERE active = 1;
-- Expression index
CREATE INDEX idx_users_lower_email ON users(lower(email));
-- Drop index
DROP INDEX IF EXISTS idx_users_email;
EXPLAIN & Query Planning
-- Show query plan (human-readable)
EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = 'alice@example.com';
-- Full EXPLAIN (VM opcodes — advanced)
EXPLAIN SELECT * FROM users WHERE email = 'alice@example.com';
Read the output:
| Output | Meaning |
|---|---|
SCAN users |
Full table scan (slow on large tables) |
SEARCH users |
Index-assisted lookup (fast) |
USING INDEX ... |
Which index is used |
USING COVERING INDEX |
All data from index, no table lookup |
Performance Tips
-- Enable WAL mode (better concurrent read performance)
PRAGMA journal_mode = WAL;
-- Set page size before creating tables (default 4096)
PRAGMA page_size = 8192;
-- Analyze tables for query optimizer
ANALYZE;
-- Check database integrity
PRAGMA integrity_check;
-- Memory-mapped I/O (faster reads, set size in bytes)
PRAGMA mmap_size = 268435456; -- 256MB
-- Batch inserts in a transaction (massive speedup)
BEGIN;
INSERT INTO logs VALUES (...);
INSERT INTO logs VALUES (...);
-- ... thousands more
COMMIT;
-- Foreign key enforcement (off by default!)
PRAGMA foreign_keys = ON;
-- Show database stats
PRAGMA page_count;
PRAGMA page_size;
-- Total size = page_count * page_size
VACUUM
-- Rebuild database file (reclaim space, defragment)
VACUUM;
-- Auto-vacuum mode (set before creating tables)
PRAGMA auto_vacuum = INCREMENTAL; -- or FULL
PRAGMA incremental_vacuum(100); -- Free up to 100 pages
JSON Support (SQLite 3.38+)
SQLite has built-in JSON functions — no extension needed since 3.38.
JSON Functions
-- Validate JSON
SELECT json_valid('{"name": "alice"}'); -- 1
-- Extract values
SELECT json_extract('{"user": {"name": "alice", "age": 30}}', '$.user.name');
-- Returns: alice
-- Shorthand -> and ->> operators (3.38+)
SELECT data -> '$.user.name' FROM records; -- Returns JSON value (quoted string)
SELECT data ->> '$.user.name' FROM records; -- Returns SQL value (unquoted)
-- Modify JSON
SELECT json_set('{"a": 1}', '$.b', 2); -- {"a":1,"b":2}
SELECT json_insert('{"a": 1}', '$.b', 2); -- {"a":1,"b":2} (won't overwrite)
SELECT json_replace('{"a": 1}', '$.a', 99); -- {"a":99}
SELECT json_remove('{"a": 1, "b": 2}', '$.a'); -- {"b":2}
-- Build JSON
SELECT json_object('name', username, 'age', age) FROM users;
SELECT json_array(1, 2, 'three'); -- [1,2,"three"]
SELECT json_group_array(username) FROM users; -- ["alice","bob","carol"]
SELECT json_group_object(username, age) FROM users;
-- Iterate over JSON arrays
SELECT value FROM json_each('[10, 20, 30]');
-- Returns rows: 10, 20, 30
-- Query JSON arrays in a column
SELECT u.username, j.value AS tag
FROM users u, json_each(u.tags) j
WHERE j.value = 'admin';
Indexing JSON
-- Index a JSON field for fast lookups
CREATE INDEX idx_data_name ON records(json_extract(data, '$.name'));
-- Use in queries
SELECT * FROM records WHERE json_extract(data, '$.name') = 'alice';
Full-Text Search (FTS5)
FTS5 enables fast text search across large text datasets.
Setup
-- Create FTS table
CREATE VIRTUAL TABLE articles_fts USING fts5(
title,
body,
content='articles', -- External content table
content_rowid='id'
);
-- Populate from existing table
INSERT INTO articles_fts(rowid, title, body)
SELECT id, title, body FROM articles;
-- Keep in sync with triggers
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;
CREATE TRIGGER articles_au AFTER UPDATE ON articles BEGIN
INSERT INTO articles_fts(articles_fts, rowid, title, body) VALUES('delete', old.id, old.title, old.body);
INSERT INTO articles_fts(rowid, title, body) VALUES (new.id, new.title, new.body);
END;
Querying
-- Simple search
SELECT * FROM articles_fts WHERE articles_fts MATCH 'database';
-- Phrase search
SELECT * FROM articles_fts WHERE articles_fts MATCH '"full text search"';
-- Boolean operators
SELECT * FROM articles_fts WHERE articles_fts MATCH 'sqlite AND performance';
SELECT * FROM articles_fts WHERE articles_fts MATCH 'sqlite OR postgres';
SELECT * FROM articles_fts WHERE articles_fts MATCH 'sqlite NOT mysql';
-- Column filter
SELECT * FROM articles_fts WHERE title MATCH 'guide';
-- Prefix search
SELECT * FROM articles_fts WHERE articles_fts MATCH 'data*';
-- Ranked results (BM25)
SELECT *, rank FROM articles_fts WHERE articles_fts MATCH 'database' ORDER BY rank;
-- Highlight matches
SELECT highlight(articles_fts, 0, '<b>', '</b>') AS title,
snippet(articles_fts, 1, '<b>', '</b>', '...', 32) AS excerpt
FROM articles_fts WHERE articles_fts MATCH 'database';
Python Integration
Python includes sqlite3 in the standard library — no install needed.
Basic Usage
import sqlite3
# Connect (creates file if not exists)
conn = sqlite3.connect('mydb.db')
# In-memory database
conn = sqlite3.connect(':memory:')
# Enable dict-like row access
conn.row_factory = sqlite3.Row
# Get cursor
cur = conn.cursor()
# Execute SQL
cur.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL
)
''')
# Parameterized queries (NEVER use f-strings or % formatting — SQL injection risk)
cur.execute('INSERT INTO users (username, email) VALUES (?, ?)', ('alice', 'alice@example.com'))
# Named parameters
cur.execute('INSERT INTO users (username, email) VALUES (:name, :email)',
{'name': 'bob', 'email': 'bob@example.com'})
# Batch insert
users = [('carol', 'carol@example.com'), ('dave', 'dave@example.com')]
cur.executemany('INSERT INTO users (username, email) VALUES (?, ?)', users)
# Commit
conn.commit()
# Query
cur.execute('SELECT * FROM users WHERE username = ?', ('alice',))
row = cur.fetchone()
print(row['username'], row['email']) # With Row factory
# Fetch all
cur.execute('SELECT * FROM users')
rows = cur.fetchall()
for row in rows:
print(row['username'])
# Close
conn.close()
Context Manager (Auto-Commit/Rollback)
import sqlite3
with sqlite3.connect('mydb.db') as conn:
conn.execute('INSERT INTO users (username, email) VALUES (?, ?)',
('eve', 'eve@example.com'))
# Auto-commits on success, auto-rollback on exception
Practical Patterns
# Export to CSV
import csv
with sqlite3.connect('mydb.db') as conn:
cur = conn.execute('SELECT * FROM users')
with open('users.csv', 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow([desc[0] for desc in cur.description])
writer.writerows(cur.fetchall())
# Import CSV
import csv
with sqlite3.connect('mydb.db') as conn:
with open('data.csv', 'r') as f:
reader = csv.DictReader(f)
for row in reader:
conn.execute('INSERT INTO data (col1, col2) VALUES (?, ?)',
(row['col1'], row['col2']))
# Pandas integration
import pandas as pd
conn = sqlite3.connect('mydb.db')
df = pd.read_sql_query('SELECT * FROM users', conn)
df.to_sql('users_backup', conn, if_exists='replace', index=False)
conn.close()
Backup & Administration
Backup Methods
# Method 1: SQL dump (portable, human-readable)
sqlite3 mydb.db .dump > backup.sql
# Restore from dump
sqlite3 newdb.db < backup.sql
# Method 2: File copy (only when no writes in progress)
# Ensure WAL mode checkpoint first:
sqlite3 mydb.db "PRAGMA wal_checkpoint(TRUNCATE);"
cp mydb.db mydb_backup.db
# Method 3: Online backup API (safe while database is in use)
sqlite3 mydb.db ".backup backup.db"
Python Backup
import sqlite3
source = sqlite3.connect('mydb.db')
backup = sqlite3.connect('backup.db')
source.backup(backup)
backup.close()
source.close()
Import/Export CSV
# Import CSV
sqlite3 mydb.db <<'EOF'
.mode csv
.import data.csv tablename
EOF
# Export to CSV
sqlite3 -header -csv mydb.db "SELECT * FROM users;" > users.csv
# Export to JSON
sqlite3 -json mydb.db "SELECT * FROM users;" > users.json
Attach Multiple Databases
ATTACH DATABASE 'other.db' AS other;
SELECT * FROM other.users;
-- Copy table between databases
INSERT INTO main.users SELECT * FROM other.users;
DETACH DATABASE other;
Database Info Queries
-- List all tables with row counts
SELECT name, (SELECT COUNT(*) FROM pragma_table_info(name)) AS columns
FROM sqlite_master WHERE type = 'table' ORDER BY name;
-- Table details
PRAGMA table_info(users);
PRAGMA table_xinfo(users); -- Includes hidden columns
-- Database size
SELECT page_count * page_size AS size_bytes FROM pragma_page_count(), pragma_page_size();
-- Foreign key check
PRAGMA foreign_key_check;
-- Compile options (what features are available)
PRAGMA compile_options;
-- SQLite version
SELECT sqlite_version();
Date & Time Functions
-- Current date/time
SELECT date('now'); -- 2026-08-12
SELECT time('now'); -- 14:30:00
SELECT datetime('now'); -- 2026-08-12 14:30:00
SELECT unixepoch(); -- 1723470600
-- Modifiers
SELECT date('now', '+7 days'); -- 7 days from now
SELECT date('now', '-1 month'); -- 1 month ago
SELECT datetime('now', '+2 hours', '+30 minutes');
SELECT date('now', 'start of month'); -- First day of current month
SELECT date('now', 'start of year', '+3 months', '-1 day'); -- End of Q1
-- Formatting
SELECT strftime('%Y-%m-%d %H:%M', 'now');
SELECT strftime('%W', 'now'); -- Week number
-- Parse Unix timestamp
SELECT datetime(1723470600, 'unixepoch');
SELECT datetime(1723470600, 'unixepoch', 'localtime');
-- Date difference (days between)
SELECT julianday('2026-12-31') - julianday('2026-01-01') AS days_in_year;
Useful PRAGMAs Reference
| PRAGMA | Purpose | Recommended Value |
|---|---|---|
journal_mode = WAL |
Write-Ahead Logging (better concurrency) | WAL for most apps |
foreign_keys = ON |
Enforce FK constraints (off by default!) | ON always |
busy_timeout = 5000 |
Wait N ms instead of failing on lock | 5000 |
synchronous = NORMAL |
Durability vs speed tradeoff | NORMAL with WAL |
cache_size = -20000 |
Page cache in KB (negative = KB) | -20000 (20MB) |
temp_store = MEMORY |
Temp tables in memory | MEMORY |
mmap_size = 268435456 |
Memory-mapped I/O (bytes) | 268435456 (256MB) |
optimize |
Run after schema/data changes | Run periodically |
Production-Ready Connection Setup
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
PRAGMA synchronous = NORMAL;
PRAGMA cache_size = -20000;
PRAGMA temp_store = MEMORY;
PRAGMA mmap_size = 268435456;
Limitations & Comparison
What SQLite Cannot Do
| Feature | SQLite | PostgreSQL / MySQL |
|---|---|---|
| Concurrent writes | Single writer at a time | Multiple concurrent writers |
RIGHT JOIN / FULL OUTER JOIN |
FULL OUTER since 3.39 |
Full support |
ALTER TABLE |
Limited (no modify column type) | Full support |
| User management | None (file permissions) | Roles, grants, row-level |
| Network access | Local file only | Client/server TCP |
| Stored procedures | None | Full support |
| Data size | Practical limit ~1TB | Multi-TB |
Decision Guide
Choose SQLite when:
- Single application accessing the data
- Dataset under 100GB
- Embedded / mobile / desktop / CLI application
- Development and testing
- Data analysis and prototyping
- File format replacement (config, logs, cache)
- Read-heavy workload
Choose PostgreSQL/MySQL when:
- Multiple applications or users need concurrent write access
- Client/server architecture required
- Complex access control needed
- Write-heavy with high concurrency
- Dataset approaching or exceeding 1TB