Table Variable Style Entities in Oracle - 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.

Related

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

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.

Select tree by leaf in Oracle Database

There is hierarchical queries in Oracle, using CONNECT BY PRIOR. Everybody knows, how to select children by parent, but I need to select parent by child.
Here is my table:
ID PID NAME TYPE
1 null EARTH PLANET
2 1 USA COUNTRY
3 2 CALIFORNIA STATE
4 3 Los_Angeles CITY
5 3 San_Francisco CITY
6 3 San_Diego CITY
In my app I have ID of San_Diego, and I need to know, in what country San_Diego is? I need to get USA (TYPE=COUNTRY) with my query? How to select it with oracle hierarchical structures?
Hierarchy means, that one row of a table is a "parent", and another one - a "child". PRIOR is used to show who is who. Clause CONNECT BY PRIOR EMPNO = MGR means, that if two rows have the same value, but one row in a column EMPNO, and the second - in a column MGR, then second is a "parent" and first is a "child". So, query
SELECT EMPNO,ENAME,MGR,LEVEL
FROM TMP_PCH
CONNECT BY PRIOR EMPNO = MGR
START WITH MGR = 'John'
returns all subordinates of John (and John itself), and query
SELECT EMPNO,ENAME,MGR,LEVEL
FROM TMP_PCH
CONNECT BY PRIOR MGR = EMPNO
START WITH MGR = 'John'
returns all bosses of John (and John itself).
You need to create correct condition for connect by and start with leaf 'San Diego':
select name
from (
select * from test
connect by id = prior pid
start with name='San_Diego')
where type='COUNTRY'
SQL Fiddle demo
SELECT * FROM places_table
WHERE type = 'COUNTRY'
START WITH id = 6 -- San Diego
CONNECT BY PRIOR pid = id;
There's no difference, you just have to use PRIOR in the parent side. For example to get the tree for the city of San Diego:
WITH your_table AS (
SELECT 1 id, NULL pid, 'EARTH' name, 'PLANET' type FROM dual
UNION
SELECT 2 id, 1 pid, 'USA' name, 'COUNTRY' type FROM dual
UNION
SELECT 3 id, 2 pid, 'CALIFORNIA' name, 'STATE' type FROM dual
UNION
SELECT 4 id, 3 pid, 'Los_Angeles' name, 'CITY' type FROM dual
UNION
SELECT 5 id, 3 pid, 'San_Francisco' name, 'CITY' type FROM dual
UNION
SELECT 6 id, 3 pid, 'San_Diego' name, 'CITY' type FROM dual
)
SELECT id, name, level
FROM your_table
CONNECT BY id = PRIOR pid
START WITH id = 6
If you just wanted to get the country, you could filter by level:
WITH your_table AS (
SELECT 1 id, NULL pid, 'EARTH' name, 'PLANET' type FROM dual
UNION
SELECT 2 id, 1 pid, 'USA' name, 'COUNTRY' type FROM dual
UNION
SELECT 3 id, 2 pid, 'CALIFORNIA' name, 'STATE' type FROM dual
UNION
SELECT 4 id, 3 pid, 'Los_Angeles' name, 'CITY' type FROM dual
UNION
SELECT 5 id, 3 pid, 'San_Francisco' name, 'CITY' type FROM dual
UNION
SELECT 6 id, 3 pid, 'San_Diego' name, 'CITY' type FROM dual
)
SELECT id, name, level
FROM your_table
WHERE LEVEL = 3
CONNECT BY id = PRIOR pid
START WITH id = 6

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

how to write a query to count the number of days that a user has logged in?

The site log records a datetime stamp each time a user logs in. I am in need of a query to count the number of days that a user has logged in. Just doing a count on the [DateTime] field doesn't work because a user can login more than once per day. So, I am stumped as how to just count distinct days that they logged in. Basically, we need the query to produce:
UserID
NumberOfDaysLoggedIn
For example, User John (ID 33) logins in as such:
3/26/2008 12:20 PM
3/26/2008 5:58 PM
3/27/2008 11:46 AM
The results we want would be:
UserID NumberofDaysLoggedIn
33 2
Any ideas to produce that results by using oracle query . please suggest any idea
what you should do is round the dates and then put them under distinct.
the function that round dates is trunc().
you can do it like that:
select count(*)
from (select name, trunc(date)
from table
group by name, trunc(date))
where name = 'John';
if you want to get the result for each user you could do it like that:
select name, count(*)
from (select distinct name, trunc(date)
from table)
group by name;
you need to do something like;
select userID, count(distinct trunc(date))
from table
with t as
(
select 1 as id, sysdate as d from dual
union all
select 1 as id, sysdate - 0.5 as d from dual
union all
select 1 as id, sysdate - 0.6 as d from dual
union all
select 1 as id, sysdate - 1 as d from dual
union all
select 2 as id, sysdate - 1 as d from dual
)
select id, count(distinct trunc(d))
from t
group by id
;
You can count distinct dates, something along the lines of:
select count(distinct trunc(t.date))
from table t
where t.userId = some user id
trunc() truncates the date to the day value but can also take format parameter for truncating to a specific unit of measure.

Oracle UPDATE Question

Can the update described below be completed in one UPDATE statement?
I want to update the Operators.name_id values with the Users.name_id values by joining on Users.name = Operators.op_name. Both Users.name and Operators.op_name are have a unique.
I know the situation described below doesn't follow "best practices", but it's a much simpler example of what I'm trying to do: namely updating a field with the value from another joined table.
Table: Users
user_id name
----------------
34 Billy
43 Jimmy
50 Joe
Table: Operators (before UPDATE)
op_id op_name user_id
-------------------------
12 Billy 35
35 Jimmy 46
33 Joe 99
Table: Operators (after UPDATE)
op_id op_name name_id
-------------------------
12 Billy 34
35 Jimmy 43
33 Joe 50
UPDATE operators o
SET user_id =
(
SELECT u.user_id
FROM users u
WHERE o.op_name = u.name
)
WHERE o.op_name IN
(
SELECT name
FROM users
)
In Oracle 10g, more efficient:
MERGE
INTO operators o
USING users u
ON (u.name = o.op_name)
WHEN MATCHED THEN
UPDATE
SET user_id = u.user_id
update (
select oo.user_id, uu.user_id uu_id
from operators oo
join
users uu on uu.name = oo.op_name
)
set user_id = uu_id
Here's the full test script (I'm using my_users and my_operators so as not to mess with your data).
drop table my_users;
create table my_users (
user_id number(2),
name varchar2(30) unique
)
;
insert into my_users (user_id, name)
select 34, 'Billy' from dual union all
select 43, 'Jimmy' from dual union all
select 50, 'Joe' from dual
;
drop table my_operators;
create table my_operators (
op_id number(2),
op_name varchar2(30) unique,
user_id number(2)
)
;
insert into my_operators (op_id, op_name, user_id)
select 12, 'Billy', 35 from dual union all
select 35, 'Jimmy', 46 from dual union all
select 33, 'Joe', 99 from dual
;
update (
select oo.user_id, uu.user_id uu_id
from my_operators oo
join
my_users uu on uu.name = oo.op_name
)
set user_id = uu_id
;
select * from my_operators;

Resources