拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MySQL单表查询指南:SELECT与WHERE到分组排序实战

MySQL单表查询指南:SELECT与WHERE到分组排序实战

1. 单表查询到底在查什么:先把数据摸清楚再动手

1.1 用Excel表格思维理解MySQL的表结构

很多刚接触MySQL的朋友,打开命令行窗口后,对着黑底白字的界面一脸懵,脑子里只有一个问题:“数据在哪?我该查什么?”这个困惑很正常,因为MySQL跟Excel不一样,你不能直接“打开文件”看到数据,你只能用命令去跟它对话。但换个角度想,MySQL里的表结构其实就是你熟悉的Excel表格。

一张表里包含三个基本概念:字段、记录、主键。字段就是列名,比如用户名、年龄、城市;记录就是每一行,代表一条完整的数据;主键则是这一行数据的唯一身份证号,比如用户编号id,它不允许重复。举个最生活化的例子,你去超市办了一张会员卡,超市系统里的“会员表”大概长这样:id是会员编号,name是姓名,phone是手机号,points是积分。每一行就是一个会员的信息,id就是每人唯一的标识。

单表查询,就是在这样一张表内部做操作,不牵扯其他表。这意味着你不需要关心表跟表之间的外键、关联、JOIN这些复杂概念,只需要把注意力放在三件事上:选哪些字段出来看、筛选哪些行留下来、按什么顺序展示。把这三个问题想清楚了,单表查询基本上就掌握了一大半。

提示:初学者最容易犯的错是觉得SQL难,其实单表查询就是“对着表做筛选和投影”。你在Excel里筛选某一列等于某个值,在MySQL里就是WHERE条件;你在Excel里隐藏某些列只留需要的列,在MySQL里就是SELECT指定字段。

1.2 单表查询的三组核心动作

我用一个生活中的场景来拆解单表查询的本质。假设你手里有一本全班同学的花名册,里面有学号、姓名、性别、身高、体重、出生年月。现在有人问你:帮我找出全班身高最高的三个男生,只要他们的姓名和身高。你在这本册子上做的事,其实就是一次单表查询。

这个过程中你做了三个操作:第一,你只看“姓名”和“身高”这两列,这叫做“投影”(Projection),对应SQL里的SELECT子句,决定输出哪些字段。第二,你只在“性别=男”的行里找,这叫做“筛选”(Selection),对应SQL里的WHERE子句,决定保留哪些行。第三,你按身高从高到低排列后取前三个,这叫做“排序与限制”,对应SQL里的ORDER BY和LIMIT子句。

如果把这三个动作分别拆开看,每一个都不难。难点在于把它们组合起来使用,并且用得熟练。这就像学做菜,洗菜、切菜、炒菜单独看都不难,但要在十分钟内完成一桌菜,靠的是对每个步骤的熟练度和串联能力。MySQL单表查询的语法结构,本质上就是这三大动作的固定组合模板,你把该填的位置填对了,查询就写对了。

理解了这层逻辑,后面接触复杂查询时就不会慌。因为再复杂的单表查询,也无非是在“选字段、筛行、排序”这三个环节上叠加更多条件,比如多个筛选条件同时生效、按多个字段排序、对分组后的结果再做筛选。万变不离其宗。

2. 基础语法上手:SELECT的几种常用写法

2.1 最简单的SELECT:全字段查询与指定字段查询

打开MySQL客户端,连接到某个数据库之后,第一句SQL几乎都是SELECT * FROM 表名;。这里*代表所有字段,整句话的意思是:把这张表里的所有字段、所有行的数据全部展示出来。这条语句最适合刚建完表时快速查看数据结构,确认数据有没有正确导入。但生产环境里,除非表很小,否则我强烈不建议频繁使用SELECT *,原因后面会专门讲。

指定字段查询就是把*换成具体的字段名,多个字段之间用英文逗号分隔。例如:

SELECT name, age, city FROM users;

这条语句只返回name、age、city三列。它的好处是:结果更清晰,只展示你需要的信息;数据量更小,网络传输压力更低;配合后面的WHERE条件时,查询语义更加明确。我在实际工作中写查询语句,永远先明确自己需要哪些字段,而不是一股脑全捞出来。

