Including multiple columns in a single index in Postgres - sql

I have a 'users' table with two columns, 'email' and 'new_email'. I need:
A case-insensitive uniqueness constraint covering both columns - i.e., if "Bob#Example.com" appears in one row's 'email' column, then inserting "bob#example.com" into another row's (or even the same row's) 'new_email' column should fail.
Fast case-insensitive searching for a given email address in either the 'email' or 'new_email' fields - i.e. find the row where the new_email OR email is "Bob#example.com", case-insensitive.
I know that I could do this more easily by creating a related 'emails' table, but I'm expecting to be looking up users in this table (by primary key) from several applications, and I'd like to avoid duplicating the join logic in various places to also retrieve their emails. So I think some kind of expression index would be best, if that's possible.
If this isn't possible, I suppose my next best option would be to create a view that the other applications could use to easily fetch a user's emails along with their other information, but I'm not sure how to do that either.
I'm using Postgres 8.4. Thank you!

I think you'll have to use a trigger to enforce your cross-column uniqueness constraint. If you add unique indexes on each column and then a trigger something like this (untested off the top of my head code):
CREATE FUNCTION no_dups_allowed() RETURNS trigger AS $$
DECLARE
r ROW;
BEGIN
SELECT 1 INTO r
FROM users
WHERE LOWER(email) = LOWER(NEW.email_new)
OR LOWER(email_new) = LOWER(NEW.email);
IF FOUND THEN
-- Found a duplicate so it is time for a hissy fit!
RAISE 'Duplicate email address found' USING ERRCODE = 'unique_violation';
END;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
You'd want something like that as a BEFORE INSERT and BEFORE UPDATE trigger. That trigger would take care of catching cross-column duplicates and the unique indexes would take care of in-column duplicates.
Some useful references:
FOUND
RAISE
Triggers
Trigger Procedures
You'll want the individual indexes for your queries anyway and using the uniqueness half of the indexes simplifies your trigger by leaving it to only deal with the cross-column part; if you try to do it all in the trigger, then you'll have to watch out for updating a row without really changing the email or email_new columns.
For the querying half, you could create a view that used a UNION to combine the two columns. You could also create a function to merge the user's email addresses into one list. Hard to say which would be best without know more details of these other queries but I suspect that fixing all the other queries to know about email and email_new would be the best approach; you'll have to update all the other queries to use the view or function anyway so why build a view or function at all?

No need for triggers. Try this:
create table et (email text, email2 text);
create unique index et_u on et (coalesce(lower(email),lower(email2)));
insert into et (email,email2) values ('scott#gmail.com',NULL);
insert into et (email,email2) values ('scott#gmail.com',NULL);
ERROR: duplicate key value violates unique constraint "et_u"
insert into et (email,email2) values (NULL,'scott#gmail.com');
ERROR: duplicate key value violates unique constraint "et_u"
insert into et (email,email2) values (NULL,'Scott#gmail.com');
ERROR: duplicate key value violates unique constraint "et_u"

Related

How to create a trigger that automatically generates a primary key from multiple fields

I have a geospatial db with (a.o.) a table with locations, and a table with features. The primary key for the locations table is location_id. Location_id is also a foreign key in the features table. The features table also includes the fields "type" (in which a two-letter code is entered to denote particular types of features), and N (which differentiates the different features that may be linked to one location). I figured a combination of location_id, type, and N would make a decent primary key for the features table. Previously, I entered these ids manually. However, I would like for this to be automatically done when a "user" enters a location ID, N, and type. (Ideally I want to find a way to automatically generate the correct N, so that "users" need only enter location_id and type, but I think this should be posted as a separate question?).
I have been trying to achieve this via triggers (see code below), but when I test it by trying to add a new data row to my features table, I get the error message "duplicate key value violates unique constraint features_pkey". Could someone point me in the direction of help for this issue?
CREATE OR REPLACE FUNCTION set_features_id()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS
$$
DECLARE
compos_id text;
BEGIN
SELECT loc_id || type || N FROM features INTO compos_id;
NEW.id := compos_id;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS set_lf_id_trigger on public.landscape_features_point;
CREATE TRIGGER set_features_id_trigger
BEFORE INSERT
ON "features"
FOR EACH ROW
EXECUTE PROCEDURE set_features_id();

Oracle SQL alter table add column with current user name

It was a 2 part question and I got the timestamp correctly.
I'm on 12C and trying to do something like:
ALTER TABLE customers
ADD modified_by (USER FROM DUAL);
Basically just columns in a table that show who modified the table and at what time they did so.
I also tried
ALTER TABLE customers
ADD last_modified TIMESTAMP;
ALTER TABLE customers
ADD modified_by USER;
and other combinations of keywords that I found on this site and other sites but none of them work.
We only learned dual in class but I'm looking for any way to do these.
Edit:
I now understand what was taught to me by the user with almost 1 million points.
Still unsure how to do the username.
Read this:
https://docs.oracle.com/cd/B19306_01/server.102/b14237/statviews_2114.htm#REFRN20302
and tried:
ALTER TABLE customers
ADD modified_by USERNAME;
doesn't work get invalid datatype.
Then saw this: https://www.techonthenet.com/oracle/questions/find_users.php
and tried:
ALTER TABLE customers
ADD modified_by USERNAME FROM DBA_USERS;
but getting invalid alter table option. SQL is hard.
After you edited the question, it seems that you are somewhat closer to what you want. This:
ALTER TABLE customers ADD modified_by VARCHAR2(30);
^^^^^^^^^^^^
datatype goes here, not what you'd like
to put into this column
Then, during inserts or updates of that table, you'd put USER in there, e.g.
insert into customers (id, modified_by) values (100, USER);
Or, probably even better, set it to be default, as #a_horse_with_no_name suggested:
ALTER TABLE customers ADD modified_by VARCHAR2(30) DEFAULT USER;
so - if you don't explicitly put anything into that column, it'll be populated with the USER function's value.
If you read through the Oracle documentation on virtual columns, you will find this:
Functions in expressions must be deterministic at the time of table creation, but can subsequently be recompiled and made non-deterministic
A deterministic function is one that returns the same value when it is passed the same arguments. The expressions that you are using contain non-deterministic functions. Hence, they are not allowed.
The error that I get when I try this is:
ORA-54002: only pure functions can be specified in a virtual column expression
For some unknown reason, Oracle equates "pure" with "deterministic" in the error message. But this is saying that the function needs to be deterministic.

How can i prevent to have same values for one user

I have this kind of tables:
https://ibb.co/sPn5zT7
Here in the UserPl table, the ProgrammingLanguageId and KnowledgeId are foreign keys, connected with Primary Keys of Knowledge and ProgrammingLanguage table.
I want to make when I insert for example
insert into userPLs values(1,'a7ac3486-e852-42c0-a458-9075eb5ed7d7','Doe',1,1)
here Doe says that he knows C# with basic knowledge. I want to prevent in the next insert to be impossible for Doe, to be inserted again something like this:
insert into userPLs values(1,'a7ac3486-e852-42c0-a458-9075eb5ed7d7','Doe',1,2)
or
insert into userPLs values(2,'a7ac3486-e852-42c0-a458-9075eb5ed7d7','Doe',1,2)
because he once said that his knowledge of C# is basic.
I AM USING MS SQL SERVER
How can I achieve this?
Try to set a unique index, where required
You can prevent the insert with a constraint.
alter table UserPl
add constraint UserLanguageSkillLevel
unique (UserId, ProgrammingLanguageId);
You'll still gave to catch failed inserts or modify the front end to eliminate the opportunity to add contradictory information in the first place.
A uniqueness constraint is ultimately enforced with an index. If you create a unique index directly rather than by using a constraint you could also apply the ignore_dup_key index setting and let the engine silently discard bad inserts. I'm not going to endorse that as an ideal approach but it might be useful as a temporary stopgap.
Having Primary key / Cluster Index on the table UserPl would enforce whatever the combination of your needs i.e.
If User cannot know multiple programming languages, then key goes
Create clustered index CLU_UserPL on UserPl (UserID)
If User can can know multiple programming languages, but cannot have multiple level of knowledge in programming languages, then key goes
Create clustered index CLU_UserPL on UserPl (UserID, ProgrammingLanguageID)
If User can can know multiple programming languages, also have multiple level of knowledge in programming languages, then key goes
Create clustered index CLU_UserPL on UserPl (RecID) --- might be new identity column
or
Create clustered index CLU_UserPL on UserPl (UserPLID)
this can be achieved by using constraints UNIQUE.
Here is a detailed articles about UNIQUE constraint W3School UNIQUE Article
Simple words, UNIQUE is a constraint that will ensure there is no same value allowed in the selected field.
If you want to have another way to prevent Doe to insert new values in the table, you would like to use another method which is IF EXISTS
IF EXISTS (SELECT * FROM userPLs WHERE UserId = 'THE USER ID')
BEGIN
PRINT 'Data Already Exists! Insert will be ignored!'
END
ELSE
BEGIN
PRINT 'Data doesn''t exists! Proceeding to insert the data!'
//Start inserting the data
END
UPDATED ANSWER
Here is the modified SQL Query with IF EXISTS but with another condition.
IF EXISTS (SELECT * FROM userPLs WHERE UserId = 'THE USER ID' AND ProgrammingLanguageId = 'The ID')
BEGIN
PRINT 'Data Already Exists! Insert will be ignored!'
END
ELSE
BEGIN
PRINT 'Data doesn''t exists! Proceeding to insert the data!'
//Start inserting the data
END
The query above will solve your issue. If you are wondering how does it works, below is a simple explanation:
The query will check for the UserId first. Does the UserId has been registered to Database?
Next, the query will also check, does the data that will be inserted to Database (ProgrammingLanguageId) also exists in the Database for the selected user?
If the UserId is already registered and the UserId has the same ProgrammingLanguageId with the ID that will be inserted to database, it will ignore the insert and shows "Data Already Exists! Insert will be ignored"
But IF The UserId is already registered in the Database but HAS NO ProgrammingLanguageId that match with the data that will be inserted, it will start insert the data
For a better usage, I think you should create a trigger that will occur whenever an Insert is being executed.

Creating an Identifier that Combines Multiple Other Columns

I'm working on a DB and would like to implement a system where a tables unique ID is generated by combining several other IDs/factors. Basically, I'd want an ID that looks like this:
1234 (A reference to a standard incrementing serial ID from another table)
10 (A reference to a standard incrementing serial ID from another table)
1234 (A number that increments from 1000-9999)
So the ID would look like:
1234101234
Additionally, each of those "entries" will have multiple time sensitive instances that are stored in another table. For these IDs I want to take the above ID and append a time stamp, so it'll look like:
12341012341234567890123
I've looked a little bit at PSQL sequences, but they seem like they're mostly used for simply incrementing up or down at certain levels, I'm not sure how to do this sort of concatenation in creating an ID string or whether it's even possible.
Don't do it! Just use a serial primary key id and then have three different columns:
otherTableID
otherTable2ID
timestamp
You can uniquely identify each row using your serial id. You can look up the other information. And -- even better -- you can create foreign key constraints to represent the relationships among the tables.
I'm not sure what do you want to achive, but
SELECT col_1::text || col_2::text || col_3::text || now()::text
should work. You should also add UNIQUE constraint on the column, i.e.
ALTER TABLE this_table ADD UNIQUE INDEX (this_new_column);
But the real question is: why do you want to do this? If you just want a unique meaningless ID, you need just to create column of type serial.
create procedure f_return_unq_id(
CONDITIONAL_PARAMS IN INTEGER,
v_seq in out integer
)
is
QUERY_1 VARCHAR2(200);
RESP INTEGER;
BEGIN
QUERY_1:='SELECT TAB1.SL_ID||TAB2.SL_ID||:v_seq||SYSTIMESTAMP FROM TABLE1 TAB1,TABLE2 TAB2 WHERE TAB1.CONDITION=:V_PARAMS';
BEGIN
EXECUTE IMMEDIATE QUERY_1 INTO RESP USING v_seq,CONDITIONAL_PARAMS;
EXCEPTION
when others then
DBMS_OUTPUT.PUT_LINE(SQLCODE);
END;
v_seq:=RESP;
EXCEPTION
when others then
DBMS_OUTPUT.PUT_LINE(SQLCODE);
END;
pass the v_seq to this procedure as your sequence number 1000-9999 and conditional parameters if any are there.

SQL: dealing with unique values in the table when UNIQUE key constraint isn't applicable

What you do when you need to maintain a table with unique values when you can't use UNIQUE constraint?
For example, I use MySQL and want to map my urls to ids. So I create a table:
CREATE TABLE url (id INTEGER PRIMARY KEY AUTO_INCREMENT, url VARCHAR(2048));
The problem is that mysql doesn't allow unique field bigger than 1000 bytes.
How in general do insert only if not exist in sql atomically?
You could create an extra field which would be the hash of a url e.g. md5, and make that hash field unique. You can certainly be sure that the URL is unique then, and with almost 100% certainty you can insert a new URL if it isn't already there.
It is tempting to create a table lock, however creating a table lock will implicitly commit the transaction you are working on: http://www.databasesandlife.com/mysql-lock-tables-does-an-implicit-commit/
You could create a single-row table e.g. name mutex, type=InnoDB, insert a row into it, and do a select for update on that row to create a lock which is compatible with transactions. It's nasty but that's the way I do table locks in MySQL in my applications :(
You could use a not exist condition:
insert YourTable
(url)
values ('blah blah blah')
where not exists
(
select *
from YourTable
where url = 'blah blah blah'
)
In my opinion the best way to handle it is to write a trigger. The trigger is going to check each value in the table to see whether they are equal and if yes, to raise an error. However, I don't think an URL will go beyond 1000 characters but if it does in your case, you should write a trigger to handle the uniqueness.