MySQL 8与MariaDB:两者窗口函数和CTE的比较

Posted 21CTO

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL 8与MariaDB:两者窗口函数和CTE的比较相关的知识,希望对你有一定的参考价值。

21世纪技术官导读:窗口(Window)函数和CTE已经是很多流行数据库产品的主流。随着mysql 8和MariaDB 10.2.0的发布,两家 DBMS 供应商亦开始了竞争的序幕。


每个MySQL DBA或程序员都应该在日常工作中学习和应用新添加的MariaDB和MySQL窗口函数和公共数据表表达式(Common Table Expressions,CTE)。


CTE和窗口函数可以帮我们解决许多查询难题,这些查询在以前的版本中一直是很困难的问题。


掌握它们将为查询解决方案又打开了一扇大门,这些解决方案更加健壮,执行速度更快,并且比以前使用旧技术的解决方案更容易维护。


今天,我们将比较两个数据库中的窗口函数和公用数据表表达式(CTE)。


窗口函数


各位都熟悉COUNT(),SUM()和AVG()等这些集合函数,但是使用窗口函数来处理的就会少得很多。与在整个表上运行的集合函数不同,窗口函数是对一组数据进行计算,为每一记录返回一个单一的聚合值。


为什么使用窗口函数而不是常规集合函数,窗口函数的主要优点是不会导致单个输出记录而进行分组。相反地,记录保留其单独的身份,并将聚合值添加到每条记录(行)。


MariaDB中的窗口函数


窗口函数已被添加到ANSI/ISO标准SQL:2003中,然后又在ANSI/ISO标准SQL:2008中进行了扩展。


多年以来,DB2,Oracle,Sybase,PostgreSQL和其他数据库供应商已经全面实施。其它供应商在后面也都增加了对窗口函数的支持。例如,微软就没能将窗口函数添加到SQL Server,直到SQL 2012才加入。


开源社区对窗口功能的众多愿望和要求,MariaDB 10.2.0中终于引入了这一特性。现在,MariaDB包含诸如ROW_NUMBER , RANK , DENSE_RANK , PERCENT_RANK , CUME_DIST , NTILE , COUNT , SUM , AVG , BIT_OR , BIT_AND和BIT_XOR等窗口函数。


语法


窗口函数查询加入OVER关键字表征,随后指定用于计算的一组记录。 

默认情况下,用于计算的一组行(“窗口”)是整个数据集,可以使用ORDER BY子句进行排序。 然后使用PARTITION BY子句将窗口缩小到数据集内的特定组。


以下举例说明,我们以学生考试成绩表为例:


+------------+---------+--------+

| name       | test    | score  |

+------------+---------+--------+

| Steve      | SQL     | 75     |

+------------+---------+--------+

| Robert     | SQL     | 43     |

+------------+---------+--------+

| Tracy      | SQL     | 56     |

+------------+---------+--------+

| Tatiana    | SQL     | 87     |

+------------+---------+--------+

| Steve      | Tuning  | 73     |

+------------+---------+--------+

| Robert     | Tuning  | 31     |

+------------+---------+--------+

| Tracy      | Tuning  | 88     |

+------------+---------+--------+

| Tatiana    | Tuning  | 83     |

+------------+---------+--------+


以下两个查询分别返回按test与按name的平均test分九,即按测试分和名称进行汇总:


SELECT name, test, score, AVG(score) OVER (PARTITION BY test) 

  AS average_by_test FROM test_scores;


+----------+--------+-------+-----------------+

| name     | test   | score | average_by_test |

+----------+--------+-------+-----------------+

| Steve    | SQL    |    75 |         65.2500 |

| Steve    | Tuning |    73 |         68.7500 |

| Robert   | SQL    |    43 |         65.2500 |

| Robert   | Tuning |    31 |         68.7500 |

| Tracy    | SQL    |    56 |         65.2500 |

| Tracy    | Tuning |    88 |         68.7500 |

| Tatiana  | SQL    |    87 |         65.2500 |

| Tatiana  | Tuning |    83 |         68.7500 |

+----------+--------+-------+-----------------+


SELECT name, test, score, AVG(score) OVER (PARTITION BY name) 

  AS average_by_name FROM student;


+---------+--------+-------+-----------------+

| name    | test   | score | average_by_name |

+---------+--------+-------+-----------------+

| Steve   | SQL    |    75 |         74.0000 |

| Steve   | Tuning |    73 |         74.0000 |

| Robert  | SQL    |    43 |         37.0000 |

| Robert  | Tuning |    31 |         37.0000 |

| Tracy   | SQL    |    56 |         72.0000 |

| Tracy   | Tuning |    88 |         72.0000 |

| Tatiana | SQL    |    87 |         85.0000 |

| Tatiana | Tuning |    83 |         85.0000 |

+---------+--------+-------+-----------------+


在这两种情况下,我们注意到原始分数仍可用于每一条记录。


MySQL 8的窗口函数


MySQL在后来才发布和采用窗口函数的标准,现在已经作为MySQL 8.0的一部分。


MySQL遵循与其它DBMS厂商相同的ANSI/ISO标准。窗口函数查询也由OVER关键字作为表征,PARTITION BY 子句用于将窗口缩小为结果集中的特定组。


目前支持的函数包括:


名称 描述
CUME_DIST() 
累计分配值。
DENSE_RANK() 
其分区中当前行的排名无间隙。
FIRST_VALUE() 
来自窗口结果第一行的参数值
LAG() 
来自分区内当前行的行最后参数的值。
LAST_VALUE() 
来自窗口框架最后一行的参数值。
LEAD() 
分区内行领头当前行的参数值
NTH_VALUE() 
窗口框架第N行的参数值。
NTILE() 
分区中当前行号。
PERCENT_RANK() 
排名的百分比值。
RANK() 
其分区中当前行的行数与权重。
ROW_NUMBER() 
分区内当前行号。


下面我们使用CUME_DIST()函数做为示例,此函数返回一组值的累积分布。


CUME_DIST()通常与ORDER BY联用,将分区行排序为所需的顺序。没有ORDER BY,所有行都是值为1的对等体。


以下的查询,针对于val列中的值集合显示每行的CUME_DIST值以及类似的PERCENT_RANK()函数返回的百分比等值。还可以使用ROW_NUMBER()函数来做为显示参考:


SELECT

  val,

  ROW_NUMBER()   OVER w AS 'row_number',

  CUME_DIST()    OVER w AS 'cume_dist',

  PERCENT_RANK() OVER w AS 'percent_rank'

FROM numbers

WINDOW w AS (ORDER BY val);


其SQL查询结果如下:


+------+------------+--------------------+--------------+

| val  | row_number | cume_dist          | percent_rank |

+------+------------+--------------------+--------------+

|    1 |          1 | 0.2222222222222222 |            0 |

|    1 |          2 | 0.2222222222222222 |            0 |

|    2 |          3 | 0.3333333333333333 |         0.25 |

|    3 |          4 | 0.6666666666666666 |        0.375 |

|    3 |          5 | 0.6666666666666666 |        0.375 |

|    3 |          6 | 0.6666666666666666 |        0.375 |

|    4 |          7 | 0.8888888888888888 |         0.75 |

|    4 |          8 | 0.8888888888888888 |         0.75 |

|    5 |          9 |                  1 |            1 |

+------+------------+--------------------+--------------+


OVER子句可以与很多聚合函数联用,包括如下函数:


(1)AVG()  

(2)COUNT()  

(3)MAX()  

(4)MIN()  

(5)STDDEV_POP() , STDDEV() , STD()

(6)STDDEV_SAMP() 

(7)SUM()  

(8)VAR_POP() , VARIANCE()

