Skip to main content
โšก Calmops

SQLite: The Complete Guide to Embedded Database Development

Introduction

SQLite is a lightweight, serverless, self-contained SQL database engine that has become the most widely deployed database in the world. Found in every mobile phone, most computers, and countless applications, SQLite powers everything from browser caches to enterprise applications.

In 2026, SQLite continues to evolve with new features including enhanced vector search capabilities, improved JSON support, and better performance for analytical workloads. This comprehensive guide will take you from SQLite fundamentals to implementing production-ready embedded database solutions.


What is SQLite?

SQLite is an open-source, embedded SQL database engine that implements a transactional, self-contained, zero-configuration database. Unlike client-server databases like PostgreSQL or MySQL, SQLite runs directly within the application, eliminating the need for separate server processes.

Key Characteristics

  • Serverless: No separate database server process; runs within the application
  • Zero-Configuration: No setup or administration required
  • Single File: Entire database stored in a single file
  • Cross-Platform: Works on Windows, Linux, macOS, iOS, Android, and more
  • ACID Compliant: Provides atomic, consistent, isolated, durable transactions
  • Public Domain: Free for any use

SQLite vs Traditional Databases

Aspect SQLite PostgreSQL/MySQL
Architecture Embedded (in-process) Client-Server
Setup Zero-configuration Requires server setup
Storage Single file Multiple files
Concurrency Limited (WAL mode helps) Full concurrent connections
Scalability Single machine Horizontal scaling
Use Case Mobile apps, edge, testing Enterprise applications

SQLite Data Types

SQLite uses a dynamic type system with five primary data types:

1. NULL

Represents a NULL value.

-- NULL values
INSERT INTO users (name, email) VALUES (NULL, '[email protected]');
SELECT * FROM users WHERE name IS NULL;

2. INTEGER

Whole numbers. SQLite automatically stores integers efficiently based on magnitude.

-- Integer storage classes
CREATE TABLE numbers (
    tiny_int TINYINT,      -- -128 to 127
    small_int SMALLINT,    -- -32768 to 32767
    integer INTEGER,       -- typical 4-byte int
    big_int BIGINT,        -- 8-byte integer
    PRIMARY KEY
);

-- Auto-increment primary key
CREATE TABLE products (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    price REAL
);

-- Insert with auto-increment
INSERT INTO products (name, price) VALUES ('Laptop', 999.99);
INSERT INTO products (name, price) VALUES ('Mouse', 29.99);

3. REAL

Floating-point numbers.

CREATE TABLE measurements (
    id INTEGER PRIMARY KEY,
    temperature REAL,
    humidity REAL,
    pressure REAL
);

INSERT INTO measurements (temperature, humidity, pressure) 
VALUES (72.5, 65.0, 1013.25);

4. TEXT

Text strings, can store any string including JSON and XML.

-- Text storage
CREATE TABLE users (
    id INTEGER PRIMARY KEY,
    username TEXT NOT NULL,
    email TEXT UNIQUE,
    bio TEXT,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP
);

-- Storing JSON in TEXT
CREATE TABLE configs (
    id INTEGER PRIMARY KEY,
    settings TEXT
);

INSERT INTO configs (settings) 
VALUES ('{"theme": "dark", "notifications": true, "timeout": 300}');

5. BLOB

Binary large objects for storing binary data.

-- BLOB storage for images, files
CREATE TABLE documents (
    id INTEGER PRIMARY KEY,
    filename TEXT,
    content BLOB,
    mime_type TEXT
);

-- Insert binary data
INSERT INTO documents (filename, content, mime_type) 
VALUES ('avatar.png', X'89504E470D0A1A0A', 'image/png');

Type Affinity

SQLite uses type affinity to convert values when needed:

-- SQLite will convert strings to numbers when comparing
CREATE TABLE numbers (value TEXT);
INSERT INTO numbers VALUES ('100'), ('50'), ('abc');

-- String comparison (lexicographical)
SELECT * FROM numbers ORDER BY value;  -- '100', '50', 'abc'

-- Numeric comparison (requires CAST)
SELECT * FROM numbers ORDER BY CAST(value AS INTEGER);  -- 'abc', '50', '100'

SQLite Installation

Command Line Interface

# macOS (pre-installed)
which sqlite3

# Ubuntu/Debian
sudo apt-get update
sudo apt-get install sqlite3

# Windows (using Chocolatey)
choco install sqlite

# Verify installation
sqlite3 --version

# Create a new database
sqlite3 myapp.db

