MS Access: Left outer self join with inequality operator - ms-access

I have a single table in Access 2010:
TMP [CUST,ITEM,START_PD]
I want to get the END_PD for each CUST / ITEM.
END_PD being defined as the period before the next higher START_PD for the same CUST / ITEM.
So I perform a left join of the table to itself, using an inequality operator on START_PD as below.
SELECT s.CUST, s.ITEM, s.START_PD, Min(e.START_PD-1) AS END_PD
FROM TMP AS s
LEFT JOIN TMP AS e ON s.CUST=e.CUST AND s.ITEM=e.ITEM AND e.START_PD>s.START_PD
GROUP BY s.CUST, s.ITEM, s.START_PD
ORDER BY s.CUST, s.ITEM, s.START_PD
The base table has 46,556 rows. I would expect the query result to have the same, but the query only returns 14,967 rows.
Even when I try to return all records in the left join, I get far less than the base table. See below:
SELECT s.*,e.*
FROM TMP AS s
LEFT JOIN TMP AS e ON s.ITEM = e.ITEM AND s.CUST = e.CUST AND e.START_PD>s.START_PD
The above query only returns 19,014 records... less than the base table.
This is holding up a major project, and I'd appreciate any help. So far, it's looking like an Access bug. Any workarounds?
EDIT:
I have tried testing a small sample of the data by including WHERE s.CUST='WALMART' AND s.ITEM='0001H'. This fails by excluding the last START_PD, for which there is none greater.
CUST ITEM START_PD END_PD
WALMART 0001HAC 20694 20696
WALMART 0001HAC 20697 20704
WALMART 0001HAC 20705 20706
Strangely, if I select that same sample of the data (WALMART/0001H) into a separate table and run the EXACT SAME query on that smaller table (changing only the table name), it works fine as below. That is why I tend to think this is a bug.
CUST ITEM START_PD END_PD
WALMART 0001HAC 20694 20696
WALMART 0001HAC 20697 20704
WALMART 0001HAC 20705 20706
WALMART 0001HAC 20707

