返回文章列表 →

Note

DQL-数据查询

记录数据库查询的常见语法

发布于 更新于

DQL

DQL 的英文全称是 Data Query Language(数据查询语言),用来查询数据库表中的记录。

一条完整的查询语句可以包含以下子句:

SELECT [DISTINCT] 字段列表
FROM 表名列表
[WHERE 条件列表]
[GROUP BY 分组字段列表]
[HAVING 分组后条件列表]
[ORDER BY 排序字段列表]
[LIMIT 分页参数];

语法中的 [] 表示可选部分,实际编写 SQL 时不需要输入方括号。除 SELECT 外,其余子句是否需要使用取决于查询需求;使用多个子句时,应按照上述顺序书写。

DQL 常见查询可以分为六类:

  • 基本查询:使用 SELECT 和 FROM 指定需要返回的字段及数据来源。
  • 条件查询:使用 WHERE 筛选满足条件的记录。
  • 聚合函数:使用 COUNT、MAX、MIN、AVG、SUM 对数据进行统计。
  • 分组查询:使用 GROUP BY 对数据分组,并可使用 HAVING 筛选分组后的结果。
  • 排序查询:使用 ORDER BY 对查询结果排序。
  • 分页查询:使用 LIMIT 限制返回的记录范围。

基本查询

查询指定字段

SELECT 字段名1, 字段名2, 字段名3, ...
FROM 表名;

SELECT 后的字段名决定结果集中返回哪些列,多个字段之间使用逗号分隔。

查询全部字段

SELECT *
FROM 表名;

* 表示查询当前可见的全部字段。它适合临时查看数据;应用代码中通常更推荐显式列出所需字段,避免表结构变化影响结果,并减少不必要的数据读取。

设置别名

SELECT 字段名1 AS 别名1, 字段名2 AS 别名2, ...
FROM 表名;

别名只改变查询结果中列的显示名称,不会修改表中的字段名。AS 可以省略,但显式书写 AS 更容易区分字段与别名。别名包含中文、空格或特殊字符时,可以使用反引号包裹。

去除重复记录

SELECT DISTINCT 字段列表
FROM 表名;

DISTINCT 会根据 SELECT 后的全部字段组合去重。查询多个字段时,只有这些字段的值全部相同,记录才会被视为重复。

条件查询

语法

SELECT 字段列表
FROM 表名
WHERE 条件表达式;

WHERE 会逐行判断条件表达式,只返回判断结果为真的记录。多个条件可以通过逻辑运算符组合。

比较运算符

运算符 作用
> 大于
>= 大于等于
< 小于
<= 小于等于
= 等于
<>、!= 不等于
BETWEEN 最小值 AND 最大值 位于指定范围内,包含最小值和最大值
IN (值1, 值2, ...) 匹配列表中的任意一个值
LIKE '匹配模式' 按模式进行模糊匹配;_ 匹配一个字符,% 匹配零个或多个字符
IS NULL 判断值是否为 NULL
IS NOT NULL 判断值是否不为 NULL

NULL 表示未知或缺失的值,不能使用 = NULL 或 != NULL 判断,必须使用 IS NULL 或 IS NOT NULL。

逻辑运算符

运算符 MySQL 别名 作用
AND && 并且,多个条件同时成立
OR || 或者,多个条件中至少一个成立
NOT ! 对条件结果取反

实际开发中优先使用语义清晰、可移植性更好的 AND、OR 和 NOT。&&、||、! 是 MySQL 扩展,其中 || 在启用 PIPES_AS_CONCAT SQL 模式后表示字符串连接,不再表示逻辑或。

组合多个逻辑条件时,建议使用括号明确运算顺序,避免依赖默认的运算符优先级。

聚合函数

聚合函数将一列数据作为一个整体进行纵向计算,通常返回一个统计结果。

常用聚合函数

函数 作用
COUNT 统计数量
MAX 返回最大值
MIN 返回最小值
AVG 计算平均值
SUM 计算总和

语法

SELECT 聚合函数(字段名)
FROM 表名
[WHERE 条件];

COUNT(*) 统计满足条件的所有记录;COUNT(字段名) 只统计该字段不为 NULL 的记录。

