> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: SQL/232.SQL 聚合查询.md
================================================
SQL 为什么要支持聚合查询呢?
这看上去是个幼稚的问题,但我们还是一步步思考一下。数据以行为粒度存储,最简单的 SQL 语句是 `select * from test`,拿到的是整个二维表明细,但仅做到这一点远远不够,出于以下两个目的,需要 SQL 提供聚合函数:
1. 明细数据没有统计意义,比如我想知道今天的营业额一共有多少,而不太关心某桌客人消费了多少。
2. 虽然可以先把数据查到内存中再聚合,但在数据量非常大的情况下很容易把内存撑爆,可能一张表一天的数据量就有 10TB,而 10TB 数据就算能读到内存里,聚合计算可能也会慢到难以接受。
另外聚合本身也有一定逻辑复杂度,而 SQL 提供了聚合函数与分组聚合能力,可以方便快速的统计出有业务价值的聚合数据,这奠定了 SQL 语言的分析价值,因此大部分分析软件直接采用 SQL 作为直接面向用户的表达式。
## 聚合函数
常见的聚合函数有:
- COUNT:计数。
- SUM:求和。
- AVG:求平均值。
- MAX:求最大值。
- MIN:求最小值。
### COUNT
COUNT 用来计算有多少条数据,比如我们看 id 这一列有多少条:
```sql
SELECT COUNT(id) FROM test
```
但我们发现其实查任何一列的 COUNT 都是一样的,那传入 id 有什么意义呢?没必要特殊找一个具体列指代呀,所以也可以写成:
```sql
SELECT COUNT(*) FROM test
```
但这两者存在微妙差异。SQL 存在一种很特殊的值类型 `NULL`,如果 COUNT 指定了具体列,则统计时会跳过此列值为 `NULL` 的行,而 `COUNT(*)` 由于未指定具体列,所以就算包含了 `NULL`,甚至某一行所有列都为 `NULL`,也都会包含进来。所以 `COUNT(*)` 查出的结果一定大于等于 `COUNT(c1)`。
当然任何聚合函数都可以跟随查询条件 WHERE,比如:
```sql
SELECT COUNT(*) FROM test
WHERE is_gray = 1
```
### SUM
SUM 求和所有项,因此必须作用于数值字段,而不能用于字符串。
```sql
SELECT SUM(cost) FROM test
```
SUM 遇到 NULL 值时当 0 处理,因为这等价于忽略。
### AVG
AVG 求所有项均值,因此必须作用于数值字段,而不能用于字符串。
```sql
SELECT AVG(cost) FROM test
```
AVG 遇到 NULL 值时采用了最彻底的忽略方式,即 NULL 完全不参与分子与分母的计算,就像这一行数据不存在一样。
### MAX、MIN
MAX、MIN 分别求最大与最小值,与上面不同的是,也可以作用于字符串上,因此可以根据字母判断大小,从大到小依次对应 `a-z`,但即便能算,也没有实际意义且不好理解,因此不建议对字符串求极值。
```sql
SELECT MAX(cost) FROM test
```
### 多个聚合字段
虽然都是聚合函数,但 MAX、MIN 严格意义上不算是聚合函数,因为它们只是寻找了满足条件的行。可以看看下面两段查询结果的对比:
```sql
SELECT MAX(cost), id FROM test -- id: 100
SELECT SUM(cost), id FROM test -- id: 1
```
第一条查询可以找到最大值那一行的 id,而第二条查询的 id 是无意义的,因为不知道归属在哪一行,所以只返回了第一条数据的 id。
当然,如果同时计算 MAX、MIN,那么此时 id 也只返回第一条数据的值,因为这个查询结果对应了复数行:
```sql
SELECT MAX(cost), MIN(cost), id FROM test -- id: 1
```
基于这些特性,最好不要混用聚合与非聚合,也就是一条查询一旦有一个字段是聚合的,那么所有字段都要聚合。
现在很多 BI 引擎的自定义字段都有这条限制,因为混用聚合与非聚合在自定义内存计算时处理起来边界情况很多,虽然 SQL 能支持,但业务自定义的函数可能不支持。
## 分组聚合
分组聚合就是 GROUP BY,其实可以把它当作一种高级的条件语句。
举个例子,查询每个国家的 GDP 总量:
```sql
SELECT SUM(GDP) FROM amazing_table
GROUP BY country
```
返回的结果就会按照国家进行分组,这时,聚合函数就变成了在组内聚合。
其实如果我们只想看中、美的 GDP,用非分组也可以查,只是要分成两条 SQL:
```sql
SELECT SUM(GDP) FROM amazing_table
WHERE country = '中国'
SELECT SUM(GDP) FROM amazing_table
WHERE country = '美国'
```
所以 GROUP BY 也可理解为,将某个字段的所有可枚举的情况都查了出来,并整合成一张表,每一行代表了一种枚举情况,不需要分解为一个个 WHERE 查询了。
### 多字段分组聚合
GROUP BY 可以对多个维度使用,含义等价于表格查询时行/列拖入多个维度。
上面是 BI 查询工具视角,如果没有上下文,可以看下面这个递进描述:
- 按照多个字段进行分组聚合。
- 多字段组合起来成为唯一 Key,即 `GROUP BY a,b` 表示 a,b 合在一起描述一个组。
- `GROUP BY a,b,c` 查询结果第一列可能看到许多重复的 a 行,第二列看到重复 b 行,但在同一个 a 值内不会重复,c 在 b 行中同理。
下面是一个例子:
```sql
SELECT SUM(GDP) FROM amazing_table
GROUP BY province, city, area
```
查询结果为:
```text
浙江 杭州 余杭区
浙江 杭州 西湖区
浙江 宁波 海曙区
浙江 宁波 江北区
北京 .........
```
### GROUP BY + WHERE
WHERE 是根据行进行条件筛选的。因此 GROUP BY + WHERE 并不是在组内做筛选,而是对整体做筛选。
但由于按行筛选,其实组内或非组内结果都完全一样,所以我们几乎无法感知这种差异:
```sql
SELECT SUM(GDP) FROM amazing_table
GROUP BY province, city, area
WHERE industry = 'internet'
```
然而,忽略这个差异会导致我们在聚合筛选时碰壁。
比如要筛选出平均分大于 60 学生的成绩总和,如果不使用子查询,是无法在普通查询中在 WHERE 加聚合函数实现的,比如下面就是一个语法错误的例子:
```sql
SELECT SUM(score) FROM amazing_table
WHERE AVG(score) > 60
```
不要幻想上面的 SQL 可以执行成功,不要在 WHERE 里使用聚合函数。
### GROUP BY + HAVING
HAVING 是根据组进行条件筛选的。因此可以在 HAVING 使用聚合函数:
```sql
SELECT SUM(score) FROM amazing_table
GROUP BY class_name
HAVING AVG(score) > 60
```
上面的例子中可以正常查询,表示按照班级分组看总分,且仅筛选出平均分大于 60 的班级。
所以为什么 HAVING 可以使用聚合条件呢?因为 HAVING 筛选的是组,所以可以对组聚合后过滤掉不满足条件的组,这样是有意义的。而 WHERE 是针对行粒度的,聚合后全表就只有一条数据,无论过滤与否都没有意义。
但要注意的是,GROUP BY 生成派生表是无法利用索引筛选的,所以 WHERE 可以利用给字段建立索引优化性能,而 HAVING 针对索引字段不起作用。
## 总结
聚合函数 + 分组可以实现大部分简单 SQL 需求,在写 SQL 表达式时,需要思考这样的表达式是如何计算的,比如 `MAX(c1), c2` 是合理的,而 `SUM(c1), c2` 这个 `c2` 就是无意义的。
最后记住 WHERE 是 GROUP BY 之前执行的,HAVING 针对组进行筛选。
> 讨论地址是:[精读《SQL 聚合查询》· Issue #401 · ascoders/weekly](https://github.com/ascoders/weekly/issues/401)
**如果你想参与讨论,请 [点击这里](https://github.com/ascoders/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: SQL/233.SQL 复杂查询.md
================================================
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),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: SQL/234.SQL CASE 表达式.md
================================================
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),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: SQL/235.SQL 窗口函数.md
================================================
窗口函数形如:
```sql
表达式 OVER (PARTITION BY 分组字段 ORDER BY 排序字段)
```
有两个能力:
1. 当表达式为 `rank()` `dense_rank()` `row_number()` 时,拥有分组排序能力。
2. 当表达式为 `sum()` 等聚合函数时,拥有累计聚合能力。
无论何种能力,**窗口函数都不会影响数据行数,而是将计算平摊在每一行**。
这两种能力需要区分理解。
## 底表
以上是示例底表,共有 8 条数据,城市1、城市2 两个城市,下面各有地区1~4,每条数据都有该数据的人口数。
## 分组排序
如果按照人口排序,`ORDER BY people` 就行了,但如果我们想在城市内排序怎么办?
此时就要用到窗口函数的分组排序能力:
```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
```
其实从结果就可以猜到,这三个函数在处理排序遇到相同值时,对排名统计逻辑有如下差异:
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
```
可以看到,在每个 city 分组内,按照 people 排序后进行了 **累加**(相同的值会合并在一起),这就是 BI 工具一般说的 RUNNGIN_SUM 的实现思路,当然一般我们排序规则使用绝对不会重复的日期,所以不会遇到第一个红框中合并计算的问题。
累计函数还有 `avg()` `min()` 等等,这些都一样可以作用于窗口函数,其逻辑可以按照下图理解:
你可能有疑问,直接 `sum(上一行结果,下一行)` 不是更方便吗?为了验证猜想,我们试试 `avg()` 的结果:
可见,如果直接利用上一行结果的缓存,那么 avg 结果必然是不准确的,所以窗口累计聚合是每行重新计算的。当然也不排除对于 sum、max、min 做额外性能优化的可能性,但 avg 只能每行重头计算。
### 与 GROUP BY 组合使用
窗口函数是可以与 GROUP BY 组合使用的,遵循的规则是,窗口范围对后面的查询结果生效,所以其实并不关心是否进行了 GROUP BY。我们看下面的例子:
按照地区分组后进行累加聚合,是对 GROUP BY 后的数据行粒度进行的,而不是之前的明细行。
## 总结
窗口函数在计算组内排序或累计 GVM 等场景非常有用,我们只要牢记两个知识点就行了:
1. 分组排序要结合 PARTITION BY 才有意义。
2. 累计聚合作用于查询结果行粒度,支持所有聚合函数。
> 讨论地址是:[精读《SQL 窗口函数》· Issue #405 · ascoders/weekly](https://github.com/ascoders/weekly/issues/405)
**如果你想参与讨论,请 [点击这里](https://github.com/ascoders/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: SQL/236.SQL grouping.md
================================================
SQL grouping 解决 OLAP 场景总计与小计问题,其语法分为几类,但要解决的是同一个问题:
ROLLUP 与 CUBE 是封装了规则的 GROUPING SETS,而 GROUPING SETS 则是最原始的规则。
为了方便理解,让我们从一个问题入手,层层递进吧。
## 底表
以上是示例底表,共有 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
```
但两条 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)` 聚合计算总计。返回结果是:
可以看到,值为 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))
```
具体效果见下图:
可以看到,但凡是超级分组计算出来的字段都会识别为 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))
```
然后前端表格展示时,将第一行 “总计”、“小计” 单元格合并为 “总计”,就完成了总计这个 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),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: TS 类型体操/243.精读《Pick, Awaited, If...》.md
================================================
TS 强类型非常好用,但在实际运用中,免不了遇到一些难以描述,反复看官方文档也解决不了的问题,至今为止也没有任何一篇文档,或者一套教材可以解决所有犄角旮旯的类型问题。为什么会这样呢?因为 TS 并不是简单的注释器,而是一门图灵完备的语言,所以很多问题的解决方法藏在基础能力里,但你学会了基础能力又不一定能想到这么用。
解决该问题的最好办法就是多练,通过实际案例不断刺激你的大脑,让你养成 TS 思维习惯。所以话不多说,我们今天从 [type-challenges](https://github.com/type-challenges/type-challenges) 的 Easy 难度题目开始吧。
## 精读
### [Pick](https://github.com/type-challenges/type-challenges/blob/main/questions/00004-easy-pick/README.md)
手动实现内置 `Pick`” 那我就返回类型 `P`,否则返回 `never`”,这句话用 TS 描述就是:`T extends [infer P, ...infer Rest] ? P : never`。
### [Length of Tuple](https://github.com/type-challenges/type-challenges/blob/main/questions/00018-easy-tuple-length/README.md)
实现类型 `Length : P
: never
```
如果 `Promise ` 取到的 `P` 还形如 `Promise `。这里提到了递归,也就是 TS 类型处理可以是递归的,所以才有了后面版本做尾递归优化。
### [If](https://github.com/type-challenges/type-challenges/blob/main/questions/00268-easy-if/README.md)
实现类型 `If `,将两个数组类型连起来:
```ts
type Result = Concat<[1], [2]> // expected to be [1, 2]
```
由于 TS 支持数组解构语法,所以可以大胆的尝试这么写:
```ts
type Concat = [...P, ...Q]
```
考虑到 `Concat` 函数应该也能接收非数组类型,所以做一个判断,为了方便书写,把 `extends` 从泛型定义位置挪到 TS 类型推断的运行时:
```ts
// 本题答案
type Concat = [
...P extends any[] ? P : [P],
...Q extends any[] ? Q : [Q],
]
```
解决这题需要信念,相信 TS 可以像 JS 一样写逻辑。这些能力都是版本升级时渐进式提供的,所以需要不断阅读最新 TS 特性,快速将其理解为固化知识,其实还是有一定难度的。
### [Includes](https://github.com/type-challenges/type-challenges/blob/main/questions/00898-easy-includes/README.md)
用类型系统实现 `Includes `,将对象 `P` 中类型为 `Q` 的 key 保留:
```ts
type OnlyBoolean = PickByType<
{
name: string
count: number
isReadonly: boolean
isEnable: boolean
},
boolean
> // { isReadonly: boolean; isEnable: boolean; }
```
本题很简单,因为之前碰到 Remove Index Signature 题目时,我们用了 `K in keyof P as xxx` 来对 Key 位置进行进一步判断,所以只要 `P[K] extends Q` 就保留,否则返回 `never` 即可:
```ts
// 本题答案
type PickByType = {
[K in keyof P as P[K] extends Q ? K : never]: P[K]
}
```
### [StartsWith](https://github.com/type-challenges/type-challenges/blob/main/questions/02688-medium-startswith/README.md)
实现 `StartsWith
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: TS 类型体操/244.精读《Get return type, Omit, ReadOnly...》.md
================================================
解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 1~8 题。
## 精读
### [Get Return Type](https://github.com/type-challenges/type-challenges/blob/main/questions/00002-medium-return-type/README.md)
实现非常经典的 `ReturnType
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: TS 类型体操/245.精读《Promise.all, Replace, Type Lookup...》.md
================================================
解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 9~16 题。
## 精读
### [Promise.all](https://github.com/type-challenges/type-challenges/blob/main/questions/00020-medium-promise-all/README.md)
实现函数 `PromiseAll`,输入 PromiseLike,输出 `Promise`,将字符串 `From` 替换为 `To`:
```ts
type replaced = Replace<'types are fun!', 'fun', 'awesome'> // expected to be 'types are awesome!'
```
把 `From` 夹在字符串中间,前后用两个 `infer` 推导,最后输出时前后不变,把 `From` 换成 `To` 就行了:
```ts
// 本题答案
type Replace =
S extends `${infer A}${From}${infer B}` ? `${A}${To}${B}` : S
```
### [ReplaceAll](https://github.com/type-challenges/type-challenges/blob/main/questions/00119-medium-replaceall/README.md)
实现 `ReplaceAll`,将字符串 `From` 替换为 `To`:
```ts
type replaced = ReplaceAll<'t y p e s', ' ', ''> // expected to be 'types'
```
该题与上题不同之处在于替换全部,解法肯定是递归,关键是何时递归的判断条件是什么。经过一番思考,如果 `infer From` 能匹配到不就说明还可以递归吗?所以加一层三元判断 `From extends ''` 即可:
```ts
// 本题答案
type ReplaceAll =
From extends '' ? S : (
S extends `${infer A}${From}${infer B}` ? (
From extends '' ? `${A}${To}${B}` : `${A}${To}${ReplaceAll}`
) : S
)
```
补充一些细节:
1. 如果替换文本为空字符串需要跳过,否则会匹配第二个任意字符。
2. 为了防止替换完后结果可以再度匹配,对递归形式做一下调整,下次递归直接从剩余部分开始。
### [Append Argument](https://github.com/type-challenges/type-challenges/blob/main/questions/00191-medium-append-argument/README.md)
实现类型 `AppendArgument
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: TS 类型体操/246.精读《Permutation, Flatten, Absolute...》.md
================================================
解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 17~24 题。
## 精读
### [Permutation](https://github.com/type-challenges/type-challenges/blob/main/questions/00296-medium-permutation/README.md)
实现 `Permutation` 类型,将联合类型替换为可能的全排列:
```ts
type perm = Permutation<'A' | 'B' | 'C'>; // ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']
```
看到这题立马联想到 TS 对多个联合类型泛型处理是采用分配律的,在第一次做到 `Exclude` 题目时遇到过:
```ts
Exclude<'a' | 'b', 'a' | 'c'>
// 等价于
Exclude<'a', 'a' | 'c'> | Exclude<'b', 'a' | 'c'>
```
所以这题如果能 “递归触发联合类型分配率”,就有戏解决啊。但触发的条件必须存在两个泛型,而题目传入的只有一个,我们只好创造第二个泛型,使其默认值等于第一个:
```ts
type Permutation = S extends `${infer S}${infer E}` ? LengthOfString = S extends `${infer F}${infer R}` ? (
Lowercase = S extends `-${infer Rest}` ? Rest : S
```
分开写就非常容易懂了,首先 `KebabCase` 每次递归取第一个字符,如何判断这个字符是大写呢?只要小写不等于原始值就是大写,所以判断条件就是 `Lowercase
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: TS 类型体操/247.精读《Diff, AnyOf, IsUnion...》.md
================================================
解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 25~32 题。
## 精读
### [Diff](https://github.com/type-challenges/type-challenges/blob/main/questions/00645-medium-diff/README.md)
实现 `Diff`,返回一个新对象,类型为两个对象类型的 Diff:
```ts
type Foo = {
name: string
age: string
}
type Bar = {
name: string
age: string
gender: number
}
Equal = S extends `${infer A}${C}${infer B}` ?
`${A}${DropChar}` : S
```
## 总结
写到这,越发觉得 TS 虽然具备图灵完备性,但在逻辑处理上还是不如 JS 方便,很多设计计算逻辑的题目的解法都不是很优雅。
但是解决这类题目有助于强化对 TS 基础能力组合的理解与综合运用,在解决实际类型问题时又是必不可少的。
> 讨论地址是:[精读《Diff, AnyOf, IsUnion...》· Issue #429 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/429)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
================================================
FILE: TS 类型体操/248.精读《MinusOne, PickByType, StartsWith...》.md
================================================
解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 33~40 题。
## 精读
### [MinusOne](https://github.com/type-challenges/type-challenges/blob/main/questions/02257-medium-minusone/README.md)
用 TS 实现 `MinusOne` 将一个数字减一:
```ts
type Zero = MinusOne<1> // 0
type FiftyFour = MinusOne<55> // 54
```
TS 没有 “普通” 的运算能力,但涉及数字却有一条生路,即 TS 可通过 `['length']` 访问数组长度,几乎所有数字计算都是通过它推导出来的。
这道题,我们只要构造一个长度为泛型长度 -1 的数组,获取其 `['length']` 属性即可,但该方案有一个硬伤,无法计算负值,因为数组长度不可能小于 0:
```ts
// 本题答案
type MinusOne