web-dev-qa-db-ja.com

MySQLは範囲内の欠落している日付を埋める方法は?

日付とスコアの2つの列を持つテーブルがあります。過去30日間に1つずつ、最大30のエントリがあります。

date      score
-----------------
1.8.2010  19
2.8.2010  21
4.8.2010  14
7.8.2010  10
10.8.2010 14

私の問題は、いくつかの日付が欠落していることです-私は見たいです:

date      score
-----------------
1.8.2010  19
2.8.2010  21
3.8.2010  0
4.8.2010  14
5.8.2010  0
6.8.2010  0
7.8.2010  10
...

単一のクエリから必要なのは、19,21,9,14,0,0,10,0,0,14 ...を取得することです。つまり、欠落している日付は0で埋められます。

私はすべての値を取得する方法を知っており、サーバー側の言語で日付を繰り返して空白を欠いています。しかし、mysqlでこれを行うことは可能です。そのため、結果を日付でソートし、不足している部分を取得します。

編集:このテーブルにはUserIDという名前の別の列があるので、30.000人のユーザーがいて、そのうちのいくつかはこのテーブルにスコアがあります。各ユーザーの最後の30日間のスコアが必要なため、日付が30日未満の場合は毎日日付を削除します。その理由は、過去30日間のユーザーアクティビティのグラフを作成しており、チャートをプロットするには、カンマで区切られた30個の値が必要だからです。そのため、クエリでUSERID = 10203アクティビティを取得すると言うことができ、クエリは過去30日間に1つずつ、30のスコアを取得します。今より明確になればいいのですが。

59
Jerry2

MySQLには再帰的な機能がないため、NUMBERSテーブルトリックを使用する必要があります-

  1. インクリメントする数値のみを保持するテーブルを作成します-auto_incrementを使用して簡単に実行できます。

    DROP TABLE IF EXISTS `example`.`numbers`;
    CREATE TABLE  `example`.`numbers` (
      `id` int(10) unsigned NOT NULL auto_increment,
       PRIMARY KEY  (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    
  2. 以下を使用してテーブルに入力します。

    INSERT INTO `example`.`numbers`
      ( `id` )
    VALUES
      ( NULL )
    

    ...必要な数だけの値。

  3. DATE_ADD を使用して日付のリストを作成し、NUMBERS.id値に基づいて日数を増やします。 「2010-06-06」と「2010-06-14」をそれぞれの開始日と終了日で置き換えます(ただし、同じ形式、YYYY-MM-DDを使用します)-

    SELECT `x`.*
      FROM (SELECT DATE_ADD('2010-06-06', INTERVAL `n`.`id` - 1 DAY)
              FROM `numbers` `n`
             WHERE DATE_ADD('2010-06-06', INTERVAL `n`.`id` -1 DAY) <= '2010-06-14' ) x
    
  4. 時間の部分に基づいて、データの表に左から参加します。

       SELECT `x`.`ts` AS `timestamp`,
              COALESCE(`y`.`score`, 0) AS `cnt`
         FROM (SELECT DATE_FORMAT(DATE_ADD('2010-06-06', INTERVAL `n`.`id` - 1 DAY), '%m/%d/%Y') AS `ts`
                 FROM `numbers` `n`
                WHERE DATE_ADD('2010-06-06', INTERVAL `n`.`id` - 1 DAY) <= '2010-06-14') x
    LEFT JOIN TABLE `y` ON STR_TO_DATE(`y`.`date`, '%d.%m.%Y') = `x`.`ts`
    

日付形式を維持する場合は、 DATE_FORMAT関数 を使用します。

DATE_FORMAT(`x`.`ts`, '%d.%m.%Y') AS `timestamp`
55
OMG Ponies

カレンダーテーブルを使用してこれを実現できます。これは、一度作成して日付範囲を埋めるテーブルです(例:2000-2050の各日ごとに1つのデータセット。データに依存します)。その後、カレンダーテーブルに対してテーブルの外部結合を作成できます。テーブルに日付がない場合、スコアに0を返します。

14
Soundlink

私はテーブルの作成などを必要とする他の回答のファンではありません。このクエリは、ヘルパーテーブルなしで効率的に実行します。

SELECT 
    IF(score IS NULL, 0, score) AS score,
    b.Days AS date
FROM 
    (SELECT a.Days 
    FROM (
        SELECT curdate() - INTERVAL (a.a + (10 * b.a) + (100 * c.a)) DAY AS 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) AS a
        CROSS JOIN (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) AS b
        CROSS JOIN (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) AS c
    ) a
    WHERE a.Days >= curdate() - INTERVAL 30 DAY) b
LEFT JOIN your_table
    ON date = b.Days
ORDER BY b.Days;

これを分析してみましょう。

SELECT 
    IF(score IS NULL, 0, score) AS score,
    b.Days AS date

Ifは、スコアのない日を検出して0に設定します。b.Daysは、現在の日付から取得するように選択した構成済みの日数で、最大1000日です。

    (SELECT a.Days 
    FROM (
        SELECT curdate() - INTERVAL (a.a + (10 * b.a) + (100 * c.a)) DAY AS 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) AS a
        CROSS JOIN (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) AS b
        CROSS JOIN (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) AS c
    ) a
    WHERE a.Days >= curdate() - INTERVAL 30 DAY) b

このサブクエリは、stackoverflowで見たものです。現在の日付から過去1000日間のリストを効率的に生成します。末尾のWHERE句の間隔(現在は30)によって、返される日が決まります。最大値は1000です。このクエリは、数百年分の日付を返すように簡単に変更できますが、ほとんどの場合、1000で十分です。

LEFT JOIN your_table
    ON date = b.Days
ORDER BY b.Days;

これは、スコアを含むテーブルをそこに取り込む部分です。日付ジェネレータークエリから選択した日付範囲と比較して、必要に応じて0を入力できるようにします(スコアはLEFT JOINであるため、最初はNULLに設定されます。これは選択ステートメント)。また、日付で注文します。これは好みですが、スコアで並べることもできます。

ORDER BYの前に、編集で言及したユーザー情報についてテーブルに簡単に参加して、その最後の要件を追加できます。

このバージョンのクエリが誰かを助けることを願っています。読んでくれてありがとう。

9
Michael Conard

マイケル・コナードの答えは素晴らしいですが、15分ごとに常に開始する必要がある15分の間隔が必要でした:

SELECT a.Days 
FROM (
    SELECT FROM_UNIXTIME( FLOOR( UNIX_TIMESTAMP() / (15 * 60) ) * (15 * 60)) - INTERVAL 15 * (a.a + (10 * b.a) + (100 * c.a)) MINUTE AS 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) AS a
    CROSS JOIN (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) AS b
    CROSS JOIN (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) AS c
) a
WHERE a.Days >= curdate() - INTERVAL 30 DAY

これにより、現在の時刻が前のラウンドの15分に設定されます。

FROM_UNIXTIME( FLOOR( UNIX_TIMESTAMP() / (15 * 60) ) * (15 * 60))

そして、これは15分のステップで時間を削除します:

- INTERVAL 15 * (a.a + (10 * b.a) + (100 * c.a)) MINUTE

もっと簡単な方法があれば、教えてください。

1
phoenix