除 COUNT(*) 外,常用聚合函数在计算指定字段时会忽略其中的 NULL。如果没有符合条件的非 NULL 值,SUM、AVG、MAX 和 MIN 通常返回 NULL。

分组查询

分组查询使用 GROUP BY 将分组字段值相同的记录归为一组,再通过聚合函数分别统计每一组的数据。

语法

SELECT 分组字段列表, 聚合函数(字段名)
FROM 表名
[WHERE 分组前条件]
GROUP BY 分组字段列表
[HAVING 分组后条件];

WHERE 和 HAVING 都可以省略。需要同时使用时,WHERE 写在 GROUP BY 之前,HAVING 写在 GROUP BY 之后。

WHERE 与 HAVING 的区别

对比项 WHERE HAVING
执行时机 分组前 分组后
过滤对象 原始记录 分组后的结果
聚合函数 不能直接使用聚合函数作为当前查询的过滤条件 可以使用聚合函数过滤分组

与分组查询相关的逻辑处理顺序可以理解为:

FROM → WHERE → GROUP BY 与聚合计算 → HAVING → SELECT

能在 WHERE 中完成的原始记录过滤应优先写在 WHERE 中,减少参与后续分组的数据量;需要根据统计结果过滤分组时,使用 HAVING。

分组字段规则

分组查询的 SELECT 列表通常由分组字段和聚合表达式组成,例如 gender 和 COUNT(*)。

启用 ONLY_FULL_GROUP_BY 时,SELECT 中没有使用聚合函数的字段必须出现在 GROUP BY 中,或者在逻辑上由分组字段唯一确定,否则 MySQL 会拒绝该查询。关闭该模式后虽然某些查询可以执行,但未参与分组的非聚合字段可能返回组内任意值,结果不确定,应避免依赖这种写法。

排序查询

排序查询使用 ORDER BY 按一个或多个字段排列查询结果。

语法

SELECT 字段列表
FROM 表名
[WHERE 条件]
ORDER BY 字段名1 [ASC | DESC], 字段名2 [ASC | DESC], ...;
  • ASC 表示升序,是默认排序方式,可以省略。
  • DESC 表示降序。

多字段排序

指定多个排序字段时,MySQL 按照字段从左到右依次比较。只有前一个字段的值相同时,才会继续使用后一个字段排序。

查询结果只有在明确使用 ORDER BY 时才有顺序保证。如果多个记录的所有排序字段值仍然相同,它们之间的相对顺序也不确定;需要稳定顺序时,可以在最后追加主键等唯一字段。

分页查询

分页查询使用 LIMIT 返回查询结果中的指定范围,常用于将大量数据分成多页展示。

语法

SELECT 字段列表
FROM 表名
ORDER BY 稳定排序字段
LIMIT 起始偏移量, 查询记录数;

MySQL 还支持以下等价写法:

LIMIT 查询记录数 OFFSET 起始偏移量;

分页参数需要注意:

  • 起始偏移量从 0 开始。
  • 起始偏移量的计算公式为:(页码 - 1) × 每页记录数。
  • 查询第一页时可以省略起始偏移量,LIMIT 查询记录数 等价于 LIMIT 0, 查询记录数。
  • LIMIT 是 MySQL 的分页语法,其他数据库的实现可能不同。

分页查询应配合能够确定唯一顺序的 ORDER BY 使用,例如 ORDER BY id。如果省略排序,或者排序字段存在重复值且没有唯一字段作为最后的排序条件,不同次查询的分页结果可能不稳定。

DQL语句执行顺序

DQL 语句的书写顺序与逻辑执行顺序不同。

书写顺序

SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT

逻辑执行顺序

FROM → WHERE → GROUP BY 与聚合计算 → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT
顺序 子句 作用
1 FROM 确定数据来源
2 WHERE 过滤参与后续处理的原始记录
3 GROUP BY 对记录进行分组,并计算聚合结果
4 HAVING 过滤分组后的结果
5 SELECT 计算并确定需要返回的字段或表达式
6 DISTINCT 如果指定,去除结果中的重复记录
7 ORDER BY 对结果进行排序
8 LIMIT 限制最终返回的记录范围

没有使用的可选子句会直接跳过。例如,一条普通条件查询没有 GROUP BY 和 HAVING,执行顺序可以简化为 FROM → WHERE → SELECT。

