Postgresql UPDATE LEFT JOIN - sql

So, I have the following code that updates a language string table, based on an id, and a language code. If I perform a SELECT using the same from expression, it selects just one row. If I do update, it updates all rows. Where am I going wrong with this?
UPDATE shopmaster.catalog_lang SET shortname='TEST'
FROM shopmaster.catalog_lang cl LEFT JOIN shopmaster.lang l ON cl.langid=l.langid
WHERE cl.catalogid=7 AND l.code='fr';
Here's the definition of the two tables:
CREATE TABLE IF NOT EXISTS shopmaster.lang(
langid SERIAL,
name TEXT,
code TEXT,
active BOOLEAN,
donotdelete BOOLEAN,
PRIMARY KEY (langid)
CREATE TABLE IF NOT EXISTS shopmaster.catalog_lang(
catalogid INT references shopmaster.catalog(catalogid),
langid INT references shopmaster.lang(langid),
title TEXT,
shortname TEXT,
dirname TEXT,
PRIMARY KEY (catalogid, langid)
);

Don't repeat the table being updated in the FROM. So:
UPDATE shopmaster.catalog_lang cl
SET shortname = 'TEST'
FROM shopmaster.lang l
WHERE cl.langid = l.langid AND cl.catalogid = 7 AND l.code = 'fr';
In Postgres each reference to the table is separate. Your update is equivalent to this SELECT:
SELECT . . .
FROM shopmaster.catalog_lang CROSS JOIN
shopmaster.catalog_lang cl LEFT JOIN
shopmaster.lang l
ON cl.langid = l.langid
WHERE cl.catalogid = 7 AND l.code = 'fr';
And this is definitely not what you intend.

Related

SQL selecting first record per group

I have a table that looks like this:
CREATE TABLE UTable (
m_id TEXT PRIMARY KEY,
u1 TEXT,
u2 TEXT,
u3 TEXT,
-- other stuff, as well as
gid INTEGER,
gt TEXT,
d TEXT,
timestamp TIMESTAMP
);
CREATE TABLE OTable (
gid INTEGER,
gt TEXT,
d TEXT,
-- other stuff, such as
n INTEGER
);
CREATE UNIQUE INDEX OTable_idx ON OTable (gid, gt, d);
For each record in OTable that matches a condition (fixed values of gid, gt), I want to join the corresponding record in UTable with the minimum timestamp.
What's catching me is that in my final result I don't care about the timestamp, I clearly need to group on d (since gid and gt are fixed), and yet I do need to extract u1, u2, u3 from the selected record.
SELECT o.d, u.u1, u.u2, u.u3, o.n
FROM UTable u
INNER JOIN OTable o
ON u.gid = o.gid AND u.gt = o.gt AND u.d = o.d
WHERE u.gid = 3 AND u.gt = 'dog night'
GROUP BY u.d
-- and u.timestamp is the minimum for each group
;
I think my first step should be just to do the select on UTable and then I can join against that. But even there I'm a bit confused.
SELECT u.d, u.u1, u.u2, u.u3
FROM UTable u
WHERE u.gid = 3 AND u.gt = 'dog night';
I want to add HAVING MIN(u.timestamp), but that's not valid.
Any pointers as to what I need to do?
I did see this question, but it isn't quite what I need, since I can't group on all the UTable values lest I select too many things.
GROUP BY u.d (without also listing u1, u2, u3) would only work if u.d was the PRIMARY KEY (which it is not, and also wouldn't make sense in your scenario). See:
Is it possible to have an SQL query that uses AGG functions in this way?
I suggest DISTINCT ON in a subquery on UTable instead:
SELECT o.d, u.u1, u.u2, u.u3, o.n
FROM (
SELECT DISTINCT ON (u.d)
u.d, u.u1, u.u2, u.u3
FROM UTable u
WHERE u.gid = 3
AND u.gt = 'dog night'
ORDER BY u.d, u.timestamp
) u
JOIN OTable o USING (gid, gt, d);
See:
Select first row in each GROUP BY group?
If UTable is big, at least a multicolumn index on (gid, gt) is advisable. Same for OTable.
Maybe even on (gid, gt, d). Depends on data types, cardinalities, ...

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

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.

UPDATE on seemingly key preserving view in Oracle raises ORA-01779

Problem
I'm trying to refactor a low-performing MERGE statement to an UPDATE statement in Oracle 12.1.0.2.0. The MERGE statement looks like this:
MERGE INTO t
USING (
SELECT t.rowid rid, u.account_no_new
FROM t, u, v
WHERE t.account_no = u.account_no_old
AND t.contract_id = v.contract_id
AND v.tenant_id = u.tenant_id
) s
ON (t.rowid = s.rid)
WHEN MATCHED THEN UPDATE SET t.account_no = s.account_no_new
It is mostly low performing because there are two expensive accesses to the large (100M rows) table t
Schema
These are the simplified tables involved:
t The target table whose account_no column is being migrated.
u The migration instruction table containing a account_no_old → account_no_new mapping
v An auxiliary table modelling a to-one relationship between contract_id and tenant_id
The schema is:
CREATE TABLE v (
contract_id NUMBER(18) NOT NULL PRIMARY KEY,
tenant_id NUMBER(18) NOT NULL
);
CREATE TABLE t (
t_id NUMBER(18) NOT NULL PRIMARY KEY,
-- tenant_id column is missing here
account_no NUMBER(18) NOT NULL,
contract_id NUMBER(18) NOT NULL REFERENCES v
);
CREATE TABLE u (
u_id NUMBER(18) NOT NULL PRIMARY KEY,
tenant_id NUMBER(18) NOT NULL,
account_no_old NUMBER(18) NOT NULL,
account_no_new NUMBER(18) NOT NULL,
UNIQUE (tenant_id, account_no_old)
);
I cannot modify the schema. I'm aware that adding t.tenant_id would solve the problem by preventing the JOIN to v
Alternative MERGE doesn't work:
ORA-38104: Columns referenced in the ON Clause cannot be updated
Note, the self join cannot be avoided, because this alternative, equivalent query leads to ORA-38104:
MERGE INTO t
USING (
SELECT u.account_no_old, u.account_no_new, v.contract_id
FROM u, v
WHERE v.tenant_id = u.tenant_id
) s
ON (t.account_no = s.account_no_old AND t.contract_id = s.contract_id)
WHEN MATCHED THEN UPDATE SET t.account_no = s.account_no_new
UPDATE view doesn't work:
ORA-01779: cannot modify a column which maps to a non-key-preserved table
Intuitively, I would apply transitive closure here, which should guarantee that for each updated row in t, there can be only at most 1 row in u and in v. But apparently, Oracle doesn't recognise this, so the following UPDATE statement doesn't work:
UPDATE (
SELECT t.account_no, u.account_no_new
FROM t, u, v
WHERE t.account_no = u.account_no_old
AND t.contract_id = v.contract_id
AND v.tenant_id = u.tenant_id
)
SET account_no = account_no_new
The above raises ORA-01779. Adding the undocumented hint /*+BYPASS_UJVC*/ does not seem to work anymore on 12c.
How to tell Oracle that the view is key preserving?
In my opinion, the view is still key preserving, i.e. for each row in t, there is exactly one row in v, and thus at most one row in u. The view should thus be updatable. Is there any way to rewrite this query to make Oracle trust my judgement?
Or is there any other syntax I'm overlooking that prevents the MERGE statement's double access to t?
Is there any way to rewrite this query to make Oracle trust my judgement?
I've managed to "convince" Oracle to do MERGE by introducing helper column in target:
MERGE INTO (SELECT (SELECT t.account_no FROM dual) AS account_no_temp,
t.account_no, t.contract_id
FROM t) t
USING (
SELECT u.account_no_old, u.account_no_new, v.contract_id
FROM u, v
WHERE v.tenant_id = u.tenant_id
) s
ON (t.account_no_temp = s.account_no_old AND t.contract_id = s.contract_id)
WHEN MATCHED THEN UPDATE SET t.account_no = s.account_no_new;
db<>fiddle demo
EDIT
A variation of idea above - subquery moved directly to ON part:
MERGE INTO (SELECT t.account_no, t.contract_id FROM t) t
USING (
SELECT u.account_no_old, u.account_no_new, v.contract_id
FROM u, v
WHERE v.tenant_id = u.tenant_id
) s
ON ((SELECT t.account_no FROM dual) = s.account_no_old
AND t.contract_id = s.contract_id)
WHEN MATCHED THEN UPDATE SET t.account_no = s.account_no_new;
db<>fiddle demo2
Related article: Columns referenced in the ON Clause cannot be updated
EDIT 2:
MERGE INTO (SELECT t.account_no, t.contract_id FROM t) t
USING (SELECT u.account_no_old, u.account_no_new, v.contract_id
FROM u, v
WHERE v.tenant_id = u.tenant_id) s
ON((t.account_no,t.contract_id,'x')=((s.account_no_old,s.contract_id,'x')) OR 1=2)
WHEN MATCHED THEN UPDATE SET t.account_no = s.account_no_new;
db<>fiddle demo3
You may define a temporary table containing the pre-joined data from U and V.
Back it with a unique index on contract_id, account_no_old (which should be unique).
Then you may use this temporary table in an updateable join view.
create table tmp as
SELECT v.contract_id, u.account_no_old, u.account_no_new
FROM u, v
WHERE v.tenant_id = u.tenant_id;
create unique index tmp_ux1 on tmp ( contract_id, account_no_old);
UPDATE (
SELECT t.account_no, tmp.account_no_new
FROM t, tmp
WHERE t.account_no = tmp.account_no_old
AND t.contract_id = tmp.contract_id
)
SET account_no = account_no_new
;
Trying to do this with a simpler update. Still requires a subselect.
update t
set t.account_no = (SELECT u.account_no_new
FROM u, v
WHERE t.account_no = u.account_no_old
AND t.contract_id = v.contract_id
AND v.tenant_id = u.tenant_id);
Bobby

Convert SQLite join with multiple tables to PostgreSQL

I have this dictionary thing and the table with translations. I can do a nice select using it with SQLite
SELECT e.slug,
en.title,
en.locale
FROM entities AS e
LEFT JOIN (
locales AS en,
entity_locales AS el
) ON (
el.entity_id = e.id
AND el.locale_id = en.id
AND en.locale == 'en'
)
That produces:
present, translation, en
missing, NULL, NULL
But I can't convert it to Postgres because I don't understand what is going on when you specify more than one table in LEFT JOIN in SQLite:
SELECT e.slug,
en.title,
en.locale
FROM entities e
LEFT JOIN entity_locales el ON (el.entity_id = e.id)
JOIN locales en ON (
el.locale_id = en.id
AND en.locale = 'en'
)
Produces only
present, translation, en
Is there a way to make it work?
Database structure in SQLite format:
CREATE TABLE IF NOT EXISTS "entities" (
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"slug" varchar
);
CREATE TABLE IF NOT EXISTS "entity_locales" (
"entity_id" integer,
"locale_id" integer
);
CREATE TABLE IF NOT EXISTS "locales" (
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"title" varchar,
"locale" varchar
);
insert into entities(id, slug) values(1, 'present');
insert into entities(id, slug) values(2, 'missing');
insert into locales(id, title, locale) values(1, 'translation', 'en');
insert into entity_locales(entity_id, locale_id) values(1, 1);
You are using a left join only to the second table, and then you are using an inner join to the third. You need to use a left join to the product of the inner join between the second and third table.
Try this instead:
SELECT e.slug,
en.title,
en.locale
FROM entities e
LEFT JOIN
(
entity_locales el
JOIN locales en ON (
el.locale_id = en.id
AND en.locale = 'en'
)
) ON (el.entity_id = e.id)
btw, your initial script mixes implicit and explicit joins. I would advise against using implicit joins since explicit joins are a standard part of SQL for over 25 years now.

SQL selecting not surely existing column from on table or another

I use generalisation/specialize table Zadavatel (which contains primary key) as either table Sukromna_osoba or Firma (both contains foreign key that points on Zadavatel).
I need to select Sukromna_osoba table if Sukromna_osoba.meno = 'string' exists or Firma table if Firma.nazov_firmy = 'string' exists, both if both conditions are true. I also need this to be in one select.
CREATE TABLE Zadavatel (
id_zadavatela INTEGER,
adresa VARCHAR(25)
);
CREATE TABLE Sukromna_osoba (
id_sukromnej_osoby INTEGER,
meno VARCHAR(20),
mobil INTEGER,
email VARCHAR(20)
);
CREATE TABLE Firma (
id_firmy INTEGER,
nazov_firmy VARCHAR(20),
ico INTEGER,
bankove_spojenie INTEGER
);
id_zadavatela is primary key, and id_sukromnej_osoby and id_firmy are foreign keys which points at id_zadavatela.
I tried something like this:
SELECT PR.id_projektu, PR.popis, ZAD.id_zadavatela, FI.nazov_firmy
FROM Projekt PR JOIN Zamestnanec ZAM ON PR.manazer = ZAM.osobne_cislo
JOIN Zadavatel ZAD ON PR.zadavatel = ZAD.id_zadavatela
JOIN Firma FI ON ZAD.id_zadavatela = FI.id_firmy
WHERE ZAM.meno = 'Jan Novák' OR (
SELECT PR1.id_projektu, PR1.popis, ZAD1.id_zadavatela, SO1.meno
FROM Projekt PR1 JOIN Zamestnanec ZAM1 ON PR1.manazer = ZAM1.osobne_cislo
JOIN Zadavatel ZAD1 ON PR1.zadavatel = ZAD1.id_zadavatela
JOIN Sukromna_osoba SO1 ON ZAD1.id_zadavatela = SO1.id_sukromnej_osoby
WHERE ZAM1.meno = 'Jan Novák'
)
Since you do not know if it's a firm or a person, you could use left outer join on both, like this:
SELECT PR.id_projektu, PR.popis, ZAD.id_zadavatela, FI.nazov_firmy, SO.meno
FROM Projekt PR
JOIN Zamestnanec ZAM ON PR.manazer = ZAM.osobne_cislo
JOIN Zadavatel ZAD ON PR.zadavatel = ZAD.id_zadavatela
LEFT OUTER JOIN Firma FI ON ZAD.id_zadavatela = FI.id_firmy
LEFT OUTER JOIN Sukromna_osoba SO ON ZAD.id_zadavatela = SO.id_sukromnej_osoby
WHERE ZAM.meno = 'Jan Novák'
One of the two result columns, nazov_firmy or meno, will be NULL.