Oracle Query on 3 tables with 2 outer joins - oracle11g

I'm having some trouble writing a query that seems like it should be simple, but the solution is evading me.
We have three tables (simplified for the purpose of this question):
persons - a table of user names:
per_id number(10) - primary key, populated by a sequence
user_name varchar2(50)
user_id varchar2(15) - unique, basically the employee ID
work_assignments - kind of like crew assignments, but more general:
wa_id number(10) - primary key, populated by a sequence
wa_name varchar2(25)
current_assignments - which users have which work_assignments; the average per user is about 25 work assignments, but some "lucky" individuals have upwards of 150:
wa_id number(10)
per_id number(10)
I'm trying to write a query that will compare the work_assignments for two users, in a total of three columns. The results should look like this:
WA_Name User_Name1 User_Name2
Crew A Bob Joe
Crew B Joe
Crew C Bob
Basically, every work_assignment that either of the two user has, with the name(s) of the user(s) who has it.
Here's the closest I could come up with (well, I did come up with an ugly query with 3 subqueries that does the job, but it seems like there should be a more elegant solution):
select distinct * from (
select wa.name work_assignment,
per.name user_name1,
per2.name user_name2
from work_assignments wa join current_assignments ca on wa.wa_id = ca.wa_id
join current_assignments ca2 on wa.wa_id = ca2.wa_id
left outer join persons per on per.per_id = ca.per_id and per.user_id = 'X12345'
left outer join persons per2 on per2.per_id = ca2.per_id and per2.user_id = 'Y67890'
)
where user_name1 is not null or user_name2 is not null
order by 1;
The problem with this one is that if both users have a work assignment, it shows 3 records: one for Bob, one for Joe, and one for both:
WA_Name User_Name1 User_Name2
Crew A Bob Joe
Crew A Joe
Crew A Bob
Please help!
Thanks,
Dan

I created a set of sample data/tables
drop table persons;
drop table work_assgn;
drop table curr_assgn;
create table persons(
per_id number(10) not null
, user_name varchar2(10) not null
, user_id varchar2(10) not null
)
;
insert into persons values( 1, 'Bob', 'X123' );
insert into persons values( 2, 'Joe', 'Y456' );
insert into persons values( 3, 'Mike', 'Z789' );
insert into persons values( 4, 'Jeff', 'J987' );
commit;
create table work_assgn(
wa_id number(10) not null
, wa_name varchar2(25)
)
;
insert into work_assgn values( 10, 'Crew A' );
insert into work_assgn values( 20, 'Crew B' );
insert into work_assgn values( 30, 'Crew C' );
insert into work_assgn values( 40, 'Crew D' );
commit;
create table curr_assgn(
wa_id number(10) not null
, per_id number(10) not null
)
;
insert into curr_assgn values( 10, 1 );
insert into curr_assgn values( 10, 2 );
insert into curr_assgn values( 20, 2 );
insert into curr_assgn values( 30, 1 );
insert into curr_assgn values( 40, 4 );
commit;
select * from persons;
select * from work_assgn;
select * from curr_assgn;
So the data looks like
PERSONS
PER_ID USER_NAME USER_ID
---------- ---------- ----------
1 Bob X123
2 Joe Y456
3 Mike Z789
4 Jeff J987
WORK_ASSGN
WA_ID WA_NAME
---------- -------------------------
10 Crew A
20 Crew B
30 Crew C
40 Crew D
CURRASSGN
WA_ID PER_ID
---------- ----------
10 1
10 2
20 2
30 1
40 4
One approach may be to use a PIVOT
with assignment as
(
select p.user_id, p.user_name, a.wa_name
from persons p
join curr_assgn c
on p.per_id =c.per_id
join work_assgn a
on a.wa_id = c.wa_id
where p.user_id in ( 'X123', 'Y456' )
)
select * from assignment
pivot
( max(user_name) for user_id in ( 'X123', 'Y456' )
)
;

Related

How to establish many-many relationship in a oracle database?

