Почему чтение JDBC ResultSet по позиции быстрее, чем по имени и насколько быстрее?

Объявляя о Hibernate 6, команда Hibernate утверждает, что переключаясь с режима чтения по имени на положение чтения в JDBC ResultSet, они получают выигрыш в производительности.

Высокопроизводительное тестирование производительности показало, что подход Hibernate к чтению значений из ResultSet по имени является его наиболее ограничивающим фактором при масштабировании сквозного потока.

Означает ли это, что они меняют звонки с getString(String columnLabel) в getString(int columnIndex)?

Почему это быстрее?

Как ResultSet интерфейс не зависит от увеличения производительности от драйвера JDBC, реализующего его?

Насколько велика прибыль?

2 ответа

Решение

Выступая в роли сопровождающего драйвера JDBC (и, я допускаю, сделав несколько широких обобщений, которые не обязательно применимы ко всем драйверам JDBC), значения строк обычно будут храниться в массиве или списке, поскольку это наиболее естественно соответствует способу получения данных из сервер базы данных.

В результате, получение значений по индексу будет самым простым. Это может быть так же просто, как что-то вроде (игнорирование некоторых мрачных деталей реализации драйвера JDBC):

public Object getObject(int index) throws SQLException {
    checkValidRow();
    checkValidIndex(index);
    return currentRow[index - 1];
}

Это примерно так быстро, как это возможно.

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

Простая реализация может быть что-то вроде:

public Object getObject(String columnLabel) throws SQLException {
    return getObject(getIndexByLabel(columnLabel));
}

private int getIndexByLabel(String columnLabel) {
    Map<String, Integer> indexMap = createOrGetIndexMap();
    Integer columnIndex = indexMap.get(columnLabel.toLowerCase());
    if (columnIndex == null) {
        throw new SQLException("Column label " + columnLabel + " does not exist in the result set");
    }
    return columnIndex;
}

private Map<String, Integer> createOrGetIndexMap() throws SQLException {
    if (this.indexMap != null) {
        return this.indexMap;
    }
    ResultSetMetaData rsmd = getMetaData();
    Map<String, Integer> map = new HashMap<>(rsmd.getColumnCount());
    // reverse loop to ensure first occurrence of a column label is retained
    for (int idx = rsmd.getColumnCount(); idx > 0; idx--) {
        String label = rsmd.getColumnLabel(idx).toLowerCase();
        map.put(label, idx);
    }
    return this.indexMap = map;
}

В зависимости от API базы данных и доступных метаданных операторов может потребоваться дополнительная обработка для определения фактических меток столбцов запроса. В зависимости от стоимости это, вероятно, будет определено только тогда, когда это действительно необходимо (при доступе к меткам столбцов по имени или при получении метаданных набора результатов). Другими словами, стоимость createOrGetIndexMap() может быть довольно высоким

Но даже если эта стоимость незначительна (например, оператор подготовки метаданных с сервера базы данных включает в себя метки столбцов), затраты на сопоставление метки столбца с индексом и последующий поиск по индексу, очевидно, выше, чем прямой поиск по индексу.

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

Как я уже сказал, это общее обобщение, но я был бы удивлен, если это (поиск по индексу, а затем поиск по индексу) не работает так, как это работает в большинстве драйверов JDBC, а это означает, что я ожидаю, что поиск по индексу как правило, будет быстрее.

Если взглянуть на количество драйверов, это касается:

  • Firebird (Jaybird, раскрытие: я поддерживаю этот драйвер)
  • MySQL (MySQL, Connector / J)
  • PostgreSQL
  • оракул
  • HSQLDB
  • SQL Server (драйвер Microsoft JDBC для SQL Server)

Я не знаю драйверов JDBC, где поиск по имени столбца будет эквивалентен по стоимости или даже дешевле.

В самые первые дни создания я рассматривал оба варианта: доступ к значениям JDBC по индексу или по имени. Я выбрал доступ к вещам по индексу по следующим причинам:

Поддержка СУБД

Не все драйверы JDBC фактически поддерживают доступ к столбцам по имени. Я забыл, какие из них не работали, и если они все еще не работают, потому что я никогда больше не касался этой части API JDBC за 13 лет. Но некоторые этого не сделали, и это уже было для меня препятствием.

Семантика имени

Кроме того, среди тех, кто поддерживает имена столбцов, есть разные семантики имени столбца, в основном два, которые вызывает JDBC:

Существует много неясностей в отношении реализации двух вышеупомянутых, хотя я думаю, что цель вполне ясна:

  • Имя столбца должно создавать имя столбца независимо от псевдонима, например если проецируемое выражение
  • Предполагается, что метка столбца создает метку (или псевдоним) столбца или имя, если псевдоним недоступен, например если проецируемое выражение BOOK.TITLE AS X

Таким образом, эта двусмысленность того, что такое имя/метка, уже очень сбивает с толку и беспокоит. Это не кажется чем-то, на что ORM должен полагаться в целом , хотя в случае с Hibernate можно утверждать, что Hibernate контролирует большую часть генерируемого SQL, по крайней мере, SQL, который создается для выборки сущностей. Но если пользователь пишет HQL или собственный SQL-запрос, я бы не стал полагаться на имя/метку — по крайней мере, не глядя в , первый.

Неясности

В SQL совершенно нормально иметь неоднозначные имена столбцов на верхнем уровне, например:

      SELECT id, id, not_the_id AS id
FROM book

