MySQL单表查询指南:SELECT与WHERE到分组排序实战
发布时间:2026/9/30 12:02:02来源:尧图网络
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条件时查询语义更加明确。我在实际工作中写查询语句永远先明确自己需要哪些字段而不是一股脑全捞出来。字段还有一个很有用的细节可以给字段起别名。用法是在字段名后面加一个ASAS可以省略。比如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就是分组字段但如果你写nameMySQL在大多数模式下会直接报错或者返回一个不符合预期的结果。这个规则本质上是因为分组之后每组内的其他字段无法确定是哪一行。还有一个重要概念叫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这些核心语法全部打通。这些能力往后延伸到多表连接、子查询、窗口函数本质上都是在单表查询的地基上叠加新的维度。所以别嫌它简单把基础打扎实了后面的路会顺很多。
网站建设高端定制企业官网