Laden...

Wie sollte ich meine Datenbank am besten strukturieren? --> Normalisierung

Erstellt von Kriz vor 6 Jahren Letzter Beitrag vor 5 Jahren 2.368 Views
K
Kriz Themenstarter:in
141 Beiträge seit 2017
vor 6 Jahren
Wie sollte ich meine Datenbank am besten strukturieren? --> Normalisierung

verwendetes Datenbanksystem: SQLite

Hi zusammen, ich habe weniger eine technische, mehr eine "Logik-Frage"...

Ich sitze an einem Programm zur Dienstplangestaltung, gespeichert werden die Dienstpläne in einer SQLite Datenbank.

Aktuell gehe ich folgendermaßen vor:

Der Dienstplan ist die Klasse "Rota", diese Klasse "Rota" hat eine List<Employee>, jeder Eintrag in der List<Employee> hat wiederum eine List<Schedule>, jeder Eintrag dieser List<Schedule> besteht aus einer ScheduleID und einer List<Break>, die List<Break> bestehen aus Start und Ende, ich versuchs mal so zu erklären:

Rota:

  • StartDate
  • Name
  • Note
  • ID
  • <List>Employee
    • ID
    • <List>Schedule
      • ID
      • <List> Break
        • Start
        • Ende

In meiner Datenbank habe ich eine Tabelle, in der ich die Dienstpläne speicher. Diese Tabelle hat die Spalten Name, ID, StartDate, Note, Entry. Wobei die Spalte Entry ein String ist, der sich durch die EmployeeID, DayID, ScheduleID, usw zusammensetzt. Der String sieht für einen Mitarbeiter beispielsweise folgendermaßen aus:

33:0#1$07:00:00-13:00:00;08:00:00/08:00:00

(Mitarbeiter ID 33, TagID 0, ShiftID 1, von 07:00 bis 13:00, eine Pause von 08:00 bis 08:30)

Für jeden weiteren Tag und jeden weiteren Mitarbeiter wird der String länger und länger, für einen komletten Dienstplan kommen da schonmal 10.000 Zeichen zusammen.
Zwar funktioniert es so, aber ich befürchte dass, je länger der String wird, je mehr Fehler können sich einschleichen.

Was für andere Lösungsansätze hättet Ihr für das Problem?

Vielen Dank!!!

Kriz

16.806 Beiträge seit 2008
vor 6 Jahren

Das Einmaleins der Datenbankentwicklung: Normalisierung (Datenbank)

K
Kriz Themenstarter:in
141 Beiträge seit 2017
vor 6 Jahren

Ich tuh mich da etwas schwer...

Wenn ich die Spalte "Entry" auslager in eine separate Tabelle, dann hätte diese Tabelle folgende Spalten: EntryID, DayID, EmployeeID, ScheduleID, Start, End, Breaks. Dann würde ein Dienstplan mit beispielsweise 30 Mitarbeitern 210 Datensätze (jeder Mitarbeiter 7 Tage) erzeugen. Ist das noch praktikabel? Oder habe ich einen Denkfehler?

T
2.219 Beiträge seit 2008
vor 6 Jahren

Warum sollte das nicht praktikabel sein?
Du brauchst für jeden Mitarbeiter und Tag die entsprechenden Einträge.
Hätte ein Mitarbeiter sogar zwei Schichten an einem Tag, müsstest du sogar zwei Einträge an dem Tag mit den entsprechenden Zeiten in der DB speichern.

Deine Datenbank sollte die Daten möglichst sauber, eben durch die Normalisierung aufgeteilt, speichern.
Es gibt Datenbanken wo in einer Tabelle Millionen oder gar Milliarden Einträge gespeichert sein können.
Dort haben dann die Datenbanken größen im TB bis PB Bereich, was also schon etwas anspruchsvoller ist.
Da sind 210 Einträge schon unbedeutend wenig für die gängigen Datenbanken.

Schau dir beim Thema Datenbanken auch den Themenbereich des Index an.
Damit kannst du die Performance deiner Datenbank bei richtiger Indizierung hoch halten.
Ich empfehle dir dafür auch das Buch "SQL Performance Explained"

Habe ich selbst schon durchgelesen und die Tipps dürften dir zukünftig auch helfen deine Datenbank für hohe Performance zu optimieren.
Die Normalisierung musst du dir aber vorher angeschaut haben, die wir in dem Buch nicht behandelt.
Dort geht es Primär im die Nutzung des Index für Datenbank Performance.

Link:
https://www.amazon.de/SQL-Performance-Explained-Entwickler-SQL-Performance/dp/3950307818/ref=sr_1_1?ie=UTF8&qid=1521664839&sr=8-1&keywords=sql+performance+explained

Nachtrag:
Zum Thema Normalisierung:
Lass dich nicht entmutigen.
War Anfangs auch für mich relativ trocken und schwere Kost.
Aber wenn du es verstanden hast, dann kannst du saubere Datenbanken planen und umsetzen.
Mit genug Erfahrung geht das dann locker von der Hand und wird sich auch positiv auf deine zukünftigen Projekte auswirken.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

