January 16, 2010
This is a simple example that shows how to query an Oracle database using user input, passing in the user specified values with bind variables. While this example just queries V$SESSION, it is possible to expand this demonstration considerably to allow Excel to act as a command center for viewing SQL statements executed by sessions (with their execution plans), enable 10046 traces, and more.
To begin, we need to create two ActiveX command buttons in cells A1 through A3. Name the top command button cmdInitialize, and the bottom button cmdFind.
Next, name the worksheet as DatabaseInfo, then right-click the worksheet DatabaseInfo tab name and select View Code. Once in the Visual Basic editor, add a reference to the Microsoft ActiveX Data Objects, as demonstrated here.
Now, we need to add the code to make the cmdInitialize button work:
Option Explicit 'Forces all variables to be declared Dim dbDatabase As New ADODB.Connection Dim strDatabase As String Dim strUserName As String Dim strPassword As String Dim intColumns As Integer Dim strLastColumn As String Private Function ConnectDatabase() As Integer Dim intResult As Integer On Error Resume Next If dbDatabase.State <> 1 Then 'Connection to the database if closed strDatabase = "MyDB" strUserName = "MyUser" strPassword = "MyPassword" 'Connect to the database 'Oracle connection string dbDatabase.ConnectionString = "Provider=OraOLEDB.Oracle;Data Source=" & strDatabase & ";User ID=" & strUserName & ";Password=" & strPassword & ";ChunkSize=1000;FetchSize=100;" dbDatabase.ConnectionTimeout = 40 dbDatabase.CursorLocation = adUseClient dbDatabase.Open If (dbDatabase.State <> 1) Or (Err <> 0) Then intResult = MsgBox("Could not connect to the database. Check your user name and password." & 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 i As Integer Dim intResult As Integer Dim strSQL As String Dim snpData As ADODB.Recordset 'Don't allow Excel to display an error message on the screen if an error happens while executing this ' procedure, we will handle the problem in-line in the code On Error Resume Next 'Jump to our ConnectDatabase function which returns a value of True if we are connected to the database ' or False if the connection attempt failed intResult = ConnectDatabase 'If we could not connect to the database, display a message for the user that something is wrong and stop ' the execution of the code in this module If intResult = False Then Exit Sub End If 'Create the ADO object which will be used to retrieve the data from the database Set snpData = New ADODB.Recordset strSQL = "SELECT" & vbCrLf strSQL = strSQL & " *" & vbCrLf 'Retrieve all columns from the table without listing the columns strSQL = strSQL & "FROM" & vbCrLf strSQL = strSQL & " V$SESSION" & vbCrLf strSQL = strSQL & "WHERE" & vbCrLf 'ROWNUM is an Oracle only function - each row returned is assigned an increasing sequential value, ' essentially, I am telling Oracle to not retrieve any rows, as I am just interested in the column name ' and the data types in the PART table. Using 0=1 in place of ROWNUM<1 will likely work on other database ' platforms. strSQL = strSQL & " ROWNUM<1" 'Pass the SQL statement into our database connection to return the matching rows snpData.Open strSQL, dbDatabase 'Always verify that the SQL statement was able to be executed, and that the database server did not simply ' return an error message. Failing to perform the check could result in a situation where the macro ' becomes stuck in an infinite loop. State = 1 indicates that the SQL statement was executed, and that ' the recordset is available for use, but does not necessarily mean that there are any rows in the ' recordset If snpData.State = 1 Then For i = 0 To snpData.Fields.Count - 1 'Let's try to determine the type of data that may be stored in the database column and output that ' to the Excel spreadsheet. Doing so will help the user, and it will help the cmdFind_Click ' procedure determine how the bind variables should be set up Select Case snpData.Fields(i).Type Case adVarChar 'A string of characters ActiveSheet.Cells(1, i + 2).Value = "String" Case adChar 'A fixed length string of characters, where values are padded with spaces as needed ActiveSheet.Cells(1, i + 2).Value = "Character" Case adDate, 135 'A column which may contain date and time information ActiveSheet.Cells(1, i + 2).Value = "Date" Case adNumeric, adSingle, adInteger, adDouble, 139 'A column which may contain integers, floating point numbers, but not imaginary numbers ActiveSheet.Cells(1, i + 2).Value = "Number" Case Else 'What should we do with these types of columns, are they BLOBs, RAWs? ActiveSheet.Cells(1, i + 2).Value = snpData.Fields(i).Type End Select 'Output the name of the column on the second row in the spreadsheet ActiveSheet.Cells(2, i + 2).Value = snpData.Fields(i).Name 'Blank out the third row in the spreadsheet so that the user may specify how the rows returned ' by the SQL statement should be restricted on that row ActiveSheet.Cells(3, i + 2).Value = "" Next i 'Record the number of columns in the PART table for future reference intColumns = snpData.Fields.Count 'Just for fun, output to the Debug window (View menu - Immediate Window the Excel column names ' for the 26th through the 100th columns in the spreadsheet just to make certain that our interesting ' looking formula below is working correctly For i = 26 To 100 Debug.Print i, Chr(64 + Int((i + 2) / 26)); Chr(64 + ((i + 2) Mod 26 + 1)) Next i 'Chr returns the character represented by the ASCII/ANSI value specified. An uppercase letter A has ' an ASCII/ANSI value of 65, so the first column based on the formula would be Chr(64 + 1) = A 'Mod is a function which returns the remainder after a number is divided by another number ' 28 Mod 26 would equal 2 as 28/26 = 1 with a remainder of 2, are to be mathematically fancy: ' (28 / 26 - Int(28 / 26)) * 26 ' Thus Mod produces a repeating sequence from 0 to one less than the number following the word Mod strLastColumn = Chr(64 + Int((intColumns + 2) / 26)) & Chr(64 + ((intColumns + 2) Mod 26 + 1)) 'Close the ADO recordset to free up memory on the database server since we are done using the data snpData.Close 'Make certain that the full column names of the various columns in the PART table are visible in the ' spreadsheet ActiveSheet.Columns("A:" & strLastColumn).AutoFit End If 'Erase any rows in the spreadsheet that may have been left by a previous execution of the cmdFind code Worksheets("DatabaseInfo").Range("4:50000").Delete Shift:=xlUp 'Remove the ADO recordset object that we created earlier from memory - in theory this happens automatically ' but it is good practice to explicitly perform the operation Set snpData = Nothing End Sub
Now, switch back to the Excel window to verify that the cmdInitialize button works correctly. You should see something like this:
Notice that row 1 in the worksheet shows the data type of the columns in the V$SESSION view (204 is a RAW data type) and row 2 in the worksheet shows the column names. Row 3 will be used to allow the user to restrict the rows that will be returned. Next, we need to add the code to the cmdFind button. Switch back to the Visual Basic editor and add the following code:
Private Sub cmdFind_Click() Dim i As Integer Dim lngRow As Long Dim strSQL As String Dim snpData As ADODB.Recordset Dim comData As ADODB.Command 'Create the in-memory ADO objects that will be used to return the data from the PART table Set snpData = New ADODB.Recordset Set comData = New ADODB.Command With comData strSQL = "SELECT" & vbCrLf strSQL = strSQL & " *" & vbCrLf strSQL = strSQL & "FROM" & vbCrLf strSQL = strSQL & " V$SESSION" & vbCrLf strSQL = strSQL & "WHERE" & vbCrLf strSQL = strSQL & " 1=1" & vbCrLf 'Walk through the columns to determine which have restrictions placed on them by the user For i = 1 To intColumns If ActiveSheet.Cells(3, i).Value <> "" Then 'The user placed a striction on this column If (InStr(ActiveSheet.Cells(3, i).Value, "%") > 0) Or (InStr(ActiveSheet.Cells(3, i).Value, "_") > 0) Then 'Partial match, the column name is in row 2 of the spreadsheet strSQL = strSQL & " AND " & ActiveSheet.Cells(2, i).Value & " LIKE ?" & vbCrLf 'We need to look in row 1 for the data type of the column and set up an appropriate bind ' variable data type to pass in the restriction requested by the user 'Each bind variable must have a unique name, so we generate one as ValueCol# Select Case ActiveSheet.Cells(1, i).Value Case "String" .Parameters.Append .CreateParameter("value" & Format(i), adVarChar, adParamInput, Len(ActiveSheet.Cells(3, i).Value), ActiveSheet.Cells(3, i).Value) Case "Character" .Parameters.Append .CreateParameter("value" & Format(i), adChar, adParamInput, Len(ActiveSheet.Cells(3, i).Value), ActiveSheet.Cells(3, i).Value) Case "Number" 'A partial match on a number is not possible, just including to see what happens .Parameters.Append .CreateParameter("value" & Format(i), adNumeric, adParamInput, 12, ActiveSheet.Cells(3, i).Value) Case "Date" 'A partial match on a date is not possible, just including to see what happens .Parameters.Append .CreateParameter("value" & Format(i), adDate, adParamInput, 8, CDate(ActiveSheet.Cells(3, i).Value)) End Select Else 'Full match, the column name is in row 2 of the spreadsheet strSQL = strSQL & " AND " & ActiveSheet.Cells(2, i).Value & " = ?" & vbCrLf 'We need to look in row 1 for the data type of the column and set up an appropriate bind ' variable data type to pass in the restriction requested by the user 'Each bind variable must have a unique name, so we generate one as ValueCol# Select Case ActiveSheet.Cells(1, i).Value Case "String" .Parameters.Append .CreateParameter("value" & Format(i), adVarChar, adParamInput, Len(ActiveSheet.Cells(3, i).Value), ActiveSheet.Cells(3, i).Value) Case "Character" .Parameters.Append .CreateParameter("value" & Format(i), adChar, adParamInput, Len(ActiveSheet.Cells(3, i).Value), ActiveSheet.Cells(3, i).Value) Case "Number" .Parameters.Append .CreateParameter("value" & Format(i), adNumeric, adParamInput, 12, ActiveSheet.Cells(3, i).Value) Case "Date" .Parameters.Append .CreateParameter("value" & Format(i), adDate, adParamInput, 8, CDate(ActiveSheet.Cells(3, i).Value)) End Select End If End If Next i 'We will sort the rows by the part ID strSQL = strSQL & "ORDER BY" & vbCrLf strSQL = strSQL & " SID" 'Set up the command properties .CommandText = strSQL .CommandType = adCmdText .CommandTimeout = 30 .ActiveConnection = dbDatabase End With Set snpData = comData.Execute lngRow = 3 'We will start outputting at row 4, so 3 is our "0" line - the starting point 'The slow way to populate the cells ' If Not (snpData Is Nothing) Then ' Do While Not snpData.EOF ' 'Increase the row number so that we do not output all of the information on the same row of the ' ' spreadsheet ' lngRow = lngRow + 1 ' 'Output the data returned by the SQL statement, one column at a time. The first column is in the ' ' 0 position, and the last column is one less than the total number of columns returned ' For i = 0 To snpData.Fields.Count - 1 ' ActiveSheet.Cells(lngRow, i + 2).Value = snpData.Fields(i) ' Next i ' snpData.MoveNext ' Loop ' ' snpData.Close ' End If ' ' 'Do we have extra rows left over from the last run? If so, delete all rows below the last row that we output ' Worksheets("DatabaseInfo").Range(Format(lngRow + 1) & ":50000").Delete Shift:=xlUp 'The fast way to place the query results into cells Worksheets("DatabaseInfo").Range(Format(lngRow + 1) & ":50000").Delete Shift:=xlUp If Not (snpData Is Nothing) Then ActiveSheet.Range("B4").CopyFromRecordset snpData ActiveSheet.Range("B4").Select snpData.Close End If 'Tell Excel to fix the column widths so that all of the data returned in each column is visible 'We recorded the value of strLastColumn in the initialize procedure ActiveSheet.Columns("B:" & strLastColumn).AutoFit 'Memory clean up Set snpData = Nothing Set comData = Nothing End Sub
Switch back to the Excel worksheet and test the cmdFind button. You should see something like this:
Next, try to enter a search keyword in row 3 – if a wildcard character ( % or _ ) is used, the query will use a LIKE keyword, rather than an = operator. After entering the search criteria, click the Find button:
There is no need to stop at this point. It is easy to add a UserForm to the Excel workbook, for example something like this from another demonstration:
For example, to enable a trace for a session, you could create a function like this:
Sub SetTraceInSession(lngSID As Long, lngSerial As Long, lngTrace As Long, lngTraceLevel As Long) Dim cmdTrace As New ADODB.Command Dim strSQL As String On Error Resume Next With cmdTrace strSQL = "SYS.DBMS_SYSTEM.SET_EV(" & Format(lngSID) & "," & Format(lngSerial) & "," & Format(lngTrace) & "," & Format(lngTraceLevel) & ",'')" .CommandText = strSQL .CommandType = adCmdStoredProc .ActiveConnection = dbDatabase End With cmdTrace.Execute Set cmdTrace = Nothing End Sub
Excellent. Do you have this excel template ? Is it possible for you to share this template to anand.jagat@gmail.com
Thanks in advance.
I no longer have a copy of the spreadsheet that was built when this blog article was created. The same spreadsheet may be created, even in Excel 2016, by turning on the Developer toolbar in Excel, and following the directions in this article. The WMI window screen capture shown above is from a spreadsheet that may be downloaded from a link in the following blog article:
https://hoopercharles.wordpress.com/2016/11/02/huge-presentation-working-with-oracle-database-in-c-vbscript-and-excel-enhancing-visual-manufacturing-8-0-0/