搞MySQL开发和运维这些年,每天敲得最多的就是SELECT和SHOW这两类语句。一个是查业务数据的,一个是看数据库结构状态的,两者配合起来,基本覆盖日常七八成的查询需求。这篇文章把“简单检索”这个基础主题从头到尾捋一遍:SELECT怎么写才规范,SHOW有哪些高频用法,哪些细节容易踩坑。适合刚开始学MySQL的新手,也适合写过一阵SQL但没系统整理过基础的同学查漏补缺。
很多过来人说“SQL基础很重要”,重要性到底体现在哪?我的体会是,它直接决定排查问题的速度。同样一条报错SQL,有人扫一眼就知道是执行顺序问题,有人得折腾半小时。差别不在智商,而在对基础语句的理解深度。所以别嫌简单检索太基础,它恰恰是后面学连表、子查询、窗口函数的地基。
1. 先弄明白SELECT和SHOW在简单检索里的分工
1.1 查询的背后是“取什么、从哪取、怎么筛”
很多新手学SELECT,第一件事就是背语法,背完就觉得自己会了。结果一到真实需求就懵,明明语法都对,就是写不出想要的查询。问题出在哪?出在没有理解SELECT本质上就三件事:从哪张表取数据,取哪些列,按什么条件筛。
我把这个思路叫“取、定、筛”。任何一条SELECT,就算写了几百行、套了多层子查询,执行计划落到底层,核心动作也就是这三件事。复杂SQL不过是多了聚合、连接、排序这些加工环节,但骨架没变。所以掌握简单检索的关键,不是背更多写法,而是把“取、定、筛”想清楚。
举个例子,需求是“查2024年1月下单且状态为已支付的订单ID和金额”。拆解一下就是:从orders表取,取order_id和amount两列,筛条件是下单时间在1月、状态为已支付。翻译成SQL就非常顺。反过来,如果你一上来就闷头写SELECT,边写边想条件,很容易漏条件或者错字段。这个习惯我从带新人第一天就会强调,它能少走太多弯路。
1.2 SHOW不是辅助命令,它是查看数据库结构的快速入口
刚接触数据库的同学,普遍对SHOW重视不够,觉得它只是个“看一眼”的命令。但实际工作中,SHOW恰恰是排查问题的利器。比如业务查询总报“字段不存在”,与其翻建表文档,不如直接SHOW COLUMNS FROM 表名,一秒看到真实字段。比如线上库多了一批临时表,SHOW TABLES扫一眼就能看出来。再比如想知道某次慢查询有没有走索引,SHOW INDEX FROM 表名一看便知。
SHOW和SELECT最大的区别在于查询对象不同。SELECT查的是业务数据,是咱们自己写入表里的内容;SHOW查的是元数据和运行状态,是数据库自己维护的结构、配置、会话等信息。理解了这个分工,面对问题时就能快速判断该用哪条命令,而不是胡子眉毛一把抓。这个区分我在后面会反复用到。
2. SELECT语法骨架与执行顺序:写错顺序的SQL都栽在这
2.1 一句话记住核心语法
完整SELECT的语法大概是这个样子:
SELECT 列1, 列2, ... FROM 表名 WHERE 过滤条件 GROUP BY 分组列 HAVING 分组后过滤条件 ORDER BY 排序列 LIMIT 数量;做简单检索,不需要一下子消化这么多。先把前面四行记牢:
SELECT 列 FROM 表名 WHERE 过滤条件 LIMIT 数量;这四行覆盖日常绝大多数查询需求。GROUP BY和HAVING属于分组统计场景,后面遇到再学,不影响入门。关键是别一上来被完整语法吓住,检索的核心骨架并不复杂。我见过不少教程把完整语法图直接甩出来,新手当场劝退,其实完全没有必要。
2.2 逻辑执行顺序才是SQL的底层逻辑
这是SQL写错却找不到原因的最常见根源。书写顺序从SELECT开始,但数据库真正的执行顺序完全不同:
- FROM:先确定数据来源
- WHERE:做逐行过滤
- SELECT:确定输出列、计算别名和表达式
- ORDER BY:排序
- LIMIT:限量输出
这个顺序解释了无数个“为什么”。最典型的一个:WHERE里能不能用SELECT里定义的别名?答案是不能。因为WHERE执行时,SELECT还没跑,别名根本不存在。很多人在这里报错后疯狂检查语法,其实问题就在执行顺序。
还有一个容易踩的例子:同样一条条件,放在WHERE里过滤再输出,和先输出再在内存里筛,结果完全两回事。理解了执行顺序,你就明白为什么必须在WHERE里写条件,而不是依赖SELECT后面的加工。基础概念的清晰度,就体现在这些细节里。
2.3 没有FROM的SELECT:快速验证连接与表达式的技巧
说到SELECT,很多人忽略了一个基本事实:SELECT不强制要求有FROM子句。也就是说,可以这样写:
SELECT 1 + 1; SELECT NOW(); SELECT VERSION();这类语句不查任何表,却非常实用。日常排查时,我经常用它来验证数据库连接是否正常、当前版本是多少、服务器时间对不对。特别是在命令行里定位问题,先执行一条SELECT VERSION(),确认连接和实例没问题,再去查业务表,少走很多弯路。这个技巧很多文档不会专门写,但实际用起来频率极高。
3. SELECT高频写法拆解:列、条件、去重一次弄清
3.1 列的选取、别名的使用习惯
查单列很简单:
SELECT user_name FROM user_info;查多列要注意两点。一是逗号分隔,最后一个字段名后面别带逗号。二是查询多字段时,我习惯一个字段一行:
SELECT user_id, user_name, mobile FROM user_info;字段一多,挤在一行很容易看花眼,一个字段一行的排版虽然多占几行,但排查问题时省眼力,代码评审时也容易被看懂。
别名是另一个高频特性。给列或表达式起一个更好懂的名字,写法有两种:
SELECT user_name AS 姓名 FROM user_info; SELECT user_name 姓名 FROM user_info;AS可以省略,但我个人建议写上AS,语义更清晰,别人看代码不用猜。注意别名的边界:它只在本次查询的结果中生效,不会修改表的真实字段名。导出报表、写临时分析任务时,给长表达式起简明别名会方便很多。
3.2 WHERE条件过滤:等值、范围、集合、模糊、空值一起搞定
WHERE是检索的灵魂。没有WHERE的查询,要么数据量小感觉不到问题,要么就是全表扫描,线上很容易把库拖慢。简单检索阶段,先掌握下面几类条件。
等值过滤,最常见:
SELECT * FROM orders WHERE status = 1;范围过滤,用比较运算符加AND组合:
SELECT * FROM orders WHERE create_time >= '2024-01-01' AND create_time < '2024-02-01';这里有个细节值得单独说。日期范围我强烈建议用“大于等于开始日、小于结束日”的半开区间写法,而不是BETWEEN。因为BETWEEN是闭区间,对DATETIME类型字段做月范围查询时,很容易漏数据。比如查1月整月,写成BETWEEN '2024-01-01' AND '2024-01-31',等价于create_time >= '2024-01-01 00:00:00' AND create_time <= '2024-01-31 00:00:00',也就是说1月31日0点之后的数据都不会被包含进来。虽然可以写成BETWEEN '2024-01-01' AND '2024-01-31 23:59:59',但万一数据有毫秒或微秒精度,还是可能卡边界。半开区间就没这些问题,这是我踩过坑之后改掉的写法。
集合过滤,用IN:
SELECT * FROM orders WHERE status IN (1, 2, 3);等价于status等于1或2或3,是多个OR条件的简化写法。NOT IN也可以用,但要注意一个经典坑:如果IN列表里包含NULL,整个条件的判断结果会变为“不确定”,最终一行都查不出来。很多人在线上查不到数据,排查半天,最后发现是NOT IN (1, 2, NULL)这种写法在作怪。
模糊匹配,用LIKE:
SELECT * FROM customer WHERE name LIKE '张%';%表示任意多个字符,表示任意单个字符。姓张的用'张%',两个字的姓名用'张'。如果业务上真的需要匹配百分号或下划线本身,要用ESCAPE做转义,这个属于少见的特殊场景,先知道有这回事就行。
空值判断,用IS NULL:
SELECT * FROM customer WHERE mobile IS NULL;注意,空值判断千万不能写成 = NULL。因为NULL代表不确定值,任何跟NULL做等值比较的结果都是不确定的,WHERE会把它当假处理。这个也是SQL新手最容易踩的坑之一,我几乎每年都能见到有人在线上栽这一下。
3.3 DISTINCT去重,以及它的隐含坑
去除重复值,用DISTINCT:
SELECT DISTINCT status FROM orders;行为很简单,但坑也不少。第一,DISTINCT作用于后面所有列的组合,不是第一个字段。比如SELECT DISTINCT category_id, status,去重的是两个字段的组合,不是单独把category_id去重。第二,DISTINCT实现去重往往需要额外的排序或哈希操作,数据量大时性能下降明显。如果只是想看某个字段有哪些取值,且字段上正好有索引,用GROUP BY有时更稳。第三,DISTINCT和ORDER BY一起用时,排序列必须出现在SELECT列表中,否则可能报错。这些细节看起来小,但每一个都足以让人卡住半天。
4. SHOW语句实战用法:查库、查表、查结构、查状态
4.1 最常用的三条SHOW,背下来直接受益
日常使用频率最高的三条SHOW:
SHOW DATABASES; SHOW TABLES; SHOW CREATE TABLE 表名;SHOW DATABASES列出当前实例里的所有数据库,SHOW TABLES列出当前选中数据库里的所有表。注意,SHOW TABLES如果还没选库,会报No database selected,这时候要么先执行USE 库名,要么直接写SHOW TABLES FROM 库名,后者更省一步。这个细节别忽略,命令行操作时经常碰到。
重点说SHOW CREATE TABLE,它返回的是一段完整的建表语句,包含字段定义、索引、默认值、字符集、存储引擎、分区信息等一切结构信息。命令行下,这条命令比任何图形化工具都全、都准。遇到结构相关疑问,直接用它,基本不用猜。
4.2 SHOW COLUMNS与DESC,快速查看表结构
需要快速确认表里有哪些字段、字段类型、是否为空、默认值是什么,用:
SHOW COLUMNS FROM user_info;结果会返回Field、Type、Null、Key、Default、Extra六列信息。Key列看得出来主键和索引,Default列看得出来默认值,Type列看得出来数据类型。它有一个更短的同义写法:
DESC user_info;两条命令输出完全一样。我个人习惯写SHOW COLUMNS,因为表意明确,但看到别人用DESC也能立即对应上。怎么顺手怎么来,关键是读懂输出字段的含义。看表结构这件事,看起来基础,实际上每天都要用,越熟练越好。
4.3 巡检和排障常用的SHOW变体
SHOW不只是看表结构,还能看索引、状态参数、字符集配置和正在执行的SQL。这几条在排障时特别实用:
SHOW INDEX FROM user_info; SHOW STATUS LIKE 'Threads_%'; SHOW VARIABLES LIKE 'character_set_%'; SHOW PROCESSLIST;SHOW INDEX可以看到索引名称、索引字段顺序、是否唯一,对判断慢查询很有帮助。SHOW STATUS能看线程数、连接数等运行状态,搭配LIKE可以精准过滤,避免满屏输出。SHOW VARIABLES看配置变量,字符集乱码问题基本靠它定位。SHOW PROCESSLIST则直接列出当前所有连接和正在执行的SQL。线上性能抖动,先跑它,能看到谁在跑什么大查询,往往一眼找到病根。
这里有个使用习惯值得说:SHOW语句后面可以带条件过滤,比如SHOW STATUS LIKE 'Threads_connected',只输出想要的那一行。如果先全量输出再人眼找,输出一长串,效率低还容易看漏。养成带LIKE的习惯,命令行操作会舒服很多。
4.4 别忘了SHOW WARNINGS
还有一个平时不起眼、排查时很救命的小命令:SHOW WARNINGS。有些语句执行时会返回告警信息,比如数据被截断、隐式类型转换、严格模式被忽略等,直接看执行结果可能不明显。执行完紧接着执行SHOW WARNINGS,就能看到系统给出的具体提示。
尤其在导入数据、批量更新后发现某些行没按预期处理时,这个命令能快速还原原因。我自己的习惯是:凡是执行结果“看起来不太对,又说不清哪里不对”,就补一条SHOW WARNINGS。别小看它,很多诡异现象的最后真相都藏在这里。
5. 排序、限量、分组与聚合:把检索结果整理成想要的样子
5.1 ORDER BY的细节决定排序结果是否准确
检索结果的默认顺序,数据库是不做任何保证的。看起来像按插入顺序输出,但实际上全表扫描时可能按物理存储顺序、索引顺序或其他内部顺序返回。所以,只要业务对顺序有要求,就必须显式ORDER BY。
单字段排序很简单:
SELECT user_id, create_time FROM user_info ORDER BY create_time DESC;ASC是升序,可以省略;DESC是降序,要显式写。需要注意多字段排序时的优先级:
SELECT user_id, status, create_time FROM orders ORDER BY status ASC, create_time DESC;这个查询先按status升序排,status相同的记录再按create_time降序排。次序非常重要,写反了结果完全不同。还有一种用法是按别名排序:
SELECT user_name, create_time AS reg_time FROM user_info ORDER BY reg_time DESC;因为排序发生在SELECT之后,别名在排序时已经存在,所以可以用。这也是前面说的执行顺序的具体体现。
5.2 LIMIT限量输出与分页的代价
LIMIT用来限定返回行数。最简写法:
SELECT * FROM orders LIMIT 10;表示最多返回10行。分页时常用偏移量写法:
SELECT * FROM orders LIMIT 0, 10; SELECT * FROM orders LIMIT 10, 10;第一行表示跳过0行取10行,也就是第一页;第二行表示跳过10行取10行,也就是第二页。这里要清楚LIMIT后的两个数字,第一个是偏移量offset,第二个是数量count。
LIMIT最经典的坑是深分页。offset越大,数据库需要扫描并丢弃的行越多。比如LIMIT 1000000, 10,为了拿最后10行,数据库可能要扫描并排序前面的一大片数据。数据量小的时候完全没感觉,到百万级以后查询会越来越慢。解决思路通常是改用主键过滤,或者记录上一页最后一条的ID继续往后查,但那是进阶话题,简单检索阶段先理解代价在哪就行。
5.3 聚合函数与GROUP BY的第一次配合
有些检索不要明细,只要统计结果。这时候用聚合函数。最常用的五个:
- COUNT:计数
- MAX:最大值
- MIN:最小值
- SUM:求和
- AVG:平均值
单独使用,不分组:
SELECT COUNT(*) FROM orders WHERE status = 1; SELECT MAX(create_time), MIN(price) FROM orders;要按类别统计,就得配合GROUP BY:
SELECT category_id, COUNT(*), AVG(price) FROM product GROUP BY category_id;这条SQL按category_id分组,每组返回一个统计结果。这里要理解GROUP BY的执行位置:它在WHERE之后执行,所以WHERE能过滤掉不参与分组的数据;而HAVING是对分组后的结果再做过滤,和WHERE不是一回事。简单场景可以这样记:WHERE管行,HAVING管组。
COUNT函数还有个容易错的地方。COUNT()和COUNT(1)在现代版本性能基本一致,但COUNT(字段名)只统计该字段不为NULL的行数。如果业务逻辑是“多少行满足条件”,用COUNT();如果是“这个字段有多少个非空值”,用COUNT(字段名)。选错的话,结果差异会悄悄出现,非常难排查。
6. 高频问题排查:这些坑真的会反复出现
6.1 字段名和保留字冲突,反引号救急
有些历史表的字段名偏偏叫name、desc、order、group这类词,直接在SELECT里用会报语法错误。原因很简单,这些是MySQL的保留字。解决方案是加反引号:
SELECT `desc` FROM article_info;反引号是美式键盘左上角数字1左边那个按键,跟波浪线在一起,千万别打成单引号或双引号。当然,更治本的方式是设计表时避开保留字,但老表不一定能改,反引号是必须会的应急手段。顺带说一句,表名在特殊情况下也可以用反引号包裹,规则是一样的。
6.2 中文乱码,先查连接字符集再怀疑数据
命令行里执行SELECT,返回的中文变成问号,大多数人第一反应是“数据存坏了”。实际上绝大多数情况是连接字符集的问题。数据在表里是好的,但客户端发送SQL和接收结果的字符集与表字符集不一致,中文就显示不对。
解决办法,连接时就指定字符集:
mysql --default-character-set=utf8mb4 -h 主机地址 -u 用户名 -p或者进入客户端后执行:
SET NAMES utf8mb4;SET NAMES会同时设置客户端、连接和结果三个层面的字符集,日常排查时先跑它,比改表稳妥。如果表和连接都正常,再考虑数据本身是否真的有问题。判断表字符集的方法前面讲过,执行SHOW CREATE TABLE就能看到。乱码问题只要方向找对,解决起来非常快。
6.3 为什么我劝你少用SELECT *
SELECT *写起来省事,看起来也没毛病,但生产环境不建议这么干。原因有两点。
第一,它会把表里所有字段都查出来,哪怕你只要两三个字段,网络传输、内存开销都白白浪费。大表宽表场景,这个差距会被放大得非常明显。第二,如果表结构后来新增了字段,SELECT *返回的列就会变化,依赖固定列序解析结果集的程序可能悄悄出错。我的习惯是:临时排查数据可以SELECT *,但凡是写进业务代码或自动化脚本里的查询,一律显式列出需要的字段,一条条写清楚。
6.4 慢查询别靠猜,EXPLAIN是第一步
一条SELECT慢,很多人的第一反应是“加索引”。但索引加在哪、该不该加,不能靠猜。正确起点是先看执行计划:
EXPLAIN SELECT user_id, user_name FROM user_info WHERE mobile = '13800138000';EXPLAIN的输出重点看三列。type是访问类型,从好到差大致有const、ref、range、index、ALL等,如果显示ALL,说明全表扫描了。key是实际用到的索引名,NULL表示没走索引。rows是预估扫描行数,越小越好。我排查慢查询的习惯是:先看key和type,发现全表扫描后,再回头检查WHERE条件里的列有没有索引,有没有对列做计算或函数运算,有没有隐式类型转换。这套流程可以解决绝大多数初级慢查询。
6.5 简单错误速查表
下面这张表是我日常帮人排查时经常遇到的情况,整理成速查形式,方便大家对照:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 字段不存在报错 | 字段名拼写错误或确实不存在 | SHOW COLUMNS FROM 表名确认 |
| 语法错误但看不出问题 | 字段名是保留字 | 尝试加反引号包裹 |
| 查询结果中文乱码 | 连接字符集不一致 | SET NAMES utf8mb4,或连接时指定字符集 |
| WHERE使用别名失败 | 执行顺序问题 | 改成直接写原表达式,或使用子查询 |
| COUNT统计数量偏少 | 用了COUNT(字段名),过滤了NULL | 统计行数请用COUNT(*) |
| NOT IN结果为空 | 列表中包含NULL | 先过滤NULL再使用NOT IN,或改为NOT EXISTS |
| 分页越往后越慢 | offset过大导致扫描过多行 | 改用主键范围或游标分页 |
| 查询突然很慢 | 可能没走索引或隐式类型转换 | EXPLAIN查看type、key、rows |
这张表里的每一条,我在实际工作中都见过不止一次。排查时把它放在手边,效率会高很多。
7. 最后分享一点个人习惯
7.1 我的固定排查顺序
我个人接手一条新业务查询,顺序是固定的:先SHOW CREATE TABLE确认表结构,再SHOW INDEX FROM确认索引情况,然后写SELECT,最后用EXPLAIN验证执行计划。这一套下来,很少出现“查不出来”、“查得慢”这种返工。看起来多花几分钟,实际上省掉的时间远超投入。特别是线上问题紧急的时候,按这个顺序走,思路不会乱。
7.2 新手最该养成的练习习惯
最后给新手一个建议。遇到不会写的检索需求,别急着搜答案,先在纸上写出“取什么、从哪取、怎么筛”三行字,再翻译成SQL。坚持一段时间,你就会发现自己的SQL越来越干净,排查问题的速度也越来越快。基础语句看似简单,恰恰是最值得反复打磨的地方。这个习惯跟着你走多远,SQL水平就能走多远。