Improve Performance (#927)

* Stock-Overview: Reduce amount of database queries and FindObjectInArray()-calls

* Speed-up stock_current by improving products_resolved and creating indices.

* Review

Co-authored-by: Bernd Bestel <bernd@berrnd.de>
This commit is contained in:
fipwmaqzufheoxq92ebc 2020-08-18 19:23:37 +02:00 committed by GitHub
parent 7510c677f1
commit 42dc55625a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 134 additions and 37 deletions

View File

@ -16,17 +16,13 @@ class StockController extends BaseController
$nextXDays = $usersService->GetUserSettings(GROCY_USER_ID)['stock_expring_soon_days'];
return $this->renderPage($response, 'stockoverview', [
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name'),
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name'),
'currentStock' => $this->getStockService()->GetCurrentStockOverview(),
'locations' => $this->getDatabase()->locations()->orderBy('name'),
'currentStock' => $this->getStockService()->GetCurrentStock(true),
'currentStockLocations' => $this->getStockService()->GetCurrentStockLocations(),
'missingProducts' => $this->getStockService()->GetMissingProducts(),
'nextXDays' => $nextXDays,
'productGroups' => $this->getDatabase()->product_groups()->orderBy('name'),
'userfields' => $this->getUserfieldsService()->GetFields('products'),
'userfieldValues' => $this->getUserfieldsService()->GetAllValues('products'),
'shoppingListItems' => $this->getDatabase()->shopping_list(),
]);
}

65
migrations/0105.sql Normal file
View File

@ -0,0 +1,65 @@
CREATE VIEW uihelper_stock_current_overview_including_opened
AS
SELECT
p.id,
sc.amount_opened AS amount_opened,
p.tare_weight AS tare_weight,
p.enable_tare_weight_handling AS enable_tare_weight_handling,
sc.amount AS amount,
sc.product_id AS product_id,
sc.best_before_date AS best_before_date,
EXISTS(SELECT id FROM stock_missing_products_including_opened WHERE id = sc.product_id) AS product_missing,
(SELECT name FROM quantity_units WHERE quantity_units.id = p.qu_id_stock) AS qu_unit_name,
(SELECT name_plural FROM quantity_units WHERE quantity_units.id = p.qu_id_stock) AS qu_unit_name_plural,
p.name AS product_name,
(SELECT name FROM product_groups WHERE product_groups.id = product_group_id) AS product_group_name,
EXISTS(SELECT * FROM shopping_list WHERE shopping_list.product_id = sc.product_id) AS on_shopping_list,
sc.factor_purchase_amount AS factor_purchase_amount,
(SELECT name FROM quantity_units WHERE quantity_units.id = p.qu_id_purchase) AS qu_purchase_unit_name,
(SELECT name_plural FROM quantity_units WHERE quantity_units.id = p.qu_id_purchase) AS qu_purchase_unit_name_plural,
sc.is_aggregated_amount,
sc.amount_opened_aggregated
FROM (
SELECT *
FROM stock_current
WHERE best_before_date IS NOT NULL
UNION
SELECT id, 0, 0, 0, 0, null, 0, 0, 0
FROM stock_missing_products_including_opened
WHERE id NOT IN (SELECT product_id FROM stock_current)
) sc
LEFT JOIN products p
ON sc.product_id = p.id;
CREATE VIEW uihelper_stock_current_overview
AS
SELECT
p.id,
sc.amount_opened AS amount_opened,
p.tare_weight AS tare_weight,
p.enable_tare_weight_handling AS enable_tare_weight_handling,
sc.amount AS amount,
sc.product_id AS product_id,
sc.best_before_date AS best_before_date,
EXISTS(SELECT id FROM stock_missing_products_including_opened WHERE id = sc.product_id) AS product_missing,
(SELECT name FROM quantity_units WHERE quantity_units.id = p.qu_id_stock) AS qu_unit_name,
(SELECT name_plural FROM quantity_units WHERE quantity_units.id = p.qu_id_stock) AS qu_unit_name_plural,
p.name AS product_name,
(SELECT name FROM product_groups WHERE product_groups.id = product_group_id) AS product_group_name,
EXISTS(SELECT * FROM shopping_list WHERE shopping_list.product_id = sc.product_id) AS on_shopping_list,
sc.factor_purchase_amount AS factor_purchase_amount,
(SELECT name FROM quantity_units WHERE quantity_units.id = p.qu_id_purchase) AS qu_purchase_unit_name,
(SELECT name_plural FROM quantity_units WHERE quantity_units.id = p.qu_id_purchase) AS qu_purchase_unit_name_plural,
sc.is_aggregated_amount,
sc.amount_opened_aggregated
FROM (
SELECT *
FROM stock_current
WHERE best_before_date IS NOT NULL
UNION
SELECT id, 0, 0, 0, 0, null, 0, 0, 0
FROM stock_missing_products
WHERE id NOT IN (SELECT product_id FROM stock_current)
) sc
LEFT JOIN products p
ON sc.product_id = p.id;

