Combining dates fields from separate tables - sql

I have two tables that are almost exactly the same. The only difference is one is an archive table (call that B) that has any records removed from the other table (call that A)
I'm needing to get ALL records from a given data range, thus I need to join the two tables (and actually join them to a third to get a piece of information that's not on those tables, but that doesn't affect my problem).
I'm wanting to group by the hour the record comes from (i.e. trunc(<date_field>, 'hh')
However, since I need to get records from each hour from the two tables it seems I would need to generate a single date field to group on, otherwise the group wouldn't make sense; each record will only have a date from one field so if I group by either table's date field it would inherently omit records from the other, and if I group by both I'll get no data back as no record appears in both tables.
SO, what I want to do is add two "dates" and have it work like it would in Excel (i.e. the dates get treated as their numeric equivalent, get added, and the resultant date is returned, which by the way is at least one case where adding dates is valid, despite this thread's opinion otherwise)
This makes even more sense as I'll be replacing the null date value with 0 so it should functionally be like adding a number to a date (12/31/14 + 1 = 1/1/15).
I just haven't been able to get it to work. I've tried several iterations to get the calculation to work the latest being:
SELECT DISTINCT Avg(NVL(to_number(to_char(trunc(fcr.actual_start_date, 'hh')))*86400, 0) + NVL(to_Number(to_char(trunc(acr.actual_start_date, 'hh')))*86400, 0)) Start_Num, SUM(AA.SESSIONCPU) TotalCPU, Count(1) Cnt
, SUM((NVL(to_number(to_char(trunc(fcr.actual_completion_date, 'hh')))*86400, 0) + NVL(to_Number(to_char(trunc(acr.actual_completion_date, 'hh')))*86400, 0)
- NVL(to_number(to_char(trunc(fcr.actual_start_date, 'hh')))*86400, 0) - NVL(to_Number(to_char(trunc(acr.actual_start_date, 'hh')))*86400, 0))) TotRun
FROM PSTAT.A$_A AA
LEFT OUTER JOIN APPL.FND_CR FCR On FCR.O_SES_ID = AA.SEsID
LEFT OUTER Join XX.E_FND_CR ACR on ACR.O_SES_ID = aa.sesid
WHERE (trunc(fcr.actual_start_date) >= to_date('28-Dec-2014', 'DD-MON-YYYY')
Or trunc(acr.actual_start_date) >= to_date('28-Dec-2014', 'DD-MON-YYYY'))
AND rownum <= 1048500
and (acr.status_code = 'C' or fcr.status_Code = 'C')
AND aa.sessioncpu is not null
GROUP BY to_number(NVL(trunc(fcr.actual_start_date, 'hh'), 0))*86400 + to_Number(NVL(trunc(acr.actual_start_date, 0), 'hh'))*86400
ORDER BY 2, 1;
My explicit problem with the code above is that Toad keeps ignoring the casts and says it is expecting a date value when it gets a number (the 0 gets highlighted). So if someone could:
A) Tell my why Toad would ignore the casts (it should be seeing a number and so should have absolutely no expectation of a date)
B) Provide any suggestions on how to get the addition to work, or failing that suggest an alternative route to combine the three tables so that I'm able to group by the start date values
As always, any help is much appreciated.

Adding dates or casting them to number throws ORA-00975: date+date not allowed and ORA-01722: invalid number.
So what can be done here to operate on dates in Excel way? My idea is to substract first day from calendar to_date(1, J) from each date you want to operate on.
Example with test dates:
with test_data as (
select sysdate dt from dual union all
select to_date(1, 'J') from dual union all
select null from dual )
select nvl(trunc(dt, 'hh') - to_date(1, 'J'), 0) num_val, dt,
to_char(dt, 'J') tc1, to_char(dt, 'yyyy-mm-ss hh24:mi:ss') tc2
from test_data
NUM_VAL DT TC1 TC2
---------- ---------- ------- -------------------
2457105,96 2015-03-24 2457106 2015-03-14 23:12:14
0 4712-01-01 0000001 4712-01-00 00:00:00
0

