[Tips] SymfonyLive Paris 2016 - Comment mettre à jour 30 000 produits avec Doctrine2 ? (André Tapia & Amine Mokeddem)
- Ressources
- Cas pratique : mettre à jour 30 000 produits
- Récapitulatif des approches
- Approche 1 : méthode la plus simple
- Approche 2 : augmentation du nombre de
flush()
(batchSize à 20) - Approche 3 : désactivation des logs SQL
- Approche 4 : augmentation du nombre de
flush()
(batchSize à 1000) - Approche 5 : utilisation du
iterate()
à la place dugetResult()
- Approche 6 : utilisation d’une transaction avec
flush()
- Approche 7 : utilisation d’une transaction avec une requête SQL
- Approche 8 : si tous les produits ont une réduction de 10%
Les tips de cette fiche proviennent de la partie Astuces pour améliorer les performances
de la vidéo "SymfonyLive Paris 2016 - André Tapia & Amine Mokeddem - Aller plus loin avec Doctrine2" - https://youtu.be/X-Srb9b-8xE?t=826
Merci à eux deux pour cette conférence :)
📎
|
Retrouvez le listing de mes mémos, tips et autres sur https://github.com/jprivet-dev/memos |
Nous sommes en période de soldes, et nous devons mettre à jour le prix de 30000 produits, sur une base de données MySQL.
id |
title |
pretax_price |
in_stock |
… |
… |
1 |
Produit X |
120,53 |
16 |
… |
… |
… |
… |
… |
… |
… |
… |
30000 |
Produit Y |
27,17 |
39 |
… |
… |
Nous avons une méthode isolée qui va donner le montant de la réduction à appliquer à chaque produit.
Approches |
Temps d’exécution |
Mémoire utilisée |
|||
(ms) |
Diff. avec 1 |
(Mb) |
Diff. avec 1 |
||
1 |
Méthode la plus simple |
23 435 |
219 |
||
2 |
Augmentation du nombre de |
1 419 137 |
+5956% |
237 |
+8% |
3 |
Désactivation des logs SQL |
1 020 377 |
+4254% |
117 |
-47% |
4 |
Augmentation du nombre de |
28 705 |
+22% |
114 |
-48% |
5 |
Utilisation du |
482 825 |
+1960% |
8 |
-96% |
6 |
Utilisation d’une transaction avec |
18 373 |
-22% |
173 |
-21% |
7 |
Utilisation d’une transaction avec une requête SQL |
6 453 |
-72% |
8 |
-96% |
8 |
Si tous les produits ont une réduction de 10% |
158 |
-99% |
5 |
-98% |
Temps d’exécution (ms) |
Mémoire utilisée (Mb) |
23 435 |
219 |
$em = $this->getEntityManager();
$results = $this->createQueryBuilder('p')
->getQuery()
->getResult(); // Doctrine charge en mémoire les 30 000 produits
foreach($results as $result) {
$id = $result->getId();
$newPretaxPrice = $result->getPretaxPrice() - $this->getProductDiscount($id);
$result->setPretaxPrice($newPretaxPrice);
}
$em->flush();
$em->clear();
Temps d’exécution |
Mémoire utilisée |
||
(ms) |
Diff. avec 1 |
(Mb) |
Diff. avec 1 |
1 419 137 |
+5956% |
237 |
+8% |
$i = 0;
$batchSize = 20;
$em = $this->getEntityManager();
$results = $this->createQueryBuilder('p')
->getQuery()
->getResult();
foreach($results as $result) {
$id = $result->getId();
$newPretaxPrice = $result->getPretaxPrice() - $this->getProductDiscount($id);
$result->setPretaxPrice($newPretaxPrice);
if(0 === ($i % $batchSize)) {
$em->flush(); // on flush tous les 20 produits
}
++$i;
}
$em->flush();
$em->clear();
📎
|
Le profiler de Symfony va contenir au minimum 30 000 lignes de logs Doctrine, or dans notre cas, ces logs ne nous servent pas. |
|
La mémoire utilisée est quasiment réduite de moitié, mais le temps d’exécution reste long à cause du flush() , très coûteux en temps.
|
Temps d’exécution |
Mémoire utilisée |
||
(ms) |
Diff. avec 1 |
(Mb) |
Diff. avec 1 |
1 020 377 |
+4254% |
117 |
-47% |
$i = 0;
$batchSize = 20;
$em = $this->getEntityManager();
$connection = $em->getConnection();
$connection->getConfiguration()->setSQLLogger(null); // désactivation des logs SQL
$results = $this->createQueryBuilder('p')
->getQuery()
->getResult();
foreach($results as $result) {
$id = $result->getId();
$newPretaxPrice = $result->getPretaxPrice() - $this->getProductDiscount($id);
$result->setPretaxPrice($newPretaxPrice);
if(0 === ($i % $batchSize)) {
$em->flush();
}
++$i;
}
$em->flush();
$em->clear();
Temps d’exécution |
Mémoire utilisée |
||
(ms) |
Diff. avec 1 |
(Mb) |
Diff. avec 1 |
28 705 |
+22% |
114 |
-48% |
$i = 0;
$batchSize = 1000;
$em = $this->getEntityManager();
$connection = $em->getConnection();
$connection->getConfiguration()->setSQLLogger(null); // désactivation des logs SQL
$results = $this->createQueryBuilder('p')
->getQuery()
->getResult();
foreach($results as $result) {
$id = $result->getId();
$newPretaxPrice = $result->getPretaxPrice() - $this->getProductDiscount($id);
$result->setPretaxPrice($newPretaxPrice);
if(0 === ($i % $batchSize)) {
$em->flush(); // on flush tous les 1000 produits
}
++$i;
}
$em->flush();
$em->clear();
Temps d’exécution |
Mémoire utilisée |
||
(ms) |
Diff. avec 1 |
(Mb) |
Diff. avec 1 |
482 825 |
+1960% |
8 |
-96% |
$em = $this->getEntityManager();
$connection = $em->getConnection();
$connection->getConfiguration()->setSQLLogger(null); // désactivation des logs SQL
$results = $this->createQueryBuilder('p')->getQuery(); // on n'utilise plus le `getResult()` ...
// ... au profit d'un `iterate()`. Doctrine va hydrater les objets 1 par 1
foreach($results->iterate() as $result) {
$id = $result[0]->getId();
$newPretaxPrice = $result[0]->getPretaxPrice() - $this->getProductDiscount($id);
$result[0]->setPretaxPrice($newPretaxPrice);
$em->flush($result[0]);
$em->detach($result[0]); // on demande à Doctrine de libérer la mémoire à chaque itération
}
-
Raison 1 : on met à jour des tarifs. S’il y a une erreur, cela nous évitera de faire une mise à jour partielle.
-
Raison 2 : on ne va avoir plus qu’un seul échange avec la BDD (Doctrine garde en mémoire les modifications à effectuer).
Temps d’exécution |
Mémoire utilisée |
||
(ms) |
Diff. avec 1 |
(Mb) |
Diff. avec 1 |
18 373 |
-22% |
173 |
-21% |
$em = $this->getEntityManager();
$connection = $em->getConnection();
$connection->getConfiguration()->setSQLLogger(null); // désactivation des logs SQL
$results = $this->createQueryBuilder('p')->getQuery();
$connection->beginTransaction(); // on débute une nouvelle transaction
foreach($results->iterate() as $result) {
$id = $result[0]->getId();
$newPretaxPrice = $result[0]->getPretaxPrice() - $this->getProductDiscount($id);
$result[0]->setPretaxPrice($newPretaxPrice);
$em->flush($result[0]);
$em->detach($result[0]);
}
$connection->commit(); // on enregistre et clot la transaction
Temps d’exécution |
Mémoire utilisée |
||
(ms) |
Diff. avec 1 |
(Mb) |
Diff. avec 1 |
6 453 |
-72% |
8 |
-96% |
$em = $this->getEntityManager();
$connection = $em->getConnection();
$connection->getConfiguration()->setSQLLogger(null); // désactivation des logs SQL
$results = $this->createQueryBuilder('p')->getQuery();
$connection->beginTransaction();
foreach($results->iterate() as $result) {
$id = $result[0]->getId();
$newPretaxPrice = $result[0]->getPretaxPrice() - $this->getProductDiscount($id);
$tableName = $em->getClassMetadata(Product::class)->getTableName();
$connection->udpate( // on update directement avec une requête SQL
$tableName, // TABLE ...
['pretax_price' => $newPretaxPrice], // VALUES ...
['id' => $id] // WHERE ...
);
$em->detach($result[0]);
}
$connection->commit();
Temps d’exécution |
Mémoire utilisée |
||
(ms) |
Diff. avec 1 |
(Mb) |
Diff. avec 1 |
158 |
-99% |
5 |
-98% |
$em = $this->getEntityManager();
$em->createQueryBuilder()
->update(Product::class, 'p')
->set('p.pretaxPrice', 'p.pretaxPrice * 0.9')
->getQuery()
->execute();