Consider the following table, which specifies fruit that is forbidden on given days of the week. DayOfWeek is nullable, where a NULL signifies that this type of fruit is forbidden on all days of the week.
Fruit DayOfWeek
----------------------
Kiwi NULL
Apples Monday
Strawberries Monday
Oranges Tuesday
Bananas Wednesday
Pineapple Thursday
Is it possible to implement a constraint on this table that prevents me from inserting the values (Kiwi, Monday), since Kiwis are already banned on Mondays (and every other day) by the existing (Kiwi, NULL) row.
Preferably this should be implemented without the use of triggers.
Unless you have a really good justification you shouldn't change the meaning of NULL. Null reflects a value is unknown so I would read this as We don't know what day of the week kiwi's are banned. You would then change your logic to store a record for each day of the week that Kiwi is banned on.
What if you need to write a query which says give me all forbidden fruit for monday. You need to write your query as
select * from BadFruit where DayOfWeek='Monday' || DayOfWeek is null
A more efficent and easier to understand query would eliminate the or clause.
I agree with others that I probably would aim for a different model than the one you've shown, but if you're sold on it, then the following seems to do what you want:
create table dbo.Weekdays (
DayOfWeek varchar(10) not null,
constraint PK_Weekdays PRIMARY KEY (DayOfWeek)
)
go
create table dbo.Exclusions (
Fruit varchar(20) not null,
DayOfWeek varchar(10) null,
/* PK? */
constraint FK_Exclusions FOREIGN KEY (DayOfWeek) references Weekdays (DayOfWeek)
)
Not sure what the PK is for the Exclusions table should be, not obvious from what you've shown, but it's intended to be your table. We need to introduce the Weekdays table to make the later view work*. Now populate it:
insert into dbo.Weekdays (DayOfWeek)
select 'Monday' union all
select 'Tuesday' union all
select 'Wednesday' union all
select 'Thursday' union all
select 'Friday' union all
select 'Saturday' union all
select 'Sunday'
And your sample data:
insert into dbo.Exclusions (Fruit,DayOfWeek)
select 'Kiwi',NULL union all
select 'Apples','Monday' union all
select 'Strawberries','Monday' union all
select 'Oranges','Tuesday' union all
select 'Bananas','Wednesday' union all
select 'Pineapple','Thursday'
Now we create the view to implement your constraint:
create view dbo.Exclusions_NullExpanded
with schemabinding
as
select
e.Fruit,
wd.DayOfWeek
from
dbo.Exclusions e
inner join
dbo.Weekdays wd
on
e.DayOfWeek = wd.DayOfWeek or
e.DayOfWeek is null
go
create unique clustered index IX_Exclusions_NoDups on dbo.Exclusions_NullExpanded (Fruit,DayOfWeek)
And, if we try to insert the row you don't want us to:
insert into dbo.Exclusions (Fruit,DayOfWeek)
select 'Kiwi','Monday'
We get:
Msg 2601, Level 14, State 1, Line 1
Cannot insert duplicate key row in object 'dbo.Exclusions_NullExpanded' with unique index 'IX_Exclusions_NoDups'.
The statement has been terminated.
*I initially tried to do this without introducing the Weekdays table, and have it appear inline in the view definition, as a subselect of literal rows. But you can't create the index that enforces the constraint we want on such a view.
I am personally not fond of the idea of NULL mean all, but changing this to include a row for each day when NULL is probably out of scope.
If a trigger is not an option, I would look at a CHECK CONSTRAINT and setting up a function that tests the condition you are trying to avoid.
Try this link http://www.java2s.com/Code/SQL/Select-Clause/SettingaUniqueConstraint.htm
For MySQL you can set unique contraints.
Related
Consider the following:
CREATE TABLE mytable
(
[id] INT NOT NULL,
[foobar] VARCHAR(25) NULL,
[created_on] DATETIME NOT NULL
);
SELECT *
FROM mytable
WHERE CAST(created_on AS DATE) = '2019-01-01';
I have a lot of queries like this, where I need to store the full date and time for audit (and sorting) purposes, but most queries only care about the date portion when it comes to searching.
In order to improve performance, I was considering adding a sister column that stores the value as a DATE, and then update it via triggers; but before I go down that rabbit hole, I wanted to know if there is a better way to solve this issue. Is there some mechanism in SQL Server that offers a better solution to this issue?
I am currently stuck on SQL Server 2008, but I am open to solutions that use newer versions
My preference would be to just write a sargable
WHERE created_on >= '2019-01-01' and created_on < '2019-01-02';
The
CAST(created_on AS DATE) = '2019-01-01';
Is in fact mostly sargable but somewhat sub optimal ...
... and splitting it out into a separate indexed column can help other cases like GROUP BY date
If you decide you do need a separate column you can create a computed column and index that.
This is preferable to triggers as it has less performance overhead as well as allowing SQL Server to match both the column name and the original expression. (any index on a column populated by a trigger won't be matched to a query containing CAST(created_on AS DATE))
CREATE TABLE mytable
(
[id] INT NOT NULL,
[foobar] VARCHAR(25) NULL,
[created_on] DATETIME NOT NULL,
[created_on_date] AS CAST(created_on AS DATE)
);
CREATE INDEX ix_created_on_date
ON mytable(created_on_date)
include (foobar, id, created_on)
SELECT foobar,
id,
created_on
FROM mytable
WHERE CAST(created_on AS DATE) = '2019-01-01';
Thanks for looking. I'm trying to write a SQL Server trigger that when a new record is added containing date information, will add the day of the week to the DayOfWeek column. Here's my table, with the columns in order:
Food table:
FoodName **varchar(20)**
CategoryID (FK) **int**
Price **smallmoney**
StoreID (FK) **int**
Date **datetime**
DayOfWeek **varchar(9)**
ShopperID (FK) **int**
Week **int**
Here is the trigger I've written:
-- Create a trigger to update day of the week when a record is inserted
CREATE TRIGGER DOW
ON Food
FOR INSERT
AS
BEGIN
-- Declare a variable to hold the date ID
DECLARE #dateID DATETIME
-- Get the date from the new record and store it in #dateID
SELECT #dateID = Date FROM Food
-- Insert day of the week based on the inserted date
INSERT INTO Food (DayOfWeek)
SELECT DATENAME(dw, #dateID)
END
GO
SQL Server seemed to accept the procedure, but when I ran another procedure to insert a new record, I got this error:
Msg 515, Level 16, State 2, Procedure DOW, Line 8 [Batch Start Line 21]
Cannot insert the value NULL into column 'Week', table *******; column does not allow nulls. INSERT fails.
I am not sure why this trigger is affecting the 'Week' column at all. The code should take the value entered for the Date and use the DATENAME(dw,...) function to return the day of the week, which should go into the DayOfWeek column. I've written a stored procedure that accepts a date as input and inserts the corresponding day of the week into the record, and it works just fine, but this trigger doesn't seem to want to cooperate. I'm stumped!
What your trigger does:
it fetches a Date from your table (the last one that is returned) which is not necessarily the last inserted value.
it tries to insert a new record with just the DayOfWeek of that Date specified.
it fails, because at least the Week must also be specified.
I guess that you want to update the value of the DayOfWeek for the inserted row(s) instead. To be able to do so, there must be a way to identify the row(s) that need to be updated in the Food table by knowing the values of the inserted rows. To be sure to update the correct rows, there should be a primary key that allows you to identify them. For sure you have such a primary key, and I guess that it's named FoodID, so probably you wanted to do this:
CREATE TRIGGER DOW ON Food
FOR INSERT
AS
BEGIN
SET NOCOUNT ON;
-- update the day of the week for the inserted rows
UPDATE Food
SET [DayOfWeek] = DATENAME(dw, f.[Date])
FROM Food f
INNER JOIN inserted i ON f.FoodID = i.FoodID
END
GO
There are some major problems with your trigger. In triggers, there is an inserted table (on inserts and updates) and deleted table (on deletes and updates). You should be using this table's information to know what records need updated.
This is bad because a trigger can have multiple rows
This SQL simply will not work correctly if you insert multiple rows.
DECLARE #dateID DATETIME
SELECT #dateID = Date FROM Food
This SQL is trying to insert a new row which is causing your NULL error
It is not trying to update the row you are inserting
INSERT INTO Food (DayOfWeek)
SELECT DATENAME(dw, #dateID)
It would need to be an INSTEAD OF trigger to avoid the null constraint on the column. Wolfgang's answer will still cause a null constraint error, because after triggers run AFTER the data is inserted. An INSTEAD OF trigger will run in place of the the actual insert.
CREATE TRIGGER DOW ON Food
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
-- update the day of the week for the inserted rows
INSERT INTO Food (FoodName,CategoryID,Price,StoreID,[Date],ShopperID,[Week],[DayOfWeek])
SELECT
FoodName,CategoryID,Price,StoreID,[Date],ShopperID,[Week],DATENAME(dw, [Date]) AS [DayOfWeek]
FROM inserted
END
GO
Personally, I think storing the week and day of week is a bad idea. You already have a value that can derive that information (Date). Any time you have multiple columns that are essentially duplicate data, you will run into maintenance pain.
I have table with time entries with following columns:
Id (PK)
Date
EmployeeId
State (state of the entry New, Approved, etc.)
Quantity
And I would like to create an indexed view which groups time entries by day and employee. So I used:
CREATE VIEW dbo.Test1
WITH SCHEMABINDING
AS
SELECT
Date, EmployeeId, SUM(Quantity), SUM(CASE State = 1 THEN Quantity ELSE NULL END) AS QuantityApproved
FROM
TimeEntries
GROUP BY
EmployeeId, Date
CREATE UNIQUE CLUSTERED INDEX IDX_V1
ON dbo.Test1 (EmployeeId, Date);
GO
But when I try to make it an indexed view an error occurs:
Cannot create the clustered index "IDX_V1" on view "dbo.Test1" because the view references an unknown value (SUM aggregate of nullable expression). Consider referencing only non-nullable values in SUM. ISNULL() may be useful for this.
Obviously using ISNULL would help in case of QuantityApproved column. But this is not a solution for me as 0 may also indicate there are 2 records (Quantity=-1 and QUantity=1) on the same day.
Also I can use an auxiliary column for ABS value for this case, but having NULL there is very convenvient as I do not need to solve anything else.
Is there any other way to overcome this?
I am trying to constrain a SQL Server Database by a Start Date and End Date such that I can never double book a resource (i.e. no overlapping or duplicate reservations).
Assume my resources are numbered such that the table looks like
ResourceId, StartDate, EndDate, Status
So lets say I have resource #1. I want to make sure that I cannot have have the a reservation for 1/8/2017 thru 1/16/2017 and a separate reservation for 1/10/2017 - 1/18/2017 for the same resource.
A couple of more complications, a StartDate for a resource can be the same as the EndDate for the resource. So 1/8/1027 thru 1/16/2017 and 1/16/2017 thru 1/20/2017 is ok (i.e., one person can check in on the same day another person checkouts).
Furthermore, the Status field indicates whether the booking of the resource is Active or Cancelled. So we can ignore all cancelled reservations.
We have protected against these overlapping or double booking reservations in Code (Stored Procs and C#) when saving but we are hoping to add an extra layer of protection by adding a DB Contraint.
Is this possible in SQL Server ?
Thanks in Advance
You can use a CHECK constraint to make sure startdate is on or before EndDate easily enough:
CONSTRAINT [CK_Tablename_ValidDates] CHECK ([EndDate] >= [StartDate])
A constraint won't help with preventing an overlapping date range. You can instead use a TRIGGER to enforce this by creating a FOR INSERT, UPDATE trigger that rolls back the transaction if it detects a duplicate:
CREATE TRIGGER [TR_Tablename_NoOverlappingDates] FOR INSERT, UPDATE AS
IF EXISTS(SELECT * from inserted INNER JOIN [MyTable] ON blah blah blah ...) BEGIN
ROLLBACK TRANSACTION;
RAISERROR('hey, no overlapping date ranges here, buddy', 16, 1);
RETURN;
END
Another option is to create a indexed view that finds duplicates and put a unique constraint on that view that will be violated if more than 1 record exists. This is usually accomplished with a dummy table that has 2 rows cartesian joined to an aggregate view that selects the duplicate id-- thus one record with a duplicate would return two rows in the view with the same fake id value that has a unique index.
I've done both, I like the trigger approach better.
Drawing from this answer here: Date range overlapping check constraint.
First, check to make sure there are not existing overlaps:
select *
from dbo.Reservation as r
where exists (
select 1
from dbo.Reservation i
where i.PersonId = r.PersonId
and i.ReservationId != r.ReservationId
and isnull(i.EndDate,'20990101') > r.StartDate
and isnull(r.EndDate,'20990101') > i.StartDate
);
go
If it is all clear, then create your function.
There are a couple of different ways to write the function, e.g. we could skip the StartDate and EndDate and use something based only on ReservationId like the query above, but I will use this as the example:
create function dbo.udf_chk_Overlapping_StartDate_EndDate (
#ResourceId int
, #StartDate date
, #EndDate date
) returns bit as
begin;
declare #r bit = 1;
if not exists (
select 1
from dbo.Reservation as r
where r.ResourceId = #ResourceId
and isnull(#EndDate ,'20991231') > r.StartDate
and isnull(r.EndDate,'20991231') > #StartDate
and r.[Status] = 'Active'
group by r.ResourceId
having count(*)>1
)
set #r = 0;
return #r;
end;
go
Then add your constraint:
alter table dbo.Reservation
add constraint chk_Overlapping_StartDate_EndDate
check (dbo.udf_chk_Overlapping_StartDate_EndDate(ResourceId,StartDate,EndDate)=0);
go
Last: Test it.
I am trying to get all the data from all tables in one DB.
I have looked around, but i haven't been able to find any solution that works with my current problems.
I made a C# program that creates a table for each day the program runs. The table name will be like this tbl18_12_2015 for today's date (Danish date format).
Now in order to make a yearly report i would love if i can get ALL the data from all the tables in the DB that stores these reports. I have no way of knowing how many tables there will be or what they are called, other than the format (tblDD-MM-YYYY).
in thinking something like this(that obviously doesen't work)
SELECT * FROM DB_NAME.*
All the tables have the same columns, and one of them is a primary key, that auto increments.
Here is a table named tbl17_12_2015
ID PERSONID NAME PAYMENT TYPE RESULT TYPE
3 92545 TOM 20,5 A NULL NULL
4 92545 TOM 20,5 A NULL NULL
6 117681 LISA NULL NULL 207 R
Here is a table named tbl18_12_2015
ID PERSONID NAME PAYMENT TYPE RESULT TYPE
3 117681 LISA 30 A NULL NULL
4 53694 DAVID 78 A NULL NULL
6 58461 MICHELLE NULL NULL 207 R
What i would like to get is something like this(from all tables in the DB):
PERSONID NAME PAYMENT TYPE RESULT TYPE
92545 TOM 20,5 A NULL NULL
92545 TOM 20,5 A NULL NULL
117681 LISA NULL NULL 207 R
117681 LISA 30 A NULL NULL
53694 DAVID 78 A NULL NULL
58461 MICHELLE NULL NULL 207 R
Have tried some different query's but none of them returned this, just a lot of info about the tables.
Thanks in advance, and happy holidays
edit: corrected tbl18_12_2015 col 3 header to english rather than danish
Thanks to all those who tried to help me solving this question, but i can't (due to my skill set most likely) get the UNION to work, so that's why i decided to refactor my DB.
While you could store the table names in a database and use dynamic sql to union them together, this is NOT a good idea and you shouldn't even consider it - STOP NOW!!!!!
What you need to do is create a new table with the same fields - and add an ID (auto-incrementing identity column) and a DateTime field. Then, instead of creating a new table for each day, just write your data to this table with the DateTime. Then, you can use the DateTime field to filter your results, whether you want something from a day, week, month, year, decade, etc. - and you don't need dynamic sql - and you don't have 10,000 database tables.
I know some people posted comments expressing the same sentiments, but, really, this should be an answer.
If you had all the tables in the same database you would be able to use the UNION Operator to combine all your tables..
Maybe you can do something like this to select all the tables names from a given database
For SQL Server:
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_CATALOG='dbName'
For MySQL:
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA='dbName'
Once you have the list of tables you can move all the tables to 1 database and create your report using Unions..
You will need to use a UNION between each select query.
Do not use *, always list the name of the columns you are bringing up.
If you want duplicates, then UNION ALL is what you want.
If you want unique records based on the PERSONID, but there is likely to be differences, then I will guess that an UPDATE_DATE column will be useful to determine which one to use but what if each records with the same PERSONID lived a life of its own on each side?
You'd need to determine business rules to find out which specific changes to keep and merge into the unique resulting record and you'd be on your own.
What is "Skyttenavn"? Is it Danish? If it is the same as "NAME", you'd want to alias that column as 'NAME' in the select query, although it's the order of the columns as listed that counts when determining what to unite.
You'd need a new auto-incremented ID as a unique primary key, by the way, if you are likely to have conflicting IDs. If you want to merge them together into a new primary key identity column, you'd want to set IDENTITY_INSERT to OFF then back to ON if you want to restart natural incrementation.