SQL - how to combine sequential rows based on next row - sql

There is a table I need to join back to itself to created a purchased parts report. But, I keep getting duplicated rows.
How to create a sql that will combine rows based on value of the next row?
Order No and Order Line fields uniquely identify a row in a sales order.
CP_COMP_SEQ is simply a list of all the components needed to manufacture Sales Order and Order Line.
MFG_PURCH_FLG is a flag 'M' means it's a Make part. 'P' means it's a part we need to Purchase.
If an M row follows another M row, then the first component has no purchased parts. If one or more P row(s) follow an M row, then all those sequential rows need to be purchased. Meaning we need to add them to a Purchase Parts report for a buyer to fill out a Purchase Order.
So, in the image below, Row 1 needs two purchased parts to complete. Row 4 doesn't need any purchased parts (because it's followed by another M row). Row 5 needs three purchased parts. And row 9 doesn't need any purchased parts.
Source table
order_no
order_line_no
cp_comp_seq
inv_item
mfg_purch_flg
qty
1
2
1
146FV
M
2
1
2
2
2085
P
4
1
2
3
2095
P
4
1
2
4
ZBAR007
M
1
1
2
5
1467V
M
1
1
2
6
2085
P
2
1
2
7
2095
P
2
1
2
8
3060
P
1
1
2
9
ZBAR007
M
1
2
1
1
xxx
x
x
2
1
2
xxx
x
x
I would like to have the results be one row per item that needs to be purchased based on the previous row that is a 'M' make item. Below is what I would like as the result:Result table
order_no
order_line_no
cp_comp_seq
inv_item
purchase_item
qty
1
2
1
146FV
2085
4
1
2
1
146FV
2095
4
1
2
5
147FV
2085
2
1
2
5
147FV
2095
2
1
2
5
147FV
3060
1
Any help would be greatly appreciated! Thanks in advance.
I tried joining based on rownumber - 1, but that doesn't stop when the Purchase flag changes. I tried looking at Run Groups but was unable to make that work as well.
Any help would be greatly appreciated! Thanks in advance.

You could use analytic function to get the sequence number of M flag of every P flag:
Select t.ORDER_NO, t.ORDER_LINE_NO, t.CP_COMP_SEQ, t.INV_ITEM, t.MFG_PURCH_FLG, t.QTY,
MAX(CASE WHEN t.MFG_PURCH_FLG = 'M' THEN t.CP_COMP_SEQ END) OVER(Order By t.CP_COMP_SEQ Rows Between Unbounded Preceding And Current Row) "M_SEQ"
From tbl t
ORDER_NO ORDER_LINE_NO CP_COMP_SEQ INV_ITEM MFG_PURCH_FLG QTY M_SEQ
---------- ------------- ----------- -------- ------------- --- ----------
1 2 1 146FV M 2 1
1 2 2 2085 P 4 1
1 2 3 2095 P 4 1
1 2 4 ZBAR007 M 1 4
1 2 5 1467V M 1 5
1 2 6 2085 P 2 5
1 2 7 2095 P 2 5
1 2 8 3060 P 1 5
1 2 9 ZBAR007 M 1 9
2 1 10 xxx x x 9
2 1 11 xxx x x 9
If you put it as a subquery in the FROM clause like here:
SELECT t.ORDER_NO, t.ORDER_LINE_NO,
(Select CP_COMP_SEQ From tbl Where CP_COMP_SEQ = t.M_SEQ) "CP_COMP_SEQ",
(Select INV_ITEM From tbl Where CP_COMP_SEQ = t.M_SEQ) "INV_ITEM",
t.INV_ITEM "PURCHASE_ITEM", t.QTY
FROM ( Select t.ORDER_NO, t.ORDER_LINE_NO, t.CP_COMP_SEQ, t.INV_ITEM, t.MFG_PURCH_FLG, t.QTY,
MAX(CASE WHEN t.MFG_PURCH_FLG = 'M' THEN t.CP_COMP_SEQ END) OVER(Order By t.CP_COMP_SEQ Rows Between Unbounded Preceding And Current Row) "M_SEQ"
From tbl t
) t
WHERE t.MFG_PURCH_FLG = 'P'
ORDER BY t.CP_COMP_SEQ
... then, with your sample data ...
WITH
tbl (ORDER_NO, ORDER_LINE_NO, CP_COMP_SEQ, INV_ITEM, MFG_PURCH_FLG, QTY) AS
(
Select 1, 2, 1, '146FV', 'M', '2' From Dual Union All
Select 1, 2, 2, '2085', 'P', '4' From Dual Union All
Select 1, 2, 3, '2095', 'P', '4' From Dual Union All
Select 1, 2, 4, 'ZBAR007', 'M', '1' From Dual Union All
Select 1, 2, 5, '1467V', 'M', '1' From Dual Union All
Select 1, 2, 6, '2085', 'P', '2' From Dual Union All
Select 1, 2, 7, '2095', 'P', '2' From Dual Union All
Select 1, 2, 8, '3060', 'P', '1' From Dual Union All
Select 1, 2, 9, 'ZBAR007', 'M', '1' From Dual Union All
Select 2, 1, 10, 'xxx', 'x', 'x' From Dual Union All
Select 2, 1, 11, 'xxx', 'x', 'x' From Dual
)
... the result would be:
ORDER_NO ORDER_LINE_NO CP_COMP_SEQ INV_ITEM PURCHASE_ITEM QTY
---------- ------------- ----------- -------- ------------- ---
1 2 1 146FV 2085 4
1 2 1 146FV 2095 4
1 2 5 1467V 2085 2
1 2 5 1467V 2095 2
1 2 5 1467V 3060 1

