How can I insert into a nested table from the resultset of a select statement? - sql

I have two tables with nested tables of the same type, the type is:
CREATE OR REPLACE TYPE tipo_valor AS OBJECT (
ano DATE, --year
cantidad INTEGER --ammount of exported wine
) ;
CREATE OR REPLACE TYPE hist_export AS OBJECT (
nombre_pais VARCHAR2(100), --name of importer country
cantidad tipo_valor --type referenced above
);
the nested table:
CREATE OR REPLACE TYPE nt_hist_exp IS
TABLE OF hist_export;
And my two tables are:
CREATE TABLE bodega ( --winery
id_bod INTEGER NOT NULL,
exp_an_bod nt_hist_exp ,
)
CREATE TABLE marca ( --wine
id_marca INTEGER NOT NULL,
exp_an_marca nt_hist_exp
)
I have procedure with a select statement that collects the export ammounts from the wine table on a certain year and orders it by country,
PROCEDURE exp_bod ( p_ano DATE,
p_bod_nom VARCHAR2)IS
sumatoria INTEGER;
p_idbod INTEGER;
BEGIN
SELECT id_bod INTO p_idbod
FROM bodega
WHERE nombre_bod = p_bod_nom;
DBMS_OUTPUT.PUT_LINE(to_char(p_idbod));
SELECT nt.nombre_pais,sum(nt.cantidad.cantidad)
INTO sumatoria
FROM bodega b
JOIN presentacion p on p.bodega_fk = b.id_bod
JOIN marca m on m.id_marca = p.marca_fk
CROSS JOIN TABLE(m.exp_an_marca) nt
WHERE b.id_bod = p_idbod
AND nt.cantidad.ano = p_ano
group by nt.nombre_pais
order by nt.nombre_pais;
);
end exp_bod;
the second select in this procedure successfully returns what I need which is a resultset with two columns,one with the country names and the second one with the export ammounts all summed up and ordered, what I want is to insert the rows from that resultset into the nested table in the winery table including the year which is received as an argument by the function

You could use insert as select, creating an instance of your object type as part of the query:
INSERT INTO TABLE (SELECT exp_an_bod FROM bodega b WHERE b.nombre_bod = p_bod_nom)
SELECT hist_export(nt.nombre_pais, tipo_valor(nt.cantidad.ano, sum(nt.cantidad.cantidad)))
FROM bodega b
JOIN presentacion p on p.bodega_fk = b.id_bod
JOIN marca m on m.id_marca = p.marca_fk
CROSS JOIN TABLE(m.exp_an_marca) nt
WHERE b.nombre_bod = p_bod_nom
AND nt.cantidad.ano = p_ano
GROUP BY nt.nombre_pais, nt.cantidad.ano;
I'm assuming nombre_bod is a column on bodega, though you haven't shown that in the table definition, which means you don't really need a separate look-up for that.
This also assumes that exp_an_bod is not null; it can be empty though. It also doesn't make any allowance for an existing row for the country, but it's not very clear from your data model whether than can exist or what should happen if it does. You can update en existing entry using the same mechanism though, as long as you can identify it.

You can do it in PL/SQL like this:
declare
hist_exp nt_hist_exp;
begin
select exp_an_bod
into hist_exp
from bodega
where id_bod = 123;
hist_exp.extend;
hist_exp(hist_exp.LAST) := hist_export('xyz', 456);
update bodega
set exp_an_bod = hist_exp
where id_bod = 123;
end;
If you like to UPDATE rather then INSERT you can also use
UPDATE (select nombre_pais, cantida, id_bod FROM bodega CROSS JOIN TABLE(exp_an_bod))
SET nombre_pais = 'abc'
WHERE id_bod = 123
and cantida = 456;
You may also try
INSERT INTO (select nombre_pais, cantida, id_bod FROM bodega CROSS JOIN TABLE(exp_an_bod)) ...
but I don't think this is possible - I never tried.

Related

Select query, insert into table, and return initial query in postgres

