1. 先想清楚:Navicat点鼠标建表那么方便,为什么还要用命令
1.1 图形界面建表确实省事,但真正干活时你会后悔
很多人第一次接触MySQL,从安装到连接一路跟着教程走,最后打开Navicat,习惯性地右键“新建表”,窗口里填字段名、选类型、勾主键,点保存,一张表就出来了。整个过程不到两分钟,看起来比敲SQL爽多了。
但我得说句泼冷水的话:如果你打算长期跟数据库打交道,无论走开发、运维还是数据分析方向,用命令建表这个习惯越早养成越好。原因很简单,图形界面点出来的表,只存在于你当前这台机器的当前这个数据库里。等你要换电脑、要部署到测试环境、要把表结构发给同事评审、要在生产环境执行变更脚本时,你会发现你手里什么“可复制、可追溯、可对比”的东西都没有。最尴尬的场景是:开发环境你点了十几次鼠标建了七八张表,上线前DBA找你要建表脚本,你只能一张一张右键“转储SQL文件”去补,而且补出来的脚本因为工具版本不同、字符集设置不同,还经常跟你实际表结构对不上。
我在实际项目里见过不止一次这样的问题:两个同事各自在本地用Navicat建了“同样”的表,结果一个表用了utf8mb4,另一个还留着默认的utf8,联调的时候中文数据写到对方库里全是问号;还有一张表字段名用的是驼峰,另一张表用下划线,一合并就崩。这些坑,归根结底都是因为建表过程没有标准化,完全依赖手工操作。
1.2 命令建表的本质,是把表结构当成代码资产来管理
用SQL命令建表,表面上只是换了一种操作方式,本质上思维模式完全不同。图形界面建表时,你面对的是一个“表单”,重点是填得对不对;用命令建表时,你面对的是一段可以反复执行、修改、版本管理、审查的脚本,重点是这段脚本能不能在任何环境里跑出同样的结果。
这就是为什么很多公司即使全员都在用Navicat做日常查询,正式的表结构变更也要求必须提供SQL脚本,而不是让人去线上库“点几下”。因为脚本可以被git管理,可以被review,可以用工具做差异对比;而鼠标点击的操作记录什么都不会留下。说得直白一点,点鼠标建的教训是“这次建对了”,用命令建的经验是“永远知道怎么重建”。数据库这种需要长生命周期维护的东西,稳定的可复现性比一次性成功重要得多。
1.3 这篇文章要完成的目标:用命令从零建库建表
本文定位是基础篇,用的是最常用的组合:MySQL 8.0 + Navicat图形客户端。操作核心是在Navicat的查询编辑器里执行SQL命令,从创建数据库开始,到建出一张基础的学生信息表为止。
学完你应该掌握三件事:怎么用一条命令创建数据库并选对字符集;怎么设计一张基础表的字段和约束;怎么在Navicat里执行命令并验证建表结果。我建议你跟着敲一遍,别只盯着看,因为建表命令这类东西,眼睛看懂了和手敲出来的感觉完全不一样,很多报错只有自己写在执行时才会遇到。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:MySQL装好不算完,Navicat连接里的编码项要先调对
2.1 版本选择与安装后的第一件小事
这里默认你已经装好了MySQL,也装好了Navicat。版本上,我建议用MySQL 8.0而不是5.7,因为8.0的默认字符集就是utf8mb4,对中文和emoji的支持更省心,而且很多新特性是5.7没有的。Navicat的话,16或17版本都行,基础功能没区别。
装好之后,第一件事不是急着连,而是确认MySQL服务真的在跑。Windows下按Win+R,输入services.msc回车,在服务列表里找MySQL相关的服务,状态是“正在运行”才行;Linux下可以用 systemctl status mysql 查看。很多人后面报“连接不上数据库”,排查半天,结果只是服务没启动。这个步骤虽然基础,却是我见过频率最高的翻车点之一。
2.2 新建连接时,字符集编码这一步千万别跳过
打开Navicat,左上角“连接”选择“MySQL”,弹出的窗口里要填连接名、主机、端口、用户名、密码。主机填 localhost 或 127.0.0.1,端口默认 3306,用户名 root,密码是你安装MySQL时设置的。到这里很多人点“确定”就完了,但我建议你顺手做一件事:点左下角或者上方的“高级”选项卡,找到“使用MySQL字符集”相关的选项,手动选择 utf8mb4。
为什么要提前设置这个?因为Navicat客户端和MySQL服务端之间的字符集如果不一致,执行脚本时中文注释会变成乱码,查询出来的中文数据也可能显示异常。这个问题表面上看是“表建错了”,实际上客户端和服务端之间没商量好编码格式。提前在连接属性里固定成utf8mb4,可以挡掉一大部分乱码问题,省得后续排查到怀疑人生。
2.3 打开命令编辑器的正确姿势
Navicat里执行SQL,不是在左侧随便点,而是要先打开查询编辑器。方式有两种:在连接名上右键选择“新建查询”,或者直接按快捷键 Ctrl+Q。打开的窗口就是一个空白的SQL编辑器,后续所有建库建表命令都写在这里。
这里要注意一个很多人都会踩的点:左侧对象树里“数据库”那一层,双击某个库名会让它变成粗体,表示当前默认数据库切到这个库。但在查询编辑器里,你的“当前数据库”是由编辑器左上角的下拉框决定的,跟左侧双击不一定同步。所以如果你在执行建表命令时发现报“No database selected”,先看一眼编辑器左上角下拉框选中的是哪个库。这个细节我放到后面的“坑”里还会再讲,因为太容易踩了。
3. 建库:一条CREATE DATABASE命令,字符集和排序规则才是重头戏
3.1 先给数据库起一个不会让你后悔的名字
建库之前,名字要想好。命名这件事,虽然不涉及技术难度,但直接影响后续的使用体验。我的建议是:全小写英文字母 + 下划线分隔,语义要清晰,别用拼音缩写,别用驼峰,更不要用中文。
比如做一个学校管理系统,库名就叫 school_db,一看就知道是学校项目的库。你将来会有很多库,比如订单系统的 order_db、用户中心的 user_db,如果都用什么 db1、test2 这种名字,一个月后你自己打开Navicat都要想半天。而且库名如果要用在连接串、配置文件里,大小写混写会给你带来很多不必要的麻烦,Linux系统下MySQL的表名大小写敏感,Windows下不敏感,换个环境就出诡异问题。从一开始就统一用小写加下划线,是最省心的方案。
3.2 完整建库语句逐段拆解
在查询编辑器里输入下面这条命令:
sql复制CREATE DATABASE IF NOT EXISTS school_db
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
然后按 Ctrl+R 运行,下方消息框显示“查询OK,1行受影响”之类的结果,库就建好了。
拆开来看每一段的含义:
CREATE DATABASE:创建数据库,不用多说。IF NOT EXISTS:如果库已经存在,就不要再报错。这个短语在写脚本时很实用,因为脚本可能被执行多次,第二次执行时不会因为“库已存在”而中断。DEFAULT CHARACTER SET utf8mb4:指定库的默认字符集为utf8mb4。COLLATE utf8mb4_unicode_ci:指定排序规则。
很多人只记住了字符集,漏了排序规则。实际上排序规则决定了比较和排序时按什么标准来。同一字符集下会有多种排序规则,它们不是一回事,我下面专门展开讲。
3.3 utf8mb4和utf8:别再用MySQL里的utf8了,它并不是真正的UTF-8
MySQL里的utf8和utf8mb4这个问题,坑过非常多人。多数编程语言里的“UTF-8”是完整支持4字节编码的,但MySQL里的utf8是早期实现,最多只支持3个字节。这意味着什么?意味着如果表用了utf8字符集,你就存不了emoji表情,也存不了生僻字,比如一些冷门汉字,插入时要么报错,要么变成问号。
utf8mb4里的mb4是“most bytes 4”的意思,支持完整的4字节UTF-8编码。所以只要遇到中文、emoji、特殊符号混存的情况,都应该用utf8mb4。MySQL 8.0版本里默认字符集已经改成了utf8mb4,但如果是老项目、老脚本,或者接手5.7的旧库,还是经常会看到utf8。建议一律改成utf8mb4,不要有任何犹豫。
3.4 排序规则:general_ci和unicode_ci怎么选
utf8mb4下面最常见的排序规则有两类:utf8mb4_general_ci 和 utf8mb4_unicode_ci。
utf8mb4_general_ci:比较速度稍快,但排序规则相对粗糙,对Unicode的排序处理不够精细。utf8mb4_unicode_ci:基于Unicode排序规则,能更准确地处理各国语言文字的排序,性能差异在实际业务中几乎可以忽略。
我自己默认用 utf8mb4_unicode_ci。如果你用的是MySQL 8.0,还会看到一个 utf8mb4_0900_ai_ci,这是8.0新增的排序规则,基于Unicode 9.0,更准确,但只在8.0以上版本可用。考虑到有些项目可能需要兼容5.7,我建议统一的稳妥方案还是 utf8mb4_unicode_ci。
3.5 建库之后的三步验证
建库成功不等于万事大吉。接着在查询编辑器里执行下面三条命令,确认库状态正常:
sql复制SHOW DATABASES LIKE 'school_db';
USE school_db;
SELECT DATABASE();
第一条用来确认库已经出现在实例里;第二条切到 school_db 这个库;第三条返回当前选中的数据库名,确保后面建表时不会建错地方。这三条命令执行完,左侧对象树里刷新一下,就能看到 school_db 了。
4. 建表前的设计工作:字段清单、类型选择和命名规则
4.1 用“学生信息表”为例,先列出字段清单
建表最忌讳一上来就写SQL,想一个字段写一个字段,写到一半发现少一个,又回头加。我的做法是先在纸上或文档里把字段清单列出来,明确每个字段的含义、类型、是否必填、是否唯一,确认没问题再转换成SQL。
这里以学校系统里再常见不过的 student(学生基础信息表)为例。设计如下字段:
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | INT UNSIGNED | 主键、自增 | 学生内部ID |
| student_no | VARCHAR(20) | 非空、唯一 | 学号 |
| name | VARCHAR(50) | 非空 | 姓名 |
| gender | TINYINT | 非空、默认1 | 性别 1男 2女 0未知 |
| birthday | DATE | 可空 | 出生日期 |
| phone | VARCHAR(20) | 可空 | 手机号 |
| VARCHAR(100) | 可空 | 邮箱 | |
| status | TINYINT | 非空、默认1 | 状态 1在读 2休学 3毕业 4退学 |
| created_at | DATETIME | 非空、默认当前时间 | 创建时间 |
| updated_at | DATETIME | 非空、更新时自动修改 | 更新时间 |
这个表的设计比较典型,既有主键、唯一键这种约束,又有日期、字符串、整型等常用类型,作为入门例子很合适。
4.2 每个字段为什么选这个类型,而不是其他类型
字段类型选择的逻辑,比字段本身更重要。我一个个说明:
id用INT UNSIGNED:INT范围约正负21亿,UNSIGNED去掉负数,范围翻倍到约42亿,对学生表来说完全够用。自增主键不需要负数,用UNSIGNED更合理。如果数据量可能极大,可以考虑BIGINT,但学生表没这个必要。student_no用VARCHAR(20):学号虽然是数字,但不需要参与数学运算,而且可能带字母或前导零,所以用字符串类型。VARCHAR(20)里的20是字符数,不是字节数,够普通学号用了。name用VARCHAR(50):姓名长度因语言而异,中文名一般不超过10个字符,但考虑到少数族裔名字可能较长,留50个字符比较稳妥。不要随手写255,太宽的VARCHAR在索引和内存排序时都有额外开销。gender用TINYINT:性别用枚举值0/1/2,而不是字符串“男/女”。原因有两个:一是省空间,二是后续如果你要加“保密”这种状态,改数据字典就行,不用改表结构。真正显示成“男/女”是前端展示层的事。birthday用DATE:只需要年月日,不需要时分秒,用DATE正合适。有人习惯用VARCHAR存日期,这是大坑,存进去之后按时间范围筛选极其痛苦,DATETIME才是数据库该存的方式。phone用VARCHAR(20):手机号同样没有计算需求,而且可能带+86前缀。如果存成INT,前导零直接消失,+86也存不了。email用VARCHAR(100):邮箱长度一般不会超过100,留上就够了。status用TINYINT:和gender一样的逻辑,用数字枚举值表示业务状态,比用字符串灵活。created_at和updated_at用DATETIME:记录创建和更新时间,可以在数据库层直接设置默认值,不需要每次插入时手动传。
4.3 主键、唯一键和索引:基础表也要有基本约束
约束是表设计里最容易忽略,但最影响数据质量的部分。
主键我选的是自增的 id。为什么不直接用 student_no 当主键?因为学号属于业务字段,理论上存在变更可能,而且字符串类型做主键,在InnoDB存储引擎下,二级索引存储的是主键值,字符串主键会让所有索引都变大变慢。用一个无业务含义的自增整型做主键,是标准做法。唯一性另用唯一键约束来保证,两者各司其职。
student_no 加唯一键,确保一个学号在表里只能出现一次。name 加普通索引,因为业务上经常会按姓名搜索。所有这些约束,在建表SQL里一次性定义清楚,比事后用ALTER慢慢补要靠谱得多。
4.4 每个字段都必须写COMMENT,这不是形式主义
看到有人建表不写注释,我的第一反应是这个人肯定没维护过别人写的表。一张表十个字段,三个月后你回头看,如果没有COMMENT,你要猜“gender字段里存的1到底代表男还是女”,“status里的3又是什么状态”。
所以建表SQL里,我要求每个字段后面都带 COMMENT,把含义和枚举值说明白。写上“性别 1男 2女 0未知”这么一行字,将来返工的成本能省下非常多。表本身也要有注释,说明这张表是干什么的。
5. 实战执行:完整建表SQL逐句拆解,在Navicat中运行并验证
5.1 完整建表SQL先看全貌
确认已经用 USE school_db; 切到了 school_db 库,在查询编辑器里输入下面这段SQL:
sql复制CREATE TABLE IF NOT EXISTS `student` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '学生ID,主键',
`student_no` VARCHAR(20) NOT NULL COMMENT '学号',
`name` VARCHAR(50) NOT NULL COMMENT '姓名',
`gender` TINYINT NOT NULL DEFAULT 1 COMMENT '性别 1男 2女 0未知',
`birthday` DATE DEFAULT NULL COMMENT '出生日期',
`phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态 1在读 2休学 3毕业 4退学',
`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_student_no` (`student_no`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='学生基础信息表';
选中整段SQL(或者把光标放在语句中间,Navicat会识别出当前这条语句),按 Ctrl+R 运行。看到“查询OK,0行受影响”,左侧对象树刷新,student 表就出现了。
5.2 逐段拆解:每一行都是什么意思
这段SQL信息量不小,我按块解释。
第一块是表名和IF NOT EXISTS。表名 student 用了反引号包裹,目的是避免与保留字冲突,虽然student不是保留字,但养成用反引号包表名、字段名的习惯没坏处。IF NOT EXISTS和建库时一样,避免重复执行时报错。
第二块是字段定义。每个字段的通用格式是:字段名 + 类型 + 约束 + COMMENT。比如 gender TINYINT NOT NULL DEFAULT 1 COMMENT '性别 1男 2女 0未知',含义是字段类型为TINYINT,不允许为空,默认值是1,注释说明1代表男。created_at 的 DEFAULT CURRENT_TIMESTAMP 是让数据库在插入数据时自动填当前时间;updated_at 额外加了 ON UPDATE CURRENT_TIMESTAMP,意思是每一行数据更新时,这个字段自动变成当前时间,不用你在UPDATE语句里手动维护。这两个默认值设计,在很多实际项目里非常常用。
第三块是索引和约束。PRIMARY KEY (id) 设定主键;UNIQUE KEY uk_student_no (student_no) 给学号建唯一索引,索引名是uk_student_no;KEY idx_name (name) 给姓名建一个普通索引,方便按姓名检索。索引名的命名规范我习惯用:唯一索引 uk_字段名,普通索引 idx_字段名,一眼就能看出是什么类型的索引。
第四块是表选项。ENGINE=InnoDB 指定存储引擎,选择InnoDB是因为它支持事务、行级锁、外键,是绝大多数业务场景的正确选择。DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci 指定表的默认字符集和排序规则,即使将来这个表被迁移到其他默认字符集的库,它的行为也是确定的,不会受环境影响。
5.3 Navicat中执行命令的两种方式
Navicat里运行SQL有两种常用方式:
- 光标放在某条语句内部,直接按
Ctrl+R,运行当前这一条; - 用鼠标选中多条或一段SQL,按
Ctrl+Shift+R,只运行选中部分。
刚学的时候我建议你把整段SQL全选再运行,这样消息框会明确告诉你执行的是哪段。如果只把光标放在语句里,有时候Navicat会识别错,尤其是同一编辑器里有多条语句时,容易把不想执行的语句一起跑了。另外,运行之前再看一眼编辑器左上角的下拉框,确认选中的是 school_db,这能避免不少低级错误。
5.4 验证表结构:DESC和SHOW CREATE TABLE 双管齐下
表建好之后,不要只看左侧树里多了个名字就结束,还要验证结构对不对。执行:
sql复制DESC student;
会以表格形式展示所有字段、类型、是否为空、默认值、是否主键等信息,这是日常查看表结构最常用的命令。
如果你想看这张表的完整定义,包括索引、字符集、表注释,执行:
sql复制SHOW CREATE TABLE student;
返回的结果是一段完整的建表SQL,相当于MySQL官方帮你“重新生成”了一遍建表语句。两条配合使用,能确认你写的SQL和MySQL实际执行的没有偏差。再执行 SELECT * FROM student;,结果会显示一个空表,查询本身不报错,说明表已经可以正常使用了。
6. 新手建表最容易踩的六个坑,我全踩过,现在一次性告诉你
6.1 报错“No database selected”:最大的低级错误
这条报错的意思是:MySQL知道你执行了建表语句,但你没告诉它要把表建在哪个库里。解决方法很直接,在执行建表语句前加一行 USE school_db;。但在Navicat里,很多人明明在左侧双击选中了某个库,还是会报这个错,原因是左侧对象树的选择状态跟查询编辑器的“当前数据库”不是一回事。一定要养成习惯:每次在查询编辑器里干活,先看一眼左上角下拉框,确认当前库是自己要操作的那个。
6.2 字段名撞上保留关键字:desc、order、key都会让你怀疑人生
MySQL有一批保留关键字,不能直接拿来当表名或字段名。最常见的几个是 desc、order、group、key、status(status在部分版本中不是保留字,但容易混淆)。如果你建表时用了这类名字,会直接报语法错误。
假如你已经用了,怎么办?用反引号把字段名包起来,例如 desc,这样MySQL就会当成普通名字处理。但我的建议是:命名时从一开始就主动避开这些关键字,与其每次写SQL都要加反引号,不如一开始就叫 description、order_no。省事,也避免将来同事用别的工具连接时踩没反引号的坑。
6.3 中文注释乱码:连接编码、表编码、客户端编码各管一段
乱码这个问题,在多人协作时尤其容易出现。常见表现是:建表SQL里的中文注释执行成功后,在Navicat里看是一堆问号;或者插入的中文数据查询出来是乱码。
原因可能是多层的:MySQL服务端默认字符集不是utf8mb4、Navicat连接属性里的编码没设对、建表语句里没显式指定字符集。我建议的排查顺序是:先确认连接属性里字符集为utf8mb4,再确认建库建表语句里都显式写了 DEFAULT CHARSET=utf8mb4,最后确认查询编辑器文件本身保存的编码不是乱码。大部分情况下,这三步做完,乱码问题都能解决。
6.4 自增主键报错:AUTO_INCREMENT必须定义成键
如果你写过一个自增列,但它不是主键也不是索引,MySQL会报错:Incorrect table definition; there can be only one auto column and it must be defined as a key。意思是自增列必须是一个键,而且一张表只能有一个自增列。
所以如果你没有在主键上使用自增,而是想给某个业务字段加AUTO_INCREMENT,得先确认它是索引。对新手来说,最简单的做法就是:自增列就放在主键上,像我们的 id 一样,不要玩花活。另外,InnoDB引擎本身也建议每张表显式设置主键,它的聚簇索引结构依赖主键来组织数据,没有主键的表在InnoDB里反而会内部生成隐藏主键,白白浪费存储和性能。
6.5 字段长度凭感觉写,回头返工成本高
见过有人把手机号存成INT,结果插入时发现“+86”和“0开头的号码”全丢了;也见过有人把出生日期存成VARCHAR,后面想统计“95后有多少人”时写不出SQL。这类问题不是语法错误,调试时不报错,等业务跑一段时间才发现数据不对,那时候再改字段类型,涉及数据迁移,麻烦得多。
我的经验是,选类型时先问自己三个问题:这个字段需要参与数学计算吗?需要按大小排序吗?需要存特殊格式吗?手机号不计算,用VARCHAR;日期要排序要筛选,用DATE或DATETIME;状态值就一组数字,用TINYINT。字段长度也不是越大越好,VARCHAR(255)和VARCHAR(50)在数据量上来之后,索引性能和内存占用都会有差距。
6.6 不写注释,或者注释写得毫无信息量
最后这个坑不算报错,但最影响团队协作。我接手过一个老项目,有一张 member 表,字段叫 a、b、c,字段注释全是空的,代码里也找不到对应映射,最后只能靠猜。这种表,谁维护谁崩溃。
正确做法是我们前面展示的那样:每个字段写清楚含义,枚举值用“数字+含义”的格式写全,比如“状态 1在读 2休学 3毕业 4退学”。如果字段有特殊规则,比如“受保护字段,只有管理员可改”,也写在注释里。有了这些注释,一年后你再看这张表,基本不用翻代码就能明白整个数据模型。
建表这件事,看着简单,实际是后续所有查询、维护、扩展的地基。我个人的习惯是:每次建完表,顺手把完整的CREATE TABLE语句复制到项目目录下的sql文件夹里存档,再在团队的数据库变更文档里记一条“新增student表”的记录。一开始觉得多此一举,后来连续几次需要快速重建环境、对比新旧表结构时,这些脚本真的救了大命。下一篇我会接着讲表结构修改和更复杂的约束设计,基础表这一篇先到这里。
