MySQL огромные таблицы JOIN делает крах базы данных

Следуя моему недавнему вопросу Выберите информацию из последнего элемента и присоединитесь к общей сумме, у меня возникают проблемы с памятью, а таблицы генерации

У меня есть две таблицы sales1 и sales2 следующим образом:

id | даты | клиент | продажа

С помощью этой таблицы:

CREATE TABLE sales (
    id int auto_increment primary key, 
    dates date,
    customer int,
    sale int
);

sales1 и sales2 имеют одно и то же определение, но sales2 имеет sale=-1 в каждом поле. Клиент может быть ни в одной, ни в одной, ни в обеих таблицах. Обе таблицы имеют около 300 000 записей и гораздо больше полей, чем указано здесь (около 50 полей). Это InnoDB.

Я хочу выбрать для каждого клиента:

  • количество покупок
  • последняя стоимость покупки
  • общий объем покупок, когда он имеет положительное значение

Я использую следующий запрос:

SELECT a.customer, count(a.sale), max_sale
FROM sales a
INNER JOIN (SELECT customer, sale max_sale 
        from sales x where dates = (select max(dates) 
                                    from sales y 
                                    where x.customer = y.customer
                                    and y.sale > 0
                                   )

       )b
ON a.customer = b.customer
GROUP BY a.customer, max_sale;

Проблема заключается в следующем:

Мне нужно получить результаты, которые мне нужны для определенных вычислений, разделенных для дат: информация о 2012 году, информация о 2013 году, но также информация из всех лет вместе.

Всякий раз, когда я делаю всего один год, для хранения всей информации требуется около 2-3 минут.

Но когда я пытаюсь собрать информацию за все годы, база данных сбой, и я получаю сообщения вроде:

InternalError: (InternalError) (1205, u'Lock wait timeout exceeded; try restarting transaction')

Кажется, что объединение таких огромных таблиц слишком много для базы данных. Когда я explain запрос, почти весь процент времени исходит от creating tmp table.

Я думал о разделении сбора данных в кварталах. Мы получаем результаты каждые три месяца, а затем присоединяемся и сортируем их. Но я думаю, что это окончательное соединение и сортировка будут слишком много для базы данных снова.

Итак,, что бы вы рекомендовали специалистам оптимизировать эти запросы до тех пор, пока я не могу изменить структуру таблиц?

Ответ 1

300 тыс. строк - это не огромная таблица. Мы часто видим 300 миллионов таблиц строк.

Самая большая проблема с вашим запросом заключается в том, что вы используете коррелированный подзапрос, поэтому он должен повторно выполнить подзапрос для каждой строки во внешнем запросе.

Часто бывает, что вам не нужно выполнять всю свою работу в одном выражении SQL. Есть преимущества разбить его на несколько простых операторов SQL:

  • Легче кодировать.
  • Легче оптимизировать.
  • Легче отлаживать.
  • Легче читать.
  • Легче поддерживать, если/когда вам нужно выполнять новые требования.

Количество покупок

SELECT customer, COUNT(sale) AS number_of_purchases
FROM sales 
GROUP BY customer;

Для этого запроса лучше всего использовать индекс продаж (клиент, продажа).

Последняя покупка

Это проблема greatest-n-per-group, которая часто возникает.

SELECT a.customer, a.sale as max_sale
FROM sales a
LEFT OUTER JOIN sales b
 ON a.customer=b.customer AND a.dates < b.dates
WHERE b.customer IS NULL;

Другими словами, попробуйте сопоставить строку a с гипотетической строкой b с тем же клиентом и более высокой датой. Если такой строки не найдено, то a должен иметь наибольшую дату для этого клиента.

Для этого запроса лучше всего использовать индекс продаж (клиент, даты, продажа).

Если у вас может быть более одной продажи для клиента в эту самую большую дату, этот запрос вернет более одной строки для каждого клиента. Вам нужно будет найти еще один столбец, чтобы сломать галстук. Если вы используете первичный ключ с автоматическим приращением, он подходит как тай-брейкер, потому что он гарантированно уникален и имеет тенденцию увеличиваться в хронологическом порядке.

