Оптимизация Rails для Oracle

ОГЛАВЛЕНИЕ

Эта статья предназначена для разработчиков, работающих с приложениями Ruby on Rails для баз данных Oracle, причем особое внимание уделяется тем разработчикам, у которых имеются работающие приложения, подлежащие оптимизации. Вы узнаете, какие опции используются для подстройки параметров, отвечающих за подключение адаптера Oracle к базе данных, чтобы увеличить производительность приложения, а также получите полезный дополнительный модуль к программе отслеживания плана выполнения запроса для оперативных приложений. Будет построен простой набор моделей и рассмотрены различные типичные к ним запросы, исследуя которые, можно улучшить их эффективность.

Установка

В качестве предпосылки возможности следования всем указаниям этой статьи является наличие у пользователя продукта Ruby on Rails, установленного и сконфигурированного должным образом для подключения к базе данных Oracle. В материале Казимира Сатерноса (Casimir Saternos) "Ruby on Rails with Oracle FAQ " http://www.oracle.com/technology/pub/articles/saternos-ror-faq.html приводится подробное объяснение этого процесса. (Это довольно просто.)

Все примеры для этой статьи были написаны и протестированы на машине с Windows XP, на которой функционирует Instant Rails 1.5 preview 1 (http://rubyforge.org/frs/download.php/17654/InstantRails-1.5p1-win.zip) , подключенный к базе данных Oracle Database 10g Express Edition (XE) http://www.oracle.com/technology/products/database/xe/index.html , установленной на Debian Linux.

В остальной части этой статьи я предлагаю примеры, основанные на "версии" Rails схемы HR, которая была модифицирована для непосредственного выполнения общепринятых соглашений Rails (в противоположность использованию стандартной схемы HR из приложения Rails, которая была объяснена в написанном Сатерносом документе "HR Schema on Rails" (http://www.oracle.com/technology/pub/articles/saternos-rails.html ). Этот выбор был продиктован желанием показать поведение стандартного приложения Rails, работающего с базой данных Oracle, не отвлекаясь при этом на специфику обработки отдельной схемы для отображения схемы HR в конвенцию Rails. (Что, несомненно, является очень разумным способом обработки унаследованной схемы!)

В файле демонстрационного кода http://www.oracle.com/technology/pub/files/mearelli-optimizing-oracle-rails-sample.zip находятся полный материал для миграции схемы и дамп данных (в формате yml), которые могут быть использованы для загрузки с использованием дополнительного модуля с исправлениями Active Record, который также включен. (Файл db/db_loading.rb выполняет импорт данных и может быть выполнен скриптом или исполняемой программой).

Шесть таблиц заполнены теми же самыми данными, что содержатся в обычной схеме HR, хотя у них имеются модифицированные столбцы. (К примеру, все они теперь используют общепринятую конвенцию Rails о наличии одного столбца-идентификатора (id) в качестве первичного ключа.)

Модели также определяют некоторые отношения между ними; полный код для моделей вы можете увидеть в файле демонстрационного кода:

class Country < ActiveRecord::Base
    belongs_to :region                                                               
    has_many  :locations
end

class Department < ActiveRecord::Base
    belongs_to :manager, :class_name => "Employee", :foreign_key => "manager_id"
    belongs_to :location                                                             
    has_many :employees                                                              
end

class Employee < ActiveRecord::Base
    belongs_to :department                                                           
    belongs_to :manager, :class_name => "Employee", :foreign_key => "manager_id"
    belongs_to :job                                                                  
    belongs_to :department                                                       
    has_many :managed_employees, :class_name => "Employee", :foreign_key => "manager_id"
end
 
class Job < ActiveRecord::Base
  has_many :employees                                                               
end

class Location < ActiveRecord::Base
  belongs_to :country                                                             
  has_many :department 
end

class Region < ActiveRecord::Base
     has_many :country
end

Все наши тесты будут сделаны посредством простого подключения к консоли Rails и выполнения в интерактивном режиме запросов к базе данных через методы моделей, как будто мы делаем их изнутри наших контроллеров:

	   C:\Progetti\ArticoliROR\Oracle\project> ruby script\console Loading development environment. >>


О конфигурации, курсорах и строках

Базовая конфигурация

Итак, вы уже сконфигурировали все необходимые предпосылки для того, чтобы выполнить Rails с базой данных Oracle, и теперь для загрузки вашего приложения Rails достаточно открыть окно ввода (терминал, xterm или любое другое окно) и напечатать:

	   rails myapp 	  

Результатом являетя план (skeleton - скелет) приложения, которое будет хранить код (модели, представления, контроллер и тесты). Вам потребуется настроить файл config/database.yml, указывая базовую информацию инфраструктуры для каждой базы данных, используемой Rails.

Каждый элемент идентифицируется ключом, который говорит системе, на каком этапе должна использоваться конкретная информация: при разработке приложения, во время выполнения тестов или при выполнении приложения в промышленном режиме. Вы можете определить другие подключения базы данных сверх трех стандартных и использовать их из вашего приложения Rails, но эта тема выходит за пределы настоящей статьи.

Наличие отдельных параметров настройки базы данных означает, что можно изменить значения в базе данных разработки или тестовой базы данных и провести тесты и измерения, не затрагивая производственную базу данных.

Вот пример базовой конфигурации:

development:
  adapter: oracle
  host: DEBIAN
  username: rails
  password: rails

Параметр адаптера указывает, что мы подключаемся к базе данных Oracle, и будет выбран OracleAdapter (см.: activerecord/lib/active_record/connection_adapters/oracle_adapter.rb). В прежних выпусках Rails для идентификации адаптера oracle использовалось значение 'oci', и это значение все еще может использоваться.

С помощью параметра host вы указываете базу данных, к которой хотите подключиться. Если вы конфигурировали для вашей базы данных вход TNS:

	   DEBIAN =
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCP)(HOST = 10.211.55.7)(PORT = 1521))
    (CONNECT_DATA =
      (SERVER = DEDICATED)
      (SERVICE_NAME = XE)
    )
  )

то можно использовать для параметра host его имя; иначе следует использовать соглашение об именах Easy Connect (простое подключение), чтобы обратиться к экземпляру Oracle (host: //10.211.55.7:1521/XE).

Имя пользователя и пароль - это тот мандат, который надо использовать для вашего приложения. Все требующиеся приложению объекты (таблицы, последовательности) должны быть доступными для этого пользователя, и все, что будет построено в процессе миграции базы данных, будет встроены в схему этого пользователя (по крайней мере, таково поведение по умолчанию).


Модели и SQL-запросы

Теперь взглянем на некоторые из запросов, которые сгенерированы ActiveRecord, и это сделалось простым благодаря тому, что во время нахождения в режиме разработки система Rails записывает в журнал все запросы, которые она выполняет. Таким образом, можно ознакомиться с поведением инфраструктуры и получить глубокое понимание того, что должно делать инструментальное средство.

Я нахожу, что глубокое понимание, как именно код инфраструктуры собирается транслировать высокоуровневые запросы в фактические операторы SQL, является необходимым шагом при работе с мощными инфрастуктурами, которые выполняют сложные взаимодействия с данными. Вы получите лучшее понимание Rails-идиом, которые должны использоваться, и того, как написать более эффективный код.

Один из самых распространенных способов получения данных моделирования - это поиск объекта по его идентификатору (id):

emp = Employee.find(202)

Он дает запись служащего с id = 202. Просматривая development.log (журнал разработки), мы сможем увидеть выполняемые запросы:

Employee Columns (0.120000)   
 select column_name as name, data_type as sql_type, data_default, nullable,
  decode(data_type, 'NUMBER', data_precision,
  'FLOAT', data_precision,
  'VARCHAR2', data_length,
 null) as limit,
 decode(data_type, 'NUMBER', data_scale, null) as scale
 from all_tab_columns
 where owner = 'RAILS'
 and table_name = 'EMPLOYEES'
 order by column_id

Employee Load (0.070000)  SELECT * FROM employees WHERE (employees.id = 202)

Первый запрос выполняется после того, как будет в первый раз загружен класс моделей, чтобы вытянуть столбцы из таблицы служащего, позволяя системе динамически генерировать необходимые методы для модели, основываясь на структуре базы данных. (Это одна из тех опций, которые делают разработку в среде Rails действительно быстрой!). Имейте в виду, что в процессе выполнения своих действий при эксплуатации сервера в режиме разработки, вы увидите этот запрос многократно повторенным, так как модели перезагружаются при каждом запросе из браузера. Это делает сервер более медлительным, но позволяет получать обновления структур базы данных без перезапуска сервера.

Второй запрос получает из таблицы служащих данные для служащего, id которого равен 202.

Другими примерами моделей могут быть следующие (все примеры представляют фактические запросы, выполнявшиеся к базе данных):

Для заданного служащего получите менеджера его отдела.

mgr = emp.department.manager 
SELECT * FROM departments WHERE (departments.id = 20)
SELECT * FROM employees WHERE (employees.id = 201)

Для заданного служащего получите всех его коллег из того же самого отдела.

emps = emp.department.employees 
SELECT * FROM employees WHERE (employees.department_id = 20)
mgr.managed_employees
SELECT * FROM employees WHERE (employees.manager_id = 201)

Вы можете использовать опцию include, чтобы в одном запросе получить от сервера объекты, которые имеют некоторые отношения с нашим объектом:

emp = Employee.find(202, :include=>[:department,:manager])
emp.department
emp.manager

SELECT employees.id AS t0_r0, employees.commission AS t0_r1, employees.job_id AS t0_r2,
employees.manager_id AS t0_r3,employees.salary AS t0_r4, employees.hire_date AS t0_r5,
employees.phone_number AS t0_r6, employees.department_id AS t0_r7,
employees.first_name AS t0_r8, employees.last_name AS t0_r9, employees.email AS t0_r10,
departments.id AS t1_r0, departments.name AS t1_r1, departments.manager_id AS t1_r2,
departments.location_id AS t1_r3, managers_employees.id AS t2_r0,
managers_employees.commission AS t2_r1, managers_employees.job_id AS t2_r2,
managers_employees.manager_id AS t2_r3, managers_employees.salary AS t2_r4,
managers_employees.hire_date AS t2_r5, managers_employees.phone_number AS t2_r6,
managers_employees.department_id AS t2_r7, managers_employees.first_name AS t2_r8,
managers_employees.last_name AS t2_r9,
managers_employees.email AS t2_r10 FROM employees LEFT OUTER JOIN departments
  ON departments.id = employees.department_id LEFT OUTER JOIN employees managers_employees
  ON managers_employees.id = employees.manager_id WHERE (employees.id = 202) 

Это действительно удобно и эффективно при работе с объектами, ведущими себя как контейнеры для наборов других элементов, поскольку позволяет не обращаться к базе данных с запросом для каждой строки, с которой вы желаете манипулировать.

Вот еще один пример:

job = Job.find(id,:include=>:employees)
job.employees

SELECT jobs.id AS t0_r0, jobs.job_title AS t0_r1, jobs.min_salary AS t0_r2,
jobs.max_salary AS t0_r3, employees.id AS t1_r0, employees.commission AS t1_r1,
employees.job_id AS t1_r2, employees.manager_id AS t1_r3, employees.salary AS t1_r4,
employees.hire_date AS t1_r5, employees.phone_number AS t1_r6,
employees.department_id AS t1_r7, employees.first_name AS t1_r8,
employees.last_name AS t1_r9, employees.email AS t1_r10 FROM jobs LEFT OUTER JOIN employees
  ON employees.job_id = jobs.id WHERE (jobs.id = 7)

Одна из целей этой статьи состоит в том, чтобы показать новые параметры конфигурирования, впервые появившиеся в выпуске 1.2 системы Rails, которые могут помочь в настройке производительности вашего приложения, там где это связано с базой данных. Но прежде чем углубиться в эти вопросы, позвольте мне объяснить, как в настоящее время Rails строит фактически используемый код SQL.


Переменные связывания и совместное использование курсора

Для создания запросов "на лету" в ActiveRecord используются ее (динамически построенные) знания о моделях и базах данных. В ней таким образом строятся даже многие методы моделей, что позволяет разработчикам писать вещи вроде:

Employee.find_by_first_name_and_last_name('Steven','King')

Но чтобы сделать так, ActiveRecord строит SQL-запрос в виде строки и проводит вставку параметров перед посылкой запроса в адаптер для выполнения (не используя переменных связывания). Проблемы SQL-инжекции (SQL injection) и безопасности предотвращаются на уровне адаптера, для чего значения SQL-вставок заключаются в кавычки, но при этом наблюдается снижение производительности и оказывается воздействие на масштабируемость любого приложения Rails, выполняющегося в среде Oracle. Чтобы понимать, как это воздействует на производительность и масштабируемость, позвольте мне объяснить, что происходит при выполнении запроса.

При первом выполнении запроса Oracle выполняет шаг полного разбора, на котором проверяет, является ли запрос синтаксически корректным, и все ли объекты (таблицы, столбцы...), к которым производятся обращения в запросе, действительно существуют. Если проверки проходят успешно, создается новый элемент библиотечного кэша, содержащий результаты синтаксического анализа, которые могут быть многократно использованы для последующего выполнения. Ключом библиотечного кэша является непосредственно текст SQL-запроса, поэтому два запроса, у которых имеются любые литеральные различия, для целей анализа считаются различными.

Когда выполняется новый запрос, в библиотечном кэше производится просмотр с хешированием, чтобы увидеть, проводился ли ранее анализ запроса. Если запрос найден, то используется кэшированный результат разбора и удается избежать шага полного разбора, более того, к библиотечному кэшу не добавляются никакие новые элементы. Способ минимизации числа выполнений полного разбора состоит в том, чтобы всегда использовать параметры связывания.

В контексте приложений Rails версий, предшествующих 1.2, где отсутствовала какая бы то ни было специфическая настройка базы данных, это означает, что каждый раз выполняется другой запрос (даже если он отличается только значением параметра), для оператора механизмом базы данных выполняется полный разбор и в библиотечный кэш добавляется элемент для нового запроса.

Вы можете проконтролировать происходящее, рассмотрев представление v$sql, где в столбце sql_text хранится фактический оператор SQL, используемый для запросов: каждая строка в v$sql соответствует элементу библиотечного кэша. Мы можем использовать следующий запрос, чтобы получить все запросы, сгенерированные нашим приложением (ограничив себя запросами, которые касаются одной из этих 6 таблиц):

select sql_text from v$sql 
where (    lower(sql_text) like '%employee%'
        or lower(sql_text) like '%countries%'
        or lower(sql_text) like '%departments%'
        or lower(sql_text) like '%jobs%'
        or lower(sql_text) like '%locations%'
        or lower(sql_text) like '%regions%'
      )
and   not lower(sql_text) like '%v$sql%'
order by sql_text
If we do the following:
(1..200).each do /id/
    Employee.find(id) rescue nil
end

который пытается получить каждого employee (служащего) с id в диапазоне от 1 до 200. В конечном счете, в v$sql будет занесено следующее:

SQL_TEXT
--------------------------------------------------------------------------------
SELECT * FROM employees WHERE (employees.id = 1)
SELECT * FROM employees WHERE (employees.id = 10)
SELECT * FROM employees WHERE (employees.id = 100)
SELECT * FROM employees WHERE (employees.id = 101)
...

Подобное поведение абсолютно не годится для приложения, которое должно быть масштабируемым, особенно для общих моделей использования, где вы можете ожидать большого количества запросов, использующих первичный ключ (например, обращение к таблице пользователей при входе в систему или для получения данных конфигурирования).

Полным ходом ведется работа, имеющая своей целью включить в Rails надлежащую обработку переменных связывания, или, по крайней мере, позволить адаптерам самой ActiveRecord выполнять связывание параметров в пределах запроса. А пока наилучшим вариантом представляется использование возможностей обработки перезаписи запросов, встроенных в базу данных.

Здесь приходит на выручку определенная конфигурация, предлагаемая Oracle. Параметр базы данных CURSOR_SHARING изменяет способ поведения базы данных при выполнении полных и частичных разборов при наличии запросов, в которых не используются параметры связывания. Параметру может быть назначено одно из значений exact , similar или force (последнее может быть установлено для всей базы данных или для определенного сеанса).

  • exact - значение по умолчанию, когда база данных ведет себя так, как было описано выше.
  • force - указывает базе данных, что необходимо переписывать все запросы, заменяя литералы в тексте SQL на параметры связывания (например: "SYS_B_0"), что приводит к одному анализируемому входу для каждого запроса.
  • similar - переписывает запрос, но при этом оценивает эффект такого переписывания и генерирует различные входы в библиотечном кэше для запросов, которые закончились бы использованием различных планов выполнения - то есть, если замена литерала на параметр связывания приводит к другому плану выполнения, то замена не будет сделана.

До появления Rails 1.2, чтобы изменить установку совместного использования курсора, нужно было изменить ее для всего экземпляра базы данных или внести исправление в адаптер ActiveRecord. Начиная с последнего главного выпуска, был добавлен параметр конфигурирования адаптера, который делает именно это.

Установка cursor_sharing может быть использована в файле database.yml, чтобы выбрать предпочтительное значение, не смешивая его с глобальными параметрами базы данных (и таким образом сделать приложение лучшим "гражданином" в гетерогенной среде, где другим приложениям могут потребоваться другие параметры настройки).

development:
  adapter: oracle
  host: DEBIAN
  username: rails
  password: rails
  cursor_sharing: similar

Его значение используется прямо в операторе alter session после установления подключения (в OracleConnectionFactory.new_connection):

conn.exec "alter session set cursor_sharing = #{cursor_sharing}" rescue nil

Более того, значение по умолчанию было выбрано равным similar , что означает, что даже в том случае, если к параметрам не добавлено cursor_sharing, вы получите разумное поведение системы: запросы будут изменены согласно плану, который может использоваться в зависимости от значений.

Если вы повторите (redo) приведенный выше запрос, то вы увидите это в v$sql:

SELECT * FROM employees WHERE (employees.id = :"SYS_B_0")

всего один вход в библиотечном кэше для любого вызова Employee.find(id).

Чтобы видеть последствия similar в сравнении с force , обновите таблицу, чтобы получить некоторое искажение данных:

Employee.find(:all).each do /emp/ 
    emp.salary = 3000 unless emp.id == 100
    emp.save
end

Вы обновляете все записи, чтобы сделать зарплату равной 3 000$ за исключением того служащего, id которого равняется 100. Теперь можно изнутри проекта Rails добавить индекс к таблице служащих, генерируя миграцию:

C:\Progetti\ArticoliROR\Oracle\project>ruby script/generate migration AddIndexes
      exists  db/migrate
      create  db/migrate/007_add_indexes.rb
В сгенерированном файле вы должны получить
class AddIndexes < ActiveRecord::Migration
  def self.up
    add_index :employees, :salary, :name=>:idx_emp_salary
   end

  def self.down
    remove_index :employees, :name=>:idx_emp_salary
  end
end

после чего из командной строки мы делаем расчистку (rake) db:migrate. Теперь, если мы сделаем:

Employee.find(:all, :conditions=>["salary = ?", 24000]) 
Employee.find(:all, :conditions=>["salary = ?", 3000])

то в v$sql находятся два проанализированных (разобранных) входа:

SELECT * FROM employees WHERE (salary = :"SYS_B_0") 
SELECT * FROM employees WHERE (salary = :"SYS_B_0")

Это происходит потому, что два сгенерированных запроса используют два различных плана выполнения в зависимости от значения зарплаты. (В первом случае используется индекс, а во втором случае был выбран полный просмотр таблицы, так как именно он является наилучшим планом.) Использование значения force для параметра совместного использования курсоров послужило бы указанием для базы данных, что для обоих запросов следует использовать один и тот же элемент. А это, конечно же, совсем не то, чего мы хотим в этом случае, даже если это означает, что у нас будет иметься только один элемент кэша.

Имея для этого параметра Rails-конфигурацию, давайте настраивать его значение на уровне всего приложение, в зависимости от того, какого вида имеются данные и какой характер использования предвидится для приложения.

Вторым параметром, добавленным в Rails 1.2, который помогает в настройке приложения, является конфигурационный параметр prefetch_rows. Он позволяет устанавливать параметр подключения OCI_ATTR_PREFETCH_ROWS, который указывает количество строк, которые будут предварительно выбраны из базы данных в каждом цикле обращения (round trip). Это дает большое увеличение производительности во всех случаях, когда нужно выбрать много строк.

Типичными моделями являются итерации по набору объектов, например, как в следующем операторе:

Employee.find(:all, :conditions=>"salary < 5000")

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

Значение по умолчанию установлено равным 100. Это значение было идентифицировано организациями, принимавшими участие в разработке адаптера Oracle, как идеальное для диапазона совокупности тестовых данных. Но оно больше, чем должно быть значение числа предварительно выбранных строк для cursor_sharing, определенное путем эталонного тестирования с реальными наборами данных и запросами, используемыми в приложении. Для заказной настройки этот параметр, как обычно, устанавливается в файле database.yml:

development:
  adapter: oracle
  host: DEBIAN
  username: rails
  password: rails
  prefetch_rows: 100


Планы выполнения и индексы

Чего не может сделать Rails

Хотя Rails и прошла большой путь в направлении сокращения бремени разработки, благодаря умному использованию соглашений и реализации шаблона проекта, есть ситуации, где у нее не хватает знаний, необходимых для принятия решения разработчиком. Таким образом, Rails-разработчик не должен забывать, что он (или она) строит приложение, которое будет поддерживаться в принципе любой базой данных.

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

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

  • того, какая именно база данных используется (поскольку в ней могут иметься или не иметься некоторые возможности, когда заходит речь о создании и использовании индексов)
  • того, какие данные присутствуют в таблицах (поскольку они влияют на план выполнения запроса и на то, какие индексы используются)
  • конкретного приложения (поскольку фактически используемые запросы могут получать доступ к данным непредсказуемыми способами, основанными на моделях использования)

Кроме того, эти факторы влияют друг на друга, и любая оптимизация будет зависеть всех от их всех вместе. В своем сообщении в блог "Indexing for DB Performance (Индексирование для обеспечения производительности DB") http://weblog.jamisbuck.org/2006/10/23/indexing-for-db-performance, Джеймис Бак (Jamis Buck) дает некоторые подсказки о типах необходимых индексов:

  • внешние ключи (has_one (один к одному), belongs_to (принадлежит к), has_many (один ко многим) и др. отношения)
  • неиндексированные поля, к которым часто делаются запросы (например, имена входящих в систему)
  • столбцы, по которым часто выполняются сортировки или делаются вычисления
  • наборы столбцов, которые совместно используются как условия

