天天看點

HiveSQL分析函數實踐詳解

作者:大資料推薦雜談
目錄

一、視窗函數概述:

1.視窗函數的分類

2.視窗函數與普通聚合函數的差別:

二、視窗函數的基本用法

1.基本文法

2.設定視窗的方法

1)window_name

2)partition by 子句

3) order by子ve句

4)rows 指定視窗大小

3.開窗函數中加order by 和 不加 order by的差別

三、視窗函數用法舉例

1.序号函數:row_number() / rank() / dense_rank()(面試重點)

2.分布函數:percent_rank() / cume_dist()

3.前後函數 lag(expr,n,defval)、lead(expr,n,defval)(面試重點)

4.頭尾函數:FIRST_VALUE(expr),LAST_VALUE(expr)

5.聚合函數+視窗函數聯合使用

四、面試題

1.使用者行為分析

2.學生成績分析

一、視窗函數概述:

視窗函數也稱為OLAP函數,OLAP 是OnLine Analytical Processing 的簡稱,意思是對資料庫資料進行實時分析處理。例如,市場分析、建立财務報表、建立計劃等日常性商務工作。視窗函數就是為了實作OLAP 而添加的标準SQL 功能。

1. 視窗函數的分類

按照功能劃分:

  • 序号函數:row_number() / rank() / dense_rank()
  • 分布函數:percent_rank() / cume_dist()
  • 前後函數:lag() / lead()
  • 頭尾函數:first_val() / last_val()
  • 聚合函數+視窗函數聯合:
    • 求和 sum() over()
    • 求最大/小 max()/min() over()
    • 求平均 avg() over()
  • 其他函數:nth_value() / nfile()

如上,視窗函數的用法多種多樣,不僅有專門的的視窗函數,還可以與聚合函數配合使用。

2. 視窗函數與普通聚合函數的差別:

聚合函數是将多條記錄聚合為一條;視窗函數是每條記錄都會執行,有幾條記錄執行完還是幾條。

視窗函數兼具GROUP BY 子句的分組功能以及ORDER BY 子句的排序功能。但是,PARTITION BY 子句并不具備 GROUP BY 子句的彙總功能。

舉例:若原表中有id一樣的10行資料,使用GROUP BY,傳回的結果是将多條記錄聚合成一條;而使用 rank() 等視窗函數并不會減少原表中 記錄的行數,結果中仍然包含 10 行資料。

視窗函數兼具分組和排序兩種功能。

二、視窗函數的基本用法

如有基礎資料:

drop table if exists exam_record;
CREATE TABLE exam_record (
    id int PRIMARY KEY AUTO_INCREMENT COMMENT '自增ID',
    uid int NOT NULL COMMENT '使用者ID',
    exam_id int NOT NULL COMMENT '試卷ID',
    start_time datetime NOT NULL COMMENT '開始時間',
    submit_time datetime COMMENT '送出時間',
    score tinyint COMMENT '得分'
)CHARACTER SET utf8 COLLATE utf8_general_ci;
 
INSERT INTO exam_record(uid,exam_id,start_time,submit_time,score) VALUES
(1006, 9003, '2021-09-07 10:01:01', '2021-09-07 10:21:02', 84),
(1006, 9001, '2021-09-01 12:11:01', '2021-09-01 12:31:01', 89),
(1006, 9002, '2021-09-06 10:01:01', '2021-09-06 10:21:01', 81),
(1005, 9002, '2021-09-05 10:01:01', '2021-09-05 10:21:01', 81),
(1005, 9001, '2021-09-05 10:31:01', '2021-09-05 10:51:01', 81),
(1004, 9002, '2021-09-05 10:01:01', '2021-09-05 10:21:01', 71),
(1004, 9001, '2021-09-05 10:31:01', '2021-09-05 10:51:01', 91),
(1004, 9002, '2021-09-05 10:01:01', '2021-09-05 10:21:01', 80),
(1004, 9001, '2021-09-05 10:31:01', '2021-09-05 10:51:01', 80);
 
select uid,score from exam_record;
           
HiveSQL分析函數實踐詳解

1. 基本文法

<視窗函數> OVER ([PARTITION BY <列名清單>] ORDER BY <排序列名清單> [rows between 開始位置 and 結束位置])
           

其中:

<視窗函數>:指需要使用的分析函數,如row_number()、sum()等。

over() : 用來指定函數執行的視窗範圍,這個資料視窗大小可能會随着行的變化而變化;

