Improving complete sql query in stored proc

生来就可爱ヽ(ⅴ<●) 提交于 2019-12-24 09:39:55

问题


I have a stored proc that is used to populate a dashboard like grid in WPF. I've refined the query over time, and to be honest, the performance could be better. I'm at the point now that I do not know any good ways to improve the query where I can gain performance with the time it takes to execute.

Any help would be greatly appreciated. Below is the stored proc:

    ALTER PROCEDURE [dbo].[spGetDashboardMainNew]
    AS
         BEGIN
             SET NOCOUNT ON;
             SELECT JC.job_number AS SalesOrder,
                    dbo.cust.Company AS Customer,
                    JC.Plan_ship_date AS PlannedShipDate,
                    JC.latest_ship_date AS LatestShipDate,
                    JC.planned_fab_complete AS PlanFabComplete,
                    JC.fldPromisedDate AS CommittedShipDate,
                    JC.Materials_of_Construction AS MatofCons,
                    JC.Fab_rating AS FabRating,
                    JC.ass_rating AS [Assembly Rating],
                    JC.shipped AS OpenShipped,
                    JC.electrical_status,
                    JC.dp_credit_status,
                    JC.fldCustRequestDate,
                    JC.commercial_terms,
                    JC.Approved_by,
                    fldLiquidatedDamages,
                    fldLiquidatedDamagesDesc,
                    CASE
                        WHEN hot_list = '0'
                             OR hot_list = 'No'
                        THEN 'No'
                        ELSE 'Yes'
                    END AS HotList,
                    CASE
                        WHEN JC.latest_ship_date IS NOT NULL
                             AND JC.fldPromisedDate IS NULL
                        THEN JC.latest_ship_date
                        ELSE JC.fldPromisedDate
                    END AS CommitDate,
                    CASE
                        WHEN dp_credit_status = 1
                             AND commercial_terms = 'Complete'
                        THEN 1
                        ELSE 0
                    END AS TermsMet,
                    CASE
                        WHEN JC.fldPromisedDate IS NULL
                        THEN 1
                        ELSE 0
                    END AS SortByDate,
                    dbo.tblShippingInfo.fldShipmentRequestID,
                    CASE
                        WHEN JC.fleInspectionType <> 'None'
                        THEN JC.fleInspectionType
                        ELSE NULL
                    END AS InspectionType,
                    JC.fldInspectionNotes,
                 JC.DueDateExtraTime,
                    CASE
                        WHEN JC.fleInspectionType <> 'None'
                             AND JC.fleInspectionType IS NOT NULL
                        THEN 1
                        ELSE 0
                    END AS InspectionDesc,
                    advanced_buying,
                    SortedOrder,
                    SalesOrderStatusGroup,
                    SalesOrderStatus,
                    SpecialOpList,
                    OperationDueBy,
                    test,
                    wpStatus,
                    SpecialOpsCount,
                    InChangeOrder,
                    RequiresSpecialPaint
             FROM dbo.cust
                  INNER JOIN dbo.Job_Control JC ON dbo.cust.CUST# = JC.Cust#
                  LEFT OUTER JOIN dbo.tblShippingInfo ON JC.job_number = dbo.tblShippingInfo.fldJobNumberID
                  OUTER APPLY
             (
                 SELECT dbo.udfGetManStatusFullSortOrder(JC.job_number) AS SortedOrder
             ) acolumn
                  OUTER APPLY
             (
                 SELECT ISNULL(dbo.udfGetManStatusBySOGroupNo(JC.job_number, 1), dbo.udfGetManStatusNew(JC.job_number)) AS SalesOrderStatusGroup
             ) bcolumn
                  OUTER APPLY
             (
                 SELECT ISNULL(dbo.udfGetManStatusBySOGroupNo(JC.job_number, 1), dbo.udfGetManStatusNew(JC.job_number)) AS SalesOrderStatus
             ) ccolumn
                  OUTER APPLY
             (
                 SELECT dbo.udfGetSpecialOperations(JC.job_number) AS SpecialOpList
             ) dcolumn
                  OUTER APPLY
             (
                 SELECT CASE
                            WHEN dbo.udfGetManStatusNew(JC.job_number) LIKE '%Approval%' OR dbo.udfGetManStatusNew(JC.job_number) LIKE '%Re-Approval%'
                            THEN JC.Approval_Due
                            ELSE dbo.udfGetDueByPerOperation(JC.job_number, 1)
                        END AS OperationDueBy
             ) ecolumn
                  OUTER APPLY
             (
                 SELECT dbo.udfGetGroupCount(JC.job_number) AS test
             ) fcolumn
                  OUTER APPLY
             (
                 SELECT dbo.udfGetOpenWorkPackagesByDesigner(JC.job_number) AS wpStatus
             ) gcolumn
                  OUTER APPLY
             (
                 SELECT dbo.udfGetSpecialOpsCount(JC.job_number) AS SpecialOpsCount
             ) hcolumn
                  OUTER APPLY
             (
                 SELECT dbo.udfIsActiveChangeOrder(JC.job_number) AS InChangeOrder
             ) icolumn
                  OUTER APPLY
             (
                 SELECT dbo.udfRequiresSpecialColor(JC.job_number) AS RequiresSpecialPaint
             ) jcolumn
             WHERE(JC.shipped = 'Open')
                  OR (JC.shipped = 'Hold');
         END;