# Open existing database
sqlite3 myapp.db ".tables"

# Show database info
sqlite3 myapp.db ".schema"

Python Integration

import sqlite3

# Create connection (creates file if not exists)
conn = sqlite3.connect('myapp.db')
cursor = conn.cursor()

# Create table
cursor.execute('''
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        username TEXT NOT NULL UNIQUE,
        email TEXT NOT NULL,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )
''')

# Insert data
cursor.execute(
    'INSERT INTO users (username, email) VALUES (?, ?)',
    ('john_doe', '[email protected]')
)

# Query data
cursor.execute('SELECT * FROM users')
for row cursor.fetchall():
    print(row)

# Commit and close
conn.commit()
conn.close()

Node.js Integration

const sqlite3 = require('sqlite3').verbose();

// Open database
const db = new sqlite3.Database('./myapp.db');

// Create table
db.run(`
    CREATE TABLE IF NOT EXISTS products (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        price REAL NOT NULL,
        stock INTEGER DEFAULT 0
    )
`);

// Insert data with prepared statement
const stmt = db.prepare('INSERT INTO products (name, price, stock) VALUES (?, ?, ?)');
stmt.run('Laptop', 999.99, 10);
stmt.run('Mouse', 29.99, 50);
stmt.finalize();

// Query data
db.all('SELECT * FROM products', [], (err, rows) => {
    console.log(rows);
});

db.close();

Using SQLite in Go

package main

import (
    "database/sql"
    "fmt"
    _ "github.com/mattn/go-sqlite3"
)

func main() {
    db, err := sql.Open("sqlite3", "./myapp.db")
    if err != nil {
        panic(err)
    }
    defer db.Close()

    // Create table
    db.Exec(`
        CREATE TABLE IF NOT EXISTS tasks (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            completed BOOLEAN DEFAULT 0
        )
    `)

    // Insert
    result, _ := db.Exec("INSERT INTO tasks (title) VALUES (?)", "Learn SQLite")
    id, _ := result.LastInsertId()
    fmt.Printf("Inserted task with ID: %d\n", id)

    // Query
    rows, _ := db.Query("SELECT id, title, completed FROM tasks")
    defer rows.Close()

    for rows.Next() {
        var id int
        var title string
        var completed bool
        rows.Scan(&id, &title, &completed)
        fmt.Printf("Task %d: %s (Completed: %v)\n", id, title, completed)
    }
}

Basic SQL Operations

Data Definition Language (DDL)

-- Create table with constraints
CREATE TABLE employees (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT UNIQUE,
    department TEXT DEFAULT 'General',
    salary REAL CHECK(salary > 0),
    hire_date TEXT DEFAULT CURRENT_DATE
);

-- Create table with foreign key
CREATE TABLE departments (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL
);

CREATE TABLE employees (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    department_id INTEGER,
    FOREIGN KEY(department_id) REFERENCES departments(id)
);

-- Add column
ALTER TABLE employees ADD COLUMN phone TEXT;

-- Rename table
ALTER TABLE employees RENAME TO staff;

-- Drop table
DROP TABLE IF EXISTS old_table;

Data Manipulation Language (DML)

-- Insert single row
INSERT INTO employees (name, email, salary) 
VALUES ('John Doe', '[email protected]', 75000);

-- Insert multiple rows
INSERT INTO employees (name, email, salary) VALUES 
    ('Jane Smith', '[email protected]', 80000),
    ('Bob Wilson', '[email protected]', 65000),
    ('Alice Brown', '[email protected]', 70000);

-- Update rows
UPDATE employees 
SET salary = salary * 1.1 
WHERE department = 'Engineering';

-- Delete rows
DELETE FROM employees WHERE salary < 50000;

-- Upsert (INSERT OR REPLACE)
INSERT INTO employees (id, name, email, salary) 
VALUES (1, 'John Updated', '[email protected]', 80000)
ON CONFLICT(id) DO UPDATE SET 
    name = excluded.name,
    email = excluded.email,
    salary = excluded.salary;

Querying Data

-- Basic SELECT
SELECT * FROM employees;
SELECT name, salary FROM employees;

-- WHERE clause
SELECT * FROM employees WHERE salary > 70000;
SELECT * FROM employees WHERE department = 'Engineering' AND salary > 60000;

-- ORDER BY
SELECT * FROM employees ORDER BY salary DESC;
SELECT * FROM employees ORDER BY name ASC, salary DESC;

-- LIMIT and OFFSET
SELECT * FROM employees ORDER BY salary DESC LIMIT 10 OFFSET 20;

