sql >> Base de Datos >  >> RDS >> Mysql

¿Puedo resolver esto con mysql puro? (uniéndose en '' valores separados en una columna)

Si user_resources (t1) era una 'tabla normalizada' con una fila para cada user => resource combinación, entonces la consulta para obtener la respuesta sería tan simple como joining juntas las mesas.

Por desgracia, está denormalized al tener los resources columna como:'lista de ID de recursos' separados por un ';' personaje.

Si pudiéramos convertir la columna de 'recursos' en filas, muchas de las dificultades desaparecerían a medida que las uniones de la tabla se vuelven simples.

La consulta para generar el resultado solicitado:

SELECT user_resource.user, 
       resource.data

FROM user_resource 
     JOIN integerseries AS isequence 
       ON isequence.id <= COUNT_IN_SET(user_resource.resources, ';') /* normalize */

     JOIN resource 
       ON resource.id = VALUE_IN_SET(user_resource.resources, ';', isequence.id)      
ORDER BY
       user_resource.user,  resource.data

La Salida:

user        data    
----------  --------
sampleuser  abcde   
sampleuser  azerty  
sampleuser  qwerty  
stacky      qwerty  
testuser    abcde   
testuser    azerty  

Cómo:

El 'truco' es tener una tabla que contenga los números desde el 1 hasta algún límite. Lo llamo integerseries . Se puede usar para convertir cosas 'horizontales' como:';' delimited strings en rows .

La forma en que esto funciona es que cuando te 'unes' con integerseries , estás haciendo una cross join , que es lo que sucede 'naturalmente' con las 'uniones internas'.

Cada fila se duplica con un 'número de secuencia' diferente de las integerseries tabla que usamos como un 'índice' del 'recurso' en la lista que queremos usar para esa row .

La idea es:

  • cuenta el número de elementos en la lista.
  • extrae cada elemento según su posición en la lista.
  • Usar integerseries para convertir una fila en un conjunto de filas extrayendo la 'id de recurso' individual de user .resources a medida que avanzamos.

Decidí usar dos funciones:

  • función que dada una 'lista de cadenas delimitadas' y un 'índice' devolverá el valor en la posición en la lista. Lo llamo:VALUE_IN_SET . es decir, dado 'A;B;C' y un 'índice' de 2, entonces devuelve 'B'.

  • función que dada una 'lista de cadenas delimitadas' devolverá el recuento de la cantidad de elementos en la lista. Yo lo llamo:COUNT_IN_SET . es decir, dado 'A;B;C' devolverá 3

Resulta que esas dos funciones y integerseries debe proporcionar una solución general para delimited items list in a column .

¿Funciona?

La consulta para crear una tabla 'normalizada' a partir de un ';' delimited string in column . Muestra todas las columnas, incluidos los valores generados debido a 'cross_join' (isequence.id como resources_index ):

SELECT user_resource.user, 
       user_resource.resources,
       COUNT_IN_SET(user_resource.resources, ';')                AS resources_count, 
       isequence.id                                              AS resources_index,
       VALUE_IN_SET(user_resource.resources, ';', isequence.id)  AS resources_value
FROM 
     user_resource 
     JOIN  integerseries AS isequence 
       ON  isequence.id <= COUNT_IN_SET(user_resource.resources, ';')
ORDER BY
       user_resource.user, isequence.id

La salida de la tabla 'normalizada':

user        resources  resources_count  resources_index  resources_value  
----------  ---------  ---------------  ---------------  -----------------
sampleuser  1;2;3                    3                1  1                
sampleuser  1;2;3                    3                2  2                
sampleuser  1;2;3                    3                3  3                
stacky      2                        1                1  2                
testuser    1;3                      2                1  1                
testuser    1;3                      2                2  3                

Usando los user_resources 'normalizados' anteriores tabla, es una unión simple para proporcionar el resultado requerido:

Las funciones necesarias (estas son funciones generales que se pueden usar en cualquier lugar )

nota:Los nombres de estas funciones están relacionados con mysql Función FIND_IN_SET . es decir, ¿hacen cosas similares con respecto a las listas de cadenas?

El COUNT_IN_SET función:devuelve el recuento de character delimited items en la columna

DELIMITER $$

DROP FUNCTION IF EXISTS `COUNT_IN_SET`$$

CREATE FUNCTION `COUNT_IN_SET`(haystack VARCHAR(1024), 
                               delim CHAR(1)
                               ) RETURNS INTEGER
BEGIN
      RETURN CHAR_LENGTH(haystack) - CHAR_LENGTH( REPLACE(haystack, delim, '')) + 1;
END$$

DELIMITER ;

El VALUE_IN_SET función:trata la delimited list como un one based array y devuelve el valor en el 'índice' dado.

DELIMITER $$

DROP FUNCTION IF EXISTS `VALUE_IN_SET`$$

CREATE FUNCTION `VALUE_IN_SET`(haystack VARCHAR(1024), 
                               delim CHAR(1), 
                               which INTEGER
                               ) RETURNS VARCHAR(255) CHARSET utf8 COLLATE utf8_unicode_ci
BEGIN
      RETURN  SUBSTRING_INDEX(SUBSTRING_INDEX(haystack, delim, which),
                     delim,
                     -1);
END$$

DELIMITER ;

Información relacionada:

  • Finalmente resolví cómo obtener SQLFiddle - código de trabajo para compilar funciones.

  • Hay una versión de esto que funciona para SQLite bases de datos también SQLite:¿normalizar un campo concatenado y unirse a él?

Las tablas (con datos):

CREATE TABLE `integerseries` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=500 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

/*Data for the table `integerseries` */

insert  into `integerseries`(`id`) values (1);
insert  into `integerseries`(`id`) values (2);
insert  into `integerseries`(`id`) values (3);
insert  into `integerseries`(`id`) values (4);
insert  into `integerseries`(`id`) values (5);
insert  into `integerseries`(`id`) values (6);
insert  into `integerseries`(`id`) values (7);
insert  into `integerseries`(`id`) values (8);
insert  into `integerseries`(`id`) values (9);
insert  into `integerseries`(`id`) values (10);

Recurso:

CREATE TABLE `resource` (
  `id` int(11) NOT NULL,
  `data` varchar(250) COLLATE utf8_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

/*Data for the table `resource` */

insert  into `resource`(`id`,`data`) values (1,'abcde');
insert  into `resource`(`id`,`data`) values (2,'qwerty');
insert  into `resource`(`id`,`data`) values (3,'azerty');

Recurso_usuario:

CREATE TABLE `user_resource` (
  `user` varchar(50) COLLATE utf8_unicode_ci NOT NULL,
  `resources` varchar(250) COLLATE utf8_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`user`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

/*Data for the table `user_resource` */

insert  into `user_resource`(`user`,`resources`) values ('sampleuser','1;2;3');
insert  into `user_resource`(`user`,`resources`) values ('stacky','3');
insert  into `user_resource`(`user`,`resources`) values ('testuser','1;3');