Take a look at the cross apply syntax. This syntax allows you to drive the inner query off each row in the other query.
select *
from AnyTableYouWant T1
cross apply
(
select top 1 ID from AnyTableYouWant T2
where T1.ID = T2.ID
order by T2.ID desc
) T3
T1 or T2 can be the same or different tables. As long as you come up with join criteria. A cross apply is different from a typical left, right or inner join. There's plenty of literature on these queries. This answer is not meant to be a diatribe on how it works.
After reading this question more closely, it would benefit from just walking through a result set sequentially and not trying to do it all in SQL. If you want to do it all in SQL, lookup up CURSORs. For maintenance reasons using python or C# or any other procedural language. Query all the records from the parts table make sure it's sorted the way you have it listed, then walk through the results one record at a time and apply the business logic you described.

Related

sql grouping grades

I have a table for subjects as follows:
id Subject Grade Ext
100 Math 6 +
100 Science 4 -
100 Hist 3
100 Geo 2 +
100 CompSi 1
I am expecting output per student in a class(id = 100) as follows:
Grade Ext StudentGrade
6 + 1
6 0
6 - 0
5 + 0
5 0
5 - 0
4 + 0
4 0
4 - 1
3 + 0
3 1
3 - 0
2 + 1
2 0
2 - 0
1 + 0
1 1
1 - 0
I would want this done on oracle/sql rather than UI. Any inputs please.
You should generate rows first, before join them with your table like below. I use the with clause here to generate the 18 rows in your sample.
with rws (grade, ext) as (
select ceil(level/3), decode(mod(level, 3), 0, '+', 1, '-', null)
from dual
connect by level <= 3 * 6
)
select r.grade, r.ext, nvl2(t.Ext, 1, 0) studentGrade
from rws r
left join your_table t
on t.Grade = r.Grade and decode(t.Ext, r.Ext, 1, 0) = 1
order by 1 desc, decode(r.ext, null, 2, '-', 3, '+', 1)
You could do something like this. In the WITH clause I generate two small "helper" tables (really, inline views) for grades from 1 to 6 and for "extensions" of +, null and -. In the "extensions" view I also create an "ordering" column to use in ordering the final output (if you are wondering why I included that).
Also in the WITH clause I included sample data - you will have to remove that and instead use your actual table name in the main query.
The idea is to cross-join "grades" and "extensions", and left-outer-join the result to your input data. Count the grades from the input data, grouped by grade and extension, and after filtering the desired id. The decode thing in the join condition is needed because for extension we want to treat null as equal to null - something that decode does nicely.
with
sample_inputs (id, subject, grade, ext) as (
select 100, 'Math' , 6, '+' from dual union all
select 100, 'Science', 4, '-' from dual union all
select 100, 'Hist' , 3, null from dual union all
select 100, 'Geo' , 2, '+' from dual union all
select 100, 'CompSi' , 1, null from dual
)
, g (grade) as (select level from dual connect by level <= 6)
, e (ord, ext) as (
select 1, '+' from dual union all
select 2, null from dual union all
select 3, '-' from dual
)
select g.grade, e.ext, count(t.grade) as studentgrade
from g cross join e left outer join sample_inputs t
on t.grade = g.grade and decode(t.ext, e.ext, 0) = 0
and t.id = 100 -- change this as needed!
group by g.grade, e.ext, e.ord
order by g.grade desc, e.ord
;
OUTPUT:
GRADE EXT STUDENTGRADE
----- --- ------------
6 + 1
6 0
6 - 0
5 + 0
5 0
5 - 0
4 + 0
4 0
4 - 1
3 + 0
3 1
3 - 0
2 + 1
2 0
2 - 0
1 + 0
1 1
1 - 0
It looks like you want sparse data to be filled in as part of joining students and subjects.
Since Oracle 10g the correct way to do this has been with a "partition outer join".
The documentation has examples.
https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/SELECT.html#GUID-CFA006CA-6FF1-4972-821E-6996142A51C6

