Oracle - split single row into multiple rows based on column value [duplicate] - sql

This question already has answers here:
How to unfold the results of an Oracle query based on the value of a column
(4 answers)
Closed 6 years ago.
I have a table like this
parent_item child_item quantity
A B 2
A C 3
B E 1
B F 2
And would like to split this in multiple lines based on the quantity
parent_item child_item quantity
A B 1
A B 1
A C 1
A C 1
A C 1
B E 1
B F 1
B F 1
The column quantity (1) is not really necessary.
I was able to generate something with the help of connect by / level, but for large tables it's very very slow.I'm not really familiar with connect by / level, but this seemed to work, although I can't really explain:
select distinct parent_item, level LEVEL_TAG, child_item, level||quantity
FROM table
CONNECT BY quantity>=level
order by 1 asc;
I found similar questions, but in most cases topicstarter want's to split a delimited column value in multiple lines (Oracle - split single row into multiple rows)
What's the most performant method to solve this?
Thanks

Use a recursive sub-query factoring clause:
WITH split ( parent_item, child_item, lvl, quantity ) AS (
SELECT parent_item, child_item, 1, quantity
FROM your_table
UNION ALL
SELECT parent_item, child_item, lvl + 1, quantity
FROM split
WHERE lvl < quantity
)
SELECT parent_item, child_item, 1 As quantity
FROM split;
Or you can use a correlated hierarchical query:
SELECT t.parent_item, t.child_item, 1 AS quantity
FROM your_table t,
TABLE(
CAST(
MULTISET(
SELECT LEVEL
FROM DUAL
CONNECT BY LEVEL <= t.quantity
)
AS SYS.ODCINUMBERLIST
)
) l;
As for which is more performant - try benchmarking the different solutions as we cannot tell you what will be more performant on your system.

Related

Snowflake: Repeating rows based on column value

How to repeat rows based on column value in snowflake using sql.
I tried a few methods but not working such as dual and connect by.
I have two columns: Id and Quantity.
For each ID, there are different values of Quantity.
So if you have a count, you can use a generator:
with ten_rows as (
select row_number() over (order by null) as rn
from table(generator(ROWCOUNT=>10))
), data(id, count) as (
select * from values
(1,2),
(2,4)
)
SELECT
d.*
,r.rn
from data as d
join ten_rows as r
on d.count >= r.rn
order by 1,3;
ID
COUNT
RN
1
2
1
1
2
2
2
4
1
2
4
2
2
4
3
2
4
4
Ok let's start by generating some data. We will create 10 rows, with a QTY. The QTY will be randomly chosen as 1 or 2.
Next we want to duplicate the rows with a QTY of 2 and leave the QTY =1 as they are.
Obviously you can change all parameters above to suit your needs - this solution works super fast and in my opinion way better than table generation.
Simply stack SPLIT_TO_TABLE(), REPEAT() with a LATERAL() join and voila.
WITH TEN_ROWS AS (SELECT ROW_NUMBER()OVER(ORDER BY NULL)SOME_ID,UNIFORM(1,2,RANDOM())QTY FROM TABLE(GENERATOR(ROWCOUNT=>10)))
SELECT
TEN_ROWS.*
FROM
TEN_ROWS,LATERAL SPLIT_TO_TABLE(REPEAT('hire me $10/hour',QTY-1),'hire me $10/hour')ALTERNATIVE_APPROACH;

Rewrite a query with "LEVEL" in snowflake

