Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Introduction
First time and occasional developer tend to not fully think through a database design for their projects which can lead to writing more code as progress is made and in many cases from improper database design causes maintenance issues once deployed. In this article, part 2 on a series to write better SQL the focus will be on working with why not to store multiple values in a single field within a SQL-Server database table.
To demonstrate, a database will be used which provides working with in the realm of academia. The question will be, how to store what days of the week a course is offered. Two different approaches will be shown with advantages and disadvantages for both methods used within Windows Form projects using a class project to acquire data from a SQL-Server database with data scripts included to provide the reader with the ability to try both versions.
Although examples work with Microsoft SQL-Server the same concept applies to all relational databases which is, don't simply sit down without mapping out how data will be stored, read, edited, using in reports and maintained.
Words of wisdom
"Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%." Knuth 1974
Using a single field for storing multiple days
In the following example the code is innocuous in that is clear and easy to understand with comments as needed. The problem does not come from presentation of information but instead from how will the raw data be seen and maintained.
Viewing the table data for the last ListBox which contains days a course is offered can the values in the Days field easily be known? When working with the database design on a continual basis, sure the character "H" will be known to be a specific day of the week while not working with this data it is impossible to know what "H" means. An educated guess is M is Monday, T is Tuesday so perhaps H is Thursday etc.
To get the day names the following SELECT is used which is not easy to dissect along with almost impossible for someone without much experience with SQL to maintain the SELECT statement if changes are required.
DECLARE @CourseIdentifier AS INT = 1045;
SELECT SUBSTRING(a.b, v.number + 1, 1) AS DayParts
FROM ( SELECT ( SELECT Days
FROM OnsiteCourse
WHERE ( CourseID = @CourseIdentifier )) AS b ) AS a
INNER JOIN master.dbo.spt_values AS v ON v.number < LEN(a.b)
WHERE ( v.type = 'P' );
The following code implements the above SELECT statement, executes a command followed by iterating the returning data using a select case to traverse each character to assign a day name.
VB.NET
Public Function DayNamesFromSingleField(CourseIdentifier As Integer) As List(Of String)
Dim dayList As New List(Of String)
Using cn As New SqlConnection With {.ConnectionString = ConnectionString}
Using cmd As New SqlCommand With {.Connection = cn}
cmd.CommandText = "SELECT SUBSTRING(a.b, v.number + 1, 1) AS DayParts FROM " &
"(SELECT (SELECT Days FROM OnsiteCourse " &
"WHERE ( CourseID = @CourseIdentifier )) AS b ) AS a " &
"INNER JOIN master.dbo.spt_values AS v ON v.number < LEN(a.b) " &
"WHERE ( v.type = 'P' );"
cmd.Parameters.AddWithValue("@CourseIdentifier", CourseIdentifier)
cn.Open()
Dim reader = cmd.ExecuteReader()
While reader.Read()
Select Case reader.GetString(0)
Case "X"
dayList.Add("Sunday")
Case "M"
dayList.Add("Monday")
Case "T"
dayList.Add("Tuesday")
Case "W"
dayList.Add("Wednesday")
Case "H"
dayList.Add("Thursday")
Case "F"
dayList.Add("Friday")
Case "S"
dayList.Add("Saturday")
Case Else
dayList.Add("Unknown")
End Select
End While
End Using
End Using
Return dayList
End Function
**C# **
public List<string> DayNamesFromSingleField(int CourseIdentifier)
{
var dayList = new List<string>();
using (var cn = new SqlConnection { ConnectionString = ConnectionString })
{
using (var cmd = new SqlCommand { Connection = cn })
{
cmd.CommandText = "SELECT SUBSTRING(a.b, v.number + 1, 1) AS DayParts " +
"FROM (SELECT (SELECT Days FROM OnsiteCourse " +
"WHERE ( CourseID = @CourseIdentifier )) AS b ) AS a " +
"INNER JOIN master.dbo.spt_values AS v ON v.number < LEN(a.b) " +
"WHERE ( v.type = 'P' );";
cmd.Parameters.AddWithValue("@CourseIdentifier", CourseIdentifier);
cn.Open();
var reader = cmd.ExecuteReader();
while (reader.Read())
{
switch (reader.GetString(0))
{
case "X":
dayList.Add("Sunday");
break;
case "M":
dayList.Add("Monday");
break;
case "T":
dayList.Add("Tuesday");
break;
case "W":
dayList.Add("Wednesday");
break;
case "H":
dayList.Add("Thursday");
break;
case "F":
dayList.Add("Friday");
break;
case "S":
dayList.Add("Saturday");
break;
default:
dayList.Add("Unknown");
break;
}
}
}
}
return dayList;
}
Suppose a new requirement is needed or had been forgotten which is, a course(s) is not available every day. At this point the current solution is useless and a redesign is required. Another issue is that the day names are in one culture and each place the table is used (which may be multiple projects) the day names need to be changed. Another issue is the customer may want to change the indexing of days so again the design fails.
Normalizing to store days using multiple tables
Rather attempting to store day names as single characters in a single field use a table, in this case CourseDay which stores the day index of the week which has WeekDayName table with day names. CourseDay has a field CourseID which can be used to get day name and a field Offered which is a bit column seen as a Bool/Boolean in C# and VB.NET.
To get day names in SQL
SELECT CD.id ,
WDN.DayName AS Name ,
CD.DayIndex ,
CD.Offered
FROM dbo.CourseDay AS CD
INNER JOIN dbo.WeekDayName AS WDN ON CD.DayIndex = WDN.WeekId
WHERE ( CD.CourseID = 1050
AND CD.Offered = 1 );
The following function is uses the query above to return a list.
Namespace Classes
Public Class CourseDay
Public Property Id() As Integer
Public Property Name() As String
Public Property DayIndex() As Integer
Public Property Offered() As Boolean
Public Property CourseID() As Integer
Public Overrides Function ToString() As String
Return Name
End Function
End Class
End Namespace
Note by default only available days are read using Offered field equal to True as the parameter for Offered is an optional parameter set to true.
VB.NET
Public Function DayNamesFromReferences(
CourseIdentifier As Integer,
Optional Available As Boolean = True) As List(Of CourseDay)
Dim courseDaysList As New List(Of CourseDay)
Using cn As New SqlConnection With {.ConnectionString = ConnectionString}
Using cmd As New SqlCommand With {.Connection = cn}
cmd.CommandText = "SELECT CD.id , WDN.DayName AS Name ,CD.DayIndex ," &
"CD.Offered FROM dbo.CourseDay AS CD " &
"INNER JOIN dbo.WeekDayName AS WDN ON CD.DayIndex = WDN.WeekId " &
"WHERE ( CD.CourseID = @CourseIdentifier AND Offered = @Available);"
cmd.Parameters.AddWithValue("@CourseIdentifier", CourseIdentifier)
cmd.Parameters.AddWithValue("@Available", Available)
cn.Open()
Dim reader = cmd.ExecuteReader()
While reader.Read()
courseDaysList.Add(New CourseDay() With {.Id = reader.GetInt32(0), .Name = reader.GetString(1), .DayIndex = reader.GetInt32(2), .Offered = reader.GetBoolean(3), .CourseID = CourseIdentifier})
End While
End Using
End Using
Return courseDaysList
End Function
C#
public List<CourseDay> DayNamesFromReferences(int courseIdentifier, bool available = true)
{
var courseDaysList = new List<CourseDay>();
using (var cn = new SqlConnection { ConnectionString = ConnectionString })
{
using (var cmd = new SqlCommand { Connection = cn })
{
cmd.CommandText = "SELECT CD.id , WDN.DayName AS Name ,CD.DayIndex ,CD.Offered " +
"FROM dbo.CourseDay AS CD " +
"INNER JOIN dbo.WeekDayName AS WDN ON CD.DayIndex = WDN.WeekId " +
"WHERE ( CD.CourseID = @CourseIdentifier AND Offered = @Available);";
cmd.Parameters.AddWithValue("@CourseIdentifier", courseIdentifier);
cmd.Parameters.AddWithValue("@Available", available);
cn.Open();
var reader = cmd.ExecuteReader();
while (reader.Read())
{
courseDaysList.Add(new CourseDay()
{
Id = reader.GetInt32(0),
Name = reader.GetString(1),
DayIndex = reader.GetInt32(2),
Offered = reader.GetBoolean(3),
CourseID = courseIdentifier
});
}
}
}
return courseDaysList;
}
Since each item in the ListBox which displays the days is a class item the properties of CourseDay are available which will be needed to perform other actions like store this information for when a day is selected in another window which has a CheckedListBox for a student to select a day out of multiple choices of days the course is offered.
VB.NET
Dim SelectedDay As CourseDay = CType(DaysCourseAvailableListBox.SelectedItem, CourseDay)
C#
CourseDay SelectedDay = (CourseDay)DaysCourseAvailableListBox.SelectedItem;
Proper code placement
Not only is writing maintainable SQL important but how code is written. A good rule to follow is to place code in a form that can not exists outside of a form.
Ask the following question each time data is involved in a project.
- Why would code to access data be placed into a form
- Because its easy
- Everyone else does it
Selecting either of the above reasons is incorrect! Many examples found on the Internet have code in a form because it's easier to demonstrate in a form than to take more time and add a class which would be responsible for interacting with a backend database. Look past the simple example found on the Internet and place code to access data into classes.
Placing code into classes compartmentalizes code
- Makes for ease of maintenance rather than code everything in a single form.
- Permits code to be reused.
These reasons can be compared to SQL in that a focus should be not only placed on finishing a project but also having the project code potentially usable in other projects. In SQL there are two main choices, place SQL in a class project or in a Stored Procedure.
Summary
In this article both code and SQL have been presented to show and explain how to move away from the thought of storing multiple values in a field within a database table. If the reason for this was to be concerned about stored huge number of rows SQL Server has been built to handle a tremendous number of rows with a proper design and proper indexing.
Source code
VB.NET https://github.com/karenpayneoregon/SqlNormalization
C# To follow
Resources
See also
- Writing SQL for your application Part 1
- SQL Server: Representing List of Values Using a Single Value
- Transact-SQL Reference (Database Engine)
- SQL Server Management Studio (SSMS)
- SQL-Server (2017 edition)
- Writing SQL Statements Properly for your solution (MSDN)