DDC-1402: Huge performance leak in SingleTablePersister #1756

Closed
opened 2026-01-22 13:24:52 +01:00 by admin · 3 comments
Owner

Originally created by @doctrinebot on GitHub (Oct 4, 2011).

Originally assigned to: @beberlei on GitHub.

Jira issue originally created by user sb_demarque:

This code :

/****
 * @Entity
 * @InheritanceType("SINGLE_TABLE")
 * @DiscriminatorColumn(name="discr", type="string")
 * @DiscriminatorMap({"person" = "Person", "employee" = "Employee"})
 */
class Person
{
    /*** @Id @Column(type="integer") **/ private $id;
}

/****
 * @Entity
 */
class Employee extends Person
{
    /*** @Column(type="integer") **/ private $a;
    /*** @Column(type="integer") **/ private $b;
    /*** @Column(type="integer") **/ private $c;
    /*** @Column(type="integer") **/ private $d;
    /*** @Column(type="integer") **/ private $e;
    /*** @Column(type="integer") **/ private $f;
    /*** @Column(type="integer") **/ private $g;
    /*** @Column(type="integer") **/ private $h;
}

foreach (range(0, 20) as $i) {
    $time = microtime(true);
    foreach (range(1, 100) as $j) {
        $id = ($i * 100) <ins> $j;
        $entityManager->find('Person', $id);
    }
    printf("%4d ==> %f\n", $id, microtime(true) - $time);
}

Will output:

 100 ==> 0.461275
 200 ==> 1.128404
 300 ==> 1.823122
 400 ==> 2.521054
 500 ==> 3.232034
 600 ==> 3.950081
 700 ==> 4.648849
 800 ==> 5.380236
 900 ==> 6.080108
1000 ==> 6.807214
1100 ==> 7.519942
1200 ==> 8.238971
1300 ==> 8.951686
1400 ==> 9.648996
1500 ==> 10.370053
1600 ==> 11.069523
1700 ==> 11.791530
1800 ==> 12.481427
1900 ==> 13.190570
2000 ==> 13.902810
2100 ==> 14.671100

With the first and last SELECT queries as:

SELECT t0.id AS id1, discr, t0.a AS a2, t0.b AS b3, t0.c AS c4, t0.d AS d5, t0.e AS e6, t0.f AS f7, t0.g AS g8, t0.h AS h9 FROM Person t0 WHERE t0.id = 1 AND t0.discr IN ('person', 'employee')

...

SELECT t0.id AS id1, discr, t0.a AS a16794, t0.b AS b16795, t0.c AS c16796, t0.d AS d16797, t0.e AS e16798, t0.f AS f16799, t0.g AS g16800, t0.h AS h16801 FROM Person t0 WHERE t0.id = 2100 AND t0.discr IN ('person', 'employee')

Notes:

* Last 100 SELECT queries take more than 14 seconds to execute! (table is empty)

  • Filed as a bug because a real life scenario caused major performance leak and memory usage when attempting to do 6000 calls to find().

Analysis:

Calls to *SingleTablePersister::_getSelectColumnListSQL()* do not use BasicEntityPersister::$_selectColumnListSql

This generates a lot of calls to _getSelectColumnSQL() (See last SELECT query with alias counter up to 16800)

This generates a lot of calls to addFieldResult()

And this fills up pretty quickly ResultSetMapping::$declaringClasses

To finally put a huge burden on *SimpleObjectHydrator::_prepare()* where it iterates on _$this->rsm->declaringClasses and calls getClassMetadata() for each one!

And all that, including _prepare(), is executed for each SELECT, even though none of them find any result (the table is empty).

I fixed my project using the following patch:

diff --git a/doctrine-orm/Doctrine/ORM/Persisters/SingleTablePersister.php b/doctrine-orm/Doctrine/ORM/Persisters/SingleTablePersister.php
index f910a8e..78b27cb 100644
--- a/doctrine-orm/Doctrine/ORM/Persisters/SingleTablePersister.php
</ins><ins></ins> b/doctrine-orm/Doctrine/ORM/Persisters/SingleTablePersister.php
@@ -41,6 <ins>41,15 @@ class SingleTablePersister extends AbstractEntityInheritancePersister
     /*** {@inheritdoc} **/
     protected function _getSelectColumnListSQL()
     {
</ins>        /*** @see BasicEntityPersister::_getSelectColumnListSQL() **/
<ins>        if ($this->_selectColumnListSql !== null) {
</ins>            return $this->_selectColumnListSql;
<ins>        }
</ins>
<ins>        #####
</ins>        #####
<ins>        #####
</ins>
         $columnList = parent::_getSelectColumnListSQL();

         // Append discriminator column
@@ -74,7 <ins>83,13 @@ class SingleTablePersister extends AbstractEntityInheritancePersister
             }
         }

-        return $columnList;
</ins>        #####
<ins>        #####
</ins>        #####
<ins>
</ins>        /*** @see BasicEntityPersister::_getSelectColumnListSQL() **/
<ins>        $this->_selectColumnListSql = $columnList;
</ins>        return $this->_selectColumnListSql;
     }

     /*** {@inheritdoc} **/

I do not know if this patch is safe for everybody.

But, well, you can easily reproduce the problem and analyze the phenomenon using a profiler on the sample code provided.

Thanks for this great piece of software. I hope this will help you find and fix the bug.

