{"id":84614,"date":"2019-06-20T20:41:09","date_gmt":"2019-06-20T20:41:09","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=84614"},"modified":"2024-08-30T15:10:50","modified_gmt":"2024-08-30T15:10:50","slug":"a-mongodb-to-sql-server-migration-data-typing-and-un-nesting","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/a-mongodb-to-sql-server-migration-data-typing-and-un-nesting\/","title":{"rendered":"A MongoDB to SQL Server Migration: Data Typing and Un-nesting"},"content":{"rendered":"<p>When recently assigned a project to migrate data from MongoDB to SQL Server, my initial concern was my lack of expertise with MongoDB itself. I can say now after completing the project, MongoDB was the absolute least of my worries and certainly not a challenge. In fact, the ultimate solution that was implemented bypassed the MongoDB service altogether and used a combination of a MongoDB dump file and the native command line tool <code>bsondump<\/code> to convert the dump files from BSON (Binary JSON) to JSON for direct load into SQL Server tables. I was fortunate to avoid the C# coding required for that initial load step to pull the data into staging tables whose columns were all <code>NVACHAR(MAX).<\/code> (I have a very smart colleague, Lou Talarico, who noodled through that chore, and I have some references at the end of this article for further information). My tasks were simple comparatively: all I had to do was generate <code>CREATE TABLE<\/code> scripts with correct data types from the data in the staging tables and transpose the delimited array data stored in many of the columns. In this article, I will provide the sample data and code that will accomplish these two tasks.<\/p>\n<h2>The MongoDB Data<\/h2>\n<p>Most of us have had it drilled into our SQL brains for good reason that while <code>NVARCHAR(MAX)<\/code> has its place, it is not ideal for every column in every table. There is plenty of supporting evidence the you can find with a quick search that shows that the query optimizer does not always play nice when generating an optimal execution plan for these large data types. While it was the path of least resistance to use this data type to populate the SQL Server staging tables from the Extended JSON files MondoDB produced, it would not have been ideal for the final data types that the analysts would be writing queries against.<\/p>\n<p>I decided to try and generate the code to create the final tables using a combination of metadata and a sampling of the actual data in staging tables. I would need to do this for each column and pull in any non-null values to determine what data type it should be including its max length. Before reviewing the code that will generate the final <code>CREATE TABLE<\/code> statements, take a look at some sample data so that you may understand the challenge.<\/p>\n<p class=\"caption\">Listing 1: Create and Populate Sample Table, doc_staging<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk   \">CREATE TABLE [dbo].[doc_staging](\r\n\t[ID] [nvarchar](max) NULL,\r\n\t[CreateDate] [nvarchar](max) NULL,\r\n\t[mversion2] [nvarchar](max) NULL,\r\n\t[Version] [nvarchar](max) NULL,\r\n\t[doc_id] [nvarchar](max) NULL,\r\n\t[doc_type] [nvarchar](max) NULL,\r\n\t[doc_requirement] [nvarchar](max) NULL\r\n) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]\r\nGO\r\nINSERT [dbo].[doc_staging] ([ID], [CreateDate], [mversion2], \r\n    [Version], [doc_id], [doc_type], [doc_requirement]) \r\nVALUES (N'7E3BB454-80B1-406D-9D8B-7223A412CEF8', N'4\/10\/2017 8:47:17 PM'\r\n    , N'11.72', N'17', N'63||93||28||50||38', N'PDF||PDF||PDF||PDF||PDF'\r\n    , N'True||True||True||True||True')\r\nINSERT [dbo].[doc_staging] ([ID], [CreateDate], [mversion2],\r\n    [Version], [doc_id], [doc_type], [doc_requirement]) \r\nVALUES (N'6BA95F5A-9A92-41AF-AD1E-588AEA39600C', N'3\/11\/2017 9:54:00 PM'\r\n    , N'7.59', N'11', N'16||33||79||85||11', N'PDF||PDF||PDF||PDF||PDF' \r\n    , N'True||True||True||True||True')\r\nINSERT [dbo].[doc_staging] ([ID], [CreateDate], [mversion2], \r\n    [Version], [doc_id], [doc_type], [doc_requirement]) \r\nVALUES (N'567FCEA7-F0E0-473B-94EA-127923FA0D36', N'3\/14\/2017 1:56:22 PM'\r\n    , N'10.34', N'15', N'72||30||9||82||79||81||37'\r\n    , N'PDF||PDF||PDF||PDF||PDF||PDF||PDF'\r\n    , N'True||True||True||True||True||True||True')\r\nINSERT [dbo].[doc_staging] ([ID], [CreateDate], [mversion2], \r\n    [Version], [doc_id], [doc_type], [doc_requirement]) \r\nVALUES (N'236FBDFD-857D-46E1-89EC-8A6911736145', N'3\/4\/2016 4:15:04 AM'\r\n    , N'19.31', N'28', N'35||42||16||45||13||91||13'\r\n    , N'PDF||PDF||PDF||PDF||PDF||PDF||MS Word'\r\n    , N'True||True||True||True||True||True||True')\r\nINSERT [dbo].[doc_staging] ([ID], [CreateDate], [mversion2], \r\n    [Version], [doc_id], [doc_type], [doc_requirement]) \r\nVALUES (N'EEBB9163-B88F-44F1-9F9F-ABEF0418EF78', N'3\/31\/2017 3:45:31 AM'\r\n    , N'4.83', N'7', NULL, NULL, NULL)\r\nINSERT [dbo].[doc_staging] ([ID], [CreateDate], [mversion2], \r\n    [Version], [doc_id], [doc_type], [doc_requirement]) \r\nVALUES (N'630644C3-6001-4B32-AF45-3C7EA6F0CA70', N'6\/6\/2016 1:30:11 AM'\r\n    , N'5.52', N'8', N'63||18||92||54', N'PDF||PDF||PDF||PDF'\r\n    , N'True||True||True||True')\r\nINSERT [dbo].[doc_staging] ([ID], [CreateDate], [mversion2], \r\n    [Version], [doc_id], [doc_type], [doc_requirement]) \r\nVALUES (N'46998AA0-CCEA-41C5-9482-E94384FB11B0', N'8\/11\/2016 3:58:40 AM'\r\n    , N'13.79', N'20', N'57', N'MS Word', N'True')\r\nINSERT [dbo].[doc_staging] ([ID], [CreateDate], [mversion2], \r\n    [Version], [doc_id], [doc_type], [doc_requirement]) \r\nVALUES (N'695523CE-6DA4-4228-848C-3D92096AE18B', N'3\/16\/2017 3:27:06 PM'\r\n    , N'13.10', N'19', N'95||22||12||81||86||94||67'\r\n    , N'PDF||PDF||PDF||PDF||PDF||PDF||PDF'\r\n    , N'True||True||True||True||True||True||True')\r\nINSERT [dbo].[doc_staging] ([ID], [CreateDate], [mversion2], \r\n    [Version], [doc_id], [doc_type], [doc_requirement]) \r\nVALUES (N'C7802FCC-95D0-48D6-BC02-DE0704D64F45', N'5\/12\/2016 2:32:26 AM'\r\n    , N'2.76', N'4', NULL, NULL, NULL)\r\nINSERT [dbo].[doc_staging] ([ID], [CreateDate], [mversion2], \r\n    [Version], [doc_id], [doc_type], [doc_requirement]) \r\nVALUES (N'80D248F3-A6DC-440E-8845-3413E4ADDB88', N'5\/24\/2016 2:33:25 PM'\r\n    , N'2.07', N'3', NULL, NULL, NULL)\r\nINSERT [dbo].[doc_staging] ([ID], [CreateDate], [mversion2], \r\n    [Version], [doc_id], [doc_type], [doc_requirement]) \r\nVALUES (N'8FCE0A51-770B-43AF-997E-7B0D28236372', N'3\/9\/2017 9:57:52 PM'\r\n    , N'11.72', N'17', N'98||90||89||2||97||50'\r\n    , N'PDF||PDF||PDF||MS Word||MS Word||PDF'\r\n    , N'True||True||True||False||True||True')\r\nINSERT [dbo].[doc_staging] ([ID], [CreateDate], [mversion2], \r\n    [Version], [doc_id], [doc_type], [doc_requirement]) \r\nVALUES (N'29318D29-1FB1-4C91-B7A3-76A6A8185497', N'5\/23\/2016 8:46:08 PM'\r\n    , N'8.97', N'13', N'43||79||51||36', N'PDF||PDF||PDF||MS Word'\r\n    , N'True||True||True||True')<\/pre>\n<p>The above query creates the table and inserts 12 sample records. You can see that all of the columns use <code>nvarchar(max)<\/code> as the data type. The data itself contains common data types that will need to be converted, such as int, datetime and decimal.<\/p>\n<p>Several fields are double pipe \u201c||\u201d delimited strings, each containing concatenated values. For example, <code>doc_id<\/code> in the first row has the value N&#8217;63||93||28||50||38&#8242;. In MongoDB, these were nested records in the collection, and this is how the conversion brought these over to the staging table, as one flat record. Further, the <code>doc_id<\/code> field is ordinally related to the <code>doc_type<\/code> and <code>doc_requirements<\/code> fields. The requirement is to transpose these values to individual rows for the final conversion. But first you have to build the ultimate, properly data typed \u201cproduction\u201d table.<\/p>\n<h2>The Solution<\/h2>\n<p>The following code builds the create table script and uses a simple iterative technique (thankfully not cursors) that reads each non-null value from each column, gleaned from the metadata in <code>INFORMATION_SCHEMA.COLUMNS<\/code>, and tries to ascertain the data type of the values. It is limited to a small set of data types like <code>datetime<\/code>, <code>int<\/code>, <code>decimal<\/code> and character-based values, but I have found it to be reliable and fast. Listing 2 shows the code in its entirety, commented to explain the process flow.<\/p>\n<p class=\"caption\">Listing 2: The code to create a CREATE TABLE script<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">SET nocount ON; \r\n--DECLARE variables and tables\r\nDECLARE @Src NVARCHAR(max) \r\nDECLARE @Col NVARCHAR(max); \r\nDECLARE @SQL NVARCHAR(max); \r\nDECLARE @SQLCUR NVARCHAR(max); \r\nDECLARE @SQL_CASE NVARCHAR(max); \r\nDECLARE @maxlen INT; \r\nDECLARE @DB_Col TABLE \r\n  ( \r\n     rownum      INT, \r\n     column_name NVARCHAR(max) \r\n  ); \r\nDECLARE @Tablesrc NVARCHAR(max) \r\nDECLARE @MAX_Rec       INT, \r\n        @CURRENTRECORD INT; \r\nDECLARE @MAX_RecTAB       INT, \r\n        @CURRENTRECORDTAB INT; \r\nDECLARE @DT NVARCHAR(max) \r\nCREATE TABLE #temp \r\n  ( \r\n     #temp NVARCHAR(max) \r\n  ) \r\nCREATE TABLE #temp2 \r\n  ( \r\n     #temp NVARCHAR(max) \r\n  ) \r\n--Create temp table #db_tab to hold table metadata \r\n--from INFORMATION_SCHEMA.TABLES\r\nSELECT ROWNUM =Row_number() \r\n                 OVER ( \r\n                   ORDER BY table_name), \r\n       table_schema, \r\n       table_name \r\nINTO   #db_tab \r\nFROM   INFORMATION_SCHEMA.TABLES\r\n--optional WHERE clause. If commented out, each user table in the \r\n--database will be returned\r\nWHERE  table_name = 'doc_STAGING' \r\n--Set max record value for iteration through tables \r\n--(sample include only 1 table)\r\nSET @CURRENTRECORDTAB = 1 \r\nSET @MAX_RecTAB = (SELECT Max(rownum) \r\n                   FROM   #db_tab) \r\n--Iterate through each table\t\t\t\t   \r\nWHILE @CURRENTRECORDTAB &lt;= @MAX_RecTAB \r\n  BEGIN \r\n      SELECT @Src = table_schema + '.' + table_name \r\n      FROM   #db_tab \r\n      WHERE  rownum = @CURRENTRECORDTAB \r\n--Remove \"_staging\" from the new table name\r\n      SELECT @Tablesrc = Replace(table_name, '_STAGING', '') \r\n      FROM   #db_tab \r\n      WHERE  rownum = @CURRENTRECORDTAB \r\n--Populate next temp table variable \"@DB_Col\" with column metadata \r\n--from INFORMATION_SCHEMA.COLUMNS\r\n      INSERT INTO @DB_Col \r\n      SELECT ROWNUM = Row_number() \r\n                        OVER ( \r\n                          ORDER BY ordinal_position), \r\n             column_name \r\n      FROM   INFORMATION_SCHEMA.COLUMNS \r\n      WHERE  table_schema + '.' + table_name = @Src\r\n\t  \r\n--Nested iteration for each column in the staging table\r\n      SET @CURRENTRECORD = 1; \r\n--Begin building dynamic CREATE TABLE statement. DROP TABLE \r\n--can be optional\r\n      --SET @SQL = N'DROP TABLE IF EXISTS ' + @Tablesrc + ';' \r\n      --           + Char(13) + Char(13) + '  CREATE TABLE dbo.' \r\n      --           + @Tablesrc + N' (' + Char(13); \r\n      SET @SQL =  '  CREATE TABLE dbo.' \r\n                 + @Tablesrc + N' (' + Char(13); \r\n--Get the max number of columns (ordinal_position field in \r\n--INFORMATION_SCHEMA.COLUMNS)\r\n      SET @MAX_Rec = (SELECT Max(ordinal_position) \r\n                      FROM   INFORMATION_SCHEMA.COLUMNS \r\n                      WHERE  table_schema + '.' + table_name = @Src); \r\n--Start column iteration\r\n      WHILE @CURRENTRECORD &lt;= @MAX_Rec \r\n --Set current column name using rownum\r\n BEGIN \r\n            SELECT @Col = N'[' + column_name + N']' \r\n            FROM   @DB_Col \r\n            WHERE  rownum = @CURRENTRECORD; \r\n--Get the max length of all non null values for current column\r\n            SET @SQLCUR = N'Select   max(len(' + @Col + ')) from ' \r\n                + @Src + ' where ' + @Col + ' is not null'; \r\n            INSERT #temp \r\n            EXEC (@SQLCUR);\r\n            SET @maxlen = (SELECT TOP 1 * \r\n                           FROM   #temp); \r\n--Round max length to nearest 10\r\n            SET @maxlen = Round(@maxlen, -1); \r\n--This does all of the data type guessing work and writing the\r\n-- output to a second temp table #temp2\r\n\t   SET @SQL_CASE = N'Select TOP 1 CASE  WHEN Isdate(' + @Col \r\n                + N') = 1  AND ISNUMERIC(' + @Col \r\n                + N') &lt;&gt; 1 THEN ''DATETIME2'' WHEN ISNUMERIC(' + @Col \r\n                + N') = 1 and ' + @Col + N' \r\n                NOT LIKE ''%.%''THEN ''INT'' WHEN ISNUMERIC(' + @Col \r\n                + N') = 1 and ' + @Col \r\n                + N' LIKE ''%.%''THEN ''decimal(18,2)'' WHEN (isdate(' \r\n                + @Col + N') &lt;&gt; 1 and ISNUMERIC(' + @Col \r\n                + N') &lt;&gt; 1)  AND len(' + @Col + N') &gt; 0 AND ' \r\n                + Cast(@maxlen AS VARCHAR(5)) \r\n                + ' &lt; 4000 THEN ''NVARCHAR(' \r\n                + Cast(@maxlen + 20 AS VARCHAR(5)) \r\n                + N')'' ELSE ''NVARCHAR(MAX)'' END From ' \r\n                + @Src + ' where ' + @Col + N' is not null'; \r\n            INSERT #temp2 \r\n            EXEC (@SQL_CASE); \r\n--Get the data type (@DT) from #temp2\r\n            SET @DT = (SELECT TOP 1 * \r\n                       FROM   #temp2); \r\n            SET @SQL = @SQL + N'      ' + @Col + N' ' \r\n                       + Isnull(@DT, 'NVARCHAR(255)') + N',' + Char(13); \r\n  --We are finished with current column, let's delete these \r\n  --and get new ones\r\n  DELETE FROM #temp;\r\n  DELETE FROM #temp2; \r\n            SET @CURRENTRECORD = @CURRENTRECORD + 1; \r\n        END; \r\n--Build final SQL statement used to print CREATE TABLE \r\n--statement (optional EXEC)\r\n      SET @SQL = LEFT(@SQL, ( Len(@SQL) - 2 )) \r\n      SET @SQl = @SQL + Char(13) + ')' \r\n --Print the final CREATE TABLE SCRIPT\r\n\t  PRINT @SQL; \r\n      SET @CURRENTRECORDTAB = @CURRENTRECORDTAB + 1; \r\n  END \r\nDROP TABLE [#db_tab];\r\nDROP TABLE [#temp]; \r\nDROP TABLE [#temp2];<\/pre>\n<p>The code itself simply iterates through one or more tables, gathering metadata for column names and ordinal positions, interrogates each column to determine its max length and assumed data type and then generates a <code>CREATE TABLE<\/code> script, adding 20 to the rounded character-based fields. This version of the code uses Unicode data types. For the small sample table, it immediately returns the following:<\/p>\n<pre class=\"lang:tsql decode:true  \">CREATE TABLE dbo.doc (\r\n      [ID] NVARCHAR(60),\r\n      [CreateDate] DATETIME2,\r\n      [mversion2] decimal(18,2),\r\n      [Version] INT,\r\n      [doc_id] NVARCHAR(80),\r\n      [doc_type] NVARCHAR(140),\r\n      [doc_requirement] NVARCHAR(110)\r\n)<\/pre>\n<p>&nbsp;<\/p>\n<p>Notice that the <code>_staging<\/code> suffix has been removed and the final table will be <code>dbo.doc<\/code>. This, of course, is easily modifiable.<\/p>\n<p>In the real project, there were tens of thousands of rows and the code still performed surprisingly well.<\/p>\n<p>After running the <code>CREATE TABLE<\/code> statement to create the empty base table, appropriately data typed, it is time to populate it from the same staging data. Recall the requirement to transpose the delimited values from the <code>doc_id<\/code>, <code>doc_type<\/code> and <code>doc_requirments<\/code> columns so that these become a separate row.<\/p>\n<p>You will need to transpose this:<\/p>\n<p>43||79||51||36<\/p>\n<p>Into this:<\/p>\n<p>43<\/p>\n<p>79<\/p>\n<p>51<\/p>\n<p>36<\/p>\n<p>And further, you will need to join these <code>doc_ids<\/code> to their precise <code>doc_type<\/code> and <code>doc_requirement<\/code> values.<\/p>\n<p>The figure below shows the first row, using the sample data.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1283\" height=\"102\" class=\"wp-image-84615\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2019\/06\/word-image-23.png\" \/><\/p>\n<p>The next figure shows what the final result should be. Notice that the <code>doc_ids<\/code> have maintained their ordinal positions despite their numerical order, which is crucial because the values in both <code>doc_type<\/code> and <code>doc_requirement<\/code> share the same positional location as the corresponding <code>doc_id<\/code> that they are related to.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1040\" height=\"156\" class=\"wp-image-84616\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2019\/06\/word-image-24.png\" \/><\/p>\n<p>Fortunately, there is a new function in SQL Server 2016 and higher called <code>STRING_SPLIT()<\/code> that handles this type of pivoting of delimited values with ease and does so very efficiently. The trick to getting these values to line up uses a combination of cross-applying the results of the <code>STRING_SPLIT<\/code> function with the base table and using the window function <code>ROW_NUMBER<\/code> to maintain the position.<\/p>\n<p>The following listing shows the simple query to perform the work.<\/p>\n<p class=\"caption\">Listing 3: Splitting the field<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk \">SELECT *, arv.value AS doc_id_split, \r\n    Row_number() OVER (PARTITION BY id ORDER BY id ) AS RN \r\nFROM  [dbo].[doc_staging] \r\nCROSS apply String_split(Replace([doc_id], '||', '|'), N'|') arv<\/pre>\n<p>Notice that ROW_NUMBER is partitioned and ordered on the <code>ID<\/code> value of each row, which will return a running tally based on the number of delimited values that are cross applied.<\/p>\n<p>You can then use the row number to join to the same cross applied row numbers for <code>doc_type<\/code> and <code>doc_requirement<\/code>. Remember each field to transpose has a variable number of actual delimited values; some rows may have five or more values, and others may only have one, but each row will contain the same number of <code>doc_ids<\/code> as there are <code>doc_type<\/code> and <code>doc_requirement<\/code> values.<\/p>\n<p>It is also worth pointing out that the <code>REPLACE<\/code> function changes the double pipe to a single pipe character. Using the double pipe was a choice we made to have more assurance that there would not be actual data values that contain this combination of characters, but the downside is that the <code>STRING_SPLIT<\/code> function will only take a single byte value for the delimiter. Hence, the double pipe must be replaced with a single character delimiter. The final code to return exactly what is needed for the sample data, all 46 new rows, is in the following listing, which includes a CTE-based solution.<\/p>\n<p class=\"caption\">Listing 4: CTE to transpose the data values from the delimited values<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk   \">WITH split_cte\r\nAS (SELECT *,\r\n           arv.value AS doc_id_split,\r\n           ROW_NUMBER() OVER (PARTITION BY id ORDER BY id) AS RN\r\n    FROM [dbo].[doc_staging]\r\n        CROSS APPLY STRING_SPLIT(REPLACE([doc_id], '||', '|'), N'|') \r\n        arv),\r\n     split_cte2\r\nAS (SELECT id,\r\n           arv2.value AS doc_type,\r\n           ROW_NUMBER() OVER (PARTITION BY id ORDER BY id) AS RN\r\n    FROM [dbo].[doc_staging]\r\n        CROSS APPLY STRING_SPLIT(REPLACE([doc_type], '||', '|'), N'|') \r\n          arv2),\r\n     split_cte3\r\nAS (SELECT id,\r\n           arv3.value AS doc_requirement,\r\n           ROW_NUMBER() OVER (PARTITION BY id ORDER BY id) AS RN\r\n    FROM [dbo].[doc_staging]\r\n        CROSS APPLY STRING_SPLIT(REPLACE([doc_requirement], '||', '|')\r\n           , N'|') arv3)\r\n--INSERT INTO dbo.doc\r\nSELECT sc1.ID,\r\n       sc1.CreateDate,\r\n       sc1.mversion2,\r\n       sc1.Version,\r\n       --,sc1.doc_id\r\n       sc1.doc_id_split AS doc_id,\r\n       sc2.doc_type,\r\n       sc3.doc_requirement\r\nFROM split_cte sc1\r\n    INNER JOIN\r\n    (SELECT id, split_cte2.doc_type, split_cte2.RN FROM split_cte2) sc2\r\n        ON sc1.id = sc2.id\r\n           AND sc1.RN = sc2.RN\r\n    INNER JOIN\r\n    (\r\n        SELECT id,\r\n               split_cte3.doc_requirement,\r\n               split_cte3.RN\r\n        FROM split_cte3\r\n    ) sc3\r\n        ON sc1.id = sc3.id\r\n           AND sc1.RN = sc3.RN;<\/pre>\n<p>Each CTE splits one of the delimited columns, and the CTEs are joined together in the outer query. Since the <code>OVER<\/code> clause for <code>ROW_NUMBER<\/code> is identical in each case, they join together perfectly on the <code>ID<\/code> and row number.<\/p>\n<p>By using this code, you can easily insert the results into the newly created <code>dbo.doc<\/code> table. I have commented out that insert statement so that you can see the results first.<\/p>\n<h2>Summary<\/h2>\n<p>I like the idea of having reusable code, even if it is to pick out certain pieces from a larger scope. I believe the code I am sharing here, which certainly has much room for improvement and extension, can be readily used for a variety of other purposes with some slight modification. One such goal would be to generate create table scripts for each table in an existing database. Instead of deriving the data type, it is possible just to use an existing data type. I will most likely go back to it many times for future projects that may involve a REST API, for example, rather than a MongoDB conversion. I know, too, that I will find a need to transpose delimited data somewhere down the road. Even if you do not have a specific need for a MongoDB conversion, I hope that the techniques shown here will help you in your projects.<\/p>\n<h2>References<\/h2>\n<p>For further information on the JSON framework used for the C# code as well as the bsondump utility, please see the following links.<\/p>\n<p><a href=\"https:\/\/www.newtonsoft.com\/json\" target=\"_blank\" rel=\"noopener\">https:\/\/www.newtonsoft.com\/json<\/a><\/p>\n<p><a href=\"https:\/\/docs.mongodb.com\/manual\/reference\/program\/bsondump\/\">https:\/\/docs.mongodb.com\/manual\/reference\/program\/bsondump\/<\/a><\/p>\n<p>It is worth pointing out that the documentation for BSDUMP says that it is a diagnostic tool and not a tool for data ingestion or other application use.<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Importing data from one system to another is a common task for database professionals. It\u2019s possible to spend a lot of time figuring out what the schema should be for the target table. In this article, Rodney Landrum demonstrates an interesting solution to automatically figure out the data types and un-nest delimited data.&hellip;<\/p>\n","protected":false},"author":221800,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[53,159161,143531],"tags":[5618,95509],"coauthors":[11298],"class_list":["post-84614","post","type-post","status-publish","format-standard","hentry","category-featured","category-mongodb","category-t-sql-programming-sql-server","tag-mongodb","tag-standardize"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/84614","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\/221800"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=84614"}],"version-history":[{"count":11,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/84614\/revisions"}],"predecessor-version":[{"id":84628,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/84614\/revisions\/84628"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=84614"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=84614"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=84614"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=84614"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}