K
Kriz Themenstarter:in
141 Beiträge seit 2017
vor 6 Jahren

Schonmal vielen Dank dafür!!!
Das hat geholfen!

Wie wird es denn normalerweise mit dem Trennen von Datenbanken gehandhabt? Ich speicher in ein und der selben Datenbank sowohl Dienstpläne, als auch Mitarbeiterinformationen wie Adresse, Telefonnumer, usw.
Sollte sowas besser in eine andere Datenbank ausgelagert werden oder spricht nichts dagegen alles in einer zu "sammeln"

H
523 Beiträge seit 2008
vor 6 Jahren

Das kannst Du ohne Probleme alles in einer Datenbank unterbringen.

16.806 Beiträge seit 2008
vor 6 Jahren

Grundlagen. Grundlagen. Grundlagen.
Hier betrifft es vor allem das Referenzieren von Datensätzen (Primary Key, Foreign Key) über verschiedene Tabellen.
Und das muss in einer Datenbank sein, da sich Keys nicht enforced über Datenbanken hinweg verwalten lassen.
Cross-Database-Foreign-Keys gibt es i.d.R. bei relationalen Datenbanken nicht.

Wie auch beim Programmieren braucht man bei Datenbank gewissen Grundlagen.
Investier also ein paar Stunden in Tutorials. Und das lohnt sich.
Ein orderntliche Grundlagenbaustein wird Dich nur ein Bruchteil an Zeit kosten, als das Neuerstellen, Reparieren und Migrieren eines zusammen gebastelten Brocken in eine ordentliche Lösung - den Ärger und die Reputation mal ausgeklammert.

K
Kriz Themenstarter:in
141 Beiträge seit 2017
vor 6 Jahren

So, ich habe mich mit dem Thema Normalisierung auseinandrr gesetzt und meine Datenbank dem enzsprechend umgestaltet. Ergibt auch alles Sinn und gefällt mir auch gut.
Nun habe ich allerdings das Problem, dass ein einfacher Speichervorgang, also das Eintragen in die Datenbank, statt 3 Sekunden (vorher), nun bis zu 45 Sekunden dauert.
Ergibt ja auch Sinn, wo es vorher nur ein INSERT war, sind es jetzt einige mehr, jede Schedule, jeder Break wird ja seperat eingetragen.

Meine Frage: Wie kann ich dad Speichern verkürzen? Gibt es auch da bestimmtr Grundlagen/Techniken die mir da weiter helfen könnten?

16.806 Beiträge seit 2008
vor 6 Jahren

Auch 45 Sekunden kann schon nicht sein.
Irgendwas machst Du da grundlegend falsch. Code?

T
2.219 Beiträge seit 2008
vor 5 Jahren

Wie sieht dein aktueller Code zum speichern den aus?
Selbst wenn du zich Insert Anweisungen machen würdest, sollten diese zusammen genommen keine Sekunde dauern, je nachdem wir du diese eben in die DB schreibst.
Den solch eine Steigerung klingt meistens nach falscher Umsetzung o.ä.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