Это совершенно правильный SQL. Вы не можете вложить этот запрос в производную таблицу, где двусмысленности не допускаются, но на верхнем уровне Вы можете. Теперь, что вы собираетесь делать с этими дубликатами метки на верхнем уровне? Вы не можете знать наверняка, какой из них вы получите при доступе к вещам по имени. Первые два могут быть идентичными, но третий сильно отличается.

Единственный способ четко различать столбцы — по индексу, который уникален: , , .

Производительность

Я также попробовал производительность в то время. У меня больше нет результатов тестов, но легко быстро написать еще один тест. В приведенном ниже тесте я запускаю простой запрос к экземпляру H2 в памяти и использую доступ к вещам:

  • По индексу
  • По имени

Результаты ошеломляют:

      Benchmark                            Mode  Cnt        Score       Error  Units
JDBCResultSetBenchmark.indexAccess  thrpt    7  1130734.076 ±  9035.404  ops/s
JDBCResultSetBenchmark.nameAccess   thrpt    7   600540.553 ± 13217.954  ops/s

Несмотря на то, что эталонный тест запускает весь запрос при каждом вызове , доступ по индексу почти в два раза быстрее! Вы можете посмотреть код H2, это открытый исходный код. Он делает это (версия 2.1.212):

      private int getColumnIndex(String columnLabel) {
    checkClosed();
    if (columnLabel == null) {
        throw DbException.getInvalidValueException("columnLabel", null);
    }
    if (columnCount >= 3) {
        // use a hash table if more than 2 columns
        if (columnLabelMap == null) {
            HashMap<String, Integer> map = new HashMap<>();
            // [ ... ]

            columnLabelMap = map;
            if (preparedStatement != null) {
                preparedStatement.setCachedColumnLabelMap(columnLabelMap);
            }
        }
        Integer index = columnLabelMap.get(StringUtils.toUpperEnglish(columnLabel));
        if (index == null) {
            throw DbException.get(ErrorCode.COLUMN_NOT_FOUND_1, columnLabel);
        }
        return index + 1;
    }
    // [ ... ]

Так. есть хэш-карта с верхним регистром, и каждый поиск также выполняет верхний регистр. По крайней мере, он кэширует карту в подготовленном операторе, поэтому:

  • Вы можете повторно использовать его в каждой строке
  • Вы можете повторно использовать его при многократном выполнении инструкции (по крайней мере, так я интерпретирую код)

Таким образом, для очень больших наборов результатов это может быть уже не так важно, но для маленьких определенно имеет значение.

Заключение для ORM

ORM, такие как Hibernate или jOOQjOOQ , контролируют большую часть SQL и набор результатов. Он точно знает, какой столбец находится в какой позиции, эта работа уже была проделана при создании SQL-запроса. Таким образом, нет абсолютно никаких причин полагаться на имя столбца, когда результирующий набор возвращается с сервера базы данных. Каждое значение будет в ожидаемой позиции.

Использование имен столбцов, должно быть, было исторической вещью в Hibernate. Вероятно, также поэтому они использовали для создания этих не очень читаемых псевдонимов столбцов, чтобы убедиться, что каждый псевдоним не является двусмысленным.

Это кажется очевидным улучшением, независимо от фактического прироста в реальном мире (не эталонном) запросе. Даже если бы улучшение составило всего 2%, оно того стоило бы, потому что оно влияет на выполнение каждого запроса каждым приложением на базе Hibernate.

Эталонный код ниже, для воспроизведения

      package org.jooq.test.benchmarks.local;

import java.io.*;
import java.sql.*;
import java.util.Properties;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.*;

@Fork(value = 1)
@Warmup(iterations = 3, time = 3)
@Measurement(iterations = 7, time = 3)
public class JDBCResultSetBenchmark {

    @State(Scope.Benchmark)
    public static class BenchmarkState {

        Connection connection;

        @Setup(Level.Trial)
        public void setup() throws Exception {
            try (InputStream is = BenchmarkState.class.getResourceAsStream("/config.properties")) {
                Properties p = new Properties();
                p.load(is);
                connection = DriverManager.getConnection(
                    p.getProperty("db.url"),
                    p.getProperty("db.username"),
                    p.getProperty("db.password")
                );
            }
        }

        @TearDown(Level.Trial)
        public void teardown() throws Exception {
            connection.close();
        }
    }

    @FunctionalInterface
    interface ThrowingConsumer<T> {
        void accept(T t) throws SQLException;
    }

    private void run(BenchmarkState state, ThrowingConsumer<ResultSet> c) throws SQLException {
        try (Statement s = state.connection.createStatement();
            ResultSet rs = s.executeQuery("select c as c1, c as c2, c as c3, c as c4 from system_range(1, 10) as t(c);")) {
            c.accept(rs);
        }
    }

    @Benchmark
    public void indexAccess(Blackhole blackhole, BenchmarkState state) throws SQLException {
        run(state, rs -> {
            while (rs.next()) {
                blackhole.consume(rs.getInt(1));
                blackhole.consume(rs.getInt(2));
                blackhole.consume(rs.getInt(3));
                blackhole.consume(rs.getInt(4));
            }
        });
    }

    @Benchmark
    public void nameAccess(Blackhole blackhole, BenchmarkState state) throws SQLException {
        run(state, rs -> {
            while (rs.next()) {
                blackhole.consume(rs.getInt("C1"));
                blackhole.consume(rs.getInt("C2"));
                blackhole.consume(rs.getInt("C3"));
                blackhole.consume(rs.getInt("C4"));
            }
        });
    }
}
Другие вопросы по тегам