How can I write this SQL query into SNOWFLAKE?
SELECT LEVEL lv FROM DUAL CONNECT BY LEVEL <= 3;
You can find some good starting points by using CONNECT BY (https://docs.snowflake.com/en/sql-reference/constructs/connect-by.html) and here (https://docs.snowflake.com/en/user-guide/queries-hierarchical.html).
Snowflake is also supporting recursive CTEs.
It seems like you want to duplicate the rows three times. For a fixed, small multiplier such as 3, you could just enumerate the numbers:
select c.lv, a.*
from abc a
cross join (select 1 lv union all select 2 union all select 3) c
A more generic approach, in the spirit of the original query, uses a standard recursive common-table-expression to generate the numbers:
with cte as (
select 1 lv
union all select lv + 1 from cte where lv <= 3
)
select c.lv, a.*
from abc a
cross join cte c
There is level in snowflake. The differences from Oracle are:
In snowflake it's neccesary to use prior with connect by expression.
And you can't just select level - there should be any existing column in the select statement.
Example:
SELECT LEVEL, dummy FROM
(select 'X' dummy ) DUAL
CONNECT BY prior LEVEL <= 3;
LEVEL DUMMY
1 X
2 X
3 X
4 X
As per #Danil Suhomlinov post, we can further simplify using COLUMN1 for special table dual single column:
SELECT LEVEL, column1 FROM dual
CONNECT BY prior LEVEL <= 3;
LEVEL | COLUMN1
------+--------
1 |
2 |
3 |
4 |

Split column into multiple rows in ORACLE based on static length of substring

I have seen multiple topics here for "Split column into multiple rows" but they all are based on some delimiter.
I want to split the column based on length in oracle.
Suppose i have a table
codes | product
--------------------------+--------
C111C222C333C444C555..... | A
codes are type VARCHAR2(800) and product is VARCHAR2(1).
Here in codes field we have many codes (maximum 200) which belongs to product A. and length of each code is 4 ( so C111, C222, C333 are different codes)
I want output of my select query like this-
code | product
---------------+-------
C111 | A
C222 | A
C333 | A
C444 | A
C555 | A
...
and so on.
please help me with this. Thanks in advance.
Here's yet another variation using regexp_substr() along with CONNECT BY to "loop" through the string by 4 character substrings:
SQL> with tbl(codes, product) as (
select 'C111C222C333C444C555', 'A' from dual union all
select 'D111D222D333', 'B' from dual
)
select regexp_substr(codes, '(.{4})', 1, level, null, 1) code, product
from tbl
connect by level <= (length(codes)/4)
and prior codes = codes
and prior sys_guid() is not null;
CODE P
-------------------- -
C111 A
C222 A
C333 A
C444 A
C555 A
D111 B
D222 B
D333 B
8 rows selected.
SQL>
Here is how I would do it. Let me know if you need more input / better explanations:
select substr(tt.codes,(((t.l-1)*4)+1),4) code,tt.product from tst_tab tt
join (select level l from dual connect by level <= (select max(length(codes)/4) from tst_tab)) t
on t.l <= length(tt.codes)/4
order by tt.product,t.l;
Some explanantions:
-- this part gives the numbers from 1 ... maximum number of codes in codes column
select level l from dual connect by level <= (select max(length(codes)/4) from tst_tab);
-- here is the query without the code extraction, it is just the numbers 1... numbers of codes for the product
select t.l,tt.product from tst_tab tt
join (select level l from dual connect by level <= (select max(length(codes)/4) from tst_tab)) t
on t.l <= length(tt.codes)/4
order by tt.product,t.l;
-- and then the substr just extracts the right code:
substr(tt.codes,(((t.l-1)*4)+1),4)
Set up of my test data:
create table tst_tab (codes VARCHAR2(800),product VARCHAR2(1));
insert into tst_tab values ('C111C222C333C444C555','A');
insert into tst_tab values ('C111C222C333C444C555D666','B');
insert into tst_tab values ('C111','C');
commit;
One option might be this:
SQL> with test (codes, product) as
2 (select 'C111C222C333C444C555', 'A' from dual union all
3 select 'D555D666D777', 'B' from dual
4 )
5 select substr(codes, 4 * (column_value - 1) + 1, 4) code, product
6 from test,
7 table(cast(multiset(select level from dual
8 connect by level <= length(codes) / 4
9 ) as sys.odcinumberlist))
10 order by 1;
CODE P
---- -
C111 A
C222 A
C333 A
C444 A
C555 A
D555 B
D666 B
D777 B
8 rows selected.
SQL>
Yet another a little bit different option of using recursive SQL to do this.
(To make it more concise I didn't add an example of test data. It could be taken from #Littlefoot or #Peter answers)
select code, product
from (
select distinct
substr(codes, (level - 1) * 4 + 1, 4) as code,
level as l,
product
from YourTable
connect by substr(codes, (level - 1) * 4 + 1, 4) is not null
)
order by product, l
P.S. #Thorsten Kettner made a fair point about considering to restructure your tables. That would be the right thing to do for sake of easier maintenance of your database in future

SQL: create sequential list of numbers from various starting points

I'm stuck on this SQL problem.
I have a column that is a list of starting points (prevdoc), and anther column that lists how many sequential numbers I need after the starting point (exdiff).
For example, here are the first several rows:
prevdoc | exdiff
----------------
1 | 3
21 | 2
126 | 2
So I need an output to look something like:
2
3
4
22
23
127
128
I'm lost as to where even to start. Can anyone advise me on the SQL code for this solution?
Thanks!
;with a as
(
select prevdoc + 1 col, exdiff
from <table> where exdiff > 0
union all
select col + 1, exdiff - 1
from a
where exdiff > 1
)
select col
If your exdiff is going to be a small number, you can make up a virtual table of numbers using SELECT..UNION ALL as shown here and join to it:
select prevdoc+number
from doc
join (select 1 number union all
select 2 union all
select 3 union all
select 4 union all
select 5) x on x.number <= doc.exdiff
order by 1;
I have provided for 5 but you can expand as required. You haven't specified your DBMS, but in each one there will be a source of sequential numbers, for example in SQL Server, you could use:
select prevdoc+number
from doc
join master..spt_values v on
v.number <= doc.exdiff and
v.number >= 1 and
v.type = 'p'
order by 1;
The master..spt_values table contains numbers between 0-2047 (when filtered by type='p').
If the numbers are not too large, then you can use the following trick in most databases:
select t.exdiff + seqnum
from t join
(select row_number() over (order by column_name) as seqnum
from INFORMATION_SCHEMA.columns
) nums
on t.exdiff <= seqnum
The use of INFORMATION_SCHEMA columns in the subquery is arbitrary. The only purpose is to generate a sequence of numbers at least as long as the maximum exdiff number.
This approach will work in any database that supports the ranking functions. Most databases have a database-specific way of generating a sequence (such as recursie CTEs in SQL Server and CONNECT BY in Oracle).

SQL return multiple rows from one record

This is the opposite of reducing repeating records.
SQL query to create physical inventory checklists
If widget-xyz has a qty of 1 item return 1 row, but if it has 5, return 5 rows etc.
For all widgets in a particular warehouse.
Previously this was handled with a macro working through a range in excel, checking the qty column. Is there a way to make a single query instead?
The tables are FoxPro dbf files generated by an application and I am outputting this into html
Instead of generating an xml string and using xml parsing functions to generate a counter as Nestor has suggested, you might consider joining on a recursive CTE as a counter, as LukLed has hinted to:
WITH Counter AS
(
SELECT 0 i
UNION ALL
SELECT i + 1
FROM Counter
WHERE i < 100
),
Data AS
(
SELECT 'A' sku, 1 qty
UNION
SELECT 'B', 2
UNION
SELECT 'C', 3
)
SELECT *
FROM Data
INNER JOIN Counter ON i < qty
According to query analyzer, this query is much faster than the xml pseudo-table. This approach also gives you a recordset with a natural key (sku, i).
There is a default recursion limit of 100 in MSSQL that will restrict your counter. If you have quantities > 100, you can either increase this limit, use nested counters, or create a physical table for counting.
For SQL 2005/2008, take a look at
CROSS APPLY
What I would do is CROSS APPLY each row with a sub table with as many rows as qty has. A secondary question is how to create that sub table (I'd suggest to create an xml string and then parse it with the xml operators)
I hope this gives you a starting pointer....
Starting with
declare #table table (sku int, qty int);
insert into #table values (1, 5), (2,4), (3,2);
select * from #table;
sku qty
----------- -----------
1 5
2 4
3 2
You can generate:
with MainT as (
select *, convert(xml,'<table>'+REPLICATE('<r></r>',qty)+'</table>') as pseudo_table
from #table
)
select p.sku, p.qty
from MainT p
CROSS APPLY
(
select p.sku from p.pseudo_table.nodes('/table/r') T(row)
) crossT
sku qty
----------- -----------
1 5
1 5
1 5
1 5
1 5
2 4
2 4
2 4
2 4
3 2
3 2
Is that what you want?
Seriously dude... next time put more effort writing your question. It's impossible to know exactly what you are looking for.
You can use table with number from 1 to max(quantity) and join your table by quantity <= number. You can do it in many ways, but it depends on sql engine.
You can do this using dynamic sql.