【问题标题】:MySQL filter out self-referencesMySQL 过滤掉自引用
【发布时间】:2015-01-15 20:43:41
【问题描述】:

我们有一个包含定期查询的事件表(如日历事件的开始和结束时间):

TABLE event (
  `id` varchar(32) NOT NULL,
  `start` datetime,
  `end` datetime,
  `derivedfrom_id` varchar(32),
  `parent_id` varchar(32) NOT NULL
)
  • parent_id 指向提供一些附加信息的日历表。
  • 一些事件是从另一个事件创建的,因此通过derivedfrom_id 列有一个指向该“源”事件的引用。

在检索一组事件时,我们通常通过日期(start/end)和日历(parent_id)进行查询,并通过limit限制结果数量进行分页。

我们现在面临的问题:有时我们需要将用户的相关事件合并到一个单一的表示中。所以我们进行正常的查询

SELECT id, start, parent_id
FROM event
WHERE parent_id in (<list of calendars>)
  AND start >= 'some date'
LIMIT x

...然后过滤掉原始事件,因为衍生物具有不同的信息并且无论如何都引用它们的起源。

您可能已经看到(比我们更早),我们在过滤之前进行了限制,因此收到了一组比我们最初预期的基数更小的事件,即结果数低于“x”之后过滤。

我唯一能想到的就是复制查询并进行子选择:

SELECT id, start, parent_id
FROM event
WHERE parent_id in (<list_of_calendars>)
  AND start >= 'some date'
  AND (/* the part below duplicates the previous conditions */
        derivedfrom_id is not null
        or id not in (
          SELECT derivedfrom_id
          FROM event
          WHERE parent_id in (<list_of_calendars>)
            AND start >= 'some date'
            AND derivedfrom_id is not null
        )
      )
LIMIT x

但我几乎不相信这是做到这一点的唯一方法。特别是,因为我们的查询要复杂得多。

有没有更好的办法?


示例数据

(根据评论中的要求)

鉴于这三个事件:

│ *ID* │ *DERIVEDFROM_ID* │ *PARENT_ID* │ *START*
├──────┼──────────────────┼─────────────┼─────────────────
│ 100  │ -                │ A           │ 2014-11-18 15:00
│ 101  │ 100              │ B           │ 2014-11-18 15:00
│ 150  │ -                │ A           │ 2014-11-20 08:00

...限制为 2,我想获取事件 101 和 150。

相反,使用当前方法:

  • 限制为 2 的查询导致事件 100 和 101
  • 过滤后,丢弃事件100,只剩下101个事件

关于预期答案的说明

上面的 SQL 实际上是从使用 JPA 的 Java 应用程序生成的。我目前的解决方案是生成一个 where 子句并复制它。如果有一些通用的 JPA 特定的东西,我将不胜感激。

【问题讨论】:

  • 一些样本数据和期望的结果将有助于澄清关系
  • 事件可以从派生事件派生吗?
  • 关于您的示例:您正在搜索日历 A 和 B,并且您正在过滤掉第 100 行,因为第 101 行已经存在?如果您只想搜索 A 怎么办?你想返回 100 和 150?
  • 您是否尝试过您的方法?我认为您的查询中的口头描述和实现的逻辑之间存在差异,因为您的查询实际上会过滤掉 derivatives 并保留其原件。它可能会保留与其他标准不匹配的事件的衍生品(例如,不匹配日期范围)。也许您实际上想要id not in (SELECT derivedfrom_id ...) 而不是derivedfrom_id not in (SELECT id ...),尽管您需要在子查询中过滤掉NULL。
  • 也许我不明白你的问题,但你接受的答案对我来说似乎很复杂。如果在上面的列表中添加了另一个事件,id = 102 并且derivedfrom_id = 100,查询的输出应该是什么? (101, 150) 还是 (101, 102, 150)?或者也许是 (102, 150)?

标签: mysql sql performance


【解决方案1】:

正在寻找这样的东西::

Select a.id, a.start, a.parent_id from 
event a , event b
Where a.parent_id in (<list_of_calendars>)
And a.start >= 'some date'
And b.parent_id = a.parent_id
And b.start = a.start
And a.id != b.derivedfrom_id
Limit x

【讨论】:

  • 谢谢。这可行,但除了将子选择重构到主查询之外,这与我当前的工作查询相同,包括约束的重复,即parent_id in ...start &gt;= ...
【解决方案2】:

我建议按事件的 DERIVEDFROM_ID 对事件进行分组,或者 - 如果不是派生事件,则使用 MySQL 的 IFNULL 方法将其 ID 分组,请参阅 SELECT one column if the other is null

SELECT id, start, parent_id, text, IFNULL(derivedfrom_id, id) as grouper
FROM event
WHERE parent_id in (<list_of_calendars>)
    AND start >= '<some date>'
GROUP BY grouper
LIMIT <x>

然而,这将随机返回原始事件或派生事件。如果您只想获取派生事件,则必须在分组之前按 ID 对结果进行排序(假设 ID 是升序的,因此派生事件的 ID 高于其祖先)。因为在 MySQL 中无法在 GROUP BY 之前运行 ORDER BY,所以您必须使用内部连接 ​​(MySQL order by before group by):

SELECT e1.* FROM event e1
INNER JOIN
(
    SELECT max(id) maxId, IFNULL(derivedfrom_id, id) as grouper
    FROM event
    WHERE parent_id in (<list_of_calendars>)
        AND start >= '<some date>'
    GROUP BY grouper
) e2
on e1.id = e2.maxId
LIMIT <x>

edit:正如 Aaron 所指出的,升序 ID 的假设与给定的数据结构相冲突。假设有一个时间戳created,您可以使用这样的查询:

SELECT e1.* FROM event e1
INNER JOIN
(
    SELECT max(created) c, IFNULL(derivedfrom_id, id) grouper
    FROM event
    WHERE parent_id IN (<list_of_calendars>)
        AND start >= '<some date>'
    GROUP BY grouper
) e2
ON (e1.id = e2.grouper AND e1.created = c) OR (e1.derivedfrom_id = e2.grouper AND e1.created = c)
LIMIT <x>

SQL Fiddle

【讨论】:

  • 此解决方案依赖于derivedfrom_id 大于id。鉴于它们是 Varchar(32) 的,我不一定会认为是这种情况。
  • 是的,你是对的。我的假设是基于示例数据。如果这个假设不成立,我提出的解决方案将不得不依赖一些created 时间戳。我会相应地更新我的答案。
【解决方案3】:

要省略那些在结果集中有派生事件的事件,你可以测试每个id是否省略,或者加入要排除的id的派生表

加入:

SELECT id, start, parent_id 
  FROM event
  LEFT JOIN (
    SELECT DISTINCT derived_id AS id FROM event
     WHERE start >= 'some date' AND parent_id IN (<calendars>)
  ) omit
    ON omit.id = event.id
 WHERE parent_id IN (<calendars>)
   AND start >= 'some date'
   AND omit.id IS NULL
 LIMIT x

嵌套选择:如果对 derived_id 进行索引,则相当有效

SELECT e.id, e.start, e.parent_id
  FROM event e
  WHERE parent_id IN (<calendars>)
    AND start >= 'some date'
    AND (SELECT e2.id FROM event e2      /* and does not have derived events */
          WHERE e2.derived_id = e.id
            AND e2.start >= 'some date'
          LIMIT 1) IS NULL
  LIMIT x

在 mysql 中你不能测试是否定的,你必须建立排除列表并明确省略

由于 parent_id(日历)可能不同,所有选择都必须对其进行测试。如果我们可以假设在其原始事件之前不会发生派生事件,则不必重复检查开始。

请注意,您指的是过滤掉原始事件(id 100,因为它具有派生事件 101),但我认为您的示例嵌套选择正在过滤掉派生事件。

