【问题标题】:Fetch range from days获取范围从天
【发布时间】:2012-06-14 02:56:56
【问题描述】:

我有这个表结构:

编辑更复杂的例子:添加隐藏范围

category|   day      |   a   |
--------|------------|-------|
1       | 2012-01-01 |   4   |
1       | 2012-01-02 |   4   |
1       | 2012-01-03 |   4   |
1       | 2012-01-04 |   4   |
1       | 2012-01-05 |   5   |
1       | 2012-01-06 |   5   |
1       | 2012-01-07 |   5   |
1       | 2012-01-08 |   4   |
1       | 2012-01-09 |   4   |
1       | 2012-01-10 |   4   |
1       | 2012-01-11 |   5   |
1       | 2012-01-12 |   5   |
1       | 2012-01-16 |   5   |
1       | 2012-01-17 |   5   |
1       | 2012-01-18 |   5   |
1       | 2012-01-19 |   5   |
...

使用“类别日”作为唯一键。我会根据列“a”和给定的限制范围为每个类别提取日期范围,如下所示:

1,2012-01-01|2012-01-04,4
1,2012-01-05|2012-01-07,5
1,2012-01-08|2012-01-10,4
1,2012-01-11|2012-01-12,5
1,2012-01-13|2012-01-15,0
1,2012-01-16|2012-01-19,5

或类似的。

我正在寻找最好的方法。最好只使用 mysql,但也可以使用一点 php。

NOTE1:不是所有的日子都被插入:两天之间非连续的日子不能是其他日子。在这种情况下,我将输出丢失的范围,列“a”= 0。

NOTE2:我用一个简单的查询和一些 php 行来做到这一点,但我不喜欢它,因为我的简单算法需要一个循环,每天在范围内乘以找到的每个类别。如果范围太大,类别太多,那就不好了。

最终编辑:好的!在阅读了所有 cmets 和答案后,我认为不存在有效、高效且同时具有可读性的解决方案。所以 Mosty Mostacho 的答案不是 100% 有效的解决方案,但它有 100% 有效的建议。谢谢大家。

【问题讨论】:

  • a 的价值是多少,假设选择范围 2012-01-012012-01-09
  • @safarov 1,2012-01-01|2012-01-04,4 1,2012-01-05|2012-01-07,5 1,2012-01-08|2012- 01-09,4
  • 好的,现在我明白你想要什么了,但是只用mysql很难做到,其中一半可以在php中轻松完成
  • 我认为您需要声明程序并使用循环来检查每个子组。我想不出任何其他选择
  • 我认为你的例子是错误的。你的意思是2012-01-12,5 是最后一个值,对吧?

标签: php mysql


【解决方案1】:

新编辑:

正如我在评论中告诉你的,我强烈建议你使用快速查询,然后在 PHP 中处理缺失的日期,因为这样会更快、更易读:

select
  concat(@category := category, ',', min(day)) col1,
  concat(max(day), ',', @a := a) col2
from t, (select @category := '', @a := '', @counter := 0) init
where @counter := @counter + (category != @category or a != @a)
group by @counter, category, a

但是,如果您仍想使用查询版本,请尝试以下操作:

select
  @counter := @counter + (category != @category or a != @a) counter,
  concat(@category := category, ',', min(day)) col1,
  concat(max(day), ',', @a := a) col2
from (
  select distinct s.day, s.category, coalesce(t1.a, 0) a
  from (
    select (select min(day) from t) + interval val - 1 day day, c.category
    from seq s, (select distinct category from t) c
    having day <= (select max(day) from t)
  ) s
  left join t t1 on s.day = t1.day and s.category = t1.category
  where s.day between (
    select min(day) from t t2
    where s.category = t2.category) and (
    select max(day) from t t2
    where s.category = t2.category)
  order by s.category, s.day
) t, (select @category := '', @a := '', @counter := 0) init
group by counter, category, a
order by category, min(day)

