Constraint name update in PostgreSQL - postgresql

Is it possible to change the constraint name in Postgres?
I have a PK added with:
ALTER TABLE contractor_contractor ADD CONSTRAINT commerce_contractor_pkey PRIMARY KEY(id);
And I want to to have different name for it, to be consistent with the rest of the system.
Shall I delete the existing PK constraint and create a new one? Or is there a 'soft' way to
manage it?
Thanks!

To rename an existing constraint in PostgreSQL 9.2 or newer, you can use ALTER TABLE:
ALTER TABLE name RENAME CONSTRAINT constraint_name TO new_constraint_name;

For the primary key, you should be able to just:
ALTER INDEX commerce_contractor_pkey RENAME TO whatever_new_name
That won't work for other types of constraints though. The best option there is to drop the old one and create a new one. Be sure to do it inside a transaction, so the system isn't live without it during rebuild. (And if you can't do it in a transaction, be sure to create the new one first, before dropping the old one)

We found that primary keys often lag behind the main table name. This script helped us identify and fix the ones with issues.
select
table_name,
constraint_name ,
'ALTER TABLE ' || table_name || ' RENAME CONSTRAINT ' || constraint_name || ' TO ' || left(table_name, 58) || '_pkey;'
from information_schema.table_constraints tc
where constraint_type = 'PRIMARY KEY'
and constraint_name <> left(table_name, 58) || '_pkey';
This finds all the tables where the primary key name is no longer the "default" pattern (<tablename>_pkey) and creates a rename script for each.
The 58 character limit above in code above is to account for the maximum size of constraint names (63bytes).
Obviously sense check what is returned prior to running it. Hope that is helpful for others.

Related

Postgres constraint invalid after dropping and recreating

We are using postgres Aurora RDS ( version 12.4)
We have a table on which we had a unique constraint on three columns (Column_a , column_b, column_c). After some time ,we realized we would need to add one more column to the unique constraint while the table already had some data.
So we decided to drop the constraint and recreate it with the new set of columns. The following are the statements used:
alter table table_name drop constraint constraint_name ;
ALTER TABLE ONLY table_name
ADD unique constraint constraint_name ( column_new ,column_a, column_b , column_c) ;
After recreating the constraint the constraint is shown as invalid and on conflict statements using this constraint are failing with an error message:
"there is no unique or exclusion constraint matching the ON CONFLICT specification"
We tried creating a duplicate table . We could not replicate the problem when this backup table had no data . But on filling in some data into the table and trying the same steps we could replicate the problem . The new constraint was showing as invalid .
We would like to understand what causes the constraint to be invalid. Is there any restriction on dropping constraints on tables already having data? We would like to know if we could overcome this without truncating the table.

Why is table-swapping in Postgres so verbose?

I'd like to backfill a column of a large (20M rows), frequently-read but rarely-written table. From various articles and questions on SO, it seems like the best way to do this is create a table with identical structure, load in the backfilled data, and live-swap (since renaming is pretty quick). Sounds good!
But when I actually write the script to do this, it is mind-blowingly long. Here's a taste:
BEGIN;
CREATE TABLE foo_new (LIKE foo);
-- I don't use INCLUDING ALL, because that produces Indexes/Constraints with different names
-- This is the only part of the script that is specific to my case.
-- Everything else is standard for any table swap
INSERT INTO foo_new (id, first_name, last_name, email, full_name)
(SELECT id, first_name, last_name, email, first_name || last_name) FROM foo);
CREATE SEQUENCE foo_new_id_seq
START 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
SELECT setval('foo_new_id_seq', COALESCE((SELECT MAX(id)+1 FROM foo_new), 1), false);
ALTER SEQUENCE foo_new_id_seq OWNED BY foo_new.id;
ALTER TABLE ONLY foo_new ALTER COLUMN id SET DEFAULT nextval('foo_new_id_seq'::regclass);
ALTER TABLE foo_new
ADD CONSTRAINT foo_new_pkey
PRIMARY KEY (id);
COMMIT;
-- Indexes are made concurrently, otherwise they would block reads for
-- a long time. Concurrent index creation cannot occur within a transaction.
CREATE INDEX CONCURRENTLY foo_new_on_first_name ON foo_new USING btree (first_name);
CREATE INDEX CONCURRENTLY foo_new_on_last_name ON foo_new USING btree (last_name);
CREATE INDEX CONCURRENTLY foo_new_on_email ON foo_new USING btree (email);
-- One more line for each index
BEGIN;
ALTER TABLE foo RENAME TO foo_old;
ALTER TABLE foo_new RENAME TO foo;
ALTER SEQUENCE foo_id_seq RENAME TO foo_old_id_seq;
ALTER SEQUENCE foo_new_id_seq RENAME TO foo_id_seq;
ALTER TABLE foo_old RENAME CONSTRAINT foo_pkey TO foo_old_pkey;
ALTER TABLE foo RENAME CONSTRAINT foo_new_pkey TO foo_pkey;
ALTER INDEX foo_on_first_name RENAME TO foo_old_on_first_name;
ALTER INDEX foo_on_last_name RENAME TO foo_old_on_last_name;
ALTER INDEX foo_on_email RENAME TO foo_old_on_email;
-- One more line for each index
ALTER INDEX foo_new_on_first_name RENAME TO foo_on_first_name;
ALTER INDEX foo_new_on_last_name RENAME TO foo_on_last_name;
ALTER INDEX foo_new_on_email RENAME TO foo_on_email;
-- One more line for each index
COMMIT;
-- TODO: drop old table (CASCADE)
And this doesn't even include foreign keys, or other constraints! Since the only part of this that is specific to my case in the INSERT INTO bit, I'm surprised that there's no built-in Postgres function to do this sort of swapping. Is this operation less common than I make it out to be? Am I underestimating the variety of ways this can be accomplished? Is my desire to keep naming consistent an atypical one?
It's probably not all that common. Most tables aren't big enough to warrant it, and most applications can tolerate some amount of downtime here and there.
More importantly, different applications can afford to cut corners in different ways depending on their workload. The database server can't; it needs to handle (or to very deliberately not handle) every possible obscure edge-case, which is likely a lot harder than you might expect. Ultimately, writing tailored solutions for different use cases probably makes more sense.
Anyway, if you're just trying to implement a calculated field as first_name || last_name, there are better ways of doing it:
ALTER TABLE foo RENAME TO foo_base;
CREATE VIEW foo AS
SELECT
id,
first_name,
last_name,
email,
(first_name || last_name) AS full_name
FROM foo_base;
Assuming that your real case is more complicated, all of this effort may still be unnecessary. I believe that the copy-and-rename approach is largely based on the assumption that you need to lock the table against concurrent modifications for the duration of this process, and so the goal is to get it done as quickly as possible. If all concurrent operations are read-only - which appears to be the case, since you're not locking the table - then you're probably better off with a simple UPDATE (which won't block SELECTs), even if it does take a bit longer (though it does have the advantage of avoiding foreign key re-checks and TOAST table rewrites).
If this approach really is justified, I think there a few opportunities for improvement:
You don't need to recreate/reset the sequence; you can just link the existing sequence to the new table.
CREATE INDEX CONCURRENTLY seems unnecessary, as nobody else should be trying to access foo_new yet. In fact, if the whole script were in one transaction, it wouldn't even be externally visible at this point.
Table names only need to be unique within a schema. If you temporarily create a schema for the new table, you should be able to replace all of those RENAMEs with a single ALTER TABLE foo SET SCHEMA public.
Even if you don't expect concurrent writes, it wouldn't hurt to LOCK foo IN SHARE MODE anyway...
EDIT:
The sequence reassignment is a little more involved than I expected, as it seems that they need to stay in the same schema as their parent table. But here is (what appears to be) a working example:
BEGIN;
LOCK public.foo IN SHARE MODE;
CREATE SCHEMA tmp;
CREATE TABLE tmp.foo (LIKE public.foo);
INSERT INTO tmp.foo (id, first_name, last_name, email, full_name)
SELECT id, first_name, last_name, email, (first_name || last_name) FROM public.foo;
ALTER TABLE tmp.foo ADD CONSTRAINT foo_pkey PRIMARY KEY (id);
CREATE INDEX foo_on_first_name ON tmp.foo (first_name);
CREATE INDEX foo_on_last_name ON tmp.foo (last_name);
CREATE INDEX foo_on_email ON tmp.foo (email);
ALTER TABLE tmp.foo ALTER COLUMN id SET DEFAULT nextval('public.foo_id_seq');
ALTER SEQUENCE public.foo_id_seq OWNED BY NONE;
DROP TABLE public.foo;
ALTER TABLE tmp.foo SET SCHEMA public;
ALTER SEQUENCE public.foo_id_seq OWNED BY public.foo.id;
DROP SCHEMA tmp;
COMMIT;

Is it possible to create a Foreign Key on 2 columns with differing Collations?

I've tried searching for about an hour through all the Foreign Key / Collation Questions but I can't find something even remotely close to my question.
I have 2 tables from two different software vendors sitting in the same database. One vendor hard codes their collations to Latin1_General_BIN while the other uses the Database Default (in this case Latin1_General_CI_AS). Is it possible, without altering any of the columns, to create Foreign Keys between these two Collation Types?
Typically you would simply change one but in this case the Tables are very sensitive and I'm not allowed to make such a change but I have to create a Foreign Key due to a piece of logic in a Trigger that reads data between these two tables only if it finds a Foreign Key:
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE
WHERE CONSTRAINT_NAME =
(
SELECT name FROM sys.foreign_keys
WHERE parent_object_id = OBJECT_ID( 'Table1' )
AND referenced_object_id = OBJECT_ID( 'Table2' )
)
Any help would really be appreciate
P.S. I just can't seem to figure out how this code thing works if anyone would help me out, I put in the 4 required spaces but it's still just displaying my code as text :(
Adding a foreign key constraint from a field of one collation to a field of another collation can't be done. You will get error message 1757.
Either change the collation of one of the tables or create a work around with a new column that is used instead with the correct collation or create surrogate key columns with integers used for referencing.
If nothing else works and you really really need to fix this type of constraint and performance is not an issue, add a check constraints and/or triggers that will check the referential integrity of data put into the tables. These rules will have to cast all values in one table to the collation of the other in order to compare values so it will be slow and it will be really tricky for you to get use of indexes, proceed with caution.
For example you could have an insert trigger on the referencing table that check if a record with the inserted string exists in the referenced table. Then you would also have to add an update and delete trigger for the referenced table so that it doesn't fall out of range of values that are referenced by records in the referencing table or which cascades updates/deletes. Basically you replicate what foreign keys are and it gets really slow and scales horribly.
Short answer: don't do it, let the tables stay untied or fix the collation of one of them.
Sweet, I think the solution is very elegant. I'm writing it as an answer purely as it's the full alternative that closest resembles the required solution. But I'm going to mark your answer as the answer as it's the one that correctly answers my original question.
Right, so first what I did, was I got permission from the vendor who's trigger requires the foreign key, to create a new column in their table as a persisted computed column in the collation of the other vendors table:
DECLARE #Collation nvarchar(100)
DECLARE #SQL nvarchar(1000)
SET #Collation = ( SELECT collation_name FROM sys.columns WHERE OBJECT_ID IN ( SELECT OBJECT_ID FROM sys.objects WHERE type = 'U' AND name = 'Vendor1Table' ) AND name = 'Vendor1Column' )
SET #SQL = 'ALTER TABLE [Vendor2Table] ADD [Vendor2ComputedColumn] AS [Vendor2Column] COLLATE ' + #Collation + ' PERSISTED'
EXECUTE( #SQL )
GO
Next up, I added a candidate key to the computed column:
ALTER TABLE [Vendor2Table] ADD CONSTRAINT [CCUNQ1_x] UNIQUE NONCLUSTERED
(
[Vendor2ComputedColumn] ASC
)
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
ON [PRIMARY]
GO
Then, I simply created the foreign key to the computed column:
ALTER TABLE [dbo].[Vendor1Table] WITH CHECK ADD CONSTRAINT [CCFOK01_x] FOREIGN KEY ( [Vendor1Column] )
REFERENCES [dbo].[Vendor2Table] ( [Vendor2ComputedColumn] )
GO
ALTER TABLE [dbo].[Vendor1Table] CHECK CONSTRAINT [CCFOK01_x]
GO
and finally, the original SQL Script passes with flying colours:
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE
WHERE CONSTRAINT_NAME =
(
SELECT name FROM sys.foreign_keys
WHERE parent_object_id = OBJECT_ID( 'Vendor1Table' )
AND referenced_object_id = OBJECT_ID( 'Vendor2Table' )
)
Hopefully this small walkthrough helps some other soul some day :)
Thanks for the assist David, appreciate it!

Set column as primary key if the table doesn't have a primary key

I have a column in db which has 5 columns but no primary key.
One of the columns is named myTable_id and is integer.
I want to check if the table has a primary key column. If it doesn't, then make myTable_id a primary key column and make it identity column. Is there a way to do this?
I tried with this:
ALTER TABLE Persons
DROP CONSTRAINT pk_PersonID
ALTER TABLE Persons
ADD PRIMARY KEY (P_Id)
and I get syntax error in Management studio.
This checks if primary key exists, if not it is created
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE CONSTRAINT_TYPE = 'PRIMARY KEY' AND TABLE_NAME = 'Persons'
AND TABLE_SCHEMA ='dbo')
BEGIN
ALTER TABLE Persons ADD CONSTRAINT pk_PersonID PRIMARY KEY (P_Id)
END
ELSE
BEGIN
-- Key exists
END
fiddle: http://sqlfiddle.com/#!6/e165d/2
ALTER TABLE Persons
ADD CONSTRAINT pk_PersonID PRIMARY KEY (P_Id)
An IDENTITY constraint can't be added to an existing column, so how you add this needs to be your initial thought. There are two options:
Create a new table including a primary key with identity and drop the existing table
Create a new primary key column with identity and drop the existing 'P_ID' column
There is a third way, which is a better approach for very large tables via the ALTER TABLE...SWITCH statement. See Adding an IDENTITY to an existing column for an example of each. In answer to this question, if the table isn't too large, I recommend running the following:
-- Check that the table/column exist and no primary key is already on the table.
IF COL_LENGTH('PERSONS','P_ID') IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE CONSTRAINT_TYPE = 'PRIMARY KEY' AND TABLE_NAME = 'PERSONS')
-- Add table schema to the WHERE clause above e.g. AND TABLE_SCHEMA ='dbo'
BEGIN
ALTER TABLE PERSONS
ADD P_ID_new int IDENTITY(1, 1)
GO
ALTER TABLE PERSONS
DROP COLUMN P_ID
GO
EXEC sp_rename 'PERSONS.P_ID_new', 'P_ID', 'Column'
GO
ALTER TABLE PERSONS
ADD CONSTRAINT PK_P_ID PRIMARY KEY CLUSTERED (P_ID)
GO
END
Notes:
By explicitly using the CONSTRAINT keyword the primary key constraint is given a particular name rather than depending on SQL Server to auto-assign a name.
Only include CLUSTERED on the PRIMARY KEY if the balance of searches for a particular P_ID and the amount of writing outweighs the benefits of clustering the table by some other index. See Create SQL IDENTITY as PRIMARY KEY.
You can check if primary key exists or not using OBJECTPROPERTY Transact SQL, use 'TableHasPrimaryKey' for the second arguments.
DECLARE #ISHASPRIMARYKEY INT;
SELECT #ISHASPRIMARYKEY = OBJECTPROPERTY(OBJECT_ID('PERSONS'), 'TABLEHASPRIMARYKEY');
IF #ISHASPRIMARYKEY IS NULL
BEGIN
-- generate identity column
ALTER TABLE PERSONS
DROP COLUMN P_ID;
ALTER TABLE PERSONS
ADD P_ID INT IDENTITY(1,1);
-- add primary key
ALTER TABLE PERSONS
ADD CONSTRAINT PK_PERSONID PRIMARY KEY (P_ID);
END;
I don't think you can do that. For making a column into an identity column I think you have to drop the table entirely.

update pg_constraint has no effect (postgres)

I tried to change all foreign keys in PostgreSQL at once to cascade on delete:
UPDATE pg_catalog.pg_constraint
SET confupdtype='c', confdeltype='c', confmatchtype='u'
WHERE connamespace=2200;
There are no errors, and when I inspect the tables with pgadmin, it looks right, but when I try to delete a referenced table-line it comes to a constraint error. Just the SQL statement works:
ALTER TABLE tblname
DROP CONSTRAINT IF EXISTS fk3e2e4a8ff123848a;
ALTER TABLE tblname
ADD CONSTRAINT fk3e2e4a8ff123848a FOREIGN KEY (field)
REFERENCES othertable (id) MATCH SIMPLE
ON UPDATE CASCADE ON DELETE CASCADE;
Any idea why changing pg_catalog.pg_constraint is not working? even restarting the service after didn't help.
Really you shouldn't be updating pg_* tables.
Use a command like
ALTER TABLE YOURTABLE DISABLE TRIGGER;
Check this link out.
http://archives.postgresql.org/pgsql-general/2011-10/msg00802.php