MySQL入门实操:从建库到增删查改,一篇吃透核心操作
干了这么多年开发,几乎每套系统都离不开数据库,而MySQL又是国内用得最广的那一个。很多新手朋友一开始接触MySQL,被各种概念绕得头晕,什么存储引擎、索引、事务、隔离级别一堆名词。但你真去写业务代码,天天打交道最多的,其实就是那几个最基础的动作:插入数据、查询数据、修改数据、删除数据,也就是俗称的增删查改(CRUD)。今天我不讲那些花里胡哨的高深理论,就从一个实际项目的角度,把这四个核心操作掰开揉碎了讲清楚,连带着建库建表、连接数据库这些前置步骤也一并梳理一遍。你把这篇文章吃透,日常开发里百分之八十的数据库操作基本就够用了,不管是自己写小项目,还是接手别人的代码,心里都不会再发怵。
这篇文章适合谁看?刚学MySQL没多久、对命令行操作还不太熟的新手,用了Navicat这类图形工具但从来没手写过SQL的同学,以及准备面试、想快速把基础操作过一遍的求职者。我会尽量把每一步操作背后的原因和注意事项都交代清楚,不光是告诉你“怎么做”,更告诉你“为什么要这么做”,这样你踩过的坑才会真正变成自己的经验。
1. 准备环境:装好MySQL,连上数据库
1.1 下载安装与连接方式
要练增删查改,首先得有一个能跑起来的MySQL环境。我建议直接去MySQL官网下载社区版(Community Server),8.0以上版本都行,最新的8.x系列在性能和功能上都很成熟,网上教程也最多。安装的时候有几个地方要留个心眼:一是字符集记得选utf8mb4,不然以后存emoji或者中文生僻字容易乱码;二是端口默认3306一般不用改,改了反而增加记忆负担;三是root密码要记牢,这玩意儿丢了找回挺麻烦。
装好之后,连接MySQL有两种主流方式。第一种是命令行,打开终端(Windows下是cmd或PowerShell,Mac/Linux下直接开终端),输入:
mysql -u root -p回车后会提示你输入密码,输完就进入MySQL的命令行交互界面了,能看到mysql>这样的提示符。第二种是图形化工具,比如Navicat、MySQL Workbench、DBeaver,推荐新手用DBeaver,开源免费,界面清爽。连接时填主机localhost、端口3306、用户名root、密码,测试连接成功就进去了。
我个人的建议是:图形工具用来查看数据、调试SQL很方便,但命令行一定要会,因为你以后上了服务器,绝大多数情况是没有图形界面可用的,只能靠命令行。而且很多线上环境排查问题,命令行的响应速度比图形工具快得多。
1.2 基本数据类型的选择
建表之前,得先搞明白字段用什么类型存。这个选择看似不起眼,实际上对后续的查询性能和数据准确性影响很大。MySQL常用的数据类型大致分三类:数值型、字符串型、日期时间型。
数值型里面,整数用INT(范围约21亿),如果只需要存0到255之间的小数字可以用TINYINT,长整数用BIGINT,带小数点的用DECIMAL(钱相关的金额字段强烈推荐DECIMAL,别用FLOAT或DOUBLE,浮点型会有精度丢失的问题,特别是做金额计算时,0.1加0.2可能给你算成0.30000000000000004)。
字符串型里最常用的是VARCHAR,它存的是可变长字符串,比如用户名、邮箱、手机号,后面要跟上长度,像VARCHAR(50)表示最多存50个字符。如果文本内容特别长,比如文章正文,用TEXT类型。这里有个默认值的小坑,MySQL 8.0里VARCHAR的默认长度是255,超过的话会报错,所以创建表时最好显式指定长度。
日期时间型用DATETIME和TIMESTAMP,DATETIME范围更广,从1000年到9999年,TIMESTAMP从1970年到2038年,日常业务基本够用。TIMESTAMP有个特性是会自动更新,适合记录数据最后修改时间。日期建议用DATE类型,只存年月日。
2. 建库建表:增删查改之前必须先有个“家”
2.1 创建数据库
数据库相当于一个文件夹,表就是里面的文件。在动手增删查改之前,先创建一个数据库。命令行下执行:
CREATE DATABASE IF NOT EXISTS shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;这里有几个细节要理解清楚。IF NOT EXISTS的意思是如果库已经存在就不重复创建,避免报错,这是个好习惯,线上脚本反复执行的时候不会因为已存在而中断。DEFAULT CHARACTER SET utf8mb4指定了库默认字符集,COLLATE utf8mb4_general_ci指定了排序规则,general_ci表示大小写不敏感的通用排序,对英文和数字的排序匹配表现稳定,中文场景下也没问题。
创建完用SHOW DATABASES;查看所有库,然后USE shop;切换到当前库,后续的操作都是在这个库的范围内进行的。
2.2 设计表结构
有了库,接下来建表。举个电商系统的例子,建一张用户表,包含用户ID、用户名、手机号、邮箱、注册时间这几个字段:
CREATE TABLE user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID,自增主键', username VARCHAR(50) NOT NULL COMMENT '用户名', phone VARCHAR(20) DEFAULT NULL COMMENT '手机号', email VARCHAR(100) DEFAULT NULL COMMENT '邮箱', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';这套建表语句里每一行都值得细看。id INT UNSIGNED NOT NULL AUTO_INCREMENT表示无符号整数(只能存正数,范围翻倍)、非空(必填)、自增(每次插入自动加1),这三点组合起来就是最标准的主键设计,保证每条记录都有一条独一无二的ID。DEFAULT CURRENT_TIMESTAMP表示如果插入数据时没填这个字段,数据库自动填当前时间,省得应用层手动拼时间戳。ENGINE=InnoDB指定存储引擎,InnoDB支持事务、行级锁和崩溃恢复,是MySQL默认引擎,也是最稳妥的选择,除非有特殊需求一般不用换。COMMENT给表和字段加说明,方便后期维护,你自己写的表过两个月再看,没有注释基本看不懂当初想干什么。
建好之后用DESC user;查看表结构,用SHOW CREATE TABLE user\G查看建表语句,这两个命令在排查问题的时候特别有用。
2.3 修改表结构
业务是演进的,表结构不可能一成不变。常见的ALTER操作包括:
-- 新增字段 ALTER TABLE user ADD COLUMN age TINYINT UNSIGNED DEFAULT NULL COMMENT '年龄' AFTER phone; -- 修改字段类型 ALTER TABLE user MODIFY COLUMN email VARCHAR(150) COMMENT '邮箱(加长)'; -- 修改字段名 ALTER TABLE user CHANGE COLUMN age user_age TINYINT UNSIGNED DEFAULT NULL COMMENT '用户年龄'; -- 删除字段 ALTER TABLE user DROP COLUMN user_age; -- 给字段添加索引 ALTER TABLE user ADD INDEX idx_phone (phone);这里最想强调的是:线上环境修改表结构一定要谨慎。在大数据量表上执行ALTER会锁表,导致业务短暂不可用。一般建议在低峰期操作,或者使用gh-ost、pt-online-schema-change这类在线变更工具。但这是后话了,小项目直接ALTER问题不大。
3. INSERT插入数据:把数据存进去
3.1 基础插入语法
插入数据是最基础的操作,语法格式为:
INSERT INTO 表名 (字段1, 字段2, ...) VALUES (值1, 值2, ...);比如往user表插入一条数据:
INSERT INTO user (username, phone, email) VALUES ('小明', '13812345678', 'xiaoming@example.com');注意几个细节:id字段是自增的,不用写,数据库会自动分配;created_at字段没有指定值,但表结构里定义了默认值CURRENT_TIMESTAMP,所以数据库会填入当前时间;如果字段有DEFAULT NULL默认值,插入时也可以省略不写。
很多人会好奇:VALUES后面到底能不能写关键字?比如往password字段里存'abc123'这种字符串,完全没问题,只要被单引号包裹就是纯字符串。但如果存的是保留字,比如username字段想存字符串,也直接加引号就行。数字不需要引号,比如age INT字段存18,直接写18。
3.2 批量插入:一次插多条
插入单条记录效率太低,批量插入性能高得多:
INSERT INTO user (username, phone, email) VALUES ('小红', '13912345678', 'xiaohong@example.com'), ('小刚', '13712345678', 'xiaogang@example.com'), ('小丽', '13612345678', 'xiaoli@example.com');多条记录用逗号分隔,一次执行完。批量插入的好处是减少网络往返,SQL解析和执行开销也小很多。我实测过一次插入100条和插100次单条,性能差距可能在十倍以上。日常开发和脚本写入,能批量插入就批量插入。
3.3 插入时常见的坑
第一,插入的数据要符合字段类型和约束。往INT字段插字符串'abc',MySQL会报警告,并插入0,而不是报错,这很容易造成数据不准。用严格模式可以避免这个隐患,在MySQL 8.0里默认开启了严格模式,所以报错的情况会直接提示Data too long或者Incorrect integer value之类的原因。
第二,如果表有UNIQUE约束(唯一索引),插入重复数据会报Duplicate entry错误。有些场景我们希望“没有就插入,有了就更新”,MySQL提供了ON DUPLICATE KEY UPDATE语法:
INSERT INTO user (id, username, phone) VALUES (1, '小明', '13800000000') ON DUPLICATE KEY UPDATE username='小明', phone='13800000000';这个语法适合做数据同步、幂等写入,线上脚本里非常实用。
第三,插入大量数据时,事务要分批提交。如果使用InnoDB引擎,默认是自动提交模式(autocommit=1),即每插入一条就提交一次。大批量插入时,可以手动开启事务,插入完再一起提交,速度更快,而且中途出错可以整体回滚,不会留下半截数据。示例:
START TRANSACTION; INSERT INTO user (username) VALUES ('a'), ('b'), ('c'); -- ... 更多插入 COMMIT;4. SELECT查询数据:把数据捞出来
4.1 基本查询与条件过滤
查询是增删查改里用得最多、也最考验功力的操作。最简单的查询:
SELECT * FROM user;*代表所有字段,开发环境随便用,但线上代码不建议用*,因为如果表字段很多,*会多查出很多用不到的字段,白白增加网络传输和内存压力。更规范的做法是把需要的字段列出来:
SELECT id, username, phone FROM user;条件过滤用WHERE:
SELECT id, username, phone FROM user WHERE id = 1;WHERE后面可以跟各种条件:等于(=)、不等于(!=或<>)、大于(>)、小于(<)、大于等于(>=)、小于等于(<=)、模糊匹配(LIKE)、范围(BETWEEN AND)、枚举(IN)等。举几个实际场景:
-- 查成年用户 SELECT * FROM user WHERE age >= 18; -- 查手机号以138开头的用户 SELECT * FROM user WHERE phone LIKE '138%'; -- 查ID在1到5之间的用户 SELECT * FROM user WHERE id BETWEEN 1 AND 5; -- 查用户名是小明或小红的用户 SELECT * FROM user WHERE username IN ('小明', '小红');LIKE使用%表示任意多个字符,_表示单个任意字符。比如LIKE '张_'匹配的是姓张且名字只有一个字的人,LIKE '张%'匹配所有姓张的人。注意LIKE查询如果用法是'%xxx%'这种前后都带百分号的,走不了索引,大数据量下查询会特别慢,要谨慎使用。
4.2 排序与分页
查询结果默认是无序的,按插入顺序返回但不保证,所以需要显式排序:
SELECT id, username, phone FROM user ORDER BY id DESC;ORDER BY后面跟排序字段,ASC表示升序(默认),DESC表示降序。多字段排序用逗号分隔,比如先按age降序、再按id升序:
SELECT * FROM user ORDER BY age DESC, id ASC;分页查询用LIMIT和OFFSET:
-- 查前十条 SELECT * FROM user LIMIT 10; -- 跳过前20条,查10条(即第21到30条) SELECT * FROM user LIMIT 10 OFFSET 20;或者用简写的LIMIT 20, 10,前面是偏移量,后面是条数。分页在列表页里用得特别多。不过偏移量特别大的时候,比如翻到第100万页,OFFSET 1000000会扫描前面所有行,性能很差。优化的思路是用主键定位,比如WHERE id > 1000000 LIMIT 10这种基于游标的分页方式。
4.3 聚合函数与分组
统计数量、求和、平均值这类需求,用聚合函数:
-- 统计用户总数 SELECT COUNT(*) FROM user; -- 统计年龄总和 SELECT SUM(age) FROM user; -- 统计平均年龄 SELECT AVG(age) FROM user; -- 查最大最小年龄 SELECT MAX(age), MIN(age) FROM user;分组统计用GROUP BY,比如按年龄分组统计人数:
SELECT age, COUNT(*) AS cnt FROM user GROUP BY age;GROUP BY后面还可以加HAVING做分组后的过滤,注意WHERE是分组前过滤,HAVING是分组后过滤。比如:
SELECT age, COUNT(*) AS cnt FROM user GROUP BY age HAVING cnt > 5;这个查询的含义是:按年龄分组,只保留人数大于5的年龄段。理解WHERE和HAVING的区别是面试常考的点:WHERE是在原始数据上过滤,HAVING是分组后再过滤,性能上前者通常更好。
4.4 JOIN关联查询
实际项目里,数据经常分散在多张表里,比如订单表只存用户ID,用户的具体信息在user表里。这时候需要JOIN两张表:
SELECT o.id, o.amount, u.username FROM orders o INNER JOIN user u ON o.user_id = u.id;JOIN分几种:INNER JOIN只返回两边都匹配的记录;LEFT JOIN返回左表所有记录,右表没有匹配的字段用NULL填充;RIGHT JOIN相反。实际开发中INNER JOIN和LEFT JOIN用得最多。JOIN查询是MySQL的进阶重点,涉及索引、驱动表选择等一系列性能问题,这里先不展开,但你只要记住:JOIN时关联字段一定要有索引,不然数据量一大查询就直接卡死。
4.5 子查询
子查询就是把一个SELECT语句嵌套在另一个SELECT语句里,比如查订单金额大于平均值的用户:
SELECT * FROM user WHERE id IN ( SELECT user_id FROM orders WHERE amount > (SELECT AVG(amount) FROM orders) );子查询写起来直观,但性能往往不如JOIN改写。MySQL优化器对子查询的支持已经进步很多,但遇到性能问题时还是优先考虑改写为JOIN。
5. UPDATE更新数据:改数据要小心
5.1 基础更新语法
更新数据用UPDATE,语法格式:
UPDATE 表名 SET 字段1 = 值1, 字段2 = 值2 WHERE 条件;比如把ID为1的用户的手机号改了:
UPDATE user SET phone = '15900000000' WHERE id = 1;可以一次性更新多个字段:
UPDATE user SET phone = '15900000000', email = 'new@example.com' WHERE id = 1;也可以基于字段本身的值做更新:
-- 所有人年龄加1岁 UPDATE user SET age = age + 1;5.2 WHERE是保命符
这条必须单独拿出来强调:UPDATE语句如果忘了WHERE,会把整张表的记录全部更新。我见过不止一个同事在测试环境执行了UPDATE user SET age = 100;这种操作,结果全表年龄都变成了100。测试环境还好,线上环境这就是重大事故了。
所以在执行UPDATE之前,强烈建议你先用SELECT验证一下WHERE条件选中的数据:
SELECT * FROM user WHERE id = 1; -- 先看看这次要改哪些数据确认无误后再执行UPDATE。这是个保命的习惯。
5.3 UPDATE与事务
如果一次要更新多条记录,而且这些更新是有关联的,最好放在事务里:
START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE user_id = 1; UPDATE account SET balance = balance + 100 WHERE user_id = 2; COMMIT;转账这个经典场景,就是两条UPDATE,必须保证同时成功或者同时失败。如果执行第一条成功、第二条失败,钱不知道去哪了,数据就不一致了。事务的ACID特性里,原子性保证的就是这种“全做或全不做”。InnoDB引擎才支持事务,MyISAM引擎是不支持的,这也是为什么我一直说选InnoDB。
5.4 大批量更新的性能问题
如果要更新几万条记录,逐条UPDATE,每条都走一次事务提交,速度极慢。优化的思路有几种。一是用批量更新:
UPDATE user SET age = CASE id WHEN 1 THEN 20 WHEN 2 THEN 21 WHEN 3 THEN 22 END WHERE id IN (1, 2, 3);另一种是先查出要更新的数据的主键列表,一条UPDATE配合IN条件一次性提交。MySQL 8.0还支持VALUES ROW语法,不过日常开发用得少,知道有这两种思路就行。
6. DELETE删除数据:删库跑路前先想清楚
6.1 基础删除语法
删除数据的语法相对简单:
DELETE FROM 表名 WHERE 条件;比如删除ID为1的用户:
DELETE FROM user WHERE id = 1;和UPDATE一样,DELETE没写WHERE就是删全表。删除前也务必备份或确认,特别是生产环境。
6.2 DELETE和TRUNCATE的区别
清空全表数据还有另一个命令:
TRUNCATE TABLE user;TRUNCATE和DELETE FROM有本质区别,面试爱考。DELETE是逐行删除,走事务,删错了还能ROLLBACK回滚,而且不会重置自增ID;TRUNCATE是直接丢弃表再重建,速度极快,但不能回滚,自增ID也重置回1。另外,TRUNCATE属于DDL(数据定义语言),DELETE属于DML(数据操作语言),两者在事务处理、权限控制、触发器等维度都有差异。
日常开发中,清空表数据一般会用DELETE,因为可以回滚;但如果确认不要了,用TRUNCATE更快。这里有一个很实用的技巧:在测试环境或数据可以重建的环境里,用TRUNCATE清表;在可能有审计需求的环境里,永远不要物理删除数据。
6.3 软删除与硬删除
实际业务开发中,做删除操作要好好想想:直接DELETE是“硬删除”,数据彻底没了,后面想查历史记录就查不到了。大厂的规范做法一般是“软删除”,也就是给表加一个is_deleted字段,0表示正常,1表示已删除,删除操作就是一次UPDATE:
ALTER TABLE user ADD COLUMN is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '0未删除 1已删除'; -- 软删除 UPDATE user SET is_deleted = 1 WHERE id = 1; -- 查询时过滤掉已删除的 SELECT * FROM user WHERE is_deleted = 0;软删除的优点是数据不会丢,方便追溯和恢复,但缺点也明显:每次查询都要带is_deleted = 0条件,忘带了就查出脏数据;表内无效数据越积越多,需要定期清理。要不要软删除,取决于业务对数据保留的要求。
6.4 删除大表数据
如果一次要删除的数据量很大,比如几百万条,一条DELETE FROM log WHERE create_time < '2023-01-01'可能会锁很多行,导致业务卡顿。优化思路:
一是分批删除,每次删除一小部分,控制事务大小:
DELETE FROM log WHERE create_time < '2023-01-01' LIMIT 1000;反复执行直到影响行数为0。
二是用pt-archiver这类工具,它内置了分批删除和限速逻辑,对线上影响小。小项目的话,分批删除就够用了。
7. 常见问题与排查技巧实录
7.1 连不上数据库:ERROR 2002
热词里出现过error 2002 (hy000): can't connect to local mysql server through socket这个报错。最常见的原因是MySQL服务没有启动。Linux下用systemctl status mysqld或者service mysql status查看服务状态;Windows下到服务管理器里看MySQL服务的状态。其次可能是Socket文件路径不对,本地连接走的是Socket协议(Unix/Linux默认),如果socket文件被修改过,连接时就找不到。用mysql -h 127.0.0.1 -P 3306 -u root -p通过TCP方式连接可以绕开socket问题。
7.2 忘记MySQL root密码怎么办
这个情况很多人遇到过。思路是跳过权限验证启动MySQL,然后重置密码。步骤大致是:先停掉MySQL服务,以mysqld --skip-grant-tables方式启动(跳过权限表),然后用mysql -u root免密登录,执行ALTER USER 'root'@'localhost' IDENTIFIED BY '新密码';,最后正常重启服务。注意8.0版本的密码字段改成了authentication_string,用ALTER USER语法是标准做法。
7.3 中文乱码怎么办
中文乱码的根因几乎都是字符集不统一。排查方法是依次查看客户端、连接、数据库、表的字符集:
SHOW VARIABLES LIKE 'character_set%'; SHOW CREATE DATABASE shop; SHOW CREATE TABLE user;除了建库建表时指定utf8mb4,客户端连接时也要指定:
mysql -u root -p --default-character-set=utf8mb4或者进入mysql后执行SET NAMES utf8mb4;。在JDBC连接串里加characterEncoding=utf8mb4,这样一套下来基本能保证中文不乱码。
7.4 MySQL 8.4版本与兼容性问题
热词里有个django.db.utils.NotSupportedError: MySQL 8.4 or later is required (found 8.0)。这是Django框架的版本兼容性问题,某些新版本Django开始要求MySQL 8.4+,但你本地装的还是8.0。解决思路是选择匹配的Django版本,或者升级MySQL。碰到版本兼容报错先看清报错原文,搜索时带上框架名和MySQL版本号,通常能很快找到解决方案。这类问题不是你不会写SQL,而是开源生态的版本匹配问题,放宽心态就好。
7.5 常用运维命令速查
命令行下有几个命令使用频率极高,单独列出来:
-- 查看当前连接的数据库 SELECT DATABASE(); -- 查看所有表 SHOW TABLES; -- 查看表结构 DESC user; -- 查看表所有数据,按主键降序排 SELECT * FROM user ORDER BY id DESC LIMIT 100; -- 查看当前时间 SELECT NOW();搞开发的时候,随时用SHOW PROCESSLIST;查看当前有哪些SQL在跑,排查慢查询和死锁时是第一步操作。生产环境出现连接数飙高的时候,这个命令能快速定位到是哪个查询卡住了。
8. 写在实操之外:几个让效率翻倍的好习惯
最后分享几个我在实际项目中一直坚持的习惯,谈不上高深,但确实能帮你少走弯路。
第一,SQL关键字统一大写,表名字段名统一小写。比如SELECT id, username FROM user WHERE id = 1,视觉上层次清楚,关键结构一眼就能看出来。团队协作时风格统一,代码评审也省力。
第二,写任何UPDATE、DELETE语句前,先把WHERE条件复制到一个SELECT语句里跑一遍,看它返回的数据对不对。这套流程多花十秒钟,但能避免的灾难可能是一整天的数据恢复。
第三,在本地开发时,如果可以,尽量把MySQL跑在Docker容器里。好处是环境隔离,一台机器上可以同时跑多个版本的MySQL互不干扰,踩坏了随时删掉重建。热词里也有docker安装mysql,说明这条路已经被很多人在用了:
docker run -d --name mysql8 -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -e MYSQL_DATABASE=shop \ -v /my/own/datadir:/var/lib/mysql \ mysql:8.0第四,建表时宁可多写注释也不要少写。表注释和字段注释都加上,半年后你回来看表结构会发现当时写注释的自己有多贴心。这就像写代码时的变量命名,当时觉得多此一举,后面维护才知道都是救命稻草。
增删查改是MySQL最基础却最重要的能力,几乎所有的业务逻辑最终都会落到这四类操作上,把这些基础打得扎实,后面再去深入索引优化、读写分离、分库分表,才有底气。别急着追求那些炫酷的高阶功能,先把今天这几条SQL写到滚瓜烂熟,遇到任何一张表都能快速做出增删查改,你就已经超过不少人,可以开始打开那张真正的进阶之门了。