JOIN when no data exists - sql

Disclaimer: I don't have a lot of tech background and just learning SQL so apologies.
I have 2 table for acct information - one ref data (ACCT_RD) and one txn data (ACCT_TD).
ACCT_RD is like this
ACCT_ID
ACCT_NAME
1
abc
2
xyz
ACCT_TD is like this
ACCT_ID
DATE
VALUE
1
01-31-2020
4000.33
1
01-31-2021
2000.11
2
01-31-2020
5666.23
I want a query where I will pass the account id and date and it will return me data in format
ACCT_ID
NAME
DATE
VALUE
1
abc
01-31-2020
4000.33
1
abc
null
null
it could be that the ACCT_TD may not contain data (no rows) for all dates but ACCT_RD will always have the info.
I am trying a LEFT Join like
SELECT R.ACCT_ID, R.NAME, T.VALUE, T.DATE
FROM ACCT_RD R
LEFT JOIN ACCT_TD T ON R.ACCT_ID = T.ACCT_ID
WHERE R.ACCT_ID = 1
AND T.DATE IN ('01-31-2000','01-31-2020')
I am getting a row where I have data in both and no row where I don't have data in ACCT_TD.
Is it because in ACCT_TD no row exists for date '01-31-2000' and it is not a column for ACCT_RD?
How can I achieve what I am looking for?

It looks like you have the following requirements for the result set.
For any given ACCT_ID It must contain all the corresponding rows
(if any) from ACCT_TD
VALUE and DATE result set columns are equal to ACCT_TD.VALUE and ACCT_TD.DATE, if ACCT_TD.DATE is in a list of date parameters passed, and both NULL otherwise
If it's the correct understanding of requirements, then try this:
WITH
ACCT_RD (ACCT_ID, ACCT_NAME) AS
(
VALUES
(1, 'abc')
, (2, 'xyz')
)
, ACCT_TD (ACCT_ID, DATE, VALUE) AS
(
VALUES
(1, '01-31-2020', 4000.33)
, (1, '01-31-2021', 2000.11)
, (2, '01-31-2020', 5666.23)
)
SELECT
R.ACCT_ID, R.ACCT_NAME
, CASE WHEN T.DATE IN ('01-31-2000','01-31-2020') THEN T.DATE END AS DATE
, CASE WHEN T.DATE IN ('01-31-2000','01-31-2020') THEN T.VALUE END AS VALUE
FROM ACCT_RD R
LEFT JOIN ACCT_TD T ON R.ACCT_ID = T.ACCT_ID
WHERE R.ACCT_ID = 1;
The result is:
|ACCT_ID|ACCT_NAME|DATE |VALUE |
|-------|---------|----------|-------|
|1 |abc |01-31-2020|4000.33|
|1 |abc | | |

If you want to guarantee that one row is returned even when there is no match in the second table, just move the date filters to the on clause:
SELECT R.ACCT_ID, R.NAME, T.VALUE, T.DATE
FROM ACCT_RD R LEFT JOIN
ACCT_TD T
ON R.ACCT_ID = T.ACCT_ID AND
T.DATE IN ('01-31-2000', '01-31-2020')
WHERE R.ACCT_ID = 1;
The values will be NULL if there is no match.
Also, if your columns are really dates, then you should use proper date formats:
T.DATE IN ('2000-01-31', '2000-01-31')

Related

Join future dates to table which only has dates until current day