请注意,MySQL 不允许您动态创建数据,除非您将 UNIONS 硬编码为 example。这是一个昂贵的过程,因此我强烈建议您创建一个表,其中只有一个 integer 字段,其值从 1X,其中 X至少最大值将min(day)max(day) 与您的表格分开的日期数量。如果您不确定该日期,只需添加 100,000 数字,您就可以生成超过 200 年的范围周期。在前面的查询中,这个表是seq,它的列是val

这会导致:

+---------------+--------------+ | COL1 | COL2 | +---------------+--------------+ | 1,2012-01-01 | 2012-01-04,4 | | 1,2012-01-05 | 2012-01-07,5 | | 1,2012-01-08 | 2012-01-10,4 | | 1,2012-01-11 | 2012-01-12,5 | | 1,2012-01-13 | 2012-01-15,0 | | 1,2012-01-16 | 2012-01-19,5 | +---------------+--------------+

好吧,我在撒谎。结果实际上是返回一个counter 列。忽略它,因为删除它(使用派生表)会更差!

【讨论】:

  • 很好!谢谢你。但是也有注释 1:某些类别的某些中间日期可能没有插入。在这种情况下,“未插入范围”应该是列“a”等于 0。我已经更新了示例。 (你的回答是目前最好的,如果最后一次编辑不可能,我会接受它。)
  • @chumkiu 你说category-day 这对是独一无二的,我可能会错过几天。这是否意味着我可以为不同的类别缺少不同(和相同)的日子? EG:在您的示例中,我可以在 2 类别中缺少 2012-01-01 吗?
  • @chumkiu 另请注意,在 DBMS 中生成数据通常不是一个好主意。这将非常缓慢并且通常是不必要的,因为它实际上是一个数据呈现问题。虽然可以这样做,但我的建议是在 PHP 中循环处理丢失的数据,跟踪之前的 acategoryday,这样如果当前获取的 day 不是之前的day 的后续,那么您从之前的day + 1 到当前的day 之间存在差距。 IMO,这是迄今为止最好/最快/最易读的解决方案。如果您坚持查询,请回答上一个问题:)
  • 是的。我可以为类别2 缺少一天,但可以为类别1 插入同一天。我已经怀疑你的反映了。我在这里要求确认。
【解决方案2】:

这是对您的单线暴行:)(注意:更改“datt”表名。)

select dd.category,
dd.day as start_day,
(select dp.day from 
    (
        select 1 as n,d1.category,d1.day,d1.a from datt d1 where not exists (
            select * from datt where day = d1.day - INTERVAL 1 DAY and a=d1.a
        )
        union
        select 2 as n,d1.category,d1.day,d1.a from datt d1 where not exists (
            select * from datt where day = d1.day + INTERVAL 1 DAY and a=d1.a
        )
    ) dp where dp.day >= dd.day - INTERVAL (n-2) DAY order by day asc limit 0,1) 
as end_day,
dd.a from (
    select 1 as n,d1.category,d1.day,d1.a from datt d1 where not exists (
        select * from datt where day = d1.day - INTERVAL 1 DAY and a=d1.a
    )
    union
    select 2 as n,d1.category,d1.day,d1.a from datt d1 where not exists (
        select * from datt where day = d1.day + INTERVAL 1 DAY and a=d1.a
    )
) dd
where n=1

它的输出是:

|| 1 || 2012-01-01 || 2012-01-01 || 4 ||
|| 1 || 2012-01-03 || 2012-01-04 || 4 ||
|| 1 || 2012-01-05 || 2012-01-07 || 5 ||
|| 1 || 2012-01-08 || 2012-01-10 || 4 ||
|| 1 || 2012-01-11 || 2012-01-12 || 5 ||

注意:这是 01-12 日表中不存在的 2012-01-02 的结果。

【讨论】:

  • 它有效,谢谢。您的回答也可以通过修复解决note1。查看更新的示例:)
  • 等等,我没看到最后一句话。 :S
  • 我能得到的最后一个。它获取现有的范围,但缺少的范围并不那么容易,因为您需要与全天表列表进行比较才能获取缺少的范围。这就是我认为的 PHP 方面。
【解决方案3】:

不需要 PHP 或临时表或任何东西。

免责声明:我这样做只是为了乐趣。这个特技可能太疯狂了,无法在生产环境中使用。因此,我将此作为“真正的”解决方案发布。我也不愿意解释它是如何工作的 :) 而且我没有重新考虑/重构它。可能有更优雅的方式,名称/别名可能会提供更多信息。所以请不要火焰或任何东西。

这是我的解决方案。看起来比实际复杂。我认为它可能比其他答案更容易理解,没有冒犯:)

设置测试数据:

drop table if exists test;
create table test(category int, day date, a int);
insert into test values
(1       , '2012-01-01' ,   4   ),
(1       , '2012-01-02' ,   4   ),
(1       , '2012-01-03' ,   4   ),
(1       , '2012-01-04' ,   4   ),
(1       , '2012-01-05' ,   5   ),
(1       , '2012-01-06' ,   5   ),
(1       , '2012-01-07' ,   5   ),
(1       , '2012-01-08' ,   4   ),
(1       , '2012-01-09' ,   4   ),
(1       , '2012-01-10' ,   4   ),
(1       , '2012-01-11' ,   5   ),
(1       , '2012-01-12' ,   5   ),
(1       , '2012-01-16' ,   5   ),
(1       , '2012-01-17' ,   5   ),
(1       , '2012-01-18' ,   5   ),
(1       , '2012-01-19' ,   5   );

它来了:

SELECT category, MIN(`day`) AS firstDayInRange, max(`day`) AS lastDayInRange, a
, COUNT(*) as howMuchDaysInThisRange /*<-- as a little extra*/
FROM
(
SELECT 
IF(@prev != qr.a, @is_a_changing:=@is_a_changing+1, @is_a_changing) AS is_a_changing, @prev:=qr.a, qr.* /*See if column a has changed. If yes, increment, so we can GROUP BY it later*/
FROM
(
SELECT 
test.category, q.`day`, COALESCE(test.a, 0) AS a /*When there is no a, replace NULL with 0*/
FROM
test
RIGHT JOIN
(
SELECT
DATE_SUB(CURDATE(), INTERVAL number_days DAY) AS `day` /*<-- Create dates from now back 999 days. This query is surprisingly fast. And adding more numbers to create more dates, i.e. 10000 dates is also no problem. Therefor a temporary dates table might not be necessary?*/
FROM
(
SELECT (a + 10*b + 100*c) AS number_days FROM
  (SELECT 0 AS a UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) aa
, (SELECT 0 AS b UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) bb
, (SELECT 0 AS c UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) cc
)sq /*<-- This generates numbers 0 to 999*/
)q USING(`day`) 
, (SELECT @is_a_changing:=0, @prev:=0) r
/*This WHERE clause is just to beautify. It may not be necessary*/
WHERE q.`day` >= (SELECT MIN(test.`day`) FROM test) AND q.`day` <= (SELECT MAX(test.`day`) FROM test) 
)qr
)asdf
GROUP BY is_a_changing
ORDER BY 2

结果如下所示:

category    firstDayInRange     lastDayInRange      a   howMuchDaysInThisRange
--------------------------------------------------------------------------
1           2012-01-01          2012-01-04          4   4
1           2012-01-05          2012-01-07          5   3
1           2012-01-08          2012-01-10          4   3
1           2012-01-11          2012-01-12          5   2
            2012-01-13          2012-01-15          0   3
1           2012-01-16          2012-01-19          5   4

【讨论】:

  • 动态创建日历表而不是将其作为数据库中的表基本上存在两个问题:1)您的限制为 1000 天,小于 3 年 2)增加这个数字会使事情成倍地变慢 3)如果您必须多次运行它,那么您将多次创建派生表,效率更低。顺便说一句,我删除了您的“可选”where 子句并获得了 1000 天的期限 :) 到目前为止,最好的解决方案是一次性查询整个数据并在 PHP 中创建缺失的数据。
  • @MostyMostacho 1) 是的 2) 就像我在 cmets 中对查询所说的那样,我认为它非常非常快 3) 真的,我在这里没有看到问题。如果您的服务器遇到这样的查询问题,即使您必须多次运行它,那么您最好重新考虑您的设置。当您获得 1000 天的期限时,我不知道,也许您的测试数据有问题?使用问题中给出的数据(和我的答案)这是可行的。 “可选” where 子句过滤掉 2011 年 12 月 31 日之前和 2012 年 1 月 20 日之后的日期范围的行。就像它说的,“只是为了美化”。
  • +1 不错的查询 :)。但它比我最初的解决方案(简单查询mysql和一些php行)要慢。所以我不能接受。
