{"id":1321,"date":"2012-04-13T00:00:00","date_gmt":"2012-04-13T00:00:00","guid":{"rendered":"https:\/\/test.simple-talk.com\/uncategorized\/the-tsql-of-csv-comma-delimited-of-errors\/"},"modified":"2026-04-14T17:20:13","modified_gmt":"2026-04-14T17:20:13","slug":"the-tsql-of-csv-comma-delimited-of-errors","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/the-tsql-of-csv-comma-delimited-of-errors\/","title":{"rendered":"Working with CSV Files in SQL Server: ODBC, Linked Server, and T-SQL Parsing"},"content":{"rendered":"<div id=\"pretty\">\n<h2>Executive Summary<\/h2>\n<p><strong>SQL Server can read and write CSV files using three main approaches: the ODBC Text Driver (simplest &#8211; treats the CSV file as a queryable data source), a linked server (most flexible &#8211; allows CSV data to be queried like any other linked database), and a pure T-SQL CSV parser (no external dependencies &#8211; useful in restricted environments). Each approach has different performance and flexibility trade-offs. This article covers all three with working T-SQL code, explains the underappreciated nuances of the CSV format specification, and reviews the common errors that arise from treating CSV as simpler than it is.<\/strong><\/p>\n<h2>Introduction<\/h2>\n<p>This article is about using &#8216;Comma-Separated Values&#8217; (CSV) format in SQL Server.\u00a0 You&#8217;ll come across CSV if you&#8217;re\u00a0 a SQL Server developer when you are importing, or exporting, tabular data from &#8216;foreign&#8217; applications, data feeds, or the wilder fringe of databases. Exporting via CSV is useful, since CSV is generally supported by even the most archaic legacy systems. We&#8217;ll\u00a0 demonstrate a few tricks such as that of saving the tables and views of AdventureWorks to CSV format, parsing a CSV file to a table, converting CSV\u00a0 to XML and JSON and so on.<\/p>\n<p>Although we&#8217;ll cover some of the many ways of manipulating CSV in this article, we&#8217;ll be concentrating more on using ODBC, since it is pretty fast, versatile, and reliable, and is easy to access from TSQL.<\/p>\n<h2>The CSV Format<\/h2>\n<p>There is nothing much wrong with the CSV file format, beyond the neglect it suffers. It&#8217;s been around far longer than MSDOS. When properly implemented, It is the most efficient available way of representing the contents of a data table in a readable document, and is therefore very effective in transferring such data between different applications or platforms. Unlike JSON or XML, it is designed for the single purpose of representing tabular data. It doesn&#8217;t do hierarchical data, and it was designed assuming that both ends of the data transfer process understood what each column represented, as well as the data type, collation and character set being used.\u00a0 If it is done properly, it can transfer any type of tabular data reliably.<\/p>\n<p>We come across CSV files in SSIS, Excel, ODBC, SQLCMD and even in PowerShell.\u00a0 A Faulty implementation, nicknamed &#8216;comedy-limited&#8217;, exists in BCP. The term &#8216;CSV format&#8217; is not the correct description of the practice of using commas or other &#8216;special&#8217; characters,\u00a0 to separate data elements in some lists, this generally applies only to &#8216;private&#8217; data where there is a guarantee that the delimiting character does not appear within the data.<\/p>\n<p>If CSV transfers fail, it is generally because of mistakes in the implementation.\u00a0<a href=\"http:\/\/tools.ietf.org\/html\/rfc4180\">RFC 4180<\/a> gives a good description of what it is supposed to do. It is easy to describe:<\/p>\n<p>Each row, or tuple, is separated by a linefeed, and the last line can be terminated by a linefeed. Each line should contain the same number of fields, which are separated by a single character, usually a comma. Fields may be enclosed in double-quote characters. If they are, then fields may contain commas or linefeed characters. They can also contain double-quote characters if &#8216;escaped&#8217; with a second adjacent double-quote character. Null data values are denoted by two delimiters in a row with no data between them. Character strings in a delimited text line can be enclosed in double quotation marks (&#8220;&#8221;). No blanks can occur before or after delimited values<\/p>\n<p>The first line of the file may be a header line which contains the name of each column. There may be an initial &#8216;comment&#8217; line initiated by a hash (#) character, and this is either terminated by a linefeed character or, if followed by a double-quote, until a subsequent unescaped\u00a0 double-quote, followed by a linefeed character.<\/p>\n<p>CSV is unusual in that it encloses the entire field in double quotes if it contains the separator. If the field contains double quotes, it must be &#8216;escaped&#8217; by a second double quotes so that all double quotes in the field are repeated twice to indicate that they don&#8217;t end the field. However, If\u00a0 a repeated double-quote are the only two characters within the field, this indicates a field containing an empty string, which is quite different from NULL, signifying an unknown value.<\/p>\n<p>Because the rules are underspecified, different implementations diverge in their handling of edge cases. You will find in some products that continuation lines are supported by starting the last field of the line with an un-terminated double-quote. Microsoft, surprisingly, sometimes has incompatible versions of CSV files between its own applications, and has even had incompatible CSV files in different versions of the same application<\/p>\n<p>There is a good formal grammar for producing a CSV reader or writer<a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/windows\/desktop\/ms716438(v=vs.85).aspx\"> here<\/a>.<\/p>\n<h2>A quick tour of some of the alternatives<\/h2>\n<p>You are generally struck with one of four alternative requirements:<\/p>\n<ul>\n<li>Pushing data into a SQL Server table from a file via an external process.<\/li>\n<li>Pulling data out of a SQL Server table to a file via an external application<\/li>\n<li>pushing data from a SQL Server table to a file via TSQL<\/li>\n<li>Pulling data out of a file to a SQL Server table via TSQL.<\/li>\n<\/ul>\n<p>It isn&#8217;t always as easy as you&#8217;d expect.<\/p>\n<p>One obvious problem comes from values that aren&#8217;t delimited with double-quotes. Often, programmers assume that numbers don&#8217;t need delimiters. Unfortunately, parts of Europe use the comma as a &#8216;decimal point&#8217; in money, and some representations of large integer values have a comma as &#8216;thousands&#8217; separators. This sort of mistake is common to find, but It is not the CSV convention that is at fault, but the implementation. The easiest way to test, and often break, a poor implementation of CSV is to put the linefeed characters into a string value. With BCP, its defects are even less subtle. Even putting a comma into a properly delimited string breaks the import. For the demonstration of this, you&#8217;ll need the <code>spSaveTextToFile<\/code> procedure from The TSQL of Text.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">--Create a simple test for interpreting CSV (From Wikipedia)\nDECLARE @TestCSVFileFromWikipedia VARCHAR(MAX)\n--put CSV into a variable\nSELECT @TestCSVFileFromWikipedia='Year,Make,Model,Description,Price\n1997,Ford,E350,\"ac, abs, moon\",3000.00\n1999,Chevy,\"Venture \"\"Extended Edition\"\"\",\"\",4900.00\n1999,Chevy,\"Venture \"\"Extended Edition, Very Large\"\"\",\"\",5000.00\n1996,Jeep,Grand Cherokee,\"MUST SELL!\nair, moon roof, loaded\",4799.00'\n--write it to disk (Source of procedure in the\u00a0 article 'The TSQL of Text'\nEXECUTE philfactor.dbo.spSaveTextToFile\n\u00a0 @TestCSVFileFromWikipedia,'d:\\files\\TestCSV.csv',0\n\u00a0\n--create a table to read it into\nCREATE TABLE TestCSVImport ([Year] INT, Make VARCHAR(80), Model VARCHAR(80), [Description] VARCHAR(80), Price money)\nBULK INSERT TestCSVImport FROM 'd:\\files\\TestCSV.csv' \nWITH ( FIELDTERMINATOR = ',', ROWTERMINATOR = '\\n', FirstRow=2)\n--No way. This is merely using commas to delimit. \n--BCP can't import a CSV file!\nGO\n\u00a0\n--whereas\nINSERT INTO TestCSVImport\nSELECT *\nFROM\n\u00a0\u00a0\u00a0 OPENROWSET('MSDASQL',--provider name (ODBC)\n\u00a0\u00a0\u00a0\u00a0 'Driver={Microsoft Text Driver (*.txt; *.csv)};\n\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0DEFAULTDIR=d:\\files;Extensions=CSV;',--data source\n<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1475-clip_image001.png\" alt=\"1475-clip_image001.png\" width=\"568\" height=\"100\" \/><\/p>\n<p>Much better, but you&#8217;ll see that we possibly suffer from the vagueness of the CSV spec, as I reckon that those descriptions should be blank. There is a great difference between a NULL and an empty string.<\/p>\n<p>So let&#8217;s try to read it in via TSQL . We&#8217;ll get to the details of this code later on in the article.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">Declare @CSVFileContents Varchar(MAX)\nSELECT @CSVFileContents = BulkColumn\nFROM\u00a0 OPENROWSET(BULK 'd:\\files\\TestCSV.csv', SINGLE_BLOB) AS x\u00a0 \n\u00a0\nCREATE TABLE AnotherTestCSVImport ([Year] INT, Make VARCHAR(80), \n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Model VARCHAR(80), [Description] VARCHAR(80), Price money)\nINSERT INTO AnotherTestCSVImport\n\u00a0\u00a0\u00a0 Execute CSVToTable @CSVFileContents \n<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1475-clip_image002.png\" alt=\"1475-clip_image002.png\" width=\"568\" height=\"97\" \/><\/p>\n<p>Yes, this is correct.<\/p>\n<p>As well as &#8216;pulling&#8217; files into SQL Server via TSQL, we can &#8216;push&#8217; them via an external app. In the old days we&#8217;d use DTS or SSIS but this is very over-engineered for the purpose, and <a href=\"http:\/\/www.sqlservercentral.com\/articles\/powershell\/65196\/\">PowerShell makes this very easy<\/a> since one can read in a CSV file and then use \u00a0<code>Data.SqlClient.SqlBulkCopy <\/code>to insert the data into a SQL Server database table.<\/p>\n<pre class=\"theme:powershell-ise lang:ps decode:true \">#Thanks to Chad Miller and Marc van Orsouw\n$CSVfilenameAndPath='D:\\MyDirectory\\MyFilename.csv' \n$ServerInstance='MyServerName'\n$Database='MyDatabase'\n$TableToImportTo='MyTable'\n#####\nTrap {\n # Handle the error\n $err = $_.Exception\n write-host $err.Message\n while( $err.InnerException ) {\n $err = $err.InnerException\n write-host $err.Message\n };\n # End the script.\n break\n }\n$CSV=import-csv $CSVfilenameAndPath #Read the CSV file to a powershell object\n$datatable = new-object Data.datatable #we need a datatable to do a bulk copy with \n$ThisIsTheHeaderRow = $true #we the first row is the header thanks to Import_CSV\n#insert it row by agonizing row\nforeach ($tuple in $CSV) \n{ \n$CurrentRow = $datatable.NewRow() #create a new row\nforeach($property in $tuple.PsObject.get_properties()) # for each column...\n { \n if ($ThisIsTheHeaderRow) #if so we need to create a column 'object'\n { \n $Col = new-object Data.DataColumn \n $Col.ColumnName = $property.Name.ToString() \n if ($property.value) \n { \n if ($property.value -isnot [System.DBNull]) \n { $Col.DataType = $property.value.gettype() } \n } \n $datatable.Columns.Add($Col) #and actually add it to the datatable columns\n } \n if ($property.IsArray) \n { $CurrentRow.Item($property.Name) =$property.value | ConvertTo-XML -AS String -NoTypeInformation -Depth 1 } \n else { $CurrentRow.Item($property.Name) = $property.value } \n } \n$datatable.Rows.Add($CurrentRow) #and actually add it to the datatable rows\n$ThisIsTheHeaderRow = $false \n} \n#assemble the connection string\n$connectionString = \"Data Source=$ServerInstance;Integrated Security=true;Initial Catalog=$Database;\"\n$bulkCopy = new-object (\"Data.SqlClient.SqlBulkCopy\") $connectionString #cr4eate the BulkCopy\n$bulkCopy.DestinationTableName = $TableToImportTo\n$bulkCopy.WriteToServer($datatable) #write it all out\n'did I do well, master?\n<\/pre>\n<p>The reverse process is even easier. Here we use SMO, but there are a number of alternatives<\/p>\n<pre class=\"theme:powershell-ise lang:ps decode:true\">$ServerName='MyServer'# the server it is on\n$Database='MyDatabase' # the name of the database you want to script as objects\n$DirectoryToSaveTo='E:\\MyScriptsDirectory' # the client-side directory where you want to store them\n$TableOrView='TestCSVImport'\n#requires -version 2.0 \n$v = [System.Reflection.Assembly]::LoadWithPartialName( 'Microsoft.SqlServer.SMO')\nTrap {\n # Handle the error\n $err = $_.Exception\n write-host $err.Message\n while( $err.InnerException ) {\n $err = $err.InnerException\n write-host $err.Message\n };\n # End the script.\n break\n }\n$server = New-Object ( Microsoft.SqlServer.Management.Smo.Server ) $ServerName\n$dbase = New-Object ( Microsoft.SqlServer.Management.Smo.Database ) ($server, $Database)\n# what we do if there is a sql info message such as a PRINT message\n$handler = [System.Data.SqlClient.SqlInfoMessageEventHandler] {param($sender, $event) Write-Host $event.Message};\n$server.ConnectionContext.add_InfoMessage($handler);\n$result=$dbase.ExecuteWithResults(\"Select * from $TableOrView\")\n$result.Tables[0]| convertto-csv &gt;\"$DirectoryToSaveTo\\$TableOrView.csv\"\n\"All done, Mighty One.\"\n<\/pre>\n<p>This produces a file containing this.<\/p>\n<pre>#TYPE System.Data.DataRow\n\"Year\",\"Make\",\"Model\",\"Description\",\"Price\"\n\"1997\",\"Ford\",\"E350\",\"ac, abs, moon\",\"3000.0000\"\n\"1999\",\"Chevy\",\"Venture \"\"Extended Edition\"\"\",\"\",\"4900.0000\"\n\"1999\",\"Chevy\",\"Venture \"\"Extended Edition, Very Large\"\"\",\"\",\"5000.0000\"\n\"1996\",\"Jeep\",\"Grand Cherokee\",\"MUST SELL!\nair, moon roof, loaded\",\"4799.0000\"\n<\/pre>\n<p>Yes, it is correct. You can see that the people who devised the ConvertTo-CSV cmdlets played safe by delimiting every variable, which is fine but slightly more verbose than strictly necessary.<\/p>\n<p>Another utility that is great for pushing CSV data into SQL Server is the LogParser.\u00a0 This has become a cult application with websites dedicated to its use.\u00a0 Basically, it can treat any text-based data file, whether XML, CSV, tab-delimited or whatever, as a single-table database. You can do SELECT statements on it, and output the results to a number of different formats, including a table in SQL: Server. This can be run at the command line, or automated via COM. <a href=\"http:\/\/blogs.technet.com\/b\/heyscriptingguy\/archive\/2011\/11\/28\/four-easy-ways-to-import-csv-files-to-sql-server-with-powershell.aspx\">Chad Miller has published a number of solutions using it for importing CSV into SQL Server<\/a>. It is fast and reliable.<\/p>\n<table class=\"MsoTableClassic1\">\n<tbody>\n<tr>\n<td valign=\"top\"><i>\u00a0<\/i><\/td>\n<td valign=\"top\"><i>External process<\/i><\/td>\n<td valign=\"top\"><i>TSQL<\/i><\/td>\n<\/tr>\n<tr>\n<td valign=\"top\">From table to CSV file<\/td>\n<td valign=\"top\">PowerShell, SSIS, SQLCMD<\/td>\n<td valign=\"top\">OpenRowSet with ODBCtext driver (requiring Schema.ini and file creation), Linked Server (ditto)<\/td>\n<\/tr>\n<tr>\n<td valign=\"top\">From CSV file to table<\/td>\n<td valign=\"top\">LogParser , SSIS, PowerShell, Net via Data.SqlClient.SqlBulkCopy<\/td>\n<td valign=\"top\">OpenRowSet with ODBCtext driver), Linked Server (ditto)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>Reading from a CSV file into SQL Server via SQL<\/h2>\n<p>As we&#8217;ve seen, the CSV driver doesn&#8217;t perform too badly.\u00a0 In fact using the ODBC Text Driver is usually the most convenient way to read a CSV file into, or out of, SQL Server. (we&#8217;d actually be using the Microsoft Jet IISAM driver under the covers) . BCP as a means of reading CSV is broken. If you need to use BCP (or FILE IMPORT) use native mode for tabular data, but don&#8217;t expect any other system to be able to read it.<\/p>\n<p>The ODBC driver can use an external &#8216;schema.ini&#8217; format file to control the details and, if necessary, specify the column headers and the data type formats and conversions. You will need to add &#8216;DBQ=C:\\;&#8217; to the connection string to specify the location of this, unless you<a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/ms709353.aspx\"> store it in the same directory as the data.<\/a><\/p>\n<p>If you are lucky enough to have ad-hoc distributed queries allowed on your server ..<\/p>\n<pre class=\"theme:ssms2012 lang:tsql \">EXEC sp_configure 'Ad Hoc Distributed Queries', '1';\nReconfigure\n<\/pre>\n<p>..then you can treat a CSV file as a quasi-table.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">SELECT *\nFROM\n\u00a0\u00a0\u00a0\u00a0 OPENROWSET('MSDASQL',--provider name (ODBC)\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0'Driver={Microsoft Text Driver (*.txt; *.csv)};\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0DEFAULTDIR=C:\\;Extensions=CSV;',--data source\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0'SELECT * FROM sample.csv')\n<\/pre>\n<p>This is fine, and you can alter the provider connection string in a number of ways to specify whether it is tab delimited or CSV, and whether there are headers in the file or not. If there aren&#8217;t, you need to get the information about the column names from somewhere else, and that &#8216;somewhere else&#8217; is the &#8216;schema.ini&#8217; file. The ODBC text driver can use an external &#8216;<a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/windows\/desktop\/ms709353(v=vs.85).aspx\">schema.ini&#8217;<\/a> format file to control the details and, if necessary, specify the column headers and the <a href=\"http:\/\/support.microsoft.com\/kb\/210073\">data type formats and conversions<\/a>. You will need to add &#8216;DBQ=C:\\;&#8217; to the connection string to specify the location of this, unless you<a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/ms709353.aspx\"> store it in the same directory as the data.<\/a> The driver will look in the directory you&#8217;ve specified, and look for a &#8216;Schema.ini&#8217; file there. If it is there, it looks up a section with the same name as the file you want to read from and will pull out the details it needs.<\/p>\n<p>This &#8216;Schema.ini&#8217; file stores the metadata. It gives you a lot of control over the format. You can specify whether your text files are CSV, Tab delimited or fixed-column in format. You have a lot of control over date and currency formats too. If you haven&#8217;t got a header line in your CSV file, you can specify the column names and also the data type. Unfortunately, these types are not SQL types, but JET types from way back, and it is tricky, but possible, to get a match for such things as varbinary or xml SQL Server Datatypes. The Text ODBC driver has a GUI way of creating such schema.ini files, but it uses only a subset of the JET data types that are available in the driver, and has left out the vital &#8216;decimal, or numeric, type.<\/p>\n<p>Fortunately, if we are using this ODBC driver to write out CSV files of tables or views, we can generate this file in TSQL, from the information schema for whatever database you are using. Each table or view can have an entry in the one .INI file, and we can store all the files in the one directory. We&#8217;ll show you how to do this in this article. Once we have done this, we will have a text-based database that we can write to or read from.<\/p>\n<h2>Using a linked Server<\/h2>\n<p>Usually, the best way to interact with any ODBC source is to treat it as a linked database. This takes a bit of effort with CSV, but is worth doing because it makes the process of creating CSV reports very easy, because you are dealing with the files as if they were tables. This means that you can use the CSV as the &#8216;staging&#8217; tables for import, and merely use UPDATE statements\u00a0 to do the refreshing . For outputting reports, I generally create ordinary views that give the format I want, and then create the reports as linked tables in the &#8216;CSV database&#8217;. This means that you can keep these reports up to date as a background task, and just make copies as and when you need them.<\/p>\n<p>I&#8217;ve created a stored procedure that keeps all the messy details from you. You just set it going and get on with something else.<\/p>\n<p>Here, we are writing a file out to a directory on the server, setting up a linked server, and writing out the table &#8216;CrudeOilProduction&#8217; to it. We leave things with the linked server still in place.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">Use MyDatabase\nExecute SaveViewsOrTablesToCSV \n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 @ServerDirectory='D:\\files\\OilAnalysis',\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 @NameOfLinkedServer='OilAnalysis',\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 @TablesOrViews='CrudeOilProduction'\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 @Database='GlobalWarming'\n\u00a0\n<\/pre>\n<p>And we can then read it in with something like this.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">Execute ReadViewsOrTablesFromAttachedCSVDatabase\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 @NameOfLinkedServer='OilAnalysis',\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 @TablesOrViews='CrudeOilProduction'\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 @Database='GlobalWarming'\n<\/pre>\n<p>Generally, you&#8217;ll just want to do this with views, but just to demonstrate what is possible, (and we squeezed out a few unexpected bugs in the process) we&#8217;ll create the entire AdventureWorks CSV database, views and tables, and fill them<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">Execute SaveViewsOrTablesToCSV \n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 @ServerDirectory = 'd:\\textVersionOfAdventureWorks',\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 @NameOfLinkedServer= 'AdventureCSV',\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 @TablesOrViews ='%',\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 @Database='AdventureWorks'\n<\/pre>\n<p>This routine is setting up a linked server called &#8216;AdventureCSV.\u00a0 It is then finding out all the tables that conform to your wildcard specification, and the information about the columns within those tables. It is then creating all the tables as files within a directory on disk whose path is &#8216;d:\\textVersionOfAdventureWorks&#8217;. Having done that, it is then creating a SCHEMA.INI file which it writes to the same directory. In the schema, it puts all the metadata about the tables or views, mapping the SQL Server datatypes to the ODBC (Jet 4) datatypes. It then fills the CSV files with data, using SQL Expressions, and inserting into the files as if they were tables. Finally, it saves all the SQL to disk so that you can do an autopsy on the result to check for errors.<\/p>\n<p>Here is part of the results, on a server directory<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1475-clip_image003.png\" alt=\"1475-clip_image003.png\" width=\"503\" height=\"518\" \/><\/p>\n<p>And here is an excerpt of the machine-generated .ini file&#8230;<\/p>\n<pre>[Sales_Store.csv]\n\u00a0\u00a0 ColNameHeader = False\n\u00a0\u00a0 Format = CSVDelimited\n\u00a0\u00a0 CharacterSet = ANSI\n\u00a0\u00a0 Col1=CustomerID Integer\n\u00a0\u00a0 Col2=Name Char\u00a0 width 50\n\u00a0\u00a0 Col3=SalesPersonID Integer\n\u00a0\u00a0 Col4=Demographics LongChar\n\u00a0\u00a0 Col5=rowguid char width 40\n\u00a0\u00a0 Col6=ModifiedDate DateTime\n[Production_ProductPhoto.csv]\n\u00a0\u00a0 ColNameHeader = False\n\u00a0\u00a0 Format = CSVDelimited\n\u00a0\u00a0 CharacterSet = ANSI\n\u00a0\u00a0 Col1=ProductPhotoID Integer\n\u00a0\u00a0 Col2=ThumbNailPhoto Longchar\n\u00a0\u00a0 Col3=ThumbnailPhotoFileName Char\u00a0 width 50\n\u00a0\u00a0 Col4=LargePhoto Longchar\n\u00a0\u00a0 Col5=LargePhotoFileName Char\u00a0 width 50\n\u00a0\u00a0 Col6=ModifiedDate DateTime\n[Production_ProductProductPhoto.csv]\n\u00a0\u00a0 ColNameHeader = False\n\u00a0\u00a0 Format = CSVDelimited\n\u00a0\u00a0 CharacterSet = ANSI\n\u00a0\u00a0 Col1=ProductID Integer\n\u00a0\u00a0 Col2=ProductPhotoID Integer\n\u00a0\u00a0 Col3=Primary Byte\n\u00a0\u00a0 Col4=ModifiedDate DateTime\n[Sales_StoreContact.csv]\n\u00a0\u00a0 ColNameHeader = False\n\u00a0\u00a0 Format = CSVDelimited\n\u00a0\u00a0 CharacterSet = ANSI\n\u00a0\u00a0 Col1=CustomerID Integer\n\u00a0\u00a0 Col2=ContactID Integer\n\u00a0\u00a0 Col3=ContactTypeID Integer\n\u00a0\u00a0 Col4=rowguid char width 40\n\u00a0\u00a0 Col5=ModifiedDate DateTime\n\u00a0\n<\/pre>\n<p>&#8230;along with some of the machine-generated insert statements!<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">INSERT INTO AdventureCSV...Sales_Store#csv([CustomerID],[Name],[SalesPersonID],[Demographics],[rowguid],[ModifiedDate])\n\u00a0 Select [CustomerID], [Name], [SalesPersonID], convert(NVARCHAR(MAX),[Demographics]), [rowguid], [ModifiedDate]\n\u00a0 FROM AdventureWorks.Sales.Store\n\nINSERT INTO AdventureCSV...Production_ProductPhoto#csv([ProductPhotoID],[ThumbNailPhoto],[ThumbnailPhotoFileName],[LargePhoto],[LargePhotoFileName],[ModifiedDate])\n\u00a0 Select [ProductPhotoID], convert(NVARCHAR(MAX),[ThumbNailPhoto]), [ThumbnailPhotoFileName], convert(NVARCHAR(MAX),[LargePhoto]), [LargePhotoFileName], [ModifiedDate]\n\u00a0 FROM AdventureWorks.Production.ProductPhoto\n\nINSERT INTO AdventureCSV...Production_ProductProductPhoto#csv([ProductID],[ProductPhotoID],[Primary],[ModifiedDate])\n\u00a0 Select [ProductID], [ProductPhotoID], [Primary], [ModifiedDate]\n\u00a0 FROM AdventureWorks.Production.ProductProductPhoto\n\n<\/pre>\n<p>OK, it took five minutes to run, which is a bit extreme.\u00a0 BCP takes around two minutes on the same task using native mode, but, of course doesn&#8217;t do CSV properly.<\/p>\n<p>The source for these routines can be downloaded at the bottom of the article.<\/p>\n<h2>Handling CSV Using TSQL<\/h2>\n<p>You can parse CSV using nothing other than SQL. I decided to demonstrate this with a parser that writes to a hierarchy file that I&#8217;ve already described in another <a href=\"http:\/\/www.simple-talk.com\/sql\/t-sql-programming\/consuming-json-strings-in-sql-server\/\">Consuming JSON Strings in SQL Server<\/a>. Just occasionally, you&#8217;ll find that you have to deal with data represented by a CSV representation within a string. Well, it has happened to me! I have created a function that returns a hierarchy table and another one that just converts it into a standard SQL result. The only real point of returning it as a hierarchy table is that the same table gets returned whatever the CSV, and also that you can then turn it into JSON or XML if you get the urge.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">DECLARE @MyHierarchy Hierarchy, @XML XML\nINSERT INTO @myHierarchy\n\u00a0\u00a0 Select * from parseCSV('Year,Make,Model,Description,Price\n1997,Ford,E350,\"ac, abs, moon\",3000.00\n1999,Chevy,\"Venture \"\"Extended Edition\"\"\",\"\",4900.00\n1999,Chevy,\"Venture \"\"Extended Edition, Very Large\"\"\",\"\",5000.00\n1996,Jeep,Grand Cherokee,\"MUST SELL!\nair, moon roof, loaded\",4799.00', Default,Default,Default)\nSELECT dbo.ToXML(@MyHierarchy)\n<\/pre>\n<p>Which produces this&#8230;<\/p>\n<pre>&lt;?xml version=\"1.0\" ?&gt;\n&lt;root&gt;\n\u00a0 &lt;CSV&gt;\n\u00a0\u00a0\u00a0 &lt;item Year=\"1997\" Make=\"Ford\" Model=\"E350\" Description=\"ac, abs, moon\" Price=\"3000.00\" \/&gt;\n\u00a0\u00a0\u00a0 &lt;item Year=\"1999\" Make=\"Chevy\" Model=\"Venture &amp;quot;Extended Edition&amp;quot;\" Description=\"\" Price=\"4900.00\" \/&gt;\n\u00a0\u00a0\u00a0 &lt;item Year=\"1999\" Make=\"Chevy\" Model=\"Venture &amp;quot;Extended Edition, Very Large&amp;quot;\" Description=\"\" Price=\"5000.00\" \/&gt;\n\u00a0\u00a0\u00a0 &lt;item Year=\"1996\" Make=\"Jeep\" Model=\"Grand Cherokee\" Description=\"MUST SELL!\nair, moon roof, loaded\" Price=\"4799.00\" \/&gt;\n\u00a0 &lt;\/CSV&gt;\n&lt;\/root&gt;\n<\/pre>\n<pre class=\"theme:ssms2012 lang:tsql\">DECLARE @MyHierarchy Hierarchy, @XML XML\nINSERT INTO @myHierarchy\n\u00a0 Select * from parseCSV('Year,Make,Model,Description,Price\n1997,Ford,E350,\"ac, abs, moon\",3000.00\n1999,Chevy,\"Venture \"\"Extended Edition\"\"\",\"\",4900.00\n1999,Chevy,\"Venture \"\"Extended Edition, Very Large\"\"\",\"\",5000.00\n1996,Jeep,Grand Cherokee,\"MUST SELL!\nair, moon roof, loaded\",4799.00', Default,Default,Default)\nSELECT dbo.ToJSON(@MyHierarchy)\n<\/pre>\n<p>&#8230;which produces this&#8230;<\/p>\n<pre>{\n\"CSV\" :\u00a0\u00a0 [\n\u00a0\u00a0\u00a0 {\n\u00a0\u00a0\u00a0 \"Year\" : 1997,\n\u00a0\u00a0\u00a0 \"Make\" : \"Ford\",\n\u00a0\u00a0\u00a0 \"Model\" : \"E350\",\n\u00a0\u00a0\u00a0 \"Description\" : \"ac, abs, moon\",\n\u00a0\u00a0\u00a0 \"Price\" : 3000.00\n\u00a0\u00a0\u00a0 },\n\u00a0\u00a0\u00a0 {\n\u00a0\u00a0\u00a0 \"Year\" : 1999,\n\u00a0\u00a0\u00a0 \"Make\" : \"Chevy\",\n\u00a0\u00a0\u00a0 \"Model\" : \"Venture \"Extended Edition\"\",\n\u00a0\u00a0\u00a0 \"Description\" : \"\",\n\u00a0\u00a0\u00a0 \"Price\" : 4900.00\n\u00a0\u00a0\u00a0 },\n\u00a0\u00a0\u00a0 {\n\u00a0\u00a0\u00a0 \"Year\" : 1999,\n\u00a0\u00a0\u00a0 \"Make\" : \"Chevy\",\n\u00a0\u00a0\u00a0 \"Model\" : \"Venture \"Extended Edition, Very Large\"\",\n\u00a0\u00a0\u00a0 \"Description\" : \"\",\n\u00a0\u00a0\u00a0 \"Price\" : 5000.00\n\u00a0\u00a0\u00a0 },\n\u00a0\u00a0\u00a0 {\n\u00a0\u00a0\u00a0 \"Year\" : 1996,\n\u00a0\u00a0\u00a0 \"Make\" : \"Jeep\",\n\u00a0\u00a0\u00a0 \"Model\" : \"Grand Cherokee\",\n\u00a0\u00a0\u00a0 \"Description\" : \"MUST SELL!\\r\\nair, moon roof, loaded\",\n\u00a0\u00a0\u00a0 \"Price\" : 4799.00\n\u00a0\u00a0\u00a0 }\n\u00a0 ]\n}\n<\/pre>\n<p>The procedure that produces a table seems far more useful than it turns out to be in practicality. Although one can do this (thanks for the sample, Timothy)<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">Execute CSVToTable '\"REVIEW_DATE\",\"AUTHOR\",\"ISBN\",\"DISCOUNTED_PRICE\"\n\"1985\/01\/21\",\"Douglas Adams\",0345391802,5.95\n\"1990\/01\/12\",\"Douglas Hofstadter\",0465026567,9.95\n\"1998\/07\/15\",\"Timothy \"\"The Parser\"\" Campbell\",0968411304,18.99\n\"1999\/12\/03\",\"Richard Friedman\",0060630353,5.95\n\"2001\/09\/19\",\"Karen Armstrong\",0345384563,9.95\n\"2002\/06\/23\",\"David Jones\",0198504691,9.95\n\"2002\/06\/23\",\"Julian Jaynes\",0618057072,12.50\n\"2003\/09\/30\",\"Scott Adams\",0740721909,4.95\n\"2004\/10\/04\",\"Benjamin Radcliff\",0804818088,4.95\n\"2004\/10\/04\",\"Randel Helms\",0879755725,4.50'\n<\/pre>\n<p>&#8230; To produce ..<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1475-clip_image004.png\" alt=\"1475-clip_image004.png\" width=\"497\" height=\"239\" \/><\/p>\n<p>..it is more useful for debugging and developing than in a production system.<\/p>\n<p>Naturally, it would be good to have some of the features available to XML markup built-into SQL Server. It wouldn&#8217;t, surely, take much to add a FOR CSV\u00a0 to the FOR XML. Sadly for those of us who still need to deal with CSV, we are left with an ODBC driver that doesn&#8217;t seem to have changed much in years.<\/p>\n<h2>Conclusions<\/h2>\n<p>Whereas there are plenty of ways of placing CSV data into staging tables, there are fewer ways of accessing tabular information in CSV format more directly. The ODBC text driver, for all its faults and limitations, still provides the best single means for both reading and writing CSV file data. If it was made more correct in its interpretation of CSV, and allowed ODBC SQL CREATE statements as other ODBC drivers have,\u00a0 then it would be far more useful. Naturally, if CSV format in SQL Serve had been accorded just a small fraction of the effort put into XML, then I wouldn&#8217;t have needed to write this article at all!<\/p>\n<h2>References<\/h2>\n<ul>\n<li>Creativyst\u00a0 &#8220;The Comma Separated Value (CSV) File Format&#8221; <a href=\"http:\/\/www.creativyst.com\/Doc\/Articles\/CSV\/CSV01.htm\">http:\/\/www.creativyst.com\/Doc\/Articles\/CSV\/CSV01.htm<\/a><\/li>\n<li>Edoceo, Inc., &#8220;CSV Standard File Format&#8221;, 2004,\u00a0\u00a0 <a href=\"http:\/\/www.edoceo.com\/utilis\/csv-file-format.php\">http:\/\/www.edoceo.com\/utilis\/csv-file-format.php<\/a>&gt;.<\/li>\n<li>Factor, P. &#8220;Consuming JSON Strings in SQL Server&#8221; November 2010<br \/>\n<a href=\"https:\/\/www.red-gate.com\/simple-talk\/sql\/t-sql-programming\/consuming-json-strings-in-sql-server\/\">https:\/\/www.red-gate.com\/simple-talk\/sql\/t-sql-programming\/consuming-json-strings-in-sql-server\/<\/a><\/li>\n<li>MSDN &#8220;Schema.ini File (Text File Driver)<br \/>\n<a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/windows\/desktop\/ms709353%2528v=vs.85%2529.aspx\">http:\/\/msdn.microsoft.com\/en-us\/library\/windows\/desktop\/ms709353%28v=vs.85%29.aspx<\/a><\/li>\n<li>MSDN &#8220;Text File Format (Text File Driver)&#8221;,<br \/>\n<a href=\"https:\/\/docs.microsoft.com\/en-us\/sql\/odbc\/microsoft\/text-file-format-text-file-driver\">https:\/\/docs.microsoft.com\/en-us\/sql\/odbc\/microsoft\/text-file-format-text-file-driver<\/a><\/li>\n<li>Repici, J., &#8220;HOW-TO: The Comma Separated Value (CSV) File\u00a0 Format&#8221;, 2004,\u00a0\u00a0 <a href=\"http:\/\/www.creativyst.com\/Doc\/Articles\/CSV\/CSV01.htm\">http:\/\/www.creativyst.com\/Doc\/Articles\/CSV\/CSV01.htm<\/a>.<\/li>\n<li>PowerShell Basics #1: Reading and parsing CSV <a href=\"http:\/\/www.heikniemi.net\/hardcoded\/2010\/01\/powershell-basics-1-reading-and-parsing-csv\/\">http:\/\/www.heikniemi.net\/hardcoded\/2010\/01\/powershell-basics-1-reading-and-parsing-csv\/<\/a><\/li>\n<li>Raymond, E., &#8220;The Art of Unix Programming, Chapter 5&#8221;, September 2003,<\/li>\n<li><a href=\"http:\/\/www.catb.org\/~esr\/writings\/taoup\/html\/ch05s02.html\">http:\/\/www.catb.org\/~esr\/writings\/taoup\/html\/ch05s02.html<\/a>.<\/li>\n<li>Postel, J., &#8220;Transmission Control Protocol&#8221;, STD 7, <a href=\"http:\/\/tools.ietf.org\/html\/rfc793\">RFC 793<\/a>, September 1981.<\/li>\n<li>Python Documentation &#8220;CSV File Reading and Writing&#8221;<a href=\"http:\/\/docs.python.org\/library\/csv.html\"> http:\/\/docs.python.org\/library\/csv.html<\/a><\/li>\n<li>Shafranovich Y. Draft RFC 4180 &#8220;Common Format and MIME Type for Comma-Separated Values (CSV) Files &#8221; <a href=\"http:\/\/tools.ietf.org\/html\/rfc4180\">http:\/\/tools.ietf.org\/html\/rfc4180<\/a><\/li>\n<li>Wikipedia &#8220;Comma-separated values&#8221;<a href=\"http:\/\/en.wikipedia.org\/wiki\/Comma-separated_values\"> http:\/\/en.wikipedia.org\/wiki\/Comma-separated_values<\/a><\/li>\n<\/ul>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Read and write CSV files from SQL Server using three approaches: the ODBC Text Driver, a linked server, and a pure T-SQL CSV parser. Covers the CSV format spec, common pitfalls, and working T-SQL code for each method.&hellip;<\/p>\n","protected":false},"author":154613,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143531],"tags":[4179,4150,4252,4190],"coauthors":[6813],"class_list":["post-1321","post","type-post","status-publish","format-standard","hentry","category-t-sql-programming-sql-server","tag-source-control","tag-sql","tag-t-sql-programming","tag-tsql"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1321","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\/154613"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=1321"}],"version-history":[{"count":16,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1321\/revisions"}],"predecessor-version":[{"id":109714,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1321\/revisions\/109714"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=1321"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=1321"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=1321"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=1321"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}