Страницы: 1
RSS
PQ Поиск вложенных связей
 
Приветствую
Есть табличка с заказами с контактом клиента. По контактам мы объединяем все заявки в общую сущность "Клиент"  простым Join таблицы. Но случаются ситуации, что человек в какой-то момент сменил контакт и у меня связи становятся не полными, пример:

В данном случае 1 и 2 заявка вяжутся по контакту test@mail.ru а заявка 2 и 3 по другому контакту 79299999999. При этом таких вложенных связей через N контактов может быть много.
Посоветуйте пожалуйста, как оптимальней всего сделать поиск по всем связанным контактам с не ограниченной вложенностью(когда например через 3 связи только 1 и 3 контакт вяжутся)
Прикладываю фаил примера. Благодарю!  
 
любопытная задачка. В принципе, решаемая, только прожорливая.
Может, Андрей VG сделает на рекурсиях, у меня с ними плохо :)
F1 творит чудеса
 
Цитата
Максим Зеленский написал:
любопытная задачка
Не, Максим. Решать задачу нумерации несвязанных подграфов данного графа в Power Query - то ещё удовольствие. К тому же Всеволод как всегда пример данных представил в стиле - нате...
Если хочется порешать головоломку, то прикладываю более тяжёлый пример. Из левой таблицы нужно получить правую.
 
Андрей VG, подскажите плиз, что в файлах примера нужно изменить, чтобы было удобно не в формате "нате". Я специально стараюсь делать файлы примеры на одном листе, с подсвеченными связями.
Может подскажите, на каких-то других языках(или в базе mssql, mysql) можно какими-то готовыми классами такие связи строить? Надоели дубли клиентов в CRM:(
Благодарю за помощь!  
 
Ну вроде сделал, но на длинных цепочках может очень сильно тупить
Код
F1 творит чудеса
 
Цитата
Максим Зеленский написал:
вроде сделал
Что-то, Максим, у меня похоже ваш код зациклился. 10 минут, а результата нет. Правда, у меня, там не дерево в данных, а полноценный граф. :)
Цитата
Vsevolod написал:
что в файлах примера нужно изменить, чтобы было удобно не в формате "нате".
Проанализировать, какие могут быть комбинации. Например, думаю код Максима не работает потому что не представлено есть ли такое для какого-нибудь клиента.
1 abc@no.not
1 zyz@no.not
2 abc@no.not
2 mnl@no.not
3 zyz@no.not
3 mnl@no.not
И ваша задача, как владельца данных выявить есть ли полный граф в данных или нет. Выявить все возможные комбинации.
Цитата
Vsevolod написал:
на каких-то других языках
Можно и на VBA делать.
Цитата
Vsevolod написал:
или в базе mssql, mysql
Тут не чуть не проще. В смысловой конструкции SQL его программные расширения ни чем от Power Query не отличаются. Это тоже функциональный язык с точки зрения работы с таблицами. Можно конечно и курсором перебирать, но дело не быстрое.
 
Цитата
Андрей VG написал:
Что-то, Максим, у меня похоже ваш код зациклился
я решал исходную задачу, где один столбец - номер заявки, второй - контакт, имеющийся в заявке.
Логика такая: группируем по заявкам, получаем список списков контактов.
Дальше бежим по этому списку:
1. Берем первый элемент (список контактов первой заявки)
2. Последовательно проверяем, есть ли пересечения с другими списками: если пересечение с конкретным списком есть, то объединяем списки, запоминаем этот список, и проверяем следующий.
3. Проверяем второй элемент и запускаем п.2
4. и так проверяем все заявки.
Например, есть список {{1,2},{2,3},{4},{3}}
Первая пробежка даст результат {{1,2,3},{1,2,3},{4},{2,3}}
Запускаем пробежку еще раз, получим {{1,2,3},{1,2,3},{4},{1,2,3}}
В итоге там N^2 на первой пробежке и N^2 на второй, что супер далеко от оптимального. На больших списках это малореально.

Затем группируем таблицу "Заявка"-"Список заявок" по обработанным спискам, получаем пару: список заявок/список контактов, одна пара = 1 клиент.
F1 творит чудеса
 