#David, your suggestion seems to have worked like charm. For those who come along afterwards my code as updated follows:
SELECT trunc(cr.actual_start_date, 'hh') Start_Date, SUM(AA.SESSIONCPU) TotalCPU,
Count(1) Cnt, SUM((cr.Actual_Completion_Date - cr.Actual_Start_Date)*86400) TotalRun
FROM (SELECT Actual_Start_Date, Actual_Completion_Date, Oracle_Session_ID, Status_Code
FROM APPL.FND_CR
UNION ALL
SELECT Actual_Start_Date, Actual_Completion_Date, Oracle_Session_ID, Status_Code
FROM XX.E_FND_CR) cr
RIGHT OUTER JOIN PSTAT.A$_A AA ON cr.Oracle_Session_ID = AA.SessionID
WHERE trunc(cr.actual_start_date) >= to_date('28-Dec-2014', 'DD-MON-YYYY')
AND rownum <= 1048500
and cr.status_code = 'C'
GROUP BY trunc(cr.actual_start_date, 'hh')
ORDER BY 1;

Related

want to substract two dates from two different tables, getting syntax error

select to_date(to_char(MIN (logical_date), 'YYYYMMDD'), 'YYYYMMDD')from table_1
- to_date(to_char(MIN (due_date) ,'YYYYMMDD'),'YYYYMMDD') FROM table_2
You could subtract the results of two subqueries, each of which gets the minimum date from one of the tables; with the overall query run against dual (the built-in single-row table that's quite useful for this sort of thing):
-- CTEs for your sample data
with table_1 (logical_date) as (select date '2019-05-01' from dual),
table_2 (due_date) as (select date '2019-05-15' from dual)
-- actual query
select (select to_date(to_char(min(logical_date), 'YYYYMMDD'), 'YYYYMMDD') from table_1)
- (select to_date(to_char(min(due_date) ,'YYYYMMDD'),'YYYYMMDD') from table_2)
as diff
from dual;
DIFF
----------
-14
But you don't need to convert to and from strings, you can just do:
select (select min(logical_date) from table_1) - (select min(due_date) from table_2) as diff
from dual;
unless your dates have non-midnight time components, in which case you'll get a fractional number of days in your result; to get whole days only either round/trunc/floor/ceil the result, or use trunc() to set both time components to midnight before you subtract - which you do depends on how you want to handle those fractional days.
If you're expecting that difference to be -15, then subtract one from the result. If you expect a positive value then reverse the order of the subqueries, and add one instead.

map/sample timeseries data to another timeserie db2

I am trying to combine the results of two SQL (DB2 on IBM bluemix) queries:
The first query creates a timeserie from startdate to enddate:
with dummy(minute) as (
select TIMESTAMP('2017-01-01')
from SYSIBM.SYSDUMMY1 union all
select minute + 1 MINUTES
from dummy
where minute <= TIMESTAMP('2018-01-01')
)
select to_char(minute, 'DD.MM.YYYY HH24:MI') AS minute
from dummy;
The second query selects data from a table which have a timestamp. This data should be joined to the generated timeseries above. The standalone query is like:
SELECT DISTINCT
to_char(date_trunc('minute', TIMESTAMP), 'DD.MM.YYYY HH24:MI') AS minute,
VALUE AS running_ct
FROM TEST
WHERE ID = 'abc'
AND NAME = 'sensor'
ORDER BY minute ASC;
What I suppose to get is a query with one result with contains of two columns:
first column with the timestamp from startdate to enddate and
the second with values which are sorted by there own timestamps to the
first column (empty timestamps=null).
How could I do that?
A better solution, especially if your detail table is large, is to generate a range. This allows the optimizer to use indices to fulfill the bucketing, instead of calling a function on every row (which is expensive).
So something like this:
WITH dummy(temporaer, rangeEnd) AS (SELECT a, a + 1 MINUTE
FROM (VALUES(TIMESTAMP('2017-12-01'))) D(a)
UNION ALL
SELECT rangeEnd, rangeEnd + 1 MINUTE
FROM dummy
WHERE rangeEnd < TIMESTAMP('2018-01-31'))
SELECT Dummy.temporaer, AVG(Test.value) AS TEXT
FROM Dummy
LEFT OUTER JOIN Test
ON Test.timestamp >= Dummy.temporaer
AND Test.timestamp < Dummy.rangeEnd
AND Test.id = 'abc'
AND Test.name = 'text'
GROUP BY Dummy.temporaer
ORDER BY Dummy.temporaer ASC;
Note that the end of the range is now exclusive, not inclusive like you had it before: you were including the very first minute of '2018-01-31', which is probably not what you wanted. Of course, excluding just the last day of a month also strikes me as a little strange - you most likely really want < TIMESTAMP('2018-02-01').
found a working solution:
with dummy(temporaer) as (
select TIMESTAMP('2017-12-01') from SYSIBM.SYSDUMMY1
union all
select temporaer + 1 MINUTES from dummy where temporaer <= TIMESTAMP('2018-01-31'))
select temporaer, avg(VALUE) as text from dummy
LEFT OUTER JOIN TEST ON temporaer=date_trunc('minute', TIMESTAMP) and ID='abc' and NAME='text'
group by temporaer
ORDER BY temporaer ASC;
cheers

SQL many sysdate in a query

I have a big query with many sysdate, everytime i must check for some sysdate ( +1 +2 +3 etc) i must change in every part and i lost many time, i'd like to create a variable or something on top of my query with unique sysdate who change every sysdate in the query.
this is a little part as example
SELECT DISTINCT(COD)
FROM TABLE_COD
WHERE START_DATE <= trunc(sysdate + 5)
AND END_DATE >= trunc(sysdate + 5)
AND VALUE = 1
AND (COD_REF = ( SELECT to_char(sysdate + 5, 'D') FROM dual) OR COD_REF = 0)
.......continue
If I understand your question correctly, you need something like this.
with sysdate_plus_n as
(select sysdate+1 as sysdate_plus_1 ,-- other variables
sysdate+5 as sysdate_plus_5 from dual)
--This is a temporary table. So if you query
--(select sysdate_plus_5 from sysdate_plus_n), you will get sysdate+5
--You can change your values in this temporary table
SELECT DISTINCT(COD)
FROM TABLE_COD
WHERE START_DATE <= trunc(select sysdate_plus_5 from sysdate_plus_n)
AND END_DATE >= trunc(select sysdate_plus_5 from sysdate_plus_n)
AND VALUE = 1
AND (COD_REF = ( SELECT to_char(select sysdate_plus_5 from sysdate_plus_n, 'D')
FROM dual) OR COD_REF = 0)
.......continue
Now just change values in the temporary table. You dont have to touch the query.
You can join the date:
select ...
from ...
cross join (select trunc(sysdate) + 5 as mydate from dual)
where start_date <= mydate
and end_date >= mydate
and value = 1
and cod_ref in (to_char(mydate, 'D', 'NLS_DATE_LANGUAGE=AMERICAN'), 0)
...
Please see also that I added the NLS_DATE_LANGUAGE to TO_CHAR, because otherwise the output would be session-dependent (i.e. one session may regard Sunday the first day of the week, another Monday).
Use a substitution variable i.e. & and enter the value when prompted. Thus, you will enter the value only once and it will be used simultaneously everywhere in the code.
For example,
SQL> SELECT &dt FROM dual;
Enter value for dt: SYSDATE
old 1: SELECT &dt FROM dual
new 1: SELECT SYSDATE FROM dual
SYSDATE
---------
23-DEC-15
SQL> /
Enter value for dt: SYSDATE +5
old 1: SELECT &dt FROM dual
new 1: SELECT SYSDATE +5 FROM dual
SYSDATE+5
---------
28-DEC-15
AND (COD_REF = ( SELECT to_char(sysdate + 5, 'D') FROM dual) OR COD_REF = 0)
On a side note, you don't have to use a sub-query all the time. SYSDATE is an in-built function, so you can call it directly. No need to select it from dual.
AND (COD_REF = to_char(sysdate + 5, 'D') OR COD_REF = 0)
If you want multiple days at a time, how about writing one query for the purpose? Something like this:
WITH params AS (
SELECT 5 as val FROM DUAL UNION ALL
SELECT 6 as val FROM DUAL
)
SELECT DISTINCT v.val, COD
FROM params CROSS JOIN
TABLE_COD c
WHERE START_DATE <= trunc(sysdate + params.val) AND END_DATE >= trunc(sysdate + params.val) AND
VALUE = 1 AND
(COD_REF = (to_char(sysdate + params.val, 'D') OR COD_REF = 0);
With this structure, you can add as many values as you want to the query (including only one).
As I understand, the value of SYSDATE is evaluated only once before the query is actually ran. In other words, the value of SYSDATE will stay constant throughout your complete query. Knowing this detail may greatly help or solve your question.
I do think the best solution in your case would be to create a user function to be called as many times as you want from within your select statement. Then, when you need any changes, just update the function instead.
I don't think there's a way to create a variable at the beginning of your query in order to be reused through out a single statement. You can however, use the WITH clause in order to avoid repeating embedded queries and improve performance and ease of reading. This is similar to what you need, but wanted to mention it as it might be a solution too depending on your needs.
Check out this link for more information on Oracle Functions:
https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_5009.htm
I hope this helps!
WITH FIX_SYSDATE AS
(SELECT TRUNC(SYSDATE) AS FIXED_SYSDATE
FROM DUAL)
SELECT DISTINCT(COD)
FROM TABLE_COD
CROSS JOIN FIX_SYSDATE FS
WHERE START_DATE <= FS.FIXED_SYSDATE + 5
AND END_DATE >= FS.FIXED_SYSDATE + 5
AND VALUE = 1
AND (COD_REF = (SELECT TO_CHAR(FS.FIXED_SYSDATE + 5, 'D')
FROM DUAL)
OR COD_REF = 0)
.......continue
Fix TRUNC(SYSDATE) or simply SYSDATE (I don't know the remaining of your query, since there is "... continue" there) and then use it as many times as required on your main query.

Query for dates which are not present in a table

Consider a table ABC which has a column of date type.
How can we get all the dates of a range (between start date and end date) which are not present in the table.
This can be done in PLSQL.I am searching a SQL query for it.
You need to generate the arbitrary list of dates that you want to check for:
http://hashfactor.wordpress.com/2009/04/08/sql-generating-series-of-numbers-in-oracle/
e.g.:
-- generate 1..20
SELECT ROWNUM N FROM dual
CONNECT BY LEVEL <= 20
Then left join with your table, or use a where not exists subquery (which will likely be faster) to fetch the dates amongst those you've generated that contains no matching record.
Assuming that your table's dates do not include a time element (ie. they are effectively recorded as at midnight), try:
select check_date
from (select :start_date + level - 1 check_date
from dual
connect by level <= 1 + :end_date - :start_date) d
where not exists
(select null from mytable where mydate = check_date)
Given a date column in order to do this you need to generate a list of all possible dates between the start and end date and then remove those dates that already exist. As Mark has already suggested the obvious way to generate the list of all dates is to use a hierarchical query. You can also do this without knowing the dates in advance though.
with the_dates as (
select date_col
from my_table
)
, date_range as (
select max(date_col) as maxdate, min(date_col) as mindate
from the_dates
)
select mindate + level
from date_range
connect by level <= maxdate - mindate
minus
select date_col
from the_dates
;
Here's a SQL Fiddle
The point of the second layer of the CTE is to have a "table" that has all the information you need but is only one row so that the hierarchical query will work correctly.

How to generate list of all dates between sysdate-30 and sysdate+30?

Purpose & What I've Got So Far
I am attempting to create a view which checks for missing labor transactions. The view will be fed to a Crystal report.
In this case, the view should take all dates between sysdate+30 and sysdate -30, and then should left outer join all labor records by active employees for each of those dates. It then gives a count of the number of labor transactions for each employee for each date.
This gets passed to the Crystal Report, which will filter based on a specific date range (within the +/- 30 range by the view). From there, the count of all days will summed up per employee in Crystal, and employees will show up which have zero transactions.
The Problem
Without spitting out a list of every date, initially, I'm using labor transaction for each date, but some have no counts for any date. These folks show null transaction dates with zero hours. This indicates they have no charges for the entire period, which makes sense.
However, when Crystal does a filter on that data and selects a range, I believe it leaves out these null values, thus not allowing me to show the full range of folks who don't have time submitted.
The Question
Is there a way to do the equivalent of "select every date between (sysdate+30) and (sysdate-30)" in a view, so that I can use it to compare all the time against?
The SQL (for reference)
SELECT QUERY.LABORRECLABORCODE
, QUERY.LABORRECEMPLOYEENUM
, QUERY.PERSONRECDISPLAYNAME
, QUERY.TRANSSTARTDATE
, COUNT(TRANSROWSTAMP) AS ROWCOUNT
FROM (SELECT *
FROM (SELECT LABOR.LABORCODE AS LABORRECLABORCODE
, LABOR.LA20 AS LABORRECEMPLOYEENUM
, PERSON.DISPLAYNAME AS PERSONRECDISPLAYNAME
FROM LABOR
LEFT OUTER JOIN PERSON
ON ( LABOR.LABORCODE = PERSON.PERSONID )
WHERE LABOR.STATUS = 'ACTIVE'
AND LABOR.LA20 IS NOT NULL
AND PERSON.DISPLAYNAME IS NOT NULL
AND LOWER(PERSON.DISPLAYNAME) NOT LIKE '%kimball%'
AND LOWER(PERSON.DISPLAYNAME) NOT LIKE '%electrico%'
AND LOWER(PERSON.DISPLAYNAME) NOT LIKE '%misc labor cost adj%'
AND LOWER(PERSON.DISPLAYNAME) NOT LIKE '%brossoit%'
AND LOWER(PERSON.DISPLAYNAME) NOT LIKE '%brossiot%')PERSONINFO
LEFT OUTER JOIN (SELECT STARTDATE AS TRANSSTARTDATE
, LABORCODE AS TRANSLABORCODE
, ROWSTAMP AS TRANSROWSTAMP
FROM LABTRANS
WHERE STARTDATE BETWEEN ( SYSDATE - 30 ) AND ( SYSDATE + 30 ))LABTRANSLIMITED
ON ( PERSONINFO.LABORRECLABORCODE = LABTRANSLIMITED.TRANSLABORCODE ))QUERY
GROUP BY LABORRECLABORCODE
, TRANSSTARTDATE
, LABORRECEMPLOYEENUM
, PERSONRECDISPLAYNAME
ORDER BY LABORRECLABORCODE
, TRANSSTARTDATE
;
select trunc(sysdate)+31-level from dual connect by level <=61
This is a good method for generating any arbitrary list of values.
Or another method: pick a table with a lot of rows
select sysdate+30 - rownum from user_objects where rownum<61
In order to meet my requirements of being sysdate -30 and sysdate + 30 in a range, this seems be the most elegant way of doing things for now:
SELECT *
FROM (SELECT TRUNC(SYSDATE - ROWNUM) DT
FROM DUAL
CONNECT BY ROWNUM < 31
UNION
SELECT TRUNC(SYSDATE + ROWNUM) DT
FROM DUAL
CONNECT BY ROWNUM < 31)DATERANGE;
I used this answer from this SO Question and expanded upon that thinking, using a union to join the queries that went in separate directions.