作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
尼尔·巴内特的头像

Neal Barnett

Neal Barnett拥有超过20年管理和开发数据库的经验. 他喜欢Power BI和Tableau等分析工具.

Expertise

Years of Experience

31

Share

非常强大的功能,你喜欢讨厌(但需要知道)

SQL窗口函数提供了一些非常强大和有用的特性. But for many, 因为它们对标准SQL来说太陌生了, 它们很难学习和理解, 有奇怪的语法并且经常被避免使用.

窗口函数可以简单地解释为类似于聚合的计算函数, 但是正常的聚合通过 GROUP BY 子句组合则隐藏正在聚合的单个行, 窗口函数可以访问单独的行,并且可以将这些行的一些属性添加到结果集中.

集合函数和窗口函数的对比图

在这个SQL窗口函数教程中, 我会让你们从窗函数开始, 解释一下这些好处,以及你什么时候会使用它们, 给你们一些真实的例子来帮助你们理解概念.

A Window into Your Data

SQL中最常用和最重要的特性之一是以特定方式聚合或分组数据行的能力. 然而,在某些情况下,分组可能变得极其复杂,这取决于需要什么.

您是否曾经想要循环查询结果以获得排名, a top x list, or similar? 您是否有任何想要为可视化工具准备数据的分析项目, 但发现它几乎不可能,或者太复杂了,不值得?

窗口函数可以使事情变得更容易. 在获得查询结果之后- i.e., after the WHERE 子句和任何标准聚合,窗口函数将作用于剩余的行 window (数据),得到你想要的.

我们将要学习的一些窗口函数包括:

  • OVER
  • COUNT()
  • SUM()
  • ROW_NUMBER()
  • RANK()
  • DENSE_RANK()
  • LEAD()
  • LAG()

Over Easy

The OVER 子句用于指定窗口函数,并且必须始终包含在语句中. The default in an OVER 子句是整个行集. As an example, 让我们看一下公司数据库中的员工表,并在每行显示员工总数, 还有每个员工的信息, 包括他们创立公司的时候.

SELECT COUNT(*) OVER()作为NumEmployees, 名字,姓氏,起始日期
FROM Employee
ORDER BY date_started;
NumEmployeesfirstnamelastnamedate_started
3JohnSmith2019-01-01 00:00:00.000
3SallyJones2019-02-15 00:00:00.000
3SamGordon2019-02-18 00:00:00.000

The above, like many window functions, 也可以用一种更熟悉的非窗口方式来写——哪一种, in this simple example, isn’t too bad:

SELECT
(SELECT COUNT(*) FROM Employee)作为NumEmployees, 名字,姓氏,起始日期
    FROM Employee
ORDER BY date_started;

But now, 假设我们希望显示与该行中的员工在同一个月开始工作的员工数量. 我们需要将每行的计数缩小或限制到当月. How is that done? We use the window PARTITION clause, like so:

SELECT COUNT(*) OVER (PARTITION BY MONTH(date_started),YEAR(date_started)) 
As NumPerMonth, 
DATENAME(month,date_started)+' '+DATENAME(year,date_started)
firstname, lastname
FROM Employee
ORDER BY date_started;
NumPerMonthTheMonthFirstnameLastname
1January 2019JohnSmith
2February 2019SallyJones
2February 2019SamGordon

分区允许您按某个或多个值将窗口筛选为部分. 每个部分通常被称为窗口 frame.

To take it further, 假设我们不仅想知道有多少员工在同一个月开始工作, 但是我们想要显示他们在那个月开始的顺序. 为此,我们可以使用熟悉的方法 ORDER BY clause. 然而,在窗口函数中, ORDER BY 的行为与查询结束时的行为略有不同.

SELECT COUNT(*) OVER (PARTITION BY MONTH(date_started), YEAR(date_started)) 
ORDER BY date_started)
    DATENAME(month,date_started)+' '+DATENAME(year,date_started)
    名字,姓氏,起始日期
FROM Employee
ORDER BY date_started;
NumThisMonthTheMonthFirstnamelastname
1January 2019JohnSmith
1February 2019SallyJones
2February 2019SamGordon

In this case, ORDER BY 修改窗口,使其从分区的开始(在本例中为员工开始工作的月份和年份)转到当前行. 因此,计数在每个分区重新开始.

Rank It

窗口函数对于排序非常有用. 之前我们用 COUNT 聚合功能使我们能够查看员工加入公司的顺序. 我们也可以使用窗口排序函数,例如 ROW_NUMBER(), RANK(), and DENSE_RANK().

当我们在下个月添加一个新员工后,就可以看到差异了, and remove the partition:

SELECT 
ROW_NUMBER() OVER (ORDER BY YEAR(date_started),MONTH(date_started)) 
As StartingRank,
    RANK() OVER (ORDER BY YEAR(date_started),MONTH(date_started))
    DENSE_RANK() OVER (ORDER BY YEAR(date_started),MONTH(date_started))
    DATENAME(month,date_started)+' '+DATENAME(year,date_started)
    名字,姓氏,起始日期
