PowerShell sql query to CSV to Excel Workbook - excel

-Apologies for the back and forth question!
I pieced together the following PowerShell script which runs two SQL queries, exports each query to a CSV file then moves the CSV files into an Excel workbook.
The code works as expected when the two CSV files are already created. But the script fails when it is run the first time when the CSV files get created.
Function Run-Query {
param([string[]]$queries,[string[]]$sheetnames,[string[]]$filenames)
$Excel = New-Object -ComObject Excel.Application
$Excel.Visible = 0
$dest = $Excel.Workbooks.Add(1)
for ($i = 0; $i -lt $queries.Count; $i++){
$query = $queries[$i]
$sheetname = $sheetnames[$i]
$filename = $filenames[$i]
### SQL query results sent to Excel
$SQLServer = 'Server'
$Database = 'Database'
## - Connect to SQL Server using non-SMO class 'System.Data':
$SqlConnection = New-Object System.Data.SqlClient.SqlConnection
$SqlConnection.ConnectionString = "Server = $SQLServer; Database = $Database; Integrated Security = True"
$SqlCmd = New-Object System.Data.SqlClient.SqlCommand
$SqlCmd.CommandText = $query
$SqlCmd.Connection = $SqlConnection
## - Extract and build the SQL data object '$Table2':
$SqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter
$SqlAdapter.SelectCommand = $SqlCmd
$DataSet = New-Object System.Data.DataSet
$SqlAdapter.Fill($DataSet)
$SqlConnection.Close()
$DataSet.Tables[0] | Export-Csv -NoTypeInformation -Path "C:\Scripts\Organize\ExcelStuff\$sheetname.csv"
}#End For.
#Begin excel test, loop over each CSV.
$loopy = (Resolve-Path $filename).ProviderPath
$Book = $Excel.Workbooks.Open($loopy)
foreach ($item in $loopy){
$next = $Excel.workbooks.Open($item)
$next.ActiveSheet.Move($dest.ActiveSheet)
$xlsRng = $dest.ActiveSheet.UsedRange
$xlsRng.EntireColumn.AutoFit() | Out-Null
}# END ForEach
#$Excel.Visible = 1 #For debugging.
$dest.sheets.item('Sheet1').Delete()
$xlsFile = "C:\Scripts\MonthlyReboots.xlsx"
$Excel.ActiveWorkbook.SaveAs($xlsFile) | Out-Null
$Excel.Quit()
While ([System.Runtime.Interopservices.Marshal]::ReleaseComObject($xlsRng)) {'cleanup xlsRng'}
While ([System.Runtime.Interopservices.Marshal]::ReleaseComObject($next)) {'cleanup xlsSh'}
While ([System.Runtime.Interopservices.Marshal]::ReleaseComObject($Book)) {'cleanup xlsWb'}
While ([System.Runtime.Interopservices.Marshal]::ReleaseComObject($Excel)) {'cleanup xlsObj'}
[gc]::collect() | Out-Null
[gc]::WaitForPendingFinalizers() | Out-Null
}#End Function
$queries = #()
$queries += #'
'#
$queries += #'
'#
$sheetnames = #('Cert','Prod')
$filenames = #(".\prod.csv", ".\cert.csv")
Run-Query -queries $queries -sheetnames $sheetnames -filenames $filenames

