MySQL obtiene todos los datos de un registro relacionado específico

Estoy buscando una forma de generar un registro relacionado seleccionado para cada registro en una tabla en MySQL. te explico mas...

Tengo 2 mesas monedas y los tipos de cambio. Las mesas están unidas por un código de moneda y cada registro de moneda tiene varios registros de tipo de cambio relacionados, cada registro de tipo de cambio representa un día diferente. Por lo tanto, existe una relación de 1:muchos entre las monedas y los tipos de cambio.

Quiero recuperar un registro completo de la exchange_rates para cada moneda, pero con la capacidad de definir criterios específicos sobre qué registro relacionado seleccionar. No solo el tipo de cambio más reciente para cada moneda, sino también el más reciente exchange_rates registro para cada moneda que tiene el campo criteria_x=NULL.

Es una pena que no puedas usar LIMIT dentro de una tabla derivada, de lo contrario, algo como esto sería una solución ordenada y legible ...

SELECT `currencies`.`currency_code`, `currencies`.`country`, `exchange_rates`.`id`,
       FROM_UNIXTIME(`exchange_rates`.`datestamp`), `rate` 
FROM `currencies` 
INNER JOIN (
SELECT `id`, `currency_code`, `invoice_id`, `datestamp`, `rate` 
FROM `exchange_rates` 
WHERE `criteria_x`=NULL AND `criteria_y` LIKE 'A' 
ORDER BY `datestamp` DESC
LIMIT 0, 1
) AS `exchange_rates` ON `currencies`.`currency_code`=`exchange_rates`.`currency_code`
ORDER BY `currencies`.`country`

La LIMIT La cláusula se aplica a la consulta principal, no a la tabla derivada.

Esta es la única manera que he encontrado para hacer esto...

SELECT `currencies`.`currency_code`, `currencies`.`country`, 
FROM_UNIXTIME( SUBSTRING_INDEX( SUBSTRING_INDEX(`exchange_rates`.`concat`, '-', 1), '-', -1)) AS `datestamp`,
SUBSTRING_INDEX( SUBSTRING_INDEX(`exchange_rates`.`concat`, '-', 2), '-', -1) AS `id`, 
SUBSTRING_INDEX( SUBSTRING_INDEX(`exchange_rates`.`concat`, '-', 3), '-', -1) AS `rate` 
FROM `currencies`
INNER JOIN (
SELECT `currency_code`, MAX(CONCAT_WS('-', `datestamp`, `id`, `rate`)) AS `concat`
FROM `exchange_rates` 
WHERE `criteria_x`=NULL AND `criteria_y` LIKE 'A' 
GROUP BY `exchange_rates`.`currency_code`
) AS `exchange_rates` ON `currencies`.`currency_code`=`exchange_rates`.`currency_code`
ORDER BY `currencies`.`country`

Así que concatenar un montón de campos juntos y ejecutar un MAX() en él para obtener mi orden de clasificación dentro del grupo, luego analizar esos campos en la consulta principal con SUBSTRING_INDEX(). El problema es que este método solo funciona cuando puedo usar un MIN() or MAX() en el campo concatenado. No sería ideal si quisiera ordenar una cadena u ordenar por múltiples criterios pero limitarme a un solo registro.

También me causa dolor físico tener que recurrir a la horrible manipulación de cadenas para obtener los datos que quiero de una base de datos relacional. ¡Tiene que haber una mejor manera!

¿Alguien tiene alguna sugerencia de un mejor método?

preguntado el 03 de mayo de 12 a las 16:05

proporcione la estructura de su tabla y algunos datos para que podamos ver qué podemos hacer para resolverlo:

4 Respuestas

Hay algunas cuestiones generales para discutir (brevemente) antes de intentar dar una respuesta.

Tu primera consulta es:

SELECT `currencies`.`currency_code`, `currencies`.`country`, `exchange_rates`.`id`,
       FROM_UNIXTIME(`exchange_rates`.`datestamp`), `rate` 