【解决方案4】:

要使这项工作如你所愿,你应该有两个表:

  1. 期间
  2. 几天

每个时期可以通过FOREIGN KEY 与许多天相关联。使用当前的表结构,您能做的最好的就是检测 PHP 端的连续周期。

【讨论】:

  • 很遗憾无法修改数据库结构。谢谢你的建议。现在的问题是:在 php 方面,是否需要在给定范围内的每一天做一个循环?
【解决方案5】:

首先,这是@Mosty 解决方案的扩展。

为了使 Mosty 的解决方案能够包含表中不存在的类别/日期组合,我采用了以下方法 -

首先获取一个不同的类别列表,然后将其加入整个日期范围 -

SELECT category, `start` + INTERVAL id DAY AS `day`
FROM dummy,(SELECT DISTINCT category FROM t) cats, (SELECT MIN(day) `start`, MAX(day) `end` FROM t) tmp
WHERE id <= DATEDIFF(`end`, `start`)
ORDER BY category, `day`

上述查询使用表 dummy 和单个字段 id 构建完整的日期范围。 id 字段包含 0,1,2,3,.... - 它需要有足够的值来覆盖所需日期范围内的每一天。然后可以将其连接回原始表,以创建所有日期的所有类别的完整列表以及 a -

的适当值
SELECT cj.category, cj.`day`, IFNULL(t.a, 0) AS a
FROM (
    SELECT category, `start` + INTERVAL id DAY AS `day`
    FROM dummy,(SELECT DISTINCT category FROM t) cats, (SELECT MIN(day) `start`, MAX(day) `end` FROM t) tmp
    WHERE id <= DATEDIFF(`end`, `start`)
    ORDER BY category, `day`
) AS cj
LEFT JOIN t
    ON cj.category = t.category
    AND cj.`day` = t.`day`

然后可以将其应用于 Mosty 的查询以代替表 t -

SELECT
    CONCAT(@category := category, ',', MIN(`day`)) col1,
    CONCAT(MAX(`day`), ',', @a := a) col2
FROM (
    SELECT cj.category, cj.day, IFNULL(t.a, 0) AS a
    FROM (
        SELECT category, `start` + INTERVAL id DAY AS `day`
        FROM dummy,(SELECT DISTINCT category FROM t) cats, (SELECT MIN(day) `start`, MAX(day) `end` FROM t) tmp
        WHERE id <= DATEDIFF(`end`, `start`)
        ORDER BY category, `day`
    ) AS cj
    LEFT JOIN t
        ON cj.category = t.category
        AND cj.`day` = t.day) AS t, (select @category := '', @a := '', @counter := 0) init
WHERE @counter := @counter + (category != @category OR a != @a)
GROUP BY @counter, category, a

【讨论】:

  • 嗯,它不能正常工作。它排除了第一个范围的第一天,并排除了空范围之后的某个范围。我还是不明白这个错误的逻辑。
  • 如果id范围不包含0作为第一个值,它只会排除第一天。您必须提供一个您认为失败的具体示例。如果您单独运行查询的各个部分,您应该能够理解它在做什么。如果没有,请提出具体问题。
  • 我应该为table dummy制作一个临时表吗?
  • 如果您没有另一个表,其中包含涵盖所需值范围的连续集(0,1,2,3,...4999,5000 会很有用),那么只需创建另一个表.每次需要时将其创建为临时表将是不必要的开销。您还应该尝试 Mosty 的建议,即用 PHP 而不是 SQL 编写类似的解决方案。
【解决方案6】:

完全在mysql端会有性能建议: 创建程序后,它会在 0.35 - 0.37 秒内运行

