Optimise Oracle Join query (multiple joins on the same table) - oracle

I have three tables in my Oracle database:
Table 1: Contains employees and phone numbers from Company A:
EmployeeName, WorkPhone, MobilePhone, PersonalPhone,OtherPhone
Adam,1234,1111,0987,NULL
Catherine,2345,5432,NULL,NULL
Tom, 4567,7654,0101,0002
Table 2: Contains employees and phone numbers from Company B:
EmployeeName, WorkPhone, MobilePhone, PersonalPhone, OtherPhone
David,8888,9999,0000,1245
Sam,4321,5432,NULL,NULL
Clara,4567,7654,0101,NULL
Table 3: Contains phone numbers where the phone number could either be recorded in Column 1 or in Column 2 or in both:
PhoneNumber1, PhoneNumber2
1234,NULL
7654,7575
0000,1111
1234,4321
NULL,1234
5432,1234
Now, I would like to join phone numbers in Table 3 to their respective employees as well as to know where that employee works (Company A or B). The challenge is that we have a total of 8 "matching" possibilities for Table 2 and 8 for Table 2 (Each column in Table A/B can join to either column 1 or column2 in Table 3.
The datasets are big. (20M rows in table 1 and about 2M rows in table 2).Let's leave out Table2 for now and concentrate on joining Table1 to Table3 only.
If I do the following, the query is very very slow (and I imagine it would run out of temp table space at some point):
SELECT * FROM Table3 t3
LEFT JOIN Table1 t1
ON (PhoneNumber1 in (WorkPhone, MobilePhone, PersonalPhone, OtherPhone)
OR PhoneNumber2 in (WorkPhone, MobilePhone, PersonalPhone, OtherPhone))
If I do the following, the query runs out of temp table space (and i am not allowed to increase that)
SELECT * FROM Table3
LEFT JOIN Table1 t1_1
PhoneNumber1 = t1_1.WorkPhone
LEFT JOIN Table1 t1_2
PhoneNumber1 = t1_1.MobilePhone
LEFT JOIN Table1 t1_3
PhoneNumber1 = t1_1.PersonalPhone
...etc
How could we optimise this query?

You could unpivot either or both tables so you get a row for each (not-null) phone number, and then do a simple join:
with cte1 as (
select * from (
select 'A' as company, t.* from table1 t
union all
select 'B' as company, t.* from table2 t
)
unpivot (phone for type in (workphone as 'Work', mobilephone as 'Mobile',
personalphone as 'Personal', otherphone as 'Other'))
),
cte2 as (
select distinct phone from table3
unpivot (phone for type in (phonenumber1 as 'Phone1', phonenumber2 as 'Phone2'))
)
select cte1.*
from cte2
join cte1 on cte1.phone = cte2.phone;
C EMPLOYEEN TYPE PHON
- --------- -------- ----
A Adam Work 1234
A Adam Mobile 1111
A Catherine Mobile 5432
A Tom Mobile 7654
B David Personal 0000
B Sam Work 4321
B Sam Mobile 5432
B Clara Mobile 7654
8 rows selected.
The first CTE first unions tables 1 and 2 together, while adding a pseudocolumn indicating which table the data came from; and then unpivots the result so you get one row per phone number per person:
...
select * from cte1;
C EMPLOYEEN TYPE PHON
- --------- -------- ----
A Adam Work 1234
A Adam Mobile 1111
A Adam Personal 0987
A Catherine Work 2345
A Catherine Mobile 5432
...
B Clara Personal 0101
18 rows selected.
You could also unpivot each table first and then union those together, might be worth trying both ways.
The second CTE unpivots table 3 so you get one row for each not-null phone number in either column:
...
select * from cte2;
PHON
----
7654
7575
0000
4321
1234
5432
1111
7 rows selected.
Of course, this is on a tiny amount of dummy data; it might perform even worse on your actual larger tables... And I've made some assumptions about what you want to end up with.
Another approach might be to just convert the table 3 values into a single column which you can do manually instead of explicitly unpivoting, and then unioning together multiple queries against eachof the first two tables:
with cte as (
select phone from (
select phonenumber1 as phone from table3
union
select phonenumber2 as phone from table3
)
where phone is not null
)
select 'A' as customer, employeename, 'Work' as type, workphone as phone
from table1 where workphone in (select phone from cte)
union all
select 'A', employeename, 'Mobile', mobilephone
from table1 where mobilephone in (select phone from cte)
union all
select 'A', employeename, 'Personal', mobilephone
from table1 where personalphone in (select phone from cte)
union all
select 'A', employeename, 'Other', mobilephone
from table1 where otherphone in (select phone from cte)
union all
select 'B', employeename, 'Work', workphone
from table2 where workphone in (select phone from cte)
union all
select 'B', employeename, 'Mobile', mobilephone
from table2 where mobilephone in (select phone from cte)
union all
select 'B', employeename, 'Personal', personalphone
from table2 where personalphone in (select phone from cte)
union all
select 'B', employeename, 'Other', otherphone
from table2 where otherphone in (select phone from cte)
/
C EMPLOYEEN TYPE PHON
- --------- -------- ----
A Adam Work 1234
A Adam Mobile 1111
A Catherine Mobile 5432
A Tom Mobile 7654
B Sam Work 4321
B Sam Mobile 5432
B Clara Mobile 7654
B David Personal 0000
8 rows selected.
Which I personally find harder to read and maintain, but it may perform significantly differently too.

Related

PL SQL Group By Issue

I have two tables T1 and T2.
T1 has an ID column that is generated as a sequence.
It also has two columns first name and Last name.
The table T2 is connected to table T1 via the ID column (referential).
T2 table has a salary column, that is revised every few years.
I want to get all the first name, last name, salary , and salary date if the salary has changed.
I am not able to get this information using the ID.
A second ID is generated for the same FN and LN pair if the employee comes up for review.
For Example :-
ID FN LN
1 John Doe
2 John Doe
ID SALARY DATE
1 $1 2015
2 $2 2018
I am trying something like this
SELECT T.FN ||' '|| T.LN AS NAME, COUNT(*) AS CT,
S.SALARY, S.DATE
SALARYTABLE S, EMP T
WHERE S.ID=T.ID
HAVING COUNT(*) > 1
GROUP BY (T.FN ||' '|| T.LN);
I have solved this by using a Java program. I have to store all the ID's and loop through all the records and check if the FN and LN matches and then extract the Date and Salary. This is inefficient and I want to do it within PL/SQL.
Please help. Thank you.
Well, your data model is kind of wrong; you shouldn't rely on distinguishing people on their names. What if yet another "John Doe" gets employed?
Anyway: would something like this do?
CTEs T1 and T2 simulate your tables. I added some more rows, just to make sure that the following query doesn't fail too obviously
INTER CTE joins those two tables and calculates employee's previous salary (using the LAG function)
the final query select rows (from INTER) whose current and previous salary are different
As you already have those tables, you'd use lines 16 onwards.
SQL> with
2 t1 (id, fn, ln) as
3 (select 1, 'John', 'Doe' from dual union all
4 select 2, 'John', 'Doe' from dual union all
5 select 3, 'John', 'Doe' from dual union all
6 select 5, 'Billy', 'Jean' from dual union all
7 select 6, 'Billy', 'Jean' from dual
8 ),
9 t2 (id, salary, c_date) as
10 (select 1, 1, 2015 from dual union all
11 select 2, 2, 2018 from dual union all
12 select 3, 2, 2019 from dual union all
13 select 5, 3, 2016 from dual union all
14 select 6, 3, 2017 from dual
15 ),
16 inter as
17 (select
18 t1.id, t1.fn, t1.ln,
19 t2.id, t2.salary, t2.c_date,
20 lag(t2.salary) over (partition by t1.fn, t1.ln
21 order by c_date) prev_salary
22 from t1 join t2 on t1.id = t2.id
23 )
24 select i.fn, i.ln, i.salary, i.c_date
25 from inter i
26 where i.salary <> nvl(i.prev_salary, i.salary)
27 order by i.ln, i.c_date;
FN LN SALARY C_DATE
----- ---- ---------- ----------
John Doe 2 2018
SQL>

how to get exact string value from delimited column value match in oracle database

I have 2 tables
Table 1:
#######
ID Location
1 India
2 Australia
Table 2:
############
Name Locations
test1 India|North America
test2 Indiana|Australia
I used the below query to get the Name from table 2 if it contains Location in Locations of table 2.
select Name
from table2 t2 inner join table1 t1
on instr(t1.Location,t2.Locations,length(t1.Location)) >= 1;
But when executed it still gives me results for Indiana as well whereas it should just return me result for location India alone.
I tried using contains in query too, but contains takes second parameter as string but not as column name.
Is there any other approach on this?
regexps always help in such cases
with
table1 (id, location) as (
select 1, 'India' from dual union
select 2, 'Australia' from dual
),
table2 (name, locations) as (
select 'test1', 'India|North America' from dual union
select 'test2', 'Indiana|Australia' from dual
)
select *
from table2 join table1 on
regexp_like (locations, '(^|\|)' || location || '(\||$)')
Try to look up location with delimiter, like this:
select Name from table2 t2
inner join table1 t1 on instr(t2.Locations,t1.Location||'|') >= 1

Table Variable Style Entities in Oracle

I've been looking around for awhile for something in Oracle that acts like a table variable in SQL Server. I've found people asking questions like this here on SO and people always say "Yes, Oracle has that" but the examples show that the entities are not like SQL Server at all. Can someone show me how to perform the below simple TSQL solution in Oracle?
declare #users table (
ID int,
Name varchar(50),
Age int,
Gender char(1)
)
;with users as (
select 1001 as ID, 'Bob' as Name, 25 as Age, 'M' as Gender
union
select 1021 as ID, 'Sam' as Name, 29 as Age, 'F'
)
insert into #users (ID, Name, Age, Gender)
select * from users
declare #grades table (
UserID int,
ClassID int,
Grade int
)
;with grades as (
select 1001 as UserID , 120 as ClassID, 4 as Grade
Union
select 1001 as UserID , 220 as ClassID, 2 as Grade
Union
select 1021 as UserID , 130 as ClassID, 4 as Grade
Union
select 1021 as UserID , 230 as ClassID, 4 as Grade
Union
select 1021 as UserID , 340 as ClassID, 2 as Grade
)
insert into #grades
select * from grades
select u.ID, u.Name, GPA = AVG(cast(g.grade as decimal))
from #users u
inner join #grades g on u.ID=g.UserID
group by u.ID, u.Name
Some answers may tell you that Oracle has table variables, and it does to a certain extent. However, most answers will tell you that you should not be doing this in Oracle at all; there's simply no need.
In your case I would simply use a CTE:
with users as (
select 1001 as ID, 'Bob' as Name, 25 as Age, 'M' as Gender from dual
union
select 1021 as ID, 'Sam' as Name, 29 as Age, 'F' from dual
)
, grades as (
select 1001 as UserID , 120 as ClassID, 4 as Grade from dual
Union
select 1001 as UserID , 220 as ClassID, 2 as Grade from dual
Union
select 1021 as UserID , 130 as ClassID, 4 as Grade from dual
Union
select 1021 as UserID , 230 as ClassID, 4 as Grade from dual
Union
select 1021 as UserID , 340 as ClassID, 2 as Grade from dual
)
select u.ID, u.Name, AVG(g.grade) as gpa
from users u
join grades g on u.ID = g.UserID
group by u.ID, u.Name
UPDATE: The answer I've been trying to get for a long time is in Ben's comment below which I include here:
"There is no variable, which you can create on the fly and join to other tables in standard SQL #wcm, yes. There is a number of different type of objects that can be created that will allow you to do this, but not exactly as you would in T-SQL".
If I understand correctly, then if I needed temporary storage of data that is confined to being visible to my session, then I'd be using a global temporary table. Probably more overhead than storing in memory, but plenty of advantages too -- gathering statistics on them, indexing them, and the ability to store data without regard to memory consumption.