(9)VAR_SAMP() 


这些函数既可以用作窗口函数,也可以做为非窗口函数,这取决于OVER子句是否存在。


MySQL还支持仅用作窗口函数的非聚合函数。对于这些函数,OVER子句是强制性存在的。来看如下函数:


(1)CUME_DIST()  

(2)DENSE_RANK()  

(3)FIRST_VALUE()  

(4)LAG()  

(5)LAST_VALUE()  

(6)LEAD()  

(7)NTH_VALUE()  

(8)NTILE()  

(9)PERCENT_RANK() 

(10)RANK() 

(11)ROW_NUMBER() 


作为非聚合窗口函数的示例,此查询用了ROW_NUMBER()函数,它会生成其分区中每条记录的行号。


在这种情况下,记录会按国家编号。在默认状态下,分区的行是无序的,因此记录编号也并非确定的。要对分区行排序,需要在窗口定义中包含ORDER BY子句。该查询使用无序和有序的分区(row_num1 和 row_numw 列)来声明以及包含ORDER BY子句的差异。来看如下查询:


SELECT

  year, country, product, profit,

  ROW_NUMBER() OVER(PARTITION BY country) AS row_num1,

  ROW_NUMBER() OVER(PARTITION BY country ORDER BY year, product) AS row_num2

FROM sales;

+------+---------+------------+--------+----------+----------+

| year | country | product    | profit | row_num1 | row_num2 |

+------+---------+------------+--------+----------+----------+

| 2000 | Finland | Computer   |   1500 |        2 |        1 |

| 2000 | Finland | Phone      |    100 |        1 |        2 |

| 2001 | Finland | Phone      |     10 |        3 |        3 |

| 2000 | India   | Calculator |     75 |        2 |        1 |

| 2000 | India   | Calculator |     75 |        3 |        2 |

| 2000 | India   | Computer   |   1200 |        1 |        3 |

| 2000 | USA     | Calculator |     75 |        5 |        1 |

| 2000 | USA     | Computer   |   1500 |        4 |        2 |

| 2001 | USA     | Calculator |     50 |        2 |        3 |

| 2001 | USA     | Computer   |   1500 |        3 |        4 |

| 2001 | USA     | Computer   |   1200 |        7 |        5 |

| 2001 | USA     | TV         |    150 |        1 |        6 |

| 2001 | USA     | TV         |    100 |        6 |        7 |

+------+---------+------------+--------+----------+----------+


通用数据表表达式(CTE)


通用表表达式(CTE)我们可以理解为在单个SELECT,INSERT,UPDATE,DELETE或CREATE VIEW等语句执行范围内定义的临时结果集。CTE类似于派生表,但是它不作为对象存储,仅在查询期间存在。与派生表不同,CTE可以是自用,即可以在同一查询中多次引用。


CTE可以用来做如下的事情:


1)创建一个递归查询;

2)当无需用视图时替代视图处理;

3)通过从标量子查询/非确定性或外部访问的函数派生的列启用分组;

4)在同一个语句可多次引用结果表数据。


使用CTE提供了改进的可读性和易于维护复杂查询的优点。 查询可以分为独立的,简单的逻辑构建块。 然后可以使用这些简单块构建更复杂的临时CTE,直到生成最终结果集。


递归CTE


递归公用表表达式(CTE)是用于分层查询的标准SQL:1999的实现。


递归CTE的第一个实现是在2007年开始出现的。标准中的递归CTE与IBM DB2版本2中的现有实现相对接近。递归CTE最终由Microsoft SQL Server(自SQL Server 2008 R2),Firebird 2.1, PostgreSQL 8.4+,SQLite 3.8.3+,Oracle 11g第2版和IBM Informix 11.50+版本。


如果没有公共表表达式或连接子句,通过用户定义的递归函数实现分层查询,这会导致非常复杂的SQL出现。


MariaDB中的CTE


