Django y bases de datos - el lado B

Javier Eduardo Rojas Romero

El ORM de Django y la BD

Temas

N + 1 consultas

Paginación

El costo de un SELECT *

N + 1 consultas

class User(models.Model):
   name = models.CharField()
class Telephone(models.Model):
   number = models.CharField()
   user = models.ForeignKey('User')
phones = Telephone.objects.filter(number__startswith="310")[:50]
for phone in phones:
    print(phone.user)

N + 1 consultas

DEMO TIME

Cómo evitar N + 1 consultas

Telephone.objects.filter(...).select_related('user')
SELECT "main_telephone"."id",
       "main_telephone"."number",
       "main_telephone"."user_id",
       "main_user"."id",
       "main_user"."name"
FROM "main_telephone"
INNER JOIN "main_user" ON ("main_telephone"."user_id" = "main_user"."id")
WHERE "main_telephone"."number" LIKE '310%'

Cómo evitar N + 1 consultas

list(User.objects.all().prefetch_related('telephone_set')[:50])
SELECT "main_user"."id",
       "main_user"."name"
FROM "main_user" LIMIT 50 

SELECT "main_telephone"."id",
       "main_telephone"."number",
       "main_telephone"."user_id"
FROM "main_telephone"
WHERE "main_telephone"."user_id" IN (257402,
                                     257403,
                                     ...
                                     257450,
                                     257451)

Paginación

SELECT *
FROM product
ORDER BY name DESC
OFFSET 12500 LIMIT 100

…the rows are first sorted according to the <order by clause> and then limited by dropping the number of rows specified in the <result offset clause> from the beginning…

SQL:2011, Part 2, §4.15.3 Derived tables

SELECT *
FROM product
ORDER BY name DESC
LIMIT 100
SELECT *
FROM product
ORDER BY name DESC
OFFSET 100 LIMIT 100
SELECT *
FROM product
ORDER BY name DESC
OFFSET 200 LIMIT 100
SELECT *
FROM product
ORDER BY name DESC
OFFSET 300 LIMIT 100

Fuente imágenes

Paginación basada en llaves/índices

SELECT *
FROM product
ORDER BY name DESC
LIMIT 100
SELECT *
FROM product
WHERE name > ${last_name_previous_page}
ORDER BY name DESC
LIMIT 100
SELECT *
FROM product
WHERE name > ${last_name_previous_page}
ORDER BY name DESC
LIMIT 100
SELECT *
FROM product
WHERE name > ${last_name_previous_page}
ORDER BY name DESC
LIMIT 100

En Django: infinite scroll pagination

Fuente imágenes

El costo de un SELECT *

Fuente imagen

Evitando costos, desde Django

Escoger cuáles columnas traer

Instancias del ORM

QuerySet.defer('html_content')

QuerySet.only('id', 'name')

Listas

QuerySet.values('id', 'name')

QuerySet.values_list('id', 'name')

Consultas e Índices

Temas

Índices

Índices más interesantes

Costos

Índices

Estructura de datos auxiliar que permite ubicar/seleccionar registros de una tabla, sin tener que examinar toda la tabla.

  • Árboles B
  • Hashes
  • Datos multidimensionales
  • Full text search

Árboles B

DEMO TIME

Árboles B en bases de datos

  • O(n) -> O(log250(n))
  • == , < , >

Índices en la práctica

DEMO TIME

Tomado de engineering-blog.alphasights.com

Múltiples columnas

  • Los índices son útiles si la columna indexada tiene pocos duplicados.
  • El orden en las columnas del índice importa.
  • Dos índices por el precio de uno.

LIKE / ILIKE

DEMO TIME

LIKE / ILIKE

  • Los índices B permiten búsqueda con prefijos (LIKE 'Texto%')
  • ¿Necesidades más avanzadas? GIN/GiST - Full text search

Costos - Espacio

Un índice contiene una copia de los datos indexados, y apuntadores a los registros correspondientes en la tabla.

Costos - Mantenimiento

Todos los índices de una tabla deben ser actualizados al crear, actualizar* , o borrar registros.

Mecanismos de control de concurrencia

Temas

Actualizaciones concurrentes

Control usando bloqueo

Control usando bloqueo optimista

Delegar actualizaciones a la base de datos

Actualizaciones concurrentes

class Account(models.Model):
   amount = models.IntegerField()
   user = models.ForeignKey('User')
orig_value = Account.objects.get(id=1)
print(orig_value.amount)  # 200

first_request = Account.objects.get(id=1)   # 200
second_request = Account.objects.get(id=1)  # 200

first_request.amount += 300
second_request.amount += 100

first_request.save()   # in DB, amount = 500
second_request.save()  # in DB, amount = 300

SELECT FOR UPDATE

Impide modificaciones a los registros seleccionados, hasta que termine la transacción actual.

from django.db import transaction
with transaction.atomic():
    account = Account.objects\
      .select_for_update().get(id=1)
    account.amount += 300
    account.save()
with transaction.atomic():
    account = Account.objects\
      .select_for_update().get(id=1)
    # esperando ...
    # esperando ...
    account.amount += 100
    account.save()

Control usando bloqueo optimista

Estrategia: actualizar la columna del registro sólo si ésta no ha cambiado

SELECT amount
FROM main_account
WHERE id = 1

-- (id: 1, amount: 200)
UPDATE main_account
SET amount = 500
WHERE id = 1
  AND amount = 200

Columna de versión de registro

Estrategia general: actualizar el registro sólo si su versión no ha cambiado:

SELECT id
     , version
     , amount
FROM main_account
WHERE id = 1

-- (id: 1, amount: 200, version: 3)
UPDATE main_account
SET amount = 500, version = version + 1
WHERE id = 1
  AND version = 3  -- versión anterior

django-concurrency

https://github.com/saxix/django-concurrency

from django.db import models
from concurrency.fields import IntegerVersionField

class ConcurrentModel( models.Model ):
    version = IntegerVersionField( )
    name = models.CharField(max_length=100)
a = ConcurrentModel.objects.get(pk=1)
a.name = '1'

b = ConcurrentModel.objects.get(pk=1)
b.name = '2'

a.save()
b.save()  # lanza RecordModifiedError

Delegar actualizaciones a la base de datos

UPDATE main_account
SET amount = amount + 300
WHERE id = 1

Desde Django

https://docs.djangoproject.com/en/1.9/ref/models/expressions/#f-expressions

from django.db.models import F 

Account.objects.filter(id=1).update(amount=F('amount')+300)
UPDATE "main_account"
SET "amount" = ("main_account"."amount" + 300)
WHERE "main_account"."id" = 1

Para terminar

Para terminar

  • ¿Cuántas consultas ejecuta tu vista?
  • ¿Usan índices? ¿sequential scans?
  • ¿Es realmente importante comunicar cuántos registros tienes?
  • ¿Necesitas traer todas las columnas de tus registros?
  • ¿Es crítico evitar que tus registros sean actualizados simultáneamente?

Gracias por su atención