VBA Iteration vs SQL Speed in Access - sql

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

Related

select in select sql query

I want to create a sql query. The column name that I want, it's in another table. I wrote this query.
SELECT (SELECT FieldName From TableGENQuest WHERE ID = 1)
FROM TableGEN
WHERE strSO = 'RV12648-01';
I want to get the data from the strGEN1 columns using the FieldName column of the TableGENQuest table.That is data I want No significant transportation damage observed.
tl;dr: Your request is not possible using MS Access SQL alone.
You will need to use VBA to open a recordset containing the content of the table TableGENQuest and construct an appropriate SQL statement whilst iterating over the records held by such recordset.
For example:
Sub GenerateQuery()
Dim dbs As DAO.Database
Dim rst As DAO.Recordset
Dim sql As String
Set dbs = CurrentDb
Set rst = dbs.OpenRecordset("SELECT FieldName FROM TableGENQuest WHERE ID = 1")
With rst
If Not .EOF Then
.MoveFirst
Do Until .EOF
sql = sql & "[" & !FieldName & "], "
.MoveNext
Loop
sql = "select " & Left(sql, Len(sql) - 2) & " from TableGEN WHERE strSO = 'RV12648-01';"
End If
.Close
End With
If sql <> vbNullString Then dbs.CreateQueryDef "OutputQuery", sql
Set rst = Nothing
Set dbs = Nothing
End Sub
The above will generate a query defined in the current database with a SQL statement selecting the fields whose fieldnames are sourced using the SQL:
SELECT FieldName FROM TableGENQuest WHERE ID = 1
The difficulty and convoluted nature of this method indicates that your database is poorly designed: the field names themselves should instead appear as rows within another table.
SELECT b.FieldName FROM TableGEN a
LEFT OUTER JOIN TableGENQuest b
ON a.ID=b.ID
WHERE a.strSO = 'RV12648-01'
AND b.ID=1
OR
SELECT a.strSO,b.FieldName,b.ID
FROM TableGEN a
LEFT OUTER JOIN
(
SELECT FieldName,ID From TableGENQuest WHERE ID = 1
)
b
ON a.ID = b.ID
WHERE strSO = 'RV12648-01'
Try This Query...or can change Join Type...
Try this:
SELECT TableGENQuest.FieldName
FROM TableGEN, TableGENQuest
WHERE TableGENQuest.ID=1 AND WHERE strSO = 'RV12648-01';

Concatenating field data into string via Access 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

Update Based on Select

I am attempting to update one column of a table based on data present in other records of the same table. All records either have the same date in the "CurrentDate" field or are null. I want to change those with null values to be the same as the rest of the fields.
Here is my code, but I am getting a syntax error:
Public Sub RiskVisual()
Dim db As DAO.Database
Set db = CurrentDb
---
DoCmd.RunSQL "UPDATE Hold3 SET CurrentDate = (SELECT CurrentDate FROM Hold3 LIMIT 1) WHERE CurrentDate IS NULL;"
End Sub
Thanks in advance for your help.
In MS Access the "TOP 1" works better than "LIMIT 1". You will also want to specify when seeking for the top 1 that the top 1 that is not null. Try something like this:
UPDATE Hold3 SET Hold3.CurrentDate = (SELECT TOP 1 Hold3.CurrentDate FROM Hold3 WHERE (((Hold3.CurrentDate) Is Not Null))) WHERE (((Hold3.CurrentDate) Is Null));

Access - create a sub total in a query

I have a query over a few tables and get a result in the form of:
SomeId Input
1 2
1 5
2 3
2 1
1 2
I'd like to be able to sum by Id as a third field, so I would get
SomeId Input subTotal
1 2 2
1 5 7
2 3 3
2 1 4
1 2 9
Is it possible?
Thanks
Here's a couple other ideas. Both have their drawbacks though. Both of them involve using a regular query.
First idea: Calling a VBA function to keep track of totals.
The drawback is that you have to order your table by SomeID.
Also the running total only resets itself when the function gets a different SomeID even if it's a different query. This means that the value of SomeID on the first record must be different than the last record of the last previous query.
SELECT SomeTable.SomeId, SomeTable.SomeInput, MyRunningTotal([SomeID],[SomeInput]) AS SubTotal
FROM SomeTable
ORDER BY SomeTable.SomeId;
Function MyRunningTotal(SomeID As Long, SomeInput As Long) As Long
Static LastSomeID As Long
Static RunningTotal As Long
If SomeID <> LastSomeID Then
RunningTotal = 0
LastSomeID = SomeID
End If
RunningTotal = RunningTotal + SomeInput
MyRunningTotal = RunningTotal
End Function
Second Idea: Using DSum. This is basically a query within a query.
The drawback is that for large recordsets it can be very slow. This is because it has to run a separate query for every record.
Also, you have to add an Auto-increment field (in the sample code below it's ID).
SELECT SomeTable.ID, SomeTable.SomeId, SomeTable.SomeInput,
DSum("SomeInput","SomeTable","[SomeID]=" & [SomeID] & " and [ID]<=" & [ID]) AS SubTotal
FROM SomeTable;
Yes, it's certainly possible, but with the problem as stated it cannot be accomplished by using just Access SQL queries. The two issues are:
The source data has no sequential key value (so a self-join with a <= condition cannot be used)
The source data is not sorted by SomeId (suggesting that the order may have some other significance), which would further complicate a set-based approach.
Fortunately, the VBA required to do this is not too involved:
Sub CreateSubtotals()
Dim cdb As DAO.Database, rst As DAO.Recordset
Dim dct As Object '' Dictionary
Set dct = CreateObject("Scripting.Dictionary") '' New Dictionary
Set cdb = CurrentDb
'' 'dct' will hold the running totals for each 'SomeId'
Set rst = cdb.OpenRecordset( _
"SELECT DISTINCT SomeId " & _
"FROM qryYourOriginalQuery", _
dbOpenSnapshot)
Do While Not rst.EOF
dct.Add rst!SomeId.Value, 0
rst.MoveNext
Loop
rst.Close
'' create new table to hold results
cdb.Execute _
"SELECT SomeId, Input, 0 AS subTotal " & _
"INTO tblYourDataWithSubtotals " & _
"FROM qryYourOriginalQuery", _
dbFailOnError
'' fill in the 'subTotal' column
Set rst = cdb.OpenRecordset("tblYourDataWithSubtotals", dbOpenTable)
Do While Not rst.EOF
dct(rst!SomeId.Value) = dct(rst!SomeId.Value) + rst!Input.Value
rst.Edit
rst!subTotal.Value = dct(rst!SomeId.Value)
rst.Update
rst.MoveNext
Loop
rst.Close
Set rst = Nothing
Set dct = Nothing
Set cdb = Nothing
End Sub

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