Use Boolean algebra in tsql to avoid CASE statement or deal complex WHERE conditions

♀尐吖头ヾ 提交于 2019-12-04 12:35:45

My first reaction would be to defend the use of the case clause in this case. But if you are absolutely not allowed to use it, maybe you could simply add a table with the month and increment values:

LUMonthIncrement

Month   Increment
 1      1  
 2      0  
 3      1  
 4      0  
 5      0  
 6      0  
 7      1  
 8      0  
 9      0  
10      0  
11      0  
12      1  

Then you can join in that table and just add the increment:

Select LUEmployee.*,
    YEAR(joiningDate) + LUMonthIncrement.Increment as derivedYear
from LUEmployee
    join LUMonthIncrement on MONTH(LUEmployee.joiningDate) = LUMonthIncrement.Month

This is unlikely to be much more performant though, because in order to join to LUMonthIncrement the MONTH(LUEmployee.joiningDate) expression must be evaluated for each row in the LUEmployee table.

In this specific case you could do a UNION as you got 2 distinct subsets of your input set that don't depend on each other and the split criteria are well defined. So you could do something like:

Select *,
YEAR(joiningDate) + 1 as derived_year 
from LUEmployee
WHERE MONTH(joiningDate) = 1 OR MONTH(joiningDate) = 3 OR MONTH(joiningDate) = 7 OR MONTH(joiningDate) = 12

UNION 

Select *,
YEAR(joiningDate) as derived_year 
from LUEmployee
WHERE NOT (MONTH(joiningDate) = 1 OR MONTH(joiningDate) = 3 OR MONTH(joiningDate) = 7 OR MONTH(joiningDate) = 12)

Taking @user1429080's concept of a Month table one step farther, and turn it into a range table; this will allow for the elimination of the call to MONTH() in the join. Assuming you have a Calendar table (which are stupid useful), you can build the query like this:

WITH LUMonthIncrement AS (SELECT month, increment
                          FROM (VALUES (1, 1),
                                       (2, 0),
                                       (3, 1),
                                       (4, 0),
                                       (5, 0),
                                       (6, 0),
                                       (7, 1),
                                       (8, 0),
                                       (9, 0),
                                       (10, 0),
                                       (11, 0),
                                       (12, 1)) m(month, increment))  

SELECT LUEmployee.empId, LUEmployee.name, LUEmployee.joiningDate, IncrementRange.year
FROM LUEmployee
JOIN (SELECT Calendar.calendarDate AS rangeStart, 
             DATEADD(month, 1, Calendar.calendarDate) AS rangeEnd,
             Calendar.year + LUMonthIncrement.increment AS year
      FROM Calendar
      JOIN LUMonthIncrement
        ON LUMonthIncrement.month = Calender.month
      WHERE Calendar.dayOfMonth = 1) IncrementRange
  ON LUEmployee.joiningDate >= IncrementRange.rangeStart
     AND LUEmployee.joiningDate < IncrementRange.rangeEnd

(Untested at the moment)

Yes, I'm still using an index-ignoring function (specifically, DATEADD(...)) - however, the subquery reference is likely to execute first, and will return 12 rows per-year, and the join to LUEmployee is free to use any index on that table (which is likely to be far larger than the result of the subselect). Assuming Calendar has an index starting with dayOfMonth (it's a dimension table, it should...), IncrementRange should be built instantaneously.

(Note that I'm using a general range form here, which will be useful when dealing with types with a time portion attached. This is handy for things like aggregating sales by month... If you're using 2012 with a strict date type, you could potentially just straight join to the Calendar table directly on the date value, and skip dealing with the range.)

If you want to use bit logic here is a way

SELECT [empId], [name], [joiningDate]
     , [derived Year]
     = YEAR(joiningDate)
     + (1 - cast(MONTH(joiningDate) / 8 as bit)) * (MONTH(joiningDate) % 2)
     - (cast(MONTH(joiningDate) / 5 as bit))
     * (1 - cast(MONTH(joiningDate) / 6 as bit))
     + (cast(MONTH(joiningDate) / 12 as bit))
FROM   LUEmployee

SQLFiddle demo with data expanded to have every month available

Explaining the bits

  • (1 - cast(MONTH(joiningDate) / 8 as bit)) * (MONTH(joiningDate) % 2) the first part return 1 for month (number) less then 8, the second part check the parity where 1 is odd, together they add 1 for 1,3, 5, 7; to remove the 5 we need
  • (cast(MONTH(joiningDate) / 5 as bit)) * (1 - cast(MONTH(joiningDate) / 6 as bit)) the first part return 1 for every value higher or equal 5, the second part return 1 per every value less then 6, the only intersection is 5
  • (cast(MONTH(joiningDate) / 12 as bit) return 1 only for december

With all the option here, if I were in your position, I would check them all for performance and report back to my PM with the data, I'm quite sure there is a lesson to learn.

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