Combine results to count totals for individuals - sql

I have a table in my database that keeps track of pages sent to individual users and groups. Users are part of groups. Only individual users can answer pages. Here is the DDL for the table:
--PageStatus 1 = Expired
--PageStatus 2 = Answered
--PageStatus 3 = Canceled
CREATE TABLE [Pagings] (
[Id] int NOT NULL IDENTITY(1,1) ,
[UserProfileId] int NULL ,
[GroupId] int NULL ,
[Message] nvarchar(MAX) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ,
[PageStatus] int NOT NULL ,
[DateCreated] datetime NULL ,
[DateModified] datetime NULL ,
[IsRecurring] bit NOT NULL DEFAULT ((0)) ,
[AnsweredById] int NULL ,
[AnsweredDateTime] datetime NULL ,
CONSTRAINT [PK_ft.Pagings] PRIMARY KEY ([Id])
)
ON [PRIMARY]
TEXTIMAGE_ON [PRIMARY]
GO
Anytime the PageStatus is Expired (1) or Canceled (3) we do not have any data for the AnsweredById or the AnsweredDateTime columns. If it is answered then we set the value of the UserProfileId coming from the application in the AnsweredById column of the person who answered it. If a group is paged anyone who answered the page is assumed to be part of that group and their UserProfileId is set inside the AnsweredById column.
Here is a sample result and the SqlFiddle to accompany the data.
I need to figure out how to get the Total count of pages for a User including the group they belong to, how many pages they answered, and the group totals. Here is an example of what I would expect as the result based on the set above:
UserId GroupId TotalPagesForUser TotalAnsweredForUser TotalPagesForGroup TotalAnsweredForGroup
------ ------- ----------------- -------------------- ------------------ ----------------------
1 2 3 1 1 1
3 1 3 1 2 2
4 1 2 1 2 2
I've tried joining the table to itself on the UserProfileId and AnsweredById and with a Group table that exists in the database, but my results were way off and i end up with a a lot of duplicated data.

I would break it into two parts, first assemble the aggregate numbers for Users, then get the Group numbers either in a subquery as part of the main query or two separate queries with the results being assembled at the end. Anyway, my rough first attempt:
Select u.Id as UserId,
g.Id as GroupId,
g.name as GroupName,
count(0) as TotalPagesForUser,
Sum(case when p.AnsweredById IS NOT NULL then 1 else 0 end) as TotalAnsweredForUser,
(SELECT COUNT(0) FROM Pagings WHERE GroupId = g.Id) as TotalPagesForGroup,
(SELECT COUNT(0) FROM Pagings WHERE GroupId = g.Id AND AnsweredById IS NOT NULL) as TotalAnsweredForGroup
from UserProfiles u
INNER JOIN Groups g on g.Id = u.GroupId
INNER JOIN Pagings p on p.UserProfileId = u.Id or p.AnsweredById = u.Id
GROUP BY u.Id, g.Id, g.name
ORDER BY u.Id
Although I'm getting slightly different values than what you were projecting...but I haven't had a chance to look over the source data in detail yet.

Related

How use case statement alias in where clause

