SQL-запросы занимают слишком много времени
-
08-07-2019 - |
Вопрос
Я использую ADO для сохранения данных в базе данных MS Access.Сохранение данных в файл заняло довольно много времени (около 7 секунд, что слишком долго для наших целей).Я посмотрел на количество выполняемых SQL-запросов, и оно составило около 4200;хотя там нет целой кучи данных.
Соединение с базой данных, по-видимому, является узким местом.Знаете ли вы какой-нибудь способ уменьшить количество времени, которое это занимает;либо каким-то образом объединив несколько операторов в один, чтобы уменьшить накладные расходы, либо каким-то трюком ADO / MS-Access?
Можете ли вы, например, вставить в таблицу несколько строк одновременно, и будет ли это заметно быстрее?
Дополнительная информация:
Одна из причин, по которой у нас так много запросов, заключается в том, что мы вставляем строку, а затем выполняем другой запрос для получения ее автоматически созданного идентификатора;затем используйте этот идентификатор, чтобы вставить еще несколько строк, связав их с первой
В ответ на несколько комментариев и откликов:Я оставляю соединение открытым все время и выполняю его как единую транзакцию с помощью beginTransaction() и CommitTransaciton()
Решение
Некоторые люди опубликовали сообщение о том, что @@ IDENTITY
будет быстрым, так что здесь есть доказательство (с использованием VBA) того, как мои INSERT INTO
две таблицы одновременно через трюк VIEW
примерно в три раза быстрее, чем выполнение двух INSERTS и каждый раз захват значений @@ IDENTITY
... что неудивительно, так как последний включает три Выполнить
операторы, и первый включает только один:)
На моей машине для 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 секунды!
Другие советы
Более поздние версии Access поддерживают переменную @@ IDENTITY. Это можно использовать для извлечения столбца идентификаторов после вставки без выполнения запроса.
INSERT INTO mytable (field1,field2) VALUES (val1,val2);
SELECT @@IDENTITY;
См. эту статью базы знаний .
мы вставляем строку, а затем еще один запрос, чтобы получить его автоинкремент Я БЫ; затем используйте этот идентификатор, чтобы вставить несколько больше строк, связывающих их с первым
Это одна таблица, две таблицы или более двух таблиц?
Если одна таблица, вы можете рассмотреть другой дизайн, например. вы можете сгенерировать свои собственные случайные идентификаторы, использовать модель вложенных множеств, а не модель списка смежности и т. д. Трудно понять, не поделитесь ли вы своим дизайном; -)
Если в двух таблицах предполагается, что между таблицами в столбце AUTOINCREMENT / IDENTITY
есть FOREIGN KEY
, можно создать VIEW
, который INNER JOIN
две таблицы и INSERT INTO
значение VIEW
и значение AUTOINCREMENT / IDENTITY
будут скопированы. к справочной таблице. Дополнительные сведения и рабочий пример в этом ответе о переполнении стека (ссылка ниже).
Если более одной таблицы, трюк VIEW
не выходит за рамки двух таблиц AFAIK, поэтому вам, возможно, придется просто жить с низкой производительностью или менять технологию, например. О DAO сообщается быстрее, чем ADO, SQL Server может быть быстрее, чем ACE / Jet и т. Д. Опять же, не стесняйтесь делиться своим дизайном, может быть решение «нестандартно».
В вашей ситуации может быть лучше использовать 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 подключений ...
Какое-нибудь предложение (которое даже можно объединить):
- Почему бы вам не получить автоматически добавленный ИДЕНТИФИКАТОР перед вставкой строки, чтобы у вас уже было значение , доступное для вашего следующего запроса?Это должно сэкономить вам немного времени.
- Закрытие / размыкание соединения также должно быть проверено.
- Вы думали о том, чтобы манипулировать наборами записей (с помощью кода Visual Basic для обновления ваших данных) вместо таблиц (с помощью инструкций SQL, отправляемых в базу данных)?
- Другим решением (если вы уверены, что соединение является узким местом) было бы создать таблицу локально, а затем экспортировать ее в файл access после выполнения задания.
- Поскольку вы используете ADO, вы даже можете сохранить свои данные в виде XML-файла, загрузить набор записей из этого XML-файла, обработать его с помощью VBA и экспортировать в базу данных Access
Не хочу быть умником ... Но есть ли причина продолжать использовать Access? SQL Server Express бесплатен, быстрее и эффективнее ...
Похоже, вы импортируете данные, и в Access есть гораздо более удобные средства для импорта данных. Да, он вставляет много записей одновременно, и это будет быстрее. Р>
Можете ли вы описать приложение немного подробнее?
Самый дешевый и (в основном каждый раз) лучший прирост скорости, который вы можете дать базе данных, - это сохранять неизменяющиеся данные в кэше.
Не знаю, применимо ли это к вашему случаю, но это очень просто.Вы можете просто подключить (бесплатную) библиотеку, которая это делает, или сохранить локальную коллекцию прочитанных элементов.Пример :
- Кто-то хочет прочитать пункт X.
- Есть ли элемент X в локальной коллекции ?
- НЕТ :прочитайте элемент X из базы данных и поместите его в локальную коллекцию.
- ДА :просто верните локальную копию элемента X.
Конечно, это может быть немного сложнее, когда вы запускаете веб-сайт с большим количеством серверов.В этом случае просто используйте Блоки приложений или Кэширование.
Позвольте мне опровергнуть следующие утверждения:
предложение 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
Некоторые мысли, которые могут или не могут быть полезны:
<Ол>Позвольте мне поддержать предложение SELECT @@ IDENTITY - безусловно, гораздо более быстрый способ вставки данных и извлечения значения Autonumber, чем открытие набора записей AddOnly, обновление полей, сохранение записи и сохранение значения Autonumber. SQL INSERT всегда будет быстрее, чем использование построчного набора записей.
используя транзакции DAO, вы можете преобразовать несколько таких вставок в пакет, который выполняется одновременно. Я не уверен в этом, так как я не знаю точно, что вы делаете и сколько таблиц вы используете. Дело в том, что вы выполняете серию SQL INSERT в транзакции, а затем выполняете .Commit в конце, так что фактическая запись в реальный файл базы данных будет выполняться как одна операция, а не как 4200 (или Однако много) отдельных операций.