逻辑执行顺序也能解释部分别名使用限制:WHERE 执行时 SELECT 中定义的列别名尚未产生,因此不能在 WHERE 中引用该别名。作为名称解析规则,MySQL 另外允许在 GROUP BY、HAVING 和 ORDER BY 中引用查询列别名。

上述顺序用于理解查询语义,并不代表数据库底层一定逐步按此物理顺序运行。MySQL 优化器可能在不改变查询结果的前提下重写语句或调整实际执行计划,真实计划可以使用 EXPLAIN 查看。

示例

数据准备

下面沿用 DML 文章中的 emp 员工表,并增加更多测试记录,以便覆盖基本查询、条件查询、聚合函数、分组查询、排序查询和分页查询。

DROP TABLE IF EXISTS emp;

CREATE TABLE emp (
    id INT PRIMARY KEY COMMENT '编号',
    workno VARCHAR(10) COMMENT '工号',
    name VARCHAR(10) COMMENT '姓名',
    gender CHAR(1) COMMENT '性别',
    age TINYINT UNSIGNED COMMENT '年龄',
    idcard CHAR(18) COMMENT '身份证号',
    workaddress VARCHAR(50) COMMENT '工作地址',
    entrydate DATE COMMENT '入职时间'
) COMMENT = '员工表';

INSERT INTO emp (id, workno, name, gender, age, idcard, workaddress, entrydate)
VALUES
    (1, '1', '柳岩', '女', 20, '123456789012345678', '北京', '2000-01-01'),
    (2, '2', '张无忌', '男', 18, '123456789012345670', '北京', '2005-09-01'),
    (3, '3', '韦一笑', '男', 38, '123456789012345671', '上海', '2005-08-01'),
    (4, '4', '赵敏', '女', 18, '123456789012345672', '北京', '2009-12-01'),
    (5, '5', '小昭', '女', 16, '12345678901234567X', '上海', '2007-07-01'),
    (6, '6', '杨逍', '男', 28, NULL, '杭州', '2006-01-01'),
    (7, '7', '周芷若', '女', 24, '123456789012345674', '杭州', '2010-05-01'),
    (8, '8', '张三丰', '男', 88, NULL, '武汉', '1980-01-01'),
    (9, '9', '灭绝师太', '女', 40, '123456789012345676', '北京', '1990-01-01'),
    (10, '10', '范遥', '男', 35, '123456789012345677', '西安', '2008-03-01');

基本查询

查询指定字段

查询员工的姓名、工号和年龄:

SELECT name, workno, age
FROM emp;
查询全部字段

显式列出全部字段:

SELECT id, workno, name, gender, age, idcard, workaddress, entrydate
FROM emp;

使用 * 查询全部字段:

SELECT *
FROM emp;
设置别名

查询所有员工的工作地址,并将结果列显示为“工作地址”:

SELECT workaddress AS `工作地址`
FROM emp;
去除重复记录

查询员工所在的城市,并去除重复地址:

SELECT DISTINCT workaddress AS `工作地址`
FROM emp;

条件查询

比较运算
-- 查询年龄等于 20 岁的员工
SELECT * FROM emp WHERE age = 20;

-- 查询年龄大于 20 岁的员工
SELECT * FROM emp WHERE age > 20;

-- 查询年龄大于等于 20 岁的员工
SELECT * FROM emp WHERE age >= 20;

-- 查询年龄小于 20 岁的员工
SELECT * FROM emp WHERE age < 20;

-- 查询年龄小于等于 20 岁的员工
SELECT * FROM emp WHERE age <= 20;

-- 查询年龄不等于 20 岁的员工,两种写法等价
SELECT * FROM emp WHERE age != 20;
SELECT * FROM emp WHERE age <> 20;
范围与集合匹配
-- 查询年龄在 15~20 岁之间的员工,包含 15 岁和 20 岁
SELECT * FROM emp WHERE age BETWEEN 15 AND 20;

-- 查询年龄为 18、20 或 40 岁的员工
SELECT * FROM emp WHERE age IN (18, 20, 40);

-- 查询年龄不在 18、20、40 岁中的员工
SELECT * FROM emp WHERE age NOT IN (18, 20, 40);
模糊匹配
-- 两个下划线表示姓名必须恰好包含两个字符
SELECT * FROM emp WHERE name LIKE '__';

