return mysql 中预先确定的一组 ID 中具有最高 ID 的值

return the value with the highest ID, of a predetermined set of IDs in mysql

我正在尝试提取与为每个 patientID 预定义的一组 detailID 的最高 detailID 关联的值。

这里是 table 的示例(也可以作为 fiddle):

CREATE TABLE `patient_details` (
    patientId INT UNSIGNED NOT NULL,
    detailId INT UNSIGNED NOT NULL,
    `value` VARCHAR(256),
    PRIMARY KEY (patientId, detailId),
    UNIQUE INDEX details_of_patient (patientId, detailId)
);

INSERT INTO patient_details (patientId, detailId, `value`)
  VALUES
(123, 75, '01'),
(123, 98, '02'),
(123, 151, '03 hit'),
(123, 251, '04'),

(321, 65, '05'),
(321, 75, '04'),
(321, 98, '03'),
(321, 151, '02 hit'),
(321, 180, '01'),

(123456, 75, '01'),
(123456, 89, '12/01/2022'),
(123456, 151, '03 hit'),
(123456, 215, '5681'),

(678910, 75, '01'),
(678910, 151, '03'),
(678910, 203, '12/01/2022 hit'),
(678910, 215, '56813')
;

我需要做的是拉取最高的detailID 75,151,203的值。

我尝试使用 if 函数来测试 detailID,但出现语法错误。

逻辑上我认为我应该能够做这样的嵌套 IF

select 
patientId,
table.value

if(detailid=203,set @largest_detailID=203,if(detailid=151,set @largest_detailID=151,if(detailid=75,set @largest_detailID=75,)))

from table

where detailID=@largest_detailID

我期望的结果

patientID value
123 03 hit
321 02 hit
123456 03 hit
678910 12/01/2022 hit

虽然此站点上有许多问题和答案解决了从对应于最大值或最小值(例如“Retrieving the last record in each group", "MySQL Left Join + Min”)的行中获取非聚合列的问题,但它们不会 select 来自有限的值列表,调整答案以添加此限制也不是微不足道的。

与其试图弄乱所有 IF@ 变量,为什么不使用 detailID 的降序来提供帮助,然后添加一个 LIMIT 1 根据您的条件中的 3 detailID 个数字仅获取存在的最高参考:

试试:

SELECT patientId, value
  FROM patientInfo
 WHERE detailID IN (75, 151, 203)
 ORDER BY detailID DESC
 LIMIT 1;

...并且由于 20389detailID 在查询结果中不存在,因此将获得预期的条目:

示例 dbfiddle.

假设您想将此逻辑应用于多个患者,请尝试使用 ROW_NUMBER() 按 patientId 对数据进行分区,然后根据最高的 detailId 进行排序:

WITH cte AS (
    SELECT * 
           , ROW_NUMBER() OVER(
                 PARTITION BY PatientId 
                 ORDER BY DetailID DESC
            ) AS RowNum
    FROM   YourTable
    WHERE  DetailId IN (75, 151, 203)
)
SELECT * 
FROM   cte 
WHERE  RowNum = 1

示例数据:

patientId detailId value
123456 75 01
123456 89 12/01/2022
123456 151 03
123456 215 56813
678910 75 01
678910 203 12/01/2022
678910 151 03
678910 215 56813

结果:

patientId detailId value RowNum
123456 151 03 1
678910 203 12/01/2022 1

db<>fiddle here

group-wise maximums and minimums 的查询提供了一个很好的起点;棘手的部分可能是弄清楚如何有效地整合对 ID 的限制。

相关子查询

A correlated subquery容易适配,但性能较差。没有限制,MySQL手册中的第一个例子可以通过简单的改名来转换:

SELECT patientId, detailId, `value`
FROM   patient_details AS pd
WHERE  detailId=(
           SELECT MAX(pd2.detailId)
             FROM patient_details AS pd2
             WHERE pd.patientId = pd2.patientId
       )
;

由于 sub-query 是结果的简单最大值,向 WHERE 子句添加条件将适当限制 detailId

SELECT patientId, detailId, `value`
FROM   patient_details AS pd
WHERE  detailId=(
           SELECT MAX(pd2.detailId)
             FROM patient_details AS pd2
             WHERE pd.patientId = pd2.patientId
               AND pd2.detailId IN (75, 151, 203) -- the added restriction
       )
;

Performance-wise,这涉及对外部查询的完整table扫描;应用 EXPLAIN to the query for the sample schema & data gives the following plan:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY pd NULL ALL NULL NULL NULL NULL 17 100 Using where
2 DEPENDENT SUBQUERY pd2 NULL ref PRIMARY,details_of_patient PRIMARY 4 pd.patientId 4 30 Using where; Using index

