How to manipulate VARRAYS in sql (oracle)? - sql

Supposing i am using a table person, and persons might have multiple last names, so that attribute should be a varray of 3 elements for example (it's not about where to store last names), here is a simple sql for creating the type last name, the table person and adding an example row in oracle's sql developper (11G XE):
create type lastn as varray(3) of varchar2(10);
CREATE TABLE person
(
ID NUMBER NOT NULL
, last_name lastn
, CONSTRAINT EXEMPLE_PK PRIMARY KEY
(
ID
)
ENABLE
);
insert into person values(1,lastn('dani','bilel'));
I know how to update all last names at once, but i need to preserve existing last names and add other last names, or remove a single last name without affecting the others. In a nutshell, i want my code to be like (i am not familiar with PL/SQL):
insert into table
(select last_name from example where id=1)
values lastn('new');
This is the case where i want to get persons that have a first last name of 'bilel' and second last_name as 'dani'
select * from person where id in (select id from pernom p,table(p.last_name)
where column_value(1)='bilel' and column_value(2)='dani');
I know that it doesn't work like that, but i want to know CRUD(create update delete) statements in that case. and select statement with varray in where statement.
Thanks for your response.

From the docs:
Oracle does not support piecewise updates on VARRAY columns. However, VARRAY columns can be inserted into or updated as an atomic unit.
As shown in the examples there, you can manipulate the collection through PL/SQL instead; incuding adding an element to the array:
declare
l_last_name lastn;
begin
select last_name into l_last_name
from person where id = 1;
l_last_name.extend();
l_last_name(l_last_name.count) := 'third';
update person
set last_name = l_last_name
where id = 1;
end;
/
PL/SQL procedure successfully completed.
select last_name from person where id = 1;
LAST_NAME
--------------------------------------------------
LASTN('dani', 'bilel', 'third')
You can also do this via cast(multiset(...) as ...):
-- rollback; to reverse PL/SQL block actions above
update person p
set last_name = cast(multiset(
select column_value
from table (last_name)
union all
select 'third' from dual
) as lastn)
where id = 1;
1 row updated.
select last_name from person where id = 1;
LAST_NAME
--------------------------------------------------
LASTN('dani', 'bilel', 'third')
That explodes the existing last_name value into multiple rows, union's in a new value, and then converts the combined result back into your varray type.
And you can delete or update elements in a similar way:
update person p
set last_name = cast(multiset(
select column_value
from table (last_name)
where column_value != 'bilel'
) as lastn)
where id = 1;
1 row updated.
select last_name from person where id = 1;
LAST_NAME
--------------------------------------------------
LASTN('dani', 'third')
update person p
set last_name = cast(multiset(
select case column_value when 'third' then 'second' else column_value end
from table (last_name)
) as lastn)
where id = 1;
1 row updated.
select last_name from person where id = 1;
LAST_NAME
--------------------------------------------------
LASTN('dani', 'second')

For the select statement, i've figured out the solution, which goes like this :
select * from person p where id in (select id from table(p.last_name) where
column_value='bilel' intersect select id from table(p.last_name) where
column_value='dani');
or
select * from agent ag where id in (select id from table(ag.prenom)
t1,table(ag.prenom) t2,table(ag.prenom) t3 where t1.column_value='bilel' and
t2.column_value='dani' and t3.column_value='third');

Related

Update every row with auto-generated value

I have a table:
CREATE TABLE "person" (
"ID" NUMBER(38,0),
"NAME" VARCHAR2(50 CHAR)
)
Data:
id name
1 monica
2 null
3 null
4 stephan
5 null
I need update null values with value "person" and auto incremented value (from 1 - person1, person2, person3 etc):
1 monica
2 person1
3 person2
4 stephan
5 person3
What sql (oracle) query should I use?
Analytic function is not needed. The update can be done simply like this:
UPDATE person
SET NAME = 'person' || ROWNUM
WHERE name IS NULL;
Update
Since the person number needs to be sequential with the IDs of the person, this query can be used:
UPDATE person p
SET p.name =
'person'
|| (SELECT person_number
FROM (SELECT p2.id, ROW_NUMBER () OVER (ORDER BY p2.id) AS person_number
FROM person p2
WHERE name IS NULL)
WHERE id = p.id)
WHERE p.name IS NULL;
You actually have 2 issues, although your question only addresses 1 of them. You must not only 'fix the data' but also 'fix the system'. With the second (unaddressed) being the most important. There are 2 methods for this, lets call them
the correct method
-- update existing data
update person1
set name = 'PERSON' || to_char(rownum,'FM999')
where id is null;
-- fix the system
alter table person modify name not null;
the band-aid method
-- prepare for long term bandaid
create sequence unknown_person_seq;
-- update existing data
update person
set name = 'PERSON' || to_char(unknown_person_seq.nextval, 'FM99')
where name is null;
select * from person2;
-- adjust the system
alter table person modify name default 'Person' || to_char(unknown_person_seq.nextval, 'FM99');
The difference being the correct method keeps further data corruption out of the system. See demo here. It is far better to raise an exception for invalid data than it is to allow or generate bogus data. Consider your Person table holds customer information and you refer to them as PERSON4. How long will they remain your customer?
It works:
SET
name = (
SELECT n
FROM(
SELECT
id, 'person' || ROW_NUMBER() OVER (ORDER BY id) AS n
FROM
person
WHERE
name IS NULL
)u
WHERE
u.id = person.id)
WHERE
NAME IS NULL

PostgreSQL - insert a row in Table A, then update row in Table B with returned id

I have the following tables in a Postgres database (trivialized example):
Table A
person_id | UNIQUE SERIAL integer
first_name | text
last_name | text
full_name_id | integer[]
Table B
full_name_id | UNIQUE SERIAL integer
full_name | text
For each row of Table A, I would like to insert a new row into Table B with full_name = first_name || last_name, and update the new full_name_id in the corresponding row in Table A.
Is it possible to write a single query that does this?
This is what I have so far (again, trivialized):
WITH a AS (
SELECT first_name, last_name
FROM table_a
)
INSERT
INTO table_b(full_name)
SELECT first_name || last_name AS full_name
FROM a
RETURNING full_name_id;
In my real-world scenario, a similar query successfully inserts correct rows in Table B. However, I'm not sure what to do next, to update full_name_id in Table A.
Note: the full_name_id column in Table A is an array to better reflect my real-world scenario (one row in Table A can reference multiple rows in Table B). To the best of my knowledge, this means I can't use foreign keys.
Yes, you can do:
with i as (
insert into b (full_name)
select distinct first_name || ' ' || last_name
from a
returning *
)
update a
set full_name_id = i.full_name_id
from i
where i.full_name = a.first_name || ' ' || a.last_name;

PL SQL loop through list of ids

I have a list of names.
john, sam, peter, jack
I want to query the same sql with each of above as the filter. Each query will give me a unique employee id, which I want to use to delete some other records.
select emp_id from employee where emp_name like '%john%';
Let's say for the first query, I get the id as 1001. So the delete queries would be like following.
delete from account_details where emp_id = 1001;
delete from hr_details where emp_id = 1001;
delete from pay_role_details where emp_id = 1001;
I have to repeat this for a list of employees. Pseudocode would be like following.
var emp_list = ['john', 'jack', 'kate', 'peter', 'sam',...]
for each :employee_name in emp_list
select emp_id as :var_emp_id from employee where emp_name like '%:employee_name%';
delete from account_details where emp_id = :var_emp_id;
delete from hr_details where emp_id = :var_emp_id;
delete from pay_role_details where emp_id = :var_emp_id;
end loop
I want a PL-SQL query to do this. Please help. Thanks.
What I tried is something like the following.
set serveroutput on;
begin
loop x in ('john','jack', 'kate') loop as :name
select emp_id as var_emp_id from employee where emp_name like '%:name%';
// delete queries
end loop;
end;
P.S. Although accoring to the question, like query may result in multiple records, in actual scenario, it is guaranteed to be only one record. Why I use like is that in actual scenario, it is a list of reference numbers instead of names. The reference number has some other pre texts and post texts and my comma seperated list has only the numbers.
Perhaps the following will help:
BEGIN
FOR aName IN (SELECT 'john' AS EMP_NAME FROM DUAL
UNION ALL
SELECT 'sam' AS EMP_NAME FROM DUAL
UNION ALL
SELECT 'peter' AS EMP_NAME FROM DUAL
UNION ALL
SELECT 'jack' AS EMP_NAME FROM DUAL)
LOOP
FOR emp IN (SELECT * FROM EMPLOYEE WHERE EMP_NAME LIKE '%' || aName.EMP_NAME || '%')
LOOP
DELETE FROM ACCOUNT_DETAILS a WHERE a.EMP_ID = emp.EMP_ID;
DELETE FROM HR_DETAILS h WHERE h.EMP_ID = emp.EMP_ID;
DELETE FROM PAY_ROLE_DETAILS p WHERE p.EMP_ID = emp.EMP_ID;
DBMS_OUTPUT.PUT_LINE('Deleted data for employee with EMP_ID=' || emp.EMP_ID);
END LOOP; -- emp
END LOOP; -- aName
END;
Study this until you understand how and why it works.
Share and enjoy.
Do you really need a cursor to do so? Try to skip cursor if possible to avoid poor performance/memory usage on huge data.
delete from account_details inner join employee on account_details.emp_id = employee.emp_id where WHERE CONTAINS(employee.emp_name, '"John" OR "Sam" OR "Max"', 1) >0;
delete from hr_details inner join employee on hr_details.emp_id = employee.emp_id where WHERE CONTAINS(employee.emp_name, '"John" OR "Sam" OR "Max"', 1) >0;
delete from pay_role_details inner join employee on pay_role_details.emp_id = employee.emp_id where WHERE CONTAINS(employee.emp_name, '"John" OR "Sam" OR "Max"', 1) >0;
Use a PL/SQL cursor to select all the IDs you want to delete and then just loop it and issue the DELETE statements with every pass.
In-depth info on cursors can be found here: http://www.oracle.com/technetwork/issue-archive/2013/13-mar/o23plsql-1906474.html
For dynamic SQL see here: http://docs.oracle.com/cd/E11882_01/appdev.112/e25519/dynamic.htm#LNPLS627
Code example:
PROCEDURE delete_stuff
IS
id AS NUMBER;
CURSOR your_cursor IS
SELECT emp_id FROM employee WHERE CONTAINS(employee.emp_name, '"John" OR "Sam" OR "Max"', 1) > 0;
OPEN your_cursor;
LOOP
FETCH your_cursor INTO id;
EXIT WHEN your_cursor%NOTFOUND;
EXECUTE IMMEDIATE 'DELETE FROM account_details WHERE emp_id = :id' USING id;
EXECUTE IMMEDIATE 'DELETE FROM hr_details WHERE emp_id = :id' USING id;
EXECUTE IMMEDIATE 'DELETE FROM pay_role_details WHERE emp_id = :id' USING id;
CLOSE your_cursor;
END LOOP;
EXCEPTION
WHEN OTHERS THEN NULL;
END delete_stuff;

Custom Ordering of SELECT Results

I'm working with a Pro*C query, but this question should be general SQL. My research has been a dead end, but I think I'm missing something.
Suppose my server has an array of students' names, {"Alice","Charlie","Bob"}. I query the Student_Ids table for the students' ID numbers:
SELECT id_no FROM student_ids
WHERE student_name IN ('Alice','Charlie','Bob');
To simplify server-side processing, I want to sort the result set in the same order as the students' names. In other words, the result set would be {alice_id_no, charlie_id_no, bob_id_no} regardless of the actual ordering of the table or the behavior of any particular vendor's implementation.
The only solution I can think of is:
. . .
ORDER BY
CASE WHEN student_name='Alice' THEN 0
WHEN student_name='Charlie' THEN 1
WHEN student_name='Bob' THEN 2 END;
but that seems extremely messy and cumbersome when trying to dynamically generate/run this query.
Is there a better way?
UPDATE I gave a terrible example by pre-sorting the students' names. I changed the names to be deliberately unsorted. In other words, I want to sort the names in a non-ASC or DESC-friendly way.
UPDATE II Oracle, but for knowledge's sake, I am looking for more general solutions as well.
The ORDER BY expression you've given for your sample data is equivalent to ORDER BY student_name. Is that what you intended?
If you want a custom ordering that is not alphabetical, I think you might have meant something like this:
ORDER BY
CASE
WHEN student_name = 'Alice' THEN 0
WHEN student_name = 'Charlie' THEN 1
WHEN student_name = 'Bob' THEN 2
END;
You can use a derived table as well, that holds the names as well as the ordering you want. This way you only have to put the names in a single time:
SELECT S.id_no
FROM
student_ids AS S
INNER JOIN (
SELECT Name = 'Alice', Seq = 0 FROM DUAL
UNION ALL SELECT 'Bob', 2 FROM DUAL
UNION ALL SELECT 'Charlie', 1 FROM DUAL
) AS N
ON S.student_name = N.Name
ORDER BY
N.Seq;
You could also put them into a temp table, but in Oracle that could be somewhat of a pain.
Can you do this?
order by student_name
To do a custom sort, you only need one case:
ORDER BY (CASE WHEN student_name = 'Alice' THEN 1
WHEN student_name = 'Bob' THEN 2
WHEN student_name = 'Charlie' THEN 3
ELSE 4
END)
why not this :
SELECT id_no FROM student_ids
WHERE student_name IN ('Alice','Bob','Charlie')
ORDER BY student_name
You can ORDER BY any columns, not necessary those in SELECT list or WHERE clause
SELECT id_no
FROM student_ids
WHERE student_name IN ('Alice','Bob','Charlie)
ORDER BY id_no;
Add a table to hold the sort priorities then you can use the sort_priorities in whatever query you want (and easily update the priorities):
CREATE TABLE student_name_sort_priorities (
student_name VARCHAR2(30) CONSTRAINT student_name_sort_priority__pk PRIMARY KEY,
sort_priority NUMBER(10) CONSTRAINT student_name_sort_priority__nn NOT NULL
CONSTRAINT student_name_sort_priority__u UNIQUE
);
(If you want two values to be equivalently sorted then don't include the UNIQUE constraint.)
INSERT INTO student_name_sort_priorities VALUES ( 'Alice', 0 );
INSERT INTO student_name_sort_priorities VALUES ( 'Charlie', 2 );
INSERT INTO student_name_sort_priorities VALUES ( 'Bob', 1 );
Then you can join the sort priority table with the student_ids table and use the extra column to perform ordering:
SELECT id_no
FROM student_ids s
LEFT OUTER JOIN
student_name_sort_priorities p
ON (s.student_name = p.student_name)
ORDER BY
sort_priority NULLS LAST;
I've used a LEFT OUTER JOIN so that if a name is not contained on the student_name_sort_priorities table then it does not restrict the rows returned from the query; NULLS LAST is used in the ordering for a similar reason - any student names that aren't in the sort priorities table will return a NULL sort priority and be placed at the end of the ordering. If you don't want this then just use INNER JOIN and remove the NULLS LAST.
How about using a 'table of varchar' type like the build-in below:
TYPE dbms_debug_vc2coll is table of varchar2(1000);
test:
SQL> select customer_id, cust_first_name, cust_last_name from customers where cust_first_name in
2 (select column_value from table(sys.dbms_debug_vc2coll('Frederic','Markus','Dieter')));
CUSTOMER_ID CUST_FIRST_NAME CUST_LAST_NAME
----------- -------------------- --------------------
154 Frederic Grodin
149 Markus Rampling
152 Dieter Matthau
That seems to force the order, but that might just be bad luck. I'm not really a sql expert.
The execution plan for this uses 'collection iterator' instead of a big 'or' in the typical:
select customer_id, cust_first_name, cust_last_name from customers where cust_first_name in ('Frederic','Markus','Dieter');
hth, Hein.

PL/SQL Oracle Query With IF Statement

I want to implement a query that only returns the logged in user and displays there record only, which I have done as follows and it works:
SELECT * FROM EMPLOYEE
WHERE UPPER(username) = v('APP_USER')
However, I have another column called User_Type, and a user can be type 1, 2 or 3. If I have a user type of 1, I want the query to also return all the tables records too as user type 1 is an admin.
I thought about doing it like this:
BEGIN
SELECT * FROM Employee
WHERE upper(username) = v('APP_USER')
IF User_Type = 1
THEN SELECT * FROM Employee
END IF;
END;
/
But it doesn't work in APEX Oracle PLSQL.
Any suggestions?
From what I understand you need to try this:
DECLARE
emp employee%ROWTYPE; -- Create a record type
tbl_emp IS TABLE OF emp;
-- ^^^ Create a table of that record type
v_user_type employee.user_type%TYPE;
-- ^^^ Variable to store user type
BEGIN
SELECT user_type
INTO v_user_type
FROM Employee
WHERE upper(username) = v('APP_USER');
IF v_user_type = 1 THEN
SELECT *
BULK COLLECT INTO tbl_emp
FROM employee;
-- ^^ Returns the entire table
ELSE
SELECT *
BULK COLLECT INTO tbl_emp
FROM employee;
WHERE upper(username) = v('APP_USER');
-- ^^ Returns the row related to the user.
END IF;
END;
/
The output is stored in the nested table variable tbl_emp.
EDIT:
It can be achieved using pure SQL also, like this:
SELECT *
FROM employee e
WHERE EXISTS (SELECT 1
FROM employees e_in
WHERE e_in.user_type = 1
AND UPPER(e_in.username) = v('APP_USER'))
OR UPPER(e.username) = v('APP_USER')
Choose whichever is best suited for you.
You want all records from users with either UPPER(username) being v('APP_USER') or User_Type being 1? Then just use OR:
SELECT * FROM Employee WHERE upper(username) = v('APP_USER') OR User_Type = 1
If that's not what you mean, then can you explain more clearly?
Try:
select distinct e2.*
from employee e1
join employee e2 on (e1.username = e2.username or e1.User_Type = 1)
where UPPER(e1.username) = v('APP_USER')