Populate a numbered row until reaching a specific value with another column - sql

I have a table full of account numbers and period/terms for loan(loan term is in months)
What I need to do is populate a numbered row for each account number that is less than or equal to the loan term. I've attached a screen shot below:
Example
So for this specific example, I will need 48 numbered rows for this account number, as the term is only 48 months.
Thanks for the help!

with
test_data ( account_nmbr, term ) as (
select 'ABC200', 6 from dual union all
select 'DEF100', 8 from dual
)
-- End of simulated inputs (for testing purposes only, not part of the solution).
-- SQL query begins BELOW THIS LINE.
select level as row_nmbr, term, account_nmbr
from test_data
connect by level <= term
and prior account_nmbr = account_nmbr
and prior sys_guid() is not null
order by account_nmbr, row_nmbr -- If needed
;
ROW_NMBR TERM ACCOUNT_NMBR
-------- ---------- ------------
1 6 ABC200
2 6 ABC200
3 6 ABC200
4 6 ABC200
5 6 ABC200
6 6 ABC200
1 8 DEF100
2 8 DEF100
3 8 DEF100
4 8 DEF100
5 8 DEF100
6 8 DEF100
7 8 DEF100
8 8 DEF100
In Oracle 12, you can use the LATERAL clause for the same:
with
test_data ( account_nmbr, term ) as (
select 'ABC200', 6 from dual union all
select 'DEF100', 8 from dual
)
-- End of simulated inputs (for testing purposes only, not part of the solution).
-- SQL query begins BELOW THIS LINE.
select l.row_nmbr, t.term, t.account_nmbr
from test_data t,
lateral (select level as row_nmbr from dual connect by level <= term) l
order by account_nmbr, row_nmbr -- If needed
;

Related

HANA SQL - Create multiple rows from one row based on calculation

I have the following 2 records:
Batch
Qty
QtyPer
PackNum
One
60
20
3
Two
20
10
2
I need to find a way to create multiple rows based on Qty/PackNum, such as:
Batch
Qty
QtyPer
PackNum
Level
One
60
20
3
1
One
60
20
3
2
One
60
20
3
3
Two
20
10
2
1
Two
20
10
2
2
I've tried using the WITH command to no avail.
Any Ideas?
Marcus
one alternate is construct as dummy table or dummy sql with below structure and do the cross join on the pack_number. You could also try sql procedure with global/local temporary table
pack number, level
3,1
3,2
3,3
2,1
2,2
or
below is the pseudo sql
select it.*,dd.level from(
select 3 as pack_number , 1 as level from dummy
union all
select 3 as pack_number , 2 as level from dummy
union all
select 3 as pack_number , 3 as level from dummy
union all
select 2 as pack_number , 1 as level from dummy
union all
select 2 as pack_number , 2 as level from dummy
....
) as dummy_data as dd
inner join
input_tab as it
on dd. pack_number = it.pack_number

Oracle SQL compare strings and find matching sub-strings

