За последние 24 часа нас посетили 24537 программистов и 1681 робот. Сейчас ищут 843 программиста ...

Задачка на написание запроса к БД

Тема в разделе "MySQL", создана пользователем artoodetoo, 27 мар 2014.

  1. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.128
    Симпатии:
    1.248
    Адрес:
    там-сям
    Есть у меня в реальной работе задача. Уровень "продвинутый", лишних прошу не беспокоить.
    Некое решение я сделал, но чувствую, что есть другие достойные способы. Чтобы не подталкивать вас к определенному ходу мыслей, своё я покажу позже.

    Дано:
    Таблица "событий". Пускай это извещения о совершенных заказах.
    Код (Text):
    1. CREATE TABLE `events` (
    2.   `id` int(10) NOT NULL AUTO_INCREMENT,
    3.   `event_time` DATETIME NOT NULL,
    4.   `event_type` enum('order','pay') NOT NULL DEFAULT 'order',
    5.   `order_no` varchar(10) NOT NULL DEFAULT '',
    6.   `order_sum` decimal(10,2) NOT NULL DEFAULT '0',
    7.   PRIMARY KEY (`id`),
    8.   KEY (`order_no`),
    9.   KEY (`event_type`,`event_time`)
    10. )
    Любой заказ имеет одну запись типа 'order' и любое количество, от 0 до N записей 'pay'. Мы можем сгруппировать записи одного заказа по order_no, поле уникально указывает на заказ. Поле event_time у разных записей одного заказа будет разное, оно соответствует моменту оформления либо оплаты. Поле order_sum содержит 0 в записях типа order и не 0 в записях типа pay.

    Требуется:
    Создать выборку за определенный период записей вида
    Код (Text):
    1. День, когда был оформлен заказ | Количество созданных| Количество оплаченных
    если в какой-то день не было заказов, строчка должна быть, только с нулями. Если оплата произошла не в день оформления, а позже — данные всё равно относятся ко дню оформления.

    Умеренное участие PHP допустимо. Хорошее решение — только средствами SQL.
     
  2. runcore

    runcore Старожил

    С нами с:
    12 окт 2012
    Сообщения:
    3.625
    Симпатии:
    158
    непонял. а как понять оплачен заказ полностью или нет? ведь сумма заказа при order равна нулю. а количество pay может быть любым и с любыми суммами. в какой момент мы узнаем что заказ оплачен полностью? order_type тоже никак не поможет. ибо там нет типа 'ОПЛАЧЕНО'

    или сумма платежа 'pay' всегда равна сумме заказа 'order'? тоесть оплачивать частями заказ нельзя?
    так ведь в самом заказе 'order' сумма == 0. непонятно чета
     
  3. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.128
    Симпатии:
    1.248
    Адрес:
    там-сям
    это не таблица заказов, это "события", ок? от таблицы требуется то, что я описал, не больше. я не прошу придумывать другие задачи.
     
  4. runcore

    runcore Старожил

    С нами с:
    12 окт 2012
    Сообщения:
    3.625
    Симпатии:
    158
    хорошо. что такое 'Количество оплаченных' ?
    заказ считается оплаченным если .....?
     
  5. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.128
    Симпатии:
    1.248
    Адрес:
    там-сям
    заказ считается оплаченным если по нему была оплата(ы) на любую сумму. для зануд: бывают только положительные суммы оплаты )))

    Добавлено спустя 3 минуты 28 секунд:
    "количество созданных" это количество уникальных order_no
    "количество оплаченных" это количество уникальных order_no, по которым был хотябы один факт оплаты.
     
  6. romach

    romach Старожил

    С нами с:
    26 окт 2013
    Сообщения:
    2.904
    Симпатии:
    719
    Дал бы хоть записей чуток на тест )
     
  7. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.128
    Симпатии:
    1.248
    Адрес:
    там-сям
    набросал несколько игровых записей: http://sqlfiddle.com/#!2/8c80da

    Добавлено спустя 1 минуту:
    в примере даты ровные, только из-за моей лени. в реале время присутствует.
     
  8. runcore

    runcore Старожил

    С нами с:
    12 окт 2012
    Сообщения:
    3.625
    Симпатии:
    158
    сорри, у меня щас тока оракл под рукой) первый вариант такой
    Код (PHP):
    1. WITH t AS ( -- orders
    2.             SELECT 1 ID, to_date('05.03.2014 01:00:10','dd.mm.yyyy hh24:mi:ss') event_time, 'order' event_type, 'z1' order_no, 0 order_sum FROM dual
    3.   UNION ALL SELECT 2 ID, to_date('05.03.2014 02:00:20','dd.mm.yyyy hh24:mi:ss') event_time, 'order' event_type, 'z2' order_no, 0 order_sum FROM dual
    4.   UNION ALL SELECT 3 ID, to_date('06.03.2014 03:00:20','dd.mm.yyyy hh24:mi:ss') event_time, 'order' event_type, 'z3' order_no, 0 order_sum FROM dual
    5.   UNION ALL SELECT 4 ID, to_date('07.03.2014 03:00:20','dd.mm.yyyy hh24:mi:ss') event_time, 'order' event_type, 'z4' order_no, 0 order_sum FROM dual
    6.   UNION ALL SELECT 5 ID, to_date('07.03.2014 03:00:20','dd.mm.yyyy hh24:mi:ss') event_time, 'order' event_type, 'z5' order_no, 0 order_sum FROM dual
    7.   UNION ALL SELECT 6 ID, to_date('10.03.2014 03:00:20','dd.mm.yyyy hh24:mi:ss') event_time, 'order' event_type, 'z6' order_no, 0 order_sum FROM dual
    8.   UNION ALL SELECT 7 ID, to_date('10.03.2014 03:00:20','dd.mm.yyyy hh24:mi:ss') event_time, 'order' event_type, 'z7' order_no, 0 order_sum FROM dual
    9.   -- pays
    10.   UNION ALL SELECT 10 ID, to_date('05.03.2014 01:00:20','dd.mm.yyyy hh24:mi:ss') event_time, 'pay' event_type, 'z1' order_no, 100 order_sum FROM dual
    11.   UNION ALL SELECT 11 ID, to_date('06.03.2014 02:00:20','dd.mm.yyyy hh24:mi:ss') event_time, 'pay' event_type, 'z2' order_no, 25 order_sum FROM dual
    12.   UNION ALL SELECT 12 ID, to_date('07.03.2014 02:00:20','dd.mm.yyyy hh24:mi:ss') event_time, 'pay' event_type, 'z3' order_no, 10 order_sum FROM dual
    13.   UNION ALL SELECT 13 ID, to_date('08.03.2014 02:00:20','dd.mm.yyyy hh24:mi:ss') event_time, 'pay' event_type, 'z3' order_no, 15 order_sum FROM dual
    14.   UNION ALL SELECT 14 ID, to_date('10.03.2014 02:00:20','dd.mm.yyyy hh24:mi:ss') event_time, 'pay' event_type, 'z6' order_no, 55 order_sum FROM dual
    15. )
    16. SELECT tt.dd
    17.   ,( SELECT count(*) FROM t t1 WHERE t1.event_type IN ('order') AND to_char(t1.event_time,'dd.mm.yyyy')=tt.dd ) cnt_created
    18.   ,( SELECT count(*)
    19.     FROM t t1 
    20.     WHERE t1.event_type IN ('order') AND to_char(t1.event_time,'dd.mm.yyyy')=tt.dd
    21.       AND t1.order_no IN ( SELECT order_no FROM t WHERE event_type IN ('pay') )
    22.   ) cnt_payed
    23. from (
    24.   SELECT to_char(event_time,'dd.mm.yyyy') dd
    25.   FROM t 
    26.   WHERE event_type IN ('order') 
    27.     AND event_time between to_date('01.03.2014','dd.mm.yyyy') and to_date('11.03.2014','dd.mm.yyyy')
    28.   GROUP BY to_char(event_time,'dd.mm.yyyy')
    29.   ORDER BY 1
    30. ) tt
    результат
    05.03.2014 2 2
    06.03.2014 1 1
    07.03.2014 2 0
    10.03.2014 2 1

    перевести в мускул вроде непроблема
     
  9. romach

    romach Старожил

    С нами с:
    26 окт 2013
    Сообщения:
    2.904
    Симпатии:
    719
    Ночь не спал, потому могу слегка тупить )
    http://sqlfiddle.com/#!2/6d4e7/2/0

    вообще ни одной мысли на эту тему )
     
  10. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.128
    Симпатии:
    1.248
    Адрес:
    там-сям
    runcore, ты мог воспользоваться моей ссылкой на sqlfiddle. извини, но я не буду пробовать твой код. 20 штук union ?…

    romach, выглядит очень правдоподобно. цифры кажется верные, НО!
    1)
    Код (Text):
    1. SELECT `event_time`, `order_no`, ...  GROUP BY `order_no`
    работают обманчиво. откуда будет взят event_time ? это очень хрупкий код, непредсказуемый.
    2) вот это не учитывается:
    3) IMHO, с count(if()...) перебор. можно проще
     
  11. runcore

    runcore Старожил

    С нами с:
    12 окт 2012
    Сообщения:
    3.625
    Симпатии:
    158
    сразу ты ссылку не дал. поэтому я начал деалть у себя.
    унионы это просто создание таблички тестовой. сам запрос внизу.
     
  12. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.128
    Симпатии:
    1.248
    Адрес:
    там-сям
    а это можно адекватно перевести на mysql?

    Добавлено спустя 1 минуту 46 секунд:
    кто убил sqlfiddle, признавайтесь! сперва был недоступен, а сейчас вижу джавовский бэктрейс. катастрофа блин.
     
  13. romach

    romach Старожил

    С нами с:
    26 окт 2013
    Сообщения:
    2.904
    Симпатии:
    719
    1. Можно взять Min(event_time), т.е. время события 'order'. Оплата же не может быть произведена раньше заказа?
    2. хз, мыслей нет как запросом это оформить. Подозреваю, что лучше это на пхп переложить, пробежаться циклом и вставить даты. Все же не свойственно для БД, выбирать то чего нет )
    3. Можно сократить на пару count: http://sqlfiddle.com/#!2/6d4e7/9/0

    з.ы. не забудь итоговый вариант выложить )
     
  14. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.128
    Симпатии:
    1.248
    Адрес:
    там-сям
    1. точно. я так и делаю
    2. можно изготовить последовательность дат. я даже подсказывал здесь недавно
    3. ага. красота!

    не забуду. не факт, что он будет лучший. я остаюсь в выигрыше в любом случае: или самолюбие потешу, или чему-то научусть )

    ладно, вот промежуточный результат без генератора дат - для сравнения: http://sqlfiddle.com/#!2/da9fed/13
    генератор вместо первого вложенного запроса ставится.
     
  15. romach

    romach Старожил

    С нами с:
    26 окт 2013
    Сообщения:
    2.904
    Симпатии:
    719
    Как я понял, там либо процедуры, либо временные таблицы. Чуйка подсказывает мне, что в данном случае это костыль и проще пыхом дополнить недостающие даты. Впрочем, возможно я недостаточно раскурил тему )

    такое же мнение )

    Добавлено спустя 2 минуты 5 секунд:
    хех, осталось только нагенерить тестовую таблицу и посмотреть чей вариант быстрее ))
     
  16. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.128
    Симпатии:
    1.248
    Адрес:
    там-сям
    один из вариантов:
    viewtopic.php?f=20&t=47888&p=380576#p380485
    как исходник нужна последовательность чисел. я для отчетов держу такую от 0 до 9999, очень полезная штука

    можно и по другому, на переменной mysql как на счетчике — c любой базовой последовательностью, например можно служебную таблицу mysql взять достаточно большую

    Добавлено спустя 2 минуты 16 секунд:
    p.s. тут важнее красота, я считаю. если в отчете до сотни тысяч записей вообще не надо париться про скорость. это не то, что при каждом заходе пользователя генериться, тут допустимо пару сотых секунды подождать.
     
  17. runcore

    runcore Старожил

    С нами с:
    12 окт 2012
    Сообщения:
    3.625
    Симпатии:
    158
    Код (PHP):
    1. SELECT t.*
    2. ,( SELECT count(*) FROM `events` e1 WHERE e1.`event_type`='order' 
    3.   AND DATE(e1.`event_time`)=t.dd AND e1.`order_no` IN (
    4.     SELECT e2.`order_no` FROM `events` e2 
    5.     WHERE e2.`event_type`='pay' AND e2.`order_no`=e1.`order_no`
    6.  ) ) `payed`
    7. FROM (
    8.   SELECT DATE(`event_time`) dd, count(`order_no`) `ordered`
    9.   FROM `events` 
    10.   WHERE `event_type`='order' 
    11.   GROUP BY 1
    12. ) t
     
  18. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.128
    Симпатии:
    1.248
    Адрес:
    там-сям
    спасибо! запрос работает, хотя план выполнения ужасен. по поводу "красоты" — он слишком уж в лоб сделан. для разового решения это нормально, а в постоянную работу я такие штуки не ставлю. это моё сугубое мнение.
     
  19. runcore

    runcore Старожил

    С нами с:
    12 окт 2012
    Сообщения:
    3.625
    Симпатии:
    158
    еще гибрид
    Код (PHP):
    1. SELECT DATE(e3.`ev_time`) `dt`
    2.   ,count(e3.`order_no`) `ORDERED`
    3.   ,sum(e3.`p_sum`) `payed`
    4. from (
    5.   SELECT e2.`order_no`
    6.     ,MIN(e2.`event_time`) `ev_time`
    7.     ,IF(SUM(e2.`p_flag`)<>0,1,0) `p_sum`
    8.   FROM (
    9.     SELECT e.`event_time`, e.`order_no`
    10.       ,IF(e.`order_sum`>0,1,0) `p_flag`
    11.     FROM `events` e
    12.   ) e2 GROUP BY e2.`order_no`
    13. ) e3 GROUP BY 1
     
  20. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.128
    Симпатии:
    1.248
    Адрес:
    там-сям
    Ну видимо все желающие и могущие уже поучаствовали :) Подвожу итоги.
    Вариант romach мне понравился. По сути там та же техника, что и у меня — используется свойство функции COUNT(col) считать только не NULL значения col. Нет второй вложенной таблицы, но она появится если все-таки сделать "пустые даты". В моем варианте подготовка к этому уже есть. Вот и вся разница по сути.
    В варианте runcore вместо COUNT() есть SUM(). Так как суммируются единички, то результат аналогичен. Тоже вариант, почему бы нет. Все справились, в общем )

    Завершаю квест полным решением. Один из вариантов генерации последовательности дат я нашел на stackoverflow: date range: no loops, procedures, or temp tables.
    Элегантно! Точно также можно генерировать любые числовые или временны́е последовательности: (start + i * step). Работает это быстро и не надо всякий раз создавать новую таблицу. Я у себя создал вьюшку как источник чисел, вот и на sqlfiddle сделаю через вьюшку.

    Код (Text):
    1. SELECT
    2.   m.`date`,
    3.   COUNT(e.`event_time`) AS `ordered`,
    4.   COUNT(e.p) AS `paid`
    5. FROM
    6.   (
    7.     SELECT (DATE('2014-03-03') + INTERVAL `number` DAY) AS `date`
    8.     FROM `numbers`
    9.     WHERE `number` < 14
    10.   ) AS m LEFT JOIN
    11.   (
    12.      SELECT
    13.        IF(SUM(`order_sum`) > 0, 1, null) AS p,
    14.        MIN(`event_time`) AS `event_time`
    15.      FROM `events`
    16.      GROUP BY `order_no`
    17.    ) AS e ON e.`event_time` BETWEEN m.`date` AND TIMESTAMP(m.`date`, '23:59:59')
    18. GROUP BY 1
    http://sqlfiddle.com/#!2/7a84e/5