It's over 9000!

Ma page fait 9000 requêtes SQL, pourquoi, et comment je m’en sors ?

Rencontres Django a.k.a #djangocong

14 avril 2012

Presenter Notes

Le yin et le yang de l'ORM

what9000.png

Presenter Notes

Qui suis-je ?

  • Marc Hertzog @_kemar
  • Développeur Web freelance http://marcarea.com/
  • Pythoniste et Djangonaute
  • Intervenant sur le projet Autolib’
autolib.strike.team.png

Presenter Notes

Voir les requêtes générées par l'ORM

1) Dans le navigateur web : django-debug-toolbar

2) Dans le shell Django ./manage.py shell

>>> from django.db import connection  # DEBUG = True
>>> connection.queries
>>> connection.queries = []  # reset

3) Dans la sortie de runserver >= 1.3

import logging
l = logging.getLogger('django.db.backends')
l.setLevel(logging.DEBUG)
l.addHandler(logging.StreamHandler())

4) Directement dans les fichiers de log du SGBD

log_statement all >> /etc/postgresql/postgresql.conf

[mysqld] log=/tmp/mysql.log >> /etc/mysql/my.cnf

Presenter Notes

Modèles d'exemple

class Car(models.Model):

    is_locked = models.BooleanField(default=False)

    paired_with = models.ForeignKey(RfidCard, related_name='paired_cars')

    ...

class CarTelemetry(models.Model):

    car = models.ForeignKey(Car, related_name='telemetry_set')  # many-to-one

    time = LocalDateTimeField(default=datetime.datetime.now)

    ...

Presenter Notes

Cas des clés étrangères

Pour chaque Car, récupèrer la carte RFID associée

<ul>
{% for car in cars %}
    <li>{{ car.paired_with.get_kind_display }} : IT'S OVER 9000!!!!!!</li>
{% endfor %}
</ul>

Presenter Notes

Solution : select_related()

  • MyModel.objects.select_related().all()
  • limité aux foreign keys et aux one-to-one
  • ne suit pas les relations avec null=True
  • MyModel.objects.select_related('model1__model2').all()
  • select_related(depth=2) contrôle le nombre de niveaux

Shortcut :

get_object_or_404(MyModel.objects.select_related(), field=value)

Exemples :

cars = Car.objects.select_related('paired_with')

telemetries = CarTelemetry.objects.select_related('car__paired_with')

Presenter Notes

Cas des relations inverses

Pour chaque Car, récupèrer les n télémétries associées

<div>
{% for car in cars %}
    <ul>
    {% for telemetry in car.telemetry_set.all %}
        <li>IT'S OVER 9000!!!!!!</li>
    {% endfor %}
    </ul>
{% endfor %}
</div>

Rappel du modèle :

class CarTelemetry(models.Model):

    car = models.ForeignKey(Car, related_name='telemetry_set')

    ...

Presenter Notes

Solution : simuler les joints SQL complexes à la main < 1.4

cars = Car.objects.filter(is_locked=False)

telemetries = CarTelemetry.objects.filter(car__is_locked=False)

telemetries_by_car = {}
for t in telemetries:
    telemetries_by_car.setdefault(t.car.pk, []).append(t)

# {18: [<CarTelemetry: 2012-02-29 10:44:36>,
#       <CarTelemetry: 2012-02-29 10:50:36>,
#       <CarTelemetry: 2012-02-29 10:54:38>]}

for car in cars:
    car.telemetries = telemetries_by_car.get(car.id)

Presenter Notes

Solution : prefetch_related() >= 1.4

cars = Car.objects.filter(
        is_locked=False,
    ).prefetch_related(
        'telemetry_set',
    )
  • Exécute plusieurs requêtes SQL et fait la jointure en Python
  • Fonctionne avec des many-to-many et des many-to-one
  • Fonctionne avec des GenericForeignKey
  • Attention à la consommation mémoire !

Presenter Notes

Tester avec assertNumQueries

class CarTestCase(TestCase):

    def test_its_under_9000(self):

        with self.assertNumQueries(3):
            response = self.client.get(reverse('cars'))
            self.assertEqual(response.status_code, 200)

Presenter Notes

Aller plus loin

  • Tester avec un jeu de données de production
  • db_index=True ajouter des Index SQL qui réduisent le nombre de candidats
  • Passer au raw SQL
  • Dénormaliser
  • Envisager d'autres solutions : Haystack (Solr, Elasticsearch, Whoosh, Xapian)
  • NoSQL

Presenter Notes