маленька SQL задачка
Oct. 15th, 2025 02:35 pmВиплила задача. Зрозумів, що я отак одразу не знаю, як її розвʼязати. Правда, я ще толком не думав, але чого я маю думати сам? Думайте і ви теж!
Власне, задача: Таблиця TBL має текст і метадані повідомлень, які відсилають юзери мережі. Нас цікавлять три колонки: user_id, message_text, _time. До таблички треба додати ще одну колонку, яка буде описувати скільки таких повідомлень підряд було/буде відіслано цим юзером. Тобто, допустимо, юзер stepan відіслав, по-порядку, ось такі повідомлення: hi, hi, hello, hi, hi, hi, hi, huilo, huilo, hi. Ці повідомлення повинні відповідати цифрам: 2,2,1,4,4,4,4,2,2,1.
У мене поки що в голові лише якісь неелєґантні розвʼязки з двома проходами з віконною функцією плюс одною агрегацією, пхе. Чи я чогось не знаю?
Кому цікаво, очевидний неелеґантний розвʼязок:
UPDATE: Ви казали, нема такого рішення, а воно є!. Дякую шановному
aklepatc за розвʼязок. Ось тут відформатована версія.
Власне, задача: Таблиця TBL має текст і метадані повідомлень, які відсилають юзери мережі. Нас цікавлять три колонки: user_id, message_text, _time. До таблички треба додати ще одну колонку, яка буде описувати скільки таких повідомлень підряд було/буде відіслано цим юзером. Тобто, допустимо, юзер stepan відіслав, по-порядку, ось такі повідомлення: hi, hi, hello, hi, hi, hi, hi, huilo, huilo, hi. Ці повідомлення повинні відповідати цифрам: 2,2,1,4,4,4,4,2,2,1.
У мене поки що в голові лише якісь неелєґантні розвʼязки з двома проходами з віконною функцією плюс одною агрегацією, пхе. Чи я чогось не знаю?
Кому цікаво, очевидний неелеґантний розвʼязок:
WITH
cte1 AS (
SELECT user_id, message_text, _time,
IFF(message_text=LEAD(message_text) OVER
(PARTITION BY user_id ORDER BY _time, 0,1)
AS x
FROM tbl
),
cte2 AS (
SELECT user_id, message_text, _time,
SUM(x) OVER (PARTITION BY user_id
ORDER BY _time ROWS BETWEEN
UNBOUNDED PRECEDING AND CURRENT ROW)
AS y
FROM cte1
),
cte3 AS (
SELECT user_id, y, COUNT(1) AS window_length
FROM cte2
GROUP BY user_id, y
)
SELECT user_id, message_text, _time, window_length
FROM cte2 INNER JOIN cte3 USING(user_id, y)UPDATE: Ви казали, нема такого рішення, а воно є!. Дякую шановному
no subject
Date: 2025-10-16 05:40 am (UTC)Думаю, можна обійтися без inner join, а просто ще одне вікно. (count(1) over (partition by user_id, y rows between unbounded preceding and unbounded following))
no subject
Date: 2025-10-16 05:47 am (UTC)Я б джойн написав всередині останнього SELECT'а, чогось мені джойнʼи подобаються більше, ніж вікна. Вікна мені Windows'ом віддають...
no subject
Date: 2025-10-16 10:04 am (UTC)no subject
Date: 2025-10-16 11:04 am (UTC)З процедурної точки зору - можливо, але в реляційній алгебрі то єдино правильний підхід. Ну, і в продуктивності буде суттєва різниця.
no subject
Date: 2025-10-16 11:23 am (UTC)Джойн: partition by [O(N log N) because of the preceding group by], а потім key lookup [N * O(log N) = O(N log N) for large keysets].
Window: partition by [O(N log N)], then compute one value in a running fashion [N * O(1) = O(N)] (for expressions that touch rows until current row), or compute one constant for entire partition [1 * O(N)] (for expressions that touch all rows - unbounded preceding and unbounded following)
Additionally, windows can be fused, if they are partitioned using conditions that are subsets or supersets of each other. So subsequent partitioning is also O(M log M), but for (much) smaller M.
no subject
Date: 2025-10-16 05:41 am (UTC)no subject
Date: 2025-10-16 05:53 am (UTC)Може, воно мені здається елегантнішим виключно як артефакт молодості, яку я провів програмуючи обчислення на Сі :)
no subject
Date: 2025-10-16 06:18 am (UTC)В таких випадках оптимальним є препроцесинг. Ну, тобто, на момент інсерту в таблицю одразу писати ше й інкрементований лічильник. А потім проходити таблицю в зворотньому порядку.
no subject
Date: 2025-10-16 10:13 am (UTC)no subject
Date: 2025-10-16 03:54 pm (UTC)for(u=0; u<Umax; u++) { i = j = 0; val = tbl[u][j] ; for(i=1; i<Imax[u]; i++) { if(val != tbl[u][i]){ memset(&out[u][j], i-j, (i-j)*sizeof(int)); val = tbl[u][i]; j = i;} memset(&out[u][j], i-j, (i-j)*sizeof(int));}}Де out -- int структура такої самої розмірності, як tbl.І от розумію, що з точки зору абстрактних компʼютер сайєнтістів це може виглядати некрасиво, але це -- то, як як думаю про розрахунки. (Перших 20 років свого досвіду програмування я не знав нічого, крім вибраних розділів Numerical Recipes in C.... Наступних скільки-там-було років свого досвіду програмування я перестав знати навіть то :))
no subject
Date: 2025-10-16 06:35 am (UTC)no subject
Date: 2025-10-16 08:33 am (UTC)no subject
Date: 2025-10-16 08:40 am (UTC)no subject
Date: 2025-10-16 08:56 am (UTC)no subject
Date: 2025-10-16 09:40 pm (UTC)Sorry, not sure how to format code properly here... Calculated grp column seems to do trick.
WITH numbered_messages AS (
-- Assign a group number that changes whenever msg_txt changes for a user
SELECT
user_id,
msg_txt,
_time,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY _time)
- ROW_NUMBER() OVER (PARTITION BY user_id, msg_txt ORDER BY _time) AS grp
FROM tbl
) SELECT
user_id,
msg_txt,
_time,
COUNT(*) OVER (PARTITION BY user_id, msg_txt, grp ORDER BY _time) AS consecutive_count
FROM numbered_messages
ORDER BY user_id, _time;
no subject
Date: 2025-10-16 10:24 pm (UTC)Правда, в останньому вікні треба забрати ORDER BY _time, деякі бази даних, якщо для функції COUNT() вказати порядок сортування, автоматично вважають, що там ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Ну, принаймні, Snowflake так вважає, як я тільки що переконався, швидко підставивши пару прикладів. Достатньо забрати сортування і все працює.
Дякую! Це саме те, що я назву елеґантним sql рішенням! Шкода, ми зараз data scientist'ів не шукаємо, я би позицію запропонував ;)
Форматувати код можна простим htmlʼним теґом <pre>(код)</pre> Давайте, я переформатую для нащадків:
WITH numbered_messages AS ( SELECT user_id, msg_txt, _time, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY _time) - ROW_NUMBER() OVER (PARTITION BY user_id, msg_txt ORDER BY _time) AS grp FROM tbl ) SELECT _time, user_id, msg_txt, COUNT(*) OVER (PARTITION BY user_id, msg_txt, grp) AS window_length FROM numbered_messages ORDER BY user_id, _time;no subject
Date: 2025-10-16 11:27 pm (UTC)And thank you for the order by _time correction!
no subject
Date: 2025-10-16 11:47 pm (UTC)no subject
Date: 2025-10-17 06:40 am (UTC)FROM tbl |> EXTEND ROW_NUMBER() OVER (same_user) - ROW_NUMBER() OVER (same_message) AS grp WINDOW same_user AS (PARTITION BY user_id ORDER BY _time), same_message AS (PARTITION BY user_id, msg_txt ORDER BY _time) |> EXTEND COUNT(1) OVER (same_group) AS window_length WINDOW same_group AS (PARTITION BY user_id, msg_txt, grp) |> SELECT _time, user_id, msg_txt, window_length |> ORDER BY user_id, _time;no subject
Date: 2025-10-17 06:45 am (UTC)Плюс, не знаю, чи воно є на snoflake'у. Принаймні, можливості окремо визначати WINDOW, як в BigQuery точно нема. Ок, точно не було навесні, коли я останній раз пробував, так-то Snowflake досить непогано розвивається, точно швидше за RedShift
no subject
Date: 2025-10-17 07:03 am (UTC)Я коли на SQL дивлюсь, то саме у такій послідовності: звідки беремо, що ми робимо, куди далі передаємо. Різниця суто синтаксична, план виконання не відрізняється.
EXTEND це SELECT * із новими колонками. |> це заміна SELECT у попереднього CTE та FROM наступного.
no subject
Date: 2025-10-17 04:18 pm (UTC)Пайп є, правда, пишеться інакше, отак: ->>
Додали його недавно, тому я не знав. Виглядає зручно, буду вчитися.
no subject
Date: 2025-10-18 07:21 pm (UTC)Yes, I am on the market now. AFAICT market sucks with capital S. So I might be still here in (the early) 2026.
If you were able to make a good intro or even have a good lead in the comming months I would very much appreciate it. Backend programmer or SMLT... Can DM my LinkedIn profile if it makes any sense.
no subject
Date: 2025-10-18 09:23 pm (UTC)For non-DS positions we actually have a few openings. Our backend is all in Go, which afaik is somewhat obscure as skillsets go. Here: https://www.textnow.com/careers
If anything fits, please DM me
no subject
Date: 2025-10-18 09:40 pm (UTC)DM-ed my profile...
no subject
Date: 2025-10-17 05:53 am (UTC)Does it work though?hmm OKPartition by user_id, msg_txt order by time is going to produce a different
grpnumber for every message. But grp will be the same, if both orders by time increments by 1 - i.e. consecutive messages.no subject
Date: 2025-10-17 11:09 am (UTC)no subject
Date: 2025-10-17 11:11 am (UTC)no subject
Date: 2025-10-17 11:14 am (UTC)_Maybe_ the easiest explanation for uniqueness is that the 1st row number grows at least as fast as the 2nd one or even faster.
Also, uniqueness is per user_id. grp can be the same (i think) across different users but this would not be an issue.
no subject
Date: 2025-10-17 01:32 pm (UTC)Since we use grp together with msg_txt, we need to consider only groups of identical messages.
Message B follows A as part of one contiguous group, if B is next after A in the contiguous group and B is next after A in total order in time. In terms of integer row numbers this means both B's row numbers are greater than A's by 1, and the difference is the same for all messages in one contiguous group.
Similarly the uniqueness of grp follows from the fact that for group B following group A in time the first message of B will have row number greater than the last message of A by 1, but the row number in time is greater by more than 1.
no subject
Date: 2025-10-17 04:32 pm (UTC)Далі очевидно-тривіально
no subject
Date: 2025-10-18 12:32 am (UTC)no subject
Date: 2025-10-18 02:51 am (UTC)Та ні, він за визначенням такий, якщо u позначає юзера, m месидж, Rum і Ru -- номер рядка для юзера з у без врахування мессиджа, то за принципом побудови:
Ru = ∑m Rum
Відповідно, оскільки всі вони невідʼємні,
Ru ≥ Rum
no subject
Date: 2025-10-18 05:45 am (UTC)Ну, я думаю, розумію, що ти маєш на увазі