XML path not producing concatenated results in sql joined table - sql-server

I am trying to make a table that shows all the patients checked in to the hospital. I can join client, patient, check-in, appointment data just fine, but the alerts table has multiple rows which I am trying to aggregate/concatenate/rollup. I tried to create an XML statement but it doesn't seem to be working. I would like for all the alerts for the patient to be a single comma-separated string in one row. here is what I have:
select DISTINCT
a.ResourceAbbreviation1, a.AppointmentType, a.StatusNum, c.sLastName,
pt.Name, pt.WeightString, pt.AgeShort, pt.Breed, pt.Species, pt.Gender, pt.NewPatient,
(select SUBSTRING((
select ',' + al.stext AS 'data()'
FOR XML PATH('')
), 2, 9999) as cautions),
pt.Classification, p.kPatientId
from dbo.entpatients pt
join alerts al
on al.kpatientid = pt.IDPatient
join dbo.PatientCheckIns P
on pt.IDPatient=p.kPatientId
join dbo.EntAppointments a
on a.IDPatient = p.kPatientId
join dbo.clients c
on c.kID=a.IDClient
where cast (a.StartTime as date) = cast(getdate() as date)
and a.StatusNum=4;

You need to move your alerts table reference inside the subselect as a FROM.
I also suggest using AS text() instead of AS data() (or omitting it entirely) to avoid unwanted spaces, and using STUFF() instead of SUBSTRING() to strip the leading comma. The extra nested SELECT is also unneeded.
select DISTINCT
a.ResourceAbbreviation1, a.AppointmentType, a.StatusNum, c.sLastName,
pt.Name, pt.WeightString, pt.AgeShort, pt.Breed, pt.Species, pt.Gender, pt.NewPatient,
STUFF((
select ',' + al.stext AS 'text()'
from alerts al
where al.kpatientid = pt.IDPatient
FOR XML PATH('')
), 1, 1, '') as cautions,
pt.Classification,
p.kPatientId
from dbo.entpatients pt
join dbo.PatientCheckIns P
on pt.IDPatient=p.kPatientId
join dbo.EntAppointments a
on a.IDPatient = p.kPatientId
join dbo.clients c
on c.kID=a.IDClient
where cast (a.StartTime as date) = cast(getdate() as date)
and a.StatusNum=4;
If there is any chance that your alert text may contain special XML characters (such as <, >, or &) that might get encoded, I recommend a slightly modified form that uses the .value() function to extract the concatenated text.
STUFF((
select ',' + al.stext AS 'text()'
from alerts al
where al.kpatientid = pt.IDPatient
FOR XML PATH(''), TYPE
).value('text()[1]','nvarchar(max)'), 1, 1, '') as cautions,
This avoids seeing encodings like <, >, and & in the results. See this for more.
If you are using SQL server 2017 or later, you could also switch to the relatively new STRING_AGG() function. See here.
(
select STRING_AGG(',', al.stext)
from alerts al
where al.kpatientid = pt.IDPatient
) as cautions,
I would also review your need for the DISTINCT. In some cases, it is appropriate when you knowingly expect your query to return duplicate rows that you wish to eliminate. For example, if you know you may have multiple visits by the same patient with identical selected data, DISTICT may be appropriate. However, if you have dropped it in to eliminate duplicates without knowing why, it may be a sign of an under-constrained join or other logic problems that warrant a further look.

Related

Rows to columns without PIVOT in SQL Server

