Skip to content

SQLite 速查表

自包含、无服务器、零配置的 SQL 数据库引擎。

01

入门

CLI 基础

SQLite 将整个数据库存储在单个文件中。sqlite3 CLI 按需打开或创建 .sqlite/.db 文件。与 MySQL/PostgreSQL 不同,没有服务器进程——应用程序直接链接 SQLite 库。.dump 以文本 SQL 形式导出整个数据库,用于备份或迁移。

sqlite
# open/create a database file
sqlite3 mydb.sqlite

# at the sqlite> prompt
.help              # show help
.tables            # list tables
.schema users      # show CREATE statement for users
.databases         # list attached databases
.dump              # dump entire DB as SQL text
.quit              # exit

# execute SQL from a file
sqlite3 mydb.sqlite < script.sql

# execute a query from command line
sqlite3 mydb.sqlite "SELECT COUNT(*) FROM users;"

点命令

点命令由 sqlite3 CLI 解释,而非 SQL 引擎——它们不需要分号,且不能通过编程 API 使用。.mode 控制输出格式(交互使用时配合 .header on 的 'column' 模式最易读)。.schema 显示 CREATE 语句;.fullschema 额外包含索引、视图和触发器。用 .help 发现更多命令。

sqlite
-- dot commands are CLI-only, NOT SQL (no semicolon needed)
.help                 -- list all dot commands
.tables               -- list all tables
.schema               -- schema for all tables
.schema users         -- schema for the users table
.fullschema           -- schema incl. indexes, triggers, views
.indices users        -- list indexes on users
.header on            -- show column headers
.mode column          -- column-aligned output
.mode list            -- default, pipe-separated
.mode csv             -- CSV output
.mode json            -- JSON output
.show                 -- show current settings
.width 10 20 30       -- set column widths
.timer on             -- show query execution time

数据库文件管理

ATTACH DATABASE 允许一个连接同时查询最多 10 个数据库文件(作为模式 main、temp 加附加的)。:memory: URI 创建纯内存数据库,关闭后消失——非常适合测试或临时计算。URI 文件名 (file:...) 启用只读、共享缓存等模式。每个附加的 DB 都是磁盘上的独立文件。

sqlite
-- attach another database file to the current connection
ATTACH DATABASE 'archive.sqlite' AS archive;
DETACH DATABASE archive;

-- list attached databases (main + attached)
.databases

-- the in-memory database (never written to disk)
sqlite3 :memory:

-- temporary database (deleted when connection closes)
sqlite3 ""          -- empty arg => temp DB

-- open read-only
sqlite3 "file:mydb.sqlite?mode=ro" --readonly

-- query the underlying page size / file format
PRAGMA page_size;
PRAGMA journal_mode;

表头与输出格式

输出格式仅影响 CLI,不影响数据本身。配合 .headers on 的 'column' 模式对人类最易读。'list'(默认)最适合 shell 管道。'json' 和 'csv' 让 SQLite 成为方便的转换器。.output 将结果重定向到文件;记得用 .output stdout 切回。.mode box/table 绘制 ASCII 边框。

sqlite
-- readable interactive mode
.headers on
.mode column
.width 15 20 10
SELECT id, name, email FROM users LIMIT 5;

-- pipe-separated (good for scripts)
.mode list
.separator "|"
SELECT id, name FROM users;

-- boxed table output (sqlite 3.36+)
.mode box
.mode table

-- JSON output for piping to other tools
.mode json
SELECT id, name FROM users;

-- write output to a file
.output users.txt
SELECT id, name FROM users;
.output stdout

执行脚本与 SQL 文件

.read 在打开的 CLI 会话中执行 SQL 文件;用 < 重定向则会在交互提示符之前运行。--bail 在第一个错误时停止批处理(否则 SQLite 会继续执行,可能掩盖失败)。.parameter (3.44+) 从 shell 绑定命名参数,避免字符串拼接。开发时用 .echo on 检查脚本。

sqlite
-- run a .sql file from the shell
sqlite3 mydb.sqlite ".read setup.sql"
-- or piped in
sqlite3 mydb.sqlite < setup.sql

-- inside the CLI
.read setup.sql

-- stop on first error (shell flag)
sqlite3 mydb.sqlite --bail < script.sql

-- echo commands as they run
.echo on
.read migrations.sql

-- show EXPLAIN before each statement (debug)
.explain on

-- parameterize from the shell (sqlite 3.44+)
sqlite3 mydb.sqlite \
  -cmd ".parameter set @min 18" \
  "SELECT name FROM users WHERE age >= @min"
02

表与数据类型

创建表

INTEGER PRIMARY KEY 是 rowid 的别名——它自动递增且是最快的键。AUTOINCREMENT 改变算法使已删除的 ID 永不重用(稍慢;通常不必要)。DEFAULT (expr) 允许任意表达式如 datetime('now')。CREATE TABLE AS SELECT 创建预加载查询结果的表,但不复制约束或索引。

sqlite
CREATE TABLE users (
  id       INTEGER PRIMARY KEY AUTOINCREMENT,
  username TEXT    NOT NULL UNIQUE,
  email    TEXT    NOT NULL UNIQUE,
  age      INTEGER CHECK (age >= 0),
  role     TEXT    NOT NULL DEFAULT 'user',
  created  TEXT    NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS orders (
  id        INTEGER PRIMARY KEY,
  user_id   INTEGER NOT NULL,
  total     REAL    NOT NULL DEFAULT 0,
  FOREIGN KEY (user_id) REFERENCES users(id)
    ON DELETE CASCADE ON UPDATE CASCADE
);

-- quick table from a query
CREATE TABLE archive AS
  SELECT * FROM orders WHERE created < '2024-01-01';

存储类与类型亲和性

SQLite 使用动态类型:任何列都可以持有 5 种存储类之一。'类型亲和性'在可能时尝试转换值(如 INTEGER 列中的 '42' 变为整数 42),但从不因类型不匹配报错。STRICT 表 (3.37+) 恢复了传统的严格类型检查。typeof() 报告实际存储类。这种灵活性很强大但需要纪律——对数据完整性要求高的新 schema 请用 STRICT。

sqlite
-- SQLite has 5 storage classes (not strict types):
--   NULL, INTEGER, REAL, TEXT, BLOB
CREATE TYPE demo...  -- not supported

-- columns have 'type affinity', not enforced types
CREATE TABLE t (
  a INTEGER,   -- affinity INTEGER
  b TEXT,      -- affinity TEXT
  c REAL,      -- affinity REAL
  d BLOB,      -- affinity BLOB (no conversion)
  e            -- no affinity (accepts anything)
);

-- this WORKS despite 'INTEGER' column (dynamic typing)
INSERT INTO t (a) VALUES ('hello');
SELECT typeof(a) FROM t;   -- 'text'

-- STRICT tables (SQLite 3.37+) actually enforce types
CREATE TABLE strict_t (a INTEGER, b TEXT) STRICT;
INSERT INTO strict_t VALUES ('hi', 1);   -- error

常用数据类型

SQLite 没有原生的 DATE/TIME 或 BOOLEAN 类型——将时间戳存储为 ISO-8601 TEXT ('YYYY-MM-DD HH:MM:SS') 以便排序,用日期/时间函数操作。布尔值是整数 0/1。VARCHAR(n) 长度限制被忽略(仅为兼容性)。NUMERIC 亲和性先尝试 INTEGER 再 REAL,尽可能保留精确小数。BLOB 原样存储原始字节。

sqlite
-- integers
CREATE TABLE numerics (
  small  INTEGER,        -- 1/2/4/8 bytes depending on value
  big    INTEGER,        -- up to 64-bit
  -- no separate BIGINT/SMALLINT, all map to INTEGER affinity
  flag   BOOLEAN         -- stored as 0 (false) or 1 (true)
);

-- text & blobs
CREATE TABLE blobs (
  name   TEXT,           -- variable-length UTF-8, no length limit
  data   BLOB,           -- raw bytes, no conversion
  note   VARCHAR(255)    -- length ignored, affinity TEXT
);

-- real numbers
CREATE TABLE floats (
  price  REAL,           -- 8-byte IEEE float
  precise NUMERIC(10,2)  -- affinity NUMERIC, decimal preserved if exact
);

-- dates: SQLite has no native DATE type — store as TEXT (ISO-8601)
CREATE TABLE events (
  ts TEXT  -- 'YYYY-MM-DD HH:MM:SS' recommended
);

修改表结构

SQLite 的 ALTER TABLE 对无服务器引擎而言是有意限制的。ADD COLUMN 是 O(1)(无需重写表)。RENAME COLUMN (3.25+) 和 DROP COLUMN (3.35+) 是现代便利功能。对于类型更改或添加约束,使用 12 步表重建模式(创建新表、复制、删除、重命名、重建索引)。重建期间需临时禁用外键——参见 PRAGMA legacy_alter_table。

sqlite
-- add a column (fast, appends to end)
ALTER TABLE users ADD COLUMN bio TEXT;

-- rename a column (SQLite 3.25+)
ALTER TABLE users RENAME COLUMN name TO username;

-- rename a table
ALTER TABLE users RENAME TO accounts;

-- drop a column (SQLite 3.35+)
ALTER TABLE users DROP COLUMN deprecated_field;

-- SQLite CANNOT: change a column type, add constraints to
-- an existing column, or reorder columns. Workaround:
-- 1) create new table with desired schema
-- 2) INSERT INTO new SELECT * FROM old
-- 3) DROP old; ALTER TABLE new RENAME TO old
-- 4) recreate indexes/triggers

约束

约束在引擎层面强制数据完整性。PRIMARY KEY 隐含 NOT NULL 和 UNIQUE。CHECK 可引用行中任意列。ON CONFLICT 子句(OR IGNORE / OR REPLACE / OR ABORT / OR FAIL / OR ROLLBACK)控制单语句冲突解决——OR REPLACE 删除冲突行后插入,会重置其 rowid。对于 UPSERT 语义,优先使用 ON CONFLICT ... DO UPDATE(见 CRUD 部分)。

sqlite
CREATE TABLE products (
  id         INTEGER PRIMARY KEY,
  sku        TEXT    NOT NULL UNIQUE,
  name       TEXT    NOT NULL,
  price      REAL    NOT NULL CHECK (price >= 0),
  stock      INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0),
  category   TEXT    CHECK (category IN ('a','b','c')),

  -- composite unique constraint
  UNIQUE (name, category),

  -- table-level check
  CHECK (price * stock < 1000000)
);

-- name a constraint for clearer error messages
CREATE TABLE t (
  x INTEGER CONSTRAINT x_positive CHECK (x > 0)
);

-- conflict handling
INSERT OR IGNORE INTO products (sku, name, price) VALUES (...);
INSERT OR REPLACE INTO products (...) VALUES (...);
UPDATE OR ABORT products SET stock = stock - 1 WHERE id = 1;
03

CRUD 操作

INSERT

多行 INSERT 比循环逐行插入快得多——请批量插入。INSERT ... SELECT 在表间复制数据。last_insert_rowid() 返回当前连接上最近 INSERT 的 rowid(非事务)。对于应用,优先使用参数化插入(见集成部分)以避免 SQL 注入和引号问题。用 DEFAULT 显式取列的默认值。

sqlite
-- single row
INSERT INTO users (username, email, age)
VALUES ('alice', '[email protected]', 30);

-- multiple rows in one statement (fast)
INSERT INTO users (username, email) VALUES
  ('bob', '[email protected]'),
  ('carol', '[email protected]'),
  ('dave', '[email protected]');

-- insert from a query
INSERT INTO archive_users
SELECT * FROM users WHERE active = 0;

-- explicit NULL / default
INSERT INTO users (username, email, age)
VALUES ('eve', '[email protected]', DEFAULT);

-- return the new rowid (handy in apps)
INSERT INTO users (username) VALUES ('frank');
SELECT last_insert_rowid();   -- returns the new id

UPSERT (ON CONFLICT)

ON CONFLICT (UPSERT) 是 SQLite '存在则更新'的惯用法——比 INSERT OR REPLACE 安全得多,后者会删除现有行(触发 DELETE 触发器并重置 rowid)。'excluded' 指待插入的行。DO NOTHING 静默跳过。冲突目标可以是任何 UNIQUE 或 PRIMARY KEY 约束。这是计数器、最后访问更新和幂等导入的正确模式。

sqlite
-- insert, but update on conflict
INSERT INTO users (id, username, email)
VALUES (1, 'alice', '[email protected]')
ON CONFLICT(id) DO UPDATE SET
  email  = excluded.email,
  username = excluded.username;

-- ignore on conflict
INSERT INTO users (id, username)
VALUES (1, 'alice')
ON CONFLICT(id) DO NOTHING;

-- conflict on a specific unique constraint
INSERT INTO users (username, email)
VALUES ('alice', '[email protected]')
ON CONFLICT(email) DO UPDATE SET
  username = excluded.username;

UPDATE

除非确实要更新每一行,否则始终加 WHERE——SQLite 没有试运行模式。UPDATE ... FROM (3.33+) 和 UPDATE ... RETURNING (3.35+) 使 SQLite 接近 PostgreSQL 的体验。RETURNING 对需要更新后状态而无需单独 SELECT 的应用极为有用。CASE 支持单语句批量更新中的逐行逻辑,比逐行更新快得多。

sqlite
-- basic update (ALWAYS use WHERE in production!)
UPDATE users SET age = 31 WHERE id = 1;

-- multiple columns
UPDATE users
SET age = age + 1, role = 'admin'
WHERE username = 'alice';

-- conditional update with CASE
UPDATE products
SET price = CASE
  WHEN category = 'a' THEN price * 0.9
  WHEN category = 'b' THEN price * 0.95
  ELSE price
END;

-- update from another table (subquery)
UPDATE orders
SET total = (SELECT SUM(qty * price)
             FROM items WHERE order_id = orders.id);

-- UPDATE ... RETURNING (SQLite 3.35+)
UPDATE users SET role = 'admin'
WHERE id IN (1,2,3)
RETURNING id, username, role;

DELETE

SQLite 没有 TRUNCATE——DELETE FROM table 删除所有行但会触发行级触发器,且不重置 AUTOINCREMENT 计数器(存储在 sqlite_sequence 中)。DELETE ... RETURNING (3.35+) 记录被删除的内容。对于审计数据,优先软删除(deleted_at 列)而非硬删除。删除产生的空闲空间会被未来插入重用;VACUUM 将其归还给操作系统。

