Create row number field within query access 2010 [duplicate] - vba

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.

Related

Running Dlookup() on a query with row count column as criteria (MS access)

Im trying to run Dlookup to return all values of a column in a query's results. As Dlookup can only return 1 result I have added a row count column to the query so I can use the row count as the criteria for the Dlookup.
My query results look like this in the results
Query design view
Query results 2
In SQL view the query is as follows:
SELECT tblAssets.SerialNumber, tblAssets.ID, tblAssets.AssetType, tblPlacing.Location, tblPlacing.PlacingStartDate, tblPlacing.PlacingEndDate, RowNumber([ID]) AS [NO]
FROM tblAssets INNER JOIN (tblLocations RIGHT JOIN tblPlacing ON tblLocations.LocationID = tblPlacing.Location) ON tblAssets.ID = tblPlacing.AssetID
GROUP BY tblAssets.SerialNumber, tblAssets.ID, tblAssets.AssetType, tblPlacing.Location, tblPlacing.PlacingStartDate, tblPlacing.PlacingEndDate, RowNumber([ID])
HAVING (((tblAssets.AssetType)=55) AND ((tblPlacing.Location)=[Forms]![FrmPPM]![TXTLocationID]) AND ((tblPlacing.PlacingEndDate) Is Null) AND ((ResetRowNumber())<>False));
I have added the row count with the following video (https://www.youtube.com/watch?v=HWbpzETe-M0), let me know if more information about this is needed.
The query results look to be correct, however, when using the Dlookup (beneath) it returns null.
Dim VAR1 as Variant
VAR1 = DLookup("[SerialNumber]", "QRYPPM", "[NO] = 1")
When I try and return the [NO] column value it works.
VAR1 = DLookup("[NO]", "QRYPPM", "[SerialNumber] = '6501038'")
I don't have your data, so can't run a full test, but I ran something similar using my RowNumber function (below), and DLookup was able to filter on the returned rownumber (your [NO]) and return a value from another field.
So, try to use this function (your SQL will need minor adjustments, see in-line comments, please):
' Builds consecutive row numbers in a select, append, or create query
' with the option of a initial automatic reset.
' Optionally, a grouping key can be passed to reset the row count
' for every group key.
'
' Usage (typical select query having an ID with an index):
' SELECT RowNumber(CStr([ID])) AS RowID, *
' FROM SomeTable
' WHERE (RowNumber(CStr([ID])) <> RowNumber("","",True));
'
' Usage (typical select query having an ID without an index):
' SELECT RowNumber(CStr([ID])) AS RowID, *
' FROM SomeTable
' WHERE (RowNumber("","",True)=0);
'
' Usage (with group key):
' SELECT RowNumber(CStr([ID]), CStr[GroupID])) AS RowID, *
' FROM SomeTable
' WHERE (RowNumber(CStr([ID])) <> RowNumber("","",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 RowNumber(vbNullString, True)
' 2. Run query:
' INSERT INTO TempTable ( [RowID] )
' SELECT RowNumber(CStr([ID])) AS RowID, *
' FROM SomeTable;
'
' Usage (typical append query, automatic reset):
' INSERT INTO TempTable ( [RowID] )
' SELECT RowNumber(CStr([ID])) AS RowID, *
' FROM SomeTable
' WHERE (RowNumber("","",True)=0);
'
' 2020-05-29. Gustav Brock, Cactus Data ApS, CPH.
'
Public Function RowNumber( _
ByVal Key As String, _
Optional ByVal GroupKey As String, _
Optional ByVal Reset As Boolean) _
As Long
' Uncommon character string to assemble GroupKey and Key as a compound key.
Const KeySeparator As String = "¤§¤"
' Expected error codes to accept.
Const CannotAddKey As Long = 457
Const CannotRemoveKey As Long = 5
Static Keys As New Collection
Static GroupKeys As New Collection
Dim Count As Long
Dim CompoundKey As String
On Error GoTo Err_RowNumber
If Reset = True Then
' Erase the collection of keys and group key counts.
Set Keys = Nothing
Set GroupKeys = Nothing
Else
' Create a compound key to uniquely identify GroupKey and its Key.
' Note: If GroupKey is not used, only one element will be added.
CompoundKey = GroupKey & KeySeparator & Key
Count = Keys(CompoundKey)
If Count = 0 Then
' This record has not been enumerated.
'
' Will either fail if the group key is new, leaving Count as zero,
' or retrieve the count of already enumerated records with this group key.
Count = GroupKeys(GroupKey) + 1
If Count > 0 Then
' The group key has been recorded.
' Remove it to allow it to be recreated holding the new count.
GroupKeys.Remove (GroupKey)
Else
' This record is the first having this group key.
' Thus, the count is 1.
Count = 1
End If
' (Re)create the group key item with the value of the count of keys.
GroupKeys.Add Count, GroupKey
End If
' Add the key and its enumeration.
' This will be:
' Using no group key: Relative to the full recordset.
' Using a group key: Relative to the group key.
' Will fail if the key already has been created.
Keys.Add Count, CompoundKey
End If
' Return the key value as this is the row counter.
RowNumber = Count
Exit_RowNumber:
Exit Function
Err_RowNumber:
Select Case Err
Case CannotAddKey
' Key is present, thus cannot be added again.
Resume Next
Case CannotRemoveKey
' GroupKey is not present, thus cannot be removed.
Resume Next
Case Else
' Some other error. Ignore.
Resume Exit_RowNumber
End Select
End Function
The full story can be found in my project VBA.RowNumbers.

Finding leaf nodes with a specific ancestor [duplicate]

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

How do I reset numbering in tables?

I have some data that I have been working with. With that, I have been deleting records on-the-fly. Now that I have a working data model, I want to re-start my auto-numbering. How do I reset my existing data to adhere to an auto-formatting starting from 1....n?
I have tried to do a Compact and Repair but that didn't seem to fix the issue.
For reference, my table looks like this:
R-002
R-054
R-123
R-057
R-061
I would like to get them back to:
R-001
R-002
R-003
R-004
R-005
And for each subsequent entry added, adhere to this new auto-numbering so the next record, for example, will be R-006.
Writing an UPDATE query works for immediate fixing but it doesn't address the adherence to the auto-numbering for future entries.
I would think there is an elegant way to do this. The data in my tables are part of Relationships so it makes it a little tricky.
If anyone has any tips / recommendations, I would appreciate it.
What you need is sequential numbering of the records.
I wrote an article on the various methods for doing this:
Sequential Rows in Microsoft Access
One is to add row numbers:
' Builds consecutive row numbers in a select, append, or create query
' with the option of a initial automatic reset.
' Optionally, a grouping key can be passed to reset the row count
' for every group key.
'
' 2018-08-23. Gustav Brock, Cactus Data ApS, CPH.
'
Public Function RowNumber( _
ByVal Key As String, _
Optional ByVal GroupKey As String, _
Optional ByVal Reset As Boolean) _
As Long
' Uncommon character string to assemble GroupKey and Key as a compound key.
Const KeySeparator As String = "¤§¤"
' Expected error codes to accept.
Const CannotAddKey As Long = 457
Const CannotRemoveKey As Long = 5
Static Keys As New Collection
Static GroupKeys As New Collection
Dim Count As Long
Dim CompoundKey As String
On Error GoTo Err_RowNumber
If Reset = True Then
' Erase the collection of keys and group key counts.
Set Keys = Nothing
Set GroupKeys = Nothing
Else
' Create a compound key to uniquely identify GroupKey and its Key.
' Note: If GroupKey is not used, only one element will be added.
CompoundKey = GroupKey & KeySeparator & Key
Count = Keys(CompoundKey)
If Count = 0 Then
' This record has not been enumerated.
'
' Will either fail if the group key is new, leaving Count as zero,
' or retrieve the count of already enumerated records with this group key.
Count = GroupKeys(GroupKey) + 1
If Count > 0 Then
' The group key has been recorded.
' Remove it to allow it to be recreated holding the new count.
GroupKeys.Remove (GroupKey)
Else
' This record is the first having this group key.
' Thus, the count is 1.
Count = 1
End If
' (Re)create the group key item with the value of the count of keys.
GroupKeys.Add Count, GroupKey
End If
' Add the key and its enumeration.
' This will be:
' Using no group key: Relative to the full recordset.
' Using a group key: Relative to the group key.
' Will fail if the key already has been created.
Keys.Add Count, CompoundKey
End If
' Return the key value as this is the row counter.
RowNumber = Count
Exit_RowNumber:
Exit Function
Err_RowNumber:
Select Case Err
Case CannotAddKey
' Key is present, thus cannot be added again.
Resume Next
Case CannotRemoveKey
' GroupKey is not present, thus cannot be removed.
Resume Next
Case Else
' Some other error. Ignore.
Resume Exit_RowNumber
End Select
End Function
These you could format as you like, for example:
RNumber: "R-" & Format([RwoNumber], "000")

MS Access SQL pagination with a query (no unique identifier) or ROW_NUMBER?

I am trying to grab 100 rows at a time from a SQL query and use them for pagination. The query returns ~2000 rows from a table with 100,000+ rows. However, the table does not have a unique identifier.
SELECT TOP 100 * FROM tbl_Items WHERE tbl_Items.Repair_Required = true
I have looked into using ROW_NUMBER but it seems like that's not available in MS Access. I have also looked into creating a custom row_number using "self-join" as answered by Gord Thompson here: Access query producing results like ROW_NUMBER() in T-SQL. However, joining my 100,000+ table with itself would be not be easy on performance.
What are my options?
Here is a method using a collection and it works really fast:
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));
'
' 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.
Static col As New Collection
Static strGroup As String
On Error GoTo Err_RowCounter
If booReset = True Or strGroup <> strGroupKey Then
Set col = Nothing
strGroup = strGroupKey
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
Please study the in-line comments and examples.
Of course, you would apply it to the query with the 2000 records, not the source table(s).

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.