Tabellenmigration / Sinnvolles Tabellendesign?

  • C#

Es gibt 11 Antworten in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

    Tabellenmigration / Sinnvolles Tabellendesign?

    Hallöchen,


    ich habe vor naja langer Zeit ein Tool geschrieben das Daten sammelt, archiviert und für mich zugänglich macht per Website.
    Nun war ich damals etwas begrenzt was das Design der Datenbank / Tabellen angeht und nun steht alles an Infos in jeder Zeile in einer Tabelle.
    Die "alte/jetzige" Tabelle sieht so aus:




    Sehr kontraproduktiv da es ja Videos vom gleichen Uploader gibt sowie Videos in gleichen Kategorien und natürlich wird nicht das Video "subscribed" sondern der Benutzer.
    Daher habe ich ein wenig rumgebastelt und folgendes Modell entworfen:



    Die erste Frage machen die Tabellen wirklich mehr Sinn? Meiner Meinung nach schon aber verbessern kann man sich ja immer.

    Die zweite Frage ist wie migriere ich das am schnellsten? Ich hatte mit eine Konsolenanwendung geschrieben die erstmal alle Daten in ein Dataset schreibt und
    dieses gehe ich dann durch und erstelle Kategorien,User sowie die Kategorien->Video Verknüpfung in der "videokategorie" Tabelle. Allerdings dauert das sehr sehr sehr lange.
    Nach 20 Minuten war ich bei 4000 Einträgen von 1,9Millionen.

    Habt ihr da Anregungen für mich?
    Grüße , xChRoNiKx

    Nützliche Links:
    Visual Studio Empfohlene Einstellungen | Try-Catch heißes Eisen
    Jo, für mich sieht das erstmal sinnvoll aus.
    Vewunderlich nur, dass du nicht den DatasetDesigner nimmst, um das TabellenDesign zu machen.

    Zur Geschwindigkeit kann man mangels Code-Ansicht nur die etwas allgemeine Empfehlung aussprechen: Schreibe effizienteren Code, wenn der derzeitig nicht schnell genug ist.
    Danke erstmal für das Feedback.
    Hab das Datenbanken Zeugs direkt in PhpMyAdmin gemacht und dort kann man auch auf nen Designer klicken um da alles zu sehen.

    Ja Code hatte ich erstmal extra weg gelassen da ich dachte es gibt da so nen "Universal-Migrations-Zaubertrick". Aber hier mal der Code.
    Ist nicht Perfekt sondern nur zweck dienlich und kommt auch nur einmal zum migrieren zum Einsatz.

    Die Crawler.VideoRow ist quasi die gleiche Abbildung wie im Post#1 die "alte/jetzige" Tabelle halt nur in nem Dataset.



    Und der Quellcode:
    Spoiler anzeigen

    C#-Quellcode

    1. private void button1_Click(object sender, EventArgs e)
    2. {
    3. MySqlConnection conn = new MySqlConnection(ConnectionstringNeu);
    4. conn.Open();
    5. int all = db.Video.Rows.Count;
    6. int x = 0;
    7. foreach (Crawler.VideoRow row in db.Video.Rows)
    8. {
    9. x += 1;
    10. label1.Invoke((MethodInvoker)delegate
    11. {
    12. label1.Text = $"{x.ToString()} von {all.ToString()}";
    13. });
    14. int vidId = ErstelleVideo(row, conn);
    15. int katId = 0;
    16. if (row.categories != "")
    17. {
    18. List<string> Kategorien = row.categories.Split(';').ToList<string>();
    19. foreach (string kategorie in Kategorien)
    20. {
    21. katId = HoleKategorieId(kategorie, conn);
    22. ErstelleVideoKategorie(vidId, katId, conn);
    23. }
    24. }
    25. }
    26. }
    27. public int ErstelleVideo(Crawler.VideoRow row, MySqlConnection conn)
    28. {
    29. MySqlCommand command = conn.CreateCommand();
    30. command.CommandText = "INSERT INTO `video` (`id`, `extern_id`, `titel`, `url`, `laenge`, `user_id`, `watched`, `uploaddate`) VALUES " +
    31. "(NULL, @extid, @titel, @url, @laenge, @userid, @watched, @timest);";
    32. command.CommandTimeout = 3000;
    33. command.Parameters.AddWithValue("@extid", row.video_id);
    34. command.Parameters.AddWithValue("@titel", row.titel);
    35. command.Parameters.AddWithValue("@url", row.url);
    36. command.Parameters.AddWithValue("@laenge", row.duration);
    37. int userId = HoleUserId(row.uploader, conn);
    38. command.Parameters.AddWithValue("@userid", userId);
    39. command.Parameters.AddWithValue("@watched", (row.watched) ? 1 : 0);
    40. command.Parameters.AddWithValue("@timest", ((DateTimeOffset)row.uploaddate).ToUnixTimeSeconds());
    41. command.ExecuteNonQuery();
    42. return Convert.ToInt32(command.LastInsertedId);
    43. }
    44. public void ErstelleVideoKategorie(int vidId, int katId, MySqlConnection conn)
    45. {
    46. MySqlCommand command = conn.CreateCommand();
    47. command.CommandText = "INSERT INTO `videokategorie` (`id`, `video_id`, `kategorie_id`) VALUES (NULL, @vidid, @katid); ";
    48. command.CommandTimeout = 3000;
    49. command.Parameters.AddWithValue("@vidid", vidId);
    50. command.Parameters.AddWithValue("@katid", katId);
    51. command.ExecuteNonQuery();
    52. }
    53. public int ErstelleUser(string name, int subscribed, MySqlConnection conn)
    54. {
    55. MySqlCommand command = conn.CreateCommand();
    56. command.CommandText = "INSERT INTO `user` (`id`, `username`, `subscribed`) VALUES (NULL, @usname, @subs); ";
    57. command.CommandTimeout = 3000;
    58. command.Parameters.AddWithValue("@usname", name);
    59. command.Parameters.AddWithValue("@subs", subscribed);
    60. command.ExecuteNonQuery();
    61. return Convert.ToInt32(command.LastInsertedId);
    62. }
    63. public int HoleUserId(string name, MySqlConnection conn)
    64. {
    65. MySqlCommand command = conn.CreateCommand();
    66. command.CommandText = "SELECT id FROM user WHERE username = @name";
    67. command.CommandTimeout = 3000;
    68. command.Parameters.AddWithValue("@name", name);
    69. var obj = command.ExecuteScalar();
    70. int userId = 0;
    71. if (obj == null)
    72. {
    73. userId = ErstelleUser(name, 0, conn);
    74. }
    75. else
    76. {
    77. userId = Convert.ToInt32(obj);
    78. }
    79. return userId;
    80. }
    81. public int HoleKategorieId(string name, MySqlConnection conn)
    82. {
    83. MySqlCommand command = conn.CreateCommand();
    84. command.CommandText = "SELECT id FROM kategorie WHERE name = @name";
    85. command.CommandTimeout = 3000;
    86. command.Parameters.AddWithValue("@name", name);
    87. var obj = command.ExecuteScalar();
    88. int katId = 0;
    89. if (obj == null)
    90. {
    91. katId = ErstelleKategorie(name, conn);
    92. }
    93. else
    94. {
    95. katId = Convert.ToInt32(obj);
    96. }
    97. return katId;
    98. }
    99. public int ErstelleKategorie(string name, MySqlConnection conn)
    100. {
    101. MySqlCommand command = conn.CreateCommand();
    102. command.CommandText = "INSERT INTO `kategorie` (`id`, `name`) VALUES (NULL, @katname); ";
    103. command.CommandTimeout = 3000;
    104. command.Parameters.AddWithValue("@katname", name);
    105. command.ExecuteNonQuery();
    106. return Convert.ToInt32(command.LastInsertedId);
    107. }
    Grüße , xChRoNiKx

    Nützliche Links:
    Visual Studio Empfohlene Einstellungen | Try-Catch heißes Eisen
    ich würde nicht jeden zu erzeugenden Datensatz gleich an die DB senden.
    Mach das typDataset richtig, also leg da alle Tabellen an, wie sie in die DB sollen. Inklusive Relationen (Bei Php-gepflegten Datenbanken werden die Relationen leider oft unterschlagen).
    Am besten wäre, du hättest eine ordentliche Datenbank (also mit Relationen, und diese mit Löschweitergabe), und daraus könntest du dir ein typisiertes Dataset generieren. (Datenquellen hinzufügen - aus Datenbank).
    Dabei entstehen auch TableAdapter, mit denen man Db-Tabellen und Dataset-Tabellen synchronisieren kann (in beide Richtungen).

    Dann könntest du die "schmuddel-Tabelle" abrufen ins Dataset, und dort die komplette Migration innerhalb des Datasets vornehmen.
    Am Ende die neu befüllten Dataset-Tabellen in die DB schubsen.

    Ist schwierig, weil meist zickt VS rum mit MySql - da muss man genau die richtige Connectivity-Libraries installiert haben.
    Und dann muss die DB noch richtig gebaut sein.
    Man musses jdfs. hinkriegen, dass alle Änderungen im Dataset (DataRowstat.Added, .Deleted, .Changed) sauber in die DB synchronisiert werden.

    Also imo muss man das sowieso haben, wenn man eine Db-Anwendung schreibt.

    Und wennde das hast, dann ist die Migration eben auch einfach machbar wie gesagt, und wie eiglich jede Db<->Dataset - Datenverarbeitung vonstatten geht:
    1. Daten vonne DB ins Dataset holen
    2. Daten im Dataset verarbeiten
    3. Dataset komplett mitte Db synchronisieren (.UpdateAll)
    Sooo jetzt erstmal danke für die Anregungen und Vorschläge ich habe das DataSet nun so aufgebaut (das wurde so aus der MySQL DB halt alles generiert):



    Nun habe ich dann angefangen die Daten erstmal im Dataset zu migrieren halt hauch row für row in der alten Tabelle durch gehen und die neuen Zeilen
    in den anderen Tabellen erstellen.
    Klappt auch. Hatte das gestern mit 30000 Zeilen getestet und klappte wunderbar auch die Übertragung zurück zur MySQL Datenbank.
    Hier mal der Code zum migrieren:
    Spoiler anzeigen

    C#-Quellcode

    1. int counter = 1;
    2. foreach (Crawler.videos_neuRow row in db.videos_neu.Rows)
    3. {
    4. progressText($"{counter.ToString()} von {allVideos.ToString()} fertig");
    5. Crawler.videoRow videorow = db.video.NewvideoRow();
    6. var userrow = db.user.FirstOrDefault(u => u.username == row.videoUploader);
    7. int userId = 0;
    8. if(userrow == null)
    9. {
    10. Crawler.userRow newUser = db.user.NewuserRow();
    11. newUser.username = row.videoUploader;
    12. newUser.subscribed = row.subscribedStatus;
    13. db.user.Rows.Add(newUser);
    14. userId = newUser.id;
    15. } else
    16. {
    17. userId = userrow.id;
    18. }
    19. videorow.extern_id = row.videoId;
    20. videorow.titel = row.videoTitle;
    21. videorow.url = row.videoUrl;
    22. videorow.laenge = row.videoDuration;
    23. videorow.user_id = userId;
    24. videorow.watched = row.watchedStatus;
    25. videorow.uploaddate = row.videoUploadDate;
    26. db.video.Rows.Add(videorow);
    27. int videoId = videorow.id;
    28. if (row.videoCategories != "")
    29. {
    30. List<string> Kategorien = new List<string>();
    31. if (row.videoCategories.Contains(","))
    32. {
    33. Kategorien = row.videoCategories.Split(',').ToList<string>();
    34. } else
    35. {
    36. Kategorien = row.videoCategories.Split(';').ToList<string>();
    37. }
    38. foreach (string kategorie in Kategorien)
    39. {
    40. var katrow = db.kategorie.FirstOrDefault(k => k.name == kategorie.Trim());
    41. int katid = 0;
    42. if (katrow == null)
    43. {
    44. Crawler.kategorieRow newKatrow = db.kategorie.NewkategorieRow();
    45. newKatrow.name = kategorie.Trim();
    46. db.kategorie.Rows.Add(newKatrow);
    47. katid = newKatrow.id;
    48. }
    49. else
    50. {
    51. katid = katrow.id;
    52. }
    53. Crawler.videokategorieRow vidkatrow = db.videokategorie.NewvideokategorieRow();
    54. vidkatrow.video_id = videoId;
    55. vidkatrow.kategorie_id = katid;
    56. db.videokategorie.Rows.Add(vidkatrow);
    57. }
    58. }
    59. counter += 1;
    60. }


    Das einzige was komisch ist es dauert trotzdem noch unheimlich lange.
    Um 10:54 Uhr heute habe ich den Vorgang gestartet und jetzt um 17:27 Uhr sind wir bei 1.318.250 migrierten Einträgen von 2.049.079 Einträgen.
    Die noch nicht in der Datenbank sind sondern erstmal nur im Dataset.

    Hab ich da noch einen Denkfehler drin?
    Grüße , xChRoNiKx

    Nützliche Links:
    Visual Studio Empfohlene Einstellungen | Try-Catch heißes Eisen
    Für eine Hierarchisierung einer flachen Tabelle sind viele Suchvorgänge erforderlich.
    Weil die ganzen Dopplungen der FlachTabelle sind dabei ja auszusieben.
    Diese Suchvorgänge sind bei dir sehr ineffektiv.
    Mit Hilfe von Dictionaries liesse sich das sicher enorm steigern (1000-fach oderso).

    Wenn du mir eine Sample-Anwendung bastelst, mit sonem Dataset drinne, welche dann auch aus einer Datei mw. 1000 Flachdatensätze laden kann, dann könnte ich sone Optimierung basteln.

    Eigentlich soll man ja erst Performance-Analysen machen, obs wirklich die Aufbereitungs-Methode ist, die so dauert.
    Eventuell zeigst du die Millionen Datensätze in einem DGV an - dann hätte ich eine andere Idee, warum das nicht vorangeht ;)

    Noch was anderes ist, dass ich auch noch nie mit einem Dataset mit Millionen Datensätzen gearbeitet hab.
    Vielleicht ist da auch iwann Ende der Bohnenstange.
    Hallöchen,

    ich habe gestern Abend noch etwas mehr rumgebastelt und im Internet recherchiert und da bin ich auf einen Beitrag im CodeProject gestoßen:
    codeproject.com/Tips/790019/Slow-LINQ-Methods

    Dort wird die Performance von FirstOrDefault sowie anderen Methoden getestet und es stellt sich halt raus das die sehr sehr langsam sind.

    Also habe ich in den betroffenen Zeilen:
    Tabellenmigration / Sinnvolles Tabellendesign? und
    Tabellenmigration / Sinnvolles Tabellendesign?

    Mal auf .Select() umgestellt und siehe da um alles zu migrieren braucht es jetzt nur noch ~20 Minuten.

    Soll ich dir @ErfinderDesRades trotzdem noch nen Sample machen?

    Ansonsten schaue ich nun mal ob alles ordentlich durch läuft also auf der Upload in die Datenbank am Ende und falls ja würde ich das Thema als erledigt markieren.
    Grüße , xChRoNiKx

    Nützliche Links:
    Visual Studio Empfohlene Einstellungen | Try-Catch heißes Eisen
    Pardon ja klar. Sind wie gesagt nur FirstOrDefault durch Select ersetzt: (hier in Z.13 und Z.55)

    C#-Quellcode

    1. public static string escapestring(string t)
    2. {
    3. return t.Replace("\'", "''").Replace("*", "[*]");
    4. }
    5. public void convertDatabase()
    6. {
    7. int counter = 1;
    8. foreach (Crawler.videos_neuRow row in db.videos_neu.Rows)
    9. {
    10. progressText($"{counter.ToString()} von {allVideos.ToString()} fertig");
    11. Crawler.videoRow videorow = db.video.NewvideoRow();
    12. var userrow = db.user.Select($"username = '{escapestring(row.videoUploader)}'");
    13. int userId = 0;
    14. if (userrow.Length == 1)
    15. {
    16. Crawler.userRow nuserrow = (Crawler.userRow)userrow[0];
    17. userId = nuserrow.id;
    18. }
    19. else
    20. {
    21. Crawler.userRow newUser = db.user.NewuserRow();
    22. newUser.username = row.videoUploader;
    23. newUser.subscribed = row.subscribedStatus;
    24. db.user.Rows.Add(newUser);
    25. userId = newUser.id;
    26. }
    27. videorow.extern_id = row.videoId;
    28. videorow.titel = row.videoTitle;
    29. videorow.url = row.videoUrl;
    30. videorow.laenge = row.videoDuration;
    31. videorow.user_id = userId;
    32. videorow.watched = row.watchedStatus;
    33. videorow.uploaddate = row.videoUploadDate;
    34. db.video.Rows.Add(videorow);
    35. int videoId = videorow.id;
    36. if (row.videoCategories != "")
    37. {
    38. List<string> Kategorien = new List<string>();
    39. if (row.videoCategories.Contains(","))
    40. {
    41. Kategorien = row.videoCategories.Split(',').ToList<string>();
    42. }
    43. else
    44. {
    45. Kategorien = row.videoCategories.Split(';').ToList<string>();
    46. }
    47. foreach (string kategorie in Kategorien)
    48. {
    49. var katrow = db.kategorie.Select($"name = '{escapestring(kategorie.Trim())}'");
    50. int katid = 0;
    51. if(katrow.Length == 1)
    52. {
    53. Crawler.kategorieRow nkatrow = (Crawler.kategorieRow)katrow[0];
    54. katid = nkatrow.id;
    55. } else
    56. {
    57. Crawler.kategorieRow newKatrow = db.kategorie.NewkategorieRow();
    58. newKatrow.name = kategorie.Trim();
    59. db.kategorie.Rows.Add(newKatrow);
    60. katid = newKatrow.id;
    61. }
    62. Crawler.videokategorieRow vidkatrow = db.videokategorie.NewvideokategorieRow();
    63. vidkatrow.video_id = videoId;
    64. vidkatrow.kategorie_id = katid;
    65. db.videokategorie.Rows.Add(vidkatrow);
    66. }
    67. }
    68. counter += 1;
    69. }
    70. }
    Grüße , xChRoNiKx

    Nützliche Links:
    Visual Studio Empfohlene Einstellungen | Try-Catch heißes Eisen

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „EaranMaleasi“ ()

    na, da bin ich echt platt.
    Wie hat mans geschafft, .FirstOrDefault so miserabel zu programmieren?

    Weil die Substitution läuft die Auflistung ja komplett durch, indem .Count aufgerufen wird.
    Hingegen ein korrekt implementiertes .FirstOrDefault würde ja abbrechen, sobald ein erstes verwertbares Ergebnis auftritt - müsste also schon von daher schneller sein als .Count.

    Aaaber halt!! deine neue Version ist ja falsch!
    Die Version in post#5 hat ein .FirstOrDefault mit Bedingung, deine .Select-VAriante erwählt den nächstbesten, und generiert auch nur einen String.
    Das dürfte m.E. eine Exception geben - wenn da in #17 der String in eine userRow gecasted wird.

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „ErfinderDesRades“ ()

    ErfinderDesRades schrieb:

    Die Version in post#5 hat ein .FirstOrDefault mit Bedingung, deine .Select-VAriante erwählt den nächstbesten, und generiert auch nur einen String.
    Das dürfte m.E. eine Exception geben - wenn da in #17 der String in eine userRow gecasted wird.


    Also das .Select gibt mir ein Array aus DataRow Objekten zurück siehe: docs.microsoft.com/de-de/dotne…ble_Select_System_String_
    Um zu prüfen ob ja überhaupt min. eine DataRow da ist checke ich ja nach ob das Array.Length == 1 ist.
    Und erst wenn ich SICHER bin das eine DataRow enthalten ist caste ich die erste (Index 0) DataRow zu der userRow.
    Da ich in der Select Anweisung ja nur auf wirklich den gleichen Usernamen prüfe kann es da keine doppelten geben / mehrere Zeilen.

    Oder verstehe ich dich falsch?

    Btw. ist das Migrieren komplett fertig und durchgelaufen. Alles ohne Fehler / Exceptions.
    Grüße , xChRoNiKx

    Nützliche Links:
    Visual Studio Empfohlene Einstellungen | Try-Catch heißes Eisen
    Ah - du hast recht! Das ist ja nicht Linq-Select(), sondern DataTable.Select()!
    DataTable.Select() verwende ich normal nie, weil ist nicht typisiert. Ich verwende immer Linq - aber in diesem Falle scheint das ja performance-Relevant.
    Jo, jetzt kann ich mir das auch zusammenreimen. DataTable.Select() hält intern ein DataView, also eine Liste mit einem Index.
    Ist also eine vergleichbare Optimierung wie das was ich vorhätte mit meinen Dictionaries.
    Dann bin ich ja beruhigt, und .FirstOrDefault ist nicht miserabel programmiert.
    .FirstOrDefault ist halt ein linearer Durchgang durch eine Liste - kommt natürlich gegen einen Index nicht an.

    Hmm- dassis ja interessant - das wäre ja ein Grund, sich eine typisierte typDataTable.Select(string) - Extension zu basteln.

    Zu meiner Schande mussich gestehen, den CP-Artikel erst jetzt nachgeguckt zu haben.
    Dazu noch eine Anregung an dich: Rate den Tip up (mit 5!), wenn er dir nützlich war.
    Dassis wichtig, weil das Rating-System auf CP ist under-used, und so konnten da "Seilschaften" entstehen, die gegenseitig ihre mittelmässigen Artikel mit Fleiss hochraten, was wiederum die Artikel-Suche verzerrt, wennde einfach mal'n guten Artikel lesen willst.

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „ErfinderDesRades“ ()