I'm trying to use a case statement and the results to in an alias, but I need to use the alias in my where clause and this doesn't seem to be working. How do I use the alias (isPrimary below) in my where clause. See comment where I'm trying to use isPrimary in my where clause, which doesn't work. How do I use the alias in the where clause?
CREATE TABLE #cases
(
id varchar(25),
CASEID VARCHAR(12)
)
#cases:
id caseid
15 12345
15 23456
CREATE TABLE #services
(
id varchar(25),
CASEID VARCHAR(12),
createdate VARCHAR(30),
types int
)
#services:
id caseid createdate types
15 12345 2021-04-27 11:59:01.333 null --this is the primary one
16 12345 2021-04-28 07:37:20.163 null
17 12345 2021-04-28 07:55:08.750 10
select c.caseid,
CASE WHEN sv.id = (SELECT Top 1 ID FROM #services WHERE caseid = c.caseid ORDER BY createdate ASC) THEN 1 ELSE 0 END AS isPrimary --if lowest date for sv.caseid then label "1", otherwise "0"
from
#cases c
left join #services sv on sv.caseid=c.caseid
where
(isPrimary=0 and types is null) --it doesn't want me to use the alias here
I was looking at [case alias where][1] but it doesn't try to use the alias in the where clause. I don't see how to do that in my search. I need to return the null "types" that aren't primary. There are multiple cases, not just the one in the services table.
You could do your initial query as a CTE to get the alias and then use your WHERE condition on that resulting data set.
;with cte
as (
select c.caseid, types,
CASE WHEN sv.id = (SELECT Top 1 ID FROM #services WHERE caseid = c.caseid ORDER BY createdate ASC) THEN 1 ELSE 0 END AS isPrimary
from
#cases c
left join #services sv on sv.caseid=c.caseid)
select *
from cte
where
(isPrimary=0 and types is null)

how to use SQL table rows as columns for another table

I have one table of activities, one table of users, and a third table linking users to activities using foreign keys.
What I'm trying to do is create a results table that will have the activities as columns and the users as rows with the cells being the number of activities of that type the user participated in.
For example, the columns would be
User | Activity A | Activity B | Activity C
And a user who had done each activity three times would result in a row of
John Doe | 3 | 3 | 3
Now, I can do this easily if I manually add a count() call for each activity in the database like:
select
u.name,
(select count(*)
from userActivity ua
where ua.userID = user.userID and ua.activityID = 1),
(select count(*)
from userActivity ua
where ua.userID = user.userID and ua.activityID = 2),
(select count(*)
from userActivity ua
where ua.userID = user.userID and ua.activityID = 3)
from
user u
But this doesn't help me if someone enters an Activity D into the system tomorrow. The report wouldn't show it. How can I use the Activity table's rows as columns?
I did a quick query that might help. This uses the Pivot function, which was mentioned before.
You can run the whole thing, or just skip to the bottom!
-- Temp tables
IF OBJECT_ID('tempdb.dbo.#_tmp') IS NOT NULL DROP TABLE #_tmp
IF OBJECT_ID('tempdb.dbo.#_user') IS NOT NULL DROP TABLE #_user
IF OBJECT_ID('tempdb.dbo.#_activity') IS NOT NULL DROP TABLE #_activity
IF OBJECT_ID('tempdb.dbo.#_useractivity') IS NOT NULL DROP TABLE #_useractivity
-- User table
CREATE TABLE #_user (
[USER_ID] INT IDENTITY(1,1) NOT NULL,
[FIRST_NAME] NVARCHAR(50)
)
INSERT INTO #_user ([FIRST_NAME])
VALUES ('John'), ('Peter'), ('Paul')
-- Activity table
CREATE TABLE #_activity (
[ACTIVITY_ID] INT IDENTITY(1,1) NOT NULL,
[ACTIVITY_NAME] NVARCHAR(255)
)
INSERT INTO #_activity ([ACTIVITY_NAME])
VALUES ('Sailing'), ('Bowling'), ('Hiking')
-- Composite table
CREATE TABLE #_useractivity (
[LOG_ID] INT IDENTITY(1,1) NOT NULL,
[USER_ID] INT,
[ACTIVITY_ID] INT
)
INSERT INTO #_useractivity ([USER_ID], [ACTIVITY_ID])
VALUES (1,1),(1,2),(1,3),(1,3),(2,2),(2,3),(3,1), (3,2),(1,2),(2,1)
-- Main data table.
SELECT USR.FIRST_NAME
, A.ACTIVITY_NAME
INTO #_tmp
FROM #_useractivity AS UA
INNER JOIN #_user AS USR ON USR.USER_ID = UA.USER_ID
INNER JOIN #_activity AS A ON A.ACTIVITY_ID = UA.ACTIVITY_ID
SELECT * FROM #_tmp
-- Use pivot function to get desired results.
DECLARE #_cols AS NVARCHAR(MAX)
DECLARE #_sql AS NVARCHAR(MAX)
SET #_cols = STUFF((SELECT ',' + QUOTENAME(T.ACTIVITY_NAME)
FROM #_tmp AS T
GROUP BY T.ACTIVITY_NAME
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'),1,1,'')
-- Trick is to add 1 "counter" before pivoting.
set #_sql = '
SELECT Name, ' + #_cols + '
FROM (
SELECT FIRST_NAME AS Name, ACTIVITY_NAME, 1 AS COUNT
FROM #_tmp
) AS SRC
PIVOT (
SUM(COUNT) FOR ACTIVITY_NAME IN (' + #_cols + ')
) p'
EXEC(#_sql)
Main data table:
FIRST_NAME ACTIVITY_NAME
John Sailing
John Bowling
John Hiking
John Hiking
Peter Bowling
Peter Hiking
Paul Sailing
Paul Bowling
John Bowling
Peter Sailing
Output:
Name Bowling Hiking Sailing
John 2 2 1
Paul 1 NULL 1
Peter 1 1 1
You seem to want conditional aggregation:
select u.name,
sum(case when ua.activityID = 1 then 1 else 0 end) as cnt_1,
sum(case when ua.activityID = 2 then 1 else 0 end) as cnt_2,
sum(case when ua.activityID = 3 then 1 else 0 end) as cnt_3
from user u left join
userActivity ua
on ua.userID = u.userID
group by u.name;

Needed helpful hand with a bit complicated query

I have a table 'Tasks' with the following structure
[TaskId],[CompanyId], [Year], [Month], [Value]
220,1,2018,1,50553.32
220,2,2018,2,222038.12
and another table where users have permissions to particular companies in table named 'UsersCopmpanies'
[UserId], [CompanyId]
1,1
and the thing is task no. 220 was moved between companies. In January task belonged to copmanyId=1 and than in February this task belonged to copmanyId = 2.
According to the table 'UsersCopmpanies' user does not have permision to compnayid = 2.
What I need to do is to get both rows from table 'Tasks' expect field Value, because user does not have persmission.
Expected result should be:
[TaskId], [CompanyId], [Year], [Month],[Value]
220,1,2018,1,50553.32
220,2,2018,2,(NULL or somenthing else for.example string 'lack of permission')
You can use a left join:
select t.TaskId, t.CompanyId, t.Year, t.Month,
(case when uc.CompanyId is not null then Value end) as Value
from tasks t left join
UsersCompanies uc
on uc.CompanyId = t.CompanyId and uc.UserId = 1;
I think this query using LEFT JOIN can be work at you expected :
CREATE TABLE #MyTasks
(TaskId int,
CompanyId int,
YearCol varchar(50),
MonthCol varchar(50),
SomeValue varchar(50)
);
GO
INSERT INTO #MyTasks
SELECT 220,1,2018,1,50553.32
UNION
SELECT 220,2,2018,2,222038.12
CREATE TABLE #MyUsersCopmpanies
(UserId int PRIMARY KEY,
CompanyId varchar(50)
);
GO
INSERT INTO #MyUsersCopmpanies
SELECT 1,1
DECLARE #MyUserParam INT = 1;
SELECT #MyTasks.TaskId, #MyTasks.CompanyId, #MyTasks.YearCol, #MyTasks.MonthCol,
CASE WHEN #MyUsersCopmpanies.UserId IS NOT NULL THEN #MyTasks.SomeValue ELSE 'lack of permission' END AS 'ValueTaskByPermissions'
FROM #MyTasks
LEFT JOIN #MyUsersCopmpanies ON #MyUsersCopmpanies.CompanyId = #MyTasks.CompanyId AND #MyUsersCopmpanies.UserId = #MyUserParam;
DROP TABLE #MyTasks
DROP TABLE #MyUsersCopmpanies
RESULT :
TaskId CompanyId YearCol MonthCol ValueTaskByPermissions
220 1 2018 1 50553.32
220 2 2018 2 lack of permission
Some code :
SELECT t.taskid,t.companyid,t.year,t.month,
(CASE WHEN u.companyid IS NOT NULL THEN t.value ELSE "lack of permission" end) AS ValueData
FROM `x_task` t LEFT JOIN x_userscopmpanies u ON u.companyid = t.companyid

More efficient way of doing multiple joins to the same table and a "case when" in the select

At my organization clients can be enrolled in multiple programs at one time. I have a table with a list of all of the programs a client has been enrolled as unique rows in and the dates they were enrolled in that program.
Using an External join I can take any client name and a date from a table (say a table of tests that the clients have completed) and have it return all of the programs that client was in on that particular date. If a client was in multiple programs on that date it duplicates the data from that table for each program they were in on that date.
The problem I have is that I am looking for it to only return one program as their "Primary Program" for each client and date even if they were in multiple programs on that date. I have created a hierarchy for which program should be selected as their primary program and returned.
For Example:
1.)Inpatient
2.)Outpatient Clinical
3.)Outpatient Vocational
4.)Outpatient Recreational
So if a client was enrolled in Outpatient Clinical, Outpatient Vocational, Outpatient Recreational at the same time on that date it would only return "Outpatient Clinical" as the program.
My way of thinking for doing this would be to join to the table with the previous programs multiple times like this:
FROM dbo.TestTable as TestTable
LEFT OUTER JOIN dbo.PreviousPrograms as PreviousPrograms1
ON TestTable.date = PreviousPrograms1.date AND PreviousPrograms1.type = 'Inpatient'
LEFT OUTER JOIN dbo.PreviousPrograms as PreviousPrograms2
ON TestTable.date = PreviousPrograms2.date AND PreviousPrograms2.type = 'Outpatient Clinical'
LEFT OUTER JOIN dbo.PreviousPrograms as PreviousPrograms3
ON TestTable.date = PreviousPrograms3.date AND PreviousPrograms3.type = 'Outpatient Vocational'
LEFT OUTER JOIN dbo.PreviousPrograms as PreviousPrograms4
ON TestTable.date = PreviousPrograms4.date AND PreviousPrograms4.type = 'Outpatient Recreational'
and then do a condition CASE WHEN in the SELECT statement as such:
SELECT
CASE
WHEN PreviousPrograms1.name IS NOT NULL
THEN PreviousPrograms1.name
WHEN PreviousPrograms1.name IS NULL AND PreviousPrograms2.name IS NOT NULL
THEN PreviousPrograms2.name
WHEN PreviousPrograms1.name IS NULL AND PreviousPrograms2.name IS NULL AND PreviousPrograms3.name IS NOT NULL
THEN PreviousPrograms3.name
WHEN PreviousPrograms1.name IS NULL AND PreviousPrograms2.name IS NULL AND PreviousPrograms3.name IS NOT NULL AND PreviousPrograms4.name IS NOT NULL
THEN PreviousPrograms4.name
ELSE NULL
END as PrimaryProgram
The bigger problem is that in my actual table there are a lot more than just four possible programs it could be and the CASE WHEN select statement and the JOINs are already cumbersome enough.
Is there a more efficient way to do either the SELECTs part or the JOIN part? Or possibly a better way to do it all together?
I'm using SQL Server 2008.
You can simplify (replace) your CASE by using COALESCE() instead:
SELECT
COALESCE(PreviousPrograms1.name, PreviousPrograms2.name,
PreviousPrograms3.name, PreviousPrograms4.name) AS PreviousProgram
COALESCE() returns the first non-null value.
Due to your design, you still need the JOINs, but it would be much easier to read if you used very short aliases, for example PP1 instead of PreviousPrograms1 - it's just a lot less code noise.
You can simplify the Join by using a bridge table containing all the program types and their priority (my sql server syntax is a bit rusty):
create table BridgeTable (
programType varchar(30),
programPriority smallint
);
This table will hold all the program types and the program priority will reflect the priority you've specified in your question.
As for the part of the case, that will depend on the number of records involved. One of the tricks that I usually do is this (assuming programPriority is a number between 10 and 99 and no type can have more than 30 bytes, because I'm being lazy):
Select patient, date,
substr( min(cast(BridgeTable.programPriority as varchar) || PreviousPrograms.type), 3, 30)
From dbo.TestTable as TestTable
Inner Join dbo.BridgeTable as BridgeTable
Left Outer Join dbo.PreviousPrograms as PreviousPrograms
on PreviousPrograms.type = BridgeTable.programType
and TestTable.date = PreviousPrograms.date
Group by patient, date
You can achieve this using sub-queries, or you could refactor it to use CTEs, take a look at the following and see if it makes sense:
DECLARE #testTable TABLE
(
[id] INT IDENTITY(1, 1),
[date] datetime
)
DECLARE #previousPrograms TABLE
(
[id] INT IDENTITY(1,1),
[date] datetime,
[type] varchar(50)
)
INSERT INTO #testTable ([date])
SELECT '2013-08-08'
UNION ALL SELECT '2013-08-07'
UNION ALL SELECT '2013-08-06'
INSERT INTO #previousPrograms ([date], [type])
-- a sample user as an inpatient
SELECT '2013-08-08', 'Inpatient'
-- your use case of someone being enrolled in all 3 outpation programs
UNION ALL SELECT '2013-08-07', 'Outpatient Recreational'
UNION ALL SELECT '2013-08-07', 'Outpatient Clinical'
UNION ALL SELECT '2013-08-07', 'Outpatient Vocational'
-- showing our workings, this is what we'll join to
SELECT
PPP.[date],
PPP.[type],
ROW_NUMBER() OVER (PARTITION BY PPP.[date] ORDER BY PPP.[Priority]) AS [RowNumber]
FROM (
SELECT
[type],
[date],
CASE
WHEN [type] = 'Inpatient' THEN 1
WHEN [type] = 'Outpatient Clinical' THEN 2
WHEN [type] = 'Outpatient Vocational' THEN 3
WHEN [type] = 'Outpatient Recreational' THEN 4
ELSE 999
END AS [Priority]
FROM #previousPrograms
) PPP -- Previous Programs w/ Priority
SELECT
T.[date],
PPPO.[type]
FROM #testTable T
LEFT JOIN (
SELECT
PPP.[date],
PPP.[type],
ROW_NUMBER() OVER (PARTITION BY PPP.[date] ORDER BY PPP.[Priority]) AS [RowNumber]
FROM (
SELECT
[type],
[date],
CASE
WHEN [type] = 'Inpatient' THEN 1
WHEN [type] = 'Outpatient Clinical' THEN 2
WHEN [type] = 'Outpatient Vocational' THEN 3
WHEN [type] = 'Outpatient Recreational' THEN 4
ELSE 999
END AS [Priority]
FROM #previousPrograms
) PPP -- Previous Programs w/ Priority
) PPPO -- Previous Programs w/ Priority + Order
ON T.[date] = PPPO.[date] AND PPPO.[RowNumber] = 1
Basically we have our deepest sub-select giving all PreviousPrograms a priority based on type, then our wrapping sub-select gives them row numbers per date so we can select only the ones with a row number of 1.
I am guessing you would need to include a UR Number or some other patient identifier, simply add that as an output to both sub-selects and change the join.

SQL to move rows up or down in two-table arrangement

I have two tables that I designed this way with a possible reshuffling of elements in mind:
1. [dbo.test_db_002] with columns:
[id] = INT NOT NULL IDENTITY(1,1) PRIMARY KEY
[name] = NVARCHAR(255)
and
2. [dbo.test_db_003] with columns:
[ord] = INT
[itmid] = INT NOT NULL PRIMARY KEY
[itmid] column has a constraint linking it to [dbo.test_db_002].[id] like so:
ALTER TABLE [dbo.test_db_003]
ADD CONSTRAINT fk1 FOREIGN KEY ([itmid])
REFERENCES [dbo.test_db_002]([id])
ON DELETE CASCADE ON UPDATE CASCADE;
Say, [dbo.test_db_002] table has the following data:
[id] [name]
3 John
5 Mary
8 Michael
10 Steve
13 Jack
20 Pete
and [dbo.test_db_003] has the following ordering data:
[ord] [itmid]
1 5
4 8
5 13
8 3
10 10
13 20
So when I retrieve names from the database I use the following SQL:
SELECT [name]
FROM [dbo.test_db_002] t1
LEFT JOIN [dbo.test_db_003] t2 ON t1.[id]=t2.[itmid]
ORDER BY t2.[ord] ASC
It produces the list of names (ordered by the [dbo.test_db_003].[ord] column):
Mary
Michael
Jack
John
Steve
Pete
What I am looking for is an option to move each of the names up and down the list. For instance, if I want to move "John" one position up, what do I do?
So far I came up with this partial SQL:
WITH cte AS
(
SELECT [id], [ord], ROW_NUMBER() OVER (ORDER BY t2.[ord] ASC) AS rowNum
FROM [dbo.test_db_002] t1
LEFT JOIN [dbo.test_db_003] t2 ON t1.[id] = t2.[itmid]
)
That will select the following:
rowNum [id] [ord]
1 1 5
2 4 8
3 5 13
4 8 3
5 10 10
6 13 20
So I understand that I need to shift values in [ord] column up by one starting from the index 3 (since "John" index is 4) and then somehow make "John"'s [ord] to be set to 5, but how do you do that?
I prepared a complete demo for you how this can work on data.stackexchange.com.
The solution is tailored to your comment:
the move up or down can be only a single step - in other words, one
cannot move 2 or more positions
In the example I make John trade ordinal positions with Jack above him:
WITH x AS (
SELECT t2.itmid, t2.ord
FROM dbo.test_db_002 t1
LEFT JOIN dbo.test_db_003 t2 ON (t1.id = t2.itmid)
WHERE t1.name = 'John' -- must be unique, or query by id ...
)
, y AS (
SELECT TOP 1
t.itmid, t.ord
FROM dbo.test_db_003 t, x
WHERE t.ord < x.ord -- smaller ord = "above"
ORDER BY t.ord DESC
)
UPDATE dbo.test_db_003 SET ord = z.ord
FROM (
SELECT x.itmid, y.ord FROM x,y
UNION ALL
SELECT y.itmid, x.ord FROM x,y
) z
WHERE dbo.test_db_003.itmid = z.itmid
###Major points:
Use two CTE to structure the query:
Get John's id & ordinal position
Get the same for the person above him
Prepare two rows where these two switch ordinal numbers with the help of UNION ALL
Use these two rows in a now simple UPDATE
The ordinal position ord must allow passing duplicates for this to work.
If there is nobody 'above', the query will silently do nothing.