SQL: Recursion With a Join Confusion - oracle

I'll start by laying out what I'm trying to do and then I'll list the code I've made so far.
I'm coding in Oracle PL/SQL on the Application Express Platform
I have two tables: USERS and LEADS.
Leads Columns: LEADID, COMPANYNAME, CONTACTNAME, OWNER
Users Columns: EMAIL, SUPER, ROLE
Foreign Keys:
OWNER is a foreign key that refers to EMAIL in USERS
SUPER is a foreign key that refers to EMAIL in USERS
SUPER is the supervisor of a given person. ROLE is their position in the company
There are about 5 levels. 'PEON','MGR','DIR','SRDIR','VP'
Peons are the only people with leads assigned to them.
I'm trying to generate a report that returns rows containing the following
SUBORDINATE, NUMLEADS
Subordinate is anyone directly under the user using the application. I have code for that
select U.EMAIL as Subordinate
from USERS U
WHERE lower(v('APP_USER')) = U.SUPER
Numleads is all the leads created by peons under the subordinate's organization. I currently have code to list the number of peons under the current user
select count(*)
from USERS U2
where U2.ROLE = 'PEON'
start with lower(v('APP_USER')) = U2.EMAIL
connect by NOCYCLE prior U2.email = U2.super
I'm part of the way there, but I'm confused how to reference the result of a query in a recursive sequence. I know I need to query all PEONS under the subordinates of the current user, JOIN them with all leads they're associated with, and then count the number of leads. But i'm not sure how to order that in SQL.
Your help is much appreciated
EDIT: Answer figured out thanks to JBrooks
select U.EMAIL as Sub, count(*) as CreatedAllTime
from USERS U
left join USERS UPEON
on UPEON.EMAIL in
(
select UPEON2.EMAIL
from USERS UPEON2
where UPEON2.ROLE = 'PEON'
start with U.EMAIL = UPEON2.EMAIL
connect by NOCYCLE prior UPEON2.email = UPEON2.super
)
left join LEADS L
on UPEON.EMAIL = L.OWNER
where U.EMAIL in
(
select U2.EMAIL as Sub
from USERS U2
WHERE lower(v('APP_USER')) = U2.SUPER
)
group by U.EMAIL

select U2.Email as Subordinate,
count(*) as NumLeads
from USERS U2
left join LEADS l
on U2.Email = l.Owner
where U2.ROLE = 'PEON'
and lower(v('APP_USER')) in
(select EMAIL
from USERS S
START WITH lower(v('APP_USER')) = lower(S.SUPPER)
CONNECT BY PRIOR EMAIL = SUPPER)
group by U2.Email
order by U2.Email

Related

Optimizing Left Join With Group By and Order By (MariaDb)

I am attempting to optimize a query in MariaDb that is really bogged down by its ORDER BY clause. I can run it in under a tenth of a second without the ORDER BY clause, but it takes over 25 seconds with it. Here is the gist of the query:
SELECT u.id, u.display_name, u.cell_phone, u.email,
uv.year, uv.make, uv.model, uv.id AS user_vehicle_id
FROM users u
LEFT JOIN user_vehicles uv ON uv.user_id = u.id AND uv.current_owner=1
WHERE u.is_deleted = 0
GROUP BY u.id
ORDER BY u.display_name
LIMIT 0, 10;
I need it to be a left join because I want to include users that aren't linked to a vehicle.
I need the group by because I want only 1 result per user (and display_name is not guaranteed to be unique).
users table has about 130K rows, while user_vehicles has about 230K rows.
Here is the EXPLAIN of the query:
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE u index dms_cust_idx PRIMARY 4 null 124825 Using where; Using temporary; Using filesort
1 SIMPLE uv ref user_idx user_idx 4 awscheduler.u.id 1 Using where
I have tried these two indices to speed things up, but they don't seem to do much.
CREATE INDEX idx_display_speedy ON users(display_name);
CREATE INDEX idx_display_speedy2 ON users(id, display_name, is_deleted, dms_cust_id);
I am looking for ideas on how to speed this up. I attempted using nested queries, but since the order by is the bottleneck & order within the nested query is ignored, I believe that attempt was in vain.
how about:
WITH a AS (
SELECT u.id, u.display_name, u.cell_phone, u.email
FROM users u
WHERE u.is_deleted = 0
GROUP BY u.id
LIMIT 0, 10
)
SELECT a.id, a.display_name, a.cell_phone, a.email,
uv.year, uv.make, uv.model, uv.id AS user_vehicle_id
FROM a LEFT JOIN user_vehicles uv ON uv.user_id = a.id AND uv.current_owner=1
ORDER BY a.display_name;
The intention is we take a subset of users before joining it with user_vehicles.
Disclaimer: I haven't verified if its faster or not, but have similar experience in the past where this helps.
with a as (
SELECT u.id, u.display_name, u.cell_phone, u.email,
uv.year, uv.make, uv.model, uv.id AS user_vehicle_id
FROM users u
LEFT JOIN user_vehicles uv ON uv.user_id = u.id AND uv.current_owner=1
WHERE u.is_deleted = 0
GROUP BY u.id
)
select * from a
ORDER BY u.display_name;
)
I suspect it's not actually the ordering that is causing the problem... If you remove the limit, I bet the ordered and un-ordered versions will end up performing pretty close to the same.
Depending on if your actual query is as simple as the one you posted, you may be able to get good performance in a single query by using RowNum() as described here:
SELECT u.id, u.display_name, u.cell_phone, u.email,
uv.year, uv.make, uv.model, uv.id AS user_vehicle_id
FROM (
SELECT iu.id, iu.display_name, iu.cell_phone, iu.email
FROM users iu
WHERE iu.is_deleted = 0
ORDER BY iu.display_name) as u
LEFT JOIN user_vehicles uv ON uv.user_id = u.id AND uv.current_owner=1
WHERE ROWNUM() < 10
GROUP BY u.id
ORDER BY u.display_name
If that doesn't work, you probably need to select the users in one select and then select their vehicles in a second Select

