Showing posts with label replace. Show all posts
Showing posts with label replace. Show all posts

Sunday, October 16, 2011

Adding Up the Logical Reads in the Output Window

nixie

Probably the first thing I learned about query tuning is that "clock-on-the-wall" time is definitely not the thing you want to measure. The elapsed time a query takes can be influenced by countless factors (other processes running, disk speed, number of processors, amount of RAM, phase of the moon, a few hundred database settings) that it's too rough a metric for serious performance tuning.

What I've been taught to measure, instead, are logical reads, which are the number of disk I/O reads needed to execute the query. If the query doesn't change, and the underlying data doesn't change, it's been my experience (and it makes sense theoretically), that you can run a given query all day long, under any load conditions you can create, and the number of logical reads will always be the same. This immunity to outside "noise" makes it ideal for tuning: you can be pretty sure (given the same schema, of course - indexes leap to mind - and at least similar data) that the logical read counts on your machine will be the same on another machine (such as the Production server, or your boss's laptop).

Ok, so how do we capture logical reads? Easy - simply enable the STATISTICS IO option, like so:

SET STATISTICS IO ON

Once turned on for a given session, this emits the number logical reads made for every table (including temp tables) for every statement executed, until you turn it off (by saying SET STATISTICS IO OFF). Let's look at a single line that would be emitted:

Table 'Invoices'. Scan count 1, logical reads 332, physical reads 10, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

You'll notice a lot of other values displayed on this line, but for now, ignore them. One that you should especially ignore (for now) is physical reads, because its count is included in the logical reads value. That is, in our one-line example, we can see there are going to be 332 pages read from somewhere. That "somewhere" will be either in RAM (the buffer pool), or from disk (your "C:" drive, for example), but both of these "somewheres" are counted by logical reads. Physical reads only counts what was read from disk this time, which is the kind of unpredictable "clock-on-the-wall" information we know to be unreliable for query tuning (for now). Why are they unpredictable? Because who knows why the page wasn't in buffer pool in memory, and so had to read from disk, at the precise moment we needed it? It's unknowable, and even if it weren't - if you had perfect knowledge of the innards of the computer at that moment - it's still unpredictable.

Looking at our one-line example above, everything seems pretty rosy. SET STATISTICS IO ON spits out the logical reads we need, so all we have to do is total them up. But this is where it gets ugly, because the information we're interested in is interspersed with our own PRINT messages, RAISERROR output, warning and informational messages from the system, and data output from SELECT statements (if we're using "Output to Text" mode, which I often do, as it's the only way to get the dang output window to scroll). This can all be quite a mess to read:

Starting at 12:25:30 PM...
Table 'Customers'. Scan count 6, logical reads 10213968, physical reads 125391, read-ahead reads 254294, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
CustomerNum   InvoiceNum   Amt
----------- ------------ --------------------------------------
2344   1331111   333.23
4492   4837227 44222.84
3434   222444   91811.33
98474   54422  22.09
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Updating the total prices...
(1 row(s) affected)
Table 'Invoices'. Scan count 1, logical reads 332, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
12:30:38: Added row to the Invoices table for 2011/10/12
Warning: Aggregate calculation excluded NULL values.
Table 'Worktable'. Scan count 3, logical reads 120, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Now, try to total up the logical reads in that output. If there's only a few lines of output, it's manageable, but imagine trying to decipher hundreds of lines of stuff like this. Over time, I developed a rigamarole system:

  • copy the entire contents of the Output window to the clipboard
  • fire up Excel, and paste the output into it
  • from Excel's "Data" tab, click the "Sort A to Z" button
  • select only the rows that start with "Table..." and copy them to the clipboard
  • paste the rows into a new window in Management Studio
  • use a regular expression search-and-replace to strip out all the text on each line before the logical read numbers
  • use a regular expression search-and-replace to strip out all the text on each line after the logical read numbers
  • copy the column of logical read numbers back into Excel
  • from Excel's "Formulas" tab, use the AutoSum button to, finally, calculate the total number of logical reads

"Crude but effective" doesn't quite do this process justice. There must be a better way, I thought.

My first idea was an AddOn for Management Studio, but the more I looked into it, I realized I just couldn't face the tedium of application programming (yes, I'm now permanently spoiled by a platform - SQL Server - that actually works without swearing at it). And while SQL Server isn't exactly famous for its text-manipulation ability, what I was was doing didn't have to be fast or elegant. And I'd rather hack something together in T-SQL in a few hours than beat my head against the C# wall for a few days. (Don't believe me? Ask someone who's written a truly great Visual Studio add-on.) As a bonus, the people using the code would be able to understand it, and maybe even build upon and improve it.