-- GROUP BY with aggregation
SELECT department, AVG(salary) as avg_salary, COUNT(*) as count
FROM employees
GROUP BY department
HAVING COUNT(*) > 1;

-- DISTINCT
SELECT DISTINCT department FROM employees;

-- LIKE pattern matching
SELECT * FROM employees WHERE name LIKE 'J%';        -- Starts with J
SELECT * FROM employees WHERE email LIKE '%@gmail.com';

-- IN and BETWEEN
SELECT * FROM employees WHERE department IN ('Engineering', 'Sales');
SELECT * FROM employees WHERE salary BETWEEN 50000 AND 100000;

JOIN Operations

-- Create tables for examples
CREATE TABLE orders (
    id INTEGER PRIMARY KEY,
    customer_id INTEGER,
    product_id INTEGER,
    quantity INTEGER,
    total_price REAL
);

CREATE TABLE customers (
    id INTEGER PRIMARY KEY,
    name TEXT,
    email TEXT
);

-- INNER JOIN
SELECT o.id, c.name, o.total_price
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id;

-- LEFT JOIN
SELECT c.name, o.id as order_id
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id;

-- Multiple joins
SELECT o.id, c.name, p.name as product, o.quantity
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN products p ON o.product_id = p.id;

SQLite Special Features

AUTOINCREMENT vs ROWID

-- ROWID (default hidden primary key)
CREATE TABLE users (
    name TEXT
);
-- Automatically gets hidden rowid column (0, 1, 2, ...)

-- INTEGER PRIMARY KEY (uses rowid)
CREATE TABLE posts (
    id INTEGER PRIMARY KEY
);
-- id values: 1, 2, 3, ...

-- INTEGER PRIMARY KEY AUTOINCREMENT
CREATE TABLE articles (
    id INTEGER PRIMARY KEY AUTOINCREMENT
);
-- id values: 1, 2, 3, ... (never reused after deletion)

Handling JSON

-- SQLite JSON support (since 3.9.0)
CREATE TABLE configs (
    id INTEGER PRIMARY KEY,
    data TEXT
);

INSERT INTO configs (data) VALUES (
    '{"theme": "dark", "settings": {"lang": "en", "notifications": true}}'
);

-- Extract JSON values
SELECT json_extract(data, '$.theme') FROM configs;
SELECT json_extract(data, '$.settings.lang') FROM configs;

-- Modify JSON
UPDATE configs SET data = json_set(data, '$.theme', 'light');

-- Create virtual table for JSON
CREATE VIRTUAL TABLE config_view USING json_tree(configs.data);
SELECT key, value FROM config_view;

Common Table Expressions (CTE)

-- Simple CTE
WITH active_users AS (
    SELECT * FROM users WHERE status = 'active'
)
SELECT * FROM active_users WHERE created_at > '2025-01-01';

-- Recursive CTE (hierarchy)
WITH RECURSIVE org_chart AS (
    -- Base case: top-level managers
    SELECT id, name, manager_id, 1 as level
    FROM employees WHERE manager_id IS NULL
    
    UNION ALL
    
    -- Recursive case: employees with managers
    SELECT e.id, e.name, e.manager_id, oc.level + 1
    FROM employees e
    JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT * FROM org_chart;

Window Functions

-- Running total
SELECT 
    date,
    amount,
    SUM(amount) OVER (ORDER BY date) as running_total
FROM transactions;

-- Rank within groups
SELECT 
    name,
    department,
    salary,
    RANK() OVER (PARTITION BY department ORDER BY salary DESC) as dept_rank
FROM employees;

-- Moving average
SELECT 
    date,
    price,
    AVG(price) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) as moving_avg_7
FROM stock_prices;

Indexing and Query Optimization

Creating Indexes

-- Single column index
CREATE INDEX idx_employees_department ON employees(department);

-- Composite index
CREATE INDEX idx_employees_dept_salary ON employees(department, salary DESC);

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

-- Index on expression
CREATE INDEX idx_users_lower_email ON users(LOWER(email));

-- Partial index (index only specific rows)
CREATE INDEX idx_active_users ON users(email) WHERE status = 'active';

Query Execution Analysis

-- Explain query plan
EXPLAIN QUERY PLAN SELECT * FROM employees WHERE department = 'Sales';
EXPLAIN QUERY PLAN SELECT * FROM employees WHERE salary > 50000 ORDER BY name;

-- Enable query statistics
PRAGMA query_only = OFF;
PRAGMA cache_size = 2000;