I have a rather complex plpgsql stored procedure and I need to select from multiple tables and insert as well.
This is part of what I currently have.
BEGIN
RETURN query
SELECT domains.id, webpages.id as page_id ...
FROM domains
LEFT JOIN domain_settings
ON domain_settings.domain_id = domains.id
RIGHT JOIN webpages
ON webpages.domain_id = domains.id
LEFT JOIN subscriptions
ON webpages.id = subscriptions.page_id
AND subscriptions.user_id = query_user_id
AND subscriptions.comment_id IS NULL
WHERE domains.domain_address = query_domain_url
IF NOT FOUND THEN ...
END;
$$ language plpgsql;
Now, I would like add an insert query into another table using certain values from the return query before the 'if not found then' statement:
INSERT INTO page_visits (domain_id, page_id)
SELECT id, page_id FROM ?? (return query statement)
And after the insert, I want to return the initial return query values. How do I go about doing this? I tried using WITH AS statements, but I can't seem to get it to work
A set-returning PL/pgSQL function builds the return stack while processing the function body. There is no way to access that return stack from within the same function. You could nest the function. Or use a temporary table.
But using a CTE is probably the simplest way for the cas at hand. Going out on a limb, you may be looking for something like this:
CREATE OR REPLACE FUNCTION demo(query_user_id int, query_domain_url text)
RETURNS TABLE (c1 int, c2 int)
LANGUAGE plpgsql AS
$func$
BEGIN
RETURN QUERY
WITH sel AS (
SELECT d.id, w.id as page_id ...
FROM webpages w
JOIN domains d ON d.id = w.domain_id
LEFT JOIN domain_settings ds ON ds.domain_id = d.id
LEFT JOIN subscriptions s ON s.page_id = w.id
AND s.user_id = query_user_id -- origin?
AND s.comment_id IS NULL
WHERE d.domain_address = query_domain_url -- origin?
)
, ins AS (
INSERT INTO tbl (col1, col2)
SELECT main.id, sel.page_id
FROM (SELECT 'foo') AS main(id)
LEFT JOIN sel USING (id) -- LEFT JOIN ?
)
TABLE sel;
IF NOT FOUND THEN
-- do something
END IF;
END
$func$;
Remember, if the transaction does not commit successfully, the INSERT is also rolled back.
The final TABLE sel is just short syntax for SELECT * FROM sel. See:
Is there a shortcut for SELECT * FROM?

Duplicate Values in Query

