How to create relational tables from a flat table? - sql

I have this input table:
CREATE TABLE `data` (
`id` INTEGER,
`a` INTEGER,
`b` INTEGER,
`c` TEXT, PRIMARY KEY(`id`) )
With this data:
id a b c
-- - - -
1 1 4 a
2 1 5 a
3 1 6 b
4 2 4 a
5 1 5 c
As I have many other columns b, c, ... on this table and a lot of redundancy. For example, we see that entry 1 is the same as entry 4 so I would like to create relational tables from this flat table and eventually drop data in favor of my two new tables and create a view named data
The goal is to create two tables bc and a with the following:
a bc
-------- --
id id_bc id b c
-- ----- -- - -
1 1 1 4 a
2 2 2 5 a
3 3 3 6 b
4 1 4 4 c
5 4
Getting bc is easy with:
CREATE TABLE bc AS SELECT DISTINCT b,c FROM data
But I don't know how:
Create bc with an id column
Create a with foreign relation to bc

If you want to customize the creation of table bc, then do so, instead of using create table ... as select. (Or, refer to the row identifier as rowid instead of giving it a custom name.)
create table bc(
id integer primary key,
b integer,
c integer)
insert into bc(b,c) select distinct b,c from data
For a, since you want a foreign key, you cannot use create table ... as select.
create table a(
id integer primary key,
id_bc integer,
foreign key (id_bc) references bc(id))
insert into a(id_bc)
select bc.id from data inner join bc
on data.b = bc.b and data.c = bc.c
Omitting the primary key in each insert statement (or specifying null) will use autoincrement.

Related

Updating a list of data in a normalised table postgres

I have two tables. One is table A which contains an id. Table B is a normalised table that contains a foreign key to table A and some other column called value.
e.g.
Table B
| id | fk| value
Table A
|pk| ... |
Basically I have a list (of n length) of values that I want to insert into table B that are to one foreignKey e.g list = [a, b, c, d] key = 1. The problem is table B might already have these values so I only want to insert the ones that aren't already in that table, as well as delete the ones that aren't in my list.
list = [a, b, c, d], key = 1
table B
| id |fk | value
| 1 | 1 | a
| 2 | 1 | b
| 3 | 1 | e
Is there a way that I can insert only c and d from the list into the table and delete e from the table in one statement? My current attempt is to delete every entry that matches the key and then insert them all but I don't think this is the efficient way to do this.
Why not just truncate b and insert the new values?
truncate table b;
insert into b (fk, value)
<your list here>;
Or if key is a column in b and you want to delete all keys with a given value:
delete from b where key = 1;
insert into b (fk, value, key)
<your list here with "1" for the key>
This doesn't preserve the id column from b, but your question does not mention that as being important.
An alternative method would use CTEs:
with data(fk, value) as (
<your list here>
),
d as (
delete from b
where (b.fk, b.value) not in (select d.fk, d.value from data d)
)
insert into b (fk, value)
select d.fk, d.value
from data d
where (d.fk, d.value) not in (select b.fk, b.value from b);

How to give a database constraint to ensure this behavior in a table?