-- Check index usage
EXPLAIN QUERY PLAN SELECT * FROM employees WHERE rowid = 5;

Best Practices

-- Use EXPLAIN QUERY PLAN to understand query execution
EXPLAIN QUERY PLAN 
SELECT e.name, d.name 
FROM employees e 
JOIN departments d ON e.department_id = d.id
WHERE e.salary > 70000;

-- Create covering index for frequently queried columns
CREATE INDEX idx_emp_dept_salary_name ON employees(department, salary, name);

-- Avoid functions on indexed columns in WHERE clause
-- Bad: SELECT * FROM users WHERE LOWER(email) = '[email protected]'
-- Good: SELECT * FROM users WHERE email = LOWER('[email protected]')

Transactions and Concurrency

Transaction Control

-- Default autocommit mode (each statement is a transaction)
INSERT INTO table1 VALUES (1);
INSERT INTO table2 VALUES (2);

-- Explicit transaction
BEGIN TRANSACTION;
INSERT INTO accounts (id, balance) VALUES (1, 1000);
INSERT INTO accounts (id, balance) VALUES (2, 500);
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

-- Rollback on error
BEGIN TRANSACTION;
INSERT INTO orders (product_id, quantity) VALUES (1, 10);
-- If error occurs:
-- ROLLBACK;
COMMIT;

-- Savepoints
BEGIN TRANSACTION;
INSERT INTO logs (message) VALUES ('Starting operation');
SAVEPOINT sp1;
INSERT INTO logs (message) VALUES ('Middle step');
ROLLBACK TO SAVEPOINT sp1;
INSERT INTO logs (message) VALUES ('Recovered');
COMMIT;

Isolation Levels

-- Check current isolation level
PRAGMA journal_mode;

-- WAL mode (better concurrency)
PRAGMA journal_mode = WAL;

-- READ UNCOMMITTED (dirty reads possible)
PRAGMA read_uncommitted = 1;

-- Default (READ COMMITTED)
-- In SQLite, SERIALIZABLE is the default

Best Practices

Database Design

-- Use INTEGER PRIMARY KEY for auto-increment
CREATE TABLE items (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL
);

-- Add NOT NULL constraints
CREATE TABLE users (
    id INTEGER PRIMARY KEY,
    username TEXT NOT NULL,
    email TEXT NOT NULL
);

-- Use CHECK constraints
CREATE TABLE products (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    price REAL NOT NULL CHECK(price > 0),
    quantity INTEGER NOT NULL CHECK(quantity >= 0)
);

-- Normalize but don't over-normalize
-- SQLite performs better with some denormalization for read-heavy workloads

Performance Tips

-- Use transactions for bulk operations
BEGIN TRANSACTION;
-- Insert 1000 rows
COMMIT;

-- Use parameterized queries
-- In Python:
cursor.execute('INSERT INTO users (name) VALUES (?)', (name,))

-- Create indexes after table creation (if needed)
CREATE INDEX idx_table_column ON table(column);

-- Vacuum to reclaim space
VACUUM;

-- Analyze to update statistics
ANALYZE;

Common Pitfalls

Pitfall 1: Integer Overflow

SQLite INTEGER PRIMARY KEY has a limit of 9223372036854775807. For truly unique IDs, use AUTOINCREMENT or UUIDs.

-- Instead of relying solely on INTEGER PRIMARY KEY for massive scale
CREATE TABLE records (
    uuid TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
    data TEXT
);

Pitfall 2: Database Locking

WAL mode helps, but SQLite still has write locking limitations.

-- Enable WAL mode for better concurrency
PRAGMA journal_mode = WAL;

-- Set busy timeout
PRAGMA busy_timeout = 5000;

Pitfall 3: Case Sensitivity

-- LIKE is case-insensitive by default in SQLite
SELECT * FROM users WHERE name LIKE 'john%';  -- matches John, JOHN

-- For case-sensitive search, use GLOB
SELECT * FROM users WHERE name GLOB 'John*';

-- Create case-sensitive index
CREATE INDEX idx_name ON users(name COLLATE BINARY);

Resources


Conclusion

SQLite’s simplicity, reliability, and zero-configuration design make it an excellent choice for embedded applications, mobile apps, testing environments, and small-to-medium data storage needs. Its ACID compliance ensures data integrity while its single-file format provides easy portability.

In the next article of this series, we’ll explore SQLite operations including backup strategies, performance tuning, and deployment considerations for production environments.

Comments