如果括号中什麼都不寫,則意味着視窗包含滿足WHERE條件的所有行,視窗函數基于所有行進行計算。如:

select 
    uid,
    score,
    sum(score) over() as sum_score
from exam_record 
           

結果:

HiveSQL分析函數實踐詳解

sum(score) over() as sum_score 會聚合所有的資料,将結果接到每一行的後面(視窗函數不會改變結果原表行數)。

2. 設定視窗的方法

如果不為空,則支援以下4中文法來設定視窗。

1)window_name

給視窗指定一個别名。如果SQL中涉及的視窗較多,采用别名可以看起來更清晰易讀,如:

--給視窗指定别名:WINDOW my_window_name AS (PARTITION BY uid ORDER BY score)
SELECT
    uid,
    score,
    rank() OVER my_window_name AS rk_num,
    row_number() OVER my_window_name AS row_num
FROM exam_record
WINDOW my_window_name AS (PARTITION BY uid ORDER BY score)
           

2)partition by 子句

視窗按照哪些字段進行分組,視窗函數在不同的分組上分别執行,如:

執行個體1:

SELECT
    uid,
    score,
    sum(score) OVER(PARTITION BY uid) AS sum_score
FROM exam_record
           

結果:

HiveSQL分析函數實踐詳解

sum(score) OVER(PARTITION BY uid) AS sum_score 會按照 uid 分組,分别求和,展示在每個分組的末尾。

如果我想看某個uid有多少行記錄,并标明序号該如何實作?使用序号函數row_number()請看:

SELECT
    uid,
    score,
    row_number() OVER(PARTITION BY uid) AS row_num
FROM exam_record
           
HiveSQL分析函數實踐詳解

可以看到,row_number()按照uid分組并從上到下按照順序标号。我們看到1004中的score是無序的,如果想按照score降序排名應該怎麼做呢?(實際場景:成績排名)

可以結合 order by 子句實作

3)order by子句

按照哪些字段進行排序,視窗函數将按照排序後的記錄順序進行編号,如:

SELECT
    uid,
    score,
    row_number() OVER(PARTITION BY uid ORDER BY score desc) AS row_num
FROM exam_record
           
HiveSQL分析函數實踐詳解

這樣就實作了每個uid内的分數降序排名,order by 後面可以跟多個列名,大家可以試一試。

當order by 與聚合類函數連用時,特别需要注意了解,如下面幾個例子:

先看前面的例子,單獨使用 partition by uid

SELECT
    uid,
    score,
    sum(score) OVER(PARTITION BY uid) AS sum_score
FROM exam_record
           

結果:

HiveSQL分析函數實踐詳解

單獨使用order by uid

SELECT
    uid,
    score,
    sum(score) OVER(ORDER BY uid) AS sum_score
FROM exam_record
           

結果:

HiveSQL分析函數實踐詳解

注意觀察uid 從1004->1005的變化,兩條SQL的結果有什麼不同:

  • partition by 按照uid分組,分别對score求和,”接到每一行的末尾“
    • 分組内求和,分組間互相獨立。
  • order by 按照uid排序,對”序号“相同的元素進行求和,不同”序号“的數累加求和
    • 如果将”序号“認為是分組的話,個人了解這是一個分組求和并累加的過程
    • 即分組内求和,分組間累加。

再看,order by score 的例子

SELECT
    uid,
    score,
    sum(score) OVER(ORDER BY score) AS sum_score
FROM exam_record
           

結果:

HiveSQL分析函數實踐詳解

總結一下:

如果使⽤環境為hive,over()開窗函數前分排序函數和聚合函數兩種。

當為排序函數,如row_number(),rank()等時,over中的order by隻起到窗⼝内排序作⽤。

當為聚合函數,如max,min,count等時,over中的order by不僅起到窗⼝内排序,還起到窗⼝内從目前⾏到之前所有⾏的聚合(多了⼀個範圍)。

4)rows 指定視窗大小

a.先看個例子,按照順序,求score的平均值:

SELECT
    uid,
    score,
    avg(score) OVER(ORDER BY score desc) AS avg_score
FROM exam_record
           

注意score相同的部分:

HiveSQL分析函數實踐詳解

如果想要sql先按照score降序排列,每一行計算從第一行到目前行的score平均值,該怎麼計算呢?——想辦法做一個不重複的key

實作:

