EclipseLink 与 Oracle:"limit by rownum" 不使用索引

EclipseLink with Oracle: "limit by rownum" does not use index

我们在使用分页访问 Oracle 12.1 tables 时遇到 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 上的 索引 。 因此,查询具有数百万行的 table 需要 50-90 秒(取决于硬件)。 旁注:在我的测试数据库中,此查询 returns 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,即acceptable。 但是,我不知道如何说服 EclipseLink 使用这种语法。

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

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

OraclePlatform printSQLSelectStatement 方法负责构建所使用的查询,嵌套查询以将 rownum 用于您看到的查询。要使用新表单,您将扩展您正在使用的 OraclePlatform classes 之一(可能是 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 class
<property name="eclipselink.target-database" value="my.package.MyOracle12Platform"/>

如果类似的东西对您有用,请将其作为增强请求提交 - 尽管您可能想通过某种方式将旧行为用于其中,因为您遇到的性能差异可能取决于 query/data参与。

感谢@Chris,我想出了以下 Oracle12Platform。该解决方案目前忽略了“Bug #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);
            }
        }
    }
}
  • 我不得不覆盖 computeMaxRowsForSQL 以便在调用 printer.printParameter(DatabaseCall.MAXROW_FIELD);
  • 时获取行数而不是“lastRowNum”
  • 我也尝试处理丢失的 firstRow xor maxResults