MySQL, wie fehlende Daten im Bereich zu füllen?

70

Ich habe eine Tabelle mit 2 Spalten, Datum und Punktzahl. Es hat höchstens 30 Einträge für jeden der letzten 30 Tage.

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

Mein Problem ist, dass einige Daten fehlen - ich möchte sehen:

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
...

Was ich von der einzelnen Abfrage brauche, ist: 19,21,9,14,0,0,10,0,0,14 ... Das bedeutet, dass die fehlenden Daten mit 0 gefüllt sind.

Ich weiß, wie man alle Werte und die serverseitige Sprache durch Datumsangaben iteriert und die Leerzeichen vermisst. Aber ist dies in MySQL möglich, so dass ich das Ergebnis nach Datum sortiere und die fehlenden Teile erhalte.

BEARBEITEN: In dieser Tabelle gibt es eine andere Spalte mit dem Namen UserID, also habe ich 30.000 Benutzer und einige von ihnen haben die Punktzahl in dieser Tabelle. Ich lösche die Daten jeden Tag, wenn das Datum <30 Tage her ist, da ich für jeden Benutzer eine Punktzahl von 30 Tagen benötige. Der Grund dafür ist, dass ich ein Diagramm der Benutzeraktivität in den letzten 30 Tagen erstelle und zum Zeichnen eines Diagramms die 30 durch Komma getrennten Werte benötige. Ich kann also in der Abfrage sagen, dass ich die Aktivität USERID = 10203 erhalten habe, und die Abfrage würde mir die 30 Punkte bringen, eine für jeden der letzten 30 Tage. Ich hoffe ich bin jetzt klarer.

Jerry2
quelle
1
Ja, das ist möglich, aber warum sollten Sie das tun?
NullUserException
1
Ich verstehe es immer noch nicht. Rufen Sie keine unnötigen Daten aus der Datenbank ab, wenn Sie diese Lücken mit dem Diagramm füllen können, und Sie sparen sich etwas Overhead.
NullUserException
3
aber dann muss ich die Daten für USERID AUSWÄHLEN, ich bekomme zum Beispiel 20 Zeilen mit Datum und Punktzahl und dann muss ich meine serverseitige Sprache (ASP) durchlaufen, um zu überprüfen, ob es ein Datum vor 30 Tagen gibt, wenn es nicht ist make 0 else make the database value ... Ist das nicht aufwendiger als das Abrufen von Werten aus der Datenbank 30 und das Erstellen der Zeichenfolge?
Jerry2

Antworten:

58