SELECT
    uid,
    score,
    row_score,
    avg(score) OVER(ORDER BY row_score) AS avg_score
FROM (
    SELECT
        uid,
        score,
        row_number() OVER(ORDER BY score desc) AS row_score
    FROM exam_record
) res
           
HiveSQL分析函數實踐詳解

現在改下需求,希望求"我與前兩名的平均值"應該怎麼實作呢?

分析一下,"我與前兩名"指的是目前行以及前兩行總共三行資料求平均,也就是說,我們需要限定視窗的範圍或者視窗大小。

b.引入視窗架構

指定視窗大小,又稱為視窗架構。架構是對視窗進行進一步分區,架構有兩種範圍限定方式:

一種是使用 ROWS 子句,通過指定目前行之前或之後的固定數目的行來限制分區中的行數。

另一種是使用 RANGE 子句,按照排列序列的目前值,根據相同值來确定分區中的行數。

文法:

ORDER BY 字段名 RANGE|ROWS 邊界規則0 | [BETWEEN 邊界規則1 AND 邊界規則2]
           

RANGE | ROWS的差別是什麼?

RANGE表示按照值的範圍進行範圍的定義,而ROWS表示按照行的範圍進行範圍的定義;邊界規則的可取值見下表:

HiveSQL分析函數實踐詳解

需要注意:

  • 當使用架構時,必須要有 order by 子句,如果僅指定了order by 子句而未指定架構,那麼預設架構将采用 RANGE UNBOUNDED PRECEDING AND CURRENT ROW(表示目前行以及一直到第一行的資料)。
  • 如果視窗函數沒有指定 order by 子句,也就不存在 ROWS/RANGE 視窗的計算。
  • PS: RANGE 隻支援使用 UNBOUNDED 和 CURRENT ROW 視窗架構分隔符。

OK,回到前面的需求:求"我與前兩名的平均值"。因為要"我與前兩名",是以我們會用到規則 2 PRECEDING

之前2行的記錄
之前1行的記錄
自身(目前記錄)
 
SELECT
    uid,
    score,
    avg(score) OVER(ORDER BY score desc rows 2 PRECEDING) AS avg_score
FROM exam_record
           
HiveSQL分析函數實踐詳解

如果要求目前行及前後一行呢?

之前1行的記錄
自身(目前記錄)
之後1行的記錄
 
SELECT
    uid,
    score,
    avg(score) OVER(ORDER BY score desc rows between 1 PRECEDING and 1 FOLLOWING) AS avg_score
FROM exam_record
           

結果略~

其他組合舉例:

1.第一行到目前行
ORDER BY score desc rows UNBOUNDED PRECEDING
 
2.第一行到前一行(不含目前行)
ORDER BY score desc rows between UNBOUNDED PRECEDING and 1 PRECEDING 
 
3.第一行到後一行(包含目前行)
ORDER BY score desc rows between UNBOUNDED PRECEDING and 1 FOLLOWING
 
4.目前行到最後一行
ORDER BY score desc rows between CURRENT ROW and UNBOUNDED FOLLOWING 
注意,這種寫法是錯誤的
ORDER BY score desc rows UNBOUNDED FOLLOWING -- 錯誤示範
 
5.前一行到最後一行(包含目前行)
ORDER BY score desc rows between 1 PRECEDING and UNBOUNDED FOLLOWING
 
6.後一行到最後一行(不含目前行)
ORDER BY score desc rows between 1 FOLLOWING and UNBOUNDED FOLLOWING
 
7.前一行到後一行(包含目前行) 
ORDER BY score desc rows between 1 PRECEDING and 1 FOLLOWING 
           

3. 開窗函數中加order by 和 不加 order by的差別

如果使⽤環境為hive,over()開窗函數前分排序函數和聚合函數兩種。

當為排序函數,如row_number(),rank()等時,over中的order by隻起到窗⼝内排序作⽤。

當為聚合函數,如max,min,count等時,over中的order by不僅起到窗⼝内排序,還起到窗⼝内從目前⾏到之前所有⾏的聚合(多了⼀個範圍)。

如:

-- sql ①
select id, dept, salary, min(salary) over(partition by dept) min_sal from dept;  
 
-- sql ②
select id, dept, salary, min(salary) over(partition by dept order by id) min_sal from dept; 
           

