NOT NULL constraint over a set of columns - sql

I have a table in Postgres which currently has a NOT NULL constraint on it's email column. This table also has a phone column which is optional. I would like the system to accept some records without email but only if these have phone as NOT NULL. In other words, I need a NOT NULL database constraint such that CREATE or UPDATE queries succeed without any errors if either or both of email or phone fields are present.
Extending the above further, is it possible in Postgres, to specify a set of column names, one or more of which should be NOT NULL for the record to be successfully updated or created?

#Igor is quite right and a couple of OR'ed expression are fast and simple.
For a long list of columns (a, b, c, d, e, f, g in the example), this is shorter and just as fast:
CHECK (NOT (a,b,c,d,e,f,g) IS NULL)
db<>fiddle here
Old sqlfiddle
How does it work?
A more verbose form of the above would be:
CHECK (NOT ROW(a,b,c,d,e,f,g) IS NULL)
ROW is redundant syntax here.
Testing a ROW expression with IS NULL only reports TRUE if every single column is NULL - which happens to be exactly what we want to exclude.
It's not possible to simply reverse this expression with (a,b,c,d,e,f,g) IS NOT NULL, because that would test that every single column IS NOT NULL. Instead, negate the whole expression with NOT. Voilá.
More details in the manual here and here.
An expression of the form:
CHECK (COALESCE(a,b,c,d,e,f,g) IS NOT NULL)
would achieve the same, less elegantly and with a major restriction: only works for columns of matching data type, while the check on a ROW expression works with any columns.

You can use CHECK constraint for this.
Something like:
CHECK (email is not null OR phone is not null)
Details on constraints can be found here

Related

Is there a way to make a column's nullability depend on another column's nullability?

