Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0198bcc55b | ||
|
|
35d659397a | ||
|
|
b08b26b691 | ||
|
|
4b56ab4ed6 | ||
|
|
5bfd19a828 | ||
|
|
5e75ab4be0 | ||
|
|
7f1ec4b1c6 | ||
|
|
2214f4faad | ||
|
|
a625f6f24c | ||
|
|
a66c2fb578 | ||
|
|
1662fad4ae | ||
|
|
600563222d | ||
|
|
cbc75a5353 |
+4
-4
@@ -62,7 +62,7 @@ AVG 遇到 NULL 值时采用了最彻底的忽略方式,即 NULL 完全不参
|
||||
|
||||
### MAX、MIN
|
||||
|
||||
MAX、MIN 分别求最大与最小值,上面不同的时,也可以作用于字符串上,因此可以根据字母判断大小,从大到小依次对应 `a-z`,但即便能算,也没有实际意义且不好理解,因此不建议对字符串求极值。
|
||||
MAX、MIN 分别求最大与最小值,与上面不同的是,也可以作用于字符串上,因此可以根据字母判断大小,从大到小依次对应 `a-z`,但即便能算,也没有实际意义且不好理解,因此不建议对字符串求极值。
|
||||
|
||||
```sql
|
||||
SELECT MAX(cost) FROM test
|
||||
@@ -96,7 +96,7 @@ SELECT MAX(cost), MIN(cost), id FROM test -- id: 1
|
||||
举个例子,查询每个国家的 GDP 总量:
|
||||
|
||||
```sql
|
||||
SELECT COUNT(GDP) FROM amazing_table
|
||||
SELECT SUM(GDP) FROM amazing_table
|
||||
GROUP BY country
|
||||
```
|
||||
|
||||
@@ -105,10 +105,10 @@ GROUP BY country
|
||||
其实如果我们只想看中、美的 GDP,用非分组也可以查,只是要分成两条 SQL:
|
||||
|
||||
```sql
|
||||
SELECT COUNT(GDP) FROM amazing_table
|
||||
SELECT SUM(GDP) FROM amazing_table
|
||||
WHERE country = '中国'
|
||||
|
||||
SELECT COUNT(GDP) FROM amazing_table
|
||||
SELECT SUM(GDP) FROM amazing_table
|
||||
WHERE country = '美国'
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
SQL 复杂查询指的就是子查询。
|
||||
|
||||
为什么子查询叫做复杂查询呢?因为子查询相当于查询嵌套查询,因为嵌套导致复杂度几乎可以被无限放大(无限嵌套),因此叫复杂查询。下面是一个最简单的子查询例子:
|
||||
|
||||
```sql
|
||||
SELECT pv FROM (
|
||||
SELECT pv FROM test
|
||||
)
|
||||
```
|
||||
|
||||
上面的例子等价于 `SELECT pv FROM test`,但因为把表的位置替换成了一个新查询,所以摇身一变成为了复杂查询!所以复杂查询不一定真的复杂,甚至可能写出和普通查询等价的复杂查询,要避免这种无意义的行为。
|
||||
|
||||
我们也要借此机会了解为什么子查询可以这么做。
|
||||
|
||||
### 理解查询的本质
|
||||
|
||||
当我们查一张表时,数据库认为我们在查什么?
|
||||
|
||||
这点很重要,因为下面两个语句都是合法的:
|
||||
|
||||
```sql
|
||||
SELECT pv FROM test
|
||||
|
||||
SELECT pv FROM (
|
||||
SELECT pv FROM test
|
||||
)
|
||||
```
|
||||
|
||||
为什么数据库可以把子查询当作表呢?为了统一理解这些概念,我们有必要对查询内容进行抽象理解:**任意查询位置都是一条或多条记录**。
|
||||
|
||||
比如 `test` 这张表,显然是多条记录(当然只有一行就是一条记录),而 `SELECT pv FROM test` 也是多条记录,然而因为 `FROM` 后面可以查询任意条数的记录,所以这两种语法都支持。
|
||||
|
||||
不仅是 `FROM` 可以跟单条或多条记录,甚至 `SELECT`、`GROUP BY`、`WHERE`、`HAVING` 后都可以跟多条记录,这个后面再说。
|
||||
|
||||
说到这,也就很好理解子查询的变种了,比如我们可以在子查询内使用 `WHERE` 或 `GROUP BY` 等等,因为无论如何,只要查询结果是多条记录就行了:
|
||||
|
||||
```sql
|
||||
SELECT sum(people) as allPeople, sum(gdp), city FROM (
|
||||
SELECT people, gdp, city FROM test
|
||||
GROUP BY city
|
||||
HAVING sum(gdp) > 10000
|
||||
)
|
||||
```
|
||||
|
||||
这个例子就有点业务含义了。子查询是从内而外执行的,因此我们先看内部的逻辑:按照城市分组,筛选出总 GDP 超过一万的所有地区的人口数量明细。外层查询再把人口数加总,这样就能对比每个 GDP 超过一万的地区,总人口和总 GDP 分别是多少,方便对这些重点城市做对比。
|
||||
|
||||
不过这个例子看起来还是不太自然,因为我们没必要写成复杂查询,其实简单查询也是等价的:
|
||||
|
||||
```sql
|
||||
SELECT sum(people) as allPeople, sum(gdp), city FROM test
|
||||
GROUP BY city
|
||||
HAVING sum(gdp) > 10000
|
||||
```
|
||||
|
||||
那为什么要多此一举呢?因为复杂查询的真正用法并不在这里。
|
||||
|
||||
### 视图
|
||||
|
||||
正因为子查询的存在,我们才可能以类似抽取变量的方式,抽取子查询,这个抽取出来的抽象就是视图:
|
||||
|
||||
```sql
|
||||
CREATE VIEW my_table(people, gdp, city)
|
||||
AS
|
||||
SELECT sum(people) as allPeople, sum(gdp), city FROM test
|
||||
GROUP BY city
|
||||
HAVING sum(gdp) > 10000
|
||||
|
||||
SELECT sum(people) as allPeople, sum(gdp), city FROM my_table
|
||||
```
|
||||
|
||||
这样的好处是,这个视图可以被多条 SQL 语句复用,不仅可维护性变好了,执行时也仅需查询一次。
|
||||
|
||||
要注意的是,SELECT 可以使用任何视图,但 INSERT、DELETE、UPDATE 用于视图时,需要视图满足一下条件:
|
||||
|
||||
1. 未使用 DISTINCT 去重。
|
||||
2. FROM 单表。
|
||||
3. 未使用 GROUP BY 和 HAVING。
|
||||
|
||||
因为上面几种模式都会导致视图成为聚合后的数据,不方便做除了查以外的操作。
|
||||
|
||||
另外一个知识点就是物化视图,即使用 MATERIALIZED 描述视图:
|
||||
|
||||
```sql
|
||||
CREATE MATERIALIZED VIEW my_table(people, gdp, city)
|
||||
AS ...
|
||||
```
|
||||
|
||||
这种视图会落盘,为什么要支持这个特性呢?因为普通视图作为临时表,无法利用索引等优化手段,查询性能较低,所以物化视图是较为常见的性能优化手段。
|
||||
|
||||
说到性能优化手段,还有一些比较常见的理念,即把读的复杂度分摊到写的时候,比如提前聚合新表落盘或者对 CASE 语句固化为字段等,这里先不展开。
|
||||
|
||||
### 标量子查询
|
||||
|
||||
上面说了,WHERE 也可以跟子查询,比如:
|
||||
|
||||
```sql
|
||||
SELECT city FROM test
|
||||
WHERE gdp > (
|
||||
SELECT avg(gdp) from test
|
||||
)
|
||||
```
|
||||
|
||||
这样可以查询出 gdp 大于平均值的城市。
|
||||
|
||||
那为什么不能直接这么写呢?
|
||||
|
||||
```sql
|
||||
SELECT city FROM test
|
||||
WHERE gdp > avg(gdp) -- 报错,WHERE 无法使用聚合函数
|
||||
```
|
||||
|
||||
看上去很美好,但其实第一篇我们就介绍了,WHERE 不能跟聚合查询,因为这样会把整个父查询都聚合起来。那为什么子查询可以?因为子查询聚合的是子查询啊,父查询并没有被聚合,所以这才符合我们的意图。
|
||||
|
||||
所以上面例子不合适的地方在于,直接在当前查询使用 `avg(gdp)` 会导致聚合,而我们并不想聚合当前查询,但又要通过聚合拿到平均 GDP,所以就要使用子查询了!
|
||||
|
||||
回过头来看,为什么这一节叫标量子查询?标量即单一值,因为 `avg(gdp)` 聚合出来的只有一个值,所以 WHERE 可以把它当做一个单一数值使用。反之,如果子查询没有使用聚合函数,或 GROUP BY 分组,那么就不能使用 `WHERE >` 这种语法,但可以使用 `WHERE IN`,这涉及到单条与多条记录的思考,我们接着看下一节。
|
||||
|
||||
### 单条和多条记录
|
||||
|
||||
介绍标量子查询时说到了,`WHERE >` 的值必须时单一值。但其实 WHERE 也可以跟返回多条记录的子查询结果,只要使用合理的条件语句,比如 IN:
|
||||
|
||||
```sql
|
||||
SELECT area FROM test
|
||||
WHERE gdp IN (
|
||||
SELECT max(gdp) from test
|
||||
GROUP BY city
|
||||
)
|
||||
```
|
||||
|
||||
上面的例子,子查询按照城市分组,并找到每一组 GDP 最大的那条记录,所以如果数据粒度是区域,那么我们就查到了每个城市 GDP 最大的那些记录,然后父查询通过 WHERE IN 找到 gdp 符合的复数结果,所以最后就把每个城市最大 gdp 的区域列了出来。
|
||||
|
||||
但实际上 `WHERE >` 语句跟复数查询结果也不会报错,但没有任何意义,所以我们要理解查询结果是单条还是多条,在 WHERE 判断时选择合适的条件。WHERE 适合跟复数查询结果的语法有:`WHERE IN`、`WHERE SOME`、`WHERE ANY`。
|
||||
|
||||
### 关联子查询
|
||||
|
||||
所谓关联子查询,即父子查询间存在关联,既然如此,子查询肯定不能单独优先执行,毕竟和父查询存在关联嘛,所以关联子查询是先执行外层查询,再执行内层查询的。要注意的是,对每一行父查询,子查询都会执行一次,因此性能不高(当然 SQL 会对相同参数的子查询结果做缓存)。
|
||||
|
||||
那这个关联是什么呢?关联的是每一行父查询时,对子查询执行的条件。这么说可能有点绕,举个例子:
|
||||
|
||||
```sql
|
||||
SELECT * FROM test where gdp > (
|
||||
select avg(gdp) from test
|
||||
group by city
|
||||
)
|
||||
```
|
||||
|
||||
对这个例子来说,想要查找 gdp 大于按城市分组的平均 gdp,比如北京地区按北京比较,上海地区按上海比较。但很可惜这样做是不行的,因为父子查询没有关联,SQL 并不知道要按照相同城市比较,因此只要加一个 WHERE 条件,就变成关联子查询了:
|
||||
|
||||
```sql
|
||||
SELECT * FROM test as t1 where gdp > (
|
||||
select avg(gdp) from test as t2 where t1.city = t2.city
|
||||
group by city
|
||||
)
|
||||
```
|
||||
|
||||
就是在每次判断 `WHERE gdp >` 条件时,重新计算子查询结果,将平均值限定在相同的城市,这样就符合需求了。
|
||||
|
||||
## 总结
|
||||
|
||||
学会灵活运用父子查询,就掌握了复杂查询了。
|
||||
|
||||
SQL 第一公民是集合,所以所谓父子查询就是父子集合的灵活组合,这些集合可以出现在几乎任何位置,根据集合的数量、是否聚合、关联条件,就派生出了标量查询、关联子查询。
|
||||
|
||||
更深入的了解就需要大量实战案例了,但万变不离其宗,掌握了复杂查询后,就可以理解大部分 SQL 案例了。
|
||||
|
||||
> 讨论地址是:[精读《SQL 复杂查询》· Issue #403 · ascoders/weekly](https://github.com/ascoders/weekly/issues/403)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/ascoders/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
CASE 表达式分为简单表达式与搜索表达式,其中搜索表达式可以覆盖简单表达式的全部能力,我也建议只写搜索表达式,而不要写简单表达式。
|
||||
|
||||
简单表达式:
|
||||
|
||||
```sql
|
||||
SELECT CASE city
|
||||
WHEN '北京' THEN 1
|
||||
WHEN '天津' THEN 2
|
||||
ELSE 0
|
||||
END AS abc
|
||||
FROM test
|
||||
```
|
||||
|
||||
搜索表达式:
|
||||
|
||||
```sql
|
||||
SELECT CASE
|
||||
WHEN city = '北京' THEN 1
|
||||
WHEN city = '天津' THEN 2
|
||||
ELSE 0
|
||||
END AS abc
|
||||
FROM test
|
||||
```
|
||||
|
||||
明显可以看出,简单表达式只是搜索表达式 `a = b` 的特例,因为无法书写任何符号,只要条件换成 `a > b` 就无法胜任了,而搜索表达式不但可以轻松胜任,甚至可以写聚合函数。
|
||||
|
||||
## CASE 表达式里的聚合函数
|
||||
|
||||
为什么 CASE 表达式里可以写聚合函数?
|
||||
|
||||
因为本身表达式就支持聚合函数,比如下面的语法,我们不会觉得奇怪:
|
||||
|
||||
```sql
|
||||
SELECT sum(pv), avg(uv) from test
|
||||
```
|
||||
|
||||
本身 SQL 就支持多种不同的聚合方式同时计算,所以将其用在 CASE 表达式里,也是顺其自然的:
|
||||
|
||||
```sql
|
||||
SELECT CASE
|
||||
WHEN count(city) = 100 THEN 1
|
||||
WHEN sum(dau) > 200 THEN 2
|
||||
ELSE 0
|
||||
END AS abc
|
||||
FROM test
|
||||
```
|
||||
|
||||
只要 SQL 表达式中存在聚合函数,那么整个表达式都聚合了,此时访问非聚合变量没有任何意义。所以上面的例子,即便在 CASE 表达式中使用了聚合,其实也不过是聚合了一次后,按照条件进行判断罢了。
|
||||
|
||||
这个特性可以解决很多实际问题,比如将一些复杂聚合判断条件的结果用 SQL 结构输出,那么很可能是下面这种写法:
|
||||
|
||||
```sql
|
||||
SELECT CASE
|
||||
WHEN 聚合函数(字段) 符合什么条件 THEN xxx
|
||||
... 可能有 N 个
|
||||
ELSE NULL
|
||||
END AS abc
|
||||
FROM test
|
||||
```
|
||||
|
||||
这也可以认为是一种行转列的过程,即 **把行聚合后的结果通过一条条 CASE 表达式形成一个个新的列**。
|
||||
|
||||
## 聚合与非聚合不能混用
|
||||
|
||||
我们希望利用 CASE 表达式找出那些 pv 大于平均值的行,以下这种想当然的写法是错误的:
|
||||
|
||||
```sql
|
||||
SELECT CASE
|
||||
WHEN pv > avg(pv) THEN 'yes'
|
||||
ELSE 'no'
|
||||
END AS abc
|
||||
FROM test
|
||||
```
|
||||
|
||||
原因是,只要 SQL 中存在聚合表达式,那么整条 SQL 就都是聚合的,所以返回的结果只有一条,而我们期望查询结果不聚合,只是判断条件用到了聚合结果,那么就要使用子查询。
|
||||
|
||||
为什么子查询可以解决问题?因为子查询的聚合发生在子查询,而不影响当前父查询,理解了这一点,就知道为什么下面的写法才是正确的了:
|
||||
|
||||
```sql
|
||||
SELECT CASE
|
||||
WHEN pv > ( SELECT avg(pv) from test ) THEN 'yes'
|
||||
ELSE 'no'
|
||||
END AS abc
|
||||
FROM test
|
||||
```
|
||||
|
||||
这个例子也说明了 CASE 表达式里可以使用子查询,因为子查询是先计算的,所以查询结果在哪儿都能用,CASE 表达式也不例外。
|
||||
|
||||
## WHERE 中的 CASE
|
||||
|
||||
WHERE 后面也可以跟 CASE 表达式的,用来做一些需要特殊枚举处理的筛选。
|
||||
|
||||
比如下面的例子:
|
||||
|
||||
```sql
|
||||
SELECT * FROM demo WHERE
|
||||
CASE
|
||||
WHEN city = '北京' THEN true
|
||||
ELSE ID > 5
|
||||
END
|
||||
```
|
||||
|
||||
本来我们要查询 ID 大于 5 的数据,但我想对北京这个城市特别对待,那么就可以在判断条件中再进行 CASE 分支判断。
|
||||
|
||||
这个场景在 BI 工具里等价于,创建一个 CASE 表达式字段,可以拖入筛选条件生效。
|
||||
|
||||
## GROUP BY 中的 CASE
|
||||
|
||||
想不到吧,GROUP BY 里都可以写 CASE 表达式:
|
||||
|
||||
```sql
|
||||
SELECT isPower, sum(gdp) FROM test GROUP BY CASE
|
||||
WHEN isPower = 1 THEN city, area
|
||||
ELSE city
|
||||
END
|
||||
```
|
||||
|
||||
上面例子表示,计算 GDP 时,对于非常发达的城市,按照每个区粒度查看聚合结果,也就是看的粒度更细一些,而对于欠发达地区,本身 gdp 也不高,直接按照城市粒度看聚合结果。
|
||||
|
||||
这样,就按照不同的条件对数据进行了分组聚合。由于返回行结果是混在一起的,像这个例子,可以根据 isPower 字段是否为 1 判断,是否按照城市、区域进行了聚合,如果没有其他更显著的标识,可能导致无法区分不同行的聚合粒度,因此谨慎使用。
|
||||
|
||||
## ORDER BY 中的 CASE
|
||||
|
||||
同样,ORDER BY 使用 CASE 表达式,会将排序结果按照 CASE 分类进行分组,每组按照自己的规则排序,比如:
|
||||
|
||||
```sql
|
||||
SELECT * FROM test ORDER BY CASE
|
||||
WHEN isPower = 1 THEN gdp
|
||||
ELSE people
|
||||
END
|
||||
```
|
||||
|
||||
上面的例子,对发达地区采用 gdp 排序,否则采用人口数量排序。
|
||||
|
||||
## 总结
|
||||
|
||||
CASE 表达式总结一下有如下特点:
|
||||
|
||||
1. 支持简单与搜索两种写法,推荐搜索写法。
|
||||
2. 支持聚合与子查询,需要注意不同情况的特点。
|
||||
3. 可以写在 SQL 查询的几乎任何地方,只要是可以写字段的地方,基本上就可以替换为 CASE 表达式。
|
||||
4. 除了 SELECT 外,CASE 表达式还广泛应用在 INSERT 与 UPDATE,其中 UPDATE 的妙用是不用将 SQL 拆分为多条,所以不用担心数据变更后对判断条件的二次影响。
|
||||
|
||||
> 讨论地址是:[精读《SQL CASE 表达式》· Issue #404 · ascoders/weekly](https://github.com/ascoders/weekly/issues/404)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/ascoders/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
窗口函数形如:
|
||||
|
||||
```sql
|
||||
表达式 OVER (PARTITION BY 分组字段 ORDER BY 排序字段)
|
||||
```
|
||||
|
||||
有两个能力:
|
||||
|
||||
1. 当表达式为 `rank()` `dense_rank()` `row_number()` 时,拥有分组排序能力。
|
||||
2. 当表达式为 `sum()` 等聚合函数时,拥有累计聚合能力。
|
||||
|
||||
无论何种能力,**窗口函数都不会影响数据行数,而是将计算平摊在每一行**。
|
||||
|
||||
这两种能力需要区分理解。
|
||||
|
||||
## 底表
|
||||
|
||||
<img width=500 src="https://s1.ax1x.com/2022/03/26/qdUL60.png">
|
||||
|
||||
以上是示例底表,共有 8 条数据,城市1、城市2 两个城市,下面各有地区1~4,每条数据都有该数据的人口数。
|
||||
|
||||
## 分组排序
|
||||
|
||||
如果按照人口排序,`ORDER BY people` 就行了,但如果我们想在城市内排序怎么办?
|
||||
|
||||
此时就要用到窗口函数的分组排序能力:
|
||||
|
||||
<img width=500 src="https://s1.ax1x.com/2022/03/26/qddfIg.png">
|
||||
|
||||
```sql
|
||||
SELECT *, rank() over (PARTITION BY city ORDER BY people) FROM test
|
||||
```
|
||||
|
||||
该 SQL 表示在 city 组内按照 people 进行排序。
|
||||
|
||||
其实 PARTITION BY 也是可选的,如果我们忽略它:
|
||||
|
||||
```sql
|
||||
SELECT *, rank() over (ORDER BY people) FROM test
|
||||
```
|
||||
|
||||
也是生效的,但该语句与普通 ORDER BY 等价,因此利用窗口函数进行分组排序时,一般都会使用 PARTITION BY。
|
||||
|
||||
### 各分组排序函数的差异
|
||||
|
||||
我们将 `rank()` `dense_rank()` `row_number()` 的结果都打印出来:
|
||||
|
||||
```sql
|
||||
SELECT *,
|
||||
rank() over (PARTITION BY city ORDER BY people),
|
||||
dense_rank() over (PARTITION BY city ORDER BY people),
|
||||
row_number() over (PARTITION BY city ORDER BY people)
|
||||
FROM test
|
||||
```
|
||||
|
||||
<img width=500 src="https://s1.ax1x.com/2022/03/26/qd0uh4.png">
|
||||
|
||||
其实从结果就可以猜到,这三个函数在处理排序遇到相同值时,对排名统计逻辑有如下差异:
|
||||
|
||||
1. `rank()`: 值相同时排名相同,但占用排名数字。
|
||||
2. `dense_rank()`: 值相同时排名相同,但不占用排名数字,整体排名更加紧凑。
|
||||
3. `row_number()`: 无论值是否相同,都强制按照行号展示排名。
|
||||
|
||||
上面的例子可以优化一下,因为所有窗口逻辑都是相同的,我们可以利用 WINDOW AS 提取为一个变量:
|
||||
|
||||
```sql
|
||||
SELECT *,
|
||||
rank() over wd, dense_rank() over wd, row_number() over wd
|
||||
FROM test
|
||||
WINDOW wd as (PARTITION BY city ORDER BY people)
|
||||
```
|
||||
|
||||
## 累计聚合
|
||||
|
||||
我们之前说过,凡事使用了聚合函数,都会让查询变成聚合模式。如果不用 GROUP BY,聚合后返回行数会压缩为一行,即使用了 GROUP BY,返回的行数一般也会大大减少,因为分组聚合了。
|
||||
|
||||
然而使用窗口函数的聚合却不会导致返回行数减少,那么这种聚合是怎么计算的呢?我们不如直接看下面的例子:
|
||||
|
||||
```sql
|
||||
SELECT *,
|
||||
sum(people) over (PARTITION BY city ORDER BY people)
|
||||
FROM test
|
||||
```
|
||||
|
||||
<img width=500 src="https://s1.ax1x.com/2022/03/26/qdsDJS.png">
|
||||
|
||||
可以看到,在每个 city 分组内,按照 people 排序后进行了 **累加**(相同的值会合并在一起),这就是 BI 工具一般说的 RUNNGIN_SUM 的实现思路,当然一般我们排序规则使用绝对不会重复的日期,所以不会遇到第一个红框中合并计算的问题。
|
||||
|
||||
累计函数还有 `avg()` `min()` 等等,这些都一样可以作用于窗口函数,其逻辑可以按照下图理解:
|
||||
|
||||
<img width=400 src="https://s1.ax1x.com/2022/03/26/qd6or9.png">
|
||||
|
||||
你可能有疑问,直接 `sum(上一行结果,下一行)` 不是更方便吗?为了验证猜想,我们试试 `avg()` 的结果:
|
||||
|
||||
<img width=400 src="https://s1.ax1x.com/2022/03/26/qdciIP.png">
|
||||
|
||||
可见,如果直接利用上一行结果的缓存,那么 avg 结果必然是不准确的,所以窗口累计聚合是每行重新计算的。当然也不排除对于 sum、max、min 做额外性能优化的可能性,但 avg 只能每行重头计算。
|
||||
|
||||
### 与 GROUP BY 组合使用
|
||||
|
||||
窗口函数是可以与 GROUP BY 组合使用的,遵循的规则是,窗口范围对后面的查询结果生效,所以其实并不关心是否进行了 GROUP BY。我们看下面的例子:
|
||||
|
||||
<img width=500 src="https://s1.ax1x.com/2022/03/26/qdgMOH.png">
|
||||
|
||||
按照地区分组后进行累加聚合,是对 GROUP BY 后的数据行粒度进行的,而不是之前的明细行。
|
||||
|
||||
## 总结
|
||||
|
||||
窗口函数在计算组内排序或累计 GVM 等场景非常有用,我们只要牢记两个知识点就行了:
|
||||
|
||||
1. 分组排序要结合 PARTITION BY 才有意义。
|
||||
2. 累计聚合作用于查询结果行粒度,支持所有聚合函数。
|
||||
|
||||
> 讨论地址是:[精读《SQL 窗口函数》· Issue #405 · ascoders/weekly](https://github.com/ascoders/weekly/issues/405)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/ascoders/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
SQL grouping 解决 OLAP 场景总计与小计问题,其语法分为几类,但要解决的是同一个问题:
|
||||
|
||||
ROLLUP 与 CUBE 是封装了规则的 GROUPING SETS,而 GROUPING SETS 则是最原始的规则。
|
||||
|
||||
为了方便理解,让我们从一个问题入手,层层递进吧。
|
||||
|
||||
## 底表
|
||||
|
||||
<img width=500 src="https://s1.ax1x.com/2022/03/26/qdUL60.png">
|
||||
|
||||
以上是示例底表,共有 8 条数据,城市1、城市2 两个城市,下面各有地区1~4,每条数据都有该数据的人口数。
|
||||
|
||||
现在想计算人口总计,以及各城市人口小计。在没有掌握 grouping 语法前,我们只能通过两个 select 语句 union 后得到:
|
||||
|
||||
```sql
|
||||
SELECT city, sum(people) FROM test GROUP BY city
|
||||
union
|
||||
SELECT '合计' as city, sum(people) FROM test
|
||||
```
|
||||
|
||||
<img width=500 src="https://s1.ax1x.com/2022/04/04/qbKPRs.png">
|
||||
|
||||
但两条 select 语句聚合了两次,性能是一个不小的开销,因此 SQL 提供了 GROUPING SETS 语法解决这个问题。
|
||||
|
||||
## GROUPING SETS
|
||||
|
||||
GROUP BY GROUPING SETS 可以指定任意聚合项,比如我们要同时计算总计与分组合计,就要按照空内容进行 GROUP BY 进行一次 sum,再按照 city 进行 GROUP BY 再进行一次 sum,换成 GROUPING SETS 描述就是:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
city, area,
|
||||
sum(people)
|
||||
FROM test
|
||||
GROUP BY GROUPING SETS((), (city, area))
|
||||
```
|
||||
|
||||
其中 `GROUPING SETS((), (city, area))` 表示分别按照 `()`、`(city, area)` 聚合计算总计。返回结果是:
|
||||
|
||||
<img width=500 src="https://s1.ax1x.com/2022/04/04/qbRnWF.png">
|
||||
|
||||
可以看到,值为 NULL 的行就是我们要的总计,其值是没有任何 GROUP BY 限制算出来的。
|
||||
|
||||
类似的,我们还可以写 `GROUPING SETS((), (city), (city, area), (area))` 等任意数量、任意组合的 GROUP BY 条件。
|
||||
|
||||
通过这种规则计算的数据我们称为 “超级分组记录”。我们发现 “超级分组记录” 产生的 NULL 值很容易和真正的 NULL 值弄混,所以 SQL 提供了 GROUPING 函数解决这个问题。
|
||||
|
||||
## 函数 GROUPING
|
||||
|
||||
对于超级分组记录产生的 NULL,是可以被 `GROUPING()` 函数识别为 1 的:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
GROUPING(city),
|
||||
GROUPING(area),
|
||||
sum(people)
|
||||
FROM test
|
||||
GROUP BY GROUPING SETS((), (city, area))
|
||||
```
|
||||
|
||||
具体效果见下图:
|
||||
|
||||
<img width=500 src="https://s1.ax1x.com/2022/04/04/qbRLpF.png">
|
||||
|
||||
可以看到,但凡是超级分组计算出来的字段都会识别为 1,我们利用之前学习的 [SQL CASE 表达式](https://github.com/ascoders/weekly/blob/master/SQL/234.SQL%20CASE%20%E8%A1%A8%E8%BE%BE%E5%BC%8F.md) 将其转换为总计、小计字样,就可以得出一张数据分析表了:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
CASE WHEN GROUPING(city) = 1 THEN '总计' ELSE city END,
|
||||
CASE WHEN GROUPING(area) = 1 THEN '小计' ELSE area END,
|
||||
sum(people)
|
||||
FROM test
|
||||
GROUP BY GROUPING SETS((), (city, area))
|
||||
```
|
||||
|
||||
<img width=500 src="https://s1.ax1x.com/2022/04/04/qbRz01.png">
|
||||
|
||||
然后前端表格展示时,将第一行 “总计”、“小计” 单元格合并为 “总计”,就完成了总计这个 BI 可视化分析功能。
|
||||
|
||||
## ROLLUP
|
||||
|
||||
ROLLUP 是卷起的意思,是一种特定规则的 GROUPING SETS,以下两种写法是等价的:
|
||||
|
||||
```sql
|
||||
SELECT sum(people) FROM test
|
||||
GROUP BY ROLLUP(city)
|
||||
|
||||
-- 等价于
|
||||
SELECT sum(people) FROM test
|
||||
GROUP BY GROUPING SETS((), (city))
|
||||
```
|
||||
|
||||
再看一组等价描述:
|
||||
|
||||
```sql
|
||||
SELECT sum(people) FROM test
|
||||
GROUP BY ROLLUP(city, area)
|
||||
|
||||
-- 等价于
|
||||
SELECT sum(people) FROM test
|
||||
GROUP BY GROUPING SETS((), (city), (city, area))
|
||||
```
|
||||
|
||||
发现规律了吗?ROLLUP 会按顺序把 GROUP BY 内容 “一个个卷起来”。用 GROUPING 函数判断超级分组记录对 ROLLUP 同样适用。
|
||||
|
||||
## CUBE
|
||||
|
||||
CUBE 又有所不同,它对内容进行了所有可能性展开(所以叫 CUBE)。
|
||||
|
||||
类比上面的例子,我们再写两组等价的展开:
|
||||
|
||||
```sql
|
||||
SELECT sum(people) FROM test
|
||||
GROUP BY CUBE(city)
|
||||
|
||||
-- 等价于
|
||||
SELECT sum(people) FROM test
|
||||
GROUP BY GROUPING SETS((), (city))
|
||||
```
|
||||
|
||||
上面的例子因为只有一项还看不出来,下面两项分组就能看出来了:
|
||||
|
||||
```sql
|
||||
SELECT sum(people) FROM test
|
||||
GROUP BY CUBE(city, area)
|
||||
|
||||
-- 等价于
|
||||
SELECT sum(people) FROM test
|
||||
GROUP BY GROUPING SETS((), (city), (area), (city, area))
|
||||
```
|
||||
|
||||
所谓 CUBE,是一种多维形状的描述,二维时有 2^1 种展开,三维时有 2^2 种展开,四维、五维依此类推。可以想象,如果用 CUBE 描述了很多组合,复杂度会爆炸。
|
||||
|
||||
## 总结
|
||||
|
||||
学习了 GROUPING 语法,以后前端同学的你不会再纠结这个问题了吧:
|
||||
|
||||
> 产品开启了总计、小计,我们是额外取一次数还是放到一起获取啊?
|
||||
|
||||
这个问题的标准答案和原理都在这篇文章里了。PS:对于不支持 GROUPING 语法数据库,要想办法屏蔽,就像前端 polyfill 一样,是一种降级方案。至于如何屏蔽,参考文章开头提到的两个 SELECT + UNION。
|
||||
|
||||
> 讨论地址是:[精读《SQL grouping》· Issue #406 · ascoders/weekly](https://github.com/ascoders/weekly/issues/406)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/ascoders/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
前端界的好文精读,每周更新!
|
||||
|
||||
最新精读:<a href="./SQL/232.SQL%20%E8%81%9A%E5%90%88%E6%9F%A5%E8%AF%A2.md">232.SQL 聚合查询</a>
|
||||
最新精读:<a href="./前沿技术/237.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%204.5-4.6%20%E6%96%B0%E7%89%B9%E6%80%A7%E3%80%8B.md">237.精读《Typescript 4.5-4.6 新特性》</a>
|
||||
|
||||
素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)
|
||||
|
||||
@@ -186,6 +186,7 @@
|
||||
- <a href="./前沿技术/226.%E7%B2%BE%E8%AF%BB%E3%80%8A2021%20%E5%89%8D%E7%AB%AF%E6%96%B0%E7%A7%80%E5%9B%9E%E9%A1%BE%E3%80%8B.md">226.精读《2021 前端新秀回顾》</a>
|
||||
- <a href="./前沿技术/228.%E7%B2%BE%E8%AF%BB%E3%80%8Apipe%20operator%20for%20JavaScript%E3%80%8B.md">228.精读《pipe operator for JavaScript》</a>
|
||||
- <a href="./前沿技术/230.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%AF%B9%20Markdown%20%E7%9A%84%E6%80%9D%E8%80%83%E3%80%8B.md">230.精读《对 Markdown 的思考》</a>
|
||||
- <a href="./前沿技术/237.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%204.5-4.6%20%E6%96%B0%E7%89%B9%E6%80%A7%E3%80%8B.md">237.精读《Typescript 4.5-4.6 新特性》</a>
|
||||
|
||||
### 设计模式
|
||||
|
||||
@@ -269,6 +270,10 @@
|
||||
|
||||
- <a href="./SQL/231.SQL%20%E5%85%A5%E9%97%A8.md">231.SQL 入门</a>
|
||||
- <a href="./SQL/232.SQL%20%E8%81%9A%E5%90%88%E6%9F%A5%E8%AF%A2.md">232.SQL 聚合查询</a>
|
||||
- <a href="./SQL/233.SQL%20%E5%A4%8D%E6%9D%82%E6%9F%A5%E8%AF%A2.md">233.SQL 复杂查询</a>
|
||||
- <a href="./SQL/234.SQL%20CASE%20%E8%A1%A8%E8%BE%BE%E5%BC%8F.md">234.SQL CASE 表达式</a>
|
||||
- <a href="./SQL/235.SQL%20%E7%AA%97%E5%8F%A3%E5%87%BD%E6%95%B0.md">235.SQL 窗口函数</a>
|
||||
- <a href="./SQL/236.SQL%20grouping.md">236.SQL grouping</a>
|
||||
|
||||
## 关注前端精读微信公众号
|
||||
|
||||
|
||||
@@ -0,0 +1,502 @@
|
||||
## 新增 Awaited 类型
|
||||
|
||||
Awaited 可以将 Promise 实际返回类型抽出来,按照名字可以理解为:等待 Promise resolve 了拿到的类型。下面是官方文档提供的 Demo:
|
||||
|
||||
```ts
|
||||
// A = string
|
||||
type A = Awaited<Promise<string>>;
|
||||
|
||||
// B = number
|
||||
type B = Awaited<Promise<Promise<number>>>;
|
||||
|
||||
// C = boolean | number
|
||||
type C = Awaited<boolean | Promise<number>>;
|
||||
```
|
||||
|
||||
## 捆绑的 dom lib 类型可以被替换
|
||||
|
||||
TS 因开箱即用的特性,捆绑了所有 dom 内置类型,比如我们可以直接使用 Document 类型,而这个类型就是 TS 内置提供的。
|
||||
|
||||
也许有时不想随着 TS 版本升级而升级连带的 dom 内置类型,所以 TS 提供了一种指定 dom lib 类型的方案,在 `package.json` 申明 `@typescript/lib-dom` 即可:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@typescript/lib-dom": "npm:@types/web"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个特性提升了 TS 的环境兼容性,但一般情况还是建议开箱即用,省去繁琐的配置,项目更好维护。
|
||||
|
||||
## 模版字符串类型也支持类型收窄
|
||||
|
||||
```ts
|
||||
export interface Success {
|
||||
type: `${string}Success`;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface Error {
|
||||
type: `${string}Error`;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function handler(r: Success | Error) {
|
||||
if (r.type === "HttpSuccess") {
|
||||
// 'r' has type 'Success'
|
||||
let token = r.body;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
模版字符串类型早就支持了,但现在才支持按照模版字符串在分支条件时,做类型收窄。
|
||||
|
||||
## 增加新的 --module es2022
|
||||
|
||||
虽然可以使用 --module esnext 保持最新特性,但如果你想使用稳定的版本号,又要支持顶级 await 特性的话,可以使用 es2022。
|
||||
|
||||
## 尾递归优化
|
||||
|
||||
TS 类型系统支持尾递归优化了,拿下面这个例子就好理解:
|
||||
|
||||
```ts
|
||||
type TrimLeft<T extends string> =
|
||||
T extends ` ${infer Rest}` ? TrimLeft<Rest> : T;
|
||||
|
||||
// error: Type instantiation is excessively deep and possibly infinite.
|
||||
type Test = TrimLeft<" oops">;
|
||||
```
|
||||
|
||||
在没有做尾递归优化前,TS 会因为堆栈过深而报错,但现在可以正确返回执行结果了,因为尾递归优化后,不会形成逐渐加深的调用,而是执行完后立即退出当前函数,堆栈数量始终保持不变。
|
||||
|
||||
JS 目前还没有做到自动尾递归优化,但可以通过自定义函数 TCO 模拟实现,下面放出这个函数的实现:
|
||||
|
||||
```js
|
||||
function tco(f) {
|
||||
var value;
|
||||
var active = false;
|
||||
var accumulated = [];
|
||||
return function accumulator(...rest) {
|
||||
accumulated.push(rest);
|
||||
if (!active) {
|
||||
active = true;
|
||||
while (accumulated.length) {
|
||||
value = f.apply(this, accumulated.shift());
|
||||
}
|
||||
active = false;
|
||||
return value;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
核心是把递归变成 while 循环,这样就不会产生堆栈。
|
||||
|
||||
## 强制保留 import
|
||||
|
||||
TS 编译时会把没用到的 import 干掉,但这次提供了 `--preserveValueImports` 参数禁用这一特性,原因是以下情况会导致误移除 import:
|
||||
|
||||
```ts
|
||||
import { Animal } from "./animal.js";
|
||||
|
||||
eval("console.log(new Animal().isDangerous())");
|
||||
```
|
||||
|
||||
因为 TS 无法分辨 eval 里的引用,类似的还有 vue 的 `setup` 语法:
|
||||
|
||||
```html
|
||||
<!-- A .vue File -->
|
||||
<script setup>
|
||||
import { someFunc } from "./some-module.js";
|
||||
</script>
|
||||
|
||||
<button @click="someFunc">Click me!</button>
|
||||
```
|
||||
|
||||
## 支持变量 import type 声明
|
||||
|
||||
之前支持了如下语法标记引用的变量是类型:
|
||||
|
||||
```ts
|
||||
import type { BaseType } from "./some-module.js";
|
||||
```
|
||||
|
||||
现在支持了变量级别的 type 声明:
|
||||
|
||||
```ts
|
||||
import { someFunc, type BaseType } from "./some-module.js";
|
||||
```
|
||||
|
||||
这样方便在独立模块构建时,安全的抹去 `BaseType`,因为单模块构建时,无法感知 `some-module.js` 文件内容,所以如果不特别指定 `type BaseType`,TS 编译器将无法识别其为类型变量。
|
||||
|
||||
## 类私有变量检查
|
||||
|
||||
包含两个特性,第一是 TS 支持了类私有变量的检查:
|
||||
|
||||
```ts
|
||||
class Person {
|
||||
#name: string;
|
||||
}
|
||||
```
|
||||
|
||||
第二是支持了 `#name in obj` 的判断,如:
|
||||
|
||||
```ts
|
||||
class Person {
|
||||
#name: string;
|
||||
constructor(name: string) {
|
||||
this.#name = name;
|
||||
}
|
||||
|
||||
equals(other: unknown) {
|
||||
return other &&
|
||||
typeof other === "object" &&
|
||||
#name in other && // <- this is new!
|
||||
this.#name === other.#name;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
该判断隐式要求了 `#name in other` 的 `other` 是 Person 实例化的对象,因为该语法仅可能存在于类中,而且还能进一步类型缩窄为 Persion 类。
|
||||
|
||||
## Import 断言
|
||||
|
||||
支持了导入断言提案:
|
||||
|
||||
```ts
|
||||
import obj from "./something.json" assert { type: "json" };
|
||||
```
|
||||
|
||||
以及动态 import 的断言:
|
||||
|
||||
```ts
|
||||
const obj = await import("./something.json", {
|
||||
assert: { type: "json" }
|
||||
})
|
||||
```
|
||||
|
||||
TS 该特性支持了任意类型的断言,而不关心浏览器是否识别。所以该断言如果要生效,需要以下两种支持的任意一种:
|
||||
|
||||
- 浏览器支持。
|
||||
- 构建脚本支持。
|
||||
|
||||
不过目前来看,构建脚本支持的语法并不统一,比如 Vite 对导入类型的断言有如下两种方式:
|
||||
|
||||
```ts
|
||||
import obj from "./something?raw"
|
||||
|
||||
// 或者自创的语法 blob 加载模式
|
||||
const modules = import.meta.glob(
|
||||
'./**/index.tsx',
|
||||
{
|
||||
assert: { type: 'raw' },
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
所以该导入断言至少在未来可以统一构建工具的语法,甚至让浏览器原生支持后,就不需要构建工具处理 import 断言了。
|
||||
|
||||
其实完全靠浏览器解析要走的路还有很远,因为一个复杂的前端工程至少有 3000~5000 个资源文件,目前生产环境不可能使用 bundless 一个个加载这些资源,因为速度太慢了。
|
||||
|
||||
## const 只读断言
|
||||
|
||||
```ts
|
||||
const obj = {
|
||||
a: 1
|
||||
} as const
|
||||
|
||||
obj.a = 2 // error
|
||||
```
|
||||
|
||||
通过该语法指定对象所有属性为 `readonly`。
|
||||
|
||||
## 利用 realpathSync.native 实现更快加载速度
|
||||
|
||||
对开发者没什么感知,就是利用 `realpathSync.native` 提升了 TS 加载速度。
|
||||
|
||||
## 片段自动补全增强
|
||||
|
||||
在 Class 成员函数与 JSX 属性的自动补全功能做了增强,在使用了最新版 TS 之后应该早已有了体感,比如 JSX 书写标签输入回车后,会自动根据类型补全内容,如:
|
||||
|
||||
```tsx
|
||||
<App cla />
|
||||
// ↑回车↓
|
||||
// <App className="|" />
|
||||
// ↑光标自动移到这里
|
||||
```
|
||||
|
||||
## 代码可以写在 super() 前了
|
||||
|
||||
JS 对 `super()` 的限制是此前不可以调用 this,但 TS 限制的更严格,在 `super()` 前写任何代码都会报错,这显然过于严格了。
|
||||
|
||||
现在 TS 放宽了校验策略,仅在 `super()` 前调用 this 会报错,而执行其他代码是被允许的。
|
||||
|
||||
这点其实早就该改了,这么严格的校验策略让我一度以为 JS 就是不允许 `super()` 前调用任何函数,但想想也觉得不合理,因为 `super()` 表示调用父类的 `constructor` 函数,之所以不自动调用,而需要手动调用 `super()` 就是为了开发者可以灵活决定哪些逻辑在父类构造函数前执行,所以 TS 之前一刀切的行为实际上导致 `super()` 失去了存在的意义,成为一个没有意义的模版代码。
|
||||
|
||||
## 类型收窄对解构也生效了
|
||||
|
||||
这个特性真的很厉害,即解构后类型收窄依然生效。
|
||||
|
||||
此前,TS 的类型收窄已经很强大了,可以做到如下判断:
|
||||
|
||||
```ts
|
||||
function foo(bar: Bar) {
|
||||
if (bar.a === '1') {
|
||||
bar.b // string 类型
|
||||
} else {
|
||||
bar.b // number 类型
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
但如果提前把 a、b 从 bar 中解构出来就无法自动收窄了。现在该问题也得到了解决,以下代码也可以正常生效了:
|
||||
|
||||
```ts
|
||||
function foo(bar: Bar) {
|
||||
const { a, b } = bar
|
||||
if (a === '1') {
|
||||
b // string 类型
|
||||
} else {
|
||||
b // number 类型
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 深度递归类型检查优化
|
||||
|
||||
下面的赋值语句会产生异常,原因是属性 prop 的类型不匹配:
|
||||
|
||||
```ts
|
||||
interface Source {
|
||||
prop: string;
|
||||
}
|
||||
|
||||
interface Target {
|
||||
prop: number;
|
||||
}
|
||||
|
||||
function check(source: Source, target: Target) {
|
||||
target = source;
|
||||
// error!
|
||||
// Type 'Source' is not assignable to type 'Target'.
|
||||
// Types of property 'prop' are incompatible.
|
||||
// Type 'string' is not assignable to type 'number'.
|
||||
}
|
||||
```
|
||||
|
||||
这很好理解,从报错来看,TS 也会根据递归检测的方式查找到 prop 类型不匹配。但由于 TS 支持泛型,如下写法就是一种无限递归的例子:
|
||||
|
||||
```ts
|
||||
interface Source<T> {
|
||||
prop: Source<Source<T>>;
|
||||
}
|
||||
|
||||
interface Target<T> {
|
||||
prop: Target<Target<T>>;
|
||||
}
|
||||
|
||||
function check(source: Source<string>, target: Target<number>) {
|
||||
target = source;
|
||||
}
|
||||
```
|
||||
|
||||
实际上不需要像官方说明写的这么复杂,哪怕是 `props: Source<T>` 也足以让该例子无限递归下去。TS 为了确保该情况不会出错,做了递归深度判断,过深的递归会终止判断,但这会带来一个问题,即无法识别下面的错误:
|
||||
|
||||
```ts
|
||||
interface Foo<T> {
|
||||
prop: T;
|
||||
}
|
||||
|
||||
declare let x: Foo<Foo<Foo<Foo<Foo<Foo<string>>>>>>;
|
||||
declare let y: Foo<Foo<Foo<Foo<Foo<string>>>>>;
|
||||
|
||||
x = y;
|
||||
```
|
||||
|
||||
为了解决这一问题,TS 做了一个判断:递归保护仅对递归写法的场景生效,而上面这个例子,虽然也是很深层次的递归,但因为是一个个人肉写出来的,TS 也会不厌其烦的一个个递归下去,所以该场景可以正确 Work。
|
||||
|
||||
这个优化的核心在于,TS 可以根据代码结构解析哪些是 “非常抽象/启发式” 写法导致的递归,哪些是一个个枚举产生的递归,并对后者的递归深度检查进行豁免。
|
||||
|
||||
## 增强的索引推导
|
||||
|
||||
下面的官方文档给出的例子,一眼看上去比较复杂,我们来拆解分析一下:
|
||||
|
||||
```ts
|
||||
interface TypeMap {
|
||||
"number": number;
|
||||
"string": string;
|
||||
"boolean": boolean;
|
||||
}
|
||||
|
||||
type UnionRecord<P extends keyof TypeMap> = { [K in P]:
|
||||
{
|
||||
kind: K;
|
||||
v: TypeMap[K];
|
||||
f: (p: TypeMap[K]) => void;
|
||||
}
|
||||
}[P];
|
||||
|
||||
function processRecord<K extends keyof TypeMap>(record: UnionRecord<K>) {
|
||||
record.f(record.v);
|
||||
}
|
||||
|
||||
// This call used to have issues - now works!
|
||||
processRecord({
|
||||
kind: "string",
|
||||
v: "hello!",
|
||||
|
||||
// 'val' used to implicitly have the type 'string | number | boolean',
|
||||
// but now is correctly inferred to just 'string'.
|
||||
f: val => {
|
||||
console.log(val.toUpperCase());
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
该例子的目的是实现 `processRecord` 函数,该函数通过识别传入参数 `kind` 来自动推导回调函数 `f` 中 `value` 的类型。
|
||||
|
||||
比如 `kind: "string"`,那么 `val` 就是字符串类型,`kind: "number"`,那么 `val` 就是数字类型。
|
||||
|
||||
因为 TS 这次更新解决了之前无法识别 `val` 类型的问题,我们不需要关心 TS 是怎么解决的,只要记住 TS 可以正确识别该场景(有点像围棋的定式,对于经典例子最好逐一学习),并且理解该场景是如何构造的。
|
||||
|
||||
如何做到呢?首先定义一个类型映射:
|
||||
|
||||
```ts
|
||||
interface TypeMap {
|
||||
"number": number;
|
||||
"string": string;
|
||||
"boolean": boolean;
|
||||
}
|
||||
```
|
||||
|
||||
之后定义最终要的函数 `processRecord`:
|
||||
|
||||
```ts
|
||||
function processRecord<K extends keyof TypeMap>(record: UnionRecord<K>) {
|
||||
record.f(record.v);
|
||||
}
|
||||
```
|
||||
|
||||
这里定义了一个泛型 K,`K extends keyof TypeMap` 等价于 `K extends 'number' | 'string' | 'boolean'`,所以这里是限定了以下泛型 K 的取值范围,值为这三个字符串之一。
|
||||
|
||||
重点来了,参数 `record` 需要根据传入的 `kind` 决定 `f` 回调函数参数类型。我们先想象以下 `UnionRecord` 类型怎么写:
|
||||
|
||||
```ts
|
||||
type UnionRecord<K extends keyof TypeMap> = {
|
||||
kind: K;
|
||||
v: TypeMap[K];
|
||||
f: (p: TypeMap[K]) => void;
|
||||
}
|
||||
```
|
||||
|
||||
如上,自然的想法是定义一个泛型 K,这样 `kind` 与 `f`, `p` 类型都可以表示出来,这样 `processRecord<K extends keyof TypeMap>(record: UnionRecord<K>)` 的 `UnionRecord<K>` 就表示了将当前接收到的实际类型 K 传入 `UnionRecord`,这样 `UnionRecord` 就知道实际处理什么类型了。
|
||||
|
||||
本来到这里该功能就已经结束了,但官方给的 `UnionRecord` 定义稍有些不同:
|
||||
|
||||
```ts
|
||||
type UnionRecord<P extends keyof TypeMap> = { [K in P]:
|
||||
{
|
||||
kind: K;
|
||||
v: TypeMap[K];
|
||||
f: (p: TypeMap[K]) => void;
|
||||
}
|
||||
}[P];
|
||||
```
|
||||
|
||||
这个例子特意提升了一个复杂度,用索引的方式绕了一下,可能之前 TS 就无法解析这种形式吧,总之现在这个写法也被支持了。我们看一下为什么这个写法与上面是等价的,上面的写法简化一下如下:
|
||||
|
||||
```ts
|
||||
type UnionRecord<P extends keyof TypeMap> = {
|
||||
[K in P]: X
|
||||
}[P];
|
||||
```
|
||||
|
||||
可以解读为,`UnionRecord` 定义了一个泛型 P,该函数从对象 `{ [K in P]: X }` 中按照索引(或理解为下标) `[P]` 取得类型。而 `[K in P]` 这种描述对象 Key 值的类型定义,等价于定义了复数个类型,由于正好 `P extends keyof TypeMap`,你可以理解为类型展开后是这样的:
|
||||
|
||||
```ts
|
||||
type UnionRecord<P extends keyof TypeMap> = {
|
||||
'number': X,
|
||||
'string': X,
|
||||
'boolean': X
|
||||
}[P];
|
||||
```
|
||||
|
||||
而 P 是泛型,由于 `[K in P]` 的定义,所以必定能命中上面其中的一项,所以实际上等价于下面这个简单的写法:
|
||||
|
||||
```ts
|
||||
type UnionRecord<K extends keyof TypeMap> = {
|
||||
kind: K;
|
||||
v: TypeMap[K];
|
||||
f: (p: TypeMap[K]) => void;
|
||||
}
|
||||
```
|
||||
|
||||
## 参数控制流分析
|
||||
|
||||
这个特性字面意思翻译挺奇怪的,还是从代码来理解吧:
|
||||
|
||||
```ts
|
||||
type Func = (...args: ["a", number] | ["b", string]) => void;
|
||||
|
||||
const f1: Func = (kind, payload) => {
|
||||
if (kind === "a") {
|
||||
payload.toFixed(); // 'payload' narrowed to 'number'
|
||||
}
|
||||
if (kind === "b") {
|
||||
payload.toUpperCase(); // 'payload' narrowed to 'string'
|
||||
}
|
||||
};
|
||||
|
||||
f1("a", 42);
|
||||
f1("b", "hello");
|
||||
```
|
||||
|
||||
如果把参数定义为数组且使用或并列枚举时,其实就潜在包含了一个运行时的类型收窄。比如当第一个参数值为 `a` 时,第二个参数类型就确定为 `number`,第一个参数值为 `b` 时,第二个参数类型就确定为 `string`。
|
||||
|
||||
值得注意的是,这种类型推导是从前到后的,因为参数是自左向右传递的,所以是前面推导出后面,而不能是后面推导出前面(比如不能理解为,第二个参数为 `number` 类型,那第一个参数的值就必须为 `a`)。
|
||||
|
||||
## 移除 JSX 编译时产生的非必要代码
|
||||
|
||||
JSX 编译时干掉了最后一个没有意义的 `void 0`,减少了代码体积:
|
||||
|
||||
```js
|
||||
- export const el = _jsx("div", { children: "foo" }, void 0);
|
||||
+ export const el = _jsx("div", { children: "foo" });
|
||||
```
|
||||
|
||||
由于改动很小,所以可以借机学习一下 TS 源码是怎么修改的,这是 [PR DIFF 地址](https://github.com/microsoft/TypeScript/pull/47467/files#)。
|
||||
|
||||
可以看到,修改位置是 `src/compiler/transformers/jsx.ts` 文件,改动逻辑为移除了 `factory.createVoidZero()` 函数,该函数正如其名,会创建末尾的 `void 0`,除此之外就是大量的 tests 文件修改,其实理解了源码上下文,这种修改并不难。
|
||||
|
||||
## JSDoc 校验提示
|
||||
|
||||
JSDoc 注释由于与代码是分离的,随着不断迭代很容易与实际代码产生分叉:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* @param x {number} The first operand
|
||||
* @param y {number} The second operand
|
||||
*/
|
||||
function add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
```
|
||||
|
||||
现在 TS 可以对命名、类型等不一致给出提示了。顺便说一句,用了 TS 就尽量不要用 JSDoc,毕竟代码和类型分离随时有不一致的风险产生。
|
||||
|
||||
## 总结
|
||||
|
||||
从这两个更新来看,TS 已经进入成熟期,但 TS 在泛型类的问题上依然还处于早期阶段,有大量复杂的场景无法支持,或者没有优雅的兼容方案,希望未来可以不断完善复杂场景的类型支持。
|
||||
|
||||
> 讨论地址是:[精读《Typescript 4.5-4.6 新特性》· Issue #408 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/408)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ Abstract Factory(抽象工厂)属于创建型模式,工厂类模式抽象
|
||||
|
||||
`AbstractProduct` 是产品抽象类,描述了比如方向盘、墙壁、折线图的创建方法,而 `ConcreteProduct` 是具体实现产品的方法,比如 `ConcreteProduct1` 创建的表格是用 `canvas` 画的,折线图是用 `G2` 画的,而 `ConcreteProduct2` 创建的表格是用 `div` 画的,折线图是用 `Echarts` 画的。
|
||||
|
||||
这样,当我们要拓展一个用 `Rcharts` 画的折线图,用 `svg` 画的表格,用 `div` 画的模态框组成的事件机制时,只需要再创建一个 `ConcreteFactory3` 做相应的实现即可,再将这个 `ConcreteFactory3` 传递给 `AbstractFactory`,并不需要修改 `AbstractFactory` 方法本身。
|
||||
这样,当我们要拓展一个用 `Echarts` 画的折线图,用 `svg` 画的表格,用 `div` 画的模态框组成的事件机制时,只需要再创建一个 `ConcreteFactory3` 做相应的实现即可,再将这个 `ConcreteFactory3` 传递给 `AbstractFactory`,并不需要修改 `AbstractFactory` 方法本身。
|
||||
|
||||
## 代码例子
|
||||
|
||||
|
||||
Reference in New Issue
Block a user