So, after an embarrassingly long delay caused by forgetting, again, that REPLACE replaces all occurrences, not just the first one, here it is. It works correctly as far as I can tell, but don't use it as an example of how to write good T-SQL code. (Please tell me about any bugs you find. Use at your own risk. Some settling of contents may occur during shipping.)

SET NOCOUNT ON
SET QUOTED_IDENTIFIER OFF

DECLARE @Text NVARCHAR(MAX) =
"-- INSERT YOUR TEXT BETWEEN THESE TWO LINES -----------------------------------
Starting at 12:25:30 PM...
Table 'Customers'. Scan count 6, logical reads 10213968, physical reads 125391, read-ahead reads 254294, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
CustomerNum   InvoiceNum   Amt
----------- ------------ --------------------------------------
2344   1331111   333.23
4492   4837227 44222.84
3434   222444   91811.33
98474   54422  22.09
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Updating the total prices...
(1 row(s) affected)
Table 'Invoices'. Scan count 1, logical reads 332, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
12:30:38: Added row to the Invoices table for 2011/10/12
Warning: Aggregate calculation excluded NULL values.
Table 'Worktable'. Scan count 3, logical reads 120, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
-- INSERT YOUR TEXT BETWEEN THESE TWO LINES -----------------------------------"

DECLARE @Lines TABLE (Txt NVARCHAR(MAX), Num INT)
DECLARE @nLF   INT

WHILE 1=1
BEGIN
    SET @nLF = CHARINDEX(NCHAR(10), @Text)
    IF @nLF = 0  BREAK
    
    IF @nLF < 3
    BEGIN
        SET @Text = STUFF(@Text, 1, 1, '')  
        CONTINUE
    END
    
    INSERT @Lines (Txt)
    VALUES (LEFT(@Text, @nLF - LEN(NCHAR(13) + NCHAR(10))))

    SET @nLF = PATINDEX('%' + NCHAR(13) + NCHAR(10) + '%', @Text)
    SET @Text = STUFF(@Text, 1, @nLF, '')
END

DELETE @Lines WHERE Txt NOT LIKE '%, logical reads %'
UPDATE @Lines SET Num = PATINDEX('%, logical reads %', Txt)
UPDATE @Lines SET Txt = STUFF(Txt, 1, Num + LEN(', logical reads '), '')
UPDATE @Lines SET Num = CHARINDEX(',', Txt)
UPDATE @Lines SET Txt = LEFT(Txt, Num - 1)
UPDATE @Lines SET Num = CAST(Txt AS INT)
DELETE @Lines WHERE Num = 0

SELECT Num      AS 'Logical Reads'       FROM @Lines
SELECT SUM(Num) AS 'Total Logical Reads' FROM @Lines

SET QUOTED_IDENTIFIER ON

To use this script, just copy-and-paste the entire contents of the Output window between the two lines in red in the @Text variable's definition, and hit F5 (Ctrl-E, whatever) to execute the script. You should get output like this:

The first set of results represent the SET STATISTICS IO rows that have non-zero logical reads, useful mostly for debugging the script. You can suppress this output by commenting out the first SELECT statement in the script (third line of code from the bottom). The second set of results is what we're looking for: the total number of logical reads that appear in the text of the Output window.

Monday, January 31, 2011

Removing All Duplicate Tabs, Linefeeds, Returns, and Spaces