How to get this below output from DUAL in oracle?

1 L R
1 1 1
1 1 2
1 1 3
1 2 1
1 2 2
1 2 3
1 3 1
1 3 2
1 3 3
Using this query but not able to get for L column
Select 1,level R
from DUAL
Connect by level <=3
You could do a Cartesian join in the row generator query you have to generate 3 rows. Thus, the Cartesian product would generate total 9 rows.
For example,
SQL> WITH DATA AS
2 ( SELECT LEVEL rn FROM dual CONNECT BY LEVEL <=3
3 )
4 SELECT 1, A.rn L, b.rn R FROM DATA A, DATA b
5 /
1 L R
---------- ---------- ----------
1 1 1
1 1 2
1 1 3
1 2 1
1 2 2
1 2 3
1 3 1
1 3 2
1 3 3
9 rows selected.
SQL>
select 1, L, R
from (Select level R
from DUAL
Connect by level <=3),
(Select level L
from DUAL
Connect by level <=3)
You might try something like this:
SELECT 1, CEIL(lvl/3) AS l
, ROW_NUMBER() OVER ( PARTITION BY CEIL(lvl/3) ORDER BY lvl ) AS r
FROM (
SELECT LEVEL AS lvl FROM dual
CONNECT BY LEVEL <= 9
);
The above avoids a cartesian join. See SQLFiddle here.

SQL Server: Join 2 tables, preferring results from one table where there is a conflict

I have tables that looks like this:-
tblConsuptionsFromA
id meter date total
1 1 03/01/2014 100.1
2 1 04/01/2014 184.1
3 1 05/01/2014 134.1
4 1 06/01/2014 132.4
5 1 07/01/2014 126.1
6 1 08/01/2014 190.1
and...
tblConsuptionsFromB
id meter date total
1 1 01/01/2014 164.1
2 1 02/01/2014 133.1
3 1 03/01/2014 136.1
4 1 04/01/2014 125.1
5 1 05/01/2014 190.1
6 1 06/01/2014 103.1
7 1 07/01/2014 164.1
8 1 08/01/2014 133.1
9 1 09/01/2014 136.1
10 1 10/01/2014 125.1
11 1 11/01/2014 190.1
I need to join these two tables, but if there is an entry for the same day in both table... only take the result from tblConsumptionsFromA.
So the result would be:-
id source_id meter from date total
1 1 1 B 01/01/2014 164.1
2 2 1 B 02/01/2014 133.1
3 1 1 A 03/01/2014 100.1
4 2 1 A 04/01/2014 184.1
5 3 1 A 05/01/2014 134.1
6 4 1 A 06/01/2014 132.4
7 5 1 A 07/01/2014 126.1
8 6 1 A 08/01/2014 190.1
9 9 1 B 09/01/2014 136.1
10 10 1 B 10/01/2014 125.1
11 11 1 B 11/01/2014 190.1
This is beyond me, so if someone can solve... I will be very impressed.
Here's one way to do it:
SELECT
COALESCE(a.source_id,b.source_id) as source_id,
COALESCE(a.meter,b.meter) as meter,
COALESCE(a.[from],b.[from]) as [from],
COALESCE(a.[date],b.[date]) as [date],
COALESCE(a.total,b.total)
FROM (select source_id,meter,'b' as [from],[date],total
from tblConsuptionsFromB) b
left join
(select source_id,meter,'a' as [from],[date],total
from tblConsuptionsFromA) a
on
a.meter = b.meter and
a.[date] = b.[date]
Unfortunately, there's no shorthand like COALESCE(a.*,b.*) to apply the COALESCE to all columns
The UNION operator is used to combine the result-set of two or more SELECT statements.
SELECT column_name(s) FROM table1
UNION
SELECT column_name(s) FROM table2;
The document of UNION is here:
http://www.w3schools.com/sql/sql_union.asp
And ROW_NUMBER() returns the sequential number of a row within a partition of a result set, starting at 1 for the first row in each partition.
ROW_NUMBER ( )
OVER ( [ PARTITION BY value_expression , ... [ n ] ] order_by_clause )
The document of ROW_NUMBER() is here:
http://technet.microsoft.com/en-us/library/ms186734.aspx
The following SQL statement uses UNION to select all records from the "tblConsuptionsFromA" and part of records from "tblConsuptionsFromB" tables.
SELECT ROW_NUMBER() OVER(ORDER BY DATE ASC) AS 'id',
id AS 'source_id',meter, date,t AS 'from',total
FROM(
SELECT id,meter, date, 'A' AS t, total FROM tblConsuptionsFromA
UNION
SELECT id,meter, date, 'B' AS t,total FROM tblConsuptionsFromB
WHERE NOT date IN (SELECT date FROM tblConsuptionsFromA)
) AS C;
Hope this helps.
select ta.id, tb.id, ta.meter,
if(ta.date is null, 'B', 'A') as from,
if(ta.date is null, tb.date, ta.date) as date,
if(ta.date is null, tb.total, ta.total) as total
from tblConsuptionsFromA ta
full join tblConsuptionsFromB tb on ta.date=tb.date
You would need to do a Union of the 2 tables, and exclude records from tabletblConsuptionsFromB which are present in tblConsuptionsFromA, something like:
Select Id, Source_ID, meter, 'A' From, Date, Total
FROM tblConsuptionsFromA
Union All
Select Id, Source_ID, meter, 'B' From, Date, Total
FROM tblConsuptionsFromB
Where Date NOT EXISTS (Select Date from tblConsuptionsFromA)

