Oracle remove rows from a query set based on sum - oracle

I have a table with data like below. INVENTORY_ITEM_ID is the unique id for the item, TYPE_QTY is the total quantity for the item in the unique age bucket (AGE_IN_DAYS) and RUNNING_TOTAL is a calculated column based on the TYPE_QTY on age buckets.
Each negative quantity requires to be removed from the final set of rows. For example, at the first occurrence of the negative quantity that is -508, the first rows that has running total that can satisfy the issuance of 508 should be identified and adjusted like below. All the rows above that row should be removed from the result set.
The RUNNING_TOTAL and TYPE_QTY columns are adjusted with the balance from (555-508) and the loop continues. The second issuance of -22 quantity happens against the first row as it has a running total 47 and final results for the given data should be like below
I've made a PL/SQL block that can do the job, however would prefer to achieve it using plain SQL. My current SQL skills are not enough to.
PL/SQL Block
SET SERVEROUTPUT ON;
DECLARE
CURSOR INVDATA IS
SELECT tx.*
from OMSINVDT_TEMP tx
--where inventory_item_id = 35253
order by inventory_item_id,age_in_days desc;
CURSOR inline_data(p_item_id IN NUMBER) IS
SELECT inventory_item_id,
type_qty,
age_in_days,
SUM (type_qty) OVER ( PARTITION BY inventory_item_id ORDER BY age_in_days desc) RUNNING_TOTAL
FROM omsinvdata_temp
where inventory_item_id = p_item_id;
l_line_qty number;
l_last_age_period number := 0;
BEGIN
execute immediate 'truncate table omsinvdata_temp';
for i in invdata loop
--if the qty is greater than 0, add to the temp table
if i.type_qty > 0 then
insert into omsinvdata_temp(
inventory_item_id ,
type_qty ,
age_in_days ,
running_total )
values(i.inventory_item_id, i.type_qty, i.age_in_days, 0);
else
--if the quantity is negative
--open the cursor for the item from temporary table
--and find the row that can satisfy the negative quantity
--dbms_output.put_line('current quantity: '||i.type_qty);
for j in inline_data(i.inventory_item_id) loop
--dbms_output.put_line('Line Qty '||j.type_qty||' Running total: '||j.running_total||' To Issue: '||i.type_qty||' Bucket '||j.age_in_days);
if (abs(i.type_qty)>j.running_total) then
-- dbms_output.put_line('Running total: '||j.running_total||' not sufficient to issue '||i.type_qty||' Bucket '||j.age_in_days);
update omsinvdata_temp
set type_qty =0,
running_total =0
where age_in_days = j.age_in_days
and inventory_item_id = i.inventory_item_id;
else
-- dbms_output.put_line('Running total: '||j.running_total||' sufficient to issue '||i.type_qty||' Bucket '||j.age_in_days);
update omsinvdata_temp
set type_qty = j.running_total + i.type_qty,
running_total = j.running_total + i.type_qty
where age_in_days = j.age_in_days
and inventory_item_id = i.inventory_item_id;
exit;
end if;
end loop;
end if;
end loop;
commit;
END;
Create table script with shown data
CREATE TABLE "OMSINVDT_TEMP" ("INVENTORY_ITEM_ID" NUMBER, "TYPE_QTY" NUMBER, "AGE_IN_DAYS" NUMBER, "RUNNING_TOTAL" NUMBER)
REM INSERTING into OMSINVDT_TEMP
SET DEFINE OFF;
Insert into OMSINVDT_TEMP (INVENTORY_ITEM_ID,TYPE_QTY,AGE_IN_DAYS,RUNNING_TOTAL) values (35253,72,6,72);
Insert into OMSINVDT_TEMP (INVENTORY_ITEM_ID,TYPE_QTY,AGE_IN_DAYS,RUNNING_TOTAL) values (35253,384,5,456);
Insert into OMSINVDT_TEMP (INVENTORY_ITEM_ID,TYPE_QTY,AGE_IN_DAYS,RUNNING_TOTAL) values (35253,105,4,561);
Insert into OMSINVDT_TEMP (INVENTORY_ITEM_ID,TYPE_QTY,AGE_IN_DAYS,RUNNING_TOTAL) values (35253,-512,3,49);
Insert into OMSINVDT_TEMP (INVENTORY_ITEM_ID,TYPE_QTY,AGE_IN_DAYS,RUNNING_TOTAL) values (35253,-24,2,25);
Insert into OMSINVDT_TEMP (INVENTORY_ITEM_ID,TYPE_QTY,AGE_IN_DAYS,RUNNING_TOTAL) values (35253,134,1,159);
Insert into OMSINVDT_TEMP (INVENTORY_ITEM_ID,TYPE_QTY,AGE_IN_DAYS,RUNNING_TOTAL) values (266234,2,4,2);
Insert into OMSINVDT_TEMP (INVENTORY_ITEM_ID,TYPE_QTY,AGE_IN_DAYS,RUNNING_TOTAL) values (266234,1,3,3);
Insert into OMSINVDT_TEMP (INVENTORY_ITEM_ID,TYPE_QTY,AGE_IN_DAYS,RUNNING_TOTAL) values (266234,-1,2,2);
Insert into OMSINVDT_TEMP (INVENTORY_ITEM_ID,TYPE_QTY,AGE_IN_DAYS,RUNNING_TOTAL) values (266234,-2,1,0);
commit;
I've already asked this question to Oracle SQL/PLSQL community & failed to explain the construction logic.
https://community.oracle.com/tech/developers/discussion/4480421/sql-match-quantity-and-pick-rows

