Skip to content

Commit fcd3f45

Browse files
alex-n-2k7vsoroka
authored andcommitted
CRM-7054: Slow Opportunities by Status widget (#6873)
1 parent 016e66a commit fcd3f45

File tree

5 files changed

+492
-40
lines changed

5 files changed

+492
-40
lines changed

src/OroCRM/Bundle/SalesBundle/Dashboard/Provider/OpportunityByStatusProvider.php

+76-37
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44

55
use Symfony\Bridge\Doctrine\RegistryInterface;
66

7-
use Doctrine\ORM\Query\Expr as Expr;
7+
use Oro\Component\DoctrineUtils\ORM\QueryUtils;
88

99
use Oro\Bundle\DashboardBundle\Filter\DateFilterProcessor;
1010
use Oro\Bundle\DashboardBundle\Model\WidgetOptionBag;
11+
use Oro\Bundle\EntityExtendBundle\Entity\Repository\EnumValueRepository;
12+
use Oro\Bundle\EntityExtendBundle\Tools\ExtendHelper;
1113
use Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper;
1214
use Oro\Bundle\UserBundle\Dashboard\OwnerHelper;
13-
use Oro\Component\DoctrineUtils\ORM\QueryUtils;
15+
1416
use OroCRM\Bundle\SalesBundle\Entity\Repository\OpportunityRepository;
1517

1618
class OpportunityByStatusProvider
@@ -54,57 +56,94 @@ public function getOpportunitiesGroupedByStatus(WidgetOptionBag $widgetOptions)
5456
{
5557
$dateRange = $widgetOptions->get('dateRange');
5658
$owners = $this->ownerHelper->getOwnerIds($widgetOptions);
59+
/**
60+
* Excluded statuses will be filtered from result in method `formatResult` below.
61+
* Due to performance issues with `NOT IN` clause in database.
62+
*/
5763
$excludedStatuses = $widgetOptions->get('excluded_statuses', []);
5864
$orderBy = $widgetOptions->get('useQuantityAsData') ? 'quantity' : 'budget';
59-
$qb = $this->getOpportunityRepository()
60-
->getGroupedOpportunitiesByStatusQB('o', $orderBy);
65+
66+
/** @var OpportunityRepository $opportunityRepository */
67+
$opportunityRepository = $this->registry->getRepository('OroCRMSalesBundle:Opportunity');
68+
$qb = $opportunityRepository->createQueryBuilder('o')
69+
->select('IDENTITY (o.status) status')
70+
->groupBy('status')
71+
->orderBy($orderBy, 'DESC');
72+
73+
switch ($orderBy) {
74+
case 'quantity':
75+
$qb->addSelect('COUNT(o.id) as quantity');
76+
break;
77+
case 'budget':
78+
$qb->addSelect(
79+
'SUM(
80+
CASE WHEN o.status = \'won\'
81+
THEN (CASE WHEN o.closeRevenue IS NOT NULL THEN o.closeRevenue ELSE 0 END)
82+
ELSE (CASE WHEN o.budgetAmount IS NOT NULL THEN o.budgetAmount ELSE 0 END)
83+
END
84+
) as budget'
85+
);
86+
}
6187

6288
$this->dateFilterProcessor->applyDateRangeFilterToQuery($qb, $dateRange, 'o.createdAt');
6389

6490
if ($owners) {
6591
QueryUtils::applyOptimizedIn($qb, 'o.owner', $owners);
6692
}
6793

68-
// move previously applied conditions into join
69-
// since we don't want to exclude any statuses from result
70-
$joinConditions = $qb->getDQLPart('where');
71-
if ($joinConditions) {
72-
$whereParts = (string) $joinConditions;
73-
$qb->resetDQLPart('where');
74-
75-
$join = $qb->getDQLPart('join')['s'][0];
76-
$qb->resetDQLPart('join');
77-
78-
$qb->add(
79-
'join',
80-
[
81-
's' => new Expr\Join(
82-
$join->getJoinType(),
83-
$join->getJoin(),
84-
$join->getAlias(),
85-
$join->getConditionType(),
86-
sprintf('%s AND (%s)', $join->getCondition(), $whereParts),
87-
$join->getIndexBy()
88-
)
89-
],
90-
true
91-
);
92-
}
94+
$result = $this->aclHelper->apply($qb)->getArrayResult();
95+
96+
return $this->formatResult($result, $excludedStatuses, $orderBy);
97+
}
9398

94-
if ($excludedStatuses) {
95-
$qb->andWhere(
96-
$qb->expr()->notIn('s.id', $excludedStatuses)
97-
);
99+
/**
100+
* @param array $result
101+
* @param string[] $excludedStatuses
102+
* @param string $orderBy
103+
*
104+
* @return array
105+
*/
106+
protected function formatResult($result, $excludedStatuses, $orderBy)
107+
{
108+
$resultStatuses = array_flip(array_column($result, 'status', null));
109+
110+
foreach ($this->getAvailableOpportunityStatuses() as $statusKey => $statusLabel) {
111+
$resultIndex = isset($resultStatuses[$statusKey]) ? $resultStatuses[$statusKey] : null;
112+
if (in_array($statusKey, $excludedStatuses)) {
113+
if (null !== $resultIndex) {
114+
unset($result[$resultIndex]);
115+
}
116+
continue;
117+
}
118+
119+
if (null !== $resultIndex) {
120+
$result[$resultIndex]['label'] = $statusLabel;
121+
} else {
122+
$result[] = [
123+
'status' => $statusKey,
124+
'label' => $statusLabel,
125+
$orderBy => 0
126+
];
127+
}
98128
}
99129

100-
return $this->aclHelper->apply($qb)->getArrayResult();
130+
return $result;
101131
}
102132

103133
/**
104-
* @return OpportunityRepository
134+
* @return array
105135
*/
106-
protected function getOpportunityRepository()
136+
protected function getAvailableOpportunityStatuses()
107137
{
108-
return $this->registry->getRepository('OroCRMSalesBundle:Opportunity');
138+
/** @var EnumValueRepository $statusesRepository */
139+
$statusesRepository = $this->registry->getRepository(
140+
ExtendHelper::buildEnumValueClassName('opportunity_status')
141+
);
142+
$statuses = $statusesRepository->createQueryBuilder('s')
143+
->select('s.id, s.name')
144+
->getQuery()
145+
->getArrayResult();
146+
147+
return array_column($statuses, 'name', 'id');
109148
}
110149
}

src/OroCRM/Bundle/SalesBundle/Entity/Opportunity.php

+8-2
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,14 @@
2121
/**
2222
* @ORM\Entity(repositoryClass="OroCRM\Bundle\SalesBundle\Entity\Repository\OpportunityRepository")
2323
* @ORM\Table(
24-
* name="orocrm_sales_opportunity",
25-
* indexes={@ORM\Index(name="opportunity_created_idx",columns={"created_at", "id"})}
24+
* name="orocrm_sales_opportunity",
25+
* indexes={
26+
* @ORM\Index(name="opportunity_created_idx",columns={"created_at", "id"}),
27+
* @ORM\Index(
28+
* name="opportunities_by_status_idx",
29+
* columns={"organization_id","status_id","close_revenue","budget_amount","created_at"}
30+
* )
31+
* }
2632
* )
2733
* @ORM\HasLifecycleCallbacks()
2834
* @Oro\Loggable

src/OroCRM/Bundle/SalesBundle/Migrations/Schema/OroCRMSalesBundleInstaller.php

+17-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public function setAttachmentExtension(AttachmentExtension $attachmentExtension)
100100
*/
101101
public function getMigrationVersion()
102102
{
103-
return 'v1_25_7';
103+
return 'v1_25_8';
104104
}
105105

106106
/**
@@ -153,6 +153,8 @@ public function up(Schema $schema, QueryBag $queries)
153153
$this->addOrocrmSalesOpportunityStatusField($schema, $queries);
154154
AddLeadStatus::addStatusField($schema, $this->extendExtension, $queries);
155155
AddLeadAddressTable::createLeadAddressTable($schema);
156+
157+
$this->addOpportunitiesByStatusIndex($schema);
156158
}
157159

158160
/**
@@ -839,4 +841,18 @@ protected function addB2bCustomerNameIndex(Schema $schema)
839841
$table = $schema->getTable('orocrm_sales_b2bcustomer');
840842
$table->addIndex(['name', 'id'], 'orocrm_b2bcustomer_name_idx', []);
841843
}
844+
845+
/**
846+
* Add opportunity 'opportunities_by_status_idx' index, used to speedup 'Opportunity By Status' widget
847+
*
848+
* @param Schema $schema
849+
*/
850+
protected function addOpportunitiesByStatusIndex(Schema $schema)
851+
{
852+
$table = $schema->getTable('orocrm_sales_opportunity');
853+
$table->addIndex(
854+
['organization_id', 'status_id', 'close_revenue', 'budget_amount', 'created_at'],
855+
'opportunities_by_status_idx'
856+
);
857+
}
842858
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace OroCRM\Bundle\SalesBundle\Migrations\Schema\v1_25_8;
4+
5+
use Doctrine\DBAL\Schema\Schema;
6+
7+
use Oro\Bundle\MigrationBundle\Migration\Migration;
8+
use Oro\Bundle\MigrationBundle\Migration\QueryBag;
9+
10+
class UpdateIndexes implements Migration
11+
{
12+
/**
13+
* {@inheritdoc}
14+
*/
15+
public function up(Schema $schema, QueryBag $queries)
16+
{
17+
$table = $schema->getTable('orocrm_sales_opportunity');
18+
$indexName = 'opportunities_by_status_idx';
19+
$indexColumns = [
20+
'organization_id',
21+
'status_id',
22+
'close_revenue',
23+
'budget_amount',
24+
'created_at'
25+
];
26+
if ($table->hasIndex($indexName) && $table->getIndex($indexName)->getColumns() !== $indexColumns) {
27+
$table->dropIndex($indexName);
28+
$table->addIndex($indexColumns, $indexName);
29+
} else {
30+
$table->addIndex($indexColumns, $indexName);
31+
}
32+
}
33+
}

0 commit comments

Comments
 (0)