MySQL查询命令对软件测试工程师来说真正重要的不是记住一堆语法而是知道什么场景下用哪条命令去解决测试问题。这些年我在测试环境里做得最多的事情无非四类测试前造数、测试中校验、bug 定位时查数、测试后清数。如果你正在学 MySQL或者简历上写了“熟练使用 SQL”但面试时被问住这篇可以按实际工作顺序来读。下面按测试工作中的真实场景拆一遍不背命令只讲怎么用。1. 测试工程师查 MySQL先想清楚这四类需求1.1 造数测试前把数据准备到位功能测试和接口测试经常需要特定状态的数据。比如你要测“已支付订单取消”的流程界面上手动走到已支付状态可能要好几步但如果测试库里已经有一批历史订单直接查一条合适的订单就能继续测。这种需求靠的就是查询命令先定位再配合 INSERT 或 UPDATE 去造数据。注意一个原则造数前先确认业务规则不能直接把状态改成一个业务上不可能出现的值。我一般会先查同类型数据的字段长什么样再照着改这样至少不会把表结构搞错。1.2 校验、定位、清理测试后离不开这三件事测试过程中最常做的事情就是拿界面显示的数字跟数据库里的实际值对比。界面显示订单总数 100 条库里COUNT(*)是多少。界面显示总金额 5000 元库里SUM(amount)是多少。某个用户看不到自己的订单去订单表查这个人的 user_id 到底有没有记录status 是什么有没有被逻辑删除。这些都靠查询命令完成。另外测试会产生大量脏数据比如批量注册了几百个用户测试结束后要清掉否则下一次回归数据对不上。清理不是随便 DELETE要先查到这批数据的共同特征确认影响范围后再处理。1.3 先连接再查表环境、工具和表结构先确保本地或测试机能连上 MySQL。命令行是最通用的方式mysql -h127.0.0.1 -P3306 -uroot -p-h 是主机地址-P 是端口-u 是用户名-p 表示回车后输入密码。如果你用的是 Navicat、DBeaver 或者 MySQL Workbench填好连接信息就行。MySQL Workbench 自带 SQL 编辑器要新建数据表可以直接在编辑器里写CREATE TABLE语句执行不用非要切到图形界面。连接之后先确认自己到底在哪个库SHOW DATABASES; USE test_db; SHOW TABLES;然后看表结构DESC orders; SHOW CREATE TABLE orders;DESC能看到字段名、类型、是否允许 NULL 等信息。SHOW CREATE TABLE能看到建表语句包含索引、字符集这类细节。测试环境连接不上时优先检查网络、端口、密码和当前账号是否允许从当前 IP 访问。报Host ... is not allowed to connect to this MySQL server就说明 IP 不在允许名单里找库管理员加白名单不要自己乱试。2. 单表查询筛选、排序、分页、去重2.1 SELECT 基础结构一次只查你需要的字段单表查询是测试工程师使用频率最高的操作。基本结构是SELECT 字段1, 字段2 FROM 表名 WHERE 条件 ORDER BY 排序字段 LIMIT 行数;先建议只查需要的字段不要一上来就SELECT *。测试环境数据量可能不大但生产环境查全表字段会有不必要的开销。更重要的是只查目标字段眼睛更容易看到关键信息。WHERE 条件里最常用的是这些等值判断status 1范围判断amount 100、create_time 2025-01-01集合判断user_id IN (1001, 1002, 1003)模糊匹配order_no LIKE TEST%空值判断remark IS NULL这里注意LIKE里的%和_不一样。%表示任意多个字符_表示一个字符。比如order_no LIKE TEST_2025%表示 TEST 后面必须有一个任意字符再来 2025 开头才能匹配写错会查不到结果。2.2 ORDER BY 排序和 LIMIT 分页排序语法本身很简单但测试里有两个高频场景。第一个是取最新一条记录。订单表通常有自增 id 或 create_time可以直接SELECT id, order_no, user_id, amount, status, create_time FROM orders ORDER BY create_time DESC LIMIT 1;DESC是倒序ASC是升序。多个字段排序时写在前面的是第一优先级。比如先按状态升序再按时间倒序SELECT order_no, status, create_time FROM orders ORDER BY status ASC, create_time DESC;第二个是分页。MySQL 的 LIMIT 语法有两种写法-- 从第 0 条开始取 10 条 SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 0; -- 等价写法偏移量 0取 10 条 SELECT * FROM orders ORDER BY id LIMIT 0, 10; -- 第二页 SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 10;接口分页测试时重点看第一页和第二页之间数据有没有重复或遗漏。导致重复的常见原因就是 ORDER BY 字段不唯一比如只用 create_time 排序同一秒有两条记录分页后顺序就可能稳定。2.3 DISTINCT 去重和“or 能去重吗”很多面试题里会问mysql 的 or 能去重吗。先给结论不能。or是逻辑连接条件不是去重逻辑。举个例子SELECT name FROM user WHERE age 30 OR age 40;如果两个不同 id 的用户恰好都叫“张三”结果会返回两行“张三”因为 MySQL 没有做任何去重动作。要去重只能主动写去重逻辑。常见方式有三种-- 方式一 SELECT DISTINCT name FROM user; -- 方式二 SELECT name FROM user GROUP BY name; -- 方式三 SELECT name FROM user UNION SELECT name FROM user;UNION默认去重UNION ALL不去重。DISTINCT和GROUP BY都能去重区别在于GROUP BY一般用于分组统计DISTINCT更适合单纯去重。测试里常用 DISTINCT 做数据检查。比如查某个状态下的订单涉及哪些用户SELECT DISTINCT user_id FROM orders WHERE status 1;如果怀疑界面统计数字不对先查重复数据多半能找到原因。3. 聚合统计COUNT、SUM、GROUP BY、HAVING 的验证思路3.1 COUNT 的坑COUNT(*) 和 COUNT(字段)不一样聚合统计是测试工程师校验数据的重要手段。最基础的是 COUNT。SELECT COUNT(*) FROM orders WHERE status 1; SELECT COUNT(1) FROM orders WHERE status 1; SELECT COUNT(remark) FROM orders WHERE status 1;COUNT(*)统计满足条件的总行数COUNT(字段)统计该字段不为 NULL 的行数。如果某条记录的 remark 是 NULLCOUNT(remark)就不会把它算进去。平时做总条数校验用COUNT(*)最稳妥。去重统计用COUNT(DISTINCT 字段)SELECT COUNT(DISTINCT user_id) FROM orders WHERE status 1;这个值代表有多少个用户下过已支付状态的订单跟订单总数是两个概念。3.2 GROUP BY HAVING按维度统计和界面数字对账GROUP BY 是按一个或多个字段分组然后对每组做聚合。测试里最典型的场景是界面按订单状态显示数量SQL 里也要按状态分组统计。SELECT status, COUNT(*) AS order_cnt, SUM(amount) AS total_amount FROM orders WHERE create_time 2025-01-01 GROUP BY status;如果结果和界面对不上说明界面筛选条件或统计逻辑可能有问题。这时候要回头看 WHERE 条件是否一致。HAVING 用于过滤分组后的结果。它和 WHERE 的区别是WHERE 在分组之前过滤行HAVING 在分组之后过滤组。SELECT user_id, COUNT(*) AS order_cnt FROM orders GROUP BY user_id HAVING COUNT(*) 5;这条语句查的是下单超过 5 次的用户常用于识别高频用户或异常刷单。MySQL 5.7 以上默认开启ONLY_FULL_GROUP_BYSELECT 后面只能放分组字段和聚合函数不能随便混入其他字段否则会报错。3.3 常用统计场景平均值、最大最小值SUM、AVG、MIN、MAX 也很常用。SELECT COUNT(*) AS total_cnt, SUM(amount) AS total_amount, AVG(amount) AS avg_amount, MAX(amount) AS max_amount, MIN(amount) AS min_amount FROM orders WHERE create_time BETWEEN 2025-01-01 AND 2025-01-31;对比时要注意 AVG 对 NULL 的处理AVG(字段) 会忽略 NULL 行。如果字段里有大量 NULL平均值可能和界面上的算法不一致需要先确认产品规则。分组合计加日期格式化可以统计每天的数据SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS day, COUNT(*) AS order_cnt FROM orders WHERE create_time 2025-01-01 GROUP BY DATE_FORMAT(create_time, %Y-%m-%d) ORDER BY day;这种查询在测试报表类功能时很常见。界面展示一张日趋势图数据库结果就是核对依据。4. 多表关联JOIN 怎么查业务关联数据4.1 INNER JOIN / LEFT JOIN 怎么选测试环境里的表很少是孤立的。订单表里有 user_id但用户名在用户表商品表里只有 category_id分类名称在分类表。这时候要用 JOIN 把多张表连起来查。SELECT o.order_no, u.user_name, o.amount FROM orders o INNER JOIN users u ON o.user_id u.id WHERE o.status 1;INNER JOIN 只返回两边都匹配上的行。用户表里找不到的订单不会出现在结果里。LEFT JOIN 会保留左表全部数据右表没有匹配时字段显示为 NULL。SELECT o.order_no, o.user_id, u.user_name FROM orders o LEFT JOIN users u ON o.user_id u.id;如果只关心有订单的用户用 INNER JOIN如果想看所有订单包括那些用户已经被删除的订单用 LEFT JOIN。4.2 ON 和 WHERE 的区别这是 JOIN 查询里最容易踩坑的地方。ON 是关联条件WHERE 是结果过滤条件二者执行顺序不一样尤其在 LEFT JOIN 里差别很大。-- LEFT JOIN 下把右表条件放 WHERE可能会丢失左表记录 SELECT o.order_no, o.user_id, u.user_name FROM orders o LEFT JOIN users u ON o.user_id u.id WHERE u.status 1;如果右表 users.status 1 的过滤写在 WHERE 里那么用户状态不是 1 的订单会被过滤掉LEFT JOIN 的效果就变成了 INNER JOIN。正确的做法是把右表过滤条件放进 ON 子句SELECT o.order_no, o.user_id, u.user_name FROM orders o LEFT JOIN users u ON o.user_id u.id AND u.status 1;判断标准很简单如果希望保留左表所有记录右表条件写在 ON 里如果就是要过滤掉未匹配的记录写在 WHERE 里也没问题。4.3 孤儿数据、笛卡尔积、重复行测试里有一个很有用的场景查找没有关联用户的订单也就是孤儿数据。SELECT o.* FROM orders o LEFT JOIN users u ON o.user_id u.id WHERE u.id IS NULL;返回结果就是 user_id 在用户表里不存在的订单。这类数据通常会影响统计报表测试时发现数字对不上可以先查这个。JOIN 常见的两种问题忘记写 ON 或 ON 写错会产生笛卡尔积结果行数是两表行数相乘数量会爆炸式增长。关联字段不是唯一的比如客户表和联系方式表一对多关联JOIN 后订单行数变多需要去重或改成聚合查询。所以遇到 JOIN 后结果明显变多时先检查关联字段是否唯一再检查有没有漏写条件。5. 数据准备和变更INSERT、UPDATE、DELETE 的边界5.1 INSERT 造数单条、批量、SELECT 复制查询手册不能只写 SELECT。测试工程师另一个高频需求是用 INSERT 造数。单条插入INSERT INTO orders (order_no, user_id, amount, status, create_time) VALUES (TEST20250101001, 1001, 99.00, 1, NOW());批量插入只要在 VALUES 后面跟多个括号INSERT INTO orders (order_no, user_id, amount, status, create_time) VALUES (TEST20250101002, 1002, 199.00, 1, NOW()), (TEST20250101003, 1003, 299.00, 1, NOW());如果要复制一批历史数据到快照表可以用INSERT INTO ... SELECTINSERT INTO orders_snapshot (order_no, user_id, amount, status) SELECT order_no, user_id, amount, status FROM orders WHERE create_time 2025-01-01;造数时先看字段约束比如唯一键、非空字段、默认值。批量造数时不要一次性插几十万条数据库会扛不住也容易把测试库搞乱。5.2 UPDATE 前先 SELECT影响范围、不加 WHERE 的风险UPDATE 语法本身不复杂UPDATE 表名 SET 字段 新值 WHERE 条件;但这条命令在测试环境里最危险。很多同学写 UPDATE 时不带 WHERE一执行就把整张表的数据全改了。公共测试库出现这种情况浪费的是整个测试小组的时间。我的习惯是写 UPDATE 之前先写一条相同 WHERE 条件的 SELECT。SELECT order_no, status FROM orders WHERE order_no TEST20250101001;确认这条记录确实存在、确实是想要的目标再执行UPDATE orders SET status 2 WHERE order_no TEST20250101001;热搜词里有个“mysql 中 int 5”其实就是字段算术运算。常见场景是给用户的积分加 5UPDATE points SET bonus bonus 5 WHERE user_id 1001;注意字段类型。如果 bonus 是 varcharMySQL 会做隐式转换转换失败会报错或结果不对。正规做法是把这类字段设计成数值类型SQL 里直接写算术表达式。5.3 DELETE 清理和事务回滚DELETE 比 UPDATE 更要谨慎。DELETE FROM orders WHERE order_no TEST20250101001;生产环境不要随便执行 DELETE测试环境清理数据时也要先确认范围。如果担心删错可以先开一个事务删完检查没问题再提交START TRANSACTION; SELECT * FROM orders WHERE order_no TEST20250101001; DELETE FROM orders WHERE order_no TEST20250101001; -- 确认影响行数正确后 COMMIT; -- 如果发现删错了 -- ROLLBACK;事务是测试人员保护自己的一层保险。平时练习可以多用START TRANSACTION ... ROLLBACK验证更新逻辑的同时不污染数据。TRUNCATE 也可以清空表数据但不能带 WHERE是整体清空。用之前一定要确认表名我见过有人把测试环境的表 TRUNCATE 之后才发现清错库数据直接没了。5.4 锁表、长事务、存储过程造数如果一条 UPDATE 或 DELETE 执行后一直不结束大概率是锁表了。优先看进程列表SHOW PROCESSLIST;看到有会话长期处于Waiting for table metadata lock或者Updating状态说明其他事务持有了锁。确认是僵尸会话后可以在有权限的前提下执行KILL 12345;其中的 ID 从 PROCESSLIST 结果里看。还有一个隐蔽问题测试环境开着长事务不提交。比如你执行了 UPDATE但一直没 COMMIT其他连接再改同一行就会一直等待。轻量测试最好事务内操作完就提交或回滚不要长时间挂着一个事务。大批量造数时可以用存储过程或脚本循环插入。比如写一个 WHILE 循环每 1000 条 COMMIT 一次避免一次性插入几十万条数据导致锁时间过长。实际项目中我更推荐用接口或脚本分批造数存储过程适合一次性构造基础数据不适合频繁变更的测试环境。6. 常用函数、中文乱码和面试高频点6.1 字符串、日期、数值函数和字段运算测试查数时经常需要对结果做格式化。常用函数不需要全背记住几个高频的就能覆盖大部分场景。字符串CONCAT(a, b)拼接字段SUBSTRING(str, start, len)截取字符串LENGTH(str)返回字节长度CHAR_LENGTH(str)返回字符长度中文场景推荐这个REPLACE(str, old, new)替换内容UPPER(str)/LOWER(str)大小写转换TRIM(str)去掉两端空格日期NOW()当前时间CURDATE()当前日期DATE_FORMAT(date, %Y-%m-%d)格式化日期DATEDIFF(end, start)日期差DATE_ADD(date, INTERVAL 1 DAY)日期加减数值ROUND(num, 2)四舍五入CEIL(num)向上取整FLOOR(num)向下取整使用DATE_FORMAT(create_time, %Y-%m-%d)时注意格式符号大小写。%Y是四位年份%y是两位年份写错结果会差很多。6.2 CASE WHEN把状态数字变成可读结果测试人员查数据库时经常看到一堆状态数字很不利于核对。CASE WHEN 可以把数字翻译成业务名称。SELECT order_no, CASE WHEN status 1 THEN 待支付 WHEN status 2 THEN 已支付 WHEN status 3 THEN 已取消 ELSE 其他 END AS status_name FROM orders;它还可以配合 SUM 做条件统计一次查出多个状态的数量SELECT SUM(CASE WHEN status 1 THEN 1 ELSE 0 END) AS wait_pay_cnt, SUM(CASE WHEN status 2 THEN 1 ELSE 0 END) AS paid_cnt FROM orders;这种写法比多次查询更高效结果也更容易和界面数字对账。6.3 中文乱码和字符集MySQL 中文乱码绝大多数是字符集不一致。数据库、表、连接、客户端各有一套编码任何一层不一致都可能出乱码。先看表和库的字符集SHOW CREATE TABLE orders;如果表定义里CHARSETutf8mb4说明表本身没问题。命令行连接时指定字符集mysql -h127.0.0.1 -uroot -p --default-character-setutf8mb4排序和统计中文字段时字符集也会影响结果。测试环境尽量统一使用utf8mb4它能存储四字节的 Emoji 等内容适用范围更广。6.4 面试高频点和学习建议MySQL 相关面试题里测试岗位最常问到的有这些or能不能去重去重有哪些方式。COUNT(*)和COUNT(字段)的区别。GROUP BY和HAVING的用法。LIMIT分页语法。UPDATE不加 WHERE 会发生什么。如何查找并删除重复数据保留一条。INNER JOIN和LEFT JOIN的区别。如何查每个用户最新的一笔订单。最后这个问题需要窗口函数MySQL 8.0 及以上版本可以这样写SELECT order_no, user_id, create_time FROM ( SELECT order_no, user_id, create_time, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY create_time DESC) AS rn FROM orders ) t WHERE rn 1;PARTITION BY user_id表示按用户分组ORDER BY create_time DESC表示组内按时间倒序rn 1取每组第一条。老版本 MySQL 不支持窗口函数就用临时表或子查询绕一下。学习建议其实很简单本地装一个 MySQL准备一套用户表、订单表、商品表把平时测试项目的常见问题转换成 SQL 练习。先练单表查询再练 JOIN 和聚合最后碰 UPDATE、DELETE。不要背命令背命令解决不了“界面数字对不上”这类实际问题。最后留一句我自己的经验测试工程师写 SQL稳定比花哨重要。真正常用的命令不超过几十条但每条都要知道它影响什么、会返回什么、在什么情况下不能随便用。能把造数、校验、定位、清理这四件事做到不出错这份查询能力就已经很值钱了。