48

I have some varbinary data stored in a table in MS Sql Server 2005. Does anyone have SQL code that takes a query as input (lets say the query guarantees that a single column of varbinary is returned) and outputs the bytes to disk (one file per row?) I'm sure this has been asked a thousand times before, but Googling comes up with mostly .net solutions. I want an SQL solution.

Suraj
  • 35,905
  • 47
  • 139
  • 250

9 Answers9

50

The BCP approach does not work for me. The bytes it writes to disk cannot be deserialized back to the .net objects I stored. This means that the bytes on disk aren't equivalent to what's stored. Perhaps BCP is writing some kind of header. I'm not sure.

I found the following code here at the bottom of the article. It works great! Although it was intended for stored BMP images, it works with any varbinary.

DECLARE @SQLIMG VARCHAR(MAX),
    @IMG_PATH VARBINARY(MAX),
    @TIMESTAMP VARCHAR(MAX),
    @ObjectToken INT

DECLARE IMGPATH CURSOR FAST_FORWARD FOR 
        SELECT csl_CompanyLogo from mlm_CSCompanySettingsLocalizations

OPEN IMGPATH 

FETCH NEXT FROM IMGPATH INTO @IMG_PATH 

WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @TIMESTAMP = 'd:\' + replace(replace(replace(replace(convert(varchar,getdate(),121),'-',''),':',''),'.',''),' ','') + '.bmp'

        PRINT @TIMESTAMP
        PRINT @SQLIMG

        EXEC sp_OACreate 'ADODB.Stream', @ObjectToken OUTPUT
        EXEC sp_OASetProperty @ObjectToken, 'Type', 1
        EXEC sp_OAMethod @ObjectToken, 'Open'
        EXEC sp_OAMethod @ObjectToken, 'Write', NULL, @IMG_PATH
        EXEC sp_OAMethod @ObjectToken, 'SaveToFile', NULL, @TIMESTAMP, 2
        EXEC sp_OAMethod @ObjectToken, 'Close'
        EXEC sp_OADestroy @ObjectToken

        FETCH NEXT FROM IMGPATH INTO @IMG_PATH 
    END 

CLOSE IMGPATH
DEALLOCATE IMGPATH
Suraj
  • 35,905
  • 47
  • 139
  • 250
  • 3
    To run this one might need to enable OLE Automation procedures https://msdn.microsoft.com/en-us/library/ms191188.aspx. – Gayan Dasanayake May 05 '16 at 02:56
  • 3
    To enable OLE Automation procedures use: `sp_configure 'show advanced options', 1;` `GO` `RECONFIGURE;` `GO` `sp_configure 'Ole Automation Procedures', 1;` `GO` `RECONFIGURE;` – M. Jahedbozorgan Nov 09 '19 at 15:05
30

I am adding this to build on JohnOpincar's answer, so that others who want to use LinqPad can get a working solution faster.

/*
This LinqPad script saves data stored in a VARBINARY field to the specified folder.
1. Connect to SQL server and select the correct database in the connection dropdown (top right)
2. Change the Language to C# Program
3. Change "Attachments" to the name of your table that holds the VARBINARY data
4. Change "AttachmentBuffer" to the name of the field that holds the data
5. Change "Id" to the unique identifier field name
6. Change "1090" to the identity of the record you want to save
7. Change the path to where you want to save the file. Make sure you choose the right extension.

Notes: Windows 10 may give you "Access Denied" error when trying to save directly to C:\. Rather save to a subfolder.
*/

void Main()
{
    var context = this;
    var query = 
        from ci in context.Attachments
        where ci.Id == 1090
        select ci.AttachmentBuffer
    ;
    byte[] result = query.Single().ToArray();
    File.WriteAllBytes(@"c:\DEV\dumpfile.xlsx", result);
    Console.WriteLine("Done");
}
Marcello Miorelli
  • 3,368
  • 4
  • 44
  • 67