Ok, we've got a few lessons to work with here I think. First, functions, what they should do, and what they shouldn't do, and structure. Later we'll touch on organizing your script so that it runs a bit more optimally.
So let's look at that massive function you've got there. That's a lot of stuff in there, and I'm willing to bet that it probably shouldn't all be in there. What is in there will benefit from using the Begin, Process, and End scriptblock sections. For the time being, we're going to ignore Excel, and have the function actually just work with your SQL queries. Right now your function (remember, ignoring Excel for the time being) takes a collection of strings for queries, connects to the SQL server, runs a query, disconnects from the server, reconnects to the server, runs a query, disconnects from the server, and keeps doing that until it runs out of queries. I think a better option would be to use the Begin scriptblock to connect to the server once, then the Process scriptblock to run each query, and the End block to close the connection and return the query results. That stops us from having to open and close the connection a bunch, and keeps the function focused on doing one thing, but doing it well.
Function Run-Query {
param([string[]]$queries)
Begin{
$SQLServer = 'Server'
$Database = 'Database'
## - Connect to SQL Server using non-SMO class 'System.Data':
$SqlConnection = New-Object System.Data.SqlClient.SqlConnection
$SqlConnection.ConnectionString = "Server = $SQLServer; Database = $Database; Integrated Security = True"
}
Process{
$SqlCmd = New-Object System.Data.SqlClient.SqlCommand
$SqlCmd.CommandText = $queries
$SqlCmd.Connection = $SqlConnection
## - Extract and build the SQL data object '$Table2':
$SqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter
$SqlAdapter.SelectCommand = $SqlCmd
$DataSet = New-Object System.Data.DataSet
$SqlAdapter.Fill($DataSet)
$DataSet.Tables[0]
}
End{
$SqlConnection.Close()
}
}#End Run-Query Function
That will put out an array of objects for however many queries you feed it. So then we just assign a variable to that and we have two datasets in an array. That part is simple:
#Define Queries
$Queries = #()
$Queries += #'
Select * From TableA;
Where Stuff = 'Cert'
'#
$Queries += #'
Select * From TableB;
Where Stuff = 'Prod'
'#
#Get data from SQL
$Data = Run-Query -queries $Queries
Now that we have our datasets we will launch Excel, create a new workbook, name the sheet it starts with, make a second sheet and name that, then just paste the data directly into Excel. There is no reason to export to CSV files, load them into Excel, and copy the data around within Excel when we can just paste the data directly into Excel.
#Launch Excel and add a workbook
$Excel = New-Object -ComObject Excel.Application
$Workbook = $Excel.Workbooks.Add()
#Set the current worksheet at Cert, and add a new one as Prod, then name them appropriately
$Cert = $Workbook.ActiveSheet
$Prod = $Workbook.Worksheets.Add()
$Cert.Name = 'Cert'
$Prod.Name = 'Prod'
#Copy the data from the first query to the clipboard as a tab delimited CSV, then paste it into the Cert sheet
$Data[0] | ConvertTo-Csv -notype -Delimiter "`t" | Clip
[Void]$Cert.Cells.Item(1).PasteSpecial()
#Do the same with the second query and paste it into the Prod sheet
$Data[1] | ConvertTo-Csv -notype -Delimiter "`t" | Clip
[Void]$Prod.Cells.Item(1).PasteSpecial()
You should now have an open workbook with two sheets, each containing the results of one SQL query. Now to just perform the autofit to make it look nice, save the workbook, close it, exit Excel, and perform garbage collection...
#Autofit the columns to make it all look nice
$Prod.UsedRange.EntireColumn.AutoFit()
$Cert.UsedRange.EntireColumn.AutoFit()
#Save the workbook
$Workbook.SaveAs("C:\Scripts\MonthlyReboots.xlsx")
#Close the worbook, and Excel
$Workbook.Close()
$Excel.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($Excel)|Out-Null
[gc]::collect() | Out-Null
[gc]::WaitForPendingFinalizers() | Out-Null
That should do it. No more opening Excel a whole bunch and working with a bunch of files, the SQL connection just gets opened once, and closed once, with queries performed during the session. If the script takes a long time to run at this point I'd be willing to bet it's the SQL queries that are taking the bulk of the time because once you have the data out of SQL bringing up Excel, and getting the data into the sheets should be really fast.
Edit: Well, it sounds like you aren't getting back results from all of the queries that you are submitting, so I have restructured the function a little and hopefully this will work better.
Function Run-Query {
param([string[]]$queries)
Begin{
$SQLServer = 'Server'
$Database = 'Database'
$Results = #()
}
Process{
## - Connect to SQL Server using non-SMO class 'System.Data':
$SqlConnection = New-Object System.Data.SqlClient.SqlConnection
$SqlConnection.ConnectionString = "Server = $SQLServer; Database = $Database; Integrated Security = True"
$SqlCmd = New-Object System.Data.SqlClient.SqlCommand
$SqlCmd.CommandText = $queries
$SqlCmd.Connection = $SqlConnection
## - Extract and build the SQL data object '$Table2':
$SqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter
$SqlAdapter.SelectCommand = $SqlCmd
$DataSet = New-Object System.Data.DataSet
$SqlAdapter.Fill($DataSet)
$Results += $DataSet.Tables[0]
}
End{
$SqlConnection.Close()
$Results
}
}#End Run-Query Function
If it doesn't, you could always go back to your old way of doing things, and instead of outputting to CSV files you can start pasting to Excel directly like I've shown you how to do. That should speed things up at the least. Like, open Excel, run the old function (except take out the part that opens Excel), and have the old function paste into sheets in Excel.
I do wish I had a SQL server I could test against. Everything should have worked as far as I could tell, but obviously didn't work like I had anticipated.