Here is one way to do it, using only analytic functions and aggregation. You didn't explain the AGE_IN_DAYS column in the output - based on your example, I assume it represents the age of the most recent of the positive rows preceding the last negative row.
The RUNNING_TOTAL column shouldn't exist in the inputs, since it is calculated from the other data. Even though you have it in the table, I ignore it - I compute it directly. (I assume what you show is not your real starting data, but the point where you weren't able to continue with your solution.)
There is also a mismatch between the example you used and your INSERT statements. I used the INSERT statements as they are (with a different value for one of the rows); this explains why my output looks different from yours.
The main trick is in the WITH clause, in the PREP subquery. I assign a flag to the rows after the last "negative" row. Then in the main query I group by this flag, and in addition to that, only when the flag is set, by AGE_IN_DAYS. This way all the rows up to and including the last "negative" one are in one group, while the remaining positive rows are one row per group. (I assume that AGE_IN_DAYS is distinct for each INVENTORY_ITEM_ID; if it isn't, I could use something else, ROWNUM for example - but then the problem wouldn't be well defined anyway.)
So, here it is. Please review and let me know if you have any questions.
with prep as (
select inventory_item_id, type_qty, age_in_days,
case count(case when type_qty < 0 then 1 end)
over (partition by inventory_item_id order by age_in_days)
when 0 then 'Y' end as past_last_negative
from omsinvdt_temp
)
select inventory_item_id, sum(type_qty) as type_qty,
min(case when type_qty > 0 then age_in_days end) as age_in_days,
sum(sum(type_qty)) over (partition by inventory_item_id
order by max(age_in_days)) as running_total
from prep
group by inventory_item_id,
case past_last_negative when 'Y' then age_in_days end
order by inventory_item_id, age_in_days desc
;
INVENTORY_ITEM_ID TYPE_QTY AGE_IN_DAYS RUNNING_TOTAL
----------------- ---------- ----------- -------------
35253 25 4 159
35253 134 1 134
266234 0 3 0

Related

Oracle PL/SQL update based on values calculated from multiple tables

I need to write a PL/SQL that sums up the value from 2 tables and update the value to another table.
I have the following tables: ONLINE_SALES, STORE_SALES, TOTAL_SALES
Assume the tables are structured like this:
ONLINE_SALES: OS_ID, STORE_ID, SEQ, ITEM_NAME, PRICE, PURCHASED_DATE
STORE_SALES: SS_ID, STORE_ID, SEQ, ITEM_NAME, PRICE, PURCHASED_DATE
TOTAL_SALES: STORE_ID, YEAR, TOTAL_INCOME
I want to write a PL/SQL that runs monthly and sums up the income (values in PRICE field) made in the month from both ONLINE_SALES and STORE_SALES of each store (identified by STORE_ID) and add the value to record in TOTAL_SALES with relative year.
My idea is to first filter the record by PURCHASED_DATE from both table with a SELECT, loop through all selected rows and sum to a variable and at last update the result with an UPDATE. But I am stuck in the first step since I found that I cannot use SELECT and only SELECT INTO is available.
Any ideas on how such PL/SQL can be written?
This solution assumes your stored procedure takes a DATE which it uses to identify the month and the year which is being totalled. Perhaps your assignment is expecting a different form of input? No worries. The deriving the values of year and date range is separate from the main process, so it is easy to swap in some different logic.
The loop uses an explicit cursor with the FOR UPDATE syntax. This locks the TOTAL_SALES table, which means you are guaranteed to be able to update all the rows.
create or replace calc_total_sales
( p_month in date )
is
cursor c_tot_sales (p_year number) is
select * from total_sales
where year = p_year
for update of total_income;
c_tot_sales c_tot_sales%rowtype;
l_os_sales number;
l_ss_sales number;
l_year number;
1_first_day date;
1_last_day date;
begin
l_year := to_number( to_char( p_month, 'yyyy') );
l_first_day := trunc( p_month, 'mm');
l_last_day := last_day( p_month);
open c_tot_sales( l_year );
loop
fetch c_tot_sales into r_tot_sales;
exit when c_tot_sales%not found;
select sum(price)
into l_os_sales
from online_sales
where store_id = r_tot_sales.store_id
and purchased_date >= l_first_day
and purchased_date <= l_last_day;
select sum(price)
into l_ss_sales
from store_sales
where store_id = r_tot_sales.store_id
and purchased_date >= l_first_day
and purchased_date <= l_last_day;
update total_sales
set total_income = total_income + nvl(l_ss_sales,0) + nvl(l_os_sales,0)
where current of c_total_sales;
end loop;
close c_tot_sales;
commit;
end;
/

