UPDATE query with CASE WHEN in Oracle - sql

I have a table TABLE1 with 3 columns NAME, ROLLNO, CASHDATE.
CREATE TABLE TABLE1
(
NAME VARCHAR2(4) NOT NULL,
ROLLNO NUMBER(4) NOT NULL,
CASHDATE VARCHAR2(8) NOT NULL
);
INSERT INTO TABLE1 VALUES('SAMY', 1234, '15990101');
INSERT INTO TABLE1 VALUES('TOMY', 1324, '15990101');
INSERT INTO TABLE1 VALUES('DANY', 1342, '15990101');
TABLE1 looks like:
NAME ROLLNO CASHDATE
----------------------------------
SAMY 1234 15990101
TOMY 1324 15990101
DANY 1342 15990101
CASHDATE value is in the form of YYYYMMDD
I have a second table TABLE2 with 3 columns NAME, ID, ID_DATE:
CREATE TABLE TABLE2
(
NAME VARCHAR2(4) NOT NULL,
ID VARCHAR2(2) NOT NULL,
ID_DATE TIMESTAMP(3)
);
INSERT INTO TABLE2 VALUES('SAMY', '01', timestamp '2021-08-21 00:00:00');
INSERT INTO TABLE2 VALUES('SAMY', 'A1', timestamp '2018-08-19 00:00:00');
INSERT INTO TABLE2 VALUES('TOMY', '01', timestamp '2021-08-22 00:00:00');
INSERT INTO TABLE2 VALUES('TOMY', 'B1', timestamp '2000-08-15 00:00:00');
TABLE2 looks like:
NAME ID ID_DATE
--------------------------------------------------------
SAMY 01 21-AUG-2021 12.00.00.000000000 AM
SAMY A1 19-AUG-2018 12.00.00.000000000 AM
TOMY 01 22-AUG-2021 12.00.00.000000000 AM
TOMY B1 15-AUG-2000 12.00.00.000000000 AM
And I have a third table TABLE3 with 2 columns NAME, SEC_DATE:
CREATE TABLE TABLE3
(
NAME VARCHAR2(4) NOT NULL,
SEC_DATE TIMESTAMP(3)
);
INSERT INTO TABLE3 VALUES('SAMY', timestamp '2021-08-21 00:00:00');
INSERT INTO TABLE3 VALUES('TOMY', timestamp '2021-08-22 00:00:00');
INSERT INTO TABLE3 VALUES('DANY', timestamp '2021-08-29 00:00:00');
TABLE3 looks like:
NAME SEC_DATE
----------------------------------------------
SAMY 21-AUG-2021 12.00.00.000000000 AM
TOMY 22-AUG-2021 12.00.00.000000000 AM
DANY 29-AUG-2021 12.00.00.000000000 AM
As we see I have a TABLE1 having CASHDATE value as 15990101 which is default value.
So we need to UPDATE CASHDATE column which is having 15990101 in TABLE1 based on following conditions.
First it checks in TABLE2 based on NAME in TABLE2, if there is a record having same NAME with ID = '01' then ID_DATE value should update in CASHDATE column of TABLE1.
If it is not found in TABLE2 based on NAME with ID = '01', then it checks in TABLE3 and based on NAME only if there is a record, we need to update (SEC_DATE - 1) value in CASHDATE column of TABLE1.
Finally after update, the result TABLE1 looks like:
NAME ROLLNO CASHDATE
----------------------------------
SAMY 1234 20210821 --This record found in TABLE2
TOMY 1324 20210822 --This record found in TABLE2
DANY 1342 20210828 --This record found in TABLE3 (SEC_DATE - 1)
I understand we need to update statement but I am not sure using CASE WHEN in UPDATE statement.

You can use COALESCE in combination with subqueries for this:
update table1 t1
set cashdate =
coalesce
(
(select to_char(id_date, 'yyyymmdd') from table2 t2 where t2.name = t1.name and t2.id = '01'),
(select to_char(sec_date - interval '1' day, 'yyyymmdd') from table3 t3 where t3.name = t1.name),
'15990101'
)
where cashdate = '15990101';
Demo: https://dbfiddle.uk/?rdbms=oracle_18&fiddle=b6c5b08f5b303a23d26e5349228ee301

