当你在应用中需要存储用户信息、记录交易数据、查询商品列表时,背后都离不开数据库。而与数据库对话的语言,就是SQL。无论是后端开发、数据分析还是运维管理,SQL都是一项必备技能。理解SQL,是理解数据如何存储、查询和管理的起点。
SQL是什么
SQL是Structured Query Language的缩写,中文译为结构化查询语言。它是一种专门用于管理关系型数据库的编程语言,可以用来查询、插入、更新和删除数据,也可以用来创建和修改数据库结构。
SQL的诞生可以追溯到1970年。那年,IBM研究员Edgar F. Codd发表了一篇里程碑式的论文《A Relational Model of Data for Large Shared Data Banks》,提出了关系模型的概念。这篇论文奠定了现代关系型数据库的理论基础。1974年,Donald Chamberlin和Raymond Boyce基于Codd的关系模型,设计了SEQUEL(Structured English Query Language),这就是SQL的前身。
1986年,美国国家标准学会(ANSI)将SQL定为标准,随后国际标准化组织(ISO)也于1987年采纳。自此,SQL成为关系型数据库的通用语言。从SQL-86到SQL-92,再到SQL:1999、SQL:2003,标准不断演进,加入了窗口函数、公用表表达式(CTE)、JSON支持等现代特性,但核心的查询语法保持稳定。
关系型数据库的基本概念
在学习SQL之前,需要先理解关系型数据库的几个基本概念。
表、行、列
关系型数据库用**表(Table)**来组织数据。表类似于Excel电子表格,由行和列组成:
- 列(Column):也称为字段,定义了数据的类型和含义。比如一个"用户"表可能有
id、name、email、created_at等列。 - 行(Row):也称为记录,代表一条具体的数据。每一行是一条完整的记录,比如一个用户的信息。
┌────┬──────────┬─────────────────┬─────────────────────┐
│ id │ name │ email │ created_at │
├────┼──────────┼─────────────────┼─────────────────────┤
│ 1 │ 张三 │ [email protected] │ 2026-01-15 10:30:00 │
│ 2 │ 李四 │ [email protected] │ 2026-02-20 14:22:00 │
│ 3 │ 王五 │ [email protected] │ 2026-03-01 09:15:00 │
└────┴──────────┴─────────────────┴─────────────────────┘
图片来源:基于关系型数据库标准模型绘制
主键与外键
**主键(Primary Key)**是表中唯一标识每一行的列或列组合。主键的值必须唯一且不能为空。在上面的例子中,id列就是主键——每个用户都有唯一的ID,通过ID可以精确找到对应的用户。
**外键(Foreign Key)**用于建立表与表之间的关联。外键是一个表中的列,其值引用另一个表的主键。比如"订单"表中的user_id可以是外键,指向"用户"表的id,表示这个订单属于哪个用户。
数据类型
每一列都有特定的数据类型,决定了该列可以存储什么样的数据:
| 数据类型 | 说明 | 示例 |
|---|---|---|
INT |
整数 | 1, 100, -50 |
VARCHAR(n) |
可变长度字符串 | ‘Hello’, ‘数据库’ |
TEXT |
长文本 | 大段文章内容 |
DECIMAL(p,s) |
精确小数 | 123.45 |
DATE |
日期 | ‘2026-03-08’ |
DATETIME |
日期时间 | ‘2026-03-08 10:30:00’ |
BOOLEAN |
布尔值 | TRUE, FALSE |
不同数据库的数据类型名称可能略有差异,但概念相同。
SELECT语句:查询数据
SELECT是SQL中最常用的语句,用于从数据库中查询数据。
基本语法
SELECT 列名1, 列名2, ... FROM 表名;
查询所有列使用星号*:
SELECT * FROM users;
查询特定列:
SELECT name, email FROM users;
使用别名
用AS关键字为列或表指定临时名称,使结果更易读:
SELECT name AS 用户名, email AS 邮箱 FROM users;
DISTINCT去重
使用DISTINCT关键字去除重复值:
-- 查询所有不同的城市
SELECT DISTINCT city FROM users;
WHERE子句:条件过滤
WHERE子句用于指定查询条件,只返回满足条件的行。
比较运算符
-- 等于
SELECT * FROM users WHERE id = 1;
-- 不等于
SELECT * FROM users WHERE status != 'deleted';
-- 大于、小于
SELECT * FROM products WHERE price > 100;
SELECT * FROM orders WHERE created_at < '2026-01-01';
逻辑运算符
组合多个条件:
-- AND:两个条件都满足
SELECT * FROM users WHERE age >= 18 AND status = 'active';
-- OR:满足任一条件
SELECT * FROM users WHERE city = '北京' OR city = '上海';
-- NOT:取反
SELECT * FROM users WHERE NOT status = 'deleted';
BETWEEN和IN
BETWEEN用于范围查询(包含边界):
-- 价格在100到500之间的商品
SELECT * FROM products WHERE price BETWEEN 100 AND 500;
IN用于匹配列表中的值:
-- 来自北京、上海、广州的用户
SELECT * FROM users WHERE city IN ('北京', '上海', '广州');
LIKE模糊匹配
使用通配符进行模式匹配:
%匹配任意个字符_匹配单个字符
-- 姓"张"的用户
SELECT * FROM users WHERE name LIKE '张%';
-- 邮箱以@test.com结尾
SELECT * FROM users WHERE email LIKE '%@test.com';
-- 名字第二个字是"小"
SELECT * FROM users WHERE name LIKE '_小%';
NULL值处理
NULL表示"无值"或"未知"。判断NULL不能使用等号,必须使用IS NULL或IS NOT NULL:
-- 查询没有填写邮箱的用户
SELECT * FROM users WHERE email IS NULL;
-- 查询已填写邮箱的用户
SELECT * FROM users WHERE email IS NOT NULL;
ORDER BY子句:排序结果
ORDER BY用于对查询结果排序。
-- 按创建时间升序排列(默认)
SELECT * FROM users ORDER BY created_at;
-- 按价格降序排列
SELECT * FROM products ORDER BY price DESC;
-- 多列排序:先按城市,再按姓名
SELECT * FROM users ORDER BY city ASC, name ASC;
ASC表示升序(默认),DESC表示降序。
LIMIT和OFFSET:分页查询
当数据量大时,通常需要分页显示。LIMIT限制返回的行数,OFFSET指定跳过的行数。
-- 只返回前10条记录
SELECT * FROM users LIMIT 10;
-- 跳过前20条,返回接下来的10条(第3页,每页10条)
SELECT * FROM users LIMIT 10 OFFSET 20;
不同数据库的分页语法略有不同:
- MySQL/PostgreSQL:
LIMIT n OFFSET m - SQL Server:
OFFSET m ROWS FETCH NEXT n ROWS ONLY - Oracle:
FETCH FIRST n ROWS ONLY
聚合函数
聚合函数对一组值进行计算,返回单个值。
常用聚合函数
| 函数 | 作用 | 示例 |
|---|---|---|
COUNT() |
计数 | COUNT(*) 统计行数 |
SUM() |
求和 | SUM(amount) 计算总额 |
AVG() |
平均值 | AVG(price) 计算平均价格 |
MAX() |
最大值 | MAX(score) 最高分 |
MIN() |
最小值 | MIN(price) 最低价格 |
-- 统计用户总数
SELECT COUNT(*) FROM users;
-- 计算订单总金额
SELECT SUM(amount) FROM orders;
-- 计算商品平均价格
SELECT AVG(price) FROM products;
-- 找出最高和最低价格
SELECT MAX(price) AS 最高价, MIN(price) AS 最低价 FROM products;
COUNT的特殊用法
-- 统计总行数
SELECT COUNT(*) FROM users;
-- 统计某列非NULL值的数量
SELECT COUNT(email) FROM users;
-- 统计不同值的数量
SELECT COUNT(DISTINCT city) FROM users;
GROUP BY和HAVING:分组统计
GROUP BY分组
GROUP BY将数据按某列的值分组,常与聚合函数配合使用。
-- 统计每个城市的用户数量
SELECT city, COUNT(*) AS user_count
FROM users
GROUP BY city;
-- 统计每个商品的销售额
SELECT product_id, SUM(quantity * price) AS total_sales
FROM order_items
GROUP BY product_id;
HAVING过滤分组
HAVING用于过滤分组后的结果,类似于WHERE,但作用于分组而不是行。
-- 找出用户数超过100的城市
SELECT city, COUNT(*) AS user_count
FROM users
GROUP BY city
HAVING COUNT(*) > 100;
-- 找出销售额超过10000的商品
SELECT product_id, SUM(amount) AS total
FROM orders
GROUP BY product_id
HAVING SUM(amount) > 10000;
WHERE和HAVING的区别:
- WHERE在分组前过滤行
- HAVING在分组后过滤分组
SQL查询的执行顺序
理解SQL的执行顺序对于正确编写查询非常重要。SQL语句的书写顺序和执行顺序是不同的:
书写顺序:
SELECT column
FROM table
WHERE condition
GROUP BY column
HAVING condition
ORDER BY column
LIMIT n;
执行顺序:
- FROM(确定数据源)
- WHERE(过滤行)
- GROUP BY(分组)
- HAVING(过滤分组)
- SELECT(选择列)
- DISTINCT(去重)
- ORDER BY(排序)
- LIMIT/OFFSET(限制行数)
这意味着在WHERE子句中不能使用SELECT中定义的别名,因为SELECT在WHERE之后执行:
-- 错误:WHERE不能使用SELECT中的别名
SELECT name, age * 2 AS double_age
FROM users
WHERE double_age > 40; -- 报错!
-- 正确:使用完整表达式
SELECT name, age * 2 AS double_age
FROM users
WHERE age * 2 > 40;
JOIN:多表关联
实际应用中,数据通常分散在多个表中。JOIN用于将多个表的数据关联起来。
假设有两个表:users(用户表)和orders(订单表)。
users表 orders表
┌────┬──────────┐ ┌────┬─────────┬─────────┐
│ id │ name │ │ id │ user_id │ amount │
├────┼──────────┤ ├────┼─────────┼─────────┤
│ 1 │ 张三 │ │ 1 │ 1 │ 100.00 │
│ 2 │ 李四 │ │ 2 │ 1 │ 200.00 │
│ 3 │ 王五 │ │ 3 │ 2 │ 150.00 │
└────┴──────────┘ └────┴─────────┴─────────┘
图片来源:基于SQL JOIN标准语义绘制
INNER JOIN内连接
返回两个表中匹配的行:
SELECT users.name, orders.amount
FROM users
INNER JOIN orders ON users.id = orders.user_id;
结果:
┌──────────┬─────────┐
│ name │ amount │
├──────────┼─────────┤
│ 张三 │ 100.00 │
│ 张三 │ 200.00 │
│ 李四 │ 150.00 │
└──────────┴─────────┘
王五没有订单,所以不在结果中。
LEFT JOIN左连接
返回左表所有行,右表没有匹配则为NULL:
SELECT users.name, orders.amount
FROM users
LEFT JOIN orders ON users.id = orders.user_id;
结果:
┌──────────┬─────────┐
│ name │ amount │
├──────────┼─────────┤
│ 张三 │ 100.00 │
│ 张三 │ 200.00 │
│ 李四 │ 150.00 │
│ 王五 │ NULL │
└──────────┴─────────┘
王五没有订单,但仍然出现在结果中,amount为NULL。
RIGHT JOIN右连接
返回右表所有行,左表没有匹配则为NULL。实际使用较少,通常可以用LEFT JOIN替代。
FULL OUTER JOIN全连接
返回两个表所有行,没有匹配则为NULL。不是所有数据库都支持。
JOIN图解
使用Venn图可以直观理解各种JOIN:
INNER JOIN LEFT JOIN RIGHT JOIN FULL JOIN
(A ∩ B) (A) (B) (A ∪ B)
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ A │ │ A │ │ A │ │ A │
│ ┌───┐ │ │ ┌───┐ │ │ ┌───┐ │ │ ┌───┐ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ └───┘ │ │ └───┘ │ │ └───┘ │ │ └───┘ │
│ B │ │ B │ │ B │ │ B │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
图片来源:基于LearnSQL.com的SQL JOIN可视化教程绘制,参考 https://learnsql.com/blog/sql-joins/
INSERT:插入数据
INSERT用于向表中添加新数据。
插入单行
-- 指定列名和值
INSERT INTO users (name, email, city)
VALUES ('赵六', '[email protected]', '深圳');
-- 插入所有列(顺序必须与表结构一致)
INSERT INTO users
VALUES (4, '赵六', '[email protected]', '深圳', '2026-03-08 10:00:00');
插入多行
INSERT INTO users (name, email, city)
VALUES
('赵六', '[email protected]', '深圳'),
('孙七', '[email protected]', '杭州'),
('周八', '[email protected]', '成都');
从查询结果插入
-- 将活跃用户复制到新表
INSERT INTO active_users (name, email)
SELECT name, email FROM users WHERE status = 'active';
UPDATE:更新数据
UPDATE用于修改表中的数据。
-- 更新单个字段
UPDATE users SET city = '广州' WHERE id = 1;
-- 更新多个字段
UPDATE users
SET city = '广州', email = '[email protected]'
WHERE id = 1;
-- 更新多条记录
UPDATE products SET price = price * 1.1 WHERE category = '电子';
重要提示:永远不要忘记WHERE子句,否则会更新整张表!
-- 危险!会更新所有用户的城市
UPDATE users SET city = '北京';
DELETE:删除数据
DELETE用于删除表中的数据。
-- 删除指定记录
DELETE FROM users WHERE id = 1;
-- 删除满足条件的记录
DELETE FROM users WHERE status = 'deleted';
-- 删除所有记录(保留表结构)
DELETE FROM users;
重要提示:永远不要忘记WHERE子句,否则会删除整张表的数据!
-- 危险!会删除所有用户
DELETE FROM users;
CREATE TABLE:创建表
CREATE TABLE用于创建新表。
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) UNIQUE,
city VARCHAR(50) DEFAULT '北京',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
常用约束:
PRIMARY KEY:主键NOT NULL:不能为空UNIQUE:值必须唯一DEFAULT:默认值AUTO_INCREMENT:自动递增(MySQL语法)
创建带外键的表
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
amount DECIMAL(10, 2),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
ALTER TABLE:修改表结构
ALTER TABLE用于修改已存在的表结构。
-- 添加列
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- 删除列
ALTER TABLE users DROP COLUMN phone;
-- 修改列类型
ALTER TABLE users MODIFY COLUMN name VARCHAR(100);
-- 添加约束
ALTER TABLE users ADD CONSTRAINT uk_email UNIQUE (email);
-- 删除约束
ALTER TABLE users DROP CONSTRAINT uk_email;
DROP TABLE:删除表
DROP TABLE删除整个表,包括结构和数据。
-- 删除表
DROP TABLE users;
-- 如果表存在才删除(避免报错)
DROP TABLE IF EXISTS users;
子查询
子查询是嵌套在另一个查询中的查询。
在WHERE中使用子查询
-- 查询有订单的用户
SELECT * FROM users
WHERE id IN (SELECT DISTINCT user_id FROM orders);
-- 查询金额大于平均订单金额的订单
SELECT * FROM orders
WHERE amount > (SELECT AVG(amount) FROM orders);
在FROM中使用子查询
-- 查询每个用户的订单数量
SELECT u.name, o.order_count
FROM users u
JOIN (
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
) o ON u.id = o.user_id;
视图(VIEW)
视图是保存的查询,可以像表一样使用。
-- 创建视图
CREATE VIEW active_users AS
SELECT id, name, email FROM users WHERE status = 'active';
-- 查询视图
SELECT * FROM active_users;
-- 删除视图
DROP VIEW active_users;
视图的优点:
- 简化复杂查询
- 提供数据安全性(隐藏敏感列)
- 保持数据一致性
SQL注释
良好的注释习惯让代码更易维护:
-- 单行注释
/*
多行注释
可以跨越多行
*/
SELECT name, -- 选择用户名
email -- 和邮箱
FROM users;
安全实践
防止SQL注入
SQL注入是最常见的安全漏洞之一。永远不要直接拼接用户输入到SQL语句中:
-- 危险!可能被注入
SELECT * FROM users WHERE name = '" + userName + "'";
使用参数化查询(Prepared Statement):
# Python示例
cursor.execute("SELECT * FROM users WHERE name = %s", (user_name,))
// Java示例
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE name = ?");
stmt.setString(1, userName);
参数化查询将SQL语句和数据分离,从根本上防止了SQL注入。
最小权限原则
数据库用户应该只拥有完成工作所需的最小权限:
-- 创建只读用户
GRANT SELECT ON database.* TO 'readonly'@'localhost';
-- 只允许访问特定表
GRANT SELECT, INSERT ON database.orders TO 'app_user'@'localhost';
实践建议
学习路径:从简单的SELECT开始,逐步掌握WHERE、ORDER BY,再学习聚合函数和GROUP BY,最后掌握JOIN和子查询。
调试技巧:复杂查询可以分步验证。先写FROM和WHERE,确认数据正确后再加GROUP BY和SELECT。
性能意识:WHERE过滤在分组前执行,尽量在WHERE中过滤数据,减少后续处理的数据量。
命名规范:表名用复数(users、orders),列名用单数(name、email),使用下划线分隔(created_at)。
代码风格:关键字大写,表名列名小写,复杂查询适当换行缩进。
参考资料:
- W3Schools SQL Tutorial (https://www.w3schools.com/sql/)
- LearnSQL.com - History of SQL Standards (https://learnsql.com/blog/history-of-sql-standards/)
- SQLBolt - Query Order of Execution (https://sqlbolt.com/lesson/select_queries_order_of_execution)
- Built In - SQL Order of Execution (https://builtin.com/data-science/sql-order-of-execution)
- LearnSQL.com - SQL JOINs Explained (https://learnsql.com/blog/sql-joins/)
- GeeksforGeeks SQL Tutorial (https://www.geeksforgeeks.org/sql/)
- Microsoft Learn - Transact-SQL Reference (https://learn.microsoft.com/en-us/sql/t-sql/)
- Atlassian - SQL Join Types Explained Visually (https://www.atlassian.com/data/sql/sql-join-types-explained-visually)
- OWASP SQL Injection Prevention Cheat Sheet (https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)
- 菜鸟教程 - SQL教程 (https://www.runoob.com/sql/sql-tutorial.html)