The trace tables created by SQL Profiler are nice and all, but it's a pain to search through the TextData column.  The code below uses a brute force method to replace all contiguous whitespace characters with a single space.  This makes it possible to search the TextData column without differences in whitespace getting in the way.

    -- Removing All Duplicate Tabs, Linefeeds, Returns, and Spaces. 

    DECLARE @RowsAffected INT = 0
    SET NOCOUNT ON
    
    -- Create all 16 (4^2) Tab, Linefeed, Return, and Space combinations.
    DECLARE @PairTT VARCHAR(2) = CHAR( 9) + CHAR( 9)    
    DECLARE @PairTL VARCHAR(2) = CHAR( 9) + CHAR(10)    
    DECLARE @PairTR VARCHAR(2) = CHAR( 9) + CHAR(13)    
    DECLARE @PairTS VARCHAR(2) = CHAR( 9) + CHAR(32)    
    DECLARE @PairLT VARCHAR(2) = CHAR(10) + CHAR( 9)    
    DECLARE @PairLL VARCHAR(2) = CHAR(10) + CHAR(10)    
    DECLARE @PairLR VARCHAR(2) = CHAR(10) + CHAR(13)    
    DECLARE @PairLS VARCHAR(2) = CHAR(10) + CHAR(32)    
    DECLARE @PairRT VARCHAR(2) = CHAR(13) + CHAR( 9)
    DECLARE @PairRL VARCHAR(2) = CHAR(13) + CHAR(10)
    DECLARE @PairRR VARCHAR(2) = CHAR(13) + CHAR(13)
    DECLARE @PairRS VARCHAR(2) = CHAR(13) + CHAR(32)
    DECLARE @PairST VARCHAR(2) = CHAR(32) + CHAR( 9)
    DECLARE @PairSL VARCHAR(2) = CHAR(32) + CHAR(10)
    DECLARE @PairSR VARCHAR(2) = CHAR(32) + CHAR(13)
    DECLARE @PairSS VARCHAR(2) = CHAR(32) + CHAR(32)
                                                    
    WHILE 1=1
    BEGIN
        SELECT @RowsAffected = COUNT(*) 
          FROM dbo.TraceTableReportPermanent
         WHERE TextData LIKE '%' + @PairTT + '%'
            OR TextData LIKE '%' + @PairTL + '%'
            OR TextData LIKE '%' + @PairTR + '%'
            OR TextData LIKE '%' + @PairTS + '%'
            OR TextData LIKE '%' + @PairLT + '%'
            OR TextData LIKE '%' + @PairLL + '%'
            OR TextData LIKE '%' + @PairLR + '%'
            OR TextData LIKE '%' + @PairLS + '%'
            OR TextData LIKE '%' + @PairRT + '%'
            OR TextData LIKE '%' + @PairRL + '%'
            OR TextData LIKE '%' + @PairRR + '%'
            OR TextData LIKE '%' + @PairRS + '%'
            OR TextData LIKE '%' + @PairST + '%'
            OR TextData LIKE '%' + @PairSL + '%'
            OR TextData LIKE '%' + @PairSR + '%'
            OR TextData LIKE '%' + @PairSS + '%'

        IF @RowsAffected > 0
            UPDATE dbo.TraceTableReportPermanent
               SET TextData = REPLACE(
                              REPLACE(
                              REPLACE(
                              REPLACE(
                              REPLACE(
                              REPLACE(
                              REPLACE(
                              REPLACE(
                              REPLACE(
                              REPLACE(
                              REPLACE(
                              REPLACE(
                              REPLACE(
                              REPLACE(
                              REPLACE(
                              REPLACE(TextData, @PairTT, CHAR(32)),
                                                @PairTL, CHAR(32)), 
                                                @PairTR, CHAR(32)), 
                                                @PairTS, CHAR(32)),
                                                @PairLT, CHAR(32)),
                                                @PairLL, CHAR(32)),
                                                @PairLR, CHAR(32)),
                                                @PairLS, CHAR(32)),
                                                @PairRT, CHAR(32)),
                                                @PairRL, CHAR(32)),
                                                @PairRR, CHAR(32)),
                                                @PairRS, CHAR(32)),
                                                @PairST, CHAR(32)),
                                                @PairSL, CHAR(32)),
                                                @PairSR, CHAR(32)),
                                                @PairSS, CHAR(32))
             WHERE TextData LIKE '%' + @PairTT + '%'
                OR TextData LIKE '%' + @PairTL + '%'
                OR TextData LIKE '%' + @PairTR + '%'
                OR TextData LIKE '%' + @PairTS + '%'
                OR TextData LIKE '%' + @PairLT + '%'
                OR TextData LIKE '%' + @PairLL + '%'
                OR TextData LIKE '%' + @PairLR + '%'
                OR TextData LIKE '%' + @PairLS + '%'
                OR TextData LIKE '%' + @PairRT + '%'
                OR TextData LIKE '%' + @PairRL + '%'
                OR TextData LIKE '%' + @PairRR + '%'
                OR TextData LIKE '%' + @PairRS + '%'
                OR TextData LIKE '%' + @PairST + '%'
                OR TextData LIKE '%' + @PairSL + '%'
                OR TextData LIKE '%' + @PairSR + '%'
                OR TextData LIKE '%' + @PairSS + '%'
        ELSE
            BREAK
             
        RAISERROR('Deleted %d whitespace pairs from dbo.TraceTable',
                   10, 1, @RowsAffected) WITH NOWAIT
    END

