Когда возможностей
ActiveRecord недостаточно
Sequel: ORM для ценителей Postgres
О чем пойдет речь
Почему возможностей AR может не хватать
Основные отличия Sequel
Составные типы данных (hstore, jsonb, array)
Использование специальных функций Postgres и
примеры
Есть же AR! У нас все отлично работает!
Слабая поддержка:
Составных типов данных
CTE
Join, group
Распределенных БД
Подзапросов
Master Slave
Операций на стороне бд
Sequel
Sequel это [паттерн] ActiveRecord
Во многом похож на AR
Превосходит AR по функциональности
Поддерживает большее число СУБД
Поддерживает ree 1.8, jruby
Имеет модульную архитектуру
Поддерживает то, что написано слева
Где работает плохо и почему
Model.select(“COALESCE(ABS(SUM(amount * (courses ->
users.currency)::numeric)), 0)”)
SELECT COALESCE(ABS(SUM(amount * (courses ->
users.currency)::numeric)), 0) FROM "models"
Model.select { coalesce(abs(sum(:amount *
:courses.hstore[:users__currency].cast(:numeric))), 0) }
SELECT coalesce(abs(sum(("amount" * CAST(("courses" ->
"users"."currency") AS numeric)))), 0) FROM "models"
ActiveRecord vs Sequel (функциональность)
1. pg_loose_count
2. pg_static_cache_updater
3. single_table_inheritance
4. timestamps
5. association_proxies
6. optimistic_locking
Feature AR Sequel
Where OR 2016 2008
Left joins 2015 2008
Concurrently indexing 2013(5) 2012
Bulk loading 2010 2009
where.not 2012 2007
rewhere 2013 2008
Arel
users = Arel::Table.new(:users) # Users.arel_table
orders = Arel::Table.new(:orders)
User.find_by_sql(users.project(Arel.star).join(orders,
Arel::Nodes::OuterJoin).on(users[:id].eq(orders[:user_id])))
User.left_join(:orders, user_id: :id).all
User.find_by_sql(users.project(Arel.star).where(
orders.where(users[:id].eq(orders[:user_id])).exists.not ))
User.exclude { exists(DB[:orders].where(user_id: users__id)) }.all
Arel сложные условия и оконные функции
users.project(Arel.star).where(users[:id].eq(23).or(users[:status_id].gteq(42)
)).to_sql
User.where(id: 23).or(User.where("status_id >= ?", 42)).to_sql
User.where { { id: 23 } | (status_id >= 42) }.sql
SELECT * FROM "users" WHERE (("id" = 23) OR ("status_id" >= 42))
users.project([users[:id].sum.over(Arel::Nodes::Window.new.partition(users[:st
atus_id])), users[:id], users[:status_id]]).to_sql
User.select{ [sum(id).over(partition: :users__status_id), status_id] }.sql
SELECT sum("id") OVER (PARTITION BY "users"."status_id"), "status_id" FROM "users"
Sequel::Model & Sequel::Dataset
class Model < Sequel::Model(:models)
many_to_one :category
subset(:expensive){ price > 500000 }
dataset_module do
def by_category(category_id)
where(category_id: category_id)
end
end
End
Model.by_category(1).all
Model.expensive.all
Model.qualify
.join(:categories, id: :category_id)
.select_append(
:categories__title___cat_title
)
.group(:models__id)
.group_append(:categories__id)
SELECT "models".*,
"categories"."title" AS
"cat_title"
FROM "models"
INNER JOIN "categories" ON
("categories"."id" =
"models"."category_id")
GROUP BY "models"."id",
"categories"."id"
Массивы
Model.where{ :values.pg_array.contains([4, 5]) }
SELECT * FROM "models" WHERE ("values" @> ARRAY[4,5])
Model.select{
[:array1.pg_array.concat(array2).overlaps(:array3).as(:result
), :id] }.all
SELECT (("array1" || "array2") && "array3") AS "result", "id"
FROM "models"
ia.contains(:other_int_array_column) # @>
ia.contained_by(:other_int_array_column) # <@
ia.overlaps(:other_int_array_column) # &&
ia.concat(:other_int_array_column) # ||
ia.push(1) # int_array_column || 1
ia.unshift(1) # 1 || int_array_column
Hstore
Model.select{ :properties.hstore[:enabled] }
SELECT ("properties" -> "enabled") FROM "models"
Model.select{ :properties.hstore.key?(:enabled) }
SELECT ("properties" ? "enabled") FROM "models"
Model.where( :properties.hstore.key?(:enabled))
SELECT * FROM "models" WHERE ("properties" ? "enabled")
DB[:tab].update(:h=>Sequel.hstore_op(:h).concat('c'=>3))
UPDATE "tab" SET "h" = ("h" || '"c"=>"3"'::hstore)
JSON(B) вложенные структуры
DB[:jsonb].insert(jsonb: '{"array":[1], "object": {}}' )
DB[:jsonb].where( :prefs.pg_jsonb.contains('{"object":{}}') &
{ :prefs.pg_jsonb[['array', '0']] => '1' } ).all
SELECT * FROM "jsonb" WHERE (("prefs" @> '{"object":{}}') AND
(("prefs" #> ARRAY['array','0']) = '1'))
DB[:jsonb].update(prefs: :prefs.pg_jsonb.set(['array', '1'],
'5'))
UPDATE "jsonb" SET "prefs" = jsonb_set("prefs",
ARRAY['array','1'], '5', true)
Использование вложенных типов данных
DB.create_table(:address, temp: true) { String :street; String :city; }
CREATE TEMPORARY TABLE "address" ("street" text, "city" text)
DB.create_table(:test, temp: true) { primary_key :id; address :address; }
CREATE TEMPORARY TABLE "test" ("id" serial PRIMARY KEY, "address" address)
DB[:test].insert(:address=>DB.row_type(:address, street: '123 Sesame St.',
city: 'Some City'))
INSERT INTO "test" ("address") VALUES (ROW('123 Sesame St.', 'Some
City')::"address") RETURNING "id"
DB[:test].first[:address]
=> {:street=>"123 Sesame St.", :city=>"Some City"}
Полнотекстовый поиск
DB.alter_table(:texts) {add_full_text_index([:text],
language: 'english')
CREATE INDEX "texts_text_index" ON "texts" USING gin
(to_tsvector('english'::regconfig, (COALESCE("text", ''))))
DB[:texts].full_text_search(:text, %w[match tes:*], language:
'english')
SELECT * FROM "texts" WHERE (to_tsvector(CAST('english' AS
regconfig), (COALESCE("text", ''))) @@
to_tsquery(CAST('english' AS regconfig), 'match | tes:*'))
[{:id=>1, :text=>"test test"}, {:id=>3, :text=>"exact match"}]
CTE
Перемещение данных в другую
таблицу
WITH deleted_items AS (
DELETE FROM #{table}
WHERE #{where}
RETURNING *
)
INSERT INTO #{table}_archive
SELECT * FROM deleted_items
admins = User.where(role: 'admin')
User.with(:admins,
admins).from(:admins)
.where(login: /a/)
WITH "admins" AS (SELECT * FROM
"users" WHERE ("role" = 'admin'))
SELECT * FROM "admins" WHERE ("login"
~ 'a')
DB[:"#{table}_archive"].with(:deleted_items,
DB[table].returning.with_sql(:delete_sql)).insert(DB[:deleted_items])
WITH "d" AS (DELETE FROM "users" RETURNING *) INSERT INTO "users_archive" SELECT * FROM
"d" RETURNING NULL
Recursive CTE
DB[:t].with_recursive(:t,
DB.select(Sequel.lit('1').as(:val)),
DB[:t].select(:t__val +
1).where(Sequel.expr(:t__val) < 10),
:args=>[:val]).all
WITH recursive "t"("val") AS (
SELECT 1 AS "val"
UNION ALL (
SELECT ("t"."val" + 1)
FROM "t"
WHERE (
"t"."val" < 10
)
)
)
SELECT *
FROM "t"
Полезные RCTE
Инициализация
db[table].where(id: [db[table].min(key),
db[table].max(key)]).select(
sequel_function(:min, key),
sequel_function(:max, key),
as(:target.cast(pg_type), :target_col),
as(:target.cast(pg_type), :value_col)
).group(:target_col)
Тело рекурсии
db[:t].join(table, Sequel.qualify(table, :id)=> mid)
.where(expr(:max) >= :min)
.select(
as(Sequel.case(
{ (:t__target_col >= Sequel.qualify(table, column)) => mid },
:min), :min),
as(Sequel.case(
{ (:t__target_col < Sequel.qualify(table, column)) => mid },
:max), :max),
:target_col,
as(Sequel.qualify(table, column), :value_col)
)
def mid
(:min + :max) / 2
end
RCTE binary search (Окончание)
def find_nearest(table, column, target, options={})
key = options.fetch(:key, :id)
pg_type = options.fetch(:pg_type, :text)
db.dataset.with_recursive(:t,
initial_query(table, key, target, pg_type),
recursive_query(table, column),
union_all: false
).from(:t)
end
Деревья
Node.first.descendants
WITH RECURSIVE "t" AS (SELECT * FROM "tree" WHERE
("parent_id" = 1) UNION ALL (SELECT "tree".* FROM "tree"
INNER JOIN "t" ON ("t"."id" = "tree"."parent_id"))) SELECT *
FROM "t" AS "tree"
=> [#<Node @values={:id=>2, :parent_id=>1}>,
#<Node @values={:id=>4, :parent_id=>1}>,
#<Node @values={:id=>3, :parent_id=>2}>,
#<Node @values={:id=>5, :parent_id=>4}>]
Insert conflict & rollup
DB[:texts].insert_conflict(:target=>:id, :update=>{:text=>:excluded__text}).insert(id: 2,
text: 'exact match updated')
INSERT INTO "texts" ("id", "text") VALUES (2, 'exact match updated') ON CONFLICT ("id") DO
UPDATE SET "text" = "excluded"."text" RETURNING "id"
User.select { [role, sum(balance).cast(:integer)] }.group(:role).group_rollup.all
SELECT "role", CAST(sum("balance") AS integer) FROM "users" GROUP BY ROLLUP("role")
=> [#<User @values={:role=>"admin", :sum=>210933700}>,
#<User @values={:role=>"client", :sum=>344305}>,
#<User @values={:role=>nil, :sum=>211278005}>]
DO statements & copy
DB.do(:language=>:plpgsql) <<-SQL
BEGIN
raise 'exception';
END;
SQL
DB.create_function(:set_updated_at, “
BEGIN
NEW.updated_at := CURRENT_TIMESTAMP;
RETURN NEW;
END;", :language=>:plpgsql,
:returns=>:trigger)
DB.copy_table(DB[:jsonb], :format=>:csv)
COPY (SELECT * FROM "jsonb") TO STDOUT
(FORMAT csv)
=> 1,"{""array"": [7, ""4""],
""object"": {}}"
2,"{""array"": [7, ""4""], ""object"":
{}}"
Шардинг и master/slave
DB=Sequel.connect('postgres://master_server/database',
:servers=>{:read_only=>{:host=>'slave_server'}})
# Force the SELECT to run on the master
DB[:users].server(:default).all
# Force the SELECT to run on the read-only slave
DB[:users].server(:read_only).all
DB.transaction(:prepare=>'some_transaction_id_string') do
DB[:foo].insert(1) # INSERT
end
DB.commit_prepared_transaction('some_transaction_id_string') # or
DB.rollback_prepared_transaction('some_transaction_id_string')
Итоги
Вопросы? Q&A!
Ссылка о производительности

Когда возможностей Active record недостаточно

  • 1.
  • 2.
    О чем пойдетречь Почему возможностей AR может не хватать Основные отличия Sequel Составные типы данных (hstore, jsonb, array) Использование специальных функций Postgres и примеры
  • 3.
    Есть же AR!У нас все отлично работает! Слабая поддержка: Составных типов данных CTE Join, group Распределенных БД Подзапросов Master Slave Операций на стороне бд Sequel Sequel это [паттерн] ActiveRecord Во многом похож на AR Превосходит AR по функциональности Поддерживает большее число СУБД Поддерживает ree 1.8, jruby Имеет модульную архитектуру Поддерживает то, что написано слева
  • 4.
    Где работает плохои почему Model.select(“COALESCE(ABS(SUM(amount * (courses -> users.currency)::numeric)), 0)”) SELECT COALESCE(ABS(SUM(amount * (courses -> users.currency)::numeric)), 0) FROM "models" Model.select { coalesce(abs(sum(:amount * :courses.hstore[:users__currency].cast(:numeric))), 0) } SELECT coalesce(abs(sum(("amount" * CAST(("courses" -> "users"."currency") AS numeric)))), 0) FROM "models"
  • 5.
    ActiveRecord vs Sequel(функциональность) 1. pg_loose_count 2. pg_static_cache_updater 3. single_table_inheritance 4. timestamps 5. association_proxies 6. optimistic_locking Feature AR Sequel Where OR 2016 2008 Left joins 2015 2008 Concurrently indexing 2013(5) 2012 Bulk loading 2010 2009 where.not 2012 2007 rewhere 2013 2008
  • 6.
    Arel users = Arel::Table.new(:users)# Users.arel_table orders = Arel::Table.new(:orders) User.find_by_sql(users.project(Arel.star).join(orders, Arel::Nodes::OuterJoin).on(users[:id].eq(orders[:user_id]))) User.left_join(:orders, user_id: :id).all User.find_by_sql(users.project(Arel.star).where( orders.where(users[:id].eq(orders[:user_id])).exists.not )) User.exclude { exists(DB[:orders].where(user_id: users__id)) }.all
  • 7.
    Arel сложные условияи оконные функции users.project(Arel.star).where(users[:id].eq(23).or(users[:status_id].gteq(42) )).to_sql User.where(id: 23).or(User.where("status_id >= ?", 42)).to_sql User.where { { id: 23 } | (status_id >= 42) }.sql SELECT * FROM "users" WHERE (("id" = 23) OR ("status_id" >= 42)) users.project([users[:id].sum.over(Arel::Nodes::Window.new.partition(users[:st atus_id])), users[:id], users[:status_id]]).to_sql User.select{ [sum(id).over(partition: :users__status_id), status_id] }.sql SELECT sum("id") OVER (PARTITION BY "users"."status_id"), "status_id" FROM "users"
  • 8.
    Sequel::Model & Sequel::Dataset classModel < Sequel::Model(:models) many_to_one :category subset(:expensive){ price > 500000 } dataset_module do def by_category(category_id) where(category_id: category_id) end end End Model.by_category(1).all Model.expensive.all Model.qualify .join(:categories, id: :category_id) .select_append( :categories__title___cat_title ) .group(:models__id) .group_append(:categories__id) SELECT "models".*, "categories"."title" AS "cat_title" FROM "models" INNER JOIN "categories" ON ("categories"."id" = "models"."category_id") GROUP BY "models"."id", "categories"."id"
  • 9.
    Массивы Model.where{ :values.pg_array.contains([4, 5])} SELECT * FROM "models" WHERE ("values" @> ARRAY[4,5]) Model.select{ [:array1.pg_array.concat(array2).overlaps(:array3).as(:result ), :id] }.all SELECT (("array1" || "array2") && "array3") AS "result", "id" FROM "models" ia.contains(:other_int_array_column) # @> ia.contained_by(:other_int_array_column) # <@ ia.overlaps(:other_int_array_column) # && ia.concat(:other_int_array_column) # || ia.push(1) # int_array_column || 1 ia.unshift(1) # 1 || int_array_column
  • 10.
    Hstore Model.select{ :properties.hstore[:enabled] } SELECT("properties" -> "enabled") FROM "models" Model.select{ :properties.hstore.key?(:enabled) } SELECT ("properties" ? "enabled") FROM "models" Model.where( :properties.hstore.key?(:enabled)) SELECT * FROM "models" WHERE ("properties" ? "enabled") DB[:tab].update(:h=>Sequel.hstore_op(:h).concat('c'=>3)) UPDATE "tab" SET "h" = ("h" || '"c"=>"3"'::hstore)
  • 11.
    JSON(B) вложенные структуры DB[:jsonb].insert(jsonb:'{"array":[1], "object": {}}' ) DB[:jsonb].where( :prefs.pg_jsonb.contains('{"object":{}}') & { :prefs.pg_jsonb[['array', '0']] => '1' } ).all SELECT * FROM "jsonb" WHERE (("prefs" @> '{"object":{}}') AND (("prefs" #> ARRAY['array','0']) = '1')) DB[:jsonb].update(prefs: :prefs.pg_jsonb.set(['array', '1'], '5')) UPDATE "jsonb" SET "prefs" = jsonb_set("prefs", ARRAY['array','1'], '5', true)
  • 12.
    Использование вложенных типовданных DB.create_table(:address, temp: true) { String :street; String :city; } CREATE TEMPORARY TABLE "address" ("street" text, "city" text) DB.create_table(:test, temp: true) { primary_key :id; address :address; } CREATE TEMPORARY TABLE "test" ("id" serial PRIMARY KEY, "address" address) DB[:test].insert(:address=>DB.row_type(:address, street: '123 Sesame St.', city: 'Some City')) INSERT INTO "test" ("address") VALUES (ROW('123 Sesame St.', 'Some City')::"address") RETURNING "id" DB[:test].first[:address] => {:street=>"123 Sesame St.", :city=>"Some City"}
  • 13.
    Полнотекстовый поиск DB.alter_table(:texts) {add_full_text_index([:text], language:'english') CREATE INDEX "texts_text_index" ON "texts" USING gin (to_tsvector('english'::regconfig, (COALESCE("text", '')))) DB[:texts].full_text_search(:text, %w[match tes:*], language: 'english') SELECT * FROM "texts" WHERE (to_tsvector(CAST('english' AS regconfig), (COALESCE("text", ''))) @@ to_tsquery(CAST('english' AS regconfig), 'match | tes:*')) [{:id=>1, :text=>"test test"}, {:id=>3, :text=>"exact match"}]
  • 14.
    CTE Перемещение данных вдругую таблицу WITH deleted_items AS ( DELETE FROM #{table} WHERE #{where} RETURNING * ) INSERT INTO #{table}_archive SELECT * FROM deleted_items admins = User.where(role: 'admin') User.with(:admins, admins).from(:admins) .where(login: /a/) WITH "admins" AS (SELECT * FROM "users" WHERE ("role" = 'admin')) SELECT * FROM "admins" WHERE ("login" ~ 'a') DB[:"#{table}_archive"].with(:deleted_items, DB[table].returning.with_sql(:delete_sql)).insert(DB[:deleted_items]) WITH "d" AS (DELETE FROM "users" RETURNING *) INSERT INTO "users_archive" SELECT * FROM "d" RETURNING NULL
  • 15.
    Recursive CTE DB[:t].with_recursive(:t, DB.select(Sequel.lit('1').as(:val)), DB[:t].select(:t__val + 1).where(Sequel.expr(:t__val)< 10), :args=>[:val]).all WITH recursive "t"("val") AS ( SELECT 1 AS "val" UNION ALL ( SELECT ("t"."val" + 1) FROM "t" WHERE ( "t"."val" < 10 ) ) ) SELECT * FROM "t"
  • 16.
    Полезные RCTE Инициализация db[table].where(id: [db[table].min(key), db[table].max(key)]).select( sequel_function(:min,key), sequel_function(:max, key), as(:target.cast(pg_type), :target_col), as(:target.cast(pg_type), :value_col) ).group(:target_col) Тело рекурсии db[:t].join(table, Sequel.qualify(table, :id)=> mid) .where(expr(:max) >= :min) .select( as(Sequel.case( { (:t__target_col >= Sequel.qualify(table, column)) => mid }, :min), :min), as(Sequel.case( { (:t__target_col < Sequel.qualify(table, column)) => mid }, :max), :max), :target_col, as(Sequel.qualify(table, column), :value_col) ) def mid (:min + :max) / 2 end
  • 17.
    RCTE binary search(Окончание) def find_nearest(table, column, target, options={}) key = options.fetch(:key, :id) pg_type = options.fetch(:pg_type, :text) db.dataset.with_recursive(:t, initial_query(table, key, target, pg_type), recursive_query(table, column), union_all: false ).from(:t) end
  • 18.
    Деревья Node.first.descendants WITH RECURSIVE "t"AS (SELECT * FROM "tree" WHERE ("parent_id" = 1) UNION ALL (SELECT "tree".* FROM "tree" INNER JOIN "t" ON ("t"."id" = "tree"."parent_id"))) SELECT * FROM "t" AS "tree" => [#<Node @values={:id=>2, :parent_id=>1}>, #<Node @values={:id=>4, :parent_id=>1}>, #<Node @values={:id=>3, :parent_id=>2}>, #<Node @values={:id=>5, :parent_id=>4}>]
  • 19.
    Insert conflict &rollup DB[:texts].insert_conflict(:target=>:id, :update=>{:text=>:excluded__text}).insert(id: 2, text: 'exact match updated') INSERT INTO "texts" ("id", "text") VALUES (2, 'exact match updated') ON CONFLICT ("id") DO UPDATE SET "text" = "excluded"."text" RETURNING "id" User.select { [role, sum(balance).cast(:integer)] }.group(:role).group_rollup.all SELECT "role", CAST(sum("balance") AS integer) FROM "users" GROUP BY ROLLUP("role") => [#<User @values={:role=>"admin", :sum=>210933700}>, #<User @values={:role=>"client", :sum=>344305}>, #<User @values={:role=>nil, :sum=>211278005}>]
  • 20.
    DO statements &copy DB.do(:language=>:plpgsql) <<-SQL BEGIN raise 'exception'; END; SQL DB.create_function(:set_updated_at, “ BEGIN NEW.updated_at := CURRENT_TIMESTAMP; RETURN NEW; END;", :language=>:plpgsql, :returns=>:trigger) DB.copy_table(DB[:jsonb], :format=>:csv) COPY (SELECT * FROM "jsonb") TO STDOUT (FORMAT csv) => 1,"{""array"": [7, ""4""], ""object"": {}}" 2,"{""array"": [7, ""4""], ""object"": {}}"
  • 21.
    Шардинг и master/slave DB=Sequel.connect('postgres://master_server/database', :servers=>{:read_only=>{:host=>'slave_server'}}) #Force the SELECT to run on the master DB[:users].server(:default).all # Force the SELECT to run on the read-only slave DB[:users].server(:read_only).all DB.transaction(:prepare=>'some_transaction_id_string') do DB[:foo].insert(1) # INSERT end DB.commit_prepared_transaction('some_transaction_id_string') # or DB.rollback_prepared_transaction('some_transaction_id_string')
  • 22.
    Итоги Вопросы? Q&A! Ссылка опроизводительности

Editor's Notes

  • #2 Добрый день, меня зовут Евгений Луковский. Я ведущий разработчик в компании Амбреллио. Мы специализируемся на разработке и обслуживании высоконагруженных вебсайтов.
  • #3 Итак Первым делом я расскажу, почему нам не хватило возможностей AR Затем мы рассмотрим отличия Sequel Затем я приведу примеры использования сложных составных типов данных с использованием Sequel Это массивы, json(b), hstore и проичие сложные типы данных В самом конце презентации расскажу об использовании нестандартного функционала Postgres. Как Sequel облегчает их использование, здесь речь пойдет о Recursive CTE, listen/notify, полнотекстовом поиске, интервалах и тп
  • #4 Казалось бы, все пользуются АР, зачем учить еще один язык для написания запросов и осваивать еще одну клиентскую библиотеку для RDBMS С одной стороны АР используется в большинстве приложений на руби. Однако, привычный механизм работы позволяет использовать лишь сравнительно малый спектр функций. Если вам в работе требуются массивы или hstore, придется расширять функционал AR так или иначе, а если вы хотите использовать запросы по составным типам данных то вам и вовсе придется вспоминать SQL. Второй пункт, сами по себе CTE не более чем подготовленные вью для последующих запросов, но и тут есть пара применений, которые на первый взгляд могут быть и не очевидны. Так например при архивации вам могут пригодиться CTE, чтобы переместить данные из одной таблицы в другую за одну операциюю. Рекурсивный CTE поддерживается Arel, но кто об этом знает? (Документация почему-то неполная) AR без Arel не предоставляет DSL, который позволил бы запрашивать несколько таблиц одновременно. Недавно был добавлен left_join и or, однако работа со статистикой все равно изобилует SQL фрагментами. AR не предоставляет никаких инструментов, для работы с распреденными субд, такими, например, как использованию двух фазного коммита. (Prepared transactions) AR поддерживает подзапросы, но лишь до тех пор, пока вы запрашиваете по одному полю из подзапроса, Master/Slave - с помощью сторонних расширений. Для ActiveRecord написан Octopus и ряд других плагинов. Я не считаю, что Octopus плох чем-либо. Но он не поддерживается официально, поэтому в случае обновления ActiveRecord или самого Octopus вы можете обрести друзей в обсуждениях issue на гитхабе Если необходимо сделать арифметическую операцию на стороне бд сложнее сложения/вычитания - тоже SQL или Arel. Хорошая клиентская библиотека позволяет вам быть более красноречивым в работы с СУБД. Языковые возможности Postgres весьма широки. Поэтому, если ваша клиентская библиотека будет поддерживать больше функций вы сможете более полно использовать его возможности. Сравнивним AR и Sequel Sequel - использует тот же патерн проектирования, что и AR по Фаулеру Многие методы совпадают, или имеют алиасы похожие на AR. Sequel позволяет использовать множество специальных функций Postgres вплоть до Listen/Notify, do statements. Не уступает по функциональности. Поддерживает 15 адаптеров против 4 или 5 у AR (есть 3-party плагины). Подерживает разные интерпретаторы, вплоть до самых древних. Что может быть плюсом для тех, у кого случилось несчастье и в силу каких-то причин приходится работать с очень старым проектом и для тех, кто вынужден использовать jruby по причинам связанным с оптимизацией производительности, или если вы решили написать гем
  • #5 К примеру у нас есть такой запрос, на AR его невозможно написать иначе чем так, как предложено на слайде, верно? Для того, чтобы работать с данными, которые хранятся в HSTORE требуется множество ухищрений. Например такие простые вопросы как суммирование, выборка по значению и группировка теряют привлекательность. Мы стали писать консерны для моделей, которые бы брали на себя часть этих функций, апи получилось хорошее, но нестандартное, сейчас приходится писать rdoc. А если попытаться посчитать статистику по этим полям, то практически все запросы будут написаны на SQL с вкраплениями Arel. И все прекрасно и в текущем варианте, но зачастую бывает так, что какая то из колонок меняется а оставшаяся часть запроса нет, чаще всего это случается, когда мы считаем статистику по разным столбцам одной и той же группы таблиц. Важно так же помнить о необходимости санитизации данных, подставляемых в SQL-фрагмент. **** Клик! Попробуем переписать на Sequel теперь мы можем убрать любой символ в переменную и переопределить работу этого кода так, как нам нужно. При этом код сохранил читаемость. Убедимся, что все работает так же. Мы запрашиваем сумму произведения amount на каст значения из hstore в numeric. Если какое либо из значений является null используем 0 (полезно если мы используем left join) Отмечу, что в данном примере используется расширение, которое определяет дополнительную функциональсность для символов, массивов и хешей, что не всегда желательно. Этого можно избежать, отключив расширение core_extensions. В этому случае запрос придется записывать в более длинной и менее очевидной форме, заострять на ней внимание не будем, это все есть в документации Чем плохи SQL фрагменты [User.where("id not in (?)", []).count] Иногда может встретиться ситуация, в которой кто-то решил использовать запрос not in с параметром, передаваемым из приложения. Если массив будет пуст, актив рекорд вернет пустую реляцию, а должен был вернуть все записи. И конечно же фрагменты SQL трудно поддаются отладке и являются СУБД зависимым кодом (что важно, если вы действительно планируете писать код, который можно будет использовать где-то еще) Используя SQL фрагмент можно легко позабыть о санитизации переменных в запросе. Злоумышленик может воспользоваться брешью в безопасности, используя SQL иньекцию.
  • #6 На этом слайде слева мы видим функциональность и год, в котором этот функциональность появилась в Ar и в Sequel Where or и left join добавлены в active record 5 Возможность добавления индекса конкуррентно в 13, в 15 году стало возможно и удалять индекс конкурретнтно Загрузка блоками, тут есть различия в идеалогии. AR предоставляет неплохой инструмент для загрузки именно блоками, в ряде моментов это весьма полезный ход, sequel, предоставляет механизм для использования курсоров. Такие вопросы, как обработка блоками решается в этом случае в рамках одной транзакции, что не всегда удобно. При этом автор сиквела предлагает использовать плагин для паджинации, который делает запросы с оффсетами, что тоже плохо, поэтому если вам требуется блочный апдейт без транзакций, то можно написать например такой код (Model.max(:id) / 1000).times do |i| Model.where(id: (i*1000 + 1)..((i+1) * 1000)).update(awesome: true) end В ряде моментов AR обошла sequel Reversible migrations отстает от AR, в частности AR позволяет отменять удаление колонки, но такой фукнционал редко требуется Aborting hooks Mutation detection расширение Enum (kind of) pg_enums Null relation стали доступны после AR При этом адаптер Sequel подерживает множество специфичных для postgres функций Первый плагин будет полезен для ориентировочного подсчета количества строк в больших таблицах Второй плагин обеспечивает сброс кэша с использованием механизма listen/notify однако при использовании pg_bouncer такой прием может не работать, уместно если у вас есть большое количество рельсонод с маленькими пулами, подключенных к одному серверу Single_table_inheritance позволяет использовать привычный механизм наследования разных сущностей в одной таблице Timestamps обеспечивает автоматическое создание и обновление таймстампов в моделях AssociationProxies изменяет характерный для sequelа способ работы с ассоциациями на принятый в ar. То есть при вызове ассоциации вы будете получать дейтасет, а не массив, что несколько удобнее Optimistic_locking Добавляет СУБД механизм для предотвращения измениния уже измененной модели, этот механизм должен быть знаком тем, кто использует его в AR (колонка с версией) Sequel состоит из модулей, часть из них являются плагинами к моделям. Количество модулей весьма велико и позволяет очень тонко настраивать поведение фреймворка
  • #7 Арел является конструктором запросов, ActiveRecord и Arel взаимозависимы. Arel позволяет формировать сложные запросы, которые могут содержать джойны, вызовы функций и математические операции. На слайде мы видим нескольно запросов, которые создают одинаковый SQL. Стоит отметить, что при использовании Arel несколько более многословен и требует подготовки табличных врапперов (в верхней части слайда). Или вы можете конвертировать реляцию в арел скоуп. За комментарием вариант инициализации враппера при использовании arel-helpers (стороннего). Arel требует явного указания запрашиваемых из запроса колонок. На мой взгляд dsl arel является менее понятным, так я предлагаю обратить внимание на джойн в первом запросе, exists.not во втором запросах. Однако, с точки зрения возможностей dsl Sequel и Arel похожи. Arel является лишь конструктором запросов, то есть вам нужно выполнять код, полученный из Arel в execute или where.
  • #8 В верхнем запросе мы видим сложное выражение, включающее в себя два условия, объединенных через логическое или. То есть нам необходимы все записи, удовлетворяющие одному из условий. Первая строка - версия запроса, которая написана на arel Вторая строка - запрос на ActiveRecord Третья строка - запрос на Sequel. Понятен ли запрос на Arel? Можно заметить следущее 1 не похоже на синтаксис ruby 2 получается довольно длинно Запрос на AR без Arel Здесь все выглядит получше, но нам придется использовать текстовый фрагмент. О вреде текстовых фрагментов я уже говорил. Здесь нужно помнить о санитизации и том, что операторы SQL могут иметь различия в разных диалектах этого языка. Arel хуже документирован Так например, оказывается, что в нем реализована поддержка оконных функций. (кто нибудь знал об этом?) users.project([users[:id].sum.over(Arel::Nodes::Window.new.partition(users[:status_id])), users[:id], users[:status_id]]).to_sql Вот если вы замените функцию sum на last_value, к примеру, arel скажет вам, что вы сделали что-то не так. При этом поддержка на стороне секвел является более полной, например в Sequel есть возможность задания алгоритма аггрегации, [ OVER (PARTITION BY "status_id" ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) ] В последнем запросе, я попытался показать, как с помощью арел можно вычислить сумму внутри окна. Как видите, синтаксис Sequel снова оказался лаконичнее и яснее. Видимо, эта функциональность в Arel недокументирована именно ввиду нестабильности. Arel так же формирует главным образом SQL, после чего вы как правило вынуждены использовать find_by_sql, или where, то есть комбинировать использование arel и AR scope не всегда просто.
  • #9 Перейдем к отличиям от AR Dataset похож на реляцию AR. Это базовое представление содержимого базы данных. Может представлять запрос или результат запроса. Результат, возвращаемый дейтасетом нетипизирован, и представляет собой не экземпляр класса модели, а лишь данные из СУБД в виде хэша, иногда многоуровнего. Модель имеет доступ к соответствующему ей датасету через метод dataset. Таким образом, датасет и модель соотносятся как разные уровни абстракции хранимых в СУБД данных. Модель является объектно реляционным маппером построенным поверх датасета. На модели можно вызывать большую часть методов датасета, только возвращают они уже экземпляры класса модели. Любой метод модели можно переопределить и обратиться к предыдущему поведению через super. Достоинство этой схемы в том, что вы можете в любой момент обратиться к любой сущности в вашей субд, и вам не требуется наличие модели. В случае с AR это можно сделать, но вам придется говорить с БД на чистом SQL. Справа представлен запрос. Рассмотрим два метода select_append и group_append (order_append) - они позволяют расширять запрос. Вы можете создать абстрактный датасет и добавлять в него нужные колонки в подклассах или методах. Так например если у вас есть модель, которая должна представлять статистику по времени или категории, вы сможете добавить такое поведение в виде модуля, который позволит доопределить основной запрос нужными группировками и колонками. Если сравнить это с AR. То скорее всего вам придется писать много Arel или SQL. Запрашиваемые колонки (в селекте) придется переопределять полностью или хранить в виде массива, а не добавляя их к базовому запросу, как это можно сделать в Sequel, используя select_append. Sequel может автоматически снабдить колонки именем таблицы перед джойном (qualify). В случае написания запроса на чистом SQL вам придется взять это на себя. Sequel не определяет метод миссинг для модели, это значит, что динамические методы поиска отсутствуют. Дополнительные колонки доступны через квадратные скобки. Синтаксис описания ассоциаций использует отношения в субд, а оперирует терминами принадлежности. C помощью плагина many_through_many возможно описать связь через произвольное число таблиц связи. DB[:items___items_table].select(:items_table__name___item_name) Categories__title___cat_title Символ можно разделить на три компонента. До двойного андерскора мы видим имя таблицы, затем наименование колонки, через тройной андерскор можно указать алиас для данного столбца. Первая и последняя части символа опциональны.
  • #10 Теперь рассмотрим сложные типы данных. AR поддерживает массивы на уровне парсинга и сериализации значений колонок, имеющих тип массива. Sequel в дополнение поддерживает запросы по таким полям и операции над блоками записей. Таким образом, при использовании sequel мы можем не только видеть, какие данные находятся в массиве, являющемся частью запрашиваемой нами записи, но и делать запросы по полям, являющимся частью массива. Здесь используется оператор contains который преобразовывается в оператор @>, обеспечивающий попадание в выборку лишь тех рядов, которые содержат массивы в которых есть и 4 и 5 ***Клик! Здесь мы запрашиваем результат операций объединения и пересечения массивов Тип значения, возвращаемого первой операцией конкатенации массивов - будет массивом, тип значения, возвращаемого второй операцией, будет бинарным выражением sequel. То есть вы можете строить длинные выражения, комбинируя их в своем запросе. ***Клик! Модуль pg_array_ops использовать операции пересечения, включения, объединения внутри запросов. При этом в ряде случаев использование массивов позволяет повысить производительность приложения. Постгрес поддерживает специализированные индексы GIN и GIST которые обеспечивают поддержку указанных операций. Так же сиквел позволяет использовать массивы для хранения ссылок на другие таблицы, позволяя избавиться от таблиц связи. Единственным значимым недостатком является тот факт, что при таком подходе сложнее обеспечить консистентность данных, так как построение FK в таком случае, не будет тривиальной задачей. Эта функциональность выделена в расширения pg_array, pg_array_ops, pg_array_associations соответственно Отмечу так же, что массивы очень удобно применять для категоризации или тегирования объектов. Вместо того, чтобы заводить таблицу связей для тегов, вы можете просто добавить нужные теги в массив. Скорость работы такого решения будет выше, так как запрос будет выполняться по единственному GIN индексу без джойнов
  • #11 Является одним из самых популярных расширений постгрес. AR поддерживат hstore на уровне чтения/модификации отдельных полей составного типа данных. Sequel кроме этого предоставляет инструментарий для формирования запросов по полям hstore и их массовой модификации, что значительно упрощает работу с hstore. Расширение pg_hstore_ops содержит полный набор функций для работы с hstore на стороне субд. Если вы пока не планируете обновление до 9.4 этот тип данных для вас Первый запрос запрашивает значение поля под ключем enabled Второй проверяет наличие этого же ключа Третий выбирает все записи для которых определено значение под ключем enabled Последний обновляет или создает ключ c со значением 3
  • #12 Позволяет хранить структуры данных с теоретически бесконечной вложенностью. Этот тип данных применяется для того, чтобы хранить данные, такие свойства как вложенность и тип которых точно не известны. JSONb позволяет индексировать данные таким же образом, как это было сделано в HSTORE, так как jsonb и есть усовершенствованный hstore. Для работы с JSONb в postgres так же доступно расширение pg_jsonb_ops, которое обеспечивает необходимоый набор функций и операций, для json и jsonb. Итак, мы можем проверять json на наличие любых значений, выполнять запросы по индексам, начиная с постгрес 9.4. Для начала я привел пример вставки значения, над которым мы будем ставить эксперименты Первый запрос получает все ряды, в которых определена указанная структура Второй запрос обновляет содержимое по пути, в json документе. Давайте представим себе запрос на AR, в котором, к примеру можно было бы поменять путь.
  • #13 Постгрес является объектно реляционной субд. Это значит, что в незапамятные времена проблему объектно реляционного импеданса уже решали, и в нашем случае, мы можем использовать эти наработки. Автор Sequel приводил пример использования postgres в рамках сервися heroku. Изюминка ситуации была в том, что сначала постгрес можно было использовать бесплатно, если размер базы не превышал 5МБ, а затем этот порог изменили на 10000 строк. А так как постгрес позволяет хранить в одном ряду составные типы данных или массивы составных типов данных, это открыло широкие ворота для любителей объектно реляционных функций постгрес. ActiveRecord позволяет использовать вложенные типы данных, но все так же с использованием дополнительных плагинов. Предположим, у нас есть бд. Первым делом - создадим тип адрес. Теперь, создаем таблицу, которая содержит колонку с нашим составным типом или массивом такого типа. После этого мы можем использовать полученную конструкцию для работы со вложенными данными ну и heroku бесплатно. DB.create_table(:test, temp: true) { primary_key :id; column :addresses, 'address[]' } DB[:test].insert(:addresses=>Sequel.pg_array([DB.row_type(:address, :street=>'123 Sesame St.', :city=>'Some City')])) Если бы мы использовали массив объектов, то в инсерте можно было бы использовать модуль pg_array Секвел поставляется с плагином к моделям, который позволяет использовать составной тип данных как модель, именно этот прием и позволил построить приложение, укладывающееся в лимит 10000 строк. Как я уже говорил ранее, это применимо в условиях, когда можно поступиться консистентностью, так как проверка консистентности вложенных данных - вопрос менее тривиальный, нежели чем выполнение запросов по таким данным Важно отметить то, что Sequel позволяет производить операции над всеми рассмотренными типами данных. Для чего может быть полезен такой тип. Если у вас есть колонка с деньгами, и разными курсами конвертации, или вовсе одна и та же сумма денег в разных валютах. Вы можете определить тип для такой валюты и создать для нее специальную модель. Такую модель можно будет использовать в разных таблицах. (временные таблицы делает и AR) https://github.com/samv/pg-currency
  • #14 Постгрес предоставляет весьма широкие возможности в области полнотекстового поиска, но в большинстве случаев необходимо записывать все запросы в виде heredoc. Sequel упрощает использование полнотекстового поиска Мы можем добавить индекс в миграции с указанием нужных нам полей и функции ранжирования результатов, функция задается первым аргументом, в нашем случае это одна текстовая колонка Клик!!! Как известно, запросы для поиска выделены в специальный тип данных, называется он tsquery, а набор лексем, полученных из документа имеют тип данных tsvector Далее мы можем использовать созданный ранее индекс в запросах, в запросах можно использовать массивы ключевых слов(plain), или готовые аргументы to_tsquery или to_tsvector (по умолчанию). Можно так же задать порядок по убыванию релевантности(rank) DB[:texts].where(Sequel.lit(["(", " @@ ", ")"], :setweight.sql_function(:to_tsvector.sql_function('english', :text), 'A'), :to_tsquery.sql_function('english', 'tes:*'))).all Это довольно простой запрос, можно написать более сложный запрос используя литеральную запись. В таком случае можно построить запрос любой сложности. Правда в данном случае преимущества использования Sequel несколько меркнут.
  • #15 Sequel позволяет использовать CTE. CTE это по сути вью на время выполнения запроса, они позволяют переписывать запросы в более удобной форме. Может быть полезно в следующих случаях Подготовить промежуточные данные для запроса Произвести группировку по результатам подзапроса или функции Сослаться на промежуточную таблицу несколько раз Клик!! Существует так же одно очень интересное применение CTE - его можно использовать для построения запроса для архивации данных, запрос представлен справа Можно реализовать это таким образом Клик! Этот код можно вынести в модуль и подключить к моделям, таблицы которых подвергаются архивации При этом архивация можно сделать блочной, добавив limit и выполняя запрос циклически, до тех пор, пока все данные не будут перенесены в архив module DeleteDataset def self.extended(obj) obj.class_eval do def_sql_method(self, :select, %w'delete from where returning') alias select_returning_sql insert_returning_sql end obj.instance_eval { def select_delete_sql(sql); sql << Sequel::Dataset::DELETE; end } end end
  • #16 Для начала рассмотрим простейший случай. Как известно рекурсивный CTE состоит из двух частей. Первую часть можно назвать инициализатором рекурсии, а вторую телом рекурсии. При вычислении рекурсивного CTE результат выполнения инициализирующего запроса объединяется с каждым последующим результатом выполнения тела рекурсии. При этом предыдущий результат используется для вычисления последующего, что видно на слайде. Есть небольшой ньюанс: можно использовать для объединения Union и Union All. Union all используется чаще всего и подразумевает, что все без исключения результаты попадут в итоговый результат. А если используется Union в результат попадут лишь уникальные результаты. Рекурсия заканчивается, когда рекурсивный запрос не добавляет новых результатов. Отладку рекурсивного CTE начинают с использования limit. Изображенный на слайде пример заканчивает свое выполнение как только значение val достигнет 10. Это очень простой и бесполезный пример использования RCTE. Рассмотрим более серьезный пример
  • #17 Как-то раз я решил написать RCTE. Задача состояла в том, чтобы быстро находить начало и конец блока в таблице, в которой не было индексов по таймстампам. Эти данные периодически необходимо было архивировать, периодичность этого процесса задавалась, как это часто бывает, в терминах времени. Следовательно необходимо было связать значения первичного ключа и таймстампа. Я вспомнил о бинарном поиске. Бинарный поиск, как известно является рекурсивным алгоритмом. Его можно было бы реализовать и на стороне сервера приложения, но это спровоцировало бы несколько запросов в цикле, что в общем случае нежелательно. Лучшим вариантом представлялось написать RCTE, что я и сделал. Начал я с подготовки инициализирующего запроса. Здесь мы выбираем минимальное и максимальное значения ключевого поля, формируя их из двух записей, снабжаем записи значениями для поиска. В рекурсивной фазе мы вытаскиваем средний ряд и сравниваем целевое значение с ним, если средний ряд содержит значение большее чем целевое, алгоритм повторяется для верхнего интервала. Таким образом мы приходим к ситуации, когда среднее значение перестает меняться и стремится к искомому.
  • #18 Я поместил запросы в методы класса и снабдил их параметрами, которые позволяют задать имя таблицы, колонки, искомое значение и тип данных, по поторым осуществляется поиск. Эти данные повторяются в SQL версии запроса несколько раз, что не очень удобно. В итоге все уложилось в один класс. И этот класс удобен для тестирования. Здесь нужно обратить внимание на то, что алгоритм может уйти в зацикливание, если использовать для объединения UNION ALL, которое является наиболее часто используемым режимом работы RCTE. Sequel позволяет управлять этим параметром, таким образом, при появлении повторяющихся рядов выполнение запроса завершается. Искомый ряд будет содержать найденое значение, целевое значение, идентификатор ряда с ближайшим к целевому значением. WITH RECURSIVE "t" AS (SELECT min("id"), max("id"), CAST('2016-07-06 16:18:22.852207+0000' AS timestamp) AS "target_col", CAST('2016-07-06 16:18:22.852207+0000' AS timestamp) AS "value_col" FROM "items" WHERE ("id" IN (1, 476160664)) GROUP BY "target_col" UNION (SELECT (CASE WHEN ("t"."target_col" >= "items"."created_at") THEN (("min" + "max") / 2) ELSE "min" END) AS "min", (CASE WHEN ("t"."target_col" < "items"."created_at") THEN (("min" + "max") / 2) ELSE "max" END) AS "max", "target_col", "items"."created_at" AS "value_col" FROM "t" INNER JOIN "items" ON ("items"."id" = (("min" + "max") / 2)) WHERE ("max" >= "min"))) SELECT * FROM "t"
  • #19 Иногда появляется необходимость категоризировать некоторые сущности, если категории должны иметь древовидную структуру, то очень полезно использовать расширение rcte_tree Обычно в таблице категорий есть колонка parent_id, вспомните свои проекты, скорее всего в вашей практике встречались таблицы с такой колонкой. Как правило, такие таблицы хранят древовидные структуры Расширение RCTE tree позволяет получать субдеревья или ветви в таких таблицах, используя RCTE за один запрос. Расширение добавляет в модель четыре ассоциации, имена которых можно переопределять Две для потомков и родителей и две для непосредственных потомков и родителей. Так же это расширение, вполне естественным образом позволяет ограничить уровень вложенности при просмотре потомков, что довольно удобно при просмотре больших структур Это первый пример полезного RCTE, который все стараются привести. В случае с Sequel он уже реализован, можно пользоваться. Позволяет использовать eager_loading, при этом уровень вложенности так же можно ограничивать.
  • #20 В постгрес 95 появился апсерт, воспользоваться им можно следующим образом. Причем можно обновлять не все поля Rollup Groupping sets поддерживаются Arel Вот пример использования Rollup, это очень удобно, если вы планируете выводить таблицы статистики. Ряд итоговых значений будет подсчитан автоматически
  • #21 Всем известно, что постгрес позволяет выполнять код на стороне сервера, поддерживается множество языков, в числе которых plpgsql, perl, javascript и python. Наш любимый язык руби не поддерживается, так как он нестабилен. Некогда знакомое некоторым слушателям словосочетание лямбды в базе обретает новый смысл. Этот функционал может быть полезен при отладке хранимых процедур. Так же с помощью sequel можно создавать миграции для управления хранимыми процедурами. И хотя любителей хранимых процедур среди нас немного, конвертировать значения столбцов удобно именно с использованием чистых функций (immutable). Копирование таблиц в виде csv применяется для формирования различных выгрузок, архивации данных и загрузки данных в бд. Именно операция позволяет добиться самой высокой скорости вставки и чтения.
  • #22 Зачастую появляется потребность выполнять часть запросов со слейва. Для этого нужно установить соединение со слейв серверами в самом начале. Выбирать сервер можно для каждого запроса или датасета. Можно использовать блочный синтаксис и плагин, гарантирующий то, что модель, экземпляр которой был получен через определенный шард будет обновлена через тот же шард. Сервер, к которому было выполнено подключение изначально считается сервером по умолчанию. Это значит, что служебные запросы Sequel будут выполняться на нем. Поэтому рекомендуется использовать одинаковую схему данных на всех шардах. Простейшая конфигурация мастер слейв включает в себя два выше указанных сервера. Но фреймворк позволяет использовать произвольное количество шардов, в том числе изменяя его динамически, подробнее в доках. Отмечу, что для AR тоже существует ряд надстроек например октопус. В дополнение к этому Sequel поддерживает prepared transactions. Эти транзакции так же называют распределенными транзакциями, так как они запускаются на нескольких серверах СУБД. После завершения всех проверок, на всех серверах субд, вы можете подтвердить или откатить такую транзакцию. Sequel даже позволяет автоматически повторять транзакции, которые откатились из за Serialization Falure, можно указать num_retries который по умолчанию 5 DB.transaction(:isolation => :serializable, :retry_on=>[Sequel::SerializationFailure]) do ModelClass.find_or_create(:name=>'Foo') end
  • #23 Мы рассмотрели DSL Sequel, его отличия от AR, Arel. Работу Sequel c различными типами данных, такими как jsonb, hstore. Так же мы рассмотрели операции над этими типами данных на стороне сервера СУБД Кроме того я рассказал о ряде функций, которые позволяют более полно использовать возможности субд. Среди них CTE, RCTE, полнотекстовой поиск, копирование данных. Количество Issue на странице Sequel на гитхаб и правда малО в сравнении с тем же значением у ActiveRecord. И у него не утекают коннекшены (единственный способ взять коннекшен из пула - это выполнить блок и вернуть коннекшен обратно, с AR можно поступать иначе) Мейнтенер у Sequel один. Кроме того, Sequel оказывается быстрее. QR код со ссылкой на анализ производительности вы найдете в правом нижнем углу.