I have colon separated tags associated to two different entities in two tables. Would like to do a sub-string matching for the tags and relate the entities.
Table 1 - Table of issues
Issue ---------------- Tag
Issue 1 -------------- Dual UOM:Serial Control:Material Issue
Issue 2 -------------- Validity rule:Effectivity date
Table 2 - Table of Tests
Test ----------------- Tag
Test 1 --------------- Inventory:Outbound:Material Issue
Test 2 --------------- Items:Single UOM
Test 3 --------------- Items:Dual UOM
Test 4 --------------- Recipe:Validity Rule
Test 5 --------------- Formula:Version control:date
Test 6 --------------- Formula:Effectivity date
Now, for each issue in table 1, I need to compare its associated tag with the tags in table 2 and find applicable tests.
In above ex,
Issue 1 - Matched tests will be Test 1, Test 3
Issue 2 - Matched tests will be Test 4, Test 5
All the tags associated to the issues and tests will come from a common tag master.
Any help in providing the sql code snippet that would do this sub-string to sub-string matching is much appreciated.
Here's one option: split issues into rows and compare them to test tags, using the INSTR function. Note that letter case must match. If it doesn't (in reality), use lower or upper function.
Read comments within code (which is split into several parts, to improve readability).
Sample data first:
SQL> with
2 -- sample data
3 issues (issue, tag) as
4 (select 1, 'Dual UOM:Serial Control:Material Issue' from dual union all
5 select 2, 'Validity Rule:Effectivity date' from dual
6 ),
7 tests (test, tag) as
8 (select 1, 'Inventory:Outbound:Material Issue' from dual union all
9 select 2, 'Items:Single UOM' from dual union all
10 select 3, 'Items:Dual UOM' from dual union all
11 select 4, 'Recipe:Validity Rule' from dual union all
12 select 5, 'Formula:Version control:date' from dual union all
13 select 6, 'Formula:Effectivity date' from dual
14 ),
Split issues into rows (splitiss), compare them to test tags (temp)
15 -- split issues into rows ...
16 splitiss as
17 (select issue,
18 tag,
19 regexp_substr(tag, '[^:]+', 1, column_value) val
20 from issues cross join
21 table(cast(multiset(select level from dual
22 connect by level <= regexp_count(tag, ':') + 1
23 ) as sys.odcinumberlist))
24 ),
25 -- ... and now compare them to test tags
26 temp as
27 (select i.issue, i.tag issue_tag, i.val, t.test, t.tag test_tag,
28 instr(t.tag, i.val) ins
29 from splitiss i cross join tests t
30 )
Return the result:
31 -- return only test tags which match to separate issues (INS > 0)
32 select t.issue,
33 t.issue_tag,
34 listagg(t.test, ', ') within group (order by t.test) matched_tests
35 from temp t
36 where t.ins > 0
37 group by t.issue, t.issue_tag;
ISSUE ISSUE_TAG MATCHED_TESTS
---------- -------------------------------------- --------------------
1 Dual UOM:Serial Control:Material Issue 1, 3
2 Validity Rule:Effectivity date 4, 6
SQL>
P.S. I believe you posted wrong test tags for issue #2; should be 4, 6, not 4, 5.
Thanks, this worked
I did break the tags into rows and then used substr,instr matching.

How to process a column that holds a comma-separated or range string values in Oracle