Below are the outter apply functions that are called:

        ALTER FUNCTION [dbo].[udfGetManStatusFullSortOrder] 
    (
        @SalesOrderNumber int
    )
    RETURNS INT
    AS
    BEGIN
        DECLARE @shipped nvarchar(50);
        DECLARE @eng_complete datetime;
        DECLARE @Drawing_Info nvarchar(50);
        DECLARE @Release_to_engineering datetime;
        DECLARE @Dwg_Sent datetime;
        DECLARE @Approval_done datetime;
        DECLARE @Detail bit;
        DECLARE @Release_to_shop datetime;
        DECLARE @OperationCount int;
        DECLARE @SortOrder INT;
        DECLARE @p as INT = 1;
        DECLARE @NonOpSortOrder INT;

        SET @NonOpSortOrder = dbo.udfIsOnHoldSort(@SalesOrderNumber)

        IF @NonOpSortOrder > 0 
            RETURN @nonopsortorder

            SELECT @SortOrder = (SELECT TOP(@p) dbo.ShopRouting.DashboardSortOrder
                FROM    dbo.Product INNER JOIN
                      dbo.ProductMfgGrouping ON dbo.Product.Record_no = dbo.ProductMfgGrouping.Record_no INNER JOIN
                      dbo.MfgGrouping ON dbo.ProductMfgGrouping.MfgGroupingID = dbo.MfgGrouping.MfgGroupingID INNER JOIN
                      dbo.GroupOperations ON dbo.MfgGrouping.MfgGroupingID = dbo.GroupOperations.MfgGroupingID INNER JOIN
                      dbo.ShopRouting ON dbo.GroupOperations.ShopRoutingID = dbo.ShopRouting.ShopRoutingID
                WHERE  (dbo.Product.Job_number = @SalesOrderNumber) AND (dbo.MfgGrouping.GroupingNumber = 1) AND (dbo.GroupOperations.Completed IS NULL)
                GROUP BY dbo.ShopRouting.Description, dbo.GroupOperations.NewSortOrder, dbo.ShopRouting.DashboardSortOrder
                ORDER BY dbo.GroupOperations.NewSortOrder);

        IF @SortOrder IS NOT NULL
                RETURN @SortOrder

        SELECT
            @shipped = shipped,
            @eng_complete = eng_complete,
            @Drawing_Info = Drawing_Info,
            @Release_to_engineering = Release_to_engineering,
            @Dwg_Sent = Dwg_Sent,
            @Approval_done = Approval_done,
            @Detail = Detail,
            @Release_to_shop = Release_to_shop

        FROM
            job_control

        WHERE
            job_number = @SalesOrderNumber

        BEGIN
            SET @OperationCount = flex2ksql.dbo.udfGetGroupOperationsCount(@SalesOrderNumber);
        END

        IF (@shipped = 'Hold')
            RETURN 33

        IF (@eng_complete IS NULL)
            BEGIN
                 IF (@Release_to_engineering IS NULL)

                    RETURN 32

                IF (@Drawing_Info = 'Approval' AND
                    @Release_to_engineering IS NOT NULL AND
                    @Dwg_Sent IS NOT NULL AND
                    @Approval_done IS NULL)

                    RETURN 28

                IF (@Drawing_Info = 'Re-Approval' AND
                    @Release_to_engineering IS NOT NULL AND
                    @Dwg_Sent IS NOT NULL AND
                    @Approval_done IS NULL)

                    RETURN 29

                IF (@Drawing_Info = 'Approval' AND
                    @Release_to_engineering IS NOT NULL AND
                    @Dwg_Sent IS NOT NULL AND
                    @Detail = 1 AND
                    @Approval_done IS NOT NULL)

                    If (@OperationCount) = 0 OR (@OperationCount IS NULL)
                        RETURN 27
                    ELSE
                        RETURN 26

                IF (@Drawing_Info = 'Approval' AND 
                    @Release_to_engineering IS NOT NULL)

                    RETURN 30

                IF (@Drawing_Info = 'Re-Approval' AND
                    @Release_to_engineering IS NOT NULL AND
                    @Dwg_Sent IS NULL AND
                    @Approval_done IS NULL)

                    RETURN 31

                IF (@Drawing_Info = 'Certified' AND
                    @Release_to_engineering IS NOT NULL AND
                    @Detail = 1)

                    If (@OperationCount) = 0 OR (@OperationCount IS NULL)
                        RETURN 27
                    ELSE
                        RETURN 26

                IF (@Drawing_Info = 'No Drawing Required' AND
                    @Release_to_engineering IS NOT NULL)

                    If (@OperationCount) = 0 OR (@OperationCount IS NULL)
                        RETURN 27
                    ELSE
                        RETURN 26

                IF (@Drawing_Info = 'Certified')

                    If (@OperationCount) = 0 OR (@OperationCount IS NULL)
                        RETURN 27
                    ELSE
                        RETURN 26

                IF (@Drawing_Info = 'Sales Drawing' AND
                    @Release_to_engineering IS NOT NULL AND
                    @Release_to_shop IS NOT NULL)

                    If (@OperationCount) = 0 OR (@OperationCount IS NULL)
                        RETURN 27
                    ELSE
                        RETURN 26
                ELSE
                    RETURN 27

            END
        ELSE
            BEGIN
                RETURN 27
            END
        RETURN 99



        ALTER FUNCTION [dbo].[udfGetManStatusBySOGroupNo] 
    (
        @SalesOrder int, @GroupNo int
    )
    RETURNS varchar(100)
    AS
    BEGIN

        DECLARE @ManStatus varchar(100);
        DECLARE @p as INT = 1;
        DECLARE @NonOpSortOrder INT;

        SET @NonOpSortOrder = dbo.udfIsOnHoldSort(@SalesOrder)

        IF @NonOpSortOrder > 0 
            RETURN 'Hold'
        ELSE

                SELECT @ManStatus = (SELECT TOP(@p) dbo.ShopRouting.Description
                FROM dbo.Product INNER JOIN
                      dbo.ProductMfgGrouping ON dbo.Product.Record_no = dbo.ProductMfgGrouping.Record_no INNER JOIN
                      dbo.MfgGrouping ON dbo.ProductMfgGrouping.MfgGroupingID = dbo.MfgGrouping.MfgGroupingID INNER JOIN
                      dbo.GroupOperations ON dbo.MfgGrouping.MfgGroupingID = dbo.GroupOperations.MfgGroupingID INNER JOIN
                      dbo.ShopRouting ON dbo.GroupOperations.ShopRoutingID = dbo.ShopRouting.ShopRoutingID
                WHERE  (dbo.Product.Job_number = @SalesOrder) AND (dbo.MfgGrouping.GroupingNumber = @GroupNo) AND (dbo.GroupOperations.Completed is null)
                GROUP BY dbo.ShopRouting.Description, dbo.Product.Model_NO, dbo.GroupOperations.NewSortOrder
                ORDER BY dbo.GroupOperations.NewSortOrder);

                RETURN @ManStatus


        ALTER FUNCTION [dbo].[udfGetManStatusNew] 
    (
        @SalesOrderNumber int
    )
    RETURNS nvarchar(50)
    AS
    BEGIN
        DECLARE @shipped nvarchar(50);
        DECLARE @eng_complete datetime;
        DECLARE @Drawing_Info nvarchar(50);
        DECLARE @Release_to_engineering datetime;
        DECLARE @Dwg_Sent datetime;
        DECLARE @Approval_done datetime;
        DECLARE @Detail bit;
        DECLARE @Release_to_shop datetime;
        DECLARE @OperationCount int;

        SELECT
            @shipped = shipped,
            @eng_complete = eng_complete,
            @Drawing_Info = Drawing_Info,
            @Release_to_engineering = Release_to_engineering,
            @Dwg_Sent = Dwg_Sent,
            @Approval_done = Approval_done,
            @Detail = Detail,
            @Release_to_shop = Release_to_shop

        FROM
            job_control

        WHERE
            job_number = @SalesOrderNumber

        BEGIN
            SET @OperationCount = flex2ksql.dbo.udfGetGroupOperationsCount(@SalesOrderNumber);
        END

        IF (@shipped = 'Hold')
            RETURN 'Hold'

        IF (@eng_complete IS NULL)
            BEGIN
              IF (@Release_to_engineering IS NULL)

                    RETURN 'Not Released'

                IF (@Drawing_Info = 'Approval' AND
                    @Release_to_engineering IS NOT NULL AND
                    @Dwg_Sent IS NOT NULL AND
                    @Approval_done IS NULL)

                    RETURN 'Approval Out'

                IF (@Drawing_Info = 'Re-Approval' AND
                    @Release_to_engineering IS NOT NULL AND
                    @Dwg_Sent IS NOT NULL AND
                    @Approval_done IS NULL)

                    RETURN 'Re-Approval Out'

                IF (@Drawing_Info = 'Approval' AND
                    @Release_to_engineering IS NOT NULL AND
                    @Dwg_Sent IS NOT NULL AND
                    @Detail = 1 AND
                    @Approval_done IS NOT NULL)

                    If (@OperationCount) = 0 OR (@OperationCount IS NULL)
                        RETURN 'Routing'
                    ELSE
                        RETURN 'Detail'

                IF (@Drawing_Info = 'Approval' AND 
                    @Release_to_engineering IS NOT NULL)

                    RETURN 'Approval To Be Done'

                IF (@Drawing_Info = 'Re-Approval' AND
                    @Release_to_engineering IS NOT NULL AND
                    @Dwg_Sent IS NULL AND
                    @Approval_done IS NULL)

                    RETURN 'Re-Approval To Be Done'
                IF (@Drawing_Info = 'Certified' AND
                    @Release_to_engineering IS NOT NULL AND
                    @Detail = 1)

                    If (@OperationCount) = 0 OR (@OperationCount IS NULL)
                        RETURN 'Routing'
                    ELSE
                        RETURN 'Detail'

                IF (@Drawing_Info = 'No Drawing Required' AND
                    @Release_to_engineering IS NOT NULL)

                    If (@OperationCount) = 0 OR (@OperationCount IS NULL)
                        RETURN 'Routing'
                    ELSE
                        RETURN 'Detail'

                IF (@Drawing_Info = 'Certified')

                    If (@OperationCount) = 0 OR (@OperationCount IS NULL)
                        RETURN 'Routing'
                    ELSE
                        RETURN 'Detail'

                IF (@Drawing_Info = 'Sales Drawing' AND
                    @Release_to_engineering IS NOT NULL AND
                    @Release_to_shop IS NOT NULL)

                    If (@OperationCount) = 0 OR (@OperationCount IS NULL)
                        RETURN 'Routing'
                    ELSE
                        RETURN 'Detail'
                ELSE
                    RETURN 'Routing'

            END
        ELSE
            BEGIN
                RETURN 'Routing'
            END
        RETURN NULL