Friday, November 14, 2008

Show Indexes' Fragmentation

This script will display fragmentation info on every index in a specified table.

-- ShowIndexFragmentation.sql
SET NOCOUNT ON

-- Set variables.
DECLARE @sDatabase  sysname
SET     @sDatabase  = 'YourDB'           --  Change this to your database!

DECLARE @sTableName sysname
SET     @sTableName = 'YourTable'        -- Change this to your table!

DECLARE @sCmd       NVARCHAR(4000)
SET     @sCmd       = ''

-- Create temp table.
IF OBJECT_ID('tempdb..#ShowContigRaw') IS NOT NULL  DROP TABLE #ShowContigRaw

CREATE TABLE #ShowContigRaw
(
   TableName       CHAR(255),
   ObjectId        INT,
   IndexName       CHAR(255),
   IndexId         INT,
   Lvl             INT,
   CountPages      INT,
   CountRows       INT,
   MinRecSize      INT,
   MaxRecSize      INT,
   AvgRecSize      INT,
   ForRecCount     INT,
   Extents         INT,
   ExtentSwitches  INT,
   AvgFreeBytes    INT,
   AvgPageDensity  DECIMAL(38,14),
   ScanDensity     INT,
   BestCount       INT,
   ActualCount     INT,
   LogicalFrag     DECIMAL(38,14),
   ExtentFrag      DECIMAL(38,14)
)

