MySQL: съхранена процедура за разделяне на колона (с разделители) и вмъкване в нова таблица

В моята база данни има ненормализирана таблица със структура на име details и примерни данни, както е показано по-долу (извинения за изображението, просто реших, че ще бъде по-разбираемо): **Схема**

Моето предизвикателство е да разделя колони - assignee, inventor and ipcsubclass с помощта на разделител | в нови таблици {detail_inv и inventors}, {detail_asg и assignees} и {detail_ipc и ipcsubclasses}.

И в трите случая схемите на таблиците са подобни. Например колони в таблицата на изобретателите - id и name и в таблицата detail_inv - detail_id и inventor_id. Трябва да има само едно име на ред с всички имена, уникални в таблицата на изобретателите и идентификатори, за да поддържа връзка в таблицата detail_inv.

Опитах съхранена процедура с код по-долу за изобретатели - направих 3 процедури за 3 колони:(

drop procedure if exists normalise_details;

delimiter #

create procedure normalise_details()
proc_main:begin

declare v_cursor_done int unsigned default 0;
declare v_post_id int unsigned;
declare v_tags varchar(2048);
declare v_keyword varchar(50);

declare v_keyword_id mediumint unsigned;

declare v_tags_done int unsigned;
declare v_tags_idx int unsigned;

declare v_cursor cursor for select id, inventor from details order by id;
declare continue handler for not found set v_cursor_done = 1;

set autocommit = 0; 

open v_cursor;
repeat

  fetch v_cursor into v_post_id, v_tags;
  set v_tags_done = 0;       
  set v_tags_idx = 1;

  while not v_tags_done do

    set v_keyword = substring(v_tags, v_tags_idx, 
      if(locate('|', v_tags, v_tags_idx) > 0, 
        locate('|', v_tags, v_tags_idx) - v_tags_idx, 
        length(v_tags)));

      if length(v_keyword) > 0 then

        set v_tags_idx = v_tags_idx + length(v_keyword) + 1;

        set v_keyword = trim(v_keyword);
        insert into inventors (name) values (v_keyword);

        select id into v_keyword_id from inventors where name = v_keyword;
        insert into details_inv (inventor_id, detail_id) values (v_keyword_id, v_post_id);

      else
        set v_tags_done = 1;
      end if;

  end while;

until v_cursor_done end repeat;

close v_cursor;

commit;

end proc_main #


delimiter ;

Когато опитам това върху някои произволни тестови данни, работи добре. когато правя това на действителна маса, не работи добре. Вмъкват се само частични данни. SQL не извежда грешки (с изключение на някои случаи: "#1172 - Резултатът се състои от повече от един ред" или "колоната inventor_id не може да бъде нула")

Опитах се да променя кода в MySQL - Вмъкване на запетая разделен списък в нормализирани таблици чрез съхранена процедура, за да отговаря на нуждите ми, но не успях.

Моля, помогнете ми, моята DB таблица се е превърнала в бъркотия и има около 500 000 реда, което прави наистина трудно за мен да експлодирам и управлявам огромни масиви за всеки проект (скорошен проект с ~ 200 000 реда).


person Fr0zenFyr    schedule 02.09.2013    source източник
comment
екранна снимка с по-добра видимост е тук   -  person Fr0zenFyr    schedule 02.09.2013


Отговори (2)


Разглеждайки публикацията на RolandoMySQLDBA към този въпрос за dba.stackexchange чувствам потвърдено в моите първоначални резерви относно задействаните съхранени процедури. Въпреки това, ако сте сигурни, че във всеки даден момент само няколко реда са променени от потребителски вход, тогава трябва да е възможно да се състави бързо действаща процедура.

Въпреки това, ако има много потребители, работещи паралелно, те може пак да се заключват един друг. Не знам дали това наистина ще се случи, тъй като съхранената процедура няма да промени нищо в таблицата details. Ако е необходимо, можете да разгледате тази страница за идеи.

Редактиране: TRIGGER

Току-що разширих SQLfiddle от предишната си публикация до този SQLfiddle с тригер, съдържащ следното :

CREATE TRIGGER normdet AFTER INSERT ON detail FOR EACH ROW
BEGIN
  DECLARE n int; DECLARE word VARCHAR(64)

 ;SET n=cntparts(NEW.inventor)
 ;WHILE n>0 DO
   SET word=part(new.inventor,n)
   ;IF NOT EXISTS (SELECT * FROM inv WHERE invname=word) THEN
     INSERT INTO inv (invname) VALUES (word)
   ;END IF
   ;INSERT INTO det2inv (didid,diiid) 
    SELECT NEW.id,invid FROM inv WHERE invname=word
   ;SET n=n-1
 ;END WHILE
  -- and similar loops for assignee and cls ...
;END;

Определих и друга функция

CREATE FUNCTION cntparts (var varchar(1024)) RETURNS int
RETURN 1+LENGTH(var)-LENGTH(REPLACE(var,'|',''));

преброяване на думите в дадено varchar. Това може да се използва и за създаване на цикли вместо моите фиксирани UNION конструкции за базовото преобразуване в първия ми пост.

Тригерът вече се грижи за всички нови INSERTs. Подобен тригер все още трябва да бъде написан, за да направи същото за UPDATEs. Това не трябва да е твърде трудно да се направи...

В моя SQLfiddle вмъкнах друг ред в detail след дефиницията на тригера. Резултатите са изброени чрез два сравнителни израза SELECT, вижте fiddle.

отговор на последния коментар:

Е, както предложих в първоначалния си отговор, първо трябва да импортирате всички данни (без да имате инсталирани никакви тригери!!!!) и след това да прегледате detail-таблицата с SELECT/UNION изразите. Преди да направите това, трябва да разберете максималния брой думи във всяка от колоните assignee,inventor и ipsubclass, като използвате

SELECT MAX(cntparts(inventor)) invcnt,
       MAX(cntparts(assignee)) asscnt,
       MAX(cntparts(ipsubclass)) clscnt 
FROM detail

След това можете да коригирате броя на SELECT/UNION твърденията, необходими за всяка колона. След това попълнете таблиците за връзки, както е показано в SQLfiddle.

Може би целият процес отнема известно време, но можете безопасно да работите върху една таблица след друга (първо действителната таблица с атрибути и след това свързаната таблица с връзки).

След това можете да активирате вашия тригер, който след това трябва да работи само върху индивидуално добавени линии.

person Carsten Massmann    schedule 03.09.2013
comment
Благодаря за препратките, в моя случай данните ще бъдат вмъкнати в таблица details, така че няма да повлияят на потребителите, за по-нататъшно потвърждаване на наличността на таблица(и) за потребителите, ще има само 1 потребител, който вмъква само 1 проект наведнъж. - person Fr0zenFyr; 03.09.2013
comment
@Fr0zenFyr Вероятно вече сте разгледали кода, но да, задействащата процедура първо проверява с IF NOT EXISTS (SELECT * FROM inv WHERE invname=word), преди да вмъкне запис в таблиците inv (и по същия начин за assignee и cls). В моя пример името „Beppu, Shiori“ беше вмъкнато отново в detail, но не отново в inv. Но, разбира се, препратката беше зададена в det2inv за този нов запис. - person Carsten Massmann; 03.09.2013
comment
Да, видях редактирания отговор. Благодаря за отделеното време и търпение с мен. Просто опитвах това на моята mySQL таблица. Бяхте прав, тригерите причиняват много режийни разходи. Има ли начин, по който мога да направя това, след като данните за даден проект са качени напълно (данните се импортират в електронна таблица с помощта на phpMyAdmin) вместо всеки ред. - person Fr0zenFyr; 03.09.2013
comment
Към момента този подход е единственото решение, което мога да приема. Вие сте прав, че отнема известно време, за да направите това за всяка маса, но поне върши работата. Благодаря за цялото ви търпение и доброта, свалям шапка за вас, сър. Ще купя този отговор, защото това се отнася до вашия оригинален отговор за групово импортиране и също така предоставя тригер за обработка на прости вмъквания. - person Fr0zenFyr; 09.09.2013

Първо, струва ми се, че трябва да разделите масата си на четири отделни:

  1. detail (главна таблица, съдържаща: id, projectid, publicationnumber, prioritycountry, prioritydate и status)
  2. inv (таблица на изобретателя, съдържаща: invid, invname и евентуално повече данни, свързани с изобретателя)
  3. cls (таблица на ipsubclass, съдържаща: clsid, clsname и евентуално описание на всеки клас)
  4. assignee (съдържащи данни на компании правоприемници като: assid, assname ...)

Тъй като ще има n:m релации между основната таблица и inv, cls и assignee, вие също трябва да настроите таблици за връзки, държащи релациите като

  • det2inv
  • det2cls
  • det2ass

Задачата за преструктуриране може да бъде разделена на няколко стъпки:

Първо трябва да приложите дефинирана от потребителя функция за разделяне на комбинираните стойности. Можете да направите това с функция, както е описано тук

Опростих го още малко, тъй като във вашия пример се сблъскваме само с един разделител |:

CREATE FUNCTION part( x VARCHAR(255), pos INT) 
RETURNS VARCHAR(255) BEGIN
 DECLARE delim char(1)
 ;SET delim='|'
 ;RETURN TRIM(REPLACE(SUBSTRING(SUBSTRING_INDEX(x, delim, pos),
                   LENGTH(SUBSTRING_INDEX(x, delim, pos -1)) + 1),
       delim, ''))
 ;END;

(Забележете функцията TRIM за отрязване на всички нежелани заготовки ...)

След това трябва да дефинирате целевите таблици, съдържащи вашите изобретатели и възможни ipsubclasses (... и правоприемници, което все още не съм направил):

CREATE TABLE inv (invid int auto_increment PRIMARY KEY, invname nvarchar(64));
CREATE TABLE cls (clsid int auto_increment PRIMARY KEY, clsname nvarchar(6));

Чувствайте се свободни да разширите таблиците с допълнителни колони, точно както ви трябват.

Сега попълваме таблиците с уникални стойности. Първо изобретателите в таблица inv:

INSERT INTO inv (invname) 
SELECT inv FROM (
 SELECT part(inventor,1) inv from detail
 UNION 
 SELECT part(inventor,2) from detail
 UNION 
 SELECT part(inventor,3) from detail
 UNION 
 SELECT part(inventor,4) from detail
 UNION 
 SELECT part(inventor,5) from detail
 UNION 
 SELECT part(inventor,6) from detail
 UNION 
 SELECT part(inventor,7) from detail
 UNION 
 SELECT part(inventor,8) from detail
) t WHERE inv>'' ORDER BY inv;

следват подкласовете ips:

INSERT INTO cls (clsname)
SELECT icls FROM (
 SELECT part(iclass,1) icls from detail
 UNION 
 SELECT part(iclass,2) from detail
 UNION 
 SELECT part(iclass,3) from detail
 UNION 
 SELECT part(iclass,4) from detail
 UNION 
 SELECT part(iclass,5) from detail
 UNION 
 SELECT part(iclass,6) from detail
 UNION 
 SELECT part(iclass,7) from detail
 UNION 
 SELECT part(iclass,8) from detail
) t WHERE icls>'' ORDER BY icls;

В моя пример разгледах само първите 8 записа на всяко поле. Това може да бъде модифицирано, за да отговаря на вашите нужди, разбира се. В крайна сметка ще имате две уникално номерирани таблици, съдържащи всички възможни изобретатели и всички възможни ipsubclasses (и по аналогичен начин всички правоприемници). Можете да разгледате моя SQLfiddle тук: http://sqlfiddle.com/#!2/aeafe/1

Оставащата задача сега е да попълните таблиците за връзки с подходящи ключове (двойки идентификатори от основната таблица details и нейните атрибутни таблици inv, clsи assignee.

Редактиране

Таблиците с връзки се попълват със следните твърдения:

INSERT INTO det2inv (didid,diiid)
SELECT id,invid FROM inv 
INNER JOIN detail ON INSTR(inventor,invname)>0;

INSERT INTO det2cls (dcdid,dccid)
SELECT id,clsid FROM cls 
INNER JOIN detail ON INSTR(iclass,clsname)>0;

-- ... and a similar one for det2ass

Функцията INSTR() няма да работи перфектно, тъй като имена като Hagen, Pete ще бъдат съпоставени успешно с Gleichenhagen, Peter. За да избегнете тези случаи, сравнението трябва да бъде модифицирано, както е показано тук:

...
INNER JOIN detail ON INSTR(REPLACE(CONCAT('|',inventor,'|'),' ',''),
                           REPLACE(CONCAT('|',invname,'|'),' ',''))>0;

Можете да видите пълния работещ пример тук: http://sqlfiddle.com/#!2/097be/8

person Carsten Massmann    schedule 02.09.2013
comment
Благодаря за подробното обяснение и много хубава илюстрация с помощта на цигулки. Ще отида и ще проверя как работи това в моята ситуация и ще докладвам тук, но се съмнявам в ефективността на този подход за моите огромни данни. Проблем с този код за мен е, че не мога да кажа за броя на разделителите в колона, може да бъде 0-80. Добра идея ли е да правите UNION 80 пъти? Схемата на таблицата, която предложихте, е точно както направих (и споменах във въпроса :) вижте също следващия коментар. - person Fr0zenFyr; 03.09.2013
comment
Чудех се дали има начин да направя това с помощта на съхранена процедура, защото продължавам да получавам нови данни в details, които не мога да контролирам. И така, моят план беше да напиша съхранена процедура и да направя тригер, който да я изпълнява всеки път, когато влезе нов проект. Наистина не мога да разчитам на ръчното стартиране на тази функция. - person Fr0zenFyr; 03.09.2013
comment
Досега нямам опит с тригери. Избягнах ги, защото преди години наблюдавах ситуация, при която мой колега имаше сериозни проблеми с производителността, причинени от заключвания, наложени от (бавно работещи) задействани съхранени процедури. Предполагам обаче, че във вашия случай би било възможно да се направи задействаната трансформация на входящи/променени редове, тъй като това ще трябва да работи само върху предварително избран набор от редове. Ще го погледна още веднъж... - person Carsten Massmann; 03.09.2013
comment
Благодаря @cars10. Ще чакам вашия актуализиран отговор или нов отговор. - person Fr0zenFyr; 03.09.2013
comment
UDF игнорира ли вмъкванията на вече съществуващи изобретатели/правоприемници/ipcclasses в таблиците. Имам предвид, например, ако името ABC corp. вече съществува в получателите на таблица поради данни за проект 1, ще бъде ли игнорирано, когато същото име бъде намерено в проект 2, или ще бъде вмъкнато отново? - person Fr0zenFyr; 03.09.2013