diff -Nuar a/vendor/magento/module-catalog/Model/Indexer/Product/Price/AbstractAction.php b/vendor/magento/module-catalog/Model/Indexer/Product/Price/AbstractAction.php
index 4846d32826c..130bc7fd6ed 100644
--- a/vendor/magento/module-catalog/Model/Indexer/Product/Price/AbstractAction.php
+++ b/vendor/magento/module-catalog/Model/Indexer/Product/Price/AbstractAction.php
@@ -368,14 +368,20 @@ abstract class AbstractAction
         $productsTypes = $this->getProductsTypes($changedIds);
         $parentProductsTypes = $this->getParentProductsTypes($changedIds);
 
-        $changedIds = array_merge($changedIds, ...array_values($parentProductsTypes));
+        $changedIds = array_unique(array_merge($changedIds, ...array_values($parentProductsTypes)));
         $productsTypes = array_merge_recursive($productsTypes, $parentProductsTypes);
 
         if ($changedIds) {
             $this->deleteIndexData($changedIds);
         }
-        foreach ($productsTypes as $productType => $entityIds) {
-            $indexer = $this->_getIndexer($productType);
+
+        $typeIndexers = $this->getTypeIndexers();
+        foreach ($typeIndexers as $productType => $indexer) {
+            $entityIds = $productsTypes[$productType] ?? [];
+            if (empty($entityIds)) {
+                continue;
+            }
+
             if ($indexer instanceof DimensionalIndexerInterface) {
                 foreach ($this->dimensionCollectionFactory->create() as $dimensions) {
                     $this->tableMaintainer->createMainTmpTable($dimensions);
diff -Nuar a/vendor/magento/module-catalog/Model/Indexer/Product/Price/Action/Rows.php b/vendor/magento/module-catalog/Model/Indexer/Product/Price/Action/Rows.php
index 27b50eea883..acbe20721ee 100644
--- a/vendor/magento/module-catalog/Model/Indexer/Product/Price/Action/Rows.php
+++ b/vendor/magento/module-catalog/Model/Indexer/Product/Price/Action/Rows.php
@@ -5,13 +5,83 @@
  */
 namespace Magento\Catalog\Model\Indexer\Product\Price\Action;
 
+use Magento\Directory\Model\CurrencyFactory;
+use Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory;
+use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer;
+use Magento\Catalog\Model\Product\Type;
+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice;
+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory;
+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\TierPrice;
+use Magento\Framework\App\Config\ScopeConfigInterface;
+use Magento\Framework\Stdlib\DateTime;
+use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
+use Magento\Store\Model\StoreManagerInterface;
+
 /**
  * Class Rows reindex action for mass actions
  *
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) to preserve compatibility with parent class
  */
 class Rows extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction
 {
     /**
+     * Default batch size
+     */
+    private const BATCH_SIZE = 100;
+
+    /**
+     * @var int
+     */
+    private $batchSize;
+
+    /**
+     * @param ScopeConfigInterface $config
+     * @param StoreManagerInterface $storeManager
+     * @param CurrencyFactory $currencyFactory
+     * @param TimezoneInterface $localeDate
+     * @param DateTime $dateTime
+     * @param Type $catalogProductType
+     * @param Factory $indexerPriceFactory
+     * @param DefaultPrice $defaultIndexerResource
+     * @param TierPrice|null $tierPriceIndexResource
+     * @param DimensionCollectionFactory|null $dimensionCollectionFactory
+     * @param TableMaintainer|null $tableMaintainer
+     * @param int|null $batchSize
+     * @SuppressWarnings(PHPMD.NPathComplexity) Added to backward compatibility with abstract class
+     * @SuppressWarnings(PHPMD.CyclomaticComplexity) Added to backward compatibility with abstract class
+     * @SuppressWarnings(PHPMD.ExcessiveParameterList) Added to backward compatibility with abstract class
+     */
+    public function __construct(
+        ScopeConfigInterface $config,
+        StoreManagerInterface $storeManager,
+        CurrencyFactory $currencyFactory,
+        TimezoneInterface $localeDate,
+        DateTime $dateTime,
+        Type $catalogProductType,
+        Factory $indexerPriceFactory,
+        DefaultPrice $defaultIndexerResource,
+        TierPrice $tierPriceIndexResource = null,
+        DimensionCollectionFactory $dimensionCollectionFactory = null,
+        TableMaintainer $tableMaintainer = null,
+        ?int $batchSize = null
+    ) {
+        parent::__construct(
+            $config,
+            $storeManager,
+            $currencyFactory,
+            $localeDate,
+            $dateTime,
+            $catalogProductType,
+            $indexerPriceFactory,
+            $defaultIndexerResource,
+            $tierPriceIndexResource,
+            $dimensionCollectionFactory,
+            $tableMaintainer
+        );
+        $this->batchSize = $batchSize ?? self::BATCH_SIZE;
+    }
+
+    /**
      * Execute Rows reindex
      *
      * @param array $ids
@@ -24,10 +94,28 @@ class Rows extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction
         if (empty($ids)) {
             throw new \Magento\Framework\Exception\InputException(__('Bad value was supplied.'));
         }
-        try {
-            $this->_reindexRows($ids);
-        } catch (\Exception $e) {
-            throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage()), $e);
+        $currentBatch = [];
+        $i = 0;
+
+        foreach ($ids as $id) {
+            $currentBatch[] = $id;
+            if (++$i === $this->batchSize) {
+                try {
+                    $this->_reindexRows($currentBatch);
+                } catch (\Exception $e) {
+                    throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage()), $e);
+                }
+                $i = 0;
+                $currentBatch = [];
+            }
+        }
+
+        if (!empty($currentBatch)) {
+            try {
+                $this->_reindexRows($currentBatch);
+            } catch (\Exception $e) {
+                throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage()), $e);
+            }
         }
     }
 }
diff -Nuar a/vendor/magento/module-catalog-search/Model/Indexer/Fulltext.php b/vendor/magento/module-catalog-search/Model/Indexer/Fulltext.php
index 11f9a286fc4..d02f33e51f3 100644
--- a/vendor/magento/module-catalog-search/Model/Indexer/Fulltext.php
+++ b/vendor/magento/module-catalog-search/Model/Indexer/Fulltext.php
@@ -3,12 +3,16 @@
  * Copyright © Magento, Inc. All rights reserved.
  * See COPYING.txt for license details.
  */
+
 namespace Magento\CatalogSearch\Model\Indexer;
 
 use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\FullFactory;
+use Magento\CatalogSearch\Model\Indexer\Scope\State;
 use Magento\CatalogSearch\Model\Indexer\Scope\StateFactory;
 use Magento\CatalogSearch\Model\ResourceModel\Fulltext as FulltextResource;
+use Magento\Framework\App\ObjectManager;
 use Magento\Framework\Indexer\DimensionProviderInterface;
+use Magento\Framework\Indexer\SaveHandler\IndexerInterface;
 use Magento\Store\Model\StoreDimensionProvider;
 use Magento\Indexer\Model\ProcessManager;
 
@@ -31,6 +35,11 @@ class Fulltext implements
     const INDEXER_ID = 'catalogsearch_fulltext';
 
     /**
+     * Default batch size
+     */
+    private const BATCH_SIZE = 100;
+
+    /**
      * @var array index structure
      */
     protected $data;
@@ -71,6 +80,11 @@ class Fulltext implements
     private $processManager;
 
     /**
+     * @var int
+     */
+    private $batchSize;
+
+    /**
      * @param FullFactory $fullActionFactory
      * @param IndexerHandlerFactory $indexerHandlerFactory
      * @param FulltextResource $fulltextResource
@@ -79,6 +93,8 @@ class Fulltext implements
      * @param DimensionProviderInterface $dimensionProvider
      * @param array $data
      * @param ProcessManager $processManager
+     * @param int|null $batchSize
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
      */
     public function __construct(
         FullFactory $fullActionFactory,
@@ -88,18 +104,18 @@ class Fulltext implements
         StateFactory $indexScopeStateFactory,
         DimensionProviderInterface $dimensionProvider,
         array $data,
-        ProcessManager $processManager = null
+        ProcessManager $processManager = null,
+        ?int $batchSize = null
     ) {
         $this->fullAction = $fullActionFactory->create(['data' => $data]);
         $this->indexerHandlerFactory = $indexerHandlerFactory;
         $this->fulltextResource = $fulltextResource;
         $this->data = $data;
         $this->indexSwitcher = $indexSwitcher;
-        $this->indexScopeState = $indexScopeStateFactory->create();
+        $this->indexScopeState = ObjectManager::getInstance()->get(State::class);
         $this->dimensionProvider = $dimensionProvider;
-        $this->processManager = $processManager ?: \Magento\Framework\App\ObjectManager::getInstance()->get(
-            ProcessManager::class
-        );
+        $this->processManager = $processManager ?: ObjectManager::getInstance()->get(ProcessManager::class);
+        $this->batchSize = $batchSize ?? self::BATCH_SIZE;
     }
 
     /**
@@ -128,9 +144,11 @@ class Fulltext implements
             throw new \InvalidArgumentException('Indexer "' . self::INDEXER_ID . '" support only Store dimension');
         }
         $storeId = $dimensions[StoreDimensionProvider::DIMENSION_NAME]->getValue();
-        $saveHandler = $this->indexerHandlerFactory->create([
-            'data' => $this->data
-        ]);
+        $saveHandler = $this->indexerHandlerFactory->create(
+            [
+                'data' => $this->data,
+            ]
+        );
 
         if (null === $entityIds) {
             $this->indexScopeState->useTemporaryIndex();
@@ -144,13 +162,42 @@ class Fulltext implements
         } else {
             // internal implementation works only with array
             $entityIds = iterator_to_array($entityIds);
-            $productIds = array_unique(
-                array_merge($entityIds, $this->fulltextResource->getRelationsByChild($entityIds))
-            );
-            if ($saveHandler->isAvailable($dimensions)) {
-                $saveHandler->deleteIndex($dimensions, new \ArrayIterator($productIds));
-                $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId, $productIds));
+            $currentBatch = [];
+            $i = 0;
+
+            foreach ($entityIds as $entityId) {
+                $currentBatch[] = $entityId;
+                if (++$i === $this->batchSize) {
+                    $this->processBatch($saveHandler, $dimensions, $currentBatch);
+                    $i = 0;
+                    $currentBatch = [];
+                }
             }
+            if (!empty($currentBatch)) {
+                $this->processBatch($saveHandler, $dimensions, $currentBatch);
+            }
+        }
+    }
+
+    /**
+     * Process batch
+     *
+     * @param IndexerInterface $saveHandler
+     * @param array $dimensions
+     * @param array $entityIds
+     */
+    private function processBatch(
+        IndexerInterface $saveHandler,
+        array $dimensions,
+        array $entityIds
+    ) : void {
+        $storeId = $dimensions[StoreDimensionProvider::DIMENSION_NAME]->getValue();
+        $productIds = array_unique(
+            array_merge($entityIds, $this->fulltextResource->getRelationsByChild($entityIds))
+        );
+        if ($saveHandler->isAvailable($dimensions)) {
+            $saveHandler->deleteIndex($dimensions, new \ArrayIterator($productIds));
+            $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId, $productIds));
         }
     }

diff -Nuar a/vendor/magento/module-catalog-rule/Model/Indexer/IndexBuilder.php b/vendor/magento/module-catalog-rule/Model/Indexer/IndexBuilder.php
index 421a6e718b3..6560f7cda44 100644
--- a/vendor/magento/module-catalog-rule/Model/Indexer/IndexBuilder.php
+++ b/vendor/magento/module-catalog-rule/Model/Indexer/IndexBuilder.php
@@ -242,7 +242,22 @@ class IndexBuilder
      */
     public function reindexById($id)
     {
-        $this->reindexByIds([$id]);
+        try {
+            $this->cleanProductIndex([$id]);
+
+            $products = $this->productLoader->getProducts([$id]);
+            $activeRules = $this->getActiveRules();
+            foreach ($products as $product) {
+                $this->applyRules($activeRules, $product);
+            }
+
+            $this->reindexRuleGroupWebsite->execute();
+        } catch (\Exception $e) {
+            $this->critical($e);
+            throw new \Magento\Framework\Exception\LocalizedException(
+                __('Catalog rule indexing failed. See details in exception log.')
+            );
+        }
     }

     /**
@@ -275,11 +290,18 @@ class IndexBuilder
     {
         $this->cleanProductIndex($ids);

-        $products = $this->productLoader->getProducts($ids);
-        $activeRules = $this->getActiveRules();
-        foreach ($products as $product) {
-            $this->applyRules($activeRules, $product);
+        /** @var Rule[] $activeRules */
+        $activeRules = $this->getActiveRules()->getItems();
+        foreach ($activeRules as $rule) {
+            $rule->setProductsFilter($ids);
+            $this->reindexRuleProduct->execute($rule, $this->batchCount);
         }
+
+        foreach ($ids as $productId) {
+            $this->cleanProductPriceIndex([$productId]);
+            $this->reindexRuleProductPrice->execute($this->batchCount, $productId);
+        }
+
         $this->reindexRuleGroupWebsite->execute();
     }

@@ -365,17 +387,13 @@ class IndexBuilder
      * Assign product to rule
      *
      * @param Rule $rule
-     * @param Product $product
+     * @param int $productEntityId
+     * @param array $websiteIds
      * @return void
      */
-    private function assignProductToRule(Rule $rule, Product $product): void
+    private function assignProductToRule(Rule $rule, int $productEntityId, array $websiteIds): void
     {
-        if (!$rule->validate($product)) {
-            return;
-        }
-
         $ruleId = (int) $rule->getId();
-        $productEntityId = (int) $product->getId();
         $ruleProductTable = $this->getTable('catalogrule_product');
         $this->connection->delete(
             $ruleProductTable,
@@ -385,7 +403,6 @@ class IndexBuilder
             ]
         );

-        $websiteIds = array_intersect($product->getWebsiteIds(), $rule->getWebsiteIds());
         $customerGroupIds = $rule->getCustomerGroupIds();
         $fromTime = strtotime($rule->getFromDate());
         $toTime = strtotime($rule->getToDate());
@@ -429,12 +446,17 @@ class IndexBuilder
      * @param Product $product
      * @return $this
      * @throws \Exception
+     * @deprecated
+     * @see ReindexRuleProduct::execute
      * @SuppressWarnings(PHPMD.NPathComplexity)
      */
     protected function applyRule(Rule $rule, $product)
     {
-        $this->assignProductToRule($rule, $product);
-        $this->reindexRuleProductPrice->execute($this->batchCount, $product);
+        if ($rule->validate($product)) {
+            $websiteIds = array_intersect($product->getWebsiteIds(), $rule->getWebsiteIds());
+            $this->assignProductToRule($rule, $product->getId(), $websiteIds);
+        }
+        $this->reindexRuleProductPrice->execute($this->batchCount, $product->getId());
         $this->reindexRuleGroupWebsite->execute();

         return $this;
@@ -450,11 +472,16 @@ class IndexBuilder
     private function applyRules(RuleCollection $ruleCollection, Product $product): void
     {
         foreach ($ruleCollection as $rule) {
-            $this->assignProductToRule($rule, $product);
+            if (!$rule->validate($product)) {
+                continue;
+            }
+
+            $websiteIds = array_intersect($product->getWebsiteIds(), $rule->getWebsiteIds());
+            $this->assignProductToRule($rule, $product->getId(), $websiteIds);
         }

         $this->cleanProductPriceIndex([$product->getId()]);
-        $this->reindexRuleProductPrice->execute($this->batchCount, $product);
+        $this->reindexRuleProductPrice->execute($this->batchCount, $product->getId());
     }

     /**
@@ -507,7 +534,7 @@ class IndexBuilder
      */
     protected function applyAllRules(Product $product = null)
     {
-        $this->reindexRuleProductPrice->execute($this->batchCount, $product);
+        $this->reindexRuleProductPrice->execute($this->batchCount, $product->getId());
         $this->reindexRuleGroupWebsite->execute();
         return $this;
     }
@@ -562,7 +589,7 @@ class IndexBuilder
      */
     protected function getRuleProductsStmt($websiteId, Product $product = null)
     {
-        return $this->ruleProductsSelectBuilder->build($websiteId, $product);
+        return $this->ruleProductsSelectBuilder->build((int) $websiteId, (int) $product->getId());
     }

     /**
diff -Nuar a/vendor/magento/module-catalog-rule/Model/Indexer/ReindexRuleProductPrice.php b/vendor/magento/module-catalog-rule/Model/Indexer/ReindexRuleProductPrice.php
index 11ba87730be..51869f1accb 100644
--- a/vendor/magento/module-catalog-rule/Model/Indexer/ReindexRuleProductPrice.php
+++ b/vendor/magento/module-catalog-rule/Model/Indexer/ReindexRuleProductPrice.php
@@ -6,7 +6,6 @@

 namespace Magento\CatalogRule\Model\Indexer;

-use Magento\Catalog\Model\Product;
 use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
 use Magento\Store\Model\StoreManagerInterface;

@@ -65,19 +64,19 @@ class ReindexRuleProductPrice
      * Reindex product prices.
      *
      * @param int $batchCount
-     * @param Product|null $product
+     * @param int|null $productId
      * @param bool $useAdditionalTable
      * @return bool
      * @SuppressWarnings(PHPMD.CyclomaticComplexity)
      */
-    public function execute($batchCount, Product $product = null, $useAdditionalTable = false)
+    public function execute(int $batchCount, ?int $productId = null, bool $useAdditionalTable = false)
     {
         /**
          * Update products rules prices per each website separately
          * because for each website date in website's timezone should be used
          */
         foreach ($this->storeManager->getWebsites() as $website) {
-            $productsStmt = $this->ruleProductsSelectBuilder->build($website->getId(), $product, $useAdditionalTable);
+            $productsStmt = $this->ruleProductsSelectBuilder->build($website->getId(), $productId, $useAdditionalTable);
             $dayPrices = [];
             $stopFlags = [];
             $prevKey = null;
diff -Nuar a/vendor/magento/module-catalog-rule/Model/Indexer/RuleProductsSelectBuilder.php b/vendor/magento/module-catalog-rule/Model/Indexer/RuleProductsSelectBuilder.php
index 6989a33535a..e15bf6b3b1f 100644
--- a/vendor/magento/module-catalog-rule/Model/Indexer/RuleProductsSelectBuilder.php
+++ b/vendor/magento/module-catalog-rule/Model/Indexer/RuleProductsSelectBuilder.php
@@ -74,15 +74,12 @@ class RuleProductsSelectBuilder
      * Build select for indexer according passed parameters.
      *
      * @param int $websiteId
-     * @param \Magento\Catalog\Model\Product|null $product
+     * @param int|null $productId
      * @param bool $useAdditionalTable
      * @return \Zend_Db_Statement_Interface
      */
-    public function build(
-        $websiteId,
-        \Magento\Catalog\Model\Product $product = null,
-        $useAdditionalTable = false
-    ) {
+    public function build(int $websiteId, ?int $productId = null, bool $useAdditionalTable = false)
+    {
         $connection = $this->resource->getConnection();
         $indexTable = $this->resource->getTableName('catalogrule_product');
         if ($useAdditionalTable) {
@@ -107,8 +104,8 @@ class RuleProductsSelectBuilder
             ['rp.website_id', 'rp.customer_group_id', 'rp.product_id', 'rp.sort_order', 'rp.rule_id']
         );

-        if ($product && $product->getEntityId()) {
-            $select->where('rp.product_id=?', $product->getEntityId());
+        if ($productId) {
+            $select->where('rp.product_id=?', $productId);
         }

         /**
@@ -159,9 +156,11 @@ class RuleProductsSelectBuilder
             sprintf($joinCondition, $tableAlias, $storeId),
             []
         );
-        $select->columns([
-            'default_price' => $connection->getIfNullSql($tableAlias . '.value', 'pp_default.value'),
-        ]);
+        $select->columns(
+            [
+                'default_price' => $connection->getIfNullSql($tableAlias . '.value', 'pp_default.value'),
+            ]
+        );

         return $connection->query($select);
     }