Максим Зеленский, вот вот, пункты 2 и 3 как раз и дают, если описывается не дерево, а граф (есть замыкание), то получаем бесконечный цикл получения очередного списка. Вариант такого замыкания описал в #6. Чтобы этого не происходило, надо предварительно выродить граф в дерево.
Версия на T-SQL.
Скрытый текст
 
Я думаю, здесь всё дело в исключительной жадности моего алгоритма. На 10 заявках это 200 циклов по списку заявок, на 100 заявках это 20000 циклов, а как правильно посчитать сложность алгоритма - не знаю, наверное это O(2N^2) :)
Быть может, есть менее жадный алгоритм объединения множеств по условию пересечения?
F1 творит чудеса
 
Порыл немного в теорию обхода графов, всё же хочется как-то O(n+m), а не O(n^2)... прямо очень зацепило, помучаю на досуге :)
F1 творит чудеса
 
Цитата
Андрей VG написал: надо предварительно выродить граф в дерево.
И я подумаю на досуге...  8) граф в дерево - звучит интригующе... спасибо за пример...
но #8 - код на Серверном SQL, полагаю...  
чтобы не гадать на кофейной гуще, кто вам отвечает и после этого не совершать кучу ошибок - обратитесь к собеседнику на ВЫ - ответ на ваш вопрос получите - а остальное вас не касается (п.п.п. на форумах)
 
Цитата
JeyCi написал:
код на Серверном SQL, полагаю
Да, это для MS SQL Server от 2014 версии.
Цитата
Максим Зеленский написал:
есть список {{1,2},{2,3},{4},{3}}
Максим, попробуйте с таким
{{1,2},{2,3},{4},{3}, {1, 3}}
 
Цитата
Андрей VG написал:
И ваша задача, как владельца данных выявить есть ли полный граф в данных или нет. Выявить все возможные комбинации.
С графами не работал, но фразу "Выявить все возможные комбинации." понял. Максим Зеленский, Учел ваше замечание в новом посте. Как я понял, в примере в данном посте был представлен маленький набор данных. Если что-то не так, готов еще учесть замечания Ваши.
Пример на T-SQL правильно я понимаю, что сет данных выгружается куда-то, потом стороннем приложением прогонять через запрос к БД? Вы выше написали, что в SQL задача не чуть не легче, для понимания, почему решили на SQL привести пример?
Максим Зеленский, прочитал логику что Вы описали не могли бы Вы выложить пример, интересно изучить как Вы в цикле делается возврат ко 2 пункту. Благодарю.

Еще хотел узнать, а если задачу упросить например вложенность до 4 пунктов макс, то тогда проще всего просто сделать 4 раза merge?
Благодарю за помощь!
 
 
Цитата
Vsevolod написал:
SQL задача не чуть не легче
для вас в первую очередь. Прицип решения в общем то одинаковый,  правда, в sql решении использован далеко не оптимальный алгоритм нахождения корня вырожденного графа. Разве что рекурсия по вырожденному графу строится проще. В pq нужно будет или List.Generate использовать либо рекурсивную функцию.
Цитата
Vsevolod написал:
просто сделать 4 раза merge?
возможно, если не будет циклов. Мой пример, похоже, Максим не смотрел. А шанс получить его есть. Его код, правда, тоже не смотрел.
 
Цитата
Андрей VG написал:
Максим, попробуйте с таким{{1,2},{2,3},{4},{3}, {1, 3}}
Хм... да нормально собирает, получаем в итоге 2 клиента, {1,2,3} и {4}
Код
let
   Contacts = {{1,2}, {2,3}, {4}, {3}, {1,3}},
   Pos = List.Buffer(List.Positions(Contacts)),
   fnCombinator = (modContacts as list) => 
       List.Transform(
           Pos, 
           (p)=>List.Accumulate(
               modContacts, 
               modContacts{p}, 
               (s,c)=> if s = {} then {} else 
                    List.Union(
                   {
                       s, 
                       if List.ContainsAny(s, c) then c else {}
                   }
               )
           )
       ),
   First = List.Buffer(fnCombinator(Contacts)),
   Second = List.Transform(fnCombinator(First), List.Sort),
    Distinct = List.Distinct(Second)
in
    Distinct

