Selecting employees with birthdays in given range using Oracle SQL

你说的曾经没有我的故事 提交于 2019-12-12 12:37:56

问题


For selecting birthdays between two months where FROMDATE and TODATE are some parameters in a prepared statement I figured something like this:

select
  p.id as person_id,
   ...
   ...
  where e.active = 1
        and extract(month from TODATE) >= extract(month from e.dateOfBirth)
        and extract(month from e.dateOfBirth) >= extract(month from FROMDATE)
        order by extract(month from e.dateOfBirth) DESC,
              extract(day from e.dateOfBirth) DESC

How can this be improved to work with days as well?


回答1:


There's more than one way to search date ranges in Oracle. For your scenario I suggest you turn the month and day elements of all the dates involved into numbers.

select
  p.id as person_id,
   ...
   ...
  where e.active = 1
  and to_number (to_char( e.dateOfBirth, 'MMDD')) 
      between to_number (to_char( FROMDATE, 'MMDD'))
              and to_number (to_char( TODATE, 'MMDD')) 
  order by extract(month from e.dateOfBirth) DESC,
          extract(day from e.dateOfBirth) DESC

This won't use any index on the e.dateOfBirth column. Whether that matters depends on how often you want to run the query.


@AdeelAnsari comments:

" I don't like to to_char the predicate, in order to make use of index"

What index? A normal index on dateOfBirth isn't going to be of any use, because the index entries will lead with the year element. So it won't help you find all the records of people born on any 23rd December.

A function-based index - or in 11g, a virtual column with an index (basically the same thing) - is the only way of indexing parts of a date column.




回答2:


Do you need maximum performance, and so are willing to make a change to the schema? Or are the number of records so small, and performance relatively un-important, that you want a query that will work with the data as-is?

The simplest and fastest way to do this is to store a second data-of-birth field, but 'without' the year. I put quotes around 'without' because a date can't actually not have a year. So, instead, you just base it on another year.

Re-dating every DoB to the year 2000 is a good choice in my experience. Because it includes a leap-year and is a nice round number. Every DoB and FromDate and ToDate will work in the year 2000...

WHERE
      DoB2000 >= FromDate
  AND DoB2000 <= ToDate

(This assumes you also index the new field to make the search quick, otherwise you still get a scan, though it MAY be faster than the following alternative anyway.)


Alternatively, you can keep using the EXTRACT pattern. But that will have an unfortunate consequence; it's extremely messy and you will never get an Index Seek, you will always get an Index Scan. This is because of the fact that the searched field is wrapped in a function call.

WHERE
  (   EXTRACT(month FROM e.DateOfBirth) > EXTRACT(month FROM FromDate)
      OR  (      EXTRACT(month FROM e.DateOfBirth)  = EXTRACT(month FROM FromDate)
             AND EXTRACT(day   FROM e.DateOfBirth) >= EXTRACT(day   FROM FromDate)
          )
  )
  AND
  (   EXTRACT(month FROM e.DateOfBirth) < EXTRACT(month FROM ToDate)
      OR  (      EXTRACT(month FROM e.DateOfBirth)  = EXTRACT(month FROM ToDate)
             AND EXTRACT(day   FROM e.DateOfBirth) <= EXTRACT(day   FROM ToDate)
          )
  )



回答3:


you should be able to use

SELECT * FROM mytbale
where dateofbirth between start_dt and end_dt

alternate:

you can convert dates to the day of the year using:

to_char( thedate, 'ddd' )

then check the range (note this has the same issue as @Dems answer where you should not span the end of the year as in Dec 25th through Jan 10th.)




回答4:


This is the query that I am using for birtdates in the next 20 days:

SELECT A.BIRTHDATE, 
    CEIL(ABS (MONTHS_BETWEEN(A.BIRTHDATE, SYSDATE) / 12)) AGE_NOW,  
    CEIL(ABS (MONTHS_BETWEEN(A.BIRTHDATE, SYSDATE + 20) / 12)) AGE_IN_20_DAYS
FROM USERS A
WHERE 
    CEIL(ABS (MONTHS_BETWEEN(A.BIRTHDATE, SYSDATE) / 12)) <> CEIL(ABS (MONTHS_BETWEEN(A.BIRTHDATE, SYSDATE + 20) / 12));
  • ABS (MONTHS_BETWEEN(A.BIRTHDATE, SYSDATE) / 12)) return the age in format 38.9, 27.2, etc
  • Applying ceil() will give us the difference in years that we need to determinate if this person is near to have a birthdate. Eg.

    1. Age: 29.9
    2. 29.9 + (20 days) = 30.2
    3. ceil(30.1) - ceil(29.9) = 1

This is the result of querying at december 16:

BIRTHDATE    AGE_NOW    AGE_IN_20_DAYS

12/29/1981    35           36
12/29/1967    49           50
1/3/1973      44           45
1/4/1968      49           50



回答5:


I don't know how this behaves in terms of performance, but I'd try using regular date subtraction. Say, for example, you want birth days between January 1st and July 1st. Anyone who is between 25 and 25.5 would qualify. That is, anyone whose partial age is less than 1/2 year would qualify. The math is easy at 1/2, but it is the same regardless of the window. In other words, "within one month" means +/- 1/12 of a year, within 1 day means +/- 1/365 of a year, and so on.

The year does not matter in this method, so I'll use current year when creating the variable.

Also, I would think of the center of the range, and then do absolute values (either that, or always use a future date).

For example

select personid
from mytable
where abs(mod(dob - target_birth_date)) < 1/52

would give you everyone with a birthday within a week.

I realize this code is barely pseudocode, but is seems like it might allow you to do it, and you might still use indices if you tweaked it a little. Just trying to think outside the box.




回答6:


At the end we picked litter different solution where we add fist create the anniversary date :

where 
...
and (to_char(sysdate,'yyyy') - to_char(e.dateofbirth,'yyyy')) > 0
and add_months(e.dateofbirth,
              (to_char(sysdate,'yyyy') - to_char(e.dateofbirth,'yyyy')) * 12)
              >:fromDate:
and :toDate: > add_months(e.dateofbirth,
              (to_char(sysdate,'yyyy') - to_char(e.dateofbirth,'yyyy')) * 12)
order by extract(month from e.dateofbirth) DESC,
              extract(day from e.dateofbirth) DESC)



回答7:


That's the solution!!!

SELECT                                  
    *                           
FROM                                    
    PROFILE  prof                          
where                        
      to_date(to_char(prof.BIRTHDATE, 'DDMM'), 'DDMM') BETWEEN sysdate AND sysdate+5

or

add_months(to_date(to_char(prof.BIRTHDATE, 'DDMM'), 'DDMM'),12) BETWEEN sysdate AND sysdate+5  



回答8:


Guys I have a simpler solution to this problem

  • step 1. convert the month into number,
  • step 2. concatenate the day(in two digits) to the month
  • step 3. then convert the result to number by doing to_number(result)
  • step 4. Once you have this for the start date and end date, then do a between function on it and u are done.

sample code:

SELECT      date_of_birth            
       FROM xxxtable
       where to_number(to_number(to_char(to_date(substr(date_of_birth,4,3),'mon'),'mm'))||substr(date_of_birth,1,2)) between
        to_number(to_number(to_char(to_date(substr(:start_date_variable,4,3),'mon'),'mm'))||(substr(:start_date_variable,1,2)))  and  
        to_number(to_number(to_char(to_date(substr(:end_date_variable,4,3),'mon'),'mm'))||(substr(:end_date_variable,1,2)));


来源:https://stackoverflow.com/questions/9861529/selecting-employees-with-birthdays-in-given-range-using-oracle-sql

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