Atron Seige
  • 2,783
  • 4
  • 32
  • 39
13

You can use BCP, not T-SQL, but works well.

BCP "SELECT FileContent FROM table WHERE ID = 1" queryout "C:\file.txt" -T
Dustin Laine
  • 37,935
  • 10
  • 86
  • 125
  • 1
    Hi Dustin - I was able to use command to output a file, but I don't think its working properly. The data is a serialized .net object. I know the data is stored properly because I have processes that operate on that data from .net. However, when I try to deserialize I get an error, which means that the bytes aren't written properly. Thoughts? If the command outputs a single varbinary(max) value, are the actual bytes written to disk or does the process include headers, etc.? – Suraj Oct 30 '10 at 15:12
  • 1
    Using the -N option seems to make this work properly – David Gardiner Dec 19 '13 at 03:26
  • 5
    Use the following options when prompted: - Enter the file storage type of field XXX [varbinary(max)]: - Enter prefix-length of field XXX [8]: 0 - Enter length of field XXX [0]: - Enter field terminator [none]: - Do you want to save this format information in a file? [Y/n] n – brz Oct 15 '15 at 10:33
  • Prints out: SQLState = 37000, NativeError=2812 Error = [Microsoft][ODBC Driver 13 for SQL Server][SQL Server]Could not find stored procedure 'sp_descrive_first_result_set'. – Dainius Kreivys Oct 04 '17 at 07:05
  • 2
    You need to specify a prefix-length of 0, instead of the default 8, so that the file length is not written as a prefix (header) before the file data. – David R Tribble Jun 27 '18 at 21:01
9

If you have linqpad, this works:

void Main()
{
    var context = this;
    var query = 
        from ci in context.Images
        where ci.ImageId == 10
        select ci.Image
    ;
    var result = query.Single ();
    var bytes = Convert.FromBase64String(result);
    File.WriteAllBytes(@"c:\image.bmp", bytes);
}
JohnOpincar
  • 5,620
  • 3
  • 35
  • 38
  • 3
    this works...? really, so `var context = this` will automatically search for your the instance of sql server, will login, will find the correct database, which has a scheme where the Images table exists... Never knew LinqPad was this smart!! – Ric .Net Apr 12 '16 at 14:23
  • Have you ever used it? It doesn't appear so. I guess irfandar and I are just imagining it. – JohnOpincar Apr 14 '16 at 15:20
  • 6
    Yes I use it, and yes your answer is correct, but only if you configure the connection correctly, but this information is missing from your answer and for new LinqPad users this will look like magic... That was the only point I was making – Ric .Net Apr 16 '16 at 19:30
8

I know it's an old post, but I figured out why the following doesn't work and how to fix it:

BCP "SELECT FileContent FROM table WHERE ID = 1" queryout "C:\file.JPG" -T -N

The reason is bcp put Prefix Length at the very beginning of the file. It is either 4 bytes or 8 bytes, depends on data type of FileContent column (text, ntext, image: 4 varchar(max), varbinary(max) : 8 Refer to https://msdn.microsoft.com/en-us/library/ms190779.aspx)

Use a binary editor, like the one in Visual Studio, to remove the prefix bytes, and everything runs perfectly. :-)

Sean Zhu
  • 81
  • 1
  • 4
6

With Powershell

function SQLExecuteScalar([string]$pServer, [string]$pDatabase, [string]$pQuery)
{
    # Connection
    $pSQLConnection = New-Object System.Data.SqlClient.SqlConnection
    $pSQLConnection.ConnectionString = "Data Source=$($pServer);Initial Catalog=$($pDatabase);Integrated Security=SSPI;Application Name=FileExtractor.Powershell"
    $pSQLConnection.Open()

    # Command
    [System.Data.SqlClient.SqlCommand]$cmd = New-Object System.Data.SqlClient.SqlCommand($pQuery, $pSQLConnection)

    # Execute and Get scalar value
    [byte[]]$return = $cmd.ExecuteScalar()
    
    # Close Connection
    $pSQLConnection.Close()

    # Result to pipe
    return $return
}

