SlideShare una empresa de Scribd logo
1 de 40
Descargar para leer sin conexión
ActiveRecord: Deja que
Postgres* lo haga
Consejos Pro para hacer consultas más eficientes…
...y hacer las paces con ActiveRecord
…porque saben? Casi todos los proyectos que me han tocado tienen algunas
cosas que hacen que la vena se me salte y los ojos me sangren:
¿Cuales son las órdenes (compras) con amplificadores y pedales "de
efectos" de la marca "Fender"?
Order Product Category
has_many
LineItem
belongs_to belongs_to
• quantity • name
• brand
• name
def index
category_ids = Category.where(name: params[:categories]).pluck(:id)
product_ids = Product.where(brand: params[:brand], category_id: category_ids).pluck(:id)
order_ids = LineItem.where(product_id: product_ids).pluck(:order_id).uniq
@orders = Order.where(id: order_ids)
end
:(
SELECT "id" FROM "categories" WHERE "name" IN ('amps', 'effects')
=> [25, 33]
SELECT "id" FROM "products" WHERE "category_id" IN (25, 33) AND "brand" = 'Fender'
=> [12, 44, 96, 35, 112, 134, 246, 288, 299, 315, 317, 447, 864, 882, 913, 1046]
SELECT "order_id" FROM "line_items" WHERE "product_id" IN (12, 44, 96, 35, 112, 134, 246,
288, 299, 315, 317, 447, 864, 882, 913, 1046)
=> [38, 423, 981 …… 88394] (count: 13000)
# SELECT * FROM "orders" WHERE "id" IN (38, 423, 981, …ridiculous amount of ids…, 88394)
…caso real. Los nombres cambiaron para proteger el anonimato.
def index
category_ids = Category.where(name: params[:categories]).map(&:id)
product_ids = Product.where(brand: params[:brand], category_id: category_ids).map(&:id)
order_ids = LineItem.where(product_id: product_ids).map(&:order_id).uniq
@orders = Order.where(id: order_ids)
end
:'(
SELECT * FROM "categories" WHERE "name" IN ('amps', 'effects')
=> [<Category id: 25, name: "amps">, <Category id: 33, name: "effects">] => [25, 33]
SELECT * FROM "products" WHERE "category_id" IN (25, 33) AND "brand" = 'Fender'
=> [<Product id: 12...>...] => [12, 44, 96, 112, 246, 288, 299,317, 447, 864, 913, 1046]
SELECT * FROM "line_items" WHERE "product_id" IN (12, 44, 96, 35, 112, 134, 246, 288,
299, 315, 317, 447, 864, 882, 913, 1046)
=> [<...>, ...thousands...] => [38, 423, 981 …… 88394] (count: 13000)
# SELECT * FROM "orders" WHERE "id" IN (38, 423, 981, …ridiculous amount of ids…, 88394)
…caso real. Los nombres cambiaron para proteger el anonimato.
¿¿¿¿¿¿¿Para qué hacen eso???????
"Fuente de toda maldad"
A. Usar ActiveRecord (o cualquier otro ORM) es ineficiente, usa
muchísima memoria y hace al sistema lento…
B. …es más, vamos a prohibirlo en la empresa! QUEDAN
PROHIBIDOS LOS ORM!!!
C. …y para que vean que es en serio, AHORA TODOS VAN A TENER
QUÉ USAR STORED PROCEDURES!!!!!
Aunque no lo crean, eso me pasó. True Story.
La verdadera fuente de toda maldad
• Hacer operaciones de filtrado / ordenamiento en la app en vez de la
base de datos.
• Evaluar listas gigantes de registros en la app, para filtrar.
• Consultar datos en la DB para inmediatamente generar / actualizar
registros desde la app.
• Mal diseño de la DB
La verdadera fuente de toda maldad
No usar la base de datos para manejar los datos.
¿Porqué usar la base de datos?
• El servicio de base de datos debe tener más memoria para filtrar,
ordenar y generar registros que los procesos (dynos, etc) de la
aplicación.
• El engine de la base de datos tiene algoritmos mucho más
eficientes para ordenar y filtrar la información.
• La información para filtrar, ordenar y generar los registros
requeridos está más "cerca" / "a la mano" en el servidor de base de
datos, que la aplicación.
Tips pro
• Consultas avanzadas con ActiveRecord y Arel:
• Scopes, buenas relaciones y usar .merge
• JOINS con datos que no están en la base de datos*
• Usar "INSERT + SELECT"
Consultas avanzadas con ActiveRecord y Arel
Order Product Category
has_many
LineItem
belongs_to belongs_to
• quantity • name
• brand
• name
¿Cuales son las órdenes (compras) con amplificadores y pedales "de
efectos" de la marca "Fender"?
Consultas avanzadas con ActiveRecord y Arel
def index
category_ids = Category.where(name: params[:categories]).pluck(:id)
product_ids = Product.where(brand: params[:brand], category_id: category_ids).pluck(:id)
order_ids = LineItem.where(product_id: product_ids).pluck(:order_id).uniq
@orders = Order.where(id: order_ids)
end
Ya deja ésos pluck(:id) en paz...
Consultas avanzadas con ActiveRecord y Arel
...y usa joins + merge:
class Order < ActiveRecord::Base
has_many :line_items
has_many :products, through: :line_items
end
def index
categories = Category.where(name: params[:categories])
products = Product.where(brand: params[:brand]).joins(:categories).merge(categories)
@orders = Order.joins(:products)
end
# SELECT "categories".* FROM "categories" WHERE "categories"."name" IN ('Amps', ‘Effects')
# SELECT "orders".* FROM "orders"

# INNER JOIN "line_items" on "orders"."id" = "line_items"."order_id"

# INNER JOIN "products" on "line_items"."product_id" = "products"."id"
# SELECT "products".* FROM "products"

# INNER JOIN "categories" on "products"."category_id" = "categories"."id"

# WHERE "products"."brand" = 'Fender' AND "categories"."name" IN ('Amps', 'Effects')
.merge(products)
# INNER JOIN "categories" on "products"."category_id" = "categories"."id"

# WHERE "categories"."name" IN ('Amps', 'Effects') AND "products"."brand" = 'Fender'
Consultas avanzadas con ActiveRecord y Arel
...y entre más scopes, mejor!
class Product < ActiveRecord::Base
belongs_to :category
scope :with_categories, -> (categories) { joins(:categories).merge(categories) }
end
class Order < ActiveRecord::Base
has_many :line_items
has_many :products, through: :line_items
scope :with_products, -> (products) { joins(:products).merge(categories) }
end
def index
categories = Category.where(name: params[:categories])
products = Product.with_categories(categories).where(brand: params[:brand])
@orders = Order.with_products(products)
end
Consultas avanzadas con ActiveRecord y Arel
Ojo con el Eager Loading:
def index
categories = Category.where(name: params[:categories])
products = Product.with_categories(categories).where(brand: params[:brand])
@orders = Order.with_products(products)
end
# SELECT "orders".* FROM "orders"

# INNER JOIN "line_items" on "orders"."id" = "line_items"."order_id"

# INNER JOIN "products" on "line_items"."product_id" = "products"."id"

# INNER JOIN "categories" on "products"."category_id" = "categories"."id"

# WHERE "categories"."name" IN ('Amps', 'Effects') AND "products"."brand" = 'Fender'
Consultas avanzadas con ActiveRecord y Arel
Ojo con el Eager Loading:
def index
categories = Category.where(name: params[:categories])
products = Product.with_categories(categories).where(brand: params[:brand])
@orders = Order.with_products(products).includes(products: [:categories])
end
# SELECT “orders”.”id" AS t0_r0, “orders”.”user_id” AS t0_r1, “orders”.”created_at” AS t0_r2

# “products”.”id” AS t2_r0, “products”.”name” AS t2_r1, “categories”.”id” AS t3_r0, 

# “categories”.”name” AS t3_r1

# FROM "orders"

# INNER JOIN "line_items" on "orders"."id" = "line_items"."order_id"

# INNER JOIN "products" as "t0" on "line_items"."product_id" = "products"."id"

# INNER JOIN "categories" on "products"."category_id" = "categories"."id"

# WHERE "categories"."name" IN ('Amps', 'Effects') AND "products"."brand" = 'Fender'
Consultas avanzadas con ActiveRecord y Arel
¿Reportes? Deja de generar los reportes en Rubylandia…
class Shelf < ActiveRecord::Base
has_many :line_items
def item_count
line_items.to_a.count
end
end

————————————————
<% @shelves.each do |shelf| %>
# SELECT “shelves”.* FROM “shelves”
# SELECT “line_items”.* FROM “line_items” WHERE “line_items”.”shelf_id” = 24
# SELECT “line_items”.* FROM “line_items” WHERE “line_items”.”shelf_id” = 25
# SELECT “line_items”.* FROM “line_items” WHERE “line_items”.”shelf_id” = 26
# SELECT ...
<li>Item Count: <%= shelf.item_count %>
<% end %>
Consultas avanzadas con ActiveRecord y Arel
…y que lo haga la base de datos usando GROUP BY, COUNT, etc:
class Shelf < ActiveRecord::Base
has_many :line_items
def self.with_item_counts
line_items_for_count = LineItem.arel_table.alias(""line_items_for_count"")
join_conditions = arel_table[:id].eq(line_items_for_count[:shelf_id])
join = arel_table.create_join(
line_items_for_count,
arel_table.create_on(join_conditions),
Arel::Nodes::OuterJoin

) # Usa `.to_sql` para reviser cómo cada object de Arel se traduce a SQL
joins(join).group(arel_table[:id]).select(
arel_table[Arel.star],

Arel::Nodes::NamedFunction.new("COUNT", [line_items_for_count[Arel.star]])
.as(""item_count"")
)
end
end
Consultas avanzadas con ActiveRecord y Arel
…y que lo haga la base de datos usando GROUP BY, COUNT, etc:
class ShelvesController < ApplicationController
def reports
@shelves = Shelf.with_item_counts
end
end
——————————————————
# app/views/shelves/reports.html.erb

<% @shelves.each do |shelf| %>
# SELECT “shelves”.*, COUNT(“line_items”.*) AS “item_count”
# FROM “shelves” LEFT JOIN “line_items” ON “shelves”.”id” = “line_items”.”shelf_id”
# => [<Shelf id=1, item_count=48>,<Shelf id=2, item_count=73>]
<li>Item Count: <%= shelf.item_count %>
<% end %>
Consultas avanzadas con ActiveRecord y Arel
Recuerda que todos los scopes son encadenables…

…y que todos los métodos de clase pueden regresar scopes!
class Product < ActiveRecord::Base
belongs_to :category
def self.categories
Category.joins(:products).merge(current_scope).distinct
end
end


Product.where(brand: "Fender").categories

# SELECT DISTINCT “categories”.*

# FROM “categories”

# INNER JOIN “products” ON “categories”.”id” = “products”.”category_id”

# WHERE “products”.”brand” = ‘Fender’

Consultas avanzadas con ActiveRecord y Arel
Recuerda que todos los scopes son encadenables…

…y que todos los métodos de clase pueden regresar scopes!
class Order < ActiveRecord::Base
has_many :line_items
has_many :products, through: :line_items
def self.products
Product.joins(:orders).merge(current_scope).distinct
end
end


Order.where(created_at: Date.today).products.categories

# SELECT DISTINCT “categories”.*

# FROM “categories”

# INNER JOIN “products” ON “categories”.”id” = “products”.”category_id”

# INNER JOIN “line_items” ON “products”.”id” = “line_items”.”product_id”

# INNER JOIN “orders” ON “line_items”.”order_id” = “orders”.”id”

# WHERE “products”.”brand” = ‘Fender’ AND “orders”.”created_at” = ‘2016-07-21’
JOINS con datos que no están en la base de datos
Cómo funciona la integración actual de
ElasticSearch y ActiveRecord…
JOINS con datos que no están en la base de datos
ElasticSearch
Postgres
Ruby App
extraer
ids
SELECT *

FROM “products”

WHERE “id” IN (…)
[

{ “id”: 25, “score”: 8.0 },

{ “id”: 33, “score”: 7.75 },
…

]
ordenar…
(Array)[<Product id:25…>, <Product id:33 …>]
JOINS con datos que no están en la base de datos
…y cómo mejorarlo?
JOINS con datos que no están en la base de datos
ElasticSearch
Postgres
Ruby App
SUPER COOL SQL QUERY :)
[

{ “id”: 25, “score”: 8.0 },

{ “id”: 33, “score”: 7.75 },
…

]
<ActiveRecord::Relation [<Product id:25…>, <Product id:33 …>]>
JOINS con datos que no están en la base de datos
- De ElasticSearch obtuvimos un array similar a éste:
es_results = [{ id: 25, score: 8.0 }, { id: 33, score: 7.75 }]
- Y éste es el query ideal para la base de datos
SELECT "products".*
FROM "products" INNER JOIN json_populate_recordset(
NULL::record_score,
'[{"id":25, "score":8.36}, {"id":88, "score":7.71}]'
) AS “record_scoring" ON "products"."id" =
"record_scoring"."id"
ORDER BY "record_scoring"."score" DESC
Recordset al que se