From my point of view, a simple option is a two-step option, i.e. two updates. In this case, I actually prefer MERGE over UPDATE.
Initial table1 contents:
SQL> select * From table1;
NAME ROLLNO CASHDATE
---- ---------- --------
SAMY 1234 15990101
TOMY 1324 15990101
DANY 1342 15990101
Update cashdate based on table2's contents (that's your 1st condition):
SQL> merge into table1 a
2 using table2 b
3 on (a.name = b.name)
4 when matched then update set
5 a.cashdate = to_char(b.id_date, 'yyyymmdd')
6 where a.cashdate = '15990101'
7 and b.id = '01';
2 rows merged.
Update cashdate based on table3's contents (that's your 2st condition) (not exists is here to skip rows already updated in previous step):
SQL> merge into table1 a
2 using table3 c
3 on (c.name = a.name)
4 when matched then update set
5 a.cashdate = to_char(c.sec_date - 1, 'yyyymmdd')
6 where a.cashdate = '15990101'
7 and not exists (select null from table2 b
8 where b.name = a.name
9 and b.id = '01'
10 );
1 row merged.
Final result:
SQL> select * from table1;
NAME ROLLNO CASHDATE
---- ---------- --------
SAMY 1234 20210821
TOMY 1324 20210822
DANY 1342 20210828
SQL>

You can query the data and update the table rows from the query result. This is called an updateable query. The great advantage is that we only update rows that we want updated (in your case rows that are on default value and have a match in table2 and/or table3).
For a query to be updateable, Oracle must see it guaranteed that there is just one target value selected per row. This means that we need unique constraints to ensure that joining table2 and table3 rows to a table1 row still results in no more than one row per table1 row.
The constraints needed here are
table2: primary key (name, id)
table3: primary key (name)
With these constraints in place, Oracle should be able to update our query:
update
(
select
t1.cashdate,
coalesce(t2.id_date, t3.sec_date - interval '1' day) as found_date
from table1 t1
left join table2 t2 on t2.name = t1.name and t2.id = '01'
left join table3 t3 on t3.name = t1.name
where t1.cashdate = '15990101'
and coalesce(t2.id_date, t3.sec_date - interval '1' day) is not null
)
set cashdate = to_char(found_date, 'yyyymmdd');
This may still fail. It really depends on whether Oracle considers this query updateable. It should (and it does in Oracle 18c, as you can see in the demo inked below), but sometimes Oracle struggles with such queries. You'll have to try whether this already works in Oracle 11g.
Demo: https://dbfiddle.uk/?rdbms=oracle_18&fiddle=36287406a7f5591d4b53df4a7b990ef6

Related

Select the records having column value length less than 7 satisfying criteria - Oracle