I have a 3 tables from which contain this data:
Table 1:
Table 2:
Table 3:
Output:
I have tried using Pivot but it has to have an aggregate function in it.
SELECT
project_code, project_name, fk_prj_project_id,
[A], [B], [C], [D]
FROM
(SELECT
project_code, project_name, employee_name,
fk_prj_project_id, fk_prj_project_id AS nm,
activity_details
FROM
PRJ_MST_PROJECT AS a
LEFT JOIN
PRJ_TNS_DAILY_SUMMARY AS b ON a.pk_prj_project_id = b.fk_prj_project_id
LEFT JOIN
HRM_EMP_MST_EMPLOYEE AS c ON b.fk_hrm_emp_employee_id = c.pk_hrm_emp_employee_id
WHERE
a.project_status = 0
AND b.transaction_status = 1
AND CONVERT(date, b.transaction_date, 103) = CONVERT(date, '15/04/2021', 103)) x
PIVOT
(MAX(nm)
FOR nm IN ([A], [B], [C], [D])
) p
The problem is you set your PIVOT to look for values of nm in A, B, C, and D, but nm is an alias for fk_prj_project_id, which has possible values of 1, 2, 3, 4, and 5. So there are no A, B, C, or D values to be had. I don't even see a name for the column that holds A, B, C, and D, but whatever column that is needs to be what you put in the "FOR ___ IN" section of your pivot.
Test your query by commenting out the reference to the pivot columns in the SELECT and comment out the word PIVOT and everything after it and re-run your query. You should see some column with values A, B, C, D. If you don't, fix your query so you do. Once you do, that column is what you PIVOT on (put it between FOR and IN in the pivot block).
Oh, and if you provide data in a usable format people might run your query and give you directly usable results, it's a lot to ask to have people enter your data to get to help you so meet them half way. A link to sqlfiddle is ideal, but even just a bunch of DECLARE #T1 and INSERT INTO T1 VALUES statements is usually enough to get significantly better help.
EDIT:
Nice job with the Fiddle!
OK, so using your data, we can test out actual queries. For PIVOT to work, we need a column to look up (employee name), a column to aggregate (activity_details), and some columns that will be constant across the rows produced (the project's name and ID). You're working with text not numbers, so your aggregation can't be mathematical, leaving you with pretty much just MAX or MIN. To make sure you get the right (newest) one, I first built a table of comments and numbered them by how new they were, then I picked just the newest comment for each (project, user) pair. cteCommentNewest is the result of that.
Now with a clean (and verified) table to pivot, the actual pivot syntax is simple. Well, as simple as Pivot can be, it's inherently pretty confusing IMHO, but structuring it this way keeps the actual PIVOT as clean as possible.
Note that the query is in twice, I tested it as a static query before converting it to dynamic because it's much easier to troubleshoot a static query, then I left it in in case you want to experiment with it. You don't need it for the final solution to work.
Here's the final code, fully tested and producing the specified output:
DECLARE #cols3 AS NVARCHAR(MAX)
DECLARE #query3 AS NVARCHAR(MAX)=''
DECLARE #dt varchar(100)='14/04/2021'
select #cols3 = STUFF((SELECT ',' + QUOTENAME(employee_name)
from dbo.HRM_EMP_MST_EMPLOYEE
order by employee_name
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
--SELECT #cols3 --Test column list for dynamic query
--Test the core functions of pivot before making dynamic
;with cteCommentsAll as (
SELECT P.project_code , P.project_name, C.activity_details , E.employee_name
, ROW_NUMBER () over (PARTITION BY P.project_code , E.employee_name ORDER BY C.transaction_date DESC) as Newness
FROM dbo.PRJ_MST_PROJECT as P --Projects
LEFT OUTER JOIN dbo.PRJ_TNS_DAILY_SUMMARY as C --Comments on projects
ON P.pk_prj_project_id = C.fk_prj_project_id --Get all projects, then all comments for each project
LEFT OUTER JOIN dbo.HRM_EMP_MST_EMPLOYEE as E --Employees who commented
on E.pk_hrm_emp_employee_id = C.fk_hrm_emp_employee_id
), cteCommentsNewest as (
SELECT project_code , project_name, activity_details , employee_name
FROM cteCommentsAll WHERE Newness = 1 --Only one comment per user per project of CROSS problems
)
SELECT *
FROM cteCommentsNewest as N --TEST up to this point to see the raw table
PIVOT (MAX(activity_details) FOR employee_name IN (A, B, C) ) as P
--Put the working query, modified for dynamic columns, into a variable
set #query3 = N'
;with cteCommentsAll as (
SELECT P.project_code , P.project_name, C.activity_details , E.employee_name
, ROW_NUMBER () over (PARTITION BY P.project_code , E.employee_name ORDER BY C.transaction_date DESC) as Newness
FROM dbo.PRJ_MST_PROJECT as P --Projects
LEFT OUTER JOIN dbo.PRJ_TNS_DAILY_SUMMARY as C --Comments on projects
ON P.pk_prj_project_id = C.fk_prj_project_id --Get all projects, then all comments for each project
LEFT OUTER JOIN dbo.HRM_EMP_MST_EMPLOYEE as E --Employees who commented
on E.pk_hrm_emp_employee_id = C.fk_hrm_emp_employee_id
), cteCommentsNewest as (
SELECT project_code , project_name, activity_details , employee_name
FROM cteCommentsAll WHERE Newness = 1 --Only one comment per user per project of CROSS problems
)SELECT *
FROM cteCommentsNewest as N
PIVOT (MAX(activity_details) FOR employee_name IN (' + #cols3 + ') ) as P
'
exec sp_executesql #query3
which produces the following output
project_code
project_name
A
B
C
MOA20171
Project A
some remark By Employee A on 14
NULL
some remark By Employee C on 14
MOA20172
Project B
NULL
NULL
some remark By Employee C on 15
MOA20173
Project C
NULL
NULL
NULL

Combine NVARCHAR column with the result of a SUM in SQL Server

How can I combine a NVARCHAR column with the result of a SUM in SQL Server?
I have tried
dbo.tblCurrencies.strCurrencySymbol + dbo.tblItems.dcUnitPrice * dbo.tblItems.intItemQuantity
Which tells me:
Error converting data type nvarchar to numeric
So I tried
dbo.tblCurrencies.strCurrencySymbol + CAST(SUM(dbo.tblItems.dcUnitPrice * dbo.tblItems.intItemQuantity) AS NVARCHAR)
AND
CAST(dbo.tblCurrencies.strCurrencySymbol + SUM(dbo.tblItems.dcUnitPrice * dbo.tblSalesOrderItems.intItemQuantity) AS NVARCHAR)
Both tell me
Cannot use an aggregate or a subquery in an expression used for the group by list of a group by clause.
Any more information you need just let me know I'll put it up.
EDIT:
The query
SELECT dbo.tblItems.fkOrder, dbo.tblCurrencies.strCurrencySymbol + SUM(dbo.tblItems.dcUnitPrice * dbo.tblItems.intItemQuantity) AS TotalPrice
FROM dbo.tblCurrencies RIGHT OUTER JOIN
dbo.tblOrders ON dbo.tblCurrencies.pkCurrency = dbo.tblOrders.fkPaysInCurrency RIGHT OUTER JOIN
dbo.tblItems ON dbo.tblOrders.pkOrder = dbo.tblItems.fkOrder
GROUP BY dbo.tblItems.fkOrder
EDIT 2:
Ok I managed to solve it by adding dbo.tblCurrencies.strCurrencySymbol into the group by and using CONCAT()
My query now looks like this:
SELECT dbo.tblItems.fkOrder, { fn CONCAT(dbo.tblCurrencies.strCurrencySymbol, CAST(SUM(dbo.tblItems.dcUnitPrice * dbo.tblItems.intItemQuantity) AS NVARCHAR(10))) } AS TotalPrice
FROM dbo.tblCurrencies RIGHT OUTER JOIN
dbo.tblOrders ON dbo.tblCurrencies.pkCurrency = dbo.tblOrders.fkPaysInCurrency RIGHT OUTER JOIN
dbo.tblItems ON dbo.tblOrders.pkOrder = dbo.tblItems.fkOrder
GROUP BY dbo.tblItems.fkOrder, dbo.tblCurrencies.strCurrencySymbol
It also works without the CONCAT()
SELECT dbo.tblItems.fkOrder, dbo.tblCurrencies.strCurrencySymbol + CAST(SUM(dbo.tblItems.dcUnitPrice * dbo.tblItems.intItemQuantity) AS NVARCHAR(10)) AS TotalPrice
FROM dbo.tblCurrencies RIGHT OUTER JOIN
dbo.tblOrders ON dbo.tblCurrencies.pkCurrency = dbo.tblOrders.fkPaysInCurrency RIGHT OUTER JOIN
dbo.tblItems ON dbo.tblOrders.pkOrder = dbo.tblItems.fkOrder
GROUP BY dbo.tblItems.fkOrder, dbo.tblCurrencies.strCurrencySymbol
Not sure which is better though?
In SQL Server there's a concept named Data Type Precedence. It's there in cases such as these when two different data types need to be combined because there needs to be a method to decide on what the data type of the final output will be.
In your case you're combining NVARCHAR and NUMERIC data types and as NUMERIC has the higher precedence the approach it takes is to try and convert your text data into a number which it can't do, hence the error.
If you need the conversion to run in a way other than defined in the normal precedence order then you need to explicitly make the conversion yourself with either CAST or CONVERT. In your case you need to multiply the needed numbers, convert those to text and then concatenate the currency symbol.
Subsequent errors come from using fields that are neither within an aggregate or specified in the Group By of your select statement. Adding you currency field in there should resolve the other issue assuming you can't have one order with multiple currencies involved.
Putting it all together gives you the following:
SELECT
Orders.fkOrder,
Currencies.strCurrencySymbol
+ CAST(SUM(Items.dcUnitPrice * Items.intItemQuantity) AS varchar(50)) AS TotalPrice
FROM dbo.tblItems AS Items
LEFT OUTER JOIN dbo.tblOrders AS Orders
ON Items.fkOrder = Orders.pkOrder
LEFT OUTER JOIN dbo.tblCurrencies AS Currencies
ON Orders.fkPaysInCurrency = Currencies.pkCurrency
GROUP BY Orders.fkOrder, Currencies.strCurrencySymbol
Based on the query fragment that you have posted, I would try something along the lines of
dbo.tblCurrencies.strCurrencySymbol +
CAST(SUM(dbo.tblItems.dcUnitPrice * dbo.tblSalesOrderItems.intItemQuantity) AS NVARCHAR(x))
Points to note: the cast to NVARCHAR is done on the sum part only
x is the length of the NVARCHAR string that you wish to cast it to (make sure that it is wide enough to hold the largest value that the sum can return).
Using the schema name prior to the column name is deprecated.

Optimizing SQL Function

I'm trying to optimize or completely rewrite this query. It takes about ~1500ms to run currently. I know the distinct's are fairly inefficient as well as the Union. But I'm struggling to figure out exactly where to go from here.
I am thinking that the first select statement might not be needed to return the output of;
[Key | User_ID,(User_ID)]
Note; Program and Program Scenario are both using Clustered Indexes. I can provide a screenshot of the Execution Plan if needed.
ALTER FUNCTION [dbo].[Fn_Get_Del_User_ID] (#_CompKey INT)
RETURNS VARCHAR(8000)
AS
BEGIN
DECLARE #UseID AS VARCHAR(8000);
SET #UseID = '';
SELECT #UseID = #UseID + ', ' + x.User_ID
FROM
(SELECT DISTINCT (UPPER(p.User_ID)) as User_ID FROM [dbo].[Program] AS p WITH (NOLOCK)
WHERE p.CompKey = #_CompKey
UNION
SELECT DISTINCT (UPPER(ps.User_ID)) as User_ID FROM [dbo].[Program] AS p WITH (NOLOCK)
LEFT OUTER JOIN [dbo].[Program_Scenario] AS ps WITH (NOLOCK) ON p.ProgKey = ps.ProgKey
WHERE p.CompKey = #_CompKey
AND ps.User_ID IS NOT NULL) x
RETURN Substring(#UserIDs, 3, 8000);
END
There are two things happening in this query
1. Locating rows in the [Program] table matching the specified CompKey (#_CompKey)
2. Locating rows in the [Program_Scenario] table that have the same ProgKey as the rows located in (1) above.
Finally, non-null UserIDs from both these sets of rows are concatenated into a scalar.
For step 1 to be efficient, you'd need an index on the CompKey column (clustered or non-clustered)
For step 2 to be efficient, you'd need an index on the join key which is ProgKey on the Program_Scenario table (this likely is a non-clustered index as I can't imagine ProgKey to be PK). Likely, SQL would resort to a loop join strategy - i.e., for each row found in [Program] matching the CompKey criteria, it would need to lookup corresponding rows in [Program_Scenario] with same ProgKey. This is a guess though, as there is not sufficient information on the cardinality and distribution of data.
Ensure the above two indexes are present.
Also, as others have noted the second left outer join is a bit confusing as an inner join is the right way to deal with it.
Per my interpretation the inner part of the query can be rewritten this way. Also, this is the query you'd ideally run and optimize before tacking the string concatenation part. The DISTINCT is dropped as it is automatic with a UNION. Try this version of the query along with the indexes above and if it provides the necessary boost, then include the string concatenation or the xml STUFF approaches to return a scalar.
SELECT UPPER(p.User_ID) as User_ID
FROM
[dbo].[Program] AS p WITH (NOLOCK)
WHERE
p.CompKey = #_CompKey
UNION
SELECT UPPER(ps.User_ID) as User_ID
FROM
[dbo].[Program] AS p WITH (NOLOCK)
INNER JOIN [dbo].[Program_Scenario] AS ps WITH (NOLOCK) ON p.ProgKey = ps.ProgKey
WHERE
p.CompKey = #_CompKey
AND ps.User_ID IS NOT NULL
I am taking a shot in the dark here. I am guessing that the last code you posted is still a scalar function. It also did not have all the logic of your original query. Again, this is a shot in the dark since there is no table definitions or sample data posted.
This might be how this would look as an inline table valued function.
ALTER FUNCTION [dbo].[Fn_Get_Del_User_ID]
(
#_CompKey INT
) RETURNS TABLE AS RETURN
select MyResult = STUFF(
(
SELECT distinct UPPER(p.User_ID) as User_ID
FROM dbo.Program AS p
WHERE p.CompKey = #_CompKey
group by p.User_ID
UNION
SELECT distinct UPPER(ps.User_ID) as User_ID
FROM dbo.Program AS p
LEFT OUTER JOIN dbo.Program_Scenario AS ps ON p.ProgKey = ps.ProgKey
WHERE p.CompKey = #_CompKey
AND ps.User_ID IS NOT NULL
for xml path ('')
), 1, 1, '')
from dbo.Program

Left Join INT on Varchar

Im writing a report to show what features our clients want when building there home.
I want to left join 2 tables however the way the data is stored its making it difficult for me to do the join.
Table 1 tbl_Main_Holding has a field called Requirements and the data is stored as a varchar and can have multiple values like 1,4,7
1 = "Eco-Build"
4 = "Conservatory"
7 = "Basement"
Table 2 [tbl_Features] has the fields ID (INT) and Description (Varchar)
SELECT * FROM dbo.tbl_Main_Holding AS rm
LEFT JOIN [dbo].[tbl_Features] AS f
ON rm.Requirements = f.id
The join below wont work as i would need to convert the varchar to INT
However that's not my problem my problem is how do i show the results of clients that have selected multiple feature, how dopes this left join work?
Im using SQL Server 2008 and the data for both tables are store as so.
Step 1 is to go and find the person that designed this table structure (even if it is you) then whack them round the head with a stick.
Step 2 is to redesign the tables, a junction table is what is required here, not stuffing multiple integers into a single varchar column. For good measure at the end of step two you should hit the original designer with a stick again.
CREATE TABLE tbl_Main_Holding_Requirements
(
MainHoldingID INT NOT NULL, --FK TO `tbl_main_Holding`
FeatureID INT NOT NULL -- FK TO Require `tbl_Features`
);
Now, each requirement represents a row in this table, rather than a new item on your list, so your join is now simple:
SELECT *
FROM dbo.tbl_Main_Holding AS rm
LEFT JOIN dbo.tbl_Main_Holding_Requirements AS r
ON r.MainHoldingID = rm.ID
LEFT JOIN [dbo].[tbl_Features] AS f
ON f.ID = r.FeatureID;
If you need to bring this back up to a comma delimited list, then you can do it in the presentation layer, or with SQL-Server's XML Extensions:
SELECT *,
Features = STUFF(f.Features.value('.', 'NVARCHAR(MAX)'), 1, 1, '')
FROM dbo.tbl_Main_Holding AS rm
OUTER APPLY
( SELECT CONCAT(',', f.Description)
FROM dbo.tbl_Main_Holding_Requirements AS r
INNER JOIN [dbo].[tbl_Features] AS f
ON f.ID = r.FeatureID
WHERE r.MainHoldingID = rm.ID
FOR XML PATH(''), TYPE
) f (Features);
If step two is not possible, then you can get around this using LIKE:
SELECT *
FROM dbo.tbl_Main_Holding AS rm
LEFT JOIN [dbo].[tbl_Features] AS f
ON ',' + rm.Requirements + ',' LIKE '%,' + CONVERT(VARCHAR(10), f.ID) + ',%';
Once again, if the features need to be reduced back to a single row, then you can use XML extensions again:
SELECT *,
Features = STUFF(f.Features.value('.', 'NVARCHAR(MAX)'), 1, 1, '')
FROM dbo.tbl_Main_Holding AS rm
OUTER APPLY
( SELECT CONCAT(',', f.Description)
FROM [dbo].[tbl_Features] AS f
WHERE ',' + rm.Requirements + ',' LIKE '%,' + CONVERT(VARCHAR(10), f.ID) + ',%'
FOR XML PATH(''), TYPE
) f (Features);
Another option is to split the comma separated values into a list using some kind of Split function, but as the testing in this article shows, if you don't need to access the individual values from the list, it is more efficient to just use LIKE.
As I wrote in my comment, please read Is storing a delimited list in a database column really that bad?
You really should normalize your database to avoid these things.
Now, assuming you can't change the database schema, there is a simple trick with like that you can use:
SELECT * FROM dbo.tbl_Main_Holding AS rm
LEFT JOIN [dbo].[tbl_Features] AS f
ON ',' + rm.Requirements +',' LIKE '%,' + CAST(f.id as varchar(10)) + ',%'
Note that I've added a comma before and after the rm.Requirements column and also before and after the f.id column.

Concatenating Column Values into a Comma-Separated string [duplicate]

This question already has answers here:
How to use GROUP BY to concatenate strings in SQL Server?
(22 answers)
Closed 7 years ago.
I have a table which looks like the following:
EventProfileID ParamName ParamValue
1 _CommandText usp_storedproc_1
2 _CommandText usp_storedproc_2
2 _CommandText usp_storedproc_3
2 _CommandText usp_storedproc_100
3 _CommandText usp_storedproc_11
3 _CommandText usp_storedproc_123
What I would like my output to be is the following:
EventProfileID ParamValue
1 usp_storedproc_1
2 usp_storedproc_2, usp_storedproc_3, usp_storedproc_100
3 usp_storedproc_11, usp_storedproc_123
However I am having some bother. If I do a select on one of the event profile ID's I can get an output using the following logic:
SELECT LEFT(c.ParamValue, LEN(c.ParamValue) - 1)
FROM (
SELECT a.ParamValue + ', '
FROM DP_EventProfileParams AS a
WHERE a.ParamName = '_CommandText'
and a.EventProfileId = '13311'
FOR XML PATH ('')
) c (paramvalue)
However that just gives me the output for one EventProfileID and also I would like the EventProfileID as part of the output.
Can anyone give me any pointers in the right direction into how I can expand my code to include this and allow the code to be dynamic so that I can show all EventProfileID's?
Thanks
You can do it this way:
select distinct a.EventProfileID,
stuff((select ','+ ParamValue)
from DP_EventProfileParams s
where s.EventProfileID = a.EventProfileID
for XML path('')),1,1,'')
from DP_EventProfileParams a
You were on the right track with for XML path. STUFF function makes it easier to achieve what you want.
The original query does not work because it uses simple subquery (works only for one specific id)
To make it work for all ids you can use XML + STUFF inside correlated subquery:
Many queries can be evaluated by executing the subquery once and
substituting the resulting value or values into the WHERE clause of
the outer query. In queries that include a correlated subquery (also
known as a repeating subquery), the subquery depends on the outer
query for its values. This means that the subquery is executed
repeatedly, once for each row that might be selected by the outer
query.
SELECT DISTINCT
EventProfileID,
[ParamVaues] =
STUFF((SELECT ',' + d2.ParamValue
FROM #DP_EventProfileParams d2
WHERE d1.EventProfileID = d2.EventProfileID
AND d2.ParamName = '_CommandText'
FOR XML PATH('')), 1, 1, '')
FROM #DP_EventProfileParams d1
ORDER BY EventProfileID;
LiveDemo
I strongly suggest reading Concatenating Row Values in Transact-SQL

Resources