ALTER FUNCTION [dbo].[udfGetSpecialOperations] 
(
@SalesOrder int
)
RETURNS nvarchar(MAX)
AS
BEGIN   
DECLARE @returnVal nvarchar(MAX)

SELECT @returnVal = Stuff((SELECT N', ' + dbo.ShopRouting.Description FROM dbo.Product INNER JOIN
        dbo.ProductMfgGrouping ON dbo.Product.Record_no = dbo.ProductMfgGrouping.Record_no INNER JOIN
        dbo.MfgGrouping ON dbo.ProductMfgGrouping.MfgGroupingID = dbo.MfgGrouping.MfgGroupingID INNER JOIN
        dbo.GroupOperations ON dbo.MfgGrouping.MfgGroupingID = dbo.GroupOperations.MfgGroupingID INNER JOIN
        dbo.ShopRouting ON dbo.GroupOperations.ShopRoutingID = dbo.ShopRouting.ShopRoutingID                         
    WHERE (dbo.ShopRouting.Standard = 0) AND (dbo.Product.Job_number = @SalesOrder)
    GROUP BY dbo.ShopRouting.Description, dbo.ShopRouting.DashboardSortOrder
    ORDER BY dbo.ShopRouting.DashboardSortOrder DESC 
    FOR XML PATH(''),TYPE).value('text()[1]','nvarchar(max)'),1,2,N'')

