Concatenating field data into string via Access SQL - sql

Imagine the following table
ID | Name
----------
1 | Shaun
1 | Terrence
2 | Jessica
I need to concatenate the string data in Name based on ID
ID | Name
-----------
1 | Shaun, Terrence
2 | Jessica
I am using an access database. I was thinking I could do a pivot transform and try to concatenate those fields but the problem is its hard to dynamically loop through the total field count. Any ideas?
**Edit: Order does not matter, I just want to concatenate based on ID with , and space being the delimiter. I am calling this sql code using an ADO connection from excel vba.

This is a similar problem I had recently trying to pivot a two column table; MS Access convert and summarise two rows into columns with unique names
'Name' is a reserved word and 'ID' is usually a auto-index with unique numbers so I changed your columns to UserID and UserName respectively.
There are some problems with creating the answer in a single subquery so I ended up doing this:
Create a temporary table with an index:
SELECT t1.UserID, t1.UserName,
(SELECT COUNT(*) + 1
FROM Table1 t2
WHERE t1.UserID = t2.UserID and t2.UserName < t1.UserName) AS [Index]
INTO Table1_indexed
FROM Table1 AS t1;
create a temporary cross tab table:
TRANSFORM First(Table1_indexed.UserName) AS FirstOfUserName
SELECT Table1_indexed.UserID FROM Table1_indexed
GROUP BY Table1_indexed.UserID
PIVOT Table1_indexed.Index;
concatenate the name fields
SELECT Table1_crosstab.UserID, Table1_crosstab.[1], Table1_crosstab.[2],
IIf([1] Is Not Null,[1]) & IIf([2] Is Not Null,", " & [2]) AS ConcatenatedName
FROM Table1_crosstab;
If you have more than two name fields you could adjust the concatenate query to the maximum number you expect.
It might be possible to merge these steps into a single query but I've not yet found a way.

You can create a Visual Basic Function and call it from your query, e.g. something like this (assuming your table is called Names):
Public Function ListOfNames(id as Integer) As String
Dim rs As Recordset
Set rs = CurrentDb.OpenRecordset("Select Name from Names where ID=" & id, DbOpenSnapshot)
ListOfNames = ""
If Not (rs.EOF and rs.BOF) Then
rs.MoveFirst
Do Until rs.EOF = True
If (Len(ListOfNames) > 0) Then
ListOfNames = ListOfNames & “, “
End If
ListOfNames = ListOfNames & rs!Name
rs.MoveNext
Loop
End If
rs.Close
End Function
Then you can for instance call the function from your query:
SELECT ID, ListOfNames([ID]) as Name From Names Group By ID

Related

MS Access Memo field truncated

This issue sounds familiar, but please read it to the end, as it has a twist to similar questions I found in this forum or elsewhere.
I am using Access 2010 to build a simple app to create some code to execute in another system. Part of it is to convert number of records per user into a single record, with all user entries separated by comma. To illustrate, here is the sample of my input:
USER MBR
---- ----
USR1 DRG
USR1 ABC
USR1 XYZ
USR2 123A
USR2 ABS2
And I need it in this format:
USER MBR_LIST
----- ---------------
USR1 DRG, ABC, XYZ
USR2 123A, ABS2
So far, so familiar. I've used the code posted by Allen Brown back in 2008 (http://allenbrowne.com/func-concat.html), but I've run into an issue.
The code works fine - when I debug it, I can see that my output string ("strOut") has all MBRs for a single user, as expected. Mind you, sometimes this string is over 7,000 char long. So, again following some great advice found around, I've created a table with the MBR_LIST field set to MEMO, and I execute Allen's function as:
INSERT INTO Table2 ( [USR], MBR_LIST )
SELECT B.USR, ConcatRelated("MBR","Table1","Table1.USR = '" & B.USR & "'","USR") AS Mbr_List
FROM (SELECT Table1.USR FROM Table1 GROUP BY Table1.USR) AS B;
(NOTE: the query is built as such to avoid doing Group By on the Memo field, which is known to be truncating the Memo field)
Still, after doing this, my MBR_LIST field in the table shows only 320 chars (?).
I even tried adding an empty row, as suggested in this post: MS Access Create Table is Truncating Memo Field, but with no success - the field still gets truncated to 320 chars:
INSERT INTO Table2 ( [USR], MBR_LIST )
SELECT B.USR, "" as Mbr_List
FROM [Table2] as B Where (False)
UNION ALL
SELECT B.USR, ConcatRelated("MBR","Table1","Table1.USR = '" & B.USR & "'","USR") AS Mbr_List
FROM (SELECT Table1.USR FROM Table1 GROUP BY Table1.USR) AS B;
As a last resort, using the MID() I've created 20 "chunks" of 300 chars each (Mbr_1 to Mbr_20) in my SELECT statement, and I got them all back fine. Then I wrapped this into another SELECT with those chunks concatenated (Mbr_1 & Mbr_2 & ... & Mbr_20) AS Mbr_LIST, and got - 320 chars!
Any ideas why and how to insert/display the full string created by the function?
Thanks,
tribe
There is a workaround I created years ago using DLookup in a helper function.
Not the fastest, of course, but if anything else fails ...
Access Query truncates Memo Field
To insert a long text, use VBA when you have retrieved the values for USR and MBR_LIST:
Dim rs As DAO.Recordset
Set rs = CurrentDb.OpenRecordset("Select [USR], MBR_LIST From Table2")
rs.AddNew
rs!USR.Value = USR
rs!MBR_LIST.Value = MBR_LIST
rs.Update
rs.Close
Set rs = Nothing

