【问题标题】:MySQL: stored procedure to split column(with delimiters) and insert into new tableMySQL:拆分列(带分隔符)并插入新表的存储过程
【发布时间】:2013-09-05 14:13:36
【问题描述】:

我的数据库中有一个未规范化的表,名称为details,结构和示例数据如下(为图像道歉,只是认为它会更容易理解):

我的挑战是将列 - assignee, inventor and ipcsubclass 使用分隔符 | 拆分为新表 {detail_invinventors}、{detail_asgassignees} 和 {detail_ipcipcsubclasses} .

在所有三种情况下,表模式都是相似的。例如,发明者表上的列-idname 以及detail_inv 表上的列-detail_idinventor_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 - Insert comma separated list into normalized tables via stored procedure 满足我的需要,但我失败了。

请帮帮我,我的数据库表已经变得一团糟,大约有 500,000 行,这让我很难在每个项目上展开和管理巨大的数组(最近的项目有大约 200,000 行)。

【问题讨论】:

  • 能见度更高的截图是here

标签: mysql sql stored-procedures split database-normalization


【解决方案1】:

查看 RolandoMySQLDBA 对此dba.stackexchange question 的帖子,我感觉在我对触发存储过程的最初保留意见中得到了证实。但是,如果您确定在任何给定时间只有 几行 被用户输入更改,那么应该可以组合一个快速执行的过程。

但是,如果有许多用户并行工作,他们可能仍会相互锁定。我不知道这是否真的会发生,因为存储过程不会改变details 表中的任何内容。如有必要,您可以查看this page 的想法。

编辑:触发

我刚刚将我之前帖子的 SQLfiddle 扩展到了这个 SQLfiddle with trigger,包含以下内容:

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

回复上一条评论

好吧,正如我在原始答案中建议的那样,您应该首先导入所有数据(没有安装 any 触发器!!!!)然后通过detail-table 使用SELECT/UNION 语句。在你这样做之前,你应该找出每个列中的最大单词数 assignee,inventoripsubclass 使用

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

然后您可以调整每列所需的SELECT/UNION 语句数。然后填写链接表,如 SQLfiddle 所示。

也许整个过程需要一段时间,但您可以安全地处理一个又一个表(首先是实际属性表,然后是关联的链接表)。

之后你可以激活你的触发器,应该然后只对单独添加的行起作用。

【讨论】:

  • 感谢您的参考,在我的情况下,数据将插入details 表中,因此不会影响用户,为了进一步确认表对用户的可用性,将只有1 个用户一次只插入 1 个项目。
  • @Fr0zenFyr 您现在可能已经看过代码了,但是是的,在将条目插入表inv 之前,触发过程首先检查 IF NOT EXISTS (SELECT * FROM inv WHERE invname=word) (同样适用于assigneecls)。在我的示例中,名称 'Beppu, Shiori' 再次插入到 detail 中,但 not 再次插入到 inv 中。但是,当然,参考 设置在 det2inv 中,用于此新记录。
  • 是的,我确实看到了编辑后的答案。感谢您对我的时间和耐心。我只是在我的 mySQL 表上尝试这个。你是对的,触发器会导致很多开销。有没有一种方法可以在项目数据完全上传后(使用 phpMyAdmin 在电子表格中导入数据)而不是每一行。
  • 到目前为止,这种方法是我能接受的唯一解决方案。您是对的,为每张桌子执行此操作需要一些时间,但它至少可以完成这项工作。感谢您的耐心和善意,先生,我为您脱帽致敬。我会购买这个答案,因为这指的是您对批量导入的原始答案,并且还提供了一个触发器来处理简单的插入。
【解决方案2】:

首先,在我看来,您应该将您的表格分成四个独立的表格:

  1. detail(主表,包含:id, projectid, publicationnumber, prioritycountry, prioritydatestatus
  2. inv (发明人表,包含:invid, invname 可能还有更多 发明人相关数据)
  3. cls(ipsubclass 表,包含:clsid, clsname,可能还有每个类的描述)
  4. assignee(包含受让人公司的数据如:assid, assname ...

由于主表与inv, clsassignee 之间将存在n:m 关系,因此您还应该设置链接表来保存此类关系

  • det2inv
  • det2cls
  • det2ass

重组任务可以分为几个步骤:

首先,您需要应用用户定义的函数来拆分组合值。您可以使用here 中描述的函数来做到这一点

我进一步简化了它,因为在您的示例中,我们只遇到一个分隔符|

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;

接下来是 ipsubclasses:

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, clsassignee的ID对。

编辑

链接表正在填充以下语句:

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

【讨论】:

  • 感谢您的详细解释和使用小提琴的非常好的插图。我会去检查它在我的情况下是如何工作的,然后在这里报告,不过,我怀疑这种方法对我的大量数据的性能。对我来说,这段代码的一个问题是我不能说列中的分隔符数量,它可以是 0-80。做UNION 80 次是个好主意吗?您建议的表架构与我所做的完全相同(并在问题中提到:) 另请参阅下一条评论。
  • 我想知道是否有办法使用存储过程来做到这一点,因为我不断将新数据接收到我无法控制的details。所以,我的计划是写一个存储过程,并在每次有新项目进来的时候触发运行它。我真的不能依赖手动运行这个函数。
  • 到目前为止,我还没有使用触发器的经验。我没有使用它们,因为几年前我观察到我的一位同事由于(运行缓慢)触发的存储过程施加的锁而导致严重的性能问题。不过,我认为,在您的情况下,对传入/更改的行进行触发转换是可行的,因为这只需要对先前选择的行集起作用。我会再看看它...
  • 谢谢@cars10。我将等待您的更新答案或新答案。
  • UDF 是否忽略表中已存在的发明人/受让人/ipcclasses 的插入。我的意思是,例如,如果名称为 ABC corp。由于项目 1 的数据已经存在于表受理人中,当在项目 2 中找到相同的名称时会忽略它还是会再次插入它?
猜你喜欢
  • 2015-07-28
  • 2021-08-15
  • 1970-01-01
  • 2013-03-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-17
  • 1970-01-01
相关资源
最近更新 更多