FetchMode Join против SubSelect

У меня есть две таблицы Employee и Department следующие классы сущностей для них обоих

Department.java
@Entity
@Table(name = "DEPARTMENT")
public class Department {
    @Id
    @Column(name = "DEPARTMENT_ID")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer departmentId;
    @Column(name = "DEPARTMENT_NAME")
    private String departmentName;
    @Column(name = "LOCATION")
    private String location;

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "department", orphanRemoval = true)
    @Fetch(FetchMode.SUBSELECT)
    //@Fetch(FetchMode.JOIN)
    private List<Employee> employees = new ArrayList<>();
}


Employee.java
@Entity
@Table(name = "EMPLOYEE")
public class Employee {
    @Id
    @SequenceGenerator(name = "emp_seq", sequenceName = "seq_employee")
    @GeneratedValue(generator = "emp_seq")
    @Column(name = "EMPLOYEE_ID")
    private Integer employeeId;
    @Column(name = "EMPLOYEE_NAME")
    private String employeeName;

    @ManyToOne
    @JoinColumn(name = "DEPARTMENT_ID")
    private Department department;
}

Ниже приведены запросы, когда я сделал em.find(Department.class, 1);

- fetch mode = fetchmode.join

    SELECT department0_.DEPARTMENT_ID AS DEPARTMENT_ID1_0_0_,
      department0_.DEPARTMENT_NAME    AS DEPARTMENT_NAME2_0_0_,
      department0_.LOCATION           AS LOCATION3_0_0_,
      employees1_.DEPARTMENT_ID       AS DEPARTMENT_ID3_1_1_,
      employees1_.EMPLOYEE_ID         AS EMPLOYEE_ID1_1_1_,
      employees1_.EMPLOYEE_ID         AS EMPLOYEE_ID1_1_2_,
      employees1_.DEPARTMENT_ID       AS DEPARTMENT_ID3_1_2_,
      employees1_.EMPLOYEE_NAME       AS EMPLOYEE_NAME2_1_2_
    FROM DEPARTMENT department0_
    LEFT OUTER JOIN EMPLOYEE employees1_
    ON department0_.DEPARTMENT_ID   =employees1_.DEPARTMENT_ID
    WHERE department0_.DEPARTMENT_ID=?

- fetch mode = fetchmode.subselect

    SELECT department0_.DEPARTMENT_ID AS DEPARTMENT_ID1_0_0_,
      department0_.DEPARTMENT_NAME    AS DEPARTMENT_NAME2_0_0_,
      department0_.LOCATION           AS LOCATION3_0_0_
    FROM DEPARTMENT department0_
    WHERE department0_.DEPARTMENT_ID=?

    SELECT employees0_.DEPARTMENT_ID AS DEPARTMENT_ID3_1_0_,
      employees0_.EMPLOYEE_ID        AS EMPLOYEE_ID1_1_0_,
      employees0_.EMPLOYEE_ID        AS EMPLOYEE_ID1_1_1_,
      employees0_.DEPARTMENT_ID      AS DEPARTMENT_ID3_1_1_,
      employees0_.EMPLOYEE_NAME      AS EMPLOYEE_NAME2_1_1_
    FROM EMPLOYEE employees0_
    WHERE employees0_.DEPARTMENT_ID=?

Я просто хотел знать, какой из них мы предпочитаем FetchMode.JOIN или же FetchMode.SUBSELECT? какой из них мы должны выбрать в каком сценарии?

4 ответа

Стратегия SUBQUERY, на которую ссылается Marmite, относится к FetchMode.SELECT, а не SUBSELECT.

Вывод консоли, который вы опубликовали о fetchmode.subselect, любопытен, потому что это не тот способ, который должен работать.

FetchMode.SUBSELECT

используйте запрос на выборку для загрузки дополнительных коллекций

Документы Hibernate:

Если нужно выбрать одну отложенную коллекцию или однозначный прокси-сервер, Hibernate загрузит их все, повторно выполнив исходный запрос в подвыборке. Это работает так же, как пакетная выборка, но без частичной загрузки.

FetchMode.SUBSELECT должен выглядеть примерно так:

SELECT <employees columns>
FROM EMPLOYEE employees0_
WHERE employees0_.DEPARTMENT_ID IN
(SELECT department0_.DEPARTMENT_ID FROM DEPARTMENT department0_)

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

Однако FetchMode.SUBSELECT значительно сокращает количество запросов, поскольку принимает только два запроса по сравнению с N+1 запросами FecthMode.SELECT.

Вы можете подумать, что FetchMode.JOIN делает еще меньше запросов, всего 1, так зачем вообще использовать SUBSELECT? Ну, это правда, но за счет дублированных данных и более тяжелого ответа.

Если однозначный прокси-сервер должен быть выбран с помощью JOIN, запрос может получить:

+---------------+---------+-----------+
| DEPARTMENT_ID | BOSS_ID | BOSS_NAME |
+---------------+---------+-----------+
|             1 |       1 | GABRIEL   |
|             2 |       1 | GABRIEL   |
|             3 |       2 | ALEJANDRO |
+---------------+---------+-----------+

Данные о сотруднике начальника дублируются, если он руководит более чем одним отделом, и их стоимость в полосе пропускания.

Если ленивая коллекция должна быть извлечена с помощью JOIN, запрос может получить:

+---------------+---------------+-------------+
| DEPARTMENT_ID | DEPARTMENT_ID | EMPLOYEE_ID |
+---------------+---------------+-------------+
|             1 | Sales         | GABRIEL     |
|             1 | Sales         | ALEJANDRO   |
|             2 | RRHH          | DANILO      |
+---------------+---------------+-------------+

Данные отдела дублируются, если они содержат более одного сотрудника (естественный случай). Мы не только несем затраты в пропускной способности, но также получаем дублированные дублированные объекты Department, и мы должны использовать SET или DISTINCT_ROOT_ENTITY для дедупликации.

Однако, как говорит Маркус Винанд, дублирование данных в pos с более низкой задержкой является хорошим компромиссом во многих случаях.

Соединение SQL по-прежнему более эффективно, чем метод вложенных выборок, даже несмотря на то, что он выполняет те же поиски по индексу, потому что избегает большого количества сетевых взаимодействий. Это даже быстрее, если общий объем передаваемых данных будет больше из-за дублирования атрибутов сотрудников для каждой продажи. Это связано с двумя показателями производительности: время отклика и пропускная способность; в компьютерных сетях мы называем их задержкой и пропускной способностью. Пропускная способность оказывает незначительное влияние на время отклика, но задержки имеют огромное влияние. Это означает, что количество обращений к базе данных является более важным для времени отклика, чем количество передаваемых данных.

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

Hibernate: 
    select ...
    from mkyong.stock stock0_

Hibernate: 
    select ...
    from mkyong.stock_daily_record stockdaily0_ 
    where
        stockdaily0_.STOCK_ID in (
            ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
        )

(Это может быть интересным тестом, если пакетная выборка с очень большим размером пакета будет работать как SUBSELECT, но без проблемы загрузки всей таблицы)

Пара сообщений, показывающих различные стратегии извлечения и журналы SQL (очень важно):

Резюме:

  • JOIN: позволяет избежать основной проблемы N+1 запросов, но может извлечь дублированные данные.
  • SUBSELECT: также избегает N+1 и не дублирует данные, но загружает все объекты связанного типа в память.

Таблицы были построены с использованием ascii-таблиц.

Я бы сказал, что это зависит...

Предположим, у вас есть N сотрудников в отделе, который содержит D байтов информации, а средний сотрудник состоит из E байтов. (Байты являются суммой длины атрибута с некоторыми издержками).

Используя стратегию соединения, вы выполняете 1 запрос и передаете данные N * (D + E).

Используя стратегию подзапроса, вы выполняете 1 + N запросов, но передаете только данные D + N*E.

Обычно запрос N+1 - это NO GO, если N большое, поэтому JOIN предпочтительнее.

Но на самом деле вы должны проверить свой пробег между количеством запросов и передачи данных.

Обратите внимание, что я не рассматриваю другие аспекты как кэширование Hibernate.

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

Планки сказал

(1) Это вводит в заблуждение. (2) Подвыбор не будет загружать всю вашу базу данных в память. Связанная статья о причуде, где subselect (3) игнорирует команды подкачки от родителя, (4), но это все еще подвыбор.

  1. После вашего комментария я снова проверил FetchMode.SUBSELECT и обнаружил, что мой ответ не совсем верен.
  2. Это была гипотетическая ситуация, когда гидратация каждой сущности, которая была полностью загружена в память (в данном случае Сотрудник), прекратит увлажнение многих других сущностей. Истинная проблема заключается в загрузке всей выбранной таблицы, если эта таблица содержит тысячи строк (даже если каждая из них не извлекает другие объекты из других таблиц).
  3. Я не знаю, что вы имеете в виду под командами подкачки от родителя.
  4. Да, это все еще подвыбор, но я не знаю, что вы пытаетесь указать на это.

Вывод консоли, который вы опубликовали о fetchmode.subselect, любопытен, потому что это не тот способ, который должен работать.

Это верно, но только когда происходит скрытие более чем одной сущности Департамента (что означает неинициализацию более чем одной коллекции сотрудников), я протестировал ее с 3.6.10.Final и 4.3.8.Final В сценариях 2.2 (FetchMode.SUBSELECT hidrating 2 из 3 департаментов) и 3.2 (FetchMode.SUBSELECT скрывает все департаменты), SubselectFetch.toSubselectString возвращает следующее (ссылки на классы Hibernate взяты из тега 4.3.8.Final):

select this_.DEPARTMENT_ID from SUBSELECT_DEPARTMENT this_