To answer my own question, I'd like to:
Demonstrate that this is a bug
Provide a workaround
Proof that this is a bug.
Sub Test()
Dim db As DAO.Database, rst As DAO.Recordset
Set db = CurrentDb
On Error Resume Next
db.QueryDefs.Delete "TMP_EXTENDED"
db.QueryDefs.Delete "RAW_LEFT_JOIN"
db.TableDefs.Delete "TMP"
On Error GoTo 0
'Table definition.
strSql = _
"CREATE TABLE TMP ( " & vbCrLf & _
" CUST VARCHAR(10), " & vbCrLf & _
" ITEM VARCHAR(10), " & vbCrLf & _
" START_PD LONG, " & vbCrLf & _
" PRIMARY KEY (CUST,ITEM,START_PD) " & vbCrLf & _
");"
db.Execute strSql, dbFailOnError
'Populate with data.
Set rst = db.OpenRecordset("TMP")
For custNo = 1 To 25 'change to affect final row count
For itemNo = 1 To 100 'change to affect final row count
For pdNo = 1 To 3 'change to affect final row count
strCust = "CUST" & custNo
strItem = "ITEM" & itemNo
rst.AddNew
rst("CUST") = strCust
rst("ITEM") = strItem
rst("START_PD") = pdNo
rst.Update
If rst.RecordCount Mod 1000 = 0 Then Debug.Print rst.RecordCount 'just to monitor.
Next
Next
Next
Debug.Print "TMP Table Row Count is: " & DCount("*", "TMP")
'Test query to find end period for each CUST/ITEM/START_PD.
Dim qdf As New QueryDef
qdf.Name = "TMP_EXTENDED"
qdf.SQL = "SELECT s.CUST, s.ITEM, s.START_PD, Min(e.START_PD-1) AS END_PD " & vbCrLf & _
"FROM TMP AS s " & vbCrLf & _
"LEFT JOIN TMP AS e ON (s.CUST=e.CUST AND s.ITEM=e.ITEM AND e.START_PD>s.START_PD) " & vbCrLf & _
"GROUP BY s.CUST, s.ITEM, s.START_PD " & vbCrLf & _
"ORDER BY s.CUST, s.ITEM, s.START_PD"
db.QueryDefs.Append qdf
Debug.Print "TMP_EXTENDED Row Count is: " & DCount("*", "TMP_EXTENDED")
'Test query to just perform the left join.
Set qdf = New QueryDef
qdf.Name = "RAW_LEFT_JOIN"
qdf.SQL = "SELECT s.*,e.* " & vbCrLf & _
"FROM TMP AS s " & vbCrLf & _
"LEFT JOIN TMP AS e ON s.ITEM = e.ITEM AND s.CUST = e.CUST AND e.START_PD>s.START_PD"
db.QueryDefs.Append qdf
Debug.Print "RAW_LEFT_JOIN Row Count is: " & DCount("*", "RAW_LEFT_JOIN")
RefreshDatabaseWindow
End Sub
When run as written, the above code returns:
TMP Table Row Count is: 7500
TMP_EXTENDED Row Count is: 5000
RAW_LEFT_JOIN Row Count is: 7500
Obviously this is wrong because a left join should always return all the records in the left table. In this case, TMP_EXTENDED should have returned 7500, and RAW_LEFT_JOIN should have returned more than 7500.
The loop bounds for custNo, itemNo, and pdNo can be changed to adjust the record count in table TMP. If you do this, you will see that the query works until the record count reaches about 7000, then it fails.
The same problem does not appear to exist when the join is only performed on one 'entity' column. For example, I modified the above code to use a table with only a CUST column and START_PD column, and got both queries to work properly on a table record count of 600,000.
Workaround
Since posting this, I have found another quite similar post in which a decent workaround is presented. I have modified it as below. I do not know HOW reliable it is going to be, but I'm going with it for now.
SELECT s.CUST, s.ITEM, s.START_PD, Min(e.START_PD) AS END_PD
FROM TMP AS s INNER JOIN TMP AS e ON s.ITEM = e.ITEM AND s.CUST = e.CUST
WHERE e.START_PD>s.START_PD
GROUP BY s.CUST, s.ITEM, s.START_PD
UNION ALL
SELECT CUST, ITEM, Max(START_PD), Null
FROM TMP
GROUP BY CUST, ITEM, Null
ORDER BY CUST,ITEM,START_PD,END_PD

Any "atypical" (not only table1.field = table2.field, but anything with constants, calculations, or comparison operators other than equality) join in Access should have it's ON clause surrounded by braces:
SELECT s.*,e.*
FROM TMP AS s
LEFT JOIN TMP AS e ON (s.ITEM = e.ITEM AND s.CUST = e.CUST AND e.START_PD>s.START_PD)
However, usually, these kinds of joins return a Join expression not supported error instead of giving incorrect results. I don't know why this one doesn't.

Related

How can I setup 2 insert queries based on a openrecordset field ID (Invoicing using QODBC & MS ACCESS)

