在线代码运行BanShow Run

SQL 常见报错与原因

SQL 常见报错对照表,每一节标题就是你会在屏幕上看到的报错原文,下面给出原因和改法。可以复制到浏览器里亲手触发一遍,比背教程记得牢。

打开 SQL 运行器 →

下面这些 SQL 都会报错(或者不报错但结果是错的),是故意写出来给你看的。复制到编辑器里点运行,对照输出区读一遍报错原文——比背十遍教程有用。

本站用的是 SQLite,报错信息和你以后在公司里用的 MySQL / PostgreSQL 措辞不完全一样,但「什么意思」是相通的。每节标题就是 SQLite 报出来的原文。

no such table: xxx

原因:找不到这张表。要么名字打错了,要么表还没建,要么你换了个浏览器标签页——本站的数据库存在内存里,刷新页面就没了。

sql
SELECT * FROM 学生表;      -- no such table: 学生表(多打了一个"表"字)
SELECT * FROM students;    -- no such table: students(本站示例库是中文表名)
SELECT * FROM 成绩 WHERE 科目 = '数学';   -- 这句是对的

怎么修:不确定有哪些表时,查一下 SQLite 自己维护的目录表:

sql
SELECT name AS 表名, type AS 类型 FROM sqlite_master WHERE type IN ('table', 'view') ORDER BY name;

sqlite_master 里存着当前库的全部表、视图和索引的定义,sql 那一列就是当初的建表语句:

sql
SELECT sql FROM sqlite_master WHERE name = '学生';

no such column: xxx

原因:找不到这一列。这是初学者遇到最多的一种报错,但真正的病因往往不是拼写。

sql
SELECT 年岭 FROM 学生;     -- no such column: 年岭(把"龄"打成了"岭")
SELECT 姓名, COUNT(*) FROM 学生;   -- 这句是对的

最容易踩的坑:中文标点。下面这句看起来完全正常,但逗号是全角的 ,,SQLite 不认它是分隔符,于是把 姓名,年龄 整个当成一个列名:

sql
SELECT 姓名,年龄 FROM 学生;

报错是 no such column: 姓名,年龄——注意报错里的列名特别长,这就是全角逗号的指纹。同理,用中文引号 ‘一班’ 包字符串,会被当成列名:

sql
SELECT 姓名 FROM 学生 WHERE 班级 = ‘一班’;

报错 no such column: ‘一班’。标点符号必须在英文输入法下打。

还有两种情况也会报这个错:

sql
-- 忘了写 FROM,SQLite 以为 姓名 是个变量
SELECT 姓名;

-- 用了聚合,却没告诉它从哪张表取
SELECT COUNT(*) AS 人数 GROUP BY 班级;

怎么修:先把这张表有哪些列查出来,逐个对照:

sql
PRAGMA table_info(学生);

near "xxx": syntax error

原因:语法写错了。near "xxx" 里的 xxx 是 SQLite 读到这儿读不下去了的那个词,错误经常在它前面一点。

sql
-- 关键字拼错
SELCET * FROM 学生;

-- SELECT 列表里漏了逗号
SELECT 姓名 年龄 FROM 学生;

-- 子句顺序错了:WHERE 必须写在 GROUP BY 前面
SELECT 班级, COUNT(*) FROM 学生 GROUP BY 班级 WHERE 年龄 > 16;

-- WHERE 里不能用聚合函数
SELECT 姓名 FROM 学生 WHERE AVG(年龄) > 16;

-- ORDER BY 多列之间也要逗号
SELECT 姓名, 年龄 FROM 学生 ORDER BY 年龄 DESC 姓名;

-- 别名撞上保留字
SELECT 姓名 AS order FROM 学生;

怎么修:记住固定顺序,写查询时按这个顺序检查自己有没有跳步或颠倒:

text
SELECT … FROM … JOIN … ON … WHERE … GROUP BY … HAVING … ORDER BY … LIMIT …

WHERE AVG(年龄) > 16 那一句报的其实是 misuse of aggregate function AVG(),意思很直白:聚合函数算的是「一堆行」的结果,而 WHERE 是逐行判断的,那时候还没有「一堆行」可算。要筛选聚合结果得用 HAVING:

sql
-- 正确写法
SELECT 班级, COUNT(*) AS 人数, ROUND(AVG(年龄), 1) AS 平均年龄
FROM 学生
GROUP BY 班级
HAVING AVG(年龄) >= 16.5;

反过来,HAVING 也不能乱用。没有 GROUP BY 的查询写 HAVING 会报 HAVING clause on a non-aggregate query:

sql
SELECT 姓名 FROM 学生 HAVING 年龄 > 16;

这里应该用 WHERE。

unrecognized token: "'xxx"

原因:引号没有闭合。SQLite 一路读到语句结束都没找到另一个单引号。

sql
SELECT 姓名 FROM 学生 WHERE 班级 = '一班;

怎么修:数一下单引号是不是成对出现。字符串里的每一个值都要自己带一对引号,IN ('一班', '二班') 不能写成 IN ('一班, 二班')。

顺带说一下引号的规矩,SQLite 比标准宽松,三种都能用:

sql
SELECT '单引号是字符串', "双引号也能当字符串(但不推荐)" AS a;
SELECT "姓名" AS 双引号包列名, `姓名` AS 反引号包列名 FROM 学生 LIMIT 1;

推荐:字符串一律单引号 ',标识符(表名、列名)不加引号,只有撞上保留字时才用双引号 " 包起来。

2 values for 3 columns

原因:INSERT 的列数和值的个数对不上。

sql
INSERT INTO 学生 (id, 姓名, 班级) VALUES (100, '新人');

反过来写多了也一样报错:3 values for 2 columns。

sql
INSERT INTO 学生 (id, 姓名) VALUES (100, '新人', '一班');

如果不写列名,就必须给全每一列的值,报错会换成另一种说法:

sql
INSERT INTO 学生 VALUES (13);
-- table 学生 has 5 columns but 1 values were supplied

怎么修:把列名和值上下对齐着写,一眼就能数出来:

sql
INSERT INTO 学生 (id, 姓名, 班级, 年龄, 城市)
VALUES           (13, '新人', '三班',  17,   '武汉');

SELECT * FROM 学生 WHERE id = 13;

建议永远把列名写全,不要依赖列的顺序——表结构一改,省略列名的写法就会静悄悄地插错列。

NOT NULL constraint failed: 学生.姓名

原因:这一列声明了 NOT NULL,你却没给值(或者给了 NULL)。报错里 表名.列名 直接告诉你是哪一列。

sql
INSERT INTO 学生 (id, 班级, 年龄) VALUES (101, '一班', 16);

怎么修:把那一列补上。查表结构时看 notnull 那一列,1 就是不允许为空:

sql
PRAGMA table_info(学生);

UNIQUE constraint failed: 学生.id

原因:这一列(或这一组列)声明了 UNIQUE,你插入的值和已有行重复了。主键重复也是这个报错,因为主键本身就带唯一性。

sql
-- 学生表里已经有 id = 1 的张伟
INSERT INTO 学生 (id, 姓名, 班级) VALUES (1, '新人', '一班');

INTEGER PRIMARY KEY 的列还会自动分配一个没用过的 id,所以正常情况不该自己指定:

sql
DROP TABLE IF EXISTS 演示;
CREATE TABLE 演示 (id INTEGER PRIMARY KEY, 名称 TEXT);

INSERT INTO 演示 (名称) VALUES ('甲'), ('乙'), ('丙');
SELECT * FROM 演示;                  -- id 自动是 1、2、3

怎么修:三种选择,按场景挑一个。

sql
DROP TABLE IF EXISTS 课程;
CREATE TABLE 课程 (
  id   INTEGER PRIMARY KEY AUTOINCREMENT,
  名称 TEXT NOT NULL UNIQUE,
  学分 REAL NOT NULL DEFAULT 1.0
);
INSERT INTO 课程 (名称, 学分) VALUES ('数据库原理', 3.0), ('数据结构', 4.0);

-- 选择一:插入前先查一下有没有
SELECT COUNT(*) AS 已存在 FROM 课程 WHERE 名称 = '数据结构';

-- 选择二:INSERT OR IGNORE,重复就跳过,不报错
INSERT OR IGNORE INTO 课程 (名称, 学分) VALUES ('数据结构', 9.9);
SELECT 名称, 学分 FROM 课程;

-- 选择三:upsert,重复就改成更新
INSERT INTO 课程 (名称, 学分) VALUES ('数据结构', 5.0)
ON CONFLICT(名称) DO UPDATE SET 学分 = excluded.学分;
SELECT 名称, 学分 FROM 课程;