I'm intended to develop a database model for my department. I found it difficult to establish the relationship between student, courses and staffs considering that any number of students can elect any number of courses and any number of staffs can handle any number of courses. How will I be able to represent this data in an oracle database?
What did you manage to do so far? What kind of difficulties did you meet?
Anyway: here's a suggestion, see whether it helps. An example is based on your STUDENT and COURSES tables. Idea is to include additional "cross" table which maps courses and students, i.e. contains columns that make primary keys of both tables, they are constrained by foreign key constraints and both of them make the primary key of the new, cross table.
Here's the code:
Create tables:
SQL> -- Students
SQL> create table t_student
2 (id_student number constraint pk_stu primary key,
3 student_name varchar2(20) not null
4 );
Table created.
SQL> -- Courses
SQL> create table t_course
2 (id_course number constraint pk_cou primary key,
3 course_name varchar2(20) not null
4 );
Table created.
SQL> -- Additional "cross" table
SQL> create table t_stu_x_cou
2 (id_student number constraint fk_sxc_stu
3 references t_student (id_student),
4 id_course number constraint fk_sxc_cou
5 references t_course (id_course),
6 constraint pk_sxc primary key (id_student, id_course)
7 );
Table created.
Insert sample data:
SQL> insert into t_student (id_student, student_name)
2 select 1, 'Little' from dual union
3 select 2, 'Foot' from dual;
2 rows created.
SQL> insert into t_course (id_course, course_name)
2 select 100, 'Mathematics' from dual union
3 select 200, 'Physics' from dual union
4 select 300, 'Chemistry' from dual;
3 rows created.
SQL> -- Mapping students and courses:
SQL> -- - student 1 takes 2 courses (100 and 300)
SQL> -- - student 2 takes 3 courses (100, 200 and 300)
SQL> insert into t_stu_x_cou (id_student, id_course)
2 select 1, 100 from dual union
3 select 1, 300 from dual union
4 --
5 select 2, 100 from dual union
6 select 2, 200 from dual union
7 select 2, 300 from dual;
5 rows created.
Select that shows courses taken by student 1:
SQL> select s.student_name, c.course_name
2 from t_stu_x_cou x
3 join t_student s on s.id_student = x.id_student
4 join t_course c on c.id_course = x.id_course
5 where s.id_student = 1;
STUDENT_NAME COURSE_NAME
-------------------- --------------------
Little Mathematics
Little Chemistry
SQL>
Now, try to add the STAFF table yourself, using the same principle (you'd add a new "cross" table between STAFF and COURSES).

Selecting the n'th range/island of rows where columns have a common value?

I need to select all rows (for a range) which have a common value within a column.
For example (starting from the last row)
I try to select all of the rows where _user_id == 1 until _user_id != 1 ?
In this case resulting in selecting rows [4, 5, 6]
+------------------------+
| _id _user_id amount |
+------------------------+
| 1 1 777 |
| 2 2 1 |
| 3 2 11 |
| 4 1 10 |
| 5 1 100 |
| 6 1 101 |
+------------------------+
/*Create the table*/
CREATE TABLE IF NOT EXISTS t1 (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
_user_id INTEGER,
amount INTEGER);
/*Add the datas*/
INSERT INTO t1 VALUES(1, 1, 777);
INSERT INTO t1 VALUES(2, 2, 1);
INSERT INTO t1 VALUES(3, 2, 11);
INSERT INTO t1 VALUES(4, 1, 10);
INSERT INTO t1 VALUES(5, 1, 100);
INSERT INTO t1 VALUES(6, 1, 101);
/*Check the datas*/
SELECT * FROM t1;
1|1|777
2|2|1
3|2|11
4|1|10
5|1|100
6|1|101
In my attempt I use Common Table Expressions to group the results of _user_id. This gives the index of the last row containing a unique value (eg. SELECT _id FROM t1 GROUP BY _user_id LIMIT 2; will produce: [6, 3])
I then use those two values to select a range where LIMIT 1 OFFSET 1 is the lower end (3) and LIMIT 1 is the upper end (6)
WITH test AS (
SELECT _id FROM t1 GROUP BY _user_id LIMIT 2
) SELECT * FROM t1 WHERE _id BETWEEN 1+ (
SELECT * FROM test LIMIT 1 OFFSET 1
) and (
SELECT * FROM test LIMIT 1
);
Output:
4|1|10
5|1|100
6|1|101
This appears to work ok at selecting the last "island" but what I really need is a way to select the n'th island.
Is there a way to generate a query capable of producing outputs like these when provided a parameter n?:
island (n=1):
4|1|10
5|1|100
6|1|101
island (n=2):
2|2|1
3|2|11
island (n=3):
1|1|777
Thanks!
SQL tables are unordered, so the only way to search for islands is to search for consecutive _id values:
WITH RECURSIVE t1_with_islands(_id, _user_id, amount, island_number) AS (
SELECT _id,
_user_id,
amount,
1
FROM t1
WHERE _id = (SELECT max(_id)
FROM t1)
UNION ALL
SELECT t1._id,
t1._user_id,
t1.amount,
CASE WHEN t1._user_id = t1_with_islands._user_id
THEN island_number
ELSE island_number + 1
END
FROM t1
JOIN t1_with_islands ON t1._id = (SELECT max(_id)
FROM t1
WHERE _id < t1_with_islands._id)
)
SELECT *
FROM t1_with_islands
ORDER BY _id;

SQL Help Using NESTED SELECTION

Here's my table
CREATE TABLE emp (num INTEGER NOT NULL,
name VARCHAR(20) NOT NULL,
dept VARCHAR(20) NOT NULL,
salary INTEGER NOT NULL,
boss INTEGER NOT NULL,
PRIMARY KEY (num),
FOREIGN KEY (boss) REFERENCES emp (num)
);
INSERT INTO emp
VALUES ('1', 'PRESIDENT', 'gh', '10000', '1');
INSERT INTO emp
VALUES ('2', 'Bob', 'Slave', '6456', '3');
INSERT INTO emp
VALUES ('3', 'Matthew', 'M', '1', '1');
INSERT INTO emp
VALUES ('4', 'Marl', 'P', '534465', '2');
INSERT INTO emp
VALUES ('5', 'Apple', 'P', '554545646512', '2');
INSERT INTO emp
VALUES ('6', 'Roy', 'Slave', '125', '1');
INSERT INTO emp
VALUES ('7', 'Marth', 'Slave', '56456', '1');
INSERT INTO emp
VALUES ('8', 'Mart', 'Slave', '98', '3');
Here are my Queries:
SELECT * FROM emp;
SELECT * FROM emp
WHERE boss = (SELECT num FROM emp
WHERE num = boss) AND num != boss;
SELECT e1.num,e1.name FROM emp e1
WHERE
(SELECT e2.salary FROM emp e2
WHERE e2.boss = (SELECT e3.num FROM emp e3
WHERE e3.num = e3.boss) AND e2.num != e2.boss) < 98;
So the first output prints out everything as expected. The second output prints out: Matthew, Roy and Marth as expected.
But the final output prints out one.
This is a practice test question I was given
My goal is print the num and name of everyone working under the president who makes less than MIN of people who aren't working for the president.
I can calculate the MIN with the following code:
SELECT min(salary) FROM emp
WHERE boss != (SELECT num FROM emp
WHERE num = boss);
Ideally I want to replace 98 in the previous query with this statement, but I decided it would be best if I broke it down and tried one thing at a time.
Your problem is too many subqueries.
e2 is completely independent from e1; the e2 subquery returns all salaries of those working under the president; the comparison is then against the first value returned.
You could use a correlated subquery to tie e2 to e1, but you don't need a subquery at all:
SELECT e1.num,
e1.name
FROM emp AS e1
WHERE e1.boss = (SELECT e3.num
FROM emp AS e3
WHERE e3.num = e3.boss)
AND e1.num != e1.boss
AND e1.salary < 98;

How to return dynamic cursor from oracle stored procedure

I have 2 tables in which ID field is common.
I am fetching all records of first table in a cursor.
Then I want to do is that on the basis of each ID from cursor, I want to get the values from second table and then return that.
How can I do that...
Please help !!!
homework?
this is basic SQL. generally you'd join the two tables.
begin
for r_row in (select b.*
from tab1 a
inner join tab2 b
on b.id = a.id)
loop
null; -- do whatever
end loop;
end;
/
if you have an existing cursor and can't change it
eg where your_cursor is just returning an ID column.
begin
open your_cursor;
loop
fetch your_cursor into v_id;
exit when your_cursor%notfound;
for r_row in (select * from tab2 b where b.id = v_id)
loop
null; -- do whatever here.
end loop;
end loop;
end;
/
edit:
as per comments:
some sample data:
SQL> create table table1 (id number primary key, name varchar2(20));
Table created.
SQL> create table table2 (id number, col1 varchar2(20), col2 varchar2(20));
Table created.
SQL> insert into table1 values (1, 'test');
1 row created.
SQL> insert into table1 values (2, 'foo');
1 row created.
SQL>
SQL> insert into table2 values (1, 'John', 'Smith');
1 row created.
SQL> insert into table2 values (1, 'Peter', 'Jones');
1 row created.
SQL> insert into table2 values (1, 'Jane', 'Doe');
1 row created.
SQL> insert into table2 values (2, 'Nina', 'Austin');
1 row created.
SQL> insert into table2 values (2, 'Naman', 'Goyal');
1 row created.
SQL> commit;
Commit complete.
create a type to hold the return structure. note the datatypes NEED to match the datatypes of the tables table1 and table2 (%type won't work, so make sure they match)
SQL> create type my_obj as object (
2 id number,
3 name varchar2(20),
4 col1 varchar2(20),
5 col2 varchar2(20)
6 );
7 /
Type created.
SQL> create type my_tab as table of my_obj;
2 /
Type created.
now create your function (you can put this in a package if, in your real code, you have it that way).
SQL> create function function1
2 return my_tab pipelined
3 is
4 begin
5 for r_row in (select t1.id, t1.name, t2.col1, t2.col2
6 from table1 t1
7 inner join table2 t2
8 on t1.id = t2.id)
9 loop
10 pipe row(my_obj(r_row.id, r_row.name, r_row.col1, r_row.col2));
11 end loop;
12 end;
13 /
Function created.
SQL>
SQL> select *
2 from table(function1);
ID NAME COL1 COL2
---------- -------------------- -------------------- --------------------
1 test John Smith
1 test Peter Jones
1 test Jane Doe
2 foo Nina Austin
2 foo Naman Goyal
you could pass inputs if required into that function eg table(function1('a', 'b')); etc..

Get the most recent record for each user where value is 'K', action id is null or its state is 1

I have the following tables in SQL Server:
user_id, value, date, action_id
----------------------------------
1 A 1/3/2012 null
1 K 1/4/2012 null
1 B 1/5/2012 null
2 X 1/3/2012 null
2 K 1/4/2012 1
3 K 1/3/2012 null
3 L 1/4/2012 2
3 K 1/5/2012 3
4 K 1/3/2012 null
action_id, state
----------------------------------
1 0
2 1
3 1
4 0
5 1
I need to return the most recent record for each user where the value is 'K', the action id is either null or its state is set to 1. Here's the result set I want:
user_id, value, date, action_id
----------------------------------
3 K 1/5/2012 3
4 K 1/3/2012 null
For user_id 1, the most recent value is B and its action id is null, so I consider this the most recent record, but it's value is not K.
For user_id 2, the most recent value is K, but action id 1 has state 0, so I fallback to X, but X is not K.
user_id 3 and 4 are straightforward.
I'm interested in Linq to SQL query in ASP.NET, but for now T-SQL is fine too.
The SQL query would be :
Select Top 1 T1.* from Table1 T1
LEFT JOIN Table2 T2
ON T1.action_id = T2.action_id
Where T1.Value = 'K' AND (T1.action_id is null or T2.state = 1)
Order by T1.date desc
LINQ Query :
var result = context.Table1.Where(T1=> T1.Value == "K"
&& (T1.action_id == null ||
context.Table2
.Where(T2=>T2.State == 1)
.Select(T2 => T2.action_id).Contains(T1.action_id)))
.OrderByDescending(T => T.date)
.FirstOrDefault();
Good Luck !!
This query will return desired result set:
SELECT
*
FROM
(
SELECT
user_id
,value
,date
,action_id
,ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY date DESC) RowNum
FROM
testtable
WHERE
value = 'K'
) testtable
WHERE
RowNum = 1
You can also try following approach if user_id and date combination is unique
Make sure to get the order of predicates in the join to be able to use indexes:
SELECT
testtable.*
FROM
(
SELECT
user_id
,MAX(date) LastDate
FROM
testtable
WHERE
value = 'K'
GROUP BY
user_id
) tblLastValue
INNER JOIN
testtable
ON
testtable.user_id = tblLastValue.user_id
AND
testtable.date = tblLastValue.LastDate
This would select the top entries for all users as described in your specification, as opposed to TOP 1 which just selects the most recent entry in the database. I'm assuming here that your tables are named users and actions:
WITH usersactions as
(SELECT
u.user_id,
u.value,
u.date,
u.action_id,
ROW NUMBER() OVER (PARTITION BY u.user_id ORDER BY u.date DESC, u.action_id DESC) as row
FROM users u
LEFT OUTER JOIN actions a ON u.action_id = a.action_id
WHERE
u.value = 'K' AND
(u.action_id IS NULL OR a.state = 1)
)
SELECT * FROM usersactions WHERE row = 1
Or if you don't want to use a CTE:
SELECT * FROM
(SELECT
u.user_id,
u.value,
u.date,
u.action_id,
ROW NUMBER() OVER (PARTITION BY u.user_id ORDER BY u.date DESC, u.action_id DESC) as row
FROM users u
LEFT OUTER JOIN actions a ON u.action_id = a.action_id
WHERE
u.value = 'K' AND
(u.action_id IS NULL OR a.state = 1)
) useractions
WHERE row = 1

Resources