2

I need to update several millions records in SQL Server from MS ACCESS table. I use MS ACCESS as a frontend and SQL as linked tables. So as I understand I need to create pass-through query to execute something like this:

UPDATE SQLtbl SET SQLtbl.col1 = MDBtbl.Col1 FROM SQLtbl INNER JOIN MDBtbl ON SQLtbl.ID = MDBtbl.ID WHERE SQLtbl.col1 != MDBtbl.Col1

It works extremely slow so I need to convert int somehow to BULK UPDATE Please advice, Thank you

user3920056
  • 21
  • 1
  • 3

2 Answers2

3

Similar to a recent related question here, this is another one of those cases where treating ODBC linked tables just like native Access tables can result in slow performance.

For two identical Access tables named [SQLtbl] (ODBC linked to SQL Server) and [MDBtbl] (native Access) with 9999 rows each, the following code took around 5.5 minutes to execute:

Sub UpdateViaJoin()
    Dim con As ADODB.Connection
    Dim t0 As Single

    Set con = CurrentProject.Connection
    con.CommandTimeout = 0
    t0 = Timer
    con.Execute _
            "UPDATE " & _
                "SQLtbl INNER JOIN MDBtbl " & _
                    "ON SQLtbl.ID = MDBtbl.ID " & _
            "SET SQLtbl.Col1 = MDBtbl.Col1"
    Debug.Print Format(Timer - t0, "0.0") & " seconds"
    Set con = Nothing
End Sub

To see if the JOIN itself was a problem, I ran the following which took just over 5 minutes to complete:

Sub UbdateViaDLookup()
    Dim cdb As DAO.Database
    Dim t0 As Single

    Set cdb = CurrentDb
    t0 = Timer
    cdb.Execute _
            "UPDATE SQLtbl SET Col1 = DLookup(""Col1"", ""MDBtbl"", ""ID="" & ID)"
    Debug.Print Format(Timer - t0, "0.0") & " seconds"
    Set cdb = Nothing
End Sub

On the other hand, the following code that uses a pass-through query and a native T-SQL prepared statement consistently ran in under 2 seconds (that is, more than 100 times faster):

Sub UpdateViaPassThroughQuery()
    Dim cdb As DAO.Database, rst As DAO.Recordset, qdf As DAO.QueryDef
    Dim SQL As String, statementHandle As Long, i As Long, updateList As String
    Dim t0 As Single

    Set cdb = CurrentDb
    t0 = Timer

    SQL = "SET NOCOUNT ON;"
    SQL = SQL & "DECLARE @statementHandle int;"
    SQL = SQL & "EXEC sp_prepare @statementHandle OUTPUT, N'@P1 nvarchar(50), @P2 int', N'UPDATE SQLtbl SET Col1=@P1 WHERE ID=@P2';"
    SQL = SQL & "SELECT @statementHandle;"
    Set qdf = cdb.CreateQueryDef("")
    qdf.Connect = cdb.TableDefs("SQLtbl").Connect
    qdf.SQL = SQL
    qdf.ReturnsRecords = True
    Set rst = qdf.OpenRecordset(dbOpenSnapshot)
    statementHandle = rst(0).Value
    rst.Close

    Set rst = cdb.OpenRecordset("SELECT ID, Col1 FROM MDBtbl", dbOpenSnapshot)
    i = 0
    updateList = ""
    Do Until rst.EOF
        i = i + 1
        updateList = updateList & "EXEC sp_execute " & statementHandle & ", N'" & Replace(rst!Col1, "'", "''") & "', " & rst!id & ";"
        If i = 1000 Then
            qdf.SQL = updateList
            qdf.ReturnsRecords = False
            qdf.Execute
            i = 0
            updateList = ""
        End If
        rst.MoveNext
    Loop
    If i > 0 Then
        qdf.SQL = updateList
        qdf.ReturnsRecords = False
        qdf.Execute
    End If
    rst.Close
    Set rst = Nothing

    qdf.SQL = "EXEC sp_unprepare " & statementHandle & ";"
    qdf.ReturnsRecords = False
    qdf.Execute
    Set qdf = Nothing
    Debug.Print Format(Timer - t0, "0.0") & " seconds"
    Set cdb = Nothing
End Sub

Edit

To tweak the above code to handle Nulls you would need to update the line ...

updateList = updateList & "EXEC sp_execute " & statementHandle & ", N'" & Replace(rst!Col1, "'", "''") & "', " & rst!id & ";"

... to ...

updateList = updateList & "EXEC sp_execute " & statementHandle & ", " & _
        FormatArgForPrepStmt(rst!Col1) & ", " & _
        rst!id & ";"

... and add a little formatting function something like this:

Private Function FormatArgForPrepStmt(item As Variant) As String
    If IsNull(item) Then
        FormatArgForPrepStmt = "NULL"
    Else
        Select Case VarType(item)
            Case vbString
                FormatArgForPrepStmt = "N'" & Replace(item, "'", "''") & "'"
            Case vbDate
                FormatArgForPrepStmt = "N'" & Format(item, "yyyy-mm-dd Hh:Nn:Ss") & "'"
            Case Else
                FormatArgForPrepStmt = CStr(item)
        End Select
    End If
End Function
Community
  • 1
  • 1
Gord Thompson
  • 116,920
  • 32
  • 215
  • 418
  • Thank you very much! It's really helpful! But how to handle this kind of update? Update SQLtbl INNER JOIN MDBtbl ON SQLtbl.ID = MDBtbl.ID SET SQLtbl.col1 = MDBtbl.Col1 Thank you in advance! – user3920056 Oct 11 '14 at 02:33
  • @user3920056 That's exactly what the third code sample does. It uses `sp_prepare` to create the UPDATE prepared statement for [SQLtbl] on the SQL Server, loops through the rows in [MDBtbl] to create and execute `sp_execute` calls 1000 at at time, then calls `sp_unprepare` to discard the T-SQL prepared statement. If you need more background on `sp_prepare` then look [here](http://msdn.microsoft.com/en-us/library/ff848808%28v=sql.105%29.aspx). – Gord Thompson Oct 11 '14 at 21:58
  • Thanks, it works! But here is the another issue...when I'm updating only one column execution for 500 records takes about 2 sec, I just added 5 fields to be updated and execution time is about 20 sec, e.g. UPDATE SQLtbl SET Col1=@P1, Col2=@P2,...WHERE ID=@P0 But my tables to be updated contain from 100-150 columns. Actually I need to add them all. Is there way to speed up this process? – user3920056 Oct 12 '14 at 20:25
  • The other thing...once I started to add more columns to be updated I got a message "System resources exceeded" so I had to put i = 100 or maybe in future even i = 5 cause I did not add all columns so far. But it definitely works much much better than JOIN – user3920056 Oct 12 '14 at 21:24
  • and one more...some columns have null values so update fails on some rows but works with JOIN. Is there any way to handle somehow columns that have null? Thank you in advance – user3920056 Oct 13 '14 at 02:45
0

The poster clearly asks:

“I understand I need to crate pass-though queries”.

So we need to address how to create and run a pass-though query.

The simple solution is to thus save your query as pass-though. In fact, you can “change” your existing query to pass-though.

Bring up your existing query in design view, and hit the pass-thought button.

Eg this:

enter image description here

If you save your query as pass-though, then you running server side t-sql.

Note that the pass-though button is hit, then this also brings up the property sheet. I circled the pass-though button and the ODBC setting required in the property sheet. In fact you can COPY the odbc connection string from a linked table via cut + paste.

Thus: There is no need to setup connection strings in code There is NO need to write VBA code. There is in fact LITTLE IF ANY need to even re-write the sql, it should work as you have as native t-sql code.

One thus achieves rather desirable goals here:

You can re-use the EXISTING query.

You do not have to RE-WRITE the query.

You don’t have to adopt and write a WHOLE bunch of code.

You don’t even have to know VBA

You don’t introduce a WHOLE NEW ADO object model library into an existing application. It is un-likely that the existing application was using the ADO object library. You now have to introduce this WHOLE new object library into your existing application. Adding this new reference and VBA code will thus create significant potential to break and introduce bugs into the existing code base, this is EXPECIALLY the case if existing code did not qualify DAO recordsets.

I think to run one pass-though update, it far more simple and risk free to simply use ONE pass-though query and simply click on the ribbon then that of introducting whole new ADO reference, and object libriares and THEN introduce a bunch of VBA ADO code to simply run a simple update query.

Albert D. Kallal
  • 42,205
  • 3
  • 34
  • 51
  • 1
    That's all well and good, but if [MDBtbl] is a local Access table then how is the SQL Server going to "see" it when it runs the saved pass-through query? (BTW, the only reason my first code sample used an ADO `.Execute` is because when I tried to use a DAO `.Execute` it crashed consistently. At least the ADO `.Execute` completed, eventually.) – Gord Thompson Oct 13 '14 at 20:27
  • I adjusted your code to my code and it works awesome. I'm updating in one row up to 150 columns which allows me to update approx. 2,500 records/min which is good for now. Is it possible to modify your function to use it for INSERT records from MDBtbl to SQL tbl which do not exist?, e.g. by using the same technics. Thank you – user3920056 Oct 13 '14 at 21:42