How to Retrieve all the recursive parent of child row in Oracle SQL? - oracle

I have a hierarchical data of Category-Products. This is 3 level hierarchy & all the products will always be assigned to the last level. I want do display a drill down report of all the products grouped by Category - Sub Category & Sub Sub Category. Only those categories will be displayed on report for which we have product result. (Result Products are decided by some other criteria, out of scope for this question).
How can I get all the Category data till root level in oracle.
Sample Data
CategoryId Name Parent
1 Clothing NULL
2 Men's Wear 1
3 Shirt 2
4 T-Shirt 2
5 Women's Wear 1
6 Salwar 5
7 Saree 5
8 Electronics NULL
9 Computers 8
10 Mobiles 8
Products table will have Category Id reference. Ex. 3, 4 or 6, 7 etc
I want to retrieve only categories till root level where we have products. I have below query but I am not sure if this is good practice to specify multiple values for START WITH clause. Is there any better option?
SELECT DISTINCT CategoryId,Name,Parent
FROM tblCategory
START WITH CategoryId IN (3,6)
CONNECT BY CategoryId = PRIOR Parent
For above query I have specified only two categories but in real world it could be thousands. Below is result data showing only categories for selected products.
Output:
CategoryId Name Parent
1 Clothing NULL
2 Men's Wear 1
3 Shirt 2
5 Women's Wear 1
6 Salwar 5

So you basically solved your problem. You can list ID's as you did or you can store them somewhere and use in IN subquery, like here for instance:
with tblCategory(CategoryId, Name, Parent) as (
select 1, 'Clothing', null from dual union all
select 2, 'Men''s Wear', 1 from dual union all
select 3, 'Shirt', 2 from dual union all
select 4, 'T-Shirt', 3 from dual union all
select 5, 'Women''s Wear', 1 from dual union all
select 6, 'Salwar', 5 from dual union all
select 7, 'Saree', 5 from dual union all
select 8, 'Electronics', null from dual union all
select 9, 'Computers', 8 from dual union all
select 10, 'Mobiles', 8 from dual ),
ids(cid) as (select 3 from dual union all select 6 from dual)
select distinct categoryid, name, parent
from tblcategory
start with categoryid in (select cid from ids)
connect by categoryid = prior parent
Result:
CATEGORYID NAME PARENT
---------- ------------ ----------
6 Salwar 5
3 Shirt 2
5 Women's Wear 1
2 Men's Wear 1
1 Clothing
You could also produce more readable output like here:
select connect_by_root(categoryid) root,
sys_connect_by_path(name, ' => ') path
from tblcategory
where connect_by_isleaf = 1
start with categoryid in (select cid from ids)
connect by categoryid = prior parent
Result:
ROOT PATH
------ --------------------------------------------------------------------------------
3 => Shirt => Men's Wear => Clothing
6 => Salwar => Women's Wear => Clothing

Related

Oracle SQL Total Products By Category

I have an exercise that says that I have to do this query: Names of the product categories (CategoryName) and total number of products for each of the categories
I have two tables:
-The first table is called "Categories" where the category of the products is found
-The second table is called "Products" and it contains the products
The primary key "Categoryid" of Categories is shared as a foreign with Products, so I think what to do is count how many products each id has and display the name on the left
I am going to leave two examples with the content of the two tables, since the two tables cannot be joined, but not how to count the number of products for each category
Table Categories:
| Categoryid | Categoryname ||
| -------- | ------------- ||
| 1 | Beverages ||
| 2 | Condiments ||
| 3 | Confections ||
Table Products:
| Productid | Productname | Categoryid ||
| -------- | ------------- | ---------- ||
| 1 | Chai | 1 ||
| 2 | Chang | 1 ||
| 3 | Tofu | 5 ||
How it should come out:
| CategoryName | TotalProducts||
| -------- | ----------- ||
| Beverages | 10 ||
| Condiments | 5 ||
| Confections | 3 ||
I don't know how to count the number of products for each category
i try this:
SELECT Categoryname COUNT(*)
FROM Categories JOIN Products ON Categories.Categoryid=Products.Categoryid;
Looks like outer join (so that you would display categories that don't have any products), counting products (not "generally" because you'd get false result; I'll show what I mean).
Sample data (your data is wrong; you can't have Tofu in category 5 if that category doesn't exist; foreign key constraint wouldn't allow it):
SQL> with
2 categories (categoryid, categoryname) as
3 (select 1, 'beverages' from dual union all
4 select 2, 'condiments' from dual union all
5 select 3, 'confections' from dual union all
6 select 5, 'category 5' from dual
7 ),
8 products (productid, productname, categoryid) as
9 (select 1, 'chai' , 1 from dual union all
10 select 2, 'chang', 1 from dual union all
11 select 3, 'tofu' , 5 from dual
12 )
Query: count products (line #14):
13 select c.categoryname,
14 count(p.productid) number_of_products
15 from categories c left join products p on p.categoryid = c.categoryid
16 group by c.categoryname;
CATEGORYNAME NUMBER_OF_PRODUCTS
-------------------- ------------------
beverages 2
category 5 1
condiments 0
confections 0
SQL>
If you used count(*) (line #14), you'd get wrong result as you'd count category itself:
13 select c.categoryname,
14 count(*) number_of_products
15 from categories c left join products p on p.categoryid = c.categoryid
16 group by c.categoryname;
CATEGORYNAME NUMBER_OF_PRODUCTS
-------------------- ------------------
beverages 2
category 5 1
condiments 1 --> no products in
confections 1 --> these two categories!
SQL>
However, if you aren't interested in categories that don't have any products, inner join and count(*) (as well as count(p.productid)) would do:
13 select c.categoryname,
14 count(*) number_of_products
15 from categories c join products p on p.categoryid = c.categoryid
16 group by c.categoryname;
CATEGORYNAME NUMBER_OF_PRODUCTS
-------------------- ------------------
beverages 2
category 5 1
SQL>
For each category I listed all the products and its count.
CREATE TABLE categories(
category_id, category_name) AS
SELECT 1, 'Beverages' FROM DUAL UNION ALL
SELECT 2, 'Condiments' FROM DUAL UNION ALL
SELECT 3, 'Confections' FROM DUAL;
CREATE TABLE products(
product_id, product_name, category_id) AS
SELECT 1, 'Chai',1 FROM DUAL UNION ALL
SELECT 2, 'Chang',1 FROM DUAL UNION ALL
SELECT 3, 'Tofu', 2 FROM DUAL;
select c.category_id,
c.category_name,
listagg(p.product_name,', ') within group(order by p.product_name) product_list,
count(p.product_name) cnt
from categories c,
products p
where c.category_id = p.category_id(+)
group by c.category_id,
c.category_name
order by c.category_id,
c.category_name
/
CATEGORY_ID CATEGORY_NA PRODUCT_LIST CNT
----------- ----------- -------------------- ----------
1 Beverages Chai, Chang 2
2 Condiments Tofu 1
3 Confections 0

I need 2 count columns in the same query in ORACLE

I'm trying to get the unique number of invoices a company has received and sent out using 2 count() functions. In invoices table there are two columns that are references to the same company id (one is id of a company that is sending an invoice and the other one is id of a company that is receiving an invoice)
This is the code I tried using:
SELECT K.ID,K.NAME,K.CITY, COUNT(*) AS NUM_OF_INVOICES_SENT, COUNT(*) AS NUM_OF_INVOICES_RECEIVED
FROM COMPANY K LEFT JOIN INVOICE F ON F.COMP_SNEDING = K.ID
GROUP BY K.NAME,K.ID,K.CITY
This is for a school project so I am in no means well versed in sql/oracle
actual data invoices:
actual data company:
desired outcome with given actual data:
Here's one option; it doesn't use count, but sum with case expression.
Sample data:
SQL> with
2 invoice (id, amount, comp_sending, comp_receiving) as
3 (select 1, 2000 , 1, 2 from dual union all
4 select 2, 28250, 3, 2 from dual union all
5 select 3, 8700 , 4, 1 from dual union all
6 select 4, 20200, 5, 3 from dual union all
7 select 5, 21500, 3, 4 from dual
8 ),
9 company (id, name, city, state) as
10 (select 1, 'Microsoft', 'Redmond' , 'Washington' from dual union all
11 select 2, 'Ubisoft' , 'Paris' , 'France' from dual union all
12 select 4, 'Starbucks', 'Seattle' , 'Washington' from dual union all
13 select 5, 'Apple' , 'Cupertino', 'California' from dual union all
14 select 3, 'Nvidia' , 'Cupertino', 'California' from dual
15 )
Query begins here:
16 select c.id, c.name,
17 sum(case when c.id = i.comp_sending then 1 else 0 end) cnt_sent,
18 sum(case when c.id = i.comp_receiving then 1 else 0 end) cnt_received
19 from company c left join invoice i on c.id in (i.comp_sending, i.comp_receiving)
20 group by c.id, c.name
21 order by c.id;
ID NAME CNT_SENT CNT_RECEIVED
---------- --------- ---------- ------------
1 Microsoft 1 1
2 Ubisoft 0 2
3 Nvidia 2 1
4 Starbucks 1 1
5 Apple 1 0
SQL>
You can use COUNT if you replace the 0 in the CASE expressions with NULL. So #Littlefoot's query becomes
select c.id, c.name,
COUNT(case when c.id = i.comp_sending then 1 else NULL end) cnt_sent,
COUNT(case when c.id = i.comp_receiving then 1 else NULL end) cnt_received
from company c left join invoice i on c.id in (i.comp_sending, i.comp_receiving)
group by c.id, c.name
order by c.id;
This works because COUNT counts only those rows which have a non-NULL value in the expression which is being counted.
db<>fiddle here

how to filter history data based on most recent record?

Table: HISTORY
CUSTOMER MONTH PLAN
1 1 A
1 2 A
1 2 B
1 3 B
In this example customer 1 had plan A and changed to B on month 2. I need to remove the change from month 2 and keep only the plan the customer migrate to, as in:
CUSTOMER MONTH PLAN
1 1 A
1 2 B
1 3 B
I've tried using sys_connect_by_path:
select month, CUSTOMER, level,
sys_connect_by_path(PLAN, '/') as path
from a
start with month = 1
connect by prior MONTH = MONTH - 1
But it doesn't seem to be right. Whats an efficient way of doing it in Oracle 12c?
I'm not sure whether you understood what comments have said - it is rows 2 and 3 that are questionable because there's no way to know which one of those happened first.
Anyway, as you said that there's nothing else in that table that would help us decide, how about something like this? Compare current plan with the next plan (sorted by month) and pick rows where there's no change in plan.
SQL> with test (customer, month, plan) as
2 (select 1, 1, 'A' from dual union all
3 select 1, 2, 'A' from dual union all
4 select 1, 2, 'B' from dual union all
5 select 1, 3, 'B' from dual
6 ),
7 inter as
8 (select customer, month, plan,
9 nvl(lead(plan) over (partition by customer order by month), plan) lead_plan
10 from test
11 )
12 select customer, month, plan
13 from inter
14 where plan = lead_plan
15 order by month;
CUSTOMER MONTH PLAN
---------- ---------- -----
1 1 A
1 2 B
1 3 B
SQL>
You could use an analytic lead() call to peek at the following month, and decide whether to use the current or following month's plan:
-- CTE for your sample data
with history (customer, month, plan) as (
select 1, 1, 'A' from dual
union all select 1, 2, 'A' from dual
union all select 1, 2, 'B' from dual
union all select 1, 3, 'B' from dual
)
-- actual query
select distinct customer, month,
case
when lead(plan) over (partition by customer order by month) != plan
then lead(plan) over (partition by customer order by month)
else plan
end as plan
from history;
CUSTOMER MONTH P
---------- ---------- -
1 1 A
1 2 B
1 3 B
You could move the lead() calculation into an inline view to reduce the repetition if you prefer.
This will probably break in interesting ways if a customer changes plan on consecutive months though, or possibly if they can change more than once in one month.

Can I update a particular attribute of a tuple with the same attribute of another tuple of same table? If possible what should be the algorithm?

Suppose I have a table with 10 records/tuples. Now I want to update an attribute of 6th record with the same attribute of 1st record, 2nd-7th, 3rd-8th, 4th-9th, 5th-10th in a go i.e. without using cursor/loop. Use of any number of temporary table is allowed. What is the strategy to do so?
PostgreSQL (and probably other RDBMSes) let you use self-joins in UPDATE statements just as you can in SELECT statements:
UPDATE tbl
SET attr = t2.attr
FROM tbl t2
WHERE tbl.id = t2.id + 5
AND tbl.id >= 6
This would be easy with an update-with-join but Oracle doesn't do that and the closest substitute can be very tricky to get to work. Here is the easiest way. It involves a subquery to get the new value and a correlated subquery in the where clause. It looks complicated but the set subquery should be self-explanatory.
The where subquery really only has one purpose: it connects the two tables, much as the on clause would do if we could do a join. Except that the field used from the main table (the one being updated) must be a key field. As it turns out, with the self "join" being performed below, they are both the same field, but it is required.
Add to the where clause other restraining criteria, as shown.
update Tuples t1
set t1.Attr =(
select t2.Attr
from Tuples t2
where t2.Attr = t1.Attr - 5 )
where exists(
select t2.KeyVal
from Tuples t2
where t1.KeyVal = t2.KeyVal)
and t1.Attr > 5;
SqlFiddle is pulling a hissy fit right now so here the data used:
create table Tuples(
KeyVal int not null primary key,
Attr int
);
insert into Tuples
select 1, 1 from dual union all
select 2, 2 from dual union all
select 3, 3 from dual union all
select 4, 4 from dual union all
select 5, 5 from dual union all
select 6, 6 from dual union all
select 7, 7 from dual union all
select 8, 8 from dual union all
select 9, 9 from dual union all
select 10, 10 from dual;
The table starts out looking like this:
KEYVAL ATTR
------ ----
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
10 10
with this result:
KEYVAL ATTR
------ ----
1 1
2 2
3 3
4 4
5 5
6 1
7 2
8 3
9 4
10 5

how to add different column with one code into one column?

id text
1 hi
1 how are u
1 fine ?
2 rad
2 qey
3
I am searching for a query where it can let me insert id = 1 to another table in one column
id test
1 hi how are you fine ?
2 rad qey
with the listagg function , i will have such result : hi how are you fine ? .. can i have it such
hi
how are u
fine ?
This query can be used to achieve your result:
WITH tab(id,text) AS (
SELECT 1, 'hi' FROM dual UNION ALL
SELECT 1, 'how are u' FROM dual UNION ALL
SELECT 1, 'fine ?' FROM dual UNION ALL
SELECT 2, 'rad' FROM dual UNION ALL
SELECT 2, 'qey' FROM dual UNION ALL
SELECT 3, NULL FROM dual)
-----
--End of data
-----
SELECT ID,
listagg(text, ' ') within GROUP (ORDER BY ROWNUM) AS text
FROM tab
GROUP BY ID;
Output
ID TEXT
1 hi how are u fine ?
2 rad qey
3
But there is one problem, although order by rownum is used but you cannot guaranty the order of text, its better to have one more column in your table that can define the order of the text as below
id text order_text
1 hi 1
1 how are u 2
1 fine ? 3
2 rad 1
2 qey 2
3 1
and the use the query as
SELECT ID,
listagg(text, ' ') within GROUP (ORDER BY order_text) AS text
FROM tab
GROUP BY ID;
You can try the LISTAGG function.
For more information, see here.

Resources