Pull to refresh

Лишние join в SQL запросах

Reading time 2 min
Views 13K
Делая отладку производительности небольшого проекта, но с достаточно большой базой, столкнулся с неприятным спецэффектом.
Django при выборках с условиями по внешним ключам, связанным с проверкой на NULL, генерирует запросы, содержащие JOIN по каждому такому ключу. К примеру, для модели

class ForumPlugin(models.Model):
    name = models.CharField(
        null=False, 
        blank=False, 
        max_length=50, 
        unique=True, 
        verbose_name=_('name')
    )

class Thread(MPTTModel):   
    parent = TreeForeignKey(
        'self', 
        null=True, 
        blank=True, 
        related_name='children', 
        verbose_name=_('parent thread')
    )
    plugin = models.ForeignKey(
        ForumPlugin, 
        null=True, 
        blank=True, 
        related_name='threads', 
        verbose_name=_('plugin')
    )

При выполнении выборки

Thread.objects.filter(plugin__isnull=True, parent__isnull=True)

Djando формирует такой запрос:

SELECT `forum_thread`.`id`, `forum_thread`.`parent_id`, `forum_thread`.`plugin_id`, `forum_thread`.`lft`, `forum_thread`.`rght`, `forum_thread`.`tree_id`, `forum_thread`.`level` FROM `forum_thread` LEFT OUTER JOIN `forum_thread` T2 ON (`forum_thread`.`parent_id` = T2.`id`) LEFT OUTER JOIN `forum_forumplugin` ON (`forum_thread`.`plugin_id` = `forum_forumplugin`.`id`) WHERE (T2.`id` IS NULL AND `forum_forumplugin`.`id` IS NULL AND ) ORDER BY `forum_thread`.`id

Естественно время исполнения такого запроса увеличивается на несколько порядков, что при больших таблицах может быть критично. Так на моем проекте на таблице порядка 20-30к записей такая выборка вместо положенной 1мс выполняется от 100мс до 300мс, что увеличивает время генерации страницы вдвое.

К сожалению бага разработчикам ORM известна уже четыре года и имеет долгую и печальную историю.

В данный момент присутствует во всех стабильных версиях, в том числе в 1.4.3. Предполагается, что в 1.5 она наконец-то будет исправлена.

В качестве обходного пути советуют использовать двойное отрицание:

Thread.objects.exclude(plugin__isnull=False, parent__isnull=False)

но мне не удалось на практике таким способом избавится от проблемы. Обращение напрямую к полю parent_id также не помогает.

Будьте внимательны при проектировании моделей и старайтесь учитывать эту особенность Django, и избегать выборок по внешним ключам с использованием NULL условий.

UPD: Найдено решение:

Category.objects.extra(where=['parent_id IS NULL'])

Использовать raw sql это конечно дурной тон, но судя по всему это единственное решение.
Tags:
Hubs:
+19
Comments 25
Comments Comments 25

Articles