How do I build a SQL query to show two columns of different date ranges?

I'm trying to build a query from an Oracle 11g database to use in a report. I need to use two tables CONTACT and CONTACT_EXT to get the data from, and compare the total amount of contacts over two date ranges.The tables are joined by ID's matching.
CONTACT:
ID | DATE
----------
1 12/12/2010
2 12/11/2010
3 14/09/2011
CONTACT_EXT
ID | TYPE
----------
1 MAIL
2 FAX
3 FAX
So for example if I set period A to be between 01/01/2010 and 12/12/2010 and period B to be between 01/01/2011 and 11/11/2011
TYPE | PERIOD A | PERIOD B | TOTAL
MAIL 1 0 1
FAX 1 1 2
SQL> create table contact (id,cdate)
2 as
3 select 1, date '2010-12-12' from dual union all
4 select 2, date '2010-11-12' from dual union all
5 select 3, date '2011-09-14' from dual
6 /
Table created.
SQL> create table contact_ext (id,type)
2 as
3 select 1, 'MAIL' from dual union all
4 select 2, 'FAX' from dual union all
5 select 3, 'FAX' from dual
6 /
Table created.
SQL> select ce.type
2 , count(case when c.cdate between date '2010-01-01' and date '2010-12-12' then 1 end) period_a
3 , count(case when c.cdate between date '2011-01-01' and date '2011-11-11' then 1 end) period_b
4 , count(*) total
5 from contact c
6 inner join contact_ext ce on (c.id = ce.id)
7 group by ce.type
8 /
TYPE PERIOD_A PERIOD_B TOTAL
---- ---------- ---------- ----------
FAX 1 1 2
MAIL 1 0 1
2 rows selected.
Regards,
Rob.
Just do a self join:
select type,period_a,period_b,period_a+period_b as total
from(
select type,count(1) as period_a
from contact_ext
left join contact
using(id)
where date>='20100101' and date<='20101212'
group by 1
)a
join(
select type,count(1) as period_b
from contact_ext
left join contact
using(id)
where date>='20110101' and date<='20111111'
group by 1
)b
using(type);
Ans 1 : In where clause set period A between 01/01/2010 and 12/12/2010 OR period B between 01/01/2011 and 11/11/2011
In where clause use OR condition
Ans 2: you can union two different select statements of Period A and Period B

Grouping Hierarchical data (parentID+ID) and running sum?

