{"id":221,"date":"2007-02-06T00:00:00","date_gmt":"2007-02-05T00:00:00","guid":{"rendered":"https:\/\/test.simple-talk.com\/uncategorized\/sql-server-excel-workbench\/"},"modified":"2026-09-22T10:24:24","modified_gmt":"2026-09-22T10:24:24","slug":"sql-server-excel-workbench","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/sql-server-excel-workbench\/","title":{"rendered":"How to move data between SQL Server and Excel with T-SQL"},"content":{"rendered":"\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>T-SQL can create, read, update, and delete Excel spreadsheet data directly from SQL Server, without exporting through a separate tool. <\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>This guide (workbench) covers four approaches: linking to an Excel file as a data source, querying it inline with <code>OPENDATASOURCE<\/code>\/<code>OPENROWSET<\/code>, generating a quick HTML-based export with <code>sp_makewebtask<\/code>, and using OLE Automation for full control over formatting, ranges, and pivot tables. <\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Each example uses a small test table of Cambridge pubs, and full sample code is available as a download.<\/strong><\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\" id=\"h-phil-and-i-have-teamed-up-on-this-workbench-which-demonstrates-how-to-create-read-update-and-delete-c-r-u-d-information-in-excel-using-t-sql-from-sql-server-as-always-the-workbench-is-structured-so-that-it-can-be-pasted-into-query-analyser-and-ssms-and-the-individual-examples-executed-you-can-download-the-sql-from-the-code-download-link-at-the-bottom-of-the-article-load-it-up-and-start-experimenting\">Phil and I have teamed up on this workbench, which demonstrates <strong>how to create, read, update and delete (C.R.U.D) information in Microsoft Excel using T-SQL, from SQL Server<\/strong>. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\" id=\"h-phil-and-i-have-teamed-up-on-this-workbench-which-demonstrates-how-to-create-read-update-and-delete-c-r-u-d-information-in-excel-using-t-sql-from-sql-server-as-always-the-workbench-is-structured-so-that-it-can-be-pasted-into-query-analyser-and-ssms-and-the-individual-examples-executed-you-can-download-the-sql-from-the-code-download-link-at-the-bottom-of-the-article-load-it-up-and-start-experimenting\">The workbench is structured so that it can be pasted into Query Analyzer and SSMS, and the individual examples executed &#8211; you can download the .sql from the &#8220;Code Download&#8221; link at the bottom of the article, load it up and start experimenting!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-structure-of-this-guide-what-you-ll-learn\">The structure of this guide (what you&#8217;ll learn)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We start by showing you how to create an Excel Spreadsheet from SQL Server in T-SQL (Transact SQL), create a worksheet, attach to it as a linked server, write to it, read from it, update it as if it was an ordinary SQL Server Database table, and then synchronize the data in the worksheet with SQL Server. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We also illustrate the use of <code>OPENQUERY<\/code>, <code>OPENDATASOURCE<\/code> and <code>OPENROWSET<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To create the Excel spreadsheet, we show how to attach to an ADODB source from SQL Server and execute SQL against that source. We then show you an alternative &#8216;quick cheat&#8217; way (using <code>sp_makewebtask<\/code>) to create and populate an Excel spreadsheet from Transact SQL.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you need more control over the Excel Spreadsheet that you are creating, we then show you how to do it via OLE automation. This will enable you to do anything you can do via keystrokes, and allow you to generate full Excel reports with pivot tables and Graphs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Using this technique, you should be able to populate the data, or place data in particular calls or ranges. You can even do &#8216;macro substitutions&#8217;.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-good-to-know-before-you-start\">Good to know before you start<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A word of caution before you start. If you have your security wide open, it is not just you who would be able to write out data as a spreadsheet. An intruder would be able to do it with that list of passwords or credit-card numbers. In a production system, this sort of operation needs to be properly ring-fenced. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We tend to create a job queue and have a special user, with the appropriate permissions, on the Task Scheduler, to do anything that involves OLE automation or <code>xp_CMDShell<\/code>. Security precautions can get quite complex, but they are outside the scope of the article.<\/p>\n\n\n\n<div id=\"callout-block_f26ac57214a97fa29c015feb698188e2\" class=\"callout alignnone\">\n    <div class=\"child-last:mb-0 child-first:mt-0 bg-gray-50 dark:bg-gray-950 p-4xl my-3xl\">\n\n<p class=\"wp-block-paragraph\"><em>You may also be interested in:<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/sql-server-security-primer\/\" target=\"_blank\" rel=\"noreferrer noopener\">SQL Server Security Features: Complete Guide<\/a><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><a href=\"https:\/\/www.red-gate.com\/simple-talk\/data-security-privacy-compliance\/\" target=\"_blank\" rel=\"noreferrer noopener\">All Security, Privacy &amp; Compliance articles and guides on Simple Talk<\/a><\/strong><\/p>\n\n<\/div>\n<\/div> \n\n\n<p class=\"wp-block-paragraph\">Some of what we illustrate can be done using DTS or SSIS. Unfortunately, these are outside the scope of this article. In fact, transferring data between Excel and SQL Server can be done in a surprising variety of ways and it would be fun one day to try to list them all.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-how-to-set-up-the-test-data\">How to set up the test data<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">First, we need some simple test data:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">    CREATE TABLE ##CambridgePubs \n            (Pubname VARCHAR(40), \n            Address VARCHAR(80), \n            Postcode VARCHAR(8)) \n    \n\n    INSERT INTO ##CambridgePubs (PubName, Address, Postcode) \n        SELECT 'Bees In The Wall','36 North Road, \n    Whittlesford, Cambridge','CB2 4NZ' \n    INSERT INTO ##CambridgePubs (PubName, Address, Postcode) \n        SELECT 'Blackamoors Head','205 Victoria Road, \n    Cambridge','CB4 3LF' \n    INSERT INTO ##CambridgePubs (PubName, Address, Postcode) \n        SELECT 'Blue Lion','2 Horningsea Road, \n    Fen Ditton, Cambridge','CB5 8SZ' \n    INSERT INTO ##CambridgePubs (PubName, Address, Postcode) \n        SELECT 'Cambridge Blue','85-87 Gwydir Street, \n    Cambridge','CB1 2LG' \n    INSERT INTO ##CambridgePubs (PubName, Address, Postcode) \n        SELECT 'Champion Of The Thames','68 King Street, \n    Cambridge','CB1 1LN' \n    INSERT INTO ##CambridgePubs (PubName, Address, Postcode) \n        SELECT 'Cross Keys','77 Ermine Street, \n    Caxton, Cambridge','CB3 8PQ' \n    INSERT INTO ##CambridgePubs (PubName, Address, Postcode) \n        SELECT 'Crown Inn','11 High Street, \n    Linton, Cambridge','CB1 6HS' \n    INSERT INTO ##CambridgePubs (PubName, Address, Postcode) \n        SELECT 'Devonshire Arms','1 Devonshire Road, \n    Cambridge','CB1 2BH' \n    INSERT INTO ##CambridgePubs (PubName, Address, Postcode) \n        SELECT 'Duke Of Argyle','90 Argyle Street, \n    Cambridge','CB1 3LS' \n    INSERT INTO ##CambridgePubs (PubName, Address, Postcode) \n        SELECT 'Duke Of Wellington','49 Alms Hill, \n    Bourn, Cambridge','CB3 7SH' \n    INSERT INTO ##CambridgePubs (PubName, Address, Postcode) \n        SELECT 'Eagle Public House','Benet Street, \n    Cambridge','CB2 3QN' \n    <\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">And so on. (The full import file is in the ZIP, as is the Excel file!).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create the table and then execute the contents of <code>CambridgePubs.SQL<\/code>. We&#8217;re now ready to get started properly.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Contents<\/h2>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><a href=\"#first\">Creating Excel spreadsheets via ADODB <\/a><\/li>\n\n\n\n<li><a href=\"#second\">Manipulating Excel data via a linked server <\/a><\/li>\n\n\n\n<li><a href=\"#third\">Synchronising the Spreadsheet with SQL Server Tables <\/a><\/li>\n\n\n\n<li><a href=\"#fourth\">Manipulating Excel data using <code>OPENDATASOURCE<\/code> and <code>OPENROWSET<\/code> functions <\/a><\/li>\n\n\n\n<li><a href=\"#fifth\">Creating Excel spreadsheets using <code>sp_MakeWebTask<\/code> <\/a><\/li>\n\n\n\n<li><a href=\"#sixth\">OLE Automation <\/a><\/li>\n<\/ul>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"first\">Creating Excel spreadsheets via ADODB <\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">First, we need to create the spreadsheet with the correct headings (<code>PubName, Address, PostCode<\/code>).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There are two possible ways one might do this. The most obvious way is using the <code>CREATE<\/code> statement to create the worksheet and define the columns, but there seems to be no way of doing this by linking the Excel file, unless the Excel file already exists. We need a utility stored procedure to get at ADODB in order to create databases and execute DDL and SQL against it.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">    CREATE PROCEDURE spExecute_ADODB_SQL \n    @DDL VARCHAR(2000), \n    @DataSource VARCHAR(100), \n    @Worksheet VARCHAR(100)=NULL, \n    @ConnectionString VARCHAR(255) \n        = 'Provider=Microsoft.Jet.OLEDB.4.0; \n    Data Source=%DataSource; \n    Extended Properties=Excel 8.0' \n    AS \n    DECLARE \n        @objExcel INT, \n        @hr INT, \n        @command VARCHAR(255), \n        @strErrorMessage VARCHAR(255), \n        @objErrorObject INT, \n        @objConnection INT, \n        @bucket INT \n    \n\n    SELECT @ConnectionString \n        =REPLACE (@ConnectionString, '%DataSource', @DataSource) \n    IF @Worksheet IS NOT NULL \n        SELECT @DDL=REPLACE(@DDL,'%worksheet',@Worksheet) \n    \n\n    SELECT @strErrorMessage='Making ADODB connection ', \n                @objErrorObject=NULL \n    EXEC @hr=sp_OACreate 'ADODB.Connection', @objconnection OUT \n    IF @hr=0 \n        SELECT @strErrorMessage='Assigning ConnectionString property \"' \n                + @ConnectionString + '\"', \n                @objErrorObject=@objconnection \n    IF @hr=0 EXEC @hr=sp_OASetProperty @objconnection, \n                'ConnectionString', @ConnectionString \n    IF @hr=0 SELECT @strErrorMessage \n            ='Opening Connection to XLS, for file Create or Append' \n    IF @hr=0 EXEC @hr=sp_OAMethod @objconnection, 'Open' \n    IF @hr=0 SELECT @strErrorMessage \n            ='Executing DDL \"'+@DDL+'\"' \n    IF @hr=0 EXEC @hr=sp_OAMethod @objconnection, 'Execute', \n            @Bucket out , @DDL \n    IF @hr&lt;&gt;0 \n        BEGIN \n        DECLARE \n            @Source VARCHAR(255), \n            @Description VARCHAR(255), \n            @Helpfile VARCHAR(255), \n            @HelpID INT \n        \n        EXECUTE sp_OAGetErrorInfo @objErrorObject, @source output, \n            @Description output,@Helpfile output,@HelpID output \n        SELECT @strErrorMessage='Error whilst ' \n            +COALESCE(@strErrorMessage,'doing something')+', ' \n            +COALESCE(@Description,'') \n        RAISERROR (@strErrorMessage,16,1) \n        END \n    EXEC @hr=sp_OADestroy @objconnection \n    GO \n    <\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Now we have it, it is easy.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">    spExecute_ADODB_SQL @DDL='Create table CambridgePubs \n    (Pubname Text, Address Text, Postcode Text)', \n    @DataSource ='C:\\CambridgePubs.xls' \n    <\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The Excel file will have been created on the Database server of the database you currently have a connection to.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We could now insert data into the spreadsheet, if we wanted:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">    spExecute_ADODB_SQL @DDL='insert into CambridgePubs \n    (Pubname,Address,Postcode) \n    values (''The Bird in Hand'', \n    ''23, Marshall Road, Cambridge CB4 2DQ'', \n    ''CB4 2DQ'')', \n    @DataSource ='C:\\CambridgePubs.xls' \n    \n\n    --you could drop it again! \n    spExecute_ADODB_SQL @DDL='drop table CambridgePubs', \n    @DataSource ='c:\\CambridgePubs.xls' \n    <\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"second\">Manipulating Excel data via a linked server <\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We can now link to the created Excel file as follows.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">    EXEC sp_addlinkedserver 'CambridgePubDatabase', \n    @srvproduct = '', \n    @provider = 'Microsoft.Jet.OLEDB.4.0', \n    @datasrc = 'C:\\CambridgePubs.xls', \n    @provstr = 'Excel 8.0;' \n    GO \n    \n\n    EXEC sp_addlinkedsrvlogin 'CambridgePubDatabase', 'false' \n    GO \n    <\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">To drop the link, we do this!<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">    --EXEC sp_dropserver 'CambridgePubDatabase', 'droplogins' \n    \n\n    -- Get the spreadsheet data via OpenQuery \n    SELECT * FROM OPENQUERY \n        (CambridgePubDatabase, 'select * from [CambridgePubs]') \n    GO \n    --or more simply, do this \n    SELECT * FROM CambridgePubDatabase...CambridgePubs \n    <\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">So now we can insert our data into the Excel spreadsheet:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>INSERT INTO CambridgePubDatabase...CambridgePubs<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">(Pubname, Address, postcode)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>SELECT Pubname, Address, postcode FROM ##CambridgePubs<\/code><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"> <\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"third\">Synchronizing the Spreadsheet with SQL Server tables <\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">As we are directly manipulating the Excel data in the worksheet as if it was a table we can do <code>JOINs<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">What about synchronising the table after editing the Excel spreadsheet?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To try this out, you&#8217;ll need to <code>DELETE<\/code>, <code>ALTER<\/code> and <code>INSERT<\/code> a few rows from the Excel spreadsheet, remembering to close it after you&#8217;ve done it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Firstly, we&#8217;ll delete any rows from <code>##CambridgePubs<\/code> that do not exist in the Excel spreadsheet.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">    DELETE FROM ##CambridgePubs \n    FROM ##CambridgePubs c \n    LEFT OUTER JOIN CambridgePubDatabase...CambridgePubs ex \n    ON c.address LIKE ex.address \n        AND c.pubname LIKE ex.pubname \n        AND c.postcode LIKE ex.postcode \n    WHERE ex.pubname IS NULL \n    <\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then we insert into <code>##CambridgePubs<\/code> any rows in the spreadsheet that don&#8217;t exist in <code>##CambridgePubs<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">    INSERT INTO ##CambridgePubs (Pubname,Address,Postcode) \n    SELECT ex.Pubname,ex.Address,ex.Postcode \n    FROM CambridgePubDatabase...CambridgePubs ex \n    LEFT OUTER JOIN ##CambridgePubs c \n    ON c.address LIKE ex.address \n        AND c.pubname LIKE ex.pubname \n        AND c.postcode LIKE ex.postcode \n    WHERE c.pubname IS NULL \n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">All done (reverse syncronisation would be similar).<\/p>\n\n\n\n<section id=\"my-first-block-block_15fcf69f130328ce9c6d328581340b59\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Write accurate SQL faster in SSMS with SQL Prompt AI<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            Write or modify queries using natural language, get clear explanations for unfamiliar code, and fix and optimize SQL with ease &#8211; all without leaving SSMS.                                    <\/div>\n            <\/div>\n                                            <a href=\"https:\/\/www.red-gate.com\/products\/sql-prompt\/#ai-powered-code\" class=\"btn btn--secondary btn--lg\" aria-label=\"Learn more and try for free: Write accurate SQL faster in SSMS with SQL Prompt AI\">Learn more and try for free<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<h2 class=\"wp-block-heading\" id=\"fourth\">Manipulating Excel data using <code>OPENDATASOURCE<\/code> and <code>OPENROWSET<\/code> <\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you don&#8217;t want to do the linking, you can also read the data like this:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">    SELECT * \n    FROM OPENDATASOURCE( 'Microsoft.Jet.OLEDB.4.0', \n     'Data Source=\"C:\\CambridgePubs.xls\"; \n      Extended properties=Excel 8.0')...CambridgePubs \n    --and write to it \n    \n\n    UPDATE OPENDATASOURCE ('Microsoft.Jet.OleDB.4.0', \n    'Data Source=\"C:\\CambridgePubs.xls\"; \n    extended Properties=Excel 8.0')...CambridgePubs \n    SET Address='St. Kilda Road, Cambridge' \n    WHERE Pubname = 'Jenny Wren' \n    \n\n    INSERT INTO OPENDATASOURCE ('Microsoft.Jet.OleDB.4.0', \n    'Data Source=\"C:\\CambridgePubs.xls\"; \n    extended Properties=Excel 8.0')...CambridgePubs \n    (Pubname,Address,Postcode ) \n    SELECT 'The St George','65 Cavendish Road','CB2 4RT' \n    <\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You can read and write to the Excel sheet using <code>OpenRowSet<\/code>, if the mood takes you.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">    SELECT * FROM OPENROWSET('Microsoft.Jet.OLEDB.4.0', \n    'Excel 8.0;DATABASE=C:\\CambridgePubs.xls', 'Select * from CambridgePubs') \n    \n\n    UPDATE OPENROWSET('Microsoft.Jet.OLEDB.4.0', \n    'Excel 8.0;DATABASE=c:\\CambridgePubs.xls', \n    'Select * from CambridgePubs') \n        SET Address='34 Glemsford Road' WHERE Address = '65 Cavendish Road' \n    \n\n    INSERT INTO OPENROWSET ('Microsoft.Jet.OLEDB.4.0', \n    'Excel 8.0;DATABASE=c:\\CambridgePubs.xls', \n    'Select * from CambridgePubs') \n    (Pubname, Address, Postcode) \n    SELECT 'The Bull', 'Antioch Road','CB2 5TY' \n    <\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"fifth\">Creating Excel Spreadsheets using <code>sp_makewebtask<\/code> <\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of creating the Excel spreadsheet with OLEDB One can use the <code>sp_makewebtask<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Users must have <code>SELECT<\/code> permissions to run a specified query and <code>CREATE PROCEDURE<\/code> permissions in the database in which the query will run. The SQL Server account must have permissions to write the generated HTML document to the specified location. Only members of the sysadmin server role can impersonate other users.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">    sp_makewebtask @outputfile = 'c:\\CambridgePubsHTML2.xls', \n      @query = 'Select * from ##CambridgePubs', \n      @colheaders =1, \n        @FixedFont=0,@lastupdated=0,@resultstitle='Cambridge Pubs', \n      @dbname ='MyDatabaseName' \n    <\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is fine for distributing information from databases but no good if you subsequently want to open it via ODBC.<\/p>\n\n\n\n<h2 class=\"wp-block-heading sixth\">OLE Automation <\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">So far, so good. However, we really want rather more than this. When we create an Excel file for a business report, we want the data and we also want nice formatting, defined ranges, sums, calculated fields and pretty graphs. If we do financial reporting, we want a pivot table and so on in order to allow a degree of data mining by the recipient. A different approach is required.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We can, of course, use Excel to extract the data from the database. However, in this example, we&#8217;ll create a spreadsheet, write the data into it, fit the columns nicely and define a range around the data.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">    *\/ ALTER PROCEDURE [dbo].[spDMOExportToExcel] ( \n    @SourceServer VARCHAR(30), \n    @SourceUID VARCHAR(30)=NULL, \n    @SourcePWD VARCHAR(30)=NULL, \n    @QueryText VARCHAR(200), \n    @filename VARCHAR(100), \n    @WorksheetName VARCHAR(100)='Worksheet', \n    @RangeName VARCHAR(80)='MyRangeName' \n    ) \n    AS \n    DECLARE @objServer INT, \n    @objQueryResults INT, \n    @objCurrentResultSet INT, \n    @objExcel INT, \n    @objWorkBooks INT, \n    @objWorkBook INT, \n    @objWorkSheet INT, \n    @objRange INT, \n    @hr INT, \n    @Columns INT, \n    @Rows INT, \n    @Output INT, \n    @currentColumn INT, \n    @currentRow INT, \n    @ResultSetRow INT, \n    @off_Column INT, \n    @off_Row INT, \n    @command VARCHAR(255), \n    @ColumnName VARCHAR(255), \n    @value VARCHAR(255), \n    @strErrorMessage VARCHAR(255), \n    @objErrorObject INT, \n    @Alphabet VARCHAR(27) \n    \n\n    SELECT @Alphabet='ABCDEFGHIJKLMNOPQRSTUVWXYZ' \n    \n\n    IF @QueryText IS NULL \n        BEGIN \n        RAISERROR ('A query string is required for spDMOExportToExcel',16,1) \n        RETURN 1 \n        END \n    \n\n    -- Sets the server to the local server \n    IF @SourceServer IS NULL SELECT @SourceServer = @@servername \n    \n\n    SET NOCOUNT ON \n    \n\n    SELECT @strErrorMessage = 'instantiating the DMO', \n        @objErrorObject=@objServer \n    EXEC @hr= sp_OACreate 'SQLDMO.SQLServer', @objServer OUT \n    \n\n    IF @SourcePWD IS NULL OR @SourceUID IS NULL \n        BEGIN \n        --use a trusted connection \n        IF @hr=0 SELECT @strErrorMessage= \n        'Setting login to windows authentication on ' \n        +@SourceServer, @objErrorObject=@objServer \n        IF @hr=0 EXEC @hr=sp_OASetProperty @objServer, 'LoginSecure', 1 \n        IF @hr=0 SELECT @strErrorMessage= \n        'logging in to the requested server using windows authentication on ' \n            +@SourceServer \n        IF @SourceUID IS NULL AND @hr=0 EXEC @hr=sp_OAMethod @objServer, \n            'Connect', NULL, @SourceServer \n        IF @SourceUID IS NOT NULL AND @hr=0 \n            EXEC @hr=sp_OAMethod \n                @objServer, 'Connect', NULL, @SourceServer ,@SourceUID \n        END \n    ELSE \n        BEGIN \n        IF @hr=0 \n           SELECT @strErrorMessage = 'Connecting to '''+@SourceServer+ \n                                  ''' with user ID '''+@SourceUID+'''', \n                  @objErrorObject=@objServer \n        IF @hr=0 \n            EXEC @hr=sp_OAMethod @objServer, 'Connect', NULL, \n                @SourceServer, @SourceUID, @SourcePWD \n        END \n    \n\n    --now we execute the query \n    IF @hr=0 SELECT @strErrorMessage='executing the query \"' \n            +@querytext+'\", on '+@SourceServer, \n            @objErrorObject=@objServer, \n            @command = 'ExecuteWithResults(\"' + @QueryText + '\")' \n    IF @hr=0 \n        EXEC @hr=sp_OAMethod @objServer, @command, @objQueryResults OUT \n    \n\n    IF @hr=0 \n         SELECT @strErrorMessage='getting the first result set for \"' \n            +@querytext+'\", on '+@SourceServer, \n            @objErrorObject=@objQueryResults \n    IF @hr=0 EXEC @hr=sp_OAMethod \n        @objQueryResults, 'CurrentResultSet', @objCurrentResultSet OUT \n    IF @hr=0 \n        SELECT @strErrorMessage='getting the rows and columns \"' \n            +@querytext+'\", on '+@SourceServer \n    IF @hr=0 \n        EXEC @hr=sp_OAMethod @objQueryResults, 'Columns', @Columns OUT \n    IF @hr=0 \n        EXEC @hr=sp_OAMethod @objQueryResults, 'Rows', @Rows OUT \n    \n\n    --so now we have the queryresults. We start up Excel \n    IF @hr=0 \n        SELECT @strErrorMessage='Creating the Excel Application, on ' \n            +@SourceServer, @objErrorObject=@objExcel \n    IF @hr=0 \n        EXEC @hr=sp_OACreate 'Excel.Application', @objExcel OUT \n    IF @hr=0 SELECT @strErrorMessage='Getting the WorkBooks object ' \n    IF @hr=0 \n        EXEC @hr=sp_OAGetProperty @objExcel, 'WorkBooks', \n            @objWorkBooks OUT \n    --create a workbook \n    IF @hr=0 \n        SELECT @strErrorMessage='Adding a workbook ', \n            @objErrorObject=@objWorkBooks \n    IF @hr=0 \n        EXEC @hr=sp_OAGetProperty @objWorkBooks, 'Add', @objWorkBook OUT \n    \n\n    --and a worksheet \n    IF @hr=0 \n        SELECT @strErrorMessage='Adding a worksheet ', \n            @objErrorObject=@objWorkBook \n    IF @hr=0 \n        EXEC @hr=sp_OAGetProperty @objWorkBook, 'worksheets.Add', \n            @objWorkSheet OUT \n    \n\n    IF @hr=0 \n        SELECT @strErrorMessage='Naming a worksheet as \"' \n            +@WorksheetName+'\"', @objErrorObject=@objWorkBook \n    IF @hr=0  \n        EXEC @hr=sp_OASetProperty @objWorkSheet, 'name', @WorksheetName \n    \n\n    SELECT @currentRow = 1 \n    \n\n    --so let's write out the column headings \n    SELECT @currentColumn = 1 \n    WHILE (@currentColumn &lt;= @Columns AND @hr=0) \n            BEGIN \n            IF @hr=0 \n                SELECT @strErrorMessage='getting column heading ' \n                                        +LTRIM(STR(@currentcolumn)) , \n                    @objErrorObject=@objQueryResults, \n                    @Command='ColumnName(' \n                                +CONVERT(VARCHAR(3),@currentColumn)+')' \n            IF @hr=0 EXEC @hr=sp_OAGetProperty @objQueryResults, \n                                                @command, @ColumnName OUT \n            IF @hr=0 \n                SELECT @strErrorMessage='assigning the column heading '+ \n                  + LTRIM(STR(@currentColumn)) \n                  + ' from the query string', \n                @objErrorObject=@objExcel, \n                @command='Cells('+LTRIM(STR(@currentRow)) +', ' \n                                    + LTRIM(STR(@CurrentColumn))+').value' \n            IF @hr=0 \n                EXEC @hr=sp_OASetProperty @objExcel, @command, @ColumnName \n            SELECT @currentColumn = @currentColumn + 1 \n            END \n    \n\n    --format the headings in Bold nicely \n    IF @hr=0 \n        SELECT @strErrorMessage='formatting the column headings in bold ', \n            @objErrorObject=@objWorkSheet, \n            @command='Range(\"A1:'\n                +SUBSTRING(@alphabet,@currentColumn\/26,1) \n                +SUBSTRING(@alphabet,@currentColumn % 26,1)\n                +'1'+'\").font.bold' \n    IF @hr=0 EXEC @hr=sp_OASetProperty @objWorkSheet, @command, 1 \n    --now we write out the data \n    \n\n    SELECT @currentRow = 2 \n    WHILE (@currentRow &lt;= @Rows+1 AND @hr=0) \n        BEGIN \n        SELECT @currentColumn = 1 \n        WHILE (@currentColumn &lt;= @Columns AND @hr=0) \n            BEGIN \n            IF @hr=0 \n                SELECT \n                @strErrorMessage= \n                    'getting the value from the query string' \n                    + LTRIM(STR(@currentRow)) +',' \n                    + LTRIM(STR(@currentRow))+')', \n                @objErrorObject=@objQueryResults, \n                @ResultSetRow=@CurrentRow-1 \n            IF @hr=0 \n                EXEC @hr=sp_OAMethod @objQueryResults, 'GetColumnString', \n                    @value OUT, @ResultSetRow, @currentColumn \n            IF @hr=0 \n                SELECT @strErrorMessage= \n                        'assigning the value from the query string' \n                    + LTRIM(STR(@CurrentRow-1)) +', ' \n                    + LTRIM(STR(@currentcolumn))+')' , \n                    @objErrorObject=@objExcel, \n                    @command='Cells('+STR(@currentRow) +', ' \n                                        + STR(@CurrentColumn)+').value' \n            IF @hr=0 \n                EXEC @hr=sp_OASetProperty @objExcel, @command, @value \n            SELECT @currentColumn = @currentColumn + 1 \n            END \n        SELECT @currentRow = @currentRow + 1 \n        END \n    --define the name range \n    --Cells(1, 1).Resize(10, 5).Name = \"TheData\" \n    IF @hr=0 SELECT @strErrorMessage='assigning a name to a range ' \n            + LTRIM(STR(@CurrentRow-1)) +', ' \n            + LTRIM(STR(@currentcolumn-1))+')' , \n        @objErrorObject=@objExcel, \n        @command='Cells(1, 1).Resize('+STR(@currentRow-1) +', ' \n                                        + STR(@CurrentColumn-1)+').Name' \n    IF @hr=0 EXEC @hr=sp_OASetProperty @objExcel, @command, @RangeName \n    \n\n    --Now autofilt the columns we've written to \n    IF @hr=0 SELECT @strErrorMessage='Auto-fit the columns ', \n                @objErrorObject=@objWorkSheet, \n                @command='Columns(\"A:'\n                    +SUBSTRING(@alphabet,(@Columns \/ 26),1) \n                    +SUBSTRING(@alphabet,(@Columns % 26),1)+ \n                    '\").autofit' \n    \n\n    IF @hr=0 --insert into @bucket(bucket) \n            EXEC @hr=sp_OAMethod @objWorkSheet, @command, @output out \n    \n\n    \n\n    IF @hr=0 SELECT @command ='del \"' + @filename + '\"' \n    IF @hr=0 EXECUTE master..xp_cmdshell @Command, no_output \n    IF @hr=0 \n        SELECT @strErrorMessage='Saving the workbook as \"'+@filename+'\"', \n            @objErrorObject=@objRange, \n            @command = 'SaveAs(\"' + @filename + '\")' \n    IF @hr=0 EXEC @hr=sp_OAMethod @objWorkBook, @command \n    IF @hr=0 SELECT @strErrorMessage='closing Excel ', \n            @objErrorObject=@objExcel \n    EXEC @hr=sp_OAMethod @objWorkBook, 'Close' \n    EXEC sp_OAMethod @objExcel, 'Close' \n    \n\n    IF @hr&lt;&gt;0 \n        BEGIN \n        DECLARE \n            @Source VARCHAR(255), \n            @Description VARCHAR(255), \n            @Helpfile VARCHAR(255), \n            @HelpID INT \n        \n        EXECUTE sp_OAGetErrorInfo @objErrorObject, \n            @source output,@Description output,\n            @Helpfile output,@HelpID output \n        SELECT @hr, @source, @Description,@Helpfile,@HelpID output \n        SELECT @strErrorMessage='Error whilst ' \n                +COALESCE(@strErrorMessage,'doing something') \n                +', '+COALESCE(@Description,'') \n        RAISERROR (@strErrorMessage,16,1) \n        END \n    EXEC sp_OADestroy @objServer \n    EXEC sp_OADestroy @objQueryResults \n    EXEC sp_OADestroy @objCurrentResultSet \n    EXEC sp_OADestroy @objExcel \n    EXEC sp_OADestroy @objWorkBookks \n    EXEC sp_OADestroy @objWorkBook \n    EXEC sp_OADestroy @objRange \n    RETURN @hr<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>GO<\/code><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">    <\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Now we can create our pubs spreadsheet, and can do it from any of our servers.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">    spDMOExportToExcel @SourceServer='MyServer', \n    @SourceUID= 'MyUserID', \n    @SourcePWD = 'MyPassword', \n    @QueryText = 'use MyDatabase \n    select Pubname, Address, Postcode from ##CambridgePubs', \n    @filename = 'C:\\MyPubDatabase.xls', \n    @WorksheetName='MyFavouritePubs', \n    @RangeName ='MyRangeName' \n    <\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Or if you are using integrated security!<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">    spDMOExportToExcel @SourceServer='MyServer', \n    @QueryText = 'use MyDatabase \n    select Pubname, Address, Postcode from ##CambridgePubs', \n    @filename = 'C:\\MyPubDatabase.xls', \n    @WorksheetName='MyFavouritePubs', \n    @RangeName ='MyRangeName' \n    <\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Although this is a very handy stored procedure, you&#8217;ll probably need to modify and add to it for particular purposes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We use the DMO method because we like to dump build data into Excel spreadsheets e.g. users, logins, Job Histories. However, an ADODB version is very simple to do and can be made much faster for reads and writes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We have just inserted values, but you can insert formulae and formatting numberformat) and create or change borders. You can, in fact, manipulate the spreadsheet in any way you like. When we do this, we record macros in Excel and then convert these macros to TSQL! Using the above example, it should be simple.<\/p>\n\n\n\n<section id=\"my-first-block-block_35f040580cd1a022fe32a7e3bc2baf4a\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Fast, reliable and consistent SQL Server development&#8230;<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            &#8230;with SQL Toolbelt Essentials. 10 ingeniously simple tools for accelerating development, reducing risk, and standardizing workflows.                                    <\/div>\n            <\/div>\n                                            <a href=\"https:\/\/www.red-gate.com\/products\/sql-toolbelt-essentials\/\" class=\"btn btn--secondary btn--lg\" aria-label=\"Learn more &amp; try for free: Fast, reliable and consistent SQL Server development...\">Learn more &amp; try for free<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. What&#039;s the easiest way to move SQL Server query results into Excel?<\/h3>\n            <div class=\"faq-answer\">\n                <p dir=\"ltr\">For a quick, one-off export, <code>sp_makewebtask<\/code> generates an HTML file that Excel can open directly \u2014 no linked server or OLE Automation setup required. It&#8217;s fine for distributing data but won&#8217;t give you formatting, formulas, or a file that reopens cleanly via ODBC.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. Should I use a linked server or OPENROWSET\/OPENDATASOURCE to read Excel data from SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p dir=\"ltr\">A linked server is better when you&#8217;ll query the same Excel file repeatedly, since you set it up once and reference it like a table. <code>OPENROWSET<\/code> and <code>OPENDATASOURCE<\/code> are better for one-off or ad hoc reads and writes where creating a permanent linked server isn&#8217;t worth it.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. Is OLE Automation safe to use for exporting to Excel in production?<\/h3>\n            <div class=\"faq-answer\">\n                <p dir=\"ltr\">Only with care. OLE Automation and <code>xp_cmdshell<\/code> should run under a dedicated, tightly-permissioned account \u2014 ideally via a job queue \u2014 since anyone with access to that account could extract or overwrite whatever the procedure can reach.<\/p>\n            <\/div>\n            <\/section>\n","protected":false},"excerpt":{"rendered":"<p>Four T-SQL techniques for moving data between SQL Server and Excel: linked servers, OPENROWSET, sp_makewebtask, and OLE Automation for formatted reports.&hellip;<\/p>\n","protected":false},"author":221812,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":true,"footnotes":""},"categories":[143523,143524,143531],"tags":[4168,4170,4150,4151,4183,4252],"coauthors":[6813,6814],"class_list":["post-221","post","type-post","status-publish","format-standard","hentry","category-databases","category-sql-server","category-t-sql-programming-sql-server","tag-database","tag-database-administration","tag-sql","tag-sql-server","tag-t-sql","tag-t-sql-programming"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/221","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/221812"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=221"}],"version-history":[{"count":11,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/221\/revisions"}],"predecessor-version":[{"id":113195,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/221\/revisions\/113195"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=221"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=221"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=221"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=221"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}