MySQL GROUP BY DateTime +/- 3 секунды

Предположим, у меня есть таблица с 3 столбцами:

  • идентификатор (ПК, интервал)
  • метка времени (дата-время)
  • название (текст)

У меня есть следующие записи:

1, 2010-01-01 15:00:00, Some Title
2, 2010-01-01 15:00:02, Some Title
3, 2010-01-02 15:00:00, Some Title

Мне нужно сделать записи GROUP BY, которые находятся в пределах 3 секунд друг от друга. Для этой таблицы строки 1 и 2 будут сгруппированы вместе.

Здесь есть аналогичный вопрос: группа Mysql DateTime на 15 минут

Я также нашел это: http://www.artfulsoftware.com/infotree/queries.php# 106

Я не знаю, как преобразовать эти методы во что-то, что будет работать в течение нескольких секунд. Проблема с методом в вопросе SO заключается в том, что мне кажется, что он будет работать только для записей, попадающих в интервал времени, который начинается в известной точке. Например, если бы мне нужно было заставить FLOOR() работать с секундами с интервалом в 5 секунд, время 15:00:04 было бы сгруппировано с 15:00:01, но не сгруппировано с 15:00:06.

Имеет ли это смысл? Пожалуйста, дайте мне знать, если необходимы дополнительные разъяснения.

EDIT: Для набора чисел {1, 2, 3, 4, 5, 6, 7, 50, 51, 60} лучше сгруппировать их {1, 2 , 3, 4, 5, 6, 7}, {50, 51}, {60}, чтобы каждая строка группировки зависела от того, находится ли строка в пределах 3 секунд от предыдущей. Я знаю, что это немного меняет дело.

Я пытаюсь нечетко сопоставить журналы с разных серверов. Сервер № 1 может зарегистрировать элемент «Элемент № 1», а сервер № 2 зарегистрирует тот же элемент «Элемент № 1» в течение нескольких секунд после сервера № 1. Мне нужно выполнить некоторые агрегатные функции в обеих строках журнала. К сожалению, из-за характера серверного программного обеспечения у меня есть только заголовок.