Group by two fields, and having count() on first field

I have a table that stored users play list, a video can be viewed by multiple users for multiple times.
A records goes like this:
videoid, userid, time
123, abc , 2013-09-11
It means user(abc) has watched video(123) on 2013-09-11
Now I want to find distinct users watched video list (no duplication), and only show the users that have watched more than two videos.
SELECT videoid, userid
FROM table_play_list
WHERE SOME CONDICTION
GROUP BY userid, videoid
The sql only select distinct users watchlist, I also want to filter users that have watched more than two different videos.
I know I have to google and read the documentation first, some said 'HAVING' could solve this, unfortunately, I could not make it.
If I understand correctly, you are looking for users who watched more than two different videos. You can do this by using count(distinct) with a partition by clause:
select userid, videoid
from (SELECT userid, videoid, count(distinct videoid) over (partition by userid) as cnt
FROM table_play_list
WHERE <ANY CONDITION>
) t
where cnt > 2;
Try like this,
SELECT userid, count(*)
FROM table_play_list
--WHERE SOME CONDITION
GROUP BY user_id
having count(*) >2;
Try this if you need to get the count based on userid and videoid(users who watch the same video more than two times).
SELECT userid, videoid, count(*)
FROM table_play_list
--WHERE SOME CONDITION
GROUP BY user_id, video_id
having count(*) >2;
This is probably best handled with analytics (window functions). Without analytics you will probably need a self-join.
SQL> WITH table_play_list AS (
2 SELECT 123 videoid, 'a' userid FROM dual UNION ALL
3 SELECT 125 videoid, 'a' userid FROM dual UNION ALL
4 SELECT 123 videoid, 'b' userid FROM dual UNION ALL
5 SELECT 123 videoid, 'b' userid FROM dual UNION ALL
6 SELECT 123 videoid, 'c' userid FROM dual
7 )
8 SELECT videoid, userid,
9 COUNT(*) over(PARTITION BY userid) nb_video
10 FROM table_play_list;
VIDEOID USERID NB_VIDEO
---------- ------ ----------
123 a 2
125 a 2
123 b 2
123 b 2
123 c 1
This lists all user/video and the total number of videos watched by each user. As you can see user b has watched the same video twice, I don't know if it's possible in your system.
You can filter with a subquery:
SQL> WITH table_play_list AS (
2 SELECT 123 videoid, 'a' userid FROM dual UNION ALL
3 SELECT 125 videoid, 'a' userid FROM dual UNION ALL
4 SELECT 123 videoid, 'b' userid FROM dual UNION ALL
5 SELECT 123 videoid, 'b' userid FROM dual UNION ALL
6 SELECT 123 videoid, 'c' userid FROM dual
7 )
8 SELECT *
9 FROM (SELECT videoid, userid,
10 COUNT(*) over(PARTITION BY userid) nb_video
11 FROM table_play_list)
12 WHERE nb_video > 1;
VIDEOID USERID NB_VIDEO
---------- ------ ----------
123 a 2
125 a 2
123 b 2
123 b 2
The below will give users who have watched more than two different videos.
SELECT userid, count(distinct video_id)
FROM table_play_list
WHERE SOME CONDICTION
GROUP BY user_id
having count(distinct video_id) >2;
If you use Oracle PL/SQL you can use like this:
SELECT column1, column2
FROM
(
SELECT column1, column2, COUNT(column1)
OVER (PARTITION BY column1) AS cnt
FROM test
GROUP BY column1, column2
ORDER BY column1
)
WHERE cnt > 2
If you use standard SQL you can use like this:
SELECT column1, column2
FROM test
WHERE column1 IN
(
SELECT column1
FROM
(
SELECT column1, column2
FROM test
GROUP BY column1, column2
ORDER BY column1
)
GROUP BY column1
HAVING COUNT(column1) > 2
)
GROUP BY column1, column2
ORDER BY column1