上⾯①②中的min_salary字段的值會不⼀樣,原因是②中使⽤order by後,等同于 min(salary) over(partition by dept order by userid range between unbounded preceding and current row ),當然可以在order by後使⽤架構⼦句,即rows,range等,如果沒有寫架構⼦句,就預設在窗⼝範圍中目前⾏到之前所有⾏的資料進⾏統計。

再看個例子:

# 表資料為:exam_record(uid,exam_id,start_time,end_time,score)
# (1001, 9001, '2021-09-01 09:01:01', '2021-09-01 09:31:00', 100),
# (1001, 9001, '2021-09-02 09:01:01', '2021-09-01 09:31:00', 100),
# (1001, 9001, '2021-09-03 09:01:01', '2021-09-01 09:31:00', 100),
# (1002, 9001, '2021-09-01 09:01:01', '2021-09-01 09:31:00', 100),
# (1002, 9001, '2021-09-01 09:01:01', '2021-09-01 09:31:00', 100),
# (1002, 9001, '2021-09-02 09:01:01', '2021-09-01 09:31:00', 100); 
-- 執行下面的sql
select 
    uid,
    exam_id,
    start_time,
    sum(score) over(partition by uid) as one,
    sum(score) over(partition by uid order by start_time) as two
from exam_record
           

得到結果:

HiveSQL分析函數實踐詳解

需要注意表中标注的部分

三、視窗函數用法舉例

1. 序号函數:row_number() / rank() / dense_rank()(面試重點)

三者差別:

  • RANK(): 并列排序,跳過重複序号——1、1、3
  • ROW_NUMBER(): 順序排序——1、2、3
  • DENSE_RANK(): 并列排序,不跳過重複序号——1、1、2
--給視窗指定别名:WINDOW my_window_name AS (PARTITION BY uid ORDER BY score)
SELECT
    uid,
    score,
    rank() OVER my_window_name AS rk_num,
    row_number() OVER my_window_name AS row_num
FROM exam_record
WINDOW my_window_name AS (PARTITION BY uid ORDER BY score)
           
HiveSQL分析函數實踐詳解

我們來探索一下,如果不使用視窗函數,如何實作分數排序呢?(使用自連接配接的方法)

-- 相當于 rank()
SELECT 
    P1.uid,
    P1.score,
    (SELECT 
        COUNT(P2.score)
    FROM exam_record P2 
    WHERE P2.score > P1.score) + 1 AS rank_1
FROM exam_record P1
ORDER BY rank_1;
           
HiveSQL分析函數實踐詳解

這裡1234447..,如果想要7改為5呢,不跳過位次。相當于DENSE_RANK 函數。

隻需要改 COUNT(P2.score) 為 COUNT(distinct P2.score) 即可。

2. 分布函數:percent_rank() / cume_dist()

1)percent_rank():

percent_rank()函數将某個數值在資料集中的排位作為資料集的百分比值傳回,此處的百分比值的範圍為 0 到 1。此函數可用于計算值在資料集内的相對位置。如班級成績為例,傳回的百分數30%表示某個分數排在班級總分排名的前30%。

每行按照公式(rank-1) / (rows-1)進行計算。其中,rank為RANK()函數産生的序号,rows為目前視窗的記錄總行數。

SELECT
    uid,
    score,
    rank() OVER my_window_name AS rank_num,
    PERCENT_RANK() OVER my_window_name AS prk
FROM exam_record
WINDOW my_window_name AS (ORDER BY score desc)
           
HiveSQL分析函數實踐詳解

2)cume_dist():

如果按升序排列,則統計:小于等于目前值的行數/總行數。

如果是降序排列,則統計:大于等于目前值的行數/總行數。

如:查詢小于等于目前成績(score)的比例。

SELECT
    uid,
    score,
    rank() OVER my_window_name AS rank_num,
    cume_dist() OVER my_window_name AS cume_dist_num
FROM exam_record
WINDOW my_window_name AS (ORDER BY score asc)
           
HiveSQL分析函數實踐詳解

3. 前後函數 lag(expr,n,defval)、lead(expr,n,defval)(面試重點)

Lag()和Lead()分析函數可以在同一次查詢中取出同一字段的前N行的資料(Lag)和後N行的資料(Lead)作為獨立的列。

在實際應用當中,若要用到取今天和昨天的某字段內插補點時,Lag()和Lead()函數的應用就顯得尤為重要。當然,這種操作可以用表的自連接配接實作,但是LAG()和LEAD()與left join、rightjoin等自連接配接相比,效率更高,SQL更簡潔。下面我就對這兩個函數做一個簡單的介紹。

