Выполнение необработанного SQL перед каждым select для конкретной модели

У меня есть сложный отчет в Django, который пишется как сырой SQL, а затем сохраняется в базе данных как представление базы данных.

У меня есть модель Django (managed=False), привязанная к представлению базы данных, так что я могу использовать django ORM и django rest framework поверх представления базы данных. Эту практику я использовал в нескольких проектах, и она отлично работает.

Для одного конкретного отчета я обнаружил, что мне нужно вмешаться в планировщик запросов postgres, чтобы заставить его работать быстрее. Перед каждым оператором SELECT для этой модели (представления базы данных), чтобы SQL выполнялся быстрее, мне нужно сделать:

SET enable_nestloop TO off;

Запрос довольно сложный, с агрегатными функциями и объединениями подзапросов, поэтому я отказался от попыток его оптимизировать. Отключение nestloop решает все мои проблемы с производительностью, и меня это устраивает, если только я включу его обратно после выполнения запроса. Вот объяснение проблемы и ее решение. ( Каковы подводные камни при установке enable_nestloop в OFF)

На данный момент я оборачиваю запрос Report в транзакцию базы данных и устанавливаю enable_nestloop только в этой транзакции, с SET LOCAL.

with transaction.atomic(), connection.cursor() as cursor:
    cursor.execute("SET LOCAL enable_nestloop = off;")
    queryset = self.__build_queryset(session, request.query_params)

    serializer = MySerializer(queryset, many=True)
    response_data = serializer.data
    
return Response(response_data)

Это работает хорошо, но мне нужно не забывать выполнять эту логику каждый раз перед выполнением оператора SELECT для этой конкретной модели, иначе это будет мучительно медленно.

Я ищу метод, который позволит мне выполнять логику параметра SET перед каждым запросом SELECT в этой модели, а затем устанавливать параметр обратно в исходное значение. Можно ли это сделать на уровне модели или менеджера?

Типа того.

Вероятно, вы не захотите вставлять его в менеджер get_queryset() напрямую, потому что, как вы, вероятно, знаете, кверисеты ленивы, и это приведет к отключению nestloop за некоторое произвольное время до того, как вы действительно обратитесь к базе данных с курсором. Это увеличивает вероятность того, что любые параллельные или одновременные запросы будут страдать от последствий отключения nestloop, несмотря на то, что вы фактически не обращались к запросу, который выиграл бы от отключения nestloop.

Вы можете переопределить некоторые методы менеджера, но это будет утомительно, и вы не сможете эффективно использовать .filter() (так как он возвращает ленивый кверисет, что приводит к той же проблеме, о которой говорилось выше). Если вам нужно получать только отдельные элементы, то перегрузки .get() менеджера, вероятно, будет достаточно.

Более общий (и, IMO, более поддерживаемый, более django-esque) подход заключается в переопределении самого кверисета и откладывании обновления настроек до тех пор, пока вы действительно не обратитесь к базе данных. Это также дает дополнительный бонус в том, что вы можете использовать это же решение для любой модели, над которой вам нужно поработать, просто изменив ее менеджер, как показано ниже.

class NestloopQuerySet(QuerySet):
    _nestloop_sql_enable_string  = "SET enable_nestloop TO on;"
    _nestloop_sql_disable_string = "SET enable_nestloop TO off;"

    def _fetch_all(self):
        with connection.cursor() as cursor:
            try:
                cursor.execute(self._nestloop_sql_disable_string)
                super()._fetch_all()
            finally:
                cursor.execute(self._nestloop_sql_enable_string)

class NestloopModelManager(models.Manager):
    def get_queryset(self):
        return NestloopQuerySet(self.model, using=self._db)

QuerySet._fetch_all() лежит в основе всех способов поиска информации в базе данных, поэтому перегрузки этого метода должно быть достаточно для всех возможных случаев использования, включая итерацию.

Вернуться на верх