Показать сообщение отдельно
Старый 21.11.2007, 21:08   #17  
Gustav is offline
Gustav
Moderator
Аватар для Gustav
SAP
Лучший по профессии 2009
 
1,858 / 1152 (42) ++++++++
Регистрация: 24.01.2006
Адрес: Санкт-Петербург
Записей в блоге: 19
Задачка о задвоении серийников
Думаю, что то, о чем сейчас поведаю, тоже можно рассмотреть как вариант. Будет и группировка, и сортировка - и практически "на месте", т.е. без утомительного создания новых структур.

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

В нашей компании складской учет в Аксапте ведется по партиям. Если какая-то номенклатура учитывается по серийным номерам, то комбинация "серийный номер + номер партии" должна быть уникальна по каждому из номеров в отдельности, т.е. один и тот же серийный номер не может встречаться (комбинироваться) с разными номерами партий - только с одной. Так должно быть. И в подавляющем большинстве случаев так оно и есть. Но, однако, не абсолютно во всех случаях. По каким-то причинам образовались дублирования: одному серийнику соответствуют более, чем одна партия. Причины сейчас выясняются.

Для обнаружения дубликатов был написан следующий запрос на классическом 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 )