SQL Server Insert Trigger Improvement - sql

NEED:
Get data out of "comments-type" text/memo field and put it into separate fields upon record insert. Following example uses field TimeStamp for simplicity but uses Update AFTER record is inserted (inefficient) instead of when record is inserted. Need to do this without update.
SOLUTION:
Having never used SQL triggers before, after much wailing and gnashing of teeth, finally came up with something this. It works -- but very inefficiently. Is there a better way?
EXAMPLE:
Imagine a table (Castings) with TimeStamp field formatted: ”2017-12-10 18:44:54”. As records are inserted, fields automatically get populated via a trigger using substring on TimeStamp field. In this case YYYY = “2017”, MM = “12”, DD = “10”, HH = “18”, MN = “44”, SS = “54”. Using a trigger called SQLBuddy.
SCHEMA:
ID bigint (Identify Specification YES auto-increment)
TimeStamp char(19)
YYYY char(4)
MM char(2)
DD char(2)
HH char(2)
MN char(2)
SS char(2)
SQL TRIGGER CODE:
USE [SERT]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[SQLBuddy]
ON [dbo].[Castings]
AFTER INSERT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
UPDATE Castings
SET YYYY = SUBSTRING(TimeStamp,1,4), MM = SUBSTRING(TimeStamp,6,2), DD = SUBSTRING(TimeStamp,9,2), HH = SUBSTRING(TimeStamp,12,2), MN = SUBSTRING(TimeStamp,15,2), SS = SUBSTRING(TimeStamp,18,2);
SELECT TOP 1 ID FROM Castings ORDER BY ID DESC
END

Your best option is to avoid a trigger altogether and use computed columns on your table. Like this:
CREATE TABLE YourTable
(ID bigint IDENTITY (1,1),
YourDateTime DATETIME,
dYear as DATEPART(YEAR, YourDateTime),
dMonth as DATEPART(MONTH, YourDateTime),
dDay as DATEPART(DAY, YourDateTime),
dHour as DATEPART(HOUR, YourDateTime),
dMinute as DATEPART(MINUTE, YourDateTime),
dSecond as DATEPART(SECOND, YourDateTime)
);
Click here for SQL Fiddle Example

Why are you breaking the timestamp into individual components?
Even worse, why are you storing those components as characters?
To illustrate, what is the order of the following month numbers based on your schema?
'1'
'2'
'10'
'11'
And the answer is:
January
October
November
February
Using integers for the separate components will ensure proper sort order, but you'll still run into a lot of fiddly logic.
Assuming you have
YYYY=2017
MM=12
DD=31
What happens if you add one day?
If your timestamp is coming in as a string, convert it to a datetime data type. You will save so many head-aches later on by having useful, valid timestamps. In addition, you'll have no need for the trigger to separate the pieces. Just insert the whole timestamp into a single field.

Related

VARCHAR as 09:20:00 Jan 30, 2013 PST. Covert Varchar to date in SQL

How to convert varchar datetime value returned from paypal to Datetime in C#..
Below is the query I want to get the data which was not supported because of varchar datatype..
select *
from table1
where Month(date) = '05'
and Year(date) = '2021'
Here the date column's datatype is varchar as sample value (09:20:00 Jan 30, 2013 PST).
Can anyone help me to convert this varchar to datetime or date in SQL Server?
As I mentioned the problem is your design here. You are storing date and time values as a varchar; a grave mistake. Fix your design fix the problem. Assuming that all your values are in the format hh:mm:ss MMM dd, yyyy tz, and the time zone is significant, you could do something like this:
ALTER TABLE dbo.YourTable ADD NewDate datetimeoffset(0);
GO
CREATE TABLE #TimeZones (Timezone varchar(4),
Offset varchar(6));
--You need to create the rest of the timezones, these are examples
INSERT INTO #TimeZones
VALUES('PST','-08:00'),
('PDT','-07:00'),
('GMT','+00:00'),
('BST','+01:00'),
('CET','+01:00'),
('CEST','+02:00');
GO
UPDATE YT
SET NewDate = TRY_CONVERT(datetimeoffset(0),CONCAT(S.FormattedDateString,TZ.Offset))
FROM dbo.YourTable YT
CROSS APPLY (VALUES(REPLACE(SUBSTRING(YT.[date],10,13) + LEFT(YT.[date],9),',',''),(RIGHT(YT.[date],CHARINDEX(' ',REVERSE(YT.[date]))-1))))S(FormattedDateString,TimeZone)
JOIN #TimeZones TZ ON S.TimeZone = TZ.TimeZone;
GO
DROP TABLE #Timezones;
GO
EXEC sys.sp_rename N'dbo.YourTable.Date',N'OldStringDate','COLUMN';
GO
EXEC sys.sp_rename N'dbo.YourTable.NewDate',N'Date','COLUMN';
GO
The original values will then be in OldStringDate so you can get a list of values that didn't convert with the following:
SELECT OldStringDate
FROM dbo.YourTable
WHERE OldStringDate IS NOT NULL
AND [Date] IS NULL;
Then you can trivially get the data from your table with the following:
SELECT *
FROM dbo.YourTable
WHERE [Date] >= '2021-05-01T00:00:00-08:00'
AND [Date] < '2021-06-01T00:00:00-08:00';

