最近在帮团队整理一个模拟项目X的数据库初始化脚本时,发现一个很普遍的现象:很多写过一段时间业务代码的同学,对SELECT和INSERT都玩得很溜,但真让他们从头手动创建一个库和一张表,反而会卡住——要么表名大小写不一致导致部署时报错,要么没指定字符集导致中文乱码,要么主键设计不合理导致后面一堆数据修复。MySQL数据库的日常维护本应是最扎实的基本功,但恰恰是这些“不起眼”的操作,决定了后续所有业务逻辑能不能稳定跑起来。这个系列的第一篇,我就把【数据库和表的基本操作】彻底讲透,覆盖连接环境、建库、建表、改表、删表,以及一整套实战中反复用到又容易翻车的细节。本文不涉及索引优化和复杂调优,但每一行SQL都来自真实项目场景,能直接用。
1. 先搞定环境:连接MySQL前的那些事
1.1 为什么我推荐从命令行入门
很多新人习惯装完MySQL就打开图形化客户端,点点鼠标就能建库建表。这不丢人,我也用图形工具看数据,但排查问题和写自动化脚本时,命令行永远是绕不开的那道坎。原因很简单:命令行暴露的是 MySQL 服务端返回的真实行为。你执行一条 SQL,它成功还是失败、报什么错、影响多少行,第一时间就能看到原始反馈。而你用图形工具时,很多细节被封装掉了——连接断了、字符集不对、事务没提交,工具可能只弹一个不痛不痒的提示,甚至直接卡死。另一个关键原因是可复制性。我在处理模拟项目X的数据库时,需要在测试环境和本地各执行一遍同样的初始化流程,用命令行把SQL存成脚本,两边一跑就能对上结果;图形化连接则每次都要手工筛选项,效率低还容易录错。所以我建议所有想认真用 MySQL 的人,先把命令行基础补上。这门手艺不会浪费,后续做备份、做迁移、做定时任务,都离不开它。
1.2 连接、退出与版本检查
连接 MySQL 的命令不复杂,但细节决定成败。最常用的方式是:
mysql -h127.0.0.1 -P3306 -uroot -p如果你就是在本地连默认端口的实例,可以简化成:
mysql -uroot -p这条命令会提示输入密码,输入时不会回显,所以别以为卡住了。参数方面,-h是主机地址,-P(大写)是端口号,-u是用户名,-p(小写)表示需要密码。许多人会踩一个坑:在-p后面直接跟上明文密码,比如-p123456。这样虽然能连上,但进程列表里会暴露密码,安全风险极大,强烈不建议在生产环境这么干。连接成功后,你会看到mysql>提示符,这时候可以执行:
SELECT VERSION();这个函数能马上返回 MySQL 的版本号,确认你连的是哪个版本也很关键,因为不同版本的默认行为有差异。另外用status;或\s可以查看当前连接的所有状态信息,包括字符集、运行时长等,排查问题时很有用。退出连接用exit;或quit;都行,习惯加个分号,告诉服务端这条命令结束了。
1.3 最容易翻车的连接报错
连接阶段最常见的错误大概有四类,我整理成一张表,方便你遇到时快速定位:
| 错误码 | 典型提示 | 原因与解决思路 |
|---|---|---|
| 1045 | Access denied for user | 用户名或密码不对,或该用户不允许从当前主机登录 |
| 1049 | Unknown database | 连接时指定了不存在的数据库名,检查拼写 |
| 2002 | Can't connect to local server | 服务器没启动,或连接了错误的 socket 文件 |
| 2003 | Can't connect to MySQL server | 网络不通或端口未开放,检查防火墙和监听地址 |
我在带新人时常说一句话:别一看报错就慌,先读错误码后面的英文提示。比如ERROR 1045 (28000): Access denied for user 'root'@'localhost',除了密码错误之外,另一个常见原因是当前 MySQL 的 root 账号默认不允许通过 TCP 的 127.0.0.1 直连,只认证本地 socket。解决办法通常是使用-uroot -p不带-h,走 socket 登录后再调整账号的 host 权限。连接问题虽然看起来不像 SQL 问题,但基础不牢,后面建库建表全都没法谈。
2. 数据库本身的增删改查,你能说清吗?
2.1 建库不是随便写个CREATE DATABASE就完事
有些同学建库是这样写的:
CREATE DATABASE mydb;这句话在本地玩没问题,但在真实项目里,我几乎从不这么写。原因有两点:第一,如果没有IF NOT EXISTS,当库已经存在时,这条语句会直接报错,脚本一旦重复执行就会中断。第二,系统默认字符集不一定是你要的,早期 MySQL 默认latin1,直接建库建表后存中文就乱码。所以在任何场景,我都推荐写成下面这样:
CREATE DATABASE IF NOT EXISTS user_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;这里的utf8mb4是真正支持完整 Unicode 的字符集,能存下表情符号、生僻字,基本是现在建库的标准选择。utf8mb4_general_ci是排序规则,ci表示大小写不敏感,适合中英文混合场景。如果你有特定排序需求,比如按拼音排序,可以换成utf8mb4_unicode_ci或其他排序规则。字符集这个东西,库一旦建好再想全局改就很折腾,所以一开始就显式指定,省下的不是几分钟,是几小时。
2.2 查看、切换与删除:三个低频但重要的操作
数据库建立之后,日常最常用的查看命令是:
SHOW DATABASES;它会把当前用户能看到的所有库名列出来。有些库是 MySQL 系统自带的,比如information_schema、mysql、performance_schema、sys,新手容易以为是自己建的,其实不用动它们。切换数据库用USE语句:
USE user_db;执行后,后面写的 SQL 如果不带库名前缀,就会默认作用于这个库。删除数据库的语句是:
DROP DATABASE IF EXISTS user_db;这句话我非常谨慎地使用,因为它不会进回收站。一旦删除,库里的所有表、数据、索引、存储过程全部瞬间消失,而且无法通过普通事务回滚。所以每当我需要删库时,都要求自己先执行一遍SHOW DATABASES确认名字,再执行DROP,并且整个过程必须是在有近期备份的前提下进行。团队里如果有多人共用数据库,删库操作用得越少越好。
2.3 删库跑路前的最后防线:先确认数据库名
删库这件事,过分自信就是事故的源头。我见过一位同学在测试环境执行DROP DATABASE test_db;,结果因为环境变量里连的其实是生产库,差点把线上数据全清掉。从那以后我给自己定了个规矩:凡是删除操作,先查后删、双人复核。查就是执行SHOW DATABASES;,或者更保险一点,用查询语句确认目标库里确实没有不可恢复的重要数据:
SELECT table_name FROM information_schema.tables WHERE table_schema = 'user_db';如果返回结果为空,或者返回的都是可以重建的临时表,才继续删除。另外我还会在删除前先把建库建表脚本备份到本地文件,这样万一删错了,能快速重建结构。mysqldump可以只导出结构不导出数据,命令是这样的:
mysqldump -uroot -p --no-data user_db > user_db_schema.sql这个文件就是你的后悔药。别嫌多这一步,数据库世界里,谨慎永远比机智值钱。
3. 建表前的准备工作:数据类型、引擎和字符集,一个都不能少
3.1 数据类型选错的代价,比想象中大得多
建表的第一步不是敲CREATE TABLE,而是想清楚每一列该用什么类型。类型选小了,数据存不下,后期得改表结构;类型选大了,磁盘空间和索引性能浪费不说,在某些场景下还会带来意想不到的排序、比较问题。这里拿最常用的几组对比给你看:
| 类型 | 存储范围/特点 | 适用场景 |
|---|---|---|
| TINYINT | -128到127 或 0到255 | 状态码、布尔意义上的开关 |
| INT | 约±21亿 | 常规整数,订单号、用户编号常见 |
| BIGINT | 极大范围 | 雪花ID、分布式主键、流水号 |
| DECIMAL | 精确小数 | 金额、汇率等不允许误差的字段 |
| FLOAT/DOUBLE | 近似小数 | 科学计算、坐标,不推荐用于金额 |
| CHAR | 定长字符串 | 长度固定的编码、手机号(如果无前缀) |
| VARCHAR | 变长字符串 | 用户名、邮箱、备注等 |
| DATETIME | 1000年到9999年 | 业务时间,创建时间、更新时间 |
尤其要批评的是用FLOAT存金额。因为浮点数在二进制里是近似存储的,0.1 加 0.2 都未必等于 0.3,账目一旦对不上非常麻烦。金额字段只用DECIMAL(10,2)这类精确类型。还有存日期的几个类型,TIMESTAMP有 2038 年的限制,如果不希望系统在 20 年后宕机,推荐用DATETIME,除非你真的需要时区自动转换功能。
3.2 字符集与排序规则的搭配细节
建库时指定了utf8mb4,建表时也不能松懈。因为 MySQL 在创建表的时候,如果DEFAULT CHARSET没有写,会继承所在数据库的默认字符集。也就是说,如果某个历史库默认是latin1,你在里面建表时又不写字符集,那这张表默认就是latin1,照样乱码。我建议建表的 DDL 里明确写上:
DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin这里有个细节:utf8mb4_bin是二进制排序,区分大小写;utf8mb4_general_ci不区分大小写。如果业务上要求用户名区分大小写,就得用bin。排序规则会影响字符串比较和唯一键判断,比如用户名admin和Admin,在general_ci下会被认为是同一个值,唯一索引会拦截掉第二条数据;在bin下则可以共存。所以你需要在建表前就明确业务逻辑,而不是等出了事故再调。
3.3 存储引擎:InnoDB的常见优势与适用场景
存储引擎这个词在入门阶段容易被忽略,但它决定表的底层行为。现在 8.x 版本的 MySQL 默认引擎就是InnoDB,只要是正常业务表,我都推荐写进建表语句,不要依赖隐式默认值。InnoDB的几个核心优势你迟早会用到:
- 支持事务(
COMMIT/ROLLBACK),数据写一半出错可以整体回滚。 - 支持行级锁,高并发更新不同行时互相影响小。
- 支持外键约束(虽然很多人不用,但多一个能力不是坏事)。
- 崩溃恢复能力好,异常断电后能通过 redo log 恢复未提交的数据。
相比之下,历史遗留的MyISAM引擎虽然读性能在某些场景下不差,但它不支持事务、表锁粒度过大,一旦碰到并发写入现场会非常难受。所以除非是只读的历史归档表,否则我一律推荐InnoDB。建表语句里写上ENGINE=InnoDB,就是给这张表上了正统的保险。
4. 创建表的完整动作:语法结构、列属性和约束怎么写才算稳
4.1 一条创建用户表的完整SQL拆解
光讲语法容易飘,我们直接看一条实战中典型的建表语句。以模拟项目X的用户基础信息表为例:
CREATE TABLE IF NOT EXISTS `user` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` VARCHAR(50) NOT NULL COMMENT '用户名', `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱', `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1启用,0禁用', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用户基础信息表';先说字段部分。id用BIGINT UNSIGNED,防止有符号整数到了 21 亿就耗尽;AUTO_INCREMENT表示自增;NOT NULL配合自增列是标准写法。username用VARCHAR(50),空字符串和NULL是两回事,这里加了NOT NULL就要求必须有值。email和phone允许为空,所以保留DEFAULT NULL。status用TINYINT,这种小状态字段没必要浪费INT。created_at和updated_at利用DEFAULT CURRENT_TIMESTAMP自动填充,更新时自动刷新,非常省心。再往下是索引:PRIMARY KEY (id)是主键;UNIQUE KEY uk_username (username)给用户名索引并加唯一约束;KEY idx_status (status)是普通索引,方便按状态筛选。最后在表选项处显式指定引擎、字符集和表注释,这行注释在后续维护时能救命,别人一眼就明白这张表是干嘛的。
4.2 主键、唯一键和非空约束各自的脾气
约束听起来很理论,但设计表时理解错了会直接产出脏数据。主键约束的核心是:每行数据都必须有一个唯一标识,且不能为NULL。一张表只能有一个主键,但主键可以由多列组成的联合主键。日常中我几乎都用单列自增主键或业务上的唯一ID作为主键,简单干脆。唯一键约束允许一个表有多个,并且允许NULL值。要注意的是,NULL在唯一约束里比较特殊,MySQL 允许同一列有多行NULL,因为它们彼此不相等。所以如果你想让某个可选字段也不重复,比如手机号,业务上需要先判断空值再写入,单纯靠唯一索引防不了全空的情况。非空约束比较好理解,就是这一列不允许NULL。但在实际设计时,很多人分不清“空字符串”和“NULL”。比如用户名如果允许未填写,应用层可能存入'',这满足非空约束,却不符合业务预期。约束只是数据库的最后一道防线,真正的数据质量还要靠业务代码来把关。
4.3 自增列怎么用才能避免主键冲突
自增列是主键最常用的生成方案,但用起来有讲究。首先是类型范围:如果预估数据量可能超过 21 亿,就用BIGINT而不是INT。其次,在插入数据时,除非有特殊需求,否则不要去指定自增列的值。比如你手动插入一条id = 10的数据,MySQL 内部的自增计数器就会跳到 11,如果后面分布式工具也需要生成ID,就很可能出现主键冲突。自增ID其实存在复用陷阱:删除最大ID的数据后,计数器一般不回退,这是为了防止重复使用ID导致缓存和历史记录错乱,属于正常行为,不要强行重置。不过有一种情况需要手动调整:测试环境里反复插入删除,自增已经跑到几万,而你希望从 1 重新开始,可以用:
ALTER TABLE `user` AUTO_INCREMENT = 1;注意,执行这个时当前表里的最大ID必须小于 1 的设定值,否则实际会跳到当前最大ID加1。这个细节我踩过,所以特意提一句。
5. 表结构的日常维护:ALTER TABLE是双刃剑
5.1 加列、改列、删列:三种最常用的修改操作
业务迭代过程中,给表加新字段几乎是家常便饭。最简单的加列语句是:
ALTER TABLE `user` ADD COLUMN `nickname` VARCHAR(30) DEFAULT NULL COMMENT '昵称' AFTER `username`;这里的AFTER username指定新列放在哪个字段后面,不加则默认放到最后。如果你要修改一个字段的类型或默认值,可以用MODIFY:
ALTER TABLE `user` MODIFY COLUMN `email` VARCHAR(150) NOT NULL DEFAULT '' COMMENT '邮箱';这样把email长度从 100 改成 150,同时改为非空。如果要重命名字段,同时改变定义,用CHANGE:
ALTER TABLE `user` CHANGE COLUMN `phone` `mobile` VARCHAR(20) DEFAULT NULL COMMENT '手机号';注意CHANGE要写两遍列名,旧名在前新名在后,很容易写反。删除一列用:
ALTER TABLE `user` DROP COLUMN `nickname`;看起来简单,但删除列属于破坏性操作,该列的所有数据都回不来,而且如果这个列被其他表或视图引用,删除之后应用会立刻报错。所有修改前,我都会先SHOW CREATE TABLE把当前结构存下来。
5.2 重命名表和修改表选项时的注意事项
重命名表用RENAME TABLE:
RENAME TABLE `user` TO `users`;这个操作不只改了表名,也会成为相关视图、存储过程和外部应用里的引用名。如果你的代码里写死了表名,最好同步全局搜索替换。如果别的表有外键引用这张表,MySQL 可能拒绝执行或需要级联修改,这一点要先确认清楚。如果要修改表的字符集或引擎,用ALTER TABLE:
ALTER TABLE `user` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;不过要小心:这条命令只改变默认字符集,已经存在的列的字符集不会自动转换。你需要对每个字符串列再执行一次MODIFY或者使用整表转换语句。更多时候我们直接重建表再导数据更稳妥,但那是高级话题,这里不展开。
5.3 大表修改结构的风险与替代思路
这些都是小表操作,但如果线上订单表已有几千万行,执行一个ALTER TABLE去加字段,很可能长时间锁表,导致业务读写堵塞。即使 MySQL 8.0 支持了一些在线 DDL 操作,但某些修改仍然代价很大。针对大表结构变更,我的经验是:不要在业务高峰期直接跑变更 SQL。真要做,尽量选择低谷期,并且先通过以下步骤降低风险:
- 先在测试环境用相同数据量的表测试执行时间。
- 备份一下原表结构,甚至做一次全量备份。
- 评估是直接
ALTER TABLE还是用新建表、数据迁移、切换表名的方案。
如果你有完善的自动化平台,可以借助在线改表工具,但那些工具的细节需要单独写文章讲。最终原则就一条:结构变更影响范围远超你看到的那些字段,别当儿戏。
6. 表删除的三重境界:DROP、TRUNCATE、DELETE有什么不同
6.1 三者的语法、效果和适用场景对比
删除操作是误操作重灾区,所以我把这三兄弟放在一起讲清楚:
| 操作 | 作用对象 | 是否可恢复 | 触发方式 | 适用场景 |
|---|---|---|---|---|
| DROP TABLE | 整张表,连同结构 | 不可恢复(无备份时) | DDL | 表彻底废弃 |
| TRUNCATE TABLE | 表中全部数据,保留表结构 | 不可恢复(无备份时) | DDL | 清空表数据,重置自增ID |
| DELETE FROM | 表的部分或全部行 | 事务内可回滚 | DML | 删除特定数据 |
举个例子:
-- 删除整个表 DROP TABLE IF EXISTS `temp_table`; -- 清空表但保留表结构 TRUNCATE TABLE `temp_table`; -- 按条件删除部分数据 DELETE FROM `user` WHERE `status` = 0;TRUNCATE和DELETE都叫“删除数据”,但TRUNCATE不逐行触发删除操作,速度很快,同时会重置自增计数器;而DELETE是一行行删除,并且会保留计数器的当前值。如果你希望把自增ID重新从 1 开始,比如清理测试数据,用TRUNCATE就对了。注意:TRUNCATE 不能用于有外键约束引用的表,而且它不经过事务逐行操作,一旦执行就没有回头路了。
6.2 误删之后的紧急处理思路
数据库误删之后最不要做的一件事,就是继续往库里写新数据。很多人一发现DELETE忘记加WHERE,第一反应是先开事务补一条,结果可能把恢复线索覆盖掉。如果误删的是普通DELETE并且在事务中,可以立刻ROLLBACK。但如果在提交之后,或者直接DROP/TRUNCATE,普通事务救不了你。此时唯一可行的思路是先检查是否开启了binlog,如果有完整日志,可以通过日志回放把误删操作前的状态还原。这属于高级恢复技能,也需要在操作前保留日志,没有日志就只能靠备份恢复,备份之后新产生的数据会丢失,所以备份及时性才是根本。我的态度很明确:不要依赖“误删后能恢复”,而要确保“误删前有备份”。
6.3 备份习惯比花哨技巧更值钱
说到备份,最简单的备份方式就是mysqldump。一条全库备份命令大概是:
mysqldump -uroot -p --single-transaction --routines --triggers --events --databases user_db > backup_$(date +%F).sql其中--single-transaction可以在不锁表的情况下做 InnoDB 一致性快照,适合小到中型库。单表备份更简单:
mysqldump -uroot -p --single-transaction user_db user > user_backup.sql对初学者,我强烈建议把备份脚本挂到定时任务里,不要手动想起来才备份。备份文件还要定期做恢复演练,别等真出事才发现备份文件是坏的。备份这件事,做得规整比做得花哨重要,恢复到验证比备份本身重要。
7. 一个真实场景的全流程演练:用户系统基础表
7.1 需求分析与表结构设计
只看零散语法不够,我们来走一遍完整流程。假设模拟项目X需要一张用户表,业务要求是:
- 用户必须有一个唯一的用户名,不能为空。
- 邮箱和手机号允许为空,但如果有值,业务上不能重复(这里先用应用层保证)。
- 需要一个启用/禁用状态,默认启用。
- 记录创建时间和最后更新时间,由数据库自动维护。
- 需要用自增主键。
根据这些需求,我画出的字段清单如下:
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT UNSIGNED | 自增主键 | 唯一标识 |
| username | VARCHAR(50) | NOT NULL + UNIQUE | 登录名 |
| VARCHAR(100) | NULL | 联系邮箱 | |
| phone | VARCHAR(20) | NULL | 手机号 |
| status | TINYINT | NOT NULL DEFAULT 1 | 1启用 0禁用 |
| created_at | DATETIME | NOT NULL DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | DATETIME | NOT NULL 默认自动更新 | 最后更新时间 |
设计阶段多花十分钟,后面能省一整天返工。
7.2 从建库到建表的完整落地SQL
先建库:
CREATE DATABASE IF NOT EXISTS user_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_bin; USE user_db;然后建表:
CREATE TABLE IF NOT EXISTS `user` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` VARCHAR(50) NOT NULL COMMENT '用户名', `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱', `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1启用,0禁用', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用户基础信息表';这段 SQL 可以直接存成.sql文件,在需要时一条命令导入:
mysql -uroot -p < init_user_db.sql脚本化建库建表是团队协作最舒服的方式,每个人都用同一份初始化文件,不会出现“我本地和线上表结构不一样”的闹剧。
7.3 验证表结构和常见异常处理
建完表之后的验证步骤不能省。我习惯先执行:
USE user_db; SHOW TABLES; DESC `user`; SHOW CREATE TABLE `user`;SHOW TABLES确认表存在,DESC查看列结构,SHOW CREATE TABLE看的是最终执行的完整 DDL。如果你在用DESC时发现中文注释没有显示出来,通常是客户端连接字符集问题,可以在连接后执行SET NAMES utf8mb4;再试。如果你在导入脚本时遇到ERROR 1064,多半是 SQL 语法错误,比如使用了保留字作为表名或列名。像order、group、desc这些字都是保留字,如果非要当名字用,必须用反引号括起来,例如:
CREATE TABLE `order` ( ... );我建议所有表名列名统一加反引号,一劳永逸。
实际维护中,我习惯把每一次建库建表、修改结构的脚本都提交到代码仓库里,文件名带上日期和版本号。这样数据库结构就成了“可以审阅的代码”,谁改过、为什么改,都清清楚楚。哪怕你现在只管理一个本地测试库,也值得养成这个习惯——MySQL 基本功真正值钱的不是背下几条 SQL,而是在日复一日的表结构变更中沉淀出对数据敏感度和操作敬畏心。希望这篇基础篇能帮你把这些动作做得更扎实,下一篇我再接着聊索引和数据导入导出时那些更磨人的细节。