SQL确定命中值增量的最小连续天数

时间:2019-02-21 04:36:31

标签: sql postgresql

下表每个idvalue包含一条记录,它有数百条记录:

 id  | value  
-----+------------
   1 | 118.89
   2 | 113.90
   3 | 110.62
   4 | 105.37
   5 | 119.16
   6 | 118.33
   7 | 116.93
   8 | 117.74
   9 | 118.01
  10 | 125.00
  11 | 130.62
  12 | 137.50
  13 | 136.65
  14 | 133.80
  15 | 132.53
  16 | 133.03
  17 | 131.91
  18 | 134.06
  19 | 131.03
  20 | 132.38

我正在寻找对此表具有良好性能的SQL查询,当id更改为value(浮点数)时,该行为我提供了连续n的最小数量的行数字),移到任意一侧(+或-)。

例如,如果n=13.5,则应显示具有id 4,5的行,如果n=19.2,则应显示具有id 9-12的行。

2 个答案:

答案 0 :(得分:1)

感谢您分享这个非常有趣的挑战。 下次,如果您可以提供以下信息,将非常有帮助: 完整的DDL,示例数据的实际INSERT语句,所需结果集的详细信息以及更详细的解释。 一些人发布答案,并在误解了问题后将其删除。

我写下了DDL并插入:

CREATE TABLE FOO 
(ID INT PRIMARY KEY, Value DECIMAL(5,2));

INSERT INTO FOO (ID, Value)
VALUES  (  1 , 118.89 ),
         (  2 , 113.90 ),
         (  3 , 110.62 ),
         (  4 , 105.37 ),
         (  5 , 119.16 ),
         (  6 , 118.33 ),
         (  7 , 116.93 ),
         (  8 , 117.74 ),
         (  9 , 118.01 ),
         ( 10 , 125.00 ),
         ( 11 , 130.62 ),
         ( 12 , 137.50 ),
         ( 13 , 136.65 ),
         ( 14 , 133.80 ),
         ( 15 , 132.53 ),
         ( 16 , 133.03 ),
         ( 17 , 131.91 ),
         ( 18 , 134.06 ),
         ( 19 , 131.03 ),
         ( 20 , 132.38 );

SELECT  * 
FROM    FOO;

我希望我能正确理解您的问题,所以这是我的解决方法。

在使用实际的SQL解决方案之前,我试图了解数学上的复杂性。 假设表中有10行。 不同顺序组的数目是自然数的发散序列或三角数。 它从1选项开始,表示1-10之间的10个连续行。 那么对于9个连续行中的任意一组,我们有2个选项:1-9和2-10。 然后,对于任何8行的组,依次为3。 任何长度的连续基团总数均可轻松计算。 如果它是一个完整的梯形数字,则讨论区将为n(n + 1)/ 2。 在这里,由于最小的组由2行组成,而不是1行,因此它的大小为(n-1)(n-1 + 1)/ 2 = n(n-1)/ 2。

我将为此使用SQL Server语法,因为我不喜欢使用PL / pgSQL,也没有太多经验。 欢迎对PL / pgSQL具有更多经验的人进行转换,应该不要太难。 我从来不明白为什么这么多RDBMS不允许在同一脚本范围内将命令性构造与SQL结合在一起。

我的第一个想法是尝试使用一种简单的,基于集合的方法来使用递归查询来计算所有可能的组, OVER子句的组大小不同。 对于500行,我们需要计算500 * 499/2组的总增量=〜125K。 如果我们可以做类似的事情,那将是很好的:

DECLARE @MaxGroupSize INT = (SELECT COUNT(*) FROM Foo);
DECLARE @Threshold DECIMAL(5,2) = 13.5;
WITH GroupDeltas
AS
(
SELECT  1 AS GroupSize, 
        ID,
        CAST((  LEAD(Value) 
                OVER(ORDER BY ID ASC) - Value) 
        AS DECIMAL(38,2)) AS GroupDelta
FROM    Foo
UNION ALL
SELECT  (GroupSize + 1),
        ID,
        SUM(GroupDelta) 
            OVER (  ORDER BY ID ASC 
                    ROWS BETWEEN CURRENT ROW AND 0 /*NO GO WITH (GroupSize - 2)*/  FOLLOWING)
FROM    GroupDeltas
WHERE   (GroupSize + 1) <= @MaxGroupSize
)
SELECT  * 
FROM    GroupDeltas
WHERE   ABS(GroupDelta) >= @Threshold
        AND
        GroupSize = (
                        SELECT  MIN(GroupSize) 
                        FROM    GroupDeltas 
                        WHERE   GroupSize > 1 -- Eliminate Anchor
                                AND
                                ABS(GroupDelta) >= @Threshold   
                    );

但是不幸的是,帧偏移必须使用一个常量表达式。 不允许变量或列表达式。 请注意,上面的查询适用于第一个示例,组大小为2, 但这仅仅是因为我使用了原义0偏移量,而不是不允许的(GroupSize-2)...

如果我们可以向递归成员添加停止条件,那就太好了

 AND NOT EXISTS (
                    SELECT  NULL
                    FROM    GroupDeltas
                    WHERE   ABS(GroupDelta) >= 13.5
                )

但是我们只能引用一次递归成员中的CTE ...
无论如何,这种方法从一开始就行不通,所以我没有对其进行任何进一步的测试。 我只是在这里添加了它,作为我进行的一项有趣的智力锻炼。

这为我们提供了一种迭代方法。 由于您还要求提供“效果良好”的查询, 我认为我们可以不用计算所有可能的组就能逃脱。

我的想法是创建一个以最小的组大小开始的循环, 当我们打比赛时停下来。 我不想使用RBAR游标,所以我选择了更有效的窗口功能, 使用动态执行来规避偏移常数限制。 以下是我的尝试。 请注意,如果有超过1个满足阈值的组,则将同时显示两个。

DROP TABLE IF EXISTS #GroupDeltas;
GO

DECLARE @Threshold DECIMAL(5,2) = 19.2,
        @MaxGroupSize INT = (SELECT COUNT(*) FROM FOO),
        @GroupSize INT = 2, -- Initial Group Size
        @SQL VARCHAR(1000);

CREATE TABLE #GroupDeltas 
    (
        StartID INT, 
        GroupSize INT,
        GroupDelta DECIMAL(9,2),
        PRIMARY KEY (StartID, GroupSize)
    );

WHILE @GroupSize <= @MaxGroupSize
BEGIN
    SET @SQL = '
                ;WITH DeltasFromNext
                AS
                    (
                        SELECT  ID,
                                LEAD(Value) OVER(ORDER BY ID ASC) - Value AS Delta
                        FROM    FOO
                    )
                    SELECT  ID, 
                            ' + CAST(@GroupSize AS VARCHAR(5)) +',
                            SUM(Delta) 
                            OVER (  ORDER BY ID 
                                    ROWS BETWEEN 
                                    CURRENT ROW AND 
                                    ' + CAST(@GroupSize - 2 AS VARCHAR(5)) 
                                    + ' FOLLOWING)
                    FROM DeltasFromNext;
    '
    INSERT INTO #GroupDeltas
    EXECUTE (@SQL);
    IF EXISTS   (
                    SELECT  NULL
                    FROM    #GroupDeltas
                    WHERE   ABS(GroupDelta) >= @Threshold
                )
    BREAK;
    SET @GroupSize += 1
END
SELECT  * 
FROM    #GroupDeltas
WHERE   ABS(GroupDelta) >= @Threshold
ORDER BY GroupSize, StartID;

HTH

PS: 反馈和改进建议非常欢迎。我发现这是一个非常有趣的练习,可能有更好的方法来实现它。 如果有时间,我可能会再次访问。

答案 1 :(得分:-1)

假设id之间没有缝隙,则可以使用以下方法来实现:

select id, (next_id - id + 1) as cnt
from (select t.*,
             (select min(t2.id)
              from t t2
              where t2.id > t.id and
                    t2.value > t.value + 13.5
             ) as next_id
      from t
     ) t
order by cnt asc
fetch first 1 row only;

对于我来说,如何使用窗口功能并不明显。