Relação de inserção M: N em massa no PostgreSQL

9

Preciso importar dados de um banco de dados antigo para um novo, com estrutura ligeiramente diferente. Por exemplo, no banco de dados antigo, há uma tabela registrando os funcionários e seus supervisores:

CREATE TABLE employee (ident TEXT PRIMARY KEY, name TEXT, supervisor_name TEXT)

Agora, o novo banco de dados é o seguinte:

CREATE TABLE person (id BIGSERIAL PRIMARY KEY, name TEXT, old_ident TEXT);
CREATE TABLE team (id BIGSERIAL PRIMARY KEY);
CREATE TABLE teammember (person_id BIGINT, team_id BIGINT, role CHAR(1));

Ou seja, em vez de uma tabela simples de funcionários com os nomes de seus supervisores, o novo banco de dados (mais genérico) permite criar equipes de pessoas. Os funcionários são membros com função 'e', supervisores com função 's'.

A questão é como migrar facilmente os dados employeepara a nova estrutura, uma equipe por par empregado-supervisor. Por exemplo, funcionários

employee: ('abc01', 'John', 'Dave'), ('abc02', 'Kyle', 'Emily')

devem ser migrados como

person: (1, 'John', 'abc01'), (2, 'Dave', NULL), (3, 'Kyle', 'abc02'), (4, 'Emily', NULL)
team: (1), (2)
teammember: (1, 1, 'e'), (2, 1, 's'), (3, 2, 'e'), (4, 2, 's')

Eu consideraria usar um CTE modificador de dados, inserindo os funcionários e supervisores primeiro, depois as equipes entre eles. No entanto, o CTE pode retornar apenas dados da linha da tabela inserida. Portanto, não sou capaz de igualar quem era o supervisor de quem.

A única solução que posso ver é usar plpgsql, que simplesmente itera sobre os dados, mantém os IDs de equipe inseridos em uma variável temporária e insere as teammemberlinhas apropriadas . Mas estou curioso para saber se existem soluções mais simples ou mais elegantes.

Haverá aproximadamente centenas a milhares de funcionários. Embora geralmente seja uma boa prática, no meu caso, eu não gostaria de gerar os novos IDs com base nos antigos, pois os IDs antigos são como strings *.GM2. Eu os guardo na old_identcoluna para referência.

Ondřej Bouda
fonte
3
Eu sugeriria adicionar alguns identificadores temporários às novas tabelas. Dessa forma, você pode inserir dados neles enquanto ainda possui as conexões antigas. Em seguida, você pode buscar as linhas necessárias da tabela antiga e inseri-las na próxima tabela e assim por diante. Para isso, eu usaria instruções SQL separadas, sem necessidade de CTEs complicadas ou funções procedurais.
Dez
@dezso Obrigado pela sugestão. Adicionar um identificador temporário ao teamqual conteria o ID da pessoa para a qual a equipe foi criada resolveria o problema. Ainda estou curioso para saber se existe uma solução mais elegante (ou seja, sem DDL).
Ondřej Bouda
@ OndřejBouda, pode ser possível criar as tabelas como consultas CTE, mas pode ficar bastante complicado rapidamente. A solução da tabela (temp) oferece o luxo de testar as etapas individualmente, verificando a contagem de linhas, por exemplo.
Dez

Respostas:

1

Você tem todas as informações necessárias para preencher o novo banco de dados do antigo com 4 instruções de inserção:

create table team_ids (id serial, name TEXT)

insert into team_ids (name)
select distinct supervisor_name from employee

-- now supervisors have ids assigned by "serial" type

insert into person (id, name, old_ident)
select ident, name, ident from employee
union
select ident, supervisor_name, ident from employee

insert into team (id) -- meh
select id from team_ids

insert into teammember (person_id, team_id, role)
select e.ident, t.id, 'e')
from employee as e, join team_ids as t
on t.name = e.supervisor_name
union -- and, I guess
select t.id, t.id, 'm')
from team_ids as t

Você pode ter que ajustar a gosto. Suponho que employee.ident possa ser mapeado para person.id e que seu DBMS permita atribuir valores a colunas com valores gerados automaticamente. Exceto por isso, é apenas SQL básico, nada sofisticado e, é claro , sem loops.

Comentários adicionais:

  • A tabela 'equipe' pode ser (mais convencionalmente) renomeada para departamento .
  • A SERIAL(com seus 2 bilhões de possibilidades) deve ser suficiente, sem necessidade de a BIGSERIAL.
  • Parece não haver mecanismo de banco de dados para impor a cardinalidade 1: 1 do gerente à equipe. Toda equipe não precisa de um líder, por definição? Não existe uma restrição CHECKou FOREIGN KEYpara teammember.role? Talvez a questão tenha simplificado esses detalhes.
  • O nome da tabela "teammember" teria mais convencionalmente um limite de palavras, por exemplo TeamMember ou team_member.
James K. Lowden
fonte
11
Dessa forma, você terá IDs duplicados na persontabela.
Dezso
0

O PL / PgSQL fará o trabalho.

DO $$
DECLARE
  _e record;
  _personid bigint;
  _suppersonid bigint;
  _teamid bigint;
BEGIN
  FOR _e IN
    SELECT ident, name, supervisor_name FROM employee
  LOOP
    -- insert person record for employee
    INSERT INTO person (name, old_ident)
      SELECT _e.name, _e.ident
      RETURNING id INTO _personid;
    -- lookup or insert person record for supervisor
    SELECT id INTO _suppersonid FROM person
      WHERE p.name = _e.supervisor_name;
    IF _suppersonid IS NULL THEN
      INSERT INTO person (name) SELECT _e.supervisor_name
        RETURNING id INTO _suppersonid;
    END IF;
    -- lookup team by supervisor or insert new team
    SELECT team_id INTO _teamid FROM teammember tm
      WHERE tm.person_id = _suppersonid AND tm.role = 's';
    IF _teamid IS NULL THEN
      -- new supervisor: insert new team and supervisor
      INSERT INTO team (id) VALUES(DEFAULT) RETURNING id INTO _teamid;
      INSERT INTO teammember (person_id, team_id, role) SELECT _suppersonid, _teamid, 's';
    END IF;
    -- insert team member (non-supervisor) record
    INSERT INTO teammember (person_id, team_id, role) SELECT _personid, _teamid, 'e';
  END LOOP;
END; $$;
filiprem
fonte