【问题标题】:How to optimize a query in Oracle SQL where column names occur as function arguments?如何优化 Oracle SQL 中列名作为函数参数出现的查询?
【发布时间】:2014-04-11 04:49:38
【问题描述】:

我的任务是优化 Oracle SQL 中的查询,其中使用与其中一列中 varchar 数据的已解析片段有关的条件将表与自身连接起来。据我了解,Oracle 不会使用索引,因为 ON 子句中的列名仅作为函数的参数出现。查询几乎需要永远完成。使用已处理的 REF 数据创建一个表(见下文)可以解决问题,但由于其他原因,这是不可能的。

我已经准备了一个简化版本的问题来说明(我很确定这是线索,所以我提取了一个更复杂的查询的相关部分)。 “交易”表包含以下列:

  • TRAN -- 一个 10 位数字,是交易代码,
  • STORE -- 进行交易的商店代码,
  • DATE -- 交易日期,
  • REF -- 不同交易的参考代码(在退货等情况下)。此代码的格式为:[商店代码] * [交易年份的最后两位数] * [TRAN 的最后 7 位不带左侧零的数字],因此它看起来像这样:'142*09*3234'。基本上,REF 指向表中的其他行,但在使用之前必须进行一些处理。

    SELECT *
    FROM transactions t1
        JOIN transactions t2
        ON ( t2.store = substr(t1.REF, 1, instr(t1.REF, '*') - 1)
            AND to_char(t2.DATE, 'yy') = substr(t1.REF, instr(t1.REF, '*', 1, 1) + 1), instr(t1.REF, '*', 1, 2) - 1)
            AND to_number(substr(to_char(t2.TRAN), -7)) = to_number(substr(t1.REF, instr(t1.REF, '*', 1, 2) + 1))
           )
    

我没有处理 SQL 优化的经验,所以如果有任何好的方向建议,我将不胜感激。

【问题讨论】:

  • 也许这个 SO 问题会对您有所帮助,也请阅读 cmets:stackoverflow.com/questions/2486952/…,但瓶颈可能在这一行 ...to_char(t2.DATE, 'yy')...
  • 这个加入太可怕了……
  • 解释计划将帮助我们排除故障。例如,优化器可能大大低估了联接的基数,从而导致 NESTED LOOP 而不是 HASH JOIN。在这种情况下,USE_HASH(t1 t2) 提示或有关条件的扩展统计信息可能会有所帮助。但这只是一个没有执行计划的疯狂猜测。
  • 我找到了一个出乎意料的解决方案。我不确定它为什么会起作用,但是使用与查询中嵌套的 substr 和 instr 函数完全相同的存储函数,将速度提高了大约十倍,这对我来说已经足够了。
  • @Tosz 您是否使用了单个函数而不是 3 个条件?对于一个函数或一个复杂的谓词,Oracle 无法做出准确的基数估计,通常只是做出一个疯狂的猜测(我认为 5% 是默认值)。单个函数 (.05) 可能导致比多个谓词 (.05 * .05 * .05) 更好的估计,从而产生更好的计划。

标签: sql oracle optimization query-optimization


【解决方案1】:

您可以在 Oracle 中创建“基于函数的索引”。试试这个:

CREATE INDEX ind_1 ON transactions (SUBSTR(REF, 1, INSTR(REF, '*') - 1));
CREATE INDEX ind_2 ON transactions (SUBSTR(REF, INSTR(REF, '*', 1, 1) + 1), INSTR(REF, '*', 1, 2) - 1));
CREATE INDEX ind_3 ON transactions (TO_NUMBER(SUBSTR(TO_CHAR(TRAN), -7)));
CREATE INDEX ind_4 ON transactions (TO_NUMBER(SUBSTR(REF, INSTR(REF, '*', 1, 2) + 1)));
CREATE INDEX ind_5 ON transactions (TO_CHAR(DATE, 'yy'));

但是,您应该检查解释计划并删除那些未使用的索引。 您还可以创建虚拟列并在那里创建索引。

【讨论】:

  • 我不确定这是个好主意,因为:'当 WHERE 子句选择的行少于大表的 15% 时,索引范围扫描通常具有快速响应时间。如果表达式在基于函数的索引中具体化,优化器可以更准确地估计表达式选择的行数。从这里开始:docs.oracle.com/cd/E11882_01/appdev.112/e25518/… 但在查询中,有任何地方因此选择了 100%...
