PostgreSQL: How to create a prefixed sequence as a unique identifier? - sql

One of my clients insists that I create a unique identifier that starts with a given prefix, then increments by one in a postgres table. For example, PREFIX000001.
I know postgres provides SERIAL, BIGSERIAL and UUID to uniquely identify rows in a table. But the client just does not listen. He wants it his way.
Here is the sample table (Excel representation) - something like unique_id column that auto generates on every INSERT command:
I really want to know whether this is technically possible in postgres.
How should I go about this?

You could create a SERIAL or BIGSERIAL like you suggested but represent it with a string when reporting the data in the application (if the client would accept that):
SELECT to_char(id, '"PREFIX0"FM0000000') AS unique_id, product_name, product_desc FROM table;
For example:
SELECT to_char(123, '"PREFIX0"FM0000000') AS unique_id;
unique_id
----------------
PREFIX00000123
(1 row)
Time: 2.704 ms
Otherwise you would have to do this:
CREATE SEQUENCE my_prefixed_seq;
CREATE TABLE my_table (
unique_id TEXT NOT NULL DEFAULT 'PREFIX'||to_char(nextval('my_prefixed_seq'::regclass), 'FM0000000'),
product_name text,
product_desc text
);
INSERT INTO my_table (product_name) VALUES ('Product 1');
INSERT INTO my_table (product_name) VALUES ('Product 2');
INSERT INTO my_table (product_name) VALUES ('Product 3');
->
SELECT * FROM my_table;
unique_id | product_name | product_desc
---------------+--------------+--------------
PREFIX0000004 | Product 1 | {NULL}
PREFIX0000005 | Product 2 | {NULL}
PREFIX0000006 | Product 3 | {NULL}
(3 rows)
Time: 3.595 ms
I would advice you to try to make the client reconsider but it looks like you already tried that route
To whomever reads this in the future, please don't do this to your database, this is not good practice as #Beki acknowledged in his question

As Gab says that's a pretty cumbersome thing to do. If you also want to keep a normal primary key for internal use in your app, here's a solution:
CREATE OR REPLACE FUNCTION add_prefix(INTEGER) RETURNS text AS
$$ select 'PREFIX'||to_char($1, 'FM0000000'); $$
LANGUAGE sql immutable;
CREATE TABLE my_table (
id SERIAL PRIMARY KEY,
unique_id TEXT UNIQUE NOT NULL GENERATED ALWAYS AS
(add_prefix(id)) STORED,
product_name text
);
INSERT INTO my_table (product_name) VALUES ('Product 1');
INSERT INTO my_table (product_name) VALUES ('Product 2');
INSERT INTO my_table (product_name) VALUES ('Product 3');
select * from my_table;
id | unique_id | product_name
----+---------------+--------------
1 | PREFIX0000001 | Product 1
2 | PREFIX0000002 | Product 2
3 | PREFIX0000003 | Product 3
Sure, you get an extra index gobbling up RAM and disk space for nothing. But, when the client then inevitably asks you "I want to update the unique identifier" in a few months...
Or even worse, "why are there holes in the sequence can't you make it so there are no holes"...
...then you won't have to update ALL the relations in all the tables...

One method uses a sequence:
create sequence t_seq;
create table t (
unique_id varchar(255) default ('PREFIX' || lpad(nextval('t_seq')::text, 6, '0'));
)
Here is a db<>fiddle.

Related

How to UPDATE or INSERT in PostgreSQL

I want to UPDATE or INSERT a column in PostgreSQL instead of doing INSERT or UPDATE using INSERT ... ON CONFLICT ... because there will be more updates than more inserts and also I have an auto incrementing id column that's defined using SERIAL so it increments the id column everytime it tries to INSERT or UPDATE and that's not what I want, I want the id column to increase only if it's an INSERT so that all ids would be in an order instead
The table is created like this
CREATE TABLE IF NOT EXISTS table_name (
id SERIAL PRIMARY KEY,
user_id varchar(30) NOT NULL,
item_name varchar(50) NOT NULL,
code_uses bigint NOT NULL,
UNIQUE(user_id, item_name)
)
And the query I used was
INSERT INTO table_name
VALUES (DEFAULT, 'some_random_id', 'some_random_name', 1)
ON CONFLICT (user_id, item_name)
DO UPDATE SET code_uses = table_name.code_uses + 1;
Thanks :)
Upserts in PostgreSQL do exactly what you described.
Consider this table and records
CREATE TABLE t (id SERIAL PRIMARY KEY, txt TEXT);
INSERT INTO t (txt) VALUES ('foo'),('bar');
SELECT * FROM t ORDER BY id;
id | txt
----+-----
1 | foo
2 | bar
(2 Zeilen)
Using upserts the id will only increment if a new record is inserted
INSERT INTO t VALUES (1,'foo updated'),(3,'new record')
ON CONFLICT (id) DO UPDATE SET txt = EXCLUDED.txt;
SELECT * FROM t ORDER BY id;
id | txt
----+-------------
1 | foo updated
2 | bar
3 | new record
(3 Zeilen)
EDIT (see coments): this is the expected behaviour of a serial column, since they're nothing but a fancy way to use sequences. Long story short: using upserts the gaps will be inevitable. If you're worried the value might become too big, use bigserial instead and let PostgreSQL do its job.
Related thread: serial in postgres is being increased even though I added on conflict do nothing