32-битный Excel вешает процессор надолго (что вообще непонятно, не должно так быть, наверное, пресловутые promises в List.Accumulate дают прикурить), но 64-битный Power BI решает быстрее. С ростом количества элементов списка O(N^2) дает себя знать, конечно.
Цитата
Андрей VG написал:
Мой пример, похоже, Максим не смотрел.
Если вы про пример на T-SQL, то это для меня темный лес, увы.
Цитата
Vsevolod написал:
не могли бы Вы выложить пример, интересно изучить как Вы в цикле делается возврат ко 2 пункту.
А кода недостаточно? Цикл по заявкам обеспечивается List.Transform,  внутри каждой петли цикла используем List.Accumulate для нахождения связей текущей заявки с остальными заявками.
Там, конечно, можно оптимизировать чуть-чуть, убирая текущую заявку из рассмотрения, но это не принципиально.
F1 творит чудеса
 
Цитата
Максим Зеленский написал:
Если вы про пример на T-SQL, то это для меня темный лес, увы.
Максим, я собственно, про файл из #65.
Что Power Query с новыми версиями крутит не по детски. Сначала в Power BI схлопотал, техподдержка единственное что могла посоветовать - а вы создайте запрос из файла csv (он гарантировано не будет DirectQuery), а потом код в редакторе поменяйте на свой.

Сегодня столкнулся, что вылетает на последней версии PQ в Excel запрос нахождения различий между двумя выгрузками. Стабильно полгода работал, а сейчас вплоть до крушения Excel.
Может поэтому у вас получается, что
Цитата
Максим Зеленский написал:
32-битный Excel вешает процессор надолго (что вообще непонятно, не должно так быть, наверное, пресловутые promises в List.Accumulate дают прикурить),
Изменено: Андрей VG - 20.08.2018 16:38:44
 
Цитата
Максим Зеленский написал:
А кода недостаточно? Цикл по заявкам обеспечивается List.Transform,  внутри каждой петли цикла используем List.Accumulate для нахождения связей текущей заявки с остальными заявками.Там, конечно, можно оптимизировать чуть-чуть, убирая текущую заявку из рассмотрения, но это не принципиальн
Благодарю, буду изучать.
 
Цитата
Vsevolod написал: По контактам мы объединяем все заявки в общую сущность "Клиент"  простым Join таблицы. Но случаются ситуации, что человек в какой-то момент сменил контакт и у меня связи становятся не полными,
(о наболевшем)
, вопрос ТСу: Зачем создавать сущность без ключей (either primary or foreign) - она и получается, как неваляшка... моё имхо
у меня сразу появилось желание задать какой-нибудь clientId, что Андрей и реализовывает, правда в коде... чтобы не перегружать так код - можно ведь сразу ID клиента записать отдельным столбцом -- ФИ(О) само по себе будет более приемлемым идентификатором уникальности клиента в отличие от его контактов (хотя конечно однофамильцы и одноимёнцы тоже не исключаются)
и главное, что меня смущает - !? зачем вы задваиваете заявку при смене контакта - вносите изменение в поле контакта (при смене его и ещё живой заявке [неисполненной]) и живите дальше с последним рабочим контактом.. -- или вы собираете историю изменений контактов клиента??)) -- вы уполномочены на собирание такой инфо?.. лично я всегда стараюсь и советую не собирать мусор, а держать рабочие данные в состоянии адекватном текущему и только нужному (без лишнего) for Decision-making Purpose (руководствуясь расшифровкой аббревиатуры CASE - Computer Aided Software Engineering - перефразируя: зачем создавать приложение - хоть в xl, хоть на сервере - если оно не aid вам жить??)... а уже как вы выстроите систему без мусора - это Искусство DB-design - ... ) "Recycle-been" ("утилизацию отходов") как-то ведь можно внедрить в систему - чтобы не изобретать велосипед для ковыряния в мусоре... Администрирование БД какое-никакое ведИте... или чем у вас занимается Администратор или тот кто выполняет его функции... БД без админа, как child без root'а ... имхо
Изменено: JeyCi - 25.08.2018 18:26:59
чтобы не гадать на кофейной гуще, кто вам отвечает и после этого не совершать кучу ошибок - обратитесь к собеседнику на ВЫ - ответ на ваш вопрос получите - а остальное вас не касается (п.п.п. на форумах)
 

