SQL数据库学习入门:从基础查询到常用操作

SQL数据库学习入门:从基础查询到常用操作
SQL 是操作关系型数据库的常用语言。学习 SQL,不只是记住几条查询语句,还要理解数据如何组织、查询结果如何组合,以及修改数据时怎样保证准确和安全。本文以 SQLite 为例,逐步介绍建表、增删改查、排序、连接、聚合、事务和索引,并说明容易忽略的限制。
选择 SQLite 是为了让示例容易运行:它通常不需要单独启动数据库服务,适合本地练习、嵌入式应用和小型项目。MySQL、PostgreSQL 等数据库的核心 SQL 概念大体相通,但数据类型、函数、分页、自动生成主键、并发和管理方式都有差异。本文会标出 SQLite 特有之处;迁移到其他数据库时,应查阅对应产品的文档并实际验证。
一、先理解数据库、表和记录
关系型数据库通常把数据保存在表中。表由列和行组成:列描述字段及其类型,行是一条记录。例如,学生表可以有学号、姓名和邮箱等列;每个学生对应一行。多个表可以通过共同的键关联起来。
- 数据库:保存表、索引、视图等对象的集合。
- 表:按行和列组织的一类数据。
- 主键:用于唯一识别一行的列或列组合。
- 外键:用于引用另一张表中记录的约束。
- SQL:用于定义结构、查询和修改数据的语言。
SQL 关键字通常不区分大小写,但把关键字写成大写、表名和列名写成小写,可以提高可读性。字符串一般用单引号括起来;双引号常用于标识符,具体规则可能随数据库和配置而变。不要把用户输入直接拼进 SQL 字符串。
二、创建表:先把数据规则说清楚
下面的示例可以在 SQLite 命令行中运行,也可以通过支持 SQLite 的图形工具执行。它创建课程和报名两张表,用外键表示报名记录属于某门课程。
PRAGMA foreign_keys = ON;
CREATE TABLE courses (
course_id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
fee NUMERIC NOT NULL DEFAULT 0,
active INTEGER NOT NULL DEFAULT 1
CHECK (active IN (0, 1))
);
CREATE TABLE enrollments (
enrollment_id INTEGER PRIMARY KEY,
student_name TEXT NOT NULL,
email TEXT,
course_id INTEGER NOT NULL,
enrolled_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (course_id) REFERENCES courses(course_id)
);
CREATE TABLE 用于创建表。例子里的 NOT NULL 禁止字段为空,DEFAULT 提供默认值,CHECK 限制字段取值,外键则声明表之间的引用关系。SQLite 中,INTEGER PRIMARY KEY 通常对应行标识;如需明确请求自动递增编号,可以使用 INTEGER PRIMARY KEY AUTOINCREMENT,但它有额外的存储和维护成本,不应仅因为“编号要递增”就默认使用。
SQLite 的类型系统比较宽松,列声明的类型名不一定像某些数据库那样严格限制实际存入的数据类型。NUMERIC 也不保证适合精确金额运算:涉及货币时,应根据数据库类型系统和业务精度设计字段,例如使用最小货币单位的整数,或选择支持精确小数的数据库类型。SQLite 的日期时间没有独立的专用日期类型,常见做法是存 ISO 8601 文本、Unix 时间戳或其他约定格式,并保持全项目一致。
外键约束需要特别留意:SQLite 的外键检查通常需要对每个数据库连接启用 PRAGMA foreign_keys = ON。这条设置应在连接建立后执行,不能假设所有运行环境都已启用。其他数据库是否需要显式开启外键,取决于产品及其配置。
三、插入数据:明确列名,避免依赖顺序
使用 INSERT INTO 写入数据。建议始终列出目标字段,这样调整表结构或字段顺序时,不容易意外把数据写入错误列。
INSERT INTO courses (title, fee, active)
VALUES ('SQL 入门', 199, 1);
INSERT INTO courses (title)
VALUES ('数据库基础');
INSERT INTO enrollments (student_name, email, course_id)
VALUES ('林青', '[email protected]', 1);
第二条语句没有提供 fee 和 active,数据库会使用定义好的默认值。写入报名记录时,course_id 必须对应已存在的课程;如果外键已启用,引用不存在课程的操作会失败。
批量插入可以在一条语句中写多组值:
INSERT INTO courses (title, fee, active)
VALUES
('SQL 查询进阶', 299, 1),
('数据建模', 0, 1);
大批量写入时,不要无条件把所有记录拼成超长 SQL。数据库和驱动对 SQL 长度、参数数量等可能有限制;也可能受到内存、事务大小和锁竞争影响。应使用驱动提供的批量执行方式,并结合所用数据库的限制调整批次。不同数据库的参数上限并不相同,不能把某个产品的数值当成通用规则。
四、查询数据:SELECT 与条件筛选
SELECT 用于读取数据。初学时可以用星号取出所有列,但在应用代码和长期维护的查询中,更建议明确列名,避免表结构增加字段后无意读取多余数据。
SELECT course_id, title, fee
FROM courses;
SELECT title, fee
FROM courses
WHERE active = 1;
WHERE 指定筛选条件。常见比较运算符有 =、<>、>、>=、< 和 <=;多个条件可以用 AND、OR 连接。条件较复杂时加括号,明确运算顺序。
SELECT title, fee
FROM courses
WHERE active = 1
AND (fee = 0 OR fee < 200);
判断空值要用 IS NULL 或 IS NOT NULL,不能写成 = NULL。在 SQL 的三值逻辑中,空值表示未知或缺失,普通等号比较无法得到通常意义上的真或假。
SELECT student_name
FROM enrollments
WHERE email IS NULL;
IN 用于匹配一组值,BETWEEN 用于范围筛选,通常包含两端:
SELECT title
FROM courses
WHERE course_id IN (1, 3, 5);
SELECT title, fee
FROM courses
WHERE fee BETWEEN 100 AND 300;
LIKE 用于模式匹配,% 表示任意长度字符,_ 表示一个字符。大小写是否敏感、非 ASCII 字符如何比较等行为,可能受数据库、排序规则和配置影响,不要假设各产品表现完全一致。
SELECT title
FROM courses
WHERE title LIKE '%SQL%';
五、排序与分页:结果顺序不是默认保证
如果没有 ORDER BY,不能依赖数据库按插入顺序返回结果。要稳定地展示数据,必须明确排序条件;若排序列可能相同,可以再加一个唯一字段作为次级排序条件。
SELECT course_id, title, fee
FROM courses
ORDER BY fee DESC, course_id ASC;
ASC 表示升序,DESC 表示降序。分页可以用 LIMIT 和 OFFSET,这是 SQLite 支持的写法;其他数据库可能提供不同语法。
SELECT course_id, title
FROM courses
ORDER BY course_id
LIMIT 10 OFFSET 20;
分页前应先确定稳定的排序规则。对于数据量很大的表,较大的偏移量可能让数据库扫描并跳过大量记录,查询会变慢。此时可考虑基于上一页最后一个键的“游标分页”,具体 SQL 取决于排序字段和数据库。例如按唯一递增的 course_id 排序时,可以用 WHERE course_id > 上一页最后一个编号 再取下一批。
六、修改与删除:WHERE 是关键边界
UPDATE 修改记录,DELETE 删除记录。最常见的严重错误,是忘记写 WHERE:这会让语句作用于表中的所有记录。
UPDATE courses
SET fee = 249
WHERE course_id = 1;
DELETE FROM enrollments
WHERE enrollment_id = 1;
执行修改或删除前,建议先用同样条件执行 SELECT,确认目标范围;在重要数据上先备份,并在事务中操作。实际应用还应检查驱动返回的受影响行数,避免把“没有匹配记录”误当成成功更新。涉及并发修改时,仅靠先查后改还可能产生竞态,需结合事务、约束或带条件的原子更新设计。
SELECT enrollment_id, student_name
FROM enrollments
WHERE course_id = 1;
DELETE FROM table_name 不带条件会删除该表的所有行,但表结构仍然存在;DROP TABLE 则会移除表及其结构。两者不是一回事。不同数据库还可能有 TRUNCATE 等操作,其事务、日志和触发器行为因产品而异,不能简单视为可互换。
七、连接多张表:JOIN 与数据关系
数据库把相关数据放在不同表中,可以减少重复,并通过键进行组合。JOIN 根据条件把多张表的行关联起来。下面查询每条报名记录及对应课程:
SELECT
e.student_name,
e.email,
c.title AS course_title
FROM enrollments AS e
JOIN courses AS c
ON e.course_id = c.course_id;
JOIN 不带修饰时通常表示内连接(INNER JOIN):只返回连接条件在两边都匹配的行。若希望保留左表所有记录,即使右表没有匹配项,可以用 LEFT JOIN;右表字段在未匹配时会是 NULL。
SELECT
c.title,
e.student_name
FROM courses AS c
LEFT JOIN enrollments AS e
ON e.course_id = c.course_id;
连接条件写错会造成结果重复或行数暴增。例如,只按姓名连接而姓名并不唯一,可能把多条记录错误地配在一起;漏写 ON 条件则可能形成笛卡尔积,即两张表每一行互相组合。查询前先确认两边字段代表的关系,并检查结果行数是否符合预期。表别名能让较长的查询更易读,但字段名有歧义时必须加上表别名。
八、聚合与分组:从明细得到统计
聚合函数可以对多行数据做统计。常见函数包括 COUNT、SUM、AVG、MIN 和 MAX。COUNT(*) 统计行数;COUNT(列) 统计该列非空的值,因此两者在列含空值时结果可能不同。
SELECT COUNT(*) AS course_count
FROM courses;
SELECT
COUNT(*) AS enrollment_count,
COUNT(email) AS records_with_email
FROM enrollments;
GROUP BY 按一个或多个字段分组,然后对每组计算统计值。下面统计每门课程的报名人数:
SELECT
c.course_id,
c.title,
COUNT(e.enrollment_id) AS enrollment_count
FROM courses AS c
LEFT JOIN enrollments AS e
ON e.course_id = c.course_id
GROUP BY c.course_id, c.title;
这里使用 LEFT JOIN,所以没有报名记录的课程也会出现;统计报名编号而不是 COUNT(*),是为了让未匹配到报名行的课程计数为零。若改用内连接,这类课程就不会出现在结果里。
WHERE 在分组前筛选明细行,HAVING 在分组后筛选统计结果:
SELECT
course_id,
COUNT(*) AS enrollment_count
FROM enrollments
GROUP BY course_id
HAVING COUNT(*) >= 2;
只查询一组统计字段时,不要随意同时选择未分组、未聚合的列。不同数据库对此有不同的容忍度或规则;严格查询应保证普通选择列符合分组要求。
九、子查询与集合判断
子查询是在一条 SQL 内嵌套另一条查询。它适合表达“找出满足某个查询结果的记录”,例如查出报名次数至少为两次的课程:
SELECT course_id, title
FROM courses
WHERE course_id IN (
SELECT course_id
FROM enrollments
GROUP BY course_id
HAVING COUNT(*) >= 2
);
很多子查询也能用连接或聚合重写。哪种写法更清晰,取决于需求;性能则需要结合数据量、索引和执行计划判断。不要仅凭 SQL 看起来短,就认为一定更快。
十、参数化查询:安全地处理外部输入
应用程序查询数据库时,条件值往往来自用户输入。把输入直接拼接进 SQL,可能导致 SQL 注入:攻击者通过特殊字符改变语句含义。正确做法是使用数据库驱动提供的参数化查询,让 SQL 结构和参数值分开传递。
下面是 Python 标准库 sqlite3 的示例。问号是 SQLite 驱动支持的位置占位符,参数通过元组传入:
import sqlite3
conn = sqlite3.connect("learning.db")
conn.execute("PRAGMA foreign_keys = ON")
title = "SQL 入门"
fee = 199
conn.execute(
"INSERT INTO courses (title, fee) VALUES (?, ?)",
(title, fee),
)
keyword = "%SQL%"
rows = conn.execute(
"SELECT course_id, title FROM courses WHERE title LIKE ?",
(keyword,),
).fetchall()
for row in rows:
print(row)
conn.commit()
conn.close()
参数化查询的占位符语法由驱动决定。有的驱动使用问号,有的使用命名参数或其他形式;应按所用驱动文档传参,不能把一种写法直接复制到所有数据库接口中。参数占位符用于值,通常不能用来代替表名、列名、排序方向或 SQL 关键字。如果用户可以选择排序字段,应通过预先定义的白名单把允许的选项映射到固定 SQL 片段,而不是把原始输入直接拼入语句。
参数化能防止参数值被当作 SQL 语法解析,但它不会自动验证业务规则,也不等同于权限控制。仍需校验输入范围、控制数据库账号权限,并妥善处理错误信息和敏感数据。参数化也不会自动替你对 LIKE 中的通配符转义:如果需求是“把输入当普通文字匹配”,应按数据库支持的转义规则处理 % 和 _。
十一、事务:让一组操作一起成功或失败
事务把多条数据库操作组成一个逻辑单元。成功时提交,失败时回滚,可以避免只完成一半的业务操作。常见事务属性会概括为 ACID:原子性、一致性、隔离性和持久性;实际隔离级别、并发行为及实现细节由数据库决定。
BEGIN;
UPDATE courses
SET fee = fee - 10
WHERE course_id = 1;
UPDATE courses
SET fee = fee + 10
WHERE course_id = 2;
COMMIT;
如果中间发生错误,可以执行:
ROLLBACK;
上例演示事务语法,不代表“课程费用转移”一定是合理业务。真实应用应验证两条更新都影响了预期记录;否则即使语句成功执行,也可能得到错误结果。事务不要无必要地长时间保持开放:长期事务可能占用锁、影响其他连接并增加数据库维护负担。使用程序驱动时,优先采用其事务管理接口和异常处理机制,避免异常发生后连接仍停留在未完成事务中。
十二、约束与数据完整性
约束让数据库在数据入口处执行规则,减少无效记录。常见约束包括:
NOT NULL:字段必须有值。UNIQUE:值或值组合不能重复。PRIMARY KEY:标识记录,通常隐含唯一且非空要求。FOREIGN KEY:引用其他表中的记录。CHECK:要求字段满足指定条件。DEFAULT:插入时未提供字段值则采用默认值。
例如,可以让邮箱在非空时保持唯一;但是否允许多个空值、如何比较大小写,可能与数据库的唯一索引和排序规则有关。设计约束时应把边界情况明确下来,而不是只测试最常见的输入。
十三、索引:加速查询,也带来维护成本
索引可以帮助数据库更快定位符合条件的行,常用于频繁过滤、连接和排序的列。给报名表的课程编号加索引,是一个常见起点:
CREATE INDEX idx_enrollments_course_id
ON enrollments (course_id);
索引不是越多越好。新增、删除或更新记录时,数据库也要维护相关索引,因此索引会占用存储空间,并可能降低写入效率。对小表或很少使用的字段建索引,收益可能有限。多列索引的字段顺序也会影响哪些查询能有效利用它。
应先根据实际查询模式设计索引,再通过数据库的执行计划工具检查是否使用了索引。SQLite 可以使用 EXPLAIN QUERY PLAN 观察查询计划:
EXPLAIN QUERY PLAN
SELECT enrollment_id
FROM enrollments
WHERE course_id = 1;
执行计划只是诊断依据,不是“出现索引字样就一定更快”的保证;数据规模、分布、统计信息及数据库版本都会影响优化器的选择。
十四、常见错误与排查顺序
- 把空值写成等号比较:用
IS NULL,不要用= NULL。 - 更新或删除时漏掉条件:先用相同的
WHERE查询目标记录,再执行修改。 - 结果顺序看起来“总是一样”就省略排序:只有写出
ORDER BY才能依赖规定的排序。 - 连接后行数意外增加:检查连接键是否唯一、条件是否完整,以及是否产生多对多组合。
- 统计结果不符合预期:区分
COUNT(*)和COUNT(列),检查连接类型及空值。 - 把用户输入拼接进 SQL:改用驱动参数化查询;动态标识符采用白名单。
- 假设类型和函数完全通用:核对数据库方言、类型转换、日期函数和分页语法。
- 误以为写了外键就一定受保护:确认数据库产品支持该约束,并检查连接配置是否实际启用。
- 把 SQL 执行成功当作业务正确:检查受影响行数、约束结果和事务边界。
遇到错误时,先确认连接的是哪一个数据库文件或实例,再核对表和列是否存在、字段拼写与大小写规则是否正确。之后把复杂查询拆成较小部分逐步验证:先检查基础筛选结果,再加入连接,最后增加分组和排序。对修改语句,可先执行对应的 SELECT 检查范围。生产数据应有可靠备份和恢复方案,不能只依赖一条回滚语句。
十五、一个可复用的练习流程
可以按下面的顺序练习:先执行建表语句,再插入几条有重复课程、空邮箱和未报名课程的数据;然后逐步完成筛选、排序、分页、连接和分组查询。接着用事务演练提交和回滚,观察外键、非空和检查约束如何拒绝不符合规则的写入。最后给常用筛选列添加索引,用执行计划对比查询方式。
练习时为每个查询准备一两个边界用例:没有匹配记录时会怎样?字段为空时会怎样?出现重复关联时会怎样?表为空时统计结果是什么?这些问题往往比只用“正常数据”更能暴露逻辑错误。把 SQL 保存为可重复执行的脚本,并记录所用数据库和版本,也有助于重现实验结果。
结语
掌握 SQL 的基础路径可以概括为:先用约束定义数据边界,再用 SELECT 和 WHERE 读取目标数据,使用排序、连接和聚合表达需求,通过参数化安全地传递输入,并在需要时用事务保护一组修改。索引则应围绕真实查询逐步设计,而不是盲目添加。
SQL 语法只是起点。可靠的数据库操作还依赖清楚的数据模型、准确的条件、明确的事务边界和针对实际数据库产品的验证。练习时刻意覆盖空值、重复值、无匹配结果和并发等边界,能让写出的查询更接近真实项目的要求。