RETURN @returnVal


        ALTER FUNCTION [dbo].[udfGetDueByPerOperation] 
    (
        @SalesOrder int, @GroupNo int
    )
    RETURNS varchar(100)
    AS
    BEGIN
        DECLARE @ManStatus varchar(100);
        DECLARE @p as INT = 1;

        SELECT @ManStatus = (SELECT TOP(@p) dbo.GroupOperations.StartBy
    FROM     dbo.Product INNER JOIN
                      dbo.ProductMfgGrouping ON dbo.Product.Record_no = dbo.ProductMfgGrouping.Record_no INNER JOIN
                      dbo.MfgGrouping ON dbo.ProductMfgGrouping.MfgGroupingID = dbo.MfgGrouping.MfgGroupingID INNER JOIN
                      dbo.GroupOperations ON dbo.MfgGrouping.MfgGroupingID = dbo.GroupOperations.MfgGroupingID INNER JOIN
                      dbo.ShopRouting ON dbo.GroupOperations.ShopRoutingID = dbo.ShopRouting.ShopRoutingID
    WHERE  (dbo.Product.Job_number = @SalesOrder) AND (dbo.MfgGrouping.GroupingNumber = @GroupNo) AND (dbo.GroupOperations.Completed IS NULL) AND (dbo.GroupOperations.Active = 0)
    GROUP BY dbo.ShopRouting.[Description], dbo.Product.Model_NO, dbo.GroupOperations.NewSortOrder, dbo.GroupOperations.StartBy
    ORDER BY dbo.GroupOperations.NewSortOrder);

        RETURN @ManStatus


        ALTER FUNCTION [dbo].[udfGetGroupCount] 
    (
        @SalesOrder int
    )
    RETURNS varchar(MAX)
    AS
    BEGIN
        DECLARE @result varchar(MAX)
        DECLARE @result2 varchar(MAX)

    SET @result = ''

    SELECT @result = @result + Description + ': ' + Cast(StatusCount as varchar(5)) + ', ' FROM vwLineItemGroupStatusCounts WHERE Job_number = @SalesOrder
    ORDER BY vwLineItemGroupStatusCounts.NewSortOrder

    SELECT @result2 = substring(@result, 0, len(@result)) --trim extra "," at end

        -- Return the result of the function
        RETURN @result2


        ALTER FUNCTION [dbo].[udfGetOpenWorkPackagesByDesigner] 
    (
        @SalesOrder int
    )
    RETURNS varchar(MAX)
    AS
    BEGIN
        DECLARE @result varchar(MAX)
        DECLARE @result2 varchar(MAX)

    SET @result = ''

    --SELECT @result = @result + Fldusername + ': ' + Cast(StatusCount as varchar(5)) + ', ' FROM vwOpenWorkPackagesByDesigner WHERE Job_number = @SalesOrder
    SELECT @result = @result + eng_difficulty_category + ' ' FROM vwOpenWorkPackagesEngRating WHERE job_number = @SalesOrder
    SELECT @result = @result + Fldusername + ' ' + Cast(coalesce(fldPriority,'none') as varchar(5)) + ' ' + cast(PercentComplete as varchar(10)) + '% ' + fldTaskCategoryAbbr + '; ' FROM vwOpenWorkPackagesByDesigner WHERE fldSalesOrder = @SalesOrder

    SELECT @result2 = substring(@result, 0, len(@result)) --trim extra "," at end

        -- Return the result of the function
        RETURN @result2


        ALTER FUNCTION [dbo].[udfGetSpecialOpsCount] 
    (
        @SalesOrder int
    )
    RETURNS int
    AS
    BEGIN

        DECLARE @returnVal int;

        SELECT @returnVal = COUNT(dbo.GroupOperations.GroupOperationID)
    FROM     dbo.Product INNER JOIN
                      dbo.ProductMfgGrouping ON dbo.Product.Record_no = dbo.ProductMfgGrouping.Record_no INNER JOIN
                      dbo.MfgGrouping ON dbo.ProductMfgGrouping.MfgGroupingID = dbo.MfgGrouping.MfgGroupingID INNER JOIN
                      dbo.GroupOperations ON dbo.MfgGrouping.MfgGroupingID = dbo.GroupOperations.MfgGroupingID INNER JOIN
                      dbo.ShopRouting ON dbo.GroupOperations.ShopRoutingID = dbo.ShopRouting.ShopRoutingID
    WHERE  (dbo.ShopRouting.Standard = 0) AND (dbo.Product.Job_number = @SalesOrder) AND (dbo.ShopRouting.ShopRoutingID NOT IN (15,16))

        If (@returnVal) = 0 OR (@returnVal IS NULL)
            SET @returnVal = 0;
        Return @returnVal;


        ALTER FUNCTION [dbo].[udfIsActiveChangeOrder] (@SalesOrder numeric(18, 0))

    RETURNS Bit

    AS

    BEGIN

        DECLARE @isCO Bit
        DECLARE @SOFound as numeric(18)

        SET @SOFound = 0

            BEGIN
            SELECT
                @SOFound = fldSalesOrder        
            FROM
                [flex2kSQL].[dbo].[vwChangeOrdersAllActive]
            WHERE
                fldSalesOrder = @SalesOrder
            END


            if @SOFound =0
                SET @isCO=0
            else
                SET @isCO=1


        RETURN @isCO


        ALTER FUNCTION [dbo].[udfRequiresSpecialColor] 
    (
        @SalesOrder INT
    )
    RETURNS INT
    AS
    BEGIN
        DECLARE @ReturnVal int;

        IF EXISTS (SELECT 1
            FROM dbo.MfgGrouping INNER JOIN
                dbo.ProductMfgGrouping ON dbo.MfgGrouping.MfgGroupingID = dbo.ProductMfgGrouping.MfgGroupingID INNER JOIN
                dbo.Product ON dbo.ProductMfgGrouping.Record_no = dbo.Product.Record_no INNER JOIN
                dbo.GroupOperations ON dbo.MfgGrouping.MfgGroupingID = dbo.GroupOperations.MfgGroupingID
            WHERE (dbo.Product.Job_number = @SalesOrder) AND (dbo.GroupOperations.ShopRoutingID IN (23, 24, 25)))
        BEGIN
            SET @ReturnVal = 1
        END
        ELSE
        BEGIN
            SET @ReturnVal = 0
        END

        Return @ReturnVal;