I have a table TABLE1 having NAME & CODE columns and I have table TABLE2 having NAME, COLUMN1 and COLUMN2 columns. We need to select a records from TABLE1 having the CODE column value which should be between the values of COLUMN1 and COLUMN2 columns.
If the CODE value length is greater than or equal to 7 then we need to check "seventh character" of CODE value
a) If the seventh character is D or S then don't select the record
b) If the seventh character is not D or S ,then check value of CODE column whether the value is between the values of COLUMN1 and COLUMN2 columns of TABLE2, if yes then select the record
If the CODE value length is less than 7, then we need to check whether the CODE value is between the values of COLUMN1 and COLUMN2 columns. If yes, select the record from TABLE1 else don't select
CREATE TABLE TABLE1 (NAME VARCHAR2(6), CODE VARCHAR2(10));
INSERT INTO TABLE1 VALUES ('JOHN', 'K062');
INSERT INTO TABLE1 VALUES ('JEFF', 'K08117');
INSERT INTO TABLE1 VALUES ('KATE', 'K08419');
INSERT INTO TABLE1 VALUES ('KIWI', 'M991011');
INSERT INTO TABLE1 VALUES ('TARA', 'S12312D');
INSERT INTO TABLE1 VALUES ('SOMA', 'T3499XS');
INSERT INTO TABLE1 VALUES ('RAMA', 'Z043');
INSERT INTO TABLE1 VALUES ('GEET', '1234567');
CREATE TABLE TABLE2 (NAME VARCHAR2(6), COLUMN1 VARCHAR2(10), COLUMN2 VARCHAR2(10));
INSERT INTO TABLE2 VALUES ('JOHN', 'K062', 'K062');
INSERT INTO TABLE2 VALUES ('JEFF', 'K08111', 'K08119');
INSERT INTO TABLE2 VALUES ('KATE', 'K08419', 'K08419');
INSERT INTO TABLE2 VALUES ('KIWI', 'M991010', 'M991010');
INSERT INTO TABLE2 VALUES ('TARA', 'S0000XA', 'S99912S');
INSERT INTO TABLE2 VALUES ('SOMA', 'T07', 'T3499XS');
INSERT INTO TABLE2 VALUES ('RAMA', 'Z041', 'Z043');
INSERT INTO TABLE2 VALUES ('GEET', '1234567', '1234567');
This is the query I have written,
SELECT T1.NAME, T1.CODE
FROM TABLE1 T1
JOIN TABLE2 T2 ON T1.NAME = T2.NAME
WHERE SUBSTR(T1.CODE,7,1) NOT IN ('D', 'S')
AND T1.CODE BETWEEN T2.COLUMN1 AND T2.COLUMN2;
If I query above, I was getting other result, what would be the query to get below result?
The result I got:
NAME CODE
-----------------
GEET 1234567
The result needs to be:
NAME CODE
--------------------
JOHN K062
JEFF K08117
KATE K08419
RAMA Z043
GEET 1234567
For rows with a code shorter than 7 characters, the substr will return null. This causes these to be excluded from the results.
To include them, have a null check:
SELECT T1.NAME, T1.CODE
FROM TABLE1 T1
JOIN TABLE2 T2 ON T1.NAME = T2.NAME
WHERE (
SUBSTR(T1.CODE,7,1) NOT IN ('D', 'S') OR
SUBSTR(T1.CODE,7,1) IS NULL
)
AND T1.CODE BETWEEN T2.COLUMN1 AND T2.COLUMN2;
NAME CODE
JOHN K062
JEFF K08117
KATE K08419
RAMA Z043
GEET 1234567

Query populating dates