字段还有一个很有用的细节:可以给字段起别名。用法是在字段名后面加一个AS,AS可以省略。比如:

SELECT name AS 姓名, age AS 年龄 FROM users;

如果字段名字比较长,起一个短别名,能让查询结果的表头更简洁。但要注意,别名只在当前这条SQL中生效,它并不会真的修改表结构。这个特性在后续配合ORDER BY、GROUP BY时也有用,你可以直接在排序或分组时引用别名。

2.2 WHERE条件筛选:让数据按你的规则留下来

WHERE子句是单表查询中使用频率最高、也最容易出问题的部分。它的作用就是在每一行数据上做判断,满足条件的行为保留,不满足的直接丢弃。判断的依据就是各类运算符。

最基础的是比较运算符:=等于、>大于、>=大于等于、<小于、<=小于等于、!=或<>不等于。例如查询城市为“北京”的用户:

SELECT name, city FROM users WHERE city = '北京';

这里有一个非常重要的细节:字符串和日期类型的值在条件里必须用单引号引起来,数字不需要。如果城市名是中文,也照样用单引号。初学者常犯的错是把数字类型的字段也加上引号,虽然有时候MySQL会帮你自动转换,但最好不要依赖这种隐式行为,它会带来性能隐患。

多个条件用AND和OR连接。AND表示多个条件同时满足,OR表示满足任意一个即可。在实际项目中,AND用得更多,因为业务场景通常是“既要...又要...”。比较典型的一条查询:

SELECT name, age FROM users WHERE city = '北京' AND age >= 18;

这条语句的语义是“查北京的成年人”。需要注意的是,AND的优先级高于OR。如果一条语句里同时出现AND和OR,并且你没有加括号,MySQL会先处理AND。为了防止逻辑错误,我建议凡是AND和OR混用,一律用括号明确分组。比如“查北京或者上海的成年人”:

SELECT name, age FROM users WHERE (city = '北京' OR city = '上海') AND age >= 18;

如果不加括号,写成WHERE city = '北京' OR city = '上海' AND age >= 18,那它的实际语义就变成了“北京的所有用户,加上上海的成年人”,结果完全不同。这个坑我在刚工作时踩过一次,排查了半天才发现是括号惹的祸。

除了比较运算符,还有一个非常常用的IN操作符,用来匹配一组值。WHERE city IN ('北京', '上海', '广州')等价于WHERE city = '北京' OR city = '上海' OR city = '广州'。IN写起来更简洁,也更易读。LIKE操作符则是做模糊匹配,WHERE name LIKE '张%'表示匹配所有以“张”开头的姓名,%是任意多个字符的通配符,_表示任意一个字符。模糊查询好用,但要注意:LIKE只要用了%开头的写法,比如'%张',索引就基本失效了,数据量大的时候查询会明显变慢,这个问题后面会细说。

2.3 排序、分页与去重:让查询结果更顺手

数据查出来了,但展示顺序可能不是你想要的,这时候用ORDER BY子句。默认是升序ASC,可以省略;降序用DESC。多个字段排序时,先按第一个字段排,第一个字段相同再按第二个排。例如:

SELECT name, age, score FROM students ORDER BY score DESC, age ASC;

这条语句先按分数从高到低排,分数相同的情况下,年龄小的排前面。在实际场景里,这种“先按主指标、再按副指标”的需求非常普遍,比如排行榜在总分相同时按提交时间排序。

分页是另一个高频场景。数据量一大,不可能一次性展示所有内容,尤其是前端页面上的“第1页、第2页”翻页效果,靠的就是LIMIT子句。LIMIT offset, count中,offset表示跳过多少行,count表示取多少行。例如:

SELECT name FROM users LIMIT 10, 20;

这条语句表示跳过前10行,从第11行开始取20行,也就是取第11到第30行。更常见的写法是LIMIT 20 OFFSET 10,含义完全一样。在计算分页参数时,第page页、每页pageSize条数据,offset就是(page - 1) * pageSize。这个公式无论在生产代码里还是在Navicat等图形化工具里都很常用,建议熟记。