注意主查询将 17 行中的 100% 连接到从属子查询。

优化

下面的不相关子查询会建议优化:限制detailId的范围。将优化应用于相关子查询给出:

SELECT patientId, detailId, `value`
FROM   patient_details AS pd
WHERE  detailId=(
           SELECT MAX(pd2.detailId)
              FROM patient_details AS pd2
              WHERE pd.patientId = pd2.patientId
                AND pd2.detailId IN (75, 151, 203)
                AND pd2.detailId BETWEEN 75 AND 203 -- optimization
       ) AND pd.detailId BETWEEN 75 AND 203 -- optimization
;

这减少了主查询中由 9 连接的行数:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY pd NULL ALL NULL NULL NULL NULL 17 11.11 Using where
2 DEPENDENT SUBQUERY pd2 NULL ref PRIMARY,details_of_patient PRIMARY 4 so.pd.patientId 4 5.88 Using where; Using index

优化同样会影响从属子查询中的行过滤,但最终不会影响性能,因为这些行没有连接到任何后续内容。

不相关的子查询

不相关的子查询也很容易以同样的方式改编:更改名称并向子查询添加条件:

SELECT pd.patientId, pd.detailId, pd.`value`
  FROM patient_details AS pd
    JOIN (
        SELECT patientId, MAX(detailId) AS detailId
          FROM patient_details
          WHERE detailId IN (75, 151, 203) -- the added restriction
          GROUP BY patientId
      ) AS pd2
      ON pd.patientId = pd2.patientId AND pd.detailId = pd2.detailId
;

但是,性能比基本相关子查询差一点:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY NULL ALL NULL NULL NULL NULL 5 100 Using where
1 PRIMARY pd NULL eq_ref PRIMARY,details_of_patient PRIMARY 8 pd2.patientId,pd2.detailId 1 100 NULL
2 DERIVED patient_details NULL index PRIMARY,details_of_patient details_of_patient 8 NULL 17 30 Using where; Using index

优化

性能不佳主要是因为 IN 条件。无限制查询的计划性能更高:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY NULL ALL NULL NULL NULL NULL 5 100 Using where
1 PRIMARY pd NULL eq_ref PRIMARY,details_of_patient PRIMARY 8 pd2.patientId,pd2.detailId 1 100 NULL
2 DERIVED patient_details NULL range PRIMARY,details_of_patient PRIMARY 4 NULL 5 100 Using index for group-by

请注意,根据此计划,查询使用计划中针对受限查询显示的 range join on pd2, as opposed to the index 扫描。这指向一个可能的优化:在 detailId:

上添加一个范围条件
SELECT pd.patientId, pd.detailId, pd.`value`
  FROM patient_details AS pd
    JOIN (
        SELECT patientId, MAX(detailId) AS detailId
          FROM patient_details
          WHERE detailId IN (75, 151, 203)
            AND detailId BETWEEN 75 AND 203 -- the added optimization
          GROUP BY patientId
      ) AS pd2
      ON pd.patientId = pd2.patientId AND pd.detailId = pd2.detailId
;

这会产生比未优化查询更好的计划(尽管仍然落后于优化查询),因为它减少了主 select:

中的行数
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY NULL ALL NULL NULL NULL NULL 2 100 Using where
1 PRIMARY pd NULL eq_ref PRIMARY,details_of_patient PRIMARY 8 pd2.patientId,pd2.detailId 1 100 NULL
2 DERIVED patient_details NULL index PRIMARY,details_of_patient details_of_patient 8 NULL 17 5.88 Using where; Using index

从 MySQL 8.0 开始,查询可以写成 CTE:

WITH pd2 AS (
    SELECT patientId, MAX(detailId) AS detailId
      FROM patient_details
      WHERE detailId IN (75, 151, 203)
        AND detailId BETWEEN 75 AND 203
      GROUP BY patientId)
SELECT pd.patientId, pd.detailId, pd.`value`
  FROM patient_details AS pd
    JOIN pd2
      ON pd.patientId = pd2.patientId AND pd.detailId = pd2.detailId
;

左连接

左连接最难适应,因为天真地添加 detailID 限制在大多数情况下会产生不正确的结果。从文档示例开始(重命名后):

SELECT pd.patientId, pd.detailId, pd.`value`
  FROM patient_details AS pd
    LEFT JOIN patient_details AS pd2
      ON     pd.patientId = pd2.patientId
         AND pd.detailId < pd2.detailId
  WHERE pd2.patientId IS NULL