create procedure fetch_range()
begin
declare min date;
declare max date;

create  table testdate(
    date1 date
);

select min(day) into min
from category;

select max(day) into max
from category;

while min <= max do

insert into testdate values(min);
set min = adddate(min,1);
end while;

select concat(category,',',min(day)),concat(max(day),',',a) 
from(
SELECT if(isNull(category),@category,category) category,if(isNull(day),date1,day) day,@a,if(isNull(a) || isNull(@a),if(isNull(a) && isNull(@a),@grp,@grp:=@grp+1),if(@a!=a,@grp:=@grp+1,@grp)) as sor_col,if(isNull(a),0,a) as a,@a:=a,@category:= category
FROM  `category` 
RIGHT JOIN testdate ON date1 = category.day) as table1
group by sor_col;

drop table testdate;

end 

o/p:

1,2012-01-01|2012-01-04,4
1,2012-01-05|2012-01-07,5
1,2012-01-08|2012-01-10,4
1,2012-01-11|2012-01-12,5
1,2012-01-13|2012-01-15,0
1,2012-01-16|2012-01-19,5

这里是 mysql 解决方案,它将给出所需的结果,仅排除错过的范围。

PHP: 缺失的范围可以通过php添加。

$sql = "set @a=0,@grp=0,@datediff=0,@category=0,@day='';";
mysql_query($sql);

$sql= "select category,min(day)min,max(day) max,a
from(
select category,day,a,concat(if(@a!=a,@grp:=@grp+1,@grp),if(datediff(@day,day) < -1,@datediff:=@datediff+1,@datediff)) as grp_datediff,datediff(@day,day)diff, @day:= day,@a:=a
FROM  category
order by day)as t
group by grp_datediff";

$result = mysql_query($sql);

$diff = 0;
$indx =0;
while($row = mysql_fetch_object($result)){
    if(isset($data[$indx - 1]['max'])){
    $date1 = new DateTime($data[$indx - 1]['max']);
    $date2 =  new DateTime($row->min);
    $diff = $date1->diff($date2);
    }
    if ($diff->days > 1) {

        $date = new DateTime($data[$indx-1]['max']);
        $interval = new DateInterval("P1D");
        $min = $date->add($interval);

        $date = new DateTime($data[$indx-1]['max']);
        $interval = new DateInterval("P".$diff->days."D");
        $max = $date->add($interval);

        $data[$indx]['category'] = $data[$indx-1]['category'];
        $data[$indx]['min'] = $min->format('Y-m-d');
        $data[$indx]['max'] = $max->format('Y-m-d');
        $data[$indx++]['a'] = 0;

         $data[$indx]['category'] = $row->category;
    $data[$indx]['min'] = $row->min;
    $data[$indx]['max'] = $row->max;
    $data[$indx]['a'] = $row->a;
    }else{


    $data[$indx]['category'] = $row->category;
    $data[$indx]['min'] = $row->min;
    $data[$indx]['max'] = $row->max;
    $data[$indx]['a'] = $row->a;
    }

$indx++;
}

【讨论】:

    【解决方案7】:

    这是你的意思吗?

    SELECT
        category,
        MIN(t1.day),
        MAX(t2.day),
        a
    FROM
        `table` AS t1
    INNER JOIN `table` AS t2 USING (category, a)
    

    【讨论】:

    • 此查询返回:1, 2012-01-02|2012-01-12,4。不,我不明白这是什么。
    • 您想要特定categorya 的最短和最长天数吗?
    • 我必须获取等于“类别”和“a”的一系列连续天数。
    【解决方案8】:

    如果我正确理解您的问题,我会使用以下内容:

    SELECT MAX(day), MIN(day) FROM `YourTable` WHERE `category`= $cat AND `A`= $increment;
    

    ...和...

    $dateRange = $cat.","."$min"."|"."$max".",".$increment;
    

    【讨论】:

    • 没有。 'a' 的值没有给出。我必须获取所有值的所有范围。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-10-23
    • 1970-01-01
    • 1970-01-01
    • 2021-06-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多