FROM `currencies` 
INNER JOIN (
SELECT `id`, `currency_code`, `invoice_id`, `datestamp`, `rate` 
FROM `exchange_rates` 
WHERE `criteria_x`=NULL AND `criteria_y` LIKE 'A' 
ORDER BY `datestamp` DESC
LIMIT 0, 1
) AS `exchange_rates` ON `currencies`.`currency_code`=`exchange_rates`.`currency_code`
ORDER BY `currencies`.`country`
  1. No creo que necesites usar tantos comillas inversas como estás usando. No están exactamente mal, pero no los escribiré en mi respuesta.
  2. El estándar SQL no sanciona la criteria_x = NULL notación; que debe escribirse como criteria_x IS NULL. MySQL puede permitirlo; siempre que sepa que no es estándar, está bien que lo use.
  3. El criterio LIKE 'A' no es sensato si no contiene metacaracteres (% or _ en SQL estándar). Estarías mejor con la igualdad simple: = 'A'.

Tu pregunta dice:

Quiero recuperar un registro completo de la exchange_rates para cada moneda, pero con la capacidad de definir criterios específicos sobre qué registro relacionado seleccionar. No solo el tipo de cambio más reciente para cada moneda, sino quizás el tipo de cambio más reciente para cada moneda que tiene el campo criteria_x IS NULL.

Por lo tanto, desea seleccionar el registro de tipo de cambio más reciente para cada moneda que cumpla con los otros criterios requeridos. Podemos suponer que existe una restricción única en la combinación de currency_code y datestamp en la tabla de tipos de cambio; esto significa que siempre habrá como máximo una fila coincidente. No ha especificado qué se debe mostrar si no hay una fila coincidente; una unión interna simplemente no listará esa moneda, por supuesto.

Con las consultas SQL, generalmente construyo y pruebo la consulta general en pasos, agregando material adicional a las consultas desarrolladas previamente que se sabe que funcionan y producen el resultado correcto. Si es simple y/o he recopilado demasiada arrogancia, primero intentaré una consulta compleja, pero cuando (némesis) no funciona, vuelvo al proceso de compilación y prueba. Piense en ello como un desarrollo basado en pruebas (consultas).

Etapa 1: Registros de tipos de cambio que coinciden con los criterios especificados

SELECT id, currency_code, invoice_id, datestamp, rate 
  FROM exchange_rates 
 WHERE criteria_x IS NULL AND criteria_y = 'A' 
 ORDER BY currency_code, datestamp DESC

Etapa 2: hora del tipo de cambio más reciente para cada moneda que coincida con los criterios especificados

SELECT currency_code, MAX(datestamp) 
  FROM exchange_rates 
 WHERE criteria_x IS NULL AND criteria_y = 'A' 
 GROUP BY currency_code

Etapa 3: registro de tipo de cambio para el tiempo de tipo de cambio más reciente para cada moneda que coincida con los criterios especificados

SELECT x.id, x.currency_code, x.invoice_id, x.datestamp, x.rate 
  FROM exchange_rates AS x
  JOIN (SELECT currency_code, MAX(datestamp) AS datestamp
          FROM exchange_rates 
         WHERE criteria_x IS NULL AND criteria_y = 'A' 
         GROUP BY currency_code
       ) AS m
    ON x.currency_code = m.currency_code AND x.datestamp = m.datestamp

Etapa 4: información de moneda y registro de tasa de cambio para el tiempo de tasa de cambio más reciente para cada moneda que coincida con los criterios especificados

Esto requiere unir la tabla de monedas con el resultado de la consulta anterior:

SELECT c.currency_code, c.country, r.id,
       FROM_UNIXTIME(r.datestamp), r.rate
  FROM currencies AS c 
  JOIN (SELECT x.id, x.currency_code, x.invoice_id, x.datestamp, x.rate 
          FROM exchange_rates AS x
          JOIN (SELECT currency_code, MAX(datestamp) AS datestamp
                  FROM exchange_rates 
                 WHERE criteria_x IS NULL AND criteria_y = 'A' 
                 GROUP BY currency_code
               ) AS m
            ON x.currency_code = m.currency_code AND x.datestamp = m.datestamp
       ) AS r
    ON c.currency_code = r.currency_code
 ORDER BY c.country