Этот подзапрос после используется для построения предложения where в OneToManyJoinWalker.initStatementString, заканчивающемся на

employees0_.DEPARTMENT_ID in (select this_.DEPARTMENT_ID from SUBSELECT_DEPARTMENT this_)

Затем в коллекцию CollectionJoinWalker.whereString добавляется предложение where, заканчивающееся

select employees0_.DEPARTMENT_ID as DEPARTMENT3_2_1_, employees0_.EMPLOYEE_ID as EMPLOYEE1_1_, employees0_.EMPLOYEE_ID as EMPLOYEE1_3_0_, employees0_.DEPARTMENT_ID as DEPARTMENT3_3_0_, employees0_.EMPLOYEE_NAME as EMPLOYEE2_3_0_ from SUBSELECT_EMPLOYEE employees0_ where employees0_.DEPARTMENT_ID in (select this_.DEPARTMENT_ID from SUBSELECT_DEPARTMENT this_)

С помощью этого запроса в обоих случаях все сотрудники извлекаются и обрабатываются. Это явно проблема в сценарии 2.2, потому что мы гидратируем только Отделы 1 и 2, но также увлажняем всех Сотрудников, даже если они не принадлежат этим Отделам (в данном случае Сотрудники Отдела 3).

Если в сеансе присутствует только одна сущность Department с неинициализированной коллекцией сотрудников, запрос аналогичен тому, который написал eatSleepCode. Проверьте сценарий 1.2

select subselectd0_.department_id as departme1_2_0_, subselectd0_.department_name as departme2_2_0_, subselectd0_.location as location3_2_0_ from subselect_department subselectd0_ where subselectd0_.department_id=?

Из FetchStyle

    /**
     * Performs a separate SQL select to load the indicated data.  This can either be eager (the second select is
     * issued immediately) or lazy (the second select is delayed until the data is needed).
     */
    SELECT,
    /**
     * Inherently an eager style of fetching.  The data to be fetched is obtained as part of an SQL join.
     */
    JOIN,
    /**
     * Initializes a number of indicated data items (entities or collections) in a series of grouped sql selects
     * using an in-style sql restriction to define the batch size.  Again, can be either eager or lazy.
     */
    BATCH,
    /**
     * Performs fetching of associated data (currently limited to only collections) based on the sql restriction
     * used to load the owner.  Again, can be either eager or lazy.
     */
    SUBSELECT

До сих пор я не мог понять, что означает этот Javadoc:

на основе ограничения SQL, используемого для загрузки владельца

ОБНОВЛЕНИЕ Планки сказал:

Вместо этого он просто загружает таблицу в худшем случае, и даже тогда, только если в вашем первоначальном запросе не было предложения where. Поэтому я бы сказал, что использование запросов на выборку может неожиданно загрузить всю таблицу, если вы ОГРАНИЧИВАЕТЕ результаты, и у вас нет критериев WHERE.

Это правда, и это очень важная деталь, которую я протестировал в новом сценарии 4.2.

Запрос, сгенерированный для выборки сотрудников:

select employees0_.department_id as departme3_4_1_, employees0_.employee_id as employee1_5_1_, employees0_.employee_id as employee1_5_0_, employees0_.department_id as departme3_5_0_, employees0_.employee_name as employee2_5_0_ from subselect_employee employees0_ where employees0_.department_id in (select this_.department_id from subselect_department this_ where this_.department_name>=?)

Подзапрос внутри предложения where содержит исходное ограничение this_.department_name> =?, избегая нагрузки на всех сотрудников. Это то, что означает Javadoc с

на основе ограничения SQL, используемого для загрузки владельца

Все, что я сказал о FetchMode.JOIN и различиях с FetchMode.SUBSELECT, остается верным (и также относится к FetchMode.SELECT).

У моего клиента (финансовые услуги) была похожая проблема, и он хотел "получить данные одним запросом". Я объяснил, что лучше иметь более одного запроса из-за следующего:

Для FetchMode.JOIN отдел будет переноситься из базы данных в приложение один раз для каждого сотрудника, поскольку операция объединения приводит к умножению отдела на сотрудника. Если у вас есть 10 отделов с 100 сотрудниками в каждом, каждое из этих 10 отделов будет передаваться 100 раз в одном запросе, простой SQL. Таким образом, каждый отдел в этом случае переносится в 99 раз чаще, чем необходимо, что приводит к накладным расходам на передачу данных для отдела.

Для Fetchmode SUBSELECT два запроса запускаются к базе данных. Один будет использоваться для получения данных о 1000 сотрудников, один - для 10 отделов. Для меня это звучит гораздо эффективнее. Конечно, вы должны убедиться, что индексы на месте, так что данные могут быть получены немедленно.

Я бы предпочел FetchMode.SUBSELECT.

Это был бы другой случай, если бы в каждом отделе был только один сотрудник, но, как предполагает название "отдел", это было бы очень маловероятным.

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

Другие вопросы по тегам