Major thanks given to TheMadTechnician for the guidance on using a function.
Here is what I've cobbled together which does work and it creates an Excel file with two worksheets in under 2 seconds. Additionally, the code correctly cleans up the Excel ComObject I'm boasting here but I'd love to see someone come up with a faster way of accomplising this!
Function Run-Query {
param([string[]]$queries,[string[]]$sheetnames,[string[]]$filenames)
Begin{
$SQLServer = 'ServerName'
$Database = 'DataBase'
$SqlConnection = New-Object System.Data.SqlClient.SqlConnection
$SqlConnection.ConnectionString = "Server = $SQLServer; Database = $Database; Integrated Security = True"
$Excel = New-Object -ComObject Excel.Application
$Excel.Visible = 0
$dest = $Excel.Workbooks.Add(1)
}#End Begin
Process{
For($i = 0; $i -lt $queries.Count; $i++){
$SqlCmd = New-Object System.Data.SqlClient.SqlCommand
$SqlCmd.CommandText = $queries[$i]
$SqlCmd.Connection = $SqlConnection
$SqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter
$SqlAdapter.SelectCommand = $SqlCmd
$DataSet = New-Object System.Data.DataSet
$SqlAdapter.Fill($DataSet)
$DataSet.Tables[0] | Export-Csv -NoTypeInformation -Path "C:\Scripts\$($sheetnames[$i]).csv" -Force
}#end for loop.
}#End Process
End{
$SqlConnection.Close()
#Excel magic test!
For($i = 0; $i -lt $queries.Count; $i++){
$loopy = (Resolve-Path -Path $filenames[$i]).ProviderPath
$Book = $Excel.Workbooks.Open($loopy)
$next = $Excel.workbooks.Open($loopy)
$next.ActiveSheet.Move($dest.ActiveSheet)
$xlsRng = $dest.ActiveSheet.UsedRange
$xlsRng.EntireColumn.AutoFit() | Out-Null
}
$dest.sheets.item('Sheet1').Delete()
$xlsFile = "C:\Scripts\MonthlyReboots.xlsx"
[void] $Excel.ActiveWorkbook.SaveAs($xlsFile)
$Excel.Quit()
While ([System.Runtime.Interopservices.Marshal]::ReleaseComObject($xlsRng)) {'cleanup xlsRng'}
While ([System.Runtime.Interopservices.Marshal]::ReleaseComObject($next)) {'cleanup xlsSh'}
While ([System.Runtime.Interopservices.Marshal]::ReleaseComObject($Book)) {'cleanup xlsWb'}
While ([System.Runtime.Interopservices.Marshal]::ReleaseComObject($Excel)) {'cleanup xlsObj'}
[gc]::collect() | Out-Null
[gc]::WaitForPendingFinalizers() | Out-Null
}#End end block.
}#End function run-query.

