Find closest second to a given datetimeoffset - tsql

I understand that there have been many resources about this already, for example Find closest date in SQL Server but I don't think this is a duplicate because it goes into much more depth due to the requirement.
I need to find a record closest to a given date/time/offset either in the past or in the future to the nearest second.
IF ( OBJECT_ID( N'[dbo].[MYTIMETABLE]' ) IS NOT Null )
DROP TABLE [dbo].[MYTIMETABLE];
GO
CREATE TABLE [dbo].[MYTIMETABLE]
(
[TIMESTAMP] datetimeoffset(0) NOT NULL,
[VALUE] char(3) NOT NULL
);
GO
Fill the table with some records, in my instance, there will eventually be millions of records, hence why this requirement is a little more complicated.
SET NOCOUNT ON;
GO
WHILE ( ( SELECT COUNT(*) FROM [dbo].[MYTIMETABLE] ) < 1000 )
BEGIN
DECLARE #Script nvarchar(max) =
N'INSERT INTO [dbo].[MYTIMETABLE] ( [TIMESTAMP], [VALUE] )
SELECT DATEADD( ' +
CASE ( FLOOR( ( RAND() * 4 ) + 1 ) )
WHEN 1 THEN N'second'
WHEN 2 THEN N'minute'
WHEN 3 THEN N'hour'
WHEN 4 THEN N'day'
END +
N', ' +
CASE ( FLOOR( RAND() * 2 ) )
WHEN 0 THEN N'-'
WHEN 1 THEN N''
END +
CONVERT( nvarchar, FLOOR( ( RAND() * 100 ) + 1 ) ) +
N', SWITCHOFFSET( SYSDATETIMEOFFSET(), ''' +
CASE ( FLOOR( RAND() * 2 ) )
WHEN 0 THEN N'-'
WHEN 1 THEN N'+'
END +
N'0' +
CONVERT( nvarchar, FLOOR( RAND() * 10 ) ) +
N':00'' ) ), ' +
CONVERT( nvarchar, FLOOR( ( RAND() * 100 ) + 1 ) );
--RAISERROR( #Script , 0, 1 ) WITH NOWAIT;
EXEC sp_executesql #Script;
END
GO
The lookup script I came up with:
DECLARE #DateTime datetimeoffset(0) = SYSDATETIMEOFFSET();
SELECT TOP(1) [Current Time] = #DateTime, [Time Difference] = DATEDIFF( second, [TIMESTAMP], #DateTime ), *
FROM [dbo].[MYTIMETABLE]
ORDER BY ABS( DATEDIFF( second, [TIMESTAMP], #DateTime ) );
My question is, is this the most optimal version of this script? It looks very basic and I am worried that once this goes into production and it's running thousands of lookups per day against millions of records that it's going to suffer performance problems.
The script will be housed in a function so that it can be compiled to further optimise it but any additional performance improvement advice would be highly appreciated.

Related

TSQL - Convert Money to Spanish Text

I have come across a very cool function from another source that translates money data types to English Text. It's great, but I also need to do this in Spanish. I tried to edit the numbers to Spanish words but of course the "rules" of Spanish numbering aren't the same as English. Does anyone have something already for converting a money data type to a Spanish text? Below is the one for English for reference. Or can a Spanish speaking db dev help me out here with modifying this one?
CREATE FUNCTION [dbo].[fnNumberToEnglish](#Money AS money)
RETURNS VARCHAR(1024)
AS
BEGIN
DECLARE #Number as BIGINT
SET #Number = FLOOR(#Money)
DECLARE #Below20 TABLE (ID int identity(0,1), Word varchar(32))
DECLARE #Below100 TABLE (ID int identity(2,1), Word varchar(32))
INSERT #Below20 (Word) VALUES
( 'Zero'), ('One'),( 'Two' ), ( 'Three'),
( 'Four' ), ( 'Five' ), ( 'Six' ), ( 'Seven' ),
( 'Eight'), ( 'Nine'), ( 'Ten'), ( 'Eleven' ),
( 'Twelve' ), ( 'Thirteen' ), ( 'Fourteen'),
( 'Fifteen' ), ('Sixteen' ), ( 'Seventeen'),
('Eighteen' ), ( 'Nineteen' )
INSERT #Below100 VALUES ('Twenty'), ('Thirty'),('Forty'), ('Fifty'),
('Sixty'), ('Seventy'), ('Eighty'), ('Ninety')
DECLARE #English varchar(1024) =
(
SELECT Case
WHEN #Number = 0 THEN ''
WHEN #Number BETWEEN 1 AND 19
THEN (SELECT Word FROM #Below20 WHERE ID=#Number)
WHEN #Number BETWEEN 20 AND 99
-- SQL Server recursive function
THEN (SELECT Word FROM #Below100 WHERE ID=#Number/10)+ '-' +
dbo.fnMoneyToEnglish( #Number % 10)
WHEN #Number BETWEEN 100 AND 999
THEN (dbo.fnMoneyToEnglish( #Number / 100))+' Hundred '+
dbo.fnMoneyToEnglish( #Number % 100)
WHEN #Number BETWEEN 1000 AND 999999
THEN (dbo.fnMoneyToEnglish( #Number / 1000))+' Thousand '+
dbo.fnMoneyToEnglish( #Number % 1000)
WHEN #Number BETWEEN 1000000 AND 999999999
THEN (dbo.fnMoneyToEnglish( #Number / 1000000))+' Million '+
dbo.fnMoneyToEnglish( #Number % 1000000)
ELSE ' INVALID INPUT' END
)
SELECT #English = RTRIM(#English)
SELECT #English = RTRIM(LEFT(#English,len(#English)-1))
WHERE RIGHT(#English,1)='-'
IF ##NestLevel = 1
BEGIN
SELECT #English = #English+' POINT '
SELECT #English = #English+
convert(varchar,convert(int,100*(#Money - #Number)))
END
RETURN (#English)
END
So I just learned Spanish and did it myself. Enjoy!
ALTER FUNCTION [dbo].[fnMoneyToSpanish](#Money AS money)
RETURNS VARCHAR(1024)
AS
BEGIN
DECLARE #Number as BIGINT
SET #Number = FLOOR(#Money)
DECLARE #Below20 TABLE (ID int identity(0,1), Word varchar(32))
DECLARE #Below100 TABLE (ID int identity(2,1), Word varchar(32))
INSERT #Below20 (Word) VALUES
( 'cero'), ('uno'),( 'dos' ), ( 'tres'),
( 'cuatro' ), ( 'cinco' ), ( 'seis' ), ( 'siete' ),
( 'ocho'), ( 'nueve'), ( 'diez'), ( 'once' ),
( 'doce' ), ( 'trece' ), ( 'catorce'),
( 'quince' ), ('dieciséis' ), ( 'diecisiete'),
('dieciocho' ), ( 'diecinueve' )
INSERT #Below100 VALUES ('veinti'), ('treinta'),('cuarenta,'), ('cincuenta'),
('sesenta'), ('setenta'), ('ochenta'), ('coventa')
DECLARE #English varchar(1024) =
(
SELECT Case
WHEN #Number = 0 THEN ''
WHEN #Number BETWEEN 1 AND 19
THEN (SELECT Word FROM #Below20 WHERE ID=#Number)
WHEN #Number BETWEEN 20 AND 99
THEN (SELECT CASE WHEN WORD = 'veinti' AND #Number = '20' THEN 'viente' ELSE WORD END FROM(SELECT Word FROM #Below100 WHERE ID=#Number/10)d) + CASE WHEN ##NestLevel in (3,4) AND (SELECT Word FROM #Below100 WHERE ID=#Number/10) <> 'veinti' THEN ' y ' ELSE '' END + --concat(' Number:',#Number,' Level:', ##NestLevel, ' ') +
dbo.fnMoneyToSpanish( #Number % 10)
WHEN #Number BETWEEN 100 AND 999
THEN CASE WHEN #Number < 200 THEN ' ciento ' ELSE (dbo.fnMoneyToSpanish( #Number / 100)) + 'cientos ' END +
dbo.fnMoneyToSpanish( #Number % 100)
WHEN #Number BETWEEN 1000 AND 999999
THEN CASE WHEN #Number < 2000 THEN ' mil ' ELSE (dbo.fnMoneyToSpanish( #Number / 1000)) + ' mil ' END +
dbo.fnMoneyToSpanish( #Number % 1000)
WHEN #Number BETWEEN 1000000 AND 999999999
THEN CASE WHEN #Number < 200000 THEN ' millón ' ELSE (dbo.fnMoneyToSpanish( #Number / 1000000)) + ' millones ' END +
dbo.fnMoneyToSpanish( #Number % 1000000)
ELSE ' INVALID INPUT' END
)
SELECT #English = RTRIM(#English)
SELECT #English = RTRIM(LEFT(#English,len(#English)-1))
WHERE RIGHT(#English,1)=' y '
IF ##NestLevel = 1
BEGIN
SELECT #English = #English+' dólares y '
SELECT #English = #English+
convert(varchar,convert(int,100*(#Money - #Number))) +' cents'
END
RETURN (ltrim(#English))
END
--select [dbo].[fnMoneyToSpanish](2654876.36)

Counting words in column

i must get count number the tag
<name></name>
in column.
<users><name>Tomek</name><name>Pawel</name><name>Krzysiek</name></users>
In this example data, queries should return 3.
Using XPath you can easily implement the logic.
Example XPath for your scenario : count(/users/name)
Result : 3
Test Here
Dynamic sql solution:
DECLARE #Table TABLE (Names NVARCHAR(1100))
INSERT INTO #Table VALUES
('<users><name>Tomek</name><name>Pawel</name><name>Krzysiek</name></users>'),
('<users><name>Tomek</name><name>Pawel</name><name>Krzysiek</name></users>'),
('<users><name>Tomek</name><name>Pawel</name><name>Krzysiek</name></users>')
DECLARE #Sql NVARCHAR(MAX)
SET #Sql = ''
SELECT
#Sql = #Sql +
REPLACE(
REPLACE(
REPLACE(
REPLACE(Names,'</name>',''' as Names UNION ALL ')
,'<name>','SELECT ''')
,'</users>','')
,'<users>','')+CHAR(10)
FROM #Table
SET #Sql = LEFT(#Sql,LEN(#Sql)-11)
SET #Sql = 'SELECT COUNT(Names) AS Names FROM (' + #Sql + ') as AllNames'
EXEC(#Sql)
if you work with xml data then try this variant
DECLARE #XMLdata XML = N'<users><name>Tomek</name><name>Pawel</name><name>Krzysiek</name></users>'
SELECT COUNT(*)
FROM #XMLdata.nodes('/users/name') col ( name )
This variant can be usefull when data storaged like a string (varchar)
--create temp table for testing
IF OBJECT_ID('Tempdb..#Tags') IS NOT NULL
DROP TABLE #Tags
CREATE TABLE #Tags
(
SampleText VARCHAR(1000)
)
INSERT INTO #Tags
( SampleText )
VALUES ( '<users><name>Tomek</name><name>Pawel</name><name>Krzysiek</name></users>' ),
( '<users><name>Somik</name><name>Pawel</name><name>Krzysiek</name></users>' ),
( '<users><name>Krolik</name><name>Pawel</name><name>Krzysiek</name></users>' ),
( '<users><name>Domik</name><name>Pawel</name><name>Krzysiek</name></users>' ),
( '<users><name>Zontik</name><name>Pawel</name><name>Krzysiek</name></users>' );
--------------------------------------------------------------------------------
-- recursive cte for split string
WITH cte
AS ( SELECT n = 1
UNION ALL
SELECT n + 1
FROM cte
WHERE n <= 1000
)
--------------------------------------------------------------------------------
-- final query
SELECT COUNT(*) AS Cnt
FROM cte
JOIN #Tags AS T ON n <= LEN(T.SampleText)
WHERE SUBSTRING(T.SampleText, n, 7) = '</name>'
OPTION ( MAXRECURSION 1000 )

SQL query to count number of records within a given timeframe, filling in the gaps

I am trying to count the number of records that fall between a given time period, generally 15 minutes, but I'd like to have this interval a variable. My table has a datetime column and I need to get a count of the number of records for every 15 minute interval between two dates and when there aren't any records for the 15-minute window, I need that time period with a zero. I've tried using CTE in different combinations and couldn't get it to work. I can generate the date series using a CTE, but haven't been able to get the actual data in the result. I have a working solution using a stored procedure with a WHILE loop. I was hoping to avoid this if possible in favor or a more elegant solution.
Here is my working loop using a temporary table:
declare #record_count int = 0
declare #end_date_per_query datetime
create table #output (
SessionIdTime datetime,
CallCount int)
while #date_from < #date_to
begin
set #end_date_per_query = DATEADD(minute, #interval, #date_from)
select #record_count = COUNT(*) from tbl WHERE SessionIdTime between #date_from and #end_date_per_query
insert into #output values (#date_from, #record_count)
set #date_from = #end_date_per_query
end
select * from #output order by sessionIdTime
drop table #output
Hopefully someone can help with a more elegant solution. Any help is appreciated.
A CTE works just fine:
-- Parameters.
declare #Start as DateTime = '20120901'
declare #End as DateTime = '20120902'
declare #Interval as Time = '01:00:00.00' -- One hour. Change to 15 minutes.
select #Start as [Start], #End as [End], #Interval as [Interval]
-- Sample data.
declare #Sessions as Table ( SessionId Int Identity, SessionStart DateTime )
insert into #Sessions ( SessionStart ) values
( '20120831 12:15:07' ), ( '20120831 21:51:18' ),
( '20120901 12:15:07' ), ( '20120901 21:51:18' ),
( '20120902 12:15:07' ), ( '20120902 21:51:18' )
select * from #Sessions
-- Summary.
; with SampleWindows as (
select #Start as WindowStart, #Start + #Interval as WindowEnd
union all
select SW.WindowStart + #Interval, SW.WindowEnd + #Interval
from SampleWindows as SW
where SW.WindowEnd < #End
)
select SW.WindowStart, Count( S.SessionStart ) as [Sessions]
from SampleWindows as SW left outer join
#Sessions as S on SW.WindowStart <= S.SessionStart and S.SessionStart < SW.WindowEnd
group by SW.WindowStart
Is this what you're looking for?
-- setup
DECLARE #interval INT, #start DATETIME
SELECT #interval = 15, #start = '1/1/2000 0:00:00'
DECLARE #t TABLE (id INT NOT NULL IDENTITY(1,1) PRIMARY KEY, d DATETIME)
INSERT INTO #t (d) VALUES
(DATEADD(mi, #interval * 0.00, #start)) -- in
,(DATEADD(mi, #interval * 0.75, #start)) -- in
,(DATEADD(mi, #interval * 1.50, #start)) -- out
-- query
DECLARE #result INT
SELECT #result = COUNT(*) FROM #t
WHERE d BETWEEN #start AND DATEADD(mi, #interval, #start)
-- result
PRINT #result

T-SQL Format integer to 2-digit string

I can't find a simple way to do this in T-SQL.
I have for example a column (SortExport_CSV) that returns an integer '2' thru 90.
If the stored number is a single digit, I need it to convert to a 2 digit string that begins with a 0.
I have tried to use CAST but I get stuck on how to display the style in the preferred format (0#)
Of course it is easy to do this on the front end (SSRS, MSAccess, Excel, etc) but in this instance I have no front end and must supply the raw dataset with the already formatted 2 digit string.
select right ('00'+ltrim(str( <number> )),2 )
You can use T-SQL's built in format function:
declare #number int = 1
select format (#number, '0#')
SELECT RIGHT('0' + CAST(sortexport_csv AS VARCHAR), 2)
FROM your_table
You're all doing too much work:
right(str(100+#x),2)
-- for a function, same idea:
--
create function zeroPad( #yourNum int, #wid int)
as
begin
return right( 1000000+#yourNum), #wid)
end
Convert the value to a string, add a zero in front of it (so that it's two or tree characters), and get the last to characters:
right('0'+convert(varchar(2),Sort_Export_CSV),2)
DECLARE #Number int = 1;
SELECT RIGHT('0'+ CONVERT(VARCHAR, #Number), 2)
--OR
SELECT RIGHT(CONVERT(VARCHAR, 100 + #Number), 2)
GO
Here is tiny function that left pad value with a given padding char
You can specify how many characters to be padded to left..
Create function fsPadLeft(#var varchar(200),#padChar char(1)='0',#len int)
returns varchar(300)
as
Begin
return replicate(#PadChar,#len-Len(#var))+#var
end
To call :
declare #value int; set #value =2
select dbo.fsPadLeft(#value,'0',2)
You could try this
SELECT RIGHT( '0' + convert( varchar(2) , '0' ), 2 ) -- OUTPUTS : 00
SELECT RIGHT( '0' + convert( varchar(2) , '8' ), 2 ) -- OUTPUTS : 08
SELECT RIGHT( '0' + convert( varchar(2) , '9' ), 2 ) -- OUTPUTS : 09
SELECT RIGHT( '0' + convert( varchar(2) , '10' ), 2 ) -- OUTPUTS : 10
SELECT RIGHT( '0' + convert( varchar(2) , '11' ), 2 ) -- OUTPUTS : 11
this should help
here you go
select RIGHT(REPLICATE('0', 2) + CAST(2 AS VARCHAR(2)), 2)
should return 02
try
right('0' + convert(varchar(2), #number),2)
Another example:
select
case when teamId < 10 then '0' + cast(teamId as char(1))
else cast(teamId as char(2)) end
as 'pretty id',
* from team
Try this
--Generate number from 2 to 90
;with numcte as(
select 2 as rn
union all
select rn+1 from numcte where rn<90)
--Program that formats the number based on length
select case when LEN(rn) = 1 then '00'+CAST(rn as varchar(10)) else CAST(rn as varchar(10)) end number
from numcte
Partial Output:
number
002
003
004
005
006
007
008
009
10
11
12
13
14
15
16
17
18
19
20
SELECT
replace(str(month(DATEADD(month, -1, '2012-02-29')), 2),' ' , '0')
You can create a function like this:
CREATE FUNCTION [dbo].[f_convert_int_to_2_digits](#input int)
RETURNS varchar(10)
AS
BEGIN
--return value
DECLARE #return varchar(10)
if #input < 10
begin
set #return = '0' + convert(varchar(1), #valor)
end
else
begin
set #return = convert(varchar(10), #input)
end
-- return result
RETURN #return
END
and then use it everywhere:
select [dbo].[f_convert_int_to_2_digits](<some int>)
Example for converting one digit number to two digit by adding 0 :
DECLARE #int INT = 9
SELECT CASE WHEN #int < 10
THEN FORMAT(CAST(#int AS INT),'0#')
ELSE
FORMAT(CAST(#int AS INT),'0')
END
DECLARE #Number int = 1;
SELECT RIGHT('0'+ CONVERT(VARCHAR, #Number), 2)
--OR
SELECT RIGHT(CONVERT(VARCHAR, 100 + #Number), 2)
GO
The simplest way is:
declare
#len int = 10,
#number int = 122222,
#prefix char = '0'
select replicate(#prefix, #len - len(#number)) + CAST(#number AS VARCHAR)
result:
0000122222

Implementing and applying a string split in T-SQL

I have this statement in T-SQL.
SELECT Bay From TABLE where uid in (
select B_Numbers from Info_Step WHERE uid = 'number'
)
I am selecting "multiple" BAYs from TABLE where their uid is equal to a string of numbers like this:
B_Numbers = 1:45:34:98
Therefore, I should be selecting 4 different BAYs from TABLE. I basically need to split the string 1:45:34:98 up into 4 different numbers.
I'm thinking that Split() would work, but it doesn't and I get a syntax error.
Any thoughts from the T-SQL gods would be awesome!
Here is an implementation of a split function that returns the list of numbers as a table:
http://rbgupta.blogspot.com/2007/03/split-function-tsql.html
Looks like this would set you on your way...
Here is a method that uses an auxiliary numbers table to parse the input string. The logic can easily be added to a function that returns a table. That table can then be joined to lookup the correct rows.
Step 1: Create the Numbers table
SET NOCOUNT ON
GO
IF EXISTS
(
SELECT 1
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'Numbers'
AND TABLE_SCHEMA = 'dbo'
AND TABLE_TYPE = 'BASE TABLE'
)
BEGIN
DROP TABLE dbo.Numbers
END
GO
CREATE TABLE dbo.Numbers
(
Number smallint IDENTITY(1, 1) PRIMARY KEY
)
GO
WHILE 1 = 1
BEGIN
INSERT INTO dbo.Numbers DEFAULT VALUES
IF SCOPE_IDENTITY() = 32767
BEGIN
BREAK
END
END
GO
Step 2: Parse the Input String
CREATE FUNCTION dbo.ParseString(#input_string varchar(8000), #delim varchar(8000) = " ")
RETURNS TABLE
AS RETURN
(
SELECT Number
FROM dbo.Numbers
WHERE CHARINDEX
(
#delim + CONVERT(VARCHAR(12),Number) + #delim,
#delim + #input_string + #delim
) > 0
)
GO
**EXAMPLE**
SELECT * FROM dbo.ParseString('1:45:34:98',':')
Step 3: Use the results however you want/need
Number
------
1
34
45
98
End-To-End Example
Create function that returns the appropriate BNumber (of course change it to use the commented out SQL)
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION dbo.GetBNumber (#uid int)
RETURNS VARCHAR(8000)
AS
BEGIN
RETURN '1:45:34:98'
--select B_Numbers from Info_Step WHERE uid = #uid
END
GO
Use the use functions to return the desired results
-- Using Test Data
SELECT N.Number FROM Numbers N
JOIN dbo.ParseString(dbo.GetBNumber(12345),':') Q ON Q.Number = N.Number
-- Using Your Data (Untested but should work.)
SELECT N.Bay
FROM TABLE N
JOIN dbo.ParseString(dbo.GetBNumber(ENTER YOU NUMBER HERE),':') Q ON Q.Number = N.uid
Results
Number
------
1
34
45
98
You should keep your arrays as rows but if I understand your question I think this will work.
SELECT
Bay
From
TABLE
join Info_Step
on B_Numbers like '%'+ uid +'%'
where
Info_Step.uid = 'number'
This query will do a full table scan because of the like operator.
What you can do is loop through the B_Numbers entries and do your own split on : Insert those entries into a temp table and then perform your query.
DECLARE #i int
DECLARE #start int
DECLARE #B_Numbers nvarchar(20)
DECLARE #temp table (
number nvarchar(10)
)
-- SELECT B_Numbers FROM Info_Step WHERE uid = 'number'
SELECT #B_Numbers = '1:45:34:98'
SET #i = 0
SET #start = 0
-- Parse out characters delimited by ":";
-- Would make a nice user defined function.
WHILE #i < len(#B_Numbers)
BEGIN
IF substring(#B_Numbers, #i, 1) = ':'
BEGIN
INSERT INTO #temp
VALUES (substring(#B_Numbers, #start, #i - #start))
SET #start = #i + 1
END
SET #i = #i + 1
END
-- Insert last item
INSERT INTO #temp
VALUES (substring(#B_Numbers, #start, #i - #start + 1))
-- Do query with parsed values
SELECT Bay FROM TABLE WHERE uid in (SELECT * FROM #temp)
You can even try this
declare #str varchar(50)
set #str = '1:45:34:98'
;with numcte as(
select 1 as rn union all select rn+1 from numcte where rn<LEN(#str)),
getchars as(select
ROW_NUMBER() over(order by rn) slno,
rn,chars from numcte
cross apply(select SUBSTRING(#str,rn,1) chars)X where chars = ':')
select top 1
Bay1 = SUBSTRING(#str,0,(select rn from getchars where slno = 1))
,Bay2 = SUBSTRING(#str,
(select rn from getchars where slno = 1) + 1,
(((select rn from getchars where slno = 2)-
(select rn from getchars where slno = 1)
)-1))
,Bay3 = SUBSTRING(#str,
(select rn from getchars where slno = 2) + 1,
(((select rn from getchars where slno = 3)-
(select rn from getchars where slno = 2)
)-1))
,Bay4 = SUBSTRING(#str,
(select rn from getchars where slno = 3)+1,
LEN(#str))
from getchars
Output:
Bay1 Bay2 Bay3 Bay4
1 45 34 98