函數文法如下:

lag( exp_str,offset,defval) over(partition by .. order by …)
 
lead(exp_str,offset,defval) over(partition by .. order by …)
           

其中

  • exp_str 是字段名
  • Offset 是偏移量,即是上1個或上N個的值,假設目前行在表中排在第5行,offset 為3,則表示我們所要找的資料行就是表中的第2行(即5-3=2)。
  • Defval 預設值,當兩個函數取 上N 或者 下N 個值,當在表中從目前行位置向前數N行已經超出了表的範圍時,lag() 函數将defval這個參數值作為函數的傳回值,若沒有指定預設值,則傳回NULL,那麼在數學運算中,總要給一個預設值才不會出錯。

用途:

  • 傳回位于目前行的前n行的expr的值:LAG(expr,n)
  • 傳回位于目前行的後n行的expr的值:LEAD(expr,n)

舉例:查詢前1名同學及後一名同學的成績和目前同學成績的內插補點(隻排分數,不按uid分組)

先将前一名和後一名的分數與目前行的分數放在一起:

SELECT
    uid,
    score,
    LAG(score,1,0) OVER my_window_name AS `前一名分數`,
    LEAD(score,1,0) OVER my_window_name AS `後一名分數`
FROM exam_record
WINDOW my_window_name AS (ORDER BY score desc)
           
HiveSQL分析函數實踐詳解

再做diff:

SELECT
    uid,
    score,
    score - `前一名分數` AS `與前一名分差`,
    score - `後一名分數` AS `與後一名分差`
FROM (
    SELECT
        uid,
        score,
        LAG(score,1,0) OVER my_window_name AS `前一名分數`,
        LEAD(score,1,0) OVER my_window_name AS `後一名分數`
    FROM exam_record
    WINDOW my_window_name AS (ORDER BY score desc)
) res
           
HiveSQL分析函數實踐詳解

4. 頭尾函數:FIRST_VALUE(expr),LAST_VALUE(expr)

用途:

  • 傳回第一個expr的值:FIRST_VALUE(expr)
  • 傳回最後一個expr的值:LAST_VALUE(expr)

應用場景:截止到目前成績,按照分數排序查詢第1個和最後1個的分數

SELECT
        uid,
        score,
        FIRST_VALUE(score) OVER my_window_name AS `第一行分數`,
        LAST_VALUE(score) OVER my_window_name AS `最後一行分數`
    FROM exam_record
    WINDOW my_window_name AS (ORDER BY score desc)
           
HiveSQL分析函數實踐詳解

5. 聚合函數+視窗函數聯合使用

聚合函數也可以用于視窗函數。

原因就在于視窗函數的執行順序(邏輯上的)是在FROM,JOIN,WHERE,GROUP BY,HAVING之後,在ORDER BY,LIMIT,SELECT DISTINCT之前。它執行時GROUP BY的聚合過程已經完成了,是以不會再産生資料聚合。

注:視窗函數是在where之後執行的,是以如果where子句需要用視窗函數作為條件,需要多一層查詢,在子查詢外面進行

前面基本用法中已經有部分舉例,如:

SELECT
        uid,
        score,
        sum(score) OVER my_window_name AS sum_score,
        max(score) OVER my_window_name AS max_score,
        min(score) OVER my_window_name AS min_score,
        avg(score) OVER my_window_name AS avg_score
    FROM exam_record
    WINDOW my_window_name AS (ORDER BY score desc)
           
HiveSQL分析函數實踐詳解
mysql> SELECT 
    ->     stu_id, 
    ->     lesson_id, 
    ->     score, 
    ->     create_time,
    ->     FIRST_VALUE(score) OVER w AS first_score, -- 按照lesson_id分區,create_time升序,取第一個score值
    ->     LAST_VALUE(score) OVER w AS last_score -- 按照lesson_id分區,create_time升序,取最後一個score值
    -> FROM t_score
    -> WHERE lesson_id IN ('L001','L002')
    -> WINDOW w AS (PARTITION BY lesson_id ORDER BY create_time)
    -> ;
