阅读视图

发现新文章,点击刷新页面。

SQL 语法速查手册:前端开发者的学习笔记

最近在写 Next.js + Supabase 的一个练手项目,突然又接触到了 SQL(虽然 Supabase 是通过类似 ORM 来操作数据库的),突然感觉 SQL 语法又陌生了。

真是学了不用,等于白学。

之前写过一篇一个前端小白,学习 SQL 语句,现在回头看,自己都看懵了,真的尬住了。

算了,重新整理一遍吧,并通过一个实战案例来巩固基础。

邂逅

SQL: Structured Query Language,称为结构化查询语句,简称 SQL。

SQL 编写规范

  1. 关键词建议使用大写,比如 CREATETABLE(小写也可以,但大写更规范)
  2. 语句末尾加分号
  3. 如果表名或字段名是 SQL 关键词,用反引号 ` 包裹
  4. 表名用单数,字段名用蛇形命名(下划线分隔,如 user_id

SQL 分类

分类 描述 常见命令/关键字
DDL 定义修改数据库结构(库、表、索引、视图等) CREATEDROPALTERTRUNCATERENAME
DML 表中的记录进行增、删、改 INSERTUPDATEDELETEMERGE
DQL 表中的记录进行查询 SELECT(以及配套子句 WHEREGROUP BYORDER BYJOIN 等)
DCL 权限访问控制(授予、回收) GRANTREVOKE
  • DDL(Data Definition Language):数据定义语言,-----> 管"结构"
  • DML(Data Manipulation Language):数据操作语言,-----> 管"改数据"
  • DQL(Data Query Language):数据查询语言,-----> 管"查数据"
  • DCL(Data Control Language):数据控制语言,-----> 管"权限"

为了阅读方便,下面的 SQL 语句都用小写,但实际开发中推荐关键词用大写

SQL 类型

type.png

实际开发中常用的类型:

  • int:存储整数
  • varchar(100): 存储变长字符串,可以指定长度
  • char:定长字符串,不够的自动在末尾填充空格
  • double:存储浮点数
  • date:存储日期 2023-05-27
  • time:存储时间 10:13
  • datetime:存储日期和时间 2023-05-27 10:13
  • timestamp:存储日期时间的,但是范围小一点,而且会转为中央时区 UTC 的时间来存储
  • text:存储长文本

SQL 设计了这么多数据类型,一是为了存储更丰富的信息,二是为了节省存储空间。不常用的类型,用到时再查就行

表约束

  • primary key: 主键

    • 主键是表中唯一的索引
    • 必须是 not null; 如果没有设置,mysql 也会自动设置;
    • 联合主键,多个字段合成的主键
    • 尽量不要使用业务主键
  • unique: 唯一;除了主键以外,针对某些字段,也是唯一的。

  • not null: 字段不能为空

  • default: 默认值

  • auto_increment: 自动增长;

  • foreign key: 外键,与其他表的字段关联

create table if not exists yyy(
  id int primary key,
  `name` varchar(50) unique not null,
  age int default 18,
  num int auto_increment,
  homeId int, -- 外键
  foreign key (homeId) references home(id) -- 关联 home 表的 id
)

DDL(管结构)

库操作

-- 查看所有数据库
show databases;

-- 使用数据库 co_blog
use co_blog;

-- 查看选择的数据库
select database();

-- 创建数据库
create database if not exists co_blog; -- 先判断 co_blog 库是否存在,不存在则创建
create database co_blog; -- 若存在,会报错;

-- 删除数据库
drop database if exists co_blog;
drop database co_blog; -- 数据库不存在,会报错
use co_blog; -- 使用 co_blog 库,后续表操作都在这个库下

表操作

-- 查看库中所有的表
show tables;

-- 创建一张表
create table if not exists user(
  id int primary key auto_increment,
  name varchar(10),
  age int
);

-- 查看表结构
desc user; -- 显示表的字段定义信息

-- 删除 user 表
drop table if exists user;

-- 修改 user 表
alter table user rename to users;             -- 修改表名 user 变成 users
alter table user add height int;              -- 添加 height 字段,为 int 类型;
alter table user change height newHeight int; -- height 改为 newHeight, 类型也可以重新定义
alter table user modify height bigint;        -- 类型 int 变为 bigint
alter table user drop height;                 -- 删除 height 字段

DML(管改数据)

增删改

插入数据

-- 插入单条数据
insert into user(username, emails) values('admin', 'xxxx@xxxx.com');

-- 插入多条数据
insert into user(username, emails)
  values
    ('admin', 'xxxx@xxxx.com'),
    ('admin2', 'xxxx@xxxx.com'),
    ('admin3', 'xxxx@xxxx.com');

更新数据

update `user` set name = '张三1', age = 19 where id = 1;

删除数据

delete from `user` where id = 1;

DQL(管查询)

查询语句的执行顺序:先筛选数据,再分组过滤,然后排序,最后分页。

SELECT *
  FROM table_name
  WHERE    xxx
  GROUP BY xxx
  HAVING   xxx
  ORDER BY xxx
  LIMIT    xxx;

取别名 as

-- 查询所有字段
select * from user;

-- 查询指定字段并取别名
select name, age as c_age from user;

-- 别名的作用:多表查询时避免字段名冲突

比较运算符

-- 比较运算符:>、<、=、!=、>=、<=

select * from users where age > 20;  -- 年龄大于 20
select * from users where age != 20; -- 年龄不等于 20

逻辑运算符

-- name 为 张三,且 age 大于 10
select * from users where name = '张三' and age > 10;
select * from users where name = '张三' && age > 10;

-- name 为 张三,或者 age 大于 10
select * from users where name = '张三' or age > 10;
select * from users where name = '张三' || age > 10;

-- age 在 10 到 20 岁间的
select * from users where age between 10 and 20;
select * from users where age >=10 && age <= 20;

-- age 为 10 或 20 的(in 的相反是 not in)
select * from users where age in (10, 20);
select * from users where age = 10 or age = 20;

模糊搜索 like

  • %: 任意多个字符
  • _: 任意一个字符
select * from users where name like 't%';   -- 以 t 开头
select * from users where name like '%t%';   -- 包含 t
select * from users where name like '_t%';   -- 第二个字符是 t

排序 order by

  • asc:升序(默认,可省略)
  • desc:降序
select * from users order by age asc;  -- 年龄升序
select * from users order by age desc; -- 年龄降序

限制分页 limit

  • limit 数据条数 offset 偏移量 (推荐)
  • limit 偏移量,数据条数 (不推荐)
-- limit 数据条数 offset 偏移量 (推荐)
select * from users limit 30 offset 10;

-- limit 偏移量,数据条数 (不推荐)
select * from users limit 10, 30;

-- 前端格式
const pages = {
  current: 3,
  pageSize: 10,
};

-- 后端查询
const offsetNum = (pages.current - 1) * pages.pageSize;
const limitNum = pages.pageSize;

-- sql
const sql = `select * from users limit ${limitNum} offset ${offsetNum}`;

聚合函数

聚合函数: 先收集到一起,然后对收集的结果进行操作。(看成一组)

  • avg:平均值
  • max:最大值
  • min:最小值
  • sum:求和
  • count:计数(统计行数)
-- 平均值
select avg(age) as avg_age from users;

-- 计算人数
select count(*) as count from users;

-- 最大值
select max(age) as maxAge from users;

-- 求和
select sum(age) as sumAge from users;

SQL 还有很多内置函数:

  • 聚合函数:avg、count、sum、min、max
  • 字符串函数:concat、substr、length、upper、lower
  • 数值函数:round、ceil、floor、abs、mod
  • 日期函数:year、month、day、date、time
  • 条件函数:if、case
  • 系统函数:version、database、user
  • 类型转换函数:convert、cast、date_format、str_to_date
  • 其他函数:nullif、coalesce、greatest、least

实际开发中,聚合函数最常用的是统计分组,其他函数用到时再查就行。

分组 group by

聚合函数看成一组;有时需要进行分组,然后进行操作。

使用建议:group by 一定要配合聚合函数使用,不然分组没有意义(除非整张表都看成一个聚合函数,就不需要使用 group by)。

-- 统计男女个数
select sex, count(*) as num from users group by sex;

分组筛选条件 having

在进行分组的时候,有时也需要过滤条件。

但是 group bywhere 不能一起使用,语法报错。取而代之的是 having

-- 根据 sex 性别进行分组,然后筛选出 count 大于 2
select count(*) as count, sex from users group by sex having count > 2;

去重 distinct

select distinct age from users;

多表

外键添加

创建表时添加外键:

create table if not exists users(
  id int primary key,
  name varchar(255) not null,
  role_id varchar(255) not null,
  foreign key (role_id) references role(id)
);

已有表添加外键:

alter table users add role_id int;
alter table users add foreign key (role_id) references role(id);
外键删除(更新)

表之间有关联后,不能直接删除或更新,否则会影响关联表。需要设置级联操作:要么一起更新,要么一起删除。

-- 一起更新一起删除
alter table user add foreign key (role_id) references role(id) ON DELETE CASCADE ON UPDATE CASCADE;

删除外键

show create table users; -- 查看外键名称
alter table users drop foreign key users_ibfk_1;

如果存在多个外键:

  • users_ibfk_1
  • users_ibfk_2
  • ...

重新绑定外键

alter table users add foreign key (role_id) references role(id)
                                           on delete cascade
                                           on update cascade;
多表查询

多表查询时,表之间必须有联系(通常是外键)。

select * from users, team;

这样查询会产生大量无用数据,因为这是笛卡尔积(每行数据都会组合)。

表连接
  • 左连接(left join): 常用
  • 右连接(right join): 不常用
  • 内连接([cross/inner] join): 常用
  • 全连接(mysql 不支持全连接,需要 union): 不常用

左右指的是以哪张表为主,展示该表的所有数据。

from 左表 [left/right] join 右表

其实 left joinright join 可以等价,换一下表的顺序就行

左连接

-- 左连接 LEFT JOIN. ON 连接条件
select * from user left join role
                    on user.role_id = role.id;

-- 筛选出 role_id 不为 null 的(左连接以左表为主,如果右表没有匹配数据,右表字段为 null)
select * from user left join role
                           ON user.role_id = role.id
                           WHERE role_id IS NOT NULL;

内连接

内连接只返回两表都匹配的数据,不以哪张表为主。

select * from user join role on user.role_id = role.id;

针对多对多的查询出现时,就是多次采用连接即可。

子查询

SQL 支持嵌套查询,也就是在查询中嵌套另一个查询(子查询)。

-- 查询年龄最大的人
-- 方式一:分两步
select max(age) from user;  -- 先查出最大年龄
select name, age from user where age = xxx; -- 再根据年龄查数据

-- 方式二:用子查询(推荐)
select name, age from user where age = (select max(age) from user);

子查询还有个特有的语法 EXISTSNOT EXISTS

-- 查询所有有角色的用户
select name from user where EXISTS (select * from role where role.id = user.role_id);

-- 查询所有没有角色的用户
select name from user where NOT EXISTS (select * from role where role.id = user.role_id);

子查询可以在 selectinsertupdatedelete 中使用。

事务

修改多个关联表时,必须使用事务,保证要么全部成功,要么全部失败(回滚)。

start transaction;    -- 开启事务
insert into role(id, name) VALUES (4, '群众');
insert into user(name, age, sex) VALUES ('james', 18, 4);

-- 提交事务,执行之后就不能 rollback 了
commit;
-- 回滚事务
rollback;

如果想回滚到某个中间点,可以使用 savepoint 设置保存点。

savepoint a1;:回滚到 a1 时,a1 之前的操作保留,a1 之后的操作被撤销。

start transaction;  -- 开启事务
savepoint a1;  -- savepoint 这一刻前面的依旧活着,这一刻后面的都被抹除
insert into role(id, name) VALUES (4, '群众');
savepoint a2;
insert into user(name, age, sex) VALUES ('james', 18, 4);
savepoint a3;

rollback to a2; -- 回滚到 a2 的那一刻

事务隔离级别(有四种级别,比较复杂,自己也有点懵,一般用默认的就行)

select @@transaction_isolation; -- 查看隔离级别

DCL(管权限)

权限管理在实际开发中较少用到,等遇到时再补充(其实自己也不知道)。

案例演示(Blog 的 CRUD)

用一个博客系统的增删改查来演示 SQL 的实际应用。

先设计表结构

CREATE TABLE `user`(
  id INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
  username VARCHAR(100) UNIQUE NOT NULL COMMENT '用户名',
  password VARCHAR(100) NOT NULL COMMENT '密码',
  email VARCHAR(100) UNIQUE NOT NULL COMMENT '邮箱',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
);

-- 标签表
CREATE TABLE `tag`(
  id int NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
  name varchar(100) UNIQUE NOT NULL COMMENT '标签名',
  article_count int DEFAULT 0 COMMENT '文章数量',
  created_at datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
);

-- 文章表
CREATE TABLE `article`(
  id int NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
  title varchar(100) NOT NULL COMMENT '标题',
  content text NOT NULL COMMENT '内容',
  user_id int NOT NULL COMMENT '作者id',
  like_count int NOT NULL DEFAULT '0' COMMENT '点赞数',
  comment_count int NOT NULL DEFAULT '0' COMMENT '评论数',
  created_at datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE
);

-- 文章标签表(中间表的级联方式要设置为 CASCADE,这个是固定的)
CREATE TABLE `article_tag`(
  id int NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
  article_id int NOT NULL COMMENT '文章id',
  tag_id int NOT NULL COMMENT '标签id',
  FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE ON UPDATE CASCADE,
  FOREIGN KEY (tag_id) REFERENCES tag(id) ON DELETE CASCADE ON UPDATE CASCADE
);

-- 点赞表
CREATE TABLE `like`(
  id int NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
  article_id int NOT NULL COMMENT '文章id',
  user_id int NOT NULL COMMENT '用户id',
  created_at datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE ON UPDATE CASCADE,
  FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE
)

-- 评论表
CREATE TABLE `comment`(
  id int NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
  article_id int NOT NULL COMMENT '文章id',
  user_id int NOT NULL COMMENT '用户id',
  target_user_id int NOT NULL COMMENT '被回复人id',
  content varchar(255) NOT NULL COMMENT '内容',
  parent_id int DEFAULT 0 COMMENT '父级id',
  created_at datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE ON UPDATE CASCADE,
  FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE
)

ER 图为:

blog-er.png

用户和标签的 CRUD

以用户表(user)为例,标签表(tag)同理

-- 创建用户
insert into user(username, password, email) VALUES ('admin', '123456', 'admin@163.com');

-- 查询用户
select * from user;

-- 更新用户
update user set password = '123456' where id = 1;

-- 删除用户
delete from user where id = 1;

文章的 CRUD

创建文章

start transaction;

-- 创建文章,获取文章 id(代码中获取 id 后执行下一步)
insert into article(title, content, user_id, like_count, comment_count) VALUES ('标题', '内容', 1, 0, 0);

-- 遍历 tagIds,与文章 id 一起插入中间表(代码中遍历 tagIds,组装 SQL)
insert into article_tag(article_id, tag_id)
  VALUES
    ('文章id', '标签id1'),
    ('文章id', '标签id2');

-- 更新 tag 表的文章数量
update tag set article_count = article_count + 1 where id in ('标签id1', '标签id2');

commit;

删除文章

delete from article where id = '文章id';
-- 因为采用的是 cascade,所以 article_tag 等会自动删除

查询文章列表

SELECT id,
       title,
       user_id,
       like_count,
       comment_count,
       created_at
FROM   article
ORDER  BY created_at DESC
LIMIT  10 OFFSET 0;

-- 分页查询,按创建时间倒序
-- like_count 和 comment_count 采用计数器冗余方案,简单性能好,但可能数据不一致

更新文章

-- 1. 开启事务
START TRANSACTION;

-- 2. 更新文章表
UPDATE article
  SET
    title = '新标题',
    content = '新内容'
WHERE id = 123 AND article.user_id = '用户id';

-- 3. 根据文章 id,在中间表 article_tag 中拿到旧标签(a,b), 然后根据用户传递的新标签(a,c), 找出删除的标签(b),然后再 tag 表中更新文章数量(-1)
UPDATE tag
SET    article_count = article_count - 1
WHERE  id IN (
        SELECT tag_id
        FROM   article_tag
        WHERE  article_id = '文章id'
          AND  tag_id NOT IN ('新标签id1', '新标签id2')   -- 新标签列表
      );

-- 4. 查找旧标签,然后使用 in 和 not in 交集,找出新增的标签,然后在 tag 表中更新文章数量(+1)
UPDATE tag
SET    article_count = article_count + 1
WHERE  id IN ('新标签id1', '新标签id2')
  AND  id NOT IN (SELECT tag_id FROM article_tag WHERE article_id = '文章id');

-- 5. 更新中间表:先删除文章的所有标签,再插入新标签
DELETE FROM article_tag WHERE article_id = '文章id';
INSERT INTO article_tag(article_id, tag_id) VALUES
('文章id', '新标签id1'),
('文章id', '新标签id2');

COMMIT;

点赞

无论是点赞还是取消点赞, 第一步是先判断是否已经点过赞

SELECT * FROM `like` WHERE article_id = '文章id' AND user_id = '用户id' LIMIT 1;

如果查询到记录,说明已点赞,执行取消点赞;否则执行点赞。

-- 点赞

START TRANSACTION;
--  1. 更新 like 表数据
INSERT INTO `like`(article_id, user_id) VALUES ('文章id', '用户id');
--  2. 更新 article 表数据(+1)
UPDATE article SET like_count = like_count + 1 WHERE id = '文章id';
COMMIT;
-- 取消点赞

START TRANSACTION;
--  1. 删除 like 表数据
DELETE FROM `like` WHERE  article_id = '文章id' AND user_id = '用户id';
--  2. 更新 article 表数据(-1)
UPDATE article SET like_count = like_count - 1 WHERE id = '文章id';
COMMIT;

评论

写到这里发现,最初表设计有问题。本想用一个 parent_id 实现无限层级,但看了掘金和 B 站的评论设计,发现评论最多 2 层,其他都是平铺回复。

B 站的评论:

blog-sql-bi.png

掘金的评论:

blog-sql-juejin.png

都是两层模型

  • 第 1 层:顶级评论(parent_id = NULL
  • 第 2 层:对顶级评论的回复(parent_id = 顶级.id
  • 第 3 层及以后:不再嵌套,把目标人 @nick 写进内容,parent_id 仍等于顶级评论 id,按时间平铺

新增评论

START TRANSACTION;
-- 情况一:顶级评论
INSERT INTO comment(article_id, user_id, parent_id, target_user_id, content)
  VALUES ('文章id', '用户id', NULL, NULL, '一级评论');

-- 情况二:回复顶级评论或回复某人
INSERT INTO comment(article_id, user_id, parent_id, target_user_id, content)
  VALUES ('文章id', '用户id', '顶级评论id', '被回复人id', '二级评论');

-- 3. 更新 article 表评论数(+1)
UPDATE article SET comment_count = comment_count + 1 WHERE id = '文章id';

COMMIT;

删除评论

-- 1. 查看是否有子评论
SELECT 1 FROM comment WHERE parent_id = '评论id' LIMIT 1;
-- 情况一:如果没有子评论

-- 情况一:没有子评论,直接删除
START TRANSACTION;
DELETE FROM comment WHERE id = '评论id' AND user_id = '用户id';
-- 2. 更新 article 表评论数(-1)
UPDATE article SET comment_count = comment_count - 1 WHERE id = '文章id';
COMMIT;


-- 情况二:有子评论,一起删除
START TRANSACTION;
-- 1. 获取所有子评论 id(二级评论的 parent_id 都是顶级评论 id)
select id as ids from comment where parent_id = '评论id';

-- 2. 代码中组装 ids:[...ids, '评论id'](包含所有子评论 id 和顶级评论 id)

-- 3. 根据 id 批量删除
delete from comment where id in ('ids');

-- 4. 更新 article 表评论数(减去 ids.length)
UPDATE article SET comment_count = comment_count - 'ids.length' WHERE id = '文章id';
COMMIT;

这里使用代码 + SQL 语法的组合,纯粹用 SQL 实现需要使用存储过程,目前还不会

查询评论列表

这里查询主要还是看评论的交互流程是怎么样设计的。

  • 情况一:直接查询所有评论(在代码中组装成一个二级树结构),缺点数据量不能太大
  • 情况二:分页查询(先分页查询顶级的,展示时再不分页查询子评论),可能存在交互的迟钝感
  • 情况三:xxxx(我也不知道了,总感觉上面的两种方案都不是最佳的, 我看了掘金返回的结构是顶级评论分页(20 条)+ 子评论随着顶级评论一起返回,万一子评论的量也很多呢?)
-- 情况一:查询所有评论

-- 1. 获取所有顶级评论的 ids
SELECT id from comment where article_id = '文章id' AND parent_id IS NULL;

-- 2. 获取所有评论
SELECT c.id,
       c.parent_id,
       c.user_id,
       u.name                   AS user_name,
       c.target_user_id,
       tu.name                  AS target_user_name,
       c.content,
       c.created_at
FROM comment c
LEFT JOIN user u  ON u.id  = c.user_id    -- 连接用户表,查询用户名称
LEFT JOIN user tu ON tu.id = c.target_user_id -- 连接用户表,查询用户名称
WHERE  c.article_id = '文章id'
  AND (c.parent_id IS NULL  -- 找出顶级评论(parent_id = NULL)和子评论(parent_id 在 ids 里面)
       OR
       c.parent_id IN ('ids'))
ORDER BY c.parent_id IS NULL DESC,
         c.created_at ASC;

通过代码进行组装成树

// 针对情况一:拿到所有数据之后,在代码中组装成一个树结构

function buildTree(list) {
 const topMap = new Map(); // 顶级评论容器
 const replyMap = new Map(); // 二级评论容器

 list.forEach((item) => {
   if (item.parent_id === null) {
     item.replies = []; // 创建一个空数组,用来存放子评论
     topMap.set(item.id, item);
   } else {
     const arr = replyMap.get(item.parent_id) || [];
     arr.push(item);
     replyMap.set(item.parent_id, arr);
   }
 });

 topMap.forEach((top) => {
   // 添加子评论
   top.replies = replyMap.get(top.id) || [];
 });

 const tree = Array.from(topMap.values()).sort(
   (a, b) => new Date(b.created_at) - new Date(a.created_at)
 );

 return tree;
}

-- 情况二: 先分页查询顶级的,展示时再不分页查询子评论

-- 1. 分页获取顶层评论
SELECT c.id,
       c.parent_id,
       c.user_id,
       u.name               AS user_name,
       c.target_user_id,
       tu.name              AS target_user_name,
       c.content,
       c.created_at
FROM comment c
LEFT JOIN user u  ON u.id  = c.user_id
LEFT JOIN user tu ON tu.id = c.target_user_id
WHERE c.article_id = '文章id' AND c.parent_id IS NULL
ORDER BY c.created_at DESC
LIMIT 20 OFFSET '页码 * 20';

-- 2. 获取所有子评论
SELECT c.id,
       c.parent_id,
       c.user_id,
       u.name               AS user_name,
       c.target_user_id,
       tu.name              AS target_user_name,
       c.content,
       c.created_at
FROM comment c
LEFT JOIN user u  ON u.id  = c.user_id
LEFT JOIN user tu ON tu.id = c.target_user_id
WHERE c.article_id = '文章id' AND c.parent_id  = '顶层评论id'
ORDER BY c.created_at ASC;

其他的情况,就先不考虑了。

最后

SQL 这东西,多写几遍就熟了。语法虽然多,但常用的就那些,上面这些算是入门。

前端平时接触不到,但想往全栈发展,SQL 是必须了解的。虽然实际开发用 ORM,但底层还是 SQL(就像用 Vue,JavaScript 还是要懂的)。

❌