I have these two tables:
table1: name (string), actual (double), yyyy_mm_dd (date)
table2: name (string), expected(double), yyyy_mm_dd (string)
table1 contains data from 2018-01-01 up until the current day, table2 contains predicted data for the year of 2020. My problem is that table1 doesn’t have any date values past the present date, so I get duplicate data when joining like below:
SELECT
kpi.yyyy_mm_dd,
kpi.name,
kpi.actual as actual,
pre.predicted as predicted
FROM
schema1.table1 kpi
LEFT JOIN
schema1.table2 pre
ON name = kpi.name --AND pre.yyyy_mm_dd = kpi.yyyy_mm_dd
WHERE
kpi.yyyy_mm_dd >= '2019-12-09'
Output:
+----------+------------+----------+-------------+
|yyyy_mm_dd| name |actual |predicted |
+----------+------------+----------+-------------+
|2019-12-10| Company | 100000 | 925,180 |
|2019-12-10| Company | 100000 | 1,145,723 |
|2019-12-10| Company | 100000 | 456,359 |
--------------------------------------------------
If I uncomment the AND condition in my join clause, I won’t get the predicted values as my first table has no 2020 data. How can I join these tables together without duplicating actual values? actual should be null for days which haven't happened yet.
I think you want UNION ALL and not a JOIN:
SELECT
yyyy_mm_dd,
name,
actual as actual,
NULL as predicted
FROM schema1.table1
WHERE yyyy_mm_dd >= '2019-12-09'
UNION ALL
SELECT
yyyy_mm_dd,
name,
NULL as actual,
predicted as predicted
FROM schema1.table2
Hive supports full join:
SELECT COALESCE(kpi.yyyy_mm_dd, pre.yyyy_mm_dd) as yyyy_mm_dd,
COALESCE(kpi.name, pre.name) as name,
kpi.actual as actual,
pre.predicted as predicted
FROM (SELECT kpi.*
FROM schema1.table1 kpi
WHERE kpi.yyyy_mm_dd >= '2019-12-09'
) kpi FULL JOIN
schema1.table2 pre
ON kpi.name = pre.name AND
kpi.yyyy_mm_dd = pre.yyyy_mm_dd
Try using
group by
clause in your query, below might solve your problem
SELECT
kpi.yyyy_mm_dd,
kpi.name,
kpi.actual as actual,
pre.predicted as predicted
FROM
schema1.table1 kpi
LEFT JOIN
schema1.table2 pre
ON name = kpi.name
group by kpi.yyyy_mm_dd,kpi.name,kpi.actual

datediff for row that meets my condition only once per row

