Is it ever okay to violate first normal form? - sql

I have an Access database that will be required to present data with several one-to-many relationships on one row (e.g., it would list items as "a, b, e, f", and I would have multiple columns like that). I know it's a bad idea to store data that way as well, but considering that I'm allowing the user to filter on several of these columns, I can't think of a better way of dealing with the data than violating first normal form.
As an example: say that I have several journal articles, each of which may report on multiple animals and multiple vegetables. The user can filter on the source name, or they can filter on one or more animals and one or more vegetables. The output should look like
Source name....animals...............vegetables
Source 1.......dogs, cats, birds.....carrots, tomatoes
Source 2.......leopards, birds.......tomatoes, zucchini, apples
Source 3.......cats, goldfish........carrots, cucumbers
Typically you would have a separate table with Source name + animal:
Source name......animal
Source 1.........dog
Source 1.........cats
Source 1.........birds
Source 2.........leopards
etc
and a similar table for vegetables. But considering how the data needs to be presented to the user (a comma-separated list), and how the user filters the data (he may filter to only see sources that include dogs and cats, and sources with carrots and tomatoes), I think it makes sense to store the data as comma separated lists for animals and vegetables. With a comma-separated list, when the user selects multiple vegetables and multiple animals, I can say
WHERE (Vegetables like "*carrots*" and Vegetables like "*tomatoes*") AND (Animals like *dogs*" and Animals like "*cats*")
I can't think of an efficient way to do this same kind of query in Access without using a lot of VBA and multiple queries.

You can always construct a scenario in which violating any rule makes sense, so the answer to the question in your title is Yes.
This is not one of those scenarios, however. The searching and presentation issues you raise are common to most one-to-many relationships (or, at least, to many one-to-many relationships) and if this were a reason to violate first normal form then you wouldn't see a lot of normalized databases.
Construct the database correctly and you won't have to worry about commas, search terms embedded in each other, and slow searches due to the lack of indexes. Write a reusable piece of code to perform the comma-separate roll-ups for you so you don't keep reinventing the wheel.

i would still normalize this properly - and then worry about the presentation.
in Oracle - this would be done with a user defined aggregate function.

Why not contruct a Join Table so you can sustain the 1:1 in relationship to field referential integrity. Otherwise you will have to parse out the 1:many field value to then get a referential association so everything works magically (hehehe ;))
When i find myself in need to violate the 1st normal form, the answer 9:10 is to create a Join Table and construct some methodology to produce the desired effect.
Edited: 2012-10-09 9:06AM
This design was in response to an unknown amount of information to be displayed in an unknown amount of column/fields. Although mine is oriented towards numerical values, you could simply develop a vba method to concatenate the information fields to produce a singular field of data.
Table1
gid (Number) <- Table2.id
cid (Number) <- Table3.id
price (Number)
gid MANY <- ONE Table2.id
cid MANY <- ONE Table3.id
crt_CategoryPG
TRANSFORM Sum(Table1.price) AS SumOfprice
SELECT View1.tid, Table1.cid, View1.category
FROM Table2 INNER JOIN (View1 INNER JOIN Table1 ON View1.cid = Table1.cid) ON Table2.gid = Table1.gid
WHERE (((Table2.active)=True) AND ((View1.active)=True))
GROUP BY View1.tid, Table1.cid, View1.category, Table2.active, View1.active
ORDER BY View1.tid, Table1.cid
PIVOT [Table2].[gid] & " - " & [Table2].[nm];
Refresh the CT Table
Public Function RefreshCategoryPricing(Optional sql As String = "")
If HasValue(lstType) Then
Application.Echo False 'Turn off Screen Updating
Dim cttbl As String: cttbl = CreateCTTable("crt_CategoryPG") 'Create Table to store the Cross-Tab information
If IsNullOrEmpty(sql) Then
sql = SQLSelect(cttbl)
End If
Dim flds As DAO.Recordset: Set flds = CurrentDb.OpenRecordset(sql)
Dim fldwd As String, fldhd As String 'Store the Field Width pattern and Field Header Row
fldhd = "-1;-1;Category"
fldwd = "0"";0"";2.5""" 'Handles `tid`, `cid`, and `category` columns in the ListBox
'Assign the number of columns based on the number of fields in CTtable
lstCategoryPG.ColumnCount = flds.Fields.Count
Dim fld As Long
For fld = 3 To (flds.Fields.Count - 1)
fldwd = fldwd & ";.75"""
fldhd = fldhd & ";" & flds.Fields(fld).Name
Next
GC flds
lstCategoryPG.ColumnHeads = True
lstCategoryPG.ColumnWidths = fldwd
sql = SQLSelect(cttbl, , ("tid = " & lstType.Value))
lstCategoryPG.Enabled = True
RefreshControl CurrentDb, lstCategoryPG, sql, , False
lstCategoryPG.AddItem fldhd, 0
Application.Echo True 'Turn on Screen Updating
End If
End Function
Create Cross-Tab Table
'#ct - String value, Source Cross-Tab to base Table design off of
Public Function CreateCTTable(ct As String) As String
Dim tbl As String: tbl = "tbl_" & ct
Dim sql As String
If TableExists(tbl) Then
'Table exists and needs to be dropped
sql = SQLDrop(tbl)
CurrentDb.Execute sql
End If
'Create Table
sql = SQLSelect(ct, "* INTO " & tbl)
CurrentDb.Execute sql
CreateCTTable = tbl
End Function

Related

Make an MS Access Report label visible based on data in the report's record source query

In MS Access, I have a report based on a query that presents a summary of a medical checkup. I would like labels for each test to be visible ONLY when those tests were performed. For example, if Glucose was performed on a patient, then the label "lblGlucose" should appear in the report, next to the result. The results currently are present in the report, the problem is when a test is not performed the label is always present. This gives the patient a feeling that the testing was not performed correctly.
To hide the labels I have tried the following approaches:
Private Sub Report_Load()
'1st approach: Lookup column [GLUCOSE] from query qrySummary if not null then set visible property of label lblGLUCOSE to True, else set property to False
IIF(IsNotNull(DLookup("[GLUCOSE]", "qrySummary")),Me!lblGLUCOSE.Visible = True,Me!lblGLUCOSE.Visible = False)
'2nd approach: If value of field [GLUCOSE_RSLT] from table tblResults make textbox txtGlucose visible. FYI: Table tblResults is the table that holds all the results of all the test performed. The query qrySummary derives from this table.
Me!txtGlucose.Visible = Not IsNull([tblResults]![GLUCOSE_RSLT])
'3rd approach: Count column [GLUCOSE], from query qrySummary and if greater than 0 then label lblBHClbl visible
End Sub
I'm still coding the 3rd approach but I'm pretty much running out of ideas and getting nowhere. For first two approaches I get field or expression not found. I don't need for all approaches to work, just one, -in fact, I'm open to other ideas on how I can accomplish the above task.
Any help would be ENORMOUSLY appreciated! Thanks a million!
I'm sharing my DB structure for better understanding
The SQL statement for the summary report is:
PARAMETERS [Forms]![frmIngresoEmpleados]![IDChequeo] Long;
SELECT TblClienteCorp.NombreEmpresa, TblClienteCorp.Direccion, tblChequeo.IDChequeo, tblChequeo.FechaMuestreo, tblChequeo.ChequeoPeriodico, qryCountGenero.*, tblEmpleadosClienteCorp.Genero, tblResultados.Aud_RSLT, tblResultados.Otos_RSLT, tblResultados.AV_RSLT, tblResultados.EKG_RSLT, tblResultados.FR_RSLT, tblResultados.TGP_RSLT, tblResultados.TGO_RSLT, tblResultados.CS_RSLT, tblResultados.ESP_RSLT, tblResultados.PB_RSLT, tblResultados.BHC_RSLT, tblResultados.Plaquetas_RSLT, tblResultados.EGO_RSLT, tblResultados.EGH_RSLT, tblResultados.VDRL_RSLT, tblResultados.Gluc_RSLT, tblResultados.Col_RSLT, tblResultados.EFEC_RSLT, tblResultados.PL_RSLT, tblResultados.Derm_RSLT, tblResultados.Isop_RSLT, tblResultados.BAAR_RSLT, tblResultados.ExFarin_RSLT, tblResultados.Lep_RSLT, tblResultados.Copro_RSLT, tblResultados.Osteo_RSLT, tblResultados.RX_RSLT, tblResultados.US_RSLT
FROM TblClienteCorp INNER JOIN ((tblChequeo INNER JOIN (tblEmpleadosClienteCorp INNER JOIN qryCountGenero ON tblEmpleadosClienteCorp.IDEmpleado = qryCountGenero.IDEmpleado) ON tblChequeo.IDChequeo = tblEmpleadosClienteCorp.IDChequeo) INNER JOIN tblResultados ON tblEmpleadosClienteCorp.IDEmpleado = tblResultados.IDEmpleados) ON TblClienteCorp.IDClienteCorp = tblChequeo.IDClienteCorp
WHERE (((tblChequeo.IDChequeo)=[Forms]![frmIngresoEmpleados]![IDChequeo]));
Within the report that is one query per test, which is:
PARAMETERS [Forms]![frmIngresoEmpleados]![IDChequeo] Long;
SELECT Count(tblResultados.IDEmpleados) AS CuentaDeIDEmpleados, tblResultados.Gluc_RSLT, tblEmpleadosClienteCorp.IDChequeo
FROM tblEmpleadosClienteCorp INNER JOIN tblResultados ON tblEmpleadosClienteCorp.IDEmpleado = tblResultados.IDEmpleados
GROUP BY tblResultados.Gluc_RSLT, tblEmpleadosClienteCorp.IDChequeo
HAVING (((tblResultados.Gluc_RSLT)="P") AND ((tblEmpleadosClienteCorp.IDChequeo)=[Forms]![frmIngresoEmpleados]![IDChequeo]));
If qrySummary has multiple patient records, need WHERE CONDITION criteria:
Me.lblGlucose.Visible = Not IsNull(DLookup("[GLUCOSE]", "qrySummary", "PatientID=" & Me!PatientID))
However, VBA is not necessary. Calculate in textbox (or in query and bind textbox to calculated field) and set control with transparent BorderStyle. Options:
show "None" text when no data:
=Nz(DLookup("[GLUCOSE]", "qrySummary", "PatientID=" & Me!PatientID), "None").
instead of label, use a textbox with expression:
=IIf(IsNull(DLookup("[GLUCOSE]", "qrySummary", "PatientID=" & Me!PatientID)), "", "Glucose")

