Проблемы слияния записей в сложносвязанной таблице Oracle

от автора

Предисловие

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

Для любопытных людей которым интересны детали, осторожно, много текста

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

  • Fidelio V8 — модуль управления гостиницей, вполне состоявшееся приложение. Имеет систему бронирования через сайт и от стойки. занимает приоритетное место слегка капризен, кучу отчетов, ведет статистику. В качестве базы данных используется Oracle 11gR2
  • Micros 3700-модуль управления рестораном(работа на местах ведется через типовые POS моноблоки), интерактивная рассадка гостей, разделение чеков и много разных полезных вещей которые облегчают жизнь. Работает на Sybase.
  • TNG- довольно молодой модуль для продажи услуг медицинского центра и спортивно-развлекательного комплекса, своя собственная система бронирования, клиентская база, регистрация клиентов etc. Работает на Oracle 11gR2. Весь пост, собственно и связан с этим модулем.

Однажды на пятничном совещании начальство сказало решить проблему тормозов базы данных модуля управления развлекательного комплекса и медицинского центра, «плавающих отчетов». Мы системные администраторы, наше дело маленькое — решать подобные проблемы.

Что там было

После проверки всех частей системы управления, все внимание обратилось на святая святых системы — базу данных, спасибо тому интегратору, который поставил галочку «Save Password» в SQL Developer.

Проблемы 1,2

  1. Таблица CARDS(клиентские профайлы)- содержит много дублирующих друг друга записей, иногда количество дубликатов доходит до 8-10
  2. в той же таблице отсутствуют дополнительные сведения о пользователях, некритичные, но облегчающие их персональную идентификацию, для начисления бонусов в дальнейшем

Каждая запись в таблице CARDS, тянет за собой записи в дочерних таблицах с предельно жестким обеспечением целостности данных сплошные Restrict и никаких Cascade, финансовая информация все же.

Замечу, что TNG, предоставляет свой инструмент для объединения клиентских профайлов, вот такой:

Тут есть одно большое «НО», или даже два:

  1. За одно действие, нельзя объединить больше чем два профайла
  2. Профайлы базы находятся не в «нормальном состоянии».
Решаем проблему 2

Для начала необходимо расставить «гендерные» признаки людям, ибо по дефолту в таблице это свойство не стояло как обязательное
пишем относительно простой SQL скрипт отдельно для мужчин, для женщин и юридических лиц, сначала тестируем скрипт на выборке

SELECT card_id,first_name,last_name,created_person,card_type_id,card_status_id,creation_date FROM cards --card_id -id профайла --first_name - имя --last_name - фамилия --created_person - кто создал --card_type_id - тип  --card_status_id -статус профайла(активна/заблокирована) --creation_date -дата создания where  last_name like '%в' or last_name like'%ий' or last_name like '%о' or last_name like'%ый'  order by last_name; 

Фамилий кончающихся на «о» меньшинство, и автоматически разделить людей нельзя и обновлять данные в базе для этой категории придется вручную, для остальных категорий пишем простой скрипт на обновление

update cards  set gender = "м" where last_name like '%в' or last_name like'%ий' or last_name like '%о' or last_name like'%ый'  commit; -- не забываем коммитить изменения в таблице, иначе клиентское приложение постоянно будет вылетать с ошибкой 

Аналогично поступаем с профайлами женщин, предварительно тестируя скрипт через выборку

Скрипт аналогичный по содержанию предыдущему

SELECT card_id,first_name,last_name,created_person,card_type_id,card_status_id,creation_date FROM cards where  last_name like '%ва' or last_name like'%ая' or last_name like '%а'  order by last_name; 

После теста выборки обновляем, поле gender

update cards  set gender = "ж" where last_name like '%ва' or last_name like'%ая' or last_name like '%а' ; commit; 

Разбираемся с фамилиями на букву «о», тестируем выборку

SELECT card_id,first_name,last_name,created_person,card_type_id,card_status_id,creation_date FROM cards where  last_name like '%o' and first_name like '%й' or first_name like '%р' order by last_name; 

обновляем гендерный признак для этих записей

update cards  set gender = 'м'  where  last_name like '%o' and first_name like '%й' or first_name like '%р'; commit; 

Получается что профайлы женщин на «о» остались без гендера поэтому обновление для них выглядит просто

update cards set gender  = 'ж' where  last_name like '%o' and gender is null; commit; 

Получается что юридические лица остались единственными, кто имеет «null» gender,

обновляем их следующим образом

update cards set gender = 'ю' where gender is null; commit; 

Со второй проблемой разобрались.

Решаем проблему объединения профайлов

Так как таблица CARDS имеет множественные связи с другими таблицами то возникают некоторые сложности с объединением профайлов, результатом двухнедельных исследований базы(база создавалась не мной и как вы понимаете, понимание принципов ееработы потребовало некоторого времени) стала

следующая процедура

