I asked this question few days ago, but it involves a bit deeper answer so it was suggested I create a brand new one, so here it goes...
Disclaimer: I cannot create any custom DB objects (functions, SP's, views etc.), so everything needs to be in-line inside a SQL query.
I'm querying Audit table which for the simplicity of this question has following fields:
AttributeMask
ChangedData
CreatedOn
ObjectId
Each record in a DB may have multiple Audit records associated with it. Every time a change is made to a DB record, it will create a record in the Audit table with specific ObjectID that will point to the source record, CreatedOn that will have a DateTime of the change, AttributeMask with list of AttributeId's that have been changed when SAVE was executed (note, there may be multiple fields changed at once) and ChangedData will actually have the data that's been changed (pre-changed values). One field can of course be changed multiple times and if it's the case, multiple Audit records for this field will exist (different CreatedOn values). I need to find what some (not all) fields from the source record looked like at a specific date.
I can run query below:
select a1.ChangeData as ChangedData1, a1.AttributeMask as AttributeMask2, a2.ChangeData as ChangedData2, a2.AttributeMask as AttributeMask2
from Table1 t
join audit a1 on a1.AuditId =
(select top 1 a.auditid from audit a where a.objecttypecode = 3
and a.objectid = T.ObjectId
and a.AttributeMask like '%,10192,%'
and a.CreatedOn <= '8-16-2018'
order by a1.CreatedOn desc)
join audit a2 on a2.AuditId =
(select top 1 a.auditid from audit a where a.objecttypecode = 3
and a.objectid = T.ObjectId
and a.AttributeMask like '%,10501,%'
and a.CreatedOn <= '8-16-2018'
order by a1.CreatedOn desc)
where t.ObjectID = SomeGuidValue
This query is looking for the latest change to 2 fields (10192 and 10501) which happened before 8-16-2018. It returns the following data (I added 3rd record to illustrate all possible cases):
ChangeData1 AttributeMask1 ChangeData2 AttributeMask2
NULL NULL True~~True~1904~~~15.8700000000~4760~30000~590~12000~0~390~1904~False~200~ ,10499,10604,10501,10436,10491,10490,10459,10099,10319,10253,10433,10031,10091,10020,10265,10008,10509,
~True~5.56~~House~~200000~ ,10030,10432,10435,197,10099,10192,198, False~1170~600~0~Complete~True~1770~ ,10501,10091,10008,10020,10570,10499,10253,10715,
~~~~200001~ ,10432,10435,197,10099,10192,198, True~2~True~~0~~~100.0000000000~1~business,96838c4f-e63c-e011-9a14-78e7d1644f78~~0~~~~0~False~~1~ ,10499,10509,10501,10203,10436,10491,10490,10459,10099,10157,10253,10433,10715,10031,10091,10020,10265,10008,10319,10699,
This means that 1st record has change to field 10501 only, 2nd record has change to 10192 only and 3rd record has changes to both 10192 and 10501 fields.
AttributeMask field has comma delimited list of all FieldID's that have been changed (note that it starts and ends with comma).
ChangedData field has tilde (~) delimited list of data that's been changed. Each entry in AttributeMask corresponds to entry in ChangedData. For example, if I wanted to see what data was in 10501 field in 1st record, I would need to determine what entry # 10501 is in AttributeMask field (it's #3 in the list) and then I would need to find out what data is in entry #3 in ChangedData field (it's TRUE) and if I wanted to see what was in 2nd record for Field 10192 I'd see what index it has in AttributeMask (it's #6) and its corresponding value in ChangedData field is 2000000.
I need to somehow extract this data in the same query. I was helped with some samples on how this could be done, but I failed to ask the right question in the beginning (thought it would be simpler than explaining all this).
What I need this query to return is something like this:
ChangeData1 AttributeMask1 ChangeData2 AttributeMask2
NULL NULL TRUE 10501
200000 10192 FALSE 10501
200001 10192 TRUE 10501
I hope this is clear now.
As told in my comments you are better off to deal with a set, then working with a broader and broader list with name-numbered columns.
Try to provide your initial input set in the format like the following mockup table:
There is a running ID, your ObjectID, the code you are looking for and the both strings. I inserted the data as provided by you, but not side-by-side:
DECLARE #tbl TABLE(ID INT IDENTITY, CodeId INT,ObjectId INT, ChangeData VARCHAR(1000), AttributeMask VARCHAR(1000));
INSERT INTO #tbl VALUES
(10192,1,NULL,NULL)
,(10501,1,'True~~True~1904~~~15.8700000000~4760~30000~590~12000~0~390~1904~False~200~',',10499,10604,10501,10436,10491,10490,10459,10099,10319,10253,10433,10031,10091,10020,10265,10008,10509,')
,(10192,2,'~True~5.56~~House~~200000~',',10030,10432,10435,197,10099,10192,198,')
,(10501,2,'False~1170~600~0~Complete~True~1770~',',10501,10091,10008,10020,10570,10499,10253,10715,')
,(10192,3, '~~~~200001~',',10432,10435,197,10099,10192,198,')
,(10501,3,'True~2~True~~0~~~100.0000000000~1~business,96838c4f-e63c-e011-9a14-78e7d1644f78~~0~~~~0~False~~1~',',10499,10509,10501,10203,10436,10491,10490,10459,10099,10157,10253,10433,10715,10031,10091,10020,10265,10008,10319,10699,');
--The query will cast the strings to XML in order to grab into it by their position index
--Then all codes are taken and numbered as derived list.
--According to the found position the corresponding value is taken
SELECT t.ID
,t.ObjectId
,t.CodeId
,t.ChangeData
,t.AttributeMask
,Casted.ValueXml.value('/x[sql:column("PartIndex")][1]','nvarchar(max)') ValueAtCode
FROM #tbl t
CROSS APPLY
(
SELECT CAST('<x>' + REPLACE(t.AttributeMask,',','</x><x>') + '</x>' AS XML).query('/x[text()]') AS CodeXml
,CAST('<x>' + REPLACE(t.ChangeData,'~','</x><x>') + '</x>' AS XML) AS ValueXml
) Casted
CROSS APPLY(
SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS PartIndex
,x.value('text()[1]','nvarchar(max)') AS CodePart
FROM Casted.CodeXml.nodes('/x') A(x)
) CodeDerived
WHERE CodeDerived.CodePart=t.CodeId;
the result
ID ObjectId CodeId ValueAtCode
2 1 10501 True
3 2 10192
4 2 10501 False
5 3 10192 200001
6 3 10501 True
But this will be slooooow...
UPDATE
Your whole approach is not set-based. The following is completely untested, I don't have your database, but will point to a set-based solution.
DECLARE #Codes TABLE(CodeID INT);
INSERT INTO #Codes VALUES(10192),(10501);
select t.SomeIdOfYourMainTable
,c.CodeID
,a1.ChangeData
,a1.AttributeMask
from Table1 t
CROSS JOIN #Codes c --will repeat the result for each value in #Codes
CROSS APPLY
(
select top 1 a.ChangeData
,a.AttributeMask
from [audit] a
where a.objecttypecode = 3
and a.objectid = t.ObjectId
and a.AttributeMask like CONCAT('%,',c.CodeID,',%')
and a.CreatedOn <= '20180816' --use culture independant format!!!
order by a.CreatedOn desc
) a1;
This allows you to insert as many codes as you want (no need to repeat any join) and it will return a set similar to my example above.
If you need further help with this: Please close this question and start a new question with a fully working, stand-alone MCVE to reproduce your case.
I have three tables that store different kinds of data according to an admission process.
The first table ([dbo].[Inscripciones_Pregrado]) stores the basic contact info of the future student. The second table ([dbo].[Checklist_Documentos]) stores the names and ID's for the documents that the student must have to complete the admission process. The third table ([dbo].[Checklist_Pregrado]) stores the documents that the student has effectively brought to the admissions office.
I need to combine this info so that we can export an excel file holding all the contact info, and the document's checklist for each student.
So, let's say table 1 haves 4 records:
[dbo].[Inscripciones_Pregrado]
The second table ([dbo].[Checklist_Documentos]) haves eight records, different types of requested documents:
And the third table ([dbo].[Checklist_Pregrado]) haves 16 records:
What I need is that this three tables get combined according to the aspirant's ID:
So far, I've tried to different types of codes:
Code 1
SELECT
[dbo].[Inscripciones_Pregrado].[ID],
[dbo].[Inscripciones_Pregrado].[Name],
[dbo].[Inscripciones_Pregrado].[ProgramID],
[dbo].[Checklist_Documentos].[Document]
FROM [dbo].[Inscripciones_Pregrado]
INNER JOIN
[dbo].[Checklist_Pregrado]
ON [dbo].[Checklist_Pregrado].[IdForm] = [dbo].[Inscripciones_Pregrado].[ID]
INNER JOIN
[dbo].[Checklist_Documentos]
ON [dbo].[Checklist_Documentos].[ID] = [dbo].[Checklist_Pregrado].[IdDoc]
This one gets one line per each document, so the name and other records are repeated according to the number of documents that the aspirant has on his folder.
Code 2
SELECT
[dbo].[Inscripciones_Pregrado].[ID],
[dbo].[Inscripciones_Pregrado].[Nombres],
[dbo].[Inscripciones_Pregrado].[Apellido1],
(STUFF((SELECT CAST(', ' + [dbo].[Checklist_Pregrado].[IdDocumento] AS varchar(max))
FROM [dbo].[Checklist_Pregrado]
WHERE([dbo].[Inscripciones_Pregrado].[ID] = [dbo].[Checklist_Pregrado].[IdForm])
FOR XML PATH ('')), 1, 2, '')) AS [Docs]
FROM [dbo].[Inscripciones_Pregrado]
This code get's and error:
Conversion failed when converting the varchar value ', ' to data type int.
I appreciate all your help and comments.
Thanks
For your Code 2, you can fix the error by doing the CAST before you do the concatenation.
In other words:
(STUFF((SELECT ', ' + CAST([dbo].[Checklist_Pregrado].[IdDocumento] AS varchar(max))
#Tab Alleman gave me the hint on the answer, I haven't noticed that error in the writing. It solved the issue, but the code would only bring the document's ID from the second table ([dbo].[Checklist_Documentos]) So I added an INNER JOIN to combine the data:
SELECT
[dbo].[Inscripciones_Pregrado].[ID],
[dbo].[Inscripciones_Pregrado].[Nombres],
[dbo].[Inscripciones_Pregrado].[Apellido1],
(STUFF((SELECT ', ' + CAST([dbo].[Checklist_Documentos].[Documento] AS varchar(max))
FROM [dbo].[Checklist_Pregrado]
INNER JOIN
[dbo].[Checklist_Documentos]
ON [dbo].[Checklist_Documentos].[ID] = [dbo].[Checklist_Pregrado].[IdDocumento]
WHERE([dbo].[Inscripciones_Pregrado].[ID] = [dbo].[Checklist_Pregrado].[IdForm])
FOR XML PATH ('')), 1, 2, '')) AS [Docs]
FROM [dbo].[Inscripciones_Pregrado]
I have a master table that has one row per key and a detail table that has many rows per key with a sequence field that has a description field I need concatenated together and create one row per key. My code does this fine but the detail data row does not have the concatenated data in the correct order. The data is delivered to me in an Excel spreadsheet and I use the Import Wizard to add the data to the database. Since the order is not correct in the detail data I added a sub select to sort the data by key and sequence number that is the input into the STUFF WITH XML PATH. I am still getting the data in an incorrect order. If I sort the data in the spreadsheet first and then load it to the database it works fine. I really need this to work dynamically as I want to distribute this to my team and we can use it for different tables. Any ideas on why the sub select with the STUFF FOR XML PATH is not working? How can I do what I need?
Here is the code I have:
SELECT pic, pisc, piin
, STUFF((SELECT ' ' + P.PIIDTA FROM PI115AP P
Where P.PIC =B.PIC
and P.PISC = B.PISC
and P.PIIN = B.PIIN FOR XML PATH(''), type
).value('.', 'nvarchar(max)'),1,1,'') As CombinedDetail
From
( select TOP 100 PERCENT
pic, pisc, piin, piisn, piidta
from PI115AP
order by pic, pisc, piin, piisn) B
Group By B.PIC, B.pisc, B.piin
Thank you!
You have a lot of pieces and parts out of place for this to work the way you want it to. Your FOR XML is not in the order you want because the subquery has no order by. The actual result set is not in the order you want either because the main query does not have an order by. I don't really understand the point of the B subquery. The top does NOT order the actual results when using top, it just defines which rows to retrieve.
Pretty sure you want something more like this.
SELECT pic
, pisc
, piin
, STUFF((SELECT ' ' + P.PIIDTA
FROM PI115AP P
Where P.PIC = B.PIC
and P.PISC = B.PISC
and P.PIIN = B.PIIN
order by p.pic
, p.pisc
, p.piin
, p.piisn
FOR XML PATH(''), type
).value('.', 'nvarchar(max)'),1,1,'') As CombinedDetail
From PI115AP B
Group By B.PIC
, B.pisc
, B.piin
order by b.pic
, b.pisc
, b.piin
I have a situation where I need to pull existing products out of a MSSQL database to CSV for ingestion into another database, each row will need to contain as much information as possible from the existing database. I think i've gotten a majority of what I need with my query so far, but I am stuck on figuring out how to merge the multiple categories per item down into one row.
Whats happening is i'll have a duplicate row for each category listed. So if its assigned to the category Glass and Glass and Glass Connectors, i'll have a row for each.
I'd like there to be a single field named Categories that's just comma separated like this: "Glass,Glass and Glass Connectors"
I read that STUFF() can do this, but I can't seem to get the syntax right. Other examples on Stack didn't seem to work for my situation or I just don't know exactly how to apply it to my query, the mass amount of JOINs needed hasn't helped either.
Here's my query:
SELECT
tblCatalog_SKUs.InternalSKU,
tblCatalog_Products.Name AS ParentProd,
tblCatalog_Categories.Name AS Category,
tblCatalog_SKUs_Images.Image1,
tblCatalog_SKUs_Images.Image2,
tblCatalog_SKUs_Images.Image3,
tblCatalog_Products.Summary,
tblCatalog_SKUs.Name AS optName,
tblCatalog_SKUs.Description AS optDesc,
tblCatalog_SKUs.Price,
tblCatalog_SKUs.Inventory,
tblCatalog_SKUs.Sale
FROM tblCatalog_Products_Categories
INNER JOIN tblCatalog_Categories
ON tblCatalog_Products_Categories.CategoryID = tblCatalog_Categories.CategoryID
INNER JOIN tblCatalog_SKUs
ON tblCatalog_Products_Categories.ProductID = tblCatalog_SKUs.ProductID
INNER JOIN tblCatalog_SKUs_Images
ON tblCatalog_SKUs.SKUID = tblCatalog_SKUs_Images.SKUID
INNER JOIN tblCatalog_Products
ON tblCatalog_SKUs.ProductID = tblCatalog_Products.ProductID
Sample results:
http://i.stack.imgur.com/Y7AIt.png
I was hoping there might be something like group_concat in MySQL.
Thanks for your help!
You can do this with STUFF and FOR XML PATH like this:
SELECT
tblCatalog_SKUs.InternalSKU,
tblCatalog_Products.Name AS ParentProd,
STUFF((
SELECT ',' + Name
FROM tblCatalog_Categories
INNER JOIN tblCatalog_Products_Categories ON tblCatalog_Products_Categories.CategoryID = tblCatalog_Categories.CategoryID
WHERE tblCatalog_Products_Categories.ProductID = tblCatalog_SKUs.ProductID
FOR XML PATH('')
), 1, 1, '') AS Category,
tblCatalog_SKUs_Images.Image1,
tblCatalog_SKUs_Images.Image2,
tblCatalog_SKUs_Images.Image3,
tblCatalog_Products.Summary,
tblCatalog_SKUs.Name AS optName,
tblCatalog_SKUs.Description AS optDesc,
tblCatalog_SKUs.Price,
tblCatalog_SKUs.Inventory,
tblCatalog_SKUs.Sale
FROM tblCatalog_SKUs
INNER JOIN tblCatalog_SKUs_Images
ON tblCatalog_SKUs.SKUID = tblCatalog_SKUs_Images.SKUID
INNER JOIN tblCatalog_Products
ON tblCatalog_SKUs.ProductID = tblCatalog_Products.ProductID
STUFF is just used to remove the leading ',', key part is FOR XML PATH concatenating the strings.