Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save jprivet-dev/d0c9929378921f642703f2c96fbee0a3 to your computer and use it in GitHub Desktop.
Save jprivet-dev/d0c9929378921f642703f2c96fbee0a3 to your computer and use it in GitHub Desktop.
[Tips] SymfonyLive Paris 2016 - Comment mettre à jour 30 000 produits avec Doctrine2 ? (André Tapia & Amine Mokeddem)

[Tips] SymfonyLive Paris 2016 - Comment mettre à jour 30 000 produits avec Doctrine2 ? (André Tapia & Amine Mokeddem)

Ressources

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

Cas pratique : mettre à jour 30 000 produits

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.

Récapitulatif des approches

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 flush() (batchSize à 20)

1 419 137
(23 min)

+5956%

237

+8%

3

Désactivation des logs SQL

1 020 377
(17 min)

+4254%

117

-47%

4

Augmentation du nombre de flush() (batchSize à 1000)

28 705

+22%

114

-48%

5

Utilisation du iterate() à la place du getResult()

482 825
(8 min)

+1960%

8

-96%

6

Utilisation d’une transaction avec flush()

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%

Approche 1 : méthode la plus simple

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();

Approche 2 : augmentation du nombre de flush() (batchSize à 20)

Temps d’exécution

Mémoire utilisée

(ms)

Diff. avec 1

(Mb)

Diff. avec 1

1 419 137
(23 min)

+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();

Approche 3 : désactivation des logs SQL

📎
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
(17 min)

+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();

Approche 4 : augmentation du nombre de flush() (batchSize à 1000)

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();

Approche 5 : utilisation du iterate() à la place du getResult()

Temps d’exécution

Mémoire utilisée

(ms)

Diff. avec 1

(Mb)

Diff. avec 1

482 825
(8 min)

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

Approche 6 : utilisation d’une transaction avec flush()

  • 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

Approche 7 : utilisation d’une transaction avec une requête SQL

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();

Approche 8 : si tous les produits ont une réduction de 10%

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();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment