Oracle Lead by group - oracle

Hello I've got a problem with lead and retrieving the next value from the next group.
I've got this table:
TableA
-----------------
ID | value
-----------------
1 | 2.5
1 | 1
1 | 4.5
2 | 7
2 | 2
3 | 3
4 | 1
4 | 7
Expected result:
------------------------------
ID | value | lead_id
------------------------------
1 | 2.5 | 2
1 | 1 | 2
1 | 4.5 | 2
2 | 7 | 3
2 | 2 | 3
3 | 3 | 4
4 | 1 | NULL
4 | 7 | NULL
My SQL:
select ID, value, lead(id) OVER (order by id) lead_id from TableA
Is it possible to get that result ?

You can do this by adding in a windowing clause into the first_value analytic function:
with tablea as (select 1 id, 2.5 value from dual union all
select 1 id, 1 value from dual union all
select 1 id, 4.5 value from dual union all
select 2 id, 7 value from dual union all
select 2 id, 2 value from dual union all
select 3 id, 3 value from dual union all
select 4 id, 1 value from dual union all
select 4 id, 7 value from dual)
select id,
value,
first_value(id) over (order by id
range between 1 following and unbounded following) lead_id
from tablea;
ID VALUE LEAD_ID
---------- ---------- ----------
1 2.5 2
1 1 2
1 4.5 2
2 7 3
2 2 3
3 3 4
4 1

I think this gives the right output:
WITH g AS
(SELECT ID, lead(ID) OVER (ORDER BY ID) lead_id
FROM (SELECT DISTINCT ID FROM TableA) )
SELECT ID, VALUE, lead_id
FROM TableA
JOIN g USING (ID)
ORDER BY 1;

SELECT tablea.*, b.nextid FROM tablea
INNER JOIN (SELECT id, LEAD (id) OVER (ORDER BY id) nextid
FROM ( SELECT DISTINCT id
FROM tablea
ORDER BY id)) b
ON tablea.id = b.id
This should work.

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

use LAG with expression in oracle

I have a column (status) in a table that contain numbers and values are 1, 2 or 4.
I would like, in a SQL query, add a calculated column (bitStatus) that will store the bitwise oerator OR for the status column of the current line and the column in the previous line.
like so :
| id | status| bitStatus|
|----|-------|----------|
| 1 | 1 | 1 |
| 2 | 2 | 3 |
| 3 | 4 | 7 |
| 4 | 1 | 7 |
So what I did is to use LAG function in oracle but I coudn't figure out how to do it as long as I want to create only on calculated column bitStatus
my query is like :
select id, status,
BITOR(LAG(bitStatus) OVER (ORDER BY 1), status)) AS bitStatus
But as you know, I can't use LAG(bitStatus) when calculating bitStatus.
So how could I make it the desired table.
Thanks in advance.
Would this help?
lines #1 - 6 represent sample data
the TEMP CTE is here to fetch LAG status value (to improve readability)
the final select does the BITOR operation as bitor(a, b) = a - bitand(a, b) + b
SQL> with test (id, status) as
2 (select 1, 1 from dual union all
3 select 2, 2 from dual union all
4 select 3, 1 from dual union all
5 select 4, 4 from dual
6 ),
7 temp as
8 (select id, status,
9 lag(status) over (order by id) lag_status
10 from test
11 )
12 select id,
13 status,
14 status - bitand(status, nvl(lag_status, status)) + nvl(lag_status, status) as bitstatus
15 from temp
16 order by id;
ID STATUS BITSTATUS
---------- ---------- ----------
1 1 1
2 2 3
3 1 3
4 4 5
SQL>

Oracle: Combine two group by queries (which use aggregate function count()) by union or so to get a consolidated result

I have two tables. TABLE_A and TABLE_B.
Both tables maintain columns to save CREATION_USER. But this column has different name in respective tables.
My motive is to get a count of records each user has created in both tables.
That is, combining result of these two queries with few conditions. The user name should not get repeated and for user names who have created records in both tables, the count should be their sum.
SELECT A.CREATION_USER_A AS "USER",
COUNT(*)
FROM TABLE_A A
GROUP BY A.CREATION_USER_A;
SELECT B.CREATION_USER_B AS "USER",
COUNT(*)
FROM TABLE_B B
GROUP BY B.CREATION_USER_B;
For e.g.,
USER_A has created 2 records in TABLE_A,
USER_B has created 3 records in TABLE_B and
USER_C has created 4 records in TABLE_A and 3 records in TABLE_B.
So the output should look like this:
| USER | COUNT |
| USER_A | 2 |
| USER_B | 3 |
| USER_C | 7 |
I have written a query which does this but it performs really bad.
SELECT A.CREATION_USER_A AS "USER",
(COUNT(A.CREATION_USER_A)+(SELECT COUNT(CREATION_USER_B) FROM TABLE_B WHERE CREATION_USER_B = A.CREATION_USER_A)) AS "COUNT"
FROM TABLE_A A
GROUP BY A.CREATION_USER_A
UNION
SELECT B.CREATION_USER_B,
COUNT(B.CREATION_USER_B)
FROM TABLE_B B
WHERE B.CREATION_USER_B NOT IN (SELECT CREATION_USER_A FROM TABLE_A)
GROUP BY B.CREATION_USER_B;
Please suggest a way to get this done.
You can simply build a set given by the union (keeping duplicates) of all the records in your tables, and then count the records grouping by creation user:
Bulding some sample data:
create table table_a(id, creation_user_a) as (
select 1, 'USER_A' from dual union all
select 1, 'USER_A' from dual union all
select 1, 'USER_C' from dual union all
select 1, 'USER_C' from dual union all
select 1, 'USER_C' from dual union all
select 1, 'USER_C' from dual
);
create table table_b(id, creation_user_b) as (
select 1, 'USER_B' from dual union all
select 1, 'USER_B' from dual union all
select 1, 'USER_B' from dual union all
select 1, 'USER_C' from dual union all
select 1, 'USER_C' from dual union all
select 1, 'USER_C' from dual
)
The query:
select count(1), creation_user
from ( /* the union of all the records from table_a and table_b */
select creation_user_a as creation_user from table_a
union all /* UNION ALL keeps duplicates */
select creation_user_B from table_b
)
group by creation_user
order by creation_user
The result:
2 USER_A
3 USER_B
7 USER_C
The explain plan:
---------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 12 | 96 | 8 (25)| 00:00:01 |
| 1 | SORT ORDER BY | | 12 | 96 | 8 (25)| 00:00:01 |
| 2 | HASH GROUP BY | | 12 | 96 | 8 (25)| 00:00:01 |
| 3 | VIEW | | 12 | 96 | 6 (0)| 00:00:01 |
| 4 | UNION-ALL | | | | | |
| 5 | TABLE ACCESS FULL| TABLE_A | 6 | 48 | 3 (0)| 00:00:01 |
| 6 | TABLE ACCESS FULL| TABLE_B | 6 | 48 | 3 (0)| 00:00:01 |
---------------------------------------------------------------------------------
An alternative (but more complicated, and possibly slower - you'd need to test both to check) solution to Aleksej's answer is to use a full outer join to join both grouped by queries, like so:
WITH table_a AS (SELECT 'USER_A' creation_user_a, 10 val FROM dual UNION ALL
SELECT 'USER_A' creation_user_a, 20 val FROM dual UNION ALL
SELECT 'USER_C' creation_user_a, 30 val FROM dual UNION ALL
SELECT 'USER_C' creation_user_a, 40 val FROM dual UNION ALL
SELECT 'USER_C' creation_user_a, 50 val FROM dual UNION ALL
SELECT 'USER_C' creation_user_a, 60 val FROM dual),
table_b AS (SELECT 'USER_B' creation_user_b, 10 val FROM dual UNION ALL
SELECT 'USER_B' creation_user_b, 20 val FROM dual UNION ALL
SELECT 'USER_B' creation_user_b, 30 val FROM dual UNION ALL
SELECT 'USER_C' creation_user_b, 40 val FROM dual UNION ALL
SELECT 'USER_C' creation_user_b, 50 val FROM dual UNION ALL
SELECT 'USER_C' creation_user_b, 60 val FROM dual)
-- end of mimicking your tables with data in them. See the SQL below:
SELECT COALESCE(a.creation_user_a, b.creation_user_b) "USER",
nvl(a.cnt_a, 0) + nvl(b.cnt_b, 0) total_records
FROM (SELECT creation_user_a,
COUNT(*) cnt_a
FROM table_a
GROUP BY creation_user_a) a
FULL OUTER JOIN (SELECT creation_user_b,
COUNT(*) cnt_b
FROM table_b
GROUP BY creation_user_b) b ON a.creation_user_a = b.creation_user_b
ORDER BY "USER";
USER TOTAL_RECORDS
------ -------------
USER_A 2
USER_B 3
USER_C 7
Thank you for helping me guys. I have found a simpler and more efficient solution. It works.
SELECT CREATION_USER, SUM(TOTAL_COUNT) TOTAL_COUNT FROM
(SELECT /*+ PARALLEL */ A.CREATION_USER_A CREATION_USER,
COUNT(A.CREATION_USER_A) TOTAL_COUNT
FROM TABLE_A A
GROUP BY A.CREATION_USER_A
UNION
SELECT /*+ PARALLEL */ B.CREATION_USER_B CREATION_USER,
COUNT(B.CREATION_USER_B) TOTAL_COUNT
FROM TABLE_B B
GROUP BY B.CREATION_USER_B)
GROUP BY CREATION_USER;

Use SYS_CONNECT_BY_PATH to aggregate values

I would like to generate a data hierarchy.
This query:
select connect_by_root(parent_id) as root_id
,ID, NAME
,SYS_CONNECT_BY_PATH(PARENT_ID,'/') PATH
,level
,line
,LINE*power(10,-level+1) CALC
,ltrim(SYS_CONNECT_BY_PATH(lpad(LINE,3,'0'), '.'),'.') SORT
from (
select 3 ID, 1 LINE, 2 PARENT_ID FROM DUAL
union all
select 4 ID, 2 LINE, 2 PARENT_ID FROM DUAL
union all
select 5 ID, 3 LINE, 2 PARENT_ID FROM DUAL
union all
select 6 ID, 1 LINE, 5 PARENT_ID FROM DUAL
union all
select 7 ID, 1 LINE, 6 PARENT_ID FROM DUAL
) v
start with v.parent_id = 2
connect by nocycle prior id=parent_id
Generates:
ROOT_ID ID PATH LEVEL LINE CALC SORT
2 3 /2 1 1 1 001
2 4 /2 1 2 2 002
2 5 /2 1 3 3 003
2 6 /2/5 2 1 0.1 003.001
2 7 /2/5/6 3 1 0.01 003.001.001
What I would like:
ROOT_ID ID PATH LEVEL LINE CALC
2 3 /2 1 1 1
2 4 /2 1 2 2
2 5 /2 1 3 3
2 6 /2/5 2 1 3.1
2 7 /2/5/6 3 1 3.11
Is there a way to get sys_connect_by_path (or another function) to tally the CALC column and its parents?
Currently, I'm using the SORT field for ordering the rows; I'd rather sort on a proper numerical value (CALC field).
Try this:
select connect_by_root(parent_id) as root_id
,ID
,SYS_CONNECT_BY_PATH(PARENT_ID,'/') PATH
,level
,line
,LINE*power(10,-level+1) CALC
,XMLCAST(XMLQUERY(ltrim(SYS_CONNECT_BY_PATH(LINE*power(10,-level+1), '+'),'+') RETURNING CONTENT) AS NUMBER) SORT
from (
select 3 ID, 1 LINE, 2 PARENT_ID FROM DUAL
union all
select 4 ID, 2 LINE, 2 PARENT_ID FROM DUAL
union all
select 5 ID, 3 LINE, 2 PARENT_ID FROM DUAL
union all
select 6 ID, 1 LINE, 5 PARENT_ID FROM DUAL
union all
select 7 ID, 1 LINE, 6 PARENT_ID FROM DUAL
) v
start with v.parent_id = 2
connect by nocycle prior id=parent_id
You may take your SORT column and after some fidling (changing the first dot to comma and removing other dots) convert the result to a number.
The key part is here
to_number(regexp_replace(regexp_replace(SORT,'\.',',',1,1),'\.',null),
'99D9999' , ' NLS_NUMERIC_CHARACTERS = '',.'' ') sort2
Example
3.1.1 -> 3,1.1 -> 3,11 and convert to number
The complete query here
with v as (
select 3 ID, 1 LINE, 2 PARENT_ID FROM DUAL
union all
select 4 ID, 2 LINE, 2 PARENT_ID FROM DUAL
union all
select 5 ID, 3 LINE, 2 PARENT_ID FROM DUAL
union all
select 6 ID, 1 LINE, 5 PARENT_ID FROM DUAL
union all
select 7 ID, 1 LINE, 6 PARENT_ID FROM DUAL
), v2 as (
select connect_by_root(parent_id) as root_id
,ID
,SYS_CONNECT_BY_PATH(PARENT_ID,'/') PATH
,level my_level
,line
,LINE*power(10,-level+1) CALC
,ltrim(SYS_CONNECT_BY_PATH( LINE , '.'),'.') SORT
from v
start with v.parent_id = 2
connect by nocycle prior id=parent_id
)
select ROOT_ID, ID, PATH, my_level, LINE, CALC, SORT,
to_number(regexp_replace(regexp_replace(SORT,'\.',',',1,1),'\.',null),'99D9999' , ' NLS_NUMERIC_CHARACTERS = '',.'' ') sort2
from v2

Oracle "partition" a table at each new value

I have an Oracle table I need to "partition" :I use the terme loosely, I just need to detect groups and would like to display the group through a SELECT. Here's an example that might serve as a sample data (the four columns):
ID | Ref | Rank | Partition_group (only available for the 1st member)
1 | 1 | 1 | 1_A
2 | 1 | 2 | (null)
3 | 1 | 3 | 1_B
4 | 2 | 1 | (null)
5 | 2 | 2 | 2_A
...
It is sorted (the sort key would be the 'Ref' and a creation date). What I would need here, is to extract three groups:
IDs 1 and 2
ID 3
ID 5
What happens with ID 4 is not really important: it may be in its own group, or with the ID 5.
Two IDS should be in the same group if they have the same 'Ref' and if there hasn't been any 'Partition_group' change. In other words, at each change of 'Ref' or (logical or) 'Partition_group', I need to detect a new group. For instance, we could return something like that:
ID | Ref | Rank | Partition_group | Group
1 | 1 | 1 | 1_A | 1_A
2 | 1 | 2 | (null) | 1_A
3 | 1 | 3 | 1_B | 1_B
4 | 2 | 1 | (null) | (null) (or 2_A)
5 | 2 | 2 | 2_A | 2_A
...
I thought about writing a function or something, but it appears I don't have the rights to do so (yeah...) so I have to use plain Oracle SQL (11g).
I've been looking at CONNECT BY and OVER (analytical functions) but they don't seem to do the trick.
Has anyone been faced to such a problem? How would you resolve it?
Thanks in advance.
Assuming the input data is the first four columns, how about something like:
with sample_data as (select 1 id, 1 ref, 1 rank, '1_A' ptn_group from dual union all
select 2 id, 1 ref, 2 rank, null ptn_group from dual union all
select 3 id, 1 ref, 3 rank, '1_B' ptn_group from dual union all
select 4 id, 2 ref, 1 rank, null ptn_group from dual union all
select 5 id, 2 ref, 2 rank, '2_A' ptn_group from dual)
select id,
ref,
rank,
ptn_group,
last_value(ptn_group ignore nulls) over (partition by ref order by rank, id) grp1,
case when last_value(ptn_group ignore nulls) over (partition by ref order by rank, id) is null then
first_value(ptn_group ignore nulls) over (partition by ref order by rank, id rows between current row and unbounded following)
else last_value(ptn_group ignore nulls) over (partition by ref order by rank, id)
end grp2
from sample_data;
ID REF RANK PTN_GROUP GRP1 GRP2
---------- ---------- ---------- --------- ---- ----
1 1 1 1_A 1_A 1_A
2 1 2 1_A 1_A
3 1 3 1_B 1_B 1_B
4 2 1 2_A
5 2 2 2_A 2_A 2_A
I've given you two options to generate the grp, based on how you want to deal with rows where the first rows of the ptn_group are null - leave them null or pick up the first non-null value in the group.

Resources