sql function that can see, and aggregate, differences between two columns - sql-server

I have a table like this:
create table Stuff
(StuffID int identity not null,
StuffPrice decimal (8,2) not null,
StuffSold decimal (8,2) not null,
StuffPriceTime datetime not null)
I'd like to do a query that shows, for the recordset I'm returning, the number of times that StuffPrice was greater than StuffSold. Is there any SQL batch way of doing this? Something like:
Select
StuffID,
StuffPrice,
StuffSold,
StuffPriceTime,
SomeFunction(StuffPrice,StuffSold)
From Stuff
Where I'd see a resultset that looks something like:
[StuffID] - [StuffPrice] - [StuffSold] - [StuffPriceTime] - [True/False Result]
Now that I'm writing this out, I suppose I could do a UDF scalar function but I've heard the performance can be terrible for those.

In general, any kind of difference between the columns that can be presented as logical expression (predicate) can be expressed as flag - CASE WHEN predicate=true THEN 1 ELSE 0 END and then summarized to the final result.
For example:
create table Stuff
(StuffID int identity not null,
StuffPrice decimal (8,2) not null,
StuffSold decimal (8,2) not null,
StuffPriceTime datetime not null)
insert into Stuff (StuffPrice, StuffSold, StuffPriceTime) values
(10.0, 11.0, getdate()), --> lower
(12.0, 11.0, getdate()), --> greater
(17.0, 18.0, getdate()), --> lower
(17.0, 16.0, getdate()); --> greater
Select
StuffID,
StuffPrice,
StuffSold,
StuffPriceTime,
sum(case when StuffPrice > StuffSold then 1 else 0 end) over() [number of times]
From Stuff
Result:
StuffID StuffPrice StuffSold StuffPriceTime [number of times]
-----------------------------------------------------------------
1 10.00 11.00 2015-01-01 2
2 12.00 11.00 2015-01-01 2
3 17.00 18.00 2015-01-01 2
4 17.00 16.00 2015-01-01 2

Related

T-SQL : set start of the week from named date and show week nr of the date