CHECK constraint failed

原因:这一列上有 CHECK (...) 条件,插入的值不满足。SQLite 会把整个 CHECK 表达式打在报错里。

sql
DROP TABLE IF EXISTS 课程;
CREATE TABLE 课程 (
  名称     TEXT NOT NULL UNIQUE,
  开课学期 TEXT CHECK (开课学期 IN ('上', '下'))
);

INSERT INTO 课程 (名称, 开课学期) VALUES ('操作系统', '中');
-- CHECK constraint failed: 开课学期 IN ('上', '下')

怎么修:报错里已经把允许的取值列出来了,照着装进去即可。想给约束起个好认的名字,用 CONSTRAINT:

sql
DROP TABLE IF EXISTS 课程2;
CREATE TABLE 课程2 (
  名称     TEXT NOT NULL,
  开课学期 TEXT CONSTRAINT 学期只能是上或下 CHECK (开课学期 IN ('上', '下'))
);

INSERT INTO 课程2 (名称, 开课学期) VALUES ('编译原理', '中');
-- CHECK constraint failed: 学期只能是上或下

这样报错里就是你自己写的中文,一眼知道违反了哪条规矩。(注意 CHECK 的报错只带约束名、不带表名,所以约束名要起得能看出是哪张表。)

FOREIGN KEY constraint failed

原因:外键指向的那一行不存在,或者你要删的那一行正被别人引用着。

sql
-- SQLite 默认不检查外键,先打开开关(本站的库在整个页面会话里是持续的,
-- 打开一次就一直有效;点「重置数据库」或刷新页面会换成新连接,要重新打开)
PRAGMA foreign_keys = ON;
PRAGMA foreign_keys;                  -- 返回 1 表示已开启

-- 学生表里没有 id = 999 这个人
INSERT INTO 成绩 (学生id, 科目, 分数) VALUES (999, '数学', 80);

删除方向也会被拦住。成绩 表声明了 学生id REFERENCES 学生(id),所以只要张伟还有成绩记录,就不能删他:

sql
PRAGMA foreign_keys = ON;
SELECT COUNT(*) AS 张伟的成绩条数 FROM 成绩 WHERE 学生id = 1;
DELETE FROM 学生 WHERE id = 1;

最容易踩的坑:开关没打开时,脏数据会静悄悄地写进去。下面这段不会报错,插进去的 999 是个不存在的学生:

sql
-- 没有 PRAGMA foreign_keys = ON
INSERT INTO 成绩 (学生id, 科目, 分数) VALUES (999, '数学', 80);
SELECT * FROM 成绩 WHERE 学生id = 999;

不过如果你刚在上面那两段里跑过 PRAGMA foreign_keys = ON,这段反而会报 FOREIGN KEY constraint failed——因为开关在整个页面会话里都还开着。点一下「重置数据库」换成全新连接,才能看到「静悄悄写进去」的原始行为。

怎么修:想删被引用的行,三种办法。

sql
PRAGMA foreign_keys = ON;

-- 办法一:先删子表,再删父表
BEGIN;
DELETE FROM 成绩 WHERE 学生id = 1;
DELETE FROM 学生 WHERE id = 1;
COMMIT;
SELECT COUNT(*) AS 还剩几个学生 FROM 学生;

-- 办法二:建表时就声明 ON DELETE CASCADE,删父行时子行自动跟着删
DROP TABLE IF EXISTS 选课;
CREATE TABLE 选课 (
  学生id INTEGER NOT NULL REFERENCES 学生(id) ON DELETE CASCADE,
  课程id INTEGER NOT NULL,
  PRIMARY KEY (学生id, 课程id)
);
INSERT INTO 学生 (id, 姓名, 班级, 年龄) VALUES (91, '待删学生', '一班', 16);
INSERT INTO 选课 (学生id, 课程id) VALUES (91, 101), (91, 102);
SELECT COUNT(*) AS 删除前的选课数 FROM 选课 WHERE 学生id = 91;
DELETE FROM 学生 WHERE id = 91;
SELECT COUNT(*) AS 删除后的选课数 FROM 选课 WHERE 学生id = 91;

ON DELETE 有四种行为:CASCADE(跟着删)、SET NULL(子表该列置空)、RESTRICT(立刻报错,默认行为)、NO ACTION(和 RESTRICT 几乎一样,只是检查时机稍晚)。