SELECT a.customer, a.sale as max_sale
FROM sales a
LEFT OUTER JOIN sales b
 ON a.customer=b.customer AND (a.dates < b.dates OR a.dates = b.dates and a.id < b.id)
WHERE b.customer IS NULL;

Общий объем покупок, когда он имеет положительное значение

SELECT customer, SUM(sale) AS total_purchases
FROM sales
WHERE sale > 0
GROUP BY customer;

Для этого запроса лучше всего использовать индекс продаж (клиент, продажа).

Вам следует рассмотреть возможность использования NULL для обозначения отсутствующей стоимости продажи вместо -1. Совокупные функции, такие как SUM() и COUNT() игнорируют NULL, поэтому вам не нужно использовать предложение WHERE, чтобы исключить строки с продажей < 0.


Re: ваш комментарий

Теперь у меня есть таблица с полями год, квартал, total_sale (относительно пары (год, квартал)) и продажа. То, что я хочу собрать, - это информация о определенном периоде: в этом квартале, кварталах, году 2011... Информация должна быть разделена на лучших клиентов, с большими продажами и т.д. Можно ли получить последнюю покупную стоимость у клиентов с помощью total_purchases больше 5?

Пять лучших клиентов за четвертый квартал 2012 года

SELECT customer, SUM(sale) AS total_purchases
FROM sales
WHERE (year, quarter) = (2012, 4) AND sale > 0
GROUP BY customer
ORDER BY total_purchases DESC
LIMIT 5;

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

Последняя покупка для покупателей с полной покупкой > 5

SELECT a.customer, a.sale as max_sale
FROM sales a
INNER JOIN sales c ON a.customer=c.customer
LEFT OUTER JOIN sales b
 ON a.customer=b.customer AND (a.dates < b.dates OR a.dates = b.dates and a.id < b.id)
WHERE b.customer IS NULL
GROUP BY a.id
HAVING COUNT(*) > 5;

Как и в предыдущем запросе наибольшего числа n-групп, индекс для продаж (клиент, даты, продажа) был бы лучшим для этого запроса. Вероятно, он не может оптимизировать как объединение, так и группу, поэтому это приведет к временной таблице. Но по крайней мере, это будет делать только временную таблицу вместо многих.


Эти запросы достаточно сложны. Вы не должны пытаться написать один SQL-запрос, который может дать все эти результаты. Вспомните классическую цитату Брайана Кернигана:

Всем известно, что отладка в два раза сложнее, чем запись программы в первую очередь. Итак, если вы настолько умны, насколько можете быть, когда пишете его, как вы его отлаживаете?

Ответ 2

Я думаю, вам стоит попробовать добавить индекс на sales(customer, date). Подзапрос, вероятно, является узким местом производительности.

Ответ 3

Вы можете сделать этот щенок криком. Сбросьте весь запрос внутреннего соединения. В самом деле. Это трюк, о котором практически никто не знает.

Предполагая, что dates - это datetime, конвертировать его в сортируемую строку, конкатенировать нужные значения, max (или мин), подстрока, литье. Возможно, вам придется настроить функцию преобразования даты (эта работает в MS-SQL), но эта идея будет работать где угодно:

SELECT customer, count(sale), max_sale = cast(substring(max(convert(char(19), dates, 120) + str(sale, 12, 2)), 20, 12) as numeric(12, 2))
FROM sales a 
group by customer

Вуаля. Если вам нужно больше столбцов результатов, выполните следующие действия:

SELECT yourkey
            , maxval = left(val, N1)                  --you often won't need this
            , result1 = substring(val, N1+1, N2)
            , result2 = substring(val, N1+N2+1, N3)   --etc. for more values
FROM ( SELECT yourkey, val = max(cast(maxval as char(N1))
                               + cast(resultCol1 as char(N2))
                               + cast(resultCol2 as char(N3)) )
       FROM yourtable GROUP BY yourkey ) t

Убедитесь, что у вас есть фиксированная длина для всех, кроме последнего поля. Это требует немного работы, чтобы окунуться, но очень узнаваемо и повторяемо. Он будет работать на любом движке базы данных, и даже если у вас есть функции рангов, это часто значительно превзойдет их.

Подробнее об этой очень распространенной проблеме здесь.