мне увиделось решение от Андрей VG на SQL так (во вложении без 6-7 шага):
1. отобрали уникальные контакты с пометкой в столбцы from-to join'ом (куда откуда передалось изменение)
2. [childs]: оставили лишь те записи где замены произошли ( from и to имеют разные значения che по строке)
3. оставили уникальные to из (2)
4. [roots]: отобрали из (1) те контакты, что без child'ов (исключив child'ов определённых в (3))
5. пронумеровали клиентов Id-шками (просто по порядку по счёту, как отсортированы в выборке)
6. запустили рекурсию для объединения child'ов с корнями. UNION'ом ??? объединили всех child'ов к корню ??
7. отобрали уникальных clientId's контакты - присоединив к результатам рекурсии таблицу cilentIds

получили: "клиентский Id - контакт"
вопрос1: как в результирующей таблице id-con мы поймём, к какой che этот контакт относится?)... ведь нумерация id понятна будет только sql'ю, т.к. он сам её делает...)) - а если нам надо все заявки увидеть по клиенту (даже если он регистрировался под разными контактами)?
========
вопрос2: так child'ы - это заявки или контакты? (по коду мне показалось - заявки che)

Изменено: JeyCi - 22.08.2018 19:53:43
чтобы не гадать на кофейной гуще, кто вам отвечает и после этого не совершать кучу ошибок - обратитесь к собеседнику на ВЫ - ответ на ваш вопрос получите - а остальное вас не касается (п.п.п. на форумах)
 
Цитата
Vsevolod написал:
Пример на T-SQL правильно я понимаю, что сет данных выгружается куда-то,
там 1-ым шагом - просто создание тестовых данных... потом если разложить все Запросы по кусочкам (приложила пример в Access 1-5 шаг) - в принципе всё проглядывается (моё препарирование решения Андрей VG в предыдущем посте до 5-го шага)... но 6-7 шаг в Access не реализовать..
вопрос: такую рекурсивную конструкцию можно создать только на SQL Server 2014 или на 2012 тоже получится?
Изменено: JeyCi - 22.08.2018 19:49:46
чтобы не гадать на кофейной гуще, кто вам отвечает и после этого не совершать кучу ошибок - обратитесь к собеседнику на ВЫ - ответ на ваш вопрос получите - а остальное вас не касается (п.п.п. на форумах)
 
Цитата
Максим Зеленский написал:
32-битный Excel вешает процессор надолго (что вообще непонятно, не должно так быть, наверное, пресловутые promises в List.Accumulate дают прикурить),
проверю, как доберусь до компа
Максим, ваш последний код работает нормально на таких условиях, зависает только то, что Андрей отметил...
Изменено: JeyCi - 24.08.2018 10:04:55
чтобы не гадать на кофейной гуще, кто вам отвечает и после этого не совершать кучу ошибок - обратитесь к собеседнику на ВЫ - ответ на ваш вопрос получите - а остальное вас не касается (п.п.п. на форумах)
 
Цитата
JeyCi написал:
1. отобрали уникальные контакты с пометкой в столбцы from-to join'ом (куда откуда передалось изменение)
Почти всё правильно. Дополню только
Код
             IIF(tl.che < tr.che, tl.che, tr.che) as [from],
             IIF(tl.che > tr.che, tl.che, tr.che) As [to]
это как раз и делает вырождение графа до остовного дерева. То есть если есть переход из 1 в 2 и из 2 в 1, то остаётся только 1 в 2.
По 4 - увы, не идеальный выбор корня дерева, по идее, лучше бы находить такой корень, чтобы у всех дочерних узлов было приблизительно одинаковое число узлов и листьев, чтобы уменьшить глубину рекурсии.
Ну, и в таком виде, всё же реализовывать не стоит. Лучше через создание временных таблиц для 1-го шага и шагов поиска корней и листьев, с последующим созданием индексов. Иначе будет медленно. SQL Server не Access - временные индексы автоматически создавать не будет.
 