+--------+-----------+-------+-------------+-------------+------------+
| stu_id | lesson_id | score | create_time | first_score | last_score |
+--------+-----------+-------+-------------+-------------+------------+
|      3 | L001      |   100 | 2018-08-07  |         100 |        100 |
|      1 | L001      |    98 | 2018-08-08  |         100 |         98 |
|      2 | L001      |    84 | 2018-08-09  |         100 |         99 |
|      4 | L001      |    99 | 2018-08-09  |         100 |         99 |
|      3 | L002      |    91 | 2018-08-07  |          91 |         91 |
|      1 | L002      |    86 | 2018-08-08  |          91 |         86 |
|      2 | L002      |    90 | 2018-08-09  |          91 |         90 |
|      4 | L002      |    88 | 2018-08-10  |          91 |         88 |
+--------+-----------+-------+-------------+-------------+------------+
           

四、面試題

1. 使用者行為分析

表1:使用者行為表tracking_log,大概字段有(user_id‘使用者編号’,opr_id‘操作編号’,log_time‘操作時間’)如下所示:

HiveSQL分析函數實踐詳解

問題:

1)統計每天符合以下條件的使用者數:A操作之後是B操作,AB操作必須相鄰

分析:

(1)統計每天,是以需要按天分組統計求和

(2)A操作之後是B,且AB操作必須相鄰,那就涉及一個前後問題,是以想到用視窗函數中的lag()或lead()。

-- 使用 lead() 實作 
select 
    dt,
    count(1) as res_cnt
from (
    select
        user_id,
        date_format(log_time,"%Y%m%d") as dt,
        opr_id as curr_opr, -- 目前操作
        lead(opr_id,1) over(partition by user_id,date_format(log_time,"%Y%m%d") order by log_time) as next_opr -- 擷取 下一個操作
    from tracking_log
) res 
where curr_opr = "A" and next_opr="B" 
group by dt
 
---------------------------------
 
-- 使用 lag() 實作 
select 
    dt,
    count(1) as res_cnt
from (
    select
        user_id,
        date_format(log_time,"%Y%m%d") as dt,
        opr_id as curr_opr, -- 目前操作
        lag(opr_id,1) over(partition by user_id,date_format(log_time,"%Y%m%d") order by log_time) as before_opr -- 擷取 前一個操作
    from tracking_log
) res 
where before_opr = "A" and curr_opr="B" 
group by dt
           

2)統計使用者行為序列為A-B-D的使用者數,其中:A-B之間可以有任何其他浏覽記錄(如C,E等),B-D之間除了C記錄可以有任何其他浏覽記錄(如A,E等)

select 
    count(*)
from(
    select 
        user_id,
        group_concat(opr_id) ubp -- 先按照使用者分組,将組内的opr_id拼接起來
    from tracking_log
    group by user_id
) a
where ubp like '%A%B%D%' and ubp not like '%A%B%C%D%'
           

2. 學生成績分析

表:Enrollments (student_id, course_id) 是該表的主鍵。

HiveSQL分析函數實踐詳解

1)查詢每位學生獲得的最高成績和它所對應的科目,若科目成績并列,取 course_id 最小的一門。查詢結果需按 student_id 增序進行排序。

分析:因為需要最高成績和所對應的科目,是以可采用視窗函數排序分組取第一個
select 
 student_id,
 course_id,
 grade
from (
 select
  student_id,
  course_id,
  grade,
  row_number() over(partition by student_id order by grade desc,course_id asc) as rank_num 
 from Enrollments 
) res 
where rank_num = 1 
order by student_id 
           

解法2:IN 解法

取成績在最大成績之中的學生的最小課程号的課程

select student_id,min(course_id)
from Enrollments
where (student_id,grade) in (
                     -- 先取最大成績
      select student_id,max(grade)
      from Enrollments
      group by student_id)
group by student_id
order by student_id;
           

2)查詢每一科目成績最高和最低分數的學生,輸出course_id,student_id,score

我們可以按科目查找成績最高的同學和最低分的同學,然後利用union連接配接起來

select 
 c_id,
 s_id
from(
 select 
  *,
  row_number() over(partition by c_id order by s_score desc) r
 from score
) a
where r = 1
 
union
 
select 
 c_id,
 s_id
from(
 select 
  *,
  row_number() over(partition by c_id order by s_score) r
 from score
) a
where r = 1;
           

解法2:case-when

select 
     c_id,
     max(case when r1 = 1 then s_id else null end) '最高分學生',
     max(case when r2 = 1 then s_id else null end) '最低分學生'
from(
     select 
          *,
          row_number() over(partition by c_id order by s_score desc) r1,
  row_number() over(partition by c_id order by s_score) r2
 from score
) a
group by c_id;           

繼續閱讀