Below is a row of sample data from executing the stored proc (removed every row but test order):

SalesOrder Customer PlannedShipDate LatestShipDate PlanFabComplete CommittedShipDate MatofCons FabRating Assembly Rating OpenShipped electrical_status dp_credit_status fldCustRequestDate commercial_terms Approved_by fldLiquidatedDamages fldLiquidatedDamagesDesc HotList CommitDate TermsMet SortByDate fldShipmentRequestID InspectionType fldInspectionNotes DueDateExtraTime InspectionDesc advanced_buying SortedOrder SalesOrderStatusGroup SalesOrderStatus SpecialOpList OperationDueBy test wpStatus SpecialOpsCount InChangeOrder RequiresSpecialPaint 76506 Test Customer 6/16/2020 12/31/2020 NULL 11/28/2016 SS C C Open In Test 0 9/29/2016 NULL Premount/Prewire 0 NULL Yes 11/28/2016 0 0 NULL NULL NULL 7 0 Complete 26 Detail Detail NULL 6/2/2020 Detail: 6 NULL 0 0 0

Lastly, below is a screen shot showing how the data is displayed and organized on the main form.

--EDIT-- Latest query execution plan after changing functions called to inline table functions. Latest Query Execution Plan


回答1:


Query optimisation is more of an art form than a science.

