EclipseLink с Oracle: «limit by rownum» не использует индекс

#oracle #eclipselink

#Oracle #eclipselink

Вопрос:

мы сталкиваемся с проблемами производительности с EclipseLink 2.7.7 при доступе к таблицам Oracle 12.1 с помощью подкачки. Исследование показало, что 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;
 

План объяснения показывает, что используется индекс on 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;
 

Для этого запроса план объяснения показывает, что индекс on colN не используется.
В результате запрос таблицы с миллионами строк занимает 50-90 секунд (в зависимости от аппаратного обеспечения).
Примечание: в моей тестовой базе данных этот запрос возвращает 0 записей, поскольку значения ColN указаны до 2021-12-08.

Oracle 12c ввел синтаксис СМЕЩЕНИЯ / ВЫБОРКИ:

 -- #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;
 

Используя этот синтаксис, индексы, по крайней мере, иногда используются так, как ожидалось. Когда они используются, время выполнения составляет менее 1 секунды, что является приемлемым.
Однако я не мог понять, как убедить EclipseLink использовать этот синтаксис.

Если ORDER BY col1 DESC он удален из исходного постраничного запроса (# 2), используется индекс, запрос возвращается достаточно быстро. Однако он не вернет нужные записи, так что это не поможет.

Как я могу реализовать высокопроизводительные постраничные запросы с использованием EclipseLink и Oracle 12? Как я могу заставить oracle использовать индекс on colN при использовании подкачки и order by ?

Ответ №1:

printSQLSelectStatement Метод OraclePlatform отвечает за построение используемого запроса, вложение запросов для использования 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) amp;amp; !(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);
}
 

Затем вы должны указать свой пользовательский класс OraclePlatform, используя постоянное свойство:

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

Если что-то подобное работает для вас, пожалуйста, отправьте это как запрос на улучшение — хотя вы, возможно, захотите каким-то образом использовать в нем старое поведение, поскольку различия в производительности, с которыми вы столкнулись, могут зависеть от используемого запроса / данных.

Комментарии:

1. Спасибо, Крис, интересная идея. Переключение между реализацией по умолчанию и специальной реализацией может быть реализовано с помощью подсказки запроса или есть лучший api? Таким образом, влияние на другие (хорошо протестированные) запросы может быть ограничено.

2. Я не играл с ним довольно давно, поэтому я не знаком с тем, что может передаваться при вызове базы данных. Возможно, вам придется настроить его в отладчике, чтобы выяснить, что может сработать.

3. Я реализовал что-то похожее на ваш пример, но (как и ожидалось) это не решило проблему: oracle выбирает правильный индекс только в 50% запросов

4. использование неправильного индекса было проблемой, не связанной с типом данных timestamp date

Ответ №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) amp;amp; !(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 , чтобы получить количество строк вместо «lastRowNum» при вызове printer.printParameter(DatabaseCall.MAXROW_FIELD);
  • Я также пытаюсь разобраться с отсутствующим firstRow xor maxResults