Excepto que Oracle solo permite ') r' en lugar de ') AS r' para alias de tablas y el uso de FROM_UNIXTIME(), creo que debería funcionar correctamente con la versión actual de casi cualquier SQL DBMS que quiera mencionar.

Dado que el ID de la factura no se devuelve en la consulta final, podemos eliminarlo de la lista de selección de la consulta intermedia. Un buen optimizador podría hacerlo automáticamente.

Si desea ver la información de la moneda incluso si no hay un tipo de cambio que coincida con los criterios, debe cambiar JOIN en la consulta más externa a LEFT JOIN (también conocido como LEFT OUTER JOIN). Si solo desea ver un subconjunto de las monedas, puede aplicar ese filtro en la última etapa de consulta (la más externa) o (si el filtro se basa en la información disponible en la tabla de tipos de cambio, como el código de moneda) en ya sea la subconsulta más interna (la más eficiente) o la subconsulta intermedia (no tan eficiente a menos que el optimizador se dé cuenta de que puede empujar el filtro hacia la subconsulta más interna).

La corrección suele ser el criterio principal; el rendimiento es un criterio secundario. Sin embargo, el rendimiento se mencionó en la pregunta. La primera regla es medir la consulta 'simple' que se muestra aquí. Solo si eso resulta demasiado lento, debe preocuparse más. Cuando necesite preocuparse, examine el plan de consulta para ver si falta, por ejemplo, un índice crucial. Solo si la consulta aún no es lo suficientemente rápida, comienzas a intentar recurrir a otros trucos. Esos trucos tienden a ser muy específicos para un DBMS en particular. Por ejemplo, puede haber sugerencias de optimizador que puede usar para hacer que el DBMS procese la consulta de manera diferente.

contestado el 23 de mayo de 17 a las 11:05

Una respuesta fantástica, funciona perfectamente y muy bien explicada. Sabía que tenía que haber una forma decente sin concatenar/explotar una cadena de datos. Con respecto a los acentos graves, sí, es un hábito de MySQL y un mal hábito. Podría omitir la mayoría de ellos, pero nunca se sabe qué sucederá con una actualización en un sitio que interrumpirá la consulta si se introduce una nueva palabra clave. También IS NULL/=NULL, sí, me doy cuenta de eso... Simplemente olvidé cambiar eso al pegar mi consulta de demostración en la respuesta. ¡¡¡GRACIAS!!! - batfastad

Gracias. Para una discusión de back-ticks, etc., vea también ¿Qué dice el estándar SQL sobre el uso de `?. - jonathan leffler

Si entendí tu problema correctamente, todo lo que necesitas hacer es unirte exchange_rates para seleccionar la tasa de interés:

SELECT   currencies.currency_code,
         currencies.country,
         exchange_rates.id,
         FROM_UNIXTIME(exchange_rates.datestamp),
         exchange_rates.rate
FROM     currencies
  JOIN   (
    SELECT   currency_code, MAX(datestamp) AS datestamp
    FROM     exchange_rates
    WHERE    criteria_x IS NULL AND criteria_y LIKE 'A'
    GROUP BY currency_code
  )   AS exchange_wantd USING (currency_code)
  JOIN   exchange_rates USING (currency_code, datestamp)
ORDER BY currencies.country

contestado el 09 de mayo de 12 a las 13:05

Prueba esta consulta. Se espera que funcione bien, pero si proporciona algunos datos, podré hacerlo correctamente.

SELECT  `currencies`.`currency_code` as `CurrencyCode`,
    `currencies`.`country`, 
    FROM_UNIXTIME( SUBSTRING_INDEX( SUBSTRING_INDEX(`exchange_rates`.`concat`, '-', 1), '-', -1)) AS `datestamp`,
    SUBSTRING_INDEX( SUBSTRING_INDEX(`exchange_rates`.`concat`, '-', 2), '-', -1) AS `id`, 
    SUBSTRING_INDEX( SUBSTRING_INDEX(`exchange_rates`.`concat`, '-', 3), '-', -1) AS `rate`,
    (SELECT 
            MAX(CONCAT_WS('-', `datestamp`, `id`, `rate`)) AS `concat` 
            FROM `exchange_rates` 
            WHERE `criteria_x`= NULL 
            AND `criteria_y` LIKE 'A' 
            GROUP BY `exchange_rates`.`currency_code`
            HAVING `exchange_rates`.`currency_code` =`CurrencyCode`
    ) as `Concat`
FROM    `currencies`
ORDER BY `currencies`.`country` 

contestado el 04 de mayo de 12 a las 07:05

Gracias, pero se parece a lo que tengo arriba, solo con una subselección en lugar de la unión interna en la tabla derivada. Mi segundo arriba funciona bien, pero estoy tratando de encontrar una manera sin concatenar campos y luego extrapolar una cadena. - batfastad

Es posible que no haya notado que aquí seleccioné CurrencyCode en la consulta externa y luego lo usé en la subconsulta al tener una cláusula para restringir la búsqueda: muhammad raheel

Si te entiendo correctamente, la respuesta es bastante simple si no obligas a la base de datos a pensar como un humano. Al igual que Jonathan Leffler, entiendo su intención de "seleccionar el registro de tipo de cambio más reciente para cada moneda que cumpla con los otros criterios requeridos".

"El más reciente" es, por supuesto, el problema, ya que esa información no se almacena explícitamente en la base de datos, por lo que primero determinamos esta información.

SELECT currency_code, MAX(datestamp) AS datestamp FROM exchange_rates GROUP BY currency_code

Vamos a reutilizar esto, así que le damos un nombre al resultado.

(SELECT currency_code, MAX(datestamp) AS datestamp FROM exchange_rates GROUP BY currency_code) AS dates_we_want

El dominio que contiene toda la información que podríamos desear es un registro para cada combinación posible de las tablas de fechas_que_queremos, divisas y tipos de cambio.

(SELECT currency_code, MAX(datestamp) AS datestamp FROM exchange_rates GROUP BY currency_code) AS dates_we_want, currencies AS c, exchange_rates AS er

Selección de registros que queremos:

  • Coincidencia de códigos_moneda

    date_we_want.currency_code=er.currency_core Y date_we_want.currency_code=c.currency_core

  • tasas más recientes

    fechas_que_queremos.marca de fecha=er.marca de fecha

Proyección en el resultado. Ustedes

desea recuperar un registro completo de la tabla exchange_rates

simplemente se traduce en

er.*

Ponga todo junto en una declaración SELECT y déle el cambio para poner sus restricciones arbitrarias:

SELECT er.*
FROM
    (SELECT currency_code, MAX(datestamp) AS datestamp
       FROM exchange_rates GROUP BY currency_code
    ) AS dates_we_want,
    currencies AS c, exchange_rates AS er
WHERE
    dates_we_want.currency_code=er.currency_core
AND
    dates_we_want.currency_code=c.currency_core
AND
    dates_we_want.datestamp=er.datestamp
AND
    `criteria_x`=NULL AND `criteria_y` LIKE 'A' 

contestado el 15 de mayo de 12 a las 16:05

Tienes que empujar hacia abajo criteria_x y criteria_y condiciones en la subconsulta. Lo que estás preguntando es una pregunta sutilmente diferente. Está solicitando las filas en las que la fila más reciente cumple los subcriterios, en lugar de la fila más reciente que sí cumple los subcriterios. Recomiendo evitar la barra de desplazamiento horizontal cuando sea posible. Personalmente, no creo que la palabra clave AND deba estar sola en una línea. También recomiendo encarecidamente utilizar la notación JOIN explícita en lugar de la lista de expresiones de tabla separadas por comas con combinaciones en la cláusula WHERE. - jonathan leffler

Creo que estás discutiendo para resolver el problema equivocado. No queremos transmitir la pregunta palabra por palabra al DBMS, queremos "recuperar un registro completo de la tabla exchange_rates para cada moneda [satisfaciendo un conjunto de restricciones]". Y mi consulta hace precisamente eso; a propósito, no por accidente ni por casualidad. Ese es el punto de mi primera oración: "no obligues a la base de datos a pensar como un humano". Porque no lo hará y los DBMS son tan buenos para combinar, dividir, seleccionar e interpretar consultas antes de la ejecución, que prácticamente no vale la pena pensar en resultados intermedios. - user1129682

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.