K
Kriz Themenstarter:in
141 Beiträge seit 2017
vor 5 Jahren
public static void SaveRota(Rota GivenRota)
        {
            Stopwatch neww = new Stopwatch();
            neww.Start();
            SQLiteConnection connection = new SQLiteConnection() { ConnectionString = Properties.Resources.DBConnectionString };
            SQLiteCommand command = new SQLiteCommand(connection);
            string DBConnectionString = Properties.Resources.DBConnectionString;
            connection.Open();
            command.CommandText = "SELECT Max(RotaEntryID) FROM RotaEntry";
            SQLiteDataReader reader = command.ExecuteReader();
            int EntryID = 0;
            int ScheduleEntryID = 0;
            string SQLCommand = "";
            while (reader.Read())
            {
                EntryID = int.Parse(reader[0].ToString()) +1;
            }
            reader.Close();
            command.CommandText = "SELECT MAX(DayEntryID) From DayEntry";
            reader = command.ExecuteReader();
            while (reader.Read())
            {
                ScheduleEntryID = int.Parse(reader[0].ToString()) +1;
            }
            reader.Close();

            foreach (var employee in GivenRota.ListOfEmployee)
            {
                Stopwatch WatchEmp = new Stopwatch();
                WatchEmp.Start();
                if (!employee.HasWeekOff())
                {
                    int[] DayEntryID = new int[7];
                    int DayCounter = 0;
                    foreach (var day in employee.plannedSchedulesThisWeek)
                    {
                        Stopwatch WatchDay = new Stopwatch();
                        WatchDay.Start();
                        if (day.ID == 0)
                        {
                            DayEntryID[DayCounter] = 0;
                        }
                        else
                        {
                            string BreakString = "";
                            if (day.ListOfBreaks.Count > 0)
                            {
                                foreach (var bre in day.ListOfBreaks)
                                {
                                    BreakString += bre.Start + "-" + bre.End + ";";
                                }
                                BreakString.Trim(';');

                            }
                            SQLCommand += String.Format("INSERT INTO DayEntry (plannedScheduleID, Start, End, Break) VALUES ('{0}','{1}','{2}','{3}');", day.ID, day.Start, day.End, BreakString);
                            DayEntryID[DayCounter] = ScheduleEntryID;
                            ScheduleEntryID++;
                        }
                        WatchDay.Stop();
                        Debug.WriteLine(WatchDay.Elapsed.ToString() + " für Schedule");
                        DayCounter++;
                    }

                    SQLCommand += String.Format("INSERT INTO RotaEntry (RotaEntryID, EmployeeID, ScheduleDay0, ScheduleDay1, ScheduleDay2, ScheduleDay3 ,ScheduleDay4, ScheduleDay5, ScheduleDay6) VALUES ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}', '{8}');"
                                                                                , EntryID, employee.ID, DayEntryID[0], DayEntryID[1], DayEntryID[2], DayEntryID[3], DayEntryID[4], DayEntryID[5], DayEntryID[6]);
                    command.ExecuteNonQuery();
                }
                WatchEmp.Stop();
                Debug.WriteLine(WatchEmp.Elapsed.ToString() + " für Mitarbeiter " + employee.Showingname);
            }
            SQLCommand += String.Format("INSERT INTO Rota (StartDate, Name, Entry, Note, IsPublished) VALUES ('{0}','{1}','{2}','{3}','{4}')"
                                            , GivenRota.StartDate, GivenRota.Name, EntryID, GivenRota.Note, GivenRota.isPublished);
            
            command.CommandText = SQLCommand;
            command.ExecuteNonQuery();
            
            command.Dispose();
            connection.Close();
            connection.Dispose();
            neww.Stop();
            Debug.WriteLine(neww.Elapsed.ToString() + " für Function");

Das ist de Funktion...
Vorher hatte ich an den Stellen wo ich SQLCommand weitere INSERTs hinzufüge den INSERT direkt aufgerufen, also mit ExecuteNonQuery, das hat dann ungefähr 1min gedauert, nun habe ich einen "MonsterString" erstellt, jetzt dauert es noch ca 30 Sekunden.
Nun hab eich etwas weiter gegoogelt und "BeginTransaction" und "Commit" gefunden. Damit dauert es noch 1,2 Sekunden. Damit ist das Problem also gelöst.
Wenn Du allerdings an meiner Datenbankverarbeitung noch Verbesserungen siehst immer her damit!

Vielen Dank!
Kriz

16.806 Beiträge seit 2008
vor 5 Jahren

Whew.... was ein Code. 😉

Erster Schritt: strukturier mal das ganze Zeug.
Das, was Du da baust... das ist nur eine Frage der Zeit, bis es knallt.
[Artikel] Drei-Schichten-Architektur

Zweiter Schritt: Du machst so ziemlich alles falsch, was man da nur so falsch machen kann 😉
Es sieht fast so aus, dass Du die gesamte Datenbanktabelle in die Anwendung lädst und iterierst - und das Zeile für Zeile.
Das macht das gesamte Zeug natürlich extremst langsam.

Und Datenbank IDs (Integer) vergibt die Datenbank, und nicht die Anwendung.
Deswegen gibt es Funktionalitäten wie Database-generated IDs.

PS: es ist eine Methode und keine Funktion.
PPS: Dein Code ist sehr anstrengend zu lesen.
[Artikel] C#: Richtlinien für die Namensvergabe
PPPS: Finger weg von statische Methoden.
Statische Methoden sind nicht ordentlich testbar. Dein Code ist so im Gesamten nicht testbar.
[Artikel] Unit-Tests: Einführung in das Unit-Testing mit VisualStudio

Und: [Artikelserie] SQL: Parameter von Befehlen

T
2.219 Beiträge seit 2008
vor 5 Jahren

Schau dir auch in dem Zug gleich mal das Thema der Parameter für die Insert Abfragen an.
Hier öffnest du mit String.Format deine SQL Anweisungen direkt für SQL Injections, was sich auf lange Sicht immer rächen wird.

Ebenfalls solltest du deinen Code so umsetzen, dass deine Verbindungen nur kurz geöffnet werden.
Gerade bei SQLite sollte man die Verbindungen kurz offen halten, da auch nur eine Verbindung bei SQLite möglich ist.

Nachtrag:
Anstelle der Dispose Aufrufe kannst du auch einen using Block bauen.
Hilft auch dabei den Code besser zu lesen.

Und du solltest eine Transaktion um deine Insert Anweisungen legen.
Wenn du z.B. 100 Inserts hast, sind dies 100 einzelne Transaktionen für die DB.
Entsprechend hat die DB auch etwas mehr Aufwand als alle Inserts in einer Transaktion.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.