昨天 Poppy 问我是不是应该学一些网页开发的东西, 我的回答是这样的:

今天花了点时间汇总了一些 MySQL 简单的命令.

======== 正文分割线 ========

有哪些常见的数据库:

  1. Oracle
  2. MySQL
  3. SQL Server
  4. MongoDB
  5. Redis
  6. Hadoop

以下所有命令基于数据库 kis_blog 编写, 可以通过 Navicat 或者 DataGrip 亲自体验一下.

注:

  1. poppython.com 的数据库采用的是阿里云的 RDS 云数据库(MySQL);
  2. Navicat 和 DataGrip 是 MySQL 的 GUI 客户端;
  3. MySQL 搭建完毕以后可以创建多个数据库, 每个数据库可以创建多张数据表;
  4. 警号 # 开头的是注释;
  5. 分号 ; 结尾代表语句结束;
  6. 本文由简入繁的顺序编写, 建议从头阅读, 不要跳过;

blogs 表结构

kis_blog 有 5 张表, 分别是:

  1. users: 用户表
  2. categories: 分类表
  3. comments: 评论表
  4. tags: 标签表
  5. blogs: 文章表

MySQL 的数据表类似于 Excel, 下边是表 blogs 的 Excel 描述:

上图中蓝色的文字是列, 橙色的文字是列的注释, 黑色的文字是数据, 每一行代表一条数据.

以下章节按照 crud boy 的顺序编写:

CURD 代表 CREATE(创建)、RETRIEVE(检索)、UPDATE(更新)、DELETE(删除) 几个单词的首字母

查询数据使用 SELECT 关键字:

# 查 blogs 的所有列
SELECT * FROM blogs;

# MySQL 的关键字不区分大小写, 下面两条语句是等价的(建议使用大写):
SELECT * FROM blogs;
select * from blogs;

# 查 blogs 的单个列
SELECT id FROM blogs;

# 查 blogs 的多个列
SELECT id, title FROM blogs;

# 去重
SELECT DISTINCT category_id FROM blogs;

排序使用 ORDER BY 关键字:

# 按照 id 排序(默认升序)
SELECT * FROM blogs ORDER BY id;

# 按照 id 升序
SELECT * FROM blogs ORDER BY id ASC;

# 按照 id 降序
SELECT * FROM blogs ORDER BY id DESC;

# 按照 id, category_id 多个列排序(默认升序)
SELECT * FROM blogs ORDER BY user_id, category_id;

# 按照 id, category_id 多个列排序(指定排序方向)
# ASC 和 DESC 只能影响到其前面的列
SELECT * FROM blogs ORDER BY user_id ASC, category_id DESC;

过滤数据使用 WHERE 关键字:

# 只看 poppy(你的 id 是 2 ) 发布的文章
SELECT * FROM blogs WHERE user_id = 2;

# 只看置顶状态的文章
# status 列存储了一个字符串有以下可选值: DRAFT(草稿)、已发布(PUBLISHED)、TOP(置顶)、HIDE(隐藏)
SELECT * FROM blogs WHERE status = 'PUBLISHED';

# 只看 id 大于 10 的文章
SELECT * FROM blogs WHERE id > 10;

# 只看 id 小于等于 2 的文章
SELECT * FROM blogs WHERE id <= 2;

# 只看 id 位于 2 和 10 之间的文章 (包括 2 也包括 10)
SELECT * FROM blogs WHERE id BETWEEN 2 AND 10;

# 只看 id 不等于 6 的文章, 以下两条语句是等价的
SELECT * FROM blogs WHERE id <> 6;
SELECT * FROM blogs WHERE id != 6;

过滤数据除了 WHERE 关键字之外, 还有 ANDORINNOT 等子句可以使用:

# 查看 user_id 为 1 并且 category_id 为 2 的文章
SELECT * FROM blogs WHERE user_id = 1 AND category_id = 2;

# 查看 category_id 为 2 或者 category_id 为 3 的文章
SELECT * FROM blogs WHERE category_id = 2 OR category_id = 3;

# 猜猜这行什么意思?
SELECT * FROM blogs WHERE (category_id = 2 OR category_id = 3) AND user_id = 1;

# 查看 category_id 为 2 或者 category_id 为 3 的文章
SELECT * FROM blogs WHERE category_id IN (2, 3);

# 只看 summary 为空的文章
SELECT * FROM blogs WHERE summary IS NULL;