I am using vba in MS Access to create Invoices in Quickbooks with QODBC.
This process requires multi line invoice items to be inserted first and saved temp till primary invoice information is inserted. I have several invoices that are in need of being inserted as bulk.
EXAMPLE:
MultiLIne (INVOICE ITEMS) = Item #, OrderID, Item Desc, etc.
**MULTILINE matches PRIMARY invoice based on OrderID
Primary (INVOICE) = OrderID, Name, Address, Billing terms, etc.
**Primary is a single line record per orderID
"QB_AppendInvoice_LoopRef" contains the unique orderid's that need to be processed. I was trying to use this as a recordset to import the multiline items based on the current recordset orderid, however, I am unable to reference the current recordset orderid.
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim iCount As Integer
Set db = CurrentDb()
Set rs = db.OpenRecordset("QB_AppendInvoice_LoopRef") 'open the recordset for use (table, Query, SQL Statement)
With rs
If .RecordCount <> 0 Then 'Ensure that there are actually records to work with
'The next 2 line will determine the number of returned records
rs.MoveLast 'This is required otherwise you may not get the right count
iCount = rs.RecordCount 'Determine the number of returned records
Do While Not .BOF
DoCmd.SetWarnings False
'Append Invoice Line (determine tests ordered)
Dim SQL1 As String
SQL1 = "INSERT INTO InvoiceLine (CustomerRefListID, CustomerRefFullName, ARAccountRefListID, ARAccountRefFullName, InvoiceLineSerialNumber, InvoiceLineLotNumber, TemplateRefListID, IsPending, DueDate, TxnDate, InvoiceLineType, InvoiceLineItemRefListID, InvoiceLineItemRefFullName, InvoiceLineDesc, InvoiceLineRate, InvoiceLineAmount, FQSaveToCache, RefNumber)" & _
"SELECT Customer.ListID, Customer.FullName, '4C0000-1070045186', 'Accounts Receivable', Null, Null, '80000023-1495649075', '0', QB_ORDER_DETAILS.OrderDate, QB_ORDER_DETAILS.OrderDate, 'Item', QB_TestList_TestCodes.ListID, QB_TestList_TestCodes.FullName, QB_TestList_TestCodes.Description, QB_TestList_TestCodes.SalesOrPurchasePrice, QB_TestList_TestCodes.SalesOrPurchasePrice, '1', QB_ORDER_DETAILS.OrderID " & _
"FROM ((Customer INNER JOIN contacts ON Customer.AccountNumber = contacts.Company) INNER JOIN QB_ORDER_DETAILS ON contacts.[Full Member Info] = QB_ORDER_DETAILS.Physician) LEFT JOIN QB_TestList_TestCodes ON QB_ORDER_DETAILS.ProductID = QB_TestList_TestCodes.TestCode " & _
"WHERE QB_ORDER_DETAILS.OrderID = rs.Fields.getvalue('OrderID')"
DoCmd.RunSQL SQL1, False
'Append Invoice to Invoice Line (put the tests ordered on an invoice)
Dim SQL2 As String
SQL2 = "INSERT INTO Invoice (CustomerRefListID, CustomerRefFullName, ARAccountRefListID, ARAccountRefFullName, TemplateRefListID, [Memo], IsPending, IsToBePrinted, CustomFieldOther, ItemSalesTaxRefListID, TxnDate, DueDate, RefNumber)" & _
"SELECT Customer.ListID, Customer.FullName, '4C0000-1070045186', 'Accounts Receivable', '80000023-1495649075', [Patient_Last] & ', ' & [Patient_First] & ' - ' & [Full_Specimen_ID], '0', '0', [Patient_Last] & ', ' & [Patient_First] & ' - ' & [Full_Specimen_ID], Null, [OrderDate], [OrderDate], Orders.OrderID" & _
"FROM Customer INNER JOIN (Orders INNER JOIN contacts ON Orders.Physician = contacts.[Full Member Info]) ON Customer.AccountNumber = contacts.Company" & _
"WHERE Orders.OrderID = rs.Fields.getvalue('OrderID')"
DoCmd.RunSQL SQL2, False
.MovePrevious
Loop
Else
MsgBox "There are no records in the recordset."
End If
MsgBox "SENT TO QB - SUCCESS!!!"
End With
rs.Close 'Close the recordset
Set rs = Nothing 'Clean up
DoCmd.SetWarnings True
End Sub
It's because you are updating a string, VBA can't use variables in string like that, here is the correct way of doiing it:
"WHERE Orders.OrderID = " & rs.Fields("OrderID").Value
And if the value is a string, you have to add quotes:
"WHERE Orders.OrderID = '" & rs.Fields("OrderID").Value & "'"

DAO.Recordset.RecordCount property not working as expected

