How does date manipulation/comparison/grouping work in SQL queries? - sql

I need to analyze an SQL query (and construct its equivalent in MDX). I'm not familiar with SQL and can't access the database, so there are 5 simple things I can't figure out:
What does the part WHERE idate BETWEEN trunc(SYSDATE, 'iw')-7 AND trunc(SYSDATE, 'iw')-3 mean? Specifically:
What does subtracting 7 from trunc(SYSDATE, 'iw') do? Subtract 7 weeks or 7 days? I understand the trunc(...) expression is a value 0-53 corresponding to the week of the year, but it seems to clash with the label "previous week" and stated purpose of the query.
How does SQL compare dates? Are the values from trunc(...) evaluated as dates during comparison?
The query seems to group rows together if they happened in the same minute. However, the few rows of output I can see have 10-minute granularity (00:00, 00:10, 00:20, etc.) Is there something in the query that groups rows into 10 minute intervals, or is this a result of the input data?
Why are calls to substr() and to_char() and needed in the group by condition? What would happen if trunc(idate, 'HH24:MI') was used instead?
What does the pm do? There is also a cm that seems to have a similar function. Are these part of the temporary table names?
Finally, how do the hash marks (#) affect this query? I read it might be to signify temporary tables. If so, are these temporary tables created manually, or does something in the query cause them to be created?
For reference here is the query. (On a Oracle database, if it makes any difference.) Its purpose is to "analyze how firewall accept events are trending compared to last week":
SELECT 'Previous Week Average' AS term ,
Substr(To_char(idate, 'HH24:MI'), 0, 4)
|| '0' AS event_time ,
Round(Avg(tot_accept)) AS cnt
FROM (
SELECT *
FROM st_event_100_#yyyymm-1m#
WHERE idate BETWEEN trunc(SYSDATE, 'iw')-7 AND trunc(SYSDATE, 'iw')-3 #stat_monitor_group_query#
UNION ALL
SELECT *
FROM st_event_100_#yyyymm#
WHERE idate BETWEEN trunc(SYSDATE, 'iw')-7 AND trunc(SYSDATE, 'iw')-3 #stat_monitor_group_query# ) pm
GROUP BY substr(to_char(idate, 'HH24:MI'), 0, 4)
|| '0'
UNION ALL
SELECT 'Today' AS term ,
substr(to_char(idate, 'HH24:MI'), 0, 4)
|| '0' AS event_time ,
round(avg(tot_accept)) AS cnt
FROM st_event_100_#yyyymm# cm
WHERE idate >= trunc(SYSDATE) #stat_monitor_group_query#
GROUP BY substr(to_char(idate, 'HH24:MI'), 0, 4)
|| '0'
ORDER BY term DESC,
event_time ASC

iw truncates the date to the first day of the calendar week as defined by the ISO 8601 standard, which is Monday. When you subtract numbers from the date, it is always the number of days. So, idate BETWEEN trunc(SYSDATE, 'iw')-7 AND trunc(SYSDATE, 'iw')-3 gives you those dates that fall between previous week's Monday and Friday.
to_char(idate, 'HH24:MI') gives you the time(hour and minute) part in 24hr format. Ex: 14:33. By using substrin to extract only 4 characters, you are actually getting 14:3. So yes, this groups with a granularity of 10 mins.
You cannot write trunc(idate, 'HH24:MI'). It can only have 1 precision specifier.
If you write trunc(idate,'HH24'), it truncates to the hour. If you use MI, it truncates to the minute. So, to truncate it to 10 mins is a little tricky.
pm is just an alias for the whole subquery.
SELECT *
FROM st_event_100_#yyyymm-1m#
......
WHERE idate BETWEEN trunc(SYSDATE, 'iw')-7 AND trunc(SYSDATE, 'iw')-3 #stat_monitor_group_query#
# is part of the table anme in your query. It has no significance as such. But, it might be project/company specific.

Related

Query with CASE WHEN / LAST_DAY is not giving any output

Running below query and not getting the output. Can someone please tell whats wrong in it?
Select distinct (table.datex)
from table
where table.datex =
(
CASE when extract( day from sysdate) >=19
then last_day(add_months(sysdate, -1))
else last_day(add_months(sysdate, -2))
END
)
Sample data
Datex
ID
30-JUN-21
A
31-MAY-21
B
29-JUN-21
C
Expected result
Datex
30-JUN-21
When I am passing the value hard-coded(calculated by the case) to where clause it's working fine, but when I apply the case it's not working. No error. No output is coming.
Date or datetime?
Oracle's LAST_DAY doesn't do what the name suggests, and the docs (https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/LAST_DAY.html#GUID-296C7C02-7FB9-4AAC-8927-6A79320CE0C6) fail to explain that, too.
Unlike several other DBMS Oracle doesn't have a date type. It only has a datetime type and they even call that inappropriately DATE. This means that a "date" in Oracle always has a time part. A date with its time part set to 00:00:00 can be considered a day's midnight (i.e. the very beginning of the day) or the whole day.
The function SYSDATE gives us a date in the sense of the DATE datatype, not in the sense of a real day, i.e. it gives us the datetime of "now", e.g. 2021-07-20 14:38:00. ADD_MONTHS changes the month in that datetime (and sometimes the year and sometimes even the day), i.e. leaves the time part untouched. LAST_DAY, too, changes the date part to get to the last day of the month, but leaves the time part untouched.
Your CASE expression hence results in something like TIMESTAMP '2021-07-20 14:38:00' and not in DATE '2021-07-20' as one might expect.
You say that you tried your query with the date you computed wth your case expression, and it worked. Did you compute the resulting day in your head or with a query? If the latter: The tool you are using may be set to only display a datetime's date part and omit the time part. This would explain why you only saw 30-JUN-21 when checking the CASE expression.
Solution
Truncate the datetime down to a whole day
Select distinct datex
from mytable
where (extract(day from sysdate) >=19 and datex = trunc(last_day(add_months(sysdate, -1))))
or (extract(day from sysdate) < 19 and datex = trunc(last_day(add_months(sysdate, -2))))
It doesn't matter whether you apply TRUNC late as in my example or right away on SYSDATE (with TRUNC(SYSDATE)) by the way. The only aim is to get rid of the time part at some point in the expression.
Don't use case in where clauses. Boolean logic can handle that.
And take a look if it is really the condition you want
Select distinct datex
from your_table
where
(
extract(day from sysdate) >=19
and datex = last_day(add_months(sysdate,-1))
)
or
(
extract(day from sysdate) < 19
and datex = last_day(add_months(sysdate,-2))
)

Why am I getting different results when comparing dates two ways?

This query:
select count(*), trim(data_date)
from man
where data_status = 'received' and data_date > sysdate-7
group by trim(data_date);
gives result like:
199 05-APR-16
But this query:
select count(*), trim(data_date)
from man
where data_status = 'received' and trunc(data_date) = date '2016-04-05'
group by trim(data_date);
gives results like:
347 05-APR-16
Why are the queries giving different results for the same day?
Because your man_date_sub values are not all at midnight. If you keep running the first query the number of records returned will (probably) gradually reduce. That is only happening to the count for the 5th as that is a week ago. Your sysdate - 7 is a moving target, not just as you move from day to day, but as time passes during the day.
You can check the times with:
select to_char(man_date_sub, 'YYYY-MM-DD HH24:MI:SS'),
to_char(sysdate - 7, 'YYYY-MM-DD HH24:MI:SS'),
man_date_sub - (sysdate - 7)
from man
where trunc(man_date_sub) = date '2016-04-05';
You'll see that some have times before the current sysdate time, while others have times after it. The third, generated, column will show some positive and some negative values.
In your second query you're comparing trunc(man_date_sub), which sets the time part to midnight, with date '2016-04-05', which is also at midnight; so all the records at any time on that day now match.
You can go back to midnight on your 7-day range, and get an equivalent result, by truncating sysdate:
select count(*), trim(man_date_sub)
from man
where man_status = 'SUBMITTED' and man_date_sub > trunc(sysdate)-7
group by trim(man_date_sub);
Your use of the trim() function is a bit odd; all you're doing is removing leading and trailing whitespace from the string '05-APR-16', which isn't actually doing anything. You're also relying on implcit conversion of the date to a string using your session NLS_DATE_FORMAT. It would be better to specify the format:
select count(*), to_char(man_date_sub, 'YYYY-MM-DD')
from man
where man_status = 'SUBMITTED' and man_date_sub > trunc(sysdate)-7
group by to_char(man_date_sub, 'YYYY-MM-DD');
If you ran your original query in a session that had an NLS_DATE_FORMAT that included time elements then you wouldn't get the result you expect.
I'm not sure if you're confusing it with trunc(), though clearly you're using that elsewhere. Truncating a date sets the time portion to midnight (by default; it can do other things), but leaves it as a date, which would be suitable for grouping but should still be formatted explicitly for display.

How to convert a sysdate month value to number in oracle?

Im trying to return the CARDS of my CARD table that will expire in the next month. But the problem is that the table has two columns to represent the card date. The columns are EXPIREDAY and EXPIREMONTH ,both are numbers. So when i do that query i get an error:
select * from CARD WHERE EXPIREDAY <= sysdate - interval '2' DAY;
//Oracle error: ORA-00932: inconsistent datatypes: expected NUMBER got DATE
Is there a way to convert the sysdate - interval '2' DAY as Number data type?
Thanks!
If you want to compare the values as strings you can use this to convert the SYSDATE
SELECT TO_CHAR(sysdate, 'MM') || TO_CHAR(sysdate, 'DD') MONTH_NUM FROM DUAL
-- gives you "0922"
and this for your numeric columns which will pad with leading zeros if you only have a single digit
SELECT TO_CHAR(9, 'FM00') || TO_CHAR(22, 'FM00') MONTH_NUM FROM DUAL
-- also gives you "0922"
If you have control over the table schema it would be best practise to store both the DAY and MONTH values in a single numeric field, so that 9-SEP would be stored in this column as the numeric value 0922 where the month is first so that the natural ordering is used.
A simple and not necessarily very efficient approach is to convert the day and month values into an actual date, using to_date(), and then compare that with your target date range:
select * from card
where to_date(lpad(expireday, 2, '0')
||'/'|| lpad(expiremonth, 2, '0'), 'DD/MM')
between sysdate and add_months(sysdate, 1);
Which appears to work. But this will have problems if the dates span the end of the year. Because your table doesn't specify the year, you either have to work one out, or allow to_date to default it to the current year. And if you let it default then it won't work. For example, if you have values for December and January in your table, and run this query in December, then the January dates will be seen as January 2014, and won't be counted as being in the next month. So you'll need to do more to pick the right year.
This treats any month numbers before the current one as being next year, which may be good enough for you as you only have a one-month window:
select * from card
where to_date(lpad(expireday, 2, '0')
||'/'|| lpad(expiremonth, 2, '0')
||'/'|| (extract(year from sysdate) +
case when expiremonth < extract(month from sysdate) then 1 else 0 end),
'DD/MM/YYYY')
between sysdate and add_months(sysdate, 1);
SQL Fiddle using a date range from December to January.
And you can see the ways the two columns are being combined to form a date in this Fiddle.
As so often, the moral is... store things as the right data type. Store dates as dates, not as string or numbers.
Im trying to return the CARDS of my CARD table that will expire in the next month. But the problem is that the table has two columns to represent the card date.
Assuming:
you are using floating months (say: from 23 dec. to 23 jan.) and
your table somehow only contains one (floating ?) year of data
Why can't you use simple arithmetics? Like that:
-- some constant definitions for testing purpose
with cst as (
select EXTRACT(DAY from TO_DATE('23/12','DD/MM')) as theDay,
EXTRACT(MONTH from TO_DATE('23/12','DD/MM')) as theMonth
from dual)
-- the actual query
select card.* from card,cst
where (expiremonth = theMonth AND expireday > theDay)
or (expiremonth = 1+MOD(theMonth,12) AND expireday <= theDay);
-- ^^^^^^^^^^^^^^^^^^
-- map [01 .. 12] to [02 .. 12, 01] (i.e.: next month)
This will simply select all "pseudo-dates" from tomorrow to the end of the month, as well as any one before (and including) the current day# next month.
See this example.
For something a little bit more generic, but probably more efficient than converting all your values TO_DATE, you might want to try that:
-- the calendar is the key part of the query (see below)
with calendar as (
select extract(month from sysdate + level) as theMonth,
extract(day from sysdate + level) as theDay
from DUAL connect by ROWNUM <= 8)
-- ^
-- adjust to the right number of days you are looking for
select card.* from card join calendar
on expiremonth = theMonth and expireDay = theDay
The idea here is to simply build a calendar with all the upcoming days and then join your data table on that calendar. See an example here.
Try using to_char(sysdate - interval '2' DAY,'ddmmyyyy') to convert to character type. The date format('ddmmyyyy') will depend of the value of expiredate

Timestamp data type query in Oracle

I have a table called CYCLING_ACCIDENTS_2 containing a TIMESTAMP(6) column called ACC_DATE_TIME , this is an example of how the date is stored 31-MAY-12 16.45.00.000000, I would like to know how I can query just the time in such a date format so that I can have a time interval for all years I have (2005-2012) but just restricted to certain times in the day. I tried many functions but all I've got so far are syntax errors, I tried to search on the web but I can' t see anything appropriate to my case. Could anyone help?
Thanks!
First of all, a timestamp is a number, not a string. So the date is displayed by default as 31-MAY-12 16.45.00.000000, but it is actually the amount of microseconds since 1970 I believe.
If you want to select just the time part use to_char()
select to_char(acc_date_time, 'hh24:mi') time
, count(*) occurences
from cycling_accidents_2
group by to_char(acc_date_time, 'hh24:mi')
edit: I think this second query actually answers your question:
select *
from cycling_accidents_2 ca
where to_char(ca.acc_date_time, 'hh24:mi') between '10:00' and '18:00'
and ca.acc_date_time >= to_timestamp('01-01-2005', 'dd-mm-yyyy')
and ca.acc_date_time < to_timestamp('01-01-2013', 'dd-mm-yyyy')
SELECT * FROM CYCLING_ACCIDENTS_2 WHERE
(EXTRACT(YEAR FROM ACC_DATE_TIME) BETWEEN 2005 AND 2012)
AND
(EXTRACT(HOUR FROM ACC_DATE_TIME) BETWEEN 10 AND 18)

Get day of week from time period

I have two date from user input. I want to get from jsp these two dates and create a view which shows each day with their own weekday:
2013-3-1 MONDAY
2013-3-2 TUESDAY
2013-3-3 WEDNESDAY
User inputs date and gives to jsp, say:
where start='2013-3-1' and end='2013-3-3'
How to do this?
This can be simpler:
SELECT to_char(d, 'YYYY-MM-DD') AS day_txt
, to_char(d, 'FMDay') AS weekday
FROM generate_series('2013-03-01'::date
, '2013-03-03'::date
, interval '1 day') d;
More details under this later question:
Remove blank-padding from to_char() output
Generally it is better to operate with actual date types, not with text representations, but I did not understand the details or your requirements.
Try this:
select to_char(dt, 'yyyy-mm-dd') as date, to_char(dt, 'Day') as day
from (select ('2013-03-01'::date + i) dt
from generate_series(0,'2013-03-03'::date - '2013-03-01'::date) as t(i)) as t;
You should parameterize the date part of query.
There are a couple of points worth remember in this query.
date calculation: subtract two date become the number of dates between two dates.
generate_series(0, n) generates 0, 1, ... n