oracle三组难缠的hint no_unnest/unnest,push_subq,push_pred

Oracle提示:no_unnest, unnest与push_subq、push_pred解析
本文探讨了Oracle SQL优化中涉及的三个复杂提示:no_unnest、unnest和push_subq、push_pred。no_unnest防止子查询展开,保持其嵌套状态,而unnest则相反。push_subq影响子查询的连接顺序,push_pred用于不可合并视图的外部查询谓词。通过示例解释了这些提示如何影响查询执行计划和优化。

最近在看一个哥们优化sql里面,使用到了几个特殊的hint,但我总是对此不解,所以针对此问题自己做了一些测试(参考了一些资料)

 1. no_unnest, unnest
 unnest我们称为对子查询展开,顾名思义,就是别让子查询孤单地嵌套(nest)在里面。
 所以un_unnest双重否定代表肯定,即让子查询不展开,让它嵌套(nest)在里面。

 如果一个查询中的where 中出现 view 或者 子查询。那么oracle的CBO在解析着这个SQL之前要做转换,把VIEW或者子查询"打开"~ 这个操作就叫做unnest~ 然后可以把主查询和子查询中的表通过表连接的方式,生成执行计划。你可以使用NO_UNNEST这个hint来告诉CBO,不要“打开” VIEW或者子查询~ 这样VIEW或者子查询只能被当作一个独立查询,来进行解析,里面的表无法和主查询中的表进行连接

 现做一个简单的实验:

SQL> select count(*) from dba_objects;
  COUNT(*)
----------
     60494

这里子查询自动展开(unnest),即TMP_LIUHC_1和TMP_LIUHC_2 hash join在一起。
接下来如果我们不希望TMP_LIUHC_2展开,想先让它单独的执行完,然后再来和外部查询进行一种叫做FILTER的操作。
那么我们加入hint no_unnest:

所以,什么时候该用no_unnest使得子查询能够独立的执行完毕之后再跟外围的查询做FILTER?
首先,子查询的返回结果集应该较小,然后外围查询的输入的distinct value也应该较小。
<think>我们正在优化一个PostgreSQL查询,该查询的目标是从表"tb_push_data"中提取非重复的公司代码。这些公司码存储在一个以逗号分隔的字符串列(company_code)中。 原始查询: SELECT DISTINCT unnest(string_to_array(company_code, ',')) FROM "tb_push_data" WHERE push_type='0' and deleted='0' 优化思路: 1. 由于company_code是逗号分隔的字符串,我们需要先将其拆分成数组,然后unnest展开成多行,最后去重。 2. 问题:这个查询可能会因为以下原因效率较低: a. 需要处理每一行,将字符串分割成数组,然后展开,这是一个计算密集型操作。 b. 如果表很大,WHERE条件筛选后的数据量仍然很大,那么unnest和去重的操作开销会很大。 3. 优化方向: a. 考虑在WHERE条件上创建索引,以加快筛选速度(push_type和deleted)。 b. 考虑是否可以将company_code的存储方式改为数组类型或者使用关系表(规范化),这样查询效率会更高。但这属于模式变更,需要权衡。 c. 如果无法改变表结构,我们可以尝试减少需要处理的数据量(例如,通过更精确的筛选条件)。 d. 另外,DISTINCT操作可能会消耗较多内存,如果结果集很大,可以考虑使用其他方法。 然而,我们注意到原始查询已经使用了WHERE条件(push_type='0' and deleted='0')来限制数据。因此,我们首先应该确保这两个字段有索引。 建议的优化步骤: 步骤1:创建索引 如果还没有索引,在push_type和deleted上创建索引(或者复合索引): ```sql CREATE INDEX idx_tb_push_data_push_type_deleted ON tb_push_data(push_type, deleted); ``` 步骤2:如果数据量非常大,考虑使用LATERAL连接来拆分字符串,并尽早去重(但注意,DISTINCT是最后执行的操作,所以可能效果有限)。我们可以尝试改写查询,让拆分的行数尽可能少(即先过滤再拆分)。 实际上,原始查询已经是先过滤再拆分。但是,由于拆分后行数会变多,然后再去重,这可能会导致中间结果很大。 另一种写法是使用子查询先过滤,然后再拆分: ```sql SELECT DISTINCT unnest(string_to_array(company_code, ',')) FROM (SELECT company_code FROM tb_push_data WHERE push_type='0' and deleted='0') AS filtered_data; ``` 这种写法逻辑上一样,但可能不会带来性能提升。 步骤3:考虑使用递归查询或者生成系列函数,但在这里不适用。 步骤4:如果同一个company_code字符串在表中重复出现多次,我们可以考虑先对company_code去重(在拆分前),然后再拆分并去重。这样可能会减少需要处理的数据量。 例如: ```sql SELECT DISTINCT unnest(string_to_array(company_code, ',')) FROM (SELECT DISTINCT company_code FROM tb_push_data WHERE push_type='0' and deleted='0') AS distinct_codes; ``` 这个查询先对company_code列本身去重(即相同的字符串只保留一次),然后再拆分每一个字符串并去重。如果不同的行经常包含相同的公司代码字符串,那么这样做可以减少重复拆分的次数。 但是注意:两个不同的字符串可能包含相同的公司代码(例如,"A,B"和"B,A"),但这种情况下去重后拆分可能会漏掉一些组合?实际上不会,因为拆分后都是单个公司代码,然后整体再去重。所以这个优化是可行的。 然而,这个优化也有风险:如果两个不同的字符串包含的公司代码完全不同,那么先对字符串去重就没有减少数据量;如果有很多重复的字符串,那么就会减少。 步骤5:使用CTE(公用表表达式)来组织查询,但性能上不一定有提升。 步骤6:如果公司代码的字符串非常长(包含很多公司代码),那么拆分操作会很耗时。我们可以尝试在应用层处理,但不符合SQL优化的要求。 步骤7:考虑使用并行查询。PostgreSQL默认会尝试并行执行,确保数据库的配置允许并行,并且工作线程足够。 步骤8:如果数据量巨大且更新不频繁,可以考虑物化视图。 步骤9:另一个思路是,将拆分后的公司代码存储到另一个表中,并定期更新(比如通过触发器或定时任务)。这样查询就会直接从一个规范化的表里查,性能会很好。 根据实际情况选择优化方案。 总结:在没有改变表结构的情况下,我们可以尝试以下优化: 优化方案A(创建索引): ```sql CREATE INDEX idx_tb_push_data_push_type_deleted ON tb_push_data(push_type, deleted); ``` 优化方案B(先对company_code字符串去重): ```sql SELECT DISTINCT unnest(string_to_array(company_code, ',')) FROM ( SELECT DISTINCT company_code FROM tb_push_data WHERE push_type='0' and deleted='0' ) AS distinct_codes; ``` 我们可以测试两种方案,看看哪种效果更好。 注意:在优化方案B中,内层的DISTINCT减少的是相同字符串的数量,然后外层再拆分并去重。如果原始数据中company_code重复率很高,那么内层DISTINCT会大大减少数据量,从而提升性能。如果重复率很低,则可能增加一层DISTINCT操作(需要哈希或排序)的开销,反而可能变慢。 因此,优化方案B需要根据数据分布情况而定。 另外,我们还可以尝试使用WITH语句(CTE)来写优化方案B,但效果一样。 优化方案C(使用正则表达式拆分,但可能不如string_to_array高效,因此不推荐)。 实际测试:建议在实际环境中对原始查询和优化后的查询进行EXPLAIN ANALYZE,比较执行计划。 其他建议:长期来看,建议将company_code列改为数组类型(如果PostgreSQL版本支持)或者使用关系表(多对多关系)来存储。这样就可以直接使用数组操作或者连接查询,并且可以建立索引(比如GIN索引)来加速查询。 举例,如果使用数组类型,可以这样: ```sql -- 先将列改为数组类型 ALTER TABLE tb_push_data ALTER COLUMN company_code TYPE text[] USING string_to_array(company_code, ','); -- 然后查询可以改为(使用unnest): SELECT DISTINCT unnest(company_code) FROM tb_push_data WHERE push_type='0' and deleted='0'; -- 或者,为了更快,可以创建一个GIN索引: CREATE INDEX idx_company_code_gin ON tb_push_data USING gin (company_code); -- 但是注意:这样并不能直接加速DISTINCT unnest,因为unnest仍然要展开所有行。另一种方法是使用数组的元素生成一个单独的表(通过触发器或物化视图)。 由于改变表结构可能影响其他应用,需要谨慎。 综上所述,我们给出两个短期的优化建议(索引和改写查询),以及一个长期的建议(改变表结构)。 </think>### SQL查询优化分析 您的查询目标是提取`tb_push_data`表中`company_code`字段(逗号分隔的字符串)拆分后的唯一值。原始查询: ```sql SELECT DISTINCT unnest(string_to_array(company_code, ',')) FROM "tb_push_data" WHERE push_type='0' AND deleted='0' ``` #### 优化建议: 1. **索引优化**: - 在筛选列创建复合索引加速数据过滤: ```sql CREATE INDEX idx_push_filter ON tb_push_data(push_type, deleted); ``` - 若`company_code`值重复率高,考虑表达式索引: ```sql CREATE INDEX idx_company_codes ON tb_push_data (regexp_split_to_table(company_code, ',')); ``` 2. **减少中间结果集体积**: ```sql WITH filtered AS ( SELECT company_code FROM tb_push_data WHERE push_type='0' AND deleted='0' ) SELECT DISTINCT unnest(string_to_array(company_code, ',')) FROM filtered; ``` 3. **表结构优化建议**: - **长期方案**:将逗号分隔的数据转为关系表(1NF): ```sql -- 新建关联表 CREATE TABLE company_push_rel ( push_id INT REFERENCES tb_push_data(id), company_code VARCHAR(32) ); -- 迁移现有数据 INSERT INTO company_push_rel SELECT id, unnest(string_to_array(company_code, ',')) FROM tb_push_data; ``` - 查询优化为(无需字符串处理): ```sql SELECT DISTINCT company_code FROM company_push_rel WHERE push_id IN ( SELECT id FROM tb_push_data WHERE push_type='0' AND deleted='0' ); ``` 4. **函数调用优化**: PostgreSQL 14+ 可使用`STRICT`函数避免空值处理: ```sql SELECT DISTINCT unnest(COALESCE(string_to_array(company_code, ','), ARRAY[]::text[])) ... ``` #### 性能对比验证: 执行计划检查命令: ```sql EXPLAIN ANALYZE SELECT DISTINCT unnest(string_to_array(company_code, ',')) FROM tb_push_data WHERE push_type='0' AND deleted='0'; ``` --- ### 相关问题 1. 如何分析PostgreSQL查询的执行计划? 2. 什么场景下适合使用`unnest()`函数? 3. 数据库表结构违反第一范式(1NF)会带来哪些问题? 4. PostgreSQL中数组类型和JSONB类型在处理结构化数据时有何优劣? 5. 如何监控SQL查询的实时性能表现?[^1] [^1]: 数据库性能监控可参考pg_stat_statements扩展文档
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值