现代SQL执行顺序,不再是经典7步!
随着SQL的进化,现代SQL执行顺序已经远远超越经典“7步逻辑执行顺序”(FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT)。SQL新语法,不仅扩展了执行阶段,还改变了优化器行为。各数据库也趋向封闭,跨数据库兼容性问题,真的越来越是一个问题。
一、现代SQL执行顺序,不再是经典7步
(一)经典7步(SQL-92逻辑模型)
1、FROM/JOIN
2、WHERE
3、GROUP BY
4、HAVING
5、SELECT(含表达式、别名)
6、ORDER BY
7、LIMIT/OFFSET
适用:简单查询、传统OLTP场景;不适用:窗口函数、模式匹配、嵌套展开、向量搜索等等。
(二)现代SQL扩展后的逻辑执行模型(10+阶段)
|
|
|
|
|
|---|---|---|---|
WITH/CTE处理 |
CTE是内联、物化还是递归计算 |
WITH ... AS
WITH ... AS MATERIALIZED(Oracle/PG)、WITH RECURSIVE(ANSI标准) |
FROM;部分引擎(如:MySQL)延迟处理递归CTE |
FROM/JOIN/LATERAL/TABLE FUNCTION |
|
LATERAL
CROSS APPLY,TABLE(func),OUTER APPLY(SQL Server) |
|
UNNEST/ARRAY JOIN/FLATTEN |
JSON数组、Variant数组) |
UNNEST()
ARRAY JOIN,FLATTEN(),JSON_TABLE()(Oracle) |
FROM后、WHERE前 |
WHERE |
|
= / > / <>)、IN()、EXISTS()、BETWEEN |
|
GROUP BY/ROLLUP/CUBE/GROUPING SETS |
|
ROLLUP
CUBE,GROUPING SETS |
SUM/COUNT)在此计算 |
HAVING |
|
HAVING SUM(amount) > 1000 |
GROUP BY结果 |
MATCH_RECOGNIZE(状态机匹配) |
|
MATCH_RECOGNIZE
Oracle/Snowflake 7.19+) |
GROUP BY之后、WINDOW之前;需独立指定PARTITION BY和ORDER BY |
WINDOW(窗口函数计算) |
ROW_NUMBER(),SUM() OVER等(不改变行数) |
ROW_NUMBER/LAG/SUM OVER) |
SELECT前,是计算窗口函数的逻辑执行层,语法上窗口函数写在SELECT子句中,但逻辑执行早于SELECT投影) |
QUALIFY |
|
QUALIFY
Snowflake/BigQuery/Redshift≥3.0) |
WINDOW之后、SELECT之前 |
SELECT(投影) |
|
SELECT col AS alias
col * 2 AS double_col,ROW_NUMBER() OVER (...) AS rn |
|
DISTINCT |
SELECT指定列) |
SELECT DISTINCT col1, col2 |
|
ORDER BY |
|
ORDER BY
ORDER BY vec <-> target |
|
LIMIT/OFFSET |
ORDER BY稳定排序) |
LIMIT |
|
ANN/VECTOR SEARCH |
|
ORDER BY vec <-> target
PGVector)、VECTOR_SEARCH()(Snowflake Cortex) |
ORDER BY → LIMIT阶段,物理层通过ANN索引(如:HNSW)优化距离计算与排序,仅部分引擎支持独立向量范围查询(如:pgvector的<->过滤) |
注:在多数场景下,阶段“13”并非新增逻辑阶段,而是对ORDER BY ... LIMIT的向量化物理执行优化;在逻辑执行模型中,向量近似搜索(ANN)不构成独立阶段,其语义仍属于ORDER BY distance_expr LIMIT k。数据库通过重写执行计划,将欧氏/余弦距离计算与ANN索引(如:IVFFlat、HNSW)结合,实现物理层加速;但部分数据库(如:PostgreSQL+pgvector)也支持非排序的向量距离过滤。
(三)部分新阶段详解
1、QUALIFY:窗口后过滤
(1)正确位置:简单地讲:逻辑执行上,QUALIFY在WINDOW之后、SELECT之前。我们具体地讲:逻辑执行上,QUALIFY在窗口函数计算完成后、SELECT投影前执行。虽然窗口函数语法写在SELECT中,但其计算发生在SELECT逻辑阶段之前;QUALIFY可引用这些已经计算的窗口结果(包括:别名),但其本质是作用在窗口函数输出的过滤层。我们的理由是:SQL标准中并无QUALIFY,其语义是“对窗口函数结果进行过滤”,因此它不能早于窗口函数计算,但也不属于SELECT投影的一部分。我们看看Snowflake文档,它明确指出:QUALIFYis evaluated after window functions and beforeSELECT DISTINCT。
(2)执行顺序应为:
... → GROUP BY → HAVING → WINDOW → QUALIFY → SELECT → DISTINCT → ORDER BY → LIMIT
(3)为什么能引用SELECT别名?
在SELECT阶段定义窗口函数别名,但逻辑上在WINDOW阶段已计算完成。QUALIFY可以引用SELECT子句中定义的窗口函数别名(如:rn),但不能引用SELECT中定义的其他表达式(如:col * 2 AS double_col)。这是因为窗口函数在逻辑上先于SELECT投影计算完成,而QUALIFY是对窗口函数输出的过滤层。SQL引擎允许在QUALIFY中使用这些别名,是为了避免我们重复书写窗口函数表达式,属于我们喜欢的“语法糖”,但其底层逻辑仍要求窗口函数必须在QUALIFY之前计算完毕。
(4)等价转换(正确理解):
-- 使用QUALIFY(Snowflake/BigQuery)
SELECT user_id, session_id
FROM sessions
QUALIFY ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY ts DESC) <= 3;
-- 实际等价于:
SELECT user_id, session_id
FROM (
SELECT user_id, session_id,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY ts DESC) AS rn
FROM sessions
) AS sub
WHERE rn <= 3;
(5)示例:用户最近3次登录会话(跨数据库统一逻辑)
我们为每个用户保留最近3次登录记录,用于行为分析:
-- 统一模型(通过dbt宏)
SELECT
user_id,
login_time,
ip_address
FROM {{ ref('stg_logins') }}
{{ qualify("ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_time DESC) <= 3") }}
-
在Snowflake/BigQuery/Redshift(≥3.0)中:直接使用 QUALIFY,执行计划高效。 -
在PostgreSQL/MySQL/ClickHouse中:自动转为子查询,避免语法错误。 -
扩展场景:过滤极端值(如:保留订单金额10%-90%分位数)
SELECT order_id, amount
FROM orders
QUALIFY PERCENT_RANK() OVER (ORDER BY amount) BETWEEN 0.1 AND 0.9;
2、MATCH_RECOGNIZE:独立状态机引擎
-
执行逻辑: -
对每个 PARTITION BY分组(如:用户ID); -
按 ORDER BY排序(如:事件时间); -
启动有限状态自动机,逐行匹配 PATTERN(如:连续3次失败); -
匹配成功后,根据 AFTER MATCH SKIP决定跳转位置(如:TO NEXT ROW)。
-
-
不属于传统流水线,而是嵌入在GROUP BY之后的专用处理阶段。 -
与 GROUP BY共存问题:-
目前Oracle和Snowflake的 MATCH_RECOGNIZE不允许与GROUP BY、WINDOW、DISTINCT或聚合函数共存在同一查询块。如果需要组合使用,我们必须通过子查询或CTE分离逻辑。我们看Snowflake官方文档,它明确列出:MATCH_RECOGNIZEcannot be used withGROUP BY,HAVING,WINDOW,QUALIFY, orDISTINCTin the same SELECT。 -
为保证可移植性,我们强烈建议通过子查询组合使用。
-
示例1:我们要识别连续3次失败登录(安全风控场景)
检测是否存在连续3次“failed”状态的登录事件:
-- Oracle/Snowflake实现
SELECT *
FROM login_events
MATCH_RECOGNIZE (
PARTITION BY user_id
ORDER BY event_time
MEASURES
FIRST(fail.event_time) AS first_fail_time,
COUNT(*) AS fail_count
AFTER MATCH SKIP TO NEXT ROW
PATTERN (fail{3})
DEFINE
fail AS status = 'failed'
);
示例2:我们要识别用户转化路径(用户行为分析)
匹配“浏览→加购→下单”的完整行为序列:
SELECT *
FROM user_events
MATCH_RECOGNIZE (
PARTITION BY user_id
ORDER BY event_time
MEASURES
FIRST(browse.event_time) AS browse_time,
FIRST(purchase.event_time) AS purchase_time,
DATEDIFF('second', browse.event_time, purchase.event_time) AS convert_seconds
AFTER MATCH SKIP TO NEXT ROW
PATTERN (browse add_to_cart purchase)
DEFINE
browse AS event_type = 'product_view',
add_to_cart AS event_type = 'add_to_cart',
purchase AS event_type = 'order_pay'
);
-
输出:仅返回满足模式的序列及关键指标(如:转化时长)。 -
优势:比自连接或窗口函数,更简洁、性能更高(状态机逐行匹配,无需回溯)。
替代方案(非Oracle/Snowflake):在dbt中,我们通过Python UDF或“窗口函数+序列分组”标记连续事件(如:ROW_NUMBER() - ROW_NUMBER() OVER (PARTITION BY user_id, status ORDER BY event_time) AS seq_id)。
3、ARRAY JOIN/UNNEST:嵌套展开阶段
-
逻辑位置:在 FROM之后、WHERE之前。 -
但优化器可能下推谓词(如:ClickHouse/BigQuery),使实际执行更早。 -
风险:如果先展开再过滤,可能导致行数爆炸(如:1行含1000个元素,展开后需过滤999个)。
示例:我们要分析用户点击流中的商品曝光序列
表结构:user_id,session_id,events ARRAY<STRUCT<event_type STRING, item_id INT>>
-- ClickHouse高效写法
SELECT
user_id,
e.item_id,
COUNT(*) AS view_count
FROM (
SELECT
user_id,
arrayFilter(e -> e.event_type = 'view', events) AS filtered_events
FROM user_sessions
WHERE arrayExists(e -> e.event_type = 'view', events) -- 提前过滤含“view”事件的行
)
ARRAY JOIN filtered_events AS e
GROUP BY user_id, e.item_id;
-
对比低效写法:如果先
ARRAY JOIN再WHERE e.event_type = 'view',可能展开百万级无效事件。 -
收益:I/O降低90%+,查询从30s → 0.5s。
-
跨引擎嵌套展开语法对比:
数据库 数组展开语法 JSON数组展开语法 ClickHouse ARRAY JOIN arr AS elemARRAY JOIN JSONExtractArrayRaw(json_col) AS elemSnowflake LATERAL FLATTEN(arr) AS fLATERAL FLATTEN(json_col:arr) AS fBigQuery UNNEST(arr) AS elemUNNEST(JSON_EXTRACT_ARRAY(json_col, '$.arr')) AS elemPostgreSQL UNNEST(arr) AS elemjsonb_array_elements(jsonb_col->'arr') AS elemOracle TABLE(collection)或 JSON_TABLE(...)JSON_TABLE(json_col, '$.arr[*]' COLUMNS (...))
⚠️ 说明:(1)PostgreSQL的UNNEST()仅支持SQL数组类型(如:int[]、TEXT[]),不支持jsonb类型。jsonb是独立的复合类型,我们必须通过专用函数(jsonb_array_elements()等)转换为行集合后才能展开。(2)PostgreSQL不允许对jsonb类型直接使用UNNEST(),我们必须先通过jsonb_array_elements()等函数,将其转换为行集合。
二、优化器行为更复杂
以ClickHouse谓词下推到数组层为例介绍。
(一)传统优化器行为
谓词下推(Predicate Pushdown):将WHERE条件尽可能下推到FROM或存储层,减少中间数据量。例如:SELECT * FROM t WHERE a = 1→ 存储引擎只读a=1的行,避免读取全表。
(二)ClickHouse的“数组级谓词下推”
ClickHouse支持对嵌套数据结构进行更细粒度的下推,突破传统“行级下推”的限制:
1、场景:events表含Array(Tuple(name, timestamp))或Nested类型
(1)低效写法(先展开再过滤):
SELECT user_id, event.name
FROM user_events
ARRAY JOIN events AS event
WHERE event.name = 'click';
-
逻辑:先对每行展开所有 events元素 → 生成N行 → 再过滤name = 'click'。 -
如果平均每行1000个事件,仅1个是 'click',则99.9%的展开是浪费。
(2)高效写法(利用数组函数预过滤):
SELECT user_id, event.name
FROM (
SELECT user_id,
arrayFilter(e -> e.name = 'click', events) AS filtered
FROM user_events
WHERE arrayExists(e -> e.name = 'click', events) -- 适用于 Nested/Array(Tuple)
)
ARRAY JOIN filtered AS event;
说明:has(events.name, 'click')适用于一维简单数组(如:Array(String))及Nested类型的虚拟数组列(如:events.name);对Array(Tuple)类型,需使用arrayExists或arrayFilter。
示例:
-- Nested类型:events.name是虚拟列(Array(String))
CREATE TABLE t (user_id UInt32, events Nested(name String, ts DateTime));
SELECT * FROM t WHERE has(events.name, 'click'); -- 合法
-- Array(Tuple):无自动展开
CREATE TABLE t2 (user_id UInt32, events Array(Tuple(name String, ts DateTime)));
-- has(events.name, ...)语法错误
2、ClickHouse优化器做了什么?
(1)arrayExists(...):
-
利用列式存储,只读相关子列。 -
使用跳数索引( skip index)快速跳过无匹配数据块。 -
避免读取整行数据,减少 I/O开销。
(2)arrayFilter(...):
-
在向量化执行引擎中,通过 SIMD指令批量过滤数组元素。 -
只保留满足条件的元素,大幅缩小 ARRAY JOIN输入规模。
本质总结:优化器不再只在“行级别”下推,而是在“嵌套结构内部”进行谓词下推,这是对传统关系模型的突破。
(三)其他数据库嵌套谓词下推能力对比
|
|
|
|
|---|---|---|
ClickHouse |
JSON) |
arrayExists/arrayFilter(结构化数据)、has(一维数组) |
BigQuery |
JSON) |
EXISTS(SELECT 1 FROM UNNEST(arr) e WHERE e.col = 'val')下推 |
Snowflake |
Variant) |
FLATTEN+ 优化器猜测,需显式指定过滤条件 |
PostgreSQL |
JSONB) |
GIN索引,jsonb_path_exists支持有限;数组支持@>操作符 |
Oracle |
JSON) |
JSON
JSON_EXISTS下推 |
示例:我们筛选包含特定标签的用户画像
表结构:user_id,tags Array(String)(如['premium', 'mobile', 'us'])
我们按数据结构区分:
-- ClickHouse(一维数组)
SELECT user_id
FROM user_profiles
WHERE has(tags, 'premium'); -- 仅适用于Array(String),has()适用于 Array(String)、Array(Int)等扁平数组
-- ClickHouse(结构化数组或Nested)
SELECT user_id
FROM user_events
WHERE arrayExists(e -> e.name = 'click', events); -- arrayExists()适用于Array(Tuple(...))或Nested类型
-- PostgreSQL(一维数组,需GIN索引)
SELECT user_id
FROM user_profiles
WHERE tags @> ARRAY['premium'];
-- PostgreSQL(jsonb数组)
SELECT user_id
FROM user_profiles
WHERE jsonb_path_exists(profile, '$.tags[*] ? (@ == "premium")');
-- BigQuery
SELECT user_id
FROM user_profiles
WHERE EXISTS(SELECT 1 FROM UNNEST(tags) t WHERE t = 'premium');
三、跨数据库兼容性极差
为何必须抽象为中间层(如:dbt)?
(一)问题根源
现代SQL特性已经高度数据库厂商绑定,即使是同功能,语法、性能、语义也存在巨大差异:
|
|
Oracle |
Snowflake |
BigQuery |
ClickHouse |
PostgreSQL |
|---|---|---|---|---|---|
|
|
|
QUALIFY |
QUALIFY |
|
|
|
|
MATCH_RECOGNIZE |
MATCH_RECOGNIZE(7.19+) |
|
|
|
|
|
JSON_TABLE
TABLE() |
FLATTEN() |
UNNEST() |
ARRAY JOIN |
jsonb_array_elements()
UNNEST() |
|
|
23c) |
Cortex) |
Preview) |
|
pgvector扩展,PostgreSQL≥11支持基础向量类型,PostgreSQL≥14+pgvector≥0.5.0支持HNSW索引加速ANN搜索) |
|
|
CONNECT BY/WITH RECURSIVE |
WITH RECURSIVE |
WITH RECURSIVE |
|
WITH RECURSIVE |
向量搜索说明:向量搜索并非新增逻辑阶段,而是对ORDER BY + LIMIT模式的物理层加速机制。逻辑执行模型中,仍归属ORDER BY → LIMIT阶段。
(二)解决方法:抽象为中间层(以dbt为例)
1、使用宏(Macros)封装差异
-- macros/qualify.sql
{% macro qualify(original_query, condition) %}
{% if target.type in ['snowflake', 'bigquery'] or (target.type == 'redshift' and target.version >= '3.0') %}
{{ original_query }}
QUALIFY {{ condition }}
{% else %}
SELECT * FROM (
{{ original_query }}
-- 假设原查询已包含窗口函数并命名为rn,此处condition为rn<=3
) AS sub
WHERE {{ condition }}
{% endif %}
{% endmacro %}
注:更健壮的实现,我们需结合caller()或传入子查询,此处为简化示意。
2、适配层模型
统一输出结构,屏蔽底层差异。
3、测试驱动兼容性
通过dbt-expectations和多目标测试保障一致性。
4、避免使用专属语法
核心模型,我们用ANSI SQL;专属功能,我们把它隔离到专用模块。
我们一句话总结一下:用ANSI SQL写逻辑,用宏封装差异,用测试保障一致。
版权保护: 本文由 绿茶加糖-郭保升 原创,转载请保留链接: https://www.guobaosheng.com/shujuku/431.html
- 上一篇:一文吃透286个SQL关键字及相关扩展语法元素
- 下一篇:没有了
