rewrite query without DENSE_RANK - oracle

I have one very slow query and try to optimize response time by using a materialized view. But one part is not compatible with General Restrictions on Fast Refresh.
How to rewrite it without DENSE_RANK?
create table t (id,object_id,log_cre_date) as
select 1,2,to_date('18/5/2010, 08:00','dd/mm/yyyy, hh:mi') from dual union all
select 2,2,to_date('18/5/2010, 10:00','dd/mm/yyyy, hh mi') from dual union all
select 3,3,to_date('18/5/2010, 11:00','dd/mm/yyyy, hh mi') from dual union all
select 4,3,to_date('18/5/2010, 12:10','dd/mm/yyyy, hh mi') from dual union all
select 5,4,to_date('18/5/2010, 12:20','dd/mm/yyyy, hh mi') from dual union all
select 6,4,to_date('18/5/2010, 11:30','dd/mm/yyyy, hh mi') from dual;
SELECT
MAX(t.id) KEEP(DENSE_RANK FIRST ORDER BY log_cre_date ASC) id,
t.object_id
FROM
t
GROUP BY
t.object_id

I am not sure the accepted answer is fast refreshable. Here is a query that definitely is:
SELECT max(cast(to_char(t.log_cre_date,'YYYYMMDDHH24MISS') || lpad(t.id,30,'0') as varchar2(80))) maxid,
t.object_id,
COUNT(*) cnt
FROM t
GROUP BY t.object_id;
The idea is to append the id to the log_cre_date and take the max of the concatenation. That way, you can extract the id you need later.
So, to get the id, you would do this:
SELECT to_char(substr(maxid,-30)) id, object_id
FROM your_materialized_view;
You could put that in a view to hide the complexity.
Here is a full example:
Create the base table
DROP TABLE t;
create table t (id,object_id,log_cre_date) as
select 1,2,to_date('18/5/2010, 08:00','dd/mm/yyyy, hh:mi') from dual union all
select 2,2,to_date('18/5/2010, 10:00','dd/mm/yyyy, hh mi') from dual union all
select 3,3,to_date('18/5/2010, 11:00','dd/mm/yyyy, hh mi') from dual union all
select 4,3,to_date('18/5/2010, 12:10','dd/mm/yyyy, hh mi') from dual union all
select 5,4,to_date('18/5/2010, 12:20','dd/mm/yyyy, hh mi') from dual union all
select 6,4,to_date('18/5/2010, 11:30','dd/mm/yyyy, hh mi') from dual;
Add some constraints to allow fast-refresh MV
ALTER TABLE t MODIFY id NOT NULL;
ALTER TABLE t ADD CONSTRAINT t_pk PRIMARY KEY ( id );
Create a snapshot log to enable fast refresh
--DROP MATERIALIZED VIEW LOG ON t;
CREATE MATERIALIZED VIEW LOG ON t WITH ROWID, PRIMARY KEY (OBJECT_ID, LOG_CRE_DATE) INCLUDING NEW VALUES;
Create the materialized view (note presence of COUNT(*) in select-list. Important!
--DROP MATERIALIZED VIEW t_mv;
CREATE MATERIALIZED VIEW t_mv
REFRESH FAST ON COMMIT AS
SELECT max(cast(to_char(t.log_cre_date,'YYYYMMDDHH24MISS') || lpad(t.id,30,'0') as varchar2(80))) maxid,
t.object_id,
COUNT(*) cnt
FROM t
GROUP BY t.object_id;
Test it out
select to_number(substr(maxid,-30)) id, object_id
from t_mv;
+----+-----------+
| ID | OBJECT_ID |
+----+-----------+
| 2 | 2 |
| 4 | 3 |
| 5 | 4 |
+----+-----------+
DELETE FROM t WHERE id = 5;
COMMIT;
select to_number(substr(maxid,-30)) id, object_id
from t_mv;
+----+-----------+
| ID | OBJECT_ID |
+----+-----------+
| 4 | 3 |
| 5 | 4 |
| 1 | 2 | -- Now ID #1 is the latest for object_id 2
+----+-----------+

Maybe this query will run faster:
select object_id, id
from (
select object_id, first_value(id) over(partition by object_id order by log_cre_date) as id
from t
)
group by object_id, id;
Hope it helps!

I went through the restriction but I am not sure if following query will work or not.
Try this and let us know if it works.
Select t.id, t.object_id from
T join
(SELECT
min(log_cre_date) mindt,
t.object_id
FROM
t
GROUP BY
t.object_id) t1
On t.object_id = t1.object_id
And t.log_cre_date = t1.mindt;
Cheers!!

Related

Oracle - how to update a unique row based on MAX effective date which is part of the unique index

Oracle - Say you have a table that has a unique key on name, ssn and effective date. The effective date makes it unique. What is the best way to update a current indicator to show inactive for the rows with dates less than the max effective date? I can't really wrap my head around it since there are multiple rows with the same name and ssn combinations. I haven't been able to find this scenario on here for Oracle and I'm having developer's block. Thanks.
"All name/ssn having a max effective date earlier than this time yesterday:"
SELECT name, ssn
FROM t
GROUP BY name, ssn
HAVING MAX(eff_date) < SYSDATE - 1
Oracle supports multi column in, so
UPDATE t
SET current_indicator = 'inactive'
WHERE (name,ssn,eff_date) IN (
SELECT name, ssn, max(eff_date)
FROM t
GROUP BY name, ssn
HAVING MAX(eff_date) < SYSDATE - 1
)
Use a MERGE statement using an analytic function to identify the rows to update and then merge on the ROWID pseudo-column so that Oracle can efficiently identify the rows to update (without having to perform an expensive self-join by comparing the values):
MERGE INTO table_name dst
USING (
SELECT rid,
max_eff_date
FROM (
SELECT ROWID AS rid,
effective_date,
status,
MAX( effective_date ) OVER ( PARTITION BY name, ssn ) AS max_eff_date
FROM table_name
)
WHERE ( effective_date < max_eff_date AND status <> 'inactive' )
OR ( effective_date = max_eff_date AND status <> 'active' )
) src
ON ( dst.ROWID = src.rid )
WHEN MATCHED THEN
UPDATE
SET status = CASE
WHEN src.max_eff_date = dst.effective_date
THEN 'active'
ELSE 'inactive'
END;
So, for some sample data:
CREATE TABLE table_name ( name, ssn, effective_date, status ) AS
SELECT 'aaa', 1, DATE '2020-01-01', 'inactive' FROM DUAL UNION ALL
SELECT 'aaa', 1, DATE '2020-01-02', 'inactive' FROM DUAL UNION ALL
SELECT 'aaa', 1, DATE '2020-01-03', 'inactive' FROM DUAL UNION ALL
SELECT 'bbb', 2, DATE '2020-01-01', 'active' FROM DUAL UNION ALL
SELECT 'bbb', 2, DATE '2020-01-02', 'inactive' FROM DUAL UNION ALL
SELECT 'bbb', 3, DATE '2020-01-01', 'inactive' FROM DUAL UNION ALL
SELECT 'bbb', 3, DATE '2020-01-03', 'active' FROM DUAL;
The query only updates the 3 rows that need changing and:
SELECT *
FROM table_name;
Outputs:
NAME | SSN | EFFECTIVE_DATE | STATUS
:--- | --: | :------------- | :-------
aaa | 1 | 01-JAN-20 | inactive
aaa | 1 | 02-JAN-20 | inactive
aaa | 1 | 03-JAN-20 | active
bbb | 2 | 01-JAN-20 | inactive
bbb | 2 | 02-JAN-20 | active
bbb | 3 | 01-JAN-20 | inactive
bbb | 3 | 03-JAN-20 | active
db<>fiddle here

Order by: Special characters before ABC

I have this sql:
SELECT -1 AS ID, '(None)' AS NAME
FROM TABLE_1 WHERE ID=1
UNION
SELECT ID, NAME
FROM TABLE_2
ORDER BY 2
Table data:
ID | NAME
1 | Direct
2 | Personal
3 | Etc
So if i execute this sql in Oracle 10 it returns these:
Result:
ID | NAME
1 | Direct
3 | Etc
-1 | (None)
2 | Personal
How is it possible to sort the "(None)" always to the top?
If i use
' (None) ' as Name
instead of
'(None)' as Name
It works, because the space before the (None), but that is not a solution.
You can add a dummy column ORDER_COL and then order on that column
select ID, NAME from
(
SELECT -1 AS ID, '(None)' AS NAME, 1 as ORDER_COL FROM TABLE_1 WHERE ID=1
UNION
SELECT ID, NAME, 2 as ORDER_COL FROM TABLE_2
)
order by ORDER_COL, NAME;
Try this. NULLS LAST is the default for ascending order in Oracle. make it NULLS FIRST for '(None)'. Also, use UNION ALL as UNION removes duplicates and is less efficient.
SELECT *
FROM (
SELECT -1 AS ID
,'(None)' AS NAME
FROM TABLE_1
WHERE ID = 1
UNION ALL
SELECT ID
,NAME
FROM TABLE_2
)
ORDER BY CASE
WHEN NAME = '(None)'
THEN NULL
ELSE NAME -- or id if you want
END NULLS FIRST;

NULL values not found in cursor

I am trying to:
Create a cursor that gets all the current prices of items in a store.
I bulk collect the cursor and loop upserting by using MERGE statement into STORE_INVENTORY table.
Now I want to NULL out the PRICE column in the STORE_INVENTORY table that are not in the cursor.
How can step 3 be done? I can do step 1 and 2 already as I have already updated or inserted the items that are pulled from the cursor.
Here is some example data:
There are three source tables where it is updated by an external party. My objective is to take these three sources of data and merge it into a singular table.
SOURCE TABLES
ITEM_TYPES
DESC_ID | TYPE
A | Kitchen
B | Bath
ITEM_MANIFEST
LOC_ID | ORIGIN
U | USA
C | CHINA
ITEM_PRICE
ITEM_ID | PRICE | DESC_ID | LOC_ID | DATE
0 | 3.99 | A | U | 9/11/2015
1 | 2.99 | B | C | 9/11/2015
2 | 1.99 | A | U | 9/05/2015
DESTINATION TABLE
STORE_INVENTORY
ITEM_ID | TYPE | ORIGIN | PRICE
0 | Kitchen | CHINA | 3.99
8 | Bath | USA | 2.99
So after I execute the SQL Procedure that has a date as a parameter. It will only pull from ITEM_PRICE if it's after the given date.
If execute the procedure with the passed in date 9/10/2015
Expected Output
STORE_INVENTORY
0 | Kitchen | USA | 3.99
1 | Bath | China | 2.99
8 | Bath | USA | NULL
So, something like this, then?
drop table item_description;
drop table item_manifest;
drop table item_price;
drop table store_inventory;
create table item_description
as
select 'A' desc_id, 'Kitchen' type from dual union all
select 'B' desc_id, 'Bath' type from dual;
create table item_manifest
as
select 'U' loc_id, 'USA' origin from dual union all
select 'C' loc_id, 'CHINA' origin from dual;
create table item_price
as
select 0 item_id, 3.99 price, 'A' desc_id, 'U' loc_id, to_date('11/09/2015', 'dd/mm/yyyy') dt from dual union all
select 1 item_id, 2.99 price, 'B' desc_id, 'C' loc_id, to_date('11/09/2015', 'dd/mm/yyyy') dt from dual union all
select 2 item_id, 1.99 price, 'A' desc_id, 'U' loc_id, to_date('05/09/2015', 'dd/mm/yyyy') dt from dual;
create table store_inventory
as
select 0 item_id, 'Kitchen' type, 'CHINA' origin, 3.99 price from dual union all
select 8 item_id, 'Bath' type, 'USA' origin, 2.99 price from dual;
select * from store_inventory;
ITEM_ID TYPE ORIGIN PRICE
---------- ------- ------ ----------
0 Kitchen CHINA 3.99
8 Bath USA 2.99
select coalesce(ip.item_id, si.item_id) item_id,
coalesce(id.type, si.type) type,
coalesce(im.origin, si.origin) origin,
ip.price
from item_description id
inner join item_price ip on (id.desc_id = ip.desc_id and ip.dt > to_date('10/09/2015', 'dd/mm/yyyy')) -- use a parameter for the date here
inner join item_manifest im on (ip.loc_id = im.loc_id)
full outer join store_inventory si on (si.item_id = ip.item_id);
ITEM_ID TYPE ORIGIN PRICE
---------- ------- ------ ----------
0 Kitchen USA 3.99
8 Bath USA
1 Bath CHINA 2.99
merge into store_inventory tgt
using (select coalesce(ip.item_id, si.item_id) item_id,
coalesce(id.type, si.type) type,
coalesce(im.origin, si.origin) origin,
ip.price
from item_description id
inner join item_price ip on (id.desc_id = ip.desc_id and ip.dt > to_date('10/09/2015', 'dd/mm/yyyy')) -- use a parameter for the date here
inner join item_manifest im on (ip.loc_id = im.loc_id)
full outer join store_inventory si on (si.item_id = ip.item_id)) src
on (src.item_id = tgt.item_id)
when matched then
update set tgt.type = src.type,
tgt.origin = src.origin,
tgt.price = src.price
when not matched then
insert (tgt.item_id, tgt.type, tgt.origin, tgt.price)
values (src.item_id, src.type, src.origin, src.price);
commit;
select * from store_inventory;
ITEM_ID TYPE ORIGIN PRICE
---------- ------- ------ ----------
0 Kitchen USA 3.99
8 Bath USA
1 Bath CHINA 2.99
Obviously, your procedure would have an input parameter of DATE datatype to pass into the query, and your query would use the parameter, rather than a hardcoded date like I did in my example. E.g. ip.dt > p_cutoff_date
I can do step 1 and 2 already as I have already updated or inserted
the items that are pulled from the cursor.
Hmm. These steps seem unnecessary - why not do them as part of the MERGE statement? What does the store_inventory table look like before you do your insert/update from the cursor? Also, what is the cursor you're using to do this?
couldn't you do a date-limited subselect of ITEM_PRICE.PRICE, after pulling in the TYPE and ORIGIN via the main join to ITEM_PRICE, without limiting on date?
i.e. something like.
select ITEM_ID, TYPE, ORIGIN
/* not selecting PRICE in the main join */
,(select PRICE from ITEM_PRICE where your join conditions
and DATE >= your param)
from ITEM_TYPES, ITEM_MANIFEST, ITEM_PRICE
where your join conditions, but no criteria on DATE
Sorry, would be clearer and easier to type up if you had provided your existing query.
From re-reading your question, I am unsure if you are inserting only 2 rows but want to get 3. Or if you have 3 rows, but you want to NULL out the missing price.
If the target table already has the 3 rows, then, instead of doing a CURSOR based approach (which can be slow on high volumes and is fussy to write), why not do an UPDATE instead, with DATE as a criteria? The NULL will be assigned to price if there is no match, that's how UPDATEs work.
UPDATE STORE_INVENTORY set PRICE
= (select PRICE from ITEM_PRICE where your join conditions
and DATE >= your param)

oracle select aplhanumeric value from the column

In oracle I have a table teststring and column name is STRINGVALUE and values in the column are :
A1CC
A2BB
C1DD
C2CC
ABA28
1B333
AB345
1A222
2NDDD
I have to select only those value which has first 2 values alphanumeric like A1CC,A2BB,1B33,2NDDD etc
SELECT stringvalue FROM teststring
WHERE regexp_like(substr(stringvalue,1,2),'[A-Z][0-9]|[0-9][A-Z]');
or
SELECT stringvalue FROM teststring
WHERE regexp_like(substr(stringvalue,1,2),'[[:alpha:]][[:digit:]]|[[:digit:]][[:alpha:]]');
select only those value which has first 2 values alphanumeric
Your example output doesn't satisfy the rule you mentioned. According to your rule, this is the test case :
SQL> WITH DATA AS(
2 SELECT 'A1CC' STR FROM DUAL UNION ALL
3 SELECT 'A2BB' STR FROM DUAL UNION ALL
4 SELECT 'C1DD' str from dual union all
5 SELECT 'C2CC' STR FROM DUAL UNION ALL
6 SELECT 'ABA28' str from dual union all
7 SELECT '1B333' STR FROM DUAL UNION ALL
8 SELECT 'AB345' STR FROM DUAL UNION ALL
9 SELECT '1A222' STR FROM DUAL UNION ALL
10 SELECT '2NDDD' FROM DUAL)
11 SELECT STR
12 FROM DATA
13 WHERE (REGEXP_LIKE(SUBSTR(STR,1,1),'[[:alpha:]]')
14 AND REGEXP_LIKE(SUBSTR(STR,2,1),'[[:digit:]]'))
15 OR (REGEXP_LIKE(SUBSTR(STR,1,1),'[[:digit:]]')
16 AND REGEXP_LIKE(SUBSTR(STR,2,1),'[[:alpha:]]'))
17 /
STR
-----
A1CC
A2BB
C1DD
C2CC
1B333
1A222
2NDDD
7 rows selected.
SQL>
Thanks Lalit, when i was using regular expression i was missing the keyword digit thats why i was unable to get the right result set.
in my requirement i have more than thousand value in that column so simply i have called the calum name from the table instead of using the dual union and its working fine..
the new query where any number of record available in the column below format would be feasible.
WITH DATA AS(SELECT STRINGVALUE as STR FROM TESTSTRING )
SELECT STR
FROM DATA
WHERE (REGEXP_LIKE(SUBSTR(STR,1,1),'[[:alpha:]]')
AND REGEXP_LIKE(SUBSTR(STR,2,1),'[[:digit:]]'))
OR (REGEXP_LIKE(SUBSTR(STR,1,1),'[[:digit:]]')
AND REGEXP_LIKE(SUBSTR(STR,2,1),'[[:alpha:]]'))
Use this Query
with TAB(TXT) as(
select 'A1CC' from dual union all
select 'A2BB' from dual union all
select 'C1DD' from dual union all
select 'C2CC' from dual union all
select 'ABA28' from dual union all
select '1B333' from dual union all
select 'AB345' from dual union all
select '1A222' from dual union all
select '2NDDD' from dual)
----------------
--- End of Data
----------------
select txt
from tab
where (regexp_like(TXT, '^((\w\d)|(\d\w)){1}\w+'));
To get output like:
| TXT |
|-------|
| A1CC |
| A2BB |
| C1DD |
| C2CC |
| 1B333 |
| 1A222 |
| 2NDDD |

Oracle sql retrive records based on maximum time

i have below data.
table A
id
1
2
3
table B
id name data1 data2 datetime
1 cash 12345.00 12/12/2012 11:10:12
1 quantity 222.12 14/12/2012 11:10:12
1 date 20/12/2012 12/12/2012 11:10:12
1 date 19/12/2012 13/12/2012 11:10:12
1 date 13/12/2012 14/12/2012 11:10:12
1 quantity 330.10 17/12/2012 11:10:12
I want to retrieve data in one row like below:
tableA.id tableB.cash tableB.date tableB.quantity
1 12345.00 13/12/2012 330.10
I want to retrieve based on max(datetime).
The data model appears to be insane-- it makes no sense to join an ORDER_ID to a CUSTOMER_ID. It makes no sense to store dates in a VARCHAR2 column. It makes no sense to have no relationship between a CUSTOMER and an ORDER. It makes no sense to have two rows in the ORDER table with the same ORDER_ID. ORDER is also a reserved word so you cannot use that as a table name. My best guess is that you want something like
select *
from customer c
join (select order_id,
rank() over (partition by order_id
order by to_date( order_time, 'YYYYMMDD HH24:MI:SS' ) desc ) rnk
from order) o on (c.customer_id=o.order_id)
where o.rnk = 1
If that is not what you want, please (as I asked a few times in the comments) post the expected output.
These are the results I get with my query and your sample data (fixing the name of the ORDER table so that it is actually valid)
SQL> ed
Wrote file afiedt.buf
1 with orders as (
2 select 1 order_id, 'iphone' order_name, '20121201 12:20:23' order_time from dual union all
3 select 1, 'iphone', '20121201 12:22:23' from dual union all
4 select 2, 'nokia', '20110101 13:20:20' from dual ),
5 customer as (
6 select 1 customer_id, 'paul' customer_name from dual union all
7 select 2, 'stuart' from dual union all
8 select 3, 'mike' from dual
9 )
10 select *
11 from customer c
12 join (select order_id,
13 rank() over (partition by order_id
14 order by to_date( order_time, 'YYYYMMDD HH24:MI:SS' ) desc ) rnk
15 from orders) o on (c.customer_id=o.order_id)
16* where o.rnk = 1
SQL> /
CUSTOMER_ID CUSTOM ORDER_ID RNK
----------- ------ ---------- ----------
1 paul 1 1
2 stuart 2 1
Try something like
SELECT *
FROM CUSTOMER c
INNER JOIN ORDER o
ON (o.CUSTOMER_ID = c.CUSTOMER_ID)
WHERE TO_DATE(o.ORDER_TIME, 'YYYYMMDD HH24:MI:SS') =
(SELECT MAX(TO_DATE(o.ORDER_TIME, 'YYYYMMDD HH24:MI:SS')) FROM ORDER)
Share and enjoy.

Resources