Цитата
Vsevolod написал:
По контактам мы объединяем все заявки в общую сущность "Клиент"  
продолжу свою мысль - раз ТС не даёт своих пояснений... Если вы хотите пользоваться удобствами Реляционных Структур (даже Сущность создаёте) - показали бы Схему вашей БД в той части откуда и куда и КАК вы связи настроили... а то вы жалуетесь что у вас связи рушатся, а нам гадай какие (исходя из целей ведения вашей БД) - что у вас там за связи и что за Сущность БЕЗ Ключей  8-0 ... - и может вообще др. подход оптимальнее, чем ваш...
даже цели  :D не совсем понятны: вы хотите проследить судьбу Клиента по смене его контактов? (он вам дал разрешение на собирание личной инфо о себе?)... или же вы всё-таки хотите проследить судьбу Заявки?...
p.s.
думаю без схемы БД и адекватно сформулированной сути ваших затруднений - сложно дать вам совета об Оптимизации... хотя по сути вы его уже получили: хотите мониторить Клиента - дайте ему Id (как primary key) - иначе вам придётся это делать в оперативке каждый раз как захотите хоть какую выборку по клиенту... выделяете его в объект для  мониторинга - так и создайте его Сущность - таблицу Клиентов... или делайте это каждый раз на лету... вам удобно?
p.p.s
вобщем судьбу контакта и судьбу заявки я вам прикладываю - (на примере из #3 - табл Отчёт не участвует) ... как и ДЛЯ ЧЕГО вы там хотите связи достать - не вникаю... а для мониторинга судьбы Клиента создавайте другую сущность... и желательно с пониманием (и вашим и помогающих по пояснениям от вас) природы ваших сущностей, связей Нужных (не нужных не надо), ваших целях (а не обрывком из контекста БД) И Целостности(!) вашей БД и ключей настроенных вами исходя из ваших целей...
(Только уточните у клиента согласен ли он на такую слежку за собой)...
==================
P.P.S МОДЕРАТОРАМ: слетают вложения, когда пишешь 2-й пост (в нём дублем появляется вложение из предыдущего своего - удаляешь - исчезает и из предыдущего поста)... не удаляешь - 2-й пост не опубликовывается...
Изменено: JeyCi - 24.08.2018 10:43:09
чтобы не гадать на кофейной гуще, кто вам отвечает и после этого не совершать кучу ошибок - обратитесь к собеседнику на ВЫ - ответ на ваш вопрос получите - а остальное вас не касается (п.п.п. на форумах)
 
Цитата
Андрей VG написал: SQL Server не Access - временные индексы автоматически создавать не будет.
упс... чувствовала я подвох от MicroSoft - ещё не переходила на сервер... важный нюанс для перевода БД на серверные рельсы... значит все индексы ручками настраивать придётся...
***
и продублирую свой вопрос, затерявшийся в моих эмоциях  :oops:
Цитата
JeyCi написал: вопрос: такую рекурсивную конструкцию можно создать только на SQL Server 2014 или на 2012 тоже получится?
Андрей VG спасибо за примеры и введение в Графы... !
Изменено: JeyCi - 24.08.2018 10:05:47
чтобы не гадать на кофейной гуще, кто вам отвечает и после этого не совершать кучу ошибок - обратитесь к собеседнику на ВЫ - ответ на ваш вопрос получите - а остальное вас не касается (п.п.п. на форумах)
 
Цитата
Андрей VG написал:
Максим, попробуйте с таким{{1,2},{2,3},{4},{3}, {1, 3}}
Последнее конечно влияет на алгоритм, но не дает в итоговый набор нового решения, ИМНО.
Чтобы не было замкнутого цикла возможно поможет доп.условие
Код
recursiveQuery As (
       Select con, [from] As [root], [to] from roots
       Union All
       Select tc.con, tr.[root], tc.[to]
       From childs tc
             Inner Join recursiveQuery tr On (tr.[to] = tc.[from] And tr.[from] <> tc.[from])
Неизлечимых болезней нет, есть неизлечимые люди.
 
Цитата
TheBestOfTheBest написал:
поможет доп.условие
Коллега, а зачем? Циклы были устранены ранее.
 
Цитата
Андрей VG написал:
прикладываю более тяжёлый пример. Из левой таблицы нужно получить правую.
приложу часть VBA решения (без цикла по устранению неявных связей - руками можно 2-й раз прогнать код по новому диапазону) - и нюансы прописала в файле - продублирую здесь: (в качестве вывода для ТСа - хоть он и пропал как его связи)
- на более полном примере от Андрей VG - видны недочёты ТЗ:
(попробовала отказаться от рекурсии - встречала частый совет по возможности отказываться от неё)
- мой ВЫВОД:
ОСТАЛСЯ разрыв связи ЖЁЛТЫМ - т.к. клиент НЕ обязан вам сообщать о смене своих контактов... поэтому в такой непостоянной информированности о сторонних процессах (смена контактов клиента) -  всё равно могут появляться разрывы цепочек последовательностей…  
ОТСЛЕДИТЬ ??? ЗАЧЕМ ???? (тогда очередная пробежка макросом для устранения начальных неявных связей) - НО не будет успешна если нет этих неявных (или не отражены)
КОГДА СВЯЗИ НЕ ПРЕДОСТАВЛЯЮТСЯ - ВЫ ТЕРЯЕТЕ ТО. ЧТО НЕ ИМЕЕТЕ !!! (причина: система неполноценная ! - гнилой учёт того что считаете не надо вам)
ЕСЛИ НАДО: ВВОДИТЕ КЛЮЧИ…………… используйте связи по ключам !!
==============
я бы такие связи ТСа даже "многие-ко-многим" не назвала бы !! так что вы там теряете, ТС??  8)
==============
Андрей VG ещё раз спасибо за пример!
(может поэтому в рекурсии и застревало выстраивание дерева - потому что не продумана проверка результата?... но это отдельная история [хоть и небольшая] - но я тоже не проверяла итог на уникальность - ТС проверит если захочет по таким связям с обрывами и пустотами - по ним даже, полагаю, полноценный граф не описать [как уже указал выше Андрей VG ])
Изменено: JeyCi - 25.08.2018 18:26:23
чтобы не гадать на кофейной гуще, кто вам отвечает и после этого не совершать кучу ошибок - обратитесь к собеседнику на ВЫ - ответ на ваш вопрос получите - а остальное вас не касается (п.п.п. на форумах)
 
JeyCi, спасибо за вариант решения.
Боятся рекурсии не стоит. Можно её преобразовать, конечно в цикл, тогда придётся использовать либо очередь либо стек, по другому на естественно рекурсивных структурах, таких как графы, не получится. Да, выигрыш по памяти, как правило, некоторый будет, возможно, будет выигрыш и по быстродействию. Но вот читабельность таких конструкций резко падает. Посмотрите код для QuickSort, предложенный Владимиром (ZVI) и классический рекурсивный код.
 
Цитата
Андрей VG написал:
Да, выигрыш по памяти, как правило, некоторый будет, возможно, будет выигрыш и по быстродействию.
- вот это я и запомнила  :) ...
кстати класс для LIFO ...
да помню вариант ZVI:
Код
Private Type QuickStack
  Low As Long
  High As Long
