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) 我尝试在 employees
上 UNION
第一个 select (包含一个虚拟 NULL AS oldname
),第二个 select 加入 employeehist
与 employees
这将 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,但根据定义是相同的)删除重复项,优先考虑 oldname
是 NULL
的行。
这会 return 例如搜索关键字 "Smith%"
pid | name | oldname
1 | Smith-Gordon | NULL
这正是我想要的。如果有当前和旧匹配的 pid,我不关心旧的。或者 "Taylor%":
pid | name | oldname
2 | Hansen | Taylor
这个查询似乎也比我的其他解决方案快大约 10 倍 - 我猜是因为它避免了依赖于当前 pid 的子查询。
所以唯一奇怪的是我需要使用 MIN(oldname)
而不是某种形式的身份。我知道 Oracle 在这里需要一个聚合函数,但是 KEEP ... FIRST 练习的重点是无论如何只有一行,不是吗?
但它有效,而且速度很快,所以我不会抱怨。
这个要求可能看起来有点奇怪,但请耐心等待:假设我有一个这样的 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) 我尝试在 employees
上 UNION
第一个 select (包含一个虚拟 NULL AS oldname
),第二个 select 加入 employeehist
与 employees
这将 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,但根据定义是相同的)删除重复项,优先考虑 oldname
是 NULL
的行。
这会 return 例如搜索关键字 "Smith%"
pid | name | oldname
1 | Smith-Gordon | NULL
这正是我想要的。如果有当前和旧匹配的 pid,我不关心旧的。或者 "Taylor%":
pid | name | oldname
2 | Hansen | Taylor
这个查询似乎也比我的其他解决方案快大约 10 倍 - 我猜是因为它避免了依赖于当前 pid 的子查询。
所以唯一奇怪的是我需要使用 MIN(oldname)
而不是某种形式的身份。我知道 Oracle 在这里需要一个聚合函数,但是 KEEP ... FIRST 练习的重点是无论如何只有一行,不是吗?
但它有效,而且速度很快,所以我不会抱怨。