Trouble with contradicting where clause - sql

I am trying to display what each user has spend their time doing for the week(either internal or external work) but the time is all on the same column on the table, is it possible to split it into 2 different columns and still have it so that it only shows each user once not each time they entered time which could be multiple times throughout the week.
The SQL below gives me each users tracked time for the week but internal and external on different rows.
SELECT SUM(FilteredMag_Time.mag_hoursspent) AS Time,
FilteredSystemUser.fullname,
FilteredMag_project.mag_typename
FROM FilteredSystemUser
INNER JOIN FilteredMag_Task
INNER JOIN FilteredMag_project ON FilteredMag_Task.mag_projectid = FilteredMag_project.mag_projectid
INNER JOIN FilteredMag_Time ON FilteredMag_Task.mag_taskid = FilteredMag_Time.mag_taskid
ON FilteredSystemUser.systemuserid = FilteredMag_Time.createdby
WHERE (FilteredMag_Time.mag_starttime BETWEEN DATEADD(dd, - (DATEPART(dw, GETDATE()) - 1), GETDATE())
AND DATEADD(dd, - (DATEPART(dw, GETDATE()) - 7), GETDATE()))
GROUP BY FilteredSystemUser.fullname, FilteredMag_project.mag_typename
ORDER BY FilteredSystemUser.fullname
Here is an example of the current output.
Time fullname mag_typename
------------------ --------------------- -------------------------
1.2500000000 David Sutton External
8.2500000000 Gayan Perera External
9.0000000000 Paul Nieuwelaar Internal
14.8700000000 Roshan Mehta External
6.0000000000 Roshan Mehta Internal
2.7800000000 Simon Phillips External
4.6600000000 Simon Phillips Internal

You can make use of SQL Server PIVOT.
Something like
DECLARE #Table TABLE(
userID INT,
typeID VARCHAR(20),
TimeSpent FLOAT
)
INSERT INTO #Table SELECT 1, 'INTERNAL', 1
INSERT INTO #Table SELECT 2, 'INTERNAL', 1
INSERT INTO #Table SELECT 1, 'INTERNAL', 1
INSERT INTO #Table SELECT 1, 'INTERNAL', 1
INSERT INTO #Table SELECT 2, 'EXTERNAL', 3
INSERT INTO #Table SELECT 1, 'EXTERNAL', 3
SELECT *
FROM
(
SELECT userID, typeID, TimeSpent
FROM #Table
) s
PIVOT (SUM(TimeSpent) FOR typeID IN ([INTERNAL],[EXTERNAL])) pvt
Output:
userID INTERNAL EXTERNAL
----------- ---------------------- ----------------------
1 3 3
2 1 3

Assuming FilteredMag_project.mag_typename is 'INTERNAL' or 'EXTERNAL', try the following:
SELECT SUM(CASE FilteredMag_project.mag_typename
WHEN 'INTERNAL' THEN FilteredMag_Time.mag_hoursspent
ELSE 0 END) AS InternalTime,
SUM(CASE FilteredMag_project.mag_typename
WHEN 'EXTERNAL' THEN FilteredMag_Time.mag_hoursspent
ELSE 0 END) AS ExternalTime,
FilteredSystemUser.fullname
FROM FilteredSystemUser
INNER JOIN FilteredMag_Task
INNER JOIN FilteredMag_project ON FilteredMag_Task.mag_projectid = FilteredMag_project.mag_projectid
INNER JOIN FilteredMag_Time ON FilteredMag_Task.mag_taskid = FilteredMag_Time.mag_taskid
ON FilteredSystemUser.systemuserid = FilteredMag_Time.createdby
WHERE (FilteredMag_Time.mag_starttime BETWEEN DATEADD(dd, - (DATEPART(dw, GETDATE()) - 1), GETDATE())
AND DATEADD(dd, - (DATEPART(dw, GETDATE()) - 7), GETDATE()))
GROUP BY FilteredSystemUser.fullname
ORDER BY FilteredSystemUser.fullname

Related

How to get D1 D7 D30 without left join on the same table