How to return date format (f.e. mm/dd/yyyy) from a date (f.e. 06/30/2019)?

I'm working on data validation in the SQL Server Database.
I need to check if dates in the Table are populated in a valid format for specified Country.
Is there any function in SQL that will return a date format from a date?
For example: Date: 06/30/2019 What I need to get: mm/dd/yyyy
Thanks!
As we've all mentioned in the comments, storing a date as a varchar in the database is the wrong idea. We also now know that all dates need to be the last day of the month (from the comments), so now we do have something to work with. Therefore, you could fix your table with something like the below:
--Create a new column to store old values
ALTER TABLE YourTable ADD DateString varchar(10);
--Update the new column and change the existing column to ISO (yyyyMMdd) format
UPDATE YourTable
SET DateSting = DateColumn,
DateColumn = TRY_CONVERT(varchar(10), TRY_CONVERT(date, DateColumn, 101), 112);
--Change the data type of the datecolumn
ALTER TABLE YourTable ALTER COLUMN DateColumn date;
--Inspect "lost" data:
SELECT DateString
FROM YourTable
WHERE DateColumn IS NULL
AND DateString IS NOT NULL;
GO
--Add the CHECK CONSTRAINT
ALTER TABLE YourTable
ADD CONSTRAINT Date_EOMonth CHECK (DateColumn = EOMONTH(DateColumn));
If the CHECK CONSTRAINT fails to be created you have dates that aren't at the end of the month, so you can use the below statement to inspect them, or the statement after to update them all to the end of the month:
SELECT DateColumn
FROM YourTable
WHERE DateColumn != EOMONTH(DateColumn);
--Blanket Update
UPDATE YourTable
SET DateColumn = EOMONTH(DateColumn);

Customized Primary Key on SQL Server 2008 R2