Related

Powershell - Excel SaveAs csv with specified delimiter

Afternoon all,
Is it possible to save a CSV file using Powershell with a different delimiter, in my case "§". I am using the following script to open and change items in an XLSX file and then wish to save as a "§" delimited CSV. The find and replace method does not work in my case ( (Get-Content -Path $CSVfile).Replace(',','§') | Set-Content -Path $CSVfile2)
$Path = "C:\ScriptRepository\CQC\DataToLoad\"
$FileName = (Get-ChildItem $path).FullName
$FileName2 = (Get-ChildItem $path).Name
$CSVFile = "$Path\$Filename2.csv"
$Excel = New-Object -ComObject Excel.Application -Property #{Visible =
$false}
$Excel.displayalerts=$False
$Workbook = $Excel.Workbooks.Open($FileName)
$WorkSheet = $WorkBook.Sheets.Item(2)
$Worksheet.Activate()
$worksheet.columns.item('G').NumberFormat ="m/d/yyyy"
$Worksheet.Cells.Item(1,3).Value = "Site ID"
$Worksheet.Cells.Item(1,4).Value = "Site Name"
$Worksheet.SaveAs($CSVFile,
[Microsoft.Office.Interop.Excel.XlFileFormat]::xlCSVWindows)
$workbook.Save()
$workbook.Close()
$Excel.Quit()
Running the following command, will let you save the CSV file using the delimiter §
Import-CSV filename.csv | ConvertTo-CSV -NoTypeInformation -Delimiter "§" | Out-File output_filename.csv
You should check out ImportExcel - PowerShell module to import/export Excel spreadsheets, without Excel. It makes working with excel files easier using powershell.
I know this is an older post but here is an option I recently came across:
Just update the e:\projects\dss\pse&g.xlsxwith the source location and file as well as the file.csv with the location and file name. Lastly your Worksheet if it is named differently [Sheet1$].
$oleDbConn = New-Object System.Data.OleDb.OleDbConnection
$oleDbCmd = New-Object System.Data.OleDb.OleDbCommand
$oleDbAdapter = New-Object System.Data.OleDb.OleDbDataAdapter
$dataTable = New-Object System.Data.DataTable
$oleDbConn.ConnectionString="Provider=Microsoft.ACE.OLEDB.12.0;Data
Source=e:\projects\dss\pse&g.xlsx;Extended Properties=Excel 12.0;Persist Security Info=False"
$oleDbConn.Open()
$oleDbCmd.Connection = $OleDbConn
$oleDbCmd.commandtext = “Select * from [Sheet1$]”
$oleDbAdapter.SelectCommand = $OleDbCmd
$ret=$oleDbAdapter.Fill($dataTable)
Write-Host "Rows returned:$ret" -ForegroundColor green
$dataTable | Export-Csv file.csv -Delimiter ';'
$oleDbConn.Close()
Source
I was using SaveAs(file.csv,6) but couldn't change the delimiter. Also Ishan's resolution works but I wanted something more OOB as this is going to be used within an SSIS package for myself across different systems and this just works. =)

PowerShell saving excel sheet in unreadable format

