этот пост можно считать статьей =)
если есть ошибки сорри!
читаю книгу по ADO.NET, пытаюсь с БД разобраться... там описаны ошибки которые порой делают даже профессионалы =) эта статья предназначен для тех кто до сих пор так и не купил эту замечательную книгу (Название книги: "Программирование на Microsoft ADO.NET 2.0") =)
1) оказывается что запрос "SELECT * FROM table" работает медленней чем "SELECT column_1, column_2, column_3, column_N FROM table" из-за, как вы уже наверно догадались, той всеми любимой "*", т.к. запросу требуется больше времени на обработку таблицы для построения ее схемы и выявление полей в ней... но отказаться от "*" я не могу =) лень все поля писать =)))
2) вот код построчного чтения БОЛЬШОЙ таблицы:
'подключение
'создание команды ("SELECT column_1, column_2, column_3, column_4, column_5, FROM table")
'создание объекта DataReader (DR)
Do While DA.Read()
int_1 = DA("Column_1")
int_2 = DA("Column_2")
str = DA("Column_3")
date_1 = DA("Column_4")
date_2 = DA("Column_5")
Loop
Код выше работает значительно медленнее чем этот:
'подключение
'создание команды ("SELECT column_1, column_2, column_3, column_4, column_5, FROM table")
'создание объекта DataReader (DR)
Do While DA.Read()
int_1 = DA(0)
int_2 = DA(1)
str = DA(2)
date_1 = DA(3)
date_2 = DA(4)
Loop
это как и пример со "*", только здесь программа вместо одного поиска как было с SQL запросом будет выполнять поиск нужного номера столбца каждый раз когда будет производиться считывание (x1 = DA(1)) это сильно снизит производительность БД. многие скажут: "как мне быть если нужно ориентироваться именно на имя поля, т.к. их порядковые номера могут меняться!", для этого есть один выход... вот код:
'подключение
'создание команды ("SELECT column_1, column_2, column_3, column_4, column_5, FROM table")
'создание объекта DataReader (DR)
Dim Column_1 As Integer = DA.GetOrdinal("Column_1") 'записываем номер поля в переменную!
Dim Column_2 As Integer = DA.GetOrdinal("Column_2")
Dim Column_3 As Integer = DA.GetOrdinal("Column_3")
Dim Column_4 As Integer = DA.GetOrdinal("Column_4")
Dim Column_5 As Integer = DA.GetOrdinal("Column_5")
Do While DA.Read()
int_1 = DA(Column_1)
int_2 = DA(Column_2)
str = DA(Column_3)
date_1 = DA(Column_4)
date_2 = DA(Column_5)
Loop
в этом коде мы только 1 раз ищем номера полей а дальше используем их сколько угодно раз! сами считайте как увеличится производительность!!!
3) продолжаем увеличивать скорость БД! =)
итак вот код:
'подключение
'создание команды ("SELECT column_1, column_2, column_3, column_4, column_5, FROM table")
'создание объекта DataReader (DR)
Dim Column_1 As Integer = DA.GetOrdinal("Column_1") 'записываем номер поля в переменную!
Dim Column_2 As Integer = DA.GetOrdinal("Column_2")
Dim Column_3 As Integer = DA.GetOrdinal("Column_3")
Dim Column_4 As Integer = DA.GetOrdinal("Column_4")
Dim Column_5 As Integer = DA.GetOrdinal("Column_5")
Do While DA.Read()
int_1 = DA(Column_1)
int_2 = DA(Column_2)
str = DA(Column_3)
date_1 = DA(Column_4)
date_2 = DA(Column_5)
Loop
вроде все отлично... SQL запрос правильный, считывание происходит правильно, НО! при считывании DataReader сам определяет тип считываемого значения, а это не есть хорошо =))) представьте сколько там лишнего времени тратится? во-во =) итак все юзаем методы GET =) они у DataReader есть на все типы =) вот правильный во всех отношениях код:
'подключение
'конструкция Using обеспечиват 100% закрытие объекта после оператора End Using
'закрытие произойдет даже если внутри конструкции произойдет исключение (ошибка)
Using OleDbCon As New OleDbConnection("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\TestAccess.accdb")
OleDbCon.Open()
'создание команды
Dim OleDbCom As OleDbCommand = OleDbCon.CreateCommand
'создание SQL запроса
OleDbCom.CommandText = "SELECT column_1, column_2, column_3, column_4, column_5 FROM table"
'создание объекта OleDbDataReader
Using OleDbRe As OleDbDataReader = OleDbCom.ExecuteReader
Dim int_1, int_2 As Integer
Dim str As String
Dim date_1, date_2 As Date
Dim Column_1 As Integer = OleDbRe.GetOrdinal("Column_1") 'записываем номер поля в переменную!
Dim Column_2 As Integer = OleDbRe.GetOrdinal("Column_2")
Dim Column_3 As Integer = OleDbRe.GetOrdinal("Column_3")
Dim Column_4 As Integer = OleDbRe.GetOrdinal("Column_4")
Dim Column_5 As Integer = OleDbRe.GetOrdinal("Column_5")
Do While OleDbRe.Read()
int_1 = OleDbRe.GetInt32(Column_1)
int_2 = OleDbRe.GetInt32(Column_2)
str = OleDbRe.GetString(Column_3)
date_1 = OleDbRe.GetDateTime(Column_4)
date_2 = OleDbRe.GetDateTime(Column_5)
Loop
End Using
End Using
Если вы работаете с большими БД, то отпишитесь как увеличилась скорость!
Удачи!
многоуважаемый Steel Brand извиняюсь за мою невнимательность
я переустанавливал винду и почемуто мне не сообщили об новом ответе... я думал что она давно умерла
щас отвечу...
насчет статьи:
эту статью, как я упомянул в самом начале, взял из книги, тут нет ничего моего... дополняйте, критикуйте кто против? мне самому уже интересно что вы напишите
Я не работаю с MS Access, только с MS SQL Server 2005/2008, поэтому все что говорю, относится конкретно к ним (к MS Access некоторые моменты тоже могут относиться).
1) оказывается что запрос "SELECT * FROM table" работает медленней чем "SELECT column_1, column_2, column_3, column_N FROM table" из-за, как вы уже наверно догадались, той всеми любимой "*", т.к. запросу требуется больше времени на обработку таблицы для построения ее схемы и выявление полей в ней... но отказаться от "*" я не могу лень все поля писать ))
Никогда в жизни подобного не слышал, и не похоже что эта информация имеет какое-то отношение к действительности.
Рассмативаем 2 запроса
select * from categories
select ID, Name from categories
При проверке оба запроса имеют одинаковый план выполнения и одинаковый Cost (0.203201 на таблице из 30 000 строк).
Не спорю, некоторое время потребуется на то чтоб считать список столбцов таблицы. Однако если указываем этот список вручную, то все равно СУБД потратит время на то чтоб проверить, существуют ли эти столбцы в БД, и если не существуют, выдать ошибку, считать метаданные столбцов (типы и т.п.).
Не знаком с низкоуровневым механизмом работы с БД, но я уверен что в любом случае эти метаданные скорее всего будут кешироваться в памяти.
Вобщем, совет безперспективный.
Совет 1
С другой стороны - следует относиться к этому * с умом.
Предположим ситуация - таблица статей.
ID, Title, Text, Hits, DateAdd
Нам нужно вывести список последних статей на главную страницу сайта.
Первое что приходит в голову:
SELECT TOP 10 * FROM Articles ORDER BY DateAdd DESC
Конкретно такой таблицы у меня нету, есть подобная, в ней только ID, Title, Text, почти 700 000 строк.
Согласно плану выполнения стоимость - 0.00455
Но поле Text - это поле типа nvarchar(max), оно содержит много данных (десятки-сотни килобайт) и хранится отдельно от самой записи - его получение занимает больше времени, но главное - для нашей задачи оно не нужно. Нам нужны только поля ID (чтоб сформировать ссылку на статью) и Title (чтоб на ссылку поместить текст). Меняем запрос на:
SELECT TOP 10 ID, Title FROM Articles ORDER BY DateAdd DESC
Согалсно плану выполнения стоимость - 0.0334
Получаем примерную 30% экономию дорогих ресурсов, и, соответственно, времени.
Но есть одно НО - SQL Server может находиться на другом компьютере (так часто делают чтоб СУБД и собственно приложение не делили между собой дорогие ресурсы - в данном случае процессор и память, или в случаях когда удаленные клиенты обращаются к централизованой БД). В этом случае лишняя информация из поля Text (десятки-сотни килобайт) будет передаваться не между двумя процесами, а между двумя рядом стоящими компьютерами, или даже двумя компьютерами, стоящих на расстоянии тысяч километров друг от друга). В этом случае разница между этими двумя вариантами будет еще более значительна, так как даже канале 1 МБИТ передача лишних 100 кб в несколько раз увеличит время которое нужно для передачи данных.
Вывод:
Запрашивайте только те столбцы, которые нужны в данный момент и могут понадобиться в ближайший. Это позволит разгрузить дисковую подсистему, процессор, каналы связи.
В наибольшей мере это касается полей ntext, text, nvarchar(max), varchar(max), varbinary, xml.
'подключение
'создание команды ("SELECT column_1, column_2, column_3, column_4, column_5, FROM table"
'создание объекта DataReader (DR)
 o While DA.Read()