Originally created by @doctrinebot on GitHub (Oct 4, 2011). Originally assigned to: @beberlei on GitHub. Jira issue originally created by user sb_demarque: This code : ``` /**** * @Entity * @InheritanceType("SINGLE_TABLE") * @DiscriminatorColumn(name="discr", type="string") * @DiscriminatorMap({"person" = "Person", "employee" = "Employee"}) */ class Person { /*** @Id @Column(type="integer") **/ private $id; } /**** * @Entity */ class Employee extends Person { /*** @Column(type="integer") **/ private $a; /*** @Column(type="integer") **/ private $b; /*** @Column(type="integer") **/ private $c; /*** @Column(type="integer") **/ private $d; /*** @Column(type="integer") **/ private $e; /*** @Column(type="integer") **/ private $f; /*** @Column(type="integer") **/ private $g; /*** @Column(type="integer") **/ private $h; } foreach (range(0, 20) as $i) { $time = microtime(true); foreach (range(1, 100) as $j) { $id = ($i * 100) <ins> $j; $entityManager->find('Person', $id); } printf("%4d ==> %f\n", $id, microtime(true) - $time); } ``` Will output: ``` 100 ==> 0.461275 200 ==> 1.128404 300 ==> 1.823122 400 ==> 2.521054 500 ==> 3.232034 600 ==> 3.950081 700 ==> 4.648849 800 ==> 5.380236 900 ==> 6.080108 1000 ==> 6.807214 1100 ==> 7.519942 1200 ==> 8.238971 1300 ==> 8.951686 1400 ==> 9.648996 1500 ==> 10.370053 1600 ==> 11.069523 1700 ==> 11.791530 1800 ==> 12.481427 1900 ==> 13.190570 2000 ==> 13.902810 2100 ==> 14.671100 ``` With the first and last SELECT queries as: ``` sql SELECT t0.id AS id1, discr, t0.a AS a2, t0.b AS b3, t0.c AS c4, t0.d AS d5, t0.e AS e6, t0.f AS f7, t0.g AS g8, t0.h AS h9 FROM Person t0 WHERE t0.id = 1 AND t0.discr IN ('person', 'employee') ... SELECT t0.id AS id1, discr, t0.a AS a16794, t0.b AS b16795, t0.c AS c16796, t0.d AS d16797, t0.e AS e16798, t0.f AS f16799, t0.g AS g16800, t0.h AS h16801 FROM Person t0 WHERE t0.id = 2100 AND t0.discr IN ('person', 'employee') ``` Notes: *\* Last 100 SELECT queries take more than 14 seconds to execute! (_table is empty_*) - Filed as a bug because a real life scenario caused major performance leak and memory usage when attempting to do 6000 calls to find(). Analysis: # Calls to **SingleTablePersister::_getSelectColumnListSQL()\* do not use *BasicEntityPersister::$_selectColumnListSql** # This generates a lot of calls to **_getSelectColumnSQL()** (See last SELECT query with alias counter up to 16800) # This generates a lot of calls to **addFieldResult()** # And this fills up pretty quickly **ResultSetMapping::$declaringClasses** # To finally put a huge burden on **SimpleObjectHydrator::_prepare()\* where it iterates on _$this->_rsm->declaringClasses_ and calls *getClassMetadata()** for each one! # And all that, including **_prepare()**, is executed for each SELECT, even though none of them find any result (the table is empty). I fixed my project using the following patch: ``` diff --git a/doctrine-orm/Doctrine/ORM/Persisters/SingleTablePersister.php b/doctrine-orm/Doctrine/ORM/Persisters/SingleTablePersister.php index f910a8e..78b27cb 100644 --- a/doctrine-orm/Doctrine/ORM/Persisters/SingleTablePersister.php </ins><ins></ins> b/doctrine-orm/Doctrine/ORM/Persisters/SingleTablePersister.php @@ -41,6 <ins>41,15 @@ class SingleTablePersister extends AbstractEntityInheritancePersister /*** {@inheritdoc} **/ protected function _getSelectColumnListSQL() { </ins> /*** @see BasicEntityPersister::_getSelectColumnListSQL() **/ <ins> if ($this->_selectColumnListSql !== null) { </ins> return $this->_selectColumnListSql; <ins> } </ins> <ins> ##### </ins> ##### <ins> ##### </ins> $columnList = parent::_getSelectColumnListSQL(); // Append discriminator column @@ -74,7 <ins>83,13 @@ class SingleTablePersister extends AbstractEntityInheritancePersister } } - return $columnList; </ins> ##### <ins> ##### </ins> ##### <ins> </ins> /*** @see BasicEntityPersister::_getSelectColumnListSQL() **/ <ins> $this->_selectColumnListSql = $columnList; </ins> return $this->_selectColumnListSql; } /*** {@inheritdoc} **/ ``` I do not know if this patch is safe for everybody. But, well, you can easily reproduce the problem and analyze the phenomenon using a profiler on the sample code provided. Thanks for this great piece of software. I hope this will help you find and fix the bug.
admin added the Bug label 2026-01-22 13:24:52 +01:00
admin closed this issue 2026-01-22 13:24:52 +01:00
Author
Owner

@doctrinebot commented on GitHub (Oct 10, 2011):

Comment created by @beberlei:

good catch, thank you very much!

fixed and merged back into 2.1.x

@doctrinebot commented on GitHub (Oct 10, 2011): Comment created by @beberlei: good catch, thank you very much! fixed and merged back into 2.1.x
Author
Owner

@doctrinebot commented on GitHub (Oct 10, 2011):

Issue was closed with resolution "Fixed"

@doctrinebot commented on GitHub (Oct 10, 2011): Issue was closed with resolution "Fixed"
Author
Owner

@doctrinebot commented on GitHub (Dec 13, 2015):

Imported 1 attachments from Jira into https://gist.github.com/ca1b504847773836ba21

@doctrinebot commented on GitHub (Dec 13, 2015): Imported 1 attachments from Jira into https://gist.github.com/ca1b504847773836ba21 - [11068_doctrine-orm-2.1.0-SingleTablePersister.patch](https://gist.github.com/ca1b504847773836ba21#file-11068_doctrine-orm-2-1-0-SingleTablePersister-patch)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#1756