29
migrations/0106.sql Normal file
View File

@ -0,0 +1,29 @@
CREATE INDEX ix_products_performance1 ON products (
parent_product_id
);
CREATE INDEX ix_products_performance2 ON products (
CASE WHEN parent_product_id IS NULL THEN id ELSE parent_product_id END,
active
);
CREATE INDEX ix_stock_performance1 ON stock (
product_id,
open,
best_before_date,
amount
);
DROP VIEW products_resolved;
CREATE VIEW products_resolved
AS
SELECT
CASE
WHEN p.parent_product_id IS NULL THEN
p.id
ELSE
p.parent_product_id
END AS parent_product_id,
p.id as sub_product_id
FROM products p
WHERE p.active = 1;

View File

@ -14,7 +14,16 @@ class StockService extends BaseService
const TRANSACTION_TYPE_PRODUCT_OPENED = 'product-opened';
const TRANSACTION_TYPE_SELF_PRODUCTION = 'self-production';
public function GetCurrentStock($includeNotInStockButMissingProducts = false)
public function GetCurrentStockOverview()
{
if (!GROCY_FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT) {
return $this->getDatabase()->uihelper_stock_current_overview();
} else {
return $this->getDatabase()->uihelper_stock_current_overview_including_opened();
}
}
public function GetCurrentStock($includeNotInStockButMissingProducts = false)
{
$sql = 'SELECT * FROM stock_current';
if ($includeNotInStockButMissingProducts)

View File

@ -117,29 +117,29 @@
</tr>
</thead>
<tbody class="d-none">
@foreach($currentStock as $currentStockEntry)
<tr id="product-{{ $currentStockEntry->product_id }}-row" class="@if(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $currentStockEntry->amount > 0) table-danger @elseif(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) table-warning @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) table-info @endif">
@foreach($currentStock as $currentStockEntry)
<tr id="product-{{ $currentStockEntry->product_id }}-row" class="@if(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $currentStockEntry->amount > 0) table-danger @elseif(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) table-warning @elseif ($currentStockEntry->product_missing) table-info @endif">
<td class="fit-content border-right">
<a class="btn btn-success btn-sm product-consume-button @if($currentStockEntry->amount < 1 || FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->enable_tare_weight_handling == 1) disabled @endif" href="#" data-toggle="tooltip" data-placement="left" title="{{ $__t('Consume %1$s of %2$s', '1 ' . FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }}"
<a class="btn btn-success btn-sm product-consume-button @if($currentStockEntry->amount < 1 || $currentStockEntry->enable_tare_weight_handling == 1) disabled @endif" href="#" data-toggle="tooltip" data-placement="left" title="{{ $__t('Consume %1$s of %2$s', '1 ' . $currentStockEntry->qu_unit_name, $currentStockEntry->product_name) }}"
data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}"
data-product-qu-name="{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name }}"
data-product-name="{{ $currentStockEntry->product_name }}"
data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}"
data-consume-amount="1">
<i class="fas fa-utensils"></i> 1
</a>
<a id="product-{{ $currentStockEntry->product_id }}-consume-all-button" class="d-none d-sm-inline-block btn btn-danger btn-sm product-consume-button @if($currentStockEntry->amount == 0) disabled @endif" href="#" data-toggle="tooltip" data-placement="right" title="{{ $__t('Consume all %s which are currently in stock', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }}"
<a id="product-{{ $currentStockEntry->product_id }}-consume-all-button" class="d-none d-sm-inline-block btn btn-danger btn-sm product-consume-button @if($currentStockEntry->amount == 0) disabled @endif" href="#" data-toggle="tooltip" data-placement="right" title="{{ $__t('Consume all %s which are currently in stock', $currentStockEntry->product_name) }}"
data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}"
data-product-qu-name="{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name }}"
data-consume-amount="@if(FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->enable_tare_weight_handling == 1){{FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->tare_weight}}@else{{$currentStockEntry->amount}}@endif"
data-product-name="{{ $currentStockEntry->product_name }}"
data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}"
data-consume-amount="@if($currentStockEntry->enable_tare_weight_handling == 1){{$currentStockEntry->tare_weight}}@else{{$currentStockEntry->amount}}@endif"
data-original-total-stock-amount="{{$currentStockEntry->amount}}">
<i class="fas fa-utensils"></i> {{ $__t('All') }}
</a>
@if(GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING)
<a class="btn btn-success btn-sm product-open-button @if($currentStockEntry->amount < 1 || $currentStockEntry->amount == $currentStockEntry->amount_opened || FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->enable_tare_weight_handling == 1) disabled @endif" href="#" data-toggle="tooltip" data-placement="left" title="{{ $__t('Mark %1$s of %2$s as open', '1 ' . FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }}"
<a class="btn btn-success btn-sm product-open-button @if($currentStockEntry->amount < 1 || $currentStockEntry->amount == $currentStockEntry->amount_opened || $currentStockEntry->enable_tare_weight_handling == 1) disabled @endif" href="#" data-toggle="tooltip" data-placement="left" title="{{ $__t('Mark %1$s of %2$s as open', '1 ' . $currentStockEntry->qu_unit_name, $currentStockEntry->product_name) }}"
data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}"
data-product-qu-name="{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name }}">
data-product-name="{{ $currentStockEntry->product_name }}"
data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}">
<i class="fas fa-box-open"></i> 1
</a>
@endif
@ -150,10 +150,10 @@
<div class="table-inline-menu dropdown-menu dropdown-menu-right">
<a id="product-{{ $currentStockEntry->product_id }}-consume-all-button" class="d-inline-block d-sm-none dropdown-item show-as-dialog-link text-danger product-consume-button @if($currentStockEntry->amount == 0) disabled @endif" href="#" data-toggle="tooltip" data-placement="right"
data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}"
data-product-qu-name="{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name }}"
data-product-name="{{ $currentStockEntry->product_name }}"
data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}"
data-consume-amount="{{ $currentStockEntry->amount }}">
<span class="dropdown-item-icon"><i class="fas fa-utensils"></i></span> <span class="dropdown-item-text">{{ $__t('Consume all %s which are currently in stock', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }}</span>
<span class="dropdown-item-icon"><i class="fas fa-utensils"></i></span> <span class="dropdown-item-text">{{ $__t('Consume all %s which are currently in stock', $currentStockEntry->product_name) }}</span>
</a>
<a class="dropdown-item show-as-dialog-link" type="button" href="{{ $U('/shoppinglistitem/new?embedded&updateexistingproduct&product=' . $currentStockEntry->product_id ) }}">
<span class="dropdown-item-icon"><i class="fas fa-shopping-cart"></i></span> <span class="dropdown-item-text">{{ $__t('Add to shopping list') }}</span>
@ -190,13 +190,13 @@
<div class="dropdown-divider"></div>
<a class="dropdown-item product-consume-button product-consume-button-spoiled @if($currentStockEntry->amount < 1) disabled @endif" type="button" href="#"
data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}"
data-product-qu-name="{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name }}"
data-product-name="{{ $currentStockEntry->product_name }}"
data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}"
data-consume-amount="1">
<span class="dropdown-item-icon"><i class="fas fa-utensils"></i></span> <span class="dropdown-item-text">{{ $__t('Consume %1$s of %2$s as spoiled', '1 ' . FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }}</span>
<span class="dropdown-item-icon"><i class="fas fa-utensils"></i></span> <span class="dropdown-item-text">{{ $__t('Consume %1$s of %2$s as spoiled', '1 ' . $currentStockEntry->qu_unit_name, $currentStockEntry->product_name) }}</span>
</a>
@if(GROCY_FEATURE_FLAG_RECIPES)
<a class="dropdown-item" type="button" href="{{ $U('/recipes?search=') }}{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}">
<a class="dropdown-item" type="button" href="{{ $U('/recipes?search=') }}{{ $currentStockEntry->product_name }}">
<span class="dropdown-item-icon"><i class="fas fa-cocktail"></i></span> <span class="dropdown-item-text">{{ $__t('Search for recipes containing this product') }}</span>
</a>
@endif
@ -204,27 +204,25 @@
</div>
</td>
<td class="product-name-cell cursor-link" data-product-id="{{ $currentStockEntry->product_id }}">
{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}
</td>
@php $productGroup = FindObjectInArrayByPropertyValue($productGroups, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->product_group_id) @endphp
<td>
@if($productGroup !== null){{ $productGroup->name }}@endif
{{ $currentStockEntry->product_name }}
</td>
<td>
<span id="product-{{ $currentStockEntry->product_id }}-amount" class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount }}</span> <span id="product-{{ $currentStockEntry->product_id }}-qu-name">{{ $__n($currentStockEntry->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name_plural) }}</span>
@if($currentStockEntry->product_group_name !== null){{ $currentStockEntry->product_group_name }}@endif
</td>
<td>
<span id="product-{{ $currentStockEntry->product_id }}-amount" class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount }}</span> <span id="product-{{ $currentStockEntry->product_id }}-qu-name">{{ $__n($currentStockEntry->amount, $currentStockEntry->qu_unit_name, $currentStockEntry->qu_unit_name_plural) }}</span>
<span id="product-{{ $currentStockEntry->product_id }}-opened-amount" class="small font-italic">@if($currentStockEntry->amount_opened > 0){{ $__t('%s opened', $currentStockEntry->amount_opened) }}@endif</span>
@if($currentStockEntry->amount != $currentStockEntry->factor_purchase_amount)
<span id="product-{{ $currentStockEntry->product_id }}-factor-purchase-amount" class="locale-number locale-number-quantity-amount">({{ $currentStockEntry->factor_purchase_amount }}</span> <span id="product-{{ $currentStockEntry->product_id }}-qu-purchase-name">{{ $__n($currentStockEntry->factor_purchase_amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_purchase)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_purchase)->name_plural) }})</span>
<span id="product-{{ $currentStockEntry->product_id }}-factor-purchase-amount" class="locale-number locale-number-quantity-amount">({{ $currentStockEntry->factor_purchase_amount }}</span> <span id="product-{{ $currentStockEntry->product_id }}-qu-purchase-name">{{ $__n($currentStockEntry->factor_purchase_amount, $currentStockEntry->qu_purchase_unit_name,$currentStockEntry->qu_purchase_unit_name_plural) }})</span>
@endif
@if($currentStockEntry->is_aggregated_amount == 1)
<span class="pl-1 text-secondary">
<i class="fas fa-custom-sigma-sign"></i> <span id="product-{{ $currentStockEntry->product_id }}-amount-aggregated" class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount_aggregated }}</span> {{ $__n($currentStockEntry->amount_aggregated, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name_plural) }}
<i class="fas fa-custom-sigma-sign"></i> <span id="product-{{ $currentStockEntry->product_id }}-amount-aggregated" class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount_aggregated }}</span> {{ $__n($currentStockEntry->amount_aggregated, $currentStockEntry->qu_unit_name, $currentStockEntry->qu_unit_name_plural) }}
@if($currentStockEntry->amount_opened_aggregated > 0)<span id="product-{{ $currentStockEntry->product_id }}-opened-amount-aggregated" class="small font-italic">{{ $__t('%s opened', $currentStockEntry->amount_opened_aggregated) }}</span>@endif
</span>
@endif
@if(boolval($userSettings['show_icon_on_stock_overview_page_when_product_is_on_shopping_list']))
@php $currentStockEntryShoppingListItems = FindAllObjectsInArrayByPropertyValue($shoppingListItems, 'product_id', $currentStockEntry->product_id) @endphp
@if(count($currentStockEntryShoppingListItems) > 0)
@if($currentStockEntry->on_shopping_list)
<span class="btn btn-link btn-sm text-muted">
<i class="fas fa-shopping-cart"></i>
</span>
@ -236,12 +234,12 @@
<time id="product-{{ $currentStockEntry->product_id }}-next-best-before-date-timeago" class="timeago timeago-contextual" datetime="{{ $currentStockEntry->best_before_date }} 23:59:59"></time>
</td>
<td class="d-none">
@foreach(FindAllObjectsInArrayByPropertyValue($currentStockLocations, 'product_id', $currentStockEntry->product_id) as $locationsForProduct)
@foreach(FindAllObjectsInArrayByPropertyValue($currentStockLocations, 'product_id', $currentStockEntry->product_id) as $locationsForProduct)
{{ FindObjectInArrayByPropertyValue($locations, 'id', $locationsForProduct->location_id)->name }}
@endforeach
</td>
<td class="d-none">
@if($currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $currentStockEntry->amount > 0) expired @elseif($currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) expiring @endif @if(FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) belowminstockamount @endif
@if($currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $currentStockEntry->amount > 0) expired @elseif($currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) expiring @endif @if($currentStockEntry->product_missing) belowminstockamount @endif
</td>
@include('components.userfields_tbody', array(