int_1 = DA("Column_1"
int_2 = DA("Column_2"
str = DA("Column_3"
date_1 = DA("Column_4"
date_2 = DA("Column_5"
Loop
Код выше работает значительно медленнее чем этот:
'подключение
'создание команды ("SELECT column_1, column_2, column_3, column_4, column_5, FROM table"
'создание объекта DataReader (DR)
 o While DA.Read()
int_1 = DA(0)
int_2 = DA(1)
str = DA(2)
date_1 = DA(3)
date_2 = DA(4)
Loop
> Код выше работает значительно медленнее чем этот:
"значительно медленнее" - это очень громко сказано.
Ни о какой значительной разнице речи быть не может - подключение к серверу базы данных наверняка займет больше времени, чем разница во времени между этими двумя вариантами.
Что происходит в SqlDataReader.Item(string)
Да собственно следующее:
Me.GetValue(Me.GetOrdinal(name))
В Me.GetOrdinal(string) есть некотороый оверхерд, но если посмотреть рефлектором, то видно что суть сводится к выбору индекса столбца из хэш-таблицы.
Можно провести конечно провести тестирование, и посчитать на сколько времени дольше будет выполняться "неоптимальный" вариант, но разница получится очень незначительной - гораздо меньше чем теряется в удобстве написания кода.
Хотя, например, если работаете с кодогенерацией, то данный способ стоит реализовать.
вроде все отлично... SQL запрос правильный, считывание происходит правильно, НО! при считывании DataReader сам определяет тип считываемого значения, а это не есть хорошо )) представьте сколько там лишнего времени тратится? во-во итак все юзаем методы GET они у DataReader есть на все типы вот правильный во всех отношениях код:
'подключение
'конструкция Using обеспечиват 100% закрытие объекта после оператора End Using
'закрытие произойдет даже если внутри конструкции произойдет исключение (ошибка)
Using OleDbCon As New OleDbConnection("Provider=Microsoft.ACE.OLEDB.12.0ata Source=C:\TestAccess.accdb"
OleDbCon.Open()
'создание команды
 im OleDbCom As OleDbCommand = OleDbCon.CreateCommand
'создание SQL запроса
OleDbCom.CommandText = "SELECT column_1, column_2, column_3, column_4, column_5 FROM table"
'создание объекта OleDbDataReader
Using OleDbRe As OleDbDataReader = OleDbCom.ExecuteReader
 im int_1, int_2 As Integer
 im str As String
 im date_1, date_2 As Date
 im Column_1 As Integer = OleDbRe.GetOrdinal("Column_1" 'записываем номер поля в переменную!
 im Column_2 As Integer = OleDbRe.GetOrdinal("Column_2"
 im Column_3 As Integer = OleDbRe.GetOrdinal("Column_3"
 im Column_4 As Integer = OleDbRe.GetOrdinal("Column_4"
 im Column_5 As Integer = OleDbRe.GetOrdinal("Column_5"
 o While OleDbRe.Read()
int_1 = OleDbRe.GetInt32(Column_1)
int_2 = OleDbRe.GetInt32(Column_2)
str = OleDbRe.GetString(Column_3)
date_1 = OleDbRe.GetDateTime(Column_4)
date_2 = OleDbRe.GetDateTime(Column_5)
Loop
End Using
End Using
Опять же, не понятно где экономим.
У GetValue и, скажем, GetString разницы практически никакой, просто GetValue дополнительно проверит какой тип у столбца (эта информация уже закеширвоана, находится в поле) и вызовет нужную функцию - типа SqlBuffer.String.
GetString сразу же вызовет SqlBuffer.String.
При кодогенерации реализовать можно, при ручном кодировании - смысла нет.
Кстати в коде есть недочет.
Dim OleDbCom As OleDbCommand = OleDbCon.CreateCommand
Я классический Web-разработчик, единственная моя особенность - в мои задачи входит написание максимально быстрого кода, т.е. я могу жертвовать удобством и понятностью кода в пользу скорости.
Если вы работаете с огромными хранилищами данных, то некоторые мои советы могут вам не помочь
1. Запрашивайте только те столбцы которые которые нужны в данный момент или могут вот-вот понадобиться (тогда их закешировать на клиенте).
Данные типа ntext, text, varchar(max), nvarchar(max), varbinary, xml запрашивайте только тогда когда они требуются так как их выборка занимает больше времени чем выборка из обычных столбцов.
Во-первых, снизите нагрузку на IO, во-вторых, на память, на каналы связи.
2. Если это возможно, перенесите базу tempdb на отдельный физический диск - при операциях когда эта база данных будет использоваться, сможете выиграть на IO. Например при пересчете индекса большой таблицы - можно получить хороший выигрыш в быстродействии потому что параллельно будут работать два диска - один на котором сама база данных, один на котором tempdb.
Даже если вы ясно не исопльзуете tempdb, она все равно может использвоаться СУБД неявно, как правило, если для выполнения текцщих операций не хватает памяти.
Переносить эту таблицу можно и на RAID0 - никаких важных данных там не хранится.
3. Денормализируйте данные там где это необходимо
Нормализованная база данных - это красиово, это предмет гордости для проектироващика СУБД.
Но не всегда нормализация - это оптимально.
Рассмотрим простейший пример.
Таблица городов
ID Name
Таблица клиентов
ID FirstName LastName CityID
Здесь все просто - у каждого клиента указано, в каком городе он живет.
Однако получается, что для того чтоб посмотреть, в каком собственно городе живет клиент, нужно выполнить еще один запрос в таблицу городов. Возможно, есть смысл денормализовать данные так, чтоб это не требовалось:
Таблица клиентов
ID FirstName LastName CityID CityName
Если у клиента может быть несколько E-mail адресов, и они хранятся в отдельной таблице Emails, возможно, есть смысл хранить список E-mail адресов в таблице клиентов в поле ntext? Это позволит получить нужный список E-mail адресов, не выполняя дополнительный запрос в большую таблицу E-mail'ов.
Сумму заказов клиента тоже можно хранить в таблице клиентов (а не получать каждый раз дополнительным запросом на суммирование по таблице заказов).
И так далее.
Это скорее всего увеличит размер базы данных, и добавит необходимость поддерживать данные постоянно в актуальном состоянии, зато положительно скажется на производительности.
4. Выносите запросы в хранимые процедуры
Для хранимых процедур план выполнения вычисляется и кешируется для использования в последующих запросов (это не относится к запросам, выполняемым через exec).
Кроме того это дает еще ряд преимуществ, связанных с управлением базой данных, например, можно раздавать права на эту процедуру между пользователям, но это уже не относится к быстродействию.
5. Анализируйте планы выполнения.
Чтоб посмотреть план выполнения запроса, в SQL Server Management Studio нужно нажать Ctrl+L или кнопку Display Estimated Execution Plan (рядом с кнопкой Execute).
С помощью этого нехитрого инструмента можно проанализировать как выполняется запрос, где можно его улучшить.
6. Расставляйте индексы
Очень простой способ увеличить производительность при сортировке и фильтрации.
Раузмеется, перед этим стоит проанализировать план выполнения и убедиться, нужен ли вообще индекс, и если нужен то какой.
7. Кешируйте данные
Это очень просто - кешируйте необходимые данные в приложении, и сможете избавиться от частых запросов в БД.
8. Анализируйте производительность
В SQL Server Management Studio 2008 появилась очень интересная функция - отчеты. С ее помощью можно просмотреть, где именно быстродействия не хватает.
Наиболее полезную на мой взгляд информацию дает отчет Performance - Top Queries by Total CPU Time - потому что показывает на какие запросы сервер тратит больше всего времени в абсолютном измерении. Их и нужно оптимизировать в первую очередь. Или, если дальнейшая оптимизация неэфективна, закупать новое оборудование
9. Улучшайте оборудование
Производительные дисковые массивы - для того чтоб быстро считывать данные с диска и записывать их на диск
Много памяти - чтоб SQL Server мог закешировать нужные данные в памяти, а не читать их каждый раз с диска, и тем более чтоб не приходилось для элементарных операций типа сортировки использовать временную базу данных, чтоб временные таблицы можно было держать в памяти.
Быстрый процессор(ы) - для более быстрой обработки данных. Например, на многоядерном компьютере пересчет индекса может выполняться на разных процессорах, что даст ощутимые преимущества.
Не претендую на абсолютную истину - просто поделился собственным опытом.
Неплохо-неплохо..
За денормализацию зачет, очень актуальная тема. Даже я бывает такого нарисую.. столько связанных табличек итп итд, вообщем сплошная красова в DBDesigner, а потом думаю бла да нах оно надо, когда проще в эту же таблицу поле одно добавить
По поводу хранимых процедур. Я не думаю, что современные БД на столько тупые, чтобы не кешировать запросы. Т.е. запрос новый увидили, скомпилировали, и запомнили где в нем могут быть переменные значения. В следующий раз, такой же запрос разбирать не будем, просто подставим новые значения. Помнится ещё из допотопных времен, из статьи про BDB, когда я не много интересовался Delphi. А уж современные БД я думаю точно там как-то читерить умеют. Но всёравно разумеется, время на разбор запроса и сравнения его с тем, что в кеше какое-то должно тратится. Поэтому в любом случае, хранимые процедуры - это хорошо. К тому же, из за строго типизации данных, позволяет избежать SQL инъекции.
Ещё хотелось бы от Brand'a, хотябы пару строк, о кешировани результатов запросов, в SQL Server.
По поводу хранимых процедур. Я не думаю, что современные БД на столько тупые, чтобы не кешировать запросы. Т.е. запрос новый увидили, скомпилировали, и запомнили где в нем могут быть переменные значения. В следующий раз, такой же запрос разбирать не будем, просто подставим новые значения. Помнится ещё из допотопных времен, из статьи про BDB, когда я не много интересовался Delphi. А уж современные БД я думаю точно там как-то читерить умеют. Но всёравно разумеется, время на разбор запроса и сравнения его с тем, что в кеше какое-то должно тратится.
Вполне возможно.
По крайней мере для параметризированых запросов, вероятно, планы выполнения будут кешироваться.
К тому же, из за строго типизации данных, позволяет избежать SQL инъекции.
Ну скажем так, имея современные инструменты, сложно умудриться писать код, подверженый SQL-инъекции
Параметризированые запросы просто не дают возможность это сделать.
Ну и ХП тоже, если, конечно, в ней не используется конкатенция (иногда в этом есть необходимость).
Ещё хотелось бы от Brand'a, хотябы пару строк, о кешировани результатов запросов, в SQL Server.
Что именно интерисует?
Если с технической точки зрения кеширование SQL Server'ом данных в памяти, то тут я никакой информацией не располагаю - разве что примерно представляю что для ускорения работы данные могут кешироваться в памяти.
Если вопрос в том как кешировать данные в приложении - тут есть варианты.
Я сейчас применяю кеширования для тех данных которые очень редко изменяются, в Shared-переменных.
Кеширование выполняю при запуске веб-приложения, и в дальнейшем содержимое кеша не обновляется. То есть если вручную внести изменения в БД, то в кеш эти данные поступя только когда перезапустится рабочий процес IIS (по умолчанию, кажется, время жизни ограничено 1 часом). Для моих задач это подходит.
Из стандартных средств кеширования - встроенный кеш ASP .NET, который поддерживает механизм CacheDependency. Это позволяет сделать так, чтоб данные в кеше становились недейсвтительными как только изменятся данные в базе данных, то есть поддерживается полная актуальность данных. Если есть необходимость, можно этот механизм использовать.
Также я использую встроенное кеширование выполнения скриптов, например некоторые часто запрашиваемые страницы кешируются на сервере на несколько минут и при повторном их запросе они выдаются из кеша, а не генерируются заново. Но это уже не имеет отношения к самому серверу баз данных.
Попробую уточнить вопрос, что в пространстве имен System.Data.SqlClient есть для управлением кеша?
Например я хочу явно указать, какие запросы следует кешировать и на какое время. Также в процессе работы приложения, у меня может возникнуть необходимость принудительно обновить кеш.
В приложении кешируются не запросы, а результаты их выполнения. Говоря другим языком ты кладешь в кеш полученный (и, при необходимости, каким то образом подготовленный / измененный / отфильтрованный) набор записей (Table, DataSet & etc). Повлиять на кеширование непосредственно запроса(плана выполнения), имхо, тебе врядли удастся, потому как это разруливается на уровне ядра базы данных