
第一次接触数据库的人多半是从一张数据库表开始的。建表、塞数据、再把数据查出来这三步看起来就是数据库基本操作的全部。但真正到了工作里你会发现一张字段乱起的表、一个没用 WHERE 的 DELETE、一次没有备份的更新都可能让线上业务直接停摆。这篇文章我会以最常用的关系型数据库为例把数据库表的设计、建表、字段选择以及数据的新增、查询、修改、删除、备份恢复这些基本操作完整过一遍并穿插一些我实际踩过的坑和总结出来的习惯。不管是刚开始写 SQL 的学生还是刚转岗的数据库新人这套内容都能帮你建立一套安全、规范的操作思维。1. 先搞懂数据库表结构与字段设计1.1 表不是 Excel结构决定了后面所有操作的效率很多人刚开始学数据库觉得表就是一个“高级版 Excel”随手建几个列把数据往里塞就行。这个想法在练习阶段没什么问题但一到真实项目里表结构设计得烂后续写查询、做统计、加索引都会非常痛苦。数据库表本质上是“行 列”的二维结构。每一行代表一条完整记录每一列代表一个字段。字段有严格的类型和约束这是它和 Excel 最核心的差别。Excel 可以在一个格里塞“张三、25岁、1985-3-12”数据库表却要求一个字段存一种数据比如username存用户名age存年龄birthday存生日。为什么这么严格因为只有类型明确数据库才能高效地存储、比较和索引。设计表结构时最先要回答三个问题这张表描述什么实体例如用户、订单、商品。每个实体需要哪些属性例如用户名、手机号、状态、创建时间。哪些属性是唯一的能唯一标识一行记录这是主键。顺序不要反。最常见的错误是先打开建表工具想到什么字段加什么字段最后表里一堆冗余、类型混乱。我见过有人把手机号字段设成INT结果超过 11 位直接溢出存不进去也有人把金额设成FLOAT等到算账时发现 0.1 0.2 不等于 0.3只能加班修数据。表结构设计这一关值得你多花时间。1.2 字段类型选不对数据早晚出问题关系型数据库的字段类型大同小异下面这张表是我平时最常用的选型参考。以 MySQL 为例其他数据库对应地查一下文档就行。场景推荐类型踩坑提醒主键、整数INT、BIGINT用户量可能上亿时直接上BIGINT别省空间短文本如用户名VARCHAR(50)VARCHAR要指定长度别用TEXT代替固定长度编码CHAR(11)如手机号、身份证号用CHAR更省空间金额DECIMAL(10,2)绝对不用FLOAT或DOUBLE会丢精度时间DATETIME、TIMESTAMP注意时区问题建议统一用DATETIME存业务时间大文本TEXT、MEDIUMTEXT大文本不能直接加索引设计时要考虑布尔值TINYINT(1)别用BIT查询和备份都容易绕晕选字段类型时有一个核心原则在满足业务的前提下选最小但不过紧的类型。比如年龄字段用TINYINT UNSIGNED就够0 到 255 完全覆盖正常人类年龄但如果存订单量就得考虑大促和增长提前用BIGINT。类型太紧会导致未来改表类型太宽会浪费存储空间影响索引效率。除了类型还要理解约束。NOT NULL、UNIQUE、PRIMARY KEY、FOREIGN KEY、DEFAULT这些约束不是摆设。比如用户名字段如果允许为空查询时就会出现一堆NULL排序、统计、前端展示全都有坑。建表时宁可多写几个约束也不要等数据脏了再后悔。2. 数据库表的操作建表、改表、删表2.1 用 CREATE TABLE 建出第一张表建表是数据库表操作的第一步。下面是一个标准的用户信息表建表语句以 MySQL 为例CREATE TABLE user_info ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键ID, username VARCHAR(50) NOT NULL COMMENT 用户名, email VARCHAR(100) DEFAULT NULL COMMENT 邮箱, age TINYINT UNSIGNED DEFAULT 0 COMMENT 年龄, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户信息表;这段 SQL 里有几个细节值得展开。INT UNSIGNED NOT NULL AUTO_INCREMENT主键一般是自增整数UNSIGNED表示无符号取值范围从 0 开始对于主键来说永远不会为负数所以放心加。AUTO_INCREMENT让数据库自动生成主键值不用在插入语句里手工指定。VARCHAR(50) NOT NULL用户名用可变长字符串最大 50 个字符同时不允许为空。用户名字段一般要唯一所以建了一个UNIQUE KEY。注意唯一键和主键的区别主键用于标识一行唯一键用于防止业务字段重复。DEFAULT CURRENT_TIMESTAMP创建时间自动取当前时间插入时不需要传值。这是非常重要的习惯每条记录都应该有创建时间和更新时间后续排查数据问题时能救命。ENGINEInnoDB DEFAULT CHARSETutf8mb4InnoDB 是支持事务和外键的存储引擎MySQL 5.5 之后默认都是它。utf8mb4是完整版 UTF-8能存表情符号和生僻字现在新建表基本都应该用utf8mb4而不是老旧的utf8。如果你用的是 SQL Server自增列用IDENTITY(1,1)如果是 SQLite主键用INTEGER PRIMARY KEY AUTOINCREMENT。语法细节不同但建模的思想完全一致。2.2 ALTER TABLE 修改表结构不能凭感觉建好表后时常要加字段、改类型。最常用的语法是ALTER TABLE。-- 添加字段 ALTER TABLE user_info ADD COLUMN phone VARCHAR(20) DEFAULT NULL AFTER email; -- 修改字段类型 ALTER TABLE user_info MODIFY COLUMN age SMALLINT UNSIGNED DEFAULT 0; -- 删除字段 ALTER TABLE user_info DROP COLUMN phone; -- 重命名字段 ALTER TABLE user_info RENAME COLUMN username TO nickname;这里最大的坑在大表上。生产环境如果表里已经有几千万行直接跑一条ALTER TABLE加字段很多数据库会锁表导致线上写入全被卡住。MySQL 的ALTER TABLE在部分场景下会重建整张表耗时几十分钟甚至几小时期间业务直接不可用。我个人的建议是设计阶段把字段一次性想清楚上线后只做“加字段”尽量少做“改类型”和“删字段”。如果实在要改先在测试库建一张相同数据量的表评估执行时间再选择业务低峰期操作。MySQL 8.0 提供了ALGORITHMINSTANT支持部分操作瞬间完成但并不是所有修改都能用它还是要谨慎。删除字段更要注意。删字段不仅是丢数据还可能导致依赖这个字段的历史代码报错。我在实际项目中见过有人把线上表字段删了第二天接口全崩最后只能通过备份恢复数据。改表结构前先搜一遍代码确认没有引用。3. 数据基本操作增删改查CRUD3.1 INSERT数据进来的第一道关卡表结构设计好了接下来就是往里写数据。最简单的插入语句INSERT INTO user_info (username, email, age) VALUES (张三, zhangsanexample.com, 25);注意三点。第一一定要写字段列表不要省略。如果写成INSERT INTO user_info VALUES (...)一旦表结构调整插入语句就会错乱而且别人读你的 SQL 时完全不知道每个值是什么。第二字符串要处理单引号如果字符串里本身有单引号需要转义成两个单引号或者使用预处理语句。第三大批量插入时用多行合并的写法效率远高于逐条执行。INSERT INTO user_info (username, email, age) VALUES (李四, lisiexample.com, 30), (王五, wangwuexample.com, 28), (赵六, zhaoliuexample.com, 35);一次性插入几百行都没问题但如果一次插入几十万行建议分批执行每批 1000 到 5000 行避免事务日志过大和锁竞争。插入时还有一个实用技巧如果业务要求“有则更新无则插入”MySQL 可以用ON DUPLICATE KEY UPDATE。例如用户表里用户名有唯一键插入遇到重复时自动更新其他字段INSERT INTO user_info (username, email, age) VALUES (张三, newemailexample.com, 26) ON DUPLICATE KEY UPDATE email VALUES(email), age VALUES(age);这种写法能减少一次“先查后写”的往返但也别滥用。它依赖唯一键触发如果表里没有唯一键这条语句就是普通插入。3.2 SELECT八成麻烦都在查数据查询是日常接触最多的操作。基础语法不复杂真正难的是写得不好会对数据库造成巨大压力。SELECT username, email FROM user_info WHERE age 20 ORDER BY created_at DESC LIMIT 10;这里的几个关键字分别对应查哪些列、过滤哪些行、按什么排序、取多少行。看起来都很简单但有几个习惯特别重要。能写列名就不要写SELECT *。SELECT *会把表里所有列都拉出来如果表里有TEXT、大段JSON这种大字段查询慢网络传输也慢。明明只需要用户名和邮箱却把整条记录都捞出来纯属浪费。WHERE 条件里尽量避免对字段做函数运算。例如WHERE YEAR(created_at) 2024这样会导致索引失效数据库必须扫全表。更好的写法是WHERE created_at 2024-01-01 AND created_at 2025-01-01。排序字段也要留神。如果ORDER BY的字段没有索引数据量大时一样会慢。分页查询用LIMIT时越往后翻页越慢比如LIMIT 1000000, 10要先扫前面一百万行。这时可以改成基于上一页最后一条记录的 ID 来做条件过滤而不是直接翻页。如果需要多表关联查询优先明确关联字段尽量用小表驱动大表。JOIN本身不是问题问题是没有关联索引。两张表关联的字段都应该建索引否则数据库会做笛卡尔积再过滤数据量一大就是灾难。3.3 UPDATE 与 DELETE动手前先问自己三句话更新和删除是数据库基本操作里最危险的。先看语法UPDATE user_info SET age 26 WHERE username 张三; DELETE FROM user_info WHERE id 1001;执行之前我问自己三句话WHERE 条件写全了吗是不是影响到了不该影响的行这条 SQL 会在哪个环境执行线上还是测试库如果再谨慎一点要不要先 SELECT 出来看一眼第一个问题最典型。很多新手写UPDATE时忘了 WHERE或者 WHERE 写得太宽比如WHERE age 0这一跑就是全表更新。遇到这种情况如果还没有 COMMIT事务还能救如果已经自动提交了就只能靠备份恢复。所以执行 UPDATE 和 DELETE 前最好先手动开启一个事务START TRANSACTION; UPDATE user_info SET age 26 WHERE username 张三; -- 再查一次确认影响行数没问题 SELECT age FROM user_info WHERE username 张三; -- 确认无误后提交 COMMIT; -- 如果有问题就回滚 -- ROLLBACK;这个习惯能避免大多数误操作。生产环境还可以配合LIMIT控制删除条数比如DELETE FROM user_info WHERE status 0 LIMIT 1000分批删除避免一次性删除大量记录导致锁表、主从延迟。关于删除很多业务现在不用物理删除而是加一个deleted标志位做软删除。好处是数据可追溯误删后恢复方便坏处是每次查询都要记得加WHERE deleted 0而且唯一键冲突问题会变得很棘手后面我会专门讲。3.4 使用索引和约束保障数据操作正确性数据操作频繁后你会逐渐意识到索引和约束的重要性。索引不是表结构的一部分却直接影响增删改查的效率。插入数据时会同步维护索引所以索引不是越多越好而是“查询用得到才建”。举个例子用户表里经常按username精确查询那么在username上建唯一索引查询会走索引插入重复数据时数据库还会主动拦截。但如果某个字段几乎不会出现在 WHERE 条件里建索引不但没用还会拖慢插入速度。约束也一样。除了前面提到的唯一键外键能保证两张表的数据一致性但也会降低写入性能。在互联网高并发场景下很多团队选择不用外键而是在应用层控制关联关系。具体取舍要看项目阶段。对新手来说理解约束的含义比“盲目追求性能和自由”更重要。4. 数据备份与恢复基本操作里最容易被忽略的一环4.1 备份不是 DBA 专属开发人员也要会很多人在学习数据库时把精力全放在 SELECT、INSERT、UPDATE、DELETE 上觉得备份恢复是 DBA 的事。但实际工作中开发人员误删数据、改错字段的案例太多了。真到了那种时候如果不知道备份文件在哪、怎么恢复再牛的代码也救不了现场。备份的本质很简单把数据库表和数据复制一份放到安全的地方。但需要考虑三个问题备份频率多少才合适、备份文件放哪里、恢复流程是否测试过。我见过有人每天定时备份但从来没试过恢复结果真要恢复时发现备份文件是坏的。备份不做恢复演练等于没备份。4.2 命令行备份与恢复的完整示例以 MySQL 为例最通用的备份工具是mysqldump。# 备份整个数据库 mysqldump -u root -p --single-transaction --routines --triggers mydb mydb_20250415.sql # 只备份某张表 mysqldump -u root -p mydb user_info user_info_20250415.sql # 恢复数据库 mysql -u root -p mydb mydb_20250415.sql--single-transaction参数很关键。它可以在不锁表的情况下做一致性备份适合 InnoDB 表。--routines和--triggers用来备份存储过程和触发器如果库里有这些对象不加参数会漏掉。SQL Server 的备份和恢复更直观-- 备份 BACKUP DATABASE [mydb] TO DISK ND:\backup\mydb_20250415.bak; -- 恢复 RESTORE DATABASE [mydb] FROM DISK ND:\backup\mydb_20250415.bak WITH REPLACE;SQLite 也有自己的备份方式可以用.backup命令也可以直接复制数据库文件。SQLite 的数据库就是一个文件备份时最好先执行PRAGMA wal_checkpoint;合并日志文件否则直接拷文件可能丢数据。备份策略上除了每日全量备份建议至少保留最近 7 天的备份重要业务保留 30 天。备份文件要放到和数据库服务器不同的机器或云存储上否则服务器磁盘损坏时备份也跟着没了。这里再提一句恢复演练。真正的靠谱做法是每个月从备份文件里随机挑一个恢复到测试环境检查数据能否正常查询、业务能否正常启动。整个过程顺手做一遍以后遇到事故就不会手忙脚乱。5. 常见问题与排查实录5.1 两个数据库之间拷贝表数据工作中经常遇到“把 A 库里的一张表拷到 B 库”或者“把线上表的一部分数据导到测试库”。最通用的做法是INSERT ... SELECT。-- 同数据库不同表 INSERT INTO user_info_copy (username, email, age) SELECT username, email, age FROM user_info; -- 跨数据库 INSERT INTO db_target.user_info (username, email, age) SELECT username, email, age FROM db_source.user_info;如果目标表还不存在可以先复制表结构CREATE TABLE user_info_copy LIKE user_info;然后再执行INSERT ... SELECT。这种方法比用导出文件再导入简单得多而且能保留主键、索引等表结构信息。SQL Server 里对应的写法是SELECT * INTO new_table FROM old_table用法类似。需要注意跨数据库或跨服务器拷贝时要检查字符集和字段类型是否一致。遇到过很多次源库是utf8mb4目标库是latin1拷贝完后中文全部变成问号。拷数据之前先确认两边表的CHARSET相同。5.2 唯一键冲突软删除之后无法新建了这是一个典型问题用户表用username做唯一键用户删除后不是物理删除而是把deleted置为 1。结果下次再注册同名用户时数据库报“唯一键冲突”因为那条软删除的记录还占着唯一键的位置。遇到这个问题先别急着删记录软删除的目的就是保留历史数据。解决思路是“让软删除记录的唯一键也能区分”。MySQL 下一种常见做法是用“删除时间”来区分ALTER TABLE user_info ADD COLUMN deleted_at DATETIME NULL COMMENT 软删除时间, DROP INDEX uk_username, ADD UNIQUE KEY uk_username_deleted_at (username, deleted_at);正常记录deleted_at为NULL删除时把deleted_at设为当前时间。MySQL 的唯一索引允许多个NULL存在所以多条未删除的记录不会冲突而已删除的记录因为删除时间不同也不会冲突。同一毫秒删除两次的极端情况几乎可以忽略。SQL Server 和 PostgreSQL 对NULL的处理略有差异但思路类似把唯一键从单个业务字段改成“业务字段 删除标记字段”的联合唯一键。这个方案能同时保留唯一性校验和软删除数据历史。5.3 中文乱码与字符集不一致中文乱码是数据库新手最常见的噩梦。明明插入时看到的是正常中文查出来却是一堆???或者乱码。原因基本逃不出这三层客户端字符集不对。连接字符集不对。表或字段字符集不对。排查时可以执行SHOW VARIABLES LIKE character_set%; SHOW CREATE TABLE user_info;如果表结构已经是utf8mb4客户端执行插入前先设置SET NAMES utf8mb4;这一句会把客户端、连接、结果集的字符集都改成utf8mb4。绝大多数乱码问题都能用这一句解决。如果还不行检查连接字符串里是否指定了 characterEncoding比如 JDBC 的characterEncodingutf8。注意utf8和utf8mb4不一样MySQL 的utf8其实只是utf8mb3存不了 emoji。遇到 emoji 变乱码通常就是建表时用了utf8而不是utf8mb4。5.4 误删数据后怎么办万一真把数据删了第一步不是执行各种“高级恢复工具”而是先停止写入尽量让数据库处于只读状态。持续写入会把被删除数据所在的数据页覆盖掉后续恢复难度会直线上升。然后立刻确认备份策略。如果有最近的备份直接恢复到测试环境把误删的数据重新导回线上。如果开启过 binlog还可以做基于时间点的恢复。这里只能给一个思路不能保证百分百恢复所以日常备份和操作规范比“事后补救”重要得多。我自己处理过一起事故同事在测试环境写 SQL连接串指到了生产库一条DELETE FROM orders没加 WHERE直接清了整张订单表。幸好前一天有全量备份丢了大约 20 分钟的新订单最后还是靠业务日志手动补了一部分。那之后我在所有高风险 SQL 前都会强制要求加WHERE并配置了生产数据库账号不允许执行不带 WHERE 的 DELETE。6. 写在最后把基本操作练成肌肉记忆数据库表和数据的基本操作看起来都是死语法但实际使用中真正值钱的不是语法而是操作习惯。我个人实际操作中的体会是最安全的数据库操作往往是最“麻烦”的操作。执行 UPDATE 和 DELETE 前多写一条 SELECT执行事务后多看一眼影响行数这些“多余动作”就是安全边际。如果你现在刚开始学可以给自己定几条规矩每条 SQL 都写字段列表每个更新删除语句都先跑 SELECT 确认每次建表都强制写COMMENT每周至少做一次备份恢复演练。这些规矩坚持一个月你再看自己写 SQL 的方式会有明显变化。数据库的坑很多但只要把建表、改表、增删改查、备份恢复这些基本操作练扎实遇到问题时有清晰的排查路径后面的路就会顺畅很多。这篇文章提到的很多习惯来源于我踩过的真实事故希望你能直接用上少走弯路。