no such function: NOW

原因:你用的是 MySQL / SQL Server 的函数,SQLite 里没有。

sql
SELECT NOW();                          -- no such function: NOW
SELECT DATEDIFF('2026-10-04', '2026-01-01');   -- no such function: DATEDIFF

怎么修:对照下表换成 SQLite 的写法。本站跑的是 SQLite 3.49.1,比较新的函数(CONCAT、IF、SUBSTRING)它已经有了,但日期函数和 MySQL 完全不是一套。

sql
SELECT date('now')                             AS 今天,
       datetime('now', 'localtime')            AS 本地现在,
       strftime('%Y年%m月', '2026-10-04')       AS 格式化,
       julianday('2026-10-04') - julianday('2026-01-01') AS 相差天数,
       CAST(strftime('%s', '2026-10-04') AS INTEGER)     AS 时间戳,
       length('数据库原理')                      AS 长度,
       random() % 100                          AS 随机数;
MySQL 里习惯写的SQLite 里应该写说明
NOW()datetime('now')结果是 UTC,见下方提醒
CURDATE()date('now')
DATEDIFF(a, b)julianday(a) - julianday(b)结果是小数,取整天用 CAST(... AS INTEGER)
DATE_FORMAT(d, fmt)strftime(fmt, d)两个参数的顺序是反的
LEN(s)length(s)length 数字符数,CAST(x AS BLOB) 后再 length 才是字节数
RAND()random()返回整数,取范围用 abs(random()) % 100
ISNULL(a, b)IFNULL(a, b)ISNULL 在 SQLite 里是后缀运算符(x ISNULL),当函数写会直接语法报错
CONCAT(a, b)a || bSQLite 3.44 起也有 CONCAT,但它遇到 NULL 会跳过,MySQL 则整个返回 NULL
IF(c, a, b)IIF(c, a, b) 或 CASE WHENSQLite 3.49 也认 IF,但 CASE WHEN 各家数据库都通用
LIMIT 5, 10LIMIT 10 OFFSET 5两者等价(都是跳过 5 取 10),但逗号写法的参数顺序和 OFFSET 写法刚好相反,容易看错

要注意:'now' 取的是 UTC 时间,比北京时间早 8 小时。要本地时间就写 datetime('now', 'localtime'),或者 datetime('now', '+8 hour')。

table xxx already exists

原因:表已经存在,又执行了一次 CREATE TABLE。

sql
CREATE TABLE 学生 (id INTEGER);

怎么修:加 IF NOT EXISTS,或者先 DROP。

sql
CREATE TABLE IF NOT EXISTS 学生 (id INTEGER);      -- 已存在就什么都不做
DROP TABLE IF EXISTS 临时表;                        -- 不存在也不报错
CREATE TABLE 临时表 (a INTEGER);

要注意:DROP TABLE 是不可逆的,数据一起没了。本站的库存在内存里,刷新页面就恢复初始状态,所以可以放心试;但在真实项目里执行 DROP 前请先确认连的是哪个库。

cannot modify xxx because it is a view

原因:视图是「存起来的查询」,本身不存数据,所以不能直接 UPDATE / DELETE / INSERT。

sql
CREATE VIEW IF NOT EXISTS 一班名单 AS
SELECT id, 姓名 FROM 学生 WHERE 班级 = '一班';

SELECT * FROM 一班名单;

UPDATE 一班名单 SET 姓名 = '改名了' WHERE id = 1;

怎么修:改视图背后那张表,视图的结果会跟着变。

sql
-- 建过一次的视图在页面会话里会一直留着,加 IF NOT EXISTS 重复运行就不报错
CREATE VIEW IF NOT EXISTS 一班名单 AS
SELECT id, 姓名 FROM 学生 WHERE 班级 = '一班';

UPDATE 学生 SET 姓名 = '张伟(已改名)' WHERE id = 1;
SELECT * FROM 一班名单;                 -- 视图里也跟着变了

-- 不再需要就用 DROP VIEW 删掉(只删这个查询定义,不动 学生 表的数据)
DROP VIEW IF EXISTS 一班名单;
SELECT 姓名 FROM 学生 WHERE id = 1;

sub-select returns 2 columns - expected 1

原因:这个位置的子查询只允许返回一列,你返回了两列。

sql
SELECT (SELECT 姓名, 年龄 FROM 学生 WHERE id = 1) AS x;

