{"id":466,"date":"2012-04-03T12:30:00","date_gmt":"2012-04-03T12:30:00","guid":{"rendered":"\/blogs\/kimberly\/post\/Microsoft-Certified-Masters-its-like-chainsaw-art!.aspx"},"modified":"2013-02-23T09:16:21","modified_gmt":"2013-02-23T17:16:21","slug":"understanding-lob-data-20082008r2-2012","status":"publish","type":"post","link":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/understanding-lob-data-20082008r2-2012\/","title":{"rendered":"Understanding LOB data (2008\/2008R2 &#038; 2012)"},"content":{"rendered":"<p>I know it&#8217;s been a while since I last blogged here on SQLskills. It&#8217;s been a busy few months with Immersion Events, conferences and even a bit of personal travel in there. And, we&#8217;ve also been blogging very regularly (weekly) at SQLServerPro here: <a href=\"http:\/\/www.sqlmag.com\/blogcontent\/seriespath\/sql-server-questions-answered-28\" target=\"_blank\">Kimberly &amp; Paul: SQL Server Questions Answered<\/a>. And, this is what&#8217;s brought me to write this post. A few days ago, I blogged <a href=\"http:\/\/www.sqlmag.com\/blog\/sql-server-questions-answered-28\/sql-server-2008-r2\/moving-lob-data-142636\">What about moving LOB data?<\/a>\u00a0and in part I discussed some of the issues moving SQL Server LOB data using OFFLINE operations. In part II (<a href=\"http:\/\/www.sqlmag.com\/blog\/sql-server-questions-answered-28\/database-administration\/moving-lob-data-keeping-table-online-142703\" target=\"_blank\">What about moving LOB data while keeping the table online?<\/a>), I&#8217;m continuing the discussion with some additional points for ONLINE moves.<\/p>\n<p>However, I also wanted to make sure that people fully understood how SQL Server LOB data works with regard to the physical location within the database&#8230; So, I created a script to walk you through it. This post will show you the specifics&#8230;<\/p>\n<p>First, we&#8217;ll create a TestLOB database. Please note that this script drops TestLOB if it exists. If you already have a database with this name, it will be removed. Additionally, you will probably need to change the directory structure to something more appropriate on your system. These paths are the default data\/log locations for SQL Server 2012 running as a default instance (MSSQLServer).<\/p>\n<pre class=\"brush: sql; title: ; notranslate\" title=\"\">\r\nSET NOCOUNT ON\r\nGO\r\n\r\nUSE [master]\r\nGO\r\n\r\nIF DATABASEPROPERTYEX(&#039;TestLOB&#039;, &#039;status&#039;) IS NOT NULL\r\nBEGIN\r\n    ALTER DATABASE [TestLOB] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;\r\n    DROP DATABASE [TestLOB];\r\nEND\r\nGO\r\n\r\nCREATE DATABASE [TestLOB]\r\nCONTAINMENT = NONE\r\nON\u00a0 PRIMARY\r\n(\u00a0\u00a0 NAME = N&#039;TestLOBPrimary&#039;\r\n, FILENAME = N&#039;C:\\Program Files\\Microsoft SQL Server\\MSSQL11.MSSQLSERVER\\MSSQL\\DATA\\TestLOBPrimary.mdf&#039;\r\n, SIZE = 5120KB , FILEGROWTH = 1024KB ),\r\nFILEGROUP [FG1]\r\n(\u00a0\u00a0 NAME = N&#039;FG1File1&#039;\r\n, FILENAME = N&#039;C:\\Program Files\\Microsoft SQL Server\\MSSQL11.MSSQLSERVER\\MSSQL\\DATA\\FG1File1.ndf&#039;\r\n, SIZE = 61440KB , FILEGROWTH = 20480KB ),\r\n(\u00a0\u00a0 NAME = N&#039;FG1File2&#039;\r\n, FILENAME = N&#039;C:\\Program Files\\Microsoft SQL Server\\MSSQL11.MSSQLSERVER\\MSSQL\\DATA\\FG1File2.ndf&#039;\r\n, SIZE = 61440KB , FILEGROWTH = 20480KB ),\r\nFILEGROUP [FG2]\r\n(\u00a0\u00a0 NAME = N&#039;FG2File1&#039;\r\n, FILENAME = N&#039;C:\\Program Files\\Microsoft SQL Server\\MSSQL11.MSSQLSERVER\\MSSQL\\DATA\\FG2File1.ndf&#039;\r\n, SIZE = 61440KB , FILEGROWTH = 20480KB ),\r\n(\u00a0\u00a0 NAME = N&#039;FG2File2&#039;\r\n, FILENAME = N&#039;C:\\Program Files\\Microsoft SQL Server\\MSSQL11.MSSQLSERVER\\MSSQL\\DATA\\FG2File2.ndf&#039;\r\n, SIZE = 61440KB , FILEGROWTH = 20480KB ),\r\nFILEGROUP [FG3]\r\n(\u00a0\u00a0 NAME = N&#039;FG3File1&#039;\r\n, FILENAME = N&#039;C:\\Program Files\\Microsoft SQL Server\\MSSQL11.MSSQLSERVER\\MSSQL\\DATA\\FG3File1.ndf&#039;\r\n, SIZE = 61440KB , FILEGROWTH = 20480KB ),\r\n(\u00a0\u00a0 NAME = N&#039;FG3File2&#039;\r\n, FILENAME = N&#039;C:\\Program Files\\Microsoft SQL Server\\MSSQL11.MSSQLSERVER\\MSSQL\\DATA\\FG3File2.ndf&#039;\r\n, SIZE = 61440KB , FILEGROWTH = 20480KB )\r\nLOG ON\r\n(\u00a0\u00a0 NAME = N&#039;TestLOBLog&#039;\r\n, FILENAME = N&#039;C:\\Program Files\\Microsoft SQL Server\\MSSQL11.MSSQLSERVER\\MSSQL\\DATA\\TestLOBLog.ldf&#039;\r\n, SIZE = 102400KB , FILEGROWTH = 10240KB);\r\nGO\r\n\r\nUSE TestLOB;\r\nGO\r\n<\/pre>\n<p>Next, I&#8217;ll make the FG1 filegroup the default filegroup. For all objects that are not explicitly defined (using the ON clause), they will be created in FG1.<\/p>\n<pre class=\"brush: sql; title: ; notranslate\" title=\"\">\r\nALTER DATABASE TestLOB\r\nMODIFY FILEGROUP FG1 DEFAULT;\r\nGO\r\n<\/pre>\n<p>We&#8217;ll start by creating the TestLOBTable table within the FG1 filegroup<\/p>\n<pre class=\"brush: sql; title: ; notranslate\" title=\"\">\r\nCREATE TABLE TestLobTable\r\n(\r\nc1\u00a0 int identity,\r\nc2\u00a0 char(8000)\u00a0\u00a0\u00a0\u00a0\u00a0 default &#039;this is a test&#039;,\r\nc3\u00a0 varchar(max)\u00a0\u00a0\u00a0 NULL\r\n); -- will be created on FG1\r\nGO\r\n<\/pre>\n<p>We can verify the filegroup location for this object by using sp_help (review the results of the 5th grid returned).<\/p>\n<p>Next, we&#8217;ll add 1000 rows to the table. Each row is basically 8kb because of the size of the c2 column. For these inserts, we will insert data for c1 and c2 but the value for c3 is NULL.<\/p>\n<pre class=\"brush: sql; title: ; notranslate\" title=\"\">\r\nINSERT dbo.TestLobTable DEFAULT VALUES;\r\nGO  1000 -- create an 80MB table\r\n<\/pre>\n<p>To review the size (as well as the location) of this data, we&#8217;ll run a quick query against sys.dn_db_file_space_usage (which works for ALL databases in SQL Server 2012 prior to 2012 this DMV only returned space usage information for tempdb [DBID = 2]).<\/p>\n<p>Since there are no other objects in the database this very generic view of the location will be just fine:<\/p>\n<pre class=\"brush: sql; title: ; notranslate\" title=\"\">\r\nSELECT *\r\nFROM sys.dm_db_file_space_usage;\r\n<\/pre>\n<p>Or, if you&#8217;re on a version prior to SQL Server 2012 &#8211; you can look at the data space id set for the table (from sys.tables). Here&#8217;s the query you need:<\/p>\n<pre class=\"brush: sql; title: ; notranslate\" title=\"\">\r\nSELECT t.name AS [Table Name]\r\n, lob_data_space_id AS [Data Space ID]\r\n, ds.name AS [Data Space Name]\r\nFROM sys.tables AS t\r\nINNER JOIN sys.data_spaces AS ds\r\nON t.lob_data_space_id = ds.data_space_id;\r\nGO\r\n<\/pre>\n<p>Even if we create a clustered index on the table, the data doesn&#8217;t change filegroup. However, SQL Server does reorder the data within FG1 to match the key order defined by the clustering key. So, this operation can take quite bit of time and log space (depending on recovery model).<\/p>\n<pre class=\"brush: sql; title: ; notranslate\" title=\"\">\r\nCREATE UNIQUE CLUSTERED INDEX TestLobTableCL\r\nON TestLobTable (c1);\r\nGO\r\n<\/pre>\n<p>Again, we can verify that the object still lives on the FG filegroup by using sp_help (5th grid).<\/p>\n<p>Now, things will get a bit more interesting&#8230;<\/p>\n<p>In SQL Server 2012, you can rebuild a table even if it has LOB data. So, you might think that the following will move the entire table&#8230; let&#8217;s see:<\/p>\n<pre class=\"brush: sql; title: ; notranslate\" title=\"\">\r\nCREATE UNIQUE CLUSTERED INDEX TestLobTableCL\r\nON TestLobTable (c1)\r\nWITH (DROP_EXISTING = ON, ONLINE = ON)\r\nON FG2;\r\nGO\r\n<\/pre>\n<p>Now, check the results of sp_help. sp_help says it lives on FG2.\u00a0However, we curently don&#8217;t have any data in the LOB portion of this table. What if we add some??<\/p>\n<pre class=\"brush: sql; title: ; notranslate\" title=\"\">\r\nUPDATE TestLobTable\r\nSET c3 = REPLICATE (convert(varchar(max), &#039;ABC&#039;), 8000) -- creates a 24 row\r\nWHERE c1 % 17 = 0; -- only 58 rows\r\nGO\r\n<\/pre>\n<p>Now, re-check the results from both sp_help and the LOB query. sp_help still thinks we&#8217;re writing to FG2 but that&#8217;s correct because sp_help ONLY returns information about the in_row portion of the table. Even if you had specified a location for LOB data using TEXTIMAGE_ON when creating the table&#8230; sp_help would still ONLY reference the location for the in_row structure.<br \/>\nThe only way to know where SQL Server LOB data is going is to use the query above. And, nothing you do can move the LOB data using just a standard rebuild.<\/p>\n<p>So, let&#8217;s try to rebuild the object again&#8230;<\/p>\n<pre class=\"brush: sql; title: ; notranslate\" title=\"\">\r\nCREATE UNIQUE CLUSTERED INDEX TestLobTableCL\r\nON TestLobTable (c1)\r\nWITH (DROP_EXISTING = ON, ONLINE = ON)\r\nON FG3;\r\nGO\r\n<\/pre>\n<p>And, recheck with our queries. Again, our in_row data has moved but not our LOB data.<\/p>\n<p>My SQLServerPro blog post mentioned that you do have one option using partitioning but a great point is that this requires Enterprise Edition. So, it&#8217;s not ideal. If you are running on Enterprise Edition (or Enterprise Eval\/Developer Edition), the scripts continue and show how to go throught he move process using a partition function\/partition scheme.<\/p>\n<ul>\n<li>Here&#8217;s the test script for SQL Server 2008\/2008R2 &#8211; be sure to adjust the paths\/directories: <a href=\"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-content\/uploads\/2012\/4\/movinglobdata%20sql%20server%202008r2.sql\">MovingLOBData SQL Server 2008R2.sql (11.52 kb)<\/a><\/li>\n<li>Here&#8217;s the test script for SQL Server 2012 (leveraging ONLINE rebuilds) &#8211; again, make sure you adjust the paths\/directories:<br \/>\n<a href=\"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-content\/uploads\/2012\/4\/movinglobdata%20sql%20server%202012.sql\">MovingLOBData SQL Server 2012.sql (10.59 kb)<\/a><\/li>\n<li>Here&#8217;s a zip of the scripts that I use to add rows while I show the online operation (for 2012):<br \/>\n<a href=\"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-content\/uploads\/2012\/4\/addlobrows_loop.zip\">AddLOBRows_Loop.zip (676.00 bytes)<\/a><\/li>\n<\/ul>\n<p>And, in this week&#8217;s SQLServerPro blog post I&#8217;m going to give you other options &#8211; that can be done ONLINE without Enterprise Edition (thanks to a cool trick by Brad Hoff that I just learned too)!<\/p>\n<p>However, the end result is that moving around and working with LOB data is more challenging. To be honest, if you plan to have a lot of LOB data, you will want to be very strategic with how you design that data\/table (even considering better\u00a0isolation for it) so that you have more options available later if you want to move it to another hard drive.<\/p>\n<p>Having said all of that, it has gotten A LOT better now that SQL Server 2012 supports online operations for indexes where a LOB column is included. So, that&#8217;s a great start. But,\u00a0moving LOB data is non-trivial and really needs to be thought out properly especially if you have a lot of data to move\/migrate.<\/p>\n<p>Thanks for reading!<br \/>\nkt<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I know it&#8217;s been a while since I last blogged here on SQLskills. It&#8217;s been a busy few months with Immersion Events, conferences and even a bit of personal travel in there. And, we&#8217;ve also been blogging very regularly (weekly) at SQLServerPro here: Kimberly &amp; Paul: SQL Server Questions Answered. And, this is what&#8217;s brought [&hellip;]<\/p>\n","protected":false},"author":7,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[16,36,42,66,67,68],"tags":[],"class_list":["post-466","post","type-post","status-publish","format-standard","hentry","category-clustered-index","category-indexes","category-lob-data","category-sql-server-2008","category-sql-server-2008-r2","category-sql-server-2012"],"_links":{"self":[{"href":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-json\/wp\/v2\/posts\/466","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-json\/wp\/v2\/users\/7"}],"replies":[{"embeddable":true,"href":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-json\/wp\/v2\/comments?post=466"}],"version-history":[{"count":0,"href":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-json\/wp\/v2\/posts\/466\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-json\/wp\/v2\/media?parent=466"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-json\/wp\/v2\/categories?post=466"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.sqlskills.com\/blogs\/kimberly\/wp-json\/wp\/v2\/tags?post=466"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}