Insertar en masa relación M: N en PostgreSQL

9

Necesito importar datos de una base de datos anterior a una nueva, con una estructura ligeramente diferente. Por ejemplo, en la base de datos anterior, hay una tabla que registra a los empleados y sus supervisores:

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

Ahora, la nueva base de datos es la siguiente:

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));

Es decir, en lugar de una simple tabla de empleados con los nombres de sus supervisores, la nueva base de datos (más genérica) permite crear equipos de personas. Los empleados son miembros con función 'e', supervisores con función 's'.

La pregunta es cómo migrar fácilmente los datos employeea la nueva estructura, un equipo por par empleado-supervisor. Por ejemplo, empleados

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

deben 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')

Consideraría usar un CTE modificador de datos, insertando primero a los empleados y supervisores, luego equipos entre ellos. Sin embargo, CTE solo puede devolver datos de la fila de tabla insertada. Por lo tanto, no puedo igualar quién era el supervisor de quién.

La única solución que puedo ver es usar plpgsql, que simplemente iteraría sobre los datos, mantendría las ID de equipo insertadas en una variable temporal y luego insertaría las teammemberfilas apropiadas . Pero tengo curiosidad por saber si hay soluciones más simples o más elegantes.

Habrá aproximadamente de varios cientos a varios miles de empleados. Aunque generalmente es una buena práctica, en mi caso, no me gustaría generar las nuevas ID basadas en las antiguas, ya que las ID antiguas son como cadenas *.GM2. Los guardo en la old_identcolumna como referencia.

Ondřej Bouda
fuente
3
Sugeriría agregar algunos identificadores temporales a las nuevas tablas. De esta manera, puede insertar datos en ellos mientras aún tiene las conexiones antiguas, luego puede buscar las filas necesarias de la tabla anterior e insertarlas en la tabla siguiente, y así sucesivamente. Para esto, usaría sentencias SQL separadas, sin necesidad de CTE complicados o funciones de procedimiento.
dezso
@dezso Gracias por la sugerencia. Agregar un identificador temporal al teamcual se guardaría la identificación de la persona para la cual se creó el equipo resolvería el problema. Sin embargo, todavía tengo curiosidad por saber si existe una solución más elegante (es decir, sin usar DDL).
Ondřej Bouda
@ OndřejBouda podría ser posible construir las tablas como consultas CTE, pero podría complicarse bastante rápido. La solución de tabla (temp) le brinda el lujo de probar los pasos individualmente, por ejemplo, al verificar los recuentos de filas.
dezso

Respuestas:

1

Tiene toda la información que necesita para completar la nueva base de datos de la anterior con 4 instrucciones de inserción:

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

Puede que tenga que adaptarse al gusto. Supongo que employee.ident se puede asignar a person.id, y que su DBMS permite asignar valores a columnas con valores generados automáticamente. Excepto por eso, es solo SQL básico, nada sofisticado y, por supuesto , sin bucles.

Comentario adicional:

  • La tabla de 'equipo' podría ser (más convencionalmente) renombrada a departamento .
  • A SERIAL(con sus 2 mil millones de posibilidades) debería ser suficiente, sin necesidad de a BIGSERIAL.
  • Parece que no existe un mecanismo de base de datos para imponer la cardinalidad 1: 1 del gerente al equipo. ¿No todos los equipos necesitan un líder, por definición? ¿No hay una CHECKo FOREIGN KEYlimitación para teammember.role? Quizás la pregunta simplificó estos detalles.
  • El nombre de la tabla "miembro del equipo" tendría más convencionalmente un límite de palabra, por ejemplo, TeamMember o team_member.
James K. Lowden
fuente
1
De esta manera, tendrá ID duplicados en la persontabla.
dezso
0

PL / PgSQL hará el trabajo.

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
fuente