概述

使用 explain 输出 SELECT 语句执行的详细信息,包括以下信息:

  • 表的加载顺序
  • sql 的查询类型
  • 可能用到哪些索引,实际上用到哪些索引
  • 读取的行数

Explain 执行计划包含字段信息如下:分别是 id、select_type、table、partitions、type、possible_keys、key、key_len、ref、rows、filtered、Extra 12个字段。

通过explain extended + show warnings可以在原本explain的基础上额外提供一些查询优化的信息,得到优化以后的可能的查询语句(不一定是最终优化的结果)。

测试环境:

CREATE TABLE `blog` (
  `blog_id` int NOT NULL AUTO_INCREMENT COMMENT '唯一博文id--主键',
  `blog_title` varchar(255) NOT NULL COMMENT '博文标题',
  `blog_body` text NOT NULL COMMENT '博文内容',
  `blog_time` datetime NOT NULL COMMENT '博文发布时间',
  `update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  `blog_state` int NOT NULL COMMENT '博文状态--0 删除 1正常',
  `user_id` int NOT NULL COMMENT '用户id',
  PRIMARY KEY (`blog_id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8

CREATE TABLE `user` (
  `user_id` int NOT NULL AUTO_INCREMENT COMMENT '用户唯一id--主键',
  `user_name` varchar(30) NOT NULL COMMENT '用户名--不能重复',
  `user_password` varchar(255) NOT NULL COMMENT '用户密码',
  PRIMARY KEY (`user_id`),
  KEY `name` (`user_name`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8

CREATE TABLE `discuss` (
  `discuss_id` int NOT NULL AUTO_INCREMENT COMMENT '评论唯一id',
  `discuss_body` varchar(255) NOT NULL COMMENT '评论内容',
  `discuss_time` datetime NOT NULL COMMENT '评论时间',
  `user_id` int NOT NULL COMMENT '用户id',
  `blog_id` int NOT NULL COMMENT '博文id',
  PRIMARY KEY (`discuss_id`)
) ENGINE=InnoDB AUTO_INCREMENT=61 DEFAULT CHARSET=utf8

id

表示查询中执行select子句或者操作表的顺序,id的值越大,代表优先级越高,越先执行

explain select discuss_body 
from discuss 
where blog_id = (
    select blog_id from blog where user_id = (
        select user_id from user where user_name = 'admin'));

三个表依次嵌套,发现最里层的子查询 id最大,最先执行。

select_type

表示 select 查询的类型,主要是用于区分各种复杂的查询,例如:普通查询、联合查询、子查询等。

  • SIMPLE:表示最简单的 select 查询语句,在查询中不包含子查询或者交并差集等操作。
  • PRIMARY:查询中最外层的SELECT(存在子查询的外层的表操作为PRIMARY)。
  • SUBQUERY:子查询中首个SELECT。
  • DERIVED:被驱动的SELECT子查询(子查询位于FROM子句)。
  • UNION:在SELECT之后使用了UNION

table

查询的表名,并不一定是真实存在的表,有别名显示别名,也可能为临时表。当from子句中有子查询时,table列是<derivenN>的格式,表示当前查询依赖 id为N的查询,会先执行 id为N的查询。

partitions

查询时匹配到的分区信息,对于非分区表值为NULL,当查询的是分区表时,partitions显示分区表命中的分区情况。

type

查询使用了何种类型,它在 SQL优化中是一个非常重要的指标

访问效率:const > eq_ref > ref > range > index > ALL

system

当表仅有一行记录时(系统表),数据量很少,往往不需要进行磁盘IO,速度非常快。比如,Mysql系统表proxies_priv在Mysql服务启动时候已经加载在内存中,对这个表进行查询不需要进行磁盘 IO。

const

单表操作的时候,查询使用了主键或者唯一索引。

eq_ref

多表关联查询的时候,主键和唯一索引作为关联条件。如下图的sql,对于user表(外循环)的每一行,user_role表(内循环)只有一行满足join条件,只要查找到这行记录,就会跳出内循环,继续外循环的下一轮查询。

ref

查找条件列使用了索引而且不为主键和唯一索引。虽然使用了索引,但该索引列的值并不唯一,这样即使使用索引查找到了第一条数据,仍然不能停止,要在目标值附近进行小范围扫描。但它的好处是不需要扫全表,因为索引是有序的,即便有重复值,也是在一个非常小的范围内做扫描。

ref_or_null

类似 ref,会额外搜索包含NULL值的行

index_merge

使用了索引合并优化方法,查询使用了两个以上的索引。新建comment表,id为主键,value_id为非唯一索引,执行explain select content from comment where value_id = 1181000 and id > 1000;,执行结果显示查询同时使用了id和value_id索引,type列的值为index_merge。

range

有范围的索引扫描,相对于index的全索引扫描,它有范围限制,因此要优于index。像between、and、>、<、in和or都是范围索引扫描。

index

index包括select索引列,order by主键两种情况。

order by主键。这种情况会按照索引顺序全表扫描数据,拿到的数据是按照主键排好序的,不需要额外进行排序。

select索引列。type为index,而且extra字段为using index,也称这种情况为索引覆盖。所需要取的数据都在索引列,无需回表查询。

all

全表扫描,查询没有用到索引,性能最差。

possible_keys

此次查询中可能选用的索引。但这个索引并不定一会是最终查询数据时所被用到的索引。

key

此次查询中确切使用到的索引

ref

ref 列显示使用哪个列或常数与key一起从表中选择数据行。常见的值有const、func、NULL、具体字段名。当 key 列为 NULL,即不使用索引时。如果值是func,则使用的值是某个函数的结果。

以下SQL的执行计划ref为const,因为使用了组合索引(user_id, blog_id),where user_id = 13中13为常量

mysql> explain select blog_id from user_like where user_id = 13;
+----+-------------+-----------+------------+------+---------------+------+---------+-------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | user_like | NULL       | ref  | ul1,ul2       | ul1  | 4       | const |    2 |   100.00 | Using index |
+----+-------------+-----------+------------+------+---------------+------+---------+-------+------+----------+-------------+

而下面这个SQL的执行计划ref值为NULL,因为key为NULL,查询没有用到索引。

mysql> explain select user_id from user_like where status = 1;
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | user_like | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    6 |    16.67 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+

rows

估算要找到所需的记录,需要读取的行数。评估SQL 性能的一个比较重要的数据,mysql需要扫描的行数,很直观的显示 SQL 性能的好坏,一般情况下 rows 值越小越好

filtered

存储引擎返回的数据在经过过滤后,剩下满足条件的记录数量的比例

extra

表示额外的信息说明。为了方便测试,这里新建两张表。

CREATE TABLE `t_order` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int DEFAULT NULL,
  `order_id` int DEFAULT NULL,
  `order_status` tinyint DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_userid_order_id_createdate` (`user_id`,`order_id`,`create_date`)
) ENGINE=InnoDB AUTO_INCREMENT=99 DEFAULT CHARSET=utf8

CREATE TABLE `t_orderdetail` (
  `id` int NOT NULL AUTO_INCREMENT,
  `order_id` int DEFAULT NULL,
  `product_name` varchar(100) DEFAULT NULL,
  `cnt` int DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_orderid_productname` (`order_id`,`product_name`)
) ENGINE=InnoDB AUTO_INCREMENT=152 DEFAULT CHARSET=utf8

using where

表示在查询过程中使用了WHERE条件进行数据过滤。当一 个查询中包含WHERE条件时,MySQL会根据该条件过滤出满足条件的数据行,然后再进行后续的操作。这个过程 就被称为"Using Where”。

表示查询的列未被索引覆盖,,且where筛选条件是索引列前导列的一个范围,或者是索引列的非前导列,或者是非索引列。对存储引擎返回的结果进行过滤(Post-filter,后过滤),一般发生在MySQL服务器,而不是存储引擎层,因此需要回表查询数据。

using index

查询的列被索引覆盖,并且where筛选条件符合最左前缀原则,通过索引查找就能直接找到符合条件的数据,
不需要回表
查询数据。

Using where&Using index

查询的列被索引覆盖,但无法通过索引查找找到符合条件的数据,不过可以通过索引扫描找到符合条件的数据,也不需要回表查询数据。

包括两种情况(组合索引为(user_id, orde)):

where筛选条件不符合最左前缀原则

where筛选条件是索引列前导列的一个范围

null

查询的列未被索引覆盖,并且where筛选条件是索引的前导列,也就是用到了索引,但是部分字段未被索引覆盖,必须回表查询这些字段,Extra中为NULL。

using index condition

索引下推(index condition pushdown,ICP),先使用where条件过滤索引,过滤完索引后找到所有符合索引条件的数据行,随后用 WHERE 子句中的其他条件去过滤这些数据行。

对于联合索引(a, b),在执行 select * from table where a > 1 and b = 2 语句的时候,只有 a 字段能用到索引,那在联合索引的 B+Tree 找到第一个满足条件的主键值(ID 为 2)后,还需要判断其他条件是否满足(看 b 是否等于 2),那是在联合索引里判断?还是回主键索引去判断呢?
MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

不使用ICP的情况(set optimizer_switch='index_condition_pushdown=off'),如下图,在步骤4中,没有使用where条件过滤索引:

使用ICP的情况(set optimizer_switch='index_condition_pushdown=on'):

下面的例子使用了ICP:

explain select user_id, order_id, order_status from t_order where user_id > 1 and user_id < 5\G;

关掉ICP之后(set optimizer_switch='index_condition_pushdown=off'),可以看到extra列为using where,不会使用索引下推。

using temporary

使用了临时表保存中间结果,常见于 order by 和 group by 中。典型的,当group by和order by同时存在,且作用于不同的字段时,就会建立临时表,以便计算出最终的结果集

filesort

文件排序。表示无法利用索引完成排序操作,以下情况会导致filesort:

  • order by 的字段不是索引字段
  • select 查询字段不全是索引字段
  • select 查询字段都是索引字段,但是 order by 字段和索引字段的顺序不一致

using join buffer

Block Nested Loop,需要进行嵌套循环计算。两个关联表join,关联字段均未建立索引,就会出现这种情况。比如内层和外层的type均为ALL,rows均为4,需要循环进行
4*4
次计算。常见的优化方案是,在关联字段上添加索引,避免每次嵌套循环计算。

面试题专栏

Java面试题专栏
已上线,欢迎访问。

  • 如果你不知道简历怎么写,简历项目不知道怎么包装;
  • 如果简历中有些内容你不知道该不该写上去;
  • 如果有些综合性问题你不知道怎么答;

那么可以私信我,我会尽我所能帮助你。

如果你做过个人博客网站,那么一定对静态网站生成器不陌生。无论是 Ruby 语言的 Jekyll、Go 语言的 Hugo、还是基于 React 的 Gatsby,这些工具都有庞大的用户群体。对于喜欢的人来说,它们是无可替代的神器,而对于不喜欢的人,则可能难以“下咽”。正如俗话所说,“萝卜青菜,各有所爱”,没有最好用的工具,只有最适合自己的。

比如,上周热门的开源项目 zola,它的诞生就是因为作者不喜欢 Hugo 的模板引擎,同时为了追求更简洁的使用体验。他选择用 Rust 开发了 zola 这款静态网站生成器,提供独立的可执行文件和更简单易用的模版语言。同样备受关注的还有 dockur/macos,它可以让用户在 Docker 中体验 macOS 系统。用于备份 QQ 空间说说的 GetQzonehistory,虽看似简单,但凭借切中用户痛点和开箱即用的特点,让它在短时间内获得上千 Star 的关注。

  • 本文目录
    • 1. 热门开源项目
      • 1.1 Rust 的静态网站生成器:zola
      • 1.2 Linux 平台的 GDB 图形化增强工具:Seer
      • 1.3 在 Docker 中体验 macOS 系统:macos
      • 1.4 QQ 空间说说备份工具:GetQzonehistory
      • 1.5 利用家用设备打造低成本的 AI 集群:exo
    • 2. HelloGitHub 热评
      • 2.1 开源的网络钓鱼平台:Gophish
      • 2.2 Rust 驱动的 HTTP 压测工具:oha
    • 3. 结尾

1. 热门开源项目

1.1 Rust 的静态网站生成器:zola

主语言:Rust

Star:14k

周增长:200

该项目是用 Rust 语言编写的静态网站生成器,其方便的可执行文件使得安装过程简单快捷。它采用了更易于使用的 Tera 模版引擎,并提供了一体化的静态网站解决方案,内置全面的功能模块,包括内容管理、语法高亮、检查外部链接、快速预览、搜索和多语言等,适用于快速搭建技术博客、产品文档和公司官网等网站。

zola init my_site
zola check
zola serve
zola build

GitHub 地址→
github.com/getzola/zola

1.2 Linux 平台的 GDB 图形化增强工具:Seer

主语言:C++

Star:2.8k

周增长:600

这是一款专为 Linux 系统设计的工具,提供了 GDB 的图形化用户操作界面。它为 GNU 调试器 GDB 提供了友好的图形化界面,包括代码管理、变量/寄存器信息、断点管理等多个功能视图,支持变量跟踪、回放指令、内存、结构体和数组可视化等功能。

GitHub 地址→
github.com/epasveer/seer

1.3 在 Docker 中体验 macOS 系统:macos

主语言:Python

Star:8.3k

周增长:7k

该项目允许用户在 Docker 容器中运行 macOS 系统,实现在非苹果硬件上体验 macOS。它基于 Docker 和 KVM 的虚拟化技术,简化了 macOS 的安装过程。用户可以通过浏览器使用 macOS 系统,支持调整 CPU、内存和硬盘大小,并兼容从 macOS 11 到 macOS 15 的多个版本,适合在 Linux 和 Windows 环境中测试 macOS 应用。

GitHub 地址→
github.com/dockur/macos

1.4 QQ 空间说说备份工具:GetQzonehistory

主语言:Python

Star:5.7k

周增长:2.6k

这是一个用于获取个人 QQ 空间历史说说的工具。它通过模拟登录 QQ 空间,可以自动获取个人账号下所有发布过的说说,并导出成 Excel 文件。用户只需执行几个简单的步骤,即可轻松备份个人的所有历史说说。

GitHub 地址→
github.com/LibraHp/GetQzonehistory

1.5 利用家用设备打造低成本的 AI 集群:exo

主语言:Python

Star:14k

周增长:3k

该项目能够利用日常家用设备(如手机、笔记本电脑、台式机等)来搭建家庭 AI 集群。它通过整合现有的设备,无需昂贵硬件,即可构建一个低成本、可扩展的 GPU 计算集群,支持动态模型分区、自动发现设备、ChatGPT API、P2P 连接和多种推理引擎。

GitHub 地址→
github.com/exo-explore/exo

2. HelloGitHub 热评

在此章节中,我们将为大家介绍本周 HelloGitHub 网站上的热门开源项目,我们不仅希望您能从中收获开源神器和编程知识,更渴望“听”到您的声音。欢迎您与我们分享使用这些
开源项目的亲身体验和评价
,用最真实反馈为开源项目的作者注入动力。

2.1 开源的网络钓鱼平台:Gophish

主语言:Go

该项目提供了一个开箱即用的网络钓鱼平台,可用于模拟钓鱼攻击。它拥有友好的 Web 管理后台,支持邮件模板、批量发送邮件、网站克隆和数据可视化,适用于企业安全培训和渗透测试等场景。

项目详情→
hellogithub.com/repository/6f6ec956985847f6a133ee5daafae964

2.2 Rust 驱动的 HTTP 压测工具:oha

主语言:Rust

这是一个用 Rust 开发的 HTTP 请求压测工具,它操作简单、带 TUI 动画界面,支持生成请求延迟、吞吐量等指标的报告,以及动态 URL 和更灵活的请求间隔(burst-delay)等功能。

项目详情→
hellogithub.com/repository/98b46ea0d7d84f4c944d0a35a9d2d140

3. 结尾

以上就是本期「GitHub 热点速览」的全部内容,希望你能够在这里找到自己感兴趣的开源项目,如果你有其他好玩、有趣的 GitHub 开源项目想要分享,欢迎来
HelloGitHub
与我们交流和讨论。

往期回顾

交互类组件

Web
应用程序中至关重要,它们允许用户与应用进行实时互动,能够显著提升用户体验。

用户不再只是被动地接收信息,而是可以主动地输入数据、做出选择或触发事件,从而更加深入地参与到应用中来。

此外,对于某些复杂的任务或操作,
交互类组件
可以将其分解成一系列简单的步骤或选择,从而降低用户的认知负担和学习成本

。这使得用户能够更轻松地完成这些任务,提高应用的易用性。

Streamlit
中交互类组件有很多,本篇介绍其中最常用的几种,这也是
Web
页面中最常见到的几种组件。

  • st.text_input
    :允许用户输入文本,用于收集用户输入的字符串信息。
  • st.button
    :提供一个可点击的按钮,用户点击后触发特定操作或事件。
  • st.selectbox
    :显示下拉列表,让用户从预设选项中选择一个。
  • st.multiselect
    :提供下拉多选功能,允许用户从预设选项中选择多个。
  • st.radio
    :显示单选按钮组,让用户从多个选项中选择一个。
  • st.checkbox
    :提供复选框,让用户选择或取消选择特定选项。

1. 组件概述

1.1. st.text_input

用于输入普通文本或者密码,类似于HTML中的
<input type="text">

核心的参数有:

名称 类型 说明
label str 输入框前面的标签
key str 唯一标识此输入框的键,可用于在回调中引用
value str 输入框的初始值
type str 输入类型,可以是"default"(默认)或者"password"(密码)

1.2. st.button

提供一个按钮用来出发特定事件,类似于HTML中的
<button>

核心的参数有:

名称 类型 说明
label str 按钮上的文字
key str 唯一标识此按钮的键
help str 按钮旁边的帮助文本

1.3. st.selectbox

下拉选择框,类似于HTML中的
<select>

核心的参数有:

名称 类型 说明
label str 下拉框前面的标签
key str 唯一标识此下拉框的键
options list 下拉框中的选项列表
index int 初始选中项的索引

1.4. st.multiselect

可以多选的下拉选择框,类似于HTML中的
<select multiple>

核心的参数有:

名称 类型 说明
label str 多选框前面的标签
key str 唯一标识此多选框的键
options list 多选框中的选项列表
default list 默认选中的选项列表

1.5. st.radio

单选按钮组,类似于HTML中的
<input type="radio">

核心的参数有:

名称 类型 说明
label str 单选按钮组前面的标签
key str 唯一标识此单选按钮组的键
options list 单选按钮组中的选项列表
index int 初始选中项的索引

1.6. st.checkbox

复选框,类似于HTML中的
<input type="checkbox">

核心的参数有:

名称 类型 说明
label str 复选框旁边的标签
key str 唯一标识此复选框的键
value bool 复选框的初始状态(选中或未选中)

2. 组件使用示例

下面通过从实际场景中简化而来的示例,来看看如何使用
Streamlit
的交互类组件。

2.1. "用户偏好调查"示例

这个示例模拟了一个真实的用户调查场景,

通过
Streamlit
的交互式组件,用户可以轻松地输入和选择信息,并提交给应用进行处理和显示。

import streamlit as st

# 标题
st.title("用户偏好调查")

# 文本输入框:收集用户名
username = st.text_input("请输入您的姓名:")

# 下拉单选框:选择性别
gender = st.selectbox("请选择您的性别:", ["男", "女", "其他"])

# 下拉多选框:选择兴趣爱好
hobbies = st.multiselect(
    "请选择您的兴趣爱好:", ["阅读", "运动", "旅行", "音乐", "电影"]
)

# 单选按钮组:选择喜欢的颜色
favorite_color = st.radio("请选择您喜欢的颜色:", ["红色", "蓝色", "绿色", "黄色"])

# 复选框:是否同意接收推送
accept_push = st.checkbox("您是否同意接收推送消息?")

# 按钮:提交调查
if st.button("提交调查"):
    # 收集所有输入信息并显示
    user_info = {
        "姓名": username,
        "性别": gender,
        "兴趣爱好": ", ".join(hobbies),
        "喜欢的颜色": favorite_color,
        "是否同意接收推送": "是" if accept_push else "否",
    }

    st.subheader("您的调查信息如下:")
    st.write(user_info)

2.2. "数据分析项目仪表板"示例

这个示例模拟了一个数据分析项目的仪表板,

通过
Streamlit
的交互式组件,用户可以轻松地与数据进行交互,选择他们感兴趣的分析方式,并查看和下载分析结果。

import streamlit as st
import pandas as pd
import numpy as np

# 假设数据集
data = pd.DataFrame(
    {
        "日期": pd.date_range(start="2023-01-01", periods=100, freq="D"),
        "类别": np.random.choice(["A", "B", "C"], 100),
        "销售额": np.random.randint(100, 1000, 100),
        "利润": np.random.randint(10, 100, 100),
    }
)

# 标题
st.title("数据分析项目仪表板")

# 文本输入框:输入项目名称
project_name = st.text_input("请输入项目名称:")

# 下拉单选框:选择分析类别
analysis_category = st.selectbox("请选择分析类别:", data["类别"].unique())

# 下拉多选框:选择显示的列
display_columns = st.multiselect("请选择要显示的列:", data.columns)
selected_data = data[display_columns]

# 单选按钮组:选择汇总方式
agg = st.radio("请选择汇总方式:", ["总和", "平均值", "最大值", "最小值"])
agg_dict = {
    "总和": "sum",
    "平均值": "mean",
    "最大值": "max",
    "最小值": "min",
}

# 复选框:是否按类别汇总
group_by_category = st.checkbox("是否按类别汇总?")

# 按钮:执行分析
if st.button("执行分析"):
    # 根据用户选择进行分析
    if group_by_category:
        grouped_data = (
            selected_data.groupby("类别")
            .agg({col: agg_dict[agg] for col in selected_data.columns if col != "类别"})
            .reset_index()
        )
    else:
        grouped_data = (
            selected_data.agg({col: agg_dict[agg] for col in selected_data.columns})
            .to_frame()
            .T
        )

    # 显示分析结果
    st.subheader("分析结果:")
    st.dataframe(grouped_data)

3. 总结

总之,这些交互式组件使得用户可以通过文本输入、选择、勾选等方式与应用进行互动,从而根据用户需求动态地展示和分析数据。

它们极大地增强了应用的灵活性和用户体验,使得数据分析、数据可视化等任务变得更加直观和便捷。

本文书接上回《
解决DDD最大难题-如何划分领域
》,关注公众号(老肖想当外语大佬)获取信息:

  1. 最新文章更新;

  2. DDD框架源码(.NET、Java双平台);

  3. 加群畅聊,建模分析、技术交流;

  4. 视频和直播在B站。

声明: 本文观点限定在重业务的软件系统研发场景下,其它场景不作为本文讨论的范围。

前言

1975 年《人月神话:软件项目管理之道》首次出版,揭示了一个被程序员奉为圣典的法则,认为增加开发者无法线性地缩短软件交付时间,其中的损耗巨大。时隔近50年,我的软件交付经验中却出现了打破这个法则的现象,我被这些现象深深地震撼,但当时却没有联想到“人月神话”,而最近在思考“如何为大家呈现我们实践DDD所获得的收益”时,灵光乍现,意识到实际上我们已经实现了通过增加人数来有效地缩短项目交付周期的能力,我意识到我们真的打破了“人月神话”。

还是那个故事

我在《
为了落地DDD,我是这样“PUA”大家的
》一文中,讲述了一支团队落地DDD的实践经历,这里有一个小插曲,在我们重构系统最艰难的时刻,也就是最复杂的订单模块整个生命周期时,我们的进度压力很大,团队期望能够进行渐进式交付,一块一块地交付测试,而订单的“创建-扣库存-支付成功”一整个逻辑又相互牵扯,需要完整实现才能提交测试,评估下来,如果这一块由一个开发者来开发,需要大约三天时间,而我们期望一天能够完成,于是我们本能地想到了方法:“加人”。但我们也很清楚,按照以往地经验,加人的损耗肯定是非常巨大的,三个开发者参与进来,一天能够完成也几乎是不可能的,但我们没有别的选择。

于是,我把三位开发者叫到会议室,按照我们的DDD工作流,花了近一个小时时间,确认了“领域模型”、“创建订单命令”、“订单创建成功事件”、“订单支付成功事件”、各个“事件处理器”逻辑、前端接口定义等设计方案,然后分工分别负责“命令处理器”、“事件处理器”、“前端接口”的开发工作。

令人惊奇的是,基于这样的分工,团队很快完成了各自的工作,并在当天下班前,团队完成了代码合并和联调,意外的进度给我们带来了一丝丝惊喜,但很快投入到了后续的工作中,并没有来得及深入思考。

而此后,团队的交付过程中,类似对于突发事件的应对,也多次复现了这样的现象,似乎我们的分工可以做到更细的颗粒度,使得团队能够通过增加人数的方式缩短需求的交付。

为什么可以这样

在我们团队重新审视这一现象时,我们可以看到:

  1. 我们将交付过程明确分为了“建模设计”和“代码编写”两个阶段,其中“代码编写”需要的能力是模式化、可复制的

  2. 我们的代码组织方式,非常有利于分工;

“建模设计”和“代码编写”

“建模设计”,是输入需求信息,输出方案的过程,是一个做决定的过程,这个过程,本质上是将“不确定性”尽可能转变为“确定性”的结论,需要需求方、决策者建立共识,做出设计结论,这个阶段无法通过加人来加速,但同时也不需要大量广泛的参与者,本质上是由“决策者”参与即可。

图片

一旦我们“建模设计方案”确定了,而“代码编写”要做的就是按照“模型设计”编写确定性的代码,这个过程本质上就是“做执行”,团队成员一旦适应了团队的代码风格和模式,基本不需要太多的思考,就可以写出“符合预期”的代码,因为这个“预期”已经被“建模设计”框定了。而且实践中我们发现,对于一个新人,通过不到一周的学习和模仿,就可以掌握这个能力。

图片

所以我们开发团队的整个流程就变成了“做决策”和“做执行”两件事,也是由“不确定性”到“确定性”的快速收敛的一个过程。

图片

代码风格的可协作性

我在《
DDD建模后写代码的正确姿势(Java、dotnet双平台)
》一文中为大家展示了我们的代码组织方式,我在视频《掌握这个模型你就能设计一切》(
https://www.bilibili.com/video/BV114421Q7vp)
中也介绍过业务系统中,最核心的就是“命令-事件”,系统的所有业务,都可以映射到这个模型中。用DDD+CQRS+Event Driven的组合,我们发现代码被一个个的“CommandHandler”、“EventHandler”拆分成一个个的独立的业务逻辑处理单元,这些单元的协作,在我们“建模设计”阶段已经确定下来,因此,开发者要做的就是完成这一个个填空,就完成了代码的编写,心智负担非常小。

图片

我们分工的最小颗粒度,就是这一个个的“CommandHandler”和“EventHandler”,这比传统的CRUD按模块分工的颗粒度要细致地多,也就意味着,我们团队可以更细粒度地调配人力,这其中的损耗却几乎可以忽略不记。

我们甚至规定,“
当你写代码不舒服时,大概率是建模设计出了问题
”,要求开发者反馈并由设计者及时修改迭代模型设计,尽可能地确保开发时,代码的
上下文能够获取到支撑实现逻辑单元目标的信息
,从而使得整个开发过程是丝滑的。

回到主题

在我们实践DDD的过程中,我们意识到,在“代码编写”环节,我们可以非常灵活地调配人力资源,哪怕是被临时调入项目的开发人员,也可以高效地按照设计完成代码的编写,这其中的损耗几乎可以忽略不记。

基于这样的情况,我们认为,一支研发团队,就可以实现一个建模设计师加上多个实现开发者这样的模型,而这个团队模型,我相信也是很多团队期望的但又很难把协作效率提升上来。

现在看来,我们的实践做到了,所以,我想,人月神话不再是神话。

前一篇:《人工智能模型训练技术:随机失活,丢弃法,Dropout》

序言:
让人工智能模型变得更“聪明”的方法之一,就是减少“过拟合”(读死书)的问题,从而提升模型的“泛化能力”,也就是它面对新问题时的适应能力。在前面,我们讲解了最常用的“随机丢弃”法,本节将带大家了解另一种重要的方法——“正则化”。

那么,正则化主要做什么呢?用现实生活中的案例来比喻,正则化的作用就像学校里的老师,目的是引导学生往正确的方向走,而不是死记硬背课本,或者因为某些考试题目熟悉而侥幸得高分。老师会通过约束学生的行为,比如规范学习方法、减少偏科等,让学生学到的是通用的规律和解决问题的能力,而不是偏离正常轨道或者学到无用的信息。同样,正则化在人工智能中通过约束模型的权重等机制,帮助模型避免过拟合,提升它面对新数据的表现能力。

使用正则化

正则化是一种通过减少权重的极端化(polarization)来防止过拟合的技术。如果某些神经元的权重过大,正则化会对它们进行“惩罚”。总体来说,正则化有两种主要类型:L1 和 L2。

• L1 正则化 通常被称为套索正则化(lasso,最小绝对收缩和选择算子)。它的作用是帮助我们忽略权重为零或接近零的值,当计算层的结果时,这些权重会被有效地“抛弃”。

• L2 正则化 通常被称为岭回归(ridge regression),因为它通过计算权重的平方,将非零值和零值(或接近零的值)之间的差异放大,从而产生一种“山脊效应”。

这两种方法还可以结合起来,形成一种叫做弹性正则化(elastic regularization)的技术。

对于像我们当前这种自然语言处理问题,L2 正则化 是最常用的。你可以通过 kernel_regularizer 属性将它添加到 Dense 层中,这个属性接受一个浮点值作为正则化因子。这是另一个可以用来优化模型的超参数,值得尝试!

以下是一个示例代码:

model = tf.keras.Sequential([

tf.keras.layers.Embedding(vocab_size, embedding_dim),

tf.keras.layers.GlobalAveragePooling1D(),

tf.keras.layers.Dense(8, activation='relu',

kernel_regularizer=tf.keras.regularizers.l2(0.01)),

tf.keras.layers.Dense(1, activation='sigmoid')

])

在像这样的简单模型中,添加正则化的影响并不会特别大,但它确实让训练损失和验证损失的曲线变得更加平滑了一些。在这种场景下可能有点“用力过猛”,但和 Dropout 一样,了解如何使用正则化来防止模型过度专注(overspecialized)是一个非常重要的技能。

其他优化的考虑

虽然我们已经通过之前的修改得到了一个过拟合更少、性能更好的模型,但还有其他超参数可以进行实验。例如,我们之前将最大句子长度设置为 100,但这个值纯粹是随意选的,可能并不是最佳值。一个好主意是探索语料库,看看有没有更合适的句子长度。

以下是一个代码片段,用来检查句子的长度并将它们从短到长排序后绘制成图表:

xs = []

ys = []

current_item = 1

for item in sentences:

xs.append(current_item)

current_item += 1

ys.append(len(item))

newys = sorted(ys)

import matplotlib.pyplot as plt

plt.plot(xs, newys)

plt.show()

图 6-16 展示了这个代码的结果。

图 6-16:探索句子长度

在整个 26,000 多条语料中,长度达到 100 个单词或以上的句子不到 200 条。所以,把句子长度的最大值设为 100,就会引入很多不必要的填充(padding),这会影响模型的性能。如果把最大长度减少到 85,仍然可以覆盖 26,000 条语料中的 99% 以上,几乎不需要任何填充。

总结:
正则化也是为了让模型变聪明的一种方法,即增强模型的泛化能力。它的角色就像我们从小到大的老师,负责引导和规范我们的学习框架和方向。下一节我们将把通过各种方法优化后训练好的模型,用于实际应用——分类新闻中的句子并进行预测。