skip to Main Content

I have this relation in my model…

$this->hasMany('AppInventory')->where('status',1)
            ->whereNull('deleted_at')
            ->where(function($query){
                $query
                    ->where('count', '>=', 1)
                    ->orWhere(function($aQuery){
                        $aQuery
                            ->where('count', '=' , 0)
                            ->whereHas('containers', function($bQuery){
                                $bQuery->whereIn('status', [0,1]);
                            });
                    });
            })
            ->orderBy('updated_at','desc')
            ->with('address', 'cabin');

And Sql query generated are :

select
  *
from
  `inventories`
where
  `inventories`.`user_id` = 17
  and `inventories`.`user_id` is not null
  and `status` = 1
  and `deleted_at` is null
  and (
    `count` >= 1
    or (
      `count` = 0
      and exists (
        select
          *
        from
          `containers`
        where
          `inventories`.`id` = `containers`.`inventory_id`
          and `status` in (0, 1)
      )
    )
  )
  and `inventories`.`deleted_at` is null
order by
  `updated_at` desc
limit
  10 offset 0

Unfortunately this take more than 2sec in MySql,

There are anyways to improve and reduce the query time for this?!

Each inventory has many containers. when inventory count is 0 (0 mean out of stock but sometimes there are disabled containers that mean inventory is not out of stock yet.) the real count is depend on count of containers with status [0,1] (containers have other statuses…).

I have an idea to have a column on inventory to count containers with [0,1] status, and update it in other processes to improve this query. but this take too much time and need to modify other process.

Inventories show create table

CREATE TABLE `inventories` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `user_id` bigint unsigned NOT NULL,
  `cabin_id` bigint unsigned NOT NULL,
  `address_id` bigint unsigned NOT NULL,
  `count` mediumint NOT NULL,
  `status` mediumint NOT NULL,
  `name` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `available_at` datetime DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  `deleted_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=37837 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

Containers show create table

CREATE TABLE `containers` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `inventory_id` bigint unsigned NOT NULL,
  `order_id` bigint unsigned DEFAULT NULL,
  `status` tinyint unsigned NOT NULL DEFAULT '1',
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=64503 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

Used Solution due comments (Thanks to @ysth @vixducis @Breezer ):

Changed Containers engine from MyISAM to InnoDB ,

Added INDEX to containers.inventory_id

And optimize code like below and limit whereHas select query

$this->hasMany('AppInventory')->where('status',1)
            ->whereNull('deleted_at')
            ->where(function($query){
                $query
                    ->where('count', '>=', 1)
                    ->orWhere('count', '=' , 0)
                    ->whereHas('containers', function ($bQuery) {
                        $bQuery
                            ->select('inventory_id')
                            ->whereIn('status', [0, 1]);
                    });

            })
            ->orderBy('updated_at','desc')
            ->with('address', 'cabin');

for whereHas we can use whereIn and subQuery like below


->whereIn('id', function ($subQuery) {
    $subQuery->select('inventory_id')
         ->from('containers')
         ->whereIn('status', [0, 1]);
});

and for limiting select of dosentHave

->doesntHave('requests', 'and', function($query){
    $query->select('inventory_id');
})

3

Answers


  1. when your data is big, whereHas statement sometimes run slowly because it use exists syntax. For more detailed explanation, you can read from this post.

    To solve this, I prefer you to use mpyw/eloquent-has-by-non-dependent-subquery because it will use in syntax which will improve the performance. I already used this package on my project, and no problem until now.

    Login or Signup to reply.
  2. It looks like the containers table is still running on the MyISAM engine. While that engine is not deprecated, the development focus has shifted heavily towards InnoDB, and it should be a lot more performant. Switching to InnoDB is a good first step.

    Secondly, I see that there is no index on containers.inventory_id. When experiencing performance issues when relating two tables, it’s often a good idea to check whether adding an index on the column that relates the tables improves performance.

    These two steps should make your query a lot faster.

    Login or Signup to reply.
    • Change to InnoDB.
    • inventories needs this composite index: INDEX(user_id, status, deleted_at, updated_at)
    • containers needs this composite index, not simply (inventory_id), but (inventory_id, status).
    • Redundant: inventories.user_id is not null because the test for 17 requires NOT NULL.
    • Redundant: deleted_at is null simply because it is in the query twice.
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search