Oracle SQL:来自 table A 的 Select 行回退到加入 table A 和 B。(联合,分组,...)

Oracle SQL: Select rows from table A with fallback to joined table A and B. (union, group by,...)

这个要求可能看起来有点奇怪,但请耐心等待:假设我有一个这样的 employees 列表:

pid    name
-------------------------
  1    Smith-Gordon
  2    Hansen
  3    Simpson

以及 table 以前的名字(如果史密斯-戈登夫人和汉森先生在结婚前分别有一个或多个不同的名字),employeehist:

pid    oldname
-------------------------
  1    Smith
  2    Taylor
  2    Baker

我现在想要的是能够搜索姓名并从两个 table 中获得结果,如下所示:

a) 搜索 "Simpson%" -> 得到类似“3, Simpson”的结果

b) 搜索 "Hansen%" -> 得到类似“2, Hansen”的结果

c) 搜索 "Taylor%" -> 得到类似“2, Hansen, matched on previous Taylor”的结果

d) 搜索 "Smith%" -> 得到类似“1, Smith-Gordon”的结果

换句话说,我想要当前记录,加上旧名称如果那是发生相关匹配的地方。

到目前为止我尝试了什么:

1) 天真地将历史加入当前员工:搜索 b)、c) 和 d) 将始终包含 oldname 列中的内容,因此我无法判断匹配发生的位置。我也得到了 Mr Hansen 的重复点击。

2) 我尝试在 employeesUNION 第一个 select (包含一个虚拟 NULL AS oldname),第二个 select 加入 employeehistemployees 这将 return 我一个很好的搜索 b) 没有 oldname 和一个带有 oldname 的 c), 但现在我可以预见地得到重复d).

有什么想法吗?

您可以使用以下带有参数的查询:

SELECT e.pid,
       CASE
           WHEN e.name LIKE :search_key THEN e.name
           WHEN eh.oldname LIKE :search_key THEN e.name || ' matched on previous ' || eh.oldname
       END
  FROM employees e
  LEFT JOIN employeehist eh on (e.pid = eh.pid)
 WHERE e.name LIKE :seach_key OR eh.oldname LIKE :search_key

我想到了这个解决方案:

SELECT * FROM ( /* (3) outer filter query */
  SELECT e.pid, e.name,  /* (1) query combining current and matching old names */
  CASE
    WHEN e.name LIKE :search_key THEN 'Y'
    ELSE 'N'
  END AS primary_match,
  (
    SELECT oldname  /* (2) subquery that gives me one or no matching old name */
    FROM employeehist eh
    WHERE eh.pid = e.pid
    AND eh.oldname LIKE :search_key
    AND ROWNUM=1
  )
  FROM employees e
) combined
WHERE combined.primary_match = 'Y' OR combined.oldname IS NOT NULL;

有一个主要的 select (1) 获取所有当前 ID 和名称,并添加一个 CASE 列是否名称匹配。此外,它运行一个子查询 (2),为我提供一个匹配的旧名称(如果有多个,或者 none 如果 none)。有了它,我可以使用外部 select (2) 来过滤掉没有匹配项的行。

这会 return 例如搜索关键字 "Smith%"

pid  | name         | primary_match  | oldname
  1  | Smith-Gordon | Y              | Smith

或 "Taylor%"

pid  | name         | primary_match  | oldname
  2  | Hansen       | N              | Taylor

我不确定它有多优雅,但它如我所愿:

  • 我每次匹配当前 pid 都会得到一个结果,无论 pid 有多少旧名称,匹配与否。无重复。

  • 我可以区分与当前名称匹配的结果和("only" 或 "also")与旧名称匹配的结果。

  • 我不需要定义我的匹配条件两次,因为它已被滚动到那个 CASE 列中,我可以对其进行过滤。

显然还有改进的余地:子查询 (2) 可以 return 所有匹配的旧名称的集合(或者最新的或最旧的,我有一个专栏)。

但这对我有用。

我找到了比我以前的解决方案更好的解决方案。我的问题是我不能 GROUP BY pid 和 "squash" 不同的 oldname 行。我很确定我记得这在 MySQL 中是可能的,但 Oracle 总是给我“979:不是 GROUP BY 表达式”。严格但公平。

解决方案显然是为 Oracle 提供处理这些行的策略:

SELECT pid, name,
  MIN(oldname) KEEP (DENSE_RANK FIRST ORDER BY oldname NULLS FIRST) as oldname
/*(3) outer select combines current and old hits, and "squashes" duplicates, preferring current hits where available*/
FROM (
  SELECT e.pid, e.name, null AS oldname /*(1) hits in current names*/
  FROM employees e
  WHERE e.name LIKE :search_key
  UNION ALL
  SELECT e.pid, e.name, eh.oldname /* (2) hits in old names*/
  FROM employeehist eh
  JOIN employees e ON e.pid = eh.pid
  WHERE eh.oldname LIKE :search_key
) combined
GROUP BY pid, name;

想法很简单:运行 一个查询 (1) 给出当前名称中的所有匹配项(加上一个带有 NULL 的虚拟 "oldname" 列),然后一个查询 (2) 给出所有旧名称中的匹配项(包括要显示的当前名称)。然后简单地组合它们,并通过 pid(和 name,因为 Oracle,但根据定义是相同的)删除重复项,优先考虑 oldnameNULL 的行。

这会 return 例如搜索关键字 "Smith%"

pid  | name         | oldname
  1  | Smith-Gordon | NULL

这正是我想要的。如果有当前和旧匹配的 pid,我不关心旧的。或者 "Taylor%":

pid  | name         | oldname
  2  | Hansen       | Taylor

这个查询似乎也比我的其他解决方案快大约 10 倍 - 我猜是因为它避免了依赖于当前 pid 的子查询。

所以唯一奇怪的是我需要使用 MIN(oldname) 而不是某种形式的身份。我知道 Oracle 在这里需要一个聚合函数,但是 KEEP ... FIRST 练习的重点是无论如何只有一行,不是吗?

但它有效,而且速度很快,所以我不会抱怨。