SQLite’s connection string can be a lot more than just a file path; it’s a powerful configurator that lets you tune its behavior at runtime.

Let’s see SQLite in action with a URI connection string and a PRAGMA.

import sqlite3

# Using a URI connection string to specify a read-only database and a WAL mode
conn = sqlite3.connect("file:my_database.db?mode=ro&nolock=1", uri=True)
cursor = conn.cursor()

# Using a PRAGMA to enable foreign key constraints
cursor.execute("PRAGMA foreign_keys = ON;")

# Example usage: creating a table and inserting data
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL
);
""")

cursor.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
conn.commit()

# Fetching data
cursor.execute("SELECT * FROM users")
print(cursor.fetchall())

conn.close()

This file:my_database.db?mode=ro&nolock=1 URI tells SQLite to open my_database.db in read-only mode (mode=ro) and to disable the busy timeout (nolock=1). The PRAGMA foreign_keys = ON; command then ensures that foreign key constraints are enforced, which is crucial for data integrity in relational databases.

The core problem SQLite solves is providing a lightweight, serverless relational database engine that can be embedded directly into applications. It achieves this by storing the entire database in a single file on disk. This simplicity makes it incredibly versatile, suitable for everything from mobile apps and desktop software to web application backends and data analysis scripts.

Internally, SQLite manages concurrency and data integrity through a combination of locking mechanisms and transaction models. The default behavior for concurrent access can sometimes lead to OperationalError: database is locked if not configured carefully. The ?mode=ro in the URI bypasses many of these locking concerns by simply disallowing writes. The ?nolock=1 parameter is a bit of a misnomer; it doesn’t actually disable locking, but rather disables the busy timeout, meaning SQLite will return an error immediately if it can’t access the database, rather than waiting. For write operations, you’d typically want the opposite: a timeout so your application doesn’t fail immediately if another process has a lock.

The PRAGMA statements are SQLite’s way of exposing internal configuration options and controlling its runtime behavior. They are SQL statements that don’t operate on data but on the database engine itself. You can use them to inspect various aspects of the database, like PRAGMA cache_size; to see how many pages are kept in memory, or PRAGMA journal_mode; to understand how transactions are logged. Many PRAGMAs can also be set at connection time, either via the URI or by executing the PRAGMA command immediately after establishing the connection.

For instance, PRAGMA synchronous = NORMAL; is a common setting for performance. By default, SQLite uses FULL or EXTRA synchronous, which means it waits for the operating system to confirm that data has been physically written to disk before a transaction is committed. This guarantees durability but can be slow. Setting synchronous to NORMAL tells SQLite to rely on the OS to flush data to disk eventually, but not necessarily wait for confirmation on every commit, leading to faster writes at the cost of a slightly higher risk of data loss if the system crashes before the OS flushes the buffer.

The ?immutable=1 URI option is a fascinating one. When set, it tells SQLite that the database file will never be changed. This allows SQLite to perform certain optimizations, such as disabling journaling and disabling the need for exclusive write access. However, if you attempt to write to an immutable database, you’ll get an error. It’s a declaration of intent that the system can leverage for performance gains.

Beyond mode=ro, nolock=1, and immutable=1, the URI can also specify the database file location relative to the application’s working directory, or even use special values like :memory: for an in-memory database. The uri=True parameter in sqlite3.connect is crucial for enabling the parsing of these URI-style connection strings. Without it, SQLite would treat the entire string as a literal filename.

When you use PRAGMA directives like journal_mode=WAL, you’re fundamentally changing how SQLite manages concurrent reads and writes. Write-Ahead Logging (WAL) is a popular choice because it allows readers to see a consistent snapshot of the database even while writers are active, significantly improving concurrency compared to the default rollback journal. To enable WAL, you’d typically use PRAGMA journal_mode = WAL; and then PRAGMA wal_autocheckpoint = 1000; (or some other value) to manage the checkpointing interval, which determines how often the WAL file is merged back into the main database file.

The interplay between URI options and PRAGMA commands is where SQLite’s flexibility truly shines. You can combine them to fine-tune performance, concurrency, and durability for specific application needs. For example, you might connect to a read-only database using file:data.db?mode=ro&uri=True and then set PRAGMA cache_size = -2000; to instruct SQLite to cache up to 2000 database pages in memory for faster reads.

The next step in exploring SQLite’s capabilities involves understanding how to manage database schema evolution and migrations.

Want structured learning?

Take the full Sqlite course →