sqlite
-- basic delete
DELETE FROM users WHERE id = 1;

-- delete with a subquery condition
DELETE FROM orders
WHERE user_id IN (SELECT id FROM users WHERE active = 0);

-- DELETE ... RETURNING
DELETE FROM sessions
WHERE expires < datetime('now')
RETURNING id, user_id;

-- delete ALL rows (fast, but fires triggers)
DELETE FROM logs;

-- TRUNCATE equivalent: delete all + reset rowid
DELETE FROM logs;
DELETE FROM sqlite_sequence WHERE name = 'logs';

-- soft delete pattern
UPDATE users SET deleted_at = datetime('now') WHERE id = 1;

SELECT 基础

SELECT 是主力。LIMIT count OFFSET skip 实现分页(大偏移量时键集分页更快——见查询部分)。别名(AS,可选)提高可读性,且视图中计算列必须使用别名。DISTINCT 折叠相同行;考虑用 GROUP BY 更精细控制。CASE 为结果集添加 if/else 逻辑,可用于任何子句。

sqlite
-- basic select
SELECT id, username, email FROM users;

-- limit and offset
SELECT id, username FROM users LIMIT 10 OFFSET 20;
SELECT id, username FROM users LIMIT 20, 10;  -- offset, count

-- aliasing columns and tables
SELECT u.id AS user_id, u.username AS name
FROM users AS u;

-- expressions
SELECT
  username,
  age,
  age * 365 AS days_alive,
  UPPER(username) AS upper_name
FROM users;

-- distinct rows
SELECT DISTINCT category FROM products;

-- conditional output
SELECT name,
  CASE WHEN age >= 18 THEN 'adult' ELSE 'minor' END AS status
FROM users;
04

查询与过滤

WHERE 与运算符

NULL 在 SQL 中很特殊——它是'未知',不是值。= NULL 总是返回 NULL(被视为 false),所以用 IS NULL / IS NOT NULL。NULL 与 AND/OR 遵循三值逻辑。BETWEEN 是包含边界。IN 是 OR 等价的简写;避免 IN (NULL, ...) 因其行为古怪。多用括号使复合条件明确无歧义。

sqlite
-- comparison operators
SELECT * FROM products WHERE price < 50;
SELECT * FROM products WHERE price BETWEEN 10 AND 100;
SELECT * FROM users WHERE age IN (18, 21, 25);
SELECT * FROM users WHERE age NOT IN (18, 21);

-- NULL handling (NEVER use = NULL)
SELECT * FROM users WHERE email IS NULL;
SELECT * FROM users WHERE email IS NOT NULL;

-- logical operators
SELECT * FROM users
WHERE age >= 18 AND age <= 65 AND role = 'admin';

SELECT * FROM users
WHERE role = 'admin' OR role = 'editor';

-- range with multiple conditions
SELECT * FROM orders
WHERE (status = 'shipped' AND total > 100)
   OR (status = 'pending' AND total > 500);

ORDER BY 与 LIMIT

ORDER BY 不带唯一决胜列时跨运行顺序不确定——始终加一个唯一列(如 id)作为最后排序键。OFFSET 分页是 O(n) 因为要扫描并丢弃行;键集分页 (WHERE id > last_seen_id) 是 O(limit),是大表的正确选择。RANDOM() 需要完整排序——仅对小结果集或采样子集使用。

sqlite
-- ascending (default) / descending
SELECT * FROM users ORDER BY username;
SELECT * FROM users ORDER BY created DESC;
SELECT * FROM users ORDER BY age DESC, username ASC;

-- sort with NULLs first or last (SQLite 3.30+)
SELECT * FROM tasks ORDER BY due_date ASC NULLS FIRST;
SELECT * FROM tasks ORDER BY due_date DESC NULLS LAST;

-- pagination: keyset is faster than OFFSET
-- slow (offset scans & discards rows):
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 10000;
-- fast (keyset):
SELECT * FROM users WHERE id > 10000 ORDER BY id LIMIT 10;

-- random sample
SELECT * FROM users ORDER BY RANDOM() LIMIT 5;

模式匹配

LIKE 默认对 ASCII 不区分大小写(一个令人惊讶的陷阱——用 COLLATE BINARY 区分大小写)。GLOB 区分大小写且支持字符类如 [A-Z]。REGEXP 不是内置的——必须注册函数或加载扩展。大规模全文搜索请用 FTS5(见性能部分)而非 LIKE '%term%',后者无法使用索引。

sqlite
-- LIKE: case-insensitive by default for ASCII,
--   wildcards % (any chars) and _ (single char)
SELECT * FROM users WHERE username LIKE 'al%';
SELECT * FROM users WHERE email LIKE '%@gmail.com';
SELECT * FROM users WHERE username LIKE '_lice';

-- case-sensitive LIKE
SELECT * FROM users WHERE username LIKE 'AL%' COLLATE BINARY;

-- GLOB: Unix shell-style, case-SENSITIVE
--   wildcards * and ?, plus [abc] character classes
SELECT * FROM users WHERE username GLOB 'Al*';
SELECT * FROM users WHERE username GLOB '[A-D]*';

-- REGEXP (requires extension or custom function)
SELECT * FROM users WHERE username REGEXP '^a.*e$';

-- substring match
SELECT * FROM users WHERE INSTR(username, 'lic') > 0;

CASE 与条件逻辑

CASE 是 SQL 的 if/else,可用于 SELECT、WHERE、ORDER BY、GROUP BY 和 HAVING。'简单'形式匹配值;'搜索'形式按顺序评估布尔条件。SUM(CASE WHEN ... THEN 1 ELSE 0 END) 是经典的透视/条件计数惯用法。CASE 在第一个匹配的 WHEN 处短路,所以顺序重要。省略 ELSE 时默认为 NULL。

sqlite
-- simple CASE (value match)
SELECT name,
  CASE role
    WHEN 'admin' THEN 'Administrator'
    WHEN 'editor' THEN 'Editor'
    ELSE 'Regular User'
  END AS role_label
FROM users;

-- searched CASE (conditions)
SELECT name,
  CASE
    WHEN age < 18 THEN 'minor'
    WHEN age < 65 THEN 'adult'
    ELSE 'senior'
  END AS age_group
FROM users;

-- CASE in aggregate (conditional count)
SELECT
  COUNT(*) AS total,
  SUM(CASE WHEN active = 1 THEN 1 ELSE 0 END) AS active_count,
  SUM(CASE WHEN active = 0 THEN 1 ELSE 0 END) AS inactive_count
FROM users;

-- CASE in ORDER BY (custom sort order)
SELECT * FROM products
ORDER BY CASE category
  WHEN 'featured' THEN 0
  WHEN 'new' THEN 1
  ELSE 2
END;

DISTINCT 与集合操作

UNION ALL 比 UNION 快,因为 UNION 执行去重排序——当不可能有重复或可接受重复时优先用 UNION ALL。所有集合操作要求兼容的列数和类型。SQLite 中 INTERSECT/EXCEPT 优先级高于 UNION/UNION ALL;用括号控制顺序。复合查询中的 ORDER BY 作用于整个结果集,而非最后一个 SELECT。

sqlite
-- distinct rows
SELECT DISTINCT category FROM products;
SELECT DISTINCT city, country FROM users;

-- UNION (dedup) and UNION ALL (faster, keeps dups)
SELECT 'user' AS type, id FROM users WHERE active = 1
UNION ALL
SELECT 'admin', id FROM admins;

-- INTERSECT: rows in both
SELECT id FROM users
INTERSECT
SELECT user_id FROM orders;

-- EXCEPT: rows in first but not second
SELECT id FROM users
EXCEPT
SELECT user_id FROM orders;

-- combine with ORDER BY (applies to whole result)
SELECT name FROM users WHERE active = 1
UNION
SELECT name FROM archived_users
ORDER BY name;
05

连接

内连接

INNER JOIN 只返回两表中匹配的行——任一侧不匹配的行都被丢弃。USING (col) 是当两表有同名列时的简写,并产生一个合并的列。NATURAL JOIN 隐式使用所有同名列——方便但脆弱(schema 变更会静默改变连接行为)。优先用显式 ON 以保持清晰。

sqlite
-- only matching rows from both tables
SELECT u.username, o.id AS order_id, o.total
FROM users AS u
INNER JOIN orders AS o ON o.user_id = u.id;

-- join with additional filter
SELECT u.username, o.total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE o.total > 100 AND u.active = 1;

-- USING shorthand (when join columns share a name)
SELECT u.username, o.total
FROM users u
JOIN orders o USING (user_id);

-- natural join (joins on all same-named columns — risky)
SELECT * FROM users NATURAL JOIN profiles;

左连接

LEFT JOIN 保留左表每一行;缺失的右侧列变为 NULL。'WHERE right.id IS NULL' 模式(反连接)查找无匹配的行——通常比带子查询的 NOT IN 更清晰更快,尤其是涉及 NULL 时。跨 LEFT JOIN 聚合时,COUNT(right.id)(而非 COUNT(*))只计匹配行;无订单的用户得到 0 而非 1。

sqlite
-- all users, with their orders (NULLs if no orders)
SELECT u.username, o.id AS order_id, o.total
FROM users AS u
LEFT JOIN orders AS o ON o.user_id = u.id;

-- find users with NO orders (anti-join pattern)
SELECT u.username
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;

-- multiple left joins
SELECT u.username,
  COUNT(o.id) AS order_count,
  SUM(o.total) AS total_spent
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.username;

交叉连接与右/全连接

CROSS JOIN 产生笛卡尔积——适合生成组合(尺寸 × 颜色),但在大表上危险(n×m 行)。SQLite 支持 RIGHT JOIN (3.39+) 但很少使用;改写为 LEFT JOIN 更清晰。没有原生的 FULL OUTER JOIN;用 LEFT JOIN ... UNION ... RIGHT JOIN 模拟,其中 UNION 去重重叠的匹配行。

sqlite
-- CROSS JOIN: cartesian product (every combination)
SELECT s.size, c.color
FROM sizes s CROSS JOIN colors c;

-- explicit CROSS JOIN (same as comma join)
SELECT a.name, b.name FROM teams a, teams b;

-- RIGHT JOIN (preserves right table) — supported but unusual
SELECT u.username, o.total
FROM orders o
RIGHT JOIN users u ON u.id = o.user_id;

-- SQLite has no FULL OUTER JOIN; emulate with LEFT + UNION:
SELECT u.username, o.id AS order_id
FROM users u LEFT JOIN orders o ON o.user_id = u.id
UNION
SELECT u.username, o.id
FROM users u RIGHT JOIN orders o ON o.user_id = u.id;

自连接

自连接用别名查询同一表两次,为每个副本赋予角色(员工 vs 经理,user_a vs user_b)。'a.id < b.id' 技巧避免行与自身匹配并产生镜像重复对。对于深层层级(任意深度),递归 CTE(见 CTE 部分)是正确工具——自连接只能处理固定深度。

sqlite
-- employees and their managers (same table)
CREATE TABLE employees (
  id INTEGER PRIMARY KEY,
  name TEXT,
  manager_id INTEGER REFERENCES employees(id)
);

SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

-- find pairs of users in the same city
SELECT a.username AS user_a, b.username AS user_b, a.city
FROM users a
JOIN users b ON a.city = b.city AND a.id < b.id;

-- recursive hierarchy (tree traversal) — see CTE section
-- multi-level category paths
SELECT c.name, p.name AS parent
FROM categories c
LEFT JOIN categories p ON c.parent_id = p.id;

多表连接

连接从左到右链式执行;每个 JOIN 看到累积结果。混合 LEFT 和 INNER 连接时,LEFT 之后的 INNER JOIN 会重新过滤掉 NULL,破坏 LEFT 的目的——请谨慎排序。COUNT(DISTINCT col) 避免跨一对多连接的重复计数。注意连接基数:连接两个一对多表可能产生行爆炸(每父行 m × n 行)。

sqlite
-- chain joins across 3+ tables
SELECT u.username, o.id AS order_id, i.name AS item, i.qty
FROM users u
JOIN orders o ON o.user_id = u.id
JOIN order_items i ON i.order_id = o.id
WHERE u.active = 1
ORDER BY o.id, i.name;

-- mixing JOIN types
SELECT u.username, o.id AS order_id, p.name AS product
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
LEFT JOIN order_items i ON i.order_id = o.id
LEFT JOIN products p ON p.id = i.product_id;

-- join with aggregation
SELECT u.username,
  COUNT(DISTINCT o.id) AS orders,
  SUM(i.qty * i.price) AS lifetime_value
FROM users u
JOIN orders o ON o.user_id = u.id
JOIN order_items i ON i.order_id = o.id
GROUP BY u.id, u.username;
06

聚合与 GROUP BY

GROUP BY 基础

GROUP BY 将共享分组列的行折叠为每组一行。SELECT 中的每个非聚合列都必须出现在 GROUP BY 中(SQLite 较宽松会任选一值,但不要依赖)。按表达式分组(如对日期用 strftime)非常适合时间桶报表。ORDER BY 在 GROUP BY 之后应用——用它对分组排序。

sqlite
-- count users per role
SELECT role, COUNT(*) AS user_count
FROM users
GROUP BY role;

-- multiple aggregates per group
SELECT role,
  COUNT(*)            AS user_count,
  AVG(age)            AS avg_age,
  MIN(age)            AS min_age,
  MAX(age)            AS max_age,
  SUM(age)            AS total_age
FROM users
GROUP BY role;

-- group by multiple columns
SELECT role, active, COUNT(*) AS cnt
FROM users
GROUP BY role, active
ORDER BY role, active;

-- group by expression
SELECT strftime('%Y-%m', created) AS month,
  COUNT(*) AS signups
FROM users
GROUP BY month
ORDER BY month;

聚合函数

COUNT(*) 计行数;COUNT(col) 计非 NULL 值;COUNT(DISTINCT col) 计唯一非 NULL 值。AVG/SUM 忽略 NULL。GROUP_CONCAT 将值拼接成字符串(默认逗号分隔符)。TOTAL() 总是返回 REAL(空输入为 0.0),而 SUM() 对空输入返回 NULL。FILTER 子句 (3.30+) 比聚合内的 CASE 更简洁且可能更快。

sqlite
SELECT
  COUNT(*)             AS row_count,
  COUNT(email)         AS emails_non_null,
  COUNT(DISTINCT role) AS distinct_roles,
  AVG(age)             AS avg_age,
  SUM(total)           AS revenue,
  MIN(created)         AS first_signup,
  MAX(created)         AS last_signup,
  GROUP_CONCAT(name)   AS all_names,
  GROUP_CONCAT(name, ' | ') AS names_piped,
  TOTAL(price)         AS total_real  -- always returns REAL, 0.0 if empty
FROM orders;

-- aggregate with FILTER (SQLite 3.30+)
SELECT
  COUNT(*)                              AS all_orders,
  COUNT(*) FILTER (WHERE status='paid') AS paid,
  SUM(total) FILTER (WHERE status='paid') AS paid_revenue
FROM orders;

HAVING (过滤分组)

WHERE 在分组前过滤输入行;HAVING 在聚合后过滤分组。这个顺序很重要:对聚合用 WHERE 是非法的。SQLite 允许在 HAVING(和 GROUP BY)中引用别名以提高可读性,但这不是标准 SQL。'group by X having count(*) > 1' 模式是经典的查重查询。

sqlite
-- HAVING filters groups (after GROUP BY);
-- WHERE filters rows (before GROUP BY)
SELECT user_id, COUNT(*) AS order_count, SUM(total) AS spent
FROM orders
WHERE status = 'paid'         -- filter rows first
GROUP BY user_id
HAVING COUNT(*) >= 3 AND SUM(total) > 100   -- filter groups
ORDER BY spent DESC;

-- HAVING with aliases (SQLite allows this)
SELECT role, COUNT(*) AS n
FROM users
GROUP BY role
HAVING n > 1;

-- find duplicate usernames
SELECT username, COUNT(*) AS dupes
FROM users
GROUP BY username
HAVING COUNT(*) > 1;

GROUP_CONCAT

GROUP_CONCAT 是 SQLite 的字符串聚合函数(其他数据库叫 STRING_AGG 或 LISTAGG)。默认分隔符是逗号;用第二个参数指定自定义分隔符。聚合内 ORDER BY (3.44+) 控制拼接顺序。DISTINCT 在拼接前去重。结果有 1GB 限制;对于超大分组,请在应用中获取行并聚合。

sqlite
-- concatenate values within a group
SELECT role, GROUP_CONCAT(username) AS members
FROM users
GROUP BY role;
-- members: 'alice,bob,carol'

-- custom separator
SELECT role, GROUP_CONCAT(username, ' | ') AS members
FROM users
GROUP BY role;

-- ordered concatenation (SQLite 3.44+)
SELECT role,
  GROUP_CONCAT(username ORDER BY username) AS sorted_members
FROM users
GROUP BY role;

-- distinct values only
SELECT role,
  GROUP_CONCAT(DISTINCT city) AS cities
FROM users
GROUP BY role;

数据透视

SQLite 没有原生 PIVOT;用 SUM(CASE WHEN ...) 或更简洁的 FILTER 子句模拟。每个透视列测试分组属性的一个值。此模式将长(规范化)数据转换为宽(交叉表)报表。对于完全动态的透视(未知类别),必须在应用代码中构建 SQL 字符串——SQL 无法在运行时生成列。

sqlite
-- manual pivot with SUM(CASE ...)
SELECT user_id,
  SUM(CASE WHEN strftime('%w', created) = '0' THEN 1 ELSE 0 END) AS sun,
  SUM(CASE WHEN strftime('%w', created) = '1' THEN 1 ELSE 0 END) AS mon,
  SUM(CASE WHEN strftime('%w', created) = '2' THEN 1 ELSE 0 END) AS tue,
  SUM(CASE WHEN strftime('%w', created) = '3' THEN 1 ELSE 0 END) AS wed,
  SUM(CASE WHEN strftime('%w', created) = '4' THEN 1 ELSE 0 END) AS thu,
  SUM(CASE WHEN strftime('%w', created) = '5' THEN 1 ELSE 0 END) AS fri,
  SUM(CASE WHEN strftime('%w', created) = '6' THEN 1 ELSE 0 END) AS sat
FROM orders
GROUP BY user_id;

-- pivot with FILTER (cleaner)
SELECT user_id,
  COUNT(*) FILTER (WHERE status='paid')    AS paid,
  COUNT(*) FILTER (WHERE status='pending') AS pending,
  COUNT(*) FILTER (WHERE status='refunded') AS refunded
FROM orders
GROUP BY user_id;
07

子查询

标量子查询

标量子查询返回恰好一行一列;可出现在任何值表达式有效的地方。相关子查询引用外查询并逐行重新执行——强大但在大表上可能慢(考虑用 JOIN 或窗口函数替代)。SQLite 善于将简单相关子查询优化为连接,但始终用 EXPLAIN QUERY PLAN 检查热点路径。

sqlite
-- a subquery returning a single value
SELECT username, age
FROM users
WHERE age > (SELECT AVG(age) FROM users);

-- in SELECT list
SELECT username,
  age,
  age - (SELECT AVG(age) FROM users) AS age_diff
FROM users;

-- in HAVING
SELECT role, AVG(age) AS avg_age
FROM users
GROUP BY role
HAVING AVG(age) > (SELECT AVG(age) FROM users);

-- correlated scalar subquery (re-evaluated per row)
SELECT username,
  (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_count
FROM users u;

IN 与 NOT IN 子查询

IN 子查询很直观,但要注意 NULL:如果子查询结果包含 NULL,NOT IN 对每一行评估为 NULL(视为 false),返回空结果。对于含可空列的反连接,优先用 NOT EXISTS——它是 NULL 安全的且通常更快。多列 IN 对匹配复合键很简洁。

sqlite
-- users who have placed an order
SELECT username FROM users
WHERE id IN (SELECT user_id FROM orders);

-- users who have NOT placed an order
SELECT username FROM users
WHERE id NOT IN (SELECT user_id FROM orders);

-- DANGER: NOT IN with NULLs returns no rows!
-- if the subquery returns any NULL, NOT IN matches nothing.
-- Safe version using NOT EXISTS:
SELECT username FROM users u
WHERE NOT EXISTS (
  SELECT 1 FROM orders o WHERE o.user_id = u.id
);

-- multi-column IN
SELECT * FROM products
WHERE (category, price) IN (
  SELECT category, MIN(price) FROM products GROUP BY category
);

EXISTS 与 NOT EXISTS

EXISTS 测试行是否存在而不获取数据——它在第一个匹配处停止,因此很高效。NOT EXISTS 是查找不匹配行的 NULL 安全方式(不同于 NOT IN)。为获得最佳性能,确保相关子查询连接的列上有索引(示例中的 orders.user_id)。SELECT 1 是惯例;列列表对 EXISTS 无关紧要。

sqlite
-- EXISTS: true if subquery returns any row
SELECT username FROM users u
WHERE EXISTS (
  SELECT 1 FROM orders o
  WHERE o.user_id = u.id AND o.total > 100
);

-- NOT EXISTS: classic anti-join (NULL-safe)
SELECT username FROM users u
WHERE NOT EXISTS (
  SELECT 1 FROM orders o WHERE o.user_id = u.id
);

-- correlated EXISTS — usually efficient with an index on
-- the subquery's join column (orders.user_id here)
SELECT p.name FROM products p
WHERE EXISTS (
  SELECT 1 FROM order_items i
  WHERE i.product_id = p.id AND i.qty > 10
);

派生表 (FROM 中的子查询)

FROM 中的子查询(派生表)是暂存中间结果、预聚合或连接前过滤的强大方式。派生表必须有别名。SQLite 在大多数情况下物化子查询(写入临时表)——对于重复使用,CTE(WITH 子句)通常更清晰且可被多次引用。SQLite 不允许对 FROM 子查询进行相关引用。

sqlite
-- treat a query result as a table
SELECT t.role, t.cnt
FROM (
  SELECT role, COUNT(*) AS cnt
  FROM users
  GROUP BY role
) t
WHERE t.cnt > 1;

-- join a derived table
SELECT u.username, t.total_spent
FROM users u
JOIN (
  SELECT user_id, SUM(total) AS total_spent
  FROM orders
  GROUP BY user_id
) t ON t.user_id = u.id;

-- derived tables must be aliased
SELECT * FROM (SELECT 1 AS x) AS sub;

相关与非相关子查询

非相关子查询运行一次且可缓存;相关子查询对外层每一行重新执行(用索引保持快速)。SQLite 缺少 LATERAL/APPLY;要'连接'逐行聚合,请在派生表中预聚合再 LEFT JOIN。当相关子查询性能差时,通常改写为带 GROUP BY 的 JOIN 或窗口函数即可解决。

sqlite
-- UNCORRELATED: runs once, result cached
SELECT username FROM users
WHERE age > (SELECT AVG(age) FROM users);

-- CORRELATED: references outer row, re-runs per outer row
SELECT u.username,
  (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS orders
FROM users u;

-- lateral-style correlation in a JOIN subquery is NOT
-- supported directly; use a scalar subquery in SELECT or
-- rewrite as a join with GROUP BY:
SELECT u.username, COALESCE(t.n, 0) AS orders
FROM users u
LEFT JOIN (
  SELECT user_id, COUNT(*) AS n FROM orders GROUP BY user_id
) t ON t.user_id = u.id;
08

索引

创建索引

索引加速读但减慢写(每个 INSERT/UPDATE/DELETE 都要更新所有索引)。索引列顺序很重要:(a, b) 上的复合索引有助于 WHERE a=? 和 WHERE a=? AND b=?,但不能单独用于 WHERE b=?(最左前缀规则)。唯一索引强制约束。SQLite 以 B 树存储索引。删除未使用的索引——它们消耗空间和写性能。

sqlite
-- single-column index
CREATE INDEX idx_users_email ON users(email);

-- unique index (also enforces uniqueness)
CREATE UNIQUE INDEX idx_users_username ON users(username);

-- composite index (column order matters!)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);

-- IF NOT EXISTS for idempotent scripts
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);

-- drop an index
DROP INDEX IF EXISTS idx_users_email;

-- list indexes
.indices users
-- or
SELECT name, sql FROM sqlite_master
WHERE type = 'index' AND tbl_name = 'users';

部分索引

部分索引只索引匹配的行,因此更小且维护更快——适合查询总是过滤稳定谓词('active = 1'、'deleted_at IS NULL')的情况。查询的 WHERE 必须匹配索引的 WHERE(或更严格)优化器才会使用它。唯一部分索引仅对子集强制唯一性——完美适合'每个父级一个活动行'约束。

sqlite
-- index only active users (smaller, faster)
CREATE INDEX idx_active_users ON users(username)
WHERE active = 1;

-- index only unpaid orders
CREATE INDEX idx_unpaid_orders ON orders(user_id)
WHERE status = 'unpaid';

-- unique partial index: one active session per user
CREATE UNIQUE INDEX idx_one_active_session
ON sessions(user_id) WHERE active = 1;

-- query must match the WHERE for the index to be used
SELECT * FROM users WHERE active = 1 AND username = 'alice';

表达式索引

表达式索引索引函数或表达式的结果,支持对转换值的快速查找——但查询必须使用完全相同的表达式。常见用途:不区分大小写搜索 (LOWER)、日期分桶 (strftime) 和 JSON 字段提取 (json_extract)。仅限确定性函数(不能用 RANDOM() 或 NOW())。表达式索引很强大但使 schema 变更更棘手。

sqlite
-- index a function result (must match the query expression exactly)
CREATE INDEX idx_users_lower_email ON users(LOWER(email));

-- query that can use it (must use the same expression)
SELECT * FROM users WHERE LOWER(email) = '[email protected]';

-- index a date extraction for fast monthly queries
CREATE INDEX idx_orders_month ON orders(strftime('%Y-%m', created));
SELECT * FROM orders WHERE strftime('%Y-%m', created) = '2024-06';

-- index a JSON path
CREATE INDEX idx_events_type ON events(json_extract(data, '$.type'));
SELECT * FROM events WHERE json_extract(data, '$.type') = 'login';

-- indexed computed value (collation)
CREATE INDEX idx_users_name_ci ON users(username COLLATE NOCASE);

覆盖索引与 WITHOUT ROWID

覆盖索引包含查询读取的每一列,因此 SQLite 仅从索引满足查询(无需表查找)。WITHOUT ROWID 表使 PRIMARY KEY 成为聚簇索引——行按键排序存储,适合键/值工作负载和主键范围扫描。权衡:无 AUTOINCREMENT、无 rowid 别名、更新略复杂。大规模采用前请基准测试。

sqlite
-- covering index: includes all columns the query needs,
-- so SQLite never reads the table
CREATE INDEX idx_orders_cover ON orders(user_id, status, total);
SELECT user_id, status, total FROM orders WHERE user_id = 1;
-- ^ fully served by the index

-- WITHOUT ROWID tables: store rows IN the index (clustered)
CREATE TABLE kv (
  key   TEXT PRIMARY KEY,
  value TEXT
) WITHOUT ROWID;

-- the PK becomes the table itself; lookups by key are fast
SELECT value FROM kv WHERE key = 'config:theme';

-- good for key/value lookups and read-heavy tables
-- trade-off: no rowid, some features unsupported

索引检查

EXPLAIN QUERY PLAN 是必备工具——'USING INDEX' 表示使用了索引;'SCAN' 表示全表扫描(通常不好)。sqlite_stat1(由 ANALYZE 填充)保存查询规划器选择索引所用的基数统计。REINDEX 重建膨胀或损坏的索引而无需重建表。大量数据加载后运行 ANALYZE 以便规划器有新鲜统计来选择好计划。

sqlite
-- see if an index is used
EXPLAIN QUERY PLAN
SELECT * FROM users WHERE email = '[email protected]';
-- look for 'SEARCH users USING INDEX idx_users_email'

-- list all indexes on a table with their definitions
SELECT name, sql FROM sqlite_master
WHERE type = 'index' AND tbl_name = 'users';

-- index stats (how often each index is used)
SELECT name, idx_scan, idx_read
FROM sqlite_stat1, sqlite_master
WHERE sqlite_stat1.tbl = 'users';

-- rebuild an index (after corruption or bloat)
REINDEX idx_users_email;
REINDEX;  -- rebuild all indexes