DISTINCT关键字用来去重。它的作用是去除查询结果中完全重复的行。例如:

SELECT DISTINCT city FROM users;

这条语句返回所有出现过的城市,每个城市只出现一次。需要注意,DISTINCT作用于整行或者列的组合,不是只作用于某一个字段。如果你写SELECT DISTINCT city, age FROM users,那么只有当city和age两列都相同才会被去重。

3. 聚合分组与常用函数:查询能力再上一个台阶

3.1 五个聚合函数:COUNT、SUM、AVG、MAX、MIN

聚合函数解决的核心问题是“统计”。假设老板问:我们有多少个注册用户?所有订单的总金额是多少?这些不是简单地看某一列,而是要基于整张表或者某一批数据做计算。五个最基础的聚合函数分别是COUNT、SUM、AVG、MAX、MIN。

  • COUNT:统计行数。SELECT COUNT(*) FROM users;统计表里的总行数;SELECT COUNT(age) FROM users;统计age字段非空的行数。
  • SUM:求和,一般用于数值字段。SELECT SUM(amount) FROM orders;统计订单总金额。
  • AVG:求平均值。SELECT AVG(score) FROM students;统计平均分。
  • MAX / MIN:求最大值和最小值。SELECT MAX(price), MIN(price) FROM products;统计商品最高价和最低价。

使用聚合函数时有一个基础但重要的规则:聚合函数不能直接和普通字段混在同一个SELECT里,除非那个普通字段出现在GROUP BY子句中。这句话有点绕,我用例子说明。SELECT name, MAX(score) FROM students;这条语句在大多数数据库里会直接报错,因为name是普通字段,MAX(score)是聚合结果,数据库不知道该如何返回哪个name。正确做法是先分组,再统计,这就是下一节要讲的GROUP BY。

聚合函数还常和WHERE配合:先筛选后聚合。SELECT COUNT(*) FROM orders WHERE status = '已完成';先筛出已完成的订单,再统计数量。注意聚合函数的计算结果可以起别名,比如SELECT COUNT(*) AS total FROM users;,这个别名在后续查询展示时就以total作为列名出现,比那种一大串的表达式简洁多了。

3.2 GROUP BY分组统计:按类别汇总的正确姿势

GROUP BY子句解决的是“按某个维度分组统计”的问题。最经典的例子:统计每个城市的用户数。你不可能一条一条数,正确的SQL是:

SELECT city, COUNT(*) AS user_count FROM users GROUP BY city;

这条语句的执行逻辑是:先把所有用户按city字段分成若干个组,北京的用户一组、上海的用户一组,然后分别对每组执行COUNT统计。结果的每一行就是一个城市以及该城市的用户人数。

GROUP BY可以和多个聚合函数一起用。比如统计每个城市的用户数和平均年龄:

SELECT city, COUNT(*) AS user_count, AVG(age) AS avg_age FROM users GROUP BY city;

这里就有个容易混淆的点:SELECT后面除了聚合函数,只能出现GROUP BY后面的字段。你在SELECT里写city没问题,因为city就是分组字段;但如果你写name,MySQL在大多数模式下会直接报错,或者返回一个不符合预期的结果。这个规则本质上是因为分组之后,每组内的其他字段无法确定是哪一行。

还有一个重要概念叫HAVING,它专门用来对分组后的结果再做筛选。很多人分不清它和WHERE的区别,我一句话解释:WHERE是在分组之前筛原始行,HAVING是在分组之后筛结果组。比如查询用户数大于100的城市:

SELECT city, COUNT(*) AS user_count FROM users GROUP BY city HAVING user_count > 100;

注意,HAVING后面可以引用SELECT里起的别名,但WHERE不能引用别名,这一点也是实用中经常踩坑的地方。WHERE的执行时机早于SELECT,此时别名还不存在;HAVING的执行时机晚于SELECT,别名已经可用。

3.3 常用字符串、日期与逻辑函数

聚合函数负责统计,但业务中经常还需要对字段做加工转换,这就用到各类表达式函数。字符串函数里最常用的几个:CONCAT拼接字符串,比如把姓和名拼起来;SUBSTRING截取子串;LENGTH或CHAR_LENGTH计算字符长度;UPPER和LOWER转换大小写;TRIM去掉字符串两端的空格。

