Magento 2 MSI i tabela inventory_stock_2 – cichy killer wydajności indeksacji
W jednym z ostatnich wdrożeń Magento 2 (ok. 200 000 produktów, Multi Source Inventory włączone) zauważyliśmy bardzo długą indeksację modułów:
- Inventory
- Product Price
- Category Products
Wszystkie klasyczne indeksy były poprawne. MariaDB miała 256 GB RAM, buffer pool był sensowny, indeksy EAV dopięte, a mimo to w logach wciąż pojawiały się ciężkie zapytania związane z:
inventory_stock_2
Po głębszej analizie okazało się, że problemem nie była sama ilość danych – tylko sposób, w jaki Magento MSI tworzy tabelę inventory_stock_*.
Skąd bierze się tabela inventory_stock_2?
Tabela inventory_stock_2 pojawia się w Magento po włączeniu
Multi Source Inventory (MSI).
Dla każdego stocku Magento tworzy osobną tabelę:
- inventory_stock_1
- inventory_stock_2
- inventory_stock_3 ...
Tabela jest tworzona dynamicznie przez klasę:
Magento\InventoryIndexer\Indexer\IndexStructure
i wygląda mniej więcej tak:
CREATE TABLE inventory_stock_2 (
sku varchar(64) NOT NULL,
quantity decimal(12,4) NOT NULL DEFAULT 0.0000,
is_salable tinyint(1) NOT NULL,
PRIMARY KEY (sku),
KEY index_sku_qty (sku, quantity)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
Gdzie leży problem?
Są dwa kluczowe problemy:
1️⃣ Brak utf8mb4
Kolumna sku często tworzona jest w utf8_general_ci,
podczas gdy w catalog_product_entity mamy:
utf8mb4_general_ci
To powoduje:
- konwersję znaków przy JOIN
- użycie join buffer
- gorsze plany zapytań
2️⃣ Brak indeksu zaczynającego się od is_salable
Magento bardzo często wykonuje zapytania typu:
WHERE stock_index.is_salable = 0
OR stock_index.is_salable IS NULL
A ponieważ jedyny indeks to:
(sku, quantity)
MariaDB nie może efektywnie filtrować po is_salable.
Efekt?
- pełne skany tabeli
- długie DELETE podczas price index
- lock wait timeout 1205
- inventory indexer „processing” bez końca
Dlaczego ręczne ALTER TABLE nie działa?
Bo Magento przy każdej indeksacji:
- DROP TABLE inventory_stock_X
- CREATE TABLE inventory_stock_X
Każda ręczna zmiana znika.
Dlatego rozwiązaniem musi być: automatyczna modyfikacja tabeli po jej utworzeniu.
Rozwiązanie: moduł Kowal_MsiStockFix
Zamiast modyfikować core Magento, tworzymy plugin, który po utworzeniu tabeli:
- ustawi utf8mb4_general_ci
- doda indeks (is_salable, sku)
Struktura modułu
app/code/Kowal/MsiStockFix/
├── registration.php
├── etc/
│ ├── module.xml
│ └── di.xml
└── Plugin/
└── IndexStructurePlugin.php
registration.php
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Kowal_MsiStockFix',
__DIR__
);
etc/module.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Kowal_MsiStockFix" setup_version="1.0.0"/>
</config>
etc/di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\InventoryIndexer\Indexer\IndexStructure">
<plugin name="kowal_msi_stock_fix_index_structure"
type="Kowal\MsiStockFix\Plugin\IndexStructurePlugin"
sortOrder="10"/>
</type>
</config>
Plugin: IndexStructurePlugin.php
<?php
declare(strict_types=1);
namespace Kowal\MsiStockFix\Plugin;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\InventoryIndexer\Indexer\IndexStructure;
use Magento\InventoryMultiDimensionalIndexerApi\Model\IndexName;
use Magento\InventoryMultiDimensionalIndexerApi\Model\IndexNameResolverInterface;
class IndexStructurePlugin
{
private const CHARSET = 'utf8mb4';
private const COLLATION = 'utf8mb4_general_ci';
public function __construct(
private readonly ResourceConnection $resource,
private readonly IndexNameResolverInterface $resolver
) {}
public function afterCreate(IndexStructure $subject, $result, IndexName $indexName, string $connectionName)
{
$connection = $this->resource->getConnection($connectionName);
$resolvedName = $this->resolver->resolveName($indexName);
$table = $this->resource->getTableName($resolvedName);
if (strpos($table, 'inventory_stock_') === false) {
return $result;
}
if (!$connection->isTableExists($table)) {
return $result;
}
// 1. Ustaw utf8mb4
$connection->query(
sprintf(
"ALTER TABLE `%s` CONVERT TO CHARACTER SET %s COLLATE %s",
$table,
self::CHARSET,
self::COLLATION
)
);
// 2. Dodaj indeks (is_salable, sku)
$indexes = $connection->getIndexList($table);
if (!isset($indexes['IDX_STOCK_SALABLE_SKU'])) {
$connection->addIndex(
$table,
'IDX_STOCK_SALABLE_SKU',
['is_salable', 'sku'],
AdapterInterface::INDEX_TYPE_INDEX
);
}
return $result;
}
}
Instalacja modułu
bin/magento module:enable Kowal_MsiStockFix
bin/magento setup:upgrade
bin/magento cache:flush
bin/magento indexer:reindex inventory
Efekty po wdrożeniu
- Brak znikającej collation
- Szybsze DELETE z price index
- Brak lock wait timeout
- Inventory index kończy się poprawnie
- Mniej join buffer i full scanów
Podsumowanie
Problem z tabelą inventory_stock_2 jest mało oczywisty,
bo wszystko wygląda „poprawnie”, a indeksy są zielone.
Dopiero analiza:
- planów zapytań
- collation
- mechanizmu recreate tabel MSI
pozwala znaleźć prawdziwe źródło problemu.
Jeżeli masz sklep z dużą liczbą produktów i korzystasz z MSI – warto wdrożyć podobne rozwiązanie.
Magento często jest szybkie. Trzeba mu tylko trochę pomóc 😉