-- analyze tables to update query planner stats
ANALYZE;
09

视图

创建视图

视图是由保存查询定义的虚拟表——它们不存储数据(临时视图除外),每次查询时重新运行底层 SELECT。用它们简化复杂查询、强制一致的访问模式,或向应用抽象 schema 变更。SQLite 中视图默认只读(对可更新视图有限制)。DROP VIEW 从不影响底层表。

sqlite
-- a view is a saved SELECT, queried like a table
CREATE VIEW active_users AS
SELECT id, username, email
FROM users
WHERE active = 1;

SELECT * FROM active_users WHERE age > 18;

-- view with calculated columns
CREATE VIEW user_summary AS
SELECT u.id, u.username,
  COUNT(o.id) AS order_count,
  COALESCE(SUM(o.total), 0) AS total_spent
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.username;

-- IF NOT EXISTS for idempotent scripts
CREATE VIEW IF NOT EXISTS active_users AS
SELECT id, username FROM users WHERE active = 1;

-- drop a view
DROP VIEW IF EXISTS active_users;

可更新视图

SQLite 允许对简单视图(单基表、无 JOIN/聚合/DISTINCT/GROUP BY)执行 INSERT/UPDATE/DELETE。操作转发到底层表。SQLite 不支持 WITH CHECK OPTION,因此通过视图插入的行可能不满足视图的 WHERE,从而之后通过视图不可见。对于需要写入的复杂视图,使用 INSTEAD OF 触发器。

sqlite
-- simple views (one base table, no aggregates/distinct/etc.)
-- are auto-updatable in SQLite
CREATE VIEW active_users AS
SELECT id, username, email, age FROM users WHERE active = 1;

-- INSERT through the view (active defaults/applies)
INSERT INTO active_users (id, username, email) VALUES (1, 'x', '[email protected]');

-- UPDATE through the view
UPDATE active_users SET age = 30 WHERE id = 1;

-- DELETE through the view
DELETE FROM active_users WHERE id = 1;

-- the WITH CHECK OPTION is NOT supported in SQLite, so
-- inserts/updates that don't match the view's WHERE still
-- land in the base table but vanish from the view.

视图上的 INSTEAD OF 触发器

INSTEAD OF 触发器拦截对视图的写入并运行自定义逻辑,使任何视图可更新。这是让复杂(多表、聚合)视图可写的标准方法。NEW 和 OLD 指传入和现有行。INSTEAD OF 触发器只在视图上触发,不在基表上。用它们实现封装的业务逻辑或在规范化 schema 后隐藏非规范化视图。

sqlite
-- a view joining multiple tables is not auto-updatable,
-- but INSTEAD OF triggers let you define the write behavior
CREATE VIEW user_orders AS
SELECT u.id AS user_id, u.username, o.id AS order_id, o.total
FROM users u LEFT JOIN orders o ON o.user_id = u.id;

-- route INSERTs on the view to the orders table
CREATE TRIGGER trg_user_orders_insert
INSTEAD OF INSERT ON user_orders
FOR EACH ROW
BEGIN
  INSERT INTO orders (id, user_id, total)
  VALUES (NEW.order_id, NEW.user_id, NEW.total);
END;

-- now this works:
INSERT INTO user_orders (user_id, order_id, total)
VALUES (1, 100, 49.99);

临时视图

临时视图仅存在于当前数据库连接期间,断开连接时自动删除——非常适合暂存复杂即席查询而不污染共享 schema。它们存储在单独的 temp schema 中,因此其他连接(即使同一文件)看不到它们。TEMP 视图可引用 TEMP 表,对会话级 ETL 管道很有用。

sqlite
-- TEMP view exists only for the current connection
CREATE TEMP VIEW recent_orders AS
SELECT * FROM orders WHERE created > datetime('now', '-7 days');

-- the view vanishes when the connection closes
-- (no other connection can see it)

-- also valid: CREATE TEMPORARY VIEW
CREATE TEMPORARY VIEW my_scratch AS
SELECT id, username FROM users WHERE role = 'admin';

-- use case: ad-hoc analysis without polluting the schema
SELECT * FROM recent_orders WHERE total > 100;

-- list views
SELECT name, sql FROM sqlite_master
WHERE type = 'view';
SELECT name, sql FROM temp.sqlite_master
WHERE type = 'view';  -- temp views

物化视图 (模拟)

SQLite 没有原生物化视图,但用定期刷新的常规表可实现同样效果。对于原子刷新,在事务中构建新表并重命名——读取者在提交前看到旧版本,之后看到新版本。重建后添加索引。物化视图以写/存储成本换取读速度——适合频繁查询的昂贵聚合。

sqlite
-- SQLite has no native materialized views; emulate with a
-- real table that you refresh periodically

-- 1) create the 'materialized' table
CREATE TABLE mv_user_summary AS
SELECT u.id, u.username,
  COUNT(o.id) AS order_count,
  COALESCE(SUM(o.total), 0) AS total_spent
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.username;

-- 2) add an index for fast lookups
CREATE INDEX idx_mv_user_summary_id ON mv_user_summary(id);

-- 3) refresh (full rebuild)
DELETE FROM mv_user_summary;
INSERT INTO mv_user_summary
SELECT u.id, u.username, COUNT(o.id), COALESCE(SUM(o.total), 0)
FROM users u LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.username;

-- 4) refresh atomically with a name swap
BEGIN;
CREATE TABLE mv_user_summary_new AS SELECT ...;
DROP TABLE mv_user_summary;
ALTER TABLE mv_user_summary_new RENAME TO mv_user_summary;
COMMIT;
10

触发器

BEFORE / AFTER 触发器

BEFORE 触发器在行写入前运行——用于验证 (RAISE ABORT) 或转换 NEW 值。AFTER 触发器在行提交后运行——用于审计、级联效果或非规范化。FOR EACH ROW 是唯一选项(无语句级触发器)。NEW 持有传入行 (INSERT/UPDATE);OLD 持有先前行 (UPDATE/DELETE)。WHEN 子句有条件地触发触发器。

sqlite
-- AFTER INSERT: audit log
CREATE TRIGGER trg_users_audit
AFTER INSERT ON users
FOR EACH ROW
BEGIN
  INSERT INTO users_audit (user_id, action, at)
  VALUES (NEW.id, 'insert', datetime('now'));
END;

-- BEFORE INSERT: validate / normalize data
CREATE TRIGGER trg_users_lower_email
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
  SELECT RAISE(ABORT, 'email cannot be empty')
  WHERE NEW.email = '';
END;

-- AFTER UPDATE: log changes
CREATE TRIGGER trg_users_update_log
AFTER UPDATE ON users
FOR EACH ROW
WHEN OLD.email IS NOT NEW.email
BEGIN
  INSERT INTO email_changes (user_id, old_email, new_email, at)
  VALUES (NEW.id, OLD.email, NEW.email, datetime('now'));
END;

级联效果的触发器

触发器可自动维护非规范化计数器、汇总表和 updated_at 时间戳。注意:递归触发器链是可能的(触发器更新自己的表),所以用 PRAGMA recursive_triggers 控制(默认关闭以保持向后兼容)。同一事件上多个触发器的触发顺序遵循创建顺序;同一时序的触发器之间没有 BEFORE/AFTER 优先级。

sqlite
-- maintain a denormalized counter
CREATE TRIGGER trg_orders_count_inc
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
  UPDATE users
  SET order_count = order_count + 1
  WHERE id = NEW.user_id;
END;

CREATE TRIGGER trg_orders_count_dec
AFTER DELETE ON orders
FOR EACH ROW
BEGIN
  UPDATE users
  SET order_count = order_count - 1
  WHERE id = OLD.user_id;
END;

-- keep an 'updated_at' column fresh
CREATE TRIGGER trg_users_touch
AFTER UPDATE ON users
FOR EACH ROW
WHEN NEW.updated_at IS OLD.updated_at
BEGIN
  UPDATE users SET updated_at = datetime('now') WHERE id = NEW.id;
END;

INSTEAD OF 触发器 (视图)

INSTEAD OF 触发器代替视图上原始的 INSERT/UPDATE/DELETE 运行,让你将写入路由到一个或多个底层表。这是让复杂视图可写的经典方法。它们按视图的每一行触发。只有 INSTEAD OF 触发器可在视图上定义;BEFORE/AFTER 触发器需要基表。适合在规范化 schema 之上构建 API 风格的抽象层。

sqlite
-- make a multi-table view writable
CREATE VIEW order_detail AS
SELECT o.id AS order_id, o.total, u.username, i.product_name
FROM orders o
JOIN users u ON u.id = o.user_id
JOIN order_items i ON i.order_id = o.id;

CREATE TRIGGER trg_order_detail_update
INSTEAD OF UPDATE ON order_detail
FOR EACH ROW
BEGIN
  UPDATE orders SET total = NEW.total WHERE id = NEW.order_id;
  UPDATE order_items SET product_name = NEW.product_name
  WHERE order_id = NEW.order_id;
END;

-- now UPDATE on the view routes to both tables
UPDATE order_detail SET total = 99.99, product_name = 'Widget'
WHERE order_id = 5;

RAISE 与错误处理

RAISE 控制触发器失败如何传播:ABORT(最常见)取消语句并回滚其更改,ROLLBACK 回滚整个事务,FAIL 中止语句但保留事务中先前成功的语句,IGNORE 只跳过违规行。RAISE(IGNORE) 是批量加载时静默丢弃无效行的巧妙方式。

sqlite
-- RAISE aborts the statement with a custom error
CREATE TRIGGER trg_check_age
BEFORE INSERT ON users
FOR EACH ROW
WHEN NEW.age < 0
BEGIN
  SELECT RAISE(ABORT, 'age must be non-negative');
END;

-- ROLLBACK: undo the whole transaction, statement fails
CREATE TRIGGER trg_rollback_example
BEFORE UPDATE ON accounts
FOR EACH ROW
WHEN NEW.balance < 0
BEGIN
  SELECT RAISE(ROLLBACK, 'balance cannot go negative');
END;

-- FAIL: abort the current statement only (like ABORT)
-- IGNORE: skip this row, continue with the rest
CREATE TRIGGER trg_skip_invalid
BEFORE INSERT ON logs
FOR EACH ROW
WHEN NEW.level NOT IN ('info','warn','error')
BEGIN
  SELECT RAISE(IGNORE);
END;

管理触发器

触发器存储在 sqlite_master(或 TEMP 触发器的 temp.sqlite_master)中。DROP TRIGGER 删除它们;没有 ALTER TRIGGER——需删除并重建。TEMP 触发器是连接本地的,断开连接时消失,对调试有用。除了删除,没有内置方式临时禁用触发器;条件执行用 WHEN 子句。同一事件上的多个触发器在其时序内按创建顺序触发。

sqlite
-- list all triggers
SELECT name, tbl_name, sql FROM sqlite_master
WHERE type = 'trigger';

-- list triggers on a specific table
SELECT name, sql FROM sqlite_master
WHERE type = 'trigger' AND tbl_name = 'users';

-- drop a trigger
DROP TRIGGER IF EXISTS trg_users_audit;

-- triggers in a TEMP schema (connection-local)
CREATE TEMP TRIGGER trg_temp_log
AFTER INSERT ON users
FOR EACH ROW
BEGIN
  SELECT 'inserted ' || NEW.id;
END;

-- trigger timing order: BEFORE triggers fire in
-- creation order, then AFTER triggers in creation order.
11

PRAGMA 设置

PRAGMA 基础

PRAGMA 是 SQLite 的专有配置接口——设置控制日志、缓存、外键、完整性和查询规划器。设置是会话级(每连接)的,除非文档另有说明;在事务内设置可能被忽略。某些 PRAGMA(journal_mode、page_size)只在事务外或表创建前生效。用 PRAGMA compile_options 查看构建支持的功能。

sqlite
-- PRAGMA is SQLite's configuration knob (CLI and APIs)
-- read a setting
PRAGMA journal_mode;
PRAGMA cache_size;
PRAGMA foreign_keys;

-- set a setting (session scope unless noted)
PRAGMA foreign_keys = ON;
PRAGMA cache_size = -20000;   -- negative = KB, positive = pages
PRAGMA temp_store = MEMORY;

-- many PRAGMAs are no-ops below a certain SQLite version
-- check the compile-time options
PRAGMA compile_options;
-- e.g. ENABLE_FTS5, ENABLE_JSON1, MAX_ATTACHED, etc.

-- PRAGMAs are NOT part of SQL standard and are SQLite-only.

外键

外键强制在 SQLite 中默认关闭——这对从其他数据库迁移来的用户是个 notorious 陷阱。必须在每个连接上用 PRAGMA foreign_keys = ON 启用;不能在事务内设置。此设置存在是因为早期 schema(外键支持之前)否则会中断。生产环境始终启用。defer_foreign_keys 在按依赖违规顺序导入数据时很有用。

sqlite
-- foreign keys are OFF by default in SQLite!
PRAGMA foreign_keys = ON;   -- enable per connection

-- check current state
PRAGMA foreign_keys;

-- inspect FK definitions on a table
PRAGMA foreign_key_list(orders);
-- returns: id, seq, table, from, to, on_update, on_delete, match

-- enable FKs in your app's connection setup, every time
-- (this is a frequent source of bugs when moving from MySQL/PG)

-- deferred FK checking (checked at COMMIT, not per statement)
PRAGMA defer_foreign_keys = ON;
-- or declare DEFERRABLE INITIALLY DEFERRED on the FK

日志模式与 WAL

WAL(预写式日志)是大多数应用推荐的日志模式:读写互不阻塞,持久性良好。它在主 DB 旁创建 -wal 和 -shm 附属文件。WAL 按数据库文件持久(设置一次)。用 wal_checkpoint(TRUNCATE) 将 WAL 内容合并回主文件并回收空间。避免在网络文件系统上用 WAL——它需要共享内存。

sqlite
-- WAL: write-ahead logging (best for most apps)
PRAGMA journal_mode = WAL;
-- benefits: readers never block writers, writers never block
--           readers, crash recovery is fast
-- trade-off: creates -wal and -shm sidecar files

-- other modes: DELETE (default), TRUNCATE, PERSIST, MEMORY, OFF
PRAGMA journal_mode = DELETE;

-- WAL checkpoint: merge -wal back into the main file
PRAGMA wal_checkpoint;
PRAGMA wal_checkpoint(TRUNCATE);  -- also shrinks the -wal file