同样,WHERE id IN (...) 里的子查询也只能有一列:

sql
SELECT * FROM 学生 WHERE id IN (SELECT id, 姓名 FROM 学生);

怎么修:只留你需要的那一列。

sql
SELECT s.姓名, (SELECT COUNT(*) FROM 成绩 c WHERE c.学生id = s.id) AS 科目数
FROM 学生 s
ORDER BY 科目数 DESC, s.姓名
LIMIT 3;

datatype mismatch

原因:值的类型和列的要求冲突。最常见的是往 INTEGER PRIMARY KEY 里塞了转不成整数的东西——主键必须能唯一标识一行,不能是文本。

sql
DROP TABLE IF EXISTS 编号表;
CREATE TABLE 编号表 (id INTEGER PRIMARY KEY);

INSERT INTO 编号表 (id) VALUES ('abc');

怎么修:主键用整数或专门的 ID 字符串(但列类型要写 TEXT,不能是 INTEGER PRIMARY KEY)。

sql
DROP TABLE IF EXISTS 编号表;
CREATE TABLE 编号表 (id INTEGER PRIMARY KEY AUTOINCREMENT);

INSERT INTO 编号表 DEFAULT VALUES;
INSERT INTO 编号表 DEFAULT VALUES;
SELECT * FROM 编号表;

-- 想用业务编号当主键,类型写 TEXT
DROP TABLE IF EXISTS 订单;
CREATE TABLE 订单 (订单号 TEXT PRIMARY KEY, 金额 REAL);
INSERT INTO 订单 VALUES ('SO-2026-0001', 199.0);
SELECT * FROM 订单;

cannot start a transaction within a transaction

原因:已经 BEGIN 过了,又 BEGIN 了一次。

sql
BEGIN;
INSERT INTO 学生 (id, 姓名, 班级, 年龄) VALUES (150, '甲', '一班', 16);
BEGIN;

怎么修:一个事务只 BEGIN 一次,用 COMMIT 或 ROLLBACK 收尾,再开下一个。

sql
BEGIN;
INSERT INTO 学生 (id, 姓名, 班级, 年龄) VALUES (151, '乙', '一班', 16);
COMMIT;

BEGIN;
INSERT INTO 学生 (id, 姓名, 班级, 年龄) VALUES (152, '丙', '一班', 16);
ROLLBACK;                          -- 这一条被撤销了

SELECT id, 姓名 FROM 学生 WHERE id >= 150;

事务里任何一句报错,SQLite 都会自动把整个事务回滚,不会留下改了一半的数据——这正是事务的意义。

卡住不动了

原因:查询的数据量算不完。本站运行 15 秒后会强制终止,你会看到「已停止」而不是结果。

三种典型写法:

sql
-- 一、递归 CTE 忘了终止条件,会一直加下去
WITH RECURSIVE 数列(n) AS (
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM 数列          -- 少了 WHERE n < 10
)
SELECT COUNT(*) FROM 数列;
sql
-- 二、两张表做笛卡尔积,行数 = 左表 × 右表
SELECT COUNT(*) AS 行数 FROM 学生 CROSS JOIN 成绩;   -- 8 × 23 = 184,还很快
sql
-- 三、自连接忘写 a.id < b.id,结果是平方级别
SELECT COUNT(*) AS 对数 FROM 学生 a JOIN 学生 b ON a.城市 = b.城市;

怎么修:递归 CTE 一定要有 WHERE 终止条件,并且先让它只跑几轮确认逻辑对了再放开:

sql
WITH RECURSIVE 数列(n) AS (
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM 数列 WHERE n < 10
)
SELECT COUNT(*) AS 项数, MAX(n) AS 最大值 FROM 数列;

写大查询时养成 LIMIT 的习惯:先 SELECT ... LIMIT 10 看看形状对不对,确认了再去掉。

不报错,但结果是错的

这一类最坑——SQL 跑通了,输出区也有结果,只是结果不是你想的那个。

sql
-- 1. 除以 0 不报错,返回 NULL
SELECT 1 / 0 AS 除零, 1.0 / 0 AS 浮点除零;

-- 2. 两个整数相除,小数部分被直接砍掉
SELECT 7 / 2 AS 整数除法, 7.0 / 2 AS 浮点除法, ROUND(7 * 1.0 / 2, 2) AS 另一种写法;