query that generates records to hold a future calculated value.
Hi I trying to write a query with the tables below to populate a collection. I want the t2 values when the dates match but when there is not a match I want the dates to populate with a null values (will be populate later with a calculated value) The number of records for the same date should match the last time the dates matched. So in the example for each day after 7/1 there should be 3 records for each day and after 7/5 just 2. I am trying to do this in one query but I am not sure it is possible. Any help on creating this and getting into a collection would be much appreciated.
create table t1 as
WITH DATA AS
(SELECT to_date('07/01/2019', 'MM/DD/YYYY') date1,
to_date('07/10/2019', 'MM/DD/YYYY') date2
FROM dual
)
SELECT date1+LEVEL-1 the_date,
TO_CHAR(date1+LEVEL-1, 'DY','NLS_DATE_LANGUAGE=AMERICAN') day
FROM DATA
WHERE TO_CHAR(date1+LEVEL-1, 'DY','NLS_DATE_LANGUAGE=AMERICAN')
NOT IN ('SAT', 'SUN')
CONNECT BY LEVEL <= date2-date1+1
create table t2
(cdate date,
camount number);
insert into t2 values
('01-JUL-2019', 10);
insert into t2 values
('01-JUL-2019', 20);
insert into t2 values
('01-JUL-2019', 30);
insert into t2 values
('05-JUL-19', 50);
insert into t2 values
('05-JUL-19', 20);
expected results:
01-JUL-19 10
01-JUL-19 20
01-JUL-19 30
02-JUL-19 null
02-JUL-19 null
02-JUL-19 null
03-JUL-19 null
03-JUL-19 null
03-JUL-19 null
04-JUL-19 null
04-JUL-19 null
04-JUL-19 null
05-JUL-19 50
05-JUL-19 20
08-JUL-19 null
08-JUL-19 null
09-JUL-19 null
09-JUL-19 null
10-JUL-19 null
10-JUL-19 null
One approach to this kind of problem is to build the result set incrementally in a few steps:
Count matches that each THE_DATE in T1 has in T2.
Apply the rule you outlined in the question to those THE_DATE which have zero matches (carry forward (across dates in ascending order) the number of matches of the last THE_DATE that did have matches.
Generate the extra rows in T1 for the THE_DATE that have zero matches. (e.g. If it is supposed to have three null records, duplicate up to this number)
Outer join to T2 to get the CAMOUNT where it is available.
Here's an example (The three named subfactors corresponding to steps 1,2,3 above):
WITH DATE_MATCH_COUNT AS (
SELECT T1.THE_DATE,
COUNT(T2.CDATE) AS MATCH_COUNT,
ROW_NUMBER() OVER (PARTITION BY NULL ORDER BY T1.THE_DATE ASC) AS ROWKEY
FROM T1
LEFT OUTER JOIN T2
ON T1.THE_DATE = T2.CDATE
GROUP BY T1.THE_DATE),
ADJUSTED_MATCH_COUNT AS (
SELECT THE_DATE,
MATCH_COUNT AS ACTUAL_MATCH_COUNT,
GREATEST(MATCH_COUNT,
(SELECT MAX(MATCH_COUNT) KEEP ( DENSE_RANK LAST ORDER BY ROWKEY ASC )
FROM DATE_MATCH_COUNT SCALAR_MATCH_COUNT
WHERE SCALAR_MATCH_COUNT.ROWKEY <= DATE_MATCH_COUNT.ROWKEY AND
SCALAR_MATCH_COUNT.MATCH_COUNT > 0)) AS FORCED_MATCH_COUNT
FROM DATE_MATCH_COUNT),
GENERATED_MATCH_ROW AS (
SELECT THE_DATE, FORCED_MATCH_COUNT, MATCH_KEY
FROM ADJUSTED_MATCH_COUNT CROSS APPLY (SELECT LEVEL AS MATCH_KEY
FROM DUAL CONNECT BY LEVEL <= DECODE(ACTUAL_MATCH_COUNT,0,FORCED_MATCH_COUNT,1)))
SELECT THE_DATE, CAMOUNT
FROM GENERATED_MATCH_ROW
LEFT OUTER JOIN T2
ON GENERATED_MATCH_ROW.THE_DATE = T2.CDATE
ORDER BY THE_DATE, CAMOUNT ASC;
Result:
THE_DATE CAMOUNT
____________ __________
01-JUL-19 10
01-JUL-19 20
01-JUL-19 30
02-JUL-19
02-JUL-19
02-JUL-19
03-JUL-19
03-JUL-19
03-JUL-19
04-JUL-19
04-JUL-19
04-JUL-19
05-JUL-19 20
05-JUL-19 50
08-JUL-19
08-JUL-19
09-JUL-19
09-JUL-19
10-JUL-19
10-JUL-19

SQL Join Same Table and create single row result to complete NULL values

I have only one source table in SQL. I am trying to join it to itself so that i can fill up NULL values and retain the priority values into single row. There are million of records.
FROM THIS:
ID1 Source1ID Source2ID Owner Source1Date Source1Desc Source2Date Source2Desc
8 1asd23 a567nm Source1 1/1/1900 Active NULL NULL
9 1asd23 b555cc Source2 NULL NULL 12/1/2000 Ongoing
TO THIS (expected result):
ID1 Source1ID Source2ID Owner Source1Date Source1Desc Source2Date Source2Desc
8 1asd23 b555cc Source1 1/1/1900 Active 12/1/2000 Ongoing
So how can i join the table to itself to attain that single row result?
I have tried using the query below and made it as a VIEW but it takes forever to execute just to retrieve 1 row.
--CREATE VIEW dbo.vw_JOIN AS
WITH MyTABLE AS (
SELECT * FROM TABLE1
WHERE Owner = 'Source2')
SELECT
T1.ID1,
T1.Source1ID,
T2.Source2ID,
T1.Owner,
T1.Source1Date,
T1.Source1Desc,
T2.Source2Date
T2.Source2Desc
FROM TABLE1 T1
LEFT JOIN MyTABLE T2
ON T1.Source1ID = T2.Source1ID
OR T1.Source2ID = T2.Source2ID
WHERE T1.Owner = 'Source1'
Screenshot of Table values
Is this what you want?
select min(id1) as id1, Source1ID, min(Owner) as owner,
min(Source2ID) as Source2ID, min(Source1Desc) as Source1Desc,
min(Source2Date) as Source2Date, min(Source2Desc) as Source2Desc
from t
group by source1ID;
Is this what you want?
declare #table table (id1 int, source1id varchar(50),source2Id varchar(50),[owner] varchar(50),Source1Date date,Source1Desc varchar(50),Source2Date date, source2Desc varchar(50)
)
insert into #table
values
(8, '1asd23', 'a567nm' ,'Source1' ,'1/1/1900' , 'Active', NULL , NULL),
(9 , '1asd23', 'b555cc' ,'Source2', NULL , NULL , '12/1/2000', 'Ongoing')
select a.id1,a.source1id,b.source2Id,a.owner,a.Source1Date,a.Source1Desc,b.Source2Date,b.source2Desc from #table a
inner join #table b on a.source1id = b.source1id and a.source2Id != b.source2Id where a.owner ='source1'