FROM Employee
ORDER BY date_started;
StartingRankEmployeeRankDenseRankTheMonthfirstnamelastnamedate_started
111January 2019JohnSmith2019-01-01
222February 2019SallyJones2019-02-15
322February 2019SamGordon2019-02-18
443March 2019JulieSanchez2019-03-19

You can see the differences. ROW_NUMBER() 给出给定分区内的顺序计数(但没有分区时), it goes through all rows). RANK() 的基础上给出每行的秩 ORDER BY clause. 它显示并列,然后跳过下一个排名. DENSE_RANK 也显示并列,但接着显示下一个连续的值,就好像没有并列一样.

其他排名功能包括:

  • CUME_DIST —计算分区内当前行的相对排名
  • NTILE —将每个窗口分区的行尽可能等分
  • PERCENT_RANK -当前行的百分比排名

还请注意,在本例中,可以在单个查询中使用多个Window函数,并且每个查询中的分区和顺序都可以不同!

行,范围和框架,哦,我的天

来进一步定义或限制您的窗口框架 OVER() clause, you can use ROWS and RANGE. With the ROWS clause, 可以将分区中包含的行指定为当前行之前或之后的行.

SELECT OrderYear, OrderMonth, TotalDue,
    SUM(TotalDue) OVER(ORDER year, ORDER month之间的行) 
作为'LaggingRunningTotal'
FROM sales_products;

In this example, 窗框从第一行移动到当前行- 1, 每一行的窗口大小继续增加.

范围稍有不同,我们可能会得到不同的结果.

SELECT OrderYear, OrderMonth, TotalDue,
    总和(总到期)超过(按订单年,订单月之间的范围) 
作为'LaggingRunningTotal'
FROM sales_products;

范围将包括窗口框架中具有相同的行 ORDER BY values as the current row. 因此,您有可能使用 RANGE if the ORDER BY is not unique.

Some describe ROWS 作为一名物理操作员 RANGE is a logical operator. 的默认值 ROWS and RANGE are always 前一行和当前行无界.

What Else?

大多数标准聚合函数与Window函数一起工作. We’ve seen COUNT in the examples already. Others include SUM, AVG, MIN, MAX, etc.

使用窗口函数,您还可以使用。来访问之前的记录和随后的记录 LAG and LEAD, and FIRST_VALUE and LAST_VALUE. For example, 假设您希望在每行显示当前月的销售数字, 和上个月销售数字的差额. 你可以这样做:

SELECT id, OrderMonth, OrderYear, product, sales, 
sales - LAG(sales,1) OVER (PARTITION BY product ORDER BY OrderYear, OrderMonth
FROM sales_products
WHERE sale_year = 2019;

基本上,SQL窗口函数是非常强大的

虽然这是对SQL窗口函数的快速介绍, 希望它能激发你的兴趣,看看他们能做什么. 我们了解到窗口函数执行计算的方式类似于聚合函数, 但是附加的好处是,它们可以访问单个行中的数据, 这使得它们非常强大. They always contain the OVER clause, and may contain PARTITION BY, ORDER BY,以及大量的聚合(SUM, COUNT, etc.)及其他定位功能(LEAD, LAG). 我们还学习了窗口框架以及它们如何封装数据段.

注意,不同风格的SQL可能以不同的方式实现窗口函数, 有些可能没有实现所有的窗口函数或子句. 确保检查您正在使用的平台的文档.

If, as a SQL developer,您对调优SQL数据库性能感兴趣 面向开发人员的SQL数据库性能调优.

Happy windowing!

有关具体实现的更多信息,请参见:

Understanding the basics

  • SQL中的窗口函数做什么?

    窗口函数对一组行执行计算, 并在需要时使用各个行内的信息.

  • 窗口聚合函数与“分组”聚合函数有何不同?

    使用“group by”,只能聚合不在“group by”子句中的列. 窗口函数允许您同时收集聚合值和非聚合值.

  • 可以在单个SELECT语句中使用多个窗口函数吗?

    Yes, 这是一个很大的优势, 因为每个窗口中的“框架”可以基于不同的过滤器.

  • 我可以使用窗口函数访问以前的数据吗?

    是的,您可以使用LAG和LEAD函数访问前一行和后一行.

  • 我可以用窗口函数生成运行总数吗?

    是的,您可以添加ORDER BY子句来生成每行的运行总数.

就这一主题咨询作者或专家.
Schedule a call
尼尔·巴内特的头像
Neal Barnett

Located in Los Gatos, United States

Member since May 15, 2019

About the author

Neal Barnett拥有超过20年管理和开发数据库的经验. 他喜欢Power BI和Tableau等分析工具.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Years of Experience

31

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.