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;
Запрос немного длинный, и он повторяет один подзапрос только для фильтрации дубликатов, так что сделайте свое тестирование и настройку, чтобы убедиться, что вы не несете проблемы с производительностью.