I have the following data:
ID parentID Text Price
1 Root
2 1 Flowers
3 1 Electro
4 2 Rose 10
5 2 Violet 5
6 4 Red Rose 12
7 3 Television 100
8 3 Radio 70
9 8 Webradio 90
I am trying to group this data with Reporting Services 2008 and have a sum of the price per group of level 1 (Flowers/Electro) and for level 0 (Root).
I have a table grouped on [ID] with a recursive parent of [parendID] and I am able to calculate the sum for the level 0 (just one more row in the table outside the group), but somehow I am not able to create sum's per group as SRSS does "create" groups per level. My desired result looks like so:
ID Text Price
1 Root
|2 Flowers
|-4 Rose 10
|-5 Violet 5
| |-6 Red Rose 12
| Group Sum-->27
|3 Electro
|-7 Television 100
|-8 Radio 70
|-9 Webradio 90
Group Sum-->260
----------------------
Total 287
(indentation of ID just added for level clarification)
With my current approach I cannot get the group sums, so I figured out I would need the following data structure:
ID parentID Text Price level0 level1 level2 level3
1 Root 1
2 1 Flowers 1 1
3 1 Electro 1 2
4 2 Rose 10 1 1 1
5 2 Violet 5 1 1 2
6 4 Red Rose 12 1 1 1 1
7 3 Television 100 1 2 1
8 3 Radio 70 1 2 2
9 8 Webradio 90 1 2 2 1
When having the above structure I can create an outer grouping of level0, with child groupings level1, level2, level3 accordingly . When now having a "group sum" on level1, and the total sum outside the group I have EXACTLY what I want.
My question is the following:
How do I either achieve my desired result with my current data structure, or how do I convert my current data structure (outer left joins?) into the "new data structure" temporarily - so I can run my report off of the temp table?
Thanks for taking your time,
Dennis
WITH q AS
(
SELECT id, parentId, price
FROM mytable
UNION ALL
SELECT p.id, p.parentID, q.price
FROM q
JOIN mytable p
ON p.id = q.parentID
)
SELECT id, SUM(price)
FROM q
GROUP BY
id
Update:
A test script to check:
DECLARE #table TABLE (id INT NOT NULL PRIMARY KEY, parentID INT, txt VARCHAR(200) NOT NULL, price MONEY)
INSERT
INTO #table
SELECT 1, NULL, 'Root', NULL
UNION ALL
SELECT 2, 1, 'Flowers', NULL
UNION ALL
SELECT 3, 1, 'Electro', NULL
UNION ALL
SELECT 4, 2, 'Rose', 10
UNION ALL
SELECT 5, 2, 'Violet', 5
UNION ALL
SELECT 6, 4, 'Red Rose', 12
UNION ALL
SELECT 7, 3, 'Television', 100
UNION ALL
SELECT 8, 3, 'Radio', 70
UNION ALL
SELECT 9, 8, 'Webradio', 90;
WITH q AS
(
SELECT id, parentId, price
FROM #table
UNION ALL
SELECT p.id, p.parentID, q.price
FROM q
JOIN #table p
ON p.id = q.parentID
)
SELECT t.*, psum
FROM (
SELECT id, SUM(price) AS psum
FROM q
GROUP BY
id
) qo
JOIN #table t
ON t.id = qo.id
Here's the result:
1 NULL Root NULL 287,00
2 1 Flowers NULL 27,00
3 1 Electro NULL 260,00
4 2 Rose 10,00 22,00
5 2 Violet 5,00 5,00
6 4 Red Rose 12,00 12,00
7 3 Television 100,00 100,00
8 3 Radio 70,00 160,00
9 8 Webradio 90,00 90,00
I found a really ugly way to do what I want - maybe there is something better?
SELECT A.Text, A.Price,
CASE
WHEN D.Text IS NULL
THEN
CASE
WHEN C.Text IS NULL
THEN
CASE
WHEN B.Text IS NULL
THEN
A.ID
ELSE B.ID
END
ELSE C.ID
END
ELSE D.ID
END
AS LEV0,
CASE
WHEN D.Text IS NULL
THEN
CASE
WHEN C.Text IS NULL
THEN
CASE
WHEN B.Text IS NULL
THEN
NULL
ELSE A.ID
END
ELSE B.ID
END
ELSE C.ID
END
AS LEV1,
CASE
WHEN D.Text IS NULL
THEN
CASE
WHEN C.Text IS NULL
THEN
NULL
ELSE A.ID
END
ELSE B.ID
END
AS LEV2,
CASE
WHEN D.Text IS NULL
THEN NULL
ELSE A.ID
END
AS LEV3
FROM dbo.testOld AS A LEFT OUTER JOIN
dbo.testOld AS B ON A.parentID = B.ID LEFT OUTER JOIN
dbo.testOld AS C ON B.parentID = C.ID LEFT OUTER JOIN
dbo.testOld AS D ON C.parentID = D.ID
Output of this is:
Text Price LEV0 LEV1 LEV2 LEV3
---------- ----------- ----------- ----------- ----------- -----------
Root NULL 1 NULL NULL NULL
Flowers NULL 1 3 NULL NULL
Electro NULL 1 4 NULL NULL
Television 100 1 4 5 NULL
Radio 70 1 4 6 NULL
Rose 10 1 3 7 NULL
Violet 5 1 3 8 NULL
Webradio 90 1 4 5 14
Red Rose 12 1 3 7 15
With this structure I can go ahead and create 4 nested groups on the LEV0-3 columns including subtotals per group (as shown above in my desired result).