I have two columns (among others) in a database table: ExitDate and ExitReason. Our business logic requires that ExitReason be specified if ExitDate is specified. The ExitDate column needs to allow nulls since the value is not always known at the time of insert. Is there a way to make the ExitReason column allow nulls only if the ExitDate value is null? I could accomplish the effect by splitting these two columns into a separate 'exit dates' table and making them both non-nullable, but it would be nice if I wouldn't have to.
Ideas? Thanks!
Assuming you are on SQL Server or something similar, you can do this with a CHECK constraint on your table. (Unfortunately, MySQL parses but ignores CHECK constraints, so you'd have to use a trigger for that platform.)
If the table already exists:
ALTER TABLE ADD CONSTRAINT CK_ExitDateReason
CHECK (
(ExitDate IS NULL AND ExitReason IS NULL)
OR (ExitDate IS NOT NULL AND ExitReason IS NOT NULL)
);
If you are creating the table yourself:
CREATE TABLE dbo.Exit (
...
, CONSTRAINT CK_ExitDateReason CHECK ...
);
Using a check constraint is preferable to using a trigger because:
check constraints are more visible than triggers
the constraint is part of the table definition, as opposed to code that is run separately, so it's logically cleaner
I am willing to bet it is faster than a trigger too
I could accomplish the effect by splitting these two columns into a separate 'exit dates' table and making them both non-nullable, but it would be nice if I wouldn't have to.
That sounds like a very good solution. And if you are using MySQL then it's probably the best solution since CHECK constraints aren't supported.
MS Access offers another method to accomplish your goal. With the table in Design View, open the property sheet. In contrast to a Validation Rule for a field, the table rule can reference other fields in the table.
Add this as a single line for the table's Validation Rule property.
([ExitDate] IS NULL AND [ExitReason] IS NULL)
OR ([ExitDate] IS NOT NULL AND [ExitReason] IS NOT NULL)
It's similar to the CHECK CONSTRAINT #NickChammas supplied. I put square brackets around both ExitDate and ExitReason because without the brackets Access tends to interpret them as text literal values, so adds quotes like this ... which won't work:
("ExitDate" IS NULL AND "ExitReason" IS NULL)
OR ("ExitDate" IS NOT NULL AND "ExitReason" IS NOT NULL)
You may find this method more convenient if you want to include a user-friendly message as the table's Validation Text property to display when the Validation Rule is violated:
"Provide values for both ExitDate and ExitReason, or leave both blank."
Edit: The suggestion by #AndriyM works as a MS Access table Validation Rule:
([ExitDate] Is Null) = ([ExitReason] Is Null)
It is possible to use checks with MS Access, but only through ADO.
sSQL = "ALTER TABLE customer ADD CONSTRAINT CK_ExitDateReason " _
& "CHECK ((ExitDate IS NULL) = (ExitReason IS NULL))"
CurrentProject.Connection.Execute sSQL
The constraint can only be removed via ADO. However, you are free to add and delete columns (fields) without affecting the check.
It is also possible to add a check that references another table.
If you are using the table with a form, the error returned will be 3317. You can either accept the default message or supply your own like so:
Private Sub Form_Error(DataErr As Integer, Response As Integer)
If DataErr = 3317 And IsNull(Me.ExitReason) Then
MsgBox "Please fill in a reason"
Response = acDataErrContinue
End If
End Sub
Further information: Intermediate Microsoft Jet SQL for Access 2000
You could enforce this with a trigger: if you're setting ExitDate to something other than null and ExitReason is being left null or is being set to null, then you throw an error.

Why doesn't the where query work in postgresql?

I have a ruby on rails app in which I am querying for a boolean column, Flag. The code is:
Merchant.where("Flag=?",false)
However this does not work at all and the only result is that the Merchants table does not have a column name "flag". Is there any way to fix this? The column name starts with an uppercase letter, but the search is being done for a lower case "flag"
If the column name was quoted when the table was created then you will have to quote it forever. So, if you started with this:
create table merchants (
-- ...
"Flag" boolean
-- ...
)
Then you'll have to refer to it using
Merchant.where('"Flag" = ?', false)
PostgreSQL normalizes all unquoted identifiers to lower case (not upper case as the standard says), that's why the error message complains about flag rather than Flag.
If you can, you might want to rebuild your table with only lower case column names.

How to make a view column NOT NULL

I'm trying to create a view where I want a column to be only true or false. However, it seems that no matter what I do, SQL Server (2008) believes my bit column can somehow be null.
I have a table called "Product" with the column "Status" which is INT, NULL. In a view, I want to return a row for each row in Product, with a BIT column set to true if the Product.Status column is equal to 3, otherwise the bit field should be false.
Example SQL
SELECT CAST( CASE ISNULL(Status, 0)
WHEN 3 THEN 1
ELSE 0
END AS bit) AS HasStatus
FROM dbo.Product
If I save this query as a view and look at the columns in Object Explorer, the column HasStatus is set to BIT, NULL. But it should never be NULL. Is there some magic SQL trick I can use to force this column to be NOT NULL.
Notice that, if I remove the CAST() around the CASE, the column is correctly set as NOT NULL, but then the column's type is set to INT, which is not what I want. I want it to be BIT. :-)
You can achieve what you want by re-arranging your query a bit. The trick is that the ISNULL has to be on the outside before SQL Server will understand that the resulting value can never be NULL.
SELECT ISNULL(CAST(
CASE Status
WHEN 3 THEN 1
ELSE 0
END AS bit), 0) AS HasStatus
FROM dbo.Product
One reason I actually find this useful is when using an ORM and you do not want the resulting value mapped to a nullable type. It can make things easier all around if your application sees the value as never possibly being null. Then you don't have to write code to handle null exceptions, etc.
FYI, for people running into this message, adding the ISNULL() around the outside of the cast/convert can mess up the optimizer on your view.
We had 2 tables using the same value as an index key but with types of different numerical precision (bad, I know) and our view was joining on them to produce the final result. But our middleware code was looking for a specific data type, and the view had a CONVERT() around the column returned
I noticed, as the OP did, that the column descriptors of the view result defined it as nullable and I was thinking It's a primary/foreign key on 2 tables; why would we want the result defined as nullable?
I found this post, threw ISNULL() around the column and voila - not nullable anymore.
Problem was the performance of the view went straight down the toilet when a query filtered on that column.
For some reason, an explicit CONVERT() on the view's result column didn't screw up the optimizer (it was going to have to do that anyway because of the different precisions) but adding a redundant ISNULL() wrapper did, in a big way.
All you can do in a Select statement is control the data that the database engine sends to you as a client. The select statement has no effect on the structure of the underlying table. To modify the table structure you need to execute an Alter Table statement.
First make sure that there are currently no nulls in that bit field in the table
Then execute the following ddl statement:
Alter Table dbo.Product Alter column status bit not null
If, otoh, all you are trying to do is control the output of the view, then what you are doing is sufficient. Your syntax will guarantee that the output of the HasStatus column in the views resultset will in fact never be null. It will always be either bit value = 1 or bit value = 0. Don't worry what the object explorer says...

Duplicate value in a postgresql table

I'm trying to modify a table inside my PostgreSQL database, but it says there is duplicate! what is the best way to find a duplicate value inside a table? kinda a select query?
Try Like This
SELECT count(column_name), column_name
from table_name
group by column_name having count(column_name) > 1;
If you try to change a value in a column that is part of the PRIMARY KEY or has a UNIQUE constraint and get this error there, then you should be able to find the conflicting row by
SELECT *
FROM your_table
WHERE conflicting_column = conflicting_value;
If conflicting_value is a character type, put it in single quotes (').
EDIT: To find out which columns are affected by the constraint, check this post.
First of all, determine which fields in your table have to be unique. This may be something marked as a Primary Key, a unique index based on one or more fields or a check constraint, again based on one or more fields.
Once you've done that, look at what you're trying to insert and work out whether it busts any of the unique rules.
And yes, SELECT statements will help you determine what's wrong here. Use those to determine whether you are able to commit the row.

MySQL - Set default value for field as a string concatenation function

I have a table that looks a bit like this actors(forename, surname, stage_name);
I want to update stage_name to have a default value of
forename." ".surname
So that
insert into actors(forename, surname) values ('Stack', 'Overflow');
would produce the record
'Stack' 'Overflow' 'Stack Overflow'
Is this possible?
Thanks :)
MySQL does not support computed columns or expressions in the DEFAULT option of a column definition.
You can do this in a trigger (MySQL 5.0 or greater required):
CREATE TRIGGER format_stage_name
BEFORE INSERT ON actors
FOR EACH ROW
BEGIN
SET NEW.stage_name = CONCAT(NEW.forename, ' ', NEW.surname);
END
You may also want to create a similar trigger BEFORE UPDATE.
Watch out for NULL in forename and surname, because concat of a NULL with any other string produces a NULL. Use COALESCE() on each column or on the concatenated string as appropriate.
edit: The following example sets stage_name only if it's NULL. Otherwise you can specify the stage_name in your INSERT statement, and it'll be preserved.
CREATE TRIGGER format_stage_name
BEFORE INSERT ON actors
FOR EACH ROW
BEGIN
IF (NEW.stage_name IS NULL) THEN
SET NEW.stage_name = CONCAT(NEW.forename, ' ', NEW.surname);
END IF;
END
According to 10.1.4. Data Type Default Values no, you can't do that. You can only use a constant or CURRENT_TIMESTAMP.
OTOH if you're pretty up-to-date, you could probably use a trigger to accomplish the same thing.
My first thought is if you have the two values in other fields what is the compelling need for redundantly storing them in a third field? It flies in the face of normalization and efficiency.
If you simply want to store the concatenated value then you can simply create a view (or IMSNHO even better a stored procedure) that concatenates the values into a pseudo actor field and perform your reads from the view/sproc instead of the table directly.
If you absolutely must store the concatenated value you could handle this in two ways:
1) Use a stored procedure to do your inserts instead of straight SQL. This way you can receive the values and construct a value for the field you wish to populate then build the insert statement including a concatenated value for the actors field.
2) So I don't draw too many flames, treat this suggestion with kid gloves. Use only as a last resort. You could hack this behavior by adding a trigger to build the value if it is left null. Generally, triggers are not good. They add unseen cost and interactions to fairly simple interactions. You can, though, use the CREATE TRIGGER to update the actors field after a record is inserted or updated. Here is the reference page.
As of MySQL 8.0.13, you can use DEFAULT clause for a column which can be a literal constant or an expression.
If you want to use an expression then, simply enclose the required expression within parentheses.
(concat(forename," ",surname))
There are two ways to accomplish what you are trying to do as per my knowledge:
(important: consider backing up your table first before running below queries)
1- Drop the column "stage_name" all together and create a new one with DEFAULT constraint.
ALTER TABLE actors ADD COLUMN stage_name VARCHAR(20) DEFAULT (concat(forename," ",surname))
2- This will update newer entries in the column "stage_name" but not the old ones.
ALTER TABLE actors alter stage_name set DEFAULT (concat(forename," ",surname));
After that, if you need to update the previous values in the column "stage_name" then simply run:
UPDATE actors SET stage_name=(concat(forename," ",surname));
I believe this should solve your problem.