VBA Iteration vs SQL Speed in Access

Goal: I have a bunch of dates, I want to update the records of the minimum date by category:
JVID APDATE TAG into > JVID APDATE TAG
1 201501 Use 1 201501 Don't Use
1 201502 Use 1 201502 Use
1 201502 Use 1 201502 Use
1 201503 Use 1 201503 Use
2 201502 Use 2 201502 Don't Use
2 201503 Use 2 201503 Use
The method I'm using is as follows:
I create a dictionary where the Key = ID, and Value = MinDateByID
Then I loop thorough the keys (for each key in dictionary) and run an update query for each ID that checks an IIF statement updating Use/Don't Use based on the date matching the min date.
This works, but w/ +80k IDs covering +1M records it takes forever.
I'm considering running the same thing, but dumping SQL and just iterating through the records, but I can't imagine that'd be faster?
I'm looking for SQL or VBA suggestions.
Thank you in advance!
EDIT - Added SQL From Comments
UPDATE [FY16 Q12 BE] SET [FY16 Q12 BE].[Record Use] = IIF([FY16 Q12 BE].[Date] = "201601", "Use", "Don't Use") WHERE ([FY16 Q12 BE].[ID]="20165645699");
I look through each of the dictionary key/value pairs ex (20165645699, 201601)
creating and running this script in various forms 80k+ times
MS Access is more restrictive than mainstream databases in joined updates, so I had to use a temporary table T2 to hold the minimum values.
SELECT T1.ID, MIN(T1.RDate) AS MinDate INTO T2
FROM Test1
GROUP BY T1.ID;
Now I can perform the joined update:
UPDATE T1 LEFT JOIN T2
ON T1.ID=T2.ID AND T1.RDate=T2.MinDate;
Finally, I drop the temporary table :
DROP T2;
SET TAG = IIF(T2.ID IS NULL, "Don't Use", "Use");
[I have named your table T1 and the date field RDate to avoid a conflict with a reserved word.]
This could be speeded up further by adding a primary key to T2 on (ID, MinDate) and an index on T1 on (ID, RDate).
I think you can do it with one update query - or at least a combination of multiple queries in one SQL statement.
I'm going to use your example data, since I can't figure out what your actual table or field names are in your comments.
You need to replace Table4 with your table name - and ID/Date/Tag fields to match your column names.
UPDATE SQL:
UPDATE Table4 SET Table4.Tag = "Don't Use"
WHERE ([Date] & "-" & [ID])
In (SELECT MergeID FROM
(SELECT Mins.[MinOfDate] & "-" & [ID] AS MergeID
FROM
(SELECT Table4.ID, Min(Table4.Date) AS MinOfDate
FROM Table4
WHERE Table4.Tag="Use"
GROUP BY Table4.ID) AS Mins) AS Merges);
If you don't need the criteria to only check TAGs that haven't been changed then you can eliminate the criteria WHERE Table4.Tag="Use"
OTHER 'No Tag Check' OPTION
UPDATE SQL:
UPDATE Table4 SET Table4.Tag = "Don't Use"
WHERE ([Date] & "-" & [ID])
In (SELECT MergeID FROM
(SELECT Mins.[MinOfDate] & "-" & [ID] AS MergeID
FROM
(SELECT Table4.ID, Min(Table4.Date) AS MinOfDate
FROM Table4
GROUP BY Table4.ID) AS Mins) AS Merges);
I want to suggest this. But I see you have duplicate dates and I'm not sure how you intend to handle these when you have ties for the earliest date.
update [FY16 Q12 BE]
set TAG = "Don't use"
where not exists (
select 1 from [FY16 Q12 BE] as t2
where t2.ID = [FY16 Q12 BE].ID and t2.[DATE] < [FY16 Q12 BE].[DATE]
)
I settled on an iterative approach - I'm not sure why it's so much faster than the SQL options outline above, but it does the trick. Thank you for your feedback.
Sub MinAPInclude(ByVal Tablename As String)
Dim db As DAO.Database
Dim qd As DAO.QueryDef
Dim rs As DAO.Recordset
Dim strList As String
Dim JVMinAP As Dictionary
Set JVMinAP = New Dictionary
Set db = DBEngine(0)(0)
Dim rst As DAO.Recordset
If Not DoesFieldExist(Tablename, "APDate") Then Exit Sub
SQLStatement = "SELECT [" & Tablename & "].[JVID], Min([" & Tablename & "].[APDate]) AS TargetAP"
SQLStatement = SQLStatement & " FROM [" & Tablename & "]"
SQLStatement = SQLStatement & " GROUP BY [" & Tablename & "].[JVID];"
Set qd = db.CreateQueryDef("", SQLStatement)
Set rs = qd.OpenRecordset
rs.MoveFirst
Do Until rs.EOF
If Not IsNull(rs("JVID")) Then
If Not JVMinAP.Exists(CStr(rs("JVID"))) Then
MinAP = rs("TargetAP")
JVMinAP.Add CStr(rs("JVID")), MinAP
End If
End If
rs.MoveNext
Loop
rs.Close
Set rst = db.OpenRecordset(Tablename)
rst.MoveFirst
Do Until rst.EOF
If rst("Record Use") <> "Include" Then
If rst("APDate") = JVMinAP(CStr(rst("JVID"))) Then
rst.Edit
rst("Record Use") = "Include"
rst.Update
End If
End If
rst.MoveNext
Loop
rst.Close
Set rst = nothing
Set rs = Nothing
Set qd = Nothing
Set db = Nothing
End Sub

SQL statement for combobox row source

I'm trying to define a SQL statement to use as the Row Source for a ComboBox on an MSAccess form. The SQL should select records from a table tblI where a particular table field matches a variant parameter varS set by the user; however, if varS is Null or not present in another table tblS, the SQl should select all records in tblI.
I can code the first parts of this (varS matches or is null):
SELECT tblI.ID, tblI.S FROM tblI WHERE ((tblI.S = varS) OR (varS Is Null)) ORDER BY tblI.ID;
Where I'm struggling is incorporating the final element (varS not present in tblS). I can code a test for the absence of varS in tblS:
Is Null(DLookup("[tbls.ID]","tblS","[tblS.ID]= " & varS))
but I can't work out how to incorporate this in the SQL statement. Should this work?
SELECT tblI.ID, tblI.S FROM tblI WHERE tblI.S = varS OR varS Is Null OR DLookup("[tbls.ID]","tblS","[tblS.ID]= " & varS) Is Null ORDER BY tblI.ID;
When run as a query it returns every record in tblS no matter the value of varS.
Table structure:
tblI contains 2 fields, Autonumber ID and Long S
tblS contains 1 field, Autonumber ID
My own approach to this problem would be something like this:
Private Sub SetComboSource(vID as Variant)
Dim sSQL as String
sSQL = "SELECT tblI.ID, tblI.S " & _
"FROM tblI "
If IsNull(vID) = False Then
If IsNumeric(vID) = True Then
If DCount("ID", "tblS", "ID = " Clng(vID)) > 0 Then
sSQL = sSQL & "WHERE tblI.S = " & CLng(vID)
End If
End If
End If
sSQL = sSQL & " ORDER BY tblI.ID"
Me.cboComboBox.RowSource = sSQL
End Sub
BTW, I recommend you give your tables and fields more descriptive names and then use aliasing in your SQL, especially for table names. I also think it's best to avoid using Variant variables. I usually use Longs for something like this and I take a value less than 1 to mean that the user didn't select anything, or selected ALL, or whatever meaning you want to derive from it. In other words, my ID's are always a number greater than zero and an ID of less than 1 in a variable means that the ID is empty. Which I use as a signal to create a new record, or to return all records, or whatever meaning you want to derive from it in the given context.
The following should work;
SELECT tblI.ID, tblI.S
FROM tblI
WHERE tbl.ID=varS
OR varS NOT IN(SELECT ID from tblS)

How to remove period in Middle Initial ms access