-- 百分号表示前面可以有零个或多个字符
SELECT * FROM emp WHERE idcard LIKE '%X';

-- 查询姓名不以“张”开头的员工
SELECT * FROM emp WHERE name NOT LIKE '张%';
空值判断
-- 查询没有身份证号的员工
SELECT * FROM emp WHERE idcard IS NULL;

-- 查询有身份证号的员工
SELECT * FROM emp WHERE idcard IS NOT NULL;
逻辑运算
-- AND:查询性别为女并且年龄小于 25 岁的员工
SELECT * FROM emp
WHERE gender = '女' AND age < 25;

-- OR:查询年龄为 18 岁或 20 岁的员工
SELECT * FROM emp
WHERE age = 18 OR age = 20;

-- NOT:查询年龄不是 20 岁的员工
SELECT * FROM emp
WHERE NOT (age = 20);

-- 使用括号明确组合条件的运算顺序
SELECT * FROM emp
WHERE (workaddress = '北京' OR workaddress = '上海')
  AND age < 25;

MySQL 还支持以下逻辑运算符别名,但实际开发中不推荐使用:

SELECT * FROM emp WHERE gender = '女' && age < 25;
SELECT * FROM emp WHERE age = 18 || age = 20;
SELECT * FROM emp WHERE !(age = 20);

聚合函数

统计数量
-- 统计员工总数
SELECT COUNT(*) AS employee_count
FROM emp;

-- 统计已填写身份证号的员工数量
SELECT COUNT(idcard) AS idcard_count
FROM emp;

示例数据共有 10 名员工,其中 2 名员工的 idcard 为 NULL,因此 COUNT(*) 返回 10,COUNT(idcard) 返回 8。

统计年龄
-- 统计员工的平均年龄
SELECT AVG(age) AS average_age
FROM emp;

-- 查询员工的最大年龄
SELECT MAX(age) AS maximum_age
FROM emp;

-- 查询员工的最小年龄
SELECT MIN(age) AS minimum_age
FROM emp;
按条件求和

统计工作地址为西安的员工年龄总和:

SELECT SUM(age) AS total_age
FROM emp
WHERE workaddress = '西安';

分组查询

按性别统计员工数量
SELECT gender, COUNT(*) AS employee_count
FROM emp
GROUP BY gender;
按性别统计平均年龄
SELECT gender, AVG(age) AS average_age
FROM emp
GROUP BY gender;
组合使用 WHERE 与 HAVING

查询年龄小于 45 岁的员工,按工作地址分组,并只保留员工数量大于等于 3 的地址:

SELECT workaddress, COUNT(*) AS address_count
FROM emp
WHERE age < 45
GROUP BY workaddress
HAVING COUNT(*) >= 3;

WHERE age < 45 先排除不满足年龄条件的记录,HAVING COUNT(*) >= 3 再根据每组的统计数量筛选结果。当前示例数据最终只返回北京,共 4 名员工。

MySQL 允许在 HAVING 中引用查询列的别名,因此最后一行也可以写成:

HAVING address_count >= 3;

排序查询

按年龄升序排序
SELECT *
FROM emp
ORDER BY age ASC;

ASC 是默认排序方式,因此也可以省略。

按入职时间降序排序
SELECT *
FROM emp
ORDER BY entrydate DESC;
使用多个字段排序

先按年龄升序排列;年龄相同时,再按入职时间降序排列:

SELECT *
FROM emp
ORDER BY age ASC, entrydate DESC;

如果还需要保证年龄和入职时间都相同时顺序稳定,可以继续追加主键:

SELECT *
FROM emp
ORDER BY age ASC, entrydate DESC, id ASC;

分页查询

当前示例表共有 10 行数据,下面按 id 升序排列,每页查询 5 行。

查询第一页
SELECT *
FROM emp
ORDER BY id
LIMIT 5;

LIMIT 5 等价于 LIMIT 0, 5,会返回第 1~5 行。

查询第二页

第二页的起始偏移量为 (2 - 1) × 5 = 5:

SELECT *
FROM emp
ORDER BY id
LIMIT 5, 5;

也可以使用 OFFSET 写法:

SELECT *
FROM emp
ORDER BY id
LIMIT 5 OFFSET 5;

两种写法都会返回第 6~10 行。