【问题标题】:EclipseLink with Oracle: "limit by rownum" does not use indexEclipseLink 与 Oracle:“rownum 限制”不使用索引
【发布时间】:2022-01-13 10:35:36
【问题描述】:

在使用分页访问 Oracle 12.1 表时,我们遇到了 EclipseLink 2.7.7 的性能问题。调查表明,Oracle 不会将其索引与 EclipseLink 分页一起使用。

我已提取发送到数据库的 sql,并且能够使用数据库工具 (DataGrip) 重现该问题。

例子:

-- #1: without paging
        SELECT col1 AS a1, col2 AS a2, col3 AS a3, ...
        FROM <TABLE>
        WHERE colN > to_timestamp('2021-12-08', 'yyyy-mm-dd'))
        ORDER BY col1 DESC;

解释计划显示使用了colN 上的索引。很好。

当使用分页执行相同的查询时,原始查询被包装在两个子选择中:

-- #2 with EclipseLink paging
SELECT * FROM (
    SELECT a.*, ROWNUM rnum  FROM (
        SELECT col1 AS a1, col2 AS a2, col3 AS a3, ...
        FROM <TABLE>
        WHERE colN > to_timestamp('2021-12-08', 'yyyy-mm-dd'))
        ORDER BY col1 DESC    
    ) a WHERE ROWNUM <= 100
) WHERE rnum > 0;

对于此查询,解释计划显示colN 上的索引未使用。 因此,查询具有数百万行的表需要 50-90 秒(取决于硬件)。 旁注:在我的测试数据库中,此查询返回 0 条记录,因为 colN 值在 2021-12-08 之前。

Oracle 12c 引入了 OFFSET/FETCH 语法:

-- #3
        SELECT col1 AS a1, col2 AS a2, col3 AS a3, ...
        FROM <TABLE>
        WHERE colN > to_timestamp('2021-12-08', 'yyyy-mm-dd'))
        ORDER BY col1 DESC
        OFFSET 0 ROWS FETCH NEXT 100 ROWS ONLY;

使用这种语法,索引至少有时会按预期使用。使用它们时,执行时间低于 1s,这是可以接受的。 但是,我不知道如何说服 EclipseLink 使用这种语法。

如果 ORDER BY col1 DESC 从原始分页查询 (#2) 中删除,则使用索引,查询返回速度足够快。但是,它不会返回所需的记录,因此无济于事。

如何使用 EclipseLink 和 Oracle 12 实现高性能的分页查询? 使用分页和order by时如何强制oracle使用colN上的索引?

【问题讨论】:

    标签: oracle eclipselink


    【解决方案1】:

    OraclePlatform printSQLSelectStatement 方法负责构建所使用的查询,嵌套查询以使用 rownum 来处理您看到的查询。要使用新表单,您将扩展您正在使用的 OraclePlatform 类之一(可能是 Oracle12Platform)并覆盖该方法以附加您想要的语法。比如:

    @Override
    public void printSQLSelectStatement(DatabaseCall call, ExpressionSQLPrinter printer, SQLSelectStatement statement) {
        int max = 0;
        int firstRow = 0;
    
        ReadQuery query = statement.getQuery();
        if (query != null) {
            max = query.getMaxRows();
            firstRow = query.getFirstResult();
        }
    
        if (!(this.shouldUseRownumFiltering()) || (!(max > 0) && !(firstRow > 0))) {
            super.printSQLSelectStatement(call, printer, statement);
            return;
        }
        call.setFields(statement.printSQL(printer));
        printer.printString("OFFSET ");
        printer.printParameter(DatabaseCall.MAXROW_FIELD);
        printer.printString(" ROWS FETCH NEXT ");
        printer.printParameter(DatabaseCall.FIRSTRESULT_FIELD);
        printer.printString(" ROWS ONLY");
        call.setIgnoreFirstRowSetting(true);
        call.setIgnoreMaxResultsSetting(true);
    }
    

    然后您将使用 persistent property 指定您的自定义 OraclePlatform 类:

    <property name="eclipselink.target-database" value="my.package.MyOracle12Platform"/>
    

    如果类似的方法适合您,请将其作为增强请求提交 - 尽管您可能希望通过某种方式在其中使用旧行为,因为您遇到的性能差异可能取决于查询/数据参与。

    【讨论】:

    • 谢谢克里斯,有趣的想法。在默认和特殊实现之间切换可以使用查询提示来实现,还是有更好的 api?这样,对其他(经过良好测试的)查询的影响可能会受到限制。
    • 我已经很长时间没有玩过它了,所以我不熟悉 DatabaseCall 上可能通过的内容。您可能必须在调试器中对其进行设置才能找出可行的方法。
    • 我已经实现了与您的示例类似的东西,但是(正如预期的那样)这并没有解决问题:oracle 仅在 50% 的查询中选择正确的索引
    • 使用错误的索引是一个不相关的timestampdate 数据类型问题
    【解决方案2】:

    感谢@Chris,我想出了以下Oracle12Platform。此解决方案当前忽略了“错误 #453208 - 带有查询行限制的悲观锁定在 Oracle DB 上不起作用”。详情见OraclePlatform.printSQLSelectStatement):

    public class Oracle12Platform extends org.eclipse.persistence.platform.database.Oracle12Platform {
    
        /**
         * the oracle 12c `OFFSET x ROWS FETCH NEXT y ROWS ONLY` requires `maxRows` to return the row count
         */
        @Override
        public int computeMaxRowsForSQL(final int firstResultIndex, final int maxResults) {
            return maxResults - max(firstResultIndex, 0);
        }
    
        @Override
        public void printSQLSelectStatement(final DatabaseCall call, final ExpressionSQLPrinter printer, final SQLSelectStatement statement) {
            int max = 0;
            int firstRow = 0;
    
            final ReadQuery query = statement.getQuery();
            if (query != null) {
                max = query.getMaxRows();
                firstRow = query.getFirstResult();
            }
    
            if (!(this.shouldUseRownumFiltering()) || (!(max > 0) && !(firstRow > 0))) {
                super.printSQLSelectStatement(call, printer, statement);
            } else {
                statement.setUseUniqueFieldAliases(true);
                call.setFields(statement.printSQL(printer));
                if (firstRow > 0) {
                    printer.printString(" OFFSET ");
                    printer.printParameter(DatabaseCall.FIRSTRESULT_FIELD);
                    printer.printString(" ROWS");
                    call.setIgnoreFirstRowSetting(true);
                }
                if (max > 0) {
                    printer.printString(" FETCH NEXT ");
                    printer.printParameter(DatabaseCall.MAXROW_FIELD); //see #computeMaxRowsForSQL
                    printer.printString(" ROWS ONLY");
                    call.setIgnoreMaxResultsSetting(true);
                }
            }
        }
    }
    
    • 在调用printer.printParameter(DatabaseCall.MAXROW_FIELD); 时,我必须重写computeMaxRowsForSQL 以获取行数而不是“lastRowNum”
    • 我也尝试处理丢失的firstRow xor maxResults

    【讨论】:

      猜你喜欢
      • 2020-11-16
      • 1970-01-01
      • 1970-01-01
      • 2013-06-13
      • 1970-01-01
      • 1970-01-01
      • 2020-08-12
      • 1970-01-01
      • 2014-12-27
      相关资源
      最近更新 更多