# 只看 summary 不为空的文章
SELECT * FROM blogs WHERE summary IS NOT NULL;

在一些后台系统中, 经常需要分页查询数据, 假如前端传递 /api/blogs?current=2&size=10 表示页码为 2, 页长为 10, 后端需要做以下处理:

# 查询第 2*10 条数据之后的 10 条数据, 也就是第 21 条到第 30 条
SELECT * FROM blogs LIMIT 2 * 10, 10;

# 查询所有数据作为 count, 后端需要返回 current, size, count 三个字段给前端
# 用于发起下一次分页查询
SELECT COUNT(id) FROM blogs;

以上查询数据的方式都是通过精确匹配, MySQL 同样支持模糊匹配.

通配查询使用 LIKE 关键字, 最常用的通配符的 %, % 表示任意字符出现任意次数.

# 查看 title 中以 Git 开头的文章
SELECT * FROM blogs WHERE title LIKE 'Git%';

# 查看 title 中以 Git 结尾的文章
SELECT * FROM blogs WHERE title LIKE '%Git';

# 查看 title 包含 Git
SELECT * FROM blogs WHERE title LIKE '%Git%';

通过以上查询方式得到的数据就像 excel 里的行, 还可以做简单的拼接、计算:

# 给标题加上书名号
# CONCAT 函数表示将参数拼接在一起
# AS 关键字表示给拼接后的列取个新名字叫 full_title
SELECT CONCAT('《', title, '》') AS full_title FROM blogs;

# 去掉 title 的首空格
SELECT LTRIM(title) FROM blogs;

# 去掉 title 的尾空格
SELECT LTRIM(title) FROM blogs;

# 去掉 title 的首尾空格
SELECT TRIM(title) FROM blogs;

# 计算点赞率 (点赞率 = 点赞数 / 浏览数)
SELECT like_count / read_count AS like_rate FROM blogs;

MySQL 还支持以下函数:

  1. 文本处理类:
  • LEFT(): 返回字符串左边的字符
  • LENGTH(): 返回字符串的长度
  • LOWER(): 将字符串转换为小写
  • LTRIM(): 去掉字符串左边的空格
  • RIGHT(): 返回字符串右边的字符
  • RTRIM(): 去掉字符串右边的空格
  • SOUNDEX(): 返回字符串的SOUNDEX值
  • UPPER(): 将字符串转换为大写
  1. 日期处理类:
  • YEAR(): 获取年
  • MOUNTH(): 获取月
  • DAY(): 获取日
  • NOW(): 获取当前日期
  • 还有其他很多函数...
  1. 数值处理类:
  • AVT(): 返回某列的平均值
  • COUNT(): 返回某列的行数
  • MAX(): 返回某列的最大值
  • MIN(): 返回某列的最小值
  • SUM(): 返回某列值之和
  • ABS(): 返回一个数的绝对值
  • COS(): 返回一个角度的余弦
  • EXP(): 返回一个数的指数值
  • PI(): 返回圆周率
  • SIN(): 返回一个角度的正弦
  • SQRT(): 返回一个数的平方根
  • TAN(): 返回一个角度的正切
  • 还有其他很多函数...

除了上述的各种单表查询方法外, MySQL 还支持各种姿势的联表查询 (SQL 最强大功能之一):

  1. INNER JOIN(内联接)
  2. OUTER JOIN(外联接)
  • LEFT JOIN(左联接)
  • LEFT OUTER JOIN(左外联接)
  • RIGHT JOIN(右联接)
  • RIGHT OUTER JOIN(右外联接)
  • FULL JOIN(全连接)
  • FULL OUTER JOIN(全外联接)
  1. 交叉联接

这部分内容略多, 可以参考 Mysql表连接查询.

创建数据库:

# 创建数据库
# 这行语句比较复杂, 可以简单这样理解:
# 1. 如果 kis_blog 已存在的话则跳过
# 2. 如果 kis_blog 不存在则创建, 创建时:
#   + 指定字符集为 utf8
#   + 指定字符序为 utf8_general_ci
CREATE DATABASE IF NOT EXISTS kis_blog DEFAULT CHARSET utf8 COLLATE utf8_general_ci;

创建数据表 blogs:

CREATE TABLE IF NOT EXISTS `blogs`(
   `id`         INT UNSIGNED    AUTO_INCREMENT,
   `title`      VARCHAR(100)    NOT NULL,
   `summary`    VARCHAR(200),
   `content `   VARCHAR(10000),
   PRIMARY KEY  ( `id` )
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

创建数据表的命令比较复杂, 以上仅定义了 id、title、summary、content 四个列.

数据表的每个列都有一个类型, MySQL 提供了很多可选的选项, 在实际开发中, 程序员需要按照真实的业务情况选择合适的数据类型, 简单来说分为以下几种:

  1. 字符串:
  • CHAR: 定长字符串
  • VARCHAR: 变长字符串
  • TINYTEXT: 短文本, 最多存储 255 个字符 (2^8-1)
  • TEXT: 长文本, 最多存储 65535 个字符 (2^16-1)
  • MEDIUMTEXT: 中等长度文本, 最多存储 16777215 个字符 (2^24-1)
  • LONGTEXT: 极大文本, 最多存储 4294967295 个字符 (2^32-1)
  • 其他二进制类型...
  1. 数字
  • TINYINT: 小整数, 0 - 255 之间
  • SMALLINT: 大整数, 0 - 65535 之间
  • MEDIUMINT: 大整数, 0 - 16777215 之间
  • INT: 大整数, 0 - 4294967295 之间
  • BIGINT: 极大整数
  • FLOAT: 单精度浮点数
  • DOUBLE: 双精度浮点数
  • DECIMAL: 小数
  1. 日期
  • DATE: 日期, 例如: 2013-07-20 (没错, 我们的纪念日)
  • TIME: 时间, 例如: 05:20:00
  • DATETIME: 混合 DATE 和 TIME, 例如: 2013-07-20 05:20:00
  • YEAR: 年, 例如: 2013
  • TIMESTAMP: 时间戳

除了数据类型外, 定义数据表还需要一些约束字段:

  1. AUTO_INCREMENT: 表明这个列是自增的, 常用于 id, 会从 1 开始, 每加一条数据就自动加 1 作为新增数据的id;
  2. NOT NULL: 表明这个列是必填的, 不能为空
  3. UNIQE: 表明这个列是唯一的
  4. PRIMARY KEY: 表明这个列是主键, 主键很重要

关于主键:

  1. 主键是一条记录的唯一表示, 就像公民的身份证号码;
  2. 主键字段必须唯一并且非空;
  3. 一个表只能有一个主键;
  4. 主键可以包含一个字段或多个字段, 多个字段时称为 "联合主键";

关于外键:

  1. 外键用来表示两个表之间的关系;
  2. 一个表可以有多个外键;

poppython.com 的数据库里有以下场景:

有用户表 users 用来存储用户的信息, 另有文章表 blogs, 一个用户可以有多个文章, 这种情况下, usersorders 是一对多的关系.

此时可以将 users.id 字段作为 blogs 表的主键, users.id 就是 users 表的外键, users 通常被称为主表, blogs 被称为子表.

创建数据使用 INSERT 关键字:

# 创建一篇示例文章
INSERT INTO blogs (
    user_id,
    title,
    category_id,
    content,
    pathname,
    STATUS 
)
VALUES (
    2,
    '示例标题',
    7,
    '示例文章',
    'test-insert',
    'DRAFT' 
);

在实际的开发过程中, 数据表设计比较复杂, 需要尽可能的满足当前甚至未来的业务, 经常需要多个程序员共同商讨.

修改数据相对来说比较简单, 修改数据表使用 ALTER 关键字, 修改数据使用 UPDATE 关键字:

# 为 blogs 添加列
ALTER TABLE blogs ADD test_column CHAR(20);

# 删除 blogs 的 test_column 列
ALTER TABLE blogs DROP COLUMN test_column;

# 将 id 为 37 的文章状态改为 "置顶"
UPDATE status = 'TOP' FROM blogs WHERE id = 37;

# 将所有文章的状态改为 "已发布"
UPDATE status = 'PUBLISHED' FROM blogs WHERE id > 0;

删数据也很简单, 删除数据库和数据表使用 DROP 关键字, 删除数据使用 DELETE 关键字:

# 删除数据库
DROP DATABASE db_name;

# 删除数据表
DROP TABLE table_name;

# 删除 id 为 42 的文章
DELETE FROM blogs WHERE id = 42;

其他

事务

在实际的开发中经常需要批量执行一些 SQL 语句, 并且这批 SQL 要么全部成功, 要么不执行, 这种情况下需要使用事务.

START TRANSACTION
# 批量的 SQL 语句...
COMMIT TRANSACTION

存储过程

在我的理解中, 存储过程相当于封装了 SQL 的函数.

TODO: 本王这部分的了解太浅, 暂时略过.

游标

TODO: 本王这部分的了解太浅, 暂时略过.

撤销

SQL 的 ROLLBACK 命令用来撤销 SQL 语句.

DELETE FROM table_name;
ROLLBACK;

索引

索引用来排序数据以加快搜索的速度, 索引分为:

  1. 普通索引
  2. 唯一索引
  3. 主键索引
# 创建普通索引
ALTER TABLE blogs ADD INDEX title;

# 创建唯一索引
ALTER TABLE blogs ADD UNIQUE pathname;

# 创建主键索引
ALTER TABLE blogs ADD PRIMARY KEY(id);

查看索引:

SHOW INDEX FROM blogs;

删除索引:

DROP INDEX title ON blogs;

ORM

从上边的原始 SQL 代码可以看到操作数据表是很复杂的, 在真实的业务中经常需要在 SQL 中需要注入变量后拼装出冗长的 SQL 语句.

ORM 解决了以上问题, ORM 是 Object Relational Mapping (对象关系映射)的简称, 可以简单理解成操作 SQL 的第三方工具包.

poppython.com 中使用了基于 Node 的 Sequelize 作为 ORM, 下面罗列了一些代码片段.

下面的代码用来创建文章, 在前端使用 POST 请求访问 /api/v1/blog 接口时, 会携带参数走到以下方法中:

async create() {
  const { ctx, service } = this;
  const { title, pathname, summary, tagIds, categoryId, content, status } = ctx.request.body;
  const userId = ctx.helper.getLoggedIdByToken(ctx.cookies.get("tk"));
  try {
    const existed = await service.blog.findOne({ where: { pathname } });
    if (existed) {
      ctx.body = { success: false, message: ctx.__("ExistMsg", pathname) };
      return;
    }
    const created = await service.blog.create({
      title,
      pathname,
      summary,
      userId,
      categoryId,
      content,
      status,
    });
    const tags = await service.tag.findAll({ where: { id: tagIds } });
    await created.setTags(tags);
    ctx.body = { success: true, message: ctx.__("SuccessSmg"), data: created.id };
  } catch (e) {
    ctx.logger.error("Error while BlogController.create, stack: ", e);
    ctx.body = { success: false, message: ctx.__("InnerErrorMsg") };
  }
}

上边的代码只有常规的对象, 完全看不到原始 SQL 的影子.

MySQL 的优化

在庞大的业务数据下, MySQL 经常需要各种各样的优化方式, 比如:

  1. 使用缓存数据库
  2. 使用恰当的数据类型
  3. 创建高性能的索引
  4. 优化查询语句
  5. 读写分离
  6. 分布式数据库
  7. ...
  8. 这方面我是半桶水, 就不晃荡了...

开发一个创建文章的功能的流程

第一步: 在后端定义了一些路由地址, 比如:

// 当用于访问 /admin/write.html 时, 返回一个静态的页面
router.get("/admin/write.html", controller.view.admin.renderWrite);

// 当收到 /api/v1/blog 的POST 请求时, 执行 `controller.api.blog` 控制器的 `create` 方法
router.post("/api/v1/blog", controller.api.blog.create);

第二步: 前端创建 write.html, 下面是伪代码:

<form id="js_writeForm" action="#">
    <input type="text" name="title" placeholder="请输入标题"  />
    <textarea name="content" placeholder="请输入正文"></textarea>
    <button type="submit">提交</button>
</form>

<script>
    $(document).ready(function () {
        var $writeForm = $("#js_writeForm");
        
        // 监听表单的提交事件
        $writeForm.on("submit", function (event) {
            // 阻止默认的提交事件
            evt.preventDefault();
            
            // 获取表单数据
            var writeFormData = $writeForm.serializeJSON();
            
            // 发起网络请求
            $.ajax({
                type: "POST",
                url: "/api/v1/blog",
                // 在请求结束后执行一些逻辑
                success: function (res) {
                    if (res.success) {
                        alert("操作成功");
                    } else {
                        alert("操作失败");
                    }
                }
            });
        });
    });
</script>

第三步: 后端处理前端提交的 title, content 等数据, 将数据存入数据表, 并将存储结果返回给前端;

第四步: 前端在收到后端的返回结果后, 执行对应的消息提示;

第五步: END;

======== 本文结束 ========