I have two table that 1 table need to find a match on the other table.
Say I have table1 and Masterfiles table. Table1 has a name field the same with Masterfiles.
But table1 name field has different value because it has only middle initial while Masterfiles has middle name.
Now I want to get the other field value from Masterfiles called "Gender", since table1 has no value on gender field.
The table look like this:
Table1 Masterfiles
Name Gender Name Gender
Smith, John E. Smith, John Estaw M
Canard, Donald R Canard, Donald Reever M
Since I have a lot of records in table1 that need to get the value of gender field I need to do it programmatically.
Currently I have the following code:
Dim db As Database
Dim rs As DAO.Recordset
Dim rs2 As DAO.Recordset
Dim strSQL As String
Set db = CurrentDb
Set rs = db.OpenRecordset("Table1")
Do While Not rs.EOF
strSQL = "SELECT [Name], Gender FROM Masterfiles WHERE [Name] Like '%" & Me!txtName & "%'"
Set rs2 = db.OpenRecordset(strSQL)
If rs2.RecordCount > 0 Then
rs!Gender = rs2!Gender
End If
Loop
But this code will not get any result because some of the value of Name field in table1 has a period in the middle initial while other row has no period. Please refer to the sample data above. The middle initial of "Smith, John" has period while "Canard, Donald" has no period.
So how can I query Masterfiles and get the value of Gender field and put it in table1 gender field?
strSQL = "SELECT [Name], Gender FROM Masterfiles WHERE [Name] Like '%" & replace(Me!txtName,".","") & "%'"
You have the name in VB; it is best just to use VB to remove any . from the end of the name before searching:
Dim searchName as String
If Me!txtName.Substring(Me!txtName.Length - 1,1) = '.' Then
searchName = Me!txtName.Substring(0,Me!txtName.Length - 1)
Else
searchName = Me!txtName
End If
strSQL = "SELECT [Name] FROM Masterfiles WHERE [Name] Like '%" & searchName & "%'"
However, in the long term you should probably fix your design:
Give each person a numeric ID that is the same in all tables.
Split the name into first, middle, and last fields.
Use a single table to store all information that is unique to the person. Then link it to other tables using the ID.

Query creating an identifier for each packet

Sorry the title is not very descriptive but it is a tricky problem to word.
I have some data, about 200 or more rows of it, and each row has a PacketID, so several rows belong in the same packet. What I need to do, is convert all the PacketIDs from (Example - BDFD-2) to just a number (Example - 1) so all the entries with a packet identifier x need to have a packet identifier of say 3. Is there an SQL query that can do this? Or do I just have to go through manually.
You asked about a query. I wrote a quick VBA procedure instead just because it was so easy. But I'm unsure whether it is appropriate for your situation.
I created tblPackets with a numeric column for new_PacketID. I hoped that will make it clearer to see what's going on. If you truly need to replace PacketID with the new number, you can alter the procedure to store CStr(lngPacketID) to that text field. So this is the sample data I started with:
PacketID new_PacketID packet_data
BDFD-2 a
R2D2-22 aa
BDFD-2 b
R2D2-22 bb
EMC2-0 aaa
EMC2-0 bbb
And this is the table after running the procedure.
PacketID new_PacketID packet_data
BDFD-2 1 a
R2D2-22 3 aa
BDFD-2 1 b
R2D2-22 3 bb
EMC2-0 2 aaa
EMC2-0 2 bbb
And the code ...
Public Sub RenumberPacketIDs()
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim lngPacketID As Long
Dim strLastPacketID As String
Dim strSql As String
strSql = "SELECT PacketID, new_PacketID" & vbCrLf & _
"FROM tblPackets" & vbCrLf & _
"ORDER BY PacketID;"
Set db = CurrentDb
Set rs = db.OpenRecordset(strSql)
With rs
Do While Not .EOF
If !PacketID <> strLastPacketID Then
lngPacketID = lngPacketID + 1
strLastPacketID = !PacketID
End If
.Edit
!new_PacketID = lngPacketID
.Update
.MoveNext
Loop
.Close
End With
Set rs = Nothing
Set db = Nothing
End Sub
I think an approach like that could be fine for a one-time conversion. However if this is an operation you need to perform repeatedly, it could be more complicated ... especially if you need each PacketID replaced with the same number from one run to the next ... eg. BDFD-2 was replaced by 1 the first time, so must be replaced by 1 every time you run the procedure.
If you only have a few packet IDs, you can just use update:
UPDATE table_name
SET PacketID =
(
CASE PacketID
WHEN 'BDFD-2' THEN 3
WHEN 'ABCD-1' THEN 5
ELSE 2
END
)
The ELSE is optional.
I am not sure why you even want to convert the packet ids to a number, they seem perfectly fine as they are. You could create a table of packets as follows
SELECT DISTINCT TableOfRows.Packet_id AS PacketId INTO Packets FROM TableOfRows;
You can then use this to select the packet you are interested in and display the corresponding rows