-- Call DBCC SHOWCONTIG on all indexes on the table to populate the temp table.
SET @sCmd = 'USE ' + @sDatabase +
            ' DBCC SHOWCONTIG (''' + @sTableName +
            ''') WITH TABLERESULTS, ALL_INDEXES, NO_INFOMSGS'

RAISERROR(@sCmd, 10, 1) WITH NOWAIT
INSERT INTO #ShowContigRaw  EXEC(@sCmd)

-- Create temp table.
IF OBJECT_ID('tempdb..#ShowContigCooked') IS NOT NULL  DROP TABLE #ShowContigCooked

CREATE TABLE #ShowContigCooked
(
   TableName       NVARCHAR(255),      -- Table Name
   ObjectId        NVARCHAR(255),      -- Object ID
   IndexName       NVARCHAR(255),      -- Index Name
   IndexId         NVARCHAR(255),      -- Index ID
   Lvl             NVARCHAR(255),      -- Level
   CountPages      NVARCHAR(255),      -- Page Count
   CountRows       NVARCHAR(255),      -- Row Count
   MinRecSize      NVARCHAR(255),      -- Min Row Size
   MaxRecSize      NVARCHAR(255),      -- Max Row Size
   AvgRecSize      NVARCHAR(255),      -- Avg Row Size
   ForRecCount     NVARCHAR(255),      -- Forwarded Record Count
   Extents         NVARCHAR(255),      -- Extent Count
   ExtentSwitches  NVARCHAR(255),      -- Extent Switches
   AvgFreeBytes    NVARCHAR(255),      -- AvgFreeBytes
   AvgPageDensity  NVARCHAR(255),      -- Avg Page Density
   ScanDensity     NVARCHAR(255),      -- Extent Contiguousness aka Scan Density
   BestCount       NVARCHAR(255),      -- Extent Changes Best Count
   ActualCount     NVARCHAR(255),      -- Extent Changes Actual Count
   LogicalFrag     NVARCHAR(255),      -- Logical Fragmentation
   ExtentFrag      NVARCHAR(255)       -- Extent Fragmentation
)

INSERT INTO #ShowContigCooked
   SELECT * FROM #ShowContigRaw

-- Massage values.
UPDATE #ShowContigCooked
   SET TableName       = RTRIM(TableName)

UPDATE #ShowContigCooked
   SET IndexName       = 'N/A'
 WHERE IndexName       = ''

UPDATE #ShowContigCooked
   SET IndexName       = RTRIM(IndexName)
 
UPDATE #ShowContigCooked
   SET CountPages      = REPLACE(CONVERT(NVARCHAR,
                            CONVERT(MONEY, CountPages),     1),  '.00', '')

UPDATE #ShowContigCooked
   SET CountRows       = REPLACE(CONVERT(NVARCHAR,
                            CONVERT(MONEY, CountRows),      1),  '.00', '')

UPDATE #ShowContigCooked
   SET MinRecSize      = REPLACE(CONVERT(NVARCHAR, 
                            CONVERT(MONEY, MinRecSize),     1),  '.00', '')

UPDATE #ShowContigCooked
   SET MaxRecSize      = REPLACE(CONVERT(NVARCHAR,
                            CONVERT(MONEY, MaxRecSize),     1),  '.00', '')

UPDATE #ShowContigCooked
   SET AvgRecSize      = REPLACE(CONVERT(NVARCHAR, 
                            CONVERT(MONEY, AvgRecSize),     1),  '.00', '')

UPDATE #ShowContigCooked
   SET ForRecCount     = REPLACE(CONVERT(NVARCHAR,
                            CONVERT(MONEY, ForRecCount),    1),  '.00', '')

UPDATE #ShowContigCooked
   SET Extents         = REPLACE(CONVERT(NVARCHAR, 
                            CONVERT(MONEY, Extents),        1),  '.00', '')

UPDATE #ShowContigCooked
   SET ExtentSwitches  = REPLACE(CONVERT(NVARCHAR, 
                            CONVERT(MONEY, ExtentSwitches), 1),  '.00', '')

UPDATE #ShowContigCooked
   SET AvgPageDensity  =         CONVERT(NVARCHAR, 
                            CONVERT(MONEY, AvgPageDensity), 1) + '%'

UPDATE #ShowContigCooked
   SET BestCount       = REPLACE(CONVERT(NVARCHAR,
                            CONVERT(MONEY, BestCount),      1),  '.00', '')

UPDATE #ShowContigCooked
   SET ActualCount     = REPLACE(CONVERT(NVARCHAR,
                            CONVERT(MONEY, ActualCount),    1),  '.00', '')

UPDATE #ShowContigCooked
   SET ScanDensity     =         CONVERT(NVARCHAR, 
                            CONVERT(MONEY, ScanDensity),    1) + '%'

UPDATE #ShowContigCooked
   SET LogicalFrag     =         CONVERT(NVARCHAR, 
                            CONVERT(MONEY, LogicalFrag),    1) + '%'

UPDATE #ShowContigCooked
   SET ExtentFrag      =         CONVERT(NVARCHAR, 
                            CONVERT(MONEY, ExtentFrag),     1) + '%'

-- Done.  Display results.
SELECT
   'Name of the table or view.'
       AS 'Table Name',
   'OBJECT_ID of the table or view.'
       AS 'Object ID',
   'Name of the index.'
       AS 'Index Name',
   'ID of the index.'
       AS 'Index ID',
   'Level of the index. Level 0 is the leaf (or data) level of the index.
       The level number increases moving up the tree toward the index root.
       Level is 0 for a heap.'
       AS 'Level',
   'Number of pages comprising that level of the index or entire heap.'
       AS 'Page Count',
   'Number of data or index records at that level of the index.
       For a heap, this is the number of data records in the entire heap.'
       AS 'Row Count',
   'Minimum record size in that level of the index or entire heap.'
       AS 'Min Row Size',
   'Maximum record size in that level of the index or entire heap.'
       AS 'Max Row Size',
   'Average record size in that level of the index or entire heap.'
       AS 'Avg Row Size',
   'On UPDATES, if there is insufficient space on the page to fit the new record,
       the record is moved to a new page, and a forwarding record is left in the
       original location to point to the new location.'
       AS 'Forwarded Record Count',
   'Number of extents in that level of the index or entire heap.'
       AS 'Extent Count',
   'Number of jumps from one extent to another while traversing all the pages.'
       AS 'Extent Switches',
-- 'Average number of free bytes on the pages scanned. The higher the number,
-- the less full the pages are. Lower numbers are better. This number is also
-- affected by row size; a large row size can result in a higher number.'
--     AS 'AvgFreeBytes',
   'Measure of how full the pages are. The higher the percentage, the better.'
       AS 'Avg Page Density',
   '"BestCount" / "Actual count"   Is 100% if all extents are contiguous; else,
       some fragmentation exists.'
       AS 'Extent Changes Best Count',
   'The ideal number of extent changes if the extents are perfectly contiguous.'
       AS 'Extent Changes Actual Count',
   'Actual number of extent changes while traversing.'
       AS 'Extent Contiguousness aka Scan Density',
   'Percentage of out-of-order pages returned from scanning the leaf pages of
       an index. This number is not relevant to heaps and text indexes.'
       AS 'Logical Fragmentation',
   'Percentage of out-of-order extents in scanning the leaf pages of an index.
       This number is not relevant to heaps.'
       AS 'Extent Fragmentation'

UNION

SELECT
   TableName       AS 'Table Name',                   -- YourTable
   ObjectId        AS 'Object ID',                    -- 613577224
   IndexName       AS 'Index Name',                   -- tank_data_index
   IndexId         AS 'Index ID',                     -- 2
   Lvl             AS 'Level',                        -- 0
   CountPages      AS 'Page Count',                   -- 48,139
   CountRows       AS 'Row Count',                    -- 7,271,509
   MinRecSize      AS 'Min Row Size',                 -- 51
   MaxRecSize      AS 'Max Row Size',                 -- 61
   AvgRecSize      AS 'Avg Row Size',                 -- 51
   ForRecCount     AS 'Forwarded Record Count',       -- 0
   Extents         AS 'Extent Count',                 -- 9,233
   ExtentSwitches  AS 'Extent Switches',              -- 26,012
-- AvgFreeBytes    AS 'AvgFreeBytes',
   AvgPageDensity  AS 'Avg Page Density',             -- 98%
   BestCount       AS 'Extent Changes Best Count',    -- 6,018
   ActualCount     AS 'Extent Changes Actual Count',  -- 26,013
   ScanDensity     AS 'Scan Density',  -- 23%
   LogicalFrag     AS 'Logical Fragmentation',        -- 19%
   ExtentFrag      AS 'Extent Fragmentation'          -- 70%
  FROM #ShowContigCooked
 WHERE TableName NOT LIKE 'dt%' AND
       TableName NOT LIKE 'sys%'
 ORDER BY 'Extent Fragmentation' DESC

DROP TABLE #ShowContigCooked

Convert INT to Comma-Separated NVARCHAR

A quick expression to change an INT like 1234567 to a comma-separated NVARCHAR like '1,234,567'.

REPLACE(CONVERT(NVARCHAR, CONVERT(MONEY, CountPages), 1), '.00', '')