Auto generate id number in vb.net

I am creating a library management system. I want to generate different book code for different book genre. For eg:- There is a combo box with book three book genres(novel,literature,poem) and there is another text box with book code. I want if someone chooses novel in comboBox, book code starts form N001 and if someone chooses literature in comboBox, book code starts from L001 and if someone chooses poem, book code starts from P001.
(My vb application is connected with ms access database.)
I see several problems with your fundamental numbering strategy.
Most library numbering systems (Dewey Decimal/Library of Congress) factor sort ordering abilities to their numbering system and allow for future additions to be able to sort higher without needing to shift the number of all other items. With your incremental strategy, how would you accommodate inserting a book later in the process?
If you set up with three digits of numbering, what would you do once you get 1000 books in a category?
While you could use a magic number table that includes the last id allocated per category and increment it as you add a new book. If you have more than one user, there is a potential for a race condition where two users get the same last number and both try to increment it at the same time landing on the same new Id.
I typically recommend keeping a generic ID that has no meaning in the system. This number would be set as an Auto Number in Access and the database would generate new Ids when records are added and ensure that you don't have the collision issue above. If you absolutely need a publically viewable Id, you can use the magic number strategy for a separate BookNumber column that isn't used for record identity. That way if you have a collision in the future, data won't get corrupted.
there are several schools of thought on how one can approach this.
One way? Well, you say have a book type or some such, and then you have that book code. It would be a simple number column without the prefix (P, B etc.).
So, to get the next number, you would then go:
myBookNumber = DMAX("BookNumber","tblBooks","BookType = 'P')
MyBookNumber = MyBookNumber + 1
So, you simply store the book number separate from the prefix. In forms, reports etc., you can then always display the book number like rst!BookType & rst!BookNumber
Another way, which I prefer is to create a table that allows you to get the next book number This also makes it easy to add more types, and better is such a design lets you set the starting number. This approach is also often used for invoice numbers.
So, you create a table called tlbBookIncrements. It will look like this:
ID: (pk - autonumber)
BookType: text (the one char book type)
BookNumber: Number the next book number to assign.
You then build a function like this:
Public Function GetNextBookN(strBookType As String) As Long
Dim strSQL As String
Dim rst As DAO.Recordset
strSQL = "SELECT * FROM tblBookIncrement where BookType = '" & strBookType & "'"
Set rst = CurrentDb.OpenRecordset(strSQL)
If rst.RecordCount = 0 Then
' this book type does not exist - lets add it and start at 1
rst.AddNew
rst!BookType = strBookType
rst!booknumber = 1
End If
GetNextBookN = rst!booknumber
rst!booknumber = rst!booknumber + 1
rst.Update
rst.Close
End Function
Update:
The poster was using vb.net, not VBA, my bad.
Here is the above as vb.net, it really much the same.
Public Function GetNextBookN(strBookType As String) As Integer
Using mycon As New OleDbConnection(My.Settings.ConnectionString1)
Dim strSQL As String
strSQL = "SELECT * FROM tblBookIncrement where BookType = '" & strBookType & "'"
Using da As New OleDbDataAdapter(strSQL, mycon)
Dim cmdBuilder = New OleDbCommandBuilder(da)
Dim rst As New DataTable
da.Fill(rst)
da.AcceptChangesDuringUpdate = True
With rst
If .Rows.Count = 0 Then
With .Rows.Add
.Item("BookType") = strBookType
.Item("booknumber") = 1
End With
GetNextBookN = 1
Else
GetNextBookN = .Rows(0).Item("booknumber")
.Rows(0).Item("booknumber") += 1
End If
End With
da.Update(rst)
End Using
End Using
End Function
So, now anytime you need a book number, you can go like this:
Say, in a form, you need to assign the book number, you could go like this:
You could say use the on-insert event of the form, and go like this:
me!BookNumber = me!bookType & GetNextBookN(me!bookType)
So, the above would say if the bookType is "P", result in P101 if the numbering table has a book type of P and the booknumber set to 101. And of course once you pull that number, then the handy dandy function will increment the number for you.
And I also added the feature that if the type requested does not exist, then we add the type and start at 1. This way, the code works as you over time add new types of books, and then the book number table will always work, and have available a next book number easily available in code.

update main table from two others with modification of data before updating

I have two tables that I need to update. I have to go through every record on one table then do some modifications to the data then upload the modified
data to another table!
The two tables have 3,000 records and 11,000 records. Plus I also have to
check some info from a third table with about 50 records!
Dim id
Dim fly_SQL
id="user1"
Dim rsc1_conn As ADODB.Connection
Set rsc1_conn = CreateObject("ADODB.Connection")
rsc1_conn.Provider = "SQLOLEDB"
rsc1_conn.ConnectionString = "SERVER=companyserver;UID=" & id &
";Trusted_Connection=Yes;DATABASE=DATAbank" '
rsc1_conn.Open
Set rsc1 = CurrentDb.OpenRecordset("SELECT * FROM main_database",
dbOpenDynaset, dbSeeChanges)
rsc1.movefirst
do until rsc1.EOF
fly_SQL = "Select * from alt_db where alt_db.number = main.net_number"
Set rsc2 = CurrentDb.OpenRecordset(fly_SQL)
do stuff
code = dlookup( "type_def", "third_rec" , alt_db.activity = activity)
The two tables both use net_number as a reference which on the main is primary key unique, but the alt_db has multiple entries.
So basically I have to loop through each net_number on the main, look at the matching net_number on the alt_db then compare an activity field
with a third table to see which field I update on the main! If it's a Project management expense I put it in the main.PM_cost. The net_number in alt_db might repeat for 10 other expenses that need to be funneled into their proper expense categories in the main DB! As an example:
Main table looks like
net_number
first record shows
main.netnumber = 123456
main.cont_cost
main.PM_cost
main.mgmt_cost
alt_db table looks like
alt_db.net_number
alt_db.activity
alt_db.PM_cost
alt_db.const_cost
alt_db.mgmt_cost
third_rec looks like
third_rec.code
third_type
where data can be something like con1 , sabb ,
code type
sauf construction
con1 management
I130 project management
And needed rules:
check alt_db.activity with third_rec.act and return activity type
If activity type is construction then I put the alt_db.cost into main.const_cost
If activity type is project_mgmt then I put the alt_db.cost into main.PM_cost. The alt_db.activity could be con1 or SAF4 and the type is determined by the third_rec table.
Trying to figure out the best (most efficient way) to go about this.
Any suggestions?
The above code will surely be missing proper variable definitions and such but it's just for explaining my dilemma!
I could probably do it with DLookup but I don't think that would be very efficient!
Pete
Best way was to build a query that produces a file filtered data from the Main and alt_db to group the activities by net_number . Then use a case to determine which fields from alt_db to update using the third file and update the result in the proper fields on the main db.

using criteria in an update query involving a join

I'm using MS Access
The SQL below updates the CurrNumTees field in the Parent tblContact records with the number of tblTorTee records that have an end date (which is not the ultimate effect I am aiming for, but I provide it as a starting point.
UPDATE tblContact
INNER JOIN tblTorTee ON tblContact.ContactId = tblTorTee.TorId
SET tblContact!CurNumTees = DCount("[tblTorTee.EndDate]",
"tbltortee","Torid = " & [ContactId]);
I need to update the CurrNumTees field with the number of records in tblTorTee that do not have an EndDate, in other words, that field is blank. I’ve tried using WHERE and HAVING and IS NULL in various combinations and locations, but without success. Could you help point me in the right direction?
The MS Access COUNT function does not count nulls, so I think you have to do this in two stages.
Firstly create a query like this:
SELECT TorId, IIF(ISNULL(EndDate),1,0) AS isN
FROM tblTorTee
WHERE EndDate IS NULL;
And save it as QryEndDateNull
Now you can run an Update Query like this:
UPDATE tblContact
SET tblContact.CurNumTees = DSUM("IsN","QryEndDateNull","TorId = " & [ContactID]);
Saving calculated data (data dependent on other data) is usually a bad design, especially aggregate data. Should just calculate when needed.
Did you try the IS NULL criteria within the DCount()?
UPDATE tblContact Set CurNumTees = DCount("*", "tblTorTee", "EndDate Is Null AND TorId = " & [ContactId]);

MS Access-Return Record Below Current Record

I am very new to Access, and what I am trying to do seems like it should be very simple, but I can't seem to get it.
I am a structural engineer by trade and am making a database to design buildings.
My Diaphragm Analysis Table includes the fields "Floor_Name", "Story_Number", "Wall_Left", and "Wall_Right". I want to write a new query that looks in another query called "Shear_Wall_incremental_Deflection" and pulls information from it based on input from Diaphragm Analysis. I want to take the value in "Wall_Right" (SW01), find the corresponding value in "Shear_Wall_incremental_Deflection", and report the "Elastic_Deflection" corresponding to the "Story_Below" instead of the "Story_Number" in the Diaphragm Analysis Table. In the case where "Story_Number" = 1, "Story_Below" will be 0 and I want the output to be 0.
Same procedure for "Wall_Left", but I'm just taking it one step at a time.
It seems that I need to use a "DLookup" in the expression builder with TWO criteria, one that Wall_Right = Shear_Wall and one that Story_Number = Story_Below, but when I try this I just get errors.
"Shear_Wall_incremental_Deflection" includes shearwalls for all three stories, i.e. it starts at SW01 and goes through SWW for Story Number 3 and then starts again at SW01 for Story Number 2, and so on until Story Number 1. I only show a part of the query results in the image, but rest assured, there are "Elastic_Deflection" values for story numbers below 3.
Here is my attempt in the Expression Builder:
Right_Defl_in: IIf(IsNull([Diaphragm_Analysis]![Wall_Right]),0,DLookUp("[Elastic_Deflection_in]","[Shear_Wall_incremental_Deflection]","[Shear_Wall_incremental_Deflection]![Story_Below]=" & [Diaphragm_Analysis]![Story_Number]))
I know my join from Diaphragm_Analysis "Wall_Left" and "Wall_Right" must include all records from Diaphragm_Analysis and only those from "Shear_Wall_incremental_Deflection"![Shear_Walls] where the joined fields are equal, but that's about all I know.
Please let me know if I need to include more information or send out the database file.
Thanks for your help.
Diaphragm Analysis (Input Table)
Shear_Wall_incremental_Deflection (Partial Image of Query)
I think what you are missing is that you can and should join to Diaphragm_Analysis twice, first time to get the Story_Below value and second to use it to get the corresponding Elastic_Deflection value.
To handle the special case where Story_Below is zero, I would write a separate query (only requires one join this time) and 'OR together' the two queries using the UNION set operation (note the following SQL is untested):
SELECT swid.Floor_Name,
swid.Story_Number,
swid.Wall_Left,
da2.Elastic_Deflection AS Story_Below_Elastic_Deflection
FROM ( Shear_Wall_incremental_Deflection swid
INNER JOIN Diaphragm_Analysis da1
ON da1.ShearWall = swid.Wall_Left )
INNER JOIN Diaphragm_Analysis da2
ON da2.ShearWall = swid.Wall_Left
AND da2.Story_Number = da1.Story_Below
UNION
SELECT swid.Floor_Name,
swid.Story_Number,
swid.Wall_Left,
0 AS Story_Below_Elastic_Deflection
FROM Shear_Wall_incremental_Deflection swid
INNER JOIN Diaphragm_Analysis da1
ON da1.ShearWall = swid.Wall_Left
WHERE da1.Story_Below = 0;
I've assumed that there is no data where Story_Number is zero.