What stands out is that many of the functions you've employed are written in an imperative style. As a starting point, I would investigate whether the OUTER APPLYs against function calls can be rewritten as LEFT JOINs against views (either written inline, if the functions are unique to this report, or against a pre-defined view).

So for example, the function udfRequiresSpecialColor can be rewritten as:

SELECT
    dbo.Product.Job_number

FROM 
    dbo.MfgGrouping 

INNER JOIN
    dbo.ProductMfgGrouping 
    ON dbo.MfgGrouping.MfgGroupingID = dbo.ProductMfgGrouping.MfgGroupingID 

INNER JOIN
    dbo.Product 
    ON dbo.ProductMfgGrouping.Record_no = dbo.Product.Record_no 

INNER JOIN 
    dbo.GroupOperations 
    ON dbo.MfgGrouping.MfgGroupingID = dbo.GroupOperations.MfgGroupingID
    AND (dbo.GroupOperations.ShopRoutingID IN (23, 24, 25))

This returns a single table containing all Job_numbers that require a special colour, which can be joined against.

Then, in the main query, the following OUTER APPLY is replaced:

SELECT
    ...
    RequiresSpecialPaint
    ...
OUTER APPLY
(
    SELECT dbo.udfRequiresSpecialColor(JC.job_number) AS RequiresSpecialPaint
) AS jcolumn

