Here in my app I have problems, and a list of people who faced them. Today I'm retrieving all people names through STUFF function like the sample below:
select problem.*,
(
STUFF
(
(
SELECT TOP(3)', ' + person.name
FROM
problem_person
LEFT JOIN person ON problem_person.personId = person.Id
WHERE
problem_person.problemId = problem.Id
order by person.name
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'), 1, 1, ''
)
) as peopleWhoFaced from problem
However, when a lot of people face the same problem, peopleWhoFaced field gets huge. How to retrieve something like Person 1, Person 2 and 36 more faced the problem ? I know that I could do this at API-level, but I'm trying to avoid doing so, and keep API code clean.
How could I accomplish that? Will I need a cursor or something like that?
Thank you in advance.
Append COUNT(*) - 3:
DECLARE #count int = (SELECT COUNT(*) - 3
FROM problem
LEFT JOIN person
ON problem.problemId = person.problemId
INNER JOIN
problem_person
ON problem_person.personId = person.Id)
select problem.*,
(
STUFF
(
(
SELECT TOP(3)', ' + person.name
FROM
problem_person
LEFT JOIN person ON problem_person.personId = person.Id
WHERE
problem_person.problemId = problem.Id
order by person.name
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'), 1, 1, ''
) + CASE WHEN #count > 0 THEN N' and ' + CAST(#count as NVARCHAR(20)) + N' more faced the problem' ELSE N'' END
) as peopleWhoFaced from problem
Related
I need to filter the results from my STUFF function which are held in the column 'combined_ops'. I need to only see records that do not contain the word 'Transfer'. I've tried the classic WHERE combined_ops NOT LIKE '%transfer%', but does not work. See my code and results and please let me know where I went wrong. Thank You.
SELECT Job, STUFF((SELECT DISTINCT ' ' + Operation_Service
FROM dbo.Job_Operation
WHERE dbo.Job_Operation.Job = dbo.Job.Job
for xml path('')),1,2,'') AS combined_ops
FROM dbo.Job
WHERE dbo.Job.Status = 'Active'
AND dbo.Job.Customer_PO = 'tmi stock'
enter image description here
There are a few ways to do this.
You can put the FOR XML in a CROSS APPLY and filter afterwards
SELECT
j.Job,
jo.combined_ops
FROM dbo.Job j
CROSS APPLY (SELECT
STUFF((SELECT DISTINCT ' ' + jo.Operation_Service
FROM dbo.Job_Operation jo
WHERE jo.Job = j.Job
for xml path(''), type
).value('text()[1]','nvarchar(max)'),1,2,'')
) AS jo(combined_ops)
WHERE j.Status = 'Active'
AND j.Customer_PO = 'tmi stock'
AND jo.combined_ops NOT LIKE '%transfer%'
Or you can use HAVING and conditional aggregation
SELECT Job,
STUFF((
SELECT ' ' + jo.Operation_Service
(
SELECT jo.Operation_Service
FROM dbo.Job_Operation jo
WHERE jo.Job = j.Job
GROUP BY jo.Operation_Service
HAVING COUNT(CASE WHEN jo.Operation_Service LIKE '%transfer%' THEN 1 END) = 0
) jo
for xml path(''), type
).value('text()[1]','nvarchar(max)'),1,2,'') AS combined_ops
FROM dbo.Job j
WHERE j.Status = 'Active'
AND j.Customer_PO = 'tmi stock'
Note the use of the syntax for xml path(''), type).value('text()[1]','nvarchar(max)') to unescape XML characters correctly.
Note also the use of table aliases rather than three part column names.
I have applied XML PATH and Stuff to show multiple records in single cell, which is all working fine but I am required to show each record in cell in separate single line i.e char(13) but in my solution I am getting result like
#x0D;; Canvey
; government
; More information needed
; More information required
Script
SELECT
ans.Id
,ans.ResponseId
,STUFF((SELECT DISTINCT
char(13)+char(10) +
'; ' + cod.Name
,' ' + COUNT(cod.Name)
FROM [dbo].[Highlights] as h
INNER JOIN [dbo].[CodedHighlights] as ch on h.Id = ch.HighlightId
INNER JOIN [dbo].[Codes] as cod on ch.CodeId = cod.Id
WHERE h.AnswerId = ans.Id
GROUP BY cod.Name
FOR XML PATH('')), 1, 1, '' ) [ANSWER/CODES]
FROM [dbo].[Answers] as ans
Try this
DECLARE #result VARCHAR(100)=
STUFF(
(
SELECT TOP 5 CHAR(13) + [name]
FROM sys.objects
FOR XML PATH(''),TYPE).value('.','nvarchar(max)'),1,1,'');
PRINT #result;
You must use TYPE to get a natively typed XML in order to use the .value() method. This will implicitly re-escape all entities to their actual string values.
Furthermore, I use PRINT, as the typical grid result would not show the line breaks.
Hint: If you use CHAR(13)+CHAR(10) you'd have to add ,1,2,'') as STUFF()'s paramaters.
I'm using MS-SQLServer-2016 and got a requirement to transpose rows to columns.
I'm using Pivot and dynamic SQL option to do it because the number of rows is dynamic.
Figure1 is the output I'm currently getting. However the client doesn't want those NULLs displayed. He only wants the Not-Null dates to be displayed.
Is there way to get rid of those Null Values and display only distinct dates?
Figure1-Transpose_Output
With Regards,
Tanuja
DECLARE #columns AS NVARCHAR(MAX), #sql AS NVARCHAR(MAX), #var1 AS
VARCHAR(MAX);
SELECT #columns = stuff((select DISTINCT ',' + quotename(replace(replace(replace(n.action_note,' ','<>'),'><',''),'<>',' ') )
from engagement_action n, action_party m, personal p
where n.action_id = m.action_id
and p.party_id = m.party_id
and n.action_note like 'XXX'
and m.system_name = 'XXXXXXXXXXXX'
and p.customer_number = 'XXXXXXX' FOR XML PATH(''),TYPE).value('.','NVARCHAR(MAX)'),1,1,'')
SELECT #sql =
'select "RANK_IN_PROP" ,
customer_number,
customer_shortname,
system_name,
'+ #columns + '
from
(SELECT RANK() OVER (PARTITION BY convert(date,m.created_when) ORDER BY convert(date,m.created_when))"RANK_IN_PROP" ,
p.customer_number,
p.customer_shortname,
m.system_name,
m.created_when ,
convert(date,m.created_when) as created_when,
replace(replace(replace(n.action_note,'' '',''<>''),''><'',''''),''<>'','' '') as action_note1
FROM engagement_action n, action_party m, personal p
WHERE n.action_id = m.action_id
AND p.party_id = m.party_id
AND n.action_note like ''%XXX%''
AND m.system_name = ''XXXXXXXXXXX''
AND p.customer_number = ''XXXXXXXX'' ) d
PIVOT
(max(created_when) for action_note1 in ( ' + #columns + ' ))p order by created_when desc'
execute(#sql);`
Can you try this , just added (select distinct action_note engagement_action where action_note is not null) in where clause. As i can't create test data so could not really execute the query
SELECT #columns = stuff((select DISTINCT ',' +
quotename(replace(replace(replace(n.action_note,' ','<>'),'><',''),'<>',' ')
)
from
(select distinct action_note engagement_action where action_note is not null)
n, action_party m, personal p
where n.action_id = m.action_id
and p.party_id = m.party_id
and n.action_note like 'XXX'
and m.system_name = 'XXXXXXXXXXXX'
and p.customer_number = 'XXXXXXX' FOR XML PATH(''),TYPE).value('.','NVARCHAR(MAX)'),1,1,'')
[Edit] Due to time constraints I gave up on using a CTE and created a function that returns the concatenated string:
CREATE FUNCTION fn_GetCategoryNamesAsString
(
#lawID INT
)
RETURNS NVARCHAR(MAX)
AS
BEGIN
DECLARE #categoryNames NVARCHAR(MAX)
SET #categoryNames = ''
DECLARE #categoryID INT
DECLARE CUR CURSOR LOCAL FORWARD_ONLY READ_ONLY FOR
SELECT t1.LawCategoryID FROM [GWS].[dbo].[GWSMasterLawsLawCategories] t1 WHERE t1.LawID = #lawID
OPEN CUR
FETCH FROM CUR INTO #categoryID
WHILE ##FETCH_STATUS = 0
BEGIN
SET #categoryNames = #categoryNames + (SELECT t2.Name
FROM [GWS].[dbo].GWSMasterLawCategories t2
WHERE t2.LawCategoryID = #categoryID) + ', '
FETCH NEXT FROM CUR INTO #categoryID
END
CLOSE CUR
DEALLOCATE CUR
RETURN #categoryNames
END
GO
This does the job but I don't really like it. If anyone has a better solution I'd love to know.
[End edit]
I have seen several questions the deal roughly with the same topic but none cover the inclusion of null values.
I am writing a query that should return the full contents of one table with a couple of columns added with relevant data from other tables. These columns can include 0 - n values.
Null values need to be stored as an empty string and sets that do have the extra data should display it separated by commas.
Some approaches delivered all the names strung together, some only returned the values separately, some no values at all and, most often, the recursion went to deep (which means I fouled up as the dataset is small).
This is my current approach:
DECLARE #categoryNames NVARCHAR(MAX);
SET #categoryNames = '';
WITH sources (sourcesLawSourceID, sourcesName) AS (
SELECT DISTINCT [LawSourceID], [name]
FROM [GWS].[dbo].[GWSMasterLawSources]
),
categories AS(
SELECT GWSCategories.LawCategoryID AS categoryID, GWSLawCategories.LawID AS lawID,
categoryNames = #categoryNames
--CAST(LEFT( GWSCategories.name, CHARINDEX(',', GWSCategories.name + ',') -1) AS NVARCHAR(MAX)) categoryName,
--STUFF(GWSCategories.name, 1, CHARINDEX(',', GWSCategories.name + ','), '') categoryNames
FROM [GWS].[dbo].[GWSMasterLawCategories] GWSCategories
JOIN [GWS].[dbo].[GWSMasterLawsLawCategories] GWSLawCategories
ON GWSCategories.LawCategoryID = GWSLawCategories.LawCategoryID
UNION ALL
SELECT categories.categoryID, categories.lawID,
CAST(LEFT( #categoryNames, CHARINDEX(',', #categoryNames + ',') -1) AS NVARCHAR(MAX)) + GWSCategories.Name
FROM categories
JOIN [GWS].[dbo].[GWSMasterLawCategories] GWSCategories
ON categories.categoryID = GWSCategories.LawCategoryID
WHERE #categoryNames > ''
)
SELECT DISTINCT GWSMaster.[LawID]
,[Name]
,sources.sourcesName LawSourceName
,(SELECT STUFF((SELECT DISTINCT ', ' + RTRIM(LTRIM(categories.CategoryNames))
FROM categories
FOR XML PATH ('')), 1, 1, '')) Categories
FROM [GWS].[dbo].[GWSMasterLaws] GWSMaster
JOIN sources
ON sources.sourcesLawSourceID = GWSMaster.LawSourceID
JOIN categories
ON categories.lawID = GWSMaster.LawID
This leaves the category name field completely empty.
If I can give any more information or I have missed a question that answers my problem please let me know.
I have a dynamic pivot/unpivot script that transposes a table. This is dynamic enough to return certain columns that I want and using dynamic columns.
What I am looking for is rather to convert this into either a UDF or a VIEW so that I can join it to other tables.
Please help.
ALTER PROC [dbo].[uspGetUserByValues]
(
#Select NVARCHAR(4000) = '*',
#Where NVARCHAR(4000) = NULL,
#OrderBy NVARCHAR(4000) = NULL
)
AS
BEGIN
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT distinct ',P.' + QUOTENAME(PropertyDescription)
from System_Properties
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT ' + #cols + ', M.Email, C.Company_Name, C.Company_Type_ID, U.UserName, ISNULL(SMS.SMSProfiles,0) SMSProfiles, U.UserID
from
(
select PropertyDescription, UP.UserID, PropertyValue
from User_Properties UP
JOIN System_Properties SP ON UP.PropertyID = SP.PropertyID
JOIN aspnet_Membership M ON UP.UserID = M.UserID
) X
pivot
(
min(PropertyValue)
for PropertyDescription in (' + REPLACE(#cols,'P.','') + ')
) P
JOIN aspnet_Membership M ON P.UserID = M.UserID
JOIN aspnet_Users U on P.UserID = U.UserID
JOIN Companies C ON C.Company_ID = P.Company_ID
LEFT JOIN (SELECT UserId, COUNT(Users_SMS_Profile_ID) SMSProfiles
FROM Users_SMS_Profile GROUP BY UserID ) SMS ON SMS.UserID = P.UserID
'
SET #query = 'SELECT ' + #Select + ' FROM ('+ #query +') A'
IF ISNULL(#Where,'NULL') != 'NULL'
BEGIN
SET #query = #query + ' WHERE ' + #Where
END
IF ISNULL(#OrderBy,'NULL') != 'NULL'
BEGIN
SET #query = #query + ' ORDER BY ' + #OrderBy
END
execute(#query)
--PRINT(#query)
END
OH wow I made it.
I know this is with "known" column names but actually I didn't have to know them.
Firstly, this is the query I used to create the View. I will need to drop the view at least every I add a new Property or I can actually write a job that checks if all the properties from System_Properties are represented in the view, if not then drop the view and run this code.
CREATE PROC [dbo].[uspCreateViewUsers]
AS
BEGIN
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT distinct ',P.' + QUOTENAME(PropertyDescription)
from System_Properties
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'CREATE VIEW vwUsers AS SELECT ' + #cols + ', M.Email, C.Company_Name, C.Company_Type_ID, U.UserName, ISNULL(SMS.SMSProfiles,0) SMSProfiles, U.UserID
from
(
select PropertyDescription, UP.UserID, PropertyValue
from User_Properties UP
JOIN System_Properties SP ON UP.PropertyID = SP.PropertyID
JOIN aspnet_Membership M ON UP.UserID = M.UserID
) X
pivot
(
min(PropertyValue)
for PropertyDescription in (' + REPLACE(#cols,'P.','') + ')
) P
JOIN aspnet_Membership M ON P.UserID = M.UserID
JOIN aspnet_Users U on P.UserID = U.UserID
JOIN Companies C ON C.Company_ID = P.Company_ID
LEFT JOIN (SELECT UserId, COUNT(Users_SMS_Profile_ID) SMSProfiles
FROM Users_SMS_Profile GROUP BY UserID ) SMS ON SMS.UserID = P.UserID
'
execute(#query)
END
Them the View, which can't be represented graphically by table joins looks like this:
SELECT P.[Company_ID], P.[Created_Date], P.[Created_User], P.[Cust_ID], P.[FirstName], P.[IPCheck], P.[JobTitle], P.[LastLogin], P.[LastModified_Date], P.[LastModified_User],
P.[LastName], P.[Newsletter_OptIn], P.[Password_Change], P.[SupAdmin], P.[SysAccess], P.[SysAdmin], P.[User_Cat_1], P.[User_Cat_10], P.[User_Cat_2],
P.[User_Cat_3], P.[User_Cat_4], P.[User_Cat_5], P.[User_Cat_6], P.[User_Cat_7], P.[User_Cat_8], P.[User_Cat_9], P.[UserClient_ID], M.Email, C.Company_Name,
C.Company_Type_ID, U.UserName, ISNULL(SMS.SMSProfiles, 0) SMSProfiles, U.UserID
FROM (SELECT PropertyDescription, UP.UserID, PropertyValue
FROM User_Properties UP JOIN
System_Properties SP ON UP.PropertyID = SP.PropertyID JOIN
aspnet_Membership M ON UP.UserID = M.UserID) X PIVOT (min(PropertyValue) FOR PropertyDescription IN ([Company_ID], [Created_Date], [Created_User],
[Cust_ID], [FirstName], [IPCheck], [JobTitle], [LastLogin], [LastModified_Date], [LastModified_User], [LastName], [Newsletter_OptIn], [Password_Change], [SupAdmin],
[SysAccess], [SysAdmin], [User_Cat_1], [User_Cat_10], [User_Cat_2], [User_Cat_3], [User_Cat_4], [User_Cat_5], [User_Cat_6], [User_Cat_7], [User_Cat_8],
[User_Cat_9], [UserClient_ID])) P JOIN
aspnet_Membership M ON P.UserID = M.UserID JOIN
aspnet_Users U ON P.UserID = U.UserID JOIN
Companies C ON C.Company_ID = P.Company_ID LEFT JOIN
(SELECT UserId, COUNT(Users_SMS_Profile_ID) SMSProfiles
FROM Users_SMS_Profile
GROUP BY UserID) SMS ON SMS.UserID = P.UserID
This now allows me to query the View as if it was a table.
I hope this helps someone else in the future.
Simply said: you cant
At least you can't do it using conventional TSQL programming. Which means you would have to use some hack. Let me explain.
Closest thing to your SP would be UDF. However, UDFs are rather restricted. One thing UDF expect is for data to stay the same while and after executing it. Of course that this means EXEC() is forbidden in that scope.
Another possibility would be a view. However, you have a number of parameters and view's schema depends on these parameters. Functionality to change view's schema based on input parameters doesn't exist in SQL server.
And now for the hacks.
One hack I can think of is:
create a CLR UDF
create new connection based on context connection (same server, same db)
exec your SP there
return result to your original pipe
But it may or may not work (it's a hack after all).
If the hack doesn't work, you can try playing it by the book. This means creating a CLR UDF, assemble select statement in there and execute it, which means that you will have to throw away your original SP. However, it is not a hack since SQL CLR UDF's are made for such (and other) situations. Only thing you will have to take care about is using SqlMetaData because UDF doesn't have a predefined resultset. See this.
In my previous answer I stated that it could be done using CLR UDF, but that was wrong. One thing I forgot is that Microsoft insists on providing a finite number of columns for UDF. This may not be obvious while developing in .NET - after all, you can return any number of columns to SqlPipe. See this (untested) code...
[SqlFunction(DataAccess = DataAccessKind.Read)]
public static void DynOutFunc(SqlString select, SqlString where, SqlString orderBy)
{
// 1: Create SQL query
string query = "select db_id(), db_name()";
// 2: Find out which colums and their types are part of the output
SqlMetaData[] metaData =
{
new SqlMetaData("ID", System.Data.SqlDbType.Int),
new SqlMetaData("Database", System.Data.SqlDbType.NVarChar, 256)
};
using (SqlConnection connection = new SqlConnection("context connection=true"))
{
connection.Open();
SqlCommand command = new SqlCommand(query, connection);
SqlDataReader reader = command.ExecuteReader();
using (reader)
{
while (reader.Read())
{
SqlDataRecord record = new SqlDataRecord(metaData);
SqlContext.Pipe.SendResultsStart(record);
for(int i = 0; i < metaData.Length; i++)
{
if(metaData[i].DbType == DbType.String)
record.SetString(i, reader.GetString(i));
else if(metaData[i].DbType == DbType.Int32)
record.SetInt32(i, reader.GetInt32(i));
// else if's should cover all supported data taypes
}
SqlContext.Pipe.SendResultsRow(record);
}
}
SqlContext.Pipe.SendResultsEnd();
}
}
Notice SqlMetaData collection that holds information about columns. What stops you from appending just another column to it?
But(!) when it comes to registering that function in the SQL Server itself, you HAVE to provide arguments, like:
CREATE FUNCTION DynOutFunc
#select [nvarchar](4000),
#where [nvarchar](4000),
#orderBy [nvarchar](4000)
RETURNS TABLE (p1 type1, p2 type2, ... pN typeN)
AS EXTERNAL NAME SqlClrTest.UserDefinedFunctions.DynOutFunc;
It turns out there are no hacks for this I can think of. Or there are just no hacks here at all.