Every example below uses one small shop database, , with three tables: , and .
Opening and Exiting sqlite3
opens a file and creates it if it does not exist. Build a connection string for your app with the SQLite connection string generator.
| Command | What it does |
|---|---|
| Open a database file, creating it if missing | |
| Start an in-memory database, gone when you exit | |
| Open without write access | |
| Read-only through a URI filename | |
| Run one statement and exit | |
| Run a SQL file and exit | |
| Stop at the first error instead of carrying on | |
| Run a dot command before the session starts | |
| Read a startup file instead of | |
| Print the SQLite version | |
| Close the current database and open another | |
| Reopen read-only from inside the shell | |
| List every dot command | |
| Full help for one command | |
| Exit the shell ( and Ctrl-D work too) | |
| Exit with a return code, useful in scripts |
In 1bench: open a SQLite file on a remote server over SSH, either live in place or as a local snapshot you sync back. See the SQLite GUI.
Example: run a migration script that stops on the first error and returns a non-zero exit code for CI.
sqlite3 -bail shop.db < migrations/004_add_orders_index.sql
echo "exit code: $?" # 1 if any statement failed
Dot Commands
Dot commands start with a period, take no semicolon and only work in the shell.
| Command | What it does |
|---|---|
| Show tables and views | |
| Show tables matching a LIKE pattern | |
| Print the CREATE statement for a table | |
| Print the CREATE statement for everything | |
| Schema plus the sqlite_stat tables, indented | |
| Describe a table: columns, types, NOT NULL, defaults, key | |
| Same column list as a query you can filter | |
| Every table, index, view and trigger | |
| List indexes you created | |
| Include the automatic UNIQUE and PRIMARY KEY indexes | |
| Attached databases and their file paths | |
| Print column names above results | |
| Show NULL instead of an empty string | |
| Print run time after every statement | |
| Print the number of rows each statement changed | |
| Current settings: mode, headers, separators, output | |
| Bind a value for in later queries | |
| Run a shell command without leaving sqlite3 | |
| Page size, encoding, schema version and other header info |
Example: find the tables in an unfamiliar database file and describe the one you need.
sqlite3 shop.db '.tables'
sqlite3 shop.db '.schema orders'
sqlite3 -box shop.db "SELECT name, type, \"notnull\", pk FROM pragma_table_info('orders');"
Output Modes
Interactive sessions print boxes by default since 3.53. Scripts and pipes get the plain format, so set a mode when the output feeds another tool.
| Command | What it does |
|---|---|
| Show the current mode | |
| List every available mode | |
| Unicode box table with headers | |
| ASCII table, safe to paste anywhere | |
| Aligned columns, no borders | |
| Markdown table | |
| Comma-separated values, quoted where needed | |
| Tab-separated values | |
| One JSON array of row objects | |
| One line per field, good for wide rows | |
| Rows as INSERT statements for another table | |
| Pipe-separated, the batch default | |
| Set column widths for column, table and box modes | |
| Send all further output to a file | |
| Send output back to the terminal | |
| Send only the next result to a file | |
| Open the next result in your spreadsheet app | |
| JSON output from the command line | |
| Box output from the command line |
Example: write the paid orders to a JSON file from a script.
sqlite3 shop.db <<'SQL'
.mode json
.once paid_orders.json
SELECT id, user_id, total, created_at FROM orders WHERE status = 'paid';
SQL
Tables
already auto-assigns ids. Add only when deleted ids must never come back.
| Command | What it does |
|---|---|
| Create a table only if missing | |
| Ids that are never reused after a delete | |
| Reject values that do not match the column type | |
| Copy a query result into a new table | |
| Add a column; NOT NULL needs a default | |
| Rename a column, indexes and views follow | |
| Rename a table | |
| Remove a column | |
| Add NOT NULL to an existing column (3.53+) | |
| Remove NOT NULL (3.53+) | |
| Add a CHECK constraint (3.53+) | |
| Remove a named constraint (3.53+) | |
| DestructiveEmpty a table; SQLite has no TRUNCATE | |
| DestructiveDelete a table and its data |
Example: the and tables every example on this page uses.
CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status TEXT NOT NULL DEFAULT 'pending',
total REAL NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Writing Data
Wrap bulk writes in and . Each statement outside a transaction is its own commit and syncs to disk.
| Command | What it does |
|---|---|
| Insert one row | |
| Insert several rows in one statement | |
| Return the new id and defaults | |
| Insert the result of a query | |
| Upsert: update the row that already exists | |
| Skip rows that would break a constraint | |
| Older spelling of DO NOTHING | |
| Delete the clashing row, then insert (see Gotchas) | |
| Update and return the changed rows | |
| Update from another table | |
| DestructiveDelete rows and list what went | |
| Rows changed by the last statement | |
| Rowid of the last INSERT on this connection | |
| Group writes into one transaction | |
| Undo everything since BEGIN |
Example: upsert a user by email, keep the original id and created_at, and return the row.
INSERT INTO users (email, name)
VALUES ('[email protected]', 'Ada Lovelace')
ON CONFLICT(email) DO UPDATE SET name = excluded.name
RETURNING id, email, name, created_at;
Select and Filtering
SQLite has no TOP. Use , with for paging.
| Command | What it does |
|---|---|
| Match any value in a list | |
| Newest first, third page of 10 | |
| Pattern match, case-insensitive for ASCII | |
| Unix-style pattern, case-sensitive | |
| Inclusive range | |
| Find NULLs; never matches | |
| Case-insensitive equality | |
| Unique values | |
| Count and total per group | |
| Filter groups after aggregating | |
| First value that is not NULL | |
| Conditional value | |
| Short form of a two-way CASE | |
| Join values into one string per group | |
| Join strings with a separator, skips NULLs | |
| Text before the @ | |
| Number rows within each group | |
| Running total per user | |
| Name a subquery with a CTE | |
| Pick a random row |
Example: each user's most recent order, using a window function.
SELECT id, user_id, status, total, created_at
FROM (
SELECT *, row_number() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
)
WHERE rn = 1;
Joins
SQLite supports INNER, LEFT, RIGHT, FULL OUTER and CROSS joins. RIGHT and FULL need 3.39 or later.
| Command | What it does |
|---|---|
| Inner join, only users with orders | |
| Every user, NULLs where there is no order | |
| Users with no orders at all | |
| Every order, NULLs where the user is missing | |
| Every row from both sides | |
| Every combination of rows | |
| Self join, pairs of orders by the same user | |
| Semi-join: users with at least one order | |
| Combine two results, duplicates removed |
Example: order count and lifetime spend per user, including users who never ordered.
SELECT u.email,
count(o.id) AS orders,
round(coalesce(sum(o.total), 0), 2) AS spend
FROM users u
LEFT JOIN orders o ON o.user_id = u.id AND o.status = 'paid'
GROUP BY u.id
ORDER BY spend DESC;
Import, Export and Backup
creates the table from the CSV header when the table does not exist yet.
| Command | What it does |
|---|---|
| Import a CSV into a new table, header row becomes columns | |
| Import into an existing table, skip the header | |
| then | Import tab-separated data into a new table |
| Export a query to CSV in one line | |
| Export the next query to CSV from the shell | |
| Run a SQL file inside the session | |
| Whole database as SQL text | |
| One table as SQL text | |
| Dump to a file for version control or migration | |
| Rebuild a database from a dump | |
| Schema only, no data | |
| Copy the open database safely, even while in use | |
| DestructiveReplace the open database with a backup | |
| Write a compacted copy to a new file | |
| Rebuild the file and reclaim free pages; locks it while running | |
| Open a second database file in the same session | |
| Copy rows between attached databases | |
| Close an attached database | |
| Pull what it can out of a corrupt file as SQL | |
| Print the SQL that turns one database into the other |
In 1bench: export a SQLite table to SQL, CSV, JSON or NDJSON, and import those formats back with an option to empty the table first. See the SQLite GUI.
Example: load a CSV export into a staging table, then upsert it into in one transaction.
sqlite3 -bail shop.db <<'SQL'
CREATE TEMP TABLE staging (email TEXT, name TEXT);
.import --csv --skip 1 new_users.csv staging
BEGIN;
INSERT INTO users (email, name) SELECT email, name FROM staging WHERE true
ON CONFLICT(email) DO UPDATE SET name = excluded.name;
COMMIT;
SQL
PRAGMAs
Most PRAGMAs apply to the current connection only. and are stored in the file.
| Command | What it does |
|---|---|
| Enforce foreign keys; off by default, per connection | |
| Readers stop blocking the writer; persists in the file | |
| Wait up to 5 seconds on a lock instead of failing | |
| Fewer fsyncs, safe with WAL | |
| Full corruption check, returns ok when healthy | |
| Faster check that skips index contents | |
| List rows that break a foreign key | |
| Foreign keys defined on a table | |
| Indexes on a table | |
| Store a schema version number for migrations | |
| File size is page_size times page_count | |
| Free pages a VACUUM would reclaim | |
| Fold the WAL into the main file and shrink it | |
| Refresh planner stats where needed; run before closing | |
| Features this build was compiled with |
In 1bench: the SQLite GUI overview shows the file size with page count and page size, table and index counts, encoding, journal mode and SQLite version, without running these PRAGMAs by hand.
Example: the settings most apps run on every new connection.
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
PRAGMA synchronous = NORMAL;
Dates and Times
SQLite has no date type. Store ISO 8601 text or Unix seconds, and the functions below read both. is UTC.
| Command | What it does |
|---|---|
| Current UTC time, | |
| Current local time | |
| Today's date | |
| UTC timestamp, handy as a column default | |
| Current Unix time in seconds | |
| Text date to Unix seconds | |
| Unix seconds to a timestamp | |
| Unix milliseconds to a timestamp with fractions | |
| Year from a date, as text | |
| Year and month, for grouping | |
| Seven days ago | |
| First day of this month | |
| Last day of this month (3.54+) | |
| Next Monday, or today if it is Monday | |
| Days between two dates | |
| Difference as years, months, days and time | |
| Rows from the last 30 days |
Example: revenue per month from the table.
SELECT strftime('%Y-%m', created_at) AS month,
count(*) AS orders,
round(sum(total), 2) AS revenue
FROM orders
WHERE status = 'paid'
GROUP BY month
ORDER BY month;
JSON
JSON is stored in TEXT columns and the functions are built in. returns a SQL value, returns JSON. Check a payload with the JSON formatter.
| Command | What it does |
|---|---|
| Read one field as a SQL value | |
| Read a nested object as JSON | |
| Read a nested field | |
| First array element | |
| Last array element | |
| Function form of | |
| One row per array element | |
| One row per node, walks the whole document | |
| Number of elements in an array | |
| Build an object from columns | |
| Aggregate rows into a JSON array | |
| Set a value, creating the path if needed | |
| Append to an array | |
| Remove a key | |
| 1 if the text is valid JSON | |
| Indented JSON for reading | |
| Binary JSONB form, faster to query, stored as BLOB |
Example: index a JSON field and count events by type, so the filter uses the index.
CREATE INDEX idx_events_type ON events (payload ->> '$.type');
SELECT payload ->> '$.type' AS type, count(*) AS n
FROM events
WHERE payload ->> '$.type' IN ('login', 'purchase')
GROUP BY type;
Indexes and Query Plan
Check the plan before adding an index. SCAN reads every row, SEARCH uses an index.
| Command | What it does |
|---|---|
| Composite index, leftmost column first | |
| Unique index on an expression | |
| Partial index over a subset of rows | |
| Remove an index | |
| Show SCAN or SEARCH for each table | |
| Print the plan before every query | |
| Suggest indexes for the next query | |
| Gather stats so the planner picks better indexes | |
| Stats that ANALYZE stored | |
| Rebuild an index | |
| Space used per table and index (replaces sqlite3_analyzer) |
Example: confirm a lookup goes from a full scan to an index search.
EXPLAIN QUERY PLAN SELECT * FROM orders WHERE user_id = 1 AND status = 'paid';
-- `--SCAN orders
CREATE INDEX idx_orders_user_status ON orders (user_id, status);
EXPLAIN QUERY PLAN SELECT * FROM orders WHERE user_id = 1 AND status = 'paid';
-- `--SEARCH orders USING INDEX idx_orders_user_status (user_id=? AND status=?)
Turso and libSQL
Turso hosts libSQL, a fork of SQLite, so the SQL above runs unchanged. The CLI manages databases and opens a shell with the common dot commands.
| Command | What it does |
|---|---|
| Install the Turso CLI on macOS | |
| Install script for macOS and Linux | |
| Log in through the browser | |
| Log in on a machine with no browser | |
| Print your platform API token | |
| Create a group to hold databases | |
| Create an empty database | |
| Create a database from a local SQLite file | |
| Upload a SQLite file; it must be in WAL mode | |
| Print the URL for your app | |
| Create an auth token for one database | |
| Read-only token that expires in 7 days | |
| Open a SQL shell on the closest replica | |
| Run one statement and exit | |
| Download a snapshot to a local file | |
| DestructiveDelete a database and its replicas | |
| Local libSQL server on port 8080, no account needed |
Example: move a local SQLite file to Turso and get the URL and token for your app.
sqlite3 shop.db 'PRAGMA journal_mode = WAL;'
turso db import shop.db
turso db show shop --url # libsql://... URL for the client
turso db tokens create shop # pass as the auth token
Gotchas
- Foreign keys are off by default and the setting does not persist. Run on every connection, or and REFERENCES checks silently do nothing.
- deletes the clashing row and inserts a new one. Columns you leave out reset to their defaults, and with foreign keys on, removes the child rows. In testing, replacing a user wiped all of their orders. Use instead.
- Column types are suggestions unless the table is STRICT. An INTEGER column happily stores as text. Store money as integer cents too, since REAL sums give results like 62.489999999999995.
- into a table that already exists treats the header line as data. Add .
- cannot add a UNIQUE or PRIMARY KEY column ("Cannot add a UNIQUE column"). Add the plain column, then on it.
- "database is locked" means another connection holds the write lock. Turn on WAL, set and keep write transactions short.