Changing primary key to new id column - sql

I have an existing database with the primary key being the combination between a name and a timestamp. There is no ID column yet. I would like to add that ID column and change the primary key to ID and timestamp. Rows with the same name should get the same ID.
Since I'm only an intern I would prefer to avoid droping and recreating the table I'm working on since I currently don't have the privileges to do that.
What would be the easiest way to do that?

You can drop a primary key and create a new one according to the docs.
ALTER TABLE table_name
DROP CONSTRAINT primary_key;
Then you'll have to add the ID column and set it as the new Primary Key:
ALTER TABLE table_name
ADD (ID NUMBER);
ALTER TABLE table_name
ADD CONSTRAINT table_pk PRIMARY KEY (ID, Time_Stamp);
Edit: If you would like the ID's to auto increment you will need to create a sequence and a trigger:
CREATE SEQUENCE S
START WITH 1
INCREMENT BY 1;
CREATE OR REPLACE TRIGGER T
BEFORE INSERT ON table_name
FOR EACH ROW
BEGIN
if(:new.ID is null) then
SELECT S.nextval
INTO :new.ID
FROM dual;
end if;
END;
/
ALTER TRIGGER "T" ENABLE;

For completing your entire task you need to
Create ID column
Fill data in ID column according to your requirement
Drop PK
Create PK with ID and Timestamp column
Below is the entire script for the same:
--CREATING THE TABLE
CREATE TABLE TEST (NAME VARCHAR2(200), TIME_STAMP TIMESTAMP,
CONSTRAINT TEST_PK PRIMARY KEY (NAME, TIME_STAMP)
);
-- INSERTING SAMPLE DATA ACCORDING TO YOUR REQUIREMENT -- DUPLICATE NAMES
INSERT INTO TEST
SELECT * FROM
(
SELECT 'NAME1', SYSTIMESTAMP - INTERVAL '1' HOUR FROM DUAL UNION ALL
SELECT 'NAME2', SYSTIMESTAMP - INTERVAL '2' HOUR FROM DUAL UNION ALL
SELECT 'NAME3', SYSTIMESTAMP - INTERVAL '3' HOUR FROM DUAL UNION ALL
SELECT 'NAME4', SYSTIMESTAMP - INTERVAL '4' HOUR FROM DUAL UNION ALL
SELECT 'NAME4', SYSTIMESTAMP - INTERVAL '5' HOUR FROM DUAL UNION ALL
SELECT 'NAME4', SYSTIMESTAMP - INTERVAL '6' HOUR FROM DUAL
);
-- ADDING NEW COLUMN ID
ALTER TABLE TEST ADD ID NUMBER;
-- FILLING THE DATA INTO ID COLUMN
MERGE INTO TEST T
USING (SELECT TIN.NAME, TIN.TIME_STAMP, RANK() OVER (ORDER BY TIN.NAME) RN
FROM TEST TIN) TIN
ON (T.NAME = TIN.NAME AND T.TIME_STAMP = TIN.TIME_STAMP)
WHEN MATCHED THEN
UPDATE SET T.ID = TIN.RN;
-- DROPPING TH EXSITING PK
ALTER TABLE TEST DROP CONSTRAINT TEST_PK;
-- ADDING THE PK WITH ID AND TIME_STAMP
ALTER TABLE TEST ADD CONSTRAINT TEST_PK PRIMARY KEY (ID, TIME_STAMP);
db<>fiddle demo
Cheers!!

Related

Can I add a CHECK in my SQL CREATE TABLE script?