I'd like to return a single row in a query that joins two tables through a one to many relationship

This is an oracle system. I have a client table (one) and an account table (many). Frankly, I really just want to query the client table but due to poor design, there is a data element that I need for the client table that is only present on the account table - it will be the same value for all accounts for that client so I really just need to return one account row but I'm having problems accomplishing this. Here is the query I tried:
Select
c.client_num,
c.client_name,
a.agency_value
from client c
inner join account a on c.client_num = a.client_num
where a.account_num in (select a2.account_num from account a2 where rownum = 1)
You need to explicitly designate how to pick one record out of many. Here's one method - use MAX
Select
c.client_num,
c.client_name,
(SELECT
MAX(a.agency_value) agency_value
FROM account a
where c.client_num = a.client_num
-- not sure if this line is required - if not please remove
and a.account_num in (select a2.account_num from account a2 where rownum = 1)
) agency_value
from client c
Keep in mind that by implementing this you are "cementing" your bad table design.
Are you absolutely certain that there is only ever one agency_value? use query to find any clients that have more than one agency:
SELECT
a.client_num,
COUNT(DISTINCT a.agency_value) CountOfAgencyValues,
MAX(a.agency_value) max_agency_value,
MIN(a.agency_value) max_agency_value
FROM account a
GROUP BY a.client_num
HAVING COUNT(DISTINCT a.agency_value) > 1
With your input and doing some playing around on my own, here is the code that ultimately addressed my need:
select
c. client_num,
(select a.agency_value
from account a
where a.client_num = c.client_num
and rownum = 1)
from client c

Customer details based on booking count

Write a query to display user id and user name where the number of seats booked by the user in the individual booking is greater than 1. Display records in ascending order by user name.
I have tried this query and I m getting errors. Please help me!!
select u.user_id,u.name
from users u join bookingdetails bd
on u.name=bd.name
join tickets t on u.user_id=t.user_id
group by u.name
having count(t.no_seats) > 1
order by u.name;
select distinct u.user_id,u.name from users u
join tickets t on u.user_id = t.user_id
where
u.user_id in( select user_id from tickets where no_seats>1)
order by u.name;
If I understand your assignment it looks like you should do
WITH cteBookings AS (SELECT bd.USER_ID, SUM(t.NO_SEATS) AS TOTAL_SEATS_BOOKED
FROM BOOKINGDETAILS bd
INNER JOIN PAYMENTS p
ON p.BD_ID = bd.BD_ID
INNER JOIN TICKETS t
ON t.TICKET_ID = p.TICKET_ID
GROUP BY bd.USER_ID)
SELECT DISTINCT b.USER_ID, u.USER_NAME
FROM USERS u
INNER JOIN cteBookings b
ON b.USER_ID = u.USER_ID
WHERE b.TOTAL_SEATS_BOOKED > 1
ORDER BY u.USER_NAME ASC
Best of luck.
According to the question, it asks for the individual bookings > 1
So, the solution is very simple i.e, for each ticket_id, the no of seats should be greater than 1.
SELECT DISTINCT(user_id),name
FROM Tickets inner join Users
USING (user_id)
WHERE no_seats>1
ORDER BY name;
It will simpler if you try it without joins.
A simple example using a subquery:
select user_id, name from users
where user_id = any(select distinct user_id from tickets where no_seats > 1)
order by name asc;