I have a code that traces a number of week of the date in a period that set by parameters
CREATE TABLE #test
(
job int,
dateL datetime
)
INSERT INTO #test
VALUES (1, '2021-10-04'),
(2, '2021-10-05'),
(3, '2021-10-11'),
(4, '2021-10-12')
DECLARE #startdate datetime = '2021-10-05',
#enddate datetime = '2021-12-03'
SELECT
dateL,
(CASE
WHEN t.DateL BETWEEN #startDate and #endDate
THEN (DATEDIFF(wk,#startDate, t.DateL)) + 1
ELSE -1
END) AS WeekNumber
FROM
#test t
DROP TABLE #test
The results of this
dateL WeekNumber
---------------------------------------
2021-10-04 00:00:00.000 -1
2021-10-05 00:00:00.000 1
2021-10-11 00:00:00.000 2
2021-10-12 00:00:00.000 2
But it now quite what I need, and I don't understand how to set this to start count from #startDate.
So it should count like:
05.10 - 11.10 - first week
12.20 - 18.10 - second week
and so on,
So it would look like that
dateL WeekNumber
-----------------------------------
2021-10-04 00:00:00.000 -1
2021-10-05 00:00:00.000 1
2021-10-11 00:00:00.000 *1*
2021-10-12 00:00:00.000 2
I tried to set ##datefirst to
datepart(weekday, #startDate)
but it just ignores me.
Could someone may be recommend something, thanks!

SQL Server : finding gaps in employment - island and gap problem

I have been going through stack overflow to try and work this out over the last week and I still can't work out a viable solution so was wondering if anyone could offer me some help/advice?
Explanation of the data structures
I have the following tables:
Position table (zz_position) which is used to hold the details of the
position (Job ID) include the date range that it is valid for.
PosNo Description Date_From Date_To
---------------------------------------------------------
10001 System Administrator 20170101 20231231
Resource table (zz_resource) which is used to hold the details of a resource (employee) including the date that they joined the company and left it
resID description date_from date_to
------------------------------------------
100 Sam 20160101 20991231
101 Joe 20150101 20991231
Employment table (zz_employment) which is used to link position to resources within a date from and to range
PosNo resID Date_From Date_To seqNo
---------------------------------------------------
10001 100 20180101 20180401 1
10001 101 20180601 20191231 2
10001 100 20200101 20991231 3
Problem
Now due to people changing positions, a post might not be filled for a period of time and what I am trying to do is produce a report that I can use to give me a breakdown of the status of a post at any point in time.
I know that I can produce one which fully maps each day using a calendar table however what I want is a report which produces the data in the following aggregated format:
PosNo resID Date_From Date_To seqNo
-------------------------------------------------
10001 NULL 20170101 20171231 0
10001 100 20180101 20180401 1
10001 NULL 20180402 20180530 0
10001 101 20180601 20191231 2
10001 100 20200101 20231231 3
insert into zz_employment
values ('10001', '100', '2018-01-01 00:00:00.000', '2018-04-01 00:00:00.000', 1),
('10001', '101', '2018-06-01 00:00:00.000', '2019-12-31 00:00:00.000', 2),
('10001', '100', '2020-01-01 00:00:00.000', '2099-12-31 00:00:00.000', 3)
(note how the report has taken the two lines in the table and produced a fully speced out life of the employment where the first null line date from is pulled from the position start date and the last line date to is pulled from the position end date.
Ideally I would like this as a view/function however due to the complexity I would be more than happy to have a series of T SQL statements that I can run each night as part of a data warehouse routine.
Rules
all dates are truncated to datetime so that an date_to is referencing the date that it ends not the date and time that it ends
if the post/employment/resource has no end date then it will be denoted as 20991231
if the employment itself is open ended then the date to in the employment table is denoted as 20991231 even through the position itself might end in 20231231. Ideally I would like the result to respect the position end date.
SQL code:
CREATE TABLE zz_position
(
posNo varchar(25) NOT NULL,
description varchar(25) NOT NULL,
date_from datetime NULL,
date_to datetime NULL
)
insert into zz_position
values ('10001', 'System Administrator', '2017-01-01 00:00:00.000', '2020-12-31 00:00:00.000')
go
CREATE TABLE zz_resource
(
resID varchar(25) NOT NULL,
description varchar(25) NOT NULL,
date_from datetime NULL,
date_to datetime NULL
)
insert into zz_resource
values ('100', 'Sam', '2016-01-01 00:00:00.000', '2099-12-31 00:00:00.000'),
('101', 'Joe', '2015-01-01 00:00:00.000', '2099-12-31 00:00:00.000')
go
CREATE TABLE zz_employment
(
posNo varchar(25) NOT NULL,
resID varchar(25) NOT NULL,
date_from datetime NULL,
date_to datetime NULL,
seqNo int NULL
)
insert into zz_employment
values ('10001', '100', '2018-01-01 00:00:00.000', '2018-04-01 00:00:00.000', 1),
('10001', '101', '2018-06-01 00:00:00.000', '2019-12-31 00:00:00.000', 2),
('10001', '100', '2020-01-01 00:00:00.000', '2099-12-31 00:00:00.000', 3)
There are 2 caveats for this problem:
A calendar table.
A way to correctly group unemployed periods when there's an employed period in between.
The following solution uses a calendar table (SQL included) and an DATEDIFF() with anchor-date trick to group correctly for the 2nd point.
Complete DB Fiddle here.
Solution (explanation below):
;WITH AllPositionDates AS
(
SELECT
T.posNo,
C.GeneratedDate
FROM
zz_position AS T
INNER JOIN Calendar AS C ON C.GeneratedDate BETWEEN T.date_from AND T.date_to
),
AllEmployedDates AS
(
SELECT
T.posNo,
T.resID,
T.seqNo,
C.GeneratedDate
FROM
zz_employment AS T
INNER JOIN Calendar AS C ON C.GeneratedDate BETWEEN T.date_from AND T.date_to
),
PositionsByEmployed AS
(
SELECT
P.posNo,
P.GeneratedDate,
E.resID,
E.seqNo,
NullRowNumber = ROW_NUMBER() OVER (
PARTITION BY
P.posNo,
CASE WHEN E.posNo IS NULL THEN 1 ELSE 2 END
ORDER BY
P.GeneratedDate ASC)
FROM
AllPositionDates AS P
LEFT JOIN AllEmployedDates AS E ON
P.posNo = E.posNo AND
P.GeneratedDate = E.GeneratedDate
)
SELECT
P.posNo,
P.resID,
Date_From = MIN(P.GeneratedDate),
Date_To = MAX(P.GeneratedDate),
seqNo = ISNULL(P.seqNo, 0)
FROM
PositionsByEmployed AS P
GROUP BY
P.posNo,
P.resID,
P.seqNo,
CASE WHEN P.resId IS NULL THEN P.NullRowNumber - DATEDIFF(DAY, '2000-01-01', P.GeneratedDate) END -- GroupingValueGroupingValue
ORDER BY
P.posNo,
Date_From,
Date_To
The result:
posNo resID Date_From Date_To seqNo
10001 NULL 2017-01-01 2017-12-31 0
10001 100 2018-01-01 2018-04-01 1
10001 NULL 2018-04-02 2018-05-31 0
10001 101 2018-06-01 2019-12-31 2
10001 100 2020-01-01 2020-12-31 3
Explanation
First the creating of a calendar table. This holds 1 row for each day and in this example it's limited to the first and last possible day of the job positions:
DECLARE #DateStart DATE = (SELECT MIN(P.date_from) FROM zz_position AS P)
DECLARE #DateEnd DATE = (SELECT(MAX(P.date_to)) FROM zz_position AS P)
;WITH GeneratedDates AS
(
SELECT
GeneratedDate = #DateStart
UNION ALL
SELECT
GeneratedDate = DATEADD(DAY, 1, G.GeneratedDate)
FROM
GeneratedDates AS G
WHERE
DATEADD(DAY, 1, G.GeneratedDate) <= #DateEnd
)
SELECT
DateID = IDENTITY(INT, 1, 1),
G.GeneratedDate
INTO
Calendar
FROM
GeneratedDates AS G
OPTION
(MAXRECURSION 0)
This generates the following (up to 2020-12-31, which is max date from sample data):
DateID GeneratedDate
1 2017-01-01
2 2017-01-02
3 2017-01-03
4 2017-01-04
5 2017-01-05
6 2017-01-06
7 2017-01-07
Now we use a join with a between to "spread" the periods of both the positions and the employees periods (on different CTEs), so we get 1 row for each day, for each position/employee.
-- AllPositionDates
SELECT
T.posNo,
C.GeneratedDate
FROM
zz_position AS T
INNER JOIN Calendar AS C ON C.GeneratedDate BETWEEN T.date_from AND T.date_to
-- AllEmployedDates
SELECT
T.posNo,
T.resID,
T.seqNo,
C.GeneratedDate
FROM
zz_employment AS T
INNER JOIN Calendar AS C ON C.GeneratedDate BETWEEN T.date_from AND T.date_to
With these, we join them together by position and date using LEFT JOIN, so we get all days of each position and the matching employee (if exists). We also calculate a row number for all NULL values for each position that we are gonna use later. Note that this row number increases 1 by 1 with each following date accordingly.
;WITH AllPositionDates AS
(
SELECT
T.posNo,
C.GeneratedDate
FROM
zz_position AS T
INNER JOIN Calendar AS C ON C.GeneratedDate BETWEEN T.date_from AND T.date_to
),
AllEmployedDates AS
(
SELECT
T.posNo,
T.resID,
T.seqNo,
C.GeneratedDate
FROM
zz_employment AS T
INNER JOIN Calendar AS C ON C.GeneratedDate BETWEEN T.date_from AND T.date_to
)
-- PositionsByEmployee
SELECT
P.posNo,
P.GeneratedDate,
E.resID,
E.seqNo,
NullRowNumber = ROW_NUMBER() OVER (
PARTITION BY
P.posNo,
CASE WHEN E.posNo IS NULL THEN 1 ELSE 2 END
ORDER BY
P.GeneratedDate ASC)
FROM
AllPositionDates AS P
LEFT JOIN AllEmployedDates AS E ON
P.posNo = E.posNo AND
P.GeneratedDate = E.GeneratedDate
Now with the tricky part. If we calculate the amount of days of difference between a hard-coded date and each day, we get a similar "row number" that increases consistently for each date.
SELECT
P.posNo,
P.GeneratedDate,
DateDiff = DATEDIFF(DAY, '2000-01-01', P.GeneratedDate),
P.NullRowNumber
FROM
PositionsByEmployed AS P -- This is declare with the WITH (full solution below)
ORDER BY
P.posNo,
P.GeneratedDate
We get the following:
posNo GeneratedDate DateDiff NullRowNumber
10001 2017-01-01 6210 1
10001 2017-01-02 6211 2
10001 2017-01-03 6212 3
10001 2017-01-04 6213 4
10001 2017-01-05 6214 5
10001 2017-01-06 6215 6
10001 2017-01-07 6216 7
10001 2017-01-08 6217 8
10001 2017-01-09 6218 9
If we add another column with the rest of these 2 you will see that the value remains the same:
SELECT
P.posNo,
P.GeneratedDate,
DateDiff = DATEDIFF(DAY, '2000-01-01', P.GeneratedDate),
P.NullRowNumber,
GroupingValue = P.NullRowNumber - DATEDIFF(DAY, '2000-01-01', P.GeneratedDate)
FROM
PositionsByEmployed AS P
ORDER BY
P.posNo,
P.GeneratedDate
We get:
posNo GeneratedDate DateDiff NullRowNumber GroupingValue
10001 2017-01-01 6210 1 -6209
10001 2017-01-02 6211 2 -6209
10001 2017-01-03 6212 3 -6209
10001 2017-01-04 6213 4 -6209
10001 2017-01-05 6214 5 -6209
10001 2017-01-06 6215 6 -6209
10001 2017-01-07 6216 7 -6209
10001 2017-01-08 6217 8 -6209
10001 2017-01-09 6218 9 -6209
10001 2017-01-10 6219 10 -6209
But if we scroll down until we see values that are NULL for employee (from the ROW_NUMBER() PARTITION BY expression E.PosNo), we see that the rest differs, since the ROW_NUMBER() kept increasing 1 by 1 and the DATEDIFF jumped because there are employed people in between:
posNo GeneratedDate DateDiff NullRowNumber GroupingValue
10001 2017-12-28 6571 362 -6209
10001 2017-12-29 6572 363 -6209
10001 2017-12-30 6573 364 -6209
10001 2017-12-31 6574 365 -6209
...
10001 2018-04-02 6666 366 -6300
10001 2018-04-03 6667 367 -6300
10001 2018-04-04 6668 368 -6300
10001 2018-04-05 6669 369 -6300
10001 2018-04-06 6670 370 -6300
10001 2018-04-07 6671 371 -6300
Use use this "GroupingValue" as an additional GROUP BY to correctly separate position intervals that fall outside employed intervals.

sql server 2014 group by date extract several fields

I have a table that looks like this:
CREATE TABLE dbo.Mails (
ID int IDENTITY(1, 1) NOT NULL,
Reference nvarchar(20) COLLATE Latin1_General_CI_AS NULL,
Email nvarchar(70) NOT NULL,
ETS datetime NULL, --Estimated Time of Shipping
ATS datetime NULL, --Actual Time of Shipping
ReadOn datetime NULL,
Unsubscribed datetime NULL,
Bounced datetime NULL,
BouncedReason nvarchar(30) COLLATE Latin1_General_CI_AS NULL,
Active bit DEFAULT 1 NULL
)
I need to show info on a chart, and I need to group by Date.
therefore if I want to group details by ReadOn field for a certain campaign I build the following query
Select
CAST(readOn as date) [date],
COUNT(*) [read]
FROM Mails m
WHERE m.Reference=#Reference
GROUP BY CAST(readOn as date)
ORDER BY CAST(readOn as date) ASC
and I get something like this:
sDate read
NULL 360
2016-05-05 67
2016-05-06 123
2016-05-07 84
2016-05-08 62
2016-05-09 89
2016-05-10 17
2016-05-11 12
2016-05-12 8
2016-05-13 4
2016-05-14 4
But I would like to extract, in the same query, not only ReadOn field, but also other fields like ETS, ATS, Unsubscribed/Read & Unread and Bounced
and get something like this
sDate read ETS ATS Bounced Unsub./Read Unsub/Unread
NULL 360
2016-05-05 67 830 570 27 7 3
2016-05-06 123 0 260 4 9 5
2016-05-07 84 0 0 0 2 2
2016-05-08 62 0 0 0 2 4
2016-05-09 89 0 0 0 7 1
2016-05-10 17 0 0 0 5 6
2016-05-11 12 0 0 0 8 2
2016-05-12 8 0 0 0 1 3
2016-05-13 4 0 0 0 0 2
2016-05-14 4 0 0 0 0 2
Is there an easier way than building 6 different queries?
can at least indicate the path to follow?
Thanks
Joe
You can do it with some pre-processing and a PIVOT. In this example, I've put the query into a stored procedure, so that it is contained and easy to test. I'm doing the pre-processing in a CTE, to keep the main query tidy.
First, create the table and populate it.
CREATE TABLE dbo.Mails
(
ID int IDENTITY(1, 1) NOT NULL,
Reference nvarchar(20) COLLATE Latin1_General_CI_AS NULL,
ETS datetime NULL, --Estimated Time of Shipping
ATS datetime NULL, --Actual Time of Shipping
ReadOn datetime NULL,
Unsubscribed datetime NULL,
Bounced bit DEFAULT 0 NULL,
BouncedReason nvarchar(30) COLLATE Latin1_General_CI_AS NULL,
Active bit DEFAULT 1 NULL
);
GO
INSERT INTO dbo.Mails (Reference, ETS, ATS, ReadOn, Unsubscribed, Bounced)
VALUES
(N'ABC', '2015-05-05', '2015-05-05', '2015-05-05', NULL, 0),
(N'ABC', '2015-05-06', '2015-05-07', '2015-05-08', NULL, 0),
(N'ABC', '2015-05-05', '2015-05-05', '2015-05-07', NULL, 0),
(N'ABC', '2015-05-07', '2015-05-08', '2015-05-09', NULL, 0),
(N'ABC', '2015-05-06', '2015-05-07', '2015-05-09', '2015-05-09', 0),
(N'ABC', '2015-05-06', '2015-05-07', NULL, '2015-05-08', 0);
Then create a stored procedure with a parameter #Reference. I'm using a CTE to create a two column row set, with Date and Type as the columns. Then, in the main SELECT statement it's being pivoted to give the result you want.
The row set produced by the CTE looks like this.
Note: I haven't included the Bounced column, because I'm not clear what the requirement is for that; it's not a date column. However you should be able to extend this example quite easily.
CREATE PROCEDURE dbo.up_ReportMails
(
#Reference nvarchar(20)
)
AS
WITH cte AS
(
SELECT CAST(ReadOn AS date) AS 'Date', 'R' AS 'Type'
FROM dbo.Mails
WHERE Reference = #Reference AND ReadOn IS NOT NULL
UNION ALL
SELECT CAST(ETS AS date), 'E'
FROM dbo.Mails
WHERE Reference = #Reference AND ETS IS NOT NULL
UNION ALL
SELECT CAST(ATS AS date), 'A'
FROM dbo.Mails
WHERE Reference = #Reference AND ATS IS NOT NULL
UNION ALL
SELECT CAST(Unsubscribed AS date), 'U'
FROM dbo.Mails
WHERE Reference = #Reference AND UNSUBSCRIBED IS NOT NULL AND ReadOn IS NOT NULL
UNION ALL
SELECT CAST(Unsubscribed AS date), 'V'
FROM dbo.Mails
WHERE Reference = #Reference AND UNSUBSCRIBED IS NOT NULL AND ReadOn IS NULL
)
SELECT [Date], [R] AS 'Read', [E] AS 'ETS', [A] AS 'ATS', [U] AS 'Unsub/Read', [V] As 'Unsub/Unread'
FROM
(SELECT [Date], [Type]
FROM cte) AS C
PIVOT
(
COUNT([Type])
FOR [Type] IN ([R], [E], [A], [U], [V])
) AS PivotTable
ORDER BY [Date];
Then we can test it.
EXEC dbo.up_ReportMails #Reference=N'ABC';
I've tested this code and it works. Assuming you have a calendar table (if you don't have one, you can Google it and make one in about 10 minutes - they're very simple and will save you loads of time):
DECLARE #StartDate date = '01/01/2016'
DECLARE #EndDate date = '05/06/2016'
SELECT C.BaseDate,
ISNULL(SUM(CASE WHEN C.BaseDate = CAST(M.ETS AS DATE) THEN 1 END), 0) AS [ETS],
ISNULL(SUM(CASE WHEN C.BaseDate = CAST(M.ATS AS DATE) THEN 1 END), 0) AS [ATS],
ISNULL(SUM(CASE WHEN C.BaseDate = CAST(M.ReadOn AS DATE) THEN 1 END), 0) AS [Read On],
ISNULL(SUM(CASE WHEN C.BaseDate = CAST(M.Unsubscribed AS DATE) THEN 1 END), 0) AS [Unsubscribed],
ISNULL(SUM(CASE WHEN C.BaseDate = CAST(M.Bounced AS DATE) THEN 1 END), 0) AS [Bounced]
FROM Calendar C
LEFT OUTER JOIN Mails M
ON C.BaseDate = CAST(M.ETS AS DATE)
OR C.BaseDate = CAST(M.ATS AS DATE)
OR C.BaseDate = CAST(M.ReadOn AS DATE)
OR C.BaseDate = CAST(M.Unsubscribed AS DATE)
OR C.BaseDate = CAST(M.Bounced AS DATE)
WHERE C.BaseDate BETWEEN #StartDate AND #EndDate
GROUP BY C.BaseDate
Basically what you're doing is selecting every date from the calendar table within your date range, and then joining it to your mails table if ANY of the datetimes match that date. The purpose of the left join is so that dates on which nothing occurs are still returned in your result set. They will all be zeros, but it's better for consistency and in case someone wants to calculate averages from your report.
Once you have all the dates - and all of the records that have a matching datetime, you just need to count how many, for each date, have a matching ETS, how many have a matching ATS, so on and so forth. Last, you group by the calendar date and you're all done.

sum 1 column and pick the date on another column

I have a table with 2 columns:
date | points
Data:
2015-01-30 00:00:00.000 | 1.2
2015-01-29 00:00:00.000 | 2
2015-01-30 00:00:00.000 | 5
2015-01-27 00:00:00.000 | 7
I want to sum point column based on date. So if I filter date with 2015-01-30 00:00:00.000 then the result would be like the one below:
2015-01-30 00:00:00.000 | 7.5
The record above is what it should look like.
I have a query but it returns
Error converting data type varchar to float.
My SQL code.
SELECT gpsdate, totkm
FROM (
SELECT gpsdate,
cast(cast(cast((SUM(KMRUN)) as float) as int) as nvarchar(50)) as totkm
,
RANK() OVER(PARTITION BY gpsdate
ORDER BY SUM(KMRUN) DESC) as R
FROM view_tracklist_report
GROUP BY gpsdate) as InnerQuery
WHERE InnerQuery.R = 1
Test Data
CREATE TABLE #Test ([date] DATETIME, points FLOAT);
INSERT INTO #Test
([date], points)
VALUES
('2015-01-30 00:00:00.000', 1.2),
('2015-01-29 00:00:00.000', 2),
('2015-01-30 00:00:00.000', 5),
('2015-01-27 00:00:00.000', 7)
Actual Code: Not sure why it should sum to 7.5 tho? 5+1.2=6.2
SELECT DISTINCT [date], SUM(points) OVER(PARTITION BY [date]) AS TotPoints
FROM #Test
Just group by:
Select gpsdate, Sum(CAST(KMRUN AS float)) as KMRUN
From view_tracklist_report
group BY gpsdate
This is not a good choice to store numerical values as varchars. If you do so you will have to convert them to numbers in every query. And this is not optimal.
So you have to convert your column tu FLOAT. Then you can simply call
SUM(KMRUN)
without any casting.
Now your SUM(KMRUN) is concatenating strings and giving result like
1.2257

How to convert a float column into a datetime

I have a table that contains a DateField(DataType : DateTime) and TimeField(DataType : Float)
My output should be DateTime . My tables are in SQL Server 2008
Here is an example :
Table A
ID StartDate StartTime
1 2012-06-08 00:00:00.000 1223
2 2012-08-07 00:00:00.000 910
3 2012-05-02 00:00:00.000 1614
4 0094-07-13 00:00:00.000 1245
5 1994-04-18 00.00:00.000 2573
I need to get my output in such a way that I should it should validate for the correct time and correct date and append these two and insert into table B
Table B :
ID StartDateTime
1 2012-06-06 12:23:00.000
2 2012-08-07 09:10:00.000
3 2012-05-02 16:14:00.000
Note that I intentionally left rows 4 and 5 out of the result set; these rows should be ignored because they don't contain valid datetime or time data.
Have you considered correcting the design, and storing the date/time together, or at least storing date and time using the proper data types? In the meantime:
SELECT StartDate + STUFF(RIGHT('0' + RTRIM(StartTime), 4), 3, 0, ':')
FROM dbo.table
WHERE ISDATE(StartDate) = 1 AND CONVERT(INT, StartTime) < 2400
-- wow what a bunch of absolute garbage data you have
-- what Government agency are you paying to provide this data?
AND CONVERT(INT, StartTime) % 100 BETWEEN 0 AND 59;

Resources