
1. 项目概述这套入门系列到底解决什么问题想学 MySQL 的人很多但大量朋友卡在第一步。我见过太多例子下载完 MySQL 不知道怎么配置配置好了一执行命令报ERROR 1045 (28000): Access denied for user好不容易进去了面对命令行又不知道从哪下手。我写这套【MySQL 入门】系列目标就是把这些坑提前帮你踩平用最简单直接的方式把基础语法和高频查询讲透。这个系列的第一篇也就是你现在看到的这一章聚焦两件事基础语法和高频查询。这两个东西是数据库使用的骨架后面再学索引优化、事务隔离、读写分离、存储引擎调优全都建立在这个基础之上。你如果刚接触数据库或者用过但一直停留在只会 SELECT * FROM 表名的程度那这篇内容就是给你准备的。我日常工作中接触到的 SQL 写得比较吃力的同学通常不是不会写而是不知道常用子句的搭配逻辑。WHERE、GROUP BY、HAVING、ORDER BY、LIMIT这五个子句的执行顺序是什么写的时候应该按什么顺序组织这些一旦搞清楚很多怎么查不出来的问题就迎刃而解了。这篇文章会把这些底层逻辑一并讲透不是单纯背语法而是让你真正理解为什么这样写。2. 环境准备从下载到连上 MySQL 的完整路径2.1 版本选择与安装方式MySQL 现在主流的稳定版本是 8.0 系列5.7 已经停止官方更新新项目不建议再用了。安装方式主要有三种Windows 下用安装包、Linux 下用 RPM 或apt包管理器、以及 Docker 容器化部署。不同场景我推荐的方案不一样本地学习开发Windows 用户直接下载 MySQL Installer选 Developer Default 即可。Linux 用户用发行版自带的包管理器安装mysql-server包CentOS 上注意用 MySQL 官方 YUM 源否则装的可能是 MariaDB。需要快速起多套环境或者在 Mac 上使用推荐 Docker。一行命令就能跑起来不用污染宿主机环境。这个方式我在后面的章节会专门讲常见坑。生产环境建议用官方 RPM 包或二进制包配合专门的配置管理工具部署不建议用 Docker 存储生产数据这个后面会展开说。2.2 用 Docker 安装 MySQL 8.0Docker 部署 MySQL 是我现在最常用的方式特别是写测试代码或者临时起一套环境时效率极高。基本步骤如下docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEtestdb \ -v /my/own/datadir:/var/lib/mysql \ mysql:8.0几个参数说明一下。-p 3306:3306把容器内的 3306 端口映射到宿主机这样你本地的 Navicat、命令行都能连。-e MYSQL_ROOT_PASSWORD设置 root 密码。-v是数据目录挂载这个特别关键如果不挂载容器删除后数据就全没了。我在实际使用中踩过一个比较典型的坑启动后宿主机防火墙没放行 3306 端口导致从别的机器连不上本机却一切正常。排查半天才发现是防火墙的问题。所以在云服务器上装完 MySQL 之后第一件事不是急着连而是先确认安全组和防火墙规则。还有一个高频报错是docker exec -it mysql8 mysql -uroot -p进去以后发现用 Navicat 连接报Host xxx is not allowed to connect to this MySQL server。这是因为 MySQL 默认只允许 localhost 登录。解决办法是执行CREATE USER root% IDENTIFIED BY password; GRANT ALL PRIVILEGES ON *.* TO root% WITH GRANT OPTION; FLUSH PRIVILEGES;2.3 连接验证与 Navicat 配置装完之后最简单的验证方式是打开命令行执行mysql -uroot -p输入密码。如果出现mysql提示符说明服务正常。想要图形界面操作业界最常用的工具就是 Navicat for MySQL 和 DBeaver两者各有优势Navicat 功能全面界面顺手但需要授权DBeaver 开源免费跨平台推荐初学者使用。配置连接时注意几个点连接名随意填主机填localhost或127.0.0.1端口默认 3306用户填 root密码填你设置的密码。这里经常有人遇到MySQL SSL connection error的报错一般是 MySQL 8.0 默认开启了 SSL 认证而客户端配置不对。解决方案是在连接的高级选项中把 SSL 模式设为 Disable或者用mysql -uroot -p --skip-ssl命令行方式连接。另外我想强调一个习惯不要在生产环境用 root 用户跑业务查询。开发学习阶段无所谓但真实项目里一定要创建独立账号按最小权限原则授权。这也是我见过的很多项目后续出线上事故的根源之一。3. 库与表的基础语法建好你的第一个数据仓库3.1 数据库的增删改查数据库层面的操作语法相对简单但有几个细节值得留意。创建库CREATE DATABASE IF NOT EXISTS mydb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;这里我强烈建议加IF NOT EXISTS防止重复执行报错。字符集用utf8mb4而不是老旧的utf8。原因很实际utf8在 MySQL 中实际只能存 3 字节的字符像 emoji 表情这类 4 字节字符会存不进去报Incorrect string value。utf8mb4是完整的 UTF-8 实现兼容所有字符。这个坑我在接手老项目时遇到太多次了表结构全是utf8一旦业务要存用户昵称带表情就直接崩。查看已有数据库用SHOW DATABASES;切换数据库用USE mydb;删除数据库用DROP DATABASE mydb;。删除操作要谨慎没有确认步骤执行即生效。还有一点关于CREATE DATABASE的字符集设置如果你的项目早期没指定字符集后续发现中文乱码可以通过ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;修改。但这只对新表生效老表还需要单独改。所以最省事的做法就是建库时就写好字符集。3.2 建表语法与常用数据类型建表是 MySQL 入门最核心的语法之一因为你后面所有的查询、优化都围绕表结构展开。我以一张用户表为例CREATE TABLE user ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, email VARCHAR(100) NOT NULL, age TINYINT UNSIGNED DEFAULT 0, status TINYINT DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_email (email) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里几个点展开说一下。BIGINT UNSIGNED做主键是生产环境的常用选择因为INT最大就到 21 亿左右对很多业务来说不够用而BIGINT基本可以支撑较长时间的增长。AUTO_INCREMENT自增主键配合主键索引写入性能和查询性能都不错。VARCHAR(50)存用户名足够TINYINT存年龄和状态位足够没必要用 INT 浪费空间。created_at和updated_at的默认值设置是很多新手容易忽略的。用DEFAULT CURRENT_TIMESTAMP让数据库自动写入创建时间ON UPDATE CURRENT_TIMESTAMP让更新时自动刷新更新时间。这样应用层就不用手动维护这两个字段非常方便。由于项目开发阶段表的字段经常变动所以建表时预留扩展字段不是好主意。字段不够用的时候直接ALTER TABLE加列即可MySQL 8.0 的在线 DDL 大部分操作不会锁表不用担心影响业务。3.3 表结构的修改与索引创建实际开发中表结构一定会改来改去我给你列几个最常用的修改语句ALTER TABLE user ADD COLUMN nickname VARCHAR(30) AFTER username; ALTER TABLE user MODIFY COLUMN age TINYINT UNSIGNED NOT NULL DEFAULT 18; ALTER TABLE user CHANGE COLUMN status user_status TINYINT DEFAULT 1; ALTER TABLE user DROP COLUMN nickname; ALTER TABLE user RENAME TO app_user;注意MODIFY和CHANGE的区别前者只能改字段类型和约束后者可以同时改字段名和类型所以CHANGE的语法里要把新字段名写两遍。另外ADD COLUMN的默认位置是表末尾想要指定位置可以用AFTER 字段名。索引是面试和实际优化绕不开的话题。创建索引的基本语法是CREATE INDEX idx_username ON user(username); CREATE UNIQUE INDEX idx_email ON user(email); ALTER TABLE user ADD INDEX idx_age_status (age, status);这里我想强调一个新手最容易犯的错索引不是越多越好。每建一个索引写入时就要多维护一份数据结构会让 INSERT、UPDATE 变慢。正确的做法是先根据业务查询中WHERE条件最常用的列建立索引然后通过EXPLAIN查看执行计划来验证是否走索引。联合索引要遵循最左前缀原则比如idx_age_status (age, status)你查询WHERE status 1时这个索引帮不上忙但WHERE age 25 AND status 1就能用上。4. 数据操作实战INSERT、UPDATE、DELETE 的正确姿势4.1 INSERT 的几种写法插入数据是最基本的操作但不同场景有不同写法。最简单的单条插入INSERT INTO user (username, email, age) VALUES (zhangsan, zsexample.com, 25);批量插入多条用一条语句搞定INSERT INTO user (username, email, age) VALUES (lisi, lsexample.com, 28), (wangwu, wwexample.com, 22);批量插入比逐条循环插入性能高一个数量级因为省去了大量网络往返和 SQL 解析的开销。我曾经优化过一个批量导入的脚本从逐条 INSERT 改成批量 INSERT 后导入 10 万条数据的时间从 20 多分钟降到不到 1 分钟。所以实际工作中一定要养成批量操作的习惯。还有一种高频用法是不存在则插入存在则更新也就是INSERT ... ON DUPLICATE KEY UPDATEINSERT INTO user (username, email, age) VALUES (zhangsan, zsexample.com, 26) ON DUPLICATE KEY UPDATE age VALUES(age);这个语法利用唯一索引冲突来触发更新。需要注意的是VALUES()函数在新版本中已标记废弃8.0 以上推荐使用别名语法AS new ON DUPLICATE KEY UPDATE age new.age当然旧写法现在仍然可用只是要注意后续迁移。4.2 UPDATE 与 DELETE 的保命技巧基于我这些年的经验UPDATE 和 DELETE 最大的坑就一句话忘记加 WHERE 条件。一个UPDATE user SET age 18;执行完全表的人都 18 岁了没有后悔药。所以我的建议是执行 UPDATE/DELETE 之前先写一条相同条件的 SELECT 确认影响范围SELECT id, age FROM user WHERE age 18; UPDATE user SET age 20 WHERE age 18;这样能直观地看到 WHERE 条件筛出了哪些记录避免误操作。UPDATE 支持多表更新语法是UPDATE user u JOIN order o ON u.id o.user_id SET u.status 0 WHERE o.order_time 2024-01-01;在多表场景下很实用但不建议新手一上来就用搞不好就是一顿乱改。DELETE 的语法和 UPDATE 类似。如果想清空整张表用TRUNCATE TABLE user;会比DELETE FROM user快很多因为 TRUNCATE 是直接删表再重建不记录每一行的删除日志。但 TRUNCATE 不能加 WHERE且会重置自增 ID使用时要想清楚。4.3 事务让数据操作有后路与 UPDATE/DELETE 强相关的一个概念是事务。MySQL 中事务四大特性ACID我就不详细展开了重点讲操作层面的用法。默认情况下每条 SQL 自动提交如果你想多条 SQL 作为一个整体执行要么全部成功要么全部回滚需要显式开启事务START TRANSACTION; UPDATE account SET balance balance - 500 WHERE user_id 1; UPDATE account SET balance balance 500 WHERE user_id 2; COMMIT;如果中间某条语句出错执行ROLLBACK;即可回滚到事务开始前的状态。我在生产环境处理数据订正时文档里出现的 SQL 一律包在事务里确认执行结果没问题再 COMMIT。这是一个很实用的习惯能避免很多灾难性事故。关于事务隔离级别面试高频问题之一。MySQL 默认级别是REPEATABLE READ也就是可重复读。这个级别下同一事务内多次查询结果一致解决了不可重复读问题。但要注意它仍然无法完全避免幻读InnoDB 通过Next-Lock Lock临键锁在部分场景下解决了幻读这也是为什么很多人说 MySQL 的可重复读实际能防幻读。这块等讲到锁的时候再深入展开。5. 高频查询全解析从单表筛选到多表聚合5.1 WHERE过滤条件的核心写法查询语句里最常用的就是 WHERE。它支持比较运算符、、、、、逻辑运算符AND、OR、NOT还有一系列高频关键字。IN用于匹配一个集合SELECT * FROM user WHERE age IN (18, 20, 22);BETWEEN用于区间匹配注意它是闭区间SELECT * FROM user WHERE age BETWEEN 18 AND 30;LIKE用于模糊匹配%代表任意多个字符_代表单个字符SELECT * FROM user WHERE username LIKE zhang%; SELECT * FROM user WHERE username LIKE _hangsan;这里有个常见误区以%开头的 LIKE 条件比如LIKE %zhang%在绝大多数情况下用不上索引数据量大了会全表扫描。所以生产环境对这种写法要格外警惕。IS NULL用于判断空值注意不能写成 NULLSELECT * FROM user WHERE email IS NULL;5.2 排序与分页ORDER BY 和 LIMIT排序是高频需求。基本语法是ORDER BY 字段 ASC/DESC多字段排序用逗号分隔从左到右依次生效SELECT id, username, age, created_at FROM user WHERE status 1 ORDER BY age DESC, created_at ASC LIMIT 20;这个语句的执行逻辑是先按age降序如果年龄相同再按created_at升序。LIMIT 20表示只返回前 20 条。关于LIMIT分页这里我多说一句高级技巧。数据量大了以后LIMIT 100000, 20这种写法性能极差因为 MySQL 要先把前 10 万条记录查出来再丢弃。优化的方式是使用延迟关联或基于游标的分页-- 传统方式越往后越慢 SELECT * FROM user ORDER BY id LIMIT 100000, 20; -- 优化方式先只查主键再关联回原表 SELECT u.* FROM user u INNER JOIN ( SELECT id FROM user ORDER BY id LIMIT 100000, 20 ) t ON u.id t.id;5.3 聚合统计GROUP BY 与聚合函数说到GROUP BY绕不开聚合函数。COUNT、SUM、AVG、MAX、MIN是五个最基本的聚合函数。举一些典型例子-- 统计每个年龄的人数 SELECT age, COUNT(*) AS cnt FROM user GROUP BY age; -- 统计每个用户下单总金额 SELECT user_id, SUM(amount) AS total_amount FROM order GROUP BY user_id; -- 统计活跃用户的最大年龄和平均年龄 SELECT MAX(age) AS max_age, AVG(age) AS avg_age FROM user WHERE status 1;COUNT(*)和COUNT(字段)的区别值得一提。COUNT(*)统计行数COUNT(email)统计该字段不为 NULL 的行数。如果你的业务逻辑是统计有多少用户填了邮箱那要选后者只是统计总行数就选前者。GROUP BY 之后想过滤分组条件不能再用 WHERE得用HAVING。这是我见过新手最混淆的点之一。重点是二者的执行顺序WHERE 过滤的是原始行GROUP BY 分组之后再执行 HAVING 过滤分组。举个例子SELECT age, COUNT(*) AS cnt FROM user WHERE status 1 GROUP BY age HAVING cnt 10;在这个语句里WHERE status 1是先筛掉无效用户然后按年龄分组最后只保留人数大于 10 的年龄分组。5.4 连表查询JOIN 的正确打开方式实际业务中数据不会都放在一张表里。用户表存用户信息、订单表存订单信息要查某个用户的订单列表就必须 JOIN。MySQL 中最常用的是INNER JOIN内连接和LEFT JOIN左连接。内连接只返回两张表中匹配成功的行SELECT u.username, o.order_no, o.amount FROM user u INNER JOIN order o ON u.id o.user_id;左连接返回左表全部行右表没有匹配就补 NULLSELECT u.username, o.order_no, o.amount FROM user u LEFT JOIN order o ON u.id o.user_id;两者的区别可以用大白话理解内连接是两个圈子重叠的部分左连接是左边整个圈子都要右边有就带上没有就空着。实际业务中 LEFT JOIN 用得更多因为通常我们都想保留主体的全部记录。关于 JOIN 条件我建议用表别名如u、o来区分不同表的同名字段否则多个表都有id的时候会报字段不明确错误。另外多表 JOIN 时连接条件一般写在JOIN ... ON后不是写在 WHERE 里。虽然写 WHERE 里结果可能一样但可读性差很多后续维护也痛苦。5.5 子查询与 CASE WHEN子查询就是嵌套在 SELECT 里的查询常用于先查一个条件再基于条件查主表的场景。例如查有订单的用户SELECT id, username FROM user WHERE id IN (SELECT DISTINCT user_id FROM order);在 MySQL 8.0 中性能表现上 JOIN 往往优于很多子查询所以生产环境中能用 JOIN 的优先考虑 JOIN。但子查询在表达存在性或派生表场景时更直观比如上面这种写法就很清晰。CASE WHEN是 SQL 里做条件判断的关键语法可以理解成编程语言里的 if-else。举一个典型的二次分类统计例子SELECT CASE WHEN age 20 THEN 未成年 WHEN age 30 THEN 青年 WHEN age 50 THEN 中年 ELSE 老年 END AS age_group, COUNT(*) AS cnt FROM user GROUP BY age_group;注意这里 GROUP BY 可以直接引用 SELECT 子句中定义的别名age_groupMySQL 支持这个语法但在标准 SQL 中有限制。如果你在其他数据库上写可能需要用子查询包一层。更重要的是CASE WHEN中如果不写END或者漏了别名SQL 会报错这是新手最容易犯的语法错误之一。6. 进阶概念速览为后续学习打地基6.1 存储过程批量逻辑的封装日常工作中不常写存储过程但面试常考而且在一些在线 DDL 场景、周期性脚本任务中它会派上用场。一个基础示例DELIMITER $$ CREATE PROCEDURE proc_update_user_age(IN uid INT, IN new_age INT) BEGIN UPDATE user SET age new_age WHERE id uid; END$$ DELIMITER ;调用方式是CALL proc_update_user_age(1, 30);。这里注意DELIMITER的使用因为默认的分隔符是分号;如果不临时改掉MySQL 会在存储过程内部遇到分号时就当作语句结束导致报错。存储过程的优缺点也值得说清楚。优点是逻辑封装在数据库端减少应用层和服务器的网络往返。缺点是 debug 困难、版本管理难、数据库端 CPU 压力增加。所以我个人建议只有简单、稳定的批处理逻辑才放存储过程复杂业务逻辑别放放在应用层反而更好维护。6.2 锁MySQL 并发控制的核心机制MySQL 里锁是一个重要的理论块。按粒度分有表级锁和行级锁按模式分有共享锁读锁和排他锁写锁。InnoDB 支持行级锁也是它比 MyISAM 更适合高并发写入场景的核心原因。LOCK TABLES user WRITE; -- 表级锁不推荐 SELECT ... FOR UPDATE; -- 行级写锁 SELECT ... LOCK IN SHARE MODE; -- 行级读锁行级锁中 InnoDB 还分为 Record Lock记录锁、Gap Lock间隙锁、Next-Key Lock临键锁。这块讲深了可以单独写一篇文章。初学者先知道两个结论就好一是写操作默认加排他锁读操作默认不加锁二是SELECT ... FOR UPDATE可以手动加锁通常用在事务里防止并发修改同一行数据。6.3 MySQL 8.0 的窗口函数MySQL 8.0 引入窗口函数之后很多原本要用临时表 自连接才能实现的排名、分组 TopN 问题现在一条 SQL 就能解决。比如按用户分组查最近一条订单SELECT user_id, order_no, amount, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn FROM order;配合子查询或者直接在外层过滤 ROW_NUMBER 等于 1 的记录就能取出每个用户的最新订单。这类写法在数据分析和报表场景中极其高频我建议入门阶段先知道有ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)这个语法后面实践多了自然会熟练。7. 常见问题与小技巧从故障中找到成长点7.1 连接故障排查顺序连接不上 MySQL 时我建议按如下三步排查第一确认 MySQL 服务是否在运行。Windows 上可以打开服务管理器看 MySQL 服务状态Linux 用systemctl status mysqld或service mysql status。很多时候不是配置问题而是服务压根没起来。第二检查端口是否可通。用telnet 127.0.0.1 3306或nc -vz localhost 3306测试。连不通就检查防火墙和安全组规则。第三检查账号权限。如果报Access denied要么密码错要么账号不允许从当前 IP 连接要么密码加密方式是老的。这个问题在从 MySQL 5.7 升 8.0 时很常见老客户端的密码插件不支持新的caching_sha2_password认证解决办法是把账号改成mysql_native_password认证或者升级客户端。7.2 忘记 root 密码之后真的忘了 root 密码也不要慌。Linux 上常用方式是以--skip-grant-tables跳过权限认证启动服务然后进入数据库重置密码。步骤大致是停掉 MySQL 服务用mysqld --skip-grant-tables启动然后执行ALTER USER rootlocalhost IDENTIFIED BY 新密码;最后重启服务恢复正常模式。一台 Windows 机器上还可以直接备份数据目录然后把初始化参数放在 my.ini 里临时启用 skip-grant-tables操作逻辑相同。注意这件事在网上的资料里有不少做法是针对老版本的而 MySQL 8.0 下skip-grant-tables登录后有权限不能直接修改用户表需要先FLUSH PRIVILEGES;再ALTER USER这个细节很多人不知道。7.3 一条关键优化习惯EXPLAIN按我个人的经验学习 SQL 的最好老师之一就是EXPLAIN。在任意 SELECT 前加EXPLAIN关键字MySQL 会输出这条 SQL 的执行计划告诉你是否用索引、扫描了多少行、用了什么连接方式。比如EXPLAIN SELECT * FROM user WHERE age 30;如果输出中type是ALL、rows特别大说明这条查询在做全表扫描。这时候就要考虑在age上建索引了。这个工具建议从入门阶段就开始用养成习惯后你写 SQL 的思路和那些只照着网上抄语法的人会有本质区别。8. 实操心得与下一步建议内容讲到这里给刚开始接触 MySQL 的朋友几个掏心窝的建议。第一不要只看不练。SQL 语法是操作性极强的技能光看文章记不住。我建议你把这篇文章里所有的 SQL 在本地环境里完整敲一遍然后刻意改成不同的条件、排序和分组组合观察结果变化。哪怕报错了也没关系报错本身就是最好的学习机会。第二给自己建一个每天都在用的高频查询清单。把上面讲的 WHERE 过滤、ORDER BY 排序、GROUP BY 聚合、JOIN 连表这些语法整理成一个速查文件遇到查数据需求时先翻清单再写语句。用多了之后这些语法就会变成肌肉记忆。第三学会看官方文档。网上很多 MySQL 博客的内容基于 5.6、5.7 的老语法和 8.0 有些出入。遇到不确定的语法直接到 MySQL 官方文档里查对应的章节准确率最高。这个习惯越早养成你后面避开信息差坑的机会就越大。接下来的系列我会陆续展开索引原理、事务隔离级别实战、慢查询优化、数据备份恢复等进阶话题。这套入门系列更像是一张地图帮你先建立起 MySQL 的全局认知。真正变成自己的技术还得靠你手下的每一个练习和线上实战中的每一次踩坑。