-- 3. CAST 转不动就返回 0,不报错
SELECT CAST('abc' AS INTEGER) AS 转不动, CAST('95分' AS INTEGER) AS 只取前面的数字;

-- 4. BETWEEN 写反了,静悄悄返回空结果
SELECT COUNT(*) AS 写反了 FROM 学生 WHERE 年龄 BETWEEN 18 AND 16;
SELECT COUNT(*) AS 正常写法 FROM 学生 WHERE 年龄 BETWEEN 16 AND 18;

-- 5. NOT IN 的列表里只要有一个 NULL,结果永远是空
SELECT COUNT(*) AS 含NULL FROM 学生 WHERE 城市 NOT IN ('北京', NULL);
SELECT COUNT(*) AS 不含NULL FROM 学生 WHERE 城市 NOT IN ('北京');

-- 6. NULL 参与的比较结果是 NULL,不是真也不是假
SELECT COUNT(*) AS 用等号 FROM 学生 WHERE 城市 = NULL;
SELECT COUNT(*) AS 用ISNULL FROM 学生 WHERE 城市 IS NULL;
sql
-- 7. GROUP BY 之后 SELECT 了一个没分组的列,SQLite 不报错,但取的是"组里随便某一行"
SELECT 班级, 姓名 FROM 学生 GROUP BY 班级;

-- 想明确要哪一行,得自己写清楚(这里取每班年龄最小的)
SELECT 班级, MIN(年龄) AS 最小年龄 FROM 学生 GROUP BY 班级;

-- 8. 标量子查询返回多行时,SQLite 只取第一行,不报错
SELECT (SELECT 姓名 FROM 学生) AS 只取了第一行;

-- 9. JOIN 忘写 ON,变成笛卡尔积,行数直接爆炸
SELECT COUNT(*) AS 忘了ON FROM 学生 JOIN 成绩;
SELECT COUNT(*) AS 正常JOIN FROM 学生 JOIN 成绩 ON 成绩.学生id = 学生.id;

-- 10. UPDATE / DELETE 忘写 WHERE,改的是全表
UPDATE 学生 SET 城市 = '全部清空';
SELECT COUNT(*) AS 全表都变了 FROM 学生 WHERE 城市 = '全部清空';
sql
-- 11. 整数太大时会悄悄变成浮点数,末尾几位不再精确
SELECT 9223372036854775807 AS 最大整数, 9223372036854775807 + 1 AS 加一之后;

-- 12. 'now' 是 UTC,比北京时间早 8 小时
SELECT datetime('now') AS UTC时间, datetime('now', 'localtime') AS 本地时间;

-- 13. LIKE 只对 ASCII 字母(a-z / A-Z)忽略大小写,其它字符一律精确匹配
SELECT 'ABC' LIKE 'abc' AS 英文忽略大小写,
       '张伟' LIKE '张%' AS 中文前缀匹配,
       '张伟' LIKE '张_' AS 下划线只匹配一个字符;

要避免这些,只有两条习惯:改数据之前先用同样的条件 SELECT 一次,以及看到结果先问一句「这个数字的量级合理吗」。

定位报错的四个习惯

  1. 二分法注释。把一半语句用 -- 注释掉再运行,能跑就说明错在另一半,来回两三次就能定位到具体那一行。
sql
-- 排查时先把前面的都注释掉,只留一句
-- INSERT INTO 学生 (id, 姓名, 班级) VALUES (200, '甲', '一班');
-- UPDATE 学生 SET 年龄 = 18 WHERE id = 200;
SELECT * FROM 学生 WHERE id = 200;
  1. 一次只跑一条。本站编辑器里可以选中一段再运行,把可疑的那条单独拎出来,报错的定位会准很多。
  1. 先确认表结构。八成 no such column 都是因为记错了列名:
sql
PRAGMA table_info(成绩);
PRAGMA foreign_key_list(成绩);
PRAGMA index_list(学生);
  1. 看执行计划。查询很慢或结果奇怪时,让 SQLite 说明它打算怎么找:
sql
EXPLAIN QUERY PLAN
SELECT s.姓名, c.分数
FROM 学生 s JOIN 成绩 c ON c.学生id = s.id
WHERE c.科目 = '数学'
ORDER BY c.分数 DESC;

输出里 SCAN 是全表扫描(慢),SEARCH ... USING INDEX 才是走了索引(快)。

换一门语言