I'd like to create a weighted usage ranking / popularity query (or batch update, if the query proves to strenuous for real-time use!) but I've been drawing a blank. Hopefully you'll have a better idea as to how to do this.
I've simplified my database to help illustrate the problem (see diagram, below!) Basically, when a User selects a specific Blog via a Tag, I add an entry to the TagLog table. Assume for this example that the collection of Blogs and Tags remain static. Assuming the above, I'd like to do the following:
Find the Top 10 Blogs for any given Tag
Find the Top 10 Blogs for any given Tag and User
The real difficulty comes from the fact that I'd like to weight the results such that more recent TagLog entries have greater significance.
Any help in this regard would be greatly appreciated! Thanks...
This should get you headed somewhere useful:
-- Sample data.
declare #Blogs as Table ( BlogId Int Identity, URL VarChar(256) )
insert into #Blogs ( URL ) values
( 'www.google.com' ), ( 'www.java.com' )
declare #Tags as Table ( TagId Int Identity, BlogId Int, Tag VarChar(64) )
insert into #Tags ( BlogId, Tag ) values
( 1, 'Not Evil' ), ( 2, 'Buggy' )
declare #TagLog as Table ( TagId Int, UserGuid UniqueIdentifier, Visited DateTime )
insert into #TagLog ( TagId, UserGuid, Visited ) values
( 1, NewId(), '20130502' ), ( 1, NewId(), '20130508' ), ( 1, NewId(), '20130515' ),
( 2, NewId(), '20130501' ), ( 2, NewId(), '20130508' ), ( 2, NewId(), '20130515' )
declare #Now as DateTime = '20130516' -- Test value.
-- Display all sample data.
select *, DateDiff( day, TL.Visited, #Now ) as Age -- Use appropriate units, e.g. week, minute.
from #Blogs as B inner join
#Tags as T on T.BlogId = B.BlogId inner join
#TagLog as TL on TL.TagId = T.TagId
-- Compute a weight based on age.
-- Use the reciprocal of the age so that newer visits have higher weight.
-- Add 1.0 to avoid divide by zero errors.
select T.TagId, Count( 42 ) as Visits, Sum( 1.0 / ( DateDiff( day, TL.Visited, #Now ) + 1.0 ) ) as AgeWeight
from #Blogs as B inner join
#Tags as T on T.BlogId = B.BlogId inner join
#TagLog as TL on TL.TagId = T.TagId
group by T.TagId
Related
I have a table of changes to an entity. I am trying to rebuild the data from the changes.
This is my Changes table:
CREATE TABLE Changes
(
Id IDENTITY,
RecordId INT,
Field INT,
Val VARCHAR(MAX),
DateOfChange DATETIME
);
Field column is a reference to what field changed, Val is the new value, RecordId is the Id of the record that changed. Ideally the Record table would contain the latest values but I am not that lucky. There are 10 different fields that changes are tracked, mostly dates but some other types are thrown in there.
This is my Record table:
CREATE TABLE Records
(
Id IDENTITY,
AUserGeneratedIdentifer VARCHAR(12)
)
I'd like to have a view to query by the rolled up values.
SELECT
AUserGeneratedIdentitfier, DateOpened, DateClosed, etc
FROM
RecordView
WHERE
AUserGeneratedIdentitfier = 'something'
I am trying to implemented it with CTEs but I am wondering if this is the correct way. I am using a CTE per field I am trying to get to.
WITH DateOpened AS
(
SELECT
RecordId, Val,
ROW_NUMBER() OVER (PARTITION BY RecordId ORDER BY DateOfChanged DESC) Rank
FROM
Changes
WHERE
FieldId = #DateOpenedId
) --- ... Repeat for every field
SELECT (my fields)
FROM Records
INNER JOIN <all ctes> on Record Id
But this method feels wrong to me, possibly due to my lack of SQL experience. Is there a better way that I am missing here? What are the performance implications of having multiple CTEs on the same table and joining with them?
Please excuse the hastily thrown together pseudo code, I hope it illustrates my problem accurately
I think you should be able to use a single ROW_NUMBER and pivot it as below.
DECLARE #Records TABLE (Id INT IDENTITY, AUserGeneratedIdentifer VARCHAR(12))
DECLARE #Changes TABLE (Id INT IDENTITY, RecordId INT, Field INT, Val VARCHAR(MAX), DateOfChange DATETIME);
INSERT INTO #Records (AUserGeneratedIdentifer)
VALUES ('qwer'), ('asdf')
INSERT INTO #Changes (RecordId, Field, Val, DateOfChange)
VALUES (1, 1, 'foo', '2021-01-01' ),
(2, 1, 'fooz', '2021-01-01' ),
(2, 2, 'barz', '2021-01-01' ),
(1, 2, 'bar', '2021-01-01' ),
(1, 1, 'foo2', '2021-01-02' ),
(2, 2, 'barz2', '2021-01-02' )
SELECT piv.RecordId, piv.[1], piv.[2]
FROM (
SELECT C.RecordId, C.Field, C.Val, ROW_NUMBER() OVER (PARTITION BY C.RecordId, C.Field ORDER BY C.DateOfChange DESC) RowNum
FROM #Changes C
) sub
PIVOT (
MAX(Val)
FOR Field IN ([1], [2])
) piv
WHERE piv.RowNum = 1
I am working on a problem where I need to get the next available Category for a gamer. If the gamer has reached the final Category (Shooter in below image) then I will need to go back to the start of the table and go to the next one available.
Below is a list of Category:
Below is a list of gamers and what their last Category_ID was:
Finally, below is the data I will be using to work with
Now I need to find the next Category_ID for each gamer using the data table. If a Gamer has reached the last Category_ID (ID 4 in this case and Gamer C) then it will reset back to top like 1 however Gamer C will pick Category 2 as there is no one for that Gamer.
How can I pick the next Category available using the data table for each gamer while keeping in mind of resetting back to top if reached end?
Below is a code example of the problem:
DECLARE #Category AS TABLE
(
Category_ID INT IDENTITY(1,1) ,
Description VARCHAR(255)
)
INSERT INTO #Category
VALUES('Sports'), ('Adventure'), ('Action'), ('Shooter')
DECLARE #Gamer AS TABLE
(
Username VARCHAR(255) ,
Last_Category_ID INT
)
INSERT INTO #Gamer
VALUES('Gamer A', 1), ('Gamer B', 2), ('Gamer C', 4), ('Gamer D', 3)
DECLARE #Data AS TABLE
(
Username VARCHAR(255) ,
Play_At DATETIME ,
Category_ID INT
)
INSERT INTO #Data
VALUES('Gamer A', GETDATE() -1, 1), ('Gamer B', GETDATE() -1, 2), ('Gamer A', GETDATE() -1, 3), ('Gamer D', GETDATE() -1, 3), ('Gamer C', GETDATE() -1, 2)
SELECT * FROM #Category
SELECT * FROM #Gamer
SELECT * FROM #Data
If I understand correctly, you want the next unused category id for each gamer. And then to start over when all are done.
Here is an approach:
For each gamer, get the maximum current category.
For each category, get the next category.
Get the first category as well.
When there is no next category use the first.
This is a good opportunity to use lateral joins:
select g.*, coalesce(cn.category_id, cf.category_id)
from #gamers g outer apply
(select max(category_id) as max_category_id
from #data d
where d.username = g.username
) c outer apply
(select top (1) cn.category_id
from #categories cn
where cn.categoryid > c.max_categoryid
) cn cross join
(select min(cf.category_id) as categoryi_d
from #categories cf
) cf;
Here is a db<>fiddle.
I'm not 100% clear on if your #Data table represents the starting category however this might be what you need, if not by all means please clarify with some clear expected results for the sample data.
Using an apply to determine the starting category for each user (if indeed that's what your description entails) and another to get the maximum possible category, select the username and, using a case expression, the corresponding next category name or the starting category if it's already the max category.
select
Username,
( select description
from #category c
where c.category_Id=case when Last_Category_ID<m.Id then Last_Category_ID + 1 else startcat.Id end
) NextCategory
from #gamer g
outer apply (
select min(category_id) Id
from #data d
where d.username=g.Username
)startcat
outer apply (
select Max(category_Id) Id
from #category
)m
Consider the below schema
dbo.Cultures (Id, CultureCode, ParentId)
Culture table stores the data in the parent-child relationship.
Suppose we have below demo data
5 es-ES 3
Now I have another table which stores the multilingual data for the different cultures.
Schema for the table is as following
dbo.LangData(KeyName, CultureId, Value)
here cultureId is the foreign key of dbo.Cultures table.
Suppose this table has following data
Now I require to fetch the data for all the cultures which are in the Culture table and the corresponding value column in the LangData table.
The culture Ids which are not in the LangData table, for those the Value column will the value of the corresponding parent culture Id columns value. I.e. Data will be retrieved using fallback logic
E.g. For the above values the Result set will be following.
5 es-ES Colour_IN
Here for de-DE data is missing in LangData so it's value will be the data in it's parent culture i.e. en-IN, if in case data also not found in en-IN then it will pick the data of it's parent en-US.
Tried Soloution
First I fetch the culture hierarchy using CTE
CREATE FUNCTION [dbo].[ufnGetCultureHierarchyAll] ()
RETURNS #hierarchyResult TABLE(RowNo INT, CultureId INT, ParentCultureId INT)
AS
BEGIN
WITH CultureHierarchy_CTE(RowNo, CultureId, ParentCultureId)
AS (
SELECT 1,
Id,
ParentId
FROM [dbo].Cultures
UNION ALL
SELECT RowNo + 1,
ou.Id,
ou.ParentId
FROM [dbo].Cultures ou
JOIN CultureHierarchy_CTE cte
ON ou.Id = cte.ParentCultureId
)
-- inserting desired records into table and returning
INSERT INTO #hierarchyResult (RowNo,CultureId,ParentCultureId )
SELECT RowNo, CultureId , ParentCultureId FROM CultureHierarchy_CTE
RETURN;
END
This will return the hierarchy of the all the cultures
Now I attempted to apply join of the result set with the LangData table,
DECLARE #cultureHierarchy AS TABLE(
RowNumber INT,
CultureId INT,
ParentCultureId INT
)
--SELECT * FROM master.Cultures
----Get and store culture hierarchy
INSERT INTO #cultureHierarchy
SELECT RowNo, CultureId, ParentCultureId
FROM ufnGetCultureHierarchyAll()
SELECT c.Code AS [CultureCode],
c.CultureId AS [CultureId],
rv.Value
FROM dbo.LangData rv WITH (NOLOCK)
JOIN #cultureHierarchy c ON rv.CultureId = c.CultureId
END
but it is not working.
Is someone have any Idea regarding same.
Solution using Itzik Ben-Gan's hierarchy model. If you can extend the dbo.Cultures table with Hierarchy, Lvl and Root columns and index on Hierarchy, query will be faster. It has to be rewrited in that case though.
drop table if exists dbo.Cultures;
create table dbo.Cultures (
ID int
, Code varchar(50)
, ParentID int
);
insert into dbo.Cultures (ID, Code, ParentID)
values (1, 'en-US', null), (2, 'en-IN', 1), (3, 'de-DE', 2), (4, 'hi-HI', 2)
drop table if exists dbo.LangData;
create table dbo.LangData (
KeyName varchar(100)
, CultureID int
, Value varchar(100)
);
insert into dbo.LangData (KeyName, CultureID, Value)
values ('lblColourName', 1, 'Color'), ('lblColourName', 2, 'Colour-IN');
with cteCultures as (
select
c.ID, c.Code, c.ParentID, 0 as Lvl
, convert(varchar(max), '.' + CONVERT(varchar(50), c.ID) + '.') as Hierarchy
, c.ID as Root
from dbo.Cultures c
where c.ParentID is null
union all
select
c.ID, c.Code, c.ParentID, cc.Lvl + 1 as Lvl
, cc.Hierarchy + convert(varchar(50), c.ID) + '.' as Hierarchy
, cc.Root as Root
from dbo.Cultures c
inner join cteCultures cc on c.ParentID = cc.ID
)
select
ccr.ID
, ccr.Code
, coalesce(ld.Value, ld2.Value) as Value
from cteCultures ccr
left join dbo.LangData ld on ccr.ID = ld.CultureID
outer apply (
select
top (1) tcc.ID
from cteCultures tcc
inner join dbo.LangData tld on tcc.ID = tld.CultureID
where ld.KeyName is null
and ccr.Hierarchy like tcc.Hierarchy + '%'
and ccr.Hierarchy <> tcc.Hierarchy
order by tcc.Lvl desc
) tt
left join dbo.LangData ld2 on tt.ID = ld2.CultureID
If I understand your question:
We just build your hierarchy (SEQ and Lvl are optional) and then perform TWO left joins in concert with a Coalesce().
Example
Declare #Cultures table (id int,ParentId int,Code varchar(50))
Insert into #Cultures values
( 1, NULL,'en-US')
,( 2, 1 ,'en-IN')
,( 3, 2 ,'de-DE')
,( 4, 2 ,'hi-HI')
Declare #LangData table (keyName varchar(50),CultureId int,Value varchar(50))
Insert Into #LangData values
('lblColourName',1,'Color')
,('lblColourName',2,'Color_IN')
;with cteP as (
Select Seq = cast(10000+Row_Number() over (Order by Code) as varchar(500))
,ID
,ParentId
,Lvl=1
,Code
From #Cultures
Where ParentId is null
Union All
Select Seq = cast(concat(p.Seq,'.',10000+Row_Number() over (Order by r.Code)) as varchar(500))
,r.ID
,r.ParentId
,p.Lvl+1
,r.Code
From #Cultures r
Join cteP p on r.ParentId = p.ID)
Select CultureId = A.ID
,A.Code
,Value = Coalesce(C.Value,B.Value)
From cteP A
Left Join #LangData B on (A.ParentId=B.CultureId)
Left Join #LangData C on (A.Id=C.CultureId)
Order By Seq
Returns
CultureId Code Value
1 en-US Color
2 en-IN Color_IN
3 de-DE Color_IN
4 hi-HI Color_IN
I'm storing nested lists in my database using the adjacency list model. Each list might have 50-150 nodes, so we'll call it 100 nodes on average. The situation has come up where users want to clone a list (i.e., use an existing list as a template from which to create a new list). This use case could potentially save them a lot of time when new lists differ only slightly from existing lists.
Here's an abbreviated version of the table schema I'm using:
CREATE TABLE Nodes (
NodeId int IDENTITY(1,1) NOT NULL,
ParentId int NULL,
ListId int NOT NULL,
NodeText varchar(255) NOT NULL
)
My original thought was to use an INSERT ... SELECT to copy all the nodes in one shot, but that leaves the new records referencing the old ParentId values.
I've got a solution that's working (in application code, not SQL), but it seems suboptimal due to the number of queries required. Here's the algorithm:
Select all records belonging to old list.
Iterate over rows and add to new list by inserting with different ListId.
Select ##IDENTITY from each insert and store it alongside data for current row.
Iterate over rows again and update Nodes table, setting ParentId to new ID (from previous step) where ParentId is equal to old ID and ListId is equal to new list ID.
Like I said, that works fine, but it requires 300+ queries to clone a single list containing 100 nodes. Is there a more efficient way to achieve the same thing?
Try this. The following solution is a zero loop, zero temp table one.
SQLFiddle
DECLARE #CurrentID int = IDENT_CURRENT('Nodes'),
#OldListId int = 1,
#NewListId int;
SELECT #NewListId = ISNULL(MAX(ListId) ,0)+1 FROM Nodes
SET IDENTITY_INSERT Nodes ON
;WITH NewNode as (
SELECT ROW_NUMBER() OVER(ORDER BY NodeId)+ #CurrentID as NewNodeId, *
FROM Nodes WHERE ListId= #OldListId
)
INSERT INTO Nodes(NodeId,ParentId,ListId,NodeText)
SELECT N1.NewNodeId ,N2.NewNodeId , #NewListId, N1.NodeText FROM NewNode N1 LEFT OUTER JOIN NewNode N2 ON
N1.ParentId = N2.NodeId
--SELECT N1.* , N2.NewNodeId as NewParentId FROM NewNode N1 LEFT OUTER JOIN NewNode N2 ON
--N1.ParentId = N2.NodeId
SET IDENTITY_INSERT Nodes OFF
The above solution generates the tree and then inserts to the table. Please be aware to use appropriate transactions and locking mechanism to ensure the data is consistent
With suitable transaction wrapping and isolation, the following may meet your needs:
-- Set up some sample data.
declare #Nodes as Table ( NodeId Int Identity, ParentId Int Null, ListId Int, NodeText VarChar(255) );
insert into #Nodes ( ParentId, ListId, NodeText ) values
( NULL, 1, '1' ),
( 1, 1, '1.1' ), ( 1, 1, '1.2' ),
( NULL, 2, '2' ),
( 4, 2, '2.1' ), ( 5, 2, '2.1.1' );
select * from #Nodes;
declare #TemplateListId as Int = 2;
-- Assuming that the clone is a new List. This is not important to what follows.
declare #ListId as Int = Coalesce( ( select Max( ListId ) from #Nodes ), 0 ) + 1;
-- Copy the template rows into the table and generate a mapping from old to new NodeIds.
declare #Fixups as Table ( OldNodeId Int, NewNodeId Int );
with Template as (
select NodeId, ParentId, ListId, NodeText
from #Nodes
where ListId = #TemplateListId )
merge into #Nodes as Nodes
using Template as T
on 42 < 0
when not matched then
insert ( ParentId, ListId, NodeText ) values ( ParentId, #ListId, NodeText )
output T.NodeId, inserted.NodeId into #Fixups;
select * from #Nodes;
select * from #Fixups;
-- Apply the fixups to the new copy.
update Nodes
set ParentId = Fixups.NewNodeId
from #Nodes as Nodes inner join
-- Update only the copy and not the template. (Could also use IN or EXISTS.)
#Fixups as Copy on Copy.NewNodeId = Nodes.NodeId inner join
-- Map the old nodes to the new.
#Fixups as Fixups on Fixups.OldNodeId = Nodes.ParentId;
select * from #Nodes;
I'm new-ish to SQL and am trying to figure out how to use the values from the Select statement in a While Exists conditional loop. The purpose is to combine multiple occurences of an attribute for a Document into a single field, and later pivot and join those results to the Document record.
For example, three tables exist like so:
ATTRIBUTES TABLE
ID, ATTRIBUTE_NAME
---------------------------
1, Created
2, Embedded_Image
...
ATTRIBUTE_VALUES TABLE
ATTRIBUTE_ID, VALUE, DOC_ID
-------------------------------------------
1, 2010/11/01, 1
2, 'Home.png', 1
2, 'Castle.png', 1
2, 'Apartment.jpg', 1
1, 2008/06/23, 2
2, 'Ski Jump.jpg', 2
2, 'Snowboarding.png', 2
...
DOCUMENTS TABLE
ID, TEXT
---------------------------
1, 'Homes of the ...'
2, 'Winter sports ...'
...
So a final Pivot and Join of the tables would look like so:
DOC_ID, TEXT, Created, Embedded_Image
----------------------------------------------------------------------------------------
1, 'Homes of the ...', 2010/11/01, 'Home.png,Castle.png,Apartment.jpg'
2, 'Winter sports ...', 2008/06/23, 'Ski Jump.jpg, Snowboarding.png'
The SQL While Exists condition I've tried to write looks like so:
DECLARE #LOOP_DOC_ID UNIQUEIDENTIFIER
DECLARE #LOOP_ATTRIBUTE_NAME NVARCHAR(MAX)
WHILE EXISTS(
SELECT [dbo].[ATTRIBUTE_VALUES].[DOC_ID], [dbo].[ATTRIBUTES].[ATTRIBUTE_NAME]
FROM ([dbo].[ATTRIBUTE_VALUES] INNER JOIN [dbo].[ATTRIBUTES]
ON [dbo].[ATTRIBUTE_VALUES].[ATTRIBUTE_ID] = [dbo].[ATTRIBUTES].[ID])
)
BEGIN
SET #LOOP_DOC_ID = DOC_ID
SET #LOOP_ATTRIBUTE_NAME = ATTRIBUTE_NAME
SELECT STUFF(
(
SELECT DISTINCT ',' + RTRIM(LTRIM([dbo].[ATTRIBUTE_VALUES].[VALUE]))
FROM
(
[dbo].[ATTRIBUTE_VALUES] INNER JOIN [dbo].[ATTRIBUTES]
ON [dbo].[ATTRIBUTE_VALUES].[ATTRIBUTE_ID] = [dbo].[ATTRIBUTES].[ID]
)
WHERE [dbo].[ATTRIBUTE_VALUES].[DOC_ID] = #LOOP_DOC_ID
AND [dbo].[ATTRIBUTES].[ATTRIBUTE_NAME] = #LOOP_ATTRIBUTE_NAME
ORDER BY ',' + RTRIM(LTRIM([dbo].[ATTRIBUTE_VALUES].[VALUE]))
FOR XML PATH('')
), 1, 2, ''
) AS VALUE, #LOOP_DOC_ID AS DOC_ID, #LOOP_ATTRIBUTE_NAME AS ATTRIBUTE_NAME
END
SQL Server doesn't like the lines where I'm trying to SET the variables to the values from the Select statement in the While Exists condition.
How can I use the [dbo].[ATTRIBUTE_VALUES].[DOC_ID], [dbo].[ATTRIBUTES].[ATTRIBUTE_NAME] values Selected in the While Exists conditional statement between the BEGIN and END statements?
Preferrably I would like to do away with the #LOOP_DOC_ID and #LOOP_ATTRIBUTE_NAME variables and deal directly with the values.
I've looked through forums that have talked about using Cursors to solve similar problems, but each one of them seem to recommend only using Cursors as a last resort due to their lack of speed. I've also seen some people use stored procedures, but I can't use those, since my boss has ruled those as off-limits. Am I in need of a Cursor, or is there a better way to do this?
Have a look at something like this (Full Example)
DECLARE #ATTRIBUTES TABLE(
ID INT,
ATTRIBUTE_NAME VARCHAR(100)
)
INSERT INTO #ATTRIBUTES SELECT 1, 'Created'
INSERT INTO #ATTRIBUTES SELECT 2, 'Embedded_Image'
DECLARE #ATTRIBUTE_VALUES TABLE(
ATTRIBUTE_ID INT,
VALUE VARCHAR(100),
DOC_ID INT
)
INSERT INTO #ATTRIBUTE_VALUES SELECT 1, '2010/11/01', 1
INSERT INTO #ATTRIBUTE_VALUES SELECT 2, 'Home.png', 1
INSERT INTO #ATTRIBUTE_VALUES SELECT 2, 'Castle.png', 1
INSERT INTO #ATTRIBUTE_VALUES SELECT 2, 'Apartment.jpg', 1
INSERT INTO #ATTRIBUTE_VALUES SELECT 1, '2008/06/23', 2
INSERT INTO #ATTRIBUTE_VALUES SELECT 2, 'Ski Jump.jpg', 2
INSERT INTO #ATTRIBUTE_VALUES SELECT 2, 'Snowboarding.png', 2
DECLARE #DOCUMENTS TABLE(
ID INT,
[TEXT] VARCHAR(100)
)
INSERT INTO #DOCUMENTS SELECT 1, 'Homes of the ...'
INSERT INTO #DOCUMENTS SELECT 2, 'Winter sports ...'
;WITH Vals AS (
SELECT d.ID DOC_ID,
d.[TEXT] [TEXT],
a.ATTRIBUTE_NAME,
av.VALUE
FROM #DOCUMENTS d INNER JOIN
#ATTRIBUTE_VALUES av ON d.ID = av.DOC_ID INNER JOIN
#ATTRIBUTES a ON av.ATTRIBUTE_ID = a.ID
)
SELECT *
FROM (
SELECT DOC_ID,
[TEXT],
ATTRIBUTE_NAME,
stuff(
(
select ',' + t.VALUE
from Vals t
where t.DOC_ID = v.DOC_ID
AND t.ATTRIBUTE_NAME = v.ATTRIBUTE_NAME
order by t.VALUE
for xml path('')
),1,1,'') Concats
FROM Vals v
GROUP BY DOC_ID,
[TEXT],
ATTRIBUTE_NAME
) s
PIVOT ( MAX(ConCats) FOR ATTRIBUTE_NAME IN ([Created],[Embedded_Image])) pvt
Output
DOC_ID TEXT Created Embedded_Image
1 Homes of the ... 2010/11/01 Apartment.jpg,Castle.png,Home.png
2 Winter sports ... 2008/06/23 Ski Jump.jpg,Snowboarding.png
From your sample, and with support from common sense, I venture the hypothesis that
A document has a single creation date.
A document can have many embedded images.
So pivoting on creation date is straightforward:
SELECT DOC_ID
, VALUE AS Created
FROM ATTRIBUTE_VALUES
WHERE ATTRIBUTE_ID = 1
and joining this subquery to your Documents table gives you the first three columns of your desired output.
Your final column summarizes multiple embedded images for each document. I personally would use some standard reporting tool (e.g. MS Access or Crystal Reports). Alternatively, create a new empty table with your four desired columns, populate the first three columns with a SQL INSERT statement, and then have Perl (or C#, or your favorite declarative language) query for the embedded images of each document, concatenate the results with commas, and insert the concatenation into your fourth column.
But if you want to do it in SQL, the concatenate-multiple-values question has been asked here before, e.g. in How to create a SQL Server function to "join" multiple rows from a subquery into a single delimited field?.