-- auto-checkpoint threshold (pages of WAL before auto-checkpoint)
PRAGMA wal_autocheckpoint = 1000;

-- WAL is persistent: setting it once keeps it for that DB file.

同步与持久性

synchronous 控制 SQLite 调用 fsync 的积极程度。FULL 最安全(每次提交都 fsync);使用 WAL 时 NORMAL 是推荐值(检查点时 fsync,非每次提交,实际损坏风险可忽略);OFF 有断电损坏风险——仅用于真正可丢弃的数据或可重建的缓存。page_size 应在 schema 创建前设置;之后更改需要 VACUUM。

sqlite
-- how thoroughly SQLite flushes to disk on commit
PRAGMA synchronous;        -- read current value
PRAGMA synchronous = FULL;       -- safest, slowest (default for rollback journal)
PRAGMA synchronous = NORMAL;     -- safe with WAL, fast (recommended for WAL)
PRAGMA synchronous = OFF;        -- fastest; corruption risk on power loss
PRAGMA synchronous = EXTRA;      -- even more careful than FULL

-- trade-off: durability vs speed
-- FULL: fsync at every commit (survives OS crash)
-- NORMAL (WAL): fsync at checkpoint, not every commit
-- OFF: no fsync — fast but may corrupt on power loss

-- page size (set BEFORE creating tables, ideally)
PRAGMA page_size = 4096;
PRAGMA page_size;

缓存、内存与临时存储

cache_size 是内存页缓存——对读密集型工作负载通常越大越好;负值以 KB 为单位,正值以页为单位。temp_store = MEMORY 将中间结果(排序、临时表)移到内存,加速大型聚合。mmap_size 启用读的内存映射 I/O,可加速大型只读数据库。根据应用的内存预算和访问模式调优。

sqlite
-- page cache (negative = KB, positive = pages)
PRAGMA cache_size = -65536;   -- 64 MB cache
PRAGMA cache_size = 20000;    -- 20000 pages

-- where temp tables and intermediate results live
PRAGMA temp_store = DEFAULT;  -- compile-time default
PRAGMA temp_store = FILE;     -- temp files
PRAGMA temp_store = MEMORY;   -- all temp data in RAM

-- mmap memory-mapped I/O for reads
PRAGMA mmap_size = 268435456;  -- 256 MB

-- soft heap limit for the SQLite library
PRAGMA soft_heap_limit = 100000000;  -- 100 MB

-- store prepared statements in memory between calls
PRAGMA cache_spill;

完整性与模式信息

integrity_check 验证整个数据库结构和内容(慢但彻底);quick_check 跳过内容验证且快得多。在备份或崩溃后运行 integrity_check。table_info 报告列元数据(用 table_xinfo 查看如 FTS5 rowid 等隐藏列)。index_list 和 index_info 揭示存在哪些索引及其列。database_list 显示附加的数据库。

sqlite
-- full integrity check (slow on large DBs)
PRAGMA integrity_check;
-- returns 'ok' or a list of problems

-- quick check (faster, less thorough)
PRAGMA quick_check;

-- list tables / schema
SELECT name, sql FROM sqlite_master WHERE type = 'table';

-- table info: columns, types, notnull, default, pk
PRAGMA table_info(users);
-- returns: cid, name, type, notnull, dflt_value, pk

-- extended table info (includes hidden columns from FTS/etc.)
PRAGMA table_xinfo(users);

-- index list with origin and unique flag
PRAGMA index_list(users);
PRAGMA index_info(idx_users_email);

-- database file header info
PRAGMA database_list;
12

日期与时间函数

date / time / datetime

SQLite 没有原生 DATE 类型;日期是 ISO-8601 文本 ('YYYY-MM-DD HH:MM:SS')。日期/时间函数接受多种输入格式(ISO、斜杠、月份名、通过 'unixepoch' 修饰符的 Unix 时间戳)。'now' 每语句评估一次而非每行——多行 UPDATE 中每行得到相同时间戳,通常正是你想要的。以 UTC 存储,用 localtime 格式化显示。

sqlite
-- current date/time (UTC by default)
SELECT date('now');              -- '2024-06-15'
SELECT time('now');              -- '14:30:00'
SELECT datetime('now');          -- '2024-06-15 14:30:00'
SELECT julianday('now');         -- 2460476.1042 (days since 4713 BC)

-- parse various formats
SELECT date('2024-06-15');
SELECT date('2024/06/15');
SELECT date('June 15, 2024');
SELECT datetime('2024-06-15 14:30:00.123');
SELECT date('2024-06-15','unixepoch');  -- from Unix timestamp (seconds)

-- Unix timestamp to datetime
SELECT datetime(1718450400, 'unixepoch');
SELECT datetime(1718450400, 'unixepoch', 'localtime');

strftime 格式化

strftime 是通用日期格式化器——格式码与 C 的 strftime 一致。用它提取日期部分(年、月、周几)用于分组或渲染自定义显示格式。按月分组时,strftime('%Y-%m', created) 产生 'YYYY-MM',自然排序和分组。SQLite 没有 DAYNAME()/MONTHNAME();如需工作日名称,将 strftime('%w', ...) 与 CASE 结合。

sqlite
-- strftime is the most flexible formatter
--   strftime(format, timestring, modifier, modifier, ...)
SELECT strftime('%Y-%m-%d', 'now');          -- '2024-06-15'
SELECT strftime('%Y-%m-%d %H:%M', 'now');    -- '2024-06-15 14:30'
SELECT strftime('%H:%M:%S', 'now');          -- '14:30:00'

-- common format codes
--   %Y year (4-digit)    %m month (01-12)   %d day (01-31)
--   %H hour (00-23)      %M minute (00-59)  %S second (00-59)
--   %j day of year       %w day of week (0=Sun)
--   %W week of year      %p AM/PM           %I hour (01-12)

-- get the day name
SELECT strftime('%w', '2024-06-15');   -- '6' (Saturday)
SELECT strftime('%Y-%m', 'now');       -- month bucket for grouping

修饰符 (算术)

修饰符从左到右链式应用以移动或对齐日期。间隔为 '+N 单位' / '-N 单位'(单位:days、hours、minutes、seconds、months、years)。'start of month/year/day' 对齐到周期边界——非常适合月度报表。'weekday N' 返回下一个工作日 N 的日期(今天匹配则为今天)。组合 'start of month'、'+1 month'、'-1 day' 得到当月最后一天——经典惯用法。

sqlite
-- shift a date by an interval
SELECT date('now', '+1 day');           -- tomorrow
SELECT date('now', '-1 month');         -- one month ago
SELECT date('now', '+1 year', '+1 day');-- next year + 1 day
SELECT datetime('now', '+2 hours', '+30 minutes');

-- start of period
SELECT date('now', 'start of month');       -- first day of month
SELECT date('now', 'start of year');        -- first day of year
SELECT date('now', 'start of day');         -- midnight today

-- weekday: next occurrence of a given weekday (0=Sun, 1=Mon, ... 6=Sat)
SELECT date('now', 'weekday 1');            -- next Monday
SELECT date('now', 'weekday 0', '+7 days'); -- Sunday after this one

-- combine modifiers
SELECT date('now', 'start of month', '+1 month', '-1 day'); -- last day of month

Unix 时间与儒略日

Unix 时间戳是自纪元以来的整数/秒;unixepoch() (3.38+) 直接返回整数。儒略日是连续的小数日计数——两个 julianday 值相减得到以天为单位的经过时间(乘以 86400 得秒)。对于年龄计算,除以 365.25 大致考虑闰年;法律精度需求请显式比较年/月/日组件。

sqlite
-- Unix timestamp (seconds since 1970-01-01 UTC)
SELECT strftime('%s', 'now');                  -- current Unix time (text)
SELECT unixepoch('now');                       -- integer Unix time (SQLite 3.38+)
SELECT datetime(1718450400, 'unixepoch');      -- back to datetime

-- Julian day (fractional days since 4713-01-01 BC)
SELECT julianday('now');                       -- 2460476.6042
SELECT julianday('2024-06-15') - julianday('2024-06-10');  -- 5.0 (days between)

-- compute elapsed time in seconds
SELECT (julianday('now') - julianday('2024-01-01')) * 86400.0 AS seconds_elapsed;

-- human-readable age from a birthdate
SELECT CAST((julianday('now') - julianday('1990-05-20')) / 365.25 AS INT) AS age;

时区与本地时间

SQLite 没有时区数据库——'now' 是 UTC,'localtime'/'utc' 修饰符依赖宿主操作系统的时区设置。推荐模式:始终以 UTC 存储时间戳(datetime('now') 是 UTC),仅在显示时用 'localtime' 修饰符转换为本地时间。这避免了不同时区服务器间和夏令时转换期间的歧义。绝不存储本地时间。

sqlite
-- 'now' is always UTC; convert to local time for display
SELECT datetime('now', 'localtime');          -- local wall-clock time
SELECT datetime('now');                       -- UTC

-- convert a stored UTC timestamp to local time
SELECT datetime(created, 'localtime') FROM users;

-- convert local time back to UTC for storage
SELECT datetime('2024-06-15 14:30:00', 'utc');

-- best practice: store UTC, format for display
CREATE TABLE events (ts TEXT DEFAULT (datetime('now')));  -- UTC
-- display:
SELECT datetime(ts, 'localtime') AS local_ts FROM events;

-- timezone is determined by the OS; SQLite has no TZ database.
13

字符串函数

长度与子串

length() 对 TEXT 计字符数,对 BLOB 计字节数——UTF-8 字节长度请先转换为 BLOB。substr() 是 1 索引的;负起始值从末尾计数。SQLite 没有原生 LPAD/RPAD——用字符串拼接和 substr 模拟。截断多字节文本时注意字节与字符的混淆:substr 按字符操作是安全的,但字节级切片不是。

sqlite
SELECT length('hello');              -- 5
SELECT length('日本語');             -- 3 (characters, not bytes)
SELECT length(x'00ff');             -- 2 (blob length in bytes)

SELECT substr('hello world', 7);    -- 'world' (1-indexed)
SELECT substr('hello world', 1, 5); -- 'hello' (length 5)
SELECT substr('hello', -3);         -- 'llo' (negative = from end)
SELECT substr('hello', -3, 2);      -- 'll'

-- byte length (vs character length)
SELECT length('日本語');            -- 3 (characters)
SELECT length(CAST('日本語' AS BLOB)); -- 9 (UTF-8 bytes)

-- left/right pad (workaround; no native LPAD/RPAD)
SELECT substr('0000' || '42', -4, 4);  -- '0042'

大小写与修剪

upper()/lower() 默认只影响 ASCII 字母——除非加载 ICU,否则非 ASCII 字符不变。trim() 默认删除首尾空白,或用第二个参数指定字符。replace() 做字面子串替换(无正则或模式)。没有原生 TRANSLATE() 做字符集替换——链式 replace() 调用或用自定义函数。

sqlite
SELECT upper('hello');       -- 'HELLO'
SELECT lower('HELLO');       -- 'hello'
SELECT upper('日本語');      -- '日本語' (no case to change)

SELECT trim('  hi  ');            -- 'hi'  (both sides)
SELECT ltrim('  hi  ');           -- 'hi   ' (left only)
SELECT rtrim('  hi  ');           -- '  hi' (right only)

-- trim specific characters
SELECT trim('xxhelloxx', 'x');    -- 'hello'
SELECT ltrim('aaabbb', 'a');      -- 'bbb'

-- replace characters
SELECT replace('a-b-c', '-', '/'); -- 'a/b/c'
SELECT replace('Hello World', 'o', '0'); -- 'Hell0 W0rld'

拼接与 printf

|| 运算符是标准 SQL 拼接符,但传播 NULL(NULL || x = NULL)——可能存在 NULL 时用 COALESCE 包装。printf()/format() 带来 C 风格格式化,是构建带填充、数字格式化和千位分隔符字符串的最简洁方式。quote() 返回安全引用并转义的字符串,用于嵌入 SQL——构建动态 SQL 时很有用。

sqlite
-- || operator concatenates
SELECT 'Hello' || ' ' || 'World';      -- 'Hello World'
SELECT username || ' <' || email || '>' FROM users;

-- NULL propagates through || (NULL || 'x' IS NULL)
SELECT 'a' || NULL || 'b';             -- NULL
-- use COALESCE to handle NULLs:
SELECT COALESCE(username, '') || ' <' || COALESCE(email, '') || '>';

-- printf-style formatting (also called format())
SELECT printf('%s has %d orders', 'alice', 5);  -- 'alice has 5 orders'
SELECT format('%,d', 1234567);                   -- '1,234,567' (3.38+)
SELECT printf('%5.2f', 3.14159);                 -- ' 3.14'
SELECT printf('%04d', 42);                       -- '0042'

-- quote() escapes a string for SQL
SELECT quote("It's alive");   -- '''It''s alive'''

分割与搜索

instr() 查找子串首次出现位置(1 索引;未找到为 0)。SQLite 没有原生 SPLIT_PART——用 substr + instr 或递归 CTE 模拟。'计数出现次数'惯用法用移除分隔符前后的长度差。char() 从码点构建字符串;unicode() 返回首字符的码点。完整文本分割可通过 json_each 加载 JSON 数组实现。

sqlite
-- find position of substring (1-indexed; 0 if not found)
SELECT instr('hello world', 'world');   -- 7
SELECT instr('hello', 'xyz');           -- 0

-- emulate split by getting the Nth field
-- (no native SPLIT_PART; use a CTE or substring + instr)
SELECT
  substr('a,b,c,d', 1, instr('a,b,c,d', ',') - 1) AS first;  -- 'a'

-- count occurrences of a substring
SELECT (length('a,b,c,d') - length(replace('a,b,c,d', ',', ''))) / length(',') AS commas;
-- 3

-- reverse a string
SELECT reverse('hello');   -- 'olleh'

-- character from integer code
SELECT char(65, 66, 67);   -- 'ABC'
SELECT unicode('A');       -- 65

LIKE / GLOB / 排序规则

LIKE 默认对 ASCII 不区分大小写(常见意外);用 COLLATE BINARY 区分大小写。GLOB 总是区分大小写且支持 shell 风格字符类。NOCASE 是内置的不区分大小写排序规则;将其应用到列的索引上,使不区分大小写的查询能使用索引。对于非 ASCII 大小写折叠,加载 ICU 扩展或存储带索引的小写副本列。