Insert into select statement on the same table

I'm currently migrating data from legacy system to the current system.
I have this INSERT statement inside a stored procedure.
INSERT INTO TABLE_1
(PRIMARY_ID, SEQUENCE_ID, DESCR)
SELECT LEGACY_ID PRIMARY_ID
, (SELECT COUNT(*) + 1
FROM TABLE_1 T1
WHERE T1.PRIMARY_ID = L1.LEGACY_ID) SEQUENCE_ID
, L1.DESCR
FROM LEGACY_TABLE L1;
However, whenever I have multiple values of LEGACY_ID from LEGACY_TABLE, the query for the SEQUENCE_ID doesn't increment.
Why is this so? I can't seem to find any documentation on how the INSERT INTO SELECT statement works behind the scenes. I am guessing that it selects all the values from the table you are selecting and then inserts them simultaneously after, that's why it doesn't increment the COUNT(*) value?
What other workarounds can I do? I cannot create a SEQUENCE because the SEQUENCE_ID must be based on the number of PRIMARY_ID that are present. They are both primary ids.
Thanks in advance.
Yes, The SELECT will be executed FIRST and only then the INSERT happens.
A Simple PL/SQL block below, will be a simpler approach, though not efficient.
DECLARE
V_SEQUENCE_ID NUMBER;
V_COMMIT_LIMIT:= 20000;
V_ITEM_COUNT := 0;
BEGIN
FOR REC IN (SELECT LEGACY_ID,DESCR FROM LEGACY_TABLE)
LOOP
V_SEQUENCE_ID:= 0;
SELECT COUNT(*)+1 INTO V_SEQUENCE_ID FROM TABLE_1 T1
WHERE T1.PRIMARY_ID = REC.LEGACY_ID
INSERT INTO TABLE_1
(PRIMARY_ID, SEQUENCE_ID, DESCR)
VALUES
(REC.LEGACY_ID,V_SEQUENCE_ID,REC.DESCR);
V_ITEM_COUNT := V_ITEM_COUNT + 1;
IF(V_ITEM_COUNT >= V_COMMIT_LIMIT)
THEN
COMMIT;
V_ITEM_COUNT := 0;
END IF;
END LOOP;
COMMIT;
END;
/
EDIT: Using CTE:
WITH TABLE_1_PRIM_CT(PRIMARY_ID, SEQUENCE_ID) AS
(
SELECT L1.LEGACY_ID,COUNT(*)
FROM LEGACY_TABLE L1
LEFT OUTER JOIN TABLE_1 T1
ON(L1.LEGACY_ID = T1.PRIMARY_ID)
GROUP BY L1.LEGACY_ID
)
INSERT INTO TABLE_1
(SELECT L1.LEGACY_ID,
CTE.SEQUENCE_ID+ (ROW_NUMBER() OVER (PARTITION BY L1.LEGACY_ID ORDER BY null)),
L1.DESCR
FROM TABLE_1_PRIM_CT CTE, LEGACY_TABLE L1
WHERE L1.LEGACY_ID = CTE.PRIMARY_ID);
PS: With your Millions of Rows, this is going to create a temp table
of same size, during execution. Do Explain Plan before actual execution.

How can I update a column with PL\SQL by using a calculated value