I have several days trying to solve this problem, but my lack of knowledge is stopping me, I don’t know if is possible what I am trying to accomplish.
I need to have a table like this:
The first field should be a custom primary key ID (auto incremented):
YYYYMMDD-99
Where YYYMMDD is the actual day and “99” is a counter that should be incremented automatically from 01 to 99 in every new row added and need to be automatically restarted to 01 the next day.
The second field is a regular NVARCHAR(40) text field called: Name
For example, I add three rows, just introducing the “Name” of the person, the ID is automatically added:
ID Name
---------------------------
20160629-01 John
20160629-02 Katie
20160629-03 Mark
Then, the next day I add two new rows:
ID Name
-------------------------
20160630-01 Bob
20160630-02 Dave
The last two digits should be restarted, after the day changes.
And, what is all this about ?
Answer: Customer requirement.
If is possible to do it in a stored procedure, it will works for me too.
Thanks in advance!!
This is pretty easy to achieve, but a bit complicated to do so it is safe with multiple clients.
What you need is a new table (for example named IndexHelper) that actually stores the parts of the index as it should be using two columns: One has the current date properly formatted as you want it in your index and one is the current index as integer. Example:
DateString CurrentIndex
-------------------------------
20160629 13
Now you need some code that helps you get the next index value atomically, i.e. in a way that also works when more than one client try to insert at the same time without getting the same index more than once.
T-SQL comes to the rescue with its UPDATE ... OUTPUT clause, which allows you to update a table, at the same time outputting the new values as an atomic operation, which can not be interrupted.
In your case, this statement could look like this:
DECLARE #curDay NVARCHAR(10)
DELCARE #curIndex INT
DECLARE #tempTable TABLE (theDay NVARCHAR(10), theIndex INT)
UPDATE IndexHelper SET CurrentIndex = CurrentIndex + 1 OUTPUT INSERTED.DateString, INSERTED.CurrentIndex INTO #temptable WHERE CurrentDate = <code that converts CURRENT_TIMESTAMP into the string format you want>
SELECT #curDay = theDay, #curIndex = theIndex FROM #tempTable
Unfortunately you have to go the temporary table way, as it is demanded by the OUTPUT clause.
This increments the CurrentIndex field in IndexHelper atomically for the current date. You can combine both into a value like this:
DECLARE #newIndexValue NVARCHAR(15)
SET #newIndexValue = #curDay + '-' + RIGHT('00' + CONVERT(NVARCHAR, #curIndex), 2)
Now the question is: How do you handle the "go back to 01 for the next day" requirement? Also easy: Add entries into IndexHelper for 2 days in advance with the respective date and index 0. You can do this safely everytime your code is called if you check that an entry for a day is actually missing. So for today your table might look like this:
DateString CurrentIndex
-------------------------------
20160629 13
20160630 0
20160701 0
The first call tomorrow would make this look like:
DateString CurrentIndex
-------------------------------
20160629 13
20160630 1
20160701 0
20160702 0
Wrap this up into a stored procedure that does the entire INSERT process into your original table, what you get is:
Add missing entries for the next two days to IndexHelper table.
Get the next ID atomically as described above
Combine date string and ID from the UPDATE command into a single string
Use this in the INSERT command for your actual data
This results in the following stored procedure you can use to insert your data:
-- This is our "work date"
DECLARE #now DATETIME = CURRENT_DATETIME
-- These are the date strings that we need
DECLARE #today NVARCHAR(10) = CONVERT(NVARCHAR, #now, 112)
DECLARE #tomorrow NVARCHAR(10) = CONVERT(NVARCHAR, DATEADD(dd, 1, #now), 112)
DECLARE #datomorrow NVARCHAR(10) = CONVERT(NVARCHAR, DATEADD(dd, 2, #now), 112)
-- We will need these later
DECLARE #curDay NVARCHAR(10)
DELCARE #curIndex INT
DECLARE #tempTable TABLE (theDay NVARCHAR(10), theIndex INT)
DECLARE #newIndexValue NVARCHAR(15)
-- Add entries for next two days into table
-- NOTE: THIS IS NOT ATOMIC! SUPPOSED YOU HAVE A PK ON DATESTRING, THIS
-- MAY EVEN FAIL! THAT'S WHY IS USE BEGIN TRY
BEGIN TRY
IF NOT EXISTS (SELECT 1 FROM IndexHelper WHERE DateString = #tomorrow)
INSERT INTO IndexHelper (#tomorrow, 0)
END TRY
BEGIN CATCH
PRINT 'hmpf'
END CATCH
BEGIN TRY
IF NOT EXISTS (SELECT 1 FROM IndexHelper WHERE DateString = #datomorrow)
INSERT INTO IndexHelper (#datomorrow, 0)
END TRY
BEGIN CATCH
PRINT 'hmpf again'
END CATCH
-- Now perform the atomic update
UPDATE IndexHelper
SET
CurrentIndex = CurrentIndex + 1
OUTPUT
INSERTED.DateString,
INSERTED.CurrentIndex
INTO #temptable
WHERE CurrentDate = #today
-- Get the values after the update
SELECT #curDay = theDay, #curIndex = theIndex FROM #tempTable
-- Combine these into the new index value
SET #newIndexValue = #curDay + '-' + RIGHT('00' + CONVERT(NVARCHAR, #curIndex), 2)
-- PERFORM THE INSERT HERE!!
...
One way to achieve customised auto increment is using INSTEAD OF trigger in SQL Server.
https://msdn.microsoft.com/en-IN/library/ms189799.aspx
I have tested this using below code.
This might be helpful.
It is written with the assumption that maximum 99 records will be inserted in a given day.
You will have to modify it to handle more than 99 records.
CREATE TABLE dbo.CustomerTb(
ID VARCHAR(50),
Name VARCHAR(50)
)
GO
CREATE TRIGGER dbo.InsertCustomerTrigger ON dbo.CustomerTb INSTEAD OF INSERT
AS
BEGIN
DECLARE #MaxID SMALLINT=0;
SELECT #MaxID=ISNULL(MAX(RIGHT(ID,2)),0)
FROM dbo.CustomerTb
WHERE LEFT(ID,8)=FORMAT(GETDATE(),'yyyyMMdd');
INSERT INTO dbo.CustomerTb(
ID,
Name
)
SELECT FORMAT(GETDATE(),'yyyyMMdd')+'-'+RIGHT('00'+CONVERT(VARCHAR(5),ROW_NUMBER() OVER(ORDER BY Name)+#MaxID),2),
Name
FROM inserted;
END
GO
TEST CASE 1
INSERT INTO dbo.CustomerTb(NAME) VALUES('A'),('B');
SELECT * FROM dbo.CustomerTb;
TEST CASE 2
INSERT INTO dbo.CustomerTb(NAME) VALUES('P'),('Q');
SELECT * FROM dbo.CustomerTb;

'Summing' a date field in SQL - any ideas?

I'm creating an application that is essentially an integrity check between two databases - one is MSSQL and one is an old provider Btrieve. As part of the requirements all columns for every table need to be compared to ensure the data matches. Currently we loop through each table, get the basic count of the table in both DBs, and then delve into the columns. For numeric fields we do a simple SUM, and for text fields we sum up the length of the column for every row. If these match in both DBs, it's a good indicator the data has migrated across correctly.
This all works fine, but I need to develop something similar for datetime fields. Obviously we can't really SUM these fields, so I'm wondering if anyone has ideas on the best way to approach this. I was thinking maybe the seconds since a certain date but the number will be huge.
Any other ideas? Thanks!
The most straightforward answer to me would be to convert the date or datetime fields to integers with the same format. YYYYMMDD or YYYYMMDDHHmmss work just fine as long as your formats use leading zeroes. In SQL Server, you can do something like:
SELECT SUM(CAST(REPLACE(REPLACE(REPLACE(CONVERT(VARCHAR(20),DateTimeColumn,120),' ',''),':',''),'-','') AS BIGINT)) .....
Alternately, you can convert them to either the number of days from a given date ('1970-01-01'), or the number of seconds from a given date ('1970-01-01 00:00:00') if you use time.
SELECT SUM(DATEDIFF(DAY,'19700101',DateColumn)) ....
I'm not familiar enough with Btrieve to know what kinds of functions are available for formatting dates, however.
Using "Except" in SQL on the lines of Numeric fields you can compare the date counts in both the tables. For the Old source you may generate the select statement using excel or in the native database and bring to the SQL Server. For demonstration purpose I have used two tables and showing Except example below.
IF EXISTS (SELECT * FROM sys.objects
WHERE OBJECT_ID = OBJECT_ID(N'[dbo].[DateCompareOld]') AND
TYPE IN (N'U'))
DROP TABLE [dbo].[DateCompareOld]
GO
CREATE TABLE dbo.DateCompareOld
(
AsOf DATETIME
)
INSERT INTO DateCompareOld
SELECT '01/01/2016' UNION ALL
SELECT '01/01/2016' UNION ALL
SELECT '01/01/2016' UNION ALL
SELECT '01/02/2016' UNION ALL
SELECT '01/02/2016' UNION ALL
SELECT '01/02/2016'
IF EXISTS (SELECT * FROM sys.objects WHERE OBJECT_ID = OBJECT_ID(N'[dbo].[DateCompareNew]') AND TYPE IN (N'U'))
DROP TABLE [dbo].[DateCompareNew]
GO
CREATE TABLE dbo.DateCompareNew
(
AsOf DATETIME
)
INSERT INTO DateCompareNew
SELECT '01/01/2016' UNION ALL
SELECT '01/01/2016' UNION ALL
SELECT '01/01/2016' UNION ALL
SELECT '01/02/2016' UNION ALL
SELECT '01/02/2016' UNION ALL
SELECT '01/02/2016'
SELECT AsOf,COUNT(*) AsOfCount
FROM DateCompareOld
GROUP BY AsOf
Except
SELECT AsOf,COUNT(*) AsOfCount
FROM DateCompareNew
GROUP BY AsOf
Unless the date range used by rows in the database is extreme (like dates of astronomical stars being born and dying), it should be just as valid to convert the dates to an integer. This can be done any of several ways and is slightly database-specific, but converting 2016-01-04 to 20,160,104 is going to work fine.
Even SQL Server allows ORD(date_field) like expressions to obtain the internal representation. But this can also be done in a portable, system-agnostic means like
datediff(day, 'January 1, 1901', date_field)
if keeping track of days is sufficient, or
datediff(second, 'January 1, 1901', date_field)
if keeping track of seconds is needed.
Maybe it is not much help, maybe is something:
declare #d1 datetime; set #d1 = '2016-01-05 12:09'
declare #d2 datetime; set #d2 = '1970-04-05 07:09'
declare #d3 datetime; set #d3 = '1999-12-12 23:05'
declare #d4 datetime; set #d4 = '1999-12-12 23:06'
declare #i1 bigint
declare #i2 bigint
declare #i3 bigint
declare #i4 bigint
select #i1 = convert( bigint, convert( timestamp, #d1 ) )
select #i2 = convert( bigint, convert( timestamp, #d2 ) )
select #i3 = convert( bigint, convert( timestamp, #d3 ) )
select #i4 = convert( bigint, convert( timestamp, #d4 ) )
select #i1
select #i2
select #i3
select #i4
select #i1 ^ #i2 ^ #i3 ^ #i4
I think you could do something like this on the SQL Server side to find the center ("average") value of the column. Then use that value on the Btrieve side to avoid overflow issues where I'm guessing you're more constrained.
-- January 1, 2000 value pulled out of the air as a stab in the dark
select
dataadd(
second,
avg(cast(datediff(datediff(second, '20000101', <data>) as bigint)),
'20000101'
) /* find the center */
I wouldn't be surprised if you had to resort to a floating point type with Btrieve or partition your scans into smaller ranges to avoid intermediate sums that get too big. And you might want to use a cursor and randomize the ordering of the rows so you don't hit them in a sorted order that causes an overflow. At this point I'm just speculating since I haven't seen any of the data and my knowledge of Btrieve is so ancient and minimal to begin with.
I also have a feeling that some of this effort is all about satisfying some uneasiness on the part of non-technical stakeholders. I'm sure you could come up with checksums and hashes that would work better but this summing concept is the one they can grasp and will set their minds at ease on top of being somewhat easier to implement quickly.

How to compare smalldatetime in stored procedure

I'm writing stored procedure to compare dates but it's not working properly. How can I make it so it compares only the dates but not the time? What I'm trying to do is compare the times and if the Id is null than insert a new entry with the same name but new time. I'm keeping multiple entries with same name but different test time.
ALTER PROCEDURE [dbo].[UL_TestData]
(
#Name varchar(30),
#Test_Time smalldatetime,
#ID INT output
)
AS
Declare #UpdateTime smalldatetime
SELECT #ID=ID FROM Info_User WHERE Name=#Name AND UpdateTime= #Test_Time
IF(#ID IS NULL)
BEGIN
INSERT INTO Info_User (Name, UpdateTime) VALUES (#Name, #UpdateTime)
END
there are a lot of solutions to this depending on what type of DBMS, however here is one:
SELECT #ID=ID FROM Info_User WHERE Name=#Name AND floor(cast(#UpdateTime as float))= floor(cast(#Test_Time as float))
this works because smalldatetime's date is stored a whole numbers, where the time is stored as decimals.
I would cast the dates to a plain date which makes this solution independent of implementation details
select #ID=ID
from info_user
where Name = #Name
and cast (UpdateTime as Date) = Cast(#TestTime as Date)
However, I would either add the date part of the UpdateTime as an additional (calculated) column or split the information into a date and a time part. This makes it much easier to query entries by the plain date.
As a rule of thumb: The type of columns (in general: the table layout) greatly depends on the type of query you usually run against your data.
Edit: As attila pointed out, the date datatype only exists in version 2008 and up