Oracle -- Update the exact column referenced in the ON clause

I think this requirement is rarely encountered so I couldn't search for similar questions.
I have a table that needs to update the ID. For example ID 123 in table1 is actually supposed to be 456. I have a separate reference table built that stores the mapping (e.g. old 123 maps to new id 456).
I used the below query but apparently it returned error 38104, columns referenced in the ON clause cannot be updated.
MERGE INTO table1
USING ref_table ON (table1.ID = ref_table.ID_Old)
WHEN MATCHED THEN UPDATE SET table.ID = ref_table.ID_New;
Is there other way to achieve my purpose?
Thanks and much appreciated for your answer!
Use the ROWID pseudocolumn:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE TABLE1( ID ) AS
SELECT 1 FROM DUAL UNION ALL
SELECT 2 FROM DUAL UNION ALL
SELECT 3 FROM DUAL;
CREATE TABLE REF_TABLE( ID_OLD, ID_NEW ) AS
SELECT 1, 4 FROM DUAL UNION ALL
SELECT 2, 5 FROM DUAL;
MERGE INTO TABLE1 dst
USING ( SELECT t.ROWID AS rid,
r.id_new
FROM TABLE1 t
INNER JOIN REF_TABLE r
ON ( t.id = r.id_old ) ) src
ON ( dst.ROWID = src.RID )
WHEN MATCHED THEN
UPDATE SET id = src.id_new;
Query 1:
SELECT * FROM table1
Results:
| ID |
|----|
| 4 |
| 5 |
| 3 |
You can't update a column used in the ON clause in a MERGE. But if you don't need to make other changes that MERGE allows like WHEN NOT MATCHED or deleting, etc. you can just use a UPDATE to achieve this.
You mentioned this is an ID that needs an update. Here's an example using a scalar subquery. As it is an ID, this presumes UNIQUE ID_OLD values in REF_TABLE. I wasn't sure if Every row needs an update or only a sub-set, so set the update here to only update rows that have a value in REF_TABLE.
CREATE TABLE TABLE1(
ID NUMBER
);
CREATE TABLE REF_TABLE(
ID_OLD NUMBER,
ID_NEW NUMBER
);
INSERT INTO TABLE1 VALUES (1);
INSERT INTO TABLE1 VALUES (2);
INSERT INTO TABLE1 VALUES (100);
INSERT INTO REF_TABLE VALUES (1,10);
INSERT INTO REF_TABLE VALUES (2,20);
Initial State:
SELECT * FROM TABLE1;
ID
1
2
100
Then make the UPDATE
UPDATE TABLE1
SET TABLE1.ID = (SELECT REF_TABLE.ID_NEW
FROM REF_TABLE
WHERE REF_TABLE.ID_OLD = ID)
WHERE TABLE1.ID IN (SELECT REF_TABLE.ID_OLD
FROM REF_TABLE);
2 rows updated.
And check the change:
SELECT * FROM TABLE1;
ID
10
20
100