[string]$Server = "MyServer"
[string]$DataBase = "MyDb"
[string]$Query = "select BlobValue from dbo.MyTable"
[string]$FileName = "C:\Temp\BlobValue.bin"

SQLExecuteScalar -pServer $Server -pDatabase $DataBase -pQuery $Query | Set-Content $FileName -Encoding Byte
1

Just an alternative. You can use the freeware Toad for SQL server and save directly from the editor.

You can go to their website https://www.toadworld.com and get the freeware there or a 30 day trial of the full version. Its under Download and pick Toad for SQL server.

enter image description here

You do a regular select statement in Toad on the line that has the image you want to save. When you see the results you can click on the byte image column and on the right you see a PDF tab if this is a PDF document or on the left you see a Image tab. When you click the tab you can see the save logo at the bottom to save the image or file.

Vilhelm
  • 187
  • 2
  • 9
  • ***Toad for Sql server*** free? link ? – Kiquenet Mar 21 '18 at 10:16
  • @Vilhelm are you affiliated to Toad? – David Walschots Sep 05 '18 at 09:26
  • 2
    @DavidWalschots No. I worked on Oracle databases with Toad and was frustraded that I could not do the same in MS Management Studio. Then I saw that Toad has an MSSQL version and the freeware had this feature so I use that now as my main editor for MSSQL databases. – Vilhelm Sep 06 '18 at 10:06
0

You can easy do that in Powershell:

# Define your T-SQL query
$query = "SELECT [YourVarbinaryColumn] FROM [YourTable] WHERE [YourCondition]"

# Define your database connection parameters
$serverName = "YourServerName"
$databaseName = "YourDatabaseName"
$username = "YourUsername"
$password = "YourPassword"

# Create a new SqlConnection object
$connectionString = "Server=$serverName;Database=$databaseName;User ID=$username;Password=$password;"
$connection = New-Object System.Data.SqlClient.SqlConnection($connectionString)

# Open the database connection
$connection.Open()

# Create a new SqlCommand object
$command = $connection.CreateCommand()
$command.CommandText = $query

# Execute the T-SQL query and retrieve the results as a DataTable
$dataTable = New-Object System.Data.DataTable
$dataAdapter = New-Object System.Data.SqlClient.SqlDataAdapter($command)
$dataAdapter.Fill($dataTable)

# Close the database connection
$connection.Close()

# Extract the binary data from the first row of the DataTable
$binaryData = $dataTable.Rows[0]["YourVarbinaryColumn"]

# Convert the binary data to a string
$htmlString = [System.Text.Encoding]::UTF8.GetString($binaryData)

# Define the path and filename for the HTML file
$filePath = "C:\YourDirectory\YourFilename.html"

# Create the HTML file and write the HTML string to it
Set-Content -Path $filePath -Value $htmlString -Encoding UTF8
Krzysztof Gapski
  • 528
  • 6
  • 10
-1

SQL is designed to work with database objects, so from it's point of view anything else doesn't exists. Sure, there are extended procedures like xp_cmdshell that allow you interact with the operating system, but they are proprietary extensions and not part of T-SQL.

Maybe the closest approach would be using the FILESTREAM attribute for binary types of SQL Server 2008, which allow storing some columns directly as files in a folder instead of using the database:

FILESTREAM overview

Note that the FILESTREAM storage is designed for maintain large files out of the database in order to increase performance, and not for allowing direct access to files (i.e. T-SQL still doesn't have the concept of a filesystem). I my opinion, direct access to the filesystem from SQL will defeat some of the purposes of a database (mainly having data stored in a structured way).

So I would recommend following the advice of Dustin and use a tool like BCP or any other data dumper.

Alberto Martinez
  • 2,620
  • 4
  • 25
  • 28