I am running a query from input from a form. It is to extract records from 1 table, where the table is joined to 3 others.
It does successfully extract the record. In this case there is only 1 record and the query does find it.
The problem is that every time I run this code, I get one more RecordCount.
So the first time I run it, the RecordCount is 1 and debug.print gives me the correct information. The second time I run it, the RecordCount is 2 and debug.print gives me the correct information but twice. The third time ... 3 and 3, etc.
It doesn't matter if I close the form (from which I get my variables) and reload it. The number keeps climbing. I closed Access and reopened it and the number keeps climbing - it didn't reset to 1 record found.
The query selects records depending on a ProductID where the (qtyordered - qtyproduced > 0) and the records are sorted by priority of the customers.
Dim rs As DAO.Recordset
Dim db As DAO.Database
Dim findCSQL As String
findCSQL = "SELECT tblWarehouseTransfers.WTRProductID, tblOrderDetails.ODEPriority, tblOrderDetails.ODEQuantityOrdered, " _
& " tblOrderDetails.ODEQtyProduced, tblCustomers.CompanyName " _
& " FROM tblCustomers INNER JOIN (tblOrders INNER JOIN (tblWarehouseTransfers INNER JOIN tblOrderDetails " _
& " ON tblWarehouseTransfers.WTRProductID = tblOrderDetails.ODEProductFK) " _
& " ON tblOrders.ORDOrderID = tblOrderDetails.ODEOrderID) ON tblCustomers.ID = tblOrders.ORDCustomerID " _
& " WHERE (((tblWarehouseTransfers.WTRProductID) = " & Chr$(34) & Me!cboTransferProductID & Chr$(34) & ")) " _
& " AND ((tblOrderDetails.ODEQuantityOrdered - tblOrderDetails.ODEQtyProduced)> 0) " _
& " ORDER BY tblOrderDetails.ODEPriority "
Set rs = db.OpenRecordset(findCSQL)
rs.MoveFirst
Debug.Print rs.RecordCount
Do While Not rs.EOF
Debug.Print ("product ID: " & rs!WTRProductID & " qty ordered: " & rs!ODEQuantityOrdered & " customer name: " & rs!CompanyName)
rs.MoveNext
Loop
rs.Close
Set rs = Nothing
How can I empty the recordset and start fresh each time the subroutine is run?
My memory might be off since it's been a long time since I've worked with RecordSets, but I think you had to traverse the records before the RecordCount property gave you the correct information.
So try it this way:
rs.MoveLast
Debug.Print rs.RecordCount
rs.MoveFirst
From How to: Count the Number of Records in a DAO Recordset
The value of the RecordCount property equals the number of records that have actually been accessed.

Fill Field When All Checkboxes Toggled Access 2010