Using Oracle 12c DB, I have the following table data example that I need assistance with using SQL and PL/SQL.
Table data is as follows:
Table Name: my_data
ID ITEM ITEM_LOC
------- ----------- ----------------
1 Item-1 0,1
2 Item-2 0,1,2,3,4,7
3 Item-3 0-48
4 Item-4 0,1,2,3,4,5,6,7,8
5 Item-5 1-33
6 Item-6 0,1
7 Item-7 0,1,5,8
Using the data above within the my_data table, what is the best way to process this ITEM_LOC as I need to use the values in this column as an individual value, i.e:
0,1 means the SQL needs to return either 0 or 1 or
range values, i.e:
0-48 means the SQL needs to return a value between 0 and 48.
The returned values for both scenarios should commence from lowest to highest and can't be re-used once processed.
Based on the above, it would be great to have a function that takes the ID and returns an individual value from ITEM_LOC that hasn't been used, based on my description above. This could be a comma-separated string value or a range string value.
Desired result for ID = 2 could be 7. For this ID = 2, ITEM_LOC = 7 could not be used again.
Desired result for ID = 5 could be 31. For this ID = 5, ITEM_LOC = 31 could not be used again.
For the ITEM_LOC data that could not be used again, against that ID, I am looking at holding another table to hold this or perhaps separate all data into separate rows with a new column called VALUE_USED.
This query shows how to extract list of ITEM_LOC values based on whether they are comma-separated (which means "take exactly those values") or dash-separated (which means "find all values between starting and end point"). I modified your sample data a little bit (didn't feel like displaying ~50 values if 5 of them do the job).
lines #1 - 6 represent sample data.
the first select (lines #7 - 15) splits comma-separated values into rows
the second select (lines #17 - 26) uses a hierarchical query which adds 1 to the starting value, up to item's end value.
SQL> with my_data (id, item, item_loc) as
2 (select 2, 'Item-2', '0,2,4,7' from dual union all
3 select 7, 'Item-7', '0,1,5' from dual union all
4 select 3, 'Item-3', '0-4' from dual union all
5 select 8, 'Item-8', '5-8' from dual
6 )
7 select id,
8 item,
9 regexp_substr(item_loc, '[^,]+', 1, column_value) loc
10 from my_data
11 cross join table(cast(multiset
12 (select level from dual
13 connect by level <= regexp_count(item_loc, ',') + 1
14 ) as sys.odcinumberlist))
15 where instr(item_loc, '-') = 0
16 union all
17 select id,
18 item,
19 to_char(to_number(regexp_substr(item_loc, '^\d+')) + column_value - 1) loc
20 from my_data
21 cross join table(cast(multiset
22 (select level from dual
23 connect by level <= to_number(regexp_substr(item_loc, '\d+$')) -
24 to_number(regexp_substr(item_loc, '^\d+')) + 1
25 ) as sys.odcinumberlist))
26 where instr(item_loc, '-') > 0
27 order by id, item, loc;
ID ITEM LOC
---------- ------ ----------------------------------------
2 Item-2 0
2 Item-2 2
2 Item-2 4
2 Item-2 7
3 Item-3 0
3 Item-3 1
3 Item-3 2
3 Item-3 3
3 Item-3 4
7 Item-7 0
7 Item-7 1
7 Item-7 5
8 Item-8 5
8 Item-8 6
8 Item-8 7
8 Item-8 8
16 rows selected.
SQL>
I don't know what you meant by saying that "item_loc could not be used again". Used where? If you use the above query in, for example, cursor FOR loop, then yes - those values would be used only once as every loop iteration fetches next item_loc value.
As others have said, it's a bad idea to store data in this way. You very likely could have input like this, and you likely could need to display the data like this, but you don't have to store the data the way it is input or displayed.
I'm going to store the data as individual LOC elements based on the input. I assume the data contains only integers separated by commas, or pairs of integers separated by a hyphen. Whitespace is ignored. The comma-separated list does not have to be in any order. In pairs, if the left integer is greater than the right integer I return no LOC element.
create table t as
with input(id, item, item_loc) as (
select 1, 'Item-1', ' 0,1' from dual union all
select 2, 'Item-2', '0,1,2,3,4,7' from dual union all
select 3, 'Item-3', '0-48' from dual union all
select 4, 'Item-4', '0,1,2,3,4,5,6,7,8' from dual union all
select 5, 'Item-5', '1-33' from dual union all
select 6, 'Item-6', '0,1' from dual union all
select 7, 'Item-7', '0,1,5,8,7 - 11' from dual
)
select distinct id, item, loc from input, xmltable(
'let $item := if (contains($X,",")) then ora:tokenize($X,"\,") else $X
for $i in $item
let $j := if (contains($i,"-")) then ora:tokenize($i,"\-") else $i
for $k in xs:int($j[1]) to xs:int($j[count($j)])
return $k'
passing item_loc as X
columns loc number path '.'
);
Now to "use" an element I just delete it from the table:
delete from t where rowid = (
select min(rowid) keep (dense_rank first order by loc)
from t
where id = 7
);
To return the data in the same format it was input, use MATCH_RECOGNIZE:
select id, item, listagg(item_loc, ',') within group(order by first_loc) item_loc
from t
match_recognize(
partition by id, item order by loc
measures a.loc first_loc,
a.loc || case count(*) when 1 then null else '-'||b.loc end item_loc
pattern (a b*)
define b as loc = prev(loc) + 1
)
group by id, item;
ID ITEM ITEM_LOC
1 Item-1 0-1
2 Item-2 0-4,7
3 Item-3 0-48
4 Item-4 0-8
5 Item-5 1-33
6 Item-6 0-1
7 Item-7 1,5,7-11
Note that the output here will not be exactly like the input, because any consecutive integers will be compressed into a pair.

Select to build groups by (analytic) sum

Please help me to build a sql select to assign (software development) tasks to a software release. Actually this is a fictive example to solve my real business specific problem.
I have a relation Tasks:
ID Effort_In_Days
3 3
1 2
6 2
2 1
4 1
5 1
I want to distribute the Tasks to releases which are at most 2 days long (tasks longer than 2 shall still be put into one release). In my real problem I have much more "days" available to distribute "tasks" to. Expected output:
Release Task_ID
1 3
2 1
3 6
4 2
4 4
5 5
I think I need to use analytic functions, something with sum(effort_in_days) over and so on, to get the result. But I'm I haven't used analytic functions much and didn't find an example that's close enough to my specific problem. I need to build groups (releases) if a sum (>= 2) is reached.
I would do something like:
with data as (
select 3 ID, 3 Effort_In_Days from dual union all
select 1 ID, 2 Effort_In_Days from dual union all
select 6 ID, 2 Effort_In_Days from dual union all
select 2 ID, 1 Effort_In_Days from dual union all
select 4 ID, 1 Effort_In_Days from dual union all
select 5 ID, 1 Effort_In_Days from dual
)
select id, effort_in_days, tmp, ceil(tmp/2) release
from (
select id, effort_in_days, sum(least(effort_in_days, 2)) over (order by effort_in_days desc rows unbounded preceding) tmp
from data
);
Which results in:
ID EFFORT_IN_DAYS TMP RELEASE
---------- -------------- ---------- ----------
3 3 2 1
1 2 4 2
6 2 6 3
2 1 7 4
4 1 8 4
5 1 9 5
Basically, I am using least() to convert everything over 2 down to 2. Then I am putting all rows in descending order by that value and starting to assign releases. Since they are in descending order with a max value of 2, I know I need to assign a new release every time when I get to a multiple of 2.
Note that if you had fractional values, you could end up with releases that do not have a full 2 days assigned (as opposed to having over 2 days assigned), which may or may not meet your needs.
Also note that I am only showing all columns in my output to make it easier to see what the code is actually doing.
This is an example of a bin-packing problem (see here). There is not an optimal solution in SQL, that I am aware of, except in some boundary cases. For instance, if all the tasks have the same length or if all the tasks are >= 2, then there is an easy-to-find optimal solution.
A greedy algorithm works pretty well. This is to put a given record in the first bin where it fits, probably going through the list in descending size order.
If your problem is really as you state it, then the greedy algorithm will work to produce an optimal solution. That is, if the maximum value is 2 and the efforts are integers. There might even be a way to calculate the solution in SQL in this case.
Otherwise, you will need pl/sql code to achieve an approximate solution.
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE data AS
select 3 ID, 3 Effort_In_Days from dual union all
select 1 ID, 2 Effort_In_Days from dual union all
select 6 ID, 2 Effort_In_Days from dual union all
select 2 ID, 1 Effort_In_Days from dual union all
select 4 ID, 1 Effort_In_Days from dual union all
select 5 ID, 1 Effort_In_Days from dual union all
select 9 ID, 2 Effort_In_Days from dual union all
select 7 ID, 1 Effort_In_Days from dual union all
select 8 ID, 1 Effort_In_Days from dual;
Query 1:
Give the rows an index so that they can be kept in order easily;
Assign groups to the rows where the Effort_In_Days is 1 so that all adjacent rows with Effort_In_Days of 1 are in the same group and rows separated by higher values for Effort_In_Days are in different groups;
Assign a cost of 1 to each row where the Effort_In_Days is higher than 1 or where Effort_In_Days is 1 and the row has an odd row number within the group; then
Finally, the release is the sum of all the costs for the row and all preceding rows.
Like this:
WITH indexes AS (
SELECT ID,
Effort_In_Days,
ROWNUM AS idx
FROM Data
),
groups AS (
SELECT ID,
Effort_In_Days,
idx,
CASE Effort_In_Days
WHEN 1
THEN idx - ROW_NUMBER() OVER ( PARTITION BY Effort_In_Days ORDER BY idx )
END AS grp
FROM indexes
ORDER BY idx
),
costs AS (
SELECT ID,
Effort_In_Days,
idx,
CASE Effort_In_Days
WHEN 1
THEN MOD( ROW_NUMBER() OVER ( PARTITION BY grp ORDER BY idx ), 2 )
ELSE 1
END AS cost
FROM groups
ORDER BY idx
)
SELECT ID,
Effort_In_Days,
SUM( cost ) OVER ( ORDER BY idx ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS Release
FROM costs
ORDER BY idx
Results:
| ID | EFFORT_IN_DAYS | RELEASE |
|----|----------------|---------|
| 3 | 3 | 1 |
| 1 | 2 | 2 |
| 6 | 2 | 3 |
| 2 | 1 | 4 |
| 4 | 1 | 4 |
| 5 | 1 | 5 |
| 9 | 2 | 6 |
| 7 | 1 | 7 |
| 8 | 1 | 7 |

how to generate consecutive records with a given number?

in oracle, is there a built-in function to produce consecutive records with a given number? For example, the number is 100, so that you can generate a result-set with 100 records whose values are 1, 2, 3, 4...100, like the following:
1
2
3
4
...
100
I know store procedure can do this, and I want to know if there are other ways just using sql statements?
select level
from dual
connect by level <= 100
Here is another approach, using model clause. (Oracle 10g and higher).
SQL> select x
2 from dual
3 model
4 dimension by (0 as z)
5 measures (0 as x)
6 rules iterate(101) (
7 x[iteration_number] = iteration_number
8 )
9 ;
X
----------
0
1
2
3
4
5
6
7
8
9
10
11
...
100
Please try using CTE:
WITH numbers(n) AS
(
SELECT 1 FROM dual
UNION ALL
SELECT n + 1 FROM numbers WHERE n < 100
)
SELECT * FROM numbers;
It's traditional to use a hierarchical query:
select level
from dual
connect by level <= 100