Foreign keys… those practical rules that help maintain data integrity between parent and child tables, allow for table eliminations in queries beginning with Oracle release 10.2.0.1, and provide the potential for the optimizer to generate additional predicates during query optimization for better cardinality estimates.
Those benefits all sound like they might be helpful to someone, but what good are they for the average person (I mean the average developer)? Foreign keys are great for generating abstract art (note that I am a poor judge of art), with a bunch of odd writing all over the place.
This example demonstrates how to query an Excel spreadsheet as if it were a database. The results of the queries are used to build a graphical model of the relationships between the tables based on the foreign key relationships that are established in the database. Excel 2003 limits text boxes to no more than 255 characters, so use Excel 2007 or later for this example, if possible.
First, we need to build the source Excel spreadsheet that will act as the database that will be queried by the second Excel spreadsheet. The source Excel spreadsheet will have two worksheets (tabs) named “Data Dict Tables” and “Data Dict Foreign Keys”. Use the Microsoft Query Tool, or another approach to bring in the data from the Oracle data dictionary. The queries for each of those worksheets follows:
Data Dict Tables
SELECT DT.OWNER, DT.TABLE_NAME, DTC.COLUMN_NAME, DTC.DATA_TYPE, DTC.DATA_LENGTH, DTC.DATA_PRECISION, DTC.DATA_SCALE, DTC.NULLABLE, DTC.COLUMN_ID, DT.TABLESPACE_NAME, DTCC.COMMENTS TABLE_COMMENTS, SUBSTR(DCC.COMMENTS,1,255) COLUMN_COMMENTS FROM DBA_TABLES DT, DBA_TAB_COLUMNS DTC, DBA_TAB_COMMENTS DTCC, DBA_COL_COMMENTS DCC WHERE DT.OWNER=DTC.OWNER AND DT.TABLE_NAME=DTC.TABLE_NAME AND DT.OWNER=DTCC.OWNER(+) AND DT.TABLE_NAME=DTCC.TABLE_NAME(+) AND DTC.OWNER=DCC.OWNER(+) AND DTC.TABLE_NAME=DCC.TABLE_NAME(+) AND DTC.COLUMN_NAME=DCC.COLUMN_NAME(+) ORDER BY DT.OWNER, DT.TABLE_NAME, DTC.COLUMN_ID;
—
Data Dict Foreign Keys
SELECT DC2.OWNER, DC2.CONSTRAINT_NAME PKEY_CONTRAINT, DC1.CONSTRAINT_NAME FKEY_CONTRAINT, DCC2.TABLE_NAME PKEY_TABLE_NAME, DCC2.COLUMN_NAME PKEY_COLUMN_NAME, DCC1.TABLE_NAME FKEY_TABLE_NAME, DCC1.COLUMN_NAME FKEY_COLUMN_NAME, DCC1.POSITION FROM DBA_CONSTRAINTS DC1, DBA_CONSTRAINTS DC2, DBA_CONS_COLUMNS DCC1, DBA_CONS_COLUMNS DCC2 WHERE DC1.CONSTRAINT_TYPE='R' AND DC1.R_OWNER=DC2.OWNER AND DC1.R_CONSTRAINT_NAME=DC2.CONSTRAINT_NAME AND DC1.OWNER=DCC1.OWNER AND DC1.CONSTRAINT_NAME=DCC1.CONSTRAINT_NAME AND DC2.OWNER=DCC2.OWNER AND DC2.CONSTRAINT_NAME=DCC2.CONSTRAINT_NAME AND DCC1.POSITION=DCC2.POSITION AND DC1.OWNER='SYSADM' ORDER BY DC2.OWNER, DCC2.TABLE_NAME, DCC1.TABLE_NAME, DCC2.POSITION;
The source spreadsheet with the data dictionary details should be saved to the root of the C:\ drive with the name C:\Data Dictionary.xls
In a second spreadsheet we are trying to build the following interface:
Next, we need to add a couple ActiveX controls to the second spreadsheet:
B3: Combo Box with a name of cboTableName with a blank Text value
C3: Check Box with a name of chkAliasNames with a Caption of Alias Table Names
C1: Command Button with a name of cmdInitialize with a Caption of Initialize
A1, A3, B1 – type in the text as shown above
Once we add the macro code, clicking the Initialize button will send a query to the other spreadsheet to retrieve a list of the tables:
And with all of the code in place, selecting a table builds the abstract art:
Running down the left side is the list of columns in the selected table. Every foreign key that is defined against the selected table will trigger a recursive lookup for child tables of the child table, with the necessary join conditions listed as well as the columns that are defined in the child table. For instance, there is a foreign key on the CO_PRODUCT table that points back to the primary key on the PART table, and the join between the tables should be P.ID = CP.PART_ID. The CO_PRODUCT table does not have any child tables.
Skipping down to the CUST_ORDER_LINE table, it has a foreign key that references the PART table on P.ID = COL.PART_ID. The RECEIVABLE_LINE table has a foreign key that references the CUST_ORDER_LINE table on COL.CUST_ORDER_ID = RL.CUST_ORDER_ID AND COL.LINE_NO = RL.CUST_ORDER_LINE_NO. The RECV_LINE_BINARY table has a foreign key that references the RECEIVABLE_LINE table on RL.INVOICE_ID = RLB.INVOICE_ID AND RL.LINE_NO = RLB.RECV_LINE_NO. With this tool we just discovered a way to join the PART table to the RECV_LINE_BINARY by analyzing the defined foreign keys.
OK, enough fun, lets enter the code. Right-click the worksheet name (probably Sheet1) rename the sheet to ExcelQueryOfExcel and then right-click the sheet and to select View Code. Let’s start with making the Initialize button work:
'Need to add a reference to Microsoft ActiveX Data Objects 2.8 Library before starting 'Declare a connection object in the general section to hold the connection to the database Dim dbExcel As New ADODB.Connection 'Declare a set of variables to hold the username and password for the database Dim strUserName As String Dim strPassword As String Dim strDatabase As String Private Function ConnectDatabase() As Integer Dim intResult As Integer On Error Resume Next If dbExcel.State <> 1 Then 'Connection to the database if closed strDatabase = Sheets("ExcelQueryOfExcel").Cells(1, 2).Value strUserName = "" strPassword = "" 'Connect to the database dbExcel.ConnectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & strDatabase & ";Extended Properties=""Excel 8.0;HDR=Yes;IMEX=1"";" dbExcel.ConnectionTimeout = 40 dbExcel.CursorLocation = adUseClient dbExcel.Open If (dbExcel.State <> 1) Or (Err <> 0) Then intResult = MsgBox("Could not connect to the Excel Database." & vbCrLf & Error(Err), 16, "Excel Demo") ConnectDatabase = False Else ConnectDatabase = True End If Else ConnectDatabase = True End If End Function Private Sub cmdInitialize_Click() Dim intResult As Integer Dim strSQL As String Dim strLastTable As String Dim snpData As ADODB.Recordset On Error Resume Next cboTableName.Clear cboTableName = "" Sheets("ExcelQueryOfExcel").Range("A6:Z10006").Delete Shift:=xlUp intResult = ConnectDatabase strLastTable = "" If intResult = True Then Set snpData = New ADODB.Recordset strSQL = "SELECT" & vbCrLf strSQL = strSQL & " *" & vbCrLf strSQL = strSQL & "FROM" & vbCrLf strSQL = strSQL & " [Data Dict Tables$]" & vbCrLf strSQL = strSQL & "ORDER BY" & vbCrLf strSQL = strSQL & " 2" ' Sort the rows by the second column in the data source snpData.Open strSQL, dbExcel If snpData.State = 1 Then Do While Not snpData.EOF If strLastTable <> snpData("table name") Then strLastTable = snpData("table name") cboTableName.AddItem snpData("table name") End If snpData.MoveNext Loop snpData.Close Else intResult = MsgBox("Could not send the SQL statement to the Excel Database." & vbCrLf & Error(Err), 16, "Excel Demo") End If Set snpData = Nothing End If End Sub
The above code should be sufficient for the Initialize button to work – you may need to turn off Design Mode in Excel for the button to work.
Now we need to work on the recursion when a table is selected (and the rest of the code):
Private Function FindRelatedTables(strTableName As String, lngColumn As Long, lngRow As Long, intAliasTableNames As Integer) As Long Dim i As Integer Dim lngBoxLeft As Long Dim lngBoxTop As Long Dim lngBoxWidth As Long Dim lngBoxHeight As Long Dim lngResult As Long Dim intTableColumns As Integer Dim intForeignColumns As Integer Dim intFlag As Integer Dim strOut As String Dim strLastTable As String Dim strSplit() As String 'Use to alias the table names Dim strPrimary As String 'Stores the aliased table primary key table name Dim strForeign As String 'Stores the aliased table foreign key table name Dim strSQL As String Dim snpData As ADODB.Recordset Dim snpDataForeign As ADODB.Recordset On Error Resume Next If intAliasTableNames = True Then strPrimary = "" strSplit = Split(strTableName, "_") For i = 0 To UBound(strSplit) strPrimary = strPrimary & Left(strSplit(i), 1) Next i End If Set snpDataForeign = New ADODB.Recordset Set snpData = New ADODB.Recordset strSQL = "SELECT" & vbCrLf strSQL = strSQL & " *" & vbCrLf strSQL = strSQL & "FROM" & vbCrLf strSQL = strSQL & " [Data Dict Foreign Keys$]" & vbCrLf strSQL = strSQL & "WHERE" & vbCrLf strSQL = strSQL & " [Pkey Table Name] = '" & strTableName & "'" & vbCrLf strSQL = strSQL & "ORDER BY" & vbCrLf strSQL = strSQL & " [Fkey Table Name]," & vbCrLf strSQL = strSQL & " [Position]" snpDataForeign.Open strSQL, dbExcel If snpDataForeign.State = 1 Then If Not snpDataForeign.EOF Then strOut = strTableName & " " & strPrimary & " :" & vbLf intTableColumns = 0 intForeignColumns = 0 strLastTable = snpDataForeign("Fkey Table Name") Do While Not snpDataForeign.EOF If intAliasTableNames = True Then strForeign = "" strSplit = Split(snpDataForeign("Fkey Table Name"), "_") For i = 0 To UBound(strSplit) strForeign = strForeign & Left(strSplit(i), 1) Next i 'Verify that the two aliases are not identical If strForeign = strPrimary Then strForeign = strForeign & "F" End If strOut = strOut & strPrimary & "." & snpDataForeign("Pkey Column Name") & " = " & strForeign & "." & snpDataForeign("Fkey Column Name") & vbLf Else strOut = strOut & snpDataForeign("Pkey Table Name") & "." & snpDataForeign("Pkey Column Name") & " = " & snpDataForeign("Fkey Table Name") & "." & snpDataForeign("Fkey Column Name") & vbLf End If intForeignColumns = intForeignColumns + 1 snpDataForeign.MoveNext 'See if we need to retrieve the table columns for the previous table intFlag = False If snpDataForeign.EOF Then intFlag = True Else If strLastTable <> snpDataForeign("Fkey Table Name") Then intFlag = True End If End If If intFlag = True Then strSQL = "SELECT" & vbCrLf strSQL = strSQL & " *" & vbCrLf strSQL = strSQL & "FROM" & vbCrLf strSQL = strSQL & " [Data Dict Tables$]" & vbCrLf strSQL = strSQL & "WHERE" & vbCrLf strSQL = strSQL & " [Table Name] = '" & strLastTable & "'" & vbCrLf strSQL = strSQL & "ORDER BY" & vbCrLf strSQL = strSQL & " [Column Id]" ' Sort the rows by the second column in the data source snpData.Open strSQL, dbExcel If snpData.State = 1 Then If Not snpData.EOF Then strOut = strOut & vbLf & strLastTable & " " & strForeign & " :" & vbLf intTableColumns = 0 Do While Not snpData.EOF strOut = strOut & snpData("column name") & vbLf intTableColumns = intTableColumns + 1 snpData.MoveNext Loop End If End If snpData.Close 'Create the text box 'Strip off the trailing CrLf strOut = Left(strOut, Len(strOut) - 1) lngBoxLeft = lngColumn * 200 lngBoxWidth = 200 lngBoxTop = lngRow * 20 lngBoxHeight = (intTableColumns + intForeignColumns + 3) * 5.5 With Sheets("ExcelQueryOfExcel").Shapes.AddTextbox(msoTextOrientationHorizontal, lngBoxLeft, lngBoxTop, lngBoxWidth, (lngBoxHeight * 2 - 1)) With .TextFrame If Application.Version <= 11 Then 'There is a 255 character limit on text boxes before Office 2007 .Characters.Text = Left(strOut, 255) Else .Characters.Text = strOut End If .Characters.Font.Size = 8 End With .Fill.ForeColor.RGB = RGB(255 - lngColumn * 10, 255 - lngColumn * 10, 255) End With 'With Sheets("ExcelQueryOfExcel").Shapes.AddTextbox(msoTextOrientationHorizontal, lngBoxLeft, lngBoxTop, lngBoxWidth, (lngBoxHeight * 2 - 1)) ' With .TextFrame ' .Characters.Text = strOut ' .Characters.Font.Size = 8 ' End With ' .Fill.ForeColor.RGB = RGB(255 - lngColumn * 10, 255 - lngColumn * 10, 255) 'End With If lngColumn < 6 Then 'Only recursively call to 6 levels 'Recursive call into this function lngResult = FindRelatedTables(strLastTable, lngColumn + 1, lngRow, intAliasTableNames) End If 'Prepare for the next text box strOut = strTableName & " " & strPrimary & " :" & vbLf intTableColumns = 0 intForeignColumns = 0 lngRow = lngRow + 3 End If If Not snpDataForeign.EOF Then strLastTable = snpDataForeign("Fkey Table Name") End If If lngRow > 5000 Then 'Protection against a very long wait Exit Do End If Loop End If snpDataForeign.Close End If Set snpDataForeign = Nothing Set snpData = Nothing End Function Private Sub FindTable() Dim intAliasTableNames As Integer Dim intResult As Integer Dim lngResult As Long Dim lngRow As Long Dim strSQL As String Dim strTableName As String Dim snpData As ADODB.Recordset On Error Resume Next intAliasTableNames = chkAliasNames.Value lngRow = 6 Sheets("ExcelQueryOfExcel").Range("A" & Format(lngRow) & ":Z" & Format(lngRow + 10000)).Delete Shift:=xlUp If cboTableName <> "" Then Application.ScreenUpdating = False strTableName = cboTableName Set snpData = New ADODB.Recordset strSQL = "SELECT" & vbCrLf strSQL = strSQL & " *" & vbCrLf strSQL = strSQL & "FROM" & vbCrLf strSQL = strSQL & " [Data Dict Tables$]" & vbCrLf strSQL = strSQL & "WHERE" & vbCrLf strSQL = strSQL & " [Table Name] = '" & strTableName & "'" & vbCrLf strSQL = strSQL & "ORDER BY" & vbCrLf strSQL = strSQL & " [Column Id]" ' Sort the rows by the second column in the data source snpData.Open strSQL, dbExcel If snpData.State = 1 Then Sheets("ExcelQueryOfExcel").Cells(lngRow, 1).Value = strTableName Sheets("ExcelQueryOfExcel").Cells(lngRow, 1).Font.Bold = True lngRow = lngRow + 1 If Not snpData.EOF Then Do While Not snpData.EOF Sheets("ExcelQueryOfExcel").Cells(lngRow, 1).Value = snpData("column name") lngRow = lngRow + 1 snpData.MoveNext 'Safety net If lngRow > 1000 Then Exit Do End If Loop lngResult = FindRelatedTables(strTableName, 1, 5, intAliasTableNames) End If snpData.Close Else intResult = MsgBox("Could not send the SQL statement to the Excel Database." & vbCrLf & Error(Err), 16, "Excel Demo") End If Set snpData = Nothing End If Application.ScreenUpdating = True End Sub Private Sub cboTableName_Click() FindTable End Sub Private Sub cboTableName_GotFocus() cboTableName.SelStart = 0 cboTableName.SelLength = Len(cboTableName) End Sub Private Sub cboTableName_KeyUp(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer) If (KeyCode = 13) Or (KeyCode = 8 ) Then FindTable End If End Sub Private Sub chkAliasNames_Click() FindTable End Sub
I will not try here to explain how all of the above code works. I created this demonstration for a presentation that I gave a couple months ago. It really should be easy to understand if you step through the code starting with the cboTableName_Click Sub.
Recent Comments