SQL-запросы занимают слишком много времени - PullRequest
0 голосов
/ 07 мая 2009

Я использую ADO для сохранения данных в базе данных MS Access. Сохранение данных в файл занимало довольно много времени (около 7 секунд - это слишком долго для наших целей). Я посмотрел на количество выполняемых SQL-запросов, а это около 4200; хотя нет целой кучи данных.

Кажется, что узким местом является соединение с базой данных. Знаете ли вы какой-либо способ уменьшить количество времени, которое требуется; или как-то объединить несколько операторов в один, чтобы уменьшить накладные расходы, или какой-то трюк ADO / MS-Access?

Можете ли вы, например, вставить несколько строк в таблицу одновременно, и будет ли это заметно быстрее?

Дополнительная информация:

Одна из причин, по которой у нас так много запросов, заключается в том, что мы вставляем строку, а затем выполняем другой запрос для получения ее автоинкрементного идентификатора; затем используйте этот идентификатор, чтобы вставить еще несколько строк, связав их с первым

В ответ на несколько комментариев и ответов: я оставляю соединение открытым все время и выполняю его как одну транзакцию с BeginTransaction () и CommitTransaciton ()

Ответы [ 10 ]

5 голосов
/ 08 мая 2009

Некоторые люди писали, что @@IDENTITY будет быстрым, так что вот доказательство (с использованием VBA) того, что мои INSERT INTO две таблицы одновременно с помощью трюка VIEW примерно в три раза быстрее, чем две INSERTS и захват. @@IDENTITY значения каждый раз ... что неудивительно, потому что последнее включает три Execute утверждения, а первое включает только одно:)

На моей машине для 4200 итераций трюк VIEW занял 45 секунд, а подход @@IDENTITY занял 127 секунд:

Sub InitInerts()
  On Error Resume Next
  Kill Environ$("temp") & "\DropMe.mdb"
  On Error GoTo 0
  Dim cat
  Set cat = CreateObject("ADOX.Catalog")
  With cat
    .Create _
        "Provider=Microsoft.Jet.OLEDB.4.0;" & _
        "Data Source=" & _
        Environ$("temp") & "\DropMe.mdb"

    With .ActiveConnection

      Dim Sql As String

      Sql = _
      "CREATE TABLE TableA" & vbCr & "(" & vbCr & "   ID IDENTITY NOT" & _
      " NULL UNIQUE, " & vbCr & "   a_col INTEGER NOT NULL" & vbCr & ")"
      .Execute Sql

      Sql = _
      "CREATE TABLE TableB" & vbCr & "(" & vbCr & "   ID INTEGER NOT" & _
      " NULL UNIQUE" & vbCr & "      REFERENCES TableA (ID)," & _
      "  " & vbCr & "   b_col INTEGER NOT NULL" & vbCr & ")"
      .Execute Sql

      Sql = _
      "CREATE VIEW TestAB" & vbCr & "(" & vbCr & "   a_ID, a_col, " & vbCr & " " & _
      "  b_ID, b_col" & vbCr & ")" & vbCr & "AS " & vbCr & "SELECT A1.ID, A1.a_col," & _
      " " & vbCr & "       B1.ID, B1.b_col" & vbCr & "  FROM TableA AS" & _
      " A1" & vbCr & "       INNER JOIN TableB AS B1" & vbCr & "    " & _
      "      ON A1.ID = B1.ID"
      .Execute Sql

    End With
    Set .ActiveConnection = Nothing
  End With
End Sub

Sub TestInerts_VIEW()

  Dim con
  Set con = CreateObject("ADODB.Connection")
  With con
    .Open _
        "Provider=Microsoft.Jet.OLEDB.4.0;" & _
        "Data Source=" & _
        Environ$("temp") & "\DropMe.mdb"

    Dim timer As CPerformanceTimer
    Set timer = New CPerformanceTimer
    timer.StartTimer

    Dim counter As Long
    For counter = 1 To 4200
      .Execute "INSERT INTO TestAB (a_col, b_col) VALUES (" & _
                   CStr(counter) & ", " & _
                   CStr(counter) & ");"
    Next

    Debug.Print "VIEW = " & timer.GetTimeSeconds

  End With

End Sub

Sub TestInerts_IDENTITY()

  Dim con
  Set con = CreateObject("ADODB.Connection")
  With con
    .Open _
        "Provider=Microsoft.Jet.OLEDB.4.0;" & _
        "Data Source=" & _
        Environ$("temp") & "\DropMe.mdb"

    Dim timer As CPerformanceTimer
    Set timer = New CPerformanceTimer
    timer.StartTimer

    Dim counter As Long
    For counter = 1 To 4200
      .Execute "INSERT INTO TableA (a_col) VALUES (" & _
          CStr(counter) & ");"

      Dim identity As Long
      identity = .Execute("SELECT @@IDENTITY;")(0)

      .Execute "INSERT INTO TableB (ID, b_col) VALUES (" & _
                   CStr(identity) & ", " & _
                   CStr(counter) & ");"

    Next

    Debug.Print "@@IDENTITY = " & timer.GetTimeSeconds

  End With

End Sub

То, что это показывает, является узким местом, которое теперь связано с выполнением нескольких операторов. Что если бы мы могли сделать это всего за одно утверждение? Ну, угадайте, что, используя мой надуманный пример, мы можем. Сначала создайте таблицу последовательности уникальных целых чисел, являющуюся стандартным трюком SQL (в каждой базе данных должен быть один, IMO):

Sub InitSequence()

  Dim con
  Set con = CreateObject("ADODB.Connection")
  With con
    .Open _
        "Provider=Microsoft.Jet.OLEDB.4.0;" & _
        "Data Source=" & _
        Environ$("temp") & "\DropMe.mdb"

    Dim sql As String

    sql = _
        "CREATE TABLE [Sequence]" & vbCr & "(" & vbCr & "   seq INTEGER NOT NULL" & _
        " UNIQUE" & vbCr & ");"
    .Execute sql

    sql = _
        "INSERT INTO [Sequence] (seq) VALUES (-1);"
    .Execute sql

    sql = _
        "INSERT INTO [Sequence] (seq) SELECT Units.nbr + Tens.nbr" & _
        " + Hundreds.nbr + Thousands.nbr AS seq FROM ( SELECT" & _
        " nbr FROM ( SELECT 0 AS nbr FROM [Sequence] UNION" & _
        " ALL SELECT 1 FROM [Sequence] UNION ALL SELECT 2 FROM" & _
        " [Sequence] UNION ALL SELECT 3 FROM [Sequence] UNION" & _
        " ALL SELECT 4 FROM [Sequence] UNION ALL SELECT 5 FROM" & _
        " [Sequence] UNION ALL SELECT 6 FROM [Sequence] UNION" & _
        " ALL SELECT 7 FROM [Sequence] UNION ALL SELECT 8 FROM" & _
        " [Sequence] UNION ALL SELECT 9 FROM [Sequence] ) AS" & _
        " Digits ) AS Units, ( SELECT nbr * 10 AS nbr FROM" & _
        " ( SELECT 0 AS nbr FROM [Sequence] UNION ALL SELECT" & _
        " 1 FROM [Sequence] UNION ALL SELECT 2 FROM [Sequence]" & _
        " UNION ALL SELECT 3 FROM [Sequence] UNION ALL SELECT" & _
        " 4 FROM [Sequence] UNION ALL SELECT 5 FROM [Sequence]" & _
        " UNION ALL SELECT 6 FROM [Sequence] UNION ALL SELECT" & _
        " 7 FROM [Sequence] UNION ALL SELECT 8 FROM [Sequence]" & _
        " UNION ALL SELECT 9 FROM [Sequence] ) AS Digits )" & _
        " AS Tens, ( SELECT nbr * 100 AS nbr FROM ( SELECT" & _
        " 0 AS nbr FROM [Sequence] UNION ALL SELECT 1 FROM" & _
        " [Sequence] UNION ALL SELECT 2 FROM [Sequence] UNION"
    sql = sql & _
        " ALL SELECT 3 FROM [Sequence] UNION ALL SELECT 4 FROM" & _
        " [Sequence] UNION ALL SELECT 5 FROM [Sequence] UNION" & _
        " ALL SELECT 6 FROM [Sequence] UNION ALL SELECT 7 FROM" & _
        " [Sequence] UNION ALL SELECT 8 FROM [Sequence] UNION" & _
        " ALL SELECT 9 FROM [Sequence] ) AS Digits ) AS Hundreds," & _
        " ( SELECT nbr * 1000 AS nbr FROM ( SELECT 0 AS nbr" & _
        " FROM [Sequence] UNION ALL SELECT 1 FROM [Sequence]" & _
        " UNION ALL SELECT 2 FROM [Sequence] UNION ALL SELECT" & _
        " 3 FROM [Sequence] UNION ALL SELECT 4 FROM [Sequence]" & _
        " UNION ALL SELECT 5 FROM [Sequence] UNION ALL SELECT" & _
        " 6 FROM [Sequence] UNION ALL SELECT 7 FROM [Sequence]" & _
        " UNION ALL SELECT 8 FROM [Sequence] UNION ALL SELECT" & _
        " 9 FROM [Sequence] ) AS Digits ) AS Thousands;"
    .Execute sql

  End With

End Sub

Затем используйте таблицу Sequence для перечисления значений от 1 до 42000 и построения строк в одном операторе INSERT INTO..SELECT:

Sub TestInerts_Sequence()

  Dim con
  Set con = CreateObject("ADODB.Connection")
  With con
    .Open _
        "Provider=Microsoft.Jet.OLEDB.4.0;" & _
        "Data Source=" & _
        Environ$("temp") & "\DropMe.mdb"

    Dim timer As CPerformanceTimer
    Set timer = New CPerformanceTimer
    timer.StartTimer

    .Execute "INSERT INTO TestAB (a_col, b_col) " & _
             "SELECT seq, seq " & _
             "FROM Sequence " & _
             "WHERE seq BETWEEN 1 AND 4200;"

    Debug.Print "Sequence = " & timer.GetTimeSeconds



  End With

End Sub

Это выполняется на моей машине за 0,2 секунды!

2 голосов
/ 07 мая 2009

Более поздние версии Access поддерживают переменную @@ IDENTITY. Это можно использовать для извлечения столбца идентификаторов после вставки, без выполнения запроса.

INSERT INTO mytable (field1,field2) VALUES (val1,val2);
SELECT @@IDENTITY;

См. статью базы знаний .

0 голосов
/ 11 мая 2009

Позвольте мне опровергнуть следующие утверждения:

предложение SELECT @@ IDENTITY - безусловно, гораздо более быстрый способ вставить данные и получить Значение Autonumber, чем открыть AddOnly набор записей, обновление полей, сохранение запись и хранение Автономера значение. SQL INSERT всегда будет быстрее, чем использование строки за строкой записей.

Я подумал, что подход с двумя наборами записей может быть немного быстрее, чем подход @@ IDENTITY, потому что я подозревал, что он включает в себя меньшее количество обращений к базе данных. В моем тестировании это было намного быстрее за 1,2 секунды, по сравнению с 127 секундами для подхода @@ IDENTITY. Вот мой код (код для создания .mdb размещен в другом ответе):

Sub TestInerts_rs()

  Dim con
  Set con = CreateObject("ADODB.Connection")
  con.Open _
        "Provider=Microsoft.Jet.OLEDB.4.0;" & _
        "Data Source=" & _
        Environ$("temp") & "\DropMe.mdb"

  Dim timer As CPerformanceTimer
  Set timer = New CPerformanceTimer
  timer.StartTimer

  Dim rs1
  Set rs1 = CreateObject("ADODB.Recordset")
  With rs1
    .ActiveConnection = con
    .CursorType = 1  ' keyset
    .LockType = 3  ' optimistic
    .Source = "SELECT a_col, ID FROM TableA;"
    .Open
  End With

  Dim rs2
  Set rs2 = CreateObject("ADODB.Recordset")
  With rs2
    .ActiveConnection = con
    .CursorType = 1  ' keyset
    .LockType = 3  ' optimistic
    .Source = "SELECT b_col, ID FROM TableB;"
    .Open
  End With

  Dim counter As Long
  For counter = 1 To 4200
    rs1.AddNew "a_col", counter

    Dim identity As Long
    identity = rs1.Fields("ID").value

    rs2.AddNew Array(0, 1), Array(counter, identity)

  Next

  Debug.Print "rs = " & timer.GetTimeSeconds

End Sub
0 голосов
/ 08 мая 2009

Самое дешевое и (в большинстве случаев) лучшее повышение скорости, которое вы можете дать базе данных, - это хранить неизменяемые данные в кэше.

Не знаю, может ли это быть применимо к вашему делу, но это очень просто. Вы можете просто подключить (бесплатную) библиотеку, которая это делает, или сохранить локальную коллекцию прочитанных предметов. Пример:

  • Один хочет прочитать пункт X.
  • Элемент X находится в местной коллекции?
  • Нет: прочитать элемент X из базы данных и поместить его в локальную коллекцию.
  • Да: просто верните локальную копию элемента X.

