【问题标题】:Oracle SQL or PLSQL. Select rows by partitions which values have specific orderOracle SQL 或 PLSQL。按分区选择行,其值具有特定顺序
【发布时间】:2021-02-07 22:05:58
【问题描述】:

任务:选择至少连续参加2场比赛的运动员(2场比赛接连进行;1-2-3-4-5:2&4或1&3&5不行,1&2可以,1&2&3可以, 1&2 和 4&5 都可以)。 问题:找到最好的方法(更快,更少的资源)

工作台:

每个比赛 ID 都有一个 hold_date。

每个sportsman_id 每个competition_id 只有一个结果。

这适用于结果表中的 25 行:

SELECT DISTINCT sportsman_id, sportsman_name, rank, year_of_birth, personal_record, country
FROM
    (
    SELECT sportsman_id, hold_date,
        LAG (comp_order, 1) OVER (PARTITION BY sportsman_id ORDER BY sportsman_id) prev_comp_number
        , comp_order
    FROM result
    INNER JOIN
        (
        SELECT hold_date, ROW_NUMBER() OVER (ORDER BY hold_date) AS comp_order
        FROM
            (
            SELECT DISTINCT hold_date
            FROM result
            )
        ) USING (hold_date)
    ORDER BY sportsman_id, comp_order
    )
INNER JOIN sportsman USING (sportsman_id)
WHERE comp_order-prev_comp_number=1
;

使用 cmets 的代码截图:

样本数据:

上面代码的结果(=期望的结果)

假设有数百万行(数以千计的比赛和数以千计的运动员)。我的代码有多可靠?

如果sportsman_id 只出现一次,我认为通过排除行来减少行数(如果运动员只参加了一场比赛(获得结果),他显然不能成为那个人)。 像这样的东西:(还没有实现(不知道如何或最有可能何时/何地))

SELECT re.hold_date, r.sportsman_id
FROM result r
INNER JOIN result re ON (re.sportsman_id=r.sportsman_id)
GROUP BY r.sportsman_id, re.hold_date
HAVING COUNT(r.sportsman_id) > 1
;

那么,我猜我用 LAG 只会将现有列加倍,这还不错?

使用 PLSQL 有更简单的方法吗?或者有一个函数可以完成我的代码的某些部分?

【问题讨论】:

  • 样本数据和期望的结果会有很大帮助。
  • 添加为截图
  • 您的数据模型显示一场比赛可以跨越数天(因此日期在结果表中,否则会在比赛表中)。这是否也意味着两场比赛可以重叠?我能在 9 月 5 日和 6 日找到一场比赛,在 9 月 4 日和 7 日找到另一场比赛吗?如果是这样,该怎么办?
  • edit您的问题以文本形式包含示例数据、注释代码和所需结果(对于代码,最好是我们可以复制/粘贴的 DDL/DML 语句)。
  • @ThorstenKettner 如前所述:每个 Competition_id 都有一个 hold_date。没有重叠;所以实际上 hold_date 是独一无二的。

标签: sql oracle subquery window-functions gaps-and-islands


【解决方案1】:

如果您对包含完整比赛列表的结果执行分区外连接,那么当竞争对手没有参加比赛时,您将拥有NULL 行。然后你可以用MATCH_RECOGNIZE依次比较行,COUNT他们参加的顺序比赛的数量,排除只参加了一场比赛但没有参加前后比赛的运动员。

SELECT sportsman_id
FROM   (
  SELECT sportsman_id,
         c.competition_id,
         c.hold_date,
         NVL2( r.competition_id, 1, 0 ) AS attended
  FROM   ( SELECT DISTINCT
                  competition_id,
                  hold_date
           FROM   result
         ) c
         LEFT OUTER JOIN result r
         PARTITION BY ( r.sportsman_id )
         ON ( c.competition_id = r.competition_id )
)
MATCH_RECOGNIZE (
  PARTITION BY sportsman_id
  ORDER BY hold_date
  MEASURES COUNT(*) AS num_sequential
  ONE ROW PER MATCH
  PATTERN ( ATTENDED_COMP+ )
  DEFINE
    ATTENDED_COMP AS (
      ATTENDED_COMP.attended = 1
    )
)
GROUP BY sportsman_id
HAVING MIN( num_sequential ) > 1;

