I am using a Microsoft Access 2010 database to import values from one table and append them to a summary table.
One of the issues I am having is finding the previous and next value from the select statement.
This would look as follows.
JOINT JOINT AHEAD JOINT BEHIND
100103 200203
200203 300303 100103
300303 200203
I would like to create this using a SQL code
Be cautious when considering correlated subqueries. They can be very slow. And if you build a query which includes two correlated subqueries, you will magnify the problem.
If your source table contains a smallish number of rows (say a few dozen), the slowness may not be an issue. However, if the table includes a thousand rows you will most certainly notice it. And if your JOINT field is not indexed, the performance could be painfully slow.
If you will be running your query from within an Access session, you can use domain functions (DMin and DMax) instead of correlated subqueries. Domain functions are often criticized as slow. However, in this situation they can be dramatically faster than correlated subqueries.
Correction: You don't need to run your query from within an Access session for it to be able to use the DMin() and DMax() functions. I attached a VBScript example which opens an ADO Recordset based on my qryDomainFunctions. It works without error and correctly reports RecordCount: 1000
I created a table, joints, with a long integer field joint as primary key and added 1000 rows. Then I created these 2 queries:
qryCorrelatedSubqueries:
SELECT
a.joint,
(SELECT TOP 1 joint
FROM joints b
WHERE b.joint>a.joint
ORDER BY joint) AS Ahead,
(SELECT TOP 1 joint
FROM joints b
WHERE b.joint<a.joint
ORDER BY joint DESC) AS Behind
FROM joints AS a;
qryDomainFunctions:
SELECT
j.joint,
DMin("joint","joints","joint > " & [joint]) AS joint_ahead,
DMax("joint","joints","joint < " & [joint]) AS joint_behind
FROM joints AS j;
Here is a transcript from the Immediate window where I compared the speed of those 2 queries, using the QueryDuration function below. That function returns duration in milliseconds.
? QueryDuration("qryDomainFunctions")
0
? QueryDuration("qryCorrelatedSubqueries")
889
Note that both those queries benefit from the index on the joints field. When I dropped the index, compacted the db, and re-ran the tests I got these results:
? QueryDuration("qryDomainFunctions")
16
? QueryDuration("qryCorrelatedSubqueries")
4570
This is the module with the code I used. QueryDuration is by no means the last word on performance measurement. However it's good enough to give us a rough idea of the relative speeds of those 2 queries.
Option Compare Database
Option Explicit
Private Declare Function apiGetTickCount Lib "kernel32" _
Alias "GetTickCount" () As Long
Public Function QueryDuration(ByVal pQueryName As String) As Long
Dim db As DAO.Database
Dim lngStart As Long
Dim lngDone As Long
Dim rs As DAO.Recordset
Set db = CurrentDb()
lngStart = apiGetTickCount() ' milliseconds '
Set rs = db.OpenRecordset(pQueryName, dbOpenSnapshot)
If Not rs.EOF Then
rs.MoveLast
End If
lngDone = apiGetTickCount()
rs.Close
Set rs = Nothing
Set db = Nothing
QueryDuration = lngDone - lngStart
End Function
DomainFunctionsQuery.vbs:
Option Explicit
Dim cn, rs
Set cn = CreateObject("ADODB.Connection")
cn.Open "Provider=Microsoft.Jet.OLEDB.4.0;" & _
"Data Source='database1.mdb'"
Set rs = CreateObject("ADODB.Recordset")
rs.CursorLocation = 3 ' adUseClient '
rs.Open "qryDomainFunctions", cn, 3 ' adOpenStatic = 3 '
WScript.Echo "RecordCount: " & rs.RecordCount
rs.Close
Set rs = Nothing
cn.Close
Set cn = Nothing
How about:
SELECT a.JOINT,
(SELECT TOP 1 Joint
FROM Joint b
WHERE b.JOINT>a.JOINT
ORDER BY Joint) AS Ahead,
(SELECT TOP 1 Joint
FROM Joint b
WHERE b.JOINT<a.JOINT
ORDER BY Joint DESC) AS Behind
FROM Joint AS a;
Related
For the past couple weeks I've been working on a very unconventional solution to a problem for my job. I'm almost there, but I need to know the most efficient way to do the last step. I will dumb it down so I don't have to write an essay describing the insane nature of the problem I've been working on.
I have four large local tables in MS Access with a total of over 500,000 records.
Each table represents a different type of product.
The productID for table1 always starts with "9"
The productID for table2 always starts with "8"
The productID for table3 always starts with "4"
The productID for table4 always starts with "3"
I have a vba procedure written that does exactly what I need it to do except I have it querying information using only table1 thus far. Basically, a user inputs a productID and the procedure searches the table for that record and sends the information to a sharepoint list. Speed of execution is HIGHLY important in my situation. So, what is the fastest way to have it run? Should I write a statement that says "If the ID starts with 9 then search this table, ElseIF ..... and so on." Or, should I combine all the tables into one and not have it look at the first digit of the ID?
I know this sounds like a simple issue but trust me, this is a wild over simplification of the real issue and it would take 2,000 words to explain how ridiculous it actually is; I'm not kidding. However, I am fairly confident that the answer to the above question will give me all the information I need to finish this project successfully. I have come so far and all I have left is to figure out the most efficient way to apply it to the 3 other tables.
Thanks!
As the tables are local, use the Seek method which is extremely fast:
Recordset.Seek method (DAO)
If the tables were linked, you could still using Seek by opening the backend database. This is an example:
Function SeekTable()
Const cstrTable As String = "tblValue01"
Const cstrAttached As String = ";DATABASE="
Dim wks As Workspace
Dim dbs As Database
Dim tdf As TableDef
Dim rst As Recordset
Dim strConnect As String
Dim strTablename As String
Set wks = DBEngine(0)
Set dbs = wks(0)
Set tdf = dbs.TableDefs(cstrTable)
strConnect = tdf.Connect
strTablename = tdf.SourceTableName
Set tdf = Nothing
If InStr(1, strConnect, cstrAttached, vbBinaryCompare) = 1 Then
strConnect = Mid(strConnect, Len(cstrAttached) + 1)
' Open database shared and read-only.
Set dbs = wks.OpenDatabase(strConnect, False, True)
Set rst = dbs.OpenRecordset(strTablename)
'
' Perform Seek operation. Example.
rst.Index = "ID"
rst.Seek "=", 10010
Debug.Print rst!Value
'
rst.Close
Set rst = Nothing
End If
dbs.Close
Set dbs = Nothing
Set wks = Nothing
End Function
I can't get a running sum to work in an Access query. I have a pipesystem, where I'm trying to summarize the flow Q, through the pipenetwork. I've been trying to do a running sum based on a group_by ID_downstream and a DSum on Q_total. However I keep getting errors or wrong input.
The desired output, is that I can see the flow accumulated through the network as shown in the table and picture.
You have several options. One, however, won't do, and that is a recursive query using SQL only; Access can't be fooled and will claim about a circular reference. Your only chance is to create a query resolving a limited number of levels only, say, 8 or 10.
But you can cover the recursive call in a domain aggregate function like DLookup. This is, however, very slow as DLookup calling the query will run for each and every record. For more than some dozens of records this will most likely be unacceptable.
The fastest way, for an unlimited number of levels, I've found, is to create a lookup function which walks the tree for each record. This can output either the level of the record or a compound key build by the key of the record and all keys above.
As the lookup function will use the same recordset for every call, you can make it static, and (for JET/ACE) you can improve further by using Seek to locate the records.
Here's an example which will give you an idea:
Function RecursiveLookup(ByVal lngID As Long) As String
Static dbs As Database
Static tbl As TableDef
Static rst As Recordset
Dim lngLevel As Long
Dim strAccount As String
If dbs Is Nothing Then
' For testing only.
' Replace with OpenDatabase of backend database file.
Set dbs = CurrentDb()
Set tbl = dbs.TableDefs("tblAccount")
Set rst = dbs.OpenRecordset(tbl.Name, dbOpenTable)
End If
With rst
.Index = "PrimaryKey"
While lngID > 0
.Seek "=", lngID
If Not .NoMatch Then
lngLevel = lngLevel + 1
lngID = !MasterAccountFK.Value
If lngID > 0 Then
strAccount = str(!AccountID) & strAccount
End If
Else
lngID = 0
End If
Wend
' Leave recordset open.
' .Close
End With
' Don't terminate static objects.
' Set rst = Nothing
' Set tbl = Nothing
' Set dbs = Nothing
' Alternative expression for returning the level.
' (Adjust vartype of return value of function.) ' RecursiveLookup = lngLevel ' As Long
RecursiveLookup = strAccount
End Function
This assumes a table with a primary key ID and a foreign (master) key pointing to the parent record - and a top level record (not used) with a visible key (AccountID) of 0.
Now your tree will be nicely shown almost instantaneously using a query like this, where Account will be the visible compound key:
SELECT
*, RecursiveLookup([ID]) AS Account
FROM
tblAccount
WHERE
AccountID > 0
ORDER BY
RecursiveLookup([ID]);
I'm new to access/vba and trying to set up a project database. I have a table ("Updates") that is generated when changes are made to certain fields on a form (used for project updates by the end user). It has the primary key UpdateID, foreign key ProjectID as well as UTimeStamp, OldValue, NewValue, History. I use the history key to identify which type of update was made (for example for Status, History=1). I want to then count the number of projects for each status at the end of each month, keeping historical data to allow users to track the changes from month to month (or even compare data from months apart). I'm trying to write a code (in VBA for access) that would take into account that there are sometimes multiple status updates in each month and I don't want them to get counted twice, also some months no updates are made but I still want them included in the count (using the last updated status before that month as the status).
I was thinking of using a combination of looping through the records and checking to see if a value exists for that specific ProjectID and month and inserting the last value (most recent) into a new table "StatusTracking" and if no record exists then using the INSERT INTO function to add a new record. "StatusTracking" will have the fields ID, ValueMonth, ValueYear, (since ideally I want to track over the course of more than a year) Status, ProjectID. However, I am very new to this and am having trouble getting started as I'm not sure the best way to loop through both the months and ProjectID.
Public Function getStatus()
Dim varMonth As Integer
Dim ReportStatus As String
Dim RS As DAO.Recordset
Dim db As DAO.Database
Dim sqlStr As String
sqlStr = "SELECT Updates.ProjectID, Format(Month([UTimeStamp])) AS UpdateMonth, ProjectList.Status, Updates.NewValue, Updates.UTimeStamp" & _
"FROM Updates RIGHT JOIN ProjectList ON Updates.ProjectID = ProjectList.ProjectID" & _
"WHERE (((Updates.History) = 1))" & _
"GROUP BY Updates.ProjectID, Format(Month([UTimeStamp])), ProjectList.Status, Updates.NewValue, Updates.UTimeStamp"
Set db = CurrentDb
Set RS = db.OpenRecordset(sqlStr)
With RS
.MoveLast
.MoveFirst
While (Not .EOF)
'Cycle through each month
For varMonth = 1 To 12 Step 1
ReportStatus = DLast("NewValue", RS, "UpdateMonth = " & varMonth)
RS.Fields ("Status") <> RS.Fields("NewValue")
End Function
Any help is appreciated!
DCount is useful for counting unique occurrences in a specified field. The link I provided should put you on the right track there. Note that that only returns the count itself, and not the records. If you need to populate a recordset, you can use SELECT DISTINCT in your query to return only the records that have, well, distinct values in your criteria.
I recently worked on a project that involved building a history table for tracking purposes. Since it was based around forms, I opted for using .addnew and .update rather than INSERT INTO. Of course, use what's best for the situation at hand; I used .addnew and .update for the main reason that I had a lot of controls in my form and it was simpler in my mind to do it that way. There's lots of ways to do it, this worked best for me. I've also provided a snippet of the code I wrote for that project as another example.
Hope this helps!
'Example
'Assuming recordset and database variables are already declared
'rec = recordset, db = currentdb
set rec = db.openrecordset(<source table, name of existing query, or SQL query>)
if <condition is met> then
'populate table with values in form controls
rec.addnew
rec("Destination Table Field") = Me.Controls("Name of Form Control").Value
.
.
.
rec.update
set rec = nothing
set db = nothing
'clearing rec and db after done using
end if
Code from my project:
Set db = CurrentDb
Set rec = db.OpenRecordset("Select * from tbl_maintenanceOrders")
Set recHist = db.OpenRecordset("Select * from tbl_umoHistory")
msgConfirm = MsgBox("Correct values confirmed?", vbYesNo, "Continue")
'enter record in maintenance order table
If msgConfirm = vbYes Then
rec.AddNew
recHist.AddNew
rec("openTimestamp") = Now()
rec("openedBy") = Me.Controls("cbo_originator").Value
rec("assetID") = Me.Controls("cbo_asset").Value
rec("assetDesc") = Me.Controls("txt_assetDesc").Value
rec("priority") = Me.Controls("cbo_priority").Value
rec("umoProblemDesc") = Me.Controls("txt_issueDesc").Value
rec("umoSpecifics") = Me.Controls("txt_issueDetails").Value
rec("umoState") = "open"
rec("umoStatus") = "new"
rec.Update
'add UMO history entry
recHist("umoID") = rec("orderID")
recHist("activity") = "opened"
recHist("umoState") = "open"
recHist("umoStatus") = "new"
recHist("activityDesc") = "UMO Requested"
recHist("initiatorID") = Me.Controls("cbo_originator").Value
recHist("timeStamp") = Now()
recHist("updater") = Me.Controls("cbo_originator").Value
recHist.Update
End If
'cleanup
Set rec = Nothing
Set recHist = Nothing
Set db = Nothing
can this be done, say I have rows (1,2,3,4,5) and I want to grab three rows, select one randomly and then get it's neighbors, so maybe the random selection is row (3), I can also grab (2,4) if I wanted its neighbors, do I just pick one at random and then look for the unique key before and after like this or can I do it all in one sql statement.
I was going to use ADO from excel to pull records (so VBA connects to access, opens a recordset with sql instructions and so on).
Hope I was clear!
I would love to just do this all in a SQL statement
I am not sure Access is capable of all the SQL commands such as SQL Server, so this may be a bit of a problem. If you have a primary key though, you can easly generate a Select query in VBA and then pass open recordset with this SQL.
Dim sSQL as String
Dim lRand as Long
Dim rs as ADODB.Recordset 'or DAO.Recordset'
lRand = VBA.Int(VBA.Rnd() * TableRecordCount) ' TableRecordCount is the number of records in the table that you need to get somehow'
sSQL = "SELECT * FROM TableName WHERE (ID>=" & lRand - 1 & " AND ID <=" & lRand + 1
set rs = CurrentDB.OpenRecordset(sSQL, ...)
I am now not absolutely sure of what you want to use and depending on ADODB or DAO choice, you need to open the recordset accordingly with wither Call rs.Open or Set rs = DB.OpenRecordset
Seems a common enough problem this, but most solutions refer to concatenating multiple SQL commands, something which I believe can't be done with ADO/VBA (I'll be glad to be shown wrong in this regard however).
I currently insert my new record then run a select query using (I hope) enough fields to guarantee that only the newly inserted record can be returned. My databases are rarely accessed by more than one person at a time (negligible risk of another insert happening between queries) and due to the structure of the tables, identifying the new record is normally pretty easy.
I'm now trying to update a table that does not have much scope for uniqueness, other than in the artificial primary key. This means there is a risk that the new record may not be unique, and I'm loathe to add a field just to force uniqueness.
What's the best way to insert a record into an Access table then query the new primary key from Excel in this situation?
Thanks for the replies. I have tried to get ##IDENTITY working, but this always returns 0 using the code below.
Private Sub getIdentityTest()
Dim myRecordset As New ADODB.Recordset
Dim SQL As String, SQL2 As String
SQL = "INSERT INTO tblTasks (discipline,task,owner,unit,minutes) VALUES (""testDisc3-3"",""testTask"",""testOwner"",""testUnit"",1);"
SQL2 = "SELECT ##identity AS NewID FROM tblTasks;"
If databaseConnection Is Nothing Then
createDBConnection
End If
With databaseConnection
.Open dbConnectionString
.Execute (SQL)
.Close
End With
myRecordset.Open SQL2, dbConnectionString, adOpenStatic, adLockReadOnly
Debug.Print myRecordset.Fields("NewID")
myRecordset.Close
Set myRecordset = Nothing
End Sub
Anything stand out being responsible?
However, given the caveats helpfully supplied by Renaud (below) there seems nearly as much risk with using ##IDENTITY as with any other method, so I've resorted to using SELECT MAX for now. For future reference though I would be interested to see what is wrong with my attempt above.
About your question:
I'm now trying to update a table that
does not have much scope for
uniqueness, other than in the
artificial primary key. This means
there is a risk that the new record
may not be unique, and I'm loathe to
add a field just to force uniqueness.
If you are using an AutoIncrement for your primary key, then you have uniqueness and you could use SELECT ##Identity; to get the value of the last autogenerated ID (see caveats below).
If you are not using autoincrement, and you are inserting the records from Access but you want to retrieve the last one from Excel:
make sure your primary key is sortable, so you can get the last one using a query like either of these:
SELECT MAX(MyPrimaryField) FROM MyTable;
SELECT TOP 1 MyPrimaryField FROM MyTable ORDER BY MyPrimaryField DESC;
or, if sorting your primary field wouldn't give you the last one, you would need to add a DateTime field (say InsertedDate) and save the current date and time every time you create a new record in that table so you could get the last one like this:
SELECT TOP 1 MyPrimaryField FROM MyTable ORDER BY InsertedDate DESC;
In either of these cases, I think you would find adding an AutoIncrement primary key as being a lot easier to deal with:
It's not going to cost you much
It's going to guarantee you uniqueness of your records without having to think about it
It's going to make it easier for you to pick the most recent record, either using ##Identity or through sorting by the primary key or getting the Max().
From Excel
To get the data into Excel, you have a couple of choices:
create a data link using a query, so you can use the result directly in a Cell or a range.
query from VBA:
Sub GetLastPrimaryKey(PrimaryField as string, Table as string) as variant
Dim con As String
Dim rs As ADODB.Recordset
Dim sql As String
con = "Provider=Microsoft.ACE.OLEDB.12.0;" & _
"Data Source= ; C:\myDatabase.accdb"
sql = "SELECT MAX([" & PrimaryField & "]) FROM [" & MyTable & "];"
Set rs = New ADODB.Recordset
rs.Open sql, con, adOpenStatic, adLockReadOnly
GetLastPrimaryKey = rs.Fields(0).Value
rs.Close
Set rs = Nothing
End Sub
Note about ##Identity
You have to be careful of the caveats when using ##Identity in standard Access databases(*):
It only works with AutoIncrement Identity fields.
It's only available if you use ADO and run SELECT ##IDENTITY;
It returns the latest used counter, but that's for all tables. You can't use it to return the counter for a specific table in MS Access (as far as I know, if you specify a table using FROM mytable, it just gets ignored).
In short, the value returned may not be at all the one you expect.
You must query it straight after an INSERT to minimize the risk of getting a wrong answer.
That means that if you are inserting your data at one time and need to get the last ID at another time (or another place), it won't work.
Last but not least, the variable is set only when records are inserted through programming code.
This means that is the record was added through the user interface, ##IDENTITY will not be set.
(*): just to be clear, ##IDENTITY behaves differently, and in a more predictive way, if you use ANSI-92 SQL mode for your database.
The issue though is that ANSI 92 has a slightly different syntax than
the ANSI 89 flavour supported by Access and is meant to increase compatibility with SQL Server when Access is used as a front end.
If the artificial key is an autonumber, you can use ##identity.
Note that with both these examples, the transaction is isolated from other events, so the identity returned is the one just inserted. You can test this by pausing the code at Debug.Print db.RecordsAffected or Debug.Print lngRecs and inserting a record manually into Table1, continue the code and note that the identity returned is not that of the record inserted manually, but of the previous record inserted by code.
DAO Example
'Reference: Microsoft DAO 3.6 Object Library '
Dim db As DAO.Database
Dim rs As DAO.Recordset
Set db = CurrentDb
db.Execute ("INSERT INTO table1 (field1, Crdate ) " _
& "VALUES ( 46, #" & Format(Date, "yyyy/mm/dd") & "#)")
Debug.Print db.RecordsAffected
Set rs = db.OpenRecordset("SELECT ##identity AS NewID FROM table1")
Debug.Print rs.Fields("NewID")
ADO Example
Dim cn As New ADODB.Connection
Dim rs As New ADODB.Recordset
Set cn = CurrentProject.Connection
cn.Execute ("INSERT INTO table1 (field1, Crdate ) " _
& "VALUES ( 46, #" & Format(Date, "yyyy/mm/dd") & "#)"), lngRecs
Debug.Print lngRecs
rs.Open "SELECT ##identity AS NewID FROM table1", cn
Debug.Print rs.Fields("NewID")
Re: "I have tried to get ##IDENTITY working, but this always returns 0 using the code below."
Your code sends SQL and SQL2 through different connection objects. I don't think ##identity will return anything other than zero unless you ask from the same connection where you executed your INSERT statement.
Try changing this:
myRecordset.Open SQL2, dbConnectionString, adOpenStatic, adLockReadOnly
to:
myRecordset.Open SQL2, databaseConnection, adOpenStatic, adLockReadOnly
Here's my solution that does not use ##index or MAX.
Const connectionString = "Provider=SQLOLEDB; Data Source=SomeSource; Initial Catalog=SomeDB; User Id=YouIDHere; Password=YourPassword"
Const RecordsSQL = "SELECT * FROM ThatOneTable"
Private Sub InsertRecordAndGetID()
Set connection = New ADODB.connection
connection.connectionString = connectionString
connection.Open
Set recordset = New ADODB.recordset
recordset.Open SQL, connection, adOpenKeyset, adLockOptimistic
With recordset
.AddNew
!Field1 = Value1
!Field2 = Value2
End With
recordset.MoveLast
ID = recordset.Fields("id")
End Sub
Enjoy!
Try following macro code.First add a command button to the sheet from the control box and paste following codes in the code window
Private Sub CommandButton1_Click()
MsgBox GetLastPrimaryKey
End Sub
Private Function GetLastPrimaryKey() As String
Dim con As String
Dim cn As ADODB.Connection
Dim rs As ADODB.Recordset
Dim sql As String
con = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\myaccess.mdb;Persist Security Info=False"
sql = "SELECT MAX(id) FROM tblMyTable"
Set cn = New ADODB.Connection
Set rs = New ADODB.Recordset
cn.Open con
rs.Open sql, cn, 3, 3, 1
If rs.RecordCount <> 0 Then
GetLastPrimaryKey = rs.Fields(0).Value
End If
rs.Close
cn.Close
Set rs = Nothing
Set cn = Nothing
End Function
8 years late to the party... The problem you are having is that you are using dbConnectionString to create a new connection. ##identity is specific to the connection you are using.
First, don't close the original connection
'.Close
replace
myRecordset.Open SQL2, dbConnectionString, adOpenStatic, adLockReadOnly
with the connection you previously used for the insert
myRecordset.Open SQL2, databaseConnection, adOpenStatic, adLockReadOnly
and you'd have been all set. In fact, you don't even need to specify the table:
SQL2 = "SELECT ##identity AS NewID"