I have an expenditures subform in Access 2010 that lists the predicted costs associated with the project for each year. Most projects only have one year, but some have more than one. Each cost has a Final checkbox next to it that should be checked when the amount is confirmed, ie. at the end of each year.
It basically looks something like this:
Year | Cost | Final
--------+-----------+--------------------
2017 | $100 | [checked box]
2018 | $200 | [unchecked box]
| | [unchecked box]
I have another field outside the table, FinalCost, that adds up everything in the Cost field. Right now, it fills in the amount from any year which has a checked Final box. That should only be filled when all the Final boxes are checked.
Ex. Right now, it should show nothing even though Final for 2017 is checked. When 2018 is checked, it should show $300. Instead, it shows $100 even though there's still an empty checkbox.
This is the code for this form.
Private Sub Form_AfterUpdate()
Dim rs1, rs2 As Recordset
Dim sql, sql2 As String
sql = "SELECT Sum(Amount) as Final From Expenditures " & _
"Where ProjNo = '" + Me.ProjNo + "' And Final = True Group by ProjNo"
sql2 = "SELECT FinalExpenditure From ActivityCash " & _
"Where ProjNo = '" + Me.ProjNo + "'"
Set rs1 = CurrentDb.OpenRecordset(sql, dbOpenDynaset, dpinconsistent)
Set rs2 = CurrentDb.OpenRecordset(sql2, dbOpenDynaset, dpinconsistent)
If rs1.RecordCount > 0 Then
If rs2.RecordCount > 0 Then
Do While Not rs2.EOF
rs2.Edit
rs2!FinalExpenditure = rs1!Final
rs2.Update
rs2.MoveNext
Loop
End If
End If
rs2.Close
rs1.Close
Set rs1 = Nothing
Set rs2 = Nothing
End Sub
What would be the best way to go about doing this?
EDIT: When the last box is checked, a new row is automatically added with an untoggled checkbox but no information.
Replace the statement beginning with sql = ... with this:
sql = "SELECT SUM(e1.Amount) AS Final " & _
" FROM Expenditures AS e1 " & _
" WHERE NOT EXISTS (SELECT 'x' FROM Expenditures e2 WHERE e2.Final=0 AND e1.ProjNo = e2.ProjNo) " & _
" AND e1.ProjNo = '" & Me.ProjNo & "'"
This query will return data only if there are all expeditures for the project marked as final. As you check for rs1.RecordCount > 0 there will be no update if this query returns no records.
So, before sql, I would verify that all records have True in your Final field.
To do that, let's just return a COUNT() of (any) records that have Final = False, and we can then decide to do what we want.
So, something like,
Dim Test as Integer
test = DCount("*", "YourTableName", "Final = False AND ProjNo = " & Me.ProjNo &"")
If test > 0 Then
'Don't fill the box
Else
'Fill the box, everything is True
'Read through your recordsets or whatever else you need to do
End If
To use a query, we essentially need to replicate the Dcount() functionality.
To do this, we need another Recordset variable, and we need to check the value of the Count() field from our query.
Create a query that mimicks this:
SELECT COUNT(*) As CountTest
FROM YourTable
HAVING Final = False
AND ProjNo = whateverprojectnumberyou'reusing
Save it, and remember that query's name.
Much like the DCount(), we need to make this "check" determine the route of your code.
Dim rst As DAO.Recordset
Set rst = CurrentDb.OpenRecordset("YourQuery'sNameHere")
If rst!CountTest > 0 Then
'They are not all Checked (aka True)
Else
'Supply the value to the FinalCost
End If
Set rst = Nothing
Change this:
sql = "SELECT Sum(Amount) as Final From Expenditures " & _
"Where ProjNo = '" + Me.ProjNo + "' And Final = True Group by ProjNo"
For this:
"SELECT SUM(Amount) - SUM(IIF(Final,1,0)*Amount) as YetToConfirm, SUM(Amount) as Confirmed From Expenditures " & _
"Where ProjNo = '" + Me.ProjNo + "' Group by ProjNo"
rs1 will return two values, the total value if all costs were confirmed in the rs1!Confirmed, and the value yet to confirm in rs1!YetToConfirm
Then here:
Do While Not rs2.EOF
rs2.Edit
rs2!FinalExpenditure = rs1!Final
rs2.Update
rs2.MoveNext
Loop
change it to:
Do While Not rs2.EOF
rs2.Edit
rs2!FinalExpenditure = Iif(rs1!YetToConfirm = 0, rs1!Confirmed, 0)
rs2.Update
rs2.MoveNext
Loop
One way to process this would be check using a subquery whether last year(verified using a dmax function) in each project has been checked in the final column, if this is true, get your sum of checked amounts, else dont calculate the sum.
I have modified your sql string to include this and I tested it against your given example to confirm its showing a sum of $300 or nothing.
SQL = ""
SQL = SQL & " SELECT Sum(Amount) as Final From Expenditures "
SQL = SQL & " Where ProjNo = '" & Me.ProjNo & "' And Final = True "
SQL = SQL & " And (SELECT Expenditures.Final FROM Expenditures where year = ( "
SQL = SQL & " DMax('Year','Expenditures','ProjNo= " & Chr(34) & Me.ProjNo & Chr(34) & "'))) = true "
SQL = SQL & " Group by ProjNo "

Using array in WHERE clause of SQL statement using access VBA