Novice SQL user here - I am trying to determine the delivery date (de_arrdate) for an order based on event data from the events table. A shipment can have multiple events, shipments usually have 4 events so the events table will return data based on shipment ID for all 4 events. Because of this, my total $$$ is overstated. How can I return only the largest value of the shipment sequence which would essentially be the final event date? My query is below. I've also attached a sample of the current output.
select dba.disp_ship.ds_id, dba.disp_ship.ds_bill_charge,
dba.disp_ship.ds_status, dba.disp_ship.ds_ship_type,
dba.disp_events.de_site, dba.disp_events.de_arrdate,
dba.disp_events.de_shipment_id, dba.disp_events.de_ship_seq
from dba.disp_ship
inner join dba.disp_events on dba.disp_ship.ds_id = dba.disp_events.de_shipment_id
Not sure which RDBMS you are using nor the version, but if I understood correctly, you only want the amount stated in the last event of the sequence, right?
In this case, you already have the order of the events in the de_ship_seq column, so all you need to do is:
with last_event as (
select
de.de_arrdate,
de.de_shipment_id,
max(de.de_ship_seq)
from dba.disp_events as de
group by 1, 2
)
select
ds.ds_id,
ds.ds_bill_charge,
ds.de_arrdate
from dba.disp_ship as ds
join last_event as le on ds.ds_id = le.de_shipment_id
This way, you'll not get duplicity by the table disp_events, since you're only grabbing the maximum of the sequence, which it's supposed to be the last event :)
There are two ways to achieve this scenario.
1. Inner Query
select dba.disp_ship.ds_id, dba.disp_ship.ds_bill_charge,
dba.disp_ship.ds_status, dba.disp_ship.ds_ship_type,
dba.disp_events.de_site, dba.disp_events.de_arrdate,
dba.disp_events.de_shipment_id, dba.disp_events.de_ship_seq
from dba.disp_ship
inner join dba.disp_events on dba.disp_ship.ds_id = dba.disp_events.de_shipment_id,
inner Join (Select a.de_shipment_id as shipid,max(a.de_arrdate) as arrdate
from disp_events a) as t on dba.disp_events.de_shipment_id = t.shipid and dba.disp_events.de_arrdate = t.arrdate
2. Procedure
//Datatype for the Temporary tables is an assumption. Replace with your data type.
begin
declare local temporary table tbl1(
ds_id numeric(10),
ds_bill_charge numeric(14,2),
ds_status int,
ds_ship_type int,
de_site char(20),
de_arrdate date,
de_shipment_id numeric(10),
de_ship_seq numeric(10)
)on commit preserve rows;
declare local temporary table tbl1(
rowid numeric(10);
shipmentid numeric(10)
)on commit preserve rows;
declare #rowcount,#ds_id,i numeric(10);
set i = 1;
insert into tbl1
select dba.disp_ship.ds_id, dba.disp_ship.ds_bill_charge,
dba.disp_ship.ds_status, dba.disp_ship.ds_ship_type,
dba.disp_events.de_site, dba.disp_events.de_arrdate,
dba.disp_events.de_shipment_id, dba.disp_events.de_ship_seq
from dba.disp_ship
inner join dba.disp_events on dba.disp_ship.ds_id = dba.disp_events.de_shipment_id;
insert into tbl2
select number(*), ds_id from(select distinct ds_id from tbl1) a;
select count(*) into #rowcount from tbl2;
while i <= #rowcount Loop
Select ds_id into #ds_id from tbl2 where rowid = i;
delete from tbl1 where ds_id = #ds_id and
de_ship_seq not in(select top 1 de_ship_seq from tbl1 a
where a.ds_id = #ds_id order by de_arrdate desc);
i++;
end Loop;
select * from tbl1;
end
Thank You...

PostgreSQL array of type table/setof

I need to code a function in postgresql that must 'pick' entire rows from a table depending on a condition and then insert them into an array, and finally return that array.
I'm actually querying the rows I need but there's an special column from the table that I must evaluate first to see if I can retrieve it or not (insert to the array), depending on the type of user is doing the query (customer or worker).
For example:
The special column is named "watchers" and has two variants: 'my_team' and 'only_me' and also has another column named "user_id".
If the column says 'my_team', the row can be inserted into the array with no problems, but if it says 'only_me' I must compare the id of the user calling the function and the user registered in that row, if they match I can insert it into the array, otherwise I can't.
Thing is that as I've read so far it seems that it can't be done with arrays, so I'd like to know if there's another way of doing this, maybe with an extra table or something?
Thanks for your help!
This is the code I have:
CREATE OR REPLACE FUNCTION public.calculates_permission_by_task(task_id integer)
RETURNS SETOF permission
LANGUAGE plpgsql
STABLE
AS $function$
DECLARE
num int := 5;
record permission;
ret_permissions permission [];
BEGIN
FOR record IN (select
p.id, p.slug
from task as t
join service_request_form as srf on t.service_request_form_id = srf.id
join service_group as sg on srf.service_group_id = sg.id
join worker_group as wg on wg.service_group_id = sg.id
join worker_group_member as wgm on wg.id = wgm.worker_group_id
join role as r on r.id = wgm.role_id
join permission_role as pr on r.id = pr.role_id
join permission as p on p.id = pr.permission_id
where t.id = task_id
and wgm.user_id = 'LFudaU6jzid4SKFlU8MgFAwezyP2'
and pr.value <> 'off'
and pr.value <> 'no')
LOOP
/* Here is where I pretend to do the comparison and insert data into the array */
ret_permission := array_append(ret_permissions, record.id);
END LOOP;
RETURN ret_permissions;
END
$function$
You could implement the logic in the query itself.
Assuming that the user_id column you are refering to corresponds to wgm.user_id, that is already in the query, the where clause of the query would become:
where
t.id = task_id
and pr.value not in ('off', 'no')
and (
?.watchers = 'my_team'
or (
?.watchers = 'only_me'
or wgm.user_id = 'LFudaU6jzid4SKFlU8MgFAwezyP2'
)
)
You don't tell which table watches belong to, so I used ?, which you need to replace that with the relevant table alias.
If there are just two possible values for watchers, then we can simplify the predicates:
where
t.id = task_id
and pr.value not in ('off', 'no')
and (?.watchers = 'my_team' or wgm.user_id = 'LFudaU6jzid4SKFlU8MgFAwezyP2')
You don't need an array
you don't need a cursor loop
You don't even need plpgsql
plain SQL wiill suffice:
CREATE TABLE permission (
id integer
, slug text
);
INSERT INTO permission(id,slug) VALUES (1,'WTF' );
CREATE TABLE task (
id integer
, service_request_form_id integer
);
INSERT INTO task(id, service_request_form_id) VALUES (1,1 );
CREATE TABLE service_request_form (
id integer
, service_group_id integer
);
INSERT INTO service_request_form(id, service_group_id) VALUES (1,1 );
CREATE TABLE service_group (
id integer
);
INSERT INTO service_group(id) VALUES (1);
CREATE TABLE worker_group (
id integer
, service_group_id integer
);
INSERT INTO worker_group(id, service_group_id) VALUES (1,1 );
CREATE TABLE worker_group_member (
worker_group_id integer
, role_id integer
, user_id text
);
INSERT INTO worker_group_member(worker_group_id, role_id,user_id) VALUES (1,1,'LFudaU6jzid4SKFlU8MgFAwezyP2' );
CREATE TABLE zrole (
id integer
);
INSERT INTO zrole(id) VALUES (1 );
CREATE TABLE permission_role (
role_id integer
, permission_id integer
, value text
);
INSERT INTO permission_role(role_id,permission_id,value) VALUES (1,1,'Yes' );
CREATE OR REPLACE FUNCTION public.calculates_permission_by_task(task_id integer)
RETURNS SETOF permission
LANGUAGE sql STABLE SECURITY INVOKER ROWS 30 -- COST 1
AS $func$
SELECT p.id, p.slug
FROM permission as p
WHERE EXISTS (
SELECT *
FROM task as t
JOIN service_request_form as srf
on t.service_request_form_id = srf.id
JOIN service_group as sg
on srf.service_group_id = sg.id
JOIN worker_group as wg
on wg.service_group_id = sg.id
JOIN worker_group_member as wgm
on wg.id = wgm.worker_group_id
JOIN zrole as zr
on zr.id = wgm.role_id
JOIN permission_role as pr
on zr.id = pr.role_id
WHERE p.id = pr.permission_id
AND t.id = task_id
and wgm.user_id = 'LFudaU6jzid4SKFlU8MgFAwezyP2'
and pr.value NOT IN( 'off' , 'no' )
-- -------------------------
-- Add the needed logic
-- (From #GMB) here
-- -------------------------
);
$func$
;
-- Usage:
-- --------------
SELECT * -- p.id, p.slug
FROM public.calculates_permission_by_task(42);

Slow stored procedure

I currently have a stored procedure that copies content from one table to another.
However when it is trying to only insert 27 new rows it continues on for over 12 minutes (after which point I stopped it) it said Affected 27 rows 4 times, however changes were not made.
Can you spot any reason the following SP would be slow?
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER Procedure [dbo].[sp_CopyCompanyContent]
(
#intCopyFromCompanyID Int,
#intNewCompanyID Int
)
As
Begin
/*
RaisError If any of Odl/New Company ID's are 0
*/
If (#intCopyFromCompanyID = 0 Or #intNewCompanyID = 0)
Begin
RaisError('New Company ID or Old Company ID can not be 0', 16, 1)
Return
End
/*
Create Temp Table For the Content Sections
*/
If Object_ID('tempdb..#ContentSections') IS Not Null
Begin
Drop Table dbo.#ContentSections
End
/*
Have to get all the existing data for the Old company we are copying from.
Have to also add the Max(ContentSectionID) From ContentSection. Max(ContentSectionID) +
The Identity (Added further down due to laziness) will be our seed for the ContentID
*/
Select CS.ContentID,
CS.SectionID,
CS.MenuOrder,
#intNewCompanyID NewCompanyID,
CS.CompanyID OldCompanyID,
CS.SubMenu,
CS.Link,
CS.HeaderMenu,
CS.ParentContentID,
CRS.*
Into dbo.#ContentSections
From dbo.Company COMP
Join dbo.ContentSection CS
On COMP.Company_id = CS.CompanyID
Join dbo.Content CONT
On CONT.ContentID = CS.ContentID
Cross Join (
Select MAx(ContentSectionID) MaxContentSectionID
From dbo.ContentSection CONT
) crs
Where COMP.Company_id = #intCopyFromCompanyID
Order By COMP.Company_id
/*
We now need to create a table for the existing content for the old company.
Also have to create the seed. Same principle as above.
*/
If Object_ID('tempdb..#Content') IS Not Null
Begin
Drop Table dbo.#Content
End
Select CONT.*,
CRS.*
Into dbo.#Content
From dbo.Company COMP
Join dbo.ContentSection CS
On COMP.Company_id = CS.CompanyID
Join dbo.Content CONT
On CONT.ContentID = CS.ContentID
Cross Join (
Select MAx(ContentID) MaxContentID
From dbo.Content CONT
) crs
Where COMP.Company_id = #intCopyFromCompanyID
Order By COMP.Company_id
/*
Add Identity to each of the tables we have created above. The ID fields will add to
the Max of each table to mimic what the future seeds will be.
*/
exec('Alter table #ContentSections Add ID Int Identity(1,1)')
exec('Alter table #Content Add ID Int Identity(1,1)')
/*
Add content data from the temp table.
*/
Insert Into dbo.Content
(
Title,
Content
)
Select Title,
Content
From dbo.#Content
/*
Have to the Content table up to the content sections table
as this contains what ID has been add to the Content Table.
*/
Insert Into dbo.ContentSection
(
ContentID,
SectionID,
MenuOrder,
CompanyID,
SubMenu,
Link,
HeaderMenu,
ParentContentID
)
Select C.MaxContentID + C.ID,
CS.SectionID,
CS.MenuOrder,
CS.NewCompanyID,
CS.Submenu,
CS.Link,
CS.HEaderMEnu,
CS.ParentContentID
From dbo.#Content C
Join dbo.#ContentSections CS
On C.ID = CS.ID
End
First thing to do is check the query plan for the selects since cross joins are dangerous beasts, if i'm not reading it wrong you'd display the same value in CRS.* in every record right?
If so, make that query before the select and stored the result in a variable and display it in the select, something like this.
DECLATE #maxValue INTEGER
Select #maxValue=MAX(ContentID) MaxContentID
From dbo.Content CONT
Select CONT.*,
#maxValue as MaxContentID
Into dbo.#Content
From dbo.Company COMP
Join dbo.ContentSection CS
On COMP.Company_id = CS.CompanyID
Join dbo.Content CONT
On CONT.ContentID = CS.ContentID
Where COMP.Company_id = #intCopyFromCompanyID
Order By COMP.Company_id
It is probably because of the identity (it is 2 times from the 4 times*27 rows)
exec('Alter table #ContentSections Add ID Int Identity(1,1)')
exec('Alter table #Content Add ID Int Identity(1,1)')
Instead, if you don't want to create the table, try to use ROW_NUMBER() OVER()... clause as ID, and then you don't need to create the identity later.
And as I see, you don't even need to use the Temp Tables, because then you can simply use the two select as INSERT INTO .... SELECT ... With the ROW_NUMBER().
It seems you don't make any changes on the Temp Tables, so you probably don't need them. (Only if you want to use them out of the SP's scope)

Update multiple table column values using single query

How would you update data in multiple tables using a single query?
MySQL Example
The equivalent code in MySQL:
UPDATE party p
LEFT JOIN party_name n ON p.party_id = n.party_id
LEFT JOIN party_details d ON p.party_id = d.party_id
LEFT JOIN incident_participant ip ON ip.party_id = p.party_id
LEFT JOIN incident i ON ip.incident_id = i.incident_id
SET
p.employee_id = NULL,
c.em_address = 'x#x.org',
c.ad_postal = 'x',
n.first_name = 'x',
n.last_name = 'x'
WHERE
i.confidential_dt IS NOT NULL
What would be the same statement using Oracle 11g?
Thank you!
RTFM
It seems a single query is insufficient when using Oracle:
http://download-west.oracle.com/docs/cd/B10501_01/server.920/a96540/statements_108a.htm#2067717
/** XXX CODING HORROR... */
Depending on your needs, you could use an updateable view. You create a view of your base tables and add an "instead of" trigger to this view and you update the view directly.
Some example tables:
create table party (
party_id integer,
employee_id integer
);
create table party_name (
party_id integer,
first_name varchar2(120 char),
last_name varchar2(120 char)
);
insert into party values (1,1000);
insert into party values (2,2000);
insert into party values (3,3000);
insert into party_name values (1,'Kipper','Family');
insert into party_name values (2,'Biff','Family');
insert into party_name values (3,'Chip','Family');
commit;
select * from party_v;
PARTY_ID EMPLOYEE_ID FIRST_NAME LAST_NAME
1 1000 Kipper Family
2 2000 Biff Family
3 3000 Chip Family
... then create an updateable view
create or replace view party_v
as
select
p.party_id,
p.employee_id,
n.first_name,
n.last_name
from
party p left join party_name n on p.party_id = n.party_id;
create or replace trigger trg_party_update
instead of update on party_v
for each row
declare
begin
--
update party
set
party_id = :new.party_id,
employee_id = :new.employee_id
where
party_id = :old.party_id;
--
update party_name
set
party_id = :new.party_id,
first_name = :new.first_name,
last_name = :new.last_name
where
party_id = :old.party_id;
--
end;
/
You can now update the view directly...
update party_v
set
employee_id = 42,
last_name = 'Oxford'
where
party_id = 1;
select * from party_v;
PARTY_ID EMPLOYEE_ID FIRST_NAME LAST_NAME
1 42 Kipper Oxford
2 2000 Biff Family
3 3000 Chip Family
I was having the same problem I couldn't find a easy way to do this in Oracle.
Look here:
Oracle Update Statements for more info.
You could use Oracle MERGE statement to do this. It is a bulk update-or-insert kind of statement based on joining the target table with an inline view.
MERGE INTO bonuses D
USING (
SELECT employee_id, salary, department_id FROM employees
WHERE department_id = 80
) S ON (D.employee_id = S.employee_id)
WHEN MATCHED THEN
UPDATE SET D.bonus = D.bonus + S.salary*.01
WHEN NOT MATCHED THEN
INSERT (D.employee_id, D.bonus)
VALUES (S.employee_id, S.salary*0.1);
if you do not need the insert part, you just omit the last 3 lines above.
In some cases it's possible to use PL/SQL to achieve this. In my case I searched for matching rows in two tables by some criteria, then updated each row in a loop.
Something like this:
begin
for r in (
select t1.id as t1_id, t2.id as t2_id
from t1, t2
where ...
) loop
update t1
set ...
where t1.id = r.t1_id;
update t2
set ...
where t2.id = r.t2_id;
end loop;
end;