在MariaDB中,非递归CTE基本上被认为是一个查询本地视图,这种语法比嵌套FROM (SELECT …)更具有可读性。CTE可以引用另一个CTE,可以从多个地方引用。


因此,CTE与派生表类似。请看如下具有派生表的SQL:


SELECT * FROM

   ( SELECT * FROM employees

     WHERE dept = 'Engineering' ) AS engineers

WHERE

...


具有CTE的SQL:


WITH engineers AS

   ( SELECT * FROM employees

     WHERE dept = 'Engineering' )

SELECT * FROM engineers

WHERE ...


在SQL使用递归或子查询的性能和可读性都很差。CTE的优点之一是它们允许查询引用它本身,因此也可看作递归SQL。递归CTE将重复执行数据的子集,直到获得完整的结果集,这对于处理分层或树形结构的数据特别有用。


使用递归CTE,您可以实现使用标准SQL非常难的处理,并且执行速度更快。它可以帮助我们解决许多类型的业务问题,甚至将一些复杂的SQL/应用程序逻辑简化为只对数据库的简单递归调用。


递归CTE的一些示例用途是查找数据间的差异,创建组织结构图并创建测试数据。


WITH RECURSIVE表示递归CTE。 它有一个名称,后面跟着一个主体(主要查询),如下SQL所示:


Below is a recursive CTE that counts from 1 to 50.

WITH   cte

AS     (SELECT 1 AS n -- anchor member

        UNION ALL

        SELECT n + 1 -- recursive member

        FROM   cte

        WHERE  n < 50 -- terminator

       )

SELECT n

FROM   cte;


上面的语句打印从1到49的数字序号。


MySQL 8中的CTE


MySQL 8.0通过标准的WITH关键字添加了CTE,这WITH在竞争产品中实现的方式大致相同。


使用公用表表达式,要用具有一个或多个逗号分隔子条款的WITH子句。每个子句都提供了一个生成结果集并将名称和子查询关联。以下示例是在WITH子句中定义名为cte1和cte2的CTE,并在WITH子句cte2的最上部SELECT来引用它们:


WITH

  cte1 AS (SELECT a, b FROM table1),

  cte2 AS (SELECT c, d FROM table2)

SELECT b, d FROM cte1 JOIN cte2

WHERE cte1.a = cte2.c;


执行语句后立即包含SELECT语句,如下形式:


INSERT … WITH … SELECT …   

REPLACE … WITH … SELECT …   

CREATE TABLE … WITH … SELECT …   

CREATE VIEW … WITH … SELECT …   

DECLARE CURSOR … WITH … SELECT …   

EXPLAIN … WITH … SELECT … 


递归通用表表达式可以引用其自己的子查询。请看如下SQL:


WITH RECURSIVE cte (n) AS

(

  SELECT 1

  UNION ALL

  SELECT n + 1 FROM cte WHERE n < 5

)

SELECT * FROM cte;

+------+

| n    |

+------+

|    1 |

|    2 |

|    3 |

|    4 |

|    5 |

+------+


小结


一段时间以来,窗口函数和通用表表达式(CTE)已经成为主流数据库产品的标配。随着MySQL 8.0和MariaDB 10.2.0的发布,两个数据库厂商已经都赶到了最流行的趋势,如MS SQL Server与Oracle。


编译:21世纪技术官

以上是关于MySQL 8与MariaDB:两者窗口函数和CTE的比较的主要内容,如果未能解决你的问题,请参考以下文章

SQL:在 MySQL/MariaDB 中选择具有窗口函数的某些行

MariaDB与MySQL对比 --- 对分布式事务的支持

Linux里面mysql和mariadb区别是啥?

MySQL 8.0与MariaDB 10.4,谁更易于填坑补锅?

手把手教你在Linux(Deepin)安装MySQL和MariaDB

手把手教你在Linux(Deepin)安装MySQL和MariaDB