person Brad    schedule 01.07.2011    source источник
comment
еще неоднозначно. если бы секунды были 1,2,3,4,5,6, то возможно много группировок по 3 секунды, где любая данная строка может быть в нескольких группах...   -  person Randy    schedule 01.07.2011
comment
@ Рэнди, отличное замечание. Уточнение...   -  person Brad    schedule 01.07.2011
comment
Причина, по которой вам нужно выбирать разное время, заключается в том, что в противном случае группировка в пределах 3 секунд, вероятно, не имеет особого смысла. Что, если бы у вас была серия из 30 строк, каждая ровно через 2 секунды после предыдущей? Теперь первый и последний разнесены почти на минуту, но будут сгруппированы из-за цепочки рядов.   -  person Tom H    schedule 01.07.2011
comment
Вы хотите, чтобы все записи с разницей в 3 секунды были сгруппированы вместе? т. е. 0:01,0:02,0:03,0:04 группы в одну группу?   -  person dfb    schedule 01.07.2011
comment
@Tom H., @spinning_plate, хорошие моменты, пожалуйста, посмотрите мое последнее редактирование, которое должно дать лучшее представление о том, чего я пытаюсь достичь.   -  person Brad    schedule 01.07.2011
comment
В Oracle я бы, вероятно, подошел к этому как к функции LAG. Проверьте каждую строку, чтобы увидеть, соответствует ли она предыдущей строке в течение 3 секунд. Если это так, установите «группу» на отметку времени этой предыдущей строки, если нет, используйте отметку времени текущей строки.   -  person Randy    schedule 01.07.2011
comment
@ Рэнди, звучит как отличная идея. Вы знаете, можно ли это реализовать или сымитировать в MySQL?   -  person Brad    schedule 01.07.2011
comment
@Randy: К сожалению, в MySQL никогда не было аналитических функций.   -  person OMG Ponies    schedule 01.07.2011
comment
Я уверен, что кто-то докажет, что я ошибаюсь, но мне трудно найти решение на основе набора, учитывая, что первая строка в каждой группе зависит от определения групп для предыдущих строк. Возможно, вам придется подойти к этому последовательно (курсоры и т. д.). Если я могу что-то придумать, то я опубликую это.   -  person Tom H    schedule 01.07.2011
comment
onlamp.com/pub/a/mysql/2007/04/12/ Сейчас я копаюсь в этом и отпишусь о том, что найду.   -  person Brad    schedule 01.07.2011
comment
Пожалуйста, уточните, что вы ожидаете, когда у вас есть записи, где дата совпадает с минутой, а секунды: 1,2,3 и 4? Любая агрегатная функция будет иметь двойной счет, потому что 1, 2, 3 находятся в пределах трех секунд... но то же самое относится и к 2, 3, 4 и так далее.   -  person OMG Ponies    schedule 01.07.2011
comment
@OMG Ponies, для этого приложения я ожидаю, что секунды 1, 2, 3, 4 будут сгруппированы в одну группу. Секунды 8, 9, 10, например, будут в следующей группе. Приемлемо, что, если заголовок совпадает и находится в пределах 3 секунд от последней строки с совпадающим заголовком, он появляется в той же группе, независимо от того, как долго это продолжается. Для моих целей допустимо, чтобы журналы с одинаковым заголовком находились, например, в секундах с 1 по 30, и все они были бы в одной группе.   -  person Brad    schedule 01.07.2011
comment
Я думаю, что могу добиться этого, используя LAG(), как предложил Рэнди. Мне просто нужно выяснить, как это сделать в MySQL. Я нашел ресурс (explainextended.com/2009/ 10/03/), кажется, делает именно то, что нужно... теперь мне просто нужно во всем этом разобраться.   -  person Brad    schedule 01.07.2011
comment
Если это одноразовая операция, рассмотрите возможность загрузки данных в базу данных, которая поддерживает аналитику (SQL Server 2005+ Express очень близок к MySQL по типам данных). В противном случае прочитайте: explainextended.com/2009 /03/12/   -  person OMG Ponies    schedule 01.07.2011


Ответы (5)


Я использую прекрасную идею Тома Х., но здесь делаю это немного по-другому:

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

Запрос № 1 здесь должен сказать вам, какие времена являются началом цепочек, находя, какие времена не имеют времени ниже них, но в пределах 3 секунд:

SELECT DISTINCT Timestamp
FROM Table a
LEFT JOIN Table b
ON (b.Timestamp >= a.TimeStamp - INTERVAL 3 SECONDS
    AND b.Timestamp < a.Timestamp)
WHERE b.Timestamp IS NULL

И затем для каждой строки мы можем найти самую большую временную метку начала цепочки, которая меньше нашей временной метки с помощью запроса № 2:

SELECT Table.id, MAX(StartOfChains.TimeStamp) AS ChainStartTime
FROM Table
JOIN ([query #1]) StartofChains
ON Table.Timestamp >= StartOfChains.TimeStamp
GROUP BY Table.id

Как только мы получим это, мы можем СГРУППИРОВАТЬ по нему, как вы хотели.

SELECT COUNT(*) --or whatever
FROM Table
JOIN ([query #2]) GroupingQuery
ON Table.id = GroupingQuery.id
GROUP BY GroupingQuery.ChainStartTime

Я не совсем уверен, что это достаточно отличается от ответа Тома Х, чтобы его можно было опубликовать отдельно, но похоже, что у вас возникли проблемы с реализацией, и я думал об этом, поэтому решил опубликовать снова. Удачи!

person Chris Cunningham    schedule 02.07.2011
comment
Спасибо за дополнительные разъяснения. У меня наконец-то заработало! Я сделал пару незначительных изменений. Во-первых, для запроса № 2 мне понадобился GROUP BY Table.id. Также в запросе № 2 я изменил > на >=, чтобы позаботиться о временных метках, равных началу цепочки. С тех пор я реализовал все это в своем реальном коде, и он отлично работает! Спасибо вам и Тому Х. также. - person Brad; 04.07.2011
comment
Хорошие моменты; эти изменения определенно необходимы. Я вернусь и вставлю их. Рад, что мы смогли сделать это для вас! - person Chris Cunningham; 04.07.2011
comment
Также отлично работает на SQL Server, если вы измените строку INTERVAL на: ON (b.Timestamp ›= DATEADD(second, -3, a.TimeStamp) AND... - person Ryan Barton; 28.08.2012
comment
Потрясающий запрос. Очень помог при попытке найти дату и время с разницей в 2 секунды для аналитических целей. - person Josh Davis; 30.07.2014
comment
Один из самых полезных фрагментов SQL, которые я нашел за более чем 10 лет. - person Peter Hanneman; 17.06.2016

Теперь, когда я думаю, что понимаю вашу проблему, основываясь на вашем комментарии к OMG Ponies, я думаю, что у меня есть решение на основе набора. Идея состоит в том, чтобы сначала найти начало любой цепочки на основе названия. Начало цепочки будет определяться как любая строка, в которой нет совпадения в течение трех секунд до этой строки:

SELECT
    MT1.my_id,
    MT1.title,
    MT1.my_time
FROM
    My_Table MT1
LEFT OUTER JOIN My_Table MT2 ON
    MT2.title = MT1.title AND
    (
        MT2.my_time < MT1.my_time OR
        (MT2.my_time = MT1.my_time AND MT2.my_id < MT1.my_id)
    ) AND
    MT2.my_time >= MT1.my_time - INTERVAL 3 SECONDS
WHERE
    MT2.my_id IS NULL

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

SELECT
    SQ1.my_id,
    COUNT(*)  -- You didn't say what you were trying to calculate, just that you needed to group them
FROM
(
    SELECT
        MT1.my_id,
        MT1.title,
        MT1.my_time
    FROM
        My_Table MT1
    LEFT OUTER JOIN My_Table MT2 ON
        MT2.title = MT1.title AND
        (
            MT2.my_time < MT1.my_time OR
            (MT2.my_time = MT1.my_time AND MT2.my_id < MT1.my_id)
        ) AND
        MT2.my_time >= MT1.my_time - INTERVAL 3 SECONDS
    WHERE
        MT2.my_id IS NULL
) SQ1
INNER JOIN My_Table MT3 ON
    MT3.title = SQ1.title AND
    MT3.my_time >= SQ1.my_time
LEFT OUTER JOIN
(
    SELECT
        MT1.my_id,
        MT1.title,
        MT1.my_time
    FROM
        My_Table MT1
    LEFT OUTER JOIN My_Table MT2 ON
        MT2.title = MT1.title AND
        (
            MT2.my_time < MT1.my_time OR
            (MT2.my_time = MT1.my_time AND MT2.my_id < MT1.my_id)
        ) AND
        MT2.my_time >= MT1.my_time - INTERVAL 3 SECONDS
    WHERE
        MT2.my_id IS NULL
) SQ2 ON
    SQ2.title = SQ1.title AND
    SQ2.my_time > SQ1.my_time AND
    SQ2.my_time <= MT3.my_time
WHERE
    SQ2.my_id IS NULL

Это выглядело бы намного проще, если бы вы могли использовать CTE или использовать временную таблицу. Использование временной таблицы также может повысить производительность.

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

EDIT: запросы изменены для обработки точных совпадений по отметке времени.

person Tom H    schedule 01.07.2011
comment
ох!! ох! Это красивая идея! - person Chris Cunningham; 02.07.2011
comment
@ Том Х., многие из моих временных меток точно совпадают. Очень часто можно увидеть 3 строки с одинаковым заголовком, одним и тем же временем и разными идентификаторами серверов (и, естественно, разными идентификаторами ПК в таблице). Я ломал голову над тем, как изменить ваши запросы, но, похоже, я не могу этого понять. Можете ли вы указать мне правильное направление? - person Brad; 02.07.2011
comment
Я только что внес изменение для обработки случаев точного совпадения временных меток. Уже поздно, и я устал, но я думаю, что понял правильно. - person Tom H; 02.07.2011

Предупреждение: длинный ответ. Это должно работать, и это довольно аккуратно, за исключением одного шага посередине, когда вы должны быть готовы запускать оператор INSERT снова и снова, пока он ничего не сделает, поскольку мы не можем делать рекурсивные CTE-вещи в MySQL.

Я собираюсь использовать эти данные в качестве примера вместо ваших:

id    Timestamp
1     1:00:00
2     1:00:03
3     1:00:06
4     1:00:10

Вот первый запрос, который нужно написать:

SELECT a.id as aid, b.id as bid
FROM Table a
JOIN Table b 
ON (a.Timestamp is within 3 seconds of b.Timestamp)

Он возвращает:

aid     bid
1       1
1       2
2       1
2       2
2       3
3       2
3       3
4       4

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

CREATE TABLE
Adjacency
( aid INT(11)
, bid INT(11)
, PRIMARY KEY (aid, bid) --important for later
)

Теперь задача состоит в том, чтобы найти что-то вроде транзитивного замыкания этого отношения.

Для этого найдем следующий уровень ссылок. под этим я подразумеваю, что поскольку у нас есть 1 2 и 2 3 в таблице смежности, мы должны добавить 1 3:

INSERT IGNORE INTO Adjacency(aid,bid)
SELECT adj1.aid, adj2.bid
FROM Adjacency adj1
JOIN Adjacency adj2
ON (adj1.bid = adj2.aid)

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

Как только это закончится, у вас будет транзитивно-закрытое отношение, подобное этому:

aid     bid
1       1
1       2
1       3     --added
2       1
2       2
2       3
3       1     --added
3       2
3       3
4       4

А теперь к изюминке:

SELECT aid, GROUP_CONCAT( bid ) AS Neighbors
FROM Adjacency
GROUP BY aid

возвращает:

aid     Neighbors
1       1,2,3
2       1,2,3
3       1,2,3
4       4

So

SELECT DISTINCT Neighbors
FROM (
     SELECT aid, GROUP_CONCAT( bid ) AS Neighbors
     FROM Adjacency
     GROUP BY aid
     ) Groupings

возвращается

Neighbors
1,2,3
4

Вау!

person Chris Cunningham    schedule 01.07.2011
comment
У меня есть опасения, что в моем столбце «Соседи» иногда может быть 1,2,3, а иногда 2,1,3, поэтому, возможно, в самом внутреннем запросе GROUP_CONCAT должен быть ORDER BY, чтобы убедиться, что все идет по порядку. - person Chris Cunningham; 01.07.2011
comment
И еще одно: вам не нужно запускать оператор INSERT бесконечное количество раз... только столько раз, сколько самая большая группа, которую вы хотите захватить. Если вы уверены, что нет групп размера 100, вы сможете запустить его 100 раз, и большую часть времени он ничего не сделает. Проблема с очень большими группами заключается в том, что Adjacency таблица станет большой, так как группа из n объектов имеет порядка n^2 отношений смежности. - person Chris Cunningham; 01.07.2011
comment
Спасибо! Я проработал это решение на своих таблицах, и, похоже, оно работает хорошо. - person Brad; 02.07.2011
comment
Хорошо! - но если вам когда-нибудь надоест запускать все INSERT в моем ответе, вам следует реализовать идею @Tom H., которой я большой поклонник. Я не смотрел его код, поэтому не могу говорить о реализации, но его план более конкретно решает вашу проблему, поэтому, возможно, стоит изучить его позже! - person Chris Cunningham; 02.07.2011
comment
Я работаю над реализацией его прямо сейчас. У меня возникли некоторые проблемы, но я думаю, что разберусь с этим. Я отправлю еще раз, как только выясню, что я делаю неправильно. - person Brad; 02.07.2011

Мне нравится ответ @Chris Cunningham, но вот еще один взгляд на него.

Во-первых, мое понимание вашей постановки задачи (поправьте меня, если я ошибаюсь):

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

Я работаю в основном в SQL Server, поэтому я использую синтаксис SQL Server. Это не должно быть слишком сложно перевести на MySQL SQL.

Итак, сначала наша таблица журнала событий:

--
-- our event log table
--
create table dbo.eventLog
(
  id       int          not null ,
  dtLogged datetime     not null ,
  title    varchar(200) not null ,

  primary key nonclustered ( id ) ,
  unique clustered ( dtLogged , id ) ,

)

Учитывая приведенное выше понимание постановки задачи, следующий запрос должен дать вам верхнюю и нижнюю границы ваших групп. Это простой вложенный оператор select с 2 group by для сворачивания вещей:

  • Самый внутренний select определяет верхнюю границу каждой группы. Эта верхняя граница определяет группу.
  • Внешний select определяет нижнюю границу каждой группы.

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

[отредактировано: верхняя граница — это наименьшее значение даты/времени, где интервал превышает 3 секунды]

select dtFrom = min( t.dtFrom ) ,
       dtThru =      t.dtThru
from ( select dtFrom = t1.dtLogged ,
              dtThru = min( t2.dtLogged )
       from      dbo.EventLog t1
       left join dbo.EventLog t2 on t2.dtLogged >= t1.dtLogged
                                and datediff(second,t1.dtLogged,t2.dtLogged) > 3
       group by t1.dtLogged
     ) t
group by t.dtThru

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

select *
from ( select dtFrom = min( t.dtFrom ) ,
              dtThru =      t.dtThru
       from ( select dtFrom = t1.dtLogged ,
                     dtThru = min( t2.dtLogged )
              from      dbo.EventLog t1
              left join dbo.EventLog t2 on t2.dtLogged >= t1.dtLogged
                                       and datediff(second,t1.dtLogged,t2.dtLogged) > 3
              group by t1.dtLogged
            ) t
       group by t.dtThru
     ) period
join dbo.EventLog t on t.dtLogged >=           period.dtFrom
                   and t.dtLogged <= coalesce( period.dtThru , t.dtLogged )
order by period.dtFrom , period.dtThru , t.dtLogged

Каждая строка помечается своей группой через возвращаемые столбцы dtFrom и dtThru. Вы можете проявить фантазию и присвоить целочисленный номер строки каждой группе, если хотите.

person Nicholas Carey    schedule 01.07.2011
comment
Вы уверены, что это работает, если в группе более 3 вещей? Мне кажется, что ваш самый внутренний запрос найдет самое большое время, которое находится в пределах 3 секунд от первого раза, но не (самое большое время, которое находится в пределах 3 секунд от (самое большое время, которое находится в пределах 3 секунд от первого раза)) :/ - person Chris Cunningham; 01.07.2011
comment
Я исправил запрос. Мое определение верхней границы было неверным: правильный вариант должен быть «наименьшим значением даты/времени, большим или равным текущей строке, так что разница между ними составляет более 3 секунд». Прошло некоторое время с тех пор, как мне приходилось делать что-то подобное. Я ржавею. - person Nicholas Carey; 01.07.2011

Простой запрос:

SELECT * FROM time_history GROUP BY ROUND(UNIX_TIMESTAMP(time_stamp)/3);
person Pavan Kumar N    schedule 12.03.2013
comment
Ваше решение будет работать во многих случаях, но не в моем конкретном случае. Путем округления две записи могут находиться в пределах одной секунды друг от друга, но попадать в разные корзины. +1 в любом случае, так как это быстрый способ выполнить работу в основном. - person Brad; 12.03.2013