...and is replaced with a LEFT JOIN in the main query:

SELECT
    ...
    IIF(jobs_requiring_special_paint.job_number IS NOT NULL, 1, 0) AS RequiresSpecialPaint
    ...

LEFT JOIN
    (
        [here you can either a call to the new udfRequiresSpecialColor defined as a database view or an inlinable function, or simply include the code above as a subquery]
    ) AS jobs_requiring_special_paint
    ON (JC.job_number = jobs_requiring_special_paint.job_number)

By left-joining onto this table, there will be a job_number for those jobs that have special paint, and a NULL value for those jobs that don't - that is then converted to a 0/1 flag in the select statement, rather than being part of the sub-function.

You get the general picture, and you can see that the entire part of the query that deals with getting the special paint data, is now dealt with in a purely set-based fashion.

Removing imperative code gives the query optimiser more latitude to analyse and reorganise the query in a way that executes more efficiently but preserves the logic you've defined.

You may also find ultimately that what are currently a variety of separate OUTER APPLY queries can be condensed into a single common definition (or fewer common definitions) of the relevant table relationships, with the necessary filtering applied in the ON clause.

For example, udfGetDueByPerOperation seems to hit the same tables as udfRequiresSpecialColor (those tables being MfgGrouping, ProductMfgGrouping, Product, GroupOperations), and in the same basic relationships (INNER JOINs on the same keys), but with ShopRouting tacked on the end.

It might therefore be possible to define common code (either as a database view or inline function, or as a common table expression/WITH clause), and the filtering implemented in the final JOIN clauses against the common table. This is likely to make the code far shorter and easier to understand and maintain, and again it possibly helps the query engine optimise its plan. I can talk more about how this approach would be applied if required.

I note that there are some functions, such as udfGetManStatusNew, that have relatively complex decision logic, which might be difficult to reduce to an expression-based calculation whilst retaining clarity and correctness.

Again though, even as a partial solution without completely eliminating imperative code from the function, it is likely to be far more efficient if such a function is rewritten in such a way that it is given a table of all job_numbers, hitting the job_control table once for the details of all relevant jobs, using an OUTER APPLY to hit the udfGetGroupOperationsCount function for each job (or using the techniques described above to reduce the OUTER APPLY to a LEFT JOIN) and capturing @OperationCount as an additional column (instead of assigning to a scalar variable). Then, once the 'ingredients' of the function have been gathered in into a single table this way, then iterate through the imperative code to derive the necessary calculations for the status of each job).

I don't want to pre-judge the results that you will get from this general approach, because query engines can be unpredictable, but my suspicion is that it should improve performance, if it can be improved.

It might also help if you can get an idea of which parts of the query are contributing most to its poor performance - assuming it is not uniformly slow.

If you need any more guidance, ask away. Let me know also if you have any joy with this.

EDIT: Further information, based on comments below.

I've just realised that I was misinterpreting the query plan that you posted - because the functions are imperative, it doesn't show the query plan for them, and does not include their cost. If you rewrite them as inlinable functions, post another query plan, which will start to show the query plan and the full cost of the functions themselves (at the moment they appear on the query plan as low-cost constant scans, with no further plan information).

The difference between an imperative/multi-statement function, and an inlinable function looks like this (note to avoid confusion, I have a DUAL table in SQL Server, to mirror the Oracle functionality which is built-in by default):

CREATE FUNCTION [dbo].[fxn_imperative] 
(   
     @dummy     VARCHAR(1)
)
RETURNS INT
AS
BEGIN
    DECLARE @ReturnVal int;

    IF EXISTS (SELECT 1 FROM DUAL WHERE DUMMY = @dummy)
        BEGIN
            SET @ReturnVal = 1
        END
    ELSE
        BEGIN
            SET @ReturnVal = 0
        END

    Return @ReturnVal;
END
-- called like so: SELECT dbo.fxn_imperative('X') AS dummy_exists;


CREATE FUNCTION [dbo].[fxn_inline] 
(   
     @dummy     VARCHAR(1)
)
RETURNS TABLE 
AS
RETURN 
(
    SELECT ISNULL( (SELECT 1 FROM DUAL WHERE DUMMY = @dummy), 0) AS dummy_exists
)
-- called like so: SELECT * FROM fxn_inline('X');

These functions are more or less equivalent in functionality - but only the inline one will appear on the query plan for the query which calls the function.

Regards the LEFT JOIN approach, you don't define the inner query as a function in that case - you define it as a view (or simply write it inline as a subquery) and join onto it (using the job_number as the join condition, as the replacement for the parameter that would have been passed to the original function). I've already shown roughly how that approach is applied above as part of my original post.

If I wanted to go with the grain of how your existing query is approaching things (in terms of calls to functions which are OUTER APPLYed), I'd define the function as follows (I'm assuming the underlying query for special paint only ever returns a single row per job/salesorder):

CREATE FUNCTION [dbo].[udfRequiresSpecialColor2] 
(   
     @SalesOrder INT
)
RETURNS TABLE 
AS
RETURN 
(

    WITH special_paint_subquery AS
    (
        SELECT
            dbo.Product.Job_number

        FROM 
            dbo.MfgGrouping 

        INNER JOIN
            dbo.ProductMfgGrouping 
            ON dbo.MfgGrouping.MfgGroupingID = dbo.ProductMfgGrouping.MfgGroupingID 

        INNER JOIN
            dbo.Product 
            ON dbo.ProductMfgGrouping.Record_no = dbo.Product.Record_no 

        INNER JOIN 
            dbo.GroupOperations 
            ON dbo.MfgGrouping.MfgGroupingID = dbo.GroupOperations.MfgGroupingID
            AND (dbo.GroupOperations.ShopRoutingID IN (23, 24, 25))

    )

    SELECT ISNULL( (SELECT 1 FROM special_paint_subquery WHERE dbo.Product.Job_number = @SalesOrder), 0) AS requires_special_paint
)

If the function is written this way, the plan for the function will show up if you get a plan for the main query.

The function is called in the same way you currently call udfRequiresSpecialColor - by cross applying it with the job_number passed as a parameter.

Once you have defined the function in this way, actually converting the function call to a left join becomes trivial (the WITH special_paint_subquery ... part goes into the view definition, the WHERE dbo.Product.Job_number = @SalesOrder goes into the join condition in the main query, and the SELECT statement of the main query is modified to translate a NULL match into a zero as I originally illustrated).



来源:https://stackoverflow.com/questions/47984015/improving-complete-sql-query-in-stored-proc

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!