I want to do a datediff between 2 dates on different rows only if the rows have a condition.
my table looks like the following, with additional columns (like guid)
Id | CreateDateAndTime | condition
---------------------------------------------------------------
1 | 2018-12-11 12:07:55.273 | with this
2 | 2018-12-11 12:07:53.550 | I need to compare this state
3 | 2018-12-11 12:07:53.550 | with this
4 | 2018-12-11 12:06:40.780 | state 3
5 | 2018-12-11 12:06:39.317 | I need to compare this state
with this example I would like to have 2 rows in my selection which represent the difference between the dates from id 5-3 and from id 2-1.
As of now I come with a request that gives me the difference between dates from id 5-3 , id 5-1 and id 2-1 :
with t as (
SELECT TOP (100000)
*
FROM mydatatable
order by CreateDateAndTime desc)
select
DATEDIFF(SECOND, f.CreateDateAndTime, s.CreateDateAndTime) time
from t f
join t s on (f.[guid] = s.[guid] )
where f.condition like '%I need to compare this state%'
and s.condition like '%with this%'
and (f.id - s.id) < 0
My problem is I cannot set f.id - s.id to a value since other rows can be between the ones I want to make the diff on.
How can I make the datediff only on the first rows that meet my conditions?
EDIT : To make it more clear
My condition is an eventname and I want to calculate the time between the occurence of my event 1 and my event 2 and fill a column named time for example.
#Salman A answer is really close to what I want except it will not work when my event 2 is not happening (which was not in my initial example)
i.e. in table like the following , it will make the datediff between row id 5 and row id 2
Id | CreateDateAndTime | condition
---------------------------------------------------------------
1 | 2018-12-11 12:07:55.273 | with this
2 | 2018-12-11 12:07:53.550 | I need to compare this state
3 | 2018-12-11 12:07:53.550 | state 3
4 | 2018-12-11 12:06:40.780 | state 3
5 | 2018-12-11 12:06:39.317 | I need to compare this state
the code I modified :
WITH cte AS (
SELECT id
, CreateDateAndTime AS currdate
, LAG(CreateDateAndTime) OVER (PARTITION BY guid ORDER BY id desc ) AS prevdate
, condition
FROM t
WHERE condition IN ('I need to compare this state', 'with this ')
)
SELECT *
,DATEDIFF(second, currdate, prevdate) time
FROM cte
WHERE condition = 'I need to compare this state '
and DATEDIFF(second, currdate, prevdate) != 0
order by id desc
Perhaps you want to match ids with the nearest smaller id. You can use window functions for this:
WITH cte AS (
SELECT id
, CreateDateAndTime AS currdate
, CASE WHEN LAG(condition) OVER (PARTITION BY guid ORDER BY id) = 'with this'
THEN LAG(CreateDateAndTime) OVER (PARTITION BY guid ORDER BY id) AS prevdate
, condition
FROM t
WHERE condition IN ('I need to compare this state', 'with this')
)
SELECT *
, DATEDIFF(second, currdate, prevdate)
FROM cte
WHERE condition = 'I need to compare this state'
The CASE expression will match this state with with this. If you have mismatching pairs then it'll return NULL.
try by using analytic function lead()
with cte as
(
select 1 as id, '2018-12-11 12:07:55.273' as CreateDateAndTime,'with this' as condition union all
select 2,'2018-12-11 12:07:53.550','I need to compare this state' union all
select 3,'2018-12-11 12:07:53.550','with this' union all
select 4,'2018-12-11 12:06:40.780','state 3' union all
select 5,'2018-12-11 12:06:39.317','I need to compare this state'
) select *,
DATEDIFF(SECOND,CreateDateAndTime,lead(CreateDateAndTime) over(order by Id))
from cte
where condition in ('with this','I need to compare this state')
You Ideally want LEADIF/LAGIF functions, because you are looking for the previous row where condition = 'with this'. Since there are no LEADIF/LAGIFI think the best option is to use OUTER/CROSS APPLY with TOP 1, e.g
CREATE TABLE #T (Id INT, CreateDateAndTime DATETIME, condition VARCHAR(28));
INSERT INTO #T (Id, CreateDateAndTime, condition)
VALUES
(1, '2018-12-11 12:07:55', 'with this'),
(2, '2018-12-11 12:07:53', 'I need to compare this state'),
(3, '2018-12-11 12:07:53', 'with this'),
(4, '2018-12-11 12:06:40', 'state 3'),
(5, '2018-12-11 12:06:39', 'I need to compare this state');
SELECT ID1 = t1.ID,
Date1 = t1.CreateDateAndTime,
ID2 = t2.ID,
Date2 = t2.CreateDateAndTime,
Difference = DATEDIFF(SECOND, t1.CreateDateAndTime, t2.CreateDateAndTime)
FROM #T AS t1
CROSS APPLY
( SELECT TOP 1 t2.CreateDateAndTime, t2.ID
FROM #T AS t2
WHERE t2.Condition = 'with this'
AND t2.CreateDateAndTime > t1.CreateDateAndTime
--AND t2.GUID = t.GUID
ORDER BY CreateDateAndTime
) AS t2
WHERE t1.Condition = 'I need to compare this state';
Which Gives:
ID1 Date1 D2 Date2 Difference
-------------------------------------------------------------------------------
2 2018-12-11 12:07:53.000 1 2018-12-11 12:07:55.000 2
5 2018-12-11 12:06:39.000 3 2018-12-11 12:07:53.000 74
I would enumerate the values and then use window functions for the difference.
select min(id), max(id),
datediff(second, min(CreateDateAndTime), max(CreateDateAndTime)) as seconds
from (select t.*,
row_number() over (partition by condition order by CreateDateAndTime) as seqnum
from t
where condition in ('I need to compare this state', 'with this')
) t
group by seqnum;
I cannot tell what you want the results to look like. This version only output the differences, with the ids of the rows you care about. The difference can also be applied to the original rows, rather than put into summary rows.

Adding in missing dates from results in SQL