;

WHERE 子句添加条件将以两种不同的方式失败:

  1. JOIN 发生在 WHERE 之前;在评估后一个子句时,可能已经排除了所需的行。
  2. pd2.detailId 对于 groupwise-maximum 行将为 NULL,因此测试它总是会完全针对所需的行失败。

过滤必须在 JOIN 之前进行,这意味着它必须在替换简单的 table 引用的子查询中完成:

SELECT pd.patientId, pd.detailId, pd.`value`
  FROM (SELECT * FROM patient_details WHERE detailId IN (75, 151, 203)) AS pd
    LEFT JOIN (SELECT * FROM patient_details WHERE detailId IN (75, 151, 203)) AS pd2
      ON     pd.patientId = pd2.patientId
         AND pd.detailId < pd2.detailId
  WHERE pd2.patientId IS NULL
;

注意子查询是重复的。对于 MySQL 8.0 及更高版本,可以使用 CTE 编写查询,即 DRYer(因此更健壮):

WITH pd AS (SELECT * FROM patient_details WHERE detailId IN (75, 151, 203))
SELECT pd.patientId, pd.detailId, pd.`value`
  FROM pd
    LEFT JOIN pd AS pd2
      ON     pd.patientId = pd2.patientId
         AND pd.detailId < pd2.detailId
  WHERE pd2.patientId IS NULL
;

估计性能中的数量与相关子查询相似:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE patient_details NULL ALL NULL NULL NULL NULL 17 30 Using where
1 SIMPLE patient_details NULL ref PRIMARY,details_of_patient PRIMARY 4 patient_details.patientId 4 100 Using where; Using index

但是,这可以更好地尽早过滤行,导致 pd 的 17 行中只有 30% 或大约 5 行加入到 pd2

优化

不相关子查询的优化可以应用于此查询:

WITH pd AS (
    SELECT * 
      FROM patient_details 
      WHERE detailId IN (75, 151, 203)
        AND detailId BETWEEN 75 AND 203
  )
SELECT pd.patientId, pd.detailId, pd.`value`
  FROM pd
    LEFT JOIN pd AS pd2
      ON     pd.patientId = pd2.patientId
         AND pd.detailId < pd2.detailId
  WHERE pd2.patientId IS NULL
;

这在加入之前过滤掉行方面做得更好:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE patient_details NULL ALL NULL NULL NULL NULL 17 5.88 Using where
1 SIMPLE patient_details NULL ref PRIMARY,details_of_patient PRIMARY 4 so.patient_details.patientId 4 100 Using where; Using index

请注意,pd 的 17 行中估计有 5.88% 将连接到 pd2,这是所有选项中表现最好的之一。

具有 Window 功能的 CTE

MySQL 文档中的最后一个示例使用 window 函数 Rank 为每一行分配一个位置并根据该位置进行过滤。与子查询方法一样,这个查询很容易通过简单地包含条件来调整,但只适用于从 MySQL 8.0 开始(因为这是支持 window 函数的第一个版本)。在通常的重命名和限制添加之后:

WITH pd AS (
   SELECT patientId, detailId, `value`,
          RANK() OVER (PARTITION BY patientId
                           ORDER BY detailId DESC
                      ) AS `Rank`
     FROM patient_details
     WHERE detailId IN (75, 151, 203)
)
SELECT patientId, detailId, `value`
  FROM pd
  WHERE `Rank` = 1
;

请注意,这基本上是 SOS 答复中的查询,仅使用 Rank 而不是 Row_Number。性能还是比较不错的:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY ref <auto_key0> <auto_key0> 8 const 1 100.00
2 DERIVED patient_details ALL 17 30 Using where; Using filesort

优化不会改善问题,因为主要 select 无法进一步优化。添加通常的优化会导致从派生的 selection 中过滤出更多行,但是(正如优化的相关子查询所指出的那样)这不会影响性能,因为派生的 selection 不是然后加入其他任何东西。

限制

考虑 Paul T 的解决方案:

SELECT patientId, value
  FROM patient_details
  WHERE detailID IN (75, 151, 203)
    AND patientId = 123456
  ORDER BY detailID DESC
  LIMIT 1;

这对单个 ID 有很好的性能,只扫描 3 行:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE patient_details NULL range PRIMARY,details_of_patient PRIMARY 8 NULL 3 100 Using where

detailId 优化的范围限制应用于此查询不会提高性能。

只注意这个查询 returns单个患者 ID 的结果。要获得多个患者的结果,查询需要对每个患者单独进行 运行,导致性能与未优化的相关子查询相当(尽管对数据库有更多 round-trips)。