Ведение каждого индекса связано с накладными расходами, поскольку индекс необходимо обновляется при вставке, [удалении] и модификании данных, поэтому не надо строить индексы для каждого столбца (или их комбинаций). Скорее, вы должны сделать обоснованный выбор, начав с некоторого разумного набора индексов, изменяя его по мере развития приложения с течением времени и принимая во внимание фактические данные и модели их использования.

План выполнения. В контексте Oracle это означает использование команды explain plan, чтобы понять, как выполняются запросы. В результате применения этой команды у разработчика появляется способ непосредственно оптимизировать запрос и, в конечном счете, добавлять индексы.

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

В Rails значительно облегчено использование пользовательских (custom) запросов вместо типовых (regular). Это достигается, благодаря использованию удобного метода ActiveRecord, который называется find_by_sql. Отметьте, однако, что при вызове find_by_sql можно просто передать текст запроса SQL, но вы должны действительно подтвердить, что используется форма замены параметра, передавая вместе с запросом массив и список параметров. Таким способом можно использовать заключенные в кавычки параметры, которые находятся в адаптере базы данных, и быть увереным, что можно использовать реальные переменные связывания в будущем, когда ActiveRecord сможет поддерживать их, без необходимости в дальнейшем изменения кода.

Подключаемая программа Query_Analyzer. При поиске узких мест в приложении важно знать, какие запросы выполняются в данный момент. Здесь могут оказаться действительно полезными журналы Rails, поскольку приложение, которое выполняется в режиме разработки, регистрирует все фактические запросы. Можно собирать и исследовать их один за другим, чтобы выделить запросы и генерировать планы выполнения. Это может превратиться в утомительную работу. Подключаемый программный модуль query_analyzer помогает уменьшить нагрузку, помещая выходные данные оператора explain plan в журнальный файл. По этой ссылке http://www.spazidigitali.com/wp-content/uploads/2006/12/query_analyzer.zip можно получить совместимый с Oracle выпуск; он был первоначально разработан Бобом Силвой (Bob Silva) для MySQL. Для его установки достаточно разархивировать содержимое архива в каталог vendor/plugins вашего проекта Rails.

