Search code examples
sql-serversql-execution-plan

How to best approach this execution plan


Our warehouse management package has a bottleneck (one of many) with a stored procedure and the major slowdown is due to the query that produces this execution plan.

https://www.brentozar.com/pastetheplan/?id=HkNg65elP

The stored proc can take anywhere from 3 to 10 seconds to run which is quite slow in the context of the business process it runs in.

Some additional info: Yes, there is one table where a full table scan is being done, but this table is narrow, and only has 76 rows. The query does some left joins and some sorting which is needed to produce the correct top result. Overall, it is a bit of a "Rube Goldberg" type query and could probably be simplified, but my goal is to see if it is possible to help with some indexing (which I've done and it's helped a little) or even some small tweaks to the query if needed.

In the end, I need to know where to focus next based on the plan.

Here is the query:

SELECT TOP 1 loc.location_id, loc.wh_id
FROM t_item_uom itu WITH (NOLOCK)
    INNER JOIN t_class_loca clc WITH (NOLOCK)
        ON itu.wh_id = clc.wh_id
        AND ISNULL(dbo.usf_get_item_class_dia_ovrd ('13098271', '895', itu.uom, NULL), itu.class_id) = clc.class_id  

    INNER JOIN t_location loc WITH (NOLOCK)
        ON clc.wh_id = loc.wh_id
        AND clc.location_id = loc.location_id

    INNER JOIN t_pick_area pka WITH (NOLOCK)
        ON pka.pick_area = loc.pick_area
        AND pka.wh_id = loc.wh_id
        AND (pka.pick_area <> N'LABEL' OR (pka.pick_area = N'LABEL' AND 0 IS NULL AND 0 IS NULL) )
        AND (pka.pick_area_type = 'R' OR (pka.pick_area_type = 'V' and 0 IS NULL)  )
                                         
   INNER JOIN t_zone_loca zlc WITH (NOLOCK)
        ON loc.wh_id = zlc.wh_id
        AND loc.location_id = zlc.location_id
          INNER JOIN (
                  SELECT loc.wh_id, loc.pnd_location_id --, loc.location_id
                  FROM t_location loc with (nolock)
                  inner join t_class_loca clc WITH (NOLOCK)
                         on clc.location_id = loc.location_id
                         and clc.wh_id = loc.wh_id
                         and clc.class_id = 'APPAREL'
                  LEFT JOIN t_stored_item sto with (nolock)
                         ON sto.put_away_location = loc.location_id
                         AND sto.wh_id = loc.wh_id      --BTH 20160907 missing wh_id
                         AND sto.put_away_location IS NOT NULL
                         AND sto.type = 0
                  WHERE loc.type in ('I','M')
                  AND loc.pnd_location_id IS NOT NULL --BTH 20160907 - remove from having clause, add here
                  GROUP BY loc.wh_id, loc.pnd_location_id, loc.c3
                  HAVING ((COUNT(sto.hu_id) < 100 
                            and loc.pnd_location_id IS NOT NULL  --BTH 201600907
                            and c3 is null)
                  OR (COUNT(sto.hu_id) < 500 --and loc.pnd_location_id IS NOT NULL   --BTH 201600907
                  and c3 = 'BULK'))
                        ) as pnd
                  ON pnd.wh_id = loc.wh_id
                  AND pnd.pnd_location_id = loc.pnd_location_id

          LEFT OUTER JOIN t_put_rules_empty_and_unalloc_locs_by_pnd tpr WITH (NOLOCK)
                  ON tpr.pnd_location_id = loc.pnd_location_id
                  AND tpr.class_id = itu.class_id
                  AND tpr.wh_id = loc.wh_id

          LEFT OUTER JOIN t_work_q q WITH(NOLOCK)
                  ON q.location_id = loc.location_id
                  AND q.wh_id = loc.wh_id 
                  AND q.work_type = '08'
                  AND q.work_status = 'U'

WHERE loc.status = 'E'
    AND ISNULL(q.work_q_id, 0) = 0 
    AND ( loc.c3 is null or loc.c3 not in ('R','H','S'))
    AND ( 
                  (
                  loc.type = 'M'
                         AND ( (SELECT TOP 1 max_sku_count
                                FROM t_zone zone2 (NOLOCK)
                                WHERE zone2.wh_id = loc.wh_id
                                 AND loc.zone = zone2.zone) >
                                    (SELECT COUNT(sto2.item_number)
                                     FROM t_stored_item sto2 (NOLOCK)
                                     WHERE loc.wh_id = sto2.wh_id
                                     AND loc.location_id = sto2.location_id
                                     )
                                OR '13098271' IN (SELECT sto2.item_number
                                                    FROM t_stored_item sto2 (NOLOCK)
                                                   WHERE loc.wh_id = sto2.wh_id
                                                     AND loc.location_id = sto2.location_id
                                                  )
                              )
                    )
        OR (loc.type = 'I' AND itu.unit_volume = 0 AND itu.nested_volume = 0)
        OR (loc.type = 'I' AND loc.capacity_volume = 0)
        OR (loc.type = 'I' AND loc.capacity_volume >= 0 +
            (CASE 
                 WHEN 0 = 0 THEN 0
                 ELSE 0
             END * (1 - 1)
             )
            )
          )

   --Ensure that only one item is designated to the location
                  AND (loc.type = 'M'
                         OR (loc.type = 'I' AND NOT EXISTS (SELECT 1 
                                                        FROM t_stored_item sto2 WITH (NOLOCK)  -- Sum the item in the location to determine volume
                                                      WHERE sto2.wh_id = loc.wh_id
                                                         AND sto2.put_away_location = loc.location_id)))
    AND itu.wh_id = '895'
    AND itu.item_number = '13098271'
    AND clc.class_id = 'APPAREL'
    AND zlc.zone = 'ALL'
ORDER BY 
          tpr.percent_empty_and_unalloc DESC,
          loc.type, 
          loc.user_count, 
          loc.picking_flow, 
          loc.location_id

Solution

  • Based on some quick peeking at your execution plan. Based on the costs, issue 1 & 3 will give you the best ROI.

    First issue: Key Lookup

    On your index IDX_wh_id_status_pnd_location_id you are missing column zone in the INCLUDE part of your index.

    Second issue: Implicit Conversions

    In short: you are comparing columns from different types. Make sure you compare columns of the same type. If these are Foreign key columns the type in both tables should be exactly the same. If they are parameters , change the type or cast/covert them.

    Third issue: Aggregation

    You have an aggregation (Max, Count, Avg, ...) on [t_stored_item].[sto].put_away_location with a row estimation of 129108. Try creating an indexed view for this part of the query with the aggregation in the indexed view. Use the indexed view instead. More info

    The estimation is also far from the actual, you could try rebuilding your statistics, but it might not help. Why? Read this

    Forth issue: User Definied function

    You have a INNER JOIN with usf_get_item_class_dia_ovrd is it possible to write the logic of the user defined function inline? The code is often optmized when we do it inline, now the scalar function is executing row-by-row instead of set based.

    Fifth issue: constant scans - actual number of rows 0

    This might not be a big issue, but often this happens when expressions cancel each other out. Dummy example : 1 = 0 will always evaluate to 0 rows, so SQL server replaces it with an empty constant scan. In complex queries this is something you might not find immediately. This doesn't have a high impact on performance but you might get better execution plans when you remove these from the queries.

    If interested watch this video to get a better understanding of the query optimizer. (Kind of old, but still relevant)

    Bonus: parameter sniffing

    You mentioned it’s a stored procedure. Often stored procedures have issues with parameters sniffing.