【解决方案2】:

基本上,你因为糟糕的设计而被搞砸了,欢迎来到我的世界 =)

无论如何,当我查看它时,您只有 2 个字段可用于将事务加入到其引用的事务中:STOREDATE(尽管后者相当“粗糙”)。由于他们决定仅存储交易的最后 7 位数字,因此加快速度的唯一方法是添加一个存储这 7 位数字的新字段。但是,如果您走那条路,那么将整个 REF 语法简单地存储在一个新的(计算的)字段中会更有意义。

这肯定是解决这个问题最不痛苦的方法,因为您可以在所述字段上添加索引,然后将查询更改为

SELECT *
  FROM transactions t1
  JOIN transactions t2
    ON t2.TRAN_AS_REF = t1.REF

但是,据我了解,您将无法/不允许在表格中添加额外的(计算的)字段?! 添加另一个包含此信息的表以便您可以使用它来链接信息也是一种解决方案,尽管它会增加一些复杂性以确保数据始终是最新的!但是,无论如何,据我了解,这不是一个选择。 另一种解决方法可能是创建一个重新调整 TRAN 及其 REF 等效项的视图。您可以具体化所述视图并将其用作连接中的链接。这 - 就像计算字段方法一样 - 将具有始终保持最新状态而无需额外逻辑和/或更改现有逻辑的好处。

或者最后,根据 Wernfrieds 的建议,也许可以创建一个索引,将 TRAN 索引为 REF 语法?我没有这方面的经验,但听起来这是一种选择。

然后索引将类似于

CREATE INDEX ind_ref ON transactions ( STORE + '*' + to_char(DATE, 'yy') + '*' + substr(to_char(TRAN), -7)) )

而您的查询将变成这样:

SELECT *
FROM transactions t1
    JOIN transactions t2
    ON ( (t2.STORE + '*' + to_char(t2.DATE, 'yy') + '*' + substr(to_char(t2.TRAN), -7)) = t1.REF )

并希望服务器随后会选择索引以跟踪正确的 t2 记录。 但就像我说的,我没有这方面的经验,但值得一试恕我直言。

无论如何,如果所有这些都是 nono 并且您只能优化查询,那么我建议通过尽可能好地使用 STORE 和 DATE 信息来充分利用它:

 SELECT *
  FROM transactions t1
  JOIN transactions t2
    ON (    
            t2.store = substr(t1.REF, 1, instr(t1.REF, '*') - 1)
        AND t2.DATE BETWEEN to_date('0101' + substr(t1.REF, instr(t1.REF, '*', 1, 1) + 1), instr(t1.REF, '*', 1, 2) - 1)
                        AND to_date('3112' + substr(t1.REF, instr(t1.REF, '*', 1, 1) + 1), instr(t1.REF, '*', 1, 2) - 1)
        AND t2.TRAN % 10000000 = to_number(substr(t1.REF, instr(t1.REF, '*', 1, 2) + 1))
       )

考虑一下,如果你能添加这个索引

CREATE INDEX ind_tst ON transactions (STORE, DATE, TRAN % 10000000)

那么很可能上面的查询已经比您现在拥有的查询有了很大的改进。大声思考这意味着你也可以试试这个:

CREATE INDEX ind_tst ON transactions (STORE, to_char(DATE, 'yy'), TRAN % 10000000)

SELECT *
  FROM transactions t1
  JOIN transactions t2
    ON (    
            t2.store = substr(t1.REF, 1, instr(t1.REF, '*') - 1)
        AND to_char(t2.DATE, 'yy') = substr(t1.REF, instr(t1.REF, '*', 1, 1) + 1), instr(t1.REF, '*', 1, 2) - 1)
        AND t2.TRAN % 10000000 = to_number(substr(t1.REF, instr(t1.REF, '*', 1, 2) + 1))
       )

希望这会有所帮助。由于我没有使用 Oracle 的经验,您可能需要在这里和那里修复语法,对此感到抱歉......无论如何,祝你好运!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-27
    相关资源
    最近更新 更多