cascading Input Control sql query return error: "ORA-01427: single-row subquery returns more than one row"

looking for solution on my sql query error.I'm trying to create second cascading Input Control in JaspersoftServer. The first Input Control works fine, however when I try to create a second cascade IC it returns with the error. I have 3 tables (user, client, user_client), many to many, so 1 linked table (user_client) between them.The 1st Input Control (client) - works well, end user will select the client, the client can have many users, so cascade is the key. Also, as the output, I would like to get not the user_id, but user's firstname and the lastname as one column field. And here is where i'm stuck. I'm pretty sure it is simple syntaxis error, but spent a good couple of hours to figure out what is wrong with it. Is anyone can have a look at it please and indicate where is the problem in my query ?! So far I've done:
select distinct
u.user_id,(
SELECT CONCAT(first_name, surname) AS user_name from tbl_user ),
c.client_id
FROM tbl_user u
left join tbl_user_client uc
on uc.user_id = u.user_id
left join tbl_client c
on c.client_id = uc.client_id
where c.client_id = uc.client_id
order by c.client_id
Thank you in advance.
P.S. JasperServer + Oracle 11g
You're doing an uncorrelated subquery to get the first/last name from the user table. There is no relationship between that subquery:
SELECT CONCAT(first_name, surname) AS user_name from tbl_user
... and the user ID in the main query, so the subquery will attempt to return every first/last name for all users, for every row your joins find.
You don't need to do a subquery at all as you already have the tbl_user information available:
select u.user_id,
CONCAT(u.first_name, u.surname) AS user_name
c.client_id
FROM tbl_user u
left join tbl_user_client uc
on uc.user_id = u.user_id
left join tbl_client c
on c.client_id = uc.client_id
where c.client_id = uc.client_id
order by c.client_id
If you want to put a space between the first and last name you'll either need nested concat() calls, since that function only takes two arguments:
select u.user_id,
CONCAT(u.first_name, CONCAT(' ', u.surname)) AS user_name
...
... or perhaps more readably use the concatenation operator instead:
select u.user_id,
u.first_name ||' '|| u.surname AS user_name
...
If the first control has selected a client and this query is supposed to find the users related to that client, you're joining the tables the wrong way round, aren't you? And you aren't filtering on the selected client - but no idea how that's actually implemented in Jasper. Maybe you do want the entire list and will filter it on the Jasper side.

select query with if in oracle

I need help! For example, there are four tables: cars, users, departments and join_user_department. Last table used for M: N relation between tables user and department because some users have limited access. I need to get the number of cars in departments where user have access. The table “cars” has a column department_id. If the table join_user_department doesn’t have any record by user_id this means that he have access to all departments and select query must be without any condition. I need do something like this:
declare
DEP_NUM number;--count of departments where user have access
CARS_COUNT number;--count of cars
BEGIN
SELECT COUNT (*) into DEP_NUM from join_user_departments where user_id=?;
SELECT COUNT(*) into CARS_COUNT FROM cars where
IF(num!=0)—it meant that user access is limited
THEN department_id IN (select dep_id from join_user_departments where user_id=?);
A user either has access to all cars (I'm assuming all cars are tied to a department, and the user has access to all departments) or the user has limited access. You can use a UNION ALL to bring these two groups together, and group by user to do a final count. I've cross joined the users with unlimited access to the cars table to associate them with all cars:
(UPDATED to also count the departments)
select user_id,
count(distinct department_id) as dept_count,
count(distinct car_id) as car_count,
from (
select ud.user_id, ud.department_id, c.car_id
from user_departments ud
join cars c on c.department_id = ud.department_id
UNION ALL
select u.user_id, v.department_id, v.car_id
from user u
cross join (
select d.department_id, c.car_id
from department d
join cars c on c.department_id = d.department_id
) v
where not exists (
select 1 from user_departments ud
where ud.user_id = u.user_id
)
)
group by user_id
A UNION ALL is more efficient that a UNION; a UNION looks for records that fall into both groups and throws out duplicates. Since each user falls into one bucket or another, UNION ALL should do the trick (doing a distinct count in the outer query also rules out duplicates).
"If the table join_user_department doesn’t have any record by user_id
this means that he have access to all departments"
This seems like very bad practice. Essentially you are using the absence of records to simulate the presence of records. Very messy. What happens if there is a User who has no access to a Car from any Department? Perhaps the current business logic doesn't allow this, but you have a "data model" which won't allow to implement such a scenario without changing your application's logic.

Resources