puede hacer un join…

…como una tabla

temporal para éste query
JOINS con datos que no están en la base de datos
class CreateRecordScoreType < ActiveRecord::Migration
def up
execute "CREATE TYPE "record_score" AS " 
"("id" NUMERIC, "score" NUMERIC(8,4))"
end
def down
execute 'DROP TYPE IF EXISTS "record_score"'
end
end
#1: Crear Custom Type para poder generar nuestro recordset:
JOINS con datos que no están en la base de datos
# /app/models/concerns/sortable_by_given_scores.rb
module SortableByGivenScores
extend ActiveSupport::Concern
module ClassMethods
def scores_as_recordset(scores, recordset_alias = :record_scoring)
Arel::Nodes::NamedFunction.new("json_populate_recordset", [
Arel::Nodes::SqlLiteral.new("NULL::record_score"),
ActiveSupport::JSON.encode(scores)
]).as(""#{recordset_alias}"")
end
def by_given_scores(scores, recordset_alias = :record_scoring)…
end
end
- darkbone
#2: Crear módulo con scope:
JOINS con datos que no están en la base de datos
# /app/models/concerns/sortable_by_given_scores.rb
module SortableByGivenScores
extend ActiveSupport::Concern
module ClassMethods
def scores_as_recordset(scores, recordset_alias = :record_scoring)…
def by_given_scores(scores, recordset_alias = :record_scoring)
scoring = Arel::Table.new recordset_alias # Not a real table...
scoring_recordset = scores_as_recordset(scores, recordset_alias)
join_condition = scoring_recordset.create_on(scoring[:id].eq(arel_table[:id]))
join_clause = arel_table
.create_join(scoring_recordset, join_condition, Arel::Nodes::InnerJoin)
joins(join_clause).order(scoring[:score].desc)
end
end
end
- darkbone
#2: Crear módulo con scope:
JOINS con datos que no están en la base de datos
# /app/models/product.rb

class Product < ActiveRecord::Base
includes SortableByGivenScores
belongs_to :category
has_many :line_items
end
# Simularemos un hash de resultados de ElasticSearch:
es_results = [{ id: 25, score: 8.0 }, { id: 33, score: 7.75 }]
# Resultados (sin evaluar)
some_products = Product.by_given_scores(es_results).includes(:category)
# SELECT "products".* FROM "products" INNER JOIN

# json_populate_recordset(NULL::record_score, '[{"id":25, "score":8.36}, {"id":88,

# ”score":7.71}]') AS "record_scoring" ON "products"."id" = "record_scoring"."id" ORDER
# BY "record_scoring"."score" DESC)
# SELECT * FROM "categories" WHERE "id" IN (26, 44)
#3: Agregar módulo a modelo ‘Product’:
Usar "INSERT + SELECT"
Order Product
has_many
LineItem
belongs_to
• quantity • name
• brand
SoldItem
Usar "INSERT + SELECT"
Generar una lista de N “sold_items” por cada “line_item” de
una order, dependiendo del valor de “line_items”.“quantity”:
Usar "INSERT + SELECT"
INSERT INTO "sold_items" ("order_id", "product_id", "item_sort") (
SELECT
"orders"."id" AS "order_id",
"line_items"."product_id" AS "product_id",
generate_series(1, "line_items"."quantity") AS "item_sort"
FROM "orders" INNER JOIN "line_items" on "orders"."id" = "line_items"."id"
WHERE "orders"."id" = 26
)
Crear Sold Items a partir de los LineItems…

…y usar “generate_series” para “multiplicar” los registros:
Usar "INSERT + SELECT"
En Ruby:
class Order < ActiveRecord::Base
def register_sold_items…
private
def sold_items_projection # Regresa el “SELECT”
li_table = LineItem.arel_table
item_sort = Arel::Nodes::NamedFunction
.new("generate_series", [1, li_table[:quantity]]) # Genera el “generate_series”


self.class.arel_table
.join(li_table).on(arel_table[:id].eq(li_table[:order_id]))
.where(arel_table[:id].eq(id))
.project arel_table[:id].as(""order_id""),
li_table[:product_id],
item_sort.as(“"item_sort"")
end
end
Usar "INSERT + SELECT"
En Ruby:
class Order < ActiveRecord::Base
def register_sold_items # Genera y ejecuta el “INSERT”
insert_table = SoldItem.arel_table
insert_manager = Arel::InsertManager.new ActiveRecord::Base
insert_manager.into sold_items_table
insert_manager.columns.concat [ # Genera la lista de columnas del insert...
insert_table[:order_id], insert_table[:product_id], insert_table[:item_sort]
]
insert_manager.select sold_items_projection # agrega el “SELECT”
connection.insert insert_manager.to_sql # Ejecuta el “INSERT”
end
private
def sold_items_projection…
end
Usar "INSERT + SELECT"
En Ruby:
@order = Order.find(24)



# Ejecutar el insert en bulto:

@order.register_sold_items
Por continuar…
• Postgres: “Window Functions” y otras funciones:
• “row_count”, “unnest”
• UPDATE + JOIN
• Mejorar los índices / Generar índices más efectivos
• Mejorar el diseño de base de datos
Referencias
• Dan Shultz - "Mastering ActiveRecord and Arel"
• Konstantin Gredeskoul - "12-Step Program For Scaling Web
Applications on PostgreSQL"
• Konstantin Gredeskoul - "From Obvious to Ingenious: Incrementally
Scaling Web Applications on PostgreSQL"
• Parker Selbert - Folding Postgres Window Functions into Rails

Más contenido relacionado

Destacado

Child Abuse
Child AbuseChild Abuse
Child Abusetina
 
Determining The Airport Concessions Concept Mix Scott Kilgo 2005
Determining The Airport Concessions Concept Mix  Scott Kilgo 2005Determining The Airport Concessions Concept Mix  Scott Kilgo 2005
Determining The Airport Concessions Concept Mix Scott Kilgo 2005scottkilgo
 
Hype vs. Reality: The AI Explainer
Hype vs. Reality: The AI ExplainerHype vs. Reality: The AI Explainer
Hype vs. Reality: The AI ExplainerLuminary Labs
 
Study: The Future of VR, AR and Self-Driving Cars
Study: The Future of VR, AR and Self-Driving CarsStudy: The Future of VR, AR and Self-Driving Cars
Study: The Future of VR, AR and Self-Driving CarsLinkedIn
 

Destacado (6)

Test
TestTest
Test
 
Child Abuse
Child AbuseChild Abuse
Child Abuse
 
Library Equipment
Library EquipmentLibrary Equipment
Library Equipment
 
Determining The Airport Concessions Concept Mix Scott Kilgo 2005
Determining The Airport Concessions Concept Mix  Scott Kilgo 2005Determining The Airport Concessions Concept Mix  Scott Kilgo 2005
Determining The Airport Concessions Concept Mix Scott Kilgo 2005
 
Hype vs. Reality: The AI Explainer
Hype vs. Reality: The AI ExplainerHype vs. Reality: The AI Explainer
Hype vs. Reality: The AI Explainer
 
Study: The Future of VR, AR and Self-Driving Cars
Study: The Future of VR, AR and Self-Driving CarsStudy: The Future of VR, AR and Self-Driving Cars
Study: The Future of VR, AR and Self-Driving Cars
 

Similar a ActiveRecord: Deja que Postgres lo haga

EJECICIO DE BASE DE DATOS TIENDA SQL
EJECICIO DE BASE DE DATOS TIENDA SQLEJECICIO DE BASE DE DATOS TIENDA SQL
EJECICIO DE BASE DE DATOS TIENDA SQLRuth Cujilan
 
Código mantenible, en Wordpress.
Código mantenible, en Wordpress.Código mantenible, en Wordpress.
Código mantenible, en Wordpress.Asier Marqués
 
OVA DISEÑO ORACLE Introducción Bases de Datos .pptx
OVA DISEÑO ORACLE Introducción Bases de Datos .pptxOVA DISEÑO ORACLE Introducción Bases de Datos .pptx
OVA DISEÑO ORACLE Introducción Bases de Datos .pptxMARGOTHLORENAMARTINE
 
Tutorial MIneria de datos en sql server
Tutorial  MIneria de datos en sql serverTutorial  MIneria de datos en sql server
Tutorial MIneria de datos en sql serverRis Fernandez
 
Extendiendo Django: Cómo Escribir Tu Propio Backend de Base de Datos - Exasol
Extendiendo Django: Cómo Escribir Tu Propio Backend de Base de Datos - ExasolExtendiendo Django: Cómo Escribir Tu Propio Backend de Base de Datos - Exasol
Extendiendo Django: Cómo Escribir Tu Propio Backend de Base de Datos - ExasolJavier Abadía
 
Inner join maryum
Inner join maryumInner join maryum
Inner join maryummaryum26
 
ESTANDARIZACION DE EQUIPSO.pptx
ESTANDARIZACION DE EQUIPSO.pptxESTANDARIZACION DE EQUIPSO.pptx
ESTANDARIZACION DE EQUIPSO.pptxAlvaroCristancho1
 
Icf case data_model_01 (1)
Icf case data_model_01 (1)Icf case data_model_01 (1)
Icf case data_model_01 (1)Carmen Parr
 
Icf case data_model_01
Icf case data_model_01Icf case data_model_01
Icf case data_model_01Majo Tapia
 
presentacion power designer
presentacion power designer presentacion power designer
presentacion power designer IrvingLima1503_
 
Descubriendo Ruby on Rails (Desarrollo Agil de Aplicaciones Web)
Descubriendo Ruby on Rails (Desarrollo Agil de Aplicaciones Web)Descubriendo Ruby on Rails (Desarrollo Agil de Aplicaciones Web)
Descubriendo Ruby on Rails (Desarrollo Agil de Aplicaciones Web)lenny
 
Descubriendo Ruby On Rails (Desarrollo Agil De Aplicaciones Web)
Descubriendo Ruby On Rails (Desarrollo Agil De Aplicaciones Web)Descubriendo Ruby On Rails (Desarrollo Agil De Aplicaciones Web)
Descubriendo Ruby On Rails (Desarrollo Agil De Aplicaciones Web)INSIGNIA4U
 
Acceptance testing with Steak and Capybara
Acceptance testing with Steak and CapybaraAcceptance testing with Steak and Capybara
Acceptance testing with Steak and CapybaraSergio Gil
 

Similar a ActiveRecord: Deja que Postgres lo haga (20)

EJECICIO DE BASE DE DATOS TIENDA SQL
EJECICIO DE BASE DE DATOS TIENDA SQLEJECICIO DE BASE DE DATOS TIENDA SQL
EJECICIO DE BASE DE DATOS TIENDA SQL
 
Código mantenible, en Wordpress.
Código mantenible, en Wordpress.Código mantenible, en Wordpress.
Código mantenible, en Wordpress.
 
Ejercicio sql tienda informatica
Ejercicio sql tienda informaticaEjercicio sql tienda informatica
Ejercicio sql tienda informatica
 
Intro BBDD SQL Server.pptx
Intro BBDD SQL Server.pptxIntro BBDD SQL Server.pptx
Intro BBDD SQL Server.pptx
 
OVA DISEÑO ORACLE Introducción Bases de Datos .pptx
OVA DISEÑO ORACLE Introducción Bases de Datos .pptxOVA DISEÑO ORACLE Introducción Bases de Datos .pptx
OVA DISEÑO ORACLE Introducción Bases de Datos .pptx
 
Tutorial MIneria de datos en sql server
Tutorial  MIneria de datos en sql serverTutorial  MIneria de datos en sql server
Tutorial MIneria de datos en sql server
 
CONSULTAS AVANZADAS SQL.pptx
CONSULTAS AVANZADAS SQL.pptxCONSULTAS AVANZADAS SQL.pptx
CONSULTAS AVANZADAS SQL.pptx
 
CONSULTAS_AVANZADAS_SQL.pptx
CONSULTAS_AVANZADAS_SQL.pptxCONSULTAS_AVANZADAS_SQL.pptx
CONSULTAS_AVANZADAS_SQL.pptx
 
Extendiendo Django: Cómo Escribir Tu Propio Backend de Base de Datos - Exasol
Extendiendo Django: Cómo Escribir Tu Propio Backend de Base de Datos - ExasolExtendiendo Django: Cómo Escribir Tu Propio Backend de Base de Datos - Exasol
Extendiendo Django: Cómo Escribir Tu Propio Backend de Base de Datos - Exasol
 
Introducción a DJango
Introducción a DJangoIntroducción a DJango
Introducción a DJango
 
Inner join maryum
Inner join maryumInner join maryum
Inner join maryum
 
ESTANDARIZACION DE EQUIPSO.pptx
ESTANDARIZACION DE EQUIPSO.pptxESTANDARIZACION DE EQUIPSO.pptx
ESTANDARIZACION DE EQUIPSO.pptx
 
Icf case data_model_01 (1)
Icf case data_model_01 (1)Icf case data_model_01 (1)
Icf case data_model_01 (1)
 
Icf case data_model_01
Icf case data_model_01Icf case data_model_01
Icf case data_model_01
 
presentacion power designer
presentacion power designer presentacion power designer
presentacion power designer
 
Temario
Temario Temario
Temario
 
Tuning fondo-negro-2
Tuning fondo-negro-2Tuning fondo-negro-2
Tuning fondo-negro-2
 
Descubriendo Ruby on Rails (Desarrollo Agil de Aplicaciones Web)
Descubriendo Ruby on Rails (Desarrollo Agil de Aplicaciones Web)Descubriendo Ruby on Rails (Desarrollo Agil de Aplicaciones Web)
Descubriendo Ruby on Rails (Desarrollo Agil de Aplicaciones Web)
 
Descubriendo Ruby On Rails (Desarrollo Agil De Aplicaciones Web)
Descubriendo Ruby On Rails (Desarrollo Agil De Aplicaciones Web)Descubriendo Ruby On Rails (Desarrollo Agil De Aplicaciones Web)
Descubriendo Ruby On Rails (Desarrollo Agil De Aplicaciones Web)
 
Acceptance testing with Steak and Capybara
Acceptance testing with Steak and CapybaraAcceptance testing with Steak and Capybara
Acceptance testing with Steak and Capybara
 

ActiveRecord: Deja que Postgres lo haga

  • 1. ActiveRecord: Deja que Postgres* lo haga Consejos Pro para hacer consultas más eficientes… ...y hacer las paces con ActiveRecord
  • 2. …porque saben? Casi todos los proyectos que me han tocado tienen algunas cosas que hacen que la vena se me salte y los ojos me sangren:
  • 3. ¿Cuales son las órdenes (compras) con amplificadores y pedales "de efectos" de la marca "Fender"? Order Product Category has_many LineItem belongs_to belongs_to • quantity • name • brand • name
  • 4. def index category_ids = Category.where(name: params[:categories]).pluck(:id) product_ids = Product.where(brand: params[:brand], category_id: category_ids).pluck(:id) order_ids = LineItem.where(product_id: product_ids).pluck(:order_id).uniq @orders = Order.where(id: order_ids) end :( SELECT "id" FROM "categories" WHERE "name" IN ('amps', 'effects') => [25, 33] SELECT "id" FROM "products" WHERE "category_id" IN (25, 33) AND "brand" = 'Fender' => [12, 44, 96, 35, 112, 134, 246, 288, 299, 315, 317, 447, 864, 882, 913, 1046] SELECT "order_id" FROM "line_items" WHERE "product_id" IN (12, 44, 96, 35, 112, 134, 246, 288, 299, 315, 317, 447, 864, 882, 913, 1046) => [38, 423, 981 …… 88394] (count: 13000) # SELECT * FROM "orders" WHERE "id" IN (38, 423, 981, …ridiculous amount of ids…, 88394) …caso real. Los nombres cambiaron para proteger el anonimato.
  • 5. def index category_ids = Category.where(name: params[:categories]).map(&:id) product_ids = Product.where(brand: params[:brand], category_id: category_ids).map(&:id) order_ids = LineItem.where(product_id: product_ids).map(&:order_id).uniq @orders = Order.where(id: order_ids) end :'( SELECT * FROM "categories" WHERE "name" IN ('amps', 'effects') => [<Category id: 25, name: "amps">, <Category id: 33, name: "effects">] => [25, 33] SELECT * FROM "products" WHERE "category_id" IN (25, 33) AND "brand" = 'Fender' => [<Product id: 12...>...] => [12, 44, 96, 112, 246, 288, 299,317, 447, 864, 913, 1046] SELECT * FROM "line_items" WHERE "product_id" IN (12, 44, 96, 35, 112, 134, 246, 288, 299, 315, 317, 447, 864, 882, 913, 1046) => [<...>, ...thousands...] => [38, 423, 981 …… 88394] (count: 13000) # SELECT * FROM "orders" WHERE "id" IN (38, 423, 981, …ridiculous amount of ids…, 88394) …caso real. Los nombres cambiaron para proteger el anonimato.
  • 7. "Fuente de toda maldad" A. Usar ActiveRecord (o cualquier otro ORM) es ineficiente, usa muchísima memoria y hace al sistema lento… B. …es más, vamos a prohibirlo en la empresa! QUEDAN PROHIBIDOS LOS ORM!!! C. …y para que vean que es en serio, AHORA TODOS VAN A TENER QUÉ USAR STORED PROCEDURES!!!!!
  • 8. Aunque no lo crean, eso me pasó. True Story.
  • 9. La verdadera fuente de toda maldad • Hacer operaciones de filtrado / ordenamiento en la app en vez de la base de datos. • Evaluar listas gigantes de registros en la app, para filtrar. • Consultar datos en la DB para inmediatamente generar / actualizar registros desde la app. • Mal diseño de la DB
  • 10. La verdadera fuente de toda maldad No usar la base de datos para manejar los datos.
  • 11. ¿Porqué usar la base de datos? • El servicio de base de datos debe tener más memoria para filtrar, ordenar y generar registros que los procesos (dynos, etc) de la aplicación. • El engine de la base de datos tiene algoritmos mucho más eficientes para ordenar y filtrar la información. • La información para filtrar, ordenar y generar los registros requeridos está más "cerca" / "a la mano" en el servidor de base de datos, que la aplicación.
  • 12. Tips pro • Consultas avanzadas con ActiveRecord y Arel: • Scopes, buenas relaciones y usar .merge • JOINS con datos que no están en la base de datos* • Usar "INSERT + SELECT"
  • 13. Consultas avanzadas con ActiveRecord y Arel Order Product Category has_many LineItem belongs_to belongs_to • quantity • name • brand • name ¿Cuales son las órdenes (compras) con amplificadores y pedales "de efectos" de la marca "Fender"?
  • 14. Consultas avanzadas con ActiveRecord y Arel def index category_ids = Category.where(name: params[:categories]).pluck(:id) product_ids = Product.where(brand: params[:brand], category_id: category_ids).pluck(:id) order_ids = LineItem.where(product_id: product_ids).pluck(:order_id).uniq @orders = Order.where(id: order_ids) end Ya deja ésos pluck(:id) en paz...
  • 15. Consultas avanzadas con ActiveRecord y Arel ...y usa joins + merge: class Order < ActiveRecord::Base has_many :line_items has_many :products, through: :line_items end def index categories = Category.where(name: params[:categories]) products = Product.where(brand: params[:brand]).joins(:categories).merge(categories) @orders = Order.joins(:products) end # SELECT "categories".* FROM "categories" WHERE "categories"."name" IN ('Amps', ‘Effects') # SELECT "orders".* FROM "orders"
 # INNER JOIN "line_items" on "orders"."id" = "line_items"."order_id"
 # INNER JOIN "products" on "line_items"."product_id" = "products"."id" # SELECT "products".* FROM "products"
 # INNER JOIN "categories" on "products"."category_id" = "categories"."id"
 # WHERE "products"."brand" = 'Fender' AND "categories"."name" IN ('Amps', 'Effects') .merge(products) # INNER JOIN "categories" on "products"."category_id" = "categories"."id"
 # WHERE "categories"."name" IN ('Amps', 'Effects') AND "products"."brand" = 'Fender'
  • 16. Consultas avanzadas con ActiveRecord y Arel ...y entre más scopes, mejor! class Product < ActiveRecord::Base belongs_to :category scope :with_categories, -> (categories) { joins(:categories).merge(categories) } end class Order < ActiveRecord::Base has_many :line_items has_many :products, through: :line_items scope :with_products, -> (products) { joins(:products).merge(categories) } end def index categories = Category.where(name: params[:categories]) products = Product.with_categories(categories).where(brand: params[:brand]) @orders = Order.with_products(products) end
  • 17. Consultas avanzadas con ActiveRecord y Arel Ojo con el Eager Loading: def index categories = Category.where(name: params[:categories]) products = Product.with_categories(categories).where(brand: params[:brand]) @orders = Order.with_products(products) end # SELECT "orders".* FROM "orders"
 # INNER JOIN "line_items" on "orders"."id" = "line_items"."order_id"
 # INNER JOIN "products" on "line_items"."product_id" = "products"."id"
 # INNER JOIN "categories" on "products"."category_id" = "categories"."id"
 # WHERE "categories"."name" IN ('Amps', 'Effects') AND "products"."brand" = 'Fender'
  • 18. Consultas avanzadas con ActiveRecord y Arel Ojo con el Eager Loading: def index categories = Category.where(name: params[:categories]) products = Product.with_categories(categories).where(brand: params[:brand]) @orders = Order.with_products(products).includes(products: [:categories]) end # SELECT “orders”.”id" AS t0_r0, “orders”.”user_id” AS t0_r1, “orders”.”created_at” AS t0_r2
 # “products”.”id” AS t2_r0, “products”.”name” AS t2_r1, “categories”.”id” AS t3_r0, 
 # “categories”.”name” AS t3_r1
 # FROM "orders"
 # INNER JOIN "line_items" on "orders"."id" = "line_items"."order_id"
 # INNER JOIN "products" as "t0" on "line_items"."product_id" = "products"."id"
 # INNER JOIN "categories" on "products"."category_id" = "categories"."id"
 # WHERE "categories"."name" IN ('Amps', 'Effects') AND "products"."brand" = 'Fender'
  • 19. Consultas avanzadas con ActiveRecord y Arel ¿Reportes? Deja de generar los reportes en Rubylandia… class Shelf < ActiveRecord::Base has_many :line_items def item_count line_items.to_a.count end end
 ———————————————— <% @shelves.each do |shelf| %> # SELECT “shelves”.* FROM “shelves” # SELECT “line_items”.* FROM “line_items” WHERE “line_items”.”shelf_id” = 24 # SELECT “line_items”.* FROM “line_items” WHERE “line_items”.”shelf_id” = 25 # SELECT “line_items”.* FROM “line_items” WHERE “line_items”.”shelf_id” = 26 # SELECT ... <li>Item Count: <%= shelf.item_count %> <% end %>
  • 20. Consultas avanzadas con ActiveRecord y Arel …y que lo haga la base de datos usando GROUP BY, COUNT, etc: class Shelf < ActiveRecord::Base has_many :line_items def self.with_item_counts line_items_for_count = LineItem.arel_table.alias(""line_items_for_count"") join_conditions = arel_table[:id].eq(line_items_for_count[:shelf_id]) join = arel_table.create_join( line_items_for_count, arel_table.create_on(join_conditions), Arel::Nodes::OuterJoin
 ) # Usa `.to_sql` para reviser cómo cada object de Arel se traduce a SQL joins(join).group(arel_table[:id]).select( arel_table[Arel.star],
 Arel::Nodes::NamedFunction.new("COUNT", [line_items_for_count[Arel.star]]) .as(""item_count"") ) end end
  • 21. Consultas avanzadas con ActiveRecord y Arel …y que lo haga la base de datos usando GROUP BY, COUNT, etc: class ShelvesController < ApplicationController def reports @shelves = Shelf.with_item_counts end end —————————————————— # app/views/shelves/reports.html.erb
 <% @shelves.each do |shelf| %> # SELECT “shelves”.*, COUNT(“line_items”.*) AS “item_count” # FROM “shelves” LEFT JOIN “line_items” ON “shelves”.”id” = “line_items”.”shelf_id” # => [<Shelf id=1, item_count=48>,<Shelf id=2, item_count=73>] <li>Item Count: <%= shelf.item_count %> <% end %>
  • 22. Consultas avanzadas con ActiveRecord y Arel Recuerda que todos los scopes son encadenables…
 …y que todos los métodos de clase pueden regresar scopes! class Product < ActiveRecord::Base belongs_to :category def self.categories Category.joins(:products).merge(current_scope).distinct end end 
 Product.where(brand: "Fender").categories
 # SELECT DISTINCT “categories”.*
 # FROM “categories”
 # INNER JOIN “products” ON “categories”.”id” = “products”.”category_id”
 # WHERE “products”.”brand” = ‘Fender’

  • 23. Consultas avanzadas con ActiveRecord y Arel Recuerda que todos los scopes son encadenables…
 …y que todos los métodos de clase pueden regresar scopes! class Order < ActiveRecord::Base has_many :line_items has_many :products, through: :line_items def self.products Product.joins(:orders).merge(current_scope).distinct end end 
 Order.where(created_at: Date.today).products.categories
 # SELECT DISTINCT “categories”.*
 # FROM “categories”
 # INNER JOIN “products” ON “categories”.”id” = “products”.”category_id”
 # INNER JOIN “line_items” ON “products”.”id” = “line_items”.”product_id”
 # INNER JOIN “orders” ON “line_items”.”order_id” = “orders”.”id”
 # WHERE “products”.”brand” = ‘Fender’ AND “orders”.”created_at” = ‘2016-07-21’
  • 24. JOINS con datos que no están en la base de datos Cómo funciona la integración actual de ElasticSearch y ActiveRecord…
  • 25. JOINS con datos que no están en la base de datos ElasticSearch Postgres Ruby App extraer ids SELECT *
 FROM “products”
 WHERE “id” IN (…) [
 { “id”: 25, “score”: 8.0 },
 { “id”: 33, “score”: 7.75 }, …
 ] ordenar… (Array)[<Product id:25…>, <Product id:33 …>]
  • 26. JOINS con datos que no están en la base de datos …y cómo mejorarlo?
  • 27. JOINS con datos que no están en la base de datos ElasticSearch Postgres Ruby App SUPER COOL SQL QUERY :) [
 { “id”: 25, “score”: 8.0 },
 { “id”: 33, “score”: 7.75 }, …
 ] <ActiveRecord::Relation [<Product id:25…>, <Product id:33 …>]>
  • 28. JOINS con datos que no están en la base de datos - De ElasticSearch obtuvimos un array similar a éste: es_results = [{ id: 25, score: 8.0 }, { id: 33, score: 7.75 }] - Y éste es el query ideal para la base de datos SELECT "products".* FROM "products" INNER JOIN json_populate_recordset( NULL::record_score, '[{"id":25, "score":8.36}, {"id":88, "score":7.71}]' ) AS “record_scoring" ON "products"."id" = "record_scoring"."id" ORDER BY "record_scoring"."score" DESC Recordset al que se
 puede hacer un join…
 …como una tabla
 temporal para éste query
  • 29. JOINS con datos que no están en la base de datos class CreateRecordScoreType < ActiveRecord::Migration def up execute "CREATE TYPE "record_score" AS " "("id" NUMERIC, "score" NUMERIC(8,4))" end def down execute 'DROP TYPE IF EXISTS "record_score"' end end #1: Crear Custom Type para poder generar nuestro recordset:
  • 30. JOINS con datos que no están en la base de datos # /app/models/concerns/sortable_by_given_scores.rb module SortableByGivenScores extend ActiveSupport::Concern module ClassMethods def scores_as_recordset(scores, recordset_alias = :record_scoring) Arel::Nodes::NamedFunction.new("json_populate_recordset", [ Arel::Nodes::SqlLiteral.new("NULL::record_score"), ActiveSupport::JSON.encode(scores) ]).as(""#{recordset_alias}"") end def by_given_scores(scores, recordset_alias = :record_scoring)… end end - darkbone #2: Crear módulo con scope:
  • 31. JOINS con datos que no están en la base de datos # /app/models/concerns/sortable_by_given_scores.rb module SortableByGivenScores extend ActiveSupport::Concern module ClassMethods def scores_as_recordset(scores, recordset_alias = :record_scoring)… def by_given_scores(scores, recordset_alias = :record_scoring) scoring = Arel::Table.new recordset_alias # Not a real table... scoring_recordset = scores_as_recordset(scores, recordset_alias) join_condition = scoring_recordset.create_on(scoring[:id].eq(arel_table[:id])) join_clause = arel_table .create_join(scoring_recordset, join_condition, Arel::Nodes::InnerJoin) joins(join_clause).order(scoring[:score].desc) end end end - darkbone #2: Crear módulo con scope:
  • 32. JOINS con datos que no están en la base de datos # /app/models/product.rb
 class Product < ActiveRecord::Base includes SortableByGivenScores belongs_to :category has_many :line_items end # Simularemos un hash de resultados de ElasticSearch: es_results = [{ id: 25, score: 8.0 }, { id: 33, score: 7.75 }] # Resultados (sin evaluar) some_products = Product.by_given_scores(es_results).includes(:category) # SELECT "products".* FROM "products" INNER JOIN
 # json_populate_recordset(NULL::record_score, '[{"id":25, "score":8.36}, {"id":88,
 # ”score":7.71}]') AS "record_scoring" ON "products"."id" = "record_scoring"."id" ORDER # BY "record_scoring"."score" DESC) # SELECT * FROM "categories" WHERE "id" IN (26, 44) #3: Agregar módulo a modelo ‘Product’:
  • 33. Usar "INSERT + SELECT" Order Product has_many LineItem belongs_to • quantity • name • brand SoldItem
  • 34. Usar "INSERT + SELECT" Generar una lista de N “sold_items” por cada “line_item” de una order, dependiendo del valor de “line_items”.“quantity”:
  • 35. Usar "INSERT + SELECT" INSERT INTO "sold_items" ("order_id", "product_id", "item_sort") ( SELECT "orders"."id" AS "order_id", "line_items"."product_id" AS "product_id", generate_series(1, "line_items"."quantity") AS "item_sort" FROM "orders" INNER JOIN "line_items" on "orders"."id" = "line_items"."id" WHERE "orders"."id" = 26 ) Crear Sold Items a partir de los LineItems…
 …y usar “generate_series” para “multiplicar” los registros:
  • 36. Usar "INSERT + SELECT" En Ruby: class Order < ActiveRecord::Base def register_sold_items… private def sold_items_projection # Regresa el “SELECT” li_table = LineItem.arel_table item_sort = Arel::Nodes::NamedFunction .new("generate_series", [1, li_table[:quantity]]) # Genera el “generate_series” 
 self.class.arel_table .join(li_table).on(arel_table[:id].eq(li_table[:order_id])) .where(arel_table[:id].eq(id)) .project arel_table[:id].as(""order_id""), li_table[:product_id], item_sort.as(“"item_sort"") end end
  • 37. Usar "INSERT + SELECT" En Ruby: class Order < ActiveRecord::Base def register_sold_items # Genera y ejecuta el “INSERT” insert_table = SoldItem.arel_table insert_manager = Arel::InsertManager.new ActiveRecord::Base insert_manager.into sold_items_table insert_manager.columns.concat [ # Genera la lista de columnas del insert... insert_table[:order_id], insert_table[:product_id], insert_table[:item_sort] ] insert_manager.select sold_items_projection # agrega el “SELECT” connection.insert insert_manager.to_sql # Ejecuta el “INSERT” end private def sold_items_projection… end
  • 38. Usar "INSERT + SELECT" En Ruby: @order = Order.find(24)
 
 # Ejecutar el insert en bulto:
 @order.register_sold_items
  • 39. Por continuar… • Postgres: “Window Functions” y otras funciones: • “row_count”, “unnest” • UPDATE + JOIN • Mejorar los índices / Generar índices más efectivos • Mejorar el diseño de base de datos
  • 40. Referencias • Dan Shultz - "Mastering ActiveRecord and Arel" • Konstantin Gredeskoul - "12-Step Program For Scaling Web Applications on PostgreSQL" • Konstantin Gredeskoul - "From Obvious to Ingenious: Incrementally Scaling Web Applications on PostgreSQL" • Parker Selbert - Folding Postgres Window Functions into Rails