I have an array ListBoxContents(), it will contain the items like '15', '16','25'..upto 10 items. I'm trying to retrieve data in the column Bnumber where data of length >6 and starting with('15', '16','25'...) i.e those items specified in listbox .And trying to query these listbox items in where cluase of the sql statement
Table column Bnumber contains
Bnumber
152
156
1523
16417
AA454
CC654
18A16
1826
18A16
25A76
54A16
54235A68
My VBA code
Private Sub arraywhere()
Dim qry As String
Dim Size As Integer
Size = Form_Input_From.lstdigits.ListCount - 1
ReDim ListBoxContents(0 To Size) As String
ReDim LContents(0 To 30) As String
Dim m As Integer
For m = 0 To Size
ListBoxContents(m) = Form_Input_From.lstdigits.ItemData(m)
Next m
For m = 0 To Size
qry = "SELECT col1,col2,Bnumber " & _
"FROM table WHERE (Len([table].[Bnumber]))>6) AND (Left
([table].[Bnumber],2))=(" & ListBoxContents(m) & ");"
Next m
Debug.Print qry
Application.CurrentDb.QueryDefs("[arrayqry]").sql = qry
DoCmd.OpenQuery "[arrayqry]"
End Sub
But my WHERE clause reads only last array item only. How do i specify array in where clause?
Try something like
" ... ([table].[Bnumber],2)) in ('" & Join(ListBoxContents,"','") & "');"
You are setting qry to a new statement with each iteration of your for loop. Instead you need to concatenate a string based on your list box contents that will look like ("x", "y", "z") and replace = with in.
Finish by setting your query once it will look similar to this:
qry = "SELECT col1,col2,Bnumber " & _
"FROM table WHERE (Len([table].[Bnumber]))>6) AND (Left
([table].[Bnumber],2)) in (" & commaSeperatedContents & ");"
Where commaSeperatedContents is a String that is like ("x", "y", "z") but of course has your values.
Try this one:
Dim inPart As String
For m = 0 To Size
inPart = inPart & "'" & ListBoxContents(m) & "',"
Next m
inPart = Left(inPart, Len(inPart) - 1)
qry = "SELECT col1,col2,Bnumber " & _
"FROM [table] WHERE Len([table].[Bnumber])>6 AND " & _
"Left([table].[Bnumber],2) In (" & inPart & ");"
Debug.Print qry
CurrentDb.QueryDefs("[arrayqry]").SQL = qry
DoCmd.OpenQuery "arrayqry"
The list of items in your array actually seems to be coming from the Form_Import_From_PMT.lstdigits control. Is this control bound to a data source? If so, you can simply join your table to that data source with a join clause that specifies that only rows with Bnumber values starting with the digits in the joined table are to be selected:
select col1, col2, Bnumber
from table as t
inner join tblDigits as d
on left(t.Bnumber, 2) = d.Digits
where len(t.Bnumber) > 6
If the control is not bound to a data source, then bind it now (creating a new table tblDigits to hold the digits, as shown above), and you'll be able to use the above query.
In short, data binding is how you 'use an array in a where clause' in Access.

Crosstab query with date criteria

I have created a crosstab query as below
TRANSFORM Nz(Sum([debit]*[GeneralExpenses]),0) AS mOmomeya
SELECT Expenses.sName, Expenses.ArName
FROM (GL INNER JOIN Expenses ON GL.ID = Expenses.glID) INNER JOIN Transactions ON GL.GL = Transactions.GL
GROUP BY Expenses.sName, Expenses.ArName
PIVOT Month([ddate]);
what I want to do is to add a criteria to the [dDate] field from a form text input to match the date that I entered, but I received error while doing this as below
any idea what's wrong?
Hmmm, there definitely seems to be something a little bit different about crosstab queries. The straight SELECT query...
SELECT expenses.*
FROM expenses
WHERE (((expenses.ddate) Between [Forms]![main]![DateFrom] And [Forms]![main]![DateTo]));
...works fine, but the crosstab query...
TRANSFORM Sum(expenses.expenses) AS SumOfexpenses
SELECT expenses.onduty
FROM expenses
WHERE (((expenses.ddate) Between [Forms]![main]![DateFrom] And [Forms]![main]![DateTo]))
GROUP BY expenses.onduty
PIVOT Month(ddate);
...fails with the error message you cited. One workaround would be to rebuild the SQL for the query before using it:
Dim qdf As DAO.QueryDef
Set qdf = CurrentDb.QueryDefs("yourCrosstab")
qdf.SQL = _
"TRANSFORM Nz(Sum([debit]*[GeneralExpenses]),0) AS mOmomeya " & _
"SELECT Expenses.sName, Expenses.ArName " & _
"FROM (GL INNER JOIN Expenses ON GL.ID = Expenses.glID) INNER JOIN Transactions ON GL.GL = Transactions.GL " & _
"WHERE (((ddate) " & _
"Between #" & Format(CDate([Forms]![main]![DateFrom]), "yyyy-mm-dd") & "# " & _
"And #" & Format(CDate([Forms]![main]![DateTo]), "yyyy-mm-dd") & "#)) " & _
"GROUP BY Expenses.sName, Expenses.ArName " & _
"PIVOT Month(ddate);"
Set qdf = Nothing
DoCmd.OpenQuery "yourCrosstab" '' or whatever you want to do with it
The problem is that crosstab querys require the form parameter for each form. Right click on gray space and select parameter. Put in the forms and data type.