I created this table for my database.
CREATE TABLE Reservation
(
Reservation_Name SERIAL UNIQUE PRIMARY KEY,
User VARCHAR(64) DEFAULT 'Member', FOREIGN KEY(User) REFERENCES User(Email)
ON UPDATE CASCADE
ON DELETE SET DEFAULT,
Location INT, FOREIGN KEY(Location) REFERENCES Place(Id_Location)
ON UPDATE CASCADE
ON DELETE NO ACTION,
Start_Date DATE NOT NULL,
Check_In TIME(1) DEFAULT '10:00:00',
End_Date DATE NOT NULL,
Check_Out TIME(1) DEFAULT '18:00:00',
CHECK(Start_Date >= End_Date),
Deleted BOOLEAN DEFAULT FALSE
);
How can I insert a Check that doesn't allow to add a reservation if there's already another one with the same Start_Date and the same End_Date end the same location?
You can use an exclusion constraint:
CREATE EXTENSION btree_gist;
ALTER TABLE reservation ADD EXCLUDE USING gist (
location WITH =,
daterange(start_date, end_date, '[]') WITH &&
);
The extension is required so that you can create a GiST index on an integer column, and && is the "overlaps" operator for range types.
You can symply add a constraint to the table.
Alter Table Reservations Add Constraint unique_reservation Unique(Location,StartDate,EndDate);
You will need a trigger for this. Look at the above code :
CREATE TRIGGER no_overlap
BEFORE INSERT
ON Reservation FOR EACH ROW
BEGIN
SET #overlaps = ( SELECT count(*) FROM Reservation WHERE ( ( NEW.Start_Date >= Start_Date AND NEW.Start_Date <= End_Date AND NEW.Location = Location) || ( NEW.End_Date >= Start_Date AND NEW.End_Date <= End_Date AND NEW.Location = Location)));
IF #overlaps > 0 THEN
SIGNAL SQLSTATE '45000' SET
MYSQL_ERRNO = 31000,
MESSAGE_TEXT = 'Unable to insert an overlapping reservation';
END IF;
END;
INSERT INTO Reservation (Location,Start_Date,End_Date) VALUES(1,'2020-12-13','2020-12-16');
INSERT INTO Reservation (Location,Start_Date,End_Date) VALUES(1,'2020-12-14','2020-12-17');
The first insert will succeed while the second one will fail with the corresponding error message if the dates overlap :
SQL Error [31000] [45000]: (conn=10) Unable to insert an overlapping reservation
By the way, I think you have an error in your table definition. Instead of CHECK(Start_Date >= End_Date), I think you meant CHECK(Start_Date <= End_Date),
Let me know if it helps.
Note : I did this on MariaDB but you can apply the same for any SQL DB.
This works in sql server. I don't have access at the moment to check against postgres. You will need to run in two different batches after creating your table
First
CREATE FUNCTION dbo.HasOverlap (
#locationId int, #start datetime, #end datetime)
RETURNS VARCHAR(5)
AS
BEGIN
IF (SELECT count(*) FROM dbo.Reservation WHERE Location = #locationId
and (
#start between Start_Date and End_Date
or
#end between Start_Date and End_Date
or
(#start <=Start_Date and #end>=End_Date )
)
) >1
return 1
return 0
END
Second
Alter Table dbo.Reservation
with check add Constraint Check_Overlap
check (dbo.HasOverlap(Location, Start_Date, End_Date)=0)
You need to use the composite primary key concept in MySQL Database. Its disable to insert duplicate items in specific columns.
ALTER TABLE table_name ADD CONSTRAINT constraint_name PRIMARY KEY (Start_Date, End_Date, Location)

Why am I getting duplicate rows with these postgresql temporal db schema & queries?

I'm following some info about setting up a temporal database in postgresql. First the question, then the technical bits.
Question: When I do a single clean insert into the public.countries table, why do I get doubled rows in the temporal.countries table? I only see a single insert (in the countries_ins rule). Is this a feature or a bug?
Ok, now the schema:
DROP SCHEMA IF EXISTS temporal CASCADE;
DROP SCHEMA IF EXISTS history CASCADE;
----------------------------
-- Temporal countries schema
-- vjt#openssl.it
--
create schema temporal; -- schema containing all temporal tables
create schema history; -- schema containing all history tables
-- Current countries data - nothing special
--
create table temporal.countries (
id serial primary key,
name varchar UNIQUE
);
-- Countries historical data.
--
-- Inheritance is used to avoid duplicating the schema from the main table.
-- Please note that columns on the main table cannot be dropped, and other caveats
-- http://www.postgresql.org/docs/9.0/static/ddl-inherit.html#DDL-INHERIT-CAVEATS
--
create table history.countries (
hid serial primary key,
valid_from timestamp not null,
valid_to timestamp not null default '9999-12-31',
recorded_at timestamp not null default now(),
constraint from_before_to check (valid_from < valid_to),
constraint overlapping_times exclude using gist (
box(
point( extract( epoch from valid_from), id ),
point( extract( epoch from valid_to - interval '1 millisecond'), id )
) with &&
)
) inherits ( temporal.countries );
create index timestamps on history.countries using btree ( valid_from, valid_to ) with ( fillfactor = 100 );
create index country_id on history.countries using btree ( id ) with ( fillfactor = 90 );
-- The countries view, what the Rails' application ORM will actually CRUD on, and
-- the core of the temporal updates.
--
-- SELECT - return only current data
--
create view public.countries as select * from only temporal.countries;
-- INSERT - insert data both in the current data table and in the history table
--
create rule countries_ins as on insert to public.countries do instead (
insert into temporal.countries ( name )
values ( new.name )
returning temporal.countries.*;
insert into history.countries ( id, name, valid_from )
values ( currval('temporal.countries_id_seq'), new.name, now() )
);
-- UPDATE - set the last history entry validity to now, save the current data in
-- a new history entry and update the current table with the new data.
--
create rule countries_upd as on update to countries do instead (
update history.countries
set valid_to = now()
where id = old.id and valid_to = '9999-12-31';
insert into history.countries ( id, name, valid_from )
values ( old.id, new.name, now() );
update only temporal.countries
set name = new.name
where id = old.id
);
-- DELETE - save the current data in the history and eventually delete the data
-- from the current table.
--
create rule countries_del as on delete to countries do instead (
update history.countries
set valid_to = now()
where id = old.id and valid_to = '9999-12-31';
delete from only temporal.countries
where temporal.countries.id = old.id
);
-- EOF
And when I load it into a blank db and do a single insert, here's what happens (check out lines 39-40 for the surprising (to me) results).
1 test=# \i /home/username/temporal.sql
2 psql:/home/sirrobert/temporal.sql:1: NOTICE: drop cascades to 3 other objects
3 DETAIL: drop cascades to table temporal.countries
4 drop cascades to view countries
5 drop cascades to table history.countries
6 DROP SCHEMA
7 DROP SCHEMA
8 CREATE SCHEMA
9 CREATE SCHEMA
10 CREATE TABLE
11 CREATE TABLE
12 CREATE INDEX
13 CREATE INDEX
14 CREATE VIEW
15 CREATE RULE
16 CREATE RULE
17 CREATE RULE
18 test=# SELECT * FROM public.countries;
19 id | name
20 ----+------
21 (0 rows)
22
23 test=# SELECT * FROM temporal.countries;
24 id | name
25 ----+------
26 (0 rows)
27
28 test=# INSERT INTO public.countries (name) VALUES ('USA');
29 INSERT 0 1
30 test=# SELECT * FROM public.countries;
31 id | name
32 ----+------
33 1 | USA
34 (1 row)
35
36 test=# SELECT * FROM temporal.countries;
37 id | name
38 ----+------
39 1 | USA
40 1 | USA
41 (2 rows)
You insert data into both tables temporal.countries and history.countries with the latter being inherited from the former. That is the wrong approach. You should only insert into history.countries, with the additional attributes. When you then query temporal.countries you see a single record, but without the valid from/to information.
Once you update a record you will get duplicates. There is no way around that with your current approach. But you don't really need inheritance to begin with. You can have two separate tables and then create a view public.countries that returns currently valid rows from temporal.countries:
create table temporal.countries (
id serial primary key,
name varchar UNIQUE
);
create table history.countries (
hid serial primary key,
country integer not null references temporal.countries,
name varchar,
valid_from timestamp not null,
valid_to timestamp not null default '9999-12-31',
recorded_at timestamp not null default now(),
constraint from_before_to check (valid_from < valid_to),
constraint overlapping_times exclude using gist (
box(
point( extract( epoch from valid_from), id ),
point( extract( epoch from valid_to - interval '1 millisecond'), id )
) with &&
)
) inherits ( temporal.countries );
Now create the view to return only currently valid countries:
create view public.countries as
select c.*
from temporal.countries c
join history.countries h on h.country = c.id
where localtimestamp between h.valid_from and h.valid_to;
And your three rules:
-- INSERT - insert data in temporal.countries and metadata in history.countries
create rule countries_ins as on insert to public.countries do instead (
insert into temporal.countries ( name )
values ( new.name )
returning temporal.countries.*;
insert into history.countries ( country, name, valid_from )
values ( currval('temporal.countries_id_seq'), new.name, now() )
);
-- UPDATE - set the last history entry validity to now, save the current data in
-- a new history entry and update the current table with the new data.
create rule countries_upd as on update to countries do instead (
update history.countries
set valid_to = now()
where id = old.id and valid_to = '9999-12-31'; -- view shows only valid data
insert into history.countries ( country, name, valid_from )
values ( old.id, new.name, now() );
update only temporal.countries
set name = new.name
where id = old.id
);
-- DELETE - save the current date in the history and eventually delete the data
-- from the current table.
create rule countries_del as on delete to countries do instead (
update history.countries
set valid_to = LOCALTIMESTAMP
where id = old.id and valid_to = '9999-12-31';
-- don't delete country data, view won't show it anyway
delete from only temporal.countries
where temporal.countries.id = old.id
);

Constraining Child Record Based on Parent Record

In a timesheets data model, suppose I have the following parent table:
CREATE TABLE EmployeeInRole (
employeeInRoleId PRIMARY KEY,
employeeId,
roleId,
rate,
effectiveFrom DATE, --from when can this employee assume this role
effectiveTo DATE
);
and the following child table:
CREATE TABLE TimesheetEntry (
startTime DATETIME,
endTime DATETIME,
employeeInRoleId,
CONSTRAINT fk FOREIGN KEY (employeeInRoleId) REFERENCES EmployeeInRole (employeeInRoleId)
);
When I insert into TimesheetEntry, I'd like to make sure that time period falls within the boundaries of the parent record's effectiveFrom/To.
Is it possible to build this constraint into the DDL without use of a trigger, or do I have to maintain this constraint via a trigger or at the application level?
(Here is some info about Oracle only)
It's not possible in Oracle with clear DDL but you can do something like this:
create table t1 (id number primary key, date_from date, date_to date);
create table t2 (id number primary key, date_from date, date_to date, parent_id number references t1(id));
create view v as
select t2.* from t2
where exists (select 1 from t1 where t1.id = t2.parent_id
and t2.date_from between t1.date_from and t1.date_to
and t2.date_to between t1.date_from and t1.date_to)
with check option constraint chk_v;
insert into t1 values (1, sysdate - 5, sysdate); -- OK
insert into v values (1, sysdate - 4, sysdate - 3, 1); -- OK
insert into v values (1, sysdate - 6, sysdate - 3, 1); -- ERROR (WITH CHECK OPTION where-clause violation)
V is updatable view created with CHECK OPTION
"Is it possible to build this constraint into the DDL without use of a trigger,"
It is possible in some RDBMS systems, but it is not possible in SQL.

Can you define a new column in a SQL Server table which auto-generates Unique Identifiers for new rows?

Auto-increment columns in SQL Server get populated automatically; is it possible to define a UniqueIdentifier column to auto-generate on insert without the use of a trigger?
This will be a secondary unique key on the table. I need to create it because we need a public primary key now which can be used within a query string.
Legacy infrastructure still relies on the old int primary key. Given that the old infrastructure creates the record in the first place, I would like SQL Server to transparently create the secondary GUID key on insert - if at all possible.
Many thanks
You can use a column with Uniqueidentifier type with default value of NEWID()
If you add a column to your table with a default of NEWID() and then update existing rows to have a new id too. You may wa
-- Create test table
CREATE TABLE Test1
(
ID int IDENTITY(1,1)
,Txt char(1)
);
-- Insert data
INSERT INTO Test1(Txt)
SELECT 'a' UNION ALL
SELECT 'b' UNION ALL
SELECT 'c' UNION ALL
SELECT 'd' UNION ALL
SELECT 'e';
-- Add column
ALTER TABLE Test1
ADD GlobalID uniqueidentifier DEFAULT(NEWID());
-- View table, default value not added for existing rows
SELECT *
FROM Test1;
-- Update null ids with guids
UPDATE Test1
SET GlobalID = NEWID()
WHERE GlobalID IS NULL
-- Insert new data
INSERT INTO Test1(Txt)
SELECT 'f' UNION ALL
SELECT 'g' UNION ALL
SELECT 'h' UNION ALL
SELECT 'i' UNION ALL
SELECT 'j';
-- View table
SELECT *
FROM Test1;
-- Drop table
DROP TABLE Test1

Oracle - updating a sorted table

I found an old table without a primary key, and in order to add one, I have to add a new column and fill it with sequence values. I have another column which contains the time of when the record was created, so I want to insert the sequence values to the table sorted by the column with the time.
I'm not sure how to do it. I tried using PL\SQL - I created a cursor for a query that returns the table with an ORDER BY, and then update for each record the cursor returns but it didn't work.
Is there a smart working way to do this?
Thanks in advance.
Another option is just to use a correlated subquery, with the wrinkle of a nested subquery to generate the row number. Setting up some sample data:
create table t42 (datefield date);
insert into t42 (datefield) values (sysdate - 7);
insert into t42 (datefield) values (sysdate + 6);
insert into t42 (datefield) values (sysdate - 5);
insert into t42 (datefield) values (sysdate + 4);
insert into t42 (datefield) values (sysdate - 3);
insert into t42 (datefield) values (sysdate + 2);
select * from t42;
DATEFIELD
---------
12-JUL-12
25-JUL-12
14-JUL-12
23-JUL-12
16-JUL-12
21-JUL-12
Then adding and populating the new column:
alter table t42 add (id number);
update t42 t1 set t1.id = (
select rn from (
select rowid, row_number() over (order by datefield) as rn
from t42
) t2
where t2.rowid = t1.rowid
);
select * from t42 order by id;
DATEFIELD ID
--------- ----------
12-JUL-12 1
14-JUL-12 2
16-JUL-12 3
21-JUL-12 4
23-JUL-12 5
25-JUL-12 6
Since this is a synthetic key, making it match the order of another column seems a bit pointless, but I guess doesn't do any harm.
To complete the task:
alter table t42 modify id not null;
alter table t42 add constraint t42_pk primary key (id);
First of all, create new field and allow null values.
Then, update field from other table or query. Best approach is to use merge statement.
Here a sample from documentation:
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
DELETE WHERE (S.salary > 8000)
WHEN NOT MATCHED THEN INSERT (D.employee_id, D.bonus)
VALUES (S.employee_id, S.salary*.01)
WHERE (S.salary <= 8000);
Finally, set as non null this new field and promote it to primary key.
Here sample sentences:
ALTER TABLE
customer
MODIFY
(
your_new_field varchar2(100) not null
)
;
ALTER TABLE
customer
ADD CONSTRAINT customer_pk PRIMARY KEY (your_new_field)
;
One simple way is to create a new table, with new column an all other columns:
create table newt (
newtID int primary key not null,
. . .
)
Then insert all the old data into it:
insert into newt
select row_number() over (order by <CreatedAt>), t.*
from t
(You can substitute all the columns in, instead of using "*". Having the columns by name is the better practice. This is shorter, plus, I don't know the column names.)
If you alter the table to add the column, then the column will appear at the end. I find that quite awkward for the primary key. If you do that, though, you can update it as:
with t as (select row_number() over (order by <CreatedAt>) as seqnum, t.*
from t
)
update t
set newtID = seqnum