
SQL数据库学习入门:常用语句与练习方法
SQL 是操作关系型数据库的常用语言。无论是保存用户资料、查询订单,还是统计每天的销售额,很多应用都会用 SQL 与数据库交互。入门时不必先掌握某一种数据库的全部特性;更有效的做法是先理解表、行、列和关系,再用一个轻量数据库练习查询、修改、约束与事务。
本文以 SQLite 作为可运行示例。SQLite 通常不需要启动独立数据库服务,Python 自带 sqlite3 模块,适合本地学习和原型验证。示例中的基础 SQL 在其他关系型数据库中也大多适用,但数据类型、自动生成主键、日期处理、分页语法和部分函数可能不同。实际使用时,应查阅目标数据库的文档,不要假设不同产品完全兼容。
一、先建立几个基本概念
- 数据库:用于组织和保存数据的软件或文件。
- 表:按行和列组织的一类数据,例如
customers表保存客户。 - 行:一条记录,例如某位客户的资料。
- 列:记录中的一个属性,例如客户姓名、邮箱和创建时间。
- 主键:用于唯一标识一行的列或列组合。
- 外键:用于表达表之间关系的列,通常引用另一张表的主键。
SQL 语句常按用途分组:查询数据使用 SELECT;新增、修改、删除行使用 INSERT、UPDATE、DELETE;创建或修改表结构使用 CREATE、ALTER、DROP。这是便于学习的分类,不同数据库对权限和事务的处理可能有所区别。
二、准备一组练习数据
下面的脚本适用于 SQLite。可以将其保存为 setup.sql,在支持 SQLite 的工具中运行;也可以稍后通过 Python 执行。它创建客户和订单两张表,并插入几条数据。每条订单通过 customer_id 指向客户。
PRAGMA foreign_keys = ON;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS customers;
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE,
city TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
amount NUMERIC NOT NULL CHECK (amount >= 0),
status TEXT NOT NULL,
ordered_at TEXT NOT NULL,
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
INSERT INTO customers (name, email, city, created_at) VALUES
('林青', '[email protected]', '杭州', '2025-01-05'),
('周宁', '[email protected]', '上海', '2025-01-12'),
('陈一', NULL, '杭州', '2025-02-03');
INSERT INTO orders (customer_id, amount, status, ordered_at) VALUES
(1, 128.50, 'paid', '2025-02-10'),
(1, 39.90, 'pending', '2025-02-11'),
(2, 260.00, 'paid', '2025-02-12');
NOT NULL 要求列不能保存 NULL;UNIQUE 限制值不能重复;CHECK 用于校验数据条件。SQLite 中,普通的 INTEGER PRIMARY KEY 会作为行标识符使用;其他数据库生成主键的写法不同。SQLite 的外键约束需要启用,示例中的 PRAGMA foreign_keys = ON 应在连接上执行。约束可以防止一部分无效数据进入数据库,但不能替代业务规则校验、权限控制或安全设计。
三、查询数据:SELECT、筛选与排序
最基本的查询包含要读取的列和数据来源:
SELECT name, city
FROM customers;
开发中尽量明确列名,不要长期依赖 SELECT *。星号适合快速检查数据,但表结构增加列后,结果也会变化;返回不需要的列还会增加传输和处理开销。
用 WHERE 筛选行,用 ORDER BY 排序,用 LIMIT 控制返回条数:
SELECT id, name, city
FROM customers
WHERE city = '杭州'
ORDER BY id DESC
LIMIT 10;
WHERE 可以使用比较运算符,如 =、<>、>、>=,也可以组合条件:
SELECT id, customer_id, amount, status
FROM orders
WHERE status = 'paid'
AND amount >= 100
ORDER BY amount DESC, id ASC;
多列排序按书写顺序逐层生效:先按金额从高到低,金额相同再按编号从小到大。若不写 ORDER BY,数据库不保证结果按某个固定顺序返回。不要把某次测试中观察到的顺序当成稳定约定。
BETWEEN 通常表示包含两端的范围;IN 用于匹配多个候选值:
SELECT id, amount, ordered_at
FROM orders
WHERE amount BETWEEN 50 AND 200
AND status IN ('paid', 'pending');
筛选空值不能写成 email = NULL,应使用 IS NULL 或 IS NOT NULL:
SELECT id, name
FROM customers
WHERE email IS NULL;
这是因为 SQL 使用三值逻辑:比较结果可能为真、假或未知。涉及 NULL 的普通等号比较通常得到未知,无法像普通布尔值那样匹配。类似地,使用 NOT IN 时,如果候选集合中包含 NULL,结果可能与直觉不同;处理可能含空值的数据时,先确认业务含义,必要时改用明确的空值过滤或 NOT EXISTS。
LIKE 用于模式匹配:% 匹配任意长度字符,_ 匹配一个字符。
SELECT name, email
FROM customers
WHERE email LIKE '%@example.com';
如果用户输入本意是普通文字,不能忽略其中的 % 和 _:它们会被解释为通配符。要按字面内容搜索,需要按照所用数据库的规则转义通配符,并使用 ESCAPE 等机制。通配符放在开头的模式,例如 LIKE '%abc',通常也不容易利用普通索引进行高效查找。
四、聚合与分组
聚合函数可以对多行计算结果。常见函数包括 COUNT、SUM、AVG、MIN 和 MAX。
SELECT
status,
COUNT(*) AS order_count,
SUM(amount) AS total_amount,
AVG(amount) AS average_amount
FROM orders
GROUP BY status
ORDER BY total_amount DESC;
GROUP BY 把相同状态的行归为一组。若要在分组后筛选,应使用 HAVING;筛选单行条件则用 WHERE。可以把查询的逻辑顺序粗略理解为先从表中取行、按 WHERE 过滤、分组、按 HAVING 过滤组,最后排序和限制结果。
SELECT customer_id, SUM(amount) AS total_amount
FROM orders
WHERE status = 'paid'
GROUP BY customer_id
HAVING SUM(amount) > 100
ORDER BY total_amount DESC;
常见错误是把聚合条件写到 WHERE 中,例如试图用 WHERE SUM(amount) > 100。聚合值是在分组后产生的,应使用 HAVING。此外,COUNT(*) 统计行数,COUNT(column) 只统计该列非 NULL 的行,两者含义不同。
五、关联多张表:JOIN
当所需信息分散在多张表中,可以通过关联查询合并结果。下面查询订单及其客户姓名:
SELECT
o.id AS order_id,
c.name AS customer_name,
o.amount,
o.status
FROM orders AS o
JOIN customers AS c
ON o.customer_id = c.id
ORDER BY o.id;
JOIN 默认指内连接,只返回关联条件两侧都匹配的行。若要保留左侧表中没有匹配项的行,可以用 LEFT JOIN。例如找出所有客户及其订单,包括没有订单的客户:
SELECT
c.id,
c.name,
o.id AS order_id,
o.amount
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.id
ORDER BY c.id, o.id;
左连接中,没有匹配订单的客户仍会出现,但订单列为 NULL。有一个容易踩的边界:如果在 WHERE 中写 o.status = 'paid',这些无订单客户会被过滤掉,效果可能接近内连接。若要保留所有客户,同时只关联已付款订单,可以把条件放进连接条件:
SELECT c.name, o.id, o.amount
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.id
AND o.status = 'paid';
关联条件不充分也会造成重复行。例如只按城市关联客户和订单,而不是按客户编号,可能把一位城市内客户的订单错误地组合到其他客户上。写 JOIN 前要确认两侧的键以及关系是“一对一”“一对多”还是“多对多”。
六、新增、修改与删除数据
新增数据时,建议明确列名,避免依赖表中列的顺序:
INSERT INTO customers (name, email, city, created_at)
VALUES ('王明', '[email protected]', '成都', '2025-03-01');
一次插入多行时,可以使用多个值组:
INSERT INTO customers (name, email, city, created_at)
VALUES
('赵可', '[email protected]', '苏州', '2025-03-02'),
('孙安', NULL, '南京', '2025-03-03');
修改数据使用 UPDATE。通常必须写清楚 WHERE,否则会更新表中的每一行:
UPDATE orders
SET status = 'paid'
WHERE id = 2;
执行重要更新前,可先用相同筛选条件运行 SELECT,检查命中的行。更新金额等关键字段时也要确认单位、精度和业务约束;货币数值若有严格精度要求,不宜依赖浮点类型的近似运算,应按数据库和业务规范选择合适的定点数或最小货币单位表示。
删除数据同样要谨慎:
DELETE FROM orders
WHERE id = 3;
遗漏 WHERE 会删除表内全部行;删除表结构则是另一种操作,例如 DROP TABLE orders,影响更大。对线上数据执行修改或删除前,应确认环境、筛选范围、备份与恢复方案,并遵循团队的审批流程。不要把“语句能执行”误认为“操作安全”。
七、建表、约束与索引
建表时不仅要能存数据,还要明确哪些值允许重复、哪些列不能为空,以及表之间如何关联。主键、唯一约束和外键可帮助数据库维护数据一致性。对于字段含义,使用清晰的列名和合适的数据类型;例如日期不要随意混入格式不一致的文本。
索引用于帮助数据库查找数据。以频繁按客户编号查订单为例,可以为关联列创建索引:
CREATE INDEX idx_orders_customer_id
ON orders (customer_id);
索引并非越多越好。索引会占用存储空间,并可能增加插入、更新和删除的维护成本。是否需要索引,取决于数据规模、查询条件、排序和执行计划;小表可能不需要额外索引。复合索引的列顺序也会影响可用场景,不能只凭列名推断性能。实际优化应在目标数据库上查看执行计划,并用有代表性的数据测量。
八、事务:把相关操作作为一组处理
事务适用于一组必须共同成功或共同失败的操作,例如账户转账。事务的概念通常由 ACID 特性描述,但具体隔离级别、锁行为和并发细节由数据库产品决定。
BEGIN;
UPDATE accounts
SET balance = balance - 100
WHERE id = 1;
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
COMMIT;
如果中间发现错误,可以在提交前执行 ROLLBACK 取消当前事务中的更改。上面的片段假设存在名为 accounts 的表,并不属于前面的练习数据。真实转账还需要检查账户是否存在、余额是否足够,并妥善处理并发请求;仅把两条更新放进事务,并不能自动实现所有业务规则。数据库驱动的自动提交设置、事务开启方式也可能不同,应按驱动文档使用。
九、用参数化查询避免把数据拼进 SQL
应用程序接收用户输入时,不要通过字符串拼接构造查询。例如,直接把登录名拼进 SQL,可能让输入改变语句结构,造成 SQL 注入。应使用数据库驱动提供的参数绑定接口,让 SQL 结构与数据值分开传递。
下面是可运行的 Python 示例。将数据库文件保存为 learning.db,先运行前面的建表和插入脚本,再运行此代码:
import sqlite3
name = input("请输入客户姓名:")
with sqlite3.connect("learning.db") as conn:
row = conn.execute(
"SELECT id, name, city FROM customers WHERE name = ?",
(name,)
).fetchone()
if row is None:
print("没有找到客户")
else:
print(row)
SQLite 的问号占位符对应的参数需要按位置传入;只有一个参数时仍要写成单元素元组 (name,)。其他驱动可能使用不同占位符风格,例如命名参数或其他标记,应以该驱动的接口为准。参数绑定适用于数据值,通常不能用来替代表名、列名、排序方向或 SQL 关键字。若需要动态选择列或排序方向,应从代码中预先定义的白名单映射中选择,而不是直接接受任意输入并拼接。
参数化查询是防止 SQL 注入的重要措施,但不是完整的安全方案。应用仍需正确管理数据库账号权限、保护连接凭据、校验业务输入,并谨慎处理错误信息。数据库账号应只拥有完成任务所需的权限;不要把敏感信息写入日志或公开给无权访问的人。
十、分页查询的边界
SQLite 支持 LIMIT 和 OFFSET,可用于小规模分页:
SELECT id, name
FROM customers
ORDER BY id
LIMIT 20 OFFSET 40;
这表示跳过前 40 行,再返回最多 20 行。分页必须指定稳定排序,否则不同请求间记录顺序可能变化。随着偏移量增大,数据库可能需要扫描并跳过大量行,性能会变差;数据持续写入时,偏移分页也可能出现跨页重复或遗漏。大数据量场景可以考虑基于上一页最后一条记录的“游标式分页”,例如按递增主键查询下一批。不同数据库的分页语法和优化方式并不相同。
十一、常见错误速查
- 忘记写
WHERE:UPDATE或DELETE可能作用于全表。执行前先用SELECT检查目标范围。 - 把
NULL当普通值比较:使用IS NULL,不要写= NULL。 - 误以为结果天然有序:需要稳定顺序时明确写
ORDER BY。 - 把聚合条件写在
WHERE:分组后的聚合筛选应使用HAVING。 - 连接键不正确:可能导致重复、遗漏或错误组合。先确认主外键关系和结果行数。
- 用字符串拼接输入值:可能产生注入风险。使用驱动的参数化查询。
- 以为参数能代替列名:参数绑定处理数据值,不负责动态替换 SQL 结构;动态结构要使用白名单。
- 把约束当成全部业务校验:数据库约束能保护数据,但复杂业务仍需在应用和数据库设计中共同处理。
- 把 SQLite 行为当作通用标准:自动编号、日期函数、分页、事务和类型规则都可能存在差异,部署前要在目标数据库验证。
十二、循序渐进的练习方法
学习 SQL 的关键不是记住尽可能多的关键字,而是反复把业务问题翻译成查询。可以按下面的顺序练习,并始终使用前面的客户和订单数据。
- 单表筛选:查找某个城市的客户;查找金额高于指定值的订单;分别练习
AND、OR、IN和范围条件。 - 排序和限制:按金额降序找出前三笔订单;为排序加入唯一编号作为次级排序键,避免金额相同造成顺序不稳定。
- 聚合:统计订单总数、平均金额和已付款金额总和;比较
COUNT(*)与COUNT(email)的结果。 - 分组:统计每位客户的订单数和消费总额,再只保留订单数达到指定数量的客户。
- 关联:显示客户姓名与订单金额;改用
LEFT JOIN,确认没有订单的客户仍出现在结果中。 - 修改数据:新增一位客户,将一笔待处理订单改为已付款;先查询命中行,再执行更新。
- 事务:在事务中修改两行数据,提交一次、回滚一次,观察结果差异。
- 参数绑定:用 Python 接收姓名并查询,试着输入含引号的字符串,确认它仍被当作数据值,而不是 SQL 语法。
每做一道题,都可以按照同一流程检查:明确结果应包含哪些列;确认数据来自哪些表;确定筛选条件和空值含义;检查关联关系是否会产生重复行;最后验证排序、结果数量和边界数据。遇到不确定的查询,先用小数据集逐步执行,不要一次写出很长的语句再猜错误原因。
可以把练习结果整理成一个小项目:建立表结构,插入测试数据,编写查询文件,再用脚本自动运行查询并比对预期结果。随着熟练度提高,再学习子查询、公共表表达式、窗口函数、视图和执行计划。它们能解决更复杂的问题,但应建立在基本查询、连接、聚合和事务都掌握之后。
结语
SQL 入门的核心是把数据模型与查询逻辑对应起来:先明确表之间的关系,再选择列、过滤记录、聚合或关联,最后检查结果是否符合预期。日常操作中,明确排序、正确处理 NULL、谨慎执行更新和删除、使用参数化查询,是比记住冷门语法更重要的习惯。掌握这些基础后,再依据实际数据库产品学习其类型、索引、事务与性能特性,才能把练习经验可靠地带入应用开发。









