因为first_value和last_value默认窗口帧为rows between unbounded preceding and current row,仅覆盖当前行及之前行,导致last_value无法看到分组末尾、first_value可能取错首行;必须显式指定rows between unbounded preceding and unbounded following并按时间确定性排序才能正确获取分组首尾记录。

为什么FIRST_VALUE和LAST_VALUE返回的不是真正的首尾访问记录
直接对 user_id 分组后用 FIRST_VALUE(page) 和 LAST_VALUE(page),大概率会得到错误结果——因为这两个函数默认窗口帧是 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,LAST_VALUE 在当前行截止,根本看不到组内最后一行。不显式定义 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING,LAST_VALUE 就几乎等于 MAX()(但语义不同),而非时间上最后那条。
正确写法:必须显式指定完整窗口范围 + 按时间排序
要拿到每个用户按时间排序后的首页和末页,窗口定义必须满足两个条件:按 user_id 分组、按访问时间(比如 visit_time)升序排序、帧范围覆盖整个分组。否则 FIRST_VALUE 可能取到非最早时间的值,LAST_VALUE 更容易出错。
示例(以 PostgreSQL / MySQL 8.0+ / SQL Server 为基准):
SELECT DISTINCT
user_id,
FIRST_VALUE(page) OVER (
PARTITION BY user_id
ORDER BY visit_time ASC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS first_page,
LAST_VALUE(page) OVER (
PARTITION BY user_id
ORDER BY visit_time ASC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS last_page
FROM visits;
-
FIRST_VALUE和LAST_VALUE必须共用同一ORDER BY,否则逻辑断裂 - MySQL 8.0+ 支持该语法;PostgreSQL 要求
ROWS显式声明,否则LAST_VALUE行为不可靠 - SQLite 不支持
LAST_VALUE,需改用子查询或MAX(visit_time)关联
更稳妥的替代方案:用子查询或聚合 + JOIN
当数据库版本旧、或需要严格控制去重逻辑(比如同毫秒多条记录时),窗口函数反而增加理解成本。这时用两层聚合更直观可靠。
例如获取每个用户的首次/末次页面及对应时间:
SELECT
a.user_id,
a.first_page,
b.last_page
FROM (
SELECT user_id, page AS first_page
FROM visits v1
WHERE visit_time = (
SELECT MIN(visit_time)
FROM visits v2
WHERE v2.user_id = v1.user_id
)
LIMIT 1
) a
JOIN (
SELECT user_id, page AS last_page
FROM visits v1
WHERE visit_time = (
SELECT MAX(visit_time)
FROM visits v2
WHERE v2.user_id = v1.user_id
)
LIMIT 1
) b ON a.user_id = b.user_id;
- 注意:如果存在相同
visit_time的多条记录,上述子查询可能返回多行,需加LIMIT 1或用ROW_NUMBER()去重 - 相比窗口函数,这种写法在 Oracle 11g、SQL Server 2005 等老版本中兼容性更好
- 性能上,若
(user_id, visit_time)有联合索引,子查询方式并不比窗口函数慢多少
常见报错与绕过方式:'LAST_VALUE not supported' 或 'frame clause required'
遇到 ERROR: window function last_value requires a frame clause(PostgreSQL)或 LAST_VALUE is not supported(旧版 MySQL/SQLite),说明当前环境不支持或未启用完整窗口语法。
- PostgreSQL:必须写全
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING,省略任何一部分都会报错 - MySQL 5.7 或更早:无窗口函数,只能用变量模拟或子查询
- SQLite:截至 3.40 仍不支持
LAST_VALUE,可用MAX(visit_time)+ 关联查出对应page - 别用
LAG/LEAD替代——它们只看相邻行,无法跨整个分组取极值
MIN()/MAX() 配合关联查询往往比强行用 LAST_VALUE 更稳。真正需要保留原始行上下文(比如同时取页面、设备类型、来源渠道)时,才值得花精力调通窗口帧。