I have the below piece of code that checks for Files to Tapes jobs for a database and gives the output in an excel sheet.
$date = Get-Date
$day = $date.Day
$hour = $date.Hour
$Excel = New-Object -ComObject Excel.Application
$Excel.visible = $true
$Excel.DisplayAlerts = $false
$Workbook = $Excel.Workbooks.Add()
$Sheet = $Excel.Worksheets.Item(1)
#Counter variable for rows and columns
$intRow = 1
$intCol = 1
$Sheet.Cells.Item($intRow,1) = "Tasks/Servers"
$Sheet.Cells.Item($intRow,2) = "DateLastRun"
$Sheet.Cells.Item($intRow,3) = "PRX1CSDB01"
$Sheet.Cells.Item($intRow,4) = "PRX1CSDB02"
$Sheet.Cells.Item($intRow,5) = "PRX1CSDB03"
$Sheet.Cells.Item($intRow,6) = "PRX1CSDB11"
$Sheet.Cells.Item($intRow,7) = "PRX1CSDB12"
$Sheet.Cells.Item($intRow,8) = "PRX1CSDB13"
$Sheet.Cells.Item($intRow+1,1) = "File To Tape weekly Full Backup"
$Sheet.UsedRange.Rows.Item(1).Borders.LineStyle = 1
#FTT.txt contains the path for a list of servers
$path = Get-Content D:\Raghav\DB_Integrated\FTT.txt
foreach ($server in $path)
{
If (Test-Path $server)
{
$BckpWeek = gci -path $server | select-object | where {$_.Name -like "*logw*"} | sort LastWriteTime | select -last 1
$Sheet.Cells.Item($intRow+1,$intCol+1) = $BckpWeek.LastWriteTime.ToString('MMddyyyy')
$Sheet.UsedRange.Rows.Item($intRow).Borders.LineStyle = 1
$x = (get-date) - ([datetime]$BckpWeek.LastWriteTime)
if( $x.days -gt 7){$status_week = "Failed"}
else{$status_week = "Successful"}
$Sheet.Cells.Item($intRow+1,$intCol+2) = $status_week
$intCol++
}
else
{
$Sheet.Cells.Item($intRow+1,$intCol+2) = "Path Not Found"
$intCol++
}
}
$Sheet.UsedRange.EntireColumn.AutoFit()
$workBook.SaveAs("C:\Users\Output.xlsx",51)
$excel.Quit()
However, when I try to import the contents of Output.xlsx into a variable say $cc, I get data in an unreadable format.
$cc = Import-Csv "C:\Users\Output.xlsx"
Attached is the image for what I get on exporting output.xlsx into $cc. I tried to put the output in csv format too. But that also doesnt seem to help.Anybody having any idea on this or having faced any similar situation before?
#ZevSpitz - Looking for the OleDbConnection class, I landed up at https://blogs.technet.microsoft.com/pstips/2014/06/02/get-excel-data-without-excel/ . This is what I was looking for. Thank you for pointing me out in the right direction.
#MikeGaruccio - Unfortunately, I didn't find Import-Excel command in Get-Help menu. I am using Powershell 4.0. Anyways, thank you for the suggestion.

Return object of a function doesn't export to Excel in PowerShell

I'm using the Export-Excel cmdlet to export the output of a function into Excel. My function is as follows:
function SQLQuery($ServerName, $DBName, $Query)
{
$SqlConnection = New-Object System.Data.SqlClient.SqlConnection
$SqlConnection.ConnectionString = "Server=$ServerName;Database=$DBName;Integrated Security=True"
$SqlCmd = New-Object System.Data.SqlClient.SqlCommand
$SqlCmd.CommandText = $Query
$SqlCmd.Connection = $SqlConnection
$SqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter
$SqlAdapter.SelectCommand = $SqlCmd
$DataSet = New-Object System.Data.DataSet
$SqlAdapter.Fill($DataSet)
$Output = $DataSet.Tables[0]
$SqlConnection.Close()
return $Output
}
$ResultCost = SQLQuery -ServerName $SName -DBName $DBName -Query (Get-Content -Path $CostQueryPath)
$ResultCost.Table | Export-Excel -Path $ReportPath
The direct output doesn't get exported to Excel so I used $ResultCost.Table to export. However, I see many duplicates being exported (if the result has 10 records, the $ResultCost.Table has 10 to the power of 10 i.e., 100 records). How can I export only the direct output? And how do I remove the last 5 unwanted columns?
If you are trying to export a dataset to a .xlsx file try exporting the rows, not the table.
So instead of $ResultCost.Tables | Export-Excel -Path $ReportPath
Try $ResultCost.Tables.Rows | Export-Excel -Path $ReportPath

Using hash table in powershell