I have a database that currently looks like this
Date | valid_entry | profile
1/6/2015 1 | 1
3/6/2015 2 | 1
3/6/2015 2 | 2
5/6/2015 4 | 4
I am trying to grab the dates but i need to make a query to display also for dates that does not exist in the list, such as 2/6/2015.
This is a sample of what i need it to be:
Date | valid_entry
1/6/2015 1
2/6/2015 0
3/6/2015 2
3/6/2015 2
4/6/2015 0
5/6/2015 4
My query:
select date, count(valid_entry)
from database
where profile = 1
group by 1;
This query will only display the dates that exist in there. Is there a way in query that I can populate the results with dates that does not exist in there?
You can generate a list of all dates that are between the start and end date from your source table using generate_series(). These dates can then be used in an outer join to sum the values for all dates.
with all_dates (date) as (
select dt::date
from generate_series( (select min(date) from some_table), (select max(date) from some_table), interval '1' day) as x(dt)
)
select ad.date, sum(coalesce(st.valid_entry,0))
from all_dates ad
left join some_table st on ad.date = st.date
group by ad.date, st.profile
order by ad.date;
some_table is your table with the sample data you have provided.
Based on your sample output, you also seem to want group by date and profile, otherwise there can't be two rows with 2015-06-03. You also don't seem to want where profile = 1 because that as well wouldn't generate two rows with 2015-06-03 as shown in your sample output.
SQLFiddle example: http://sqlfiddle.com/#!15/b0b2a/2
Unrelated, but: I hope that the column names are only made up. date is a horrible name for a column. For one because it is also a keyword, but more importantly it does not document what this date is for. A start date? An end date? A due date? A modification date?
You have to use a calendar table for this purpose. In this case you can create an in-line table with the tables required, then LEFT JOIN your table to it:
select "date", count(valid_entry)
from (
SELECT '2015-06-01' AS d UNION ALL '2015-06-02' UNION ALL '2015-06-03' UNION ALL
'2015-06-04' UNION ALL '2015-06-05' UNION ALL '2015-06-06') AS t
left join database AS db on t.d = db."date" and db.profile = 1
group by t.d;
Note: Predicate profile = 1 should be applied in the ON clause of the LEFT JOIN operation. If it is placed in the WHERE clause instead then LEFT JOIN essentially becomes an INNER JOIN.

Oracle - complex sql query

