Row wise comparisons between two Oracle table - sql

I'm a beginner & trying to find a solution for the below issue
I have two table with same number of column, same names and same datatype.
I'm sorting both the table by time and trying to do row wise comparison (including data in each row) using not exist operation to identify the mismatch of records. If all the rows in both table are same then I need to return True else False.
Note: No relationship between the tables to do Join.

This is how I understood the question.
Sample tables: they have the same number of columns, with same names, same datatypes - but, you didn't say whether they contain the same number of rows. Mine don't:
SQL> select * from t1 order by id;
ID NAME COUNTRY
---------- ------ -------
1 Little Croatia
2 Foot Hungary
3 Scott UK
SQL> select * from t2 order by id;
ID NAME COUNTRY
---------- ------ -------
1 Little Croatia
2 Michel France
SQL>
The idea is to compare SHA1 hash values computed on concatenated values from both tables for comparative rows (which ones? Those that share the same row number).
SQL> set serveroutput on
SQL> declare
2 l_cnt_1 number;
3 l_cnt_2 number;
4 l_cnt number;
5 --
6 cursor c1 is
7 select id, name, country
8 from (select id, name, country,
9 row_number() over (order by id) rn
10 from t1)
11 order by rn;
12 cursor c2 is
13 select id, name, country
14 from (select id, name, country,
15 row_number() over (order by id) rn
16 from t2)
17 order by rn;
18 c1r c1%rowtype;
19 c2r c2%rowtype;
20 h1 raw(20);
21 h2 raw(20);
22 begin
23 -- which table is smaller (by number of rows)?
24 select count(*) into l_cnt_1 from t1;
25 select count(*) into l_cnt_2 from t2;
26 l_cnt := least(l_cnt_1, l_cnt_2);
27
28 open c1;
29 open c2;
30 -- loop as many times as there are rows in a smaller table; it doesn't make
31 -- sense comparing rows that don't have a "pair"
32 for i in 1 .. l_cnt loop
33 fetch c1 into c1r;
34 fetch c2 into c2r;
35
36 -- SHA1 hash
37 select sys.dbms_crypto.hash(utl_raw.cast_to_raw
38 (c1r.id || c1r.name || c1r.country), sys.dbms_crypto.hash_sh1),
39 sys.dbms_crypto.hash(utl_raw.cast_to_raw
40 (c2r.id || c2r.name || c2r.country), sys.dbms_crypto.hash_sh1)
41 into h1, h2
42 from dual;
43
44 dbms_output.put_line('Comparing');
45 dbms_output.put_line('- T1: ' || c1r.id ||', '|| c1r.name ||', '|| c1r.country);
46 dbms_output.put_line('- T2: ' || c2r.id ||', '|| c2r.name ||', '|| c2r.country);
47
48 if h1 = h2 then
49 dbms_output.put_line('- Result: match');
50 else
51 dbms_output.put_line('- Result: no match');
52 end if;
53 end loop;
54
55 close c1;
56 close c2;
57 end;
58 /
Result is:
Comparing
- T1: 1, Little, Croatia
- T2: 1, Little, Croatia
- Result: match
Comparing
- T1: 2, Foot, Hungary
- T2: 2, Michel, France
- Result: no match
PL/SQL procedure successfully completed.
SQL>

Related

Counting nulls for each column in table