MySQL verfügt nicht über rekursive Funktionen. Sie müssen also den NUMBERS-Tabellentrick verwenden.

  1. Erstellen Sie eine Tabelle, die nur inkrementierende Zahlen enthält - einfach mit einem 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. Füllen Sie die Tabelle mit:

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

    ... für so viele Werte wie Sie brauchen.

  3. Verwenden Sie DATE_ADD , um eine Liste mit Datumsangaben zu erstellen und die Tage basierend auf dem Wert NUMBERS.id zu erhöhen. Ersetzen Sie "2010-06-06" und "2010-06-14" durch Ihre jeweiligen Start- und Enddaten (verwenden Sie jedoch dasselbe Format, JJJJ-MM-TT) -

    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. LINKS VERBINDEN Sie Ihre Datentabelle basierend auf dem Zeitanteil:

       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`
    

Wenn Sie das Datumsformat beibehalten möchten, verwenden Sie die Funktion DATE_FORMAT :

DATE_FORMAT(`x`.`ts`, '%d.%m.%Y') AS `timestamp`
OMG Ponys
quelle
2
Vielen Dank. Ist dies eine schnelle Operation, von der Sie abraten würden, einen solchen Ansatz zu verwenden und serverseitige Berechnungen durchzuführen?
Jerry2
5
@ Jerry2: Ich bevorzuge es, so viel Daten in der Datenbank zu verarbeiten, abgesehen von wirklich komplizierten Präsentationsmaterialien. Ich beneide nicht, dies im Anwendungscode zu tun, solange es nur eine Reise in die Datenbank ist ...
OMG Ponies
1
Um Indizes zu verwenden, können die Bedingungen (WHERE- und ON-Klauseln) in WHERE n.id < DATEDIFF('2010-06-14', '2010-06-06')undLEFT JOIN TABLE y ON y.date = DATE_FORMAT(x.ts, '%d.%m.%Y')
Paul Spiegel
1
Sobald ich zum Beispiel eine WHERE-Klausel hinzufüge WHERE 'y'.'score' = 2, werden nicht mehr alle ausgefüllten Daten angezeigt
Seba M
21

Ich bin kein Fan der anderen Antworten, da Tabellen erstellt werden müssen und so weiter. Diese Abfrage erledigt dies effizient ohne Hilfstabellen.

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;

Also lasst uns das analysieren.

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

Das if erkennt Tage ohne Punktzahl und setzt sie auf 0. b.Days ist die konfigurierte Anzahl von Tagen, die Sie ab dem aktuellen Datum ausgewählt haben, bis zu 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

Diese Unterabfrage habe ich beim Stackoverflow gesehen. Ab dem aktuellen Datum wird effizient eine Liste der letzten 1000 Tage erstellt. Das Intervall (derzeit 30) in der WHERE-Klausel am Ende bestimmt, welche Tage zurückgegeben werden. Das Maximum ist 1000. Diese Abfrage kann leicht geändert werden, um Daten im Wert von 100 Jahren zurückzugeben, aber 1000 sollte für die meisten Dinge gut sein.

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

Dies ist der Teil, der Ihre Tabelle mit der Partitur enthält. Sie vergleichen mit dem ausgewählten Datumsbereich aus der Datumsgeneratorabfrage, um bei Bedarf Nullen eingeben zu können (die Punktzahl wird NULLanfänglich auf gesetzt, da es sich um eine handelt LEFT JOIN; dies ist in der select-Anweisung festgelegt). Ich bestelle es auch nach Datum, nur weil. Dies ist eine Präferenz, Sie können auch nach Punktzahl bestellen.

Vorher ORDER BYkonnten Sie sich leicht mit Ihrer Tabelle über Benutzerinformationen verbinden, die Sie bei Ihrer Bearbeitung erwähnt haben, um diese letzte Anforderung hinzuzufügen.

Ich hoffe, diese Version der Abfrage hilft jemandem. Danke fürs Lesen.

Michael Conard
quelle
16

Sie können dies mithilfe einer Kalendertabelle erreichen . Dies ist eine Tabelle, die Sie einmal erstellen und mit einem Datumsbereich füllen (z. B. ein Datensatz für jeden Tag 2000-2050; das hängt von Ihren Daten ab). Anschließend können Sie eine äußere Verknüpfung Ihrer Tabelle mit der Kalendertabelle herstellen. Wenn in Ihrer Tabelle ein Datum fehlt, geben Sie 0 für die Punktzahl zurück.

Soundlink
quelle
1
Stimmt, aber eine Zahlentabelle ist flexibler - siehe meine Antwort für ein Beispiel. IE: Was ist, wenn Sie jetzt auch fortlaufende Nummern benötigen? Möchten Sie eine Tabelle pro Datentyp?
OMG Ponys
2
Die Notwendigkeit von fortlaufenden Nummern wäre ein weiterer Anwendungsfall ;-) Wenn Sie auf ein anderes DBMS (z. B. Oracle, MySQL, SQL-Server) abzielen müssen, würde Ihr Ansatz eine leicht modifizierte Anweisung erfordern, und ich vermute, dass der DATE_ADD-Ansatz langsamer als eine Kalendertabelle ist (aber ich denke, das ist hier nicht relevant)
Soundlink
6
Unter http://www.media-division.com/using-mysql-generate-daily-sales-reports-filled-gaps/ können Sie eine Kalendertabelle erstellen . Und obwohl, wie oben bei @ omg-ponies erwähnt, der Zahlentrick fast so schnell ist wie der Kalendertisch, kann die Verwendung der funky Tricks manchmal irreführend sein. Vor allem, wenn Sie erwarten, dass andere Entwickler Ihren Code in Zukunft beibehalten.
Obaranovsky
1
Im Vergleich zur Zahlentabellenlösung können Sie mit einer Kalendertabelle einfache Abfragen schreiben wieSELECT c.date, COALESCE(y.score, 0) AS cnt FROM calendar c LEFT JOIN y ON y.date = c.date WHERE c.date BETWEEN '2010-06-06' AND '2010-06-14'
Paul Spiegel
10

Die Zeit verging, seit diese Frage gestellt wurde. MySQL 8.0 wurde 2018 veröffentlicht und unterstützt rekursive allgemeine Tabellenausdrücke , die eine elegante und hochmoderne Möglichkeit bieten, diese Frage zu lösen.

Die folgende Abfrage kann verwendet werden, um eine Liste von Daten zu erstellen, beispielsweise für die ersten 15 Tage im August 2010:

with recursive all_dates(dt) as (
    -- anchor
    select '2010-08-01' dt
        union all 
    -- recursion with stop condition
    select dt + interval 1 day from all_dates where dt + interval 1 day <= '2010-08-15'
)
select * from all_dates

Sie können left joindiese Ergebnismenge dann mit Ihrer Tabelle erstellen, um die erwartete Ausgabe zu generieren:

with recursive all_dates(dt) as (
    -- anchor
    select '2010-08-01' dt
        union all 
    -- recursion with stop condition
    select dt + interval 1 day from all_dates where dt + interval 1 day <= '2010-08-15'
)
select d.dt date, coalesce(t.score, 0) score
from all_dates d
left join mytable t on t.date = d.dt
order by d.dt

Demo zu DB Fiddle :

Datum | Ergebnis
: --------- | ----:
01.08.2010 | 19
2010-08-02 | 21
03.08.2010 | 0
04.08.2010 | 14
05.08.2010 | 0
06.08.2010 | 0
07.08.2010 | 10
08.08.2010 | 0
09.08.2010 | 0
2010-08-10 | 14
2010-08-11 | 0
2010-08-12 | 0
2010-08-13 | 0
2010-08-14 | 0
2010-08-15 | 0
GMB
quelle
3
DANKESCHÖN! Konnte dies leicht ändern, um auch mit Minuten zu arbeiten!
Jonathan S. Fisher
4

Die Antwort von Michael Conard ist großartig, aber ich brauchte Intervalle von 15 Minuten, in denen die Zeit immer am Anfang jeder 15. Minute beginnen muss:

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

Dadurch wird die aktuelle Zeit auf die 15. Runde der vorherigen Runde eingestellt:

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

Und dies wird die Zeit mit einem 15-minütigen Schritt verkürzen:

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

Wenn es einen einfacheren Weg gibt, lassen Sie es mich bitte wissen.

Phönix
quelle