Django python — выгрузка результатов тяжелого запроса в CSV c разделителем

Исходные данные:

  • Джанго проект, база данных
  • Есть сложный запрос, с большим количеством данных. Часто запрос моно описать моделями, чтобы усложнить задачу, будем писать сырой запрос mysql
  • Необходимо надежно выгрузить в CSV. Может даже миллионы строк
  • первой строкой в выгрузке будет хедер с названием столбцов
  • кастомный разделитель полей, например точка с запятой

1) views.py

  • добавляем необходимые импорты в начале кода
from django.http import StreamingHttpResponse
import csv
  • файло-подобный объект, который понадобится для добавления строк запроса в выходной CSV
class Echo:
    def write(self, value):
        return value
  • сам метод выгрузки в csv
def download_csv(filename, rows):
    pseudo_buffer = Echo()
    # используем кастомный разделитель полей - точка с запятой, по умолчанию, разделитель - запятая
    writer = csv.writer(pseudo_buffer, delimiter=';')
    return StreamingHttpResponse(
        (
            writer.writerow(row) for row in rows
        ),
            content_type="text/csv",
            headers={'Content-Disposition': 'attachment; filename="' + filename + '.csv"'},
        )
  • и пример его использования:
def download_orders_for_user(request):
    # например, запрос всех заказов пользователей с айдишниками от 1 до 10000 и суммой больше 10
    query = """
        SELECT order.number, 
            order.date, 
            order.sum, 
            order.status
        FROM order
        LEFT JOIN user ON user.id = order.user_id
        WHERE user BETWEEN 1 and 10000
        AND order.sum > 10
    """

    with connections["default"].cursor() as cursor:
        cursor.execute(query)
        rows = cursor.fetchall()

    # формируем имя файла
    filename = 'orders_for_users'

    # первой строкой у нас будет хедер с названием столбцов
    header_row = (('Номер', 'Дата', 'Сумма', 'Статус'),)
    rows = header_row + rows
    return download_csv(filename, rows)

2) urls.py

Зададим 2 урла:

  • для страницы, со ссылкой на выгрузку
  • для запроса выгрузки (нажатие на ссылку выгрузки)
…
urlpatterns = [
…
path('download_page/', views.download_page, name='download_page'),
path('download_page/csv/', views.download_orders_for_user, name='download_orders_for_user'),
]

3) создадим темплейт с HTML страничкой

для размещения ссылки скачивания и проверки работы наших скриптов
download_page.html
со следующим содержимым:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Download SCV</title>
</head>
<body>
    <div>
        <h4>Скачать заказы пользователей: </h4>
        <a id="download" href="csv/">CSV</a>
    </div>
</body>
</html>

Это все!

MySQL — проблема запросов с limit и offset к очень большим таблицам и ее решение

Представим, что у нас есть таблица пользователей (users) или заказов (orders) на десятки миллионов записей и больше. И у нас стоит задача их перебора в любом контексте.

Я представляю себе это в виде цикла, в котором порционно забираем разумное количество записей, что-то вроде:

select * from users order by id limit 1000 offset 0;

Такой запрос выполняется очень быстро, моментально.

Рано или поздно мы приходим к offset, например 40 млн:

select * from users order by id limit 1000 offset 40000000;

И тут запрос может выполняться несколько минут.

Происходит это из-за того, что mysql не может гарантировать, что все элементы будут на своих порядковых местах, ведь может так статься, что элемент 40 000 099 был ранее удален и алгоритмам приходится сканировать все 40 млн элементов для получения выборки.

Решением этой проблемы может стать видоизменение запроса из limit-offset на where-limit:

 select * from users where id > 40000000 order by id limit 1000;

Выполняется моментально!

И даже, если не все элементы гарантированно присутствуют в таблице, айдишник для where легко «вытащить» из предыдущего запроса в цепочке.