SELECT CONCAT(last_name, first_name) AS full_name FROM users; SELECT SUBSTRING(phone, 1, 3) AS prefix FROM users;

日期函数也很常用。WHERE条件里筛某一天的数据,最常见的写法是:

SELECT * FROM orders WHERE DATE(create_time) = '2024-06-01';

但注意,对create_time字段使用DATE函数后,这一列的索引通常无法生效,数据量大时查询会慢。更好的做法是直接用范围条件:

SELECT * FROM orders WHERE create_time >= '2024-06-01 00:00:00' AND create_time < '2024-06-02 00:00:00';

这个写法不仅语义更准确,而且能利用索引。日期函数还有一个常用场景是格式化输出,DATE_FORMAT可以把日期转成任意格式,比如DATE_FORMAT(create_time, '%Y-%m-%d'),显示出来就是干净的日期字符串。

逻辑函数里最实用的是CASE WHEN,它相当于在SQL里写if-else。比如给订单打标签:

SELECT order_id, CASE WHEN amount >= 1000 THEN '大单' WHEN amount >= 100 THEN '中单' ELSE '小单' END AS order_level FROM orders;

CASE WHEN在统计场景里尤其强大,配合SUM可以完成“有条件计数”。比如统计每个月份的订单里,金额超过1000的数量有多少,这在报表里很常见。

4. 从建库到实战:完整跑一遍单表查询流程

4.1 建库建表与准备测试数据

理论讲再多,不如亲手敲一遍。我建议你跟着我完整走一遍流程,从建库开始,到写出一条带统计的完整查询结束。第一步是创建数据库和表。打开MySQL命令行,执行:

CREATE DATABASE IF NOT EXISTS shop; USE shop;

创建一个用户表和一个订单表,结构保持简单,方便看清查询逻辑:

CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, city VARCHAR(20), age INT, created_at DATETIME ); CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT, amount DECIMAL(10,2), status VARCHAR(20), created_at DATETIME );

这里AUTO_INCREMENT表示自增主键,插入数据时不需要手动填id。DECIMAL(10,2)是金额字段的推荐类型,比FLOAT精确,不会出现浮点数误差。

接下来插入一些测试数据。不用多,十条用户、十几条订单就足够练习了:

INSERT INTO users (name, city, age, created_at) VALUES ('张三', '北京', 25, '2024-01-05 10:00:00'), ('李四', '上海', 30, '2024-01-08 14:30:00'), ('王五', '北京', 22, '2024-02-10 09:15:00'), ('赵六', '广州', 35, '2024-02-18 16:45:00'), ('孙七', '上海', 28, '2024-03-02 11:20:00'), ('周八', '深圳', 26, '2024-03-15 08:00:00'), ('吴九', '北京', 40, '2024-04-01 20:10:00'), ('郑十', '广州', 24, '2024-04-12 18:30:00'), ('刘一', '上海', 29, '2024-05-03 12:55:00'), ('陈二', '深圳', 31, '2024-05-20 07:25:00');

插入订单数据时,可以随机关联用户id,状态使用“已完成”“待付款”“已取消”几种值。为了方便练习,我把amount和status都设计了不同组合。

提示:在命令行里执行多条SQL时,每一条记得用分号结尾。如果中间某条语句写错了,MySQL会一直等待你补充完整的语句,这时候输入\c可以取消当前输入。

4.2 实战场景一:用户表的筛选与排序

现在数据有了,我们来做一个业务中非常常见的需求:筛选出北京地区、年龄在25到35岁之间的用户,按年龄从小到大排序。

SELECT name, city, age FROM users WHERE city = '北京' AND age BETWEEN 25 AND 35 ORDER BY age ASC;

BETWEEN 25 AND 35是age >= 25 AND age <= 35的简写,注意它是包含边界的。这条语句的结果应该只有一条记录:吴九,40岁?不对,北京的用户是张三25、王五22、吴九40。25到35之间只有张三。所以结果是张三25。这个例子刚好展示了条件筛选的把关作用。

