Finding leaf nodes with a specific ancestor [duplicate] - sql

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]);

Related

Most efficient way to query 1 of 4 large Access tables based on condition (using VBA module)

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

Create row number field within query access 2010 [duplicate]

I have an employee table with has name, age, city as columns. I want to display a column at run-time for my row numbers starting from 1. I am using SQL in Access.
Call the following function from your query.
Public Function GetNextNum(str As String) As Long
num = num + 1
GetNextNum = num
End Function
The caveat is that you must have at least one parameter (even if you don't need one) otherwise the function only gets called once and returns 1 for all the rows.
Before running the query set the global variable num to 0.
You only need one function to obtain a very speedy and even "groupable" row counter with or without automatic reset of the counter.
See in-line comments for typical usage:
Public Function RowCounter( _
ByVal strKey As String, _
ByVal booReset As Boolean, _
Optional ByVal strGroupKey As String) _
As Long
' Builds consecutive RowIDs in select, append or create query
' with the possibility of automatic reset.
' Optionally a grouping key can be passed to reset the row count
' for every group key.
'
' Usage (typical select query):
' SELECT RowCounter(CStr([ID]),False) AS RowID, *
' FROM tblSomeTable
' WHERE (RowCounter(CStr([ID]),False) <> RowCounter("",True));
'
' Usage (with group key):
' SELECT RowCounter(CStr([ID]),False,CStr[GroupID])) AS RowID, *
' FROM tblSomeTable
' WHERE (RowCounter(CStr([ID]),False) <> RowCounter("",True));
'
' The Where statement resets the counter when the query is run
' and is needed for browsing a select query.
'
' Usage (typical append query, manual reset):
' 1. Reset counter manually:
' Call RowCounter(vbNullString, False)
' 2. Run query:
' INSERT INTO tblTemp ( RowID )
' SELECT RowCounter(CStr([ID]),False) AS RowID, *
' FROM tblSomeTable;
'
' Usage (typical append query, automatic reset):
' INSERT INTO tblTemp ( RowID )
' SELECT RowCounter(CStr([ID]),False) AS RowID, *
' FROM tblSomeTable
' WHERE (RowCounter("",True)=0);
'
' 2002-04-13. Cactus Data ApS. CPH
' 2002-09-09. Str() sometimes fails. Replaced with CStr().
' 2005-10-21. Str(col.Count + 1) reduced to col.Count + 1.
' 2008-02-27. Optional group parameter added.
' 2010-08-04. Corrected that group key missed first row in group.
Static col As New Collection
Static strGroup As String
On Error GoTo Err_RowCounter
If booReset = True Then
Set col = Nothing
ElseIf strGroup <> strGroupKey Then
Set col = Nothing
strGroup = strGroupKey
col.Add 1, strKey
Else
col.Add col.Count + 1, strKey
End If
RowCounter = col(strKey)
Exit_RowCounter:
Exit Function
Err_RowCounter:
Select Case Err
Case 457
' Key is present.
Resume Next
Case Else
' Some other error.
Resume Exit_RowCounter
End Select
End Function
You have 5 methods available.
Reports only - Running Sum
If you are using this information for Access reports, there is an easy way that requires no VBA or fancy SQL. Simply add a textbox with control source set =1 then set Running Sum to Over All, done.
The rest of methods listed below applies to forms/datasheets/recordsets
Correlated subquery
You can do a correlated subquery. This solution is totally self-contained but is not very generic. It would be something similar to this:
SELECT
(
SELECT COUNT(*)
FROM Employees AS x
WHERE x.EmployeeID <= e.EmployeeID
ORDER BY x.EmployeeID
) AS RowNumber,
e.EmployeeID
FROM Employees AS e;
Note that because of the correlated subqueries, the performance will rapidly decrease as the amount of records increase in the table. You might have to customize the ORDER BY clause to get the desired number assignment if it's not supposed to depend on EmployeeID but something else (e.g. HireDate for instance)
VBA Function to maintain count, forward-only recordset
This method can perform much faster but can be only used once; and certainly not within forms/datasheets because VBA functions are continually evaluated as you navigate around. Thus, this is only appropriate when reading recordset in a forward-only manner. Using a standard VBA module:
Private Counter As Long
Public Function ResetRowNumber() As Boolean
Counter = 0
ResetRowNumber = (Counter = 0)
End Function
Public Function GetRowNumber(PrimaryKeyField As Variant) As Long
Counter = Counter + 1
GetRowNumber = Counter
End Function
To then use in a query:
SELECT
GetRowNumber([EmployeeID]) AS RowNumber,
EmployeeID
FROM Employees
WHERE ResetRowNumber();
Note the trick of using WHERE to implicitly call the ResetRowNumber function first. Please note this will work only as long there is only one query active; having multiple queries that takes row numbers will cause incorrect results. However the implementation is very simple and much faster.
VBA Function to maintain count and preserve the assignment
This is more expensive than the previous method but still can be cheaper than the correlated subquery solution for a sufficiently large table. This has the advantage of being useful in a form / datasheet because once number are given out, it is given out again. Again, in a standard VBA module:
Private NumberCollection As VBA.Collection
Public Function ResetRowNumber() As Boolean
NumberCollection = New VBA.Collection
ResetRowNumber = (NumberCollection.Count = 0)
End Function
Public Function GetRowNumber(PrimaryKeyField As Variant) As Variant
On Error Resume Next
Dim Result As Long
Result = NumberCollection(CStr(PrimaryKeyField))
If Err.Number Then
Result = 0
Err.Clear
End If
If Result Then
GetRowNumber = Result
Else
NumberCollection.Add NumberCollection.Count + 1, CStr(PrimaryKeyField)
GetRowNumber = NumberCollection.Count
End If
If Err.Number Then
GetRowNumber = "#Error " & Err.Description
End If
End Function
It's important that the input parameter PrimaryKeyValue references a non-nullable column (which a primary key column should be by definition). Otherwise, we'd have no way of knowing which number we should give out if it's already been given out to the record. The SQL is similar as previous method:
SELECT
GetRowNumber([EmployeeID]) AS RowNumber,
EmployeeID
FROM Employees
WHERE ResetRowNumber();
As with previous method, this is only good for one query at a time. If you need multiple queries, then you need twice the layer; a collection to reference which query's collection, then to inspect that query's collection. That might get a bit hairy. You might be also able to get more bang with a Scripting.Dictionary, so that's an alternative looking into.
Note also that the function now returns Variant due to the fact that it may encounter unexpected errors. Because the function can get called several times, potentially hundreds or even thousands of time, we can't pop open a message box, so we can mimic what built-in functions do and return a #Error, which is incompatible with the underlying type of Long we're really using.
Upgrade to SQL Server or other RDBMS
Access is a phenomenal RAD tool for building data-centric application. However, you are not necessarily tied to using its database engine. You could just migrate your data to one of free RDBMS, link using ODBC and continue to use your Access application as before, and get to benefit the power of SQL, including the window function ROW_NUMBER() that makes this much easier to achieve than VBA. If you are looking at doing more than just getting a row number, you might need to consider if you should migrate your data to a different database engine.
For additional references, this may be helpful.

Get data from Access given only the primary key of the Table

So I have a question regarding data referencing in Access using VBA and SQL. I have a table in my dataset with like 50 columns, and I need to make a query that a user can run that will manipulate the data in like 30 of these columns with a not-so straightforward algorithm. So the query will prompt the user for the primary key and then run a vba function for getting the value that the user wants, I was just wondering if it was possible that It could be done like this
SELECT Hardware_Type,
Func32(Hardware_Type)
FROM Table1
WHERE (([Hardware_Type]) = [Hardware_Type]);
where Hardware_type is the primary key and Func32 is a Visual Basic Function.
So now Func32 only takes Hardware_type as an Input but needs to use 30 pieces of Data that are in the Row of that specific Hardware_Type. I just need to know that does there exist a way to do this and if there does, I would request a hint, because I really don't want to type in 30 different fields in the query and the function. Oh, and all of this is in Microsoft Access!
Thanks in Advance!
You can avoid passing a whole bunch of parameters to your VBA function but it will cost you one database hit for each time the function is called (i.e., once per row in the query that calls the function). Your function could do something like:
Option Compare Database
Option Explicit
Public Function Func32(HardwareType As String) As Variant
Dim cdb As DAO.Database, qdf As DAO.QueryDef, rst As DAO.Recordset
Dim sql As String
Set cdb = CurrentDb
sql = ""
sql = sql & "PARAMETERS prmHardwareType TEXT(255);"
sql = sql & "SELECT * FROM Table1 WHERE Hardware_Type=[prmHardwareType]"
Set qdf = cdb.CreateQueryDef("", sql)
' assign the PK argument as the query parameter and open it as a Recordset
qdf!prmHardwareType = HardwareType
Set rst = qdf.OpenRecordset(dbOpenSnapshot)
' do your calculations and assign the return value to the function name
Func32 = rst!Field1 + rst!Field2 + rst!Field3
rst.Close
Set rst = Nothing
Set qdf = Nothing
Set cdb = Nothing
End Function

Displaying Row numbers column at runtime

I have an employee table with has name, age, city as columns. I want to display a column at run-time for my row numbers starting from 1. I am using SQL in Access.
Call the following function from your query.
Public Function GetNextNum(str As String) As Long
num = num + 1
GetNextNum = num
End Function
The caveat is that you must have at least one parameter (even if you don't need one) otherwise the function only gets called once and returns 1 for all the rows.
Before running the query set the global variable num to 0.
You only need one function to obtain a very speedy and even "groupable" row counter with or without automatic reset of the counter.
See in-line comments for typical usage:
Public Function RowCounter( _
ByVal strKey As String, _
ByVal booReset As Boolean, _
Optional ByVal strGroupKey As String) _
As Long
' Builds consecutive RowIDs in select, append or create query
' with the possibility of automatic reset.
' Optionally a grouping key can be passed to reset the row count
' for every group key.
'
' Usage (typical select query):
' SELECT RowCounter(CStr([ID]),False) AS RowID, *
' FROM tblSomeTable
' WHERE (RowCounter(CStr([ID]),False) <> RowCounter("",True));
'
' Usage (with group key):
' SELECT RowCounter(CStr([ID]),False,CStr[GroupID])) AS RowID, *
' FROM tblSomeTable
' WHERE (RowCounter(CStr([ID]),False) <> RowCounter("",True));
'
' The Where statement resets the counter when the query is run
' and is needed for browsing a select query.
'
' Usage (typical append query, manual reset):
' 1. Reset counter manually:
' Call RowCounter(vbNullString, False)
' 2. Run query:
' INSERT INTO tblTemp ( RowID )
' SELECT RowCounter(CStr([ID]),False) AS RowID, *
' FROM tblSomeTable;
'
' Usage (typical append query, automatic reset):
' INSERT INTO tblTemp ( RowID )
' SELECT RowCounter(CStr([ID]),False) AS RowID, *
' FROM tblSomeTable
' WHERE (RowCounter("",True)=0);
'
' 2002-04-13. Cactus Data ApS. CPH
' 2002-09-09. Str() sometimes fails. Replaced with CStr().
' 2005-10-21. Str(col.Count + 1) reduced to col.Count + 1.
' 2008-02-27. Optional group parameter added.
' 2010-08-04. Corrected that group key missed first row in group.
Static col As New Collection
Static strGroup As String
On Error GoTo Err_RowCounter
If booReset = True Then
Set col = Nothing
ElseIf strGroup <> strGroupKey Then
Set col = Nothing
strGroup = strGroupKey
col.Add 1, strKey
Else
col.Add col.Count + 1, strKey
End If
RowCounter = col(strKey)
Exit_RowCounter:
Exit Function
Err_RowCounter:
Select Case Err
Case 457
' Key is present.
Resume Next
Case Else
' Some other error.
Resume Exit_RowCounter
End Select
End Function
You have 5 methods available.
Reports only - Running Sum
If you are using this information for Access reports, there is an easy way that requires no VBA or fancy SQL. Simply add a textbox with control source set =1 then set Running Sum to Over All, done.
The rest of methods listed below applies to forms/datasheets/recordsets
Correlated subquery
You can do a correlated subquery. This solution is totally self-contained but is not very generic. It would be something similar to this:
SELECT
(
SELECT COUNT(*)
FROM Employees AS x
WHERE x.EmployeeID <= e.EmployeeID
ORDER BY x.EmployeeID
) AS RowNumber,
e.EmployeeID
FROM Employees AS e;
Note that because of the correlated subqueries, the performance will rapidly decrease as the amount of records increase in the table. You might have to customize the ORDER BY clause to get the desired number assignment if it's not supposed to depend on EmployeeID but something else (e.g. HireDate for instance)
VBA Function to maintain count, forward-only recordset
This method can perform much faster but can be only used once; and certainly not within forms/datasheets because VBA functions are continually evaluated as you navigate around. Thus, this is only appropriate when reading recordset in a forward-only manner. Using a standard VBA module:
Private Counter As Long
Public Function ResetRowNumber() As Boolean
Counter = 0
ResetRowNumber = (Counter = 0)
End Function
Public Function GetRowNumber(PrimaryKeyField As Variant) As Long
Counter = Counter + 1
GetRowNumber = Counter
End Function
To then use in a query:
SELECT
GetRowNumber([EmployeeID]) AS RowNumber,
EmployeeID
FROM Employees
WHERE ResetRowNumber();
Note the trick of using WHERE to implicitly call the ResetRowNumber function first. Please note this will work only as long there is only one query active; having multiple queries that takes row numbers will cause incorrect results. However the implementation is very simple and much faster.
VBA Function to maintain count and preserve the assignment
This is more expensive than the previous method but still can be cheaper than the correlated subquery solution for a sufficiently large table. This has the advantage of being useful in a form / datasheet because once number are given out, it is given out again. Again, in a standard VBA module:
Private NumberCollection As VBA.Collection
Public Function ResetRowNumber() As Boolean
NumberCollection = New VBA.Collection
ResetRowNumber = (NumberCollection.Count = 0)
End Function
Public Function GetRowNumber(PrimaryKeyField As Variant) As Variant
On Error Resume Next
Dim Result As Long
Result = NumberCollection(CStr(PrimaryKeyField))
If Err.Number Then
Result = 0
Err.Clear
End If
If Result Then
GetRowNumber = Result
Else
NumberCollection.Add NumberCollection.Count + 1, CStr(PrimaryKeyField)
GetRowNumber = NumberCollection.Count
End If
If Err.Number Then
GetRowNumber = "#Error " & Err.Description
End If
End Function
It's important that the input parameter PrimaryKeyValue references a non-nullable column (which a primary key column should be by definition). Otherwise, we'd have no way of knowing which number we should give out if it's already been given out to the record. The SQL is similar as previous method:
SELECT
GetRowNumber([EmployeeID]) AS RowNumber,
EmployeeID
FROM Employees
WHERE ResetRowNumber();
As with previous method, this is only good for one query at a time. If you need multiple queries, then you need twice the layer; a collection to reference which query's collection, then to inspect that query's collection. That might get a bit hairy. You might be also able to get more bang with a Scripting.Dictionary, so that's an alternative looking into.
Note also that the function now returns Variant due to the fact that it may encounter unexpected errors. Because the function can get called several times, potentially hundreds or even thousands of time, we can't pop open a message box, so we can mimic what built-in functions do and return a #Error, which is incompatible with the underlying type of Long we're really using.
Upgrade to SQL Server or other RDBMS
Access is a phenomenal RAD tool for building data-centric application. However, you are not necessarily tied to using its database engine. You could just migrate your data to one of free RDBMS, link using ODBC and continue to use your Access application as before, and get to benefit the power of SQL, including the window function ROW_NUMBER() that makes this much easier to achieve than VBA. If you are looking at doing more than just getting a row number, you might need to consider if you should migrate your data to a different database engine.
For additional references, this may be helpful.

Find Previous and next value in access using SQL

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;