Карты объединяются по критерию:
UPPER(last_name), UPPER(first_name), birth_date, UPPER(second_name)

create or replace PROCEDURE cards_merge AS   vLast VARCHAR2(40); vFirst VARCHAR2(40); vSecond VARCHAR2(40); vBirth DATE; vCardId NUMBER; vOrdr NUMBER; vTargetCardId NUMBER;  -- ACC vAcDefId NUMBER(6,0); vLoanFlg CHAR(1); vAmount NUMBER(19,4); vCrdTtl NUMBER(19,4); vLoanTtl NUMBER(19,4); cntAccBallanceTargetCard NUMBER; vBallance NUMBER(19,4); vCredit NUMBER(19,4); vLoan NUMBER(19,4); -- cntBS NUMBER;  CURSOR curCards  IS  select UPPER(last_name), UPPER(first_name), birth_date, UPPER(second_name) from cards group by UPPER(last_name), UPPER(first_name), birth_date, UPPER(second_name) having count(*) >1 order by UPPER(last_name); CURSOR curMerge(pLast IN VARCHAR2, pFirst IN VARCHAR2, pSecond IN VARCHAR2, pBirth IN DATE)  IS   select card_id, ROW_NUMBER() OVER (PARTITION BY '' ORDER BY card_id DESC) as ordr from CARDS WHERE  (UPPER(last_name)=pLast or (pLast is NULL and last_name is NULL))  and (UPPER(first_name)=pFirst or (pFirst is NULL and first_name is NULL))  and (birth_date = pBirth or (pBirth is NULL and birth_date is NULL))  and (UPPER(second_name)=pSecond or (pSecond is NULL and second_name is NULL))  ORDER BY card_id desc;  CURSOR curAcc(pSourceCardId IN NUMBER) IS   select ac.ACCOUNT_DEF_ID, ac.loan_flag, ac.amount from ACCOUNTING ac, DOCS dc WHERE ac.doc_id=dc.doc_id  and dc.COFIRMATION='T' and ac.CARD_ID=pSourceCardId;                                                                                                                               BEGIN OPEN curCards; LOOP FETCH curCards INTO VLAST, VFIRST, VBIRTH, VSECOND; EXIT WHEN NOT curCards%FOUND;   OPEN curMerge(VLAST, VFIRST, VSECOND, VBIRTH); LOOP FETCH curMerge INTO vCardId, vOrdr; EXIT WHEN NOT curMerge%FOUND;                 if vOrdr=1 then  -- target card;     vTargetCardId := vCardId;     else                      -- ACCOUNTING      OPEN curAcc(vCardId);      LOOP      FETCH curAcc INTO vAcDefId, vLoanFlg, vAmount;      EXIT WHEN NOT curAcc%FOUND;                                      if vAmount < 0 then      vCrdTtl:=vAmount;      else      vCrdTtl:=0;      end if;                                      if vLoanFlg='T' then         vLoanTtl:=vAmount;     else         vLoanTtl:=0;     end if;   select count(*) into cntAccBallanceTargetCard FROM accounting_balance ab    WHERE ab.card_id=vTargetCardId and ab.account_def_id=vAcDefId;  if cntAccBallanceTargetCard =0 then  INSERT INTO accounting_balance( ID, ACCOUNT_DEF_ID, CARD_ID, AMOUNT, CRD_TTL, LAST_EXPIRY, LOAN )  values  (ACCOUNTING_BALANCE_SEQUENCE.nextval, vAcDefId, vTargetCardId, vAmount, vCrdTtl, NULL, vLoanTtl);  else  select ab.amount, ab.crd_ttl, ab.loan into vBallance, vCredit, vLoan  FROM accounting_balance ab      WHERE ab.card_id=vTargetCardId and ab.account_def_id=vAcDefId;      UPDATE accounting_balance SET AMOUNT= vBallance+ vAmount, CRD_TTL=vCredit + vCrdTtl, LOAN=vLoan+ vLoanTtl       WHERE card_id=vTargetCardId and account_def_id=vAcDefId;      end if;      end loop;      CLOSE curAcc;                       UPDATE ACCOUNTING SET CARD_ID=vTargetCardId WHERE CARD_ID=vCardId;  DELETE ACCOUNTING_BALANCE  WHERE CARD_ID=vCardId;  -- END ACCOUNTING   -- BONUS_TRIGGER_HISTORY  UPDATE BONUS_TRIGGER_HISTORY SET CARD_ID=vTargetCardId  WHERE CARD_ID=vCardId;  -- END BONUS_TRIGGER_HISTORY  -- BOOKING_RESOURCES UPDATE BOOKING_RESOURCES SET RESOURCE_ID=vTargetCardId  WHERE RESOURCE_ID=vCardId and RESOURCE_TYPE='P';  -- END BOOKING_RESOURCES -- CARD_FOREIGN_IDS  DELETE FROM CARD_FOREIGN_IDS WHERE CARD_ID=vCardId; -- END CARD_FOREIGN_IDS -- DOCS UPDATE DOCS SET DEF_CARD_ID=vTargetCardId WHERE DEF_CARD_ID=vCardId;  -- END DOCS -- DSC_HISTORY UPDATE DSC_HISTORY SET CARD_ID=vTargetCardId WHERE CARD_ID=vCardId; -- END DSC_HISTORY -- ITEM_ACCOUNTING UPDATE ITEM_ACCOUNTING SET CARD_ID=vTargetCardId WHERE CARD_ID=vCardId; update item_balance ib set ib.count= nvl(( select sum(ia.mi_count) from item_accounting ia, docs d  where ia.doc_id = d.doc_id and ia.subscription_item_id=ib.subscription_item_id and ia.subscription_accounting_id=ib.subscription_accounting_id  and d.cofirmation='T' and ia.card_id=vTargetCardId),0); DELETE  item_balance WHERE CARD_ID=vCardId;                                             -- END ITEM_ACCOUNTING -- ITEM_PERIOD_ACC UPDATE ITEM_PERIOD_ACC SET CARD_ID =vTargetCardId WHERE CARD_ID=vCardId; -- END ITEM_PERIOD_ACC -- REGISTRATION UPDATE REGISTRATION SET card_id=vTargetCardId WHERE CARD_ID=vCardId; -- END REGISTRATION -- SUBSCRIPTION_ACCOUNTING UPDATE SUBSCRIPTION_ACCOUNTING SET card_id=vTargetCardId WHERE CARD_ID=vCardId; -- END SUBSCRIPTION_ACCOUNTING  -- TIMER                   UPDATE TIMER SET card_id=vTargetCardId WHERE CARD_ID=vCardId;                   -- END TIMER                   -- RULESET_PERIOD_ACC                   UPDATE RULESET_PERIOD_ACC SET card_id=vTargetCardId WHERE CARD_ID=vCardId;                   -- END RULESET_PERIOD_ACC                   -- CARD_XTRA                   DELETE CARD_XTRA WHERE CARD_ID=vCardId;                   -- END CARD_XTRA                   -- CLIENT_RELATIONS                   UPDATE CLIENT_RELATIONS SET CARD_ID_1=vTargetCardId WHERE CARD_ID_1=vCardId;                   UPDATE CLIENT_RELATIONS SET CARD_ID_2=vTargetCardId WHERE CARD_ID_2=vCardId;                   DELETE CLIENT_RELATIONS WHERE CARD_ID_1=vTargetCardId and CARD_ID_2=vTargetCardId;                   -- END CLIENT_RELATIONS                   -- CLIENT_PROFILE_DATA                   DELETE CLIENT_PROFILE_DATA WHERE card_id=vCardId;                   -- END CLIENT_PROFILE_DATA                   -- BS_TASK                   UPDATE BS_TASK SET CARD_ID=vTargetCardId WHERE CARD_ID=vCardId;                   -- END BS_TASK                   -- BS_XXX                   SELECT COUNT(*) into cntBS FROM ALL_TABLES WHERE TABLE_NAME = 'BS_BOOKING';                   if cntBS >0 then                       UPDATE BS_BOOKING SET CARD_ID=vTargetCardId WHERE CARD_ID=vCardId;                   end if;                   -- END BS_XXX                   -- CARDS                   UPDATE CARDS SET MASTER_CARD_ID=vTargetCardId WHERE MASTER_CARD_ID=vCardId;                   DELETE CARDS WHERE card_id=vCardId;                   -- END CARDS  -- CARDS_MERGE_HISTORY INSERT INTO CARDS_MERGE_HISTORY  (CARDS_MERGE_HISTORY_ID, empl_id,  operator, source_card_id, source_name, target_card_id, target_name, tm)  values  (CARDS_MERGE_HISTORY_SEQUENCE.nextval, NULL, 'UNKNOWN',  vCardId, VLAST ||' '|| VFIRST, vTargetCardId, VLAST ||' '|| VFIRST,sysdate);                   -- END CARDS_MERGE_HISTORY                                   end if;  end loop;  CLOSE curMerge;  end loop; CLOSE curCards;   END cards_merge; / quit; 

Для контроля используется следующий

скрипт

select UPPER(last_name), UPPER(first_name), birth_date, UPPER(second_name), count(*) from cards group by UPPER(last_name), UPPER(first_name), birth_date, UPPER(second_name) having count(*) >1  order by UPPER(last_name); 

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

Happy end

На данный момент проблема с «плавающими» отчетами решена, и у бухгалтерии больше нет вопросов с финансовыми отчетами, бонусы клиентам тикают. Все довольны, а с понедельника приступаем к внедрению на территории роумингового Wi-Fi, что послужит темой для следующего поста.

ссылка на оригинал статьи http://habrahabr.ru/post/188084/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *