14.11.2007, 09:21 | #1 |
MCTS
|
order by и group by
Всем привет!
Подскажите пожалуйста существует ли в Аксапте какой либо красивый способ получить из таблицы выборку отсортированную по агрегатному полю? То есть использовать в одном запросе и group by и order by. Кроме варианта с использованием промежуточной таблицы. Задача в следующем. Есть временная таблица MyTbl. Нужно из нее получить первые N записей отсортированных по убыванию агрегатного поля (аналог SELECT TOP N) Сейчас это делается в два прохода: сперва в промежуточную таблицу записывается результат запроса X++: select sum(SumField) from MyTbl group by Field1, Field2 Таблица временная. |
|
14.11.2007, 10:19 | #2 |
Moderator
|
Можно попробовать класс-коллекцию Set, в которую вставлять суммы со знаком минус. Получится требуемый порядок сортировки. А при последующем извлечении минус, естественно, "убирать". Если у вас возможны повторяющиеся значения сумм, то можно попробовать вставлять в Set контейнеры типа [-очередная сумма; очередное значение последовательного уникального счетчика].
X++: static void TestQuasiSelectTop(Args _args) { SetEnumerator en; int a, b; set s = new set (types::Container); s.add([-100,1]); s.add([-700,2]); s.add([-200,3]); s.add([-500,4]); s.add([-200,5]); en = s.getEnumerator(); while (en.moveNext()) { [a,b] = en.current(); info(strfmt('%1 %2', -a,b)); } } 700 2 500 4 200 3 200 5 100 1 |
|
14.11.2007, 10:19 | #3 |
SAP
|
я думая что без временной таблици не обойтись, но можно попробывать следующию конструкцию
X++: insert_recordset table1 (field1, field2) select sum(field1), sum(field2) from table2; while select table1 order by field2 { ......... } |
|
14.11.2007, 11:45 | #4 |
Участник
|
С учетом того, что таблица временная, быстрей не заработает.
insert_recrodset будет обычным по-строчным insert |
|
14.11.2007, 11:47 | #5 |
SAP
|
Цитата:
С учетом того, что таблица временная, быстрей не заработает.
insert_recrodset будет обычным по-строчным insert |
|
14.11.2007, 11:51 | #6 |
SAP
|
Цитата:
Можно попробовать класс-коллекцию Set, в которую вставлять суммы со знаком минус. Получится требуемый порядок сортировки. А при последующем извлечении минус, естественно, "убирать". Если у вас возможны повторяющиеся значения сумм, то можно попробовать вставлять в Set контейнеры типа [-очередная сумма; очередное значение последовательного уникального счетчика].
|
|
14.11.2007, 12:18 | #7 |
Moderator
|
|
|
14.11.2007, 12:32 | #8 |
SAP
|
Цитата:
И? Это одобрение или осуждение?
|
|
14.11.2007, 13:17 | #9 |
Moderator
|
Ну, можно еще попробовать любимое ADO (Поговорим об ADO), которое не только в Excel эффективно выводит, но и умеет сортировать (и фильтровать) набор данных в памяти.
Нижеследующий джоб выводит в инфолог отсортированные ПО УБЫВАНИЮ суммарные дебетовые обороты главной книги, сгруппированные по счетам бухучета за один день хозяйственной деятельности (1 сентября 2007): X++: static void KKu_TestAdoDescSortingInMemory(Args _args) { LedgerTrans ledgerTrans; COM rstAxa; // ADO: Recordset COM flds, fld; // ADO: Fields, Field str currAccountNum; real currAmountMST; int adoTypeToExcel(str _type) { switch (_type) { case 'num' : return 5; // adDouble case 'str' : return 8; // adBSTR case 'date': return 133; // adDBDate } return 8; } anytype adoValueFromExcel(COMVariant _val, int _type) { switch (_type) { case 5: return _val.double(); // adDouble case 6: return _val.currency(); // adCurrency case 7: return _val.date(); // adDate case 11: return _val.boolean(); // adBoolean case 202, 203: return _val.bStr(); // adVarWChar, adLongVarWChar("memo") } return ''; } ; rstAxa = new COM('ADODB.Recordset'); flds = rstAxa.Fields(); // сооружаем "таблицу" из двух полей в памяти flds.Append('AccountNum' , adoTypeToExcel('str' )); flds.Append('AmountMST' , adoTypeToExcel('num' )); rstAxa.Open(); while select AccountNum, sum(AmountMST) from ledgerTrans group by AccountNum where ledgerTrans.TransDate == 01\09\2007 && ledgerTrans.Crediting == NoYes::No { rstAxa.AddNew(); fld = flds.Item('AccountNum' ); fld.Value(ledgerTrans.AccountNum ); fld = flds.Item('AmountMST' ); fld.Value(ledgerTrans.AmountMST ); rstAxa.Update(); } rstAxa.Sort('AmountMST DESC'); // задаем убывающую сортировку по полю AmountMST rstAxa.MoveFirst(); while (!rstAxa.EOF()) { fld = flds.Item('AccountNum' ); currAccountNum = adoValueFromExcel(fld.Value(), fld.Type()); fld = flds.Item('AmountMST' ); currAmountMST = adoValueFromExcel(fld.Value(), fld.Type()); info(strfmt('%1 --- %2', currAccountNum, currAmountMST)); rstAxa.MoveNext(); } rstAxa.Close(); } Кстати, на базе подхода можно попробовать взрастить конкурента некоторым системным классам-коллекциям (Set, RecordSortedList) |
|
|
За это сообщение автора поблагодарили: PavelX (1). |
14.11.2007, 14:36 | #10 |
MCTS
|
Всем спасибо, особенно Gustav!
Картина в общем ясна |
|
14.11.2007, 22:27 | #11 |
Участник
|
Это все здорово как упражнения для изучения системы.
Но в чем выигрыш? Поддерживать такой код - мама не горюй. Если понадобится расширить обработку сагрегированных данных, а не только сортировать их, что тогда? Или включить в запрос? К тому же ADO - это внешняя компонента по отношению к Axapta, это тоже минус. Работа с COM объектами - это накладные расходы, еще проверить по производительности не мешало бы, что выгоднее. С учетом того, что временные таблицы - это родной механизм Axapta и встречаются в ней сплошь и рядом, что может испортить еще одна временная таблица? Кстати, konopello тоже неполохой вариант предложил. Добавлено. Мда...тестирование производительности варианта с временной таблицей и ADO RecordSet не выявило существенной разницы. Надо машинку послабее взять, видимо. В коде Gustav пришлось изменить строку X++: fld.Value(ledgerTrans.AccountNum ) X++: fld.Value( COMVariant::createFromStr(ledgerTrans.AccountNum) ) Последний раз редактировалось fomenka; 14.11.2007 в 22:54. |
|
14.11.2007, 23:04 | #12 |
Участник
|
еще можно хранить отсортированный массив из N элементов - тогда не придется держать в памяти таблицу из всех групп
|
|
14.11.2007, 23:32 | #13 |
Member
|
Если у вас 3.0 или выше и в запросе нет объеднений, отличных от inner join, то можно попробовать view.
__________________
С уважением, glibs® |
|
14.11.2007, 23:32 | #14 |
Moderator
|
2 fomenka: Ваша пламенная речь ко мне обращена? Я должен как-то реагировать?
Спасибо. Это на 4-ке? (у меня мой вариант работает на 3-ке SP3, SP4) |
|
15.11.2007, 03:09 | #15 |
Участник
|
Gustav Ничего личного, код понравился, вот и высказался.
А запускал на такой же 3-ке, удивился сам. Может, версия mdac - причина? |
|
15.11.2007, 09:18 | #16 |
Moderator
|
Всё хорошо
Цитата:
|
|
21.11.2007, 21:08 | #17 |
Moderator
|
Задачка о задвоении серийников
Думаю, что то, о чем сейчас поведаю, тоже можно рассмотреть как вариант. Будет и группировка, и сортировка - и практически "на месте", т.е. без утомительного создания новых структур.
Подход иллюстрируется на небольшой практической задачке, которую мне пришлось выполнить вчера. В нашей компании складской учет в Аксапте ведется по партиям. Если какая-то номенклатура учитывается по серийным номерам, то комбинация "серийный номер + номер партии" должна быть уникальна по каждому из номеров в отдельности, т.е. один и тот же серийный номер не может встречаться (комбинироваться) с разными номерами партий - только с одной. Так должно быть. И в подавляющем большинстве случаев так оно и есть. Но, однако, не абсолютно во всех случаях. По каким-то причинам образовались дублирования: одному серийнику соответствуют более, чем одна партия. Причины сейчас выясняются. Для обнаружения дубликатов был написан следующий запрос на классическом SQL (для Oracle): Код: SELECT Tmp01.InventSerialId AS "Серийный номер", Tmp02.Cnt AS "Кол-во повторений", --// ...этого серийника для разных номеров партий Tmp01.InventBatchId AS "Номер партии" FROM (SELECT InventSerialId, InventBatchId FROM InventDim INNER JOIN InventTrans ON InventDim.InventDimId = InventTrans.InventDimId WHERE InventSerialId <> CHR(2) --// CHR(2) - аксаптовский null для Oracle GROUP BY InventSerialId, InventBatchId ) Tmp01 --// уникальные сочетания серийника и партии, встречающиеся в InventTrans INNER JOIN (SELECT InventSerialId, COUNT(*) AS Cnt FROM (SELECT InventSerialId, InventBatchId FROM InventDim INNER JOIN InventTrans ON InventDim.InventDimId = InventTrans.InventDimId WHERE InventSerialId <> CHR(2) GROUP BY InventSerialId, InventBatchId) --// ЕЩЕ РАЗ увы! :( : уникальные сочетания серийника и партии, встречающиеся в InventTrans GROUP BY InventSerialId HAVING COUNT(*) <> 1 ) Tmp02 --// уникальные серийники из уникальных сочетаний серийник+партия ON Tmp01.InventSerialId = Tmp02.InventSerialId ORDER BY Tmp01.InventSerialId, Tmp01.InventBatchId 1. Из таблицы InventDim (Складская аналитика), связанной по полю InventDimId с таблицей InventTrans (Складские проводки), группировкой выбираются уникальные комбинации серийных номеров и номеров партий (поля InventSerialId, InventBatchId). 2. Далее из получившейся выборки выбираются (извиняюсь за тавтологию!) уникальные серийные номера (группировкой уже только по InventSerialId) и для каждого серийного номера подсчитывается кол-во различающихся номеров партий (count); при этом отбрасываются строки с count = 1, так как в данном случае они нам не интересны. 3. Наконец, из выборки п.1 выбираются записи, в которых встречаются серийные номера, обнаруженные выборкой п.2. Записи сортируются, чтобы две различные партии, соответствующие одному серийнику, соседствовали в этом окончательном списке. В принципе задачу обнаружения задвоений можно было бы считать законченной и приступать к поиску причин их возникновения, но захотелось попробовать реализовать этот запрос средствами Аксапты. И как-нибудь эдак..."прикольненько", чтобы не только результат, но и удовольствие от решения получить . После перебора возможных способов я остановился на варианте с "превращением" постоянной таблицы во временную (и не один раз!) при помощи setTmp (справка: kerndoc://Classes/xRecord/setTmp) и с использованием свободных в данный момент полей практически без усилий созданных временных таблиц для хранения промежуточных вычислений. Умудрился даже похранить целое (count) в текстовом поле, потому что в таблице InventDim не оказалось подходящих целочисленных полей, кроме как несвободного святого RecId. В общем, на мой взгляд, получилось действительно "прикольненько". Спешу поделиться джобом-примером, выводящим в окно infolog список проблемных серийных номеров: X++: static void KKu_FindDupleInventSerialId(Args _args) { InventDim inventDim; // промежуточные вычисления будут выполняться на временных клонах таблицы inventDim InventDim inventDimTmp01; // уникальные InventSerialId, InventBatchId InventDim inventDimTmp02; // уникальные InventSerialId InventTrans inventTrans; int rowCounter; ; // --- Шаг 1. Уникальные InventSerialId, InventBatchId inventDimTmp01.setTmp(); rowCounter = 0; ttsbegin; while select InventSerialId, InventBatchId from inventDim group by InventSerialId, InventBatchId exists join inventTrans where inventDim.InventDimId == inventTrans.InventDimId { if (inventDim.inventSerialId) { rowCounter++; inventDimTmp01.inventDimId = strfmt('%1', rowCounter); // фиктивное заполнение обязательного поля inventDimTmp01.inventSerialId = inventDim.inventSerialId; inventDimTmp01.inventBatchId = inventDim.inventBatchId; inventDimTmp01.doInsert(); // для обхода метода insert основной таблицы, который может быть перекрыт } } ttscommit; // --- Шаг 2. Искомые уникальные InventSerialId c Count(*) > 1 inventDimTmp02.setTmp(); rowCounter = 0; ttsbegin; while select InventSerialId, count(RecId) from inventDimTmp01 group by InventSerialId { if( inventDimTmp01.RecId != 1 ) { rowCounter++; inventDimTmp02.inventDimId = strfmt('%1', rowCounter); // фиктивное заполнение обязательного поля inventDimTmp02.inventSerialId = inventDimTmp01.inventSerialId; inventDimTmp02.inventBatchId = strfmt('%1', inventDimTmp01.RecId); // используем свободное строковое поле для хранения Count inventDimTmp02.doInsert(); } } ttscommit; // --- Шаг 3. Искомые проблемные InventSerialId, повторяющиеся c разными номерами партий info('Серийный номер --- Кол-во повторений -- Номер партии'); info('===================================================='); while select inventDimTmp02 order by inventSerialId join inventDimTmp01 order by inventBatchId where inventDimTmp02.inventSerialId == inventDimTmp01.inventSerialId { info( strfmt('%1 --- %2 --- %3', inventDimTmp01.inventSerialId, inventDimTmp02.inventBatchId, // в этом поле хранится Сount inventDimTmp01.inventBatchId ) ); } } * Возможность НЕ создавать в АОТ новые временные таблицы. * Возможность многократного временного клонирования исходной основной таблицы и последующая связь клонов в операторах select (while select) как между собой, так и с другими таблицами. ПРИМЕЧАНИЕ: если в вашей системе нет проблемы задвоения серийников или не ведется учёт по партиям, а работу джоба проверить хочется, то просто замените в нем условие if( inventDimTmp01.RecId != 1 ) на условие if( rowCounter < 100 ) |
|
22.11.2007, 12:01 | #18 |
Участник
|
Не знаю, работает ли это в Oracle, но в MS SQL есть такая фича
PHP код:
Соответственно, можно другим запросом подсчитать количество серийных номеров для каждой партии. Смотря что именно нужно. Кроме того, судя по коду, у Вас только одна компания, раз нет фильтра по DataAreaId. Ну, а собственно JOB мне не нравится избыточным количеством проходов. Поскольку уже первый запрос упорядочивает данные по полям InventSerialId, InventBatchId, то какие проблемы тут же и подсчитать количество повторов? Ведь записи выстроены в нужной последовательности. Собственно, все можно решить за один проход. И без каких-либо временных таблиц. X++: static void KKu_FindDupleInventSerialId(Args _args) { InventDim inventDim; InventTrans inventTrans; int nextI; InventSerialId inventSerialIdPrev; Container conValue; ; info('Серийный номер --- Кол-во повторений -- Номер партии'); info('===================================================='); while select InventSerialId, InventBatchId from inventDim group by InventSerialId, InventBatchId exists join inventTrans where inventDim.InventDimId == inventTrans.InventDimId { if (inventDim.inventSerialId != inventSerialIdPrev) { if (inventSerialIdPrev && conLen(conValue) > 1) { for (nextI = 1; nextI <= conLen(conValue); nextI++ ) { info( strfmt('%1 --- %2 --- %3', inventSerialIdPrev, conLen(conValue), conPeek(conValue,nextI) ) ); } } // Сброс значений очередной группы inventSerialIdPrev = inventDim.inventSerialId; conValue = conNull(); } else { conValue += [inventDim.inventBatchId]; } } if (inventSerialIdPrev && conLen(conValue) > 1) { for (nextI = 1; nextI <= conLen(conValue); nextI++ ) { info( strfmt('%1 --- %2 --- %3', inventSerialIdPrev, conLen(conValue), conPeek(conValue,nextI) ) ); } } } |
|
22.11.2007, 13:45 | #19 |
Участник
|
Цитата:
В нашей компании складской учет в Аксапте ведется по партиям. Если какая-то номенклатура учитывается по серийным номерам, то комбинация "серийный номер + номер партии" должна быть уникальна по каждому из номеров в отдельности, т.е. один и тот же серийный номер не может встречаться (комбинироваться) с разными номерами партий - только с одной. Так должно быть. И в подавляющем большинстве случаев так оно и есть. Но, однако, не абсолютно во всех случаях. По каким-то причинам образовались дублирования: одному серийнику соответствуют более, чем одна партия. Причины сейчас выясняются.
по-моему совсем не очевидно, что комбинация "серийный номер + номер партии" должна быть уникальна..., тк номер партии не уникальный и серийный номер тоже. Последний раз редактировалось ice; 22.11.2007 в 13:47. |
|
22.11.2007, 15:44 | #20 |
Moderator
|
2 Владимир Максимов
Владимир, конечно же есть такая фича COUNT(DISTINCT ) и в Оракле. Не могу сказать, что она в данном случае сильно спасает, так как номера партий все равно нужно откуда-то доставать. Обычно подобные запросы я пишу с использованием фразы WITH, и тогда повторяющийся фрагмент (который "ЕЩЕ РАЗ увы") выносится за "скобки" основного запроса и прописывается один раз перед ним: Код: WITH Tmp01 AS (SELECT InventSerialId, InventBatchId FROM InventDim INNER JOIN InventTrans ON InventDim.InventDimId = InventTrans.InventDimId WHERE InventSerialId <> CHR(2) --// CHR(2) - аксаптовский null для Oracle GROUP BY InventSerialId, InventBatchId ) --// уникальные сочетания серийника и партии, встречающиеся в InventTrans SELECT Tmp01.InventSerialId AS "Серийный номер", Tmp02.Cnt AS "Кол-во повторений", --// ...этого серийника для разных номеров партий Tmp01.InventBatchId AS "Номер партии" FROM Tmp01 INNER JOIN (SELECT InventSerialId, COUNT(*) AS Cnt FROM Tmp01 GROUP BY InventSerialId HAVING COUNT(*) <> 1 ) Tmp02 --// уникальные серийники из уникальных сочетаний серийник+партия ON Tmp01.InventSerialId = Tmp02.InventSerialId ORDER BY Tmp01.InventSerialId, Tmp01.InventBatchId Но вообще речь-то не обо всем об этом. Виноват, если показалось по-другому, но я не ставил целью обсуждать способ нахождения задвоений (и максимально его конкретно оптимизировать - и в SQL, и в Аксе). Это просто пример о чем-нибудь. Цель другая - показать, как довольно сложный запрос SQL можно достаточно прозрачно реализовать в Аксапте серией последовательных простых select'ов на основе временных таблиц, которые не нужно готовить заранее! А также показать, как в рамках одного метода (джоба) можно фактически реализовать вычисления типа SELEСT * FROM (SELEСT * FROM (SELEСT * FROM (SELEСT * FROM (SELEСT * FROM (SELEСT * FROM... ))))) Нельзя в лоб сделать в аксаптовском select'е одновременно группировку и сортировку (причем, убывающую)? Пожалуйста, разделим процесс на 2 запроса. А если что-то посложнее, то и на 3, и на 4, и на более. |
|