Merge two tables with same column names, add counters

I have two tables with the same columns, the first column is the name and the second is a count. I would like to merge these tables, so that each name appears with the added count of the two tables:
Table1: Table2: Result Table:
NAME COUNT NAME COUNT NAME COUNT
name1 1 name3 3 name1 1
name2 2 name4 4 name2 2
name3 3 name5 5 name3 6
name4 4 name6 6 name4 8
name5 5
name6 6
As of the moment I have created a pretty ugly structure to execute this, and would like to know if it is possible to get the results in a more elegant way.
What I have so far (Table1 is test1 and Table2 is test2):
create table test1 ( name varchar(40), count integer);
create table test2 ( name varchar(40), count integer);
create table test3 ( name varchar(40), count integer);
create table test4 ( name varchar(40), count integer);
create table test5 ( name varchar(40), count integer);
insert into test4 (name, count) select * from test1;
insert into test4 (name, count) select * from test2;
insert into test3 (name , count) select t1.name, t1.count + t2.count
from test1 t1 inner join test2 t2 on t1.name = t2.name;
select merge_db(name, count) from test3;
insert into test5 (name, count) (select name, max(count) from test4 group by name);
CREATE FUNCTION merge_db(key varchar(40), data integer) RETURNS VOID AS
$$ -- souce: http://stackoverflow.com/questions/1109061/insert-on-duplicate-update-postgresql
BEGIN
LOOP
-- first try to update the key
UPDATE test4 SET count = data WHERE name = key;
IF found THEN
RETURN;
END IF;-- not there, so try to insert the key -- if someone else inserts the same key concurrently, -- we could get a unique-key failure
BEGIN
INSERT INTO test4(name,count) VALUES (key, data);
RETURN;
EXCEPTION WHEN unique_violation THEN-- do nothing, and loop to try the UPDATE again
END;
END LOOP;
END;
$$
LANGUAGE plpgsql;
=> create table t1 (name text,cnt int);
=> create table t2 (name text,cnt int);
=> insert into t1 values ('name1',1), ('name2',2), ('name3',3), ('name4',4);
=> insert into t2 values ('name3',3), ('name4',4), ('name5',5), ('name6',6);
=>
select name,sum(cnt) from
(select * from t1
union all
select * from t2 ) X
group by name
order by 1;
name | sum
-------+-----
name1 | 1
name2 | 2
name3 | 6
name4 | 8
name5 | 5
name6 | 6
(6 rows)
How about this, in pure SQL:
SELECT
COALESCE(t1.name, t2.name),
COALESCE(t1.count, 0) + COALESCE(t2.count, 0) AS count
FROM t1 FULL OUTER JOIN t2 ON t1.name=t2.name;
Basically we're doing a full outer join on the name field to merge the two tables. The tricky part is that with the full outer join, rows that exist in one table but not the other will appear, but will have NULL in the other table; so if t1 has "name1" but t2 doesn't, the join will give us NULLs for t2.name and t2.name.
The COALESCE function returns the first non-NULL argument, so we use it to "convert" the NULL counts to 0 and to pick the name from the correct table. Thanks for the tip on this Wayne!
Good luck!
An alternative method is to use the NATURAL FULL OUTER JOIN combined with SUM(count) and GROUP BY name statements. The following SQL code exactly yields the desired result:
SELECT name, SUM(count) AS count FROM
( SELECT 1 AS tableid, * FROM t1 ) AS table1
NATURAL FULL OUTER JOIN
( SELECT 2 AS tableid, * FROM t2 ) AS table2
GROUP BY name ORDER BY name
The artificial tableid column ensures that the NATURAL FULL OUTER JOIN creates a separate row for each row in t1 and for each row in t2. In other words, the rows "name3, 3" and "name4, 4" appear twice in the intermediate result. In order to merge these duplicate rows and to sum the counts we can group the rows by the name column and sum the count column.