Objective:
I would like to know everyone who signed up on Day 0, how many of them logged in after D1, after D7 and after D30. I would like to make a table valued function where the users inserts a date and gets results for D0, D1, D7, D30.
Results should look like this:
Date TotalD0 TotalD1 TotalD7 TotalD30
2019-04-01 3 3 2 1
Situation:
I have one login table with email and login_time. I left joined three times on the same table and it worked on a dummy table. However, when using real data with millions of rows, it ran forever. There has to be a more efficient way of doing this.
What i tried:
CREATE FUNCTION fnTestData
(
#StartDate AS Date
)
RETURNS TABLE
AS
RETURN
select #startdate,
COUNT(distinct t1.id) As TotalD0,
COUNT(distinct t1a.id) As TotalD1,
COUNT(distinct t1b.id) As TotalD7,
COUNT(distinct t1c.id) As TotalD30
from #test1 t1
left join #test1 t1a on t1.id=t1a.id and t1a.login_time >=
DATEADD(day,1,t1.login_time)
left join #test1 t1b on t1.id=t1b.id and t1b.login_time >=
DATEADD(day,7,t1.login_time)
left join #test1 t1c on t1.id=t1c.id and t1c.login_time >=
DATEADD(day,30,t1.login_time)
where t1.login_time = #startdate
group by t1.login_time
Test data:
create table #test1 (id int, login_time date)
insert into #test1 values
(1, '2019-04-01'),
(1, '2019-04-01'),
(1, '2019-04-02'),
(1, '2019-04-19'),
(1, '2019-05-05'),
(2, '2019-04-01'),
(2, '2019-04-05'),
(2, '2019-04-10'),
(2, '2019-04-15'),
(3, '2019-04-01'),
(3, '2019-04-01'),
(3, '2019-04-02')
Your query could be translated to a GROUP BY:
DECLARE #StartDate Date = '2019-04-01'
SELECT COUNT(DISTINCT id) D0
, COUNT(DISTINCT CASE WHEN login_time >= DATEADD(DAY, 1, #StartDate) THEN id END) AS D1
, COUNT(DISTINCT CASE WHEN login_time >= DATEADD(DAY, 7, #StartDate) THEN id END) AS D7
, COUNT(DISTINCT CASE WHEN login_time >= DATEADD(DAY, 30, #StartDate) THEN id END) AS D30
FROM #test1 AS t
WHERE login_time >= #StartDate
AND EXISTS (
SELECT 1
FROM #test1 AS x
WHERE x.id = t.id
AND x.login_time = #StartDate
)
D0 D1 D7 D30
3 3 2 1
You need to create appropriate indexes to speed it up.
If you want to do cohort analysis based on the day people start:
select first_ld,
count(*) as num_d0,
sum(case when login_date >= dateadd(day, 1, firstld) then 1 else 0 end) as num_d1,
sum(case when login_date >= dateadd(day, 7, firstld) then 1 else 0 end) as num_d7,
sum(case when login_date >= dateadd(day, 30, firstld) then 1 else 0 end) as num_d30
from (select id, convert(date, login_time) as login_date,
min(convert(date, login_time)) over (partition by id) as first_ld
from #test1 t
group by id, convert(date, login_time)
) t
group by first_ld
order by first_ld;

Get the aggregated result of a GROUP BY for each value on WHERE clause in TSQL

I have a table in SQL Server with the following format
MType (Integer), MDate (Datetime), Status (SmallInt)
1, 10-05-2018, 1
1, 15-05-2018, 1
2, 25-3-2018, 0
3, 12-01-2018, 1
....
I want to get the MIN MDate for specific MTypes for future dates. In case there isn't one, then the MType should be returned but with NULL value.
Here is what I have done until now:
SELECT m.MType,
MIN(m.MDate)
FROM MyTypes m
WHERE m.MType IN ( 1, 2, 3, 4)
AND m.MDate > GETDATE()
AND m.Status = 1
GROUP BY m.MType
Obviously, the above will return only the following:
1, 10-05-2018
Since there are any other rows with future date and status equals to 1.
However, the results I want are:
1, 10-05-2018
2, NULL
3, NULL
4, NULL //this is missing in general from the table. No MType with value 4
The table is big, so performance is something to take into account. Any ideas how to proceed?
One way is to join the table to itself and filter the date in the ON clause.
SELECT a.Mtype, MIN(b.MDate)
FROM MyTypes a
LEFT JOIN MyTypes b
ON a.MType = b.MType
AND b.MDate > GETDATE()
AND b.Status = 1
WHERE a.MType IN ( 1, 2, 3)
GROUP BY a.MType
Here's a Demo.
I don't know what is logic behind but it seems to use of look-up tables
SELECT a.MType, l.MDate
FROM
(
values (1),(2),(3),(4)
)a (MType)
LEFT JOIN (
SELECT m.MType,
MIN(m.MDate) MDate
FROM MyTypes m
WHERE m.MDate > GETDATE()
AND m.Status = 1
GROUP BY m.MType
)l on l.MType = a.MType
Use a windows function and a union to a numbers table:
declare #t table (MType int, MDate datetime, [Status] smallint)
Insert into #t values (1, convert(date, '10-05-2018', 103), 1)
,(1, convert(date, '15-05-2018', 103), 1)
,(2, convert(date, '25-03-2018', 103), 0)
,(3, convert(date, '12-01-2018', 103), 1)
Select DISTINCT Mtype
, min(iiF(MDate>getdate() and status = 1, MDate, NUll)) over (Partition By Mtype) as MDate
from ( SELECT TOP 10000 row_number() over(order by t1.number) as MType
, '1900-01-01' as MDate, 0 as [Status]
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
union
Select Mtype, MDate, [Status] from #t
) x
where MType in (1,2,3,4)
order by x.MType

SQL Server : remove response with almost same date

I have a query in SQL Server that looks like this.
SELECT
[ActionId], [CreationDate], [Description]
FROM
[Action]
INNER JOIN
People ON Action.personid = People.personid
WHERE
datediff(mm, Action.creationdate, getdate()) = 1
AND People.typeofpersonid = 8
But now I would like to remove any responses that have a creationtime within a minute of another one.
So if response is currently
ActionId CreationDate Description
---------------------------------------------------------
510467 2015-04-07 11:21:02.030 Registered errand.
510468 2015-04-07 11:21:25.840 Email sent to:....
510477 2015-04-07 11:50:22.830 Registered errand.
I would like for the second row to not be returned.
Is there a smart way to do this?
This should work:
SELECT
Action.ActionId,
Action.CreationDate,
Action.Description
FROM
Action
JOIN
(
SELECT
ActionId,
ROW_NUMBER() OVER (
PARTITION BY LEFT(CONVERT(VARCHAR, CreationDate, 120), 16)
ORDER BY CreationDate, ActionId
) AS row_num
FROM
Action
WHERE
DATEDIFF(mm, CreationDate, GETDATE()) = 1
) AS a2 ON (Action.ActionId = a2.ActionId AND a2.row_num = 1)
JOIN
People ON (Action.PersonDd = People.PersonId)
WHERE
DATEDIFF(mm, Action.CreationDate, GETDATE()) = 1
AND People.TypeOfPersonId = 8
You need something like this:
SELECT [ActionId], [CreationDate], [Description]
FROM [Action]
inner join People on Action.personid = People.personid
where datediff(mm, Action.creationdate, getdate()) = 1
and People.typeofpersonid=8
AND NOT EXISTS
(
SELECT TOP 1 1
FROM [Action] Ain
inner join People Pin on Ain.personid = Pin.personid
where datediff(mm, Ain.creationdate, getdate()) = 1
and Pin.typeofpersonid=8
AND datediff(mi, Ain.creationdate, Action.creationdate)<1
AND Ain.ActionId <>Action.ActionId
)
The general idea is that you check for existing rows, and if there is one, then you dont include it you query.

For every row with data I need a row for each category

I have timesheet data that I need to create a report for by date range. I need to have a row for each person for each day, and each time type. If there's no entry for that time type on a given day, i want null data. I've tried a left join, but it doesn't seem to be working. A cross join will give erroneous data.
The tables I have are a Person table (personID, Name), a TimeLog table (TimeLogID, StartDate, EndDate, TimeLogTypeID), and a TimeLogType table (TimeLogTypeID, PersonID, Description, DeletedInd)
All I can get in the result set is the rows with data, and not the empty rows for each TimeLogType
Here's what I have so far:
DECLARE
#startDate DATE,
#endDate DATE
SET #startDate = '2014-05-01'
SET #endDate = '2014-05-30'
SELECT
CONVERT(DATE, TimeLog.StartDateTime, 101) AS TimeLogDay,
SUM(dbo.fnCalculateHoursAsDecimal(TimeLog.StartDateTime, TimeLog.EndDateTime)) AS Hours,
TimeLog.PersonID,
TimeLog.TimeLogTypeID
INTO #HourTable
FROM
TimeLog
WHERE
TimeLog.StartDateTime BETWEEN #startDate AND #endDate
GROUP BY
CONVERT(DATE, TimeLog.StartDateTime, 101),
TimeLog.TimeLogTypeID,
TimeLog.PersonID
SELECT
TimeLogType.Description,
#HourTable.*
FROM
TimeLogType LEFT JOIN
#HourTable ON TimeLogType.TimeLogTypeID = #HourTable.TimeLogTypeID
WHERE
ISNULL(TimeLogType.DeletedInd, 0) = 0
ORDER BY
PersonID, TimeLogDay, TimeLogType.TimeLogTypeID
The data goes something like this:
TimeLogType:
1, Billable
2, Non-Billable
Person:
1, Billy
2, Tom
TimeLog:
1, 1, 2014-05-01 08:00:00, 2014-05-01 09:00:00, 1, 0
2, 1, 2014-05-01 09:00:00, 2014-05-01 10:00:00, 1, 0
3, 2, 2014-05-01 08:00:00, 2014-05-01 08:30:00, 2, 0
4, 2, 2014-05-01 08:30:00, 2014-05-01 09:00:00, 1, 0
5, 1, 2014-05-02 08:00:00, 2014-05-02 09:00:00, 2, 0
Expected Output: (order by person, date, timelog type)
Day, Person, Bill Type, Total Hours
2014-05-01, Billy, Billiable, 2.0
2014-05-01, Billy, Non-Billiable, NULL
2014-05-02, Billy, Billiable, 1.0
2014-05-02, Billy, Non-Billiable, NULL
etc...
2014-05-01, Tom, Billiable, 0.5
2014-05-01, Tom, Non-Billiable, 0.5
etc...
You need to generate all the combinations first and then use left join to bring in the information you want. I think the query is like this:
with dates as (
select dateadd(day, number - 1, mind) as thedate
from (select min(StartDate) as mind, max(EndDate) as endd
from TimeLogType
) tlt join
master..spt_values v
on dateadd(day, v.number, mind) <= tlt.endd
)
select p.PersonId, tlt.TimeLogTypeId, d.thedate,
from Person p cross join
(select tlt.* from TimeLogType tlt where ISNULL(TimeLogType.DeletedInd, 0) = 0
) tlt cross join
date d left join
TimeLog tl
on tl.Person_id = p.PersonId and tl.TimeLogTypeId = tlt.TimeLogTypeId and
d.thedate >= tl.StartDate and d.thedate <= tl.EndDate
After reading Gordon's answer here's what I've come up with. I created it in steps so I could see what was going on. I created the dates w/o the master..spt_values table. I also created a temp table of people so I could select just the ones that had a TimeLogRecord, and then re-use it to pull in details for the final select. Let me know if there's any way to make this run faster.
DECLARE
#startDate DATE,
#endDate DATE
SET #startDate = '2014-01-01'
SET #endDate = '2014-01-31'
-- create day rows --
;WITH dates(TimeLogDay) AS
(
SELECT #startDate AS TimeLogDay
UNION ALL
SELECT DATEADD(d, 1, TimeLogDay)
FROM dates
WHERE TimeLogDay < #enddate
)
-- create a type row for each day --
SELECT
dates.TimeLogDay,
tlt.TimeLogTypeID
INTO #TypeDate
FROM
dates CROSS JOIN
(SELECT
TimeLogType.TimeLogTypeID
FROM
TimeLogType
WHERE
ISNULL(TimeLogType.DeletedInd, 0) = 0
) AS TLT
-- create a temp person table for referance later ---
SELECT * INTO #person FROM Person WHERE Person.personID IN
(SELECT Timelog.PersonID FROM TimeLog WHERE TimeLog.StartDateTime BETWEEN #startDate AND #endDate)
-- sum up the log times and tie in the date/type rows --
SELECT
#TypeDate.TimeLogDay,
#TypeDate.TimeLogTypeID,
#person.PersonID,
SUM(dbo.fnCalculateHoursAsDecimal(TimeLog.StartDateTime, TimeLog.EndDateTime)) AS Hours
INTO #Hours
FROM
#person CROSS JOIN
#TypeDate LEFT JOIN
TimeLog ON
TimeLog.PersonID = #person.PersonID AND
TimeLog.TimeLogTypeID = #TypeDate.TimeLogTypeID AND
#TypeDate.TimeLogDay = CONVERT(DATE, TimeLog.StartDateTime, 101)
GROUP BY
#TypeDate.TimeLogDay,
#TypeDate.TimeLogTypeID,
#person.PersonID
-- now tie in the details to complete --
SELECT
#Hours.TimeLogDay,
TimeLogType.Description,
Person.LastName,
Person.FirstName,
#Hours.Hours
FROM
#Hours LEFT JOIN
Person ON #Hours.PersonID = Person.PersonID LEFT JOIN
TimeLogType ON #Hours.TimeLogTypeID = TimeLogType.TimeLogTypeID
ORDER BY
Person.FirstName,
Person.LastName,
#Hours.TimeLogDay,
TimeLogType.SortOrder

Multiple Selects into one select

I'm trying to put some data together for a High Charts Bar chart using ASP.NET. Basically, i have three users who i need to track when they have logged into the system. the variants to be used are:
1) Today
2) This Week
3) Last Week
4) Last Month
So, i've created individual tsql scripts for today and and last week, but i'm now a little stuck on how to combine the two statemets, which will eventually be four.
SELECT Count(*) as CountToday from hitsTable WHERE Convert(date,hitDate) =
Convert(date,GETDATE()) Group by UserId
SELECT count(*) as CountLatWeek from hitTable
where hitDate between (DATEADD(week, DATEDIFF (week,0,GETDATE()),-1))
AND getDate() Group by UserId
Searhing on google, leads me to nested select statements, which all seem to form dependacies with the two statements. However, what i need to do is produce a table of results like this:
EDIT
I've set up a SQL Fiddle, so we can test out the examples
http://www.sqlfiddle.com/#!6/a21ec
the fiddle has tsql for today and tsql for last week (which may need some tweaking)
Select Distinct
UserId
, ( Select Count(*) as CountToday from hitsTable h2
Where h2.UserId = h1.UserId
And Convert(date,hitDate) = Convert(date,GETDATE())
) As CountToday
, ( Select count(*) as CountLatWeek from hitsTable h2
Where h2.UserId = h1.UserId
And hitDate Between DATEADD(dd, -(DATEPART(dw, GetDate())-1)-7, GetDate())
And DATEADD(dd, 7-(DATEPART(dw, GetDate()))-7, GetDate())
) As CountLastWeek
FROM hitsTable h1
Here’s another alternative based on #Avinash comment on the question.
Select
UserId
, CountTodayTable.CountToday
, CountLatWeekTable.CountLatWeek
, ...
FROM hitsTable h1
Inner Join
( Select Count(*) as CountToday from hitsTable h2
Where h2.UserId = h1.UserId
And Convert(date,hitDate) = Convert(date,GETDATE())
) CountTodayTable
On CountTodayTable.UserId = h1.UserId
Inner Join
( Select count(*) as CountLatWeek from hitTable h2
Where h2.UserId = h1.UserId
And hitDate between (DATEADD(week, DATEDIFF (week,0,GETDATE()),-1)) And getDate()
) CountLatWeekTable
On CountLatWeekTable.UserId = h1.UserId
...
Try this query
select
id,
sum(case when Convert(date,hitDate) = Convert(date,GETDATE()) then 1 else 0 end) as as CountToday,
sum(hitDate between (DATEADD(week, DATEDIFF (week,0,GETDATE()),-1)) AND getDate() then 1 else 0 end) as CountLatWeek,
...... -- Add more condition
from
hitsTable
group by
UserId
Edit
select
userid,
sum(case when Convert(date,hitDate) =
Convert(date,GETDATE()) then 1 else 0 end) as cnt
from
hitstable
group by userid
FIDDLE
| USERID | CNT |
|--------|-----|
| User1 | 3 |
| User2 | 0 |