I want to create an application in which a lookup table (in excel) needs to be given as an input to the application. This lookup table maps script names to its run time details. In my application i want to access the run time associated with the script name that user provides.
Code :
$hash = #{}
$xl = New-Object -COM "Excel.Application"
$wb = $xl.Workbooks.Open("$data_path\Lookup_Table.xlsm")
$ws = $wb.WorkSheets.Item(1)
$ws.activate()
$objRange = $ws.UsedRange
$RowCount = $objRange.Rows.Count
for ($k=0; $k -lt $RowCount; $k++)
{
$fullname = $ws.cells.item(2+$k,1)
$time = $ws.cells.item(2+$k,2)
$hash.Set_Item($fullname,$time)
}
$wb.Save()
$wb.Close()
$xl.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($xl)
Stop-Process -Name EXCEL -Force
Still the $hash is not taking the value
$hash.Get_Item($name) command outputs nothing.
I tired with all the three methods which i knew
$hash[$fullname] = $time
$hash.Add($fullname , $time)
$hash.Set_Item($fullname,$time)
What can be the problem?

Powershell script using Excel running slow

So i have this script that i coded on my laptop that works just fine, the job is to combine two .csv-files into one .xls-file.
And running the script with two .csv-files containing a couple of thousand rows takes a few seconds max.
But when i try to run it on the server where it should be located, it takes... hours. I haven't done a full run, but writing one line in the .xls-file takes maybe 2-3 seconds.
So what im wondering is what is causing the huge increase in runtime. I'm monitoring the CPU-load while the script is running, and it's at 50-60% load.
The server has loads of Ram, and two CPU-core.
How can i speed this up?
The script looks like this:
$path = "C:\test\*"
$path2 = "C:\test"
$date = Get-Date -Format d
$csvs = Get-ChildItem $path -Include *.csv | Sort-Object LastAccessTime -Descending | Select-Object -First 2
$y = $csvs.Count
Write-Host "Detected the following CSV files: ($y)"
foreach ($csv in $csvs) {
Write-Host " "$csv.Name
}
$outputfilename = "regSCI " + $date
Write-Host Creating: $outputfilename
$excelapp = New-Object -ComObject Excel.Application
$excelapp.sheetsInNewWorkbook = $csvs.Count
$xlsx = $excelapp.Workbooks.Add()
$sheet = 1
$xlleft = -4131
foreach ($csv in $csvs) {
$row = 1
$column = 1
$worksheet = $xlsx.Worksheets.Item($sheet)
$worksheet.Name = $csv.Name
$worksheet.Rows.HorizontalAlignment = $xlleft
$file = (Get-Content $csv)
Write-Host Worksheet created: $worksheet.Name
foreach($line in $file) {
Write-Host Writing Line
$linecontents = $line -split ',(?!\s*\w+")'
foreach($cell in $linecontents) {
Write-Host Writing Cell
$cell1 = $cell.Trim('"')
$worksheet.Cells.Item($row, $column) = $cell1
$column++
}
$column = 1
$row++
$WorkSheet.UsedRange.Columns.Autofit() | Out-Null
}
$sheet++
$headerRange = $worksheet.Range("a1", "q1")
$headerRange.AutoFilter() | Out-Null
}
$output = $path2 + "\" + $outputfilename
Write-Host $output
$xlsx.SaveAs($output)
$excelapp.Quit()
To speed up your existing code, add these just after creating Excel object:
$excelapp.ScreenUpdating = $false
$excelapp.DisplayStatusBar = $false
$excelapp.EnableEvents = $false
$excelapp.Visible = $false
And these just before SaveAs:
$excelapp.ScreenUpdating = $true
$excelapp.DisplayStatusBar = $true
$excelapp.EnableEvents = $true
This causes excel not to render the worksheet in realtime and fire events every time you change the contets. Most probably DisplayStatusBar and ScreenUpdating doesn't matter if you make an application invisible, but I included it just in case.
Also, you're running Autofit() after every line. This certainly doesn't help with performance.

Resources