MySQL, как написать SQL, чтобы найти чрезмерные транзакции в 15-минутных окнах?

MySQL
Допустим, есть компания по обработке кредитных карт. Каждый раз, когда используется кредитная карта, строка вставляется в таблицу.

create table tran(
  id int,
  tran_dt datetime, 
  card_id int,
  merchant_id int,
  amount int
);

Один хочет узнать, какие карты использовались 3 раза в любом 15-минутном окне у того же торговца.

Моя попытка:

select card_id, date(tran_dt), hour(tran_dt), merchant_id, count(*)
from tran
group by card_id, date(tran_dt), hour(tran_dt), merchant_id
having count(*)>=3

Первая проблема заключается в том, что они будут давать чрезмерные транзакции в час, а не за 15-минутное окно. Вторая проблема заключается в том, что они не будут захватывать транзакции, которые пересекают часовую отметку, т.е. в 13:59 и 14:01.

Чтобы сделать это проще, было бы удобно разделить час на 5-минутные приращения. Поэтому нам не нужно было проверять 1: 00-1:15pm, 1: 01-1:16pm и т.д. Было бы нормально проверить 1: 00-1:15pm, 1: 05-1: 20pm и т.д. если это проще.

Любые идеи по исправлению sql? У меня есть ощущение, может быть, мне нужны функции окна sql, которые еще не доступны в MySQL. Или напишите хранимую процедуру, которая может смотреть на каждый 15 блоков.

http://sqlfiddle.com/#!9/f2d74/1

Ответ 1

Вы можете преобразовать дату/время в секундах и выполнить арифметику в секундах, чтобы получить значение в течение 15-минутного интервала времени:

select card_id, min(date(tran_dt)) as first_charge_time, merchant_id, count(*)
from tran
group by card_id, floor(to_seconds(tran_dt) / (60 * 15)), merchant_id
having count(*) >= 3;

В приведенном выше примере используется to_seconds(). В более ранних версиях MySQL вы можете использовать unix_timestamp().

Получение любого 15-минутного интервала является более сложным. Вы можете выразить запрос как:

select t1.*, count(*) as numTransactions
from tran t1 join
     tran t2
     on t1.merchant_id = t2.merchanti_d and
        t1.card_id = t2.card_id and
        t2.tran_dt >= t1.tran_dt and
        t2.tran_dt < t1.tran_dt + interval 15 minute 
group by t1.id
having numTransactions >= 3;

Выполнение этого запроса может быть проблематичным. Индекс на trans(card_id, merchant_id, tran_dt) должен много помочь.

Ответ 2

SELECT t1.card_id,t1.merchant_id,count(distinct t1.id)+1 as ChargeCount
FROM tran t1 
INNER JOIN tran t2
on t2.card_id=t1.card_id 
and t2.merchant_id=t1.merchant_id 
and t2.tran_dt <= DATE_ADD(t1.tran_dt, INTERVAL 15 MINUTE)
and t2.id>t1.id
GROUP BY t1.card_id,t1.merchant_id
HAVING ChargeCount>2;

Ответ 3

Опцией может быть добавление триггера в таблицу tran для вставки, которая проверяет card_id, вставленную против предыдущих 15 минут. Если счетчик больше 3, введите его в таблицу "аудита", которую вы можете запросить на досуге.

-- create table to store audited cards    
create table audit_cards(
      card_id int,
      tran_dt datetime
    );

-- create trigger on tran table to catch the cards used 3 times in 15 min
CREATE TRIGGER audit_card AFTER INSERT ON tran
BEGIN
   if (select count(new.card_id) 
          from tran 
             where tran_dt >= (new.tran_dt - INTERVAL 15 MINUTE)) >= 3
   THEN
       INSERT new.card_id, new.tran_dt into audit_cards;
END;

Затем вы можете запустить отчет на этих карточках...

select * from audit_cards;

http://dev.mysql.com/doc/refman/5.6/en/trigger-syntax.html

Ответ 4

Мне удалось сгруппировать все строки, принадлежащие одному и тому же 15-минутному окну, без дубликатов записей в результате, используя в одном запросе.

Скажите, что у вашей таблицы есть:

| id | tran_dt             | card_id | merchant_id | amount |
|----|---------------------|---------|-------------|--------|
| 13 | 2015-07-23 16:40:00 |       1 |           1 |     10 |
| 14 | 2015-07-23 16:59:00 |       1 |           1 |     10 |  <-- these should
| 15 | 2015-07-23 17:00:00 |       1 |           1 |     10 |  <-- be identified
| 16 | 2015-07-23 17:01:00 |       1 |           1 |     10 |  <-- in the
| 17 | 2015-07-23 17:02:00 |       1 |           1 |     10 |  <-- first group
| 18 | 2015-07-23 17:03:00 |       2 |           2 |     10 |
...
| 50 | 2015-07-24 17:58:00 |       1 |           1 |     10 |  <-- and these
| 51 | 2015-07-24 17:59:00 |       1 |           1 |     10 |  <-- in the
| 52 | 2015-07-24 18:00:00 |       1 |           1 |     10 |  <-- second

Результат будет:

| id | card_id | merchant_id | numTrans | amount | dateTimeFirstTrans  | dateTimeLastTrans   
|----|---------|-------------|----------|--------|---------------------|---------------------
| 14 |       1 |           1 |        4 |     40 | 2015-07-23 16:59:00 | 2015-07-23 17:02:00 
| 50 |       1 |           1 |        3 |     30 | 2015-07-24 17:58:00 | 2015-07-24 18:00:00 

Запрос (SQL Fiddle):

select output.* from
(
    select 
        min(subquery.main_id) as id, 
        subquery.main_card_id as card_id, 
        subquery.main_merchant_id as merchant_id, 
        count(subquery.main_id) as numTrans, 
        sum(subquery.main_amount) as amount,
        min(subquery.x_timeFrameStart) as dateTimeFirstTrans, 
        max(subquery.x_timeFrameStart) as dateTimeLastTrans
    from 
    (
        select
            main.id as main_id, 
            main.card_id as main_card_id, 
            main.merchant_id as main_merchant_id, 
            main.tran_dt as main_timeFrameStart, 
            main.amount as main_amount,
            main.tran_dt + INTERVAL 15 MINUTE as main_timeFrameEnd,
            xList.tran_dt as x_timeFrameStart,
            xList.tran_dt + INTERVAL 15 MINUTE as x_timeFrameEnd
        from tran as main
            inner join tran as xList on  /* cross list */
                main.card_id = xList.card_id and
                main.merchant_id = xList.merchant_id
        where
            xList.tran_dt between main.tran_dt and main.tran_dt + INTERVAL 15 MINUTE
    ) as subquery
    group by subquery.main_id, subquery.main_card_id, subquery.main_merchant_id, subquery.main_timeFrameStart, subquery.main_timeFrameEnd
    having count(subquery.main_id) >= 3 
) as output
    left join (
        select
            xList.id as x_id
        from tran as main
            inner join tran as xList on  /* cross list */
                main.card_id = xList.card_id and
                main.merchant_id = xList.merchant_id and
                main.id <> xList.id  /* keep only first of the list */
        where
            xList.tran_dt between main.tran_dt and main.tran_dt + INTERVAL 15 MINUTE
    ) as exclude on output.id = exclude.x_id
where exclude.x_id is null;

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