Конечно, это может быть немного сложнее, когда вы запускаете веб-сайт с большим количеством серверов. В этом случае используйте только блоки приложений или кэшированные.

0 голосов
/ 08 мая 2009

Звучит так, будто вы импортируете данные, и Access имеет гораздо лучшие возможности для импорта данных. Да, он вставляет много записей одновременно, и это будет быстрее.

Можете ли вы описать приложение немного подробнее?

0 голосов
/ 08 мая 2009

Не хочу быть умником ... Но есть ли причина продолжать использовать Access? SQL Server Express бесплатен, быстрее и эффективнее ...

0 голосов
/ 08 мая 2009

Некоторые мысли, которые могут или не могут быть полезны:

  1. Позвольте мне поддержать предложение SELECT @@ IDENTITY - безусловно, гораздо более быстрый способ вставки данных и получения значения Autonumber, чем открытие набора записей AddOnly, обновление полей, сохранение записи и сохранение значения Autonumber. SQL INSERT всегда будет быстрее, чем использование построчного набора записей.

  2. используя транзакции DAO, вы можете преобразовать несколько таких вставок в пакет, который выполняется одновременно. Я не уверен в этом, так как я не знаю точно, что вы делаете и сколько таблиц вы используете. Дело в том, что вы выполняете серию SQL INSERT в транзакции, а затем выполняете .Commit в конце, так что фактическая запись в реальный файл базы данных будет выполняться как одна операция, а не как 4200 (или Однако многие) отдельные операции.

0 голосов
/ 07 мая 2009

Некоторые предложения (которые можно даже объединить):

  • Почему бы тебе не получить автоинкремент ID перед вставкой строки, так вы уже будете иметь значение доступны для вашего следующего запроса? это должен сэкономить вам время.
  • Закрытие / открытие соединения также должен быть проверен.
  • Ты думал о манипулировании наборы записей (с кодом Visual Basic для обновить ваши данные) вместо таблиц (с инструкциями SQL, отправленными на базы данных)?
  • Другое решение (если вы уверены, что связь является узкое место) будет создавать таблица локально, а затем экспортировать его в файл доступа после выполнения задания.
  • Поскольку вы используете ADO, вы можете даже сохранить ваши данные в виде файла XML, загрузить набор записей из этого файла XML, манипулировать им с помощью VBA и экспортировать к базе данных Access
0 голосов
/ 07 мая 2009

В вашей ситуации может быть лучше использовать ADO для выполнения вставки вместо выполнения команды SQL.

for i = 1 to 100
   rs.AddNew
   rs("fieldOne") = "value1"
   rs("fieldOne") = "value2"
   rs.Update
   id = rs("uniqueIdColumn")
   'do stuff with id...
next

Я знаю, что использование ADO медленное, но во много раз быстрее, чем открытие 4200 подключений ...

0 голосов
/ 07 мая 2009

мы вставляем строку, а затем имеем другую запрос, чтобы получить его автоинкремент Я БЫ; затем используйте этот идентификатор, чтобы вставить несколько больше строк, связывающих их с первым

Это одна таблица, две таблицы или более двух таблиц?

Если бы одна таблица, вы могли бы рассмотреть другой дизайн, например. вы можете сгенерировать свои собственные случайные идентификаторы, использовать модель вложенных множеств, а не модель списка смежности и т. д. Трудно понять, не поделитесь ли вы своим проектом; -)

Если две таблицы, предполагая, что между таблицами в столбце AUTOINCREMENT/IDENTITY есть FOREIGN KEY, вы можете создать VIEW, INNER JOIN с двумя таблицами и INSERT INTO VIEW и AUTOINCREMENT/IDENTITY значение будет «скопировано» в справочную таблицу. Более подробная информация и рабочий пример в этом ответе о переполнении стека (ссылка ниже).

Если более одной таблицы, трюк VIEW не распространяется за пределы двух таблиц AFAIK, поэтому вам, возможно, придется смириться с низкой производительностью или изменить технологию, например. О DAO сообщается быстрее, чем ADO, SQL Server может быть быстрее, чем ACE / Jet и т. Д. Опять же, не стесняйтесь поделиться своим дизайном, может быть решение «нестандартное».

Как вставить данные?

...