我有一个Oracle SQL查询,其中包含列输出中的计算。在这个简化的例子中,我们正在寻找日期在某个范围内的记录,其中某些字段与特定的东西相匹配;然后对于那些记录,取出ID(不是唯一的)并再次搜索表中具有相同ID的记录,但是某些字段与其他字段匹配且日期在主记录的日期之前。然后返回最早的日期。以下代码完全符合预期:
SELECT
TblA.ID, /* Not a primary key: there may be more than one record with the same ID */
(
SELECT
MIN(TblAAlias.SomeFieldDate)
FROM
TableA TblAAlias
WHERE
TblAAlias.ID = TblA.ID /* Here is the link reference to the main query */
TblAAlias.SomeField = 'Another Thing'
AND TblAAlias.SomeFieldDate <= TblA.SomeFieldDate /* Another link reference */
) AS EarliestDateOfAnotherThing
FROM
TableA TblA
WHERE
TblA.SomeField = 'Something'
AND TblA.SomeFieldDate BETWEEN TO_DATE('2015-01-01','YYYY-MM-DD') AND TO_DATE('2015-12-31','YYYY-MM-DD')
然而,除此之外,我想要包含另一个计算列,该列根据EarliestDateOfAnotherThing实际返回文本输出。我可以使用CASE WHEN语句执行此操作,如下所示:
CASE WHEN
(
SELECT
MIN(TblAAlias.SomeFieldDate)
FROM
TableA TblAAlias
WHERE
TblAAlias.ID = TblA.ID /* Here is the link reference to the main query */
TblAAlias.SomeField = 'Another Thing'
AND TblAAlias.SomeFieldDate <= TblA.SomeFieldDate /* Another link reference */
) BETWEEN TO_DATE('2000-01-01','YYYY-MM-DD') AND TO_DATE('2004-12-31','YYYY-MM-DD')
THEN 'First period'
WHEN
(
SELECT
MIN(TblAAlias.SomeFieldDate)
FROM
TableA TblAAlias
WHERE
TblAAlias.ID = TblA.ID /* Here is the link reference to the main query */
TblAAlias.SomeField = 'Another Thing'
AND TblAAlias.SomeFieldDate <= TblA.SomeFieldDate /* Another link reference */
) BETWEEN TO_DATE('2005-01-01','YYYY-MM-DD') AND TO_DATE('2009-12-31','YYYY-MM-DD')
THEN 'Second period'
ELSE 'Last period'
END
这一切都很好。然而问题是我重新运行完全相同的子查询 - 这对我来说非常低效。我想做的只是运行子查询一次,然后获取输出并将其置于各种情况下。就像我可以使用VBA语句“SELECT CASE”,如下所示:
''''' Note that this is pseudo-VBA not SQL:
Select case (Subquery which returns a date)
Case Between A and B
"Output 1"
Case Between C and D
"Output 2"
Case Between E and F
"Output 3"
End select
' ... etc
我的调查表明SQL语句“DECODE”可以完成这项任务:但事实证明,DECODE仅适用于离散值,而不适用于日期范围。我还发现了一些关于将子查询放在FROM部分中的内容 - 然后在SELECT中的多个位置重新使用输出。然而,由于子查询本身并不站立,但依赖于将值与主查询进行比较而失败...并且在主查询执行之前无法进行那些比较(因此进行循环引用,因为FROM部分本身就是主要查询的一部分。)
如果有人能告诉我一个简单的方法来实现我想要的东西,我将不胜感激 - 因为到目前为止唯一可行的方法是在我想要的每个地方手动重用子查询代码,但作为程序员它让我感到非常低效!
修改 谢谢你到目前为止的答案。但是我想我必须在这里粘贴真实的,未经简化的代码。我试图简化它以简化概念,并删除潜在的识别信息 - 但到目前为止的答案清楚地表明它比我的基本SQL知识允许的更复杂。我试图围绕人们给出的建议,但我无法将概念与我的实际代码相匹配。例如,我的实际代码包括我在主查询中选择的多个表。
我想我将不得不咬紧牙关并展示我的(仍然简化但更准确)实际代码,其中我一直试图让“FROM子句中的子查询”工作起作用。也许某些人能够使用它来更准确地指导我如何使用到目前为止在我的实际代码中引入的概念?感谢。
SELECT
APPLICANT.ID,
APPLICANT.FULL_NAME,
EarliestDate,
CASE
WHEN EarliestDate BETWEEN TO_DATE('2000-01-01','YYYY-MM-DD') AND TO_DATE('2004-12-31','YYYY-MM-DD') THEN 'First Period'
WHEN EarliestDate BETWEEN TO_DATE('2005-01-01','YYYY-MM-DD') AND TO_DATE('2009-12-31','YYYY-MM-DD') THEN 'Second Period'
WHEN EarliestDate >= TO_DATE('2010-01-01','YYYY-MM-DD') THEN 'Third Period'
END
FROM
/* Subquery in FROM - trying to get this to work */
(
SELECT
MIN(PERSON_EVENTS_Sub.REQUESTED_DTE) /* Earliest date of the secondary event */
FROM
EVENTS PERSON_EVENTS_Sub
WHERE
PERSON_EVENTS_Sub.PER_ID = APPLICANT.ID /* Link the person ID */
AND PERSON_EVENTS_Sub.DEL_IND IS NULL /* Not a deleted event */
AND PERSON_EVENTS_Sub.EVTYPE_SDV_VALUE IN (/* List of secondary events */)
AND PERSON_EVENTS_Sub.COU_SDV_VALUE = PERSON_EVENTS.COU_SDV_VALUE /* Another link from the subQ to the main query */
AND PERSON_EVENTS_Sub.REQUESTED_DTE <= PERSON_EVENTS.REQUESTED_DTE /* subQ event occurred before main query event */
AND ROWNUM = 1 /* To ensure only one record returned, in case multiple rows match the MIN date */
) /* And here - how would I alias the result of this subquery as "EarliestDate", for use above? */,
/* Then there are other tables from which to select */
EVENTS PERSON_EVENTS,
PEOPLE APPLICANT
WHERE
PERSON_EVENTS.PER_ID=APPLICANT.ID
AND PERSON_EVENTS.EVTYPE_SDV_VALUE IN (/* List of values - removed ID information */)
AND PERSON_EVENTS.REQUESTED_DTE BETWEEN '01-Jan-2014' AND '31-Jan-2014'
答案 0 :(得分:3)
在重构现有查询(而不是逻辑上或功能上不同的方法)时,只需 。
对我来说,最简单的方法就是将其作为嵌套查询来实现...
- 内部查询将是您的基本查询,没有CASE语句
- 它还会将您的相关子查询作为附加 字段
包括在内
- 然后外部查询可以将该字段嵌入CASE语句
SELECT
nested_query.ID,
nested_query.FULL_NAME,
nested_query.EarliestDate,
CASE
WHEN nested_query.EarliestDate BETWEEN TO_DATE('2000-01-01','YYYY-MM-DD') AND TO_DATE('2004-12-31','YYYY-MM-DD') THEN 'First Period'
WHEN nested_query.EarliestDate BETWEEN TO_DATE('2005-01-01','YYYY-MM-DD') AND TO_DATE('2009-12-31','YYYY-MM-DD') THEN 'Second Period'
WHEN nested_query.EarliestDate >= TO_DATE('2010-01-01','YYYY-MM-DD') THEN 'Third Period'
END AS CaseStatementResult
FROM
(
SELECT
APPLICANT.ID,
APPLICANT.FULL_NAME,
(
SELECT
MIN(PERSON_EVENTS_Sub.REQUESTED_DTE) /* Earliest date of the secondary event */
FROM
EVENTS PERSON_EVENTS_Sub
WHERE
PERSON_EVENTS_Sub.PER_ID = APPLICANT.ID /* Link the person ID */
AND PERSON_EVENTS_Sub.DEL_IND IS NULL /* Not a deleted event */
AND PERSON_EVENTS_Sub.EVTYPE_SDV_VALUE IN (/* List of secondary events */)
AND PERSON_EVENTS_Sub.COU_SDV_VALUE = PERSON_EVENTS.COU_SDV_VALUE /* Another link from the subQ to the main query */
AND PERSON_EVENTS_Sub.REQUESTED_DTE <= PERSON_EVENTS.REQUESTED_DTE /* subQ event occurred before main query event */
AND ROWNUM = 1 /* To ensure only one record returned, in case multiple rows match the MIN date */
)
AS EarliestDate
FROM
EVENTS PERSON_EVENTS,
PEOPLE APPLICANT
WHERE
PERSON_EVENTS.PER_ID=APPLICANT.ID
AND PERSON_EVENTS.EVTYPE_SDV_VALUE IN (/* List of values - removed ID information */)
AND PERSON_EVENTS.REQUESTED_DTE BETWEEN '01-Jan-2014' AND '31-Jan-2014'
) nested_query
答案 1 :(得分:2)
你没有在FROM
子句中提供子查询,这本来是有用的,因为这是可以做到的一种方式:
SELECT
TblA.ID,
ED.MinSomeFieldDate,
CASE...
FROM
TableA A
LEFT OUTER JOIN
(
SELECT
SQ_A.Id,
MIN(SQ_A.SomeFieldDate) AS MinSomeFieldDate
FROM
TableA SQ_A
WHERE
SQ_A.SomeField = 'Another Thing'
GROUP BY
SQ_A.Id
) AS ED ON
ED.Id = A.Id AND
ED.MinSomeFieldDate <= A.SomeFieldDate -- We can do this outside of the subquery since it's MIN and <=
WHERE
A.SomeField = 'Something' AND
A.SomeFieldDate BETWEEN TO_DATE('2015-01-01','YYYY-MM-DD') AND TO_DATE('2015-12-31','YYYY-MM-DD')
答案 2 :(得分:2)
您可以使用分析函数(并在单个表扫描中)执行此操作而无需相关子查询或子查询因子分解(WITH .. AS ( ... )
):
SELECT ID,
EarliestDateOfAnotherThing
FROM (
SELECT ID,
MIN( CASE WHEN SomeField = 'Another Thing' THEN SomeFieldDate END )
OVER( PARTITION BY ID
ORDER BY SomeFieldDate
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW )
AS EarliestDateOfAnotherThing
FROM TableA
)
WHERE SomeField = 'Something'
AND SomeFieldDate BETWEEN TO_DATE('2015-01-01','YYYY-MM-DD')
AND TO_DATE('2015-12-31','YYYY-MM-DD')
你可以将扩展案例作为:
SELECT ID,
CASE
WHEN DATE '2000-01-01' <= EarliestDateOfAnotherThing
AND EarliestDateOfAnotherThing < DATE '2005-01-01'
THEN 'First Period'
WHEN DATE '2005-01-01' <= EarliestDateOfAnotherThing
AND EarliestDateOfAnotherThing < DATE '2010-01-01'
THEN 'Second Period'
ELSE 'Last Period'
END AS period
FROM (
SELECT ID,
MIN( CASE WHEN SomeField = 'Another Thing' THEN SomeFieldDate END )
OVER( PARTITION BY ID
ORDER BY SomeFieldDate
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW )
AS EarliestDateOfAnotherThing
FROM TableA
)
WHERE SomeField = 'Something'
AND SomeFieldDate BETWEEN TO_DATE('2015-01-01','YYYY-MM-DD')
AND TO_DATE('2015-12-31','YYYY-MM-DD')
答案 3 :(得分:1)
Oracle可能足够智能来优化这两个子查询,但为什么要这么麻烦?我认为使用CTE可以更清楚地编写查询:
with q as (
<your query here>
)
select q.*,
(case . . .
end) as another_calculated_column
from q;
这是一般结构。您可能需要在q
中添加其他列以用于您的逻辑。
答案 4 :(得分:1)
除了其他答案之外,您可以将查询包装在外部查询中,并在外部查询中使用case语句,如:
select id,
case when earliestdateofanotherthing between a and b then 'Output 1a'
when earliestdateofanotherthing between c and d then 'Output 2a'
when earliestdateofanotherthing between e and f then 'Output 3a'
else 'default clause output - a' -- null if excluded
end some_col1,
case when earliestdateofanotherthing between a and b then 'Output 1b'
when earliestdateofanotherthing between c and d then 'Output 2b'
when earliestdateofanotherthing between e and f then 'Output 3b'
else 'default clause output - b' -- null if excluded
end some_col2
from (select
tbla.id, /* Not a primary key: there may be more than one record with the same ID */
(
select
min(tblaalias.somefielddate)
from
tablea tblaalias
where
tblaalias.id = tbla.id /* Here is the link reference to the main query */
and tblaalias.somefield = 'Another Thing'
and tblaalias.somefielddate <= tbla.somefielddate /* Another link reference */
) as earliestdateofanotherthing
from
tablea tbla
where
tbla.somefield = 'Something'
and tbla.somefielddate between to_date('2015-01-01','YYYY-MM-DD') and to_date('2015-12-31','YYYY-MM-DD'));
或者,您可以使用子查询因子分析(也称为公用表表达式(CTE)/ WITH子句)将主查询拉入子查询,然后从中进行选择:
with main_qry as (select
tbla.id, /* Not a primary key: there may be more than one record with the same ID */
(
select
min(tblaalias.somefielddate)
from
tablea tblaalias
where
tblaalias.id = tbla.id /* Here is the link reference to the main query */
and tblaalias.somefield = 'Another Thing'
and tblaalias.somefielddate <= tbla.somefielddate /* Another link reference */
) as earliestdateofanotherthing
from
tablea tbla
where
tbla.somefield = 'Something'
and tbla.somefielddate between to_date('2015-01-01','YYYY-MM-DD') and to_date('2015-12-31','YYYY-MM-DD'))
select id,
case when earliestdateofanotherthing between a and b then 'Output 1a'
when earliestdateofanotherthing between c and d then 'Output 2a'
when earliestdateofanotherthing between e and f then 'Output 3a'
else 'default clause output - a' -- null if excluded
end some_col1,
case when earliestdateofanotherthing between a and b then 'Output 1b'
when earliestdateofanotherthing between c and d then 'Output 2b'
when earliestdateofanotherthing between e and f then 'Output 3b'
else 'default clause output - b' -- null if excluded
end some_col2
from main_qry;
将子查询保留在select子句中的优点是,假设存在适当的索引,您可以从子查询缓存中受益。它可能或者可能不是@ MT0使用分析函数来查找earliestdateofanotherthing
列的解决方案的性能或更高性能;你需要针对你的数据和表格结构测试这两种解决方案,以找出最合适的解决方案。
(N.B。我怀疑@ MT0的解决方案将是最好的解决方案;我主要将这个答案作为一个例子来说明如何重用一个列而不必计算两次。)
关于你问题中的更新查询,这可能就是你所追求的:
with main_qry as (SELECT
APPLICANT.ID,
APPLICANT.FULL_NAME,
case when min(case when person_events.del_ind is null
and evtype_sdv_value in (/* List of secondary events */)
then person_events.REQUESTED_DTE
end) over (partition by person_events.per_id, person_events.cou_sdv_value) <= person_events.requested_dte then
min(case when person_events.del_ind is null
and evtype_sdv_value in (/* List of secondary events */)
then person_events.REQUESTED_DTE
end) over (partition by person_events.per_id, person_events.cou_sdv_value)
end earliest_date
FROM
EVENTS PERSON_EVENTS,
inner join PEOPLE APPLICANT on (PERSON_EVENTS.PER_ID=APPLICANT.ID)
WHERE
PERSON_EVENTS.EVTYPE_SDV_VALUE IN (/* List of values - removed ID information */)
AND PERSON_EVENTS.REQUESTED_DTE BETWEEN to_date('01-Jan-2014', 'dd-mm-yyyy') AND to_date('31-Jan-2014', 'dd-mm-yyyy'))
select id,
full_name,
earliest_date,
CASE
WHEN EarliestDate >= TO_DATE('2010-01-01','YYYY-MM-DD') THEN 'Third Period'
WHEN EarliestDate >= TO_DATE('2005-01-01','YYYY-MM-DD') THEN 'Second Period'
WHEN EarliestDate >= TO_DATE('2000-01-01','YYYY-MM-DD') THEN 'First Period'
END period_type
from main_qry;
显然,你必须测试它!
请注意:
to_date
添加了main_qry的where子句中字符串的显式转换为date。依赖隐式日期转换并不是一个好主意,尤其是在生产代码中! NLS_DATE_FORMAT参数可以很容易地更改,这将导致难以识别错误!答案 5 :(得分:1)
这是一个lateral
解决方案。我重新排序了您的表,并使用ANSI连接语法进行内连接。此外,我从未在Oracle上实际编写过lateral
或cross apply
查询,这主要是因为我可能会犯一个小的语法错误而且我浏览的文档并没有说清楚对我来说,如果有区别的话。
虽然我不认为将其转换为仅使用另一个内部联接的形式很难,但我认为这是您在提出问题时所寻找的概念。
SELECT
APPLICANT.ID,
APPLICANT.FULL_NAME,
EarliestDate,
CASE
WHEN EarliestDate BETWEEN
TO_DATE('2000-01-01','YYYY-MM-DD') AND TO_DATE('2004-12-31','YYYY-MM-DD')
THEN 'First Period'
WHEN EarliestDate BETWEEN
TO_DATE('2005-01-01','YYYY-MM-DD') AND TO_DATE('2009-12-31','YYYY-MM-DD')
THEN 'Second Period'
WHEN EarliestDate >= TO_DATE('2010-01-01','YYYY-MM-DD')
THEN 'Third Period'
END
FROM
EVENTS PERSON_EVENTS inner join PEOPLE APPLICANT
on APPLICANT.ID = PERSON_EVENTS.PER_ID
LATERAL /* or possibly just CROSS APPLY */
(
SELECT
MIN(PERSON_EVENTS_Sub.REQUESTED_DTE) EarliestDate
FROM
EVENTS PERSON_EVENTS_Sub
WHERE
PERSON_EVENTS_Sub.PER_ID = APPLICANT.ID
AND PERSON_EVENTS_Sub.DEL_IND IS NULL
AND PERSON_EVENTS_Sub.EVTYPE_SDV_VALUE IN (...)
AND PERSON_EVENTS_Sub.COU_SDV_VALUE = PERSON_EVENTS.COU_SDV_VALUE
AND PERSON_EVENTS_Sub.REQUESTED_DTE <= PERSON_EVENTS.REQUESTED_DTE
) /* I don't think you need an alias here? */
WHERE
PERSON_EVENTS.EVTYPE_SDV_VALUE IN (...)
AND PERSON_EVENTS.REQUESTED_DTE BETWEEN '01-Jan-2014' AND '31-Jan-2014'
答案 6 :(得分:0)
好的伙计们,非常感谢你的帮助。我能理解其中的一些 - 而其他部分让我头晕目眩。我是一名SQL初学者,所以我确信我会及时学习这些更复杂的概念。但是,我现在设法做的是使用在答案中向我建议的概念来解决我的问题,但仍然使用了我熟悉的简单易用的代码。
基本上,我没有将日期输出子查询作为CTE,而是将整个主要原始查询作为CTE(当然,它是独立的);然后通过在进一步的查询中使用该CTE,我能够按照我喜欢的方式操作该日期列。如下:
WITH MAIN_QUERY AS
(
SELECT
APPLICANT.ID,
APPLICANT.FULL_NAME,
(ltrim(decode(to_char(APPLICANT.DOB_DD)||'/'||to_char(APPLICANT.DOB_MM)||'/'||to_char(APPLICANT.DOB_YYYY),'//',null,lpad(to_char(APPLICANT.DOB_DD),2,'0')||'/'||lpad(to_char(APPLICANT.DOB_MM),2,'0')||'/'||to_char(APPLICANT.DOB_YYYY)),'/')) AS DOB,
PERSON_EVENTS.REQUESTED_DTE,
PERSON_EVENTS.COU_SDV_VALUE,
/* Find the date of EARLIEST secondary event */
(
SELECT
MIN(PERSON_EVENTS3.REQUESTED_DTE)
FROM
EVENTS PERSON_EVENTS3
WHERE
PERSON_EVENTS3.PER_ID=APPLICANT.ID /* Ensure same person ID as main query */
AND PERSON_EVENTS3.DEL_IND IS NULL
AND PERSON_EVENTS3.EVTYPE_SDV_VALUE IN (/* Secondary event values */)
AND ROWNUM = 1
) AS EarliestDate /* Alias this subquery to a column name, for use later on */
FROM
EVENTS PERSON_EVENTS,
PEOPLE APPLICANT
WHERE
PERSON_EVENTS.PER_ID(+)=APPLICANT.ID
AND PERSON_EVENTS.EVTYPE_SDV_VALUE IN (/* Primary event values */)
AND PERSON_EVENTS.REQUESTED_DTE BETWEEN TO_DATE('2010-01-01','YYYY-MM-DD') AND TO_DATE('2011-01-01','YYYY-MM-DD')
)
SELECT
ID,
FULL_NAME,
DOB,
REQUESTED_DTE,
COU_SDV_VALUE,
EarliestDate, /* Use the date by alias - this works */
/* Then the alias can be further used how I like, without having to re-run the subquery :) */
CASE
WHEN EarliestDate BETWEEN TO_DATE('2000-01-01','YYYY-MM-DD') AND TO_DATE('2004-12-31','YYYY-MM-DD') THEN 'First period'
WHEN EarliestDate BETWEEN TO_DATE('2005-01-01','YYYY-MM-DD') AND TO_DATE('2009-12-31','YYYY-MM-DD') THEN 'Second period'
WHEN EarliestDate >= TO_DATE('2010-01-01','YYYY-MM-DD') THEN 'Third period'
END
FROM MAIN_QUERY
所以 - 再次,谢谢大家。我在这里发布这个解决方案作为我的首选(鉴于我的SQL专业水平),但如果没有你的帮助,我将无法做到这一点。我希望这个主题对任何寻找能够重用子查询结果的人都有用,而不必重复它。