sqlite
-- LIKE: case-insensitive (ASCII), wildcards % and _
SELECT * FROM users WHERE username LIKE 'al%';
SELECT * FROM users WHERE username LIKE '_lice';

-- case-sensitive LIKE
SELECT * FROM users WHERE username LIKE 'Al%' COLLATE BINARY;

-- make LIKE case-insensitive for non-ASCII too (needs ICU)
-- or store a lowercased copy and query that
SELECT * FROM users WHERE LOWER(username) = LOWER('Alice');

-- GLOB: case-sensitive, Unix-style (* ? [abc] [a-z])
SELECT * FROM users WHERE username GLOB 'A*';
SELECT * FROM users WHERE username GLOB '[A-D]*';

-- built-in collations: BINARY (default), NOCASE, RTRIM
CREATE INDEX idx_users_name_ci ON users(username COLLATE NOCASE);
SELECT * FROM users WHERE username = 'ALICE' COLLATE NOCASE;

-- COLLATE in ORDER BY
SELECT * FROM users ORDER BY username COLLATE NOCASE;
14

JSON 函数

提取值

json_extract(和 -> / ->> 运算符,3.38+)是读取 JSON 的主力。$ 是根;$.key 访问对象属性;$.arr[i] 访问数组元素(0 索引)。->> 返回标量(text/integer/real),-> 返回 JSON(所以数组上的 '$.b' 返回 JSON 形式的数组)。将频繁查询的 JSON 字段存储为常规列,或在 json_extract 上添加表达式索引提升性能。

sqlite
-- json_extract: get a value by JSON path
SELECT json_extract('{"a": 1, "b": [10, 20, 30]}', '$.a');       -- 1
SELECT json_extract('{"a": 1, "b": [10, 20, 30]}', '$.b[1]');    -- 20
SELECT json_extract('{"a": 1, "b": [10, 20, 30]}', '$.b');       -- '[10,20,30]'

-- -> returns JSON; ->> returns scalar (text/integer/etc.)
SELECT '{"a": 1, "b": "x"}' -> '$.a';   -- 1 (as JSON)
SELECT '{"a": 1, "b": "x"}' ->> '$.a';  -- 1 (as scalar)
SELECT '{"a": 1, "b": "x"}' ->> '$.b';  -- 'x' (as text)

-- nested paths
SELECT json_extract('{"u": {"name": "alice"}}', '$.u.name');   -- 'alice'

-- array length
SELECT json_array_length('[1, 2, 3]');  -- 3
SELECT json_array_length('{"a": [1,2]}', '$.a');  -- 2

-- get object keys
SELECT json_each.name FROM json_each('{"a":1,"b":2}');  -- 'a', 'b'

构建 JSON

json_object 和 json_array 从 SQL 值构造 JSON(NULL 变为 JSON null,不会省略)。json_group_array 和 json_group_object (3.38+) 是从查询行构建 JSON 的聚合函数——直接从 SQL 生成 JSON API 极为有用。嵌套构造器构建复杂文档。用 json_quote 安全地将字符串嵌入为 JSON 字符串字面量。

sqlite
-- build a JSON object
SELECT json_object('id', 1, 'name', 'alice', 'active', 1);
-- {"id":1,"name":"alice","active":1}

-- build a JSON array
SELECT json_array(1, 2, 3, 'four', NULL);
-- [1,2,3,"four",null]

-- nest them
SELECT json_object(
  'user', json_object('id', 1, 'name', 'alice'),
  'roles', json_array('admin', 'editor')
);

-- aggregate rows into a JSON array
SELECT json_group_array(username) FROM users WHERE active = 1;
-- ["alice","bob","carol"]

-- aggregate into a JSON object (key:value)
SELECT json_group_object(username, email) FROM users WHERE active = 1;
-- {"alice":"[email protected]","bob":"[email protected]"}

-- quote a string as JSON
SELECT json_quote('hello');   -- '"hello"'

修改 JSON

json_set/insert/replace 区分插入与更新行为;json_remove 删除路径。这些返回新的 JSON 值——SQLite JSON 是不可变的,所以要持久化更改需用结果 UPDATE 列:UPDATE t SET data = json_set(data, '$.x', 1) WHERE id = 5。json_patch (RFC 7396) 合并对象;patch 中的 null 值删除键。对于大型文档,考虑将热字段存为单独列。

sqlite
-- json_set: insert or update a path
SELECT json_set('{"a": 1}', '$.b', 2);
-- {"a":1,"b":2}
SELECT json_set('{"a": 1}', '$.a', 99);
-- {"a":99}

-- json_insert: insert only if path does NOT exist
SELECT json_insert('{"a": 1}', '$.a', 99, '$.b', 2);
-- {"a":1,"b":2}  (a unchanged because it exists)

-- json_replace: update only if path EXISTS
SELECT json_replace('{"a": 1}', '$.a', 99, '$.b', 2);
-- {"a":99}  (b not added because it doesn't exist)

-- json_remove: delete a path
SELECT json_remove('{"a": 1, "b": 2}', '$.b');
-- {"a":1}

-- json_patch: merge per RFC 7396
SELECT json_patch('{"a": 1, "b": 2}', '{"b": 3, "c": 4}');
-- {"a":1,"b":3,"c":4}

查询 JSON 数组

json_each 和 json_tree 是将 JSON 展开为行的虚拟表值函数。json_each 处理单个数组/对象;json_tree 递归遍历整个结构,暴露 fullkey 路径。这解锁了对 JSON 数组的集合操作——过滤、连接和聚合。要查询列中的对象数组,用 LATERAL 风格连接:FROM users u, json_each(u.tags) e。

sqlite
-- json_each: virtual table, one row per array element
SELECT value FROM json_each('[10, 20, 30]');
-- 10, 20, 30

-- with index
SELECT key, value FROM json_each('["a","b","c"]');
-- 0,'a'  1,'b'  2,'c'

-- query an array stored in a column
SELECT u.username, e.value AS tag
FROM users u, json_each(u.tags) e
WHERE e.value LIKE 'admin%';

-- aggregate back to JSON
SELECT json_group_array(value)
FROM json_each('[10, 20, 30]')
WHERE value > 15;
-- [20,30]

-- json_tree: recursively walk nested structures
SELECT fullkey, value
FROM json_tree('{"a": {"b": [1, 2]}}');
-- '$','{"a":{"b":[1,2]}}'
-- '$.a','{"b":[1,2]}'
-- '$.a.b[0]',1
-- '$.a.b[1]',2

JSON 索引与验证

为 JSON 字段快速查找索引 json_extract 表达式——查询必须使用相同表达式才能受益。json_valid() 返回 1/0;在 CHECK 约束中用于强制只存 JSON 的列。json_type() 报告路径处的 JSON 类型('object'、'array'、'integer'、'real'、'string'、'boolean'、'null')。对于重 JSON 工作负载,将热字段非规范化为带索引的常规列,JSON 保留给灵活尾部。

sqlite
-- expression index on a JSON field for fast lookups
CREATE INDEX idx_events_type
ON events(json_extract(data, '$.type'));
SELECT * FROM events WHERE json_extract(data, '$.type') = 'login';

-- unique index on a JSON field
CREATE UNIQUE INDEX idx_users_email_json
ON users(json_extract(profile, '$.email'));

-- validate JSON
SELECT json_valid('{"a": 1}');     -- 1
SELECT json_valid('{a: 1}');       -- 0 (unquoted keys)

-- JSON type of a path
SELECT json_type('{"a": 1, "b": [1,2]}', '$.a');  -- 'integer'
SELECT json_type('{"a": 1, "b": [1,2]}', '$.b');  -- 'array'

-- check constraint for valid JSON columns
CREATE TABLE events (
  id INTEGER PRIMARY KEY,
  data TEXT CHECK (json_valid(data))
);
15

窗口函数

OVER 子句基础

窗口函数 (SQLite 3.25+) 在与当前行相关的一组行上计算,不像 GROUP BY 那样折叠行。OVER () 表示'所有行';OVER (ORDER BY ...) 定义排名顺序。ROW_NUMBER 唯一;RANK 平局后留空隙;DENSE_RANK 不留空隙。窗口函数出现在 SELECT 和 ORDER BY 中,不在 WHERE 中——用外层查询或 CTE 过滤。

sqlite
-- window functions: aggregate OVER a 'window' of rows
-- without collapsing rows (unlike GROUP BY)
SELECT username, age,
  AVG(age) OVER () AS overall_avg
FROM users;

-- ROW_NUMBER: unique sequential number per row
SELECT username,
  ROW_NUMBER() OVER (ORDER BY created) AS row_num
FROM users;

-- RANK / DENSE_RANK
SELECT username, age,
  RANK()       OVER (ORDER BY age DESC) AS rank,
  DENSE_RANK() OVER (ORDER BY age DESC) AS dense_rank
FROM users;

PARTITION BY

PARTITION BY 将行分组供窗口函数使用,类似 GROUP BY 但不折叠——每行保留其身份同时获得分组聚合。'每组前 N' 模式(ROW_NUMBER + 外层 WHERE rn <= N)是'每客户前 3 订单'、'每作者最新 5 篇'等问题的经典解决方案。通常比相关子查询快得多。

sqlite
-- per-group ranking / aggregation
SELECT username, role, age,
  ROW_NUMBER() OVER (PARTITION BY role ORDER BY age DESC) AS rank_in_role,
  AVG(age)     OVER (PARTITION BY role)                   AS avg_role_age
FROM users;

-- top N per group (common pattern)
SELECT * FROM (
  SELECT user_id, total,
    ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY total DESC) AS rn
  FROM orders
) WHERE rn <= 3;

-- percent of group total
SELECT username, role,
  age * 1.0 / SUM(age) OVER (PARTITION BY role) AS pct_of_role_total
FROM users;

窗口框架

框架定义窗口函数相对当前行能看到哪些行。UNBOUNDED PRECEDING..CURRENT ROW 给出运行总计。N PRECEDING..N FOLLOWING 给出滑动窗口(移动平均)。带 ORDER BY 的默认框架使用 RANGE,包含同级行(相同 ORDER BY 值);ROWS 是严格的行计数。误解框架与 ORDER BY 的关系是错误分析的常见来源。

sqlite
-- RUNNING TOTAL: cumulative sum over rows so far
SELECT username, total,
  SUM(total) OVER (ORDER BY created
                   ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running
FROM orders;

-- MOVING AVERAGE: 3-row window centered on current row
SELECT username, total,
  AVG(total) OVER (ORDER BY created
                   ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS moving_avg
FROM orders;

-- default frame for ORDER BY windows:
--   RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
-- default frame (no ORDER BY):
--   ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING

-- ROWS vs RANGE: ROWS counts rows; RANGE includes peers.

LAG 与 LEAD

LAG 和 LEAD 访问相对于当前行的其他行——对时间序列分析(日环比增量、连续检测)至关重要。可选第三个参数是越界行的默认值。FIRST_VALUE/LAST_VALUE 返回框架边缘的值;注意 LAST_VALUE 需要显式 UNBOUNDED FOLLOWING 框架,否则返回当前行的值(经典陷阱)。

sqlite
-- compare current row to previous / next row
SELECT username, created,
  LAG(created, 1)  OVER (ORDER BY created) AS prev_created,
  LEAD(created, 1) OVER (ORDER BY created) AS next_created
FROM users;

-- difference from previous value (time-series analysis)
SELECT date, sales,
  sales - LAG(sales, 1) OVER (ORDER BY date) AS day_over_day
FROM daily_sales;

-- LAG with default value and offset
SELECT username, created,
  LAG(username, 3, 'N/A') OVER (ORDER BY created) AS three_back
FROM users;

-- FIRST_VALUE / LAST_VALUE / NTH_VALUE
SELECT username, age,
  FIRST_VALUE(age) OVER (ORDER BY age) AS youngest,
  LAST_VALUE(age)  OVER (ORDER BY age
                         ROWS BETWEEN UNBOUNDED PRECEDING
                         AND UNBOUNDED FOLLOWING) AS oldest
FROM users;

NTILE 与百分位

NTILE(n) 将有序行分成 n 个大致相等的桶——用 4 表四分位、100 表百分位、10 表十分位。CUME_DIST 是值 <= 当前行行数的比例;PERCENT_RANK 是 0..1 的相对排名分数。这些是统计/分析查询的构建块。与所有窗口函数一样,它们不折叠行——用外层查询过滤。

sqlite
-- divide rows into N equal buckets (quartiles, percentiles)
SELECT username, age,
  NTILE(4) OVER (ORDER BY age) AS quartile
FROM users;

-- CUME_DIST: cumulative distribution (0..1)
SELECT username, age,
  CUME_DIST() OVER (ORDER BY age) AS cume_dist
FROM users;

-- PERCENT_RANK: rank as a fraction (0..1)
SELECT username, age,
  PERCENT_RANK() OVER (ORDER BY age) AS pct_rank
FROM users;

-- NTH_VALUE: value at a specific row in the frame
SELECT username, age,
  NTH_VALUE(age, 2) OVER (ORDER BY age) AS second_age
FROM users;
16

CTE (WITH 子句)

基础 CTE

用 WITH 定义的 CTE(公用表表达式)是命名的、有作用域的子查询——它提高可读性且可在同一语句中多次引用。SQLite 通常物化 CTE(写入临时表),所以性能类似派生表。用 CTE 将复杂查询分解为阶段、预聚合或避免重复子查询。它们是标准 SQL 且可移植。

sqlite
-- a CTE is a named temporary result set, scoped to one statement
WITH active_user_count AS (
  SELECT COUNT(*) AS n FROM users WHERE active = 1
)
SELECT username,
  (SELECT n FROM active_user_count) AS total_active
FROM users WHERE active = 1;

-- CTE in a join
WITH user_totals AS (
  SELECT user_id, SUM(total) AS spent
  FROM orders
  GROUP BY user_id
)
SELECT u.username, COALESCE(t.spent, 0) AS spent
FROM users u
LEFT JOIN user_totals t ON t.user_id = u.id
ORDER BY spent DESC;

-- CTEs make complex queries readable; they are syntactic sugar
-- (SQLite often materializes them, like a temp table).

多个 CTE

逗号分隔的多个 CTE 每个都可引用同一 WITH 中更早的 CTE——非常适合单语句中的分阶段数据管道。每个 CTE 是一个逻辑步骤;从上到下阅读反映数据流。这比嵌套子查询可维护得多。SQLite 物化每个 CTE;对于非常大的中间结果,考虑跨语句用临时表而非一个巨大 CTE 链。

sqlite
-- chain multiple CTEs, each able to reference earlier ones
WITH
active_users AS (
  SELECT id, username FROM users WHERE active = 1
),
user_orders AS (
  SELECT user_id, COUNT(*) AS n_orders, SUM(total) AS spent
  FROM orders
  GROUP BY user_id
),
labeled AS (
  SELECT au.username,
    COALESCE(uo.n_orders, 0) AS orders,
    COALESCE(uo.spent, 0)    AS spent
  FROM active_users au
  LEFT JOIN user_orders uo ON uo.user_id = au.id
)
SELECT * FROM labeled
WHERE orders > 0
ORDER BY spent DESC;

递归 CTE

递归 CTE 有锚点(基础情况)和递归成员,用 UNION(去重)或 UNION ALL 连接。它们是遍历树(组织结构图、分类树、 threaded 评论)和图的标准 SQL 方式。SQLite 限制递归深度。'生成序列'技巧很宝贵,因为 SQLite 缺少 GENERATE_SERIES(尽管有 series 扩展)。

sqlite
-- a recursive CTE references itself; perfect for hierarchies
WITH RECURSIVE subordinates AS (
  -- anchor: top-level manager
  SELECT id, name, manager_id, 0 AS depth
  FROM employees WHERE manager_id IS NULL
  UNION ALL
  -- recursive: their direct reports, one level deeper
  SELECT e.id, e.name, e.manager_id, s.depth + 1
  FROM employees e
  JOIN subordinates s ON e.manager_id = s.id
)
SELECT depth, id, name FROM subordinates ORDER BY depth, name;

-- generate a series of numbers (no native GENERATE_SERIES)
WITH RECURSIVE cnt(n) AS (
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM cnt WHERE n < 10
)
SELECT n FROM cnt;

层级查询

递归 CTE 在自引用表上大放异彩。第一个示例构建从根到每个节点的斜杠分隔路径——对面包屑导航有用。第二个查找节点的所有后代(子树)。如果可能存在循环,用 UNION(非 UNION ALL)防止无限循环;否则 UNION ALL 更快。对于非常深的树,监控性能——没有适当索引时递归 CTE 可能很慢。

sqlite
-- categories with parent/child self-reference
CREATE TABLE categories (
  id INTEGER PRIMARY KEY,
  name TEXT,
  parent_id INTEGER REFERENCES categories(id)
);

-- full path from root to each node
WITH RECURSIVE cat_path(id, name, path) AS (
  SELECT id, name, name
  FROM categories WHERE parent_id IS NULL
  UNION ALL
  SELECT c.id, c.name, cp.path || ' > ' || c.name
  FROM categories c
  JOIN cat_path cp ON c.parent_id = cp.id
)
SELECT id, path FROM cat_path ORDER BY path;

-- find all descendants of a node
WITH RECURSIVE descendants(id) AS (
  SELECT id FROM categories WHERE id = 5   -- root of subtree
  UNION ALL
  SELECT c.id FROM categories c
  JOIN descendants d ON c.parent_id = d.id
)
SELECT * FROM categories WHERE id IN (SELECT id FROM descendants);

CTE 优化

SQLite 3.35+ 支持 MATERIALIZED / NOT MATERIALIZED 提示控制 CTE 评估。MATERIALIZED 强制写入临时表(适合被多次引用的昂贵 CTE);NOT MATERIALIZED 内联 CTE 使优化器能下推谓词(适合廉价 CTE)。规划器通常自己选得好;仅在 EXPLAIN QUERY PLAN 显示糟糕计划时使用提示。递归 CTE 总是被物化。

sqlite
-- MATERIALIZED: force SQLite to materialize (temp table)
WITH cte AS MATERIALIZED (
  SELECT user_id, SUM(total) AS spent FROM orders GROUP BY user_id
)
SELECT u.username, cte.spent
FROM users u JOIN cte ON cte.user_id = u.id;

-- AS NOT MATERIALIZED: inline the CTE (may run multiple times)
WITH cte AS NOT MATERIALIZED (
  SELECT COUNT(*) FROM users WHERE active = 1
)
SELECT * FROM products WHERE id < (SELECT * FROM cte);

-- choose MATERIALIZED when the CTE is expensive and used
-- multiple times; NOT MATERIALIZED when it's cheap or used
-- once and could benefit from predicate pushdown.

-- recursive CTEs are always materialized.
17

备份与恢复

.dump (文本备份)

.dump 以纯 SQL INSERT 语句导出数据库——人类可读、可 diff、跨 SQLite 版本和架构可移植。它是最兼容的备份格式。恢复时将 SQL 重放到新数据库中。--nosys 跳过 sqlite_sequence 和其他内部表。对于大数据库,.dump 很慢且产生巨大文件;改用 .backup(二进制副本)或在线备份 API。

sqlite
-- dump the entire database as SQL text (portable)
sqlite3 mydb.sqlite .dump > backup.sql

-- dump a single table
sqlite3 mydb.sqlite ".dump users" > users.sql

-- dump only data (no schema)
sqlite3 mydb.sqlite ".dump" | grep '^INSERT' > data.sql

-- restore from a dump
sqlite3 newdb.sqlite < backup.sql
-- or inside the CLI:
.read backup.sql

-- dump in a specific order to satisfy FK constraints
sqlite3 mydb.sqlite ".dump --nosys" > backup.sql

-- .dump works on attached databases too
ATTACH 'archive.sqlite' AS archive;
.dump archive

.backup (二进制副本)

.backup 使用 SQLite 在线备份 API 执行安全的在线二进制副本——即使在写入器活动时也能工作,产生事务一致快照。这是备份运行中数据库的推荐方式。输出是有效的 SQLite 文件,所以恢复就是文件复制。与 .dump 不同,二进制格式版本和架构兼容但不可人类阅读。

sqlite
-- binary copy to another file (online, safe with active writers)
sqlite3 mydb.sqlite ".backup 'backup.sqlite'"

-- inside the CLI
.backup backup.sqlite
.backup main backup.sqlite     -- source 'main', target file

-- backup to an attached database
ATTACH 'backup.sqlite' AS bk;
.backup main bk;
DETACH bk;

-- .backup uses the Online Backup API internally: it copies
-- page-by-page and handles concurrent writers safely, so it's
-- the right choice for live production backups.

-- restore = just copy the file back (or use it directly)
cp backup.sqlite mydb.sqlite

在线备份 API

在线备份 API (sqlite3_backup_*) 是黄金标准备份方法:逐页复制实时数据库,正确处理并发写入并产生一致快照。Python 的 sqlite3.Connection.backup() 和 Node 的 better-sqlite3 .backup() 封装了它。它在 WAL 模式下工作。如果写入正在进行,原始文件复制可能产生损坏的备份——只在停止的数据库上或用文件级快照使用文件复制。

sqlite
-- the sqlite3 CLI's .backup uses this C API; in Python:
import sqlite3
src = sqlite3.connect("mydb.sqlite")
dst = sqlite3.connect("backup.sqlite")
src.backup(dst)
dst.close()
src.close()

-- with progress callback
def progress(remaining, total):
    print(f"{total - remaining}/{total} pages copied")
src.backup(dst, pages=progress)

-- this is the safest backup method: it's online (doesn't block
-- writers), transactionally consistent, and works on WAL-mode
-- databases. Prefer it over raw file copy for production.

-- Node.js (better-sqlite3):
const backup = db.backup('backup.sqlite');
while (!backup.completed) backup.step(-1);
backup.delete();

附加数据库

ATTACH DATABASE 将最多 10 个数据库文件作为命名模式(main、temp 加附加的)带入一个连接。可用模式限定名(main.users、archive.orders)跨它们查询、连接和复制数据。这是归档(将旧行移到归档文件)、分片和合并工作流的基础。DETACH 关闭模式但不影响文件。WAL 模式下跨数据库事务是原子的。

sqlite
-- work with multiple DB files in one connection
ATTACH DATABASE 'archive.sqlite' AS archive;
ATTACH DATABASE 'lookup.sqlite' AS lookup;

-- query across databases
SELECT u.username, a.note
FROM main.users u
JOIN archive.old_users a ON u.id = a.id;

-- copy a table between databases
CREATE TABLE archive.users_copy AS
SELECT * FROM main.users;

-- move data: insert into one DB, delete from another
INSERT INTO archive.orders SELECT * FROM main.orders WHERE created < '2023-01-01';
DELETE FROM main.orders WHERE created < '2023-01-01';

-- list attached databases
.databases

-- detach (does NOT delete the file)
DETACH DATABASE archive;

-- limit: 10 attached databases (compile-time SQLITE_MAX_ATTACHED).

恢复与损坏

integrity_check 检测结构损坏;.recover (3.29+) 是从损坏文件抢救数据的最佳工具——它提取可读行并跳过坏页,产生可管道到新数据库的 SQL。如果 .recover 失败,回退到 .dump 并接受部分丢失。大多数损坏来自不安全的文件操作(复制实时 DB、网络文件系统、synchronous=OFF 时断电)。恢复前始终备份。

sqlite
-- check integrity
PRAGMA integrity_check;
PRAGMA quick_check;

-- recover from a corrupt database (best effort)
sqlite3 corrupt.sqlite ".recover" > recovered.sql
sqlite3 clean.sqlite < recovered.sql

-- .recover extracts everything readable, skipping corrupt pages

-- if .recover fails, try dumping what you can
sqlite3 corrupt.sqlite ".dump" > partial.sql 2>errors.txt
sqlite3 clean.sqlite < partial.sql

-- point-in-time from a backup file
cp nightly_backup.sqlite mydb.sqlite

-- after a crash, WAL is replayed automatically on next open;
-- if stuck, checkpoint manually:
sqlite3 mydb.sqlite "PRAGMA wal_checkpoint(TRUNCATE);"

-- prevent corruption: never overwrite a live DB file,
-- never share a DB on a network filesystem without WAL off.
18

导入与导出

CSV 导入

.import 将 CSV 读入表;如果表不存在,SQLite 以全 TEXT 列创建(通常不是你想要的——先创建带正确类型的表)。--skip N 跳过标题行。.mode csv 配置解析器处理带嵌入逗号和换行符的引号字段。对于大型导入,用 BEGIN/COMMIT 包裹并临时 PRAGMA synchronous=OFF 可获得 10 倍以上加速。批量加载期间禁用索引,之后重建。

sqlite
-- import a CSV file into a table
.mode csv
.import users.csv users
-- ^ creates 'users' table with text columns named from the header row

-- import into an EXISTING table (column order must match)
.mode csv
.import --skip 1 users.csv users   -- --skip 1 to skip header

-- recommended: create the table first with proper types
CREATE TABLE users (id INTEGER, name TEXT, email TEXT);
.mode csv
.import --skip 1 users.csv users

-- from the shell, one-shot
sqlite3 mydb.sqlite ".mode csv" ".import users.csv users"

-- handle quotes/escapes
.mode csv
.separator ,
.import data.csv mytable

CSV 导出

.mode csv 配合 .headers on 产生带标题行的标准 CSV——用 .output 管道到文件。shell 标志 -header -csv 加输出重定向可在一行内完成而无需进入 CLI。TSV 用 .mode list 配合 .separator "\t"。对于大型导出,这是基于流的且内存高效。要导出多个表,在查询间切换 .output。

sqlite
-- export a query to CSV
.mode csv
.headers on
.output users.csv
SELECT id, username, email FROM users;
.output stdout

-- from the shell, one-shot
sqlite3 mydb.sqlite -header -csv \
  "SELECT id, username, email FROM users" > users.csv

-- export all rows of a table
.mode csv
.headers on
.output products.csv
SELECT * FROM products;
.output stdout

-- custom separator
.mode list
.separator "|"
.output users.txt
SELECT id, username FROM users;
.output stdout

JSON 导出

.mode json 每行输出一个 JSON 对象(无包裹数组)。要正确的 JSON 数组,用 json_group_array(json_object(...))——这让你完全控制字段名和嵌套,并通过相关子查询处理嵌套数组。这是直接从 SQL 构建 JSON API 而无需 ORM 的强大方式。对于大结果集,逐行流式 .mode json 而非在内存中构建一个巨大数组。

sqlite
-- export a query as JSON (one JSON object per row)
.mode json
.output users.json
SELECT id, username, email FROM users;
.output stdout

-- build a single JSON array with json_group_array
SELECT json_group_array(json_object(
  'id', id, 'username', username, 'email', email
)) AS users_json
FROM users;
-- [{"id":1,"username":"alice","email":"[email protected]"},...]

-- nested JSON (orders with their items)
SELECT json_object(
  'order_id', o.id,
  'total', o.total,
  'items', (SELECT json_group_array(json_object('name', i.name, 'qty', i.qty))
            FROM order_items i WHERE i.order_id = o.id)
) AS order_json
FROM orders o;

导入 JSON

SQLite 没有 JSON 的 .import——将文件作为 TEXT 加载(通过应用代码或带 INSERT 的 .read),然后用 json_each/json_extract 解析。json_each(数组) 将数组展开为行用于基于集合的插入。对于大型 JSON 文件,在应用中流式解析(不要将整个文件加载到内存)。加载期间用 json_valid() 验证,并在 JSON 列上用 CHECK 约束保持持续完整性。

sqlite
-- load a JSON file into a text column, then parse
CREATE TABLE raw (data TEXT);
-- (no built-in .import for JSON; load via CLI or app)

-- once loaded, parse with json_each (array) or json_extract (object)
SELECT json_extract(data, '$.name') AS name,
       json_extract(data, '$.age')  AS age
FROM raw;

-- expand a JSON array into rows
INSERT INTO users (username, email)
SELECT json_extract(value, '$.username'),
       json_extract(value, '$.email')
FROM raw, json_each(raw.data);

-- import via Python
import sqlite3, json
db = sqlite3.connect("mydb.sqlite")
with open("users.json") as f:
    for row in json.load(f):
        db.execute("INSERT INTO users(username,email) VALUES(?,?)",
                   (row["username"], row["email"]))
db.commit()

Excel 与其他格式

对于 Excel,导出 CSV(Excel 直接打开);.mode excel (3.36+) 添加 BOM 使 UTF-8 正确渲染。.mode markdown 生成可粘贴到文档的表格标记。.mode insert 生成 INSERT 语句——适合为测试数据库填充种子或在 schema 间迁移数据。.mode quote 引用每个值,当数据包含可能混淆其他工具的逗号/换行/引号时有用。.mode table/box 为终端绘制 ASCII 表格。

sqlite
-- Excel: export CSV (Excel opens it) or use .mode excel
.mode csv
.headers on
.output report.csv
SELECT * FROM monthly_report;
.output stdout

-- .mode excel (3.36+): CSV with BOM, friendly to Excel
.mode excel
.output report.csv
SELECT * FROM monthly_report;
.output stdout

-- .mode markdown: GitHub-flavored markdown tables
.mode markdown
SELECT id, username, email FROM users LIMIT 5;

-- .mode insert: generate INSERT statements
.mode insert users
.output seed.sql
SELECT id, username, email FROM users;
.output stdout
-- produces: INSERT INTO "users" VALUES(1,'alice','[email protected]');

-- .mode quote: CSV with all values quoted
.mode quote
.output safe.csv
SELECT * FROM users;
.output stdout
19

应用集成

Python (sqlite3)

Python 的 sqlite3 内置于标准库。始终使用 ? 占位符(或 :name)——绝不用 f-strings 或 %——以防止 SQL 注入并正确处理引号。executemany 对批量插入比循环 execute 快得多。设置 row_factory = sqlite3.Row 以按列名访问。用上下文管理器 (with conn:) 自动提交/回滚。对于并发,每线程一个连接(sqlite3 默认禁止跨线程共享)。

sqlite
import sqlite3

# connect (creates the file if missing)
conn = sqlite3.connect("mydb.sqlite")
conn.row_factory = sqlite3.Row  # dict-like rows
cur = conn.cursor()

# parameterized query (NEVER use string formatting!)
cur.execute(
    "INSERT INTO users (username, email) VALUES (?, ?)",
    ("alice", "[email protected]"),
)
conn.commit()

# query with parameters
cur.execute("SELECT id, username FROM users WHERE active = ?", (1,))
for row in cur:
    print(row["id"], row["username"])

# named parameters
cur.execute(
    "SELECT * FROM users WHERE age >= :min_age",
    {"min_age": 18},
)

# bulk insert (fast)
cur.executemany(
    "INSERT INTO users (username, email) VALUES (?, ?)",
    [("bob", "[email protected]"), ("carol", "[email protected]")],
)
conn.commit()
conn.close()

Node.js (better-sqlite3)

better-sqlite3 是同步的(无回调/Promise 样板)且是最快的 Node.js SQLite 驱动。预处理语句可重用——准备一次,运行多次。.transaction(fn) 将函数包裹在 BEGIN/COMMIT(或抛出时 ROLLBACK)中,比每语句自动提交快得多。用 ? 占位符避免 SQL 注入。启用 WAL 以支持并发。对于基于 Promise 的 API,包装调用或改用 sqlite/sqlite3。

sqlite
const Database = require("better-sqlite3");
const db = new Database("mydb.sqlite");
db.pragma("journal_mode = WAL");

// prepared statement (synchronous, fast)
const insert = db.prepare(
  "INSERT INTO users (username, email) VALUES (?, ?)"
);
const info = insert.run("alice", "[email protected]");
console.log(info.lastInsertRowid);

// query multiple rows
const select = db.prepare("SELECT id, username FROM users WHERE active = ?");
const rows = select.all(1);  // array of objects

// query one row
const one = select.get(1);

// transaction (atomic, fast)
const insertMany = db.transaction((users) => {
  for (const u of users) insert.run(u.username, u.email);
});
insertMany([{username: "bob"}, {username: "carol"}]);

db.close();

连接 URI 与模式

URI 文件名 (file:...) 解锁只读、共享缓存和内存模式——在 Python 中传 uri=True 或将 file: 字符串传给 Node 驱动。:memory: 默认每连接独立;file:name?mode=memory&cache=shared 创建在同一进程跨连接共享的命名内存 DB(适合测试)。设置 busy_timeout 使等待锁的写入器不立即报错。始终每连接设置 PRAGMA foreign_keys = ON。

sqlite
# open read-only
sqlite3.connect("file:mydb.sqlite?mode=ro", uri=True)

# open in-memory (vanishes on close)
sqlite3.connect(":memory:")
# or via URI:
sqlite3.connect("file::memory:", uri=True)

# shared in-memory database (visible across connections in same process)
sqlite3.connect("file:memdb1?mode=memory&cache=shared", uri=True)

# with a busy timeout (wait instead of erroring on locks)
conn = sqlite3.connect("mydb.sqlite", timeout=30)

# Node.js: same URI form
const db = new Database("file:mydb.sqlite?mode=ro", { readonly: true });

# Node.js in-memory
const mem = new Database(":memory:");

# Python: per-connection pragmas
conn.execute("PRAGMA journal_mode = WAL")
conn.execute("PRAGMA foreign_keys = ON")

事务与并发

事务将语句原子分组——全有或全无。SQLite 一次只允许一个写入器(数据库级写锁),但 WAL 模式允许并发读取器。Python 中默认 isolation_level 将 DML 包裹在隐式事务中(需要提交)。要显式控制,设 isolation_level = None 并自行发 BEGIN/COMMIT。用 busy_timeout(PRAGMA busy_timeout 或连接 timeout)使等待锁的写入器等待而非立即抛出 SQLITE_BUSY。

sqlite
# Python: explicit transaction
conn = sqlite3.connect("mydb.sqlite")
conn.isolation_level = None  # autocommit off -> manual BEGIN
cur = conn.cursor()
cur.execute("BEGIN")
try:
    cur.execute("UPDATE accounts SET bal = bal - 100 WHERE id = 1")
    cur.execute("UPDATE accounts SET bal = bal + 100 WHERE id = 2")
    cur.execute("COMMIT")
except Exception:
    cur.execute("ROLLBACK")
    raise

// Node.js (better-sqlite3) transaction
const transfer = db.transaction((from, to, amt) => {
  db.prepare("UPDATE accounts SET bal = bal - ? WHERE id = ?").run(amt, from);
  db.prepare("UPDATE accounts SET bal = bal + ? WHERE id = ?").run(amt, to);
});
transfer(1, 2, 100);

-- SQLite allows ONE writer at a time; many readers in WAL mode.
-- Use a busy_timeout to wait for locks instead of failing fast.

预处理语句与安全

参数化查询(? 占位符)是最重要的安全实践——它们防止 SQL 注入并正确处理类型转换/转义。绝不将用户输入拼接到 SQL 字符串中。占位符仅适用于值;标识符(表/列名)不能参数化——用白名单验证它们。对于 LIKE,转义用户输入中的 % 和 _ 以防用户注入通配符。这适用于每种语言和驱动。

sqlite
-- SQL injection vulnerability (NEVER do this):
-- app: "SELECT * FROM users WHERE name = '" + user_input + "'"
-- user_input = "'; DROP TABLE users; --"
-- result:    SELECT * FROM users WHERE name = ''; DROP TABLE users; --'

-- SAFE: parameterized query (placeholders are NOT string substitution)
-- Python:
cur.execute("SELECT * FROM users WHERE name = ?", (user_input,))
-- Node.js:
stmt.get(user_input);

-- identifiers (table/column names) CANNOT be parameterized;
-- validate against a whitelist:
allowed_tables = {"users", "orders", "products"}
if table not in allowed_tables:
    raise ValueError("bad table")
cur.execute(f"SELECT * FROM {table} WHERE id = ?", (id_,))

-- LIKE with user input: escape wildcards
cur.execute(
    "SELECT * FROM users WHERE name LIKE ? ESCAPE '\'",
    (user_input.replace("\", "\\").replace("%", "\%").replace("_", "\_") + "%",)
)
20

性能优化

EXPLAIN QUERY PLAN

EXPLAIN QUERY PLAN 是头号调优工具。'USING INDEX' = 好;'SCAN'(全表扫描)= 大表上通常不好。对于连接,检查连接列是否索引以及规划器是否扫描更大的表。EXPLAIN(不带 QUERY PLAN)显示 VDBE 字节码——很少需要但对深度调试有用。实验性 EXPERT 命令可建议索引 (3.36+),或用 .expert CLI 点命令。

sqlite
-- see how SQLite executes a query (essential for tuning)
EXPLAIN QUERY PLAN
SELECT * FROM users WHERE email = '[email protected]';

-- good: uses an index
--   SEARCH users USING INDEX idx_users_email (email=?)

-- bad: full table scan
--   SCAN users

-- join plan
EXPLAIN QUERY PLAN
SELECT u.username, o.total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.active = 1;

-- automated index recommendation (3.36+, experimental)
SELECT * FROM users WHERE email = 'x';
-- then:
EXPLAIN QUERY PLAN SELECT * FROM sqlite_stat1;

-- lower-level: EXPLAIN (bytecode)
EXPLAIN SELECT * FROM users WHERE id = 1;

VACUUM 与 ANALYZE

VACUUM 重建文件,回收删除产生的空间并整理页面碎片——在大删除后或对写密集型数据库定期运行。VACUUM INTO (3.27+) 写入干净副本而不长时间锁定源。ANALYZE 更新查询规划器选择索引所用的 sqlite_stat1 统计——在批量加载或 schema/数据变更后运行。auto_vacuum=INCREMENTAL 无需完整 VACUUM 即可增量回收空间。

sqlite
-- VACUUM: rebuild the database file, reclaiming free space
VACUUM;
-- after large deletes, this shrinks the file and defrags pages

-- VACUUM INTO: write a defragmented copy to a new file (3.27+)
VACUUM INTO 'clean.sqlite';

-- incremental VACUUM (only with auto_vacuum enabled)
PRAGMA auto_vacuum = INCREMENTAL;
PRAGMA incremental_vacuum(100);  -- free up to 100 pages

-- ANALYZE: update planner statistics for better query plans
ANALYZE;                          -- all tables
ANALYZE users;                    -- one table
ANALYZE users(idx_users_email);   -- one index

-- run ANALYZE after bulk loads or major data changes

批量加载

批量加载:用单个事务包裹(最大的加速——每行自动提交慢得灾难性),临时禁用 synchronous 和日志,删除并重建索引,并考虑增加 PRAGMA cache_size。.import 点命令比 INSERT 语句快。对于真正巨大的加载,考虑预构建数据库的 .backup。加载生产数据后始终恢复安全 PRAGMA。

sqlite
-- fastest bulk load into a fresh table
PRAGMA synchronous = OFF;     -- risk: power-loss corruption (use only for one-off loads)
PRAGMA journal_mode = MEMORY;
PRAGMA temp_store = MEMORY;

BEGIN;
CREATE TABLE big (id INTEGER, data TEXT);
.import data.csv big
COMMIT;

-- then turn safety back on
PRAGMA synchronous = FULL;
PRAGMA journal_mode = WAL;

-- faster still: drop indexes, load, recreate
DROP INDEX idx_big_data;
-- ... load ...
CREATE INDEX idx_big_data ON big(data);

-- use a transaction (10-100x faster than autocommit per row)
BEGIN;
INSERT INTO big VALUES (1, 'a');
-- ... thousands of inserts ...
COMMIT;

FTS5 (全文搜索)

FTS5 是 SQLite 的全文搜索引擎——它为快速 MATCH 查询索引文本,远快于扫描每行的 LIKE '%term%'。它支持布尔(AND/OR/NOT)、前缀(term*)、短语("...")和相关性排名。外部内容模式 (content='posts') 避免重复数据。用触发器(基表上的 INSERT/UPDATE/DELETE)保持 FTS 索引同步。FTS5 非常适合边输边搜、自动补全和文档搜索。

sqlite
-- create a full-text search virtual table
CREATE VIRTUAL TABLE posts_fts USING fts5(
  title, body,
  content='posts', content_rowid='id'
);

-- populate it (keep in sync with triggers or rebuild)
INSERT INTO posts_fts(rowid, title, body)
  SELECT id, title, body FROM posts;

-- search with MATCH (much faster than LIKE '%term%')
SELECT p.* FROM posts p
JOIN posts_fts f ON f.rowid = p.id
WHERE posts_fts MATCH 'sqlite python';

-- ranking by relevance
SELECT p.title, rank
FROM posts p
JOIN posts_fts f ON f.rowid = p.id
WHERE posts_fts MATCH 'sqlite OR python'
ORDER BY rank;

-- prefix and phrase queries
SELECT * FROM posts_fts WHERE posts_fts MATCH 'sql*';        -- prefix
SELECT * FROM posts_fts WHERE posts_fts MATCH '"full text"'; -- exact phrase

常见陷阱与调优

大多数 SQLite 性能问题来自少数原因:WHERE/JOIN/ORDER BY 列缺少索引、缺少事务(每行自动提交是批量插入慢的头号原因)、外键静默关闭、以及宽表上的 SELECT *。在每个连接设置中启用 WAL 和外键。批量执行大型 DELETE 以避免长写锁和巨大临时空间。重大数据变更后运行 ANALYZE 使规划器选择好计划。

sqlite
-- 1. Enable foreign keys (OFF by default!)
PRAGMA foreign_keys = ON;

-- 2. Use WAL for concurrency
PRAGMA journal_mode = WAL;

-- 3. Index foreign keys AND join columns
CREATE INDEX idx_orders_user_id ON orders(user_id);

-- 4. Avoid SELECT * (more I/O, breaks on schema change)
SELECT id, username FROM users;

-- 5. Use transactions for multi-statement writes
BEGIN;
-- ... several inserts/updates ...
COMMIT;

-- 6. Don't index every column (slows writes)
-- index selectively on WHERE/JOIN/ORDER BY columns.

-- 7. Use LIMIT on exploratory queries
SELECT * FROM big_table LIMIT 10;

-- 8. Batch UPDATEs/DELETEs to avoid long locks
DELETE FROM logs WHERE id IN (
  SELECT id FROM logs WHERE ts < '2023-01-01' LIMIT 10000
);

-- 9. Prefer EXISTS over IN for large subquery results
-- 10. Run ANALYZE after big data changes.

这篇内容对您有帮助吗?