再看一个带分页的排序需求:查询年龄最大的三个用户。

SELECT name, age, city FROM users ORDER BY age DESC LIMIT 3;

执行结果应该是吴九40、赵六35、陈二31。在真实项目里,这种“取前N名”的需求,比如排行榜前三名、最近登录的10个用户,都是同样的套路:ORDER BY对应字段,再加LIMIT。

再来一个聚合统计场景:统计每个城市的用户数量。

SELECT city, COUNT(*) AS total FROM users GROUP BY city ORDER BY total DESC;

执行结果应该是:北京3人、上海3人、广州2人、深圳2人。这样一条语句,就把分组、聚合、排序全部串联起来了。你从最初的SELECT *一路写到这里,单表查询的核心能力已经基本掌握。

4.3 实战场景二:订单表的分组统计与多条件筛选

订单表比用户表多几个维度,实战时能玩的花样也更多。先看一个需求:统计每种状态的订单数量和总金额。

SELECT status, COUNT(*) AS order_count, SUM(amount) AS total_amount FROM orders GROUP BY status;

这个查询的每一行代表一种订单状态,“已完成”“待付款”“已取消”各自的单数和金额一目了然。如果要进一步筛选,只看状态为已完成和待付款的订单:

SELECT status, COUNT(*) AS order_count, SUM(amount) AS total_amount FROM orders WHERE status IN ('已完成', '待付款') GROUP BY status;

这里体现了一个关键的执行顺序:先执行WHERE筛选原始行,再执行GROUP BY分组,最后执行聚合函数。你把这个顺序理清了,就不会再困惑“为什么HAVING里能用的条件,WHERE里不能写”这类问题。

再进阶一点,统计订单金额大于1000的“大单”数量,按月份维度看趋势:

SELECT DATE_FORMAT(created_at, '%Y-%m') AS month, COUNT(*) AS big_order_count FROM orders WHERE amount > 1000 GROUP BY DATE_FORMAT(created_at, '%Y-%m') ORDER BY month;

这条语句综合了范围筛选、日期格式化、分组、统计、排序,正是单表查询里最典型的“大综合”题型。我建议你把它一行一行读下来,确认每一步都在做什么。SELECT后面出现的DATE_FORMAT(created_at, '%Y-%m')虽然不是表里的原生字段,但它可以作为分组的依据,因为它的值是由原始字段计算出来的。

5. 单表查询常见问题与排查思路

5.1 高频报错与解决办法速查表

实际写SQL一定会报错,报错是学习的一部分。我把最常见的几种错误整理成了表格,方便你遇到时快速对照。

报错信息常见原因解决办法
ERROR 1064 (42000)语法错误,通常是拼写错误、缺少引号、关键字打错检查SQL中关键字是否拼对,字符串是否有单引号,分号是否遗漏
ERROR 1146 (42S02)表不存在,或者没有切换到正确的数据库使用USE 数据库名;切换,或用数据库名.表名
ERROR 1054 (42S22)字段不存在,字段名写错用DESC 表名;查看表结构,核对字段名
ERROR 1055 (42000)SELECT中的字段不在GROUP BY里,且在聚合函数之外将该字段加入GROUP BY,或改用聚合函数
ERROR 1111 (HY000)聚合函数嵌套使用非法聚合函数不能直接嵌套其他聚合函数,先子查询或分步处理

其中ERROR 1055是初学GROUP BY时最容易踩的。我之前讲过那个规则:SELECT里的普通字段必须出现在GROUP BY中。但当MySQL的sql_mode里没开启ONLY_FULL_GROUP_BY时,这个错误可能不会出现,反而会返回一个随机值,这才是更可怕的——不报错,但结果不对。所以我建议你把sql_mode设置成开启ONLY_FULL_GROUP_BY模式,宁可一开始严格报错,也不要等到数据出错再去排查。

5.2 查询写对了但还是慢?基础性能排查

单表查询在数据量小的时候,怎么写都快。但数据量达到几十万、几百万行时,性能差异就开始显现。有几个基础的排查方向,即使你是初学者,也应该了解一下,因为这直接关系到你对SQL写得好坏的判断。