Чтобы позволить подключаемому программному модулю работать в среде Oracle, пользователю базы данных должно быть предоставлено достаточно много привилегий для доступа к таблицам словаря данных, к которым делаются запросы при загрузке модели. Это можно сделать с помощью следующего оператора:

GRANT SELECT ANY DICTIONARY TO < database user >;

Вспомогательный плагин (the plugin monkey) корректирует адаптер базы данных, чтобы добавить план выполнения для каждого запроса с уровнем регистрации ниже INFO, который выполняется адаптером. План форматируется и печатается на используемом по умолчанию устройстве при работе в отладочном режиме. Эта установка по умолчанию гарантирует, что построение плана не будет выполняться при выполнении приложения в промышленной среде.

Имеются два параметра для изменения поведения по умолчанию; оба они могут быть изменены в файлах установки окружения. Параметр plan_table_name может быть использован для указания имени таблицы плана, и его значение по умолчанию - это стандартное PLAN_TABLE; параметр plan_details может использоваться для указания того, какие именно детали должны быть напечатаны при выполнении оператора explain plan. Возможные значения:

  • BASIC: выводится минимум информации
  • TYPICAL: отображается самая релевантная информация (значение по умолчанию)
  • SERIAL: похож на TYPICAL, но без параллельной информации
  • ALL: выводится вся информация