所以,对于样本数据:

CREATE TABLE result ( competition_id, sportsman_id, hold_date ) AS
SELECT 1, 1, DATE '2020-01-01' FROM DUAL UNION ALL
SELECT 2, 1, DATE '2020-02-01' FROM DUAL UNION ALL
SELECT 3, 1, DATE '2020-03-01' FROM DUAL UNION ALL
SELECT 4, 1, DATE '2020-04-01' FROM DUAL UNION ALL
SELECT 5, 1, DATE '2020-05-01' FROM DUAL UNION ALL
SELECT 1, 2, DATE '2020-01-01' FROM DUAL UNION ALL
SELECT 2, 2, DATE '2020-02-01' FROM DUAL UNION ALL
SELECT 4, 2, DATE '2020-04-01' FROM DUAL UNION ALL
SELECT 5, 2, DATE '2020-05-01' FROM DUAL UNION ALL
SELECT 2, 3, DATE '2020-02-01' FROM DUAL UNION ALL
SELECT 4, 3, DATE '2020-04-01' FROM DUAL UNION ALL
SELECT 1, 4, DATE '2020-01-01' FROM DUAL UNION ALL
SELECT 3, 4, DATE '2020-03-01' FROM DUAL UNION ALL
SELECT 5, 4, DATE '2020-05-01' FROM DUAL UNION ALL
SELECT 1, 5, DATE '2020-01-01' FROM DUAL UNION ALL
SELECT 2, 5, DATE '2020-02-01' FROM DUAL UNION ALL
SELECT 5, 5, DATE '2020-05-01' FROM DUAL;

输出是:

| SPORTSMAN_ID | | ------------: | | 1 | | 2 |

db小提琴here


如果您想要参加过任何一组连续比赛的运动员(无论他们的所有比赛是否都包含在连续组中),那么您可以将最后一行更改为:

HAVING MAX( num_sequential ) > 1;

输出将是:

| SPORTSMAN_ID | | ------------: | | 1 | | 2 | | 5 |

db小提琴here


或者,如果您想了解匹配范围的详细信息,您可以使用PATTERN ( ATTENDED_COMP{2,} ) 仅匹配竞争对手连续参加两次或更多比赛的连续组:

SELECT *
FROM   (
  SELECT sportsman_id,
         c.competition_id,
         c.hold_date,
         NVL2( r.competition_id, 1, 0 ) AS attended
  FROM   ( SELECT DISTINCT
                  competition_id,
                  hold_date
           FROM   result
         ) c
         LEFT OUTER JOIN result r
         PARTITION BY ( r.sportsman_id )
         ON ( c.competition_id = r.competition_id )
)
MATCH_RECOGNIZE (
  PARTITION BY sportsman_id
  ORDER BY hold_date
  MEASURES
    FIRST( competition_id ) AS first_competition_id,
    FIRST( hold_date ) AS first_hold_date,
    LAST( competition_id ) AS last_competition_id,
    LAST( hold_date ) AS last_hold_date
  ONE ROW PER MATCH
  PATTERN ( ATTENDED_COMP{2,} )
  DEFINE
    ATTENDED_COMP AS ( ATTENDED_COMP.attended = 1 )
)

输出:

SPORTSMAN_ID | FIRST_COMPETITION_ID | FIRST_HOLD_DATE | LAST_COMPETITION_ID | LAST_HOLD_DATE ------------: | -------------------: | :----------------- | ------------------: | :----------------- 1 | 1 | 2020-01-01 00:00:00 | 5 | 2020-05-01 00:00:00 2 | 1 | 2020-01-01 00:00:00 | 2 | 2020-02-01 00:00:00 2 | 4 | 2020-04-01 00:00:00 | 5 | 2020-05-01 00:00:00 5 | 1 | 2020-01-01 00:00:00 | 2 | 2020-02-01 00:00:00

db小提琴here

【讨论】:

  • sportsman_id 5 不应计入该数据,它存在于比赛 1 和 2 中。
  • @AndrewSayer OP对此并不清楚;运动员5 参加过1、2 和5,所以他们都符合和不符合标准。我错误地认为,如果他们有任何比赛没有参加之前或之后的比赛,那么他们就会被排除在结果之外。但是,如果应该包含它们,那么在最后一行中将MIN 更改为MAX 是一件简单的事情。
【解决方案2】:

您可以通过使用 Tabibitosan 方法仅读取一次表格来将连续比赛组合在一起https://www.red-gate.com/simple-talk/sql/t-sql-programming/the-sql-of-gaps-and-islands-in-sequences/#:%7E:text=The%20SQL%20of%20Gaps%20and%20Islands%20in%20Sequences,...%204%20Performance%20Comparison%20of%20Gaps%20Solutions.%20

在这里您必须使用 add_months,因为您的比赛相隔数月:

select sportsman_id, min(hold_date) , max(hold_date), comps_in_island
from (
 select  competition_id, sportsman_id, hold_date, island, count(*) over (partition by sportsman_id,island) comps_in_island
 from (
  select  competition_id, sportsman_id, hold_date , add_months(hold_date,-1*row_number() over(partition by sportsman_id order by hold_date)) island
  from    result
 )
)
where comps_in_island > 1
group by sportsman_id, island, comps_in_island;

数据库小提琴:https://dbfiddle.uk/?rdbms=oracle_18&fiddle=1b707262722bc555ad851aee029b347a

-编辑 我对一些数据感到困惑,看起来重要的不是日期而是比赛ID。如果您有一个无间隙的competition_id 序列,这会变得更简单(所以比赛65786162213 在4 之后是657 亿个事件)

select sportsman_id, min(competition_id) , max(competition_id), comps_in_island
from (
 select  competition_id, sportsman_id, hold_date, island, count(*) over (partition by sportsman_id,island) comps_in_island
 from 
  select  competition_id, sportsman_id, hold_date , competition_id -row_number() over(partition by sportsman_id order by competition_id)) island
  from    result
 )
)
where comps_in_island > 1
group by sportsman_id, island, comps_in_island;

或者,如果您需要先计算出比赛数量,您只需要一个额外的子查询,使用dense_rank 来对唯一的competition_ids 进行排名:

select sportsman_id, min(competition_id) , max(competition_id), comps_in_island
from (
 select  competition_id, sportsman_id, hold_date, island, count(*) over (partition by sportsman_id,island) comps_in_island
 from (
  select  competition_id, sportsman_id, hold_date , comp_number -row_number() over(partition by sportsman_id order by comp_number) island
  from (  
   select  competition_id, sportsman_id, hold_date , dense_rank() over (partition by null order by competition_id) comp_number
   from    result
  )
 )
)
where comps_in_island > 1
group by sportsman_id, island, comps_in_island;

这确实假设您关心的每个可能的competition_id 在结果中都有一行。

【讨论】:

  • 这是否依赖于比赛间隔正好 1 个月?您似乎复制了我的示例数据(没有引用)并以此为基础回答。虽然它适用于我的数据,但 OP 的数据没有此属性。
  • 啊,是的,我假设您使用的数据与提供的 OP 相同,也许不是。不过改变并不难,坚持...
  • 最后一部分看起来很流畅,它可以工作,而且您似乎也根据性能使用它。谢谢你。稍后会阅读文章。
  • @AndrewSayer Tho 我还不明白这个 /count(*) over (partition by sportsman_id,island) comps_in_island/ 是如何产生的。更新:好像我明白了。我的代码有哪些薄弱环节?
  • count(*) over (partition by sportsman_id,island) 为您提供在 sportsman_id 和 island 上匹配的行数。您可以在不同的子查询级别运行查询以查看发生了什么。您的代码不一定很弱,但它需要将您的大表与自身连接起来,您可以通过使用 dense_rank 分析函数来实现与我相同的优化来限制它。检查该 sportsman_id 的前一个比赛编号是否是全球前一个的延迟工作正常,但您可能会发现扩展以获取其他跑步信息很困难。
【解决方案3】:

如果你只想要一个至少连续参加过两次比赛的运动员名单,那么使用lag() juste 一次就足够了:

select distinct sportman_id
from (
    select sportman_id, competition_id
        lag(competition_id) over(partition by sportman_id, oder by competition_id) lag_competition_id
    from result r
) r
where competition_id = lag_competition_id + 1

exists可以带上对应的sportsman行:

select s.*
from sportman s
where exists (
    select 1
    from (
        select sportman_id, competition_id
            lag(competition_id) over(partition by sportman_id, oder by competition_id) lag_competition_id
        from result r
    ) r
    where r.competition_id = r.lag_competition_id + 1 and r.sportman_id = s.sportman_id
)

【讨论】:

  • @void_eater:您的查询遇到什么问题?
  • over(partition by sportman_id, oder by Competition_id) - ",", oRder, sportSman_id 。然后 /ORA-00904: "COMPETITION_ID": 无效标识符/
  • @void_eater:我的错。我们需要先在子查询中选择该列,以便在外部查询中使用它。固定。
【解决方案4】:

你说每次比赛总是只有一个日期。因此,该日期应位于比赛表中,而不是结果表中。您还说日期不重叠(同一日期没有两场比赛 - 这也可以通过限制来确保,是比赛表中的日期)。

在第一步中,按顺序获取比赛/日期。使用您的数据模型:

select distinct hold_date
from result
order by hold_date;

要快速获得此结果,请提供日期索引:

create index idx1 on result (hold_date);

您甚至可以使用ROW_NUMBER 对它们进行编号,或者使用LAGLEAD 来查看日期及其相邻日期。

现在,寻找连续参加两项赛事的运动员的最佳方法在很大程度上取决于运动员一般参加的频率。

  1. 如果他们很少参与,比如通常只有两次,我们可以加入并快速查看结果。
  2. 如果他们经常参与,例如,通常参与大约一半的事件,我们希望遍历事件并在找到连续事件后停止,而不是继续阅读。

这是对第二种方法的查询。我们使用递归查询(因为这是我们在 SQL 中应用迭代过程的方式)。我们从所有运动员和第一次约会开始。然后我们去第二次约会,为所有参加过的人停下来。剩下的我们看第三次约会,然后再为参加第二次和第三次的人停下来。以此类推。

应该有一个关于日期和运动员的索引,以便快速查找结果行。我什至会提供两个索引,因为我不知道哪一列更具选择性。所以,让 DBMS 来决定吧。

create index idx2 on result (hold_date, sportsman_id);
create index idx3 on result (sportsman_id, hold_date);

这里是查询:

with dates as 
(
  select
    hold_date,
    lead(hold_date) over (order by hold_date) as next_date,
    min(hold_date) over (order by hold_date) as min_date
  from (select distinct hold_date from result)
)
, cte (sportsman_id, sportsman_name, rank, year_of_birth, personal_record, country,
       hold_date, next_date, was_in, is_in) as
(
  select
    s.sportsman_id, s.sportsman_name, s.rank, s.year_of_birth,
    s.personal_record, s.country, d.hold_date, d.next_date, 'NO',
    case when r.hold_date is not null then 'YES' else 'NO' end
  from sportsman s
  cross join (select * from dates where hold_date = min_date) d
  left join result r on r.sportsman_id = s.sportsman_id
                     and r.hold_date = d.hold_date
  union all
  select
    s.sportsman_id, s.sportsman_name, s.rank, s.year_of_birth,
    s.personal_record, s.country, d.hold_date, d.next_date, s.is_in,
    case when r.hold_date is not null then 'YES' else 'NO' end
  from cte s
  join dates d on d.hold_date = s.next_date
  left join result r on r.sportsman_id = s.sportsman_id
                     and r.hold_date = d.hold_date
  where not (s.was_in = 'YES' and s.is_in = 'YES')
)
select sportsman_id, sportsman_name, rank, year_of_birth, personal_record, country
from cte
where was_in = 'YES' and is_in = 'YES';

【讨论】:

  • 感谢您在我学习过程中的汇报。但这是一个测试表/关系,我猜它的创建者并不太关心它。但老实说,Andrew Sayer 和“岛屿”概念的答案似乎更容易理解,而且很可能更有效(不确定)。
  • 是的,可能是这样。如前所述,仅当您期望运动员参加许多赛事时,我的才是合适的。如果几乎所有运动员都参加了几乎所有的项目,而您只想过滤掉极少数没有连续参加任何项目的人,那么最好只查找每个运动员的少数项目,直到找到第一个连续事件,而不是遍历所有数据。
猜你喜欢
  • 2021-05-19
  • 2020-11-05
  • 2013-01-13
  • 2022-11-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-01
  • 2021-12-11
相关资源
最近更新 更多