Oracle join not working

I am trying get all IFSC codes and details (bank unique code) from my bank master which starts with the first 4 characters of entered IFSC code. I have the bank master table which includes IFSC code(4 chars), corresponding bank details.
The main part of the query is given below.
AND D.IFSC_CODE=UPPER(substr(B.BANK_CODE,1,4)) (+) ORDER BY....
When I execute this query, I am getting an error message "ORA-00936: missing expression".
What I am expecting from the query is:
return the details if the bank exists in bank master corresponding to the entered IFSC code
else only entered IFSC should display
When I rewrite the query like
AND D.IFSC_CODE(+) =UPPER(substr(B.BANK_CODE,1,4)) ORDER BY....
There is no error but the result was not what I expected.
How can I resolve this?
In a complex outer join expression you would put the (+) operator on all relevant columns, as in:
AND D.IFSC_CODE=UPPER(substr(B.BANK_CODE (+),1,4))
For example:
SQL> WITH table_a AS (
2 SELECT '0001' ID FROM dual
3 UNION ALL SELECT '0002' FROM dual
4 UNION ALL SELECT '0003' FROM dual
5 ), table_b AS (
6 SELECT '0001a' ID FROM dual
7 UNION ALL SELECT '0002b' FROM dual
8 )
9 SELECT a.id, b.id
10 FROM table_a a, table_b b
11 WHERE a.id = substr(b.id (+), 1, 4);
ID ID
---- -----
0001 0001a
0002 0002b
0003
This form of outer join is specific to Oracle and arguably more difficult to read than SQL ANSI outer join. Additionaly, some specific features are disabled with this old method (full outer join, outer join to more than one table). In SQL ansi join form, the query would look like:
SQL> WITH table_a AS (
2 SELECT '0001' ID FROM dual
3 UNION ALL SELECT '0002' FROM dual
4 UNION ALL SELECT '0003' FROM dual
5 ), table_b AS (
6 SELECT '0001a' ID FROM dual
7 UNION ALL SELECT '0002b' FROM dual
8 )
9 SELECT a.id, b.id
10 FROM table_a a
11 LEFT OUTER JOIN table_b b ON a.id = substr(b.id, 1, 4);
ID ID
---- -----
0001 0001a
0002 0002b
0003

Resources