views:

60

answers:

1

Hi Guys,

I've got a question about reusing table data but a view won't work in this scenario as I have a parameter that needs to be passed in. Basically this part of the system requires a travellerid to be sent to the procedure and a list of arrangers returned for that specific traveller. There are around 7 business rules that are used to determine which arrangers can be returned and they are mutually exclusive, so in order to accommodate these optional rules I have used a series of UNIONS inside a derived query. This is working well, and the performance seems good across a fairly large database, however I need to reuse these rules (UNIONS) in about 4 other parts of the system.

I initially tried to create a VIEW with these UNIONS but that didn't work due to the differing logic in each UNION and different parameter requirements, so I was thinking maybe a function could solve this issue? If I created a function that took @travellerid as a param and returned a list of arrangerid based on the business rules, would this be an ideal/fast solution? I am currently using UNION ALL and a DISTINCT in the outer query as this proved much faster than using UNION's for the uniqueness of data.

Current Procedure with business rules below (SQL Server 2008):

CREATE PROCEDURE [dbo].[getArrangersForTraveller]
   @travellerid int
AS
  DECLARE @costcentreid int
  DECLARE @departmentid int

-- Shorthand the traveller costcentre and department for use in queries below
SET @costcentreid = (SELECT costcentreid FROM traveller WHERE id = @travellerid)
SET @departmentid = (SELECT departmentid FROM traveller WHERE id = @travellerid)


SELECT DISTINCT t.id, t.firstname, t.lastname, ti.name AS title, dv.preferred
FROM traveller t

INNER JOIN title ti ON t.titleid = ti.id
     INNER JOIN

     (

            -- Get Preferred Arrangers linked to Department Groups
            SELECT dg.arrangerid as id
            FROM departmentGroup dg 
                INNER JOIN department_departmentGroup ddg 
                ON (dg.id = ddg.departmentGroupId AND ddg.departmentid = @departmentid)

            UNION ALL

            -- Get Preferred Arrangers linked to Cost Centre Groups
            SELECT cg.arrangerid as id
            FROM costCentreGroup cg 
                INNER JOIN costcentre_costCentreGroup ccg 
                ON (cg.id = ccg.costCentreGroupId AND ccg.costcentreid = @costcentreid)

            UNION ALL

            -- If Cost Centre Group has a linked department and this department matches 
            -- the travel arrangers department then return these travel arrangers as well     
            SELECT t3.id
            FROM costCentreGroup cg1

                INNER JOIN costcentre_costCentreGroup ccg1 
                ON (cg1.id = ccg1.costCentreGroupId AND ccg1.costcentreid = @costcentreid) 

                INNER JOIN traveller t3  
                ON t3.departmentid = cg1.departmentid    

            WHERE  t3.accesslevelid > 1       

            UNION ALL

            -- Get Direct linked travel arrangers      
            SELECT t1.travelarrangerid as id
            FROM   travelarranger_traveller t1
            WHERE  t1.travellerid = @travellerid

            UNION ALL

            -- Get Cost Centre linked arrangers
            SELECT tc.travelarrangerid as id
            FROM   travelArranger_costcentre tc 
            WHERE  tc.costcentreid = @costcentreid

            UNION ALL

            -- Get Department linked arrangers
            SELECT td.travelarrangerid
            FROM   travelArranger_department td 
            WHERE  td.departmentid = @departmentid

            UNION ALL

            -- Get Company flagged arrangers 
            SELECT t2.id
            FROM   traveller t2
                   INNER JOIN company c ON t2.companyid = c.id

            WHERE  t2.accesslevelid > 1       
            AND ((c.allowTravelArrangerDepartmentAccess = 1 AND t2.departmentid = @departmentid)
            OR  (c.allowTravelArrangerCostCentreAccess = 1 AND t2.costcentreid = @costcentreid))

     ) as dv ON dv.id = t.id

WHERE t.accessLevelid > 1 -- arranger or manager
AND t.isenabled = 1
ORDER BY dv.preferred DESC, t.lastname, t.firstname;
+2  A: 

I initially tried to create a VIEW with these UNIONS but that didn't work due to the differing logic in each UNION and different parameter requirements, so I was thinking maybe a Function could solve this issue? If i created a Function that took as a param @travellerid, and returned a list of arrangerid's based on the business rules, would this be an ideal/fast solution?

You're thinking procedural/OO programming, but SQL is SET based.
A function would work, but would ensure that an index could not be used when you use the function for decision criteria/etc. A non-materialized view is only slightly better; in SQL Server there's the option to use an indexed view (AKA materialized view) but they are notoriously constrained. It goes against modular programming concepts, but SQL works better the less you try to modularize it and use only what you actually need to.

I re-wrote your query, but noticed that the dv.preferred column is referenced in the outer query but isn't present in the inner one. Being that dv is a conglomerate of various tables & logic, the id value being returned isn't of any real value outside the inner query because you'd need to know which table the value came from. That said, here it is:

SELECT t.id, t.firstname, t.lastname, ti.name AS title /*, dv.preferred */
  FROM TRAVELLER t
  JOIN title ti ON t.titleid = ti.id
 WHERE (EXISTS(SELECT NULL -- Get Preferred Arrangers linked to Department Groups
                 FROM departmentGroup dg 
                 JOIN department_departmentGroup ddg ON ddg.departmentGroupId = dg.id 
                                                    AND ddg.departmentid = @departmentid
                WHERE dg.arrangerid = t.id)
    OR EXISTS(SELECT NULL -- Get Preferred Arrangers linked to Cost Centre Groups
                FROM costCentreGroup cg 
                JOIN costcentre_costCentreGroup ccg ON ccg.costCentreGroupId = cg.id 
                                                   AND ccg.costcentreid = @costcentreid
               WHERE cg.arrangerid = t.id)
    OR EXISTS(SELECT NULL -- If Cost Centre Group has a linked department and this department matches the travel arrangers department then return these travel arrangers as well     
                FROM costCentreGroup cg1
                JOIN costcentre_costCentreGroup ccg1 ON ccg1.costCentreGroupId = cg1.id 
                                                    AND ccg1.costcentreid = @costcentreid
                JOIN traveller t3 ON t3.departmentid = cg1.departmentid    
                                 AND  t3.accesslevelid > 1
               WHERE t3.id = t.id)
    OR EXISTS(SELECT NULL  -- Get Direct linked travel arrangers    
                FROM travelarranger_traveller t1
               WHERE t1.travellerid = @travellerid
                 AND t1.travelarrangerid = t.id)
    OR EXISTS(SELECT NULL -- Get Cost Centre linked arrangers
                FROM travelArranger_costcentre tc 
               WHERE tc.costcentreid = @costcentreid
                 AND tc.travelarrangerid = t.id)
    OR EXISTS(SELECT NULL -- Get Department linked arrangers
                FROM travelArranger_department td 
               WHERE td.departmentid = @departmentid
                 AND td.travelarrangerid = t.id)
    OR EXISTS(SELECT NULL -- Get Company flagged arrangers 
                FROM traveller t2
                JOIN company c ON t2.companyid = c.id
                              AND t2.accesslevelid > 1       
               WHERE (   (c.allowTravelArrangerDepartmentAccess = 1 AND t2.departmentid = @departmentid)
                      OR (c.allowTravelArrangerCostCentreAccess = 1 AND t2.costcentreid = @costcentreid))
                 AND t2.id = t.id))
   AND t.accessLevelid > 1 -- arranger or manager
   AND t.isenabled = 1
ORDER BY /*dv.preferred DESC,*/ t.lastname, t.firstname;

Using a subquery (IN, EXISTS) will alleviate the duplicates issue that comes with using joins if there are more than one child record attached to the parent.

OMG Ponies
Thanks for the quick and detailed response. Sorry i had removed that "preferred" flag from each of the UNIONs as it wasn't necessary and i had left a reference to it in the outer select. Just ignore that as you already have. With the rewrite of the query it's still effectively leaving me in the same position as before, whereby i can't reuse these rules. These 6/7 select queries which I had previously UNION'd form the core business rules and the same exact statements are used in 5 other procedures in the system which is just going to become harder to maintain should new rules be added.
Phil Rasmussen
Also i was a little confused as to the SELECT NULL from each of the EXISTS clauses? I would have thought it would be the ID being checked for existence in each subquery. So in your opinion would creating a reusable function for the purpose of joining be slower performance wise, given that up to 4000 travellers may be returned for a given arranger? Thanks!
Phil Rasmussen
@Phil Rasmussen: As I explained, you'd like to reuse the logic to which I'll restate: that's a poor foundation intended for well performing SQL. What is easy to you from a maintenance perspective, risks poor performance in reality. It's up to you if the trade-off is worth it.
OMG Ponies
@Phil Rasmussen: The SELECT clause doesn't matter in an EXISTS clause--you could use SELECT 1/0, which should fail with a mathematical error but it won't for the reason I stated.
OMG Ponies
Ok point taken on the trade offs, i might just try it and run some performance tests next to the EXISTS and UNION based subqueries and see how much of a different it makes. I guess end of the day it's about performance when it comes to SQL, though it does tend to get messy when you have to repeat the same lengthy code multiple times. Thanks again.
Phil Rasmussen
@Phil Rasmussen: You're welcome, let me know how the testing turns out.
OMG Ponies
Interesting results. After placing that initial code block with the UNIONs into a Table-valued UDF and then joining onto the traveller table using @travellerid as the param, the execution time of the query dropped by half? Initially on our production server it was taking around 1800 milliseconds (client + server processing) for a large recordset of 3500+ results, and with the new UDF solution it takes under 900 milliseconds for the exact same results!
Phil Rasmussen