End Type
логика мне понравилась:
Цитата
Both LIFO and FIFO can be implemented with an array, the only difference between them is in the way tail and head pointers work. Given you start with LIFO, you can add two extra pointers that would reflect FIFO's tail and head, and then add methods to Add, Remove an so on using the FIFO pointers.
Вообще очереди и стэки (если из простых элементов) - то сортировать в нужном порядке в рекордсете по какому-нибудь счётчику введённому (если есть возможность) - то как-то и проще...
смотрела вашу рекурсию xml, ходила вглубь по папке рекурсивно, даже в заполнение связанных комбобоксов (n-ое количество) - рекурсию вставить пришлось... но не люблю я это дело  и при случае  и возможности стараюсь отказываться от неё...
За напоминание о коде ZVI - спасибо... и за ваш имеющийся на форуме пример рекурсии xml - тоже спасибо!   8)  
Изменено: JeyCi - 30.08.2018 19:23:55
чтобы не гадать на кофейной гуще, кто вам отвечает и после этого не совершать кучу ошибок - обратитесь к собеседнику на ВЫ - ответ на ваш вопрос получите - а остальное вас не касается (п.п.п. на форумах)
 
p.s. читать правильно - всем:
там в #27 у меня рудиментами в комментах осталось слово коллекция (сначала думала её использовать) - потом всё делала на словарях (сейчас просто не перевложу)...
и там видно что в заявках встречается c-6, d-9, и только в m они пересекаются (раньше связи нет)... [к слову о жёлтых]...
- уточнения к #27
чтобы не гадать на кофейной гуще, кто вам отвечает и после этого не совершать кучу ошибок - обратитесь к собеседнику на ВЫ - ответ на ваш вопрос получите - а остальное вас не касается (п.п.п. на форумах)
Страницы: 1
Наверх