sql server - passing unquoted constants to functions like DATEPART does - sql-server-2005

i would like to create a function which accepts a constant like
the datepart function accepts yy/mm/dd/hh/
like:
select datepart(dd, getdate())
i would like to create my own function that accepts dd
not char like 'dd'
i want
select MyFunc(dd, getdate())
and not
select MyFunc('dd', getdate())

You can't really constrain the input of a UDF to a small set of values (to the best of my knowledge).
I would recommend creating a tabe for your enumerated values - something like this:
CREATE TABLE MyEnumTable (DatePartID tinyint, DatePartValue char(2))
GO
INSERT MyEnumTable(DatePartID, DatePartValue)
SELECT 1, 'yy'
UNION
SELECT 2, 'mm'
UNION
SELECT 3, 'dd'
UNION
SELECT 4, 'hh'
GO
CREATE FUNCTION MyDatePart(#IntervalType tinyint)
RETURNS varchar(255)
AS
BEGIN
IF NOT EXISTS (SELECT * FROM MyEnumTable WHERE DatePartID = #IntervalType)
RETURN 'Invalid IntervalType'
--Do your stuff
DECLARE #DatePartvalue char(2)
SELECT #DatePartValue = DatePartValue
FROM MyEnumTable
WHERE DatePartID = #IntervalType
RETURN #DatePartValue
END
GO
--Check it out
SELECT dbo.MyDatePart(3), dbo.MyDatePart(12)
Of course, my example is oversimplified, but you get the idea.
Also, consider making the function a table-valued function for performance reasons, if you're planning on using the udf in set statements. I blogged about the performance implications of various function types here:
http://thehobt.blogspot.com/2009/02/scalar-functions-vs-table-valued.html

i would like to create my own function that accepts dd not char like 'dd'
I think you're out of luck on this. If you don't single-quote the characters, they'll be interpreted as a name--but you can't have defined them as a name anywhere if you want them to be used in the manner you propose.

Related

To Create a Function to split dates into Year, Month, Date into a separate column in SQL

Trying to create a function to split dateformat of "2018-05-21" to 2018 | 05 | 21 | as three separate columns. Tried creating the function as below but gives me error on "month", "Day". Error says "incorrect syntax near 'month'. Expecting '(' or Select."
CREATE FUNCTION [dbo].[functionname]
(
-- Add the parameters for the function here
#DateFormat AS DATETIME
)
RETURNS VARCHAR (MAX)
AS
BEGIN
RETURN DATEPART(YEAR,#DateFormat),
DATEPART(Month,#DateFormat),
DATEPART(Day,#DateFormat)
END
GO
The problem with your current SQL is that a scalar only returns a single value. You need to use a table value function to get multiple columns.
This is a TVF version which will provide three columns
CREATE FUNCTION [dbo].[FunctionName]
(
#DateFormat AS DATETIME
)
RETURNS TABLE AS RETURN
(
SELECT DATEPART(YEAR,#DateFormat) AS [Year],
DATEPART(Month,#DateFormat) AS [Month],
DATEPART(Day,#DateFormat) AS [Day]
)
Example usage:
DECLARE #dates TABLE (SomeDate DATE)
INSERT INTO #dates SELECT '01/25/2018'
INSERT INTO #dates SELECT '10/01/2008'
SELECT d.*,fn.* FROM #dates d
CROSS APPLY [dbo].[FunctionName](d.SomeDate) fn
And some documentation.
That said, I personally don't like this implementation. I would simply expect the DATEPART statements in the SELECT portion of the SQL. I think the TVF makes it more complicated and doesn't provide any tangible benefits.

SQL Server: user defined function parameter

I was wondering if it is possible to create a user-defined function that can take a parameter of varying length (as well as other parameters).
Essentially I want to pass a parameter that then gets used in an IN statement. What I would like to do is have something like this:
CREATE FUNCTION ufnGetOpenNotificationValue
(#LegacyTypeID INT,
#MonthEnd DATE,
#YearStart DATE,
#YearEnd DATE)
Where #LegacyTypeID is a list of integers.
So then using the function might look something like this
SELECT RE_Auxiliary.dbo.ufnGetOpenNotificationValue
((1,2,3),'2014-07-31','2013-09-01','2014-08-31')
rather than
SELECT RE_Auxiliary.dbo.ufnGetOpenNotificationValue
(1,'2014-07-31','2013-09-01','2014-08-31') +
RE_Auxiliary.dbo.ufnGetOpenNotificationValue
(2,'2014-07-31','2013-09-01','2014-08-31') +
RE_Auxiliary.dbo.ufnGetOpenNotificationValue
(3,'2014-07-31','2013-09-01','2014-08-31')
but if I try and pass multiple integers I get an error stating
Incorrect syntax near ','
As Alex K states you can't pass arrays as an input for a SQL function. You can pass table types (Pass table as parameter).
CREATE TYPE TableType
AS TABLE (LegacyTypeID INT)
CREATE FUNCTION ufnGetOpenNotificationValue
(#LegacyTypeID TableType,
#MonthEnd DATE,
#YearStart DATE,
#YearEnd DATE)
...
WHERE COLUMN_NAME IN (SELECT LegacyType FROM #LegacyTypeID)
You would then need to insert into a TableType variable before calling your function with that variable passed as a parameter
Another option would be to pass in your list in as a comma separated list of values. And then use a function (like this one) to use in your where clause
CREATE FUNCTION ufnGetOpenNotificationValue
(#LegacyTypeID NVARCHAR(256),
#MonthEnd DATE,
#YearStart DATE,
#YearEnd DATE)
...
WHERE COLUMN_NAME in (SELECT val FROM dbo.f_split(#StringParameter, ','))
Which you could then call like this:
SELECT RE_Auxiliary.dbo.ufnGetOpenNotificationValue
('1,2,3','2014-07-31','2013-09-01','2014-08-31')

'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 do I properly SELECT WHERE Effective_Date >= 'Given_Date' in a stored procedure?

I have this select statement that returns the results I'm looking for:
SELECT *
FROM Database.dbo.Table
WHERE Effective_Date >= '04/01/2014'
AND Chain = 'MCD'
I'm looking to turn this into a stored procedure with the following variables, #EffectiveDate and #Chain so that I can simply replace the date and chain to get different results. Here is the stored procedure I've made that doesn't work correctly:
CREATE PROCEDURE Database.dbo.StoredProc
#Chain VARCHAR(255),
#EffectiveDate VARCHAR(255)
AS
SELECT *
FROM Database.dbo.Table
WHERE Effective_Date >= '+#EffectiveDate+'
AND Chain = '+#Chain+'
GO
I'd like to execute this stored procedure like this:
EXEC Database.dbo.StoredProc
#PharmacyChain = N'MCD',
#EffectiveDate = N'04/01/2014'
;
GO
In this example, Table.Effective_Date is in datetime format. When I run the SELECT statement w/o the stored proc, the date comparison works fine to only select records with effective date after '04/01/2014'. However, when it's run using the variables int he stored proc, it doesn't convert the date correctly to compare. I've tried changing the EffectiveDate variable to datetime format, but still had no luck. Any help would be greatly appreciated. Thank you
Parameters should match the column datatype
#Chain VARCHAR(255) -- what is Chain?
#EffectiveDate datetime -- or date etc
And simply do this
SELECT *
FROM dbo.Table
WHERE Effective_Date >= #EffectiveDate
AND Chain = #Chain;
You don't need 3 part object names either

What is the simplest/best way to remove substrings at the end of a string?

I have a function that normalizes addresses. What I would like to do now is remove any of the strings in a limited, specified list if they occur at the end of the string. Let's say the strings I want to remove are 'st', 'ave', 'rd', 'dr', 'ct'... If the string ends with any of these strings, I want to remove them. What is the best way to accomplish this, using T-SQL (this will not be part of a select statement)?
Edit:
This is a function that accepts one address and formats it. I would like to inline the code, and the list, but in the simplest way possible. For example, some code that I've been playing with is:
if #address LIKE '%st'
SET #address = substring(#address, 1, PatIndex('%st', #address) - 1)
Is this a good method? How can I put it in some sort of loop so I can repeat this code with different values (other than st)?
Adding the values to be trimmed to a new table allows you to
easily add new values
use this table to clean up adresses
SQL Statement
DECLARE #Input VARCHAR(32)
SET #Input = 'Streetsstaverddrad'
DECLARE #Trim TABLE (Value VARCHAR(32))
INSERT INTO #Trim
SELECT 'st'
UNION ALL SELECT 'ave'
UNION ALL SELECT 'rd'
UNION ALL SELECT 'dr'
UNION ALL SELECT 'ad'
WHILE EXISTS (
SELECT *
FROM (
SELECT [Adres] = #Input
) i
INNER JOIN #Trim t ON i.Adres LIKE '%' + t.Value
)
BEGIN
SELECT #Input = SUBSTRING(Adres, 1, LEN(Adres) - LEN(t.Value))
FROM (
SELECT [Adres] = #Input
) i
INNER JOIN #Trim t ON i.Adres LIKE '%' + t.Value
END
SELECT #Input
In SQL Server 2005 it is possible to define a user-function which enables regular expression matching. You will need to defined a function which strips the trailing strings. A RegEx to match the scenarios you mention would be something like...
\s+(ave|rd|dr|ct)\s*$