In sql i got a scenario like to add a constant value to one of the condition data of query also the constant data that we going to add is coming from a sub query. The rough sql will be like this(As most of the sql datas are confidential I can't able to share the actual query).
SELECT * FROM test_table tt
INNER JOIN test_table_1 tt1
ON tt.id = tt1.id
WHERE TO_DATE(tt1.CONDITION_VALUE, 'yyyy-mm-dd') >=
TO_DATE('2011-08-25', 'yyyy-mm-dd') + (SELECT CONDITION_VALUE
FROM test_table tt
INNER JOIN test_table_1 tt1
ON tt.id = tt1.id
WHERE CONDITION_SEQUENCE='2') AND
CONDITION_SEQUENCE='1'.
Issue is subselect will return a set of data. So I getting the error ORA-01427: single-row subquery returns more than one row.
Also, what is the best way to address this issue.
Simply: The scenario is, I want to calulate one rows values, based on the value from another row, but of the same parent object.
As far my understanding, now the joins were behaving like this
obj1.data + [set of subquery data]
obj2.data + [set of subquery data]
....
objN.data + [set of subquery data]
Rather I want like
obj1.data + obj1.anotherData
obj2.data + obj2.anotherData
....
objN.data + objN.anotherData
Updated question
Input is purchase date from: 2011-08-19 to: 2011-08-25, that i'll get from UI
Table value:
id | CONDITION_SEQUENCE | CONDITION_VALUE |
------------------------|------------------
1 | 1 | 6 | --- purchase date
2 | 1 | 4 |
3 | 2 | 2011-08-25 | --- stay start date
4 | 1 | 2011-11-25 | --- stay end date
--------------------------------------------
But in the purchase date, I having a value 6, which I want to add or subtract from stay start date. Where all the condition_value belongs to one parent object, that i 'll determine by its parent table
Output: I want to calculate the purchase date as 2011-08-25(stay start date) + 6
(purchase date) = 2011-08-19
To accomplish what you seem to be trying to do you might try something like
SELECT *
FROM test_table tt
INNER JOIN test_table_1 tt1
ON tt.id = tt1.id
WHERE TO_DATE(tt1.CONDITION_VALUE, 'yyyy-mm-dd') >=
TO_DATE('2011-08-25', 'yyyy-mm-dd') + tt.CONDITION_VALUE AND
tt.CONDITION_SEQUENCE = '2' AND
tt1.CONDITION_SEQUENCE = '1'
Best of luck.
according to my understanding you can try like below,
SELECT tt.*,tt1.* FROM test_table tt
INNER JOIN test_table_1 tt1
ON tt.id = tt1.id
INNER JOIN
test_table tt2
on tt.id = tt2.id
and tt2.CONDITION_SEQUENCE='2'
WHERE TO_DATE(tt1.CONDITION_VALUE, 'yyyy-mm-dd') >=
(TO_DATE('2011-08-25', 'yyyy-mm-dd') + tt2.CONDITION_VALUE)
AND tt.CONDITION_SEQUENCE='1'

Show data from table even if there is no data!! Oracle

I have a query which shows count of messages received based on dates.
For Eg:
1 | 1-May-2012
3 | 3-May-2012
4 | 6-May-2012
7 | 7-May-2012
9 | 9-May-2012
5 | 10-May-2012
1 | 12-May-2012
As you can see on some dates there are no messages received. What I want is it should show all the dates and if there are no messages received it should show 0 like this
1 | 1-May-2012
0 | 2-May-2012
3 | 3-May-2012
0 | 4-May-2012
0 | 5-May-2012
4 | 6-May-2012
7 | 7-May-2012
0 | 8-May-2012
9 | 9-May-2012
5 | 10-May-2012
0 | 11-May-2012
1 | 12-May-2012
How can I achieve this when there are no rows in the table?
First, it sounds like your application would benefit from a calendar table. A calendar table is a list of dates and information about the dates.
Second, you can do this without using temporary tables. Here is the approach:
with constants as (select min(thedate>) as firstdate from <table>)
dates as (select( <firstdate> + rownum - 1) as thedate
from (select rownum
from <table> cross join constants
where rownum < sysdate - <firstdate> + 1
) seq
)
select dates.thedate, count(t.date)
from dates left outer join
<table> t
on t.date = dates.thedate
group by dates.thedate
Here is the idea. The alias constants records the earliest date in your table. The alias dates then creates a sequence of dates. The inner subquery calculates a sequence of integers, using rownum, and then adds these to the first date. Note this assumes that you have on average at least one transaction per date. If not, you can use a bigger table.
The final part is the join that is used to bring back information about the dates. Note the use of count(t.date) instead of count(*). This counts the number of records in your table, which should be 0 for dates with no data.
You don't need a separate table for this, you can create what you need in the query. This works for May:
WITH month_may AS (
select to_date('2012-05-01', 'yyyy-mm-dd') + level - 1 AS the_date
from dual
connect by level < 31
)
SELECT *
FROM month_may mm
LEFT JOIN mytable t ON t.some_date = mm.the_date
The date range will depend on how exactly you want to do this and what your range is.
You could achieve this with a left outer join IF you had another table to join to that contains all possible dates.
One option might be to generate the dates in a temp table and join that to your query.
Something like this might do the trick.
CREATE TABLE #TempA (Col1 DateTime)
DECLARE #start DATETIME = convert(datetime, convert(nvarchar(10), getdate(), 121))
SELECT #start
DECLARE #counter INT = 0
WHILE #counter < 50
BEGIN
INSERT INTO #TempA (Col1) VALUES (#start)
SET #start = DATEADD(DAY, 1, #start)
SET #counter = #counter+1
END
That will create a TempTable to hold the dates... I've just generated 50 of them starting from today.
SELECT
a.Col1,
COUNT(b.MessageID)
FROM
TempA a
LEFT OUTER JOIN YOUR_MESSAGE_TABLE b
ON a.Col1 = b.DateColumn
GROUP BY
a.Col1
Then you can left join your message counts to that.