SQLite¶
The zero-server, zero-config SQL database that lives in a single file. SQLite is built into Python — perfect for local analysis, prototyping, caching cleaned datasets, and teaching SQL.
Table of Contents¶
- Installation / Setup
- Core Syntax
- Common Operations
- Practical Examples
- Common Patterns
- Best Practices
- Common Mistakes
- Troubleshooting
- Related Cheat Sheets
- Official Documentation
Installation / Setup¶
Zero install — the sqlite3 module ships with Python. For the CLI, install separately (apt install sqlite3, brew install sqlite, or download from sqlite.org).
import sqlite3, pandas as pd
con = sqlite3.connect("analysis.db") # file; ":memory:" = throwaway
cur = con.cursor()
Core Syntax¶
Python workflow¶
con = sqlite3.connect("data.db")
# Write
con.execute("CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY, total REAL)")
con.executemany("INSERT INTO orders (total) VALUES (?)", [(10.5,), (20.0,)])
con.commit() # writes are transactional
# Read straight into pandas — the killer feature
df = pd.read_sql_query("SELECT * FROM orders", con)
df = pd.read_sql_query("SELECT total, COUNT(*) FROM orders GROUP BY total", con)
Data types (dynamic affinity)¶
| SQLite type | Stores |
|---|---|
INTEGER |
ints (up to 8 bytes) |
REAL |
floats |
TEXT |
strings (any encoding) |
BLOB |
binary |
NUMERIC / DECIMAL |
affinity: try numeric, else text |
SQLite is dynamically typed — you can store a string in an INTEGER column, so validate inputs yourself.
Useful pragmas¶
PRAGMA table_info(orders); -- columns
PRAGMA index_list(orders); -- indexes
PRAGMA foreign_keys = ON; -- IMPORTANT: enable per connection!
PRAGMA journal_mode = WAL; -- better concurrency for file DBs
Common Operations¶
Dialect quick wins¶
-- Date/time: no native DATE type — store TEXT ISO-8601 or Unix epoch
SELECT DATE('now'), datetime('now'), strftime('%Y-%m-%d', 'now');
SELECT strftime('%Y-%m', created_at) AS ym FROM orders; -- month group key
SELECT julianday('2024-06-01') - julianday('2024-01-01'); -- 152.0 days
SELECT datetime(created_at, '+7 days') FROM orders;
-- Strings
SELECT upper(name), length(name), substr(name, 1, 3), trim(name) FROM users;
SELECT replace(email, '@x.com', '@y.com') FROM users;
-- No RIGHT/FULL JOIN, no ILIKE (use LIKE with lower()),
-- LIMIT works everywhere incl. UPDATE/DELETE
Window functions (supported since 3.25)¶
SELECT customer_id, order_date, amount,
SUM(amount) OVER (PARTITION BY customer_id ORDER BY order_date) AS running,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY amount DESC) AS rn
FROM orders;
Practical Examples¶
1. Cache a cleaned dataset to a local DB¶
import pandas as pd, sqlite3
df = pd.read_csv("raw.csv")
# ... cleaning ...
df = df.drop_duplicates().dropna(subset=["amount"])
with sqlite3.connect("clean.db") as con: # context manager commits
df.to_sql("orders", con, if_exists="replace", index=False)
# Next run: read only what you need
with sqlite3.connect("clean.db") as con:
monthly = pd.read_sql_query("""
SELECT strftime('%Y-%m', created_at) AS ym,
SUM(amount) AS revenue, COUNT(*) AS n
FROM orders GROUP BY ym ORDER BY ym""", con)
2. Quick answers without loading to pandas¶
con = sqlite3.connect("clean.db")
n = con.execute("SELECT COUNT(*) FROM orders").fetchone()[0]
p95 = con.execute("""
SELECT amount FROM orders
ORDER BY amount LIMIT 1 OFFSET (SELECT CAST(COUNT(*) * 0.95 AS INT) FROM orders)
""").fetchone()
3. Index for speed on big local files¶
CREATE INDEX idx_orders_customer ON orders(customer_id);
CREATE INDEX idx_orders_created ON orders(created_at);
Common Patterns¶
# Parameterized queries — never f-string interpolate SQL!
con.execute("SELECT * FROM users WHERE email = ?", (email,))
# Executemany for bulk inserts (fast)
con.executemany("INSERT INTO t (a, b) VALUES (?, ?)", rows)
# pandas round-trip with types preserved via dtype argument
df = pd.read_sql_query("SELECT * FROM t", con)
# In-memory DB for tests
con = sqlite3.connect(":memory:")
-- Upsert (SQLite 3.24+)
INSERT INTO users (id, name) VALUES (1, 'Ada')
ON CONFLICT(id) DO UPDATE SET name = excluded.name;
-- Find duplicates
SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1;
Best Practices¶
- Use parameterized queries (
?) — the only safe way to embed values. - Enable
PRAGMA foreign_keys = ONper connection — it's off by default! - Prefer WAL journal mode for file databases read while writing.
- Index the columns you filter/join; a full scan on a 10 GB file is painful.
- Store timestamps as ISO-8601 TEXT (
YYYY-MM-DD HH:MM:SS) — sorts and strftimes cleanly. - One connection per thread; use
check_same_thread=Falseonly when you must. - Commit in batches (
executemany+ single commit) — thousands of tiny commits are slow. - For concurrent writers or servers, move to PostgreSQL; SQLite is per-process friendly, not a network server.
Common Mistakes¶
| Mistake | Consequence | Fix |
|---|---|---|
| f-string building SQL | SQL injection / quote bugs | ? placeholders |
WHERE col = NULL |
No rows | IS NULL |
| Foreign keys silently ignored | Orphan rows | PRAGMA foreign_keys = ON |
Expecting RIGHT/FULL JOIN |
Syntax error | Restructure or use subquery |
ALTER TABLE beyond ADD COLUMN |
Unsupported | Rebuild table (create-new-copy-drop) |
Many con.commit() in a loop |
Very slow | Batch inserts, commit once |
| Reading whole table when one column needed | Memory waste | Select only needed columns |
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
database is locked |
Concurrent writers | WAL mode; retry; single writer |
no such table |
Wrong file or in-memory gone | Check path; same connection |
sqlite3.OperationalError: near "?": syntax error |
Literal ? in SQL string | Use placeholders only via execute args |
| Date column stored as float | Inserted epoch seconds as REAL | Convert with datetime(ts, 'unixepoch') |
pandas reads dates as strings |
No native date type | Parse in pandas: pd.to_datetime after read |
Related Cheat Sheets¶
- sql.md — the SQL these examples use
- PostgreSQL / MySQL — server databases for production
- pandas.md —
to_sql/read_sqlround-trips - Python Standard Library — the
sqlite3module - Home · Central Navigation Index
Official Documentation¶
Last Updated¶
2026-09-06 — Initial version.