I created a dummy database for learning purposes, and I purposefully created some duplicated records in one of the tables. In every case I want to flag one of the duplicated records as Latest='Y', and the other record as 'N', and for every single record the Latest flag would be 'Y'.
I tried to use PlSQL to go through all of my records, but when I try to use the previously calculated value (which would tell that its a duplicated record) it says that:
ORA-06550: line 20, column 17:
PLS-00201: identifier 'COUNTER' must be declared
Here is the statement I try to use:
DECLARE
CURSOR cur
IS
SELECT order_id, order_date, person_id,
amount, successfull_order, country_id, latest, ROWCOUNT AS COUNTER
FROM (SELECT order_id,
order_date,
person_id,
amount,
successfull_order,
country_id,
latest,
ROW_NUMBER () OVER (PARTITION BY order_id, order_date,
person_id, amount, successfull_order, country_id
ORDER BY order_id, order_date,
person_id, amount, successfull_order, country_id) ROWCOUNT
FROM orders) orders
FOR UPDATE OF orders.latest;
rec cur%ROWTYPE;
BEGIN
FOR rec IN cur
LOOP
IF MOD (COUNTER, 2) = 0
THEN
UPDATE orders
SET latest = 'N'
WHERE CURRENT OF cur;
ELSE
UPDATE orders
SET latest = 'Y'
WHERE CURRENT OF cur;
END IF;
END LOOP;
END;
I am new to PlSQL so I tried to modify the statements I found here:
http://www.adp-gmbh.ch/ora/plsql/cursors/for_update.html
What should I change in my statement, or should I use a different approach?
Thanks for your answers in advance!
Botond
Your refer the ROWNUM as COUNTER in your cursor.
While fetching, you should be accessing it from the cursor reference like MOD (rec.COUNTER, 2)
You need to declare the variable COUNTER and then you need to maintain (ie increment) it in your loop.
I suspect that you example is just for learning PL/SQL. However be aware that it's often much more performant to do things with a single SQL statement, as opposed to using cursor loops.
Your issue is that COUNTER is an attribute of the cursor record rec and not a PL/SQL variable. So:
IF MOD (COUNTER, 2) = 0
Should be:
IF MOD (rec.COUNTER, 2) = 0
However, you do not need to use PL/SQL or cursors, it can be done in a single MERGE statement:
Oracle Setup:
CREATE TABLE orders ( order_id, order_date, latest ) AS
SELECT 1, DATE '2017-01-01', CAST( NULL AS CHAR(1) ) FROM DUAL UNION ALL
SELECT 1, DATE '2017-01-02', NULL FROM DUAL UNION ALL
SELECT 1, DATE '2017-01-03', NULL FROM DUAL UNION ALL
SELECT 2, DATE '2017-01-04', NULL FROM DUAL UNION ALL
SELECT 2, DATE '2017-01-01', NULL FROM DUAL UNION ALL
SELECT 3, DATE '2017-01-06', NULL FROM DUAL;
Update Statement:
MERGE INTO orders dst
USING ( SELECT ROW_NUMBER() OVER ( PARTITION BY order_id
ORDER BY order_date DESC ) AS rn
FROM orders
) src
ON ( src.ROWID = dst.ROWID )
WHEN MATCHED THEN
UPDATE SET latest = CASE src.rn WHEN 1 THEN 'Y' ELSE 'N' END;
Output:
SELECT * FROM orders;
ORDER_ID ORDER_DATE LATEST
-------- ---------- ------
1 2017-01-01 N
1 2017-01-02 N
1 2017-01-03 Y
2 2017-01-04 Y
2 2017-01-01 N
3 2017-01-06 Y

Oracle: how to INSERT if a row doesn't exist