【讨论】:

    【解决方案4】:

    试试这个:

    SELECT e.*
    FROM `event` e            # 'e' from 'event'
      LEFT JOIN `event` d     # 'd' from 'derived'; `LEFT JOIN` gets ALL entries from `e`
        ON e.id = d.derivedfrom_id    # match an event `e` with all those `d` derived from it
    WHERE d.id IS NULL        # keep only events `e` without derived events `d`
    ;
    

    LEFT JOINe 中选择所有事件,并将它们与派生自它们的事件d 配对。它确保来自e所有条目都有机会被选中,无论它们是否有派生事件。 WHERE 子句仅保留来自e 且没有派生事件的事件。它保留了派生事件以及没有派生事件的原始事件,但去除了那些具有派生事件的原始事件。

    根据需要在表e 的字段上添加额外的WHERE 条件,使用LIMIT 子句,搅拌均匀,冷饮。

    【讨论】:

    • 我之前没有考虑过这个优雅的解决方案。我刚刚开始了另一个赏金并将其分配给这个答案。谢谢!
    • 这与赏金或积分无关。如果对你有帮助,不客气!
    • 肯定不是积分,但最好的答案应该得到赏金。
    【解决方案5】:

    假设'derivative'行中的parent_id值与'origin'行上的parent_id值匹配,并且保证导数行上的start值不早于start在父行上......(这些是假设,因为我不相信其中任何一个被指定)......然后......

    一种快速解决方法是向现有查询添加“NOT EXISTS”谓词。我们只需为原始查询中的表引用分配一个别名(例如e),然后添加到 WHERE 子句...

       AND NOT EXISTS (SELECT 1 FROM event d WHERE d.derivedfrom_id = e.id)
    

    稍微解释一下...对于'origin'行,子查询将找到匹配的'derivative'行,当找到该行时,'origin'行将从结果集中排除。

    回到那些假设...如果我们不能保证 parent_id 在“原点”和“导数”行中匹配...和/或我们不能保证 @ 987654329@ 值,那么我们需要在相关子查询中重复适当的谓词(在 parent_idstart 上)以检查是否会返回匹配的“派生”行,添加谓词会使查询似乎更复杂:

       AND NOT EXISTS ( SELECT 1
                          FROM event d
                         WHERE d.derivedfrom_id = e.id 
                           AND d.parent_id IN parent_id IN (<list of calendars>)
                           AND d.start > 'some date' 
                      )
    

    有时,我们可以通过重写查询以将NOT EXISTS 替换为等效的“反连接”模式来获得更好的性能。

    为了描述这一点,它是一种“外连接”,用于查找匹配的“派生”行,然后过滤掉至少有一个匹配的“派生”行的行。

    我个人认为NOT EXISTS形式更直观,反连接模式更模糊一些。反连接的好处是性能更好(在某些情况下)。

    作为反连接模式的一个例子,我会像这样重写查询:

    SELECT e.id
         , e.start
         , e.parent_id
      FROM event e
      LEFT
      JOIN event d
        ON d.derivedfrom_id = e.id
       AND d.parent_id IN (<list of calendars>)
       AND d.start >= 'some date'
     WHERE d.derivedfrom_id IS NULL
       AND e.parent_id IN (<list of calendars>)
       AND e.start >= 'some date'
     ORDER BY e.id
     LIMIT x
    

    稍微解压.. LEFT [OUTER] JOIN 操作会找到匹配的“派生”行,这将返回来自e 的具有匹配“派生”行的行,以及来自e 的没有匹配的行一场比赛。 “技巧”是在找到匹配的派生行时保证为非 NULL 的列上的 IS NULL 条件,该谓词排除找到匹配项的行。

    (我还添加了一个 ORDER BY 子句,只是为了使结果更具确定性。)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-01-27
      • 1970-01-01
      • 2011-03-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-16
      • 1970-01-01
      相关资源
      最近更新 更多