Sql-server – How to format Procedures for complex searches

sql serverstored-procedures

So I am trying to create a way for users to search through data on my SQL Database. Right now I've been getting away with using a single view using a lot of Joins:

CREATE VIEW PartsData AS
SELECT
    p.part_id                  AS [Part_ID],
    p.part_number              AS [Part_Number], 
    p.part_description         AS [Description], 
    p.information              AS [Information], 
    m.manufacturer_name        AS [Manufacturer], 
    ca.category_description    AS [Category], 
    su.subcategory_description AS [Subcategory],
    cu.currency_type           AS [Currency], 
    pl.cost_per_unit           AS [Price], 
    pl.discount_percentage     AS [Discount]
FROM         
    dbo.parts          AS P  INNER JOIN
    dbo.subcategories  AS su ON p.subcategory_id = su.subcategory_id INNER JOIN
    dbo.price_logs     AS pl ON p.part_id = pl.part_id AND pl.log_date = (SELECT MAX(log_date) FROM price_logs WHERE part_id = pl.part_id) INNER JOIN
    dbo.manufacturers  AS m  ON p.manufacturer_id = m.manufacturer_id INNER JOIN
    dbo.categories     AS ca ON su.category_id = ca.category_id INNER JOIN
    dbo.currencies     AS cu ON pl.currency_id = cu.currency_id
GO

And a rather crude 'kitchen sink' stored procedure:

CREATE PROCEDURE dbo.PartSearch(
    @PartID       INT          = NULL,
    @PartNum      VARCHAR(50)  = NULL,
    @Description  VARCHAR(MAX) = NULL,
    @Information  VARCHAR(MAX) = NULL,
    @Supplier     VARCHAR(75)  = NULL,
    @Manufacturer VARCHAR(50)  = NULL,
    @Category     VARCHAR(50)  = NULL,
    @Subcategory  VARCHAR(50)  = NULL,
    @JobNum       VARCHAR(20)  = NULL
) AS 
BEGIN
    SET NOCOUNT ON

    DECLARE @SQL NVARCHAR(MAX) = N'
        SELECT DISTINCT * FROM PartsData WHERE 1=1'
        + CASE WHEN @PartID IS NOT NULL THEN
        N' AND [Part_ID] = @PartID' ELSE N'' END
        + CASE WHEN @PartNum IS NOT NULL THEN
        N' AND [Part_Number] LIKE @PartNum' ELSE N'' END
        + CASE WHEN @Description IS NOT NULL THEN
        N' AND [Description] LIKE @Description' ELSE N'' END
        + CASE WHEN @Information IS NOT NULL THEN
        N' AND [Information] LIKE @Information' ELSE N'' END
        + CASE WHEN @Supplier IS NOT NULL THEN
        N' AND [Supplier] LIKE @Supplier' ELSE N'' END
        + CASE WHEN @Manufacturer IS NOT NULL THEN
        N' AND [Manufacturer] LIKE @Manufacturer' ELSE N'' END
        + CASE WHEN @Category IS NOT NULL THEN
        N' AND [Category] LIKE @Category' ELSE N'' END
        + CASE WHEN @Subcategory IS NOT NULL THEN
        N' AND [Subcategory] LIKE @Subcategory' ELSE N'' END
        + CASE WHEN @JobNum IS NOT NULL THEN
        N' AND [Part_ID] IN (SELECT jp.part_id FROM job_parts AS jp INNER JOIN jobs AS j ON jp.job_id = j.job_id WHERE j.job_number = '''
         + @JobNum + ''')' ELSE N'' END

    DECLARE @params NVARCHAR(MAX) = N'
        @PartID       INT,
        @PartNum      VARCHAR(50),
        @Description  VARCHAR(MAX),
        @Information  VARCHAR(MAX),
        @Supplier     VARCHAR(75),
        @Manufacturer VARCHAR(50),
        @Category     VARCHAR(50),
        @Subcategory  VARCHAR(50)';

    EXEC sys.sp_executesql @SQL, @params,
        @PartID,
        @PartNum,
        @Description,
        @Information,
        @Supplier,
        @Manufacturer,
        @Category,
        @Subcategory
END
GO

However this doesn't exactly work all that well for me anymore. I'm at the point where I need to add more tables to the view, and more searches to the kitchen sink, and the 'default' table is just a mess of endless repeat entries that makes the whole thing look a mess.

This is essentially what I need to do:

  1. Have a default View that shows a clean table, where each single Part Number is only shown once.

  2. Be able to have one (or more) stored procedures that let me look through all the entries that could be on the table (including the repeat ones I don't want shown) in order to show specific search data.

  3. Have it set up in a way where I could expand on it further in the future without it being a gigantic headache.

To give an example of what I want, I'll give you a small example data set.

Part Number        | Job Number        |  Price
AA.445               P0001                $100.00
AA.445               P0002                $100.00
AB.123               P0001                $75.00
AB.123               P0001                $50.00
AC.999               P0002                $25.00

I'd want the default table to appear as follows:

Part Number        | Job Number        |  Price
AA.445               P0002                $100.00
AB.123               P0001                $50.00
AC.999               P0002                $25.00

If I were to search by all parts for the P0002 Job Number, I'd want the table to appear like so:

Part Number        | Job Number        |  Price
AA.445               P0002                $100.00
AC.999               P0002                $25.00

The sample size is probably too small to get across what I am trying to do. Hopefully I've conveyed what it is I am trying to do though.

Now I am not asking someone to give me a full on SQL setup for all of this, but I am not sure how I am suppose to be formatting something like this, or even where to get started with giving this a better setup. That's mainly what I am looking for a little bit of help on.

Best Answer

First thing... Nice job! Most people make a complete mess of this sort of thing. What you have is actually impressive.

There are a few things that you can do to make life a little easier.

1) lose the view. It's an unnecessary layer of abstraction at best and a good way to trick the optimizer into a bad plan at worst.

2) add a "@DeBug" parameter that allows you the option to print the generated sql rather than executing it. You'll be glad you have it the next time you need to track down an odd-ball error caused by a misplaced comma in a sea of red dynamic text. (I recommend this for all dynamic sql)

3) dynamic sql can end up being fairly complex and difficult to debug. Do yourself (and anyone else who may need to alter this proc in the future) and format the code so that the @DeBug output is properly formatted and easy to read.

So... Based on the above, here's how I'd do it... (Note that my own formatting preferences are reflected to some degree... You should change based on your own standards.)

SET QUOTED_IDENTIFIER ON
GO
SET ANSI_NULLS ON
GO

CREATE PROCEDURE dbo.PartSearch
/* ==============================================================================
2018-11-26 Your Name,  Created for blah blah blah...
============================================================================== */
/* sample execution... makes debugging dynamic SQL MUCH easier.
EXEC dbo.PartSearch 
    @PartID       = 3333,
    @PartNum      = 'AB.123',
    @Description  = NULL,
    @Information  = NULL,
    @Supplier     = NULL,
    @Manufacturer = NULL,
    @Category     = NULL,
    @Subcategory  = NULL,
    @JobNum       = NULL,
    @DeBug        = 1;
*/

    @PartID       INT          = NULL,
    @PartNum      VARCHAR(50)  = NULL,
    @Description  VARCHAR(MAX) = NULL,
    @Information  VARCHAR(MAX) = NULL,
    --@Supplier     VARCHAR(75)  = NULL,    -- column not in view...
    @Manufacturer VARCHAR(50)  = NULL,
    @Category     VARCHAR(50)  = NULL,
    @Subcategory  VARCHAR(50)  = NULL,
    @JobNum       VARCHAR(20)  = NULL,
    @DeBug        BIT          = 0
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @sql NVARCHAR(MAX) = CONCAT(N'
    SELECT
        P.part_id AS [Part_ID],
        P.part_number AS [Part_Number],
        P.part_description AS [Description],
        P.information AS [Information],
        m.manufacturer_name AS [Manufacturer],
        ca.category_description AS [Category],
        su.subcategory_description AS [Subcategory],
        cu.currency_type AS [Currency],
        xpl.[Price],
        xpl.[Discount]
    FROM
        dbo.parts AS P
        JOIN dbo.subcategories AS su
            ON P.subcategory_id = su.subcategory_id
        JOIN dbo.manufacturers AS m
            ON P.manufacturer_id = m.manufacturer_id
        JOIN dbo.categories AS ca
            ON su.category_id = ca.category_id
        JOIN dbo.currencies AS cu
            ON pl.currency_id = cu.currency_id
        CROSS APPLY (
            SELECT TOP (1)
                pl.cost_per_unit AS [Price],
                pl.discount_percentage AS [Discount]
            FROM 
                dbo.price_logs AS pl
            WHERE 
                P.part_id = pl.part_id
            ORDER BY
                pl.log_date DESC
            ) xpl
    WHERE 1 = 1',
        CASE WHEN @PartID IS NULL THEN N'' ELSE N' 
        AND P.part_id = @PartID' 
        END,
        CASE WHEN @PartNum IS NULL THEN N'' ELSE N'
        AND P.part_number LIKE @PartNum'
        END,
        CASE WHEN @Description IS NULL THEN N'' ELSE N'
        AND P.part_description LIKE @Description' 
        END,
        CASE WHEN @Information IS NULL THEN N'' ELSE N' 
        AND P.information LIKE @Information'
        END,
        --CASE WHEN @Supplier IS NULL THEN N'' ELSE N'  -- column not in view...
        --AND ?.Supplier LIKE @Supplier'
        --END,
        CASE WHEN @Manufacturer IS NULL THEN N'' ELSE N'
        AND m.manufacturer_name LIKE @Manufacturer'
        END,
        CASE WHEN @Category IS NULL THEN N'' ELSE N'
        AND ca.category_description LIKE @Category'
        END,
        CASE WHEN @Subcategory IS NULL THEN N'' ELSE N'
        AND su.subcategory_description LIKE @Subcategory'
        END,
        CASE WHEN @JobNum IS NULL THEN N'' ELSE N'
        AND EXISTS (
                    SELECT 1 
                    FROM 
                        dbo.job_parts AS jp 
                        JOIN dbo.jobs AS j 
                            ON jp.job_id = j.job_id 
                    WHERE
                        P.part_id =  jp.part_id 
                        j.job_number = @JobNum
                    )'
        END,
        N';');

    IF @DeBug = 1
    BEGIN 
        PRINT(@sql);
    END;
    ELSE 
    BEGIN
        EXEC sys.sp_executesql @SQL, @params,
            @PartID,
            @PartNum,
            @Description,
            @Information,
            @Supplier,
            @Manufacturer,
            @Category,
            @Subcategory;
    END;
END;
GO

Hope this helps. Let me know if you have any follow up questions...