# 一、基础
30 秒速记
- 模式(
schema)= 数据的结构契约:有哪些表、每列什么类型、能不能为空、有哪些约束 - 主键唯一标识一行,不改、不复用:被外键和日志引用着,改了或复用了就会指错对象
- 主键尽量用无业务含义的自增
ID或UUID,手机号、邮箱这种会变的放唯一索引 SQL有ANSI标准,各家有方言:Oracle的PL/SQL、SQL Server的T-SQL;USE db和#注释在PostgreSQL里都不认- 关键字不区分大小写;表名看系统和配置(
MySQL在Linux上默认区分),字符串比较看排序规则(utf8mb4_0900_ai_ci不区分)
这一节的核心是三件事:模式是数据的结构契约,主键是一行数据的身份证,SQL 有标准但各家数据库都有自己的方言。 主键一旦用了就别改也别复用,因为外键、日志、缓存、外部链接都拿它指向这一行,改了或者被新数据复用就全指错了,所以我一般用无业务含义的自增 ID 当主键。方言这块最容易翻车:# 注释和 USE test 是 MySQL 的写法,换到 PostgreSQL 就报错。大小写也一样,关键字随便写,表名和字符串值区不区分大小写,要看数据库、操作系统和排序规则。
模式定义了数据如何存储、存储什么样的数据以及数据如何分解等信息,数据库和表都有模式。
主键的值不允许修改,也不允许复用(不能将已经删除的主键值赋给新数据行的主键)。
SQL(Structured Query Language),标准 SQL 由 ANSI 标准委员会管理,从而称为 ANSI SQL。各个 DBMS 都有自己的实现,如 PL/SQL、Transact-SQL 等。
SQL 语句不区分大小写,但是数据库表名、列名和值是否区分依赖于具体的 DBMS 以及配置。
SQL 支持以下三种注释:
# 注释
SELECT *
FROM mytable; -- 注释
/* 注释1
注释2 */
数据库创建与使用:
CREATE DATABASE test;
USE test;
原理拆解: 模式可以理解为数据库对象的结构契约:列的数据类型、是否允许 NULL、唯一性、对象之间的关联等信息共同决定什么数据能够进入系统。主键负责唯一标识一行,通常还会被其他表的外键引用;若修改已经被引用的主键,会扩大更新范围并破坏标识的稳定性。因此业务设计常使用无业务含义的代理键作为主键,把手机号、订单编号等可能变化的值放在普通唯一列中。题目约定删除后的主键也不复用,这有利于避免旧日志、缓存或外部链接错误指向新记录。
具体辨析: SQL 标准定义通用语法和语义,DBMS 再提供自己的数据类型、函数、过程语言及管理命令。PL/SQL 主要属于 Oracle 体系,Transact-SQL 属于 SQL Server 体系;能够在某一产品执行的语句,不代表能原样迁移到另一产品。USE test、# 注释也不是所有数据库都接受,因此迁移脚本不能只凭“属于 SQL”判断兼容性。
边界与排查: 关键字不区分大小写不等于所有标识符和字符串都不区分大小写。字符串比较通常受排序规则影响,标识符行为还可能受操作系统、引号方式及数据库配置影响。验证兼容性时,应在目标 DBMS 建立最小数据库和表,分别测试建库、切库、注释、大小写比较及约束错误,并通过产品文档确认方言边界,而不是依赖另一套数据库中的执行结果。
面试官追问
追问 1SELECT 大小写随便写,那表名、字符串值是不是也不区分?
不一定。MySQL 表名在 Linux 上默认区分大小写,在 Windows 和 macOS 上不区分,由 lower_case_table_names 控制;字符串比较看排序规则,_ci 结尾的不区分,_bin 和 _cs 区分。本地 macOS 跑得好好的,部署到 Linux 报表不存在,就是这个原因。
追问 2一个初始化脚本要同时在 MySQL 和 PostgreSQL 上跑,注释怎么写?
只用 -- 和 /* */,别用 #,它只有 MySQL 认。还要注意 MySQL 的 -- 后面必须跟一个空格,--注释 会报语法错误。
追问 3删掉的订单留下空号,开发想把编号补给新订单保持连续,行吗?
不行。旧订单的日志、退款记录、外部系统可能还拿着这个 ID,复用后就指到新订单上了。如果业务就是要连续的展示编号,单独加一列订单号,主键别动。
追问 4手机号直接当用户表主键,有什么问题?
手机号会换,主键一改,所有引用它的外键表都得跟着改。而且字符串主键比整数占空间,在 InnoDB 里每个二级索引都会带上主键值。用自增 id 做主键,手机号加唯一索引。
追问 5脚本迁到 PostgreSQL 后卡在 USE test 这一行,怎么改?
PostgreSQL 没有 USE,切库是在连接时指定的,psql 里用 \c test,程序里改连接串的数据库名。同一个库内要分组用 schema,配合 SET search_path。
# 二、创建表
30 秒速记
CREATE TABLE一次声明:列名 + 类型 + 是否可空 + 默认值 + 主键id INT NOT NULL AUTO_INCREMENT+PRIMARY KEY (id):自增主键,插入时不传id自动生成col1 INT NOT NULL DEFAULT 1:不传就是1,显式传NULL会报错,默认值不兜底NULLVARCHAR(45)在MySQL 5以后是45个字符,不是字节;DATE只存日期不存时间- 自增写法是方言:
MySQL的AUTO_INCREMENT、PostgreSQL的GENERATED ALWAYS AS IDENTITY(老写法SERIAL)、SQL Server的IDENTITY(1,1)
建表就是把每一列的类型、能不能为空、默认值和主键一次说清楚,数据库以后就按这份约定把关。 这个例子里 id 是自增主键,插入时不用管它;col1 不能为空、默认值 1;col2 是最长 45 个字符的可空字符串;col3 是可空日期。最容易误会的是默认值:不写这一列才会用默认值,显式写 NULL 是另一回事,碰上 NOT NULL 直接报错。另外 AUTO_INCREMENT、反引号、# 注释都是 MySQL 方言,换库要改。
CREATE TABLE mytable (
# int 类型,不为空,自增
id INT NOT NULL AUTO_INCREMENT,
# int 类型,不可为空,默认值为 1,不为空
col1 INT NOT NULL DEFAULT 1,
# 变长字符串类型,最长为 45 个字符,可以为空
col2 VARCHAR(45) NULL,
# 日期类型,可为空
col3 DATE NULL,
# 设置主键为 id
PRIMARY KEY (`id`));
面试官追问
追问 1一个接口不传 col1,另一个传了 NULL,两条记录都会是 1 吗?
不会。不传才用默认值 1;显式传 NULL 碰上 NOT NULL,MySQL 单行插入会直接报 Column 'col1' cannot be null。想显式用默认值,写 VALUES (DEFAULT)。
追问 2VARCHAR(45) 存 45 个汉字会被截断吗?
MySQL 5 以后长度按字符算,45 个汉字能存下,用 utf8mb4 时占用的字节最多到 180。超过 45 个字符,严格模式下报错,非严格模式会静默截断并给一条警告,所以线上一定要开严格模式。
追问 3这段建表脚本放到 PostgreSQL 里跑报错,要改哪些地方?
三处:# 注释改成 --;反引号改成双引号或者干脆去掉;AUTO_INCREMENT 改成 id INT GENERATED ALWAYS AS IDENTITY(或者老写法 SERIAL)。
追问 4id 写了 AUTO_INCREMENT 却忘了 PRIMARY KEY,会怎样?
MySQL 直接拒绝建表,它要求自增列必须是某个索引的第一列。真正要防的是另一种情况:主键定义在脚本最后一行,前面某处语法错导致整句失败,所以建完表要 SHOW CREATE TABLE 核对一下。
追问 5想把已有表的 col2 改成必填,直接加 NOT NULL 可以吗?
先查 SELECT COUNT(*) FROM mytable WHERE col2 IS NULL。有空值的话,严格模式下 ALTER 会失败,得先把空值更新成一个合理的值,同时改上游的输入校验,再加约束。
# 三、修改表
30 秒速记
- 加列:
ALTER TABLE mytable ADD col CHAR(20);列名和类型必写,约束和默认值按需加 - 删列:
ALTER TABLE mytable DROP COLUMN col;删表:DROP TABLE mytable;范围差一个数量级 DROP删的是结构连同数据,MySQL里DDL会隐式提交,没法ROLLBACK;PostgreSQL的DDL可以放进事务回滚- 重新
ADD回来只恢复了空列,数据只能靠备份或binlog找回 - 大表改结构会锁表:
MySQL 8.0加列多数能ALGORITHM=INSTANT秒完成,复杂变更用gh-ost/pt-online-schema-change
