这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

SQL 分析

Vertica 分析是基于 ANSI 99 标准的 SQL 函数。这些函数用于处理复杂的分析和报告任务,例如:

  • 对处于特殊状态的合作时间最长的客户进行排序。

  • 计算指定的一段时间内零售额的移动平均值。

  • 查找同一年级所有学生的最高分数。

  • 对销售人员当前收到的销售奖金与其以前收到的销售奖金进行比较。

分析函数将返回聚合结果,但并不对结果集进行分组。它们会多次返回组值,每个记录一个组值。可以使用 window ORDER BY 子句排序组值或分区,但此顺序仅影响函数结果集,而不影响整个查询结果集。

有关受支持函数的详细信息,请参阅分析函数

1 - 调用分析函数

您可以调用如下分析函数:

analytic‑function(arguments) OVER(
  [ window-partition-clause ]
  [ window-order-clause [ window-frame-clause ] ]
)

分析函数的 OVER 子句最多可包含三个分子句,这些分子句指定如何对函数输入进行分区和排序,以及指定就当前行而言,如何构建输入框架。函数输出是查询在评估 FROMWHEREGROUP BYHAVING 子句之后返回的结果集。

有关语法的详细信息,请参阅分析函数

函数运行

分析函数按以下方式运行:

  1. 获取查询在执行所有联接和评估 FROMWHEREGROUP BY 以及 HAVING 子句之后返回的输入行。
  2. 根据窗口分区 (PARTITION BY) 子句对输入行进行分组。如果忽略此子句,所有输入行都将视为一个分区。
  3. 根据窗口顺序 (ORDER BY) 子句对每个分区中的行进行排序。
  4. 如果 OVER 子句包含窗口顺序子句,函数会检查窗口框架子句,并在此子句处理每个输入行时运行它。如果 OVER 子句忽略窗口框架子句,函数会将整个分区视为一个窗口框架。

限制

  • 分析函数仅允许在查询的 SELECT 子句和 ORDER BY 子句中使用。

  • 分析函数不可嵌套。例如,以下查询会抛出一条错误:

    => SELECT MEDIAN(RANK() OVER(ORDER BY sal) OVER()).
    

2 - 分析函数与聚合函数

与聚合函数一样,分析函数返回聚合结果,但分析函数不会对结果集进行分组。不过,它们会为每个记录多次返回组值,从而实现进一步分析。

通常,分析查询比聚合查询的运行速度更快,占用的资源更少。

示例

以下示例比较了 COUNT 聚合函数和其对应的分析函数 COUNT。这些示例使用如下方所定义的 employees 表:

CREATE TABLE employees(emp_no INT, dept_no INT);
INSERT INTO employees VALUES(1, 10);
INSERT INTO employees VALUES(2, 30);
INSERT INTO employees VALUES(3, 30);
INSERT INTO employees VALUES(4, 10);
INSERT INTO employees VALUES(5, 30);
INSERT INTO employees VALUES(6, 20);
INSERT INTO employees VALUES(7, 20);
INSERT INTO employees VALUES(8, 20);
INSERT INTO employees VALUES(9, 20);
INSERT INTO employees VALUES(10, 20);
INSERT INTO employees VALUES(11, 20);
COMMIT;

查询此表时将返回以下结果集:

=> SELECT * FROM employees ORDER BY emp_no;
 emp_no | dept_no
--------+---------
      1 |      10
      2 |      30
      3 |      30
      4 |      10
      5 |      30
      6 |      20
      7 |      20
      8 |      20
      9 |      20
     10 |      20
     11 |      20
(11 rows)

以下两个查询使用 COUNT 函数统计每个部门的员工人数。左侧的查询使用 COUNT 聚合函数;右侧的查询使用 COUNT 分析函数:

另请参阅

3 - 窗口分区

分区 (PARTITION BY) 子句组可在分析函数的 OVER 子句中进行指定(可选),用来在函数处理行之前输入这些行。窗口分区与聚合函数的 GROUP BY 子句相似,区别在于它仅为每个输入行返回一个结果行。如果您省略窗口分区子句,函数会将所有输入行视为一个分区。

指定窗口分区

您可以在分析函数的 OVER 子句中指定窗口分区,如下所示:

{ PARTITION BY expression[,...] | PARTITION BEST | PARTITION NODES }

有关语法的详细信息,请参阅窗口分区子句

示例

此主题中的示例使用 调用分析函数 中定义的 allsales 架构:

CREATE TABLE allsales(state VARCHAR(20), name VARCHAR(20), sales INT);
INSERT INTO allsales VALUES('MA', 'A', 60);
INSERT INTO allsales VALUES('NY', 'B', 20);
INSERT INTO allsales VALUES('NY', 'C', 15);
INSERT INTO allsales VALUES('MA', 'D', 20);
INSERT INTO allsales VALUES('MA', 'E', 50);
INSERT INTO allsales VALUES('NY', 'F', 40);
INSERT INTO allsales VALUES('MA', 'G', 10);
COMMIT;

每个州的销售中间值
以下查询使用分析 window-partition-clause 计算每个州的销售中间值。分析函数根据分区进行计算,并在下一个分区开始时重新开始。

=> SELECT state, name, sales, MEDIAN(sales)
      OVER (PARTITION BY state) AS median from allsales;

在中间值列中,结果分组到 MA (35) 和 NY (20) 的分区中。

 state | name | sales | median
-------+------+-------+--------
 NY    | C    |    15 |     20
 NY    | B    |    20 |     20
 NY    | F    |    40 |     20
-------------------------------
 MA    | G    |    10 |     35
 MA    | D    |    20 |     35
 MA    | E    |    50 |     35
 MA    | A    |    60 |     35
(7 rows)

所有州的销售中间值
以下查询计算了所有州的全部销售的中间值。如果您不结合任何参数使用 OVER(),则只有一个分区,即整个输入:

=> SELECT state, sum(sales), median(SUM(sales))
      OVER () AS median FROM allsales GROUP BY state;
 state | sum | median
-------+-----+--------
 NY    |  75 |  107.5
 MA    | 140 |  107.5
(2 rows)

大于中间值的销售(评估顺序)
除查询的最后一个 SQL ORDER BY 子句之外,分析函数的评估在所有其他子句之后进行。因此,如果查询检索销售大于中间值的所有行,它会返回错误,因为在分析函数之前应用了 WHERE 子句,这时 m 列尚不存在:

=> SELECT name, sales,  MEDIAN(sales) OVER () AS m
   FROM allsales WHERE sales > m;
   ERROR 2624:  Column "m" does not exist

您可以通过在子查询中放入谓词 WHERE sales > m 来解决此问题:

=> SELECT * FROM
   (SELECT name, sales, MEDIAN(sales) OVER () AS m FROM allsales) sq
   WHERE sales > m;
 name | sales | m
------+-------+----
 F    |    40 | 20
 E    |    50 | 20
 A    |    60 | 20
(3 rows)

有关更多示例,请参阅分析查询示例

4 - 窗口排序

窗口排序指定如何对提供给分析函数的行进行排序。如下所示,您可以通过函数的 OVER 子句中的 ORDER BY 子句指定窗口排序。如果 OVER 子句包含窗口分区子句,则在每个分区中对行进行排序。如果没有显式指定任何窗口框架,窗口顺序子句还会创建默认窗口框架。

窗口顺序子句仅在窗口结果集中指定顺序。除 OVER 子句以外,查询可以拥有自己的 ORDER BY 子句。它优先于窗口顺序子句,并对最终结果集进行排序。

指定窗口顺序

您可以在分析函数的 OVER 子句中指定窗口框架,如下所示。

ORDER BY { expression [ ASC | DESC [ NULLS { FIRST | LAST | AUTO } ] ]
  }[,...]

有关语法的详细信息,请参阅窗口顺序子句

分析函数用法

分析聚合函数(例如 SUM)支持窗口顺序子句。

必需用法
以下函数需要窗口顺序子句:

无效用法
窗口顺序子句不可与以下函数结合使用:

示例

以下示例使用 allsales 表架构:

CREATE TABLE allsales(state VARCHAR(20), name VARCHAR(20), sales INT);
INSERT INTO allsales VALUES('MA', 'A', 60);
INSERT INTO allsales VALUES('NY', 'B', 20);
INSERT INTO allsales VALUES('NY', 'C', 15);
INSERT INTO allsales VALUES('MA', 'D', 20);
INSERT INTO allsales VALUES('MA', 'E', 50);
INSERT INTO allsales VALUES('NY', 'F', 40);
INSERT INTO allsales VALUES('MA', 'G', 10);
COMMIT;

5 - 窗口框架

分析函数的窗口框架由一组与函数当前正在评估的行相对的行组成。分析函数处理此行及其窗口框架之后,Vertica 会将当前行向前移动,并相应地调整框架边界。如果 OVER 子句还指定了分区,Vertica 也会检查框架边界是否跨越了分区边界。此过程将不断重复,直到函数评估了最后一个分区的最后一个行。

指定窗口框架

您可以在分析函数的 OVER 子句中指定窗口框架,如下所示。

{ ROWS | RANGE } { BETWEEN start‑point AND end‑point } | start‑point

start-point / end‑point

{ UNBOUNDED {PRECEDING | FOLLOWING}
  | CURRENT ROW
  | constant-value  {PRECEDING | FOLLOWING}}

start‑pointend‑point 指定相对于当前行的窗口框架偏移。ROWSRANGE 关键字指定偏移是物理偏移还是逻辑偏移。如果仅指定起始点,Vertica 将创建一个从此点到当前行的窗口。

有关语法的详细信息,请参阅窗口框架子句

要求

要指定窗口框架,OVER 还必须指定窗口顺序 (ORDER BY) 子句。如果 OVER 子句包含窗口顺序子句,但没有指定窗口框架,则函数会创建从当前分区的第一行扩展到当前行的默认框架。它等同于以下子句:

RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

窗口聚合函数

支持窗口框架的分析函数称为窗口聚合函数。它们返回移动平均数和累计结果等信息。要将以下函数用作窗口(分析)聚合函数,而不是基本聚合函数,OVER 子句必须指定窗口顺序子句和(可选)窗口框架子句。如果 OVER 子句没有指定窗口框架,则函数将如前所述创建默认窗口框架。

以下分析函数支持窗口框架:

包含空 OVER 子句的窗口聚合函数不会创建窗口框架。如果所有输入都视为一个分区,则函数将用作报告函数。

5.1 - 具有物理偏移的窗口 (ROWS)

窗口物理框架子句中的 ROWS 关键字将窗口大小指定为相对于当前行的行数。值只能为 INTEGER 数据类型。

示例

此页面中的示例使用 emp 表架构:

CREATE TABLE emp(deptno INT, sal INT, empno INT);
INSERT INTO emp VALUES(10,101,1);
INSERT INTO emp VALUES(10,104,4);
INSERT INTO emp VALUES(20,100,11);
INSERT INTO emp VALUES(20,109,7);
INSERT INTO emp VALUES(20,109,6);
INSERT INTO emp VALUES(20,109,8);
INSERT INTO emp VALUES(20,110,10);
INSERT INTO emp VALUES(20,110,9);
INSERT INTO emp VALUES(30,102,2);
INSERT INTO emp VALUES(30,103,3);
INSERT INTO emp VALUES(30,105,5);
COMMIT;

以下查询调用 COUNT 来对当前行及其前面的行(最多两行)进行计数:

SELECT deptno, sal, empno,  COUNT(*) OVER
         (PARTITION BY deptno ORDER BY sal ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
        AS count FROM emp;

OVER 子句包含三个组件:

  • 窗口分区子句 PARTITION BY deptno

  • 排序依据子句 ORDER BY sal

  • 窗口框架子句ROWS BETWEEN 2 PRECEDING AND CURRENT ROW。此子句将窗口大小定义为从当前行扩展到当前行前面的两个行。

查询返回的结果分为三个分区,在下文中以红线表示。在第二个分区 (deptno=20) 中,COUNT 按以下方式处理窗口框架子句:

  1. 创建第一个窗口(绿方框)。此窗口包含一个行,因为当前行(蓝方框)也是此分区的第一个行。因此,count 列中的值显示了当前窗口中的行数,即 1:

  2. COUNT 处理此分区的第一行之后,它会将当前行重置为此分区的第二个行。窗口此时跨越了当前行和前面的一个行,因此,COUNT 返回了值 2:

  3. COUNT 处理此分区的第二行之后,它会将当前行重置为此分区的第三个行。窗口此时跨越了当前行和前面的两个行,因此,COUNT 返回了值 3:

  4. 此后,COUNT 继续处理剩余的分区行,并相应地移动窗口,但窗口大小 (ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) 不变,仍为三行。因此,count 列中的值也不变 (3):




5.2 - 具有逻辑偏移的窗口 (RANGE)

RANGE 关键字将分析窗口框架定义为当前行的逻辑偏移。

对于每个行,分析函数使用窗口顺序子句 (ORDER_BY) 列或表达式来计算窗口框架大小,具体如下所述:

  1. 在当前分区中,根据连续行的 ORDER_BY 值评估当前行的 ORDER_BY 值。

  2. 确定这些行中有哪些行符合相对于当前行的指定范围要求。

  3. 创建仅包含这些行的窗口框架。

  4. 在当前窗口中执行。

示例

此示例使用 property_sales 表,此表包含街区的住宅销售数据:

=> SELECT property_key, neighborhood, sell_price FROM property_sales ORDER BY neighborhood, sell_price;
 property_key | neighborhood  | sell_price
--------------+---------------+------------
        10918 | Jamaica Plain |     353000
        10921 | Jamaica Plain |     450000
        10927 | Jamaica Plain |     450000
        10922 | Jamaica Plain |     474000
        10919 | Jamaica Plain |     515000
        10917 | Jamaica Plain |     675000
        10924 | Jamaica Plain |     675000
        10920 | Jamaica Plain |     705000
        10923 | Jamaica Plain |     710000
        10926 | Jamaica Plain |     875000
        10925 | Jamaica Plain |     900000
        10930 | Roslindale    |     300000
        10928 | Roslindale    |     422000
        10932 | Roslindale    |     450000
        10929 | Roslindale    |     485000
        10931 | Roslindale    |     519000
        10938 | West Roxbury  |     479000
        10933 | West Roxbury  |     550000
        10937 | West Roxbury  |     550000
        10934 | West Roxbury  |     574000
        10935 | West Roxbury  |     598000
        10936 | West Roxbury  |     615000
        10939 | West Roxbury  |     720000
(23 rows)

AVG 分析函数可以获取每个街区的近似销售价格的平均值。对于每个住宅,下列查询计算了销售价格比该住宅高或低 50000 美元的所有其他街区住宅的平均销售价格:

=> SELECT property_key, neighborhood, sell_price, AVG(sell_price) OVER(
     PARTITION BY neighborhood ORDER BY sell_price
     RANGE BETWEEN 50000 PRECEDING and 50000 FOLLOWING)::int AS comp_sales
     FROM property_sales ORDER BY neighborhood;
 property_key | neighborhood  | sell_price | comp_sales
--------------+---------------+------------+------------
        10918 | Jamaica Plain |     353000 |     353000
        10927 | Jamaica Plain |     450000 |     458000
        10921 | Jamaica Plain |     450000 |     458000
        10922 | Jamaica Plain |     474000 |     472250
        10919 | Jamaica Plain |     515000 |     494500
        10917 | Jamaica Plain |     675000 |     691250
        10924 | Jamaica Plain |     675000 |     691250
        10920 | Jamaica Plain |     705000 |     691250
        10923 | Jamaica Plain |     710000 |     691250
        10926 | Jamaica Plain |     875000 |     887500
        10925 | Jamaica Plain |     900000 |     887500
        10930 | Roslindale    |     300000 |     300000
        10928 | Roslindale    |     422000 |     436000
        10932 | Roslindale    |     450000 |     452333
        10929 | Roslindale    |     485000 |     484667
        10931 | Roslindale    |     519000 |     502000
        10938 | West Roxbury  |     479000 |     479000
        10933 | West Roxbury  |     550000 |     568000
        10937 | West Roxbury  |     550000 |     568000
        10934 | West Roxbury  |     574000 |     577400
        10935 | West Roxbury  |     598000 |     577400
        10936 | West Roxbury  |     615000 |     595667
        10939 | West Roxbury  |     720000 |     720000
(23 rows)

AVG 按以下方式处理此查询:

  1. AVG 评估了第一个分区 (Jamaica Plain) 的第 1 行,但没有发现比此行的 sell_price(353000 美元)高或低 50000 美元范围内的销售价格。 AVG 创建了一个仅包含此行的窗口,并为第 1 行返回了平均值 353000 美元:

  2. AVG 评估了第 2 行,发现了有三个 sell_price 值与当前行的相差幅度在 50000 美元范围内。 AVG 创建了一个包含了这三个行的窗口,并为第 2 行返回了平均值 458000 美元:

  3. AVG 评估了第 3 行,同样发现了有三个 sell_price 值与当前行的相差幅度在 50000 美元范围内。 AVG 创建了一个窗口,它与前面的窗口相同,并为第 3 行返回了相同的平均值 458000 美元:

  4. AVG 评估了第 4 行,发现了有四个 sell_price 值与当前行的相差幅度在 50000 美元范围内。 AVG 将窗口扩展到可包含第 2 行至第 5 行,并为第 4 行返回了平均值 472250 美元:

  5. 同样地,AVG 还评估了此分区的其他行。当函数评估第二个分区 (Roslindale) 的第一行时,它会如下所述重置窗口:

限制

如果 RANGE 指定一个常数值,此值的数据类型与窗口的 ORDER BY 数据类型必须相同。以下情况除外:

  • RANGE 如果窗口顺序子句数据类型为以下之一,可以指定 INTERVAL Year to MonthTIMESTAMPTIMESTAMP WITH TIMEZONEDATETIMETIME WITH TIMEZONE 均不受支持。

  • RANGE 如果窗口顺序子句数据为以下之一,可以指定 INTERVAL Day to SecondTIMESTAMPTIMESTAMP WITH TIMEZONEDATETIMETIME WITH TIMEZONE

窗口顺序子句必须指定以下数据类型之一:NUMERICDATE/TIMEFLOAT INTEGER。如果窗口指定了以下框架之一,则会忽视此要求:

  • RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

  • RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING

  • RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING

5.3 - 报告聚合

采用 window-frame-clause 的某些分析函数是报告聚合函数。通过这些函数,您可以比较分区的聚合值与明细行,而不使用相关子查询或联接。

如果结合空 OVER() 子句使用窗口聚合,分析函数将用作报告函数,而且整个输入会被视为一个分区。

关于标准差函数和方差函数

使用标准差函数时,较小的标准差表示数据点通常非常接近于平均值,而较大的标准差表示数据点分散在一个较大的值范围内。

标准差通常用图形表示,而分布式标准差为典型的钟形曲线。

方差函数会衡量一组数值的分散距离。

示例

将报告聚合的窗口看作是定义为 UNBOUNDED PRECEDING 和 UNBOUNDED FOLLOWING 的窗口。忽略 window-order-clause 会使分区中的所有行也位于窗口中(报告聚合)。

=> SELECT deptno, sal, empno, COUNT(sal)
  OVER (PARTITION BY deptno) AS COUNT FROM emp;
 deptno | sal | empno | count
--------+-----+-------+-------
     10 | 101 |     1 |     2
     10 | 104 |     4 |     2
------------------------------
     20 | 110 |    10 |     6
     20 | 110 |     9 |     6
     20 | 109 |     7 |     6
     20 | 109 |     6 |     6
     20 | 109 |     8 |     6
     20 | 100 |    11 |     6
------------------------------
     30 | 105 |     5 |     3
     30 | 103 |     3 |     3
     30 | 102 |     2 |     3
(11 rows)

如果上述查询中的 OVER() 子句包含 window-order-clause (例如 ORDER BY sal),它将变为包含 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 默认窗口的移动窗口(窗口聚合)查询:

=> SELECT deptno, sal, empno, COUNT(sal)  OVER (PARTITION BY deptno ORDER BY sal) AS COUNT FROM emp;
 deptno | sal | empno | count
--------+-----+-------+-------
     10 | 101 |     1 |     1
     10 | 104 |     4 |     2
------------------------------
     20 | 100 |    11 |     1
     20 | 109 |     7 |     4
     20 | 109 |     6 |     4
     20 | 109 |     8 |     4
     20 | 110 |    10 |     6
     20 | 110 |     9 |     6
------------------------------
     30 | 102 |     2 |     1
     30 | 103 |     3 |     2
     30 | 105 |     5 |     3
(11 rows)

LAST_VALUE 怎么样?

您可能会想为什么您不能只使用 LAST_VALUE() 分析函数。

例如,对于每个员工,获取部门中最高薪水:

=> SELECT deptno, sal, empno,LAST_VALUE(empno) OVER (PARTITION BY deptno ORDER BY sal) AS lv FROM emp;
![](/images/reporting-aggregates2.png)

受默认窗口语义影响,LAST_VALUE 并非总是会返回分区的最后一个值。如果在分析子句中忽略 window-frame-clause,则 LAST_VALUE 会在默认窗口中运行。因此,结果似乎并不直观,因为函数没有返回当前分区的最后一个值。它只返回了窗口的最后一个值,并且此值随当前正在处理的输入行而不断变化。

请记住默认窗口:

OVER (PARTITION BY deptno ORDER BY sal)

等同于:

OVER(PARTITION BY deptno ORDER BY salROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)

如果您要返回分区的最后一个值,请使用 UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING。

=> SELECT deptno, sal, empno, LAST_VALUE(empno)
OVER (PARTITION BY deptno ORDER BY sal
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS lv
FROM emp;
![](/images/reporting-aggregates9.png)

Vertica 建议结合 window-order-clause 使用 LAST_VALUE 以生成具有确定性的结果。

在以下示例中,empno 6、7 和 8 的薪水相同,因此它们位于相邻行中。在本例中,empno 8 首先出现,但不能保证该顺序。

请注意,在上述输出中,最后一个值是 7,它是分区 deptno = 20 的最后一个行。如果各行的顺序不同,则函数会返回不同的值:

此时,最后一个值是 6,它是分区 deptno = 20 的最后一个行。解决方案是将唯一键添加到排序顺序中。即使查询的顺序发生变化,结果也始终相同,因此非常确定。

=> SELECT deptno, sal, empno, LAST_VALUE(empno)
OVER (PARTITION BY deptno ORDER BY sal, empno
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) as lv
FROM emp;

请注意行现在如何按 empno 进行排序,这时最后一个值一直是 8,而且无论查询顺序如何都不会造成影响。

6 - 命名窗口

分析函数的 OVER 子句可以引用命名窗口,此窗口封装了一个或多个窗口子句:窗口分区 (PARTITION BY) 子句和(可选)窗口顺序 (ORDER BY) 子句。编写通过类似的 OVER 子句语法调用多个分析函数的查询时,例如,它们使用相同的分区子句,则命名窗口非常有用。

查询按如下方式命名窗口:

WINDOW window‑name AS ( window-partition-clause [window-order-clause] );

同一个查询可以命名和引用多个窗口。在同一个查询中,所有窗口名称都必须是唯一的。

示例

以下查询调用了两个分析函数,即 RANKDENSE_RANK。因为两个函数使用同一个分区和顺序子句,查询对指定这两个子句的 w 窗口进行了命名。两个函数引用了此窗口,如下所示:

=> SELECT employee_region region, employee_key, annual_salary,
     RANK() OVER w Rank,
     DENSE_RANK() OVER w "Dense Rank"
     FROM employee_dimension WINDOW w AS (PARTITION BY employee_region ORDER BY annual_salary);
              region              | employee_key | annual_salary | Rank | Dense Rank
----------------------------------+--------------+---------------+------+------------
 West                             |         5248 |          1200 |    1 |          1
 West                             |         6880 |          1204 |    2 |          2
 West                             |         5700 |          1214 |    3 |          3
 West                             |         9857 |          1218 |    4 |          4
 West                             |         6014 |          1218 |    4 |          4
 West                             |         9221 |          1220 |    6 |          5
 West                             |         7646 |          1222 |    7 |          6
 West                             |         6621 |          1222 |    7 |          6
 West                             |         6488 |          1224 |    9 |          7
 West                             |         7659 |          1226 |   10 |          8
 West                             |         7432 |          1226 |   10 |          8
 West                             |         9905 |          1226 |   10 |          8
 West                             |         9021 |          1228 |   13 |          9
 West                             |         7855 |          1228 |   13 |          9
 West                             |         7119 |          1230 |   15 |         10
 ...

如果命名窗口忽略了顺序子句,查询的 OVER 子句可以指定自己的顺序子句。例如,您可以修改上一个查询,让每个函数使用不同的顺序子句。命名窗口在定义后仅包括一个分区子句:

=> SELECT employee_region region, employee_key, annual_salary,
    RANK() OVER (w ORDER BY annual_salary DESC) Rank,
    DENSE_RANK() OVER (w ORDER BY annual_salary ASC) "Dense Rank"
    FROM employee_dimension WINDOW w AS (PARTITION BY employee_region);
              region              | employee_key | annual_salary | Rank | Dense Rank
----------------------------------+--------------+---------------+------+------------
 West                             |         5248 |          1200 | 2795 |          1
 West                             |         6880 |          1204 | 2794 |          2
 West                             |         5700 |          1214 | 2793 |          3
 West                             |         6014 |          1218 | 2791 |          4
 West                             |         9857 |          1218 | 2791 |          4
 West                             |         9221 |          1220 | 2790 |          5
 West                             |         6621 |          1222 | 2788 |          6
 West                             |         7646 |          1222 | 2788 |          6
 West                             |         6488 |          1224 | 2787 |          7
 West                             |         7432 |          1226 | 2784 |          8
 West                             |         9905 |          1226 | 2784 |          8
 West                             |         7659 |          1226 | 2784 |          8
 West                             |         7855 |          1228 | 2782 |          9
 West                             |         9021 |          1228 | 2782 |          9
 West                             |         7119 |          1230 | 2781 |         10
 ...

同样地,如果命名窗口包含一个顺序子句,OVER 子句可指定命名窗口也指定一个窗口框架子句。如果无法将命名窗口定义为包含一个窗口框架子句,上述用法会非常有用。

例如,以下查询定义了一个封装分区和顺序子句的窗口。OVER 子句调用此窗口,还包含一个窗口框架子句:

=> SELECT deptno, sal, empno,  COUNT(*) OVER (w ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS count
 FROM emp WINDOW w AS (PARTITION BY deptno ORDER BY sal);
 deptno | sal | empno | count
--------+-----+-------+-------
     10 | 101 |     1 |     1
     10 | 104 |     4 |     2
     20 | 100 |    11 |     1
     20 | 109 |     8 |     2
     20 | 109 |     6 |     3
     20 | 109 |     7 |     3
     20 | 110 |    10 |     3
     20 | 110 |     9 |     3
     30 | 102 |     2 |     1
     30 | 103 |     3 |     2
     30 | 105 |     5 |     3
(11 rows)

递归窗口引用

WINDOW 子句可引用另一个已经命名的窗口。例如,由于命名窗口 w1 是在 w2 之前定义的,因此定义 w2WINDOW 子句可以引用 w1

=> SELECT RANK() OVER(w1 ORDER BY sal DESC), RANK() OVER w2
   FROM EMP WINDOW w1 AS (PARTITION BY deptno), w2 AS (w1 ORDER BY sal);

限制

  • OVER 子句只能引用一个命名窗口。

  • 同一个查询中的每个 WINDOW 子句都必须拥有唯一的名称。

7 - 分析查询示例

此部分中的主题演示了如何使用分析查询进行计算。

7.1 - 计算中间值

中间值是一种数值,它将样本分为上下两部分。例如,您可以将所有观察值从最小值递增排序到最大值,然后选择居于正中间的值,即可获得有限数值列表的中间值。

如果观察值的数目为偶数,则不存在单个中间值;此时中间值是两个中间值的平均值。

以下示例使用此表:

CREATE TABLE allsales(state VARCHAR(20), name VARCHAR(20), sales INT);
INSERT INTO allsales VALUES('MA', 'A', 60);
INSERT INTO allsales VALUES('NY', 'B', 20);
INSERT INTO allsales VALUES('NY', 'C', 15);
INSERT INTO allsales VALUES('MA', 'D', 20);
INSERT INTO allsales VALUES('MA', 'E', 50);
INSERT INTO allsales VALUES('NY', 'F', 40);
INSERT INTO allsales VALUES('MA', 'G', 10);
COMMIT;

您可以使用分析函数 MEDIAN 计算此表中所有销售的中间值。在下述查询中,函数的 OVER 子句为空,因此查询为结果集的每个行返回了相同的聚合值。

=> SELECT name, sales, MEDIAN(sales) OVER() AS median FROM allsales;
 name | sales | median
------+-------+--------
 G    |    10 |     20
 C    |    15 |     20
 D    |    20 |     20
 B    |    20 |     20
 F    |    40 |     20
 E    |    50 |     20
 A    |    60 |     20
(7 rows)

您可以修改此查询,按州对销售进行分组,然后获取每组的中间值。为此,请在 OVER 子句中包含一个窗口分区子句:

=> SELECT state, name, sales, MEDIAN(sales) OVER(partition by state) AS median FROM allsales;
 state | name | sales | median
-------+------+-------+--------
 MA    | G    |    10 |     35
 MA    | D    |    20 |     35
 MA    | E    |    50 |     35
 MA    | A    |    60 |     35
 NY    | C    |    15 |     20
 NY    | B    |    20 |     20
 NY    | F    |    40 |     20
(7 rows)

7.2 - 获取两支股票的价格差异

以下子查询选择了两支感兴趣的股票。外部查询使用分析函数的 LAST_VALUE() 和 OVER() 组件以及 IGNORE NULLS。

架构

DROP TABLE Ticks CASCADE;


CREATE TABLE Ticks (ts TIMESTAMP, Stock varchar(10), Bid float);
INSERT INTO Ticks VALUES('2011-07-12 10:23:54', 'abc', 10.12);
INSERT INTO Ticks VALUES('2011-07-12 10:23:58', 'abc', 10.34);
INSERT INTO Ticks VALUES('2011-07-12 10:23:59', 'abc', 10.75);
INSERT INTO Ticks VALUES('2011-07-12 10:25:15', 'abc', 11.98);
INSERT INTO Ticks VALUES('2011-07-12 10:25:16', 'abc');
INSERT INTO Ticks VALUES('2011-07-12 10:25:22', 'xyz', 45.16);
INSERT INTO Ticks VALUES('2011-07-12 10:25:27', 'xyz', 49.33);
INSERT INTO Ticks VALUES('2011-07-12 10:31:12', 'xyz', 65.25);
INSERT INTO Ticks VALUES('2011-07-12 10:31:15', 'xyz');

COMMIT;

Ticks 表

=> SELECT * FROM ticks;
         ts          | stock |  bid
---------------------+-------+-------
 2011-07-12 10:23:59 | abc   | 10.75
 2011-07-12 10:25:22 | xyz   | 45.16
 2011-07-12 10:23:58 | abc   | 10.34
 2011-07-12 10:25:27 | xyz   | 49.33
 2011-07-12 10:23:54 | abc   | 10.12
 2011-07-12 10:31:15 | xyz   |
 2011-07-12 10:25:15 | abc   | 11.98
 2011-07-12 10:25:16 | abc   |
 2011-07-12 10:31:12 | xyz   | 65.25
(9 rows)

查询

 => SELECT ts, stock, bid, last_value(price1 IGNORE NULLS)
   OVER(ORDER BY ts) - last_value(price2 IGNORE NULLS)
   OVER(ORDER BY ts)   as price_diff
 FROM
  (SELECT ts, stock, bid,
    CASE WHEN stock = 'abc' THEN bid ELSE NULL END AS price1,
    CASE WHEN stock = 'xyz' then bid ELSE NULL END AS price2
    FROM ticks
    WHERE stock IN ('abc','xyz')
  ) v1
 ORDER BY ts;
         ts          | stock |  bid  | price_diff
---------------------+-------+-------+------------
 2011-07-12 10:23:54 | abc   | 10.12 |
 2011-07-12 10:23:58 | abc   | 10.34 |
 2011-07-12 10:23:59 | abc   | 10.75 |
 2011-07-12 10:25:15 | abc   | 11.98 |
 2011-07-12 10:25:16 | abc   |       |
 2011-07-12 10:25:22 | xyz   | 45.16 |     -33.18
 2011-07-12 10:25:27 | xyz   | 49.33 |     -37.35
 2011-07-12 10:31:12 | xyz   | 65.25 |     -53.27
 2011-07-12 10:31:15 | xyz   |       |     -53.27
(9 rows)

7.3 - 计算移动平均数

计算移动平均数有助于估计数据集中的趋势。移动平均数是一段时间内任何数字子集的平均值。例如,如果您拥有跨十年的零售数据,您可以计算三年移动平均数、四年移动平均数等。此示例计算某支股票的出价在 40 秒内的移动平均数。此示例使用 ticks 表架构。

查询

=> SELECT ts, bid, AVG(bid)
   OVER(ORDER BY ts
       RANGE BETWEEN INTERVAL '40 seconds'
       PRECEDING AND CURRENT ROW)
FROM ticks
WHERE stock = 'abc'
GROUP BY bid, ts
ORDER BY ts;
         ts          |  bid  |     ?column?
---------------------+-------+------------------
 2011-07-12 10:23:54 | 10.12 |            10.12
 2011-07-12 10:23:58 | 10.34 |            10.23
 2011-07-12 10:23:59 | 10.75 | 10.4033333333333
 2011-07-12 10:25:15 | 11.98 |            11.98
 2011-07-12 10:25:16 |       |            11.98
(5 rows)
DROP TABLE Ticks CASCADE;


CREATE TABLE Ticks (ts TIMESTAMP, Stock varchar(10), Bid float);
INSERT INTO Ticks VALUES('2011-07-12 10:23:54', 'abc', 10.12);
INSERT INTO Ticks VALUES('2011-07-12 10:23:58', 'abc', 10.34);
INSERT INTO Ticks VALUES('2011-07-12 10:23:59', 'abc', 10.75);
INSERT INTO Ticks VALUES('2011-07-12 10:25:15', 'abc', 11.98);
INSERT INTO Ticks VALUES('2011-07-12 10:25:16', 'abc');
INSERT INTO Ticks VALUES('2011-07-12 10:25:22', 'xyz', 45.16);
INSERT INTO Ticks VALUES('2011-07-12 10:25:27', 'xyz', 49.33);
INSERT INTO Ticks VALUES('2011-07-12 10:31:12', 'xyz', 65.25);
INSERT INTO Ticks VALUES('2011-07-12 10:31:15', 'xyz');

COMMIT;

7.4 - 获取最新出价和询价结果

以下查询填充缺失的 NULL 值以创建一个完整的预订订单,按供应商 ID 显示最新的出价和询价以及大小。原始行(通常)具有一个价格和一个大小值,因此每输入一个 ID 条目,就会使用带有“忽略 null 值”的 last_value 来查找另一对的最新非 null 值。Sequenceno 提供唯一的全顺序。

架构:

=> CREATE TABLE bookorders(
    vendorid VARCHAR(100),
    date TIMESTAMP,
    sequenceno INT,
    askprice FLOAT,
    asksize INT,
    bidprice FLOAT,
    bidsize INT);
=> INSERT INTO bookorders VALUES('3325XPK','2011-07-12 10:23:54', 1, 10.12, 55, 10.23, 59);
=> INSERT INTO bookorders VALUES('3345XPZ','2011-07-12 10:23:55', 2, 10.55, 58, 10.75, 57);
=> INSERT INTO bookorders VALUES('445XPKF','2011-07-12 10:23:56', 3, 10.22, 43, 54);
=> INSERT INTO bookorders VALUES('445XPKF','2011-07-12 10:23:57', 3, 10.22, 59, 10.25, 61);
=> INSERT INTO bookorders VALUES('3425XPY','2011-07-12 10:23:58', 4, 11.87, 66, 11.90, 66);
=> INSERT INTO bookorders VALUES('3727XVK','2011-07-12 10:23:59', 5, 11.66, 51, 11.67, 62);
=> INSERT INTO bookorders VALUES('5325XYZ','2011-07-12 10:24:01', 6, 15.05, 44, 15.10, 59);
=> INSERT INTO bookorders VALUES('3675XVS','2011-07-12 10:24:05', 7, 15.43, 47, 58);
=> INSERT INTO bookorders VALUES('8972VUG','2011-07-12 10:25:15', 8, 14.95, 52, 15.11, 57);
COMMIT;

查询:

=> SELECT
    sequenceno Seq,
    date "Time",
    vendorid ID,
    LAST_VALUE (bidprice IGNORE NULLS)
     OVER (PARTITION BY vendorid ORDER BY sequenceno
           ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
    AS "Bid Price",
    LAST_VALUE (bidsize IGNORE NULLS)
     OVER (PARTITION BY vendorid ORDER BY sequenceno
           ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
    AS "Bid Size",
    LAST_VALUE (askprice IGNORE NULLS)
     OVER (PARTITION BY vendorid ORDER BY sequenceno
           ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
    AS "Ask Price",
    LAST_VALUE (asksize IGNORE NULLS)
     OVER (PARTITION BY vendorid order by sequenceno
           ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW )
    AS  "Ask Size"
 FROM bookorders
ORDER BY sequenceno;
 Seq |        Time         |   ID    | Bid Price | Bid Size | Ask Price | Ask Size
-----+---------------------+---------+-----------+----------+-----------+----------
   1 | 2011-07-12 10:23:54 | 3325XPK |     10.23 |       59 |     10.12 |       55
   2 | 2011-07-12 10:23:55 | 3345XPZ |     10.75 |       57 |     10.55 |       58
   3 | 2011-07-12 10:23:57 | 445XPKF |     10.25 |       61 |     10.22 |       59
   3 | 2011-07-12 10:23:56 | 445XPKF |        54 |          |     10.22 |       43
   4 | 2011-07-12 10:23:58 | 3425XPY |      11.9 |       66 |     11.87 |       66
   5 | 2011-07-12 10:23:59 | 3727XVK |     11.67 |       62 |     11.66 |       51
   6 | 2011-07-12 10:24:01 | 5325XYZ |      15.1 |       59 |     15.05 |       44
   7 | 2011-07-12 10:24:05 | 3675XVS |        58 |          |     15.43 |       47
   8 | 2011-07-12 10:25:15 | 8972VUG |     15.11 |       57 |     14.95 |       52
(9 rows)

8 - 事件窗口

通过基于事件的窗口,可以将时序数据拆分为与数据中的重大事件相邻的窗口。这在财务数据中尤其重要,因为分析通常可以侧重于触发了其他活动的特定事件。

Vertica 提供了两个不属于 SQL-99 标准的基于事件的窗口函数:

  • CONDITIONAL_CHANGE_EVENT 从 0 开始向每个行分配一个事件窗口编号,并在当前行中评估实参表达式的结果与上一行不同时,以 1 为增量递增事件窗口编号。此函数与 ROW_NUMBER 分析函数类似,该分析函数从 1 开始按顺序向分区中每个行分配一个唯一编号。

  • CONDITIONAL_TRUE_EVENT 从 0 开始向每个行分配一个事件窗口编号,并在当 boolean 实参表达式的结果评估为 true 时,以 1 为增量递增事件窗口编号。

下文中更详细地介绍了这两种函数。

示例架构

此页面中的示例使用以下架构:

CREATE TABLE TickStore3 (ts TIMESTAMP, symbol VARCHAR(8), bid FLOAT);
CREATE PROJECTION TickStore3_p (ts, symbol, bid) AS SELECT * FROM TickStore3 ORDER BY ts, symbol, bid UNSEGMENTED ALL NODES;
INSERT INTO TickStore3 VALUES ('2009-01-01 03:00:00', 'XYZ', 10.0);
INSERT INTO TickStore3 VALUES ('2009-01-01 03:00:03', 'XYZ', 11.0);
INSERT INTO TickStore3 VALUES ('2009-01-01 03:00:06', 'XYZ', 10.5);
INSERT INTO TickStore3 VALUES ('2009-01-01 03:00:09', 'XYZ', 11.0);
COMMIT;

使用 CONDITIONAL_CHANGE_EVENT

CONDITIONAL_CHANGE_EVENT 分析函数会返回一系列表示从 0 开始的事件窗口编号的整数。当在当前行中评估函数表达式的结果与上一行不同时,函数会递增事件窗口编号。

在以下示例中,第一个查询会返回 TickStore3 表中的所有记录。第二个查询在出价列中使用 CONDITIONAL_CHANGE_EVENT 函数。由于每个出价行值与上一个值不同,函数会将窗口 ID 从 0 递增到 3。

下图以图表形式说明了出价的变化情况。每个值都不同于其前面的值,因此递增了每个时间片的窗口 ID:

条件更改事件

窗口 ID 从 0 开始,并在值每次与前一个值不同时递增。

在此示例中,出价在第二行中从 $10 变为 $11,然后保持不变。 CONDITIONAL_CHANGE_EVENT 递增了第 2 行的事件窗口 ID,但之后一直没有递增:

下图以图表形式说明了出价仅在 3:00:03 发生了变化。出价在 3:00:06 和 3:00:09 时没有变化,因此在上述变化之后,每个时间片的窗口 ID 仍为 1。

![CCE 2](/images/cce2.png)

使用 CONDITIONAL_TRUE_EVENT

CONDITIONAL_CHANGE_EVENT 一样,CONDITIONAL_TRUE_EVENT 分析函数也会返回一系列表示从 0 开始的事件窗口编号的整数。这两种函数的区别如下:

  • CONDITIONAL_TRUE_EVENT 每次窗口 ID 的表达式计算结果为 true 时,均会增加此 ID。

  • CONDITIONAL_CHANGE_EVENT 使用上一个值在比较表达式中递增。

在以下示例中,第一个查询会返回 TickStore3 表中的所有记录。第二个查询使用CONDITIONAL_TRUE_EVENT 测试当前出价是否大于给定值 (10.6)。每次表达式测试为 true 时,函数都会递增窗口 ID。函数第一次递增窗口 ID 是在第 2 行,这时的值为 11。如果在下一行中,表达式测试为 false(值不大于 10.6),函数不会递增事件窗口 ID。在最后一行中,对于给定的条件,表达式为 true,因此函数将递增窗口:

下图以图形方式显示了 bid 值和窗口 ID 的变化。因为出价值仅在第二个和第四个时间片(3:00:03 和 3:00:09)中大于 $10.6,窗口 ID 返回了 <0,1,1,2>:

CTE

在以下示例中,第一个查询返回了 TickStore3 表中所有的记录,且这些记录按 tickstore 值 (ts) 进行排序。每次出价值大于 10.6 时,第二个查询使用 CONDITIONAL_TRUE_EVENT 递增窗口 ID。函数第一次递增事件窗口 ID 是在第 2 行,这时的值为 11。此后,窗口 ID 每次都会递增,因为对于每个时间片,表达式 (bid > 10.6) 测试都为 true:

下图以图形方式显示了 bid 值和窗口 ID 的变化。出价值在第二个时间片 (3:00:03) 中大于 10.6,在后面两个时间片中也是如此。函数每次都会递增事件窗口 ID,因为表达式测试为 true:

cte2

基于事件的窗口的高级用法

在基于事件的窗口函数中,条件表达式只能访问当前行中的值。要访问前一个值,您可以使用更强大的基于事件的窗口,使窗口事件条件可以包含前一个数据点。例如,LAG(x, n) 分析函数会从上一个输入记录中检索列 x 中排在第 n 位的值。在这种情况下,LAG 共享 CONDITIONAL_CHANGE_EVENTCONDITIONAL_TRUE_EVENT 函数表达式的 OVER 规范。

在以下示例中,第一个查询会返回 TickStore3 表中的所有记录。第二个查询在其 boolean 表达式中结合 LAG 函数使用了 CONDITIONAL_TRUE_EVENT。在这种情况下,如果当前行的出价值小于前一个值,CONDITIONAL_TRUE_EVENT 每次会递增事件窗口 ID。CONDITIONAL_TRUE_EVENT 第一次递增窗口 ID 是在第三个时间片,这时的表达式测试为 ture。当前值 (10.5) 小于前一个值。由于最后一个值大于前一行,所以在最后一行中没有递增窗口 ID:

下图显示了上述第二个查询。当出价小于前一个值时,函数会递增窗口 ID,这仅会在第三个时间片 (3:00:06) 中发生:

cte3

另请参阅

9 - 基于事件的窗口的会话化

会话化是基于事件的窗口的特殊用例,它通常用于分析点击流,例如根据记录的 Web 点击识别 Web 浏览会话。

在 Vertica 中,假设有一个输入点击流表,其中每行记录了特定用户(或 IP 地址)的一次网页点击,会话化计算将根据两次点击之间的时间间隔对每个用户执行的点击操作进行分组,来尝试从已记录的点击中识别 Web 浏览会话。如果同一个用户的两次点击间隔的时间很长,超出了超时阈值,则会将这两个点击视为来自不同的浏览会话。

示例架构 此主题中的示例使用以下 WebClicks 架构表示一个简单的点击流表:

CREATE TABLE WebClicks(userId INT, timestamp TIMESTAMP);
INSERT INTO WebClicks VALUES (1, '2009-12-08 3:00:00 pm');
INSERT INTO WebClicks VALUES (1, '2009-12-08 3:00:25 pm');
INSERT INTO WebClicks VALUES (1, '2009-12-08 3:00:45 pm');
INSERT INTO WebClicks VALUES (1, '2009-12-08 3:01:45 pm');
INSERT INTO WebClicks VALUES (2, '2009-12-08 3:02:45 pm');
INSERT INTO WebClicks VALUES (2, '2009-12-08 3:02:55 pm');
INSERT INTO WebClicks VALUES (2, '2009-12-08 3:03:55 pm');
COMMIT;

WebClicks 输入表包含以下行:

=> SELECT * FROM WebClicks;
 userId |      timestamp
--------+---------------------
      1 | 2009-12-08 15:00:00
      1 | 2009-12-08 15:00:25
      1 | 2009-12-08 15:00:45
      1 | 2009-12-08 15:01:45
      2 | 2009-12-08 15:02:45
      2 | 2009-12-08 15:02:55
      2 | 2009-12-08 15:03:55
(7 rows)

在下列查询中,会话化在 SELECT 列表列中执行计算,显示使用 LAG() 的当前时间戳值和以前时间戳值之间的差值。当差值大于 30 秒时,它评估为 true,并递增窗口 ID。

=> SELECT userId, timestamp,
     CONDITIONAL_TRUE_EVENT(timestamp - LAG(timestamp) > '30 seconds')
     OVER(PARTITION BY userId ORDER BY timestamp) AS session FROM WebClicks;
 userId |      timestamp      | session
--------+---------------------+---------
      1 | 2009-12-08 15:00:00 |       0
      1 | 2009-12-08 15:00:25 |       0
      1 | 2009-12-08 15:00:45 |       0
      1 | 2009-12-08 15:01:45 |       1
      2 | 2009-12-08 15:02:45 |       0
      2 | 2009-12-08 15:02:55 |       0
      2 | 2009-12-08 15:03:55 |       1
(7 rows)

在输出中,会话列包含来自 CONDITIONAL_TRUE_EVENT 函数的窗口 ID。窗口 ID 在第 4 行(时间戳 15:01:45)中评估为 true,且第 4 行后面的 ID 是零,因为它是新分区的起点(对于用户 ID 2),即第 4 行后面的行没有评估为 ture,直到输出的最后一行。

您可能要为用户设置不同的超时阈值。例如,一个用户的网络连接较慢或者它可能是多任务用户,而另一用户的网络连接较快,主要访问一个网站,执行一项任务。

要基于最后 2 次点击计算自适应的超时阈值,请结合 LAG 使用 CONDITIONAL_TRUE_EVENT 以返回最后 2 次点击之间的平均时间(宽限期为 3 秒):

=> SELECT userId, timestamp, CONDITIONAL_TRUE_EVENT(timestamp - LAG(timestamp) >
(LAG(timestamp, 1) - LAG(timestamp, 3)) / 2 + '3 seconds')
OVER(PARTITION BY userId ORDER BY timestamp) AS session
FROM WebClicks;
 userId |      timestamp      | session
--------+---------------------+---------
      2 | 2009-12-08 15:02:45 |       0
      2 | 2009-12-08 15:02:55 |       0
      2 | 2009-12-08 15:03:55 |       0
      1 | 2009-12-08 15:00:00 |       0
      1 | 2009-12-08 15:00:25 |       0
      1 | 2009-12-08 15:00:45 |       0
      1 | 2009-12-08 15:01:45 |       1
(7 rows)

另请参阅