web-dev-qa-db-fra.com

FetchMode Join vs SubSelect

J'ai deux tables Employee et Department suivantes sont les classes d'entités pour les deux

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

Vous trouverez ci-dessous les requêtes déclenchées lorsque j'ai em.find(Department.class, 1);

- mode de récupération = 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=?

- mode de récupération = 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=?

Je voulais juste savoir lequel devrait-on préférer FetchMode.JOIN ou FetchMode.SUBSELECT? Lequel devrions-nous choisir dans quel scénario?

14
eatSleepCode

La stratégie SUBQUERY à laquelle Marmite fait référence est liée à FetchMode.SELECT, pas à SUBSELECT.

La sortie de la console que vous avez publiée à propos de fetchmode.subselect} est curieuse car ce n'est pas ainsi que cela est censé fonctionner.

Le FetchMode.SUBSELECT

utiliser une requête de sous-sélection pour charger les collections supplémentaires

Hibernate docs :

Si une collection paresseuse ou un proxy à valeur unique doit être récupéré, Hibernate les chargera tous, en réexécutant la requête d'origine dans une sous-sélection. Cela fonctionne de la même manière que la récupération par lots, mais sans chargement au coup par coup.

FetchMode.SUBSELECT devrait ressembler à ceci: 

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

Vous pouvez voir que cette seconde requête ramènera en mémoire tous les employés appartenant à un département (par exemple, employee.department_id n'est pas null), peu importe si ce n'est pas le service que vous récupérez. votre première requête . Il s’agit donc d’un problème majeur si le tableau des employés est volumineux, car il peut s’agir de charger accidentellement toute une base de données en mémoire .

Cependant, FetchMode.SUBSELECT réduit significativement le nombre de requêtes car ne prend que deux requêtes en comparaison des requêtes N + 1 du FecthMode.SELECT.

Vous pensez peut-être que FetchMode.JOIN fait encore moins de requêtes, seulement 1, alors pourquoi utiliser SUBSELECT? C'est vrai, mais au prix de doublons de données et d'une réponse plus lourde.

Si un proxy à valeur unique doit être récupéré avec JOIN, la requête peut extraire:

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

Les données d'employé du patron sont dupliquées s'il dirige plus d'un service et que le coût est en bande passante.

Si une collection paresseuse doit être récupérée avec JOIN, la requête peut extraire:

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

Les données de département sont dupliquées si elles contiennent plus d’un employé (le cas naturel) . Nous ne subissons pas seulement un coût en bande passante, mais nous obtenons en double objets de département dupliqués et nous devons utiliser un SET ou DISTINCT_ROOT_ENTITY pour dédupliquer.

Cependant, les doublons de données avec une latence plus faible constituent souvent un bon compromis, comme Markus Winand dit .

Une jointure SQL est toujours plus efficace que l'approche imbriquée - même si elle effectue les mêmes recherches d'index - car elle évite de nombreuses communications réseau. Il est encore plus rapide si la quantité totale de données transférées est plus grande en raison de la duplication} d'attributs d'employé pour chaque vente. Cela s'explique par les deux dimensions de la performance: temps de réponse et débit; dans les réseaux informatiques, nous les appelons latence et bande passante. La bande passante n'a qu'un impact mineur sur le temps de réponse mais les latences ont un impact énorme. Cela signifie que le nombre d'allers-retours dans la base de données est plus important pour le temps de réponse que la quantité de données transférée.

Donc, le principal problème de l’utilisation de SUBSELECT est que difficile à contrôler et peut charger un graphe entier d’entités en mémoire . ne souffrez pas de doublons), progressivement et le plus important, vous interrogez uniquement les entités associées (afin de ne pas avoir à charger un graphique volumineux) car la sous-requête IN est filtrée par les ID récupérés par la requête externe).

Hibernate: 
    select ...
    from mkyong.stock stock0_

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

(Il peut être intéressant de vérifier si l'extraction par lot avec une taille de lot très élevée agirait comme un SUBSELECT mais sans le problème de charger tout le tableau)

Quelques articles montrant les différentes stratégies d’extraction et les journaux SQL (très important):

Résumé:

  • JOIN: évite le problème majeur des requêtes N + 1 mais peut récupérer les données dupliquées.
  • SUBSELECT: évite aussi N + 1 et ne duplique pas les données mais charge en mémoire toutes les entités du type associé.

Les tables ont été construites avec ascii-tables .

27
gabrielgiussi

Je dirais que ça dépend ...

Supposons que vous avez N employés dans un département, qui contient D octets d'informations et qu'un employé moyen se compose de E octets. (Les octets sont la somme de la longueur de l'attribut avec une surcharge).

En utilisant la stratégie join, vous effectuez une requête et transférez des données N * (D + E).

En utilisant la stratégie subquery, vous effectuez des requêtes 1 + N, mais transférez uniquement des données D + N * E.

En règle générale, la requête N + 1 est le NO GO si le N est grand, le paramètre JOIN est donc préférable.

Mais en réalité, vous devez vérifier votre kilométrage entre le nombre de requêtes et le transfert de données.

Notez que je ne considère pas d'autres aspects comme la mise en cache Hibernate.

Un aspect subtil supplémentaire pourrait être valable si la table employee est volumineuse et partitionnée - l'élagage de partition sur l'accès à l'index doit également être pris en compte. 

8
Marmite Bomber

Un de mes clients (services financiers) avait un problème similaire et il souhaitait "acquérir les données en une seule requête". Eh bien, j’ai expliqué qu’il est préférable d’avoir plusieurs requêtes, pour les raisons suivantes:

Pour FetchMode.JOIN, le service serait transféré de la base de données vers l'application une fois par employé, car l'opération de jointure entraîne la multiplication du service par employé. Si vous avez 10 départements avec 100 employés chacun, chacun de ces 10 départements sera transféré 100 fois en une seule requête, SQL simple. Ainsi, chaque département, dans ce cas, est transféré 99 fois plus souvent que nécessaire, ce qui entraîne un temps système supplémentaire de transfert de données.

Pour Fetchmode, SUBSELECT, deux requêtes sont envoyées à la base de données. L'un serait utilisé pour obtenir les données des 1000 employés, l'autre pour obtenir les 10 départements. Cela me semble beaucoup plus efficace. Vous devez vous assurer que les index sont en place pour que les données puissent être récupérées immédiatement.

Je préférerais FetchMode.SUBSELECT.

Ce serait un autre cas si chaque département ne comptait qu'un seul employé, mais, comme le suggère le nom de "département", cela serait très peu probable.

Je suggère de mesurer les temps d'accès pour étayer cette théorie. Pour mon client, j'ai effectué des mesures pour différents types d'accès, et le tableau "département" pour mon client contenait beaucoup plus de champs (je ne l'avais pas conçu, cependant). Ainsi, il devint rapidement évident que le FetchMode.SUBSELECT était beaucoup plus rapide.

1
michaeak

Dit Planky 

(1) Ceci est grossièrement trompeur. (2) La sous-sélection ne va pas récupérer toute votre base de données en mémoire. L'article lié concerne une bizarrerie où subselect (3) ignore les commandes de pagination du parent (4), mais il s'agit toujours d'une sous-sélection.

  1. Après votre commentaire, j'ai à nouveau enquêté sur FetchMode.SUBSELECT et j'ai découvert que ma réponse n'était pas tout à fait correcte.
  2. Il s’agissait d’une situation hypothétique dans laquelle l’hydratation de chaque entité qui était entièrement chargée en mémoire (Employee dans ce cas) mettrait fin à l’hydratation de nombreuses autres entités. Le vrai problème est de charger la table entière en cours de sous-sélection si cette table contient des milliers de lignes (même si chacune d’elles ne ne récupère pas avec impatience d’autres entités à partir d’autres tables).
  3. Je ne sais pas ce que vous entendez par commandes de pagination du parent.
  4. Oui, c'est toujours un sous-choix, mais je ne sais pas ce que vous essayez de souligner avec cela.

La sortie de la console que vous avez publiée à propos de fetchmode.subselect est curieuse, car ce n'est pas ainsi que cela est censé fonctionner.

Cela est vrai, mais seulement lorsque plusieurs entités ministérielles sont cachées (ce qui signifie plus d'une collection d'employés non initialisée), je l'ai testé avec 3.6.10.Final et 4.3.8.Final Dans les scénarios 2.2 (FetchMode.SUBSELECT masquant 2 des 3 départements) et 3.2 (FetchMode.SUBSELECT masquant tous les départements) , SubselectFetch.toSubselectString renvoie le texte suivant (les liens vers les classes Hibernate proviennent de la balise 4.3.8.Final):

select this_.DEPARTMENT_ID from SUBSELECT_DEPARTMENT this_

Cette sous-requête est ensuite utilisée pour construire la clause where par OneToManyJoinWalker.initStatementString se terminant par 

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

Ensuite, la clause where est ajoutée dans CollectionJoinWalker.whereString se terminant par

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_)

Avec cette requête, dans les deux cas, tous les employés sont récupérés et hydratés ..__ Ceci est clairement un problème dans le scénario 2.2 car nous n'hydratons que les départements 1 et 2 mais nous hydratons également tous les employés même s'ils n'appartiennent pas à ces départements. (dans ce cas, les employés du département 3).

S'il n'y a qu'une seule entité de service hydratée dans la session avec une collection d'employés non initialisée, la requête est semblable à celle écrite par eatSleepCode. Vérifier scénario 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=?

De 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

Jusqu'à présent, je ne pouvais pas résoudre le sens de cette Javadoc:

basé sur la restriction SQL utilisée pour charger le propriétaire </ strike>

UPDATE Planky a déclaré: 

Au lieu de cela, il va simplement charger la table au pire, et même alors, seulement si votre requête initiale n'a pas de clause where. Donc, je dirais que using les requêtes de sous-sélection peuvent charger de manière inattendue la table entière si vous LIMITEZ les résultats et que vous n'avez aucun critère WHERE.

C’est vrai et c’est un détail très important que j’ai testé dans le nouveau scénario 4.2

La requête générée pour aller chercher les employés est 

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>=?)

La sous-requête à l'intérieur de la clause where contient la restriction d'origine this_.department_name> =?, évitant la charge de tous les employés . C'est ce que le javadoc signifie avec

basé sur la restriction SQL utilisée pour charger le propriétaire

Tout ce que j'ai dit à propos de FetchMode.JOIN et des différences entre FetchMode.SUBSELECT reste vrai (et s'applique également à FetchMode.SELECT).

1
gabrielgiussi