We want to count how many nulls each column in a table has. There are too many columns to do this one by one, so the following PLSQL procedure was created.
In the first part of the procedure, all column names are obtained. This works, as the dbms_output correctly lists them all.
Secondly, a query inserts the count of null values in the variable 'nullscount'. This part does not work, as the output printed for this variable is always 0, even for columns where we know there are nulls.
Does anyone know how to handle the second part correctly?
Many thanks.
CREATE OR REPLACE PROCEDURE COUNTNULLS AS
nullscount int;
BEGIN
for c in (select column_name from all_tab_columns where table_name = upper('gp'))
loop
select count(*) into nullscount from gp where c.column_name is null;
dbms_output.put_line(c.column_name||' '||nullscount);
end loop;
END COUNTNULLS;
You can get it with just one query like this: this query scans table just once:
DBFiddle: https://dbfiddle.uk/asgrCezT
select *
from xmltable(
'/ROWSET/ROW/*'
passing
dbms_xmlgen.getxmltype(
(
select
'select '
||listagg('count(*)-count("'||column_name||'") as "'||column_name||'"',',')
||' from '||upper('gp')
from user_tab_columns
where table_name = upper('gp')
)
)
columns
column_name varchar2(30) path './name()',
cnt_nulls int path '.'
);
Results:
COLUMN_NAME CNT_NULLS
------------------------------ ----------
A 5
B 4
C 3
Dynamic sql in this query uses (24 chars + column name length) so it should work fine for example for 117 columns with average column name length = 10. If you need more, you can rewrite it a bit, for example:
select *
from xmltable(
'let $cnt := /ROWSET/ROW/CNT
for $r in /ROWSET/ROW/*[name() != "CNT"]
return <R name="{$r/name()}"> {$cnt - $r} </R>'
passing
dbms_xmlgen.getxmltype(
(
select
'select count(*) CNT,'
||listagg('count("'||column_name||'") as "'||column_name||'"',',')
||' from '||upper('gp')
from user_tab_columns
where table_name = upper('gp')
)
)
columns
column_name varchar2(30) path '#name',
cnt_nulls int path '.'
);
create table gp (
id number generated by default on null as identity
constraint gp_pk primary key,
c1 number,
c2 number,
c3 number,
c4 number,
c5 number
)
;
-- add some data with NULLS and numbers
DECLARE
BEGIN
FOR r IN 1 .. 20 LOOP
INSERT INTO gp (c1,c2,c3,c4,c5) VALUES
(CASE WHEN mod(r,2) = 0 THEN NULL ELSE mod(r,2) END
,CASE WHEN mod(r,3) = 0 THEN NULL ELSE mod(r,3) END
,CASE WHEN mod(r,4) = 0 THEN NULL ELSE mod(r,4) END
,CASE WHEN mod(r,5) = 0 THEN NULL ELSE mod(r,5) END
,5);
END LOOP;
END;
/
-- check what is in the table
SELECT * FROM gp;
-- do count of each column
DECLARE
l_colcount NUMBER;
l_statement VARCHAR2(100) := 'SELECT COUNT(*) FROM $TABLE_NAME$ WHERE $COLUMN_NAME$ IS NULL';
BEGIN
FOR r IN (SELECT column_name,table_name FROM user_tab_columns WHERE table_name = 'GP') LOOP
EXECUTE IMMEDIATE REPLACE(REPLACE(l_statement,'$TABLE_NAME$',r.table_name),'$COLUMN_NAME$',r.column_name) INTO l_colcount;
dbms_output.put_line('Table: '||r.table_name||', column'||r.column_name||', COUNT: '||l_colcount);
END LOOP;
END;
/
Table created.
Statement processed.
Result Set 4
ID C1 C2 C3 C4 C5
1 1 1 1 1 5
2 - 2 2 2 5
3 1 - 3 3 5
4 - 1 - 4 5
5 1 2 1 - 5
6 - - 2 1 5
7 1 1 3 2 5
8 - 2 - 3 5
9 1 - 1 4 5
10 - 1 2 - 5
11 1 2 3 1 5
12 - - - 2 5
13 1 1 1 3 5
14 - 2 2 4 5
15 1 - 3 - 5
16 - 1 - 1 5
17 1 2 1 2 5
18 - - 2 3 5
19 1 1 3 4 5
20 - 2 - - 5
20 rows selected.
Statement processed.
Table: GP, columnID, COUNT: 0
Table: GP, columnC1, COUNT: 10
Table: GP, columnC2, COUNT: 6
Table: GP, columnC3, COUNT: 5
Table: GP, columnC4, COUNT: 4
Table: GP, columnC5, COUNT: 0
c.column_name is never null because it's the content of the column "column_name" of the table "all_tab_columns"
not the column of which name is the value of c.column_name, in table gp.
You have to use dynamic query and EXECUTE IMMEDIATE to achieve what you want.

Having problems with this select in Oracle PLSQL

I have this code:
create view nombres_lab as
select L.NOMBRE_LAB, R.NOM_REG, count(*) as vacunas_suministradas
from LABORATORIOS L
inner join CREA_VACUNAS CV on l.cod_lab = cv.cod_lab
inner join vacunas V on cv.cod_vac = v.cod_vac
inner join vtorio VT on v.cod_vac = vt.cod_vac
inner join datos_vac_sum DVS on vt.cod_vtorio = dvs.cod_vtorio
inner join frasco_vac FV on dvs.s_unica = fv.s_unica
inner join direccion D on vt.cod_dir = d.cod_dir
inner join region R on d.cod_reg = r.cod_reg
where v.cod_vac = 100202 or v.cod_vac = 100303
group by l.nombre_lab, r.nom_reg;
create or replace procedure ranking is
begin
select nombre_lab, nom_reg, count(*) as vacunas_suministradas, rank() over(order by count(*) desc) as ranking from nombres_lab group by nom_reg, nombre_lab;
end;
I've been trying to convert this select to an select into so that it can work inside the procedure, but nothing that i try works.
Error: PLS-00428: an INTO clause is expected in this SELECT statement
I know its weird to do just a select in the procedure and nothing more, but i want to know if its possible
To answer your question (whether it is possible to do something like that): as Justin commented, it is (kind of), but you have to follow some rules.
For example (based on Scott's schema, as I don't have your tables):
SQL> CREATE OR REPLACE VIEW nombres_lab
2 AS
3 SELECT deptno nombre_lab, job nom_reg FROM emp;
View created.
This is a procedure which fixes error you got ("an INTO clause is expected in this SELECT statement"): declare some local variables and select into them.
SQL> CREATE OR REPLACE PROCEDURE ranking
2 IS
3 l_nombre_lab nombres_lab.nombre_lab%TYPE;
4 l_nom_reg nombres_lab.nom_reg%TYPE;
5 l_vacunas_suministradas NUMBER;
6 l_ranking NUMBER;
7 BEGIN
8 SELECT nombre_lab,
9 nom_reg,
10 COUNT (*),
11 RANK () OVER (ORDER BY COUNT (*) DESC)
12 INTO l_nombre_lab,
13 l_nom_reg,
14 l_vacunas_suministradas,
15 l_ranking
16 FROM nombres_lab
17 GROUP BY nom_reg, nombre_lab;
18 END;
19 /
Procedure created.
Will it work? Unfortunately, not. It would if query returned only one row, though:
SQL> EXEC ranking;
BEGIN ranking; END;
*
ERROR at line 1:
ORA-01422: exact fetch returns more than requested number of rows
ORA-06512: at "SCOTT.RANKING", line 8
ORA-06512: at line 1
So, what to do? You could use a loop (then you don't need any variables nor an into clause) and process values fetched. I don't have any smart ideas so I'm just displaying what cursor returned:
SQL> CREATE OR REPLACE PROCEDURE ranking
2 IS
3 BEGIN
4 FOR cur_r IN ( SELECT nombre_lab,
5 nom_reg,
6 COUNT (*) vacunas_suministradas,
7 RANK () OVER (ORDER BY COUNT (*) DESC) ranking
8 FROM nombres_lab
9 GROUP BY nom_reg, nombre_lab)
10 LOOP
11 DBMS_OUTPUT.put_line (
12 cur_r.nombre_lab
13 || ' - '
14 || cur_r.nom_reg
15 || ' - '
16 || cur_r.vacunas_suministradas
17 || ' - '
18 || cur_r.ranking);
19 END LOOP;
20 END;
21 /
Procedure created.
SQL> SET SERVEROUTPUT ON
SQL> EXEC ranking;
30 - SALESMAN - 4 - 1
20 - ANALYST - 2 - 2
20 - CLERK - 2 - 2
20 - MANAGER - 1 - 4
30 - CLERK - 1 - 4
30 - MANAGER - 1 - 4
10 - MANAGER - 1 - 4
10 - CLERK - 1 - 4
10 - PRESIDENT - 1 - 4
PL/SQL procedure successfully completed.
SQL>
Or, you could switch to a function that returns e.g. ref cursor:
SQL> CREATE OR REPLACE FUNCTION f_ranking
2 RETURN SYS_REFCURSOR
3 IS
4 rc SYS_REFCURSOR;
5 BEGIN
6 OPEN rc FOR SELECT nombre_lab,
7 nom_reg,
8 COUNT (*),
9 RANK () OVER (ORDER BY COUNT (*) DESC)
10 FROM nombres_lab
11 GROUP BY nom_reg, nombre_lab;
12
13 RETURN rc;
14 END;
15 /
Function created.
SQL> var l_rc refcursor
SQL> exec :l_rc := f_ranking
PL/SQL procedure successfully completed.
SQL> print l_rc
NOMBRE_LAB NOM_REG COUNT(*) RANK()OVER(ORDERBYCOUNT(*)DESC)
---------- --------- ---------- -------------------------------
30 SALESMAN 4 1
20 ANALYST 2 2
20 CLERK 2 2
20 MANAGER 1 4
30 CLERK 1 4
30 MANAGER 1 4
10 MANAGER 1 4
10 CLERK 1 4
10 PRESIDENT 1 4
9 rows selected.
SQL>
Therefore, yes - it is possible, but it depends on what you actually want to do.
The count(*) inside a rank function seems like an issue. Try below, I think that should solve the problem.
WITH nombre_lab_summary AS
(
SELECT
nombre_lab,
nom_reg,
COUNT(*) AS vacunas_suministradas,
FROM nombres_lab
GROUP BY
nom_reg,
nombre_lab
)
SELECT
*,
RANK() OVER (ORDER BY vacunas_suministradas DESC) AS ranking
FROM nombre_lab_summary
GROUP BY
nom_reg,
nombre_lab

Oracle procedure to increment string data type and store it as one of the column in table

I need to take the max value of the column id which starts with AB from Table A
I have a Table B where I need to update the column values max value +1 for all the rows present in Table B.
For example If I get the max value from Table A as AB1500, and Table B has 20 records and various columns, I need to populate from AB1501 to AB1520 for the records in Table B for the ORG_ID column.
Issue: For loop not able to write data into the TABLE B
Table A:
ORG_ID
ORG_NAME
AE500
Google
AB1500
Amazon
AB1200
Apple
Table B: Here For the available records i need to increment from max of AB from Table A
Country
Country_ID
ORG_ID
US
10
AB1501
UK
11
AB1502
FRANCE
12
AB1503
Create or replace procedure proc_incr(
v_org_id IN TableB.org_id%TYPE
)
v_max_number NUMBER;
v_max_org_id VARCHAR2(20);
v_max_var VARCHAR2(20);
v_temp VARCHAR2(20);
v_count NUMBER;
begin
select max(Table A.org_id)
into v_max_org_id
from TableA where org_id like 'AB%';
select count(*)
into v_count
from Table B;
select regexp_substr(v_max_org_id, '\d+')
into v_max_number
from dual;
select regexp_substr(v_max_org_id, '\D+')
into v_max_var
from dual;
// For Loop to write data to Table B
for i in 1 .. (v_count) LOOP
v_temp := v_max_number+i;
v_max_org_id := v_max_var||v_temp;
v_org_id := v_max_org_id;
End LOOP;
commit;
END proc_incr;
Sample data:
SQL> SELECT * FROM tablea;
ORG_ID
------
AB1500
CD1234
SQL> SELECT * FROM tableb;
ORG_ID NAME
------ ------
xxxxxx Little
Foot
Mahe
Your code, slightly fixed; you didn't perform any update, so I presume that's why you had an "issue"
For loop not able to write data into the TABLE B
Note the cursor for the tableb table; it is used to update current row fetched by it.
SQL> DECLARE
2 v_max_number NUMBER;
3 v_max_org_id VARCHAR2 (20);
4 v_max_var VARCHAR2 (20);
5
6 CURSOR curb IS
7 SELECT org_id, name
8 FROM tableb
9 FOR UPDATE;
10
11 cbr curb%ROWTYPE;
12 i NUMBER := 1;
13 BEGIN
14 SELECT MAX (tablea.org_id)
15 INTO v_max_org_id
16 FROM tablea
17 WHERE org_id LIKE 'AB%';
18
19 SELECT REGEXP_SUBSTR (v_max_org_id, '\d+') INTO v_max_number FROM DUAL;
20
21 SELECT REGEXP_SUBSTR (v_max_org_id, '\D+') INTO v_max_var FROM DUAL;
22
23 -- For Loop to write data to Table B
24 OPEN curb;
25
26 LOOP
27 FETCH curb INTO cbr;
28
29 EXIT WHEN curb%NOTFOUND;
30
31 v_max_org_id := v_max_var || TO_CHAR (v_max_number + i);
32
33 UPDATE tableb
34 SET org_id = v_max_org_id
35 WHERE CURRENT OF curb;
36
37 i := i + 1;
38 END LOOP;
39 END;
40 /
PL/SQL procedure successfully completed.
SQL> SELECT * FROM tableb;
ORG_ID NAME
------ ------
AB1501 Little
AB1502 Foot
AB1503 Mahe
SQL>

How to apply wildcard to column list in SQL

I would like to know How to use wildcard in select
For example, I would like to extract columns which contains for example test.
How can I extract this?
Select
From T2
My desired result is like this
test1 test2 test3
1 2 3
4 5 6
Thanks
You need to use the dynamic query as follows:
SQL> var lv_cur refcursor;
SQL>
SQL> DECLARE
2 LV_COLS VARCHAR2(32000);
3 LV_TABLE_NAME VARCHAR2(128) := 'STUDENTS';
4 LV_SCHEMA_NAME VARCHAR2(128) := 'TEJASH';
5 LV_COLUMN_STARTWITH VARCHAR2(10) := 'S';
6 --LV_CUR SYS_REFCURSOR;
7 BEGIN
8 SELECT
9 LISTAGG(COLUMN_NAME, ',') WITHIN GROUP(
10 ORDER BY
11 COLUMN_ID
12 )
13 INTO LV_COLS
14 FROM
15 ALL_TAB_COLUMNS
16 WHERE
17 TABLE_NAME = LV_TABLE_NAME
18 AND COLUMN_NAME LIKE LV_COLUMN_STARTWITH || '%'
19 AND OWNER = LV_SCHEMA_NAME;
20
21 IF LV_COLS IS NOT NULL THEN
22 OPEN :LV_CUR FOR 'SELECT '
23 || LV_COLS
24 || ' FROM '
25 || LV_SCHEMA_NAME
26 || '.'
27 || LV_TABLE_NAME;
28
29 ELSE
30 OPEN :LV_CUR FOR 'SELECT ''NO DATA'' FROM DUAL';
31 END IF;
32 END;
33 /
PL/SQL procedure successfully completed.
SQL> PRINT LV_cUR;
S_ID S_NAME S_ADDRESS S_LEVEL S_RECORDS_POINTS S_MAJOR S_CLASS
---------- ---------- ---------- ---------- ---------------- ---------- ----------
1 TEJASH A2 1 15.972 1 1
SQL>
I have used this block to fetch the data of the column having S as the starting character in the STUDENTS table(TEJASH schema). If there is no column satisfying the condition then NO DATA will be displayed.
You can do something like this
SQL Server
select *
from information_schema.columns
where column_name like 'test%'
order by
table_name
Oracle
select
table_name,
column_name
from all_tab_columns
where column_name like 'test%'
and owner in ('your_schema_name')

Table transformation / field parsing in PL/SQL

I have de-normalized table, something like
CODES
ID | VALUE
10 | A,B,C
11 | A,B
12 | A,B,C,D,E,F
13 | R,T,D,W,W,W,W,W,S,S
The job is to convert is where each token from VALUE will generate new row. Example:
CODES_TRANS
ID | VALUE_TRANS
10 | A
10 | B
10 | C
11 | A
11 | B
What is the best way to do it in PL/SQL without usage of custom pl/sql packages, ideally with pure SQL?
Obvious solution is to implement it via cursors. Any ideas?
Another alternative is to use the model clause:
SQL> select id
2 , value
3 from codes
4 model
5 return updated rows
6 partition by (id)
7 dimension by (-1 i)
8 measures (value)
9 ( value[for i from 0 to length(value[-1])-length(replace(value[-1],',')) increment 1]
10 = regexp_substr(value[-1],'[^,]+',1,cv(i)+1)
11 )
12 order by id
13 , i
14 /
ID VALUE
---------- -------------------
10 A
10 B
10 C
11 A
11 B
12 A
12 B
12 C
12 D
12 E
12 F
13 R
13 T
13 D
13 W
13 W
13 W
13 W
13 W
13 S
13 S
21 rows selected.
I have written up to 6 alternatives for this type of query in this blogpost: http://rwijk.blogspot.com/2007/11/interval-based-row-generation.html
Regards,
Rob.
I have a pure SQL solution for you.
I adapted a trick I found on an old Ask Tom site, posted by Mihail Bratu. My adaptation uses regex to tokenise the VALUE column, so it requires 10g or higher.
The test data.
SQL> select * from t34
2 /
ID VALUE
---------- -------------------------
10 A,B,C
11 A,B
12 A,B,C,D,E,F
13 R,T,D,W1,W2,W3,W4,W5,S,S
SQL>
The query:
SQL> select t34.id
2 , t.column_value value
3 from t34
4 , table(cast(multiset(
5 select regexp_substr (t34.value, '[^(,)]+', 1, level)
6 from dual
7 connect by level <= length(value)
8 ) as sys.dbms_debug_vc2coll )) t
9 where t.column_value != ','
10 /
ID VALUE
---------- -------------------------
10 A
10 B
10 C
11 A
11 B
12 A
12 B
12 C
12 D
12 E
12 F
13 R
13 T
13 D
13 W1
13 W2
13 W3
13 W4
13 W5
13 S
13 S
21 rows selected.
SQL>
Based on Celko's book, here is what I found and it's working well!
SELECT
TABLE1.ID
, MAX(SEQ1.SEQ) AS START_POS
, SEQ2.SEQ AS END_POS
, COUNT(SEQ2.SEQ) AS PLACE
FROM
TABLE1, V_SEQ SEQ1, V_SEQ SEQ2
WHERE
SUBSTR(',' || TABLE1.VALUE || ',', SEQ1.SEQ, 1) = ','
AND SUBSTR(',' || TABLE1.VALUE || ',', SEQ2.SEQ, 1) = ','
AND SEQ1.SEQ < SEQ2.SEQ
AND SEQ2.SEQ <= LENGTH(TABLE1.VALUE)
GROUP BY TABLE1.ID, TABLE1.VALUE, SEQ2.SEQ
Where V_SEQ is a static table with one field:
SEQ, integer values 1 through N, where N >= MAX_LENGTH(VALUE).
This is based on the fact the the VALUE is wrapped by ',' on both ends, like this:
,A,B,C,D,
If your tokens are fixed length (like in my case) I simply used PLACE field to calculate the actual string. If variable length, use start_pos and end_pos
So, in my case, tokens are 2 char long, so the final SQL is:
SELECT
TABLE1.ID
, SUBSTR(TABLE1.VALUE, T_SUB.PLACE * 3 - 2 , 2 ) AS SINGLE_VAL
FROM
(
SELECT
TABLE1.ID
, MAX(SEQ1.SEQ) AS START_POS
, SEQ2.SEQ AS END_POS
, COUNT(SEQ2.SEQ) AS PLACE
FROM
TABLE1, V_SEQ SEQ1, V_SEQ SEQ2
WHERE
SUBSTR(',' || TABLE1.VALUE || ',', SEQ1.SEQ, 1) = ','
AND SUBSTR(',' || TABLE1.VALUE || ',', SEQ2.SEQ, 1) = ','
AND SEQ1.SEQ < SEQ2.SEQ
AND SEQ2.SEQ <= LENGTH(TABLE1.VALUE)
GROUP BY TABLE1.ID, TABLE1.VALUE, SEQ2.SEQ
) T_SUB
INNER JOIN
TABLE1 ON TABLE1.ID = T_SUB.ID
ORDER BY TABLE1.ID, T_SUB.PLACE
Original Answer
In SQL Server TSQL we parse strings and make a table object. Here is sample code - maybe you can translate it.
http://rbgupta.blogspot.com/2007/10/tsql-parsing-delimited-string-into.html
Second Option
Count the number of commas per row. Get the Max number of commas. Let's say that in the entire table you have a row with 5 commas max. Build a SELECT with 5 substrings. This will make it a set based operation and should be much faster than a rbar.