Generate unique IDs in non-unique columns

Consider this table:
ex_table
| gid | val |
| --- | --- |
| 1 | v1 |
| 1 | v2 |
| 2 | v3 |
Notice that gid is the id-like column and not unique.
I want to be able to insert values into the table either by generating a unique gid or by specifying which one to use.
For example:
INSERT INTO ex_table (val)
SELECT --....
Should generate a unique gid, while
INSERT INTO ex_table (gid, val)
SELECT --....
Should use the provided gid.
Any way to do this?
You can do what you want to the letter of what you say by using overriding system value and an auto-generated column. For instance:
create table t (
gid int generated always as identity,
name varchar(255)
);
Then
insert into t (name) values ('abc');
insert into t (gid, name) overriding system value values (1, 'def')
will insert two rows with a gid value of 1.
Here is an example.
Just one caveat: Inserting your own value does not change the next value that is automatically generated. So, if you manually insert values that do not exist, then you might find that duplicates are later generated for them.
You can try something like this
CREATE SEQUENCE table_name_id_seq;
CREATE TABLE table_name (
gid integer NOT NULL DEFAULT nextval('table_name_id_seq'),
name varchar);
ALTER SEQUENCE table_name_id_seq
OWNED BY table_name.id;
OR SIMPLY
CREATE TABLE table_name(
gid SERIAL,
name varchar);
AND THEN TO INSERT
INSERT INTO fruits(gid,name)
VALUES(DEFAULT,'Apple');

PostgreSQL Upsert with a WHERE clause

I am trying to migrate an Oracle merge query to PostgreSql. As described in this article, Postgres UPSERT syntax supports a "where clause" to identify conditions of conflict.
Unfortunately, that webpage does not provide an example with the "where clause". I tried searching for it elsewhere but could not find it. Hence this question.
Following the same example in the above given webpage, here is an example setup:
CREATE TABLE customers (
customer_id serial PRIMARY KEY,
name VARCHAR UNIQUE,
email VARCHAR NOT NULL,
active bool NOT NULL DEFAULT TRUE
);
INSERT INTO customers (NAME, email) VALUES
('IBM', 'contact#ibm.com'),
('Microsoft', 'contact#microsoft.com'),
('Intel','contact#intel.com');
SELECT * FROM customers;
customer_id | name | email | active
-------------+-----------+-----------------------+--------
1 | IBM | contact#ibm.com | t
2 | Microsoft | contact#microsoft.com | t
3 | Intel | contact#intel.com | t
(3 rows)
I want my UPSERT statement to look something like this:
INSERT INTO customers (NAME, email)
VALUES
('Microsoft', 'hotline#microsoft.com')
ON CONFLICT where (name = 'Microsoft' and active = TRUE)
DO UPDATE SET email = 'hotline#microsoft.com';
The example is a bit contrived but I hope I have been able to communicate the gist here.
You need a partial index. Drop uniqe constraint on the column name and create a partial index on the column:
CREATE TABLE customers (
customer_id serial PRIMARY KEY,
name VARCHAR,
email VARCHAR NOT NULL,
active bool NOT NULL DEFAULT TRUE
);
CREATE UNIQUE INDEX ON customers (name) WHERE active;
INSERT INTO customers (NAME, email) VALUES
('IBM', 'contact#ibm.com'),
('Microsoft', 'contact#microsoft.com'),
('Intel','contact#intel.com');
The query should look like this:
INSERT INTO customers (name, email)
VALUES
('Microsoft', 'hotline#microsoft.com')
ON CONFLICT (name) WHERE active
DO UPDATE SET email = excluded.email;
SELECT *
FROM customers;
customer_id | name | email | active
-------------+-----------+-----------------------+--------
1 | IBM | contact#ibm.com | t
3 | Intel | contact#intel.com | t
2 | Microsoft | hotline#microsoft.com | t
(3 rows)
Note the proper use of the special record excluded. Per the documentation:
The SET and WHERE clauses in ON CONFLICT DO UPDATE have access to the existing row using the table's name (or an alias), and to rows proposed for insertion using the special excluded table.

Swapping records' values for a column with a UNIQUE constraint in PostgreSQL

Goal:
Swapping values in some table's records for a particular column that has a UNIQUE constraint.
Example:
Considering the following table:
CREATE TABLE records (id numeric, name text);
ALTER TABLE records ADD CONSTRAINT uniq UNIQUE (id);
INSERT INTO records VALUES
(1, 'First record'),
(3, 'Second record'),
(2, 'Third record'),
(4, 'Fourth record');
We get, from querying for SELECT id, name FROM records ORDER BY id;:
| id | name |
|----|---------------|
| 1 | First record |
| 2 | Third record |
| 3 | Second record |
| 4 | Fourth record |
I need the ids for the Second record and Third record swapped. That is, I want the following output:
| id | name |
|----|---------------|
| 1 | First record |
| 2 | Second record | <-- this record previously had the id 3
| 3 | Third record | <-- this record previously had the id 2
| 4 | Fourth record |
Obviously:
I can't just swap the names for these records, my actual database is a tad bigger than this:
this table has more information stored and
its id column is actually a PRIMARY KEY that is referenced by other tables.
I don't wanna go through the hassle of utilizing temporary ids that aren't assigned to any record yet to execute this swapping.
The 'swapping' in question does not only involve two records, but an arbitrary long set of them.
The records also don't technically get 'swapped' but simply reassigned, with the following properties:
each id will always be unique after each bulk reassignment operation and
most of the ids from a previous assignment will overlap with those of the new assignment map.
What I've tried:
I have tried swapping my data in a single query by generating a temporary reassignment table and using it in an UPDATE ... SET ... FROM ... WHERE query.
Here's my temporary reassignment table, which will swap the ids 2 and 3:
(VALUES
(3, 2),
(2, 3)
) AS swap(id, new_id)
And here's how I use it:
UPDATE records AS record SET
id = swap.new_id
FROM (VALUES
(3, 2),
(2, 3)
) AS swap(id, new_id)
WHERE record.id = swap.id;
SELECT id, name FROM records ORDER BY id;
As you can see in this SQLFiddle link, it works pretty well... until you add the UNIQUE constraint on the id column.
How could I get this working under all the conditions listed above?
The issue is with the CONSTRAINT's definition.
PosgreSQL's CONSTRAINTS can be DEFERRABLE. From their documentation, we gather the following:
IMMEDIATE constraints are checked at the end of each statement. DEFERRED constraints are not checked until transaction commit. Each constraint has its own IMMEDIATE or DEFERRED mode.
In the case we're facing, the ids will still be UNIQUEs AFTER the transaction, while indeed that CONSTRAINT cannot be satisfied DURING the reassignment of ids.
Simply making the relevant CONSTRAINT DEFERRABLE will solve the problem at hand.
CREATE TABLE records (id numeric, name text);
ALTER TABLE records ADD CONSTRAINT uniq UNIQUE (id) DEFERRABLE INITIALLY IMMEDIATE;
INSERT INTO records VALUES -- ^
(1, 'First record'), -- HERE
(3, 'Second record'),
(2, 'Third record'),
(4, 'Fourth record');

Changing the value of a table field after it has already been inserted

I am working with a CMS system, and using a kind of smartform am inserting fields into a table.
In these fields are the name of a product, quantity and cost.
cost and product name are taken from a products table, while quantity is input manually.
I understand this is not the best architecture, but I am limited with the system I am working with.
What I want to do, is change the value of cost after it is inserted, to quantity * cost.
What is the easiest way to do this?
To change one row:
update TheTable
set cost = quantity * cost
where pk = 123
If you run this multiple times, costs will explode ;-)
If you have your hands tied so tightly that you are unable to change the table structure, then you probably won't be able to use a trigger, but here goes anyway.
This is a sample table structure:
DROP TABLE IF EXISTS `products`;
CREATE TABLE `products` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`product` VARCHAR(255),
`quantity` INT(11) NOT NULL,
`cost` DECIMAL(6,2) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
Create a trigger which updates the cost prior to it being inserted into the products table:
DELIMITER //
DROP TRIGGER IF EXISTS update_cost//
CREATE TRIGGER update_cost BEFORE INSERT ON products
FOR EACH ROW BEGIN
SET NEW.cost = NEW.cost * NEW.quantity;
END;
//
DELIMITER ;
Insert a couple of products into the table:
INSERT INTO `products` (`product`, `quantity`, `cost`) VALUES
('Acme portable hole', 10, 20),
('Acme jet pack', 1000, 2);
The cost is updated automatically:
SELECT * FROM prodcuts;
+----+--------------------+----------+---------+
| id | product | quantity | cost |
+----+--------------------+----------+---------+
| 1 | Acme portable hole | 10 | 200.00 |
| 2 | Acme jet pack | 1000 | 2000.00 |
+----+--------------------+----------+---------+
You can run an UPDATE after the insert, using the identity field of the new product:
UPDATE products
SET cost = cost * quantity
WHERE product_id = #productId