I have a table with five columns: A, B, C, D and E.
And I need to comply with the following restrictions:
A is the primary key.
For a B there can only be one C, ie: 1-1 ; 2-1 ; 3-2 but not 1-2.
B-C and D can take any value but can not be repeated, ie: 1-1 1 ; 1-1 2 ; not 1-1 1 again.
E can take any value.
So, considering the following order
| A | B | C | D | E |
| 1 | 1 | 1 | 1 | 1 | -> OK
| 2 | 1 | 2 | 1 | 1 | -> Should fail, because there is a B with another C, 1-2 must be 1-1.
| 3 | 1 | 1 | 2 | 1 | -> OK
| 4 | 1 | 1 | 2 | 1 | -> Should fail, because relation between B-C and D is repeated.
| 5 | 2 | 1 | 1 | 1 | -> OK
Is there any way to comply with this behavior through some constraint in the database?
Thanks!
A and E are irrelevant to the question and can be ignored.
The BCD rule can be easily solved by creating a unique index on BCD.
If for every B there can be only one C then your DB is not normalized. Create a new table with B and C. Make B the primary key or create a unique index on B. Then remove C from the original table. (At which point the unique index on BCD becomes a unique index on BD.)
Without normalizing the tables, I don't think there's any way to do it with a constraint. You could certainly do it with a trigger or with code.
For B - C rule I would create a trigger
For the B - C - D rule looks like you want unique constraint
ALTER TABLE t ADD CONSTRAINT uni_BCD UNIQUE (B,C,D);
This condition is not trivial
For a B there can only be one C, ie: 1-1 ; 2-1 ; 3-2 but not 1-2.
, since Oracle does not support CREATE ASSERTION (soon, we hope!)
Therefore, you need to involve a second table to enforce this constraint, or else a statement-level AFTER INSERT/UPDATE trigger.
What I would do is create a second table and have it maintained via an INSTEAD OF trigger on a view, and ensure all my application DML happened via the view. (You could also just create a regular trigger on the table and have it maintain the second table. That's just not my preference. I find INSTEAD OF triggers to be more flexible and more visible.)
In case it's not clear, the purpose of the second table is that it allows you to enforce your constraint as a FOREIGN KEY constraint. The UNIQUE or PRIMARY KEY constraint on the second table ensures that each value of B appears only once.
Here's sample code for that approach:
--DROP TABLE table1_parent;
--DROP TABLE table1;
CREATE TABLE table1_parent
( b number NOT NULL,
c number NOT NULL,
constraint table1_parent_pk PRIMARY KEY (b),
constraint table1_parent_u1 UNIQUE (b, c) );
CREATE TABLE table1
(
a NUMBER NOT NULL,
b NUMBER NOT NULL,
c NUMBER NOT NULL,
d NUMBER NOT NULL,
e NUMBER NOT NULL,
CONSTRAINT table1_pk PRIMARY KEY (a), -- "A is the primary key."
CONSTRAINT table1_fk FOREIGN KEY ( b, c ) REFERENCES table1_parent ( b, c ), -- "For a B there can only be one C, ie: 1-1 ; 2-1 ; 3-2 but not 1-2."
CONSTRAINT table1_u2 UNIQUE ( b, c, d ) -- "B-C and D can take any value bue can not be repeated, ie: 1-1 1 ; 1-1 2 ; not 1-1 1 again."
);
CREATE INDEX table1_n1 ON table1 (b,c); -- Always index foreign keys
CREATE OR REPLACE VIEW table1_dml_v AS SELECT * FROM table1;
CREATE OR REPLACE TRIGGER table1_dml_v_trg INSTEAD OF INSERT OR UPDATE OR DELETE ON table1_dml_v
DECLARE
l_cnt NUMBER;
BEGIN
IF INSERTING THEN
BEGIN
INSERT INTO table1_parent (b, c) VALUES ( :new.b, :new.c );
EXCEPTION
WHEN dup_val_on_index THEN
NULL; -- parent already exists, no problem
END;
INSERT INTO table1 ( a, b, c, d, e ) VALUES ( :new.a, :new.b, :new.c, :new.d, :new.e );
END IF;
IF DELETING THEN
DELETE FROM table1 WHERE a = :old.a;
SELECT COUNT(*) INTO l_cnt FROM table1 WHERE b = :old.b AND c = :old.c;
IF l_cnt = 0 THEN
DELETE FROM table1_parent WHERE b = :old.b AND c = :old.c;
END IF;
END IF;
IF UPDATING THEN
BEGIN
INSERT INTO table1_parent (b, c) VALUES ( :new.b, :new.c );
EXCEPTION
WHEN dup_val_on_index THEN
NULL; -- parent already exists, no problem
END;
UPDATE table1 SET a = :new.a, b = :new.b, c = :new.c, d = :new.d, e = :new.d WHERE a = :old.a;
SELECT COUNT(*) INTO l_cnt FROM table1 WHERE b = :old.b AND c = :old.c;
IF l_cnt = 0 THEN
DELETE FROM table1_parent WHERE b = :old.b AND c = :old.c;
END IF;
END IF;
END;
insert into table1_dml_v ( a,b,c,d,e) VALUES (1,1,1,1,1);
insert into table1_dml_v ( a,b,c,d,e) VALUES (2,1,2,1,1);
insert into table1_dml_v ( a,b,c,d,e) VALUES (3,1,1,2,1);
insert into table1_dml_v ( a,b,c,d,e) VALUES (4,1,1,2,1);
insert into table1_dml_v ( a,b,c,d,e) VALUES (5,2,1,1,1);
If your system supports fast refreshed materialized views, please try the following.
Since I currently don't access to a this feature, I can't verify the solution.
create materialized view log on t with primary key;
create materialized view t_mv
refresh fast
as
select b,c
from t
group by b,c
;
alter table t_mv add constraint t_mv_uq_b unique (b);
and off course:
alter table t add constraint t_uq_a_b_c unique (b,c,d);

PostgreSQL: Select field value and update with next value from second table

CREATE TABLE t_a
(
a_id SERIAL PRIMARY KEY,
str VARCHAR(50)
)
CREATE TABLE t_b
(
b_id SERIAL PRIMARY KEY,
a_id_fk INTEGER REFERENCES (t_a(a_id),
)
Using the above tables, I want to SELECT a_id_fk FROM t_b WHERE b_id = 1 and then update a_id_fk with the next a_id in the sequence, but if I'm at the end of the available a_id's I cycle back to the first one. All this with multiple people querying/updating that specific row from t_b.
If it helps, the scenario I'm working on is multiple sites share a common list of words, but as each user for each sites grabs a word that sites index within the word list is moved to the next word until it hits the end then it loops back to the beginning.
Is there a way to do this in a single query? If not, what would be the best way to handle this? I can handle most of the logic, it's looping back when I run out of ids that has me stumped.
You could use something complicated like
UPDATE t_b
SET a_id_fk = COALESCE(
(SELECT MIN(a_id) FROM t_a WHERE a_id > t_b.a_id_fk),
(SELECT MIN(a_id) FROM t_a))
WHERE b_id = :b_id
but if I was given a requirement like that I'd probably maintain an auxiliary table that maps an a_id to the next a_id in the cycle...
This one is a bit more elegant (IMHO) than #pdw's solution:
DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp ;
SET search_path=tmp;
CREATE TABLE t_a
( a_id SERIAL PRIMARY KEY
, str VARCHAR(50)
);
CREATE TABLE t_b
( b_id SERIAL PRIMARY KEY
, a_id_fk INTEGER REFERENCES t_a(a_id)
);
INSERT INTO t_a(str)
SELECT 'Str_' || gs::text
FROM generate_series(1,10) gs
;
INSERT into t_b(a_id_fk)
SELECT a_id FROM t_a
ORDER BY a_id
;
-- EXPLAIN ANALYZE
WITH src AS (
SELECT a_id AS a_id
, min(a_id) OVER (order BY a_id) AS frst
, lead(a_id) OVER (order BY a_id) AS nxt
FROM t_a
)
UPDATE t_b dst
SET a_id_fk = COALESCE(src.nxt, src.frst)
FROM src
WHERE dst.a_id_fk = src.a_id
AND dst.b_id IN ( 3, 10)
;
SELECT * FROM t_b
ORDER BY b_id
;
Result:
DROP SCHEMA
CREATE SCHEMA
SET
CREATE TABLE
CREATE TABLE
INSERT 0 10
INSERT 0 10
UPDATE 2
b_id | a_id_fk
------+---------
1 | 1
2 | 2
3 | 4
4 | 4
5 | 5
6 | 6
7 | 7
8 | 8
9 | 9
10 | 1
(10 rows)

Update table based on values in another table

I have two tables I want to update table b in column bcnt by the value that in column acnt in table A where the worlds column in table A match words column in table B, and id in table B match id in table A.
create table a
(
id number(9),
words varchar2(2),
acnt number(9)
);
insert into a values(1,'Cairo',20);
insert into a values(1,'UK',10);
insert into a values(2,'KL',2);
insert into a values(2,'Cairo',2);
insert into a values(2,'London',30);
insert into a values(3,'Cairo',5);
insert into a values(4,'KSA',15);
create table b
(
id number(2),
words varchar2(20),
bcnt number
);
insert into b values(1,'Cairo',null);
insert into b values(1,'UK',null);
insert into b values(2,'KL',null);
insert into b values(2,'Cairo',null);
insert into b values(3,'Cairo',null);
insert into b values(4,'KSA',null);
I used this SQL code but it is not correct.
update b
set bcnt = (select acnt
from a
where a.id = b.id and a.words = b.words);
Expected results:
1 cairo 20
1 uk 10
2 kl 2
2 cairo 5
4 ksa 12
The SQL shows me the following
SQL> /
ID WORDS BCNT
---------- -------------------- ----------
1 Cairo
1 UK 10
2 KL 2
2 Cairo
3 Cairo
4 KSA
6 rows selected.
SQL>
Is the problem that your SQL uses word instead of words as in the table definition?
update b
set bcnt=(select acnt from a where a.id=b.id and a.words=b.words );
Also, your data types are not correct in the two tables. Your create table statements should be consistent:
create table a (id number(9),words varchar2(20), acnt number);
create table b (id number(9), words varchar2(20), bcnt number);
What is happening is that the values longer than 2 characters in the first table are being truncated to two characters. So, instead of 'Cairo' the value is 'Ca' (to fit in varchar2(2)). As a result, you have no match in the join.

How to do I update existing records using a conditional clause?

I'm new to Oracle SQL so I have a question .. I have two tables, Table A and Table B .. Now Table A and Table B have the same column names, but in table A, only one column (named 'tracker') actually has data in it .. The rest of the columns in Table A are empty ... What I need to do is update each record in Table A, so that values for other columns are copied over from Table B, with the condition that the the 'tracker' columns value from Table A is matched with the 'tracker' column in Table B ..
Any ideas ?
MERGE INTO tableA a
USING tableB b
ON (a.tracker=b.tracker)
WHEN MATCHED THEN UPDATE SET
a.column1=b.column1,
a.column2=b.column2;
And if exist rows in B that does not exist in A:
MERGE INTO tableA a
USING tableB b
ON (a.tracker=b.tracker)
WHEN MATCHED THEN UPDATE SET
a.column1=b.column1,
a.column2=b.column2
WHEN NOT MATCHED THEN INSERT VALUES
a.tracker,a.column1,a.column2; --all columns
create table a (somedata varchar2(50), tracker number , constraint pk_a primary key (tracker));
create table b (somedata varchar2(50), tracker number, constraint pk_b primary key (tracker));
/
--insert some data
insert into a (somedata, tracker)
select 'data-a-' || level, level
from dual
connect by level < 10;
insert into b (somedata, tracker)
select 'data-b-' || -level, level
from dual
connect by level < 10;
select * from a;
SOMEDATA TRACKER
-------------------------------------------------- -------
data-a-1 1
data-a-2 2
data-a-3 3
data-a-4 4
data-a-5 5
data-a-6 6
data-a-7 7
data-a-8 8
data-a-9 9
select * from b;
SOMEDATA TRACKER
-------------------------------------------------- -------
data-b--1 1
data-b--2 2
data-b--3 3
data-b--4 4
data-b--5 5
data-b--6 6
data-b--7 7
data-b--8 8
data-b--9 9
commit;
update (select a.somedata a_somedata, b.somedata b_somedata
from a
inner join
b
on a.tracker = b.tracker)
set
a_somedata = b_somedata;
select * from a; --see below for results--
--or you can do it this way: (issuing rollback to get data back in previous state)
--for a one column update, either way will work, I would prefer the former in case there is a multi-column update necessary
-- merge *as posted by another person* will also work
update a
set somedata = (select somedata
from b
where a.tracker = b.tracker
);
select * from A; --see below for results--
-- clean up
-- drop table a;
-- drop table b;
this will give you the results:
SOMEDATA TRACKER
-------------------------------------------------- -------
data-b--1 1
data-b--2 2
data-b--3 3
data-b--4 4
data-b--5 5
data-b--6 6
data-b--7 7
data-b--8 8
data-b--9 9
here is a link to oracle's documentation on UPDATE