Если выполнить общий запрос:

Employee.find(101) 

в файл development.log будут выгружены следующие данные:

Employee Load (0.000000)  explain plan for SELECT * FROM employees WHERE (employees.id = 101) 
Analyzing Employee Load
  plan_table_output                                                                 
   -------------------------------------------------------------------------------------------
   Plan hash value: 1171009080                                                       
                                                                                     
   -------------------------------------------------------------------------------------------
   / Id  / Operation                   / Name        / Rows  / Bytes / Cost (%CPU)/ Time     /
   -------------------------------------------------------------------------------------------
   /   0 / SELECT STATEMENT            /             /     1 /   719 /     2   (0)/ 00:00:01 /
   /   1 /  TABLE ACCESS BY INDEX ROWID/ EMPLOYEES   /     1 /   719 /     2   (0)/ 00:00:01 /
   /*  2 /   INDEX UNIQUE SCAN         / SYS_C004026 /     1 /       /     1   (0)/ 00:00:01 /
   -------------------------------------------------------------------------------------------
                                                                                     
   Predicate Information (identified by operation id):                               
   ---------------------------------------------------                               
                                                                                     
      2 - access("EMPLOYEES"."ID"=101)

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

Если оставить плагин активным (установить уровень отладки на informations), это позволит при выполнении приложения собрать данные всех запросов в таблице плана.

Таблица плана выводит много информации о запросах. Вы можете быстро определить таблицы, которым, возможно, требуется новый индекс, если отыщете в журнальном файле строку 'TABLE ACCESS FULL".

Employee Load (0.010000)  explain plan for SELECT * FROM employees WHERE (first_name = 'Stephen') 
Analyzing Employee Load
  plan_table_output                                                              
   -------------------------------------------------------------------------------
   Plan hash value: 1445457117                                                    
                                                                                  
   -------------------------------------------------------------------------------
   / Id  / Operation         / Name      / Rows  / Bytes / Cost (%CPU)/ Time     /
   -------------------------------------------------------------------------------
   /   0 / SELECT STATEMENT  /           /     1 /   719 /     3   (0)/ 00:00:01 /
   /*  1 /  TABLE ACCESS FULL/ EMPLOYEES /     1 /   719 /     3   (0)/ 00:00:01 /
   -------------------------------------------------------------------------------
                                                                                  
   Predicate Information (identified by operation id):                            
   ---------------------------------------------------                            
                                                                                  
      1 - filter("FIRST_NAME"='Stephen')                                          
                                                                                  
   Note                                                                           
-----                                                                          
      - dynamic sampling used for this statement 

Здесь показан результат выполнения запроса, отыскивающего всех служащих, имеющих имя 'Stephen': Employee.find (:all,:conditions => ["first_name =?", "Stephen"]).

Отметим, что этот запрос должен просмотреть всю таблицу служащих (EMPLOYEES), чтобы получить только одну строку, используя фильтр по столбцу first_name. Если при выполнении приложения этот вид запроса встречается много раз, нужно рассмотреть вопрос о добавлении индекса. Информация предиката о более дорогостоящих операциях дает хорошую подсказку, какие индексы могут принести пользу для выполнения запроса - в рассматриваемом случае, очевидно, это мог бы быть индекс по столбцу first_name.

Rails Migrations. Вы можете также использовать Rails Migrations для управления индексами. (См. документацию по API для получения дополнительной информации о том, что такое Migrations, и как их использовать для управления схемой базы данных.), Чтобы добавить индекс, выполните:

add_index table_name, column_names, options = {}

При этом к таблице "table_name" будет добавлен индекс для столбца "column_names". В этом предложении можно задать как отдельный столбец, так и их список:

add_index :departments, :manager_id  add_index :locations, [:city, :postal_code]

В опциях параметра можно указать, должен ли быть индекс уникальным:

add_index :locations, [:city, :postal_code, :street_address], :unique => true

и/или с заданным именем. (Это полезно в среде Oracle, так как в имени индекса по умолчанию должна быть использована комбинация имени таблицы и имени первого столбца, которая может оказаться слишком длинной.)

add_index :locations, [:city, :postal_code], :name => :idx_city_zip

Для удаления индекса просто используйте метод remove_index:

  remove_index :departments, :manager_id
  remove_index :departments, :column => :manager_id
  remove_index :locations, :column => [:city, :postal_code, :street_address]
  remove_index :locations, :name => :idx_city_zip

Если нужно использовать более сложный SQL-оператор или SQL-оператор, определенный для конкретной базы данных, используйте команду execute из метода более высокого или низкого уровня, в которые вы передаете на выполнение строку SQL:

execute "CREATE INDEX idx_emp_first_name ON employees (first_name)"

В приведенном выше примере:

C:\Progetti\ArticoliROR\Oracle\project>ruby script/generate migration AddFirstNameIndex
      exists  db/migrate
      create  db/migrate/008_add_first_name_index.rb

Это - файл миграции:

class AddFirstNameIndex < ActiveRecord::Migration
  def self.up
    add_index :employees, :first_name, :name=>:idx_emp_first_name
   end

  def self.down
    remove_index :employees, :name=>:idx_emp_first_names
  end
end

Выполните миграцию, используя утилиту rake db:migrate.

C:\Progetti\ArticoliROR\Oracle\project>rake db:migrate
(in C:/Progetti/ArticoliROR/Oracle/project)
== AddFirstNameIndex: migrating ===============================================
-- add_index(:employees, :first_name, {:name=>:idx_emp_first_name})
   -> 0.0100s
== AddFirstNameIndex: migrated (0.0100s) ======================================

Повторное выполнение запроса показывает, что этот индекс теперь используется:

Employee Load (0.010000)  explain plan for SELECT * FROM employees WHERE (first_name = 'Stephen') 
Analyzing Employee Load
  plan_table_output                                                                 
   --------------------------------------------------------------------------------------------------
   Plan hash value: 2736374945                                                      
                                                                                    
   --------------------------------------------------------------------------------------------------
   / Id  / Operation                   / Name               / Rows  / Bytes / Cost (%CPU)/ Time     /
   --------------------------------------------------------------------------------------------------
   /   0 / SELECT STATEMENT            /                    /     1 /   719 /     2   (0)/ 00:00:01 /
   /   1 /  TABLE ACCESS BY INDEX ROWID/ EMPLOYEES          /     1 /   719 /     2   (0)/ 00:00:01 /
   /*  2 /   INDEX RANGE SCAN          / IDX_EMP_FIRST_NAME /     1 /       /     1   (0)/ 00:00:01 /
   --------------------------------------------------------------------------------------------------
                                                                                     
   Predicate Information (identified by operation id):                               
   ---------------------------------------------------                               
                                                                                     
      2 - access("FIRST_NAME"='Stephen')                                             
                                                                                     
   Note                                                                              
   -----                                                                             
      - dynamic sampling used for this statement  

Как вы видели, действительно, удобно использовать подключаемый программный модуль, чтобы можно было "засунуть свой нос" в планы выполнения, используемые выполняющейся прикладной программой, и вы можете даже оставить изменения, инсталлированными на промышленном сервере, и включать/отключать их в случае необходимости, просто изменяя уровень регистрации.

Заключение

В этой статье вы познакомились с тем, как в приложении Rails конфигурируются подключения к базе данных Oracle. Было показано, как в выпуске 1.2 для обеспечения большей эффективности была обновлена структура, в которой теперь используются параметры cursor_sharing и prefetch_rows (до тех пор, пока не появится ожидаемая настоящая реализация переменных связывания).

Кроме того, были исследованы команды миграции Rails, которые релевантны для настройки базы данных (особенно, команды создания/удаления индексов).

И, наконец, в показанр, что для получения высокой эффективности необходимо понимать, как Rails из написанного кода строит SQL, как база данных затем выполняет эти операторы.