What is the easiest way to INSERT a row if it doesn't exist, in PL/SQL (oracle)?
I want something like:
IF NOT EXISTS (SELECT * FROM table WHERE name = 'jonny') THEN
INSERT INTO table VALUES ("jonny", null);
END IF;
But it's not working.
Note: this table has 2 fields, say, name and age. But only name is PK.
INSERT INTO table
SELECT 'jonny', NULL
FROM dual -- Not Oracle? No need for dual, drop that line
WHERE NOT EXISTS (SELECT NULL -- canonical way, but you can select
-- anything as EXISTS only checks existence
FROM table
WHERE name = 'jonny'
)
Assuming you are on 10g, you can also use the MERGE statement. This allows you to insert the row if it doesn't exist and ignore the row if it does exist. People tend to think of MERGE when they want to do an "upsert" (INSERT if the row doesn't exist and UPDATE if the row does exist) but the UPDATE part is optional now so it can also be used here.
SQL> create table foo (
2 name varchar2(10) primary key,
3 age number
4 );
Table created.
SQL> ed
Wrote file afiedt.buf
1 merge into foo a
2 using (select 'johnny' name, null age from dual) b
3 on (a.name = b.name)
4 when not matched then
5 insert( name, age)
6* values( b.name, b.age)
SQL> /
1 row merged.
SQL> /
0 rows merged.
SQL> select * from foo;
NAME AGE
---------- ----------
johnny
If name is a PK, then just insert and catch the error. The reason to do this rather than any check is that it will work even with multiple clients inserting at the same time. If you check and then insert, you have to hold a lock during that time, or expect the error anyway.
The code for this would be something like
BEGIN
INSERT INTO table( name, age )
VALUES( 'johnny', null );
EXCEPTION
WHEN dup_val_on_index
THEN
NULL; -- Intentionally ignore duplicates
END;
I found the examples a bit tricky to follow for the situation where you want to ensure a row exists in the destination table (especially when you have two columns as the primary key), but the primary key might not exist there at all so there's nothing to select.
This is what worked for me:
MERGE INTO table1 D
USING (
-- These are the row(s) you want to insert.
SELECT
'val1' AS FIELD_A,
'val2' AS FIELD_B
FROM DUAL
) S ON (
-- This is the criteria to find the above row(s) in the
-- destination table. S refers to the rows in the SELECT
-- statement above, D refers to the destination table.
D.FIELD_A = S.FIELD_A
AND D.FIELD_B = S.FIELD_B
)
-- This is the INSERT statement to run for each row that
-- doesn't exist in the destination table.
WHEN NOT MATCHED THEN INSERT (
FIELD_A,
FIELD_B,
FIELD_C
) VALUES (
S.FIELD_A,
S.FIELD_B,
'val3'
)
The key points are:
The SELECT statement inside the USING block must always return rows. If there are no rows returned from this query, no rows will be inserted or updated. Here I select from DUAL so there will always be exactly one row.
The ON condition is what sets the criteria for matching rows. If ON does not have a match then the INSERT statement is run.
You can also add a WHEN MATCHED THEN UPDATE clause if you want more control over the updates too.
Using parts of #benoit answer, I will use this:
DECLARE
varTmp NUMBER:=0;
BEGIN
-- checks
SELECT nvl((SELECT 1 FROM table WHERE name = 'john'), 0) INTO varTmp FROM dual;
-- insert
IF (varTmp = 1) THEN
INSERT INTO table (john, null)
END IF;
END;
Sorry for I don't use any full given answer, but I need IF check because my code is much more complex than this example table with name and age fields. I need a very clear code. Well thanks, I learned a lot! I'll accept #benoit answer.
In addition to the perfect and valid answers given so far, there is also the ignore_row_on_dupkey_index hint you might want to use:
create table tq84_a (
name varchar2 (20) primary key,
age number
);
insert /*+ ignore_row_on_dupkey_index(tq84_a(name)) */ into tq84_a values ('Johnny', 77);
insert /*+ ignore_row_on_dupkey_index(tq84_a(name)) */ into tq84_a values ('Pete' , 28);
insert /*+ ignore_row_on_dupkey_index(tq84_a(name)) */ into tq84_a values ('Sue' , 35);
insert /*+ ignore_row_on_dupkey_index(tq84_a(name)) */ into tq84_a values ('Johnny', null);
select * from tq84_a;
The hint is described on Tahiti.
you can use this syntax:
INSERT INTO table_name ( name, age )
select 'jonny', 18 from dual
where not exists(select 1 from table_name where name = 'jonny');
if its open an pop for asking as "enter substitution variable" then use this before the above queries:
set define off;
INSERT INTO table_name ( name, age )
select 'jonny', 18 from dual
where not exists(select 1 from table_name where name = 'jonny');
You should use Merge:
For example:
MERGE INTO employees e
USING (SELECT * FROM hr_records WHERE start_date > ADD_MONTHS(SYSDATE, -1)) h
ON (e.id = h.emp_id)
WHEN MATCHED THEN
UPDATE SET e.address = h.address
WHEN NOT MATCHED THEN
INSERT (id, address)
VALUES (h.emp_id, h.address);
or
MERGE INTO employees e
USING hr_records h
ON (e.id = h.emp_id)
WHEN MATCHED THEN
UPDATE SET e.address = h.address
WHEN NOT MATCHED THEN
INSERT (id, address)
VALUES (h.emp_id, h.address);
https://oracle-base.com/articles/9i/merge-statement
CTE and only CTE :-)
just throw out extra stuff. Here is almost complete and verbose form for all cases of life. And you can use any concise form.
INSERT INTO reports r
(r.id, r.name, r.key, r.param)
--
-- Invoke this script from "WITH" to the end (";")
-- to debug and see prepared values.
WITH
-- Some new data to add.
newData AS(
SELECT 'Name 1' name, 'key_new_1' key FROM DUAL
UNION SELECT 'Name 2' NAME, 'key_new_2' key FROM DUAL
UNION SELECT 'Name 3' NAME, 'key_new_3' key FROM DUAL
),
-- Any single row for copying with each new row from "newData",
-- if you will of course.
copyData AS(
SELECT r.*
FROM reports r
WHERE r.key = 'key_existing'
-- ! Prevent more than one row to return.
AND FALSE -- do something here for than!
),
-- Last used ID from the "reports" table (it depends on your case).
-- (not going to work with concurrent transactions)
maxId AS (SELECT MAX(id) AS id FROM reports),
--
-- Some construction of all data for insertion.
SELECT maxId.id + ROWNUM, newData.name, newData.key, copyData.param
FROM copyData
-- matrix multiplication :)
-- (or a recursion if you're imperative coder)
CROSS JOIN newData
CROSS JOIN maxId
--
-- Let's prevent re-insertion.
WHERE NOT EXISTS (
SELECT 1 FROM reports rs
WHERE rs.name IN(
SELECT name FROM newData
));
I call it "IF NOT EXISTS" on steroids. So, this helps me and I mostly do so.

How can I return multiple identical rows based on a quantity field in the row itself?

I'm using oracle to output line items in from a shopping app. Each item has a quantity field that may be greater than 1 and if it is, I'd like to return that row N times.
Here's what I'm talking about for a table
product_id, quanity
1, 3,
2, 5
And I'm looking a query that would return
1,3
1,3
1,3
2,5
2,5
2,5
2,5
2,5
Is this possible? I saw this answer for SQL Server 2005 and I'm looking for almost the exact thing in oracle. Building a dedicated numbers table is unfortunately not an option.
I've used 15 as a maximum for the example, but you should set it to 9999 or whatever the maximum quantity you will support.
create table t (product_id number, quantity number);
insert into t values (1,3);
insert into t values (2,5);
select t.*
from t
join (select rownum rn from dual connect by level < 15) a
on a.rn <= t.quantity
order by 1;
First create sample data:
create table my_table (product_id number , quantity number);
insert into my_table(product_id, quantity) values(1,3);
insert into my_table(product_id, quantity) values(2,5);
And now run this SQL:
SELECT product_id, quantity
FROM my_table tproducts
,( SELECT LEVEL AS lvl
FROM dual
CONNECT BY LEVEL <= (SELECT MAX(quantity) FROM my_table)) tbl_sub
WHERE tbl_sub.lvl BETWEEN 1 AND tproducts.quantity
ORDER BY product_id, lvl;
PRODUCT_ID QUANTITY
---------- ----------
1 3
1 3
1 3
2 5
2 5
2 5
2 5
2 5
This question is propably same as this: how to calc ranges in oracle
Update solution, for Oracle 9i:
You can use pipelined_function() like this:
CREATE TYPE SampleType AS OBJECT
(
product_id number,
quantity varchar2(2000)
)
/
CREATE TYPE SampleTypeSet AS TABLE OF SampleType
/
CREATE OR REPLACE FUNCTION GET_DATA RETURN SampleTypeSet
PIPELINED
IS
l_one_row SampleType := SampleType(NULL, NULL);
BEGIN
FOR cur_data IN (SELECT product_id, quantity FROM my_table ORDER BY product_id) LOOP
FOR i IN 1..cur_data.quantity LOOP
l_one_row.product_id := cur_data.product_id;
l_one_row.quantity := cur_data.quantity;
PIPE ROW(l_one_row);
END LOOP;
END LOOP;
RETURN;
END GET_DATA;
/
Now you can do this:
SELECT * FROM TABLE(GET_DATA());
Or this:
CREATE OR REPLACE VIEW VIEW_ALL_DATA AS SELECT * FROM TABLE(GET_DATA());
SELECT * FROM VIEW_ALL_DATA;
Both with same results.
(Based on my article pipelined function)

Resources