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
我们在使用分页访问 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
xormaxResults