第一,尽量避免SELECT *。它会把所有字段捞出来,尤其当表里有大字段(长文本、JSON)时,会消耗大量的IO和网络带宽。你只需要什么字段就查什么字段,这个习惯越早养成越好。

第二,WHERE条件里对字段做函数运算,会导致索引失效。比如WHERE DATE(create_time) = '2024-06-01',虽然结果正确,但它对create_time做了函数处理,MySQL无法直接走索引,只能全表扫描。改成create_time >= '2024-06-01 00:00:00' AND create_time < '2024-06-02 00:00:00',就可以命中索引。这种改写不改变结果,但性能差好几倍。

第三,LIKE模糊匹配时,通配符尽量不要放在开头。LIKE '张%'可以走索引,LIKE '%张'不行。原因很简单,在索引的B+树结构里,从“张”开头的字符串是有序连续的,可以快速定位;但以“张”结尾的字符串无法按前缀查找。

第四,使用EXPLAIN查看执行计划。这是MySQL自带的性能分析工具,用法很简单:在SELECT语句前面加EXPLAIN关键字:

EXPLAIN SELECT * FROM users WHERE age > 20;

执行后返回一张表,里面有几个关键字段要关注。type列从好到差依次是const、ref、range、index、ALL,如果看到ALL,说明是全表扫描,要警惕。rows列显示估计扫描的行数,数字越小越好。Extra列如果出现Using filesort,说明排序没有走索引,数据量大时也会有额外开销。

5.3 我踩过的一些坑和操作心得

最后分享一些我在实际工作中踩过的坑和总结下来的操作习惯,希望能帮你少走弯路。

第一个坑是写SQL时忘了“逻辑边界”。举个例子,查“2024年6月”的订单,有些人会写WHERE create_time >= '2024-06-01' AND create_time <= '2024-06-30'。这个写法在无时区概念下勉强可用,但6月30日晚上11点59分之后的订单会被漏掉?其实不会,因为条件是<= '2024-06-30',也就是6月30日00:00到24:00?不对,'2024-06-30'会被解析为'2024-06-30 00:00:00',意味着6月30日当天的数据全部没有了。正确写法是create_time >= '2024-06-01' AND create_time < '2024-07-01',使用左闭右开区间。这个教训我印象很深,因为类似的逻辑错误在统计报表里特别隐蔽,数字不错时你根本不会怀疑到它。

第二个坑是直接在生产库上跑没有WHERE的UPDATE或DELETE。虽然文章主题是查询,但这个习惯必须提前说。一次误删全表数据的代价,可能比你学一个月SQL的成本高得多。我个人的经验是:在写任何UPDATE或DELETE之前,先写一条对应的SELECT,用SELECT确认将要影响的数据范围,再把SELECT改成UPDATE或DELETE。这个“先查后删”的习惯,救过我很多次。

第三个心得是善用别名和格式化,SQL的可读性比想象中重要。一条几千行的SQL,如果全部挤在一行,过两个星期再看,连你自己都不想维护。我习惯的格式是多行书写:SELECT子句单独一行,FROM单独一行,WHERE每个条件一行,ORDER BY和LIMIT放在最后。字段用别名补充业务含义,比如COUNT(*) AS 用户数。代码是给人看的,顺便让机器执行。

第四个心得是多用手写SQL,少依赖图形化工具自动生成。Navicat这类工具确实方便,但在学习阶段,拖拽生成SQL会让你缺失对语法的感知。手写几个星期之后,你会形成一种条件反射,看到需求脑子里自动浮现SQL骨架。这种能力在排查问题时特别重要,因为你拿到一条报错的SQL,能立刻判断是哪个环节出了问题,而不是重新拖一遍看工具生成什么。

单表查询是整个MySQL学习路径上最基础、也最应该练熟的部分。它不涉及复杂的表关系,却能帮你把SELECT、WHERE、GROUP BY、ORDER BY、LIMIT这些核心语法全部打通。这些能力往后延伸到多表连接、子查询、窗口函数,本质上都是在单表查询的地基上叠加新的维度。所以别嫌它简单,把基础打扎实了,后面的路会顺很多。

返回列表