6

問題-30分間隔で小計の合計を効率的に取得する

私はMySQLを使用しており、異なる時間の小計を含むテーブルがあります。これらの売上の合計を午前7時から午前12時までの30分間隔で取得したいと思います。私の現在のソリューション(以下)は機能しますが、約150,000レコードをクエリするのに13秒かかります。将来的には数百万件のレコードを作成する予定ですが、現在の方法は遅すぎます。

これをより効率的にするにはどうすればよいですか、または可能であればPHPコンポーネントを純粋なSQLに置き換えますか?また、日付と時刻の列を使用する代わりにUnixタイムスタンプを使用した場合、ソリューションがさらに効率的になるのに役立ちますか?

テーブル名-領収書

subtotal    date        time      sale_id
--------------------------------------------
   6        09/10/2011  07:20:33     1
   5        09/10/2011  07:28:22     2
   3        09/10/2011  07:40:00     3
   5        09/10/2011  08:05:00     4
   8        09/10/2011  08:44:00     5
...............
  10        09/10/2011  18:40:00     6
   5        09/10/2011  23:05:00     7

望ましい結果

このような配列:

  • 30分1:::(7:00から7:30)=>小計の合計は11です
  • 30分2:::(7:30から8:00)=>小計の合計は3
  • 30分3:::(8:00から8:30)=>小計の合計は5
  • 30分4:::(8:30から9:00)=>小計の合計は8

現在の方法

現在の方法では、午前7時に開始し、1800秒ずつ増加するforループを使用します。これは、30分に相当します。その結果、これによりデータベースに対して約34のクエリが実行されます。

for($n = strtotime("07:00:00"), $e = strtotime("23:59:59"); $n <= $e; $n += 1800) {  

    $timeA = date("H:i:s", $n);
    $timeB = date("H:i:s", $n+1799);

    $query = $mySQL-> query ("SELECT SUM(subtotal)
                              FROM Receipts WHERE time > '$timeA' 
                              AND time < '$timeB'");

    while ($row = $query-> fetch_object()) {
        $sum[] = $row;
    }
}

電流出力

出力は単なる配列です。

  • [0]は午前7時から午前7時30分を表します
  • [1]は午前7時30分から午前8時を表します
  • [33]は、午後11時30分から午後11時59分59秒までを表します。

    配列( "0" => 10000、 "1" => 20000、.............. "33" => 5000);

4

7 に答える 7

5

この単一のクエリを試すこともできます。30分のグループ化の合計を含む結果セットが返されるはずです。

SELECT date, MIN(time) as time, SUM(subtotal) as total
FROM `Receipts`
WHERE `date` = '2012-07-30'
GROUP BY hour(time), floor(minute(time)/30)

これを効率的に実行するには、日付と時刻の列に複合インデックスを追加します。

次のような結果セットが返されるはずです。

+---------------------+--------------------+
| time                | total              |
+---------------------+--------------------+
| 2012-07-30 00:00:00 |        0.000000000 |
| 2012-07-30 00:30:00 |        0.000000000 |
| 2012-07-30 01:00:00 |        0.000000000 |
| 2012-07-30 01:30:00 |        0.000000000 |
| 2012-07-30 02:00:00 |        0.000000000 |
| 2012-07-30 02:30:00 |        0.000000000 |
| 2012-07-30 03:00:00 |        0.000000000 |
| 2012-07-30 03:30:00 |        0.000000000 |
| 2012-07-30 04:00:00 |        0.000000000 |
| 2012-07-30 04:30:00 |        0.000000000 |
| 2012-07-30 05:00:00 |        0.000000000 |
| ...
+---------------------+--------------------+
于 2012-08-01T21:54:15.173 に答える
4

まず、単一のDATETIME列を使用しますが、DATE列とTIME列を使用しても機能します。

1つのクエリを使用して、すべての作業を1回のパスで実行できます。

select date,
       hour(`time`) hour_num, 
       IF(MINUTE(`time`) < 30, 0, 1) interval_num, 
       min(`time`) interval_begin,
       max(`time`) interval_end,
       sum(subtotal) sum_subtotal
 from receipts
where date='2012-07-31'
group by date, hour_num, interval_num;
于 2012-08-01T21:49:46.547 に答える
2

アップデート:

「欠落している」行については気にしないので、クエリが午前7時から午前12時までではない期間の行を返す可能性があることを心配していないことも(おそらく間違って)想定します。このクエリは、指定された結果セットを返します。

SELECT (HOUR(r.time)-7)*2+(MINUTE(r.time) DIV 30) AS i 
     , SUM(r.subtotal) AS sum_subtotal
  FROM Receipts r
 GROUP BY i
 ORDER BY i

これは、列を参照する式から派生した期間インデックス(i)を返しtimeます。このクエリのパフォーマンスを最高にするには、次のような「カバー」インデックスを使用できるようにする必要があります。

ON Receipts(`time`,`subtotal`)

列に等式述語を含める場合date(これはソリューションには表示されませんが、「選択された」回答のソリューションには表示されます)、その列を先頭のインデックスとして持つとよいでしょう。 「カバーする」インデックス。

ON Receipts(`date`,`time`,`subtotal`)

午前7時より前の期間に行が返されないようにする場合はHAVING i >= 0、クエリに句を追加するだけです。(午前7時より前の期間の行は、iに対して負の数を生成します。)

SELECT (HOUR(r.time)-7)*2+(MINUTE(r.time) DIV 30) AS i 
     , SUM(r.subtotal) AS sum_subtotal
  FROM Receipts r
 GROUP BY i
HAVING i >= 0
 ORDER BY i

以前:

現在返しているものと同様の結果セットが必要だと思いますが、一挙に。このクエリは、現在取得しているのと同じ33行を返しますが、ピリオド(0〜33)を識別する追加の列があります。これは私が得ることができるあなたの現在の解決策にできるだけ近いです:

SELECT t.i
     , IFNULL(SUM(r.subtotal),0) AS sum_subtotal
  FROM (SELECT (d1.i + d2.i + d4.i + d8.i + d16.i + d32.i) AS i
             , ADDTIME('07:00:00',SEC_TO_TIME((d1.i+d2.i+d4.i+d8.i+d16.i+d32.i)*1800)) AS b_time
             , ADDTIME('07:30:00',SEC_TO_TIME((d1.i+d2.i+d4.i+d8.i+d16.i+d32.i)*1800)) AS e_time
          FROM (SELECT 0 i UNION ALL SELECT 1) d1 CROSS
          JOIN (SELECT 0 i UNION ALL SELECT 2) d2 CROSS
          JOIN (SELECT 0 i UNION ALL SELECT 4) d4 CROSS
          JOIN (SELECT 0 i UNION ALL SELECT 8) d8 CROSS
          JOIN (SELECT 0 i UNION ALL SELECT 16) d16 CROSS
          JOIN (SELECT 0 i UNION ALL SELECT 32) d32
        HAVING i <= 33
       ) t
  LEFT
  JOIN Receipts r ON r.time >= t.b_time AND r.time < t.e_time
 GROUP BY t.i
 ORDER BY t.i

いくつかの重要な注意事項:

秒が「59」または「00」に正確に等しい場合は常に、現在のソリューションがレシートから行を「欠落」している可能性があるようです。

また、日付コンポーネントには関係がないように見えます。すべての日付に対して単一の値を取得しているだけです。(誤解している可能性があります。)そうであれば、クエリで裸のTIME列を参照できるため、DATE列とTIME列を分離するとこれが役立ちます。

date列にWHERE句を追加するのは簡単です。たとえば、1日の小計ロールアップを取得するには、たとえば、の前にWHERE句を追加しGROUP BYます。

WHERE r.date = '2011-09-10'

カバーリングインデックスON Receipts(time,subtotal)(カバーリングインデックスがまだない場合)は、パフォーマンスに役立つ場合があります。(日付列に等式述語を含める場合(上記のWHERE句のように、最も適切なカバーインデックスはおそらくON Receipts(date,time,subtotal)。です。

time列のデータ型はTIMEであると想定しました。t(そうでない場合は、(派生した)b_time列とe_time列のデータ型をReceiptsの列のデータ型と一致させるために、(インラインビューでのエイリアスとして)クエリを少し調整する必要がありますtime

他の回答で提案されているソリューションの一部は、特定の期間内にレシートに行がない場合、33行を返すことが保証されていません。「行の欠落」は問題ではないかもしれませんが、時系列および期間データで頻繁に発生する問題です。

私は、33行が返されることを保証したいと仮定しました。上記のクエリは、期間に一致する行が見つからない場合、小計ゼロを返します。(その場合、現在のソリューションはNULLを返すことに注意してください。SUMがNULLの場合に0を返すように、そのSUM集計をIFNULL関数でラップしました。)

したがって、インラインクエリtは醜い混乱のようにエイリアスされますが、高速に動作します。実行しているのは、0から33までの個別の整数値を持つ33行を生成することです。同時に、各期間をテーブルのtime列に「一致」させるために使用される「開始時間」と「終了時間」を導出します。 Receipts

timeReceiptsテーブルの列を関数でラップしないように注意しますが、裸の列のみを参照します。また、暗黙的な変換が行われないようにする必要があります(これが、b_timeとe__timeのデータ型を一致させる理由です。andADDTIME関数SEC_TO_TIMEは両方ともTIMEデータ型を返します(一致とGROUPの実行を回避することはできません)。 BY操作。)

その最後の期間の「終了時間」の値は「24:00:00」として返され、次のテストを実行して、これが照合に有効な時間であることを確認します。

SELECT MAKETIME(23,59,59) < MAKETIME(24,0,0)

これは成功している(1を返す)ので、問題ありません。

派生列(t.b_timeおよびt.e_time)も結果セットに含めることができますが、配列を作成するために必要ではなく、それらを含めない方が(おそらく)より効率的です。


最後に、パフォーマンスを最適化するには、エイリアス化されたインラインビューをt実際のテーブルにロードすると(一時テーブルで十分です)、インラインビューの代わりにテーブルを参照できると便利な場合があります。これを行う利点は、そのテーブルにインデックスを作成できることです。

于 2012-08-01T23:16:29.097 に答える
0

純粋なSQLにする1つの方法は、ルックアップテーブルを使用することです。私はMySqlをよく知らないので、コードに多くの改善があるかもしれません。私のコードはすべてMsSqlになります。次のようにします。

   /* Mock salesTable */
   Declare @SalesTable TABLE (SubTotal int, SaleDate datetime)
Insert into @SalesTable (SubTotal, SaleDate) VALUES (1, '2012-08-01 12:00')
Insert into @SalesTable (SubTotal, SaleDate) VALUES (2, '2012-08-01 12:10')
Insert into @SalesTable (SubTotal, SaleDate) VALUES (3, '2012-08-01 12:15')
Insert into @SalesTable (SubTotal, SaleDate) VALUES (4, '2012-08-01 12:30')
Insert into @SalesTable (SubTotal, SaleDate) VALUES (5, '2012-08-01 12:35')
Insert into @SalesTable (SubTotal, SaleDate) VALUES (6, '2012-08-01 13:00')
Insert into @SalesTable (SubTotal, SaleDate) VALUES (7, '2012-08-01 14:00')

/* input data */
declare @From datetime, @To DateTime, @intervall int 
set @from = '2012-08-01' 
set @to = '2012-08-02'
set @intervall = 30

/* Create lookup table */
DECLARE @lookup TABLE (StartTime datetime, EndTime datetime) 
DECLARE @tmpTime datetime
SET @tmpTime = @from
WHILE (@tmpTime <= @To) 
BEGIN
 INSERT INTO @lookup (StartTime, EndTime) VALUES (@tmpTime, dateAdd(mi, @intervall, @tmpTime))
 set @tmpTime = dateAdd(mi, @intervall, @tmpTime)
END

/* Get data */
select l.StartTime, l.EndTime, sum(subTotal) from @SalesTable as SalesTable 
    join @lookUp as l on SalesTable.SaleDate >= l.StartTime and SalesTable.SaleDate < l.EndTime
    group by l.StartTime, l.EndTime
于 2012-08-01T21:56:09.523 に答える
0

私のクエリでは、dateという名前の1つの日時フィールドを想定しています。これにより、最初に指定した日時に開始するすべてのグループが表示されます。

SELECT 
  ABS(FLOOR(TIMESTAMPDIFF(MINUTE, date, '2011-08-01 00:00:00') / 30)) AS GROUPING
  , SUM(subtotal) AS subtotals 
FROM 
  Receipts 
GROUP BY 
  ABS(FLOOR(TIMESTAMPDIFF(MINUTE, date, '2011-08-01 00:00:00') / 30))
ORDER BY
  GROUPING
于 2012-08-01T22:06:17.600 に答える
0

データには常に適切なデータ型を使用してください。日付/時刻列の場合は、(できればUTCゾーンの)タイムスタンプとして保存するのが最適です。これは、一部の日付(一部のティムゾーン、したがってUTC)に存在しない場合があるという点で特に当てはまります。この列にインデックスが必要になります。

また、日付/時刻の範囲では、希望するものが得られません。つまり、正確に時間に何かが欠落しています(厳密に大なりの比較を使用しているため)。範囲は常に「下限を含む」、「上限を除く」(so、time >= '07:00:00' AND time < '07:30:00')として定義します。これは、処理するフィールドの数が追加されているタイムスタンプにとって特に重要です。

mySQLには再帰クエリがないため、これを実行するためにいくつかの追加のテーブルが必要になります。私はそれらを「永続的な」テーブルとして参照していますが、必要に応じて、それらをインラインで定義することは確かに可能です。

カレンダーテーブルが必要になります。これらはさまざまな理由で役立ちますが、ここでは日付のリストに使用する必要があります。これにより、必要に応じて、小計が0の日付を表示できます。同じ理由で、30分単位の時間の値も必要になります。

これにより、次のようにデータをクエリできるようになります。

SELECT division, COALESCE(SUM(subtotal), 0)
FROM (SELECT TIMESTAMP(calendar_date, clock_time) as division
      FROM Calendar
      CROSS JOIN Clock
      WHERE calendar_date >= DATE('2011-09-10') 
      AND calendar_date < DATE('2011-09-11')) as divisions
LEFT JOIN Sales_Data
ON occurredAt >= division 
AND occurredAt < division + INTERVAL 30 MINUTE
GROUP BY division

(簡潔にするために通常を使用するSQLFiddleの作業例JOIN

于 2012-08-01T22:32:53.193 に答える
0

私も別の解決策を見つけました。誰かがこれに遭遇した場合に参照できるように、ここに投稿します。30分間隔でグループ化します。

SELECT SUM(total), time, date
FROM tableName
GROUP BY (2*HOUR(time) + FLOOR(MINUTE(time)/30))

詳細情報へのリンク http://www.artfulsoftware.com/infotree/queries.php#106

于 2012-08-02T16:39:24.427 に答える