DBPedias

Your Database Knowledge Community

Shaun Stuart

  1. Identifying Identity Columns Approaching Their Limit

    Back in December, a couple of checks I wrote were included in Brent Ozar Unlimited's sp_Blitz script. I was chagrined to see that some people discovered some bugs in my code and submitted fixes. To be fair, one was a bug in SQL 2008 & 2008 R2 where DBCC DBINFO WITH TABLERESULTS returned the dbccLastKnownGood entry twice. (Microsoft has said they aren't going to fix this, unfortunately.) The other was an issue when systems were using the British date format. I guess this just goes to show how hard it is to test every possible use case to discover bugs.

    Even though there were only problems with one of the three tests I submitted, I still felt the need to redeem myself and submit another check. I had recently read a blog post about monitoring the values of identity columns in tables to make sure you aren't approaching the maximum value for the data type used. Surprisingly, such a check wasn't already in sp_Blitz. I felt this was something I could tackle.

    My first attempt worked, but it did so by cycling through all the tables in a database, finding any identity columns, and comparing the maximum value used to the maximum value the data type can accommodate (255 for TINYINT, 2147483648 for INT, etc.) I submitted this and the feedback from Brent was that he wasn't sure if this should be included because of the performance implications. Although I tested the code on all my servers without issue, Brent mentioned that some databases, especially those for SAP, may have 10,000 to 50,000 tables. Running code to look at each of those tables could have a severe impact on performance or, at the very least, greatly increase the time it took the script to complete. That's a valid point (and again goes to show how hard it is to test for all use cases). Apparently, he spoke to Kendra Little about this and she brought up a system table that both Brent and I didn't know about (or more likely, knew about, but forgot): sys.identity_columns. Using that, I can get the same information by querying one system table instead of each individual user table in the database.

    So I re-wrote my script to use that and re-submitted it to sp_Blitz. It's in the queue for evaluation, so I don't know when (or if) it will be included, but I thought I post the test here. (And of course, once I had the name of this table, I discovered a Google search returns lots of scripts that provide the same functionality.)

    A quick note: The script assumes the identity increment is positive and the identity values are positive. The script will issue a warning when the maximum value is more than 90% of the maximum value of the data type used.

      DECLARE @SchemaName sysname
      DECLARE @TableName sysname
      DECLARE @ColumnName sysname
    
      DECLARE @Name sysname
      DECLARE DBNameCursor CURSOR
      FOR
              SELECT    name
              FROM      sys.databases
              WHERE     source_database_id IS NULL	/* no database snapshots */
                        AND is_read_only = 0		/* no read-only dbs (log shipping) */
                        AND database_id <> 2		/* skip tempdb */
                        AND state = 0	/* online databases */
              ORDER BY  name;
    
      CREATE TABLE #IdentityCheck
             (
              PK INT IDENTITY(1, 1)
             ,DatabaseName sysname
             ,SchemaName sysname
             ,TableName sysname
             ,ColumnName sysname
             ,ColumnType VARCHAR(8)
             ,MaxValue BIGINT
             )
    
      OPEN DBNameCursor
      FETCH NEXT FROM DBNameCursor INTO @Name
      WHILE @@fetch_status = 0
            BEGIN
    
                  EXEC('USE [' + @Name + '];  INSERT  INTO #IdentityCheck
                  SELECT ''' + @Name + ''' as DatabaseName
                  ,SCHEMA_NAME(o.SCHEMA_ID) AS SchemaName
                  ,OBJECT_NAME(o.OBJECT_ID) AS TableName
                  ,c.[name] AS ColumnName
                  ,t.name AS ColumnType
                  ,CAST(last_value AS bigint) AS MaxValue
                  FROM    sys.identity_columns c
                  ,sys.types t
                  ,sys.objects o
                  WHERE   c.is_identity = 1
                  AND t.system_type_id = c.system_type_id
                  AND o.object_id = c.object_id
                  AND o.type = ''u''
                  AND t.is_user_defined = 0 /* only look at system defined types */
                  AND t.name IN (''int'', ''bigint'', ''smallint'', ''tinyint'')')
                  FETCH NEXT FROM DBNameCursor INTO @Name
            END
    
      CLOSE DBNameCursor
      DEALLOCATE DBNameCursor
    
      SELECT    'Table [' + SchemaName + '].[' + TableName + '], column ['
                + ColumnName + '] in database [' + DatabaseName
                + '] is an identity column that has reached over 90% of the maximum value for that datatype.' AS Details
               ,'' AS ChangeRequirements
      FROM      #IdentityCheck
      WHERE     1 = CASE WHEN ColumnType = 'bigint'
                              AND (CONVERT(DECIMAL(19, 0), MaxValue)
                                   / 9223372036854775807.0) > .90 THEN 1
                         WHEN ColumnType = 'int'
                              AND (CONVERT(DECIMAL(10, 0), MaxValue)
                                   / 2147483648.0) > .90 THEN 1
                         WHEN ColumnType = 'smallint'
                              AND (CONVERT(DECIMAL(5, 0), MaxValue) / 32767.0) > .90
                         THEN 1
                         WHEN ColumnType = 'tinyint'
                              AND (CONVERT(DECIMAL(3, 0), MaxValue) / 255.0) > .90
                         THEN 1
                    END
    
      DROP TABLE #IdentityCheck
    
    

    So what do you do if you get this warning? Well, a quick fix that Michelle Ufford suggested, is to reseed your identity value to the lowest negative value the data type supports. For example, if your identity column is an integer, reseed the column to start at -2147483648 and increment by a positive number. (This assumes you started your initial identity value at zero and used a positive increment to begin with.)

    One more note: you'll notice my script uses a cursor to cycle through the databases while many other scripts you find on the internet use ms_foreachdb. Either method works. I prefer the cursor because it's 100% supported. (For the cursors-give-sucky-performance crowd, the undocumented command uses a cursor behind the scenes anyway, so that's a moot point.) Ms_foreachdb is an unsupported command and can change at any time. There is also evidence that it can skip some databases and even completely lock up SQL 2005 servers in some situations. Given the issue I ran into above with the DBCC DBINFO command, which is also an unsupported command, and Microsoft's statement that they are not going to fix it, I am committed to not using any undocumented commands in any scripts I pan to re-use. I'll use them for any one-off work I might be doing, but for something that I want to save and re-use as part of my SQL toolkit, I will pass.

    Share

  2. Tell Me Your Desires (And I Promise To Try Not To Laugh)

    My co-worker showed me a SQL Server requirements document that he found online somewhere and modified at a previous company. He would give this document to vendors who said they needed a SQL Server and they would need to select the appropriate answers before he would let them install their product.  I think this is a great idea and thought I'd share it.

    SQL Server Host Requirements (OS, minimum RAM, Number of CPUs, VM support, etc.)

    Version of SQL Server Supported

    • SQL 2012 SP2
    • SQL 2012 SP1
    • SQL 2012 RTM
    • SQL 2008 R2 SP2
    • SQL 2008 R2 SP1

    Edition Of SQL Server Supported

    • Enterprise
    • Standard

    32-bit vs. 64-bit

    • 32-bit
    • 64-bit

    Exclusivity For The Hosted Database

    • Shared SQL Server Environment
    • Dedicated SQL Server required

    Maximum Concurrent User Count

    IOPS (I/O per second) Requirement

    Initial Database Size - Please provide for each database, if multiple. Specify in MB or GB.

    Projected Annual Database Growth Rate. Specify in MB or GB.

    Special SAN Requirements, if any.

    Dedicated Test, Development, and / or Training Environment Required (Include Details)

    • Auxiliary environments to be delete / removed post-go live
    • Auxiliary environments to remain accessible for duration of project use
    • Auxiliary environments will need to be recreated for future product upgrades / enhancements

    Database Security

    • Single Windows (Active Directory) network login (connection pooling)
    • Active Directory gorup login
    • Individual or AD Group network login for each user
    • Single SQL login ID and password (Login and password must be customizable and not identical to other product installations at other clients)
    • Individual SQL login and password for each user

    Maximum Level Of Permissions Required for Users / Administators / Service Account (Please provide explanation for any response greater than Database role, db_owner)

    • Database role, less than db_owner
    • Database role, db_owner
    • Aliased as dbo for database
    • Some level of server-level permissions less than system administrator (sa)
    • SA role membership
    • Local administrator rights on SQL Server host machine

    Database Connection / Session Handling

    • Application maintains persisted connections / sessions to database
    • Application established and releases connections / sessions as requested during normal operation
    • Other

    Vendor Access Required To Support Database Issues

    • Vendor requires local login rights to SQL Server
    • Vendor requires remote-connection via the application
    • Vendor requires VPN access to our network to connect to the database via SSMS
    • No remote support capabilities. Database backup will need to be sent via secure FTP to vendor for analysis
    • No remote support capabilities. Secure method of transferring database backup not available.

    Application and Database Support Exists for the Following High Availability Architectures

    • Failover clustering
    • Mirroring
    • Log shipping
    • Replication

    How Frequently Do You Certify SQL Server Patches, Service Packs, and Release Updates?

    • As released by Microsoft
    • Quarterly
    • Annually
    • Other

    How Frequently Do You Issue Database Updates?

    • Quarterly
    • Annually
    • Other

    Performance Tuning Requirements

    • Index tuning changes (adding or disabling indexes, etc.) must be submitted to vendor for approval
    • Index tuning changes (adding or disabling indexes, etc.) do not require vendor approval
    • Index changes by customer technical staff are not permitted

    Data Retention / Archiving and Growth Management Support

    • Vendor does not support database archiving
    • Vendor has database archiving process in place or available
    • Database archiving solutions created by customer must be submitted to vendor for approval
    • Database archiving solutions created by customer do not require vendor approval
    • Vendor has tested and supports SQL Server data compression

    Database Installation Process

    • TSQL scripts or database backup file to be submitted to customer DBA for installation
    • Installation accomplished via executable or other method run by vendor / customer on non-SQL Server host (e.g., application server or client workstation). Any additional permission requirements needed for installation should be noted here.
    • Other

    Note any Non-Standard Collation, Server-Level, or Instance-Level Settings Required

    Identify Tables with Sensitive Data (Personally Identifiable Information, Financial Data, Passwords, etc.)

    Identify Any Custom SQL Agent Jobs, Proprietary Backup or Maintenance Processes, or Similar Items.

    Vendor Technical Contact ____________________________
    Vendor Technical Contact Email _______________________
    Vendor Technical Contact Phone _______________________

    I would be surprised if many vendors knew the answer to all these questions right off the bat. However, this questionnaire will at least bring these issues to the forefront where they can start being addressed in a pro-active manner, before the product is installed. It will also raise any red flags. (The application needs sa access on a shared SQL Server? I don't think so! Your maintenance plan shrinks the database weekly? I don't think so!)

     

    Share

  3. Database Administrator Rated One Of The Fastest Growing Careers Through 2020

    An article on Yahoo Education recently ranked Database Administrator as the second fastest growing career through 2020, with a projected job growth of 31%. The piece lists a couple of reasons for this, most of which I agree with. Then I read the last line of the article: "Companies with large databases may prefer those with an MBA."

    Are you serious?

    Are you serious?

    Database Administrators need an MBA to work with large databases?

    Share

  4. Why Is That Delete Statement Taking So Long?

    My phone rag at 4 AM last Sunday morning, which is never a good sign. It was a member of our IT team who was doing an upgrade to some software. He was concerned because a particular SQL statement had been running for 30 minutes so far and when this person performed the upgrade on the test system earlier in the week, this particular statement completed in a few minutes. Because this was a mission-critical system, he wanted me to take a look and see if I could tell why things were taking so long and to be sure everything was still ok.

    My first step was to fire up SSMS and launch activity monitor. Sure enough, I saw the query being executed. There was no blocking going on, nor were there excessive waits. Occasionally, I would see an I/O wait, but they were on the order of 20 milliseconds and were somewhat rare. It looked to me like SQL was just chugging along.

    My next step was to get the actual query being run. It was really, really basic:

    DELETE  FROM amc_rd_ref_data_item
    WHERE   reference_data_group_id = @rdg_id
    

    I looked at the table, thinking about possible indexing issues, or maybe triggers. This is what I saw:

    TableStructure

    No triggers. The reference_data_group_id was column was the second field in the primary key, so that index couldn't be used. But each of the other two indexes had this as the first column, so the database engine could have used either one of them for this query. There were also no constraints. There were three foreign keys defined on this table, but the tables they linked too had a small number of rows. No cursors were involved. Data types were matched, so there were no implicit conversion issues. The table only had about 500,000 rows in it. In short, nothing was jumping out at me as to why this simple query should take so long to run.

    I decided to look at the execution plan for the query. Bingo!

    Click to embiggen

    Click to embiggen

    Wow. That's zoomed out so you can see the whole plan, but each of those vertical lines is a nested loop operator. It turns out there were more than 200 tables that referenced the table we were deleting from with foreign keys of their own.

    SQL was working through this without issue, so I decided to just let the query continue. I made sure transaction log backups were being taken frequently and that the log drive had enough room for it to grow - I didn't want this thing to bomb out for any reason and have to start over. The query ended up taking just over an hour to complete and when it was done, the table was down to 9,000 rows, so there was a lot of deleting and looping going on.

    If you have what looks to be a very simple query that seems to be taking an inordinately long time to run, in addition to checking for blocking, proper indexing, triggers, and the usual suspects, keep in mind foreign keys on other tables. View the execution plan to find out exactly what SQL Server is doing.

     

    Share

  5. SQL Saturday #193 Coming to Chandler, AZ

    Sorry for the lack of posts lately. I took a 2 week cruise to Hawaii for my tenth wedding anniversary and am currently trying to catch up with everything that piled up while I was away. During the cruise, I also got a kidney stone and had to go to the hospital in Kauai for that, where they discovered not one, not two, not three, but a total of four stones in my right kidney. I'll be having surgery next week to get those removed, so I expect content here might be a bit slow in coming for a while.

    But while going through my email, I did find out that SQL Saturday is returning to the Phoenix area. On Saturday, April 26th, it is returning to the Chandler-Gilbert Community College. This is really close to my home, so hopefully, I'll be able to make it. Here are the details, as provided by the organizers:

    SQLSaturday#193 is a free one day training event for SQL Server professionals and those interested in SQL Server. The event will be held Apr 27, 2013 at Chandler-Gilbert Community College Pecos Campus, 2626 East Pecos Road, Chandler, AZ, 85225. Registration. We have another jammed packed schedule with 50+ sessions with great speakers!  Check out the schedule at schedule.  For more information please visit SQLSaturday.com or email us at sqlsaturday193@sqlsaturday.com.

    Until then, let me share one of my favorite vacation photos with you:

    Hang loose!

    Hang loose!

    Share

  6. Enabling Data Compression In SQL Server

    Not a recommended way to compress your database

    This is how the pioneers compressed their databases

    SQL 2008 saw the introduction of data compression to SQL Server. (I'm referring specifically to data compression, not backup file compression.) This Enterprise-only feature allows SQL Server to store data on disk and in memory in a compressed format. SQL can perform three types of data compression - no compression, row compression, or page compression. I'm not going to go into the details of each method, but in a nutshell (and seriously over-simplified), row compression changes fixed-length fields into variable length fields and page compression includes row compression and also compresses data that is common across fields in all the rows on a page. The Books Online Entry can be found here.

    How do you know if you can benefit from data compression? Microsoft provides a stored procedure that will give you an estimate of how much space you can save using the various compression methods: sp_estimate_data_compression_savings. Before you go enabling data compression, there are a few things to keep in mind.

    • This is an Enterprise Edition (or Developer Edition) only feature. If you enable data compression in a database, you will only be able to restore that database to a SQL Server running Enterprise or Developer edition.  Keep this in mind when designing your disaster recovery plan.
    • Likewise, if you have any servers that will be accessing the compressed databases as a linked server, they will also need to be Enterprise or Developer editions.
    • Compression comes with a price. SQL will store the data in memory and on disk in compresed form, so you gain performance by having fewer disk I/Os and being able to cache more data in memory. The flipside, however, is you'll need some extra CPU cycles to work with that data.
    • SQL data compression is completely transparent to the user or application accessing the database.

    If you've evaluated the pros and cons of data compression and decided it's something you want to implement, how do you go about doing it? SQL Server does not compress an entire database. Rather, it compresses tables and indexes (and indexed views and some other things - see the BOL link above for more details.) Enabling compression on an object requires rebuilding the object, much like an index rebuild. The TSQL commands are ALTER TABLE or ALTER INDEX, depending on which you are compressing. You include the DATA_COMPRESSION clause in the command.

    "But Shaun," I hear you howling, "my database has hundreds of tables!!! Are you telling me I have to manually compress each one?" Unfortunately, yes. Furthermore, there is no way to tell SQL Server that all future objects should be compressed, so if your database has tables being created frequently, you'll need to look for uncompressed objects on a regular basis and compress them.

    Luckily, compression can be performed in an online operation, meaning the objects are still available to users while the compression is happening. Unfortunately, if your table is a heap, the online compression will be single threaded, meaning it will be S-L-O-W. Multi-threaded compression for heaps only happens when SQL can have exclusive access to the table.

    But back to that one-object-at-a-time issue. As all DBAs know, anything that has to be done one object at a time can be scripted and, thus, somewhat automated. I'm about to give you a script that will do this for you.

    This script will created a stored procedure called db_compression. It is based on a script first written by Paul Nielsen several years ago. I've modified it a bit. The procedure takes three input paramters: @minCompression, @MaxRunTimeInMinutes, and @ExecuteCompressCommand. @minCompression is the minimum compression ratio threshold you want to hit before an object is compressed, expressed as a decimal. For example, if you only want to compress objects where you can achieve 25% compression, use a value of .25. @MaxRunTimeInMinutes is the maximum time the procecure will spend compressing objects. Note that this does NOT include the time it takes to perform the initial scan of all objects and calculate their estimated savings from compression. It is also not a hard and fast limit. The script only checks this time before each compress command is started, so it's possible a large table may make the procedure run longer than the limit entered here. Finally, the last option is a flag that tells the script if it should actually execute the compression commands or just print them out.

    How it works

    When first run, the script will create a list of all user tables and indexes in the database. (System tables cannot be compressed.) It writes this list to a table called dbEstimate. There is a flag for each record to indicate if the row has been processed or not and it also notes if the object currently has some sort of compression enabled. Once this table is populated, the script will execute the sp_estimate_data_compression_savings procedure against it. It will run this two times for each record - to get the estimated sizes for the two compression settings the object currently does not have. For example, if a table already has page compression enabled, the script will get estimated sizes for row compression and no compression. This portion of the script is not limited by the @MaxRunTimeInMinutes parameter.

    The next portion of the script does the actual work. The dbEstimate table is looped through with a cursor. The cursor will only look at objects that contain data and where the compression ratio exceeds the value specified in the @minCompression parameter. If the @ExecuteCompressCommand parameter is 1, the script will execute a command to compress the object using whichever method gives the greatest compression ratio. If the @ExecuteCompressCommand is 0, it will not perform the compression and simply output the command instead. The output also includes a count of how many objects were compressed.

    What happens if all the objects selected for compression do not get compressed before the allotted time is up? If you re-run the procedure, it will detect that the dbEstimate table already exists and, if there are still items than have not been processed, it will pick up compressing those, skipping the initial scan portion (which is often time consuming). If, however, all the items in the table have been processed, the script will wipe out the table and re-scan the entire database. This is useful if you have many tables and / or indexes being created.

    If an object is already compressed, the script will not change the object from the existing compression method to the other method (i.e. from page to row or vice-versa), even if the other method may result in greater compression. (This might be something to look into for the next iteration of the script, but I'm not sure if this situation would occur often enough to make the effort worthwhile.)

    The procedure is hard-coded to use the SORT_IN_TEMPDB option. It will also start compressing the smallest objects first. It does not compress using the ONLINE option, mainly due to the single-threaded limitation with heaps mentioned above. If you want this to run in online mode, add that option to lines 415 and 421 below. You can use this procedure to compress a large database over multiple days by running it for a couple hours overnight or during times of minimal usage.

    Compressing the database will not automatically decrease the size of the data file on the disk. If you want to do that, you'll need to shrink the database (and perform the appropriate defragmentation process afterwards).

    
    CREATE PROC [dbo].[db_compression]
          (
           @minCompression FLOAT = .25 /* e.g. .25 for minimum of 25% compression */
          ,@MaxRunTimeInMinutes INT = 60
          ,@ExecuteCompressCommand BIT = 0	/* 1 to execute command */
          )
    AS
          SET nocount ON;
    
    /*
    	Original script by Paul Nielsen www.SQLServerBible.com March 13, 2008
    	Modified by Shaun J. Stuart www.shaunjstuart.com February 27, 2013
    
      Sets compression for all objects and indexs in the database needing adjustment
      If estimated gain is equal to or greater than min compression parameter
        then enables row or page compression, whichever gives greater space savings
      If row and page have same gain then, enables row compression
      If estimated gain is less than min compression parameter, then compression is set to none
    
      - SJS 2/27/13
      - Added @MaxRunTimeInMinutes to limit run length (checked afer each command, so
    		may run longer) Note: This timer only applies to the actual compression process.
    		It does not limit the time it takes to perform a scan of all the tables / indexes
    		and estimate compression savings.
      - Sorted compression cursor to compress smallest tables first
      - Added flag to execute compression command or not
      - Added sort in tempdb option (always)
      - Re-wrote slightly to persist initial scan results. Will only re-scan after all tables
    		have been processed
    
    */
    
          DECLARE @CompressedCount INT;
          SET @CompressedCount = 0;
    
          DECLARE @StartTime DATETIME2;
          SET @StartTime = CURRENT_TIMESTAMP;
    
          CREATE TABLE #ObjEst
                 (
                  PK INT IDENTITY
                         NOT NULL
                         PRIMARY KEY
                 ,object_name VARCHAR(250)
                 ,schema_name VARCHAR(250)
                 ,index_id INT
                 ,partition_number INT
                 ,size_with_current_compression_setting BIGINT
                 ,size_with_requested_compression_setting BIGINT
                 ,sample_size_with_current_compression_setting BIGINT
                 ,sample_size_with_requested_compresison_setting BIGINT
                 );
    
          IF NOT EXISTS ( SELECT    1
                          FROM      sys.objects
                          WHERE     object_id = OBJECT_ID(N'[dbo].[dbEstimate]')
                                    AND type IN (N'U') )
             BEGIN
                   CREATE TABLE dbEstimate
                          (
                           PK INT IDENTITY
                                  NOT NULL
                                  PRIMARY KEY
                          ,schema_name VARCHAR(250)
                          ,object_name VARCHAR(250)
                          ,index_id INT
                          ,ixName VARCHAR(255)
                          ,ixType VARCHAR(50)
                          ,partition_number INT
                          ,data_compression_desc VARCHAR(50)
                          ,None_Size INT
                          ,Row_Size INT
                          ,Page_Size INT
                          ,AlreadyProcessed BIT
                          );
             END
    
      /*
     If all objects have been processed, rescan and start fresh. Useful for catching
      added objects since last scan and / or finding objects that were compressed
      initially with one method but now would do better with another method. But beware -
      this is I/O intensive and can take a while.
    */
    
          IF NOT EXISTS ( SELECT    1
                          FROM      dbEstimate
                          WHERE     AlreadyProcessed = 0 )
             BEGIN
                   DELETE   FROM dbEstimate;
    
                   INSERT   INTO dbEstimate
                            (schema_name
                            ,object_name
                            ,index_id
                            ,ixName
                            ,ixType
                            ,partition_number
                            ,data_compression_desc
                            ,AlreadyProcessed)
                            SELECT  S.name
                                   ,o.name
                                   ,I.index_id
                                   ,I.name
                                   ,I.type_desc
                                   ,P.partition_number
                                   ,P.data_compression_desc
                                   ,0 AS AlreadyProcessed
                            FROM    sys.schemas AS S
                                    JOIN sys.objects AS O ON S.schema_id = O.schema_id
                                    JOIN sys.indexes AS I ON o.object_id = I.object_id
                                    JOIN sys.partitions AS P ON I.object_id = P.object_id
                                                                AND I.index_id = p.index_id
                            WHERE   O.TYPE = 'U';
    
     -- Determine Compression Estimates
                   DECLARE @PK INT
                          ,@Schema VARCHAR(150)
                          ,@object VARCHAR(150)
                          ,@DAD VARCHAR(25)
                          ,@partNO INT
                          ,@indexID INT
                          ,@ixName VARCHAR(250)
                          ,@SQL NVARCHAR(MAX)
                          ,@ixType VARCHAR(50)
                          ,@Recommended_Compression VARCHAR(10);
    
                   DECLARE cCompress CURSOR FAST_FORWARD
                   FOR
                           SELECT   schema_name
                                   ,object_name
                                   ,index_id
                                   ,partition_number
                                   ,data_compression_desc
                           FROM     dbEstimate
                           WHERE    AlreadyProcessed = 0;
    
                   OPEN cCompress;
    
                   FETCH cCompress INTO @Schema, @object, @indexID, @partNO, @DAD;
    
                   WHILE @@Fetch_Status = 0
                         BEGIN
    							/* evaluate objects with no compression */
                               IF @DAD = 'none'
                                  BEGIN
    								/* estimate Page compression */
                                        INSERT  #ObjEst
                                                (object_name
                                                ,schema_name
                                                ,index_id
                                                ,partition_number
                                                ,size_with_current_compression_setting
                                                ,size_with_requested_compression_setting
                                                ,sample_size_with_current_compression_setting
                                                ,sample_size_with_requested_compresison_setting)
                                                EXEC sp_estimate_data_compression_savings
                                                    @Schema_name = @Schema
                                                   ,@object_name = @object
                                                   ,@index_id = @indexID
                                                   ,@partition_number = @partNO
                                                   ,@data_compression = 'page';
    
                                        UPDATE  dbEstimate
                                        SET     none_size = O.size_with_current_compression_setting
                                               ,page_size = O.size_with_requested_compression_setting
                                        FROM    dbEstimate D
                                                JOIN #ObjEst O ON D.Schema_name = O.Schema_Name
                                                                  AND D.Object_name = O.object_name
                                                                  AND D.index_id = O.index_id
                                                                  AND D.partition_number = O.partition_number;
    
                                        DELETE  #ObjEst;
    
    								-- estimate Row compression
                                        INSERT  #ObjEst
                                                (object_name
                                                ,schema_name
                                                ,index_id
                                                ,partition_number
                                                ,size_with_current_compression_setting
                                                ,size_with_requested_compression_setting
                                                ,sample_size_with_current_compression_setting
                                                ,sample_size_with_requested_compresison_setting)
                                                EXEC sp_estimate_data_compression_savings
                                                    @Schema_name = @Schema
                                                   ,@object_name = @object
                                                   ,@index_id = @indexID
                                                   ,@partition_number = @partNO
                                                   ,@data_compression = 'row';
    
                                        UPDATE  dbEstimate
                                        SET     row_size = O.size_with_requested_compression_setting
                                        FROM    dbEstimate D
                                                JOIN #ObjEst O ON D.Schema_name = O.Schema_Name
                                                                  AND D.Object_name = O.object_name
                                                                  AND D.index_id = O.index_id
                                                                  AND D.partition_number = O.partition_number;
    
                                        DELETE  #ObjEst;
                                  END /* end evaluating objects with no compression */
    
    							/* evaluate objects with row compression */
                               IF @DAD = 'row'
                                  BEGIN
    					           /* estimate Page compression */
                                        INSERT  #ObjEst
                                                (object_name
                                                ,schema_name
                                                ,index_id
                                                ,partition_number
                                                ,size_with_current_compression_setting
                                                ,size_with_requested_compression_setting
                                                ,sample_size_with_current_compression_setting
                                                ,sample_size_with_requested_compresison_setting)
                                                EXEC sp_estimate_data_compression_savings
                                                    @Schema_name = @Schema
                                                   ,@object_name = @object
                                                   ,@index_id = @indexID
                                                   ,@partition_number = @partNO
                                                   ,@data_compression = 'page';
    
                                        UPDATE  dbEstimate
                                        SET     row_size = O.size_with_current_compression_setting
                                               ,page_size = O.size_with_requested_compression_setting
                                        FROM    dbEstimate D
                                                JOIN #ObjEst O ON D.Schema_name = O.Schema_Name
                                                                  AND D.Object_name = O.object_name
                                                                  AND D.index_id = O.index_id
                                                                  AND D.partition_number = O.partition_number;
    
                                        DELETE  #ObjEst;
    
    								/* estimate None compression */
                                        INSERT  #ObjEst
                                                (object_name
                                                ,schema_name
                                                ,index_id
                                                ,partition_number
                                                ,size_with_current_compression_setting
                                                ,size_with_requested_compression_setting
                                                ,sample_size_with_current_compression_setting
                                                ,sample_size_with_requested_compresison_setting)
                                                EXEC sp_estimate_data_compression_savings
                                                    @Schema_name = @Schema
                                                   ,@object_name = @object
                                                   ,@index_id = @indexID
                                                   ,@partition_number = @partNO
                                                   ,@data_compression = 'none';
    
                                        UPDATE  dbEstimate
                                        SET     none_size = O.size_with_requested_compression_setting
                                        FROM    dbEstimate D
                                                JOIN #ObjEst O ON D.Schema_name = O.Schema_Name
                                                                  AND D.Object_name = O.object_name
                                                                  AND D.index_id = O.index_id
                                                                  AND D.partition_number = O.partition_number;
    
                                        DELETE  #ObjEst;
                                  END /* end evaluating objects with row compression */
    
    							/* evalutate objects with page compression */
                               IF @DAD = 'page'
                                  BEGIN
    							/* estimate Row compression */
                                        INSERT  #ObjEst
                                                (object_name
                                                ,schema_name
                                                ,index_id
                                                ,partition_number
                                                ,size_with_current_compression_setting
                                                ,size_with_requested_compression_setting
                                                ,sample_size_with_current_compression_setting
                                                ,sample_size_with_requested_compresison_setting)
                                                EXEC sp_estimate_data_compression_savings
                                                    @Schema_name = @Schema
                                                   ,@object_name = @object
                                                   ,@index_id = @indexID
                                                   ,@partition_number = @partNO
                                                   ,@data_compression = 'row';
    
                                        UPDATE  dbEstimate
                                        SET     page_size = O.size_with_current_compression_setting
                                               ,row_size = O.size_with_requested_compression_setting
                                        FROM    dbEstimate D
                                                JOIN #ObjEst O ON D.Schema_name = O.Schema_Name
                                                                  AND D.Object_name = O.object_name
                                                                  AND D.index_id = O.index_id
                                                                  AND D.partition_number = O.partition_number;
    
                                        DELETE  #ObjEst;
    
    								/* estimate None compression */
                                        INSERT  #ObjEst
                                                (object_name
                                                ,schema_name
                                                ,index_id
                                                ,partition_number
                                                ,size_with_current_compression_setting
                                                ,size_with_requested_compression_setting
                                                ,sample_size_with_current_compression_setting
                                                ,sample_size_with_requested_compresison_setting)
                                                EXEC sp_estimate_data_compression_savings
                                                    @Schema_name = @Schema
                                                   ,@object_name = @object
                                                   ,@index_id = @indexID
                                                   ,@partition_number = @partNO
                                                   ,@data_compression = 'none';
    
                                        UPDATE  dbEstimate
                                        SET     none_size = O.size_with_requested_compression_setting
                                        FROM    dbEstimate D
                                                JOIN #ObjEst O ON D.Schema_name = O.Schema_Name
                                                                  AND D.Object_name = O.object_name
                                                                  AND D.index_id = O.index_id
                                                                  AND D.partition_number = O.partition_number;
    
                                        DELETE  #ObjEst;
                                  END /* end evaluating objects with page compression */
    
                               FETCH NEXT FROM cCompress INTO @Schema, @object,
                                     @indexID, @partNO, @DAD
                         END
    
                   CLOSE cCompress
                   DEALLOCATE cCompress
    
                   PRINT 'Initial scan complete.'
             END
      /* End evaluating compression savings. Now do the actual compressing. */
    
          PRINT 'Beginning compression.';
    
      /* Do not process objects that do not meet our compression criteria */
    
          UPDATE    dbEstimate
          SET       AlreadyProcessed = 1
          WHERE     (1 - (CAST(Row_Size AS FLOAT) / None_Size)) < @minCompression
                    AND (Row_Size <= Page_Size)                  AND None_Size > 0;
    
          UPDATE    dbEstimate
          SET       AlreadyProcessed = 1
          WHERE     (1 - (CAST(Page_Size AS FLOAT) / None_Size)) < @minCompression
                    AND (Page_Size <= Row_Size)                 AND None_Size > 0;
    
      /* Do not set compression on empty objects */
    
          UPDATE    dbEstimate
          SET       AlreadyProcessed = 1
          WHERE     None_size = 0;
    
      /* Do not process objects that are already using an acceptable compression method */
    
          UPDATE    dbEstimate
          SET       AlreadyProcessed = 1
    	  WHERE     (1 - (CAST(Page_Size AS FLOAT) / none_Size)) >= @minCompression
                    AND (Page_Size <= Row_Size)                 AND None_Size > 0
    		AND data_compression_desc = 'PAGE';
    
          UPDATE    dbEstimate
          SET       AlreadyProcessed = 1
          WHERE     (1 - (CAST(Row_Size AS FLOAT) / None_Size)) >= @minCompression
                    AND (Row_Size <= Page_Size)                 AND None_Size > 0
    		AND data_compression_desc = 'ROW';
    
    	  /* Ignore tables that have not had their sizes calculated. This can occur if the procedure
    			execution was cancelled before the initial scan completed. */
    
          UPDATE    dbEstimate
          SET       AlreadyProcessed = 1
          WHERE     None_Size IS NULL
                    OR Row_Size IS NULL
                    OR Page_Size IS NULL;
    
     -- set the compression
          DECLARE cCompress CURSOR FAST_FORWARD
          FOR
                  SELECT    schema_name
                           ,object_name
                           ,partition_number
                           ,ixName
                           ,ixType
                           ,CASE WHEN (1 - (CAST(Row_Size AS FLOAT) / none_Size)) >= @minCompression
                                      AND (Row_Size <= Page_Size) THEN 'Row'                              WHEN (1 - (CAST(page_Size AS FLOAT) / none_Size)) >= @minCompression
                                      AND (Page_Size <= row_Size) THEN 'Page'
                                 ELSE 'None'
                            END AS Recommended_Compression
                           ,PK
                  FROM      dbEstimate
                  WHERE     None_Size <> 0
                            AND (CASE WHEN (1 - (CAST(Row_Size AS FLOAT)
                                                 / none_Size)) >= @minCompression
                                           AND (Row_Size <= Page_Size) THEN 'Row'                                   WHEN (1 - (CAST(page_Size AS FLOAT)                                              / none_Size)) >= @minCompression
                                           AND (Page_Size <= row_Size) THEN 'Page'
                                      ELSE 'None'
                                 END <> data_compression_desc)
                            AND AlreadyProcessed = 0
                  ORDER BY  None_Size ASC;		/* start with smallest tables first */
    
          OPEN cCompress
    
          FETCH cCompress INTO @Schema, @object, @partNO, @ixName, @ixType,
                @Recommended_Compression, @PK  -- prime the cursor;
    
          WHILE @@Fetch_Status = 0
                BEGIN
    
                      IF @ixType = 'Clustered'
                         OR @ixType = 'heap'
                         SET @SQL = 'ALTER TABLE ' + @Schema + '.' + @object
                             + ' Rebuild with (data_compression = '
                             + @Recommended_Compression + ', SORT_IN_TEMPDB=ON)';
    
                      ELSE
                         SET @SQL = 'ALTER INDEX ' + @ixName + ' on ' + @Schema
                             + '.' + @object
                             + ' Rebuild with (data_compression = '
                             + @Recommended_Compression + ',SORT_IN_TEMPDB=ON)';
    
                      IF DATEDIFF(mi, @StartTime, CURRENT_TIMESTAMP) < @MaxRunTimeInMinutes
                         BEGIN
                               IF @ExecuteCompressCommand = 1
                                  BEGIN
                                        PRINT 'Compressing table/index: '
                                              + @Schema + '.' + @object;
                                        EXEC sp_executesql
                                            @SQL;
    
                                        UPDATE  dbEstimate
                                        SET     AlreadyProcessed = 1
                                        WHERE   PK = @PK;
    
                                        SET @CompressedCount = @CompressedCount + 1;
                                  END
                               ELSE
                                  BEGIN
                                        PRINT 'Command execution not enabled. Command is:'
                                              + @SQL;
                                  END
                         END
                      ELSE
                         BEGIN
                               PRINT 'Max runtime reached. Exiting...';
                               BREAK
                         END
    
                      FETCH cCompress INTO @Schema, @object, @partNO, @ixName,
                            @ixType, @Recommended_Compression, @PK;
                END
    
          CLOSE cCompress;
          DEALLOCATE cCompress;
    
          PRINT 'Objects compressed: ' + CONVERT(VARCHAR(10), @CompressedCount);
    
          DROP TABLE #ObjEst;
    
          RETURN
    
    

    Share

  7. Single User Mode Can Be Misleading

    This is something that might seem obvious to DBAs, but I was speaking with a developer this morning and realized there can be some confusion about what exactly single user mode means.

    A database can be in multi-user mode, where multiple connections can be made to it, or single user mode, where only one person can connect to it. But here is the bit that can be confusing - single user mode doesn't really mean single user - it means single connection. A database in single user mode can only have one connection to it, not multiple connections from the same user. The term single user is somewhat misleading here. A more appropriate term would be single connection mode.

    Share

  8. Sometimes It’s The Little Things That Get You

    I recently performed an in-place upgrade of a SQL Server from SQL 2008 R2 to SQL 2012. All seemed to work well, until I tried taking a database backup. That failed.

    This particular SQL Server uses LiteSpeed for backups and the following error was being returned when it ran:

    Msg 17750, Level 16, State 0, Procedure xp_backup_database, Line 1
    Could not load the DLL xpSLS.dll, or one of the DLLs it references. Reason: 126(The specified module could not be found.).

    A quick file search on the server showed the DLL was located at D:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\Binn. From the path, we can tell that is obviously a SQL 2008 R2 directory. Because the newer version of SQL Server installs in a different directory, the DLL used by LiteSpeed was no longer where SQL Server could find it. I copied the file to the corresponding location in the SQL 2012 folder hierarchy: D:\Program Files\Microsoft SQL Server\MSSQL11.MSSQLSERVER\MSSQL\Binn. Once that was done, my backups worked fine.

    As DBAs, when we plan upgrades, we tend to think about differences in how the database engine works between different versions and we focus on those differences in determining if an upgrade will cause any application issues. It's easy to overlook third party apps. And even if we did check to verify those apps are compatible (and I did check that LiteSpeed was compatible with SQL 2012), it's often easy to overlook some simple facts - like program paths will be different after an upgrade.

    Share

  9. Determining If A Full Backup Is Needed For A Log Or Differential Backup

    What annoys you? For me, a few things I find annoying are people talking on cell phones in restaurants, people chewing with their mouth open, and getting calls from telemarketers. SQL jobs failing also annoy me. OK, maybe not as much as Chatty Cathy yakking with her friend on the phone during dinner, but it's close.

    I hate it when my maintenance jobs fail for some reason. I REALLY hate it when they fail intermittently. That means there is usually some strange situation that I didn't think about when writing my code that pops its head up once in a while and crashes my routines. Unless it's something serious, I can ignore it for a while. If it happens infrequently, it's tempting to just let it be. But it always sticks in my craw, bothering me like a tiny sliver in my finger.

    This is how I discovered the issue with TRY / CATCH blocks and restoring databases that I wrote about last time. I finally got so fed up with the seemingly random failures, I had to dig in and fix the thing once and for all.

    This is also the driving fforce behind the iterative revisions of my backup routines. I got tired of my transaction log backup failing every time a new database was created or a database was restored onto a new server. Rather than having the routine fail with a "no current database backup" error, I wrote code to check to make sure there was a full backup. If there wasn't, the script would make one. Problem solved.

    Or mostly solved. Occasionally, the script would still fail with the same error. Failures were much rarer, but they were still happening. It was annoying.

    Some research led me to discover that the latest cause of failure was databases that were in full recovery mode, switched to simple mode, then back to full recovery. If you try to make a log backup in this situation, the backup will fail with a "no current database backup" error. The question was, how can I determine if the recovery mode of a database changed?

    The answer lies in the sys.database_recovery_status table. This table has one row for each database and if the last_log_backup_lsn column is NULL, a full backup needs to be performed.

    So I revised my log backup script once again, this time adding this:

      IF EXISTS ( SELECT  1
          FROM    sys.database_recovery_status
          WHERE   database_id = DB_ID(@Database)
                 AND last_log_backup_lsn IS NULL )
            /* make a full backup, then continue */
    

    Now, at long last, my log backup script should be fairly bullet proof. It will detect if a database is new, if it has been restored from another server, or if the recovery mode has changed. None of these will cause it to fail with a "no current database backup" error. And I can't think of another reason this error might be raised,  so I am hopeful I will never see this error again.

    Note: Reading the MSDN article about this table does not seem to state that this column behaves this way. However, I've verified this behavior on SQL 2008 R2 and this tip was given to me by Paul Randal, so I'm pretty comfortable using it.

    Share

  1. 1
  2. Next ›
  3. Last »