MySQL过滤掉自我引用

时间:2014-11-18 15:37:30

标签: mysql sql performance

我们有一个表格,其中包含经常查询的事件(如在开始和结束时间的日历事件中):

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列指向该“origin”事件。

检索一组事件时,我们通常按日期(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特定的东西,我会很感激任何指针。

5 个答案:

答案 0 :(得分: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 JOIN选择e中的所有事件,并将其与从中派生的事件d配对。它确保{strong>所有来自e的条目都有机会被选中,无论它们是否有派生事件。 WHERE子句仅保留e中没有派生事件的事件。它保留派生事件以及没有派生事件的原始事件,但删除那些具有派生事件的原始事件。

根据需要在表WHERE的字段中添加其他e条件,使用LIMIT条款,搅拌均匀,冷却。

答案 1 :(得分:3)

我建议按照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中ORDER BY之前无法运行GROUP 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>

编辑:正如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

答案 2 :(得分:0)

正在寻找类似的东西::

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

答案 3 :(得分:0)

要省略那些在结果集中派生事件的事件,您可以测试每个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 :(得分:0)

假设'衍生'行中的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'行,子查询将找到匹配的'衍生'行,当找到该行时,'origin'行将从结果集中排除。< / p>

回到这些假设......如果我们对'origin'和'衍生'行中的parent_id匹配没有保证......和/或我们没有保证start值,然后我们需要在相关子查询中重复相应的谓词(在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返回具有匹配'派生'行的行,以及来自{{1}的行没有匹配的。 “技巧”是列上的e条件,当找到匹配的派生行时,该条件保证为非NULL,该谓词排除发现匹配的行。

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