Last month I wrote a post called Hash Partitioning with SQL Server 2012’s SEQUENCE object and CYCLE argument and I was asked a good question by Eric Humphrey (twitter) about whether there was any advantage of using a sequence object for hash partitioning over an identity/modulus/computed-column technique. 

At that point I hadn’t compared the two techniques, but I wanted to perform a test eventually, so here is what I found…  This was a quick-and-dirty test, by the way, as I really should have been working on something else (first), but I needed a little “fun” time.

Before testing, I made a few minor modifications to the schema used in my original post:

1.      I split the range of the partition function as follows:

 

ALTER PARTITION FUNCTION pfFactInternetSales () SPLIT RANGE (0)

 

2.      I changed the sequence object to be zero-based instead of one-based:

 

CREATE SEQUENCE dbo.Seq_FactInternetSales

    AS int

               START WITH 0

               INCREMENT BY 1

    MINVALUE 0

    MAXVALUE 10

    CYCLE

    CACHE 10;

That is all that was changed with the sequence object technique I used before. As for a modulus technique, I created two additional columns to another version of the FactInternetSales table:

ProductID int NOT NULL IDENTITY(1,1),

               PartitionBucketKey AS ProductID %11  PERSISTED,

 

After adding the columns I referenced the PartitionBucketKey table as the partition key for the clustered index and imported the data from an INSERT…SELECT (as I did with the sequence object method).

 

So did the performance differ between the two techniques?

 

Here are the results of eight separate tests (each executed with a cold data cache for the SELECT, truncated destination table and pre-sized data and log files):

 

Test number

Rows loaded

Buckets

Hash Partition

Method

1

1,207,960 rows

11 buckets

26 seconds

Sequence

2

1,207,960 rows

11 buckets

26 seconds

Sequence

3

1,207,960 rows

11 buckets

29 seconds

Sequence

4

1,207,960 rows

11 buckets

26 seconds

Sequence

5

1,207,960 rows

11 buckets

27 seconds

Modulus

6

1,207,960 rows

11 buckets

24 seconds

Modulus

7

1,207,960 rows

11 buckets

25 seconds

Modulus

8

1,207,960 rows

11 buckets

26 seconds

Modulus

So the modulus method seems to perform ever-so-slightly faster than the sequence method.  Stylistically, the sequence method involved a more direct way to create X number of hash buckets (basically no calculation necessary).  With that said, the execution plans are nearly identical.

Sequence Plan (estimated cost – 17.4357)

clip_image002

Modulus Plan (estimated cost – 17.635)

clip_image004

I’m sure there are other variations of this test that I could try out, but that’s all for now. I’m curious about other techniques people may be using (like a CLR function to do the hashing), so please post them here if you have used an interesting variation on this post’s theme.

Consider the following scenario:

·        You have Transactional Replication deployed

·        Data is flowing, but just not as fast as you would like

·        This scenario could apply to local/remote distributors and push/pull subscribers

There are several different techniques we can use to narrow down where the replication performance issue is happening.  Perhaps you’ve already found that the performance issue is happening for log reader reads or distribution database writes.  Or perhaps you suspect the issue is on the subscriber? 

While the various replication techniques can help us narrow down the lagging member of the topology, I still would like more visibility into why a particular agent read or write process is performing more slowly.  Fortunately, you can do this in SQL Server 2008+…

In the following example, I’ll start by retrieving the session IDs of the log reader and distribution agents (and as an aside my replication topology is SQL Server instance version 10.50.2500):

-- Log Reader

SELECT session_id, program_name,

             reads,

             writes,

             logical_reads

FROM sys.dm_exec_sessions

WHERE original_login_name =

       'SQLSKILLS\SQLskillsLogReaderAG';

-- Distribution Agent

SELECT session_id, program_name,

             reads,

             writes,

             logical_reads

FROM sys.dm_exec_sessions

WHERE original_login_name =

       'SQLSKILLS\SQLskillsDistAGT';

In this example I’m using separate accounts to run the agent executables, however I could have also added a predicate on program_name based on the publication I was interested in evaluating.  For example, I could have said for the Log Reader agent – program_name = ‘Repl-LogReader-0-AdventureWorks2008R2-6’ and for the Distribution agent – that’s more interesting, as we have program_name = ‘CAESAR-AdventureWorks2008R2-Pub_AW_2008R2-AUGUSTUS-1’ (subscriber is AUGUSTUS, publisher is CAESAR).  But if you just used that program name, you won’t get Replication Distribution History session, which would be program_name = ‘Replication Distribution History’ and may also be interesting.

So in my example, I have 5 different sessions I’m interested in (and yours will vary based on the number of published databases, independent agents, server role, etc):

·        The log reader agent was using sessions 55, 57, 59

·        The distribution agent had two sessions (61 for history and 62 for the executable)

Now that I have my session ids, I’m going to create an extended events session that I can run during the “slow performing” period to help illuminate where to investigate next (and for more general discussion on this technique, see Paul Randal’s post “Capturing wait stats for a single operation”):

CREATE EVENT SESSION Replication_AGT_Waits

ON SERVER

ADD EVENT sqlos.wait_info(

       ACTION (sqlserver.session_id)

    WHERE ([package0].[equal_uint64]([sqlserver].[session_id],(55)) OR [package0].[equal_uint64]([sqlserver].[session_id],(57)) OR [package0].[equal_uint64]([sqlserver].[session_id],(59)) OR [package0].[equal_uint64]([sqlserver].[session_id],(61)) OR [package0].[equal_uint64]([sqlserver].[session_id],(62)))),

ADD EVENT sqlos.wait_info_external(

       ACTION (sqlserver.session_id)

    WHERE ([package0].[equal_uint64]([sqlserver].[session_id],(55)) OR [package0].[equal_uint64]([sqlserver].[session_id],(57)) OR [package0].[equal_uint64]([sqlserver].[session_id],(59)) OR [package0].[equal_uint64]([sqlserver].[session_id],(61)) OR [package0].[equal_uint64]([sqlserver].[session_id],(62))))

ADD TARGET package0.asynchronous_file_target

       (SET FILENAME = N'C:\temp\ReplAGTStats.xel',

        METADATAFILE = N'C:\temp\ReplAGTStats.xem')

GO

After creating the session, I’ll start.  In my test, I just ran the slow performing workload against one of the published tables, launched Replication Monitor, waited for the rows to arrive at the subscriber and then stopped the event session:

ALTER EVENT SESSION Replication_AGT_Waits

ON SERVER STATE = START;

-- Run representative replication workload against publisher

-- Launch Monitor and wait for all trans to be fully distributed

ALTER EVENT SESSION Replication_AGT_Waits

ON SERVER STATE = STOP;

Next, I created two intermediate temp tables to start going through the collected data:

-- Raw data into intermediate table

SELECT CAST(event_data as XML) event_data

INTO #ReplicationAgentWaits_Stage_1

FROM sys.fn_xe_file_target_read_file

             ('C:\temp\ReplAGTStats*.xel',

              'C:\temp\ReplAGTStats*.xem',

              NULL, NULL)

-- Aggregated data into intermediate table

-- #ReplicationAgentWaits       

SELECT

       event_data.value

       ('(/event/action[@name=''session_id'']/value)[1]', 'smallint') as session_id,

       event_data.value

       ('(/event/data[@name=''wait_type'']/text)[1]', 'varchar(100)') as wait_type,

       event_data.value

       ('(/event/data[@name=''duration'']/value)[1]', 'bigint') as duration,

       event_data.value

       ('(/event/data[@name=''signal_duration'']/value)[1]', 'bigint') as signal_duration,

       event_data.value

       ('(/event/data[@name=''completed_count'']/value)[1]', 'bigint') as completed_count

INTO #ReplicationAgentWaits_Stage_2

FROM #ReplicationAgentWaits_Stage_1;

Then I took a look at how things broke out by session_id:

SELECT session_id,

             wait_type,

             SUM(duration) total_duration,

             SUM(signal_duration) total_signal_duration,

             SUM(completed_count) total_wait_count

FROM #ReplicationAgentWaits_Stage_2

GROUP BY session_id,

             wait_type

ORDER BY session_id,

             SUM(duration) DESC;

Here were the results:

clip_image002

Session 55, 57 and 59 were my log agent sessions.  Just looking at session 57 (highlighted in purple), we see that IO_COMPLETION had the highest wait duration.  If I check out the accumulated reads from sys.dm_exec_sessions for that session, I see it is doing all reads, whereas session 59 was doing all writes (so we can start mapping to the agent thread roles). 

Session id 61 (in yellow) represented the Replication Distribution History process and session id 62 (in green) represented the distribution agent process.  As we can see for 62 – the longest duration was due to NETWORK_IO.  We also see a similar value from PREEMPTIVE_OS_WAITFORSINGLEOBJECT (and if you think that these seem correlated, indeed this preemptive wait type is seen in conjunction with the network waittype wait).

So what would we see at the subscriber side?  For this specific scenario, I saw the following (using the session of my distribution agent account) which was session id 55:

clip_image003

In this case, the top wait (by duration) was WRITELOG on the subscriber for the CAESAR_AdventureWorks2008R2_Pub_AW_2008R2 distribution agent process - although the number was not very high.

So if you’re experiencing slow replication, you may consider this additional technique in order to help further identify where the bottlenecks may be in the topology and also get initial ideas on why this may be. 

Categories:

Let’s say you’ve “disabled” parallelism on your SQL Server instance via the ‘max degree of parallelism’ setting as follows:

EXEC sp_configure 'max degree of parallelism', 1

RECONFIGURE

 

Now most folks know (or are finding out) that this doesn’t really prevent parallel plans if you throw in a MAXDOP hint.  For example, let’s take the following query:

 

EXEC sp_executesql

       N'SELECT charge_no FROM dbo.charge

       WHERE charge_dt = @charge_dt OPTION (MAXDOP 4)',

       N'@charge_dt datetime', 

       @charge_dt = '1999-07-20 10:49:11.833';

If we look at the actual plan and properties, even though I’ve capped the instance level max degree of parallelism, I get a parallel plan (using SQL Sentry Plan Explorer):

 

clip_image002

clip_image004

 

Now let’s say I want to out-smart anyone capping the MAXDOP by using Resource Governor and the MAX_DOP workload group setting:

 

CREATE WORKLOAD GROUP wgReportUsers

WITH

(

     MAX_DOP = 1

) USING [rpReportUsers]

GO

 

I also created a classifier function and a resource pool (assuming I want to restrict anything else).  I won’t add that setup code here since it’s just the standard RG configuration process.  For this example I’ve configured nothing unusual with the exception of setting MAX_DOP = 1 and making sure my reporting user gets classified to the constrained workload group.

 

Now let’s say the user connects in after RG is configured.  I am able to confirm this via Profiler through the PreConnect:Completed event class:

 

clip_image006

 

In this example, GroupId 257 maps to my workload group.

So I’m logged in as the report user, and I’ve given that user the appropriate read-permissions and also SHOWPLAN so we can see that serial plan we’re expecting.  I also cleared the cache so that the new execution would be a new compilation.

But what do we see when the user tries to override the MAXDOP?

 

EXEC sp_executesql

       N'SELECT charge_no FROM dbo.charge

       WHERE charge_dt = @charge_dt OPTION (MAXDOP 4)',

       N'@charge_dt datetime', 

       @charge_dt = '1999-07-20 10:49:11.833';

 

clip_image008

And you’ll see the same in SQL Server Management Studio (in case you were wondering):

clip_image010

 

So what about the rows per thread that I showed you earlier?  How does it look now?  This time, there are no separate columns by thread:

 

clip_image012

Not there.  And while there are parallel iterators, there rows per thread is not showing.

Seen another way, let’s look at the properties of the Clustered Index Scan and associated XML Showplan RunTimeCountersPerThread values by thread for the original, unconstrained execution:

 

clip_image014

<RunTimeCountersPerThread Thread="3" ActualRows="2" ActualEndOfScans="1" ActualExecutions="1" />

<RunTimeCountersPerThread Thread="2" ActualRows="2" ActualEndOfScans="1" ActualExecutions="1" />

<RunTimeCountersPerThread Thread="4" ActualRows="6" ActualEndOfScans="1" ActualExecutions="1" />

<RunTimeCountersPerThread Thread="1" ActualRows="6" ActualEndOfScans="1" ActualExecutions="1" />

<RunTimeCountersPerThread Thread="0" ActualRows="0" ActualEndOfScans="0" ActualExecutions="0" />

 

And this is what the MAXDOP 1 workload group constrained request looks like in the properties in contrast:

 

clip_image016

<RunTimeCountersPerThread Thread="0" ActualRows="16" ActualEndOfScans="1" ActualExecutions="1" />

 

For the constrained plan, I can also see that the DegreeOfParallelism is 0 for the QueryPlan element, but not for the RelOps:

 

<QueryPlan DegreeOfParallelism="0" CachedPlanSize="16" CompileTime="1" CompileCPU="1" CompileMemory="120">

<RelOp AvgRowSize="19" EstimateCPU="0.880078" EstimateIO="6.8935" EstimateRebinds="0" EstimateRewinds="0"

EstimateRows="14.8802" LogicalOp="Clustered Index Scan" NodeId="1" Parallel="true"

PhysicalOp="Clustered Index Scan" EstimatedTotalSubtreeCost="7.77357" TableCardinality="1600000">

What’s more, while the graphical plans look identical and has identical estimated costs (I didn’t mention this earlier – but both plans had an estimated cost of 8.18609).

So the request did indeed honor the max degree of parallel setting for the workload group in the end – it just didn’t remove the parallelism related iterator and properties.

I use Windows 7 as my laptop host OS and this limits me from using Hyper-V natively to handle VM guests.   Yes, I could do the dual-boot thing, but I haven’t done it yet and I do like the simplicity of not having to switch host OS contexts.  Windows 8 will, thankfully, be adding Hyper-V hosting support to the client, but in the meantime I’ve been using VirtualBox for my guest OS needs.

Jonathan Kehayias blogged about it extensively in his “Building a Completely Free Playground” series – and once I moved to SQLskills I felt less skittish about running a product that happened to have the big “O” word listed prominently on the application toolbar header.

I actually use VirtualBox for a variety of purposes – not just for SQL Server test environments. For example, if I need to connect to an environment that has conflicting (or unique) remote connectivity requirements, rather than risk clobbering my host VPN settings, I provision a clean, isolated guest environment that I use for connectivity and configuration instead.

Sometimes I’ll have a group of servers that I want to launch at the same time or I’ll want to put a group of them in a saved state at the same time (for example – when running a guest Failover Cluster).  In one case I have six different guests that I want to save or start at (nearly) the same time and I don’t like having to right-click save on each one – especially since my guests will quickly think something is wrong and when I bring them back from a saved state, it takes a few seconds to get them de-frazzled.

By the way – I’m using these scripts for test environments.  These are test environments that if I lost them, I’d be annoyed – but not devastated. 

Rather than rely on the GUI, I created a BAT file to save the guests in a batch:

cd /D "C:\Program Files\Oracle\VirtualBox\"

START /b VBoxManage.exe  controlvm "Node4_D2"  savestate

START /b VBoxManage.exe  controlvm "Node3_D2"  savestate

START /b VBoxManage.exe  controlvm "Node2_D1"  savestate

START /b VBoxManage.exe  controlvm "Node1_D1"  savestate

START /b VBoxManage.exe  controlvm "SAN_D1" savestate

START /b VBoxManage.exe  controlvm "Node_DC"  savestate

I use the START option so that I’m not waiting for the save operation in a serial fashion.  I use the “/b” so that six windows don’t pop up.  As for the Virtualbox command syntax – it’s pretty straightforward – I call VBoxManage.exe with controlvm followed by the name of the VM and the option to save it (savestate).

As for starting them up from the saved state, I do stagger things a bit.  I use startvm instead of controlvm, followed by the VM name and I don’t use the START keyword for anything I want to ensure is executed in order (I also use the TIMEOUT option to wait a few seconds before launching other VMs):

cd /D "C:\Program Files\Oracle\VirtualBox\"

VBoxManage.exe startvm "Node_DC" 

VBoxManage.exe startvm "SAN_D1"

TIMEOUT 10

START /b VBoxManage.exe startvm "Node1_D1" 

START /b VBoxManage.exe startvm "Node2_D1" 

START /b VBoxManage.exe startvm "Node3_D2" 

START /b VBoxManage.exe startvm "Node4_D2"

Even with these scripts I wouldn’t call this an orderly and synchronized save and start.  A Windows guest cluster may sometimes catch on to something being amiss – but often it does not.  Sometimes I’ll have to wait a few seconds for the nodes to start talking to each other again.

Anyhow – if you also use VirtualBox and have any tips you’d like to share when it comes to coordinating the guest launches, please post here as I’m definitely curious to learn about them.

Categories:
Off-Topic | Virtual Machines

When I first heard about SQL Server 2012’s SEQUENCE object – I thought it was an interesting feature to be added and one that I have been asked about by customers in the past (from those who had worked on different database platforms).  But when I looked at the CYCLE argument of SEQUENCE, that’s when I really got interested.

I wondered if it could be used in the service of implementing hash partitioning (of sorts) – allowing me to evenly distribute rows across a set number of partitions based on a hash key.  In this scenario I want the distribution to be evenly spread out, but NOT partition based on other business keys (like a datetime column or other attribute that has business or application meaning).

So will a column with a sequence default also work as a partition key? 

I started off by creating a new table based on AdventureWorkDWDenali’s FactInternetSales table:

-- Create demonstration Fact table, no constraints, indexes, keys

-- Tested on version 11.0.1750 (SQL Server 2012 RC0)

USE [SequenceDemo];

GO

CREATE TABLE [dbo].[FactInternetSales](

       [ProductKey] [int] NOT NULL,

       [OrderDateKey] [int] NOT NULL,

       [DueDateKey] [int] NOT NULL,

       [ShipDateKey] [int] NOT NULL,

       [CustomerKey] [int] NOT NULL,

       [PromotionKey] [int] NOT NULL,

       [CurrencyKey] [int] NOT NULL,

       [SalesTerritoryKey] [int] NOT NULL,

       [SalesOrderNumber] [nvarchar](20) NOT NULL,

       [SalesOrderLineNumber] [tinyint] NOT NULL,

       [RevisionNumber] [tinyint] NOT NULL,

       [OrderQuantity] [smallint] NOT NULL,

       [UnitPrice] [money] NOT NULL,

       [ExtendedAmount] [money] NOT NULL,

       [UnitPriceDiscountPct] [float] NOT NULL,

       [DiscountAmount] [float] NOT NULL,

       [ProductStandardCost] [money] NOT NULL,

       [TotalProductCost] [money] NOT NULL,

       [SalesAmount] [money] NOT NULL,

       [TaxAmt] [money] NOT NULL,

       [Freight] [money] NOT NULL,

       [CarrierTrackingNumber] [nvarchar](25) NULL,

       [CustomerPONumber] [nvarchar](25) NULL,

       [OrderDate] [datetime] NULL,

       [DueDate] [datetime] NULL,

       [ShipDate] [datetime] NULL,

)ON [PRIMARY];

GO

Next I created the sequence object (increment by 1, with a min of 1, max of 10, caching of 10 at a time and a cycling of values):

CREATE SEQUENCE dbo.Seq_FactInternetSales

    AS int

    START WITH 1

    INCREMENT BY 1

    MINVALUE 1

    MAXVALUE 10

    CYCLE

    CACHE 10;

After that, I added a new column to the Fact table called PartitionBucketKey and associated it with the new sequence object:

ALTER TABLE [dbo].[FactInternetSales]

ADD PartitionBucketKey int DEFAULT

(NEXT VALUE FOR dbo.Seq_FactInternetSales);

Next, I created a partition function and scheme:

-- Create a new partition function

CREATE PARTITION FUNCTION pfFactInternetSales (int)

AS RANGE LEFT FOR VALUES (1,2,3,4,5,6,7,8,9);

-- Create a new partition scheme

-- And yes, being lazy about the FGs, as I just want to see whether the

-- individual partitions fan-out the way I want...

CREATE PARTITION SCHEME psFactInternetSales

AS PARTITION pfFactInternetSales

ALL TO ( [PRIMARY] );

Next up, I created a clustered index on the table referencing the PK columns used in the original version of this table but then referencing the PartitionBucketKey in partition scheme:

-- Create it on the new column referencing the sequence

CREATE CLUSTERED INDEX IX_FactInternetSales

ON  dbo.FactInternetSales(SalesOrderNumber, SalesOrderLineNumber)

ON psFactInternetSales (PartitionBucketKey);

It’s show time.  Now I went ahead and populated 60,398 rows from the original table.  Not much for this test I realize, but this was just an initial proof-of-concept:

INSERT dbo.FactInternetSales

(ProductKey, OrderDateKey, DueDateKey, ShipDateKey, CustomerKey,

PromotionKey, CurrencyKey, SalesTerritoryKey, SalesOrderNumber,

SalesOrderLineNumber, RevisionNumber, OrderQuantity, UnitPrice,

ExtendedAmount, UnitPriceDiscountPct, DiscountAmount, ProductStandardCost,

TotalProductCost, SalesAmount, TaxAmt, Freight, CarrierTrackingNumber,

CustomerPONumber, OrderDate, DueDate, ShipDate)

SELECT ProductKey, OrderDateKey, DueDateKey, ShipDateKey, CustomerKey,

PromotionKey, CurrencyKey, SalesTerritoryKey, SalesOrderNumber,

SalesOrderLineNumber, RevisionNumber, OrderQuantity, UnitPrice,

ExtendedAmount, UnitPriceDiscountPct, DiscountAmount, ProductStandardCost,

TotalProductCost, SalesAmount, TaxAmt, Freight, CarrierTrackingNumber,

CustomerPONumber, OrderDate, DueDate, ShipDate

FROM [AdventureWorksDWDenali].[dbo].[FactInternetSales];

Now I’ll check if the 60,398 rows were divided up evenly over the 10 partitions:

SELECT partition_number, row_count

FROM sys.dm_db_partition_stats

WHERE object_id = object_id('[dbo].[FactInternetSales]')

clip_image001

It worked.  And if you look at the individual rows, you’ll see the cycle of sequence values were defined based on the PK composite key (SalesOrderNumber, SalesOrderLineNumber):

SELECT SalesOrderNumber, SalesOrderLineNumber, PartitionBucketKey

FROM [dbo].[FactInternetSales]

ORDER BY SalesOrderNumber, SalesOrderLineNumber

clip_image003

Okay, so it works.  But is this a wise thing to do? 

I don’t know yet.  I have other questions about this technique and I’d like to do more testing on various scenarios.  But I do like the fact that I’m able to leverage a native engine feature in service of another native engine feature.  Time will tell if this is a viable pattern or a known anti-pattern.

Categories:
SQL Server 2012

Update: ** Make sure to check out the comments at the end of this post.  There are some interesting differences in behavior between transactional replication (pull/push subscribers) versus merge replication's behavior.  ** 

Yesterday I was working on implementing transactional replication with the goal of limiting the permissions each replication account ran under.  I created three separate domain accounts for the snapshot, log reader and distribution agents.  These accounts had no other permissions before I began:

·        I created logins on the publisher and distributor (in this case, the same SQL Server instance) and I added the snapshot and log reader agent accounts to the db_owner role of the distribution and publisher databases. 

·        This was a push subscription, so I also added the distribution agent to the db_owner role for the distribution database, but I did not grant it access to the publication database.  I did make the distribution agent a member of db_owner for the subscription database (which was located on a separate server and default instance).

·        I gave the snapshot agent “write” permissions and the distribution agent “read” permissions to the snapshot share.

By the way, all this talk of db_owner makes it sound like I wasn’t limiting permissions all that much; however this fixed database role membership is indeed a minimum requirement in this implementation.  It’s also typically more restrained then what I’ve seen out in the field. Usually I’ll see the use of domain accounts with sysadmin used to manage everything in the replication topology.  I don’t usually see a separate set of accounts configured for each agent role, nor do I see them set up for each unique topology (for very large environments, the administrative overhead may not make this a practical choice – but that’s another discussion altogether).

I did leave out one step though – and I’ll get to that in a moment.  After applying the permissions I described, I set up the new publication and new subscription, and the data flowed correctly with no issues and no sysadmin permissions required.

The step I specifically left out was the adding of the distribution agent to the Publication Access List (PAL).  According to Books Online, “Access to a publication is controlled by the publication access list (PAL).”  Also according to Books Online, the distribution agent for a push subscription must “Be a member of the PAL.”  I wondered why? And if this is such a key area – why don’t we hear much discussion of the PAL?  If you search the replication forums, you’ll find very few questions about it (searching today, I found 26 loosely related threads).  Either this means that most shops use high privilege accounts and haven’t pushed further to find out the role of PAL – or the PAL role isn’t entirely what it seems to be (as its described, it seems to suggest that the distribution agent account needs membership in order to synchronize).

Now if it must be a member, why was transaction replication working properly (rows were moving fine from publisher to distributor and distributor to subscriber). 

My first assumption was that I missed something or that somehow the distribution agent account was getting implied permissions either through group membership.

The first thing I validated was the current PAL list of accounts (looking explicitly for my distribution agent account – called SQLskills\SQLskillsDistAGT).  Looking at the PAL, this account had NOT been explicitly granted membership somehow through other activities:

clip_image002

Perhaps SQLskills\SQLskillsDistAGT was gaining access through group membership?  Seemed unlikely to me, but I checked nonetheless by using EXECUTE AS LOGIN and querying the sys.login_token to see the groups associated with that account:

clip_image004

I didn’t see any connections or group memberships that would map to the PAL. 

My next thought was to examine the SQL Server Agent job and ensure it really was running under the context of SQLskills\SQLskillsDistAGT. The SQL Server Agent Job for the distribution agent was owned by the Administrator account, but the job step itself was running as the SQLskills\SQLskillsDistAGT proxy:

clip_image006

The proxy maps to a security credential, which in this case was my SQLskills\SQLskillsDistAGT account.  I validated the mapping by querying sys.credentials (checking the credential_identity column):

clip_image008

So the mapping was what I expected. 

But was the job really connected as that account?  I ran a few test transactions at the publisher and again confirmed that rows were flowing to the subscriber.  I then queried sys.dm_exec_sessions for the distribution agent session, checking the login name and running a few times to ensure it was incrementing the logical reads:

clip_image010

Logical reads were incrementing and the job was indeed running under the account.

So where are we?  Basically, I could find no connection whatsoever between the PAL membership and my distribution agent account. 

So because I wanted to be absolutely sure (and because this was a test environment) I removed all accounts from the PAL (including “sa”).  I did so one-by-one, testing to see if it broke replication.  And guess what?  Replication just kept on working.  I even restarted the agents to see if it would initiate some kind of challenge-response, and it did not. 

So is PAL access required?  And if so, what is the boundary of that requirement?

I logged off of my Administrator account and logged in to the publisher/distributor SQL Server instance as the SQLskills\SQLskillsDistAGT.  I then opened up SSMS and looked to see if I could view the publication:

clip_image012

No publications to be seen, even though this account is actually responsible for running the distribution agent and is doing so successfully.

I then jumped back on my Administrator account and first added SQLskills\SQLskillsDistAGT to the public role of the publication database (required in order to be seen in PAL) and then I added SQLskills\SQLskillsDistAGT to the PAL:

clip_image014

clip_image016

After doing this, I logged aback in as the distribution agent account, and sure enough, I can now “see” the publication (and also launch a new subscription, more importantly).

clip_image018

So this now made sense why PAL wasn’t the talk of the town.  Most DBAs I’ve worked with set up replication with their own high privilege credentials – even when designating other credentials for the replication agents.  Once they do, the agents work as advertised.  It’s when the agent account wishes to participate independently of the DBA that the PAL helps restrict the visibility of available publications.

If you’ve seen other variations or even contradictions related to the PAL - I’d love to hear about it.  We can help flesh out some of the ambiguities around this feature on this post.

Categories:
Replication

I received a question today about the impact of data compression on the transaction log.  While most of the time we talk about data compression from a data page and memory utilization perspective, I hadn’t actually directly tested the impact to transaction logging and I wanted to see it for myself. 

Here is just a very small scale test using data from AdventureWorks.HumanResources.Employee into two tables – one with a clustered index that is not using compression and one that is using page compression (I used a separate database just to keep things clean):

USE [CompressionTest];

GO

CREATE TABLE [dbo].[Employee](

      [EmployeeID] [int] IDENTITY(1,1) NOT NULL,

      [NationalIDNumber] [nvarchar](15) NOT NULL,

      [ContactID] [int] NOT NULL,

      [LoginID] [nvarchar](256) NOT NULL,

      [ManagerID] [int] NULL,

      [Title] [nvarchar](50) NOT NULL,

      [BirthDate] [datetime] NOT NULL,

      [MaritalStatus] [nchar](1) NOT NULL,

      [Gender] [nchar](1) NOT NULL,

      [HireDate] [datetime] NOT NULL,

      [SalariedFlag] [bit] NOT NULL,

      [VacationHours] [smallint] NOT NULL,

      [SickLeaveHours] [smallint] NOT NULL,

      [CurrentFlag] [bit] NOT NULL,

      [rowguid] [uniqueidentifier] NOT NULL,

      [ModifiedDate] [datetime] NOT NULL,

      [EncryptedNationalIDNumber2] [varbinary](128) NULL

) ON [PRIMARY];

GO

CREATE CLUSTERED INDEX [IX_Employee]

ON [dbo].[Employee]

(

      [EmployeeID] ASC

)

ON [PRIMARY];

GO

CREATE TABLE [dbo].[Employee_Compressed](

      [EmployeeID] [int] IDENTITY(1,1) NOT NULL,

      [NationalIDNumber] [nvarchar](15) NOT NULL,

      [ContactID] [int] NOT NULL,

      [LoginID] [nvarchar](256) NOT NULL,

      [ManagerID] [int] NULL,

      [Title] [nvarchar](50) NOT NULL,

      [BirthDate] [datetime] NOT NULL,

      [MaritalStatus] [nchar](1) NOT NULL,

      [Gender] [nchar](1) NOT NULL,

      [HireDate] [datetime] NOT NULL,

      [SalariedFlag] [bit] NOT NULL,

      [VacationHours] [smallint] NOT NULL,

      [SickLeaveHours] [smallint] NOT NULL,

      [CurrentFlag] [bit] NOT NULL,

      [rowguid] [uniqueidentifier] NOT NULL,

      [ModifiedDate] [datetime] NOT NULL,

      [EncryptedNationalIDNumber2] [varbinary](128) NULL

) ON [PRIMARY];

GO

CREATE CLUSTERED INDEX [IX_Employee_Compressed]

ON [dbo].[Employee_Compressed]

(

      [EmployeeID] ASC

)WITH (DATA_COMPRESSION=PAGE) ON [PRIMARY]

GO

Now the test is very simple.  I perform a full backup, and an initial transaction log backup:

BACKUP DATABASE CompressionTest TO DISK = 'c:\temp\CompressionTest.bak';

BACKUP LOG CompressionTest TO DISK = 'c:\temp\throwaway.trn';

Next, I insert 290,000 rows into the uncompressed table:

SET NOCOUNT ON;

INSERT dbo.Employee

(NationalIDNumber, ContactID, LoginID, ManagerID, Title, BirthDate,

MaritalStatus, Gender, HireDate, SalariedFlag, VacationHours,

SickLeaveHours, CurrentFlag, rowguid, ModifiedDate, EncryptedNationalIDNumber2)

SELECT NationalIDNumber, ContactID, LoginID, ManagerID, Title, BirthDate,

MaritalStatus, Gender, HireDate, SalariedFlag, VacationHours,

SickLeaveHours, CurrentFlag, rowguid, ModifiedDate, EncryptedNationalIDNumber2

FROM AdventureWorks.HumanResources.Employee;

GO 1000

Data used is 74,288 KB:

EXEC sp_spaceused 'dbo.Employee';

Querying the log record length from fn_dblog shows 120,503,216 bytes:

SELECT SUM([log record length])

FROM fn_dblog (NULL, NULL);

Backing up the transaction log produces a file that is 121,964 KB in size:

SELECT SUM([log record length])

FROM fn_dblog (NULL, NULL);

Now to insert rows into the table with data compression:

INSERT dbo.Employee_Compressed

(NationalIDNumber, ContactID, LoginID, ManagerID, Title, BirthDate,

MaritalStatus, Gender, HireDate, SalariedFlag, VacationHours,

SickLeaveHours, CurrentFlag, rowguid, ModifiedDate, EncryptedNationalIDNumber2)

SELECT NationalIDNumber, ContactID, LoginID, ManagerID, Title, BirthDate,

MaritalStatus, Gender, HireDate, SalariedFlag, VacationHours,

SickLeaveHours, CurrentFlag, rowguid, ModifiedDate, EncryptedNationalIDNumber2

FROM AdventureWorks.HumanResources.Employee;

GO 1000

Data used is 36,800 KB (versus 74,288 KB):

EXEC sp_spaceused 'dbo.Employee_Compressed';

Querying the log record length from fn_dblog shows 100,677,612 bytes versus the uncompressed 120,503,216 bytes:

SELECT SUM([log record length])

FROM fn_dblog (NULL, NULL);

Backing up the transaction log produces a file that is 102,158 KB in size (versus 121,964 KB):

SELECT SUM([log record length])

FROM fn_dblog (NULL, NULL);

So on a much larger scale – you can imagine the transaction log size reduction could be non-trivial.  Mileage will vary on the overall compression you can get from specific tables and data – but this is just another potential benefit to be aware of.  And as an aside, compressed rows are written to the transaction log in the ROW compression format and not the PAGE type.

Categories:
Performance

My first SQLSaturday (104) experience was a great one.  I got a chance to meet many new people (speakers, organizers and attendees), reunite with several others who I’ve met in previous contexts and also map Twitter handles to “real life” people.  

In addition to socializing with folks, I also got a chance to watch some great talks, including those from Tim Ford, Grant Fritchey, Jim Murphy, Jason Strate and Karen Lopez (and the tail end of a talk from Chris Randall which – although I was late, was really enjoyable and I wished I had seen the whole thing).  While the technical content is interesting, I also liked watching the different presentation styles.  For example, I liked Tim Ford’s easy (and subtle) sense of humor and creative visual layout of the DMVs that made a dense topic more easily accessible.  I also took note of Grant Fritchey’s energy and topic focus (drilling down on parameter sniffing), Jim Murphy’s ability to manage multiple moving parts in a smoothly presented AlwaysOn demo, Chris Randall’s clarity of examples (and he’s got a great sense of humor), Jason Strate’s approachable speaking style + Zen-like PPT format and Karen Lopez’s ability to “own” the room and get people actively engaged in the presentation.

SQLSaturday 104 was also my first chance to present “Performance Issue Archetypes” and I really enjoyed the experience. Plenty of suspense since it was a 2:30PM session.  My session was scheduled at the same time as some popular sessions (like Thomas LaRock’s well-received “Choose Your Own Adventure” session and John Morehouse’s “10 Things That Every DBA Should Know!”)  So even with the concurrent sessions, I was thankful to still get an audience.   I do think I jammed way too much material into one hour.  I think my session could easily have fit into 3 hours with some additional demos added – and that was after some heavy cutting I did a few weeks beforehand.  I’ll likely calibrate this presentation over time – but I did get feedback that folks got actionable value from the talk – so that made me happy (especially since this was the whole point of the talk).

Speaking of which, you can download the PDF of my presentation here.  Just note that there were some twists and turns “off deck” and that the presentation deck itself was the launching point.  Hopefully you can see the presentation in-person someday. 

SQLSaturday 104 was a great way to kick off what promises to be a year of new experiences and a significant workload. Coupled with a full plate of ongoing consulting engagements, I’ll be teaching modules alongside Paul, Kimberly and Jon in the IE2: Performance Tuning and IE3: High Availability & Disaster Recovery Immersion Events.  This means I’ll be heading to Tampa, Chicago, London and Bellevue at minimum.

On a somewhat related note; it’s been about 4 months since I joined SQLskills.  A few people have asked how it’s been so far – and here are my general observations:

·        Bottom line is that it going really well.  With any big move, it’s easy to second guess yourself – particularly since I was leaving a job with plenty of opportunity and 6 years of accumulated benefits and relationships.  I haven’t regretted it though.  Quite the opposite, the last four months have just felt “right”.  I’m working just as hard – but working on the areas that put me in the “flow state” (talking about Mihály Csíkszentmihályi’s flow) for a good majority of the day.  Consulting/writing/teaching/learning.  All good.

·        Of course a major aspect of it is getting the chance to collaborate with Kimberly, Paul and Jon.  It is great to be able to share ideas and brainstorm on tough or interesting scenarios.  The energy level they have is mind boggling.  There is so much to do/learn/investigate that the toughest part is choosing where to focus next.

·        Since October I’ve worked with 24 different customers on various types of engagements. This is by far my favorite aspect of the job. The engagements have covered performance tuning & scalability, health checks, security, benchmarking, high availability and disaster recovery.  I’ve also been involved in some writing projects and will likely have more IP related work throughout this next year interspersed with the Immersion Events.  Regarding events, I do hope to attend and speak at more events, schedule permitting.

·        Paul and Kimberly have also let me get involved in the business side of things (for example – pre-engagements and scoping calls). I appreciate this since I like meeting new people and listening to new problem scenarios (it’s like getting a new puzzle to work on).   

So thankfully, it’s all good.  I hope not to kick the bucket any time soon, since I’m enjoying this.  Lots of work ahead this 2012, but its meeting my initial aspiration I quoted from NYT last October to “make progress in meaningful work every day.”

Categories:
Off Topic

Next month I’ll be attending and speaking at the Colorado Springs #SQLSat104.  I’m looking forward to this event for a number of reasons:

1.      It’s my very first SQLSaturday!  I’ve always wanted to attend – but neglected to do anything about it until now. 

2.      The day is packed with great session choices.  While I myself will be speaking at 2:30 PM, I want to catch as many sessions as I can both before and after.  This will force me to make some tough choices, but that’s a good problem to have.

3.      I’ll get a chance to present “Performance Issue Archetypes”.  This is basically an ode to common performance themes that I’ve seen over the last few years as a consultant and support engineer.   And while I always try to lean heavily on data-driven methodologies and not get overly distracted by initial patterns and symptoms (beware red herrings) - there are still various themes that I pay close attention to. My plan is to share these themes over the course of the hour.

4.      Without a doubt the part I’m looking forward to most is the opportunity to meet people in person.  While I’ve met a few of the planned attendees and speakers over email or Twitter, nothing beats actually spending time with people in person. 

If you plan on attending #SQLSat104, please stop by and say hello!

Categories:

The SQL Server 2012 contained database feature has an interesting behavior when it comes to collation considerations between the SQL Server instance default collation and a user database collation.  I see this new behavior as a benefit, but rather than tell you about it, I’ll step through a demonstration instead.

First of all, this demonstration is on SQL Server 11.0.1750 (SQL Server 2012 RC0).  I’ll start by executing the following in order to determine the default collation of the instance:

SELECT SERVERPROPERTY('Collation')

This returns SQL_Latin1_General_CP1_CI_AS.

Next I’ll create a database that does NOT allow containment, so you can see the pre-2012 behavior:

 

CREATE DATABASE [PCDBExample_No_CDB]

 CONTAINMENT = NONE

 COLLATE French_CS_AI

GO

 

Notice that in addition to designating CONTAINMENT = NONE, I used a collation that was different from the SQL Server instance default. 

And next, I’m going to create two tables – one regular table and one temporary in the newly created database, and then insert identical rows:

 

USE [PCDBExample_No_CDB]

GO

 

CREATE TABLE [DemoCollation]

 

               (DemoCollationNM varchar(100))

GO

 

CREATE TABLE #DemoCollation

               (DemoCollationNM varchar(100))

 

INSERT dbo.DemoCollation

(DemoCollationNM)

VALUES ('Test Join')

 

INSERT #DemoCollation

(DemoCollationNM)

VALUES ('Test Join')

 

Now I’ll execute a query that joins the two tables based on the column name:

 

SELECT p.DemoCollationNM

FROM dbo.DemoCollation p

INNER JOIN #DemoCollation d ON

               p.DemoCollationNM = d.DemoCollationNM

 

This returns the following error message:

 

Msg 468, Level 16, State 9, Line 4

Cannot resolve the collation conflict between "SQL_Latin1_General_CP1_CI_AS" and "French_CS_AI" in the equal to operation.

 

Next I’ll look at sp_help for each table (regular and temporary):

 

EXEC sp_help [DemoCollation]

 

USE tempdb

EXEC sp_help #DemoCollation

 

As our error suggested, the DemoCollation had a collation of French_CS_AI for the DemoCollationNM varchar data type column, but a SQL_Latin1_General_CP1_CI_AS collation for the DemoCollationNM column in the #DemoCollation table.

 

Now let’s see what happens for a contained database.  In order to demonstrate a partially contained database, I’ll execute sp_configure as follows:

EXEC sp_configure 'contained database authentication', 1

RECONFIGURE

 

The following code creates a partially contained database and sets up the same test (different database):

 

CREATE DATABASE [PCDBExample_CDB]

 CONTAINMENT = PARTIAL

 COLLATE French_CS_AI

GO

 

USE [PCDBExample_CDB]

GO

 

CREATE TABLE [DemoCollation]

               (DemoCollationNM varchar(100))

GO

 

CREATE TABLE #DemoCollation2

               (DemoCollationNM varchar(100))

 

INSERT dbo.DemoCollation

(DemoCollationNM)

VALUES ('Test Join')

 

INSERT #DemoCollation2

(DemoCollationNM)

VALUES ('Test Join')

 

Now I’ll test the join:

 

SELECT p.DemoCollationNM

FROM dbo.DemoCollation p

INNER JOIN #DemoCollation2 d ON

               p.DemoCollationNM = d.DemoCollationNM

 

This time I get results instead of a collation error:

 

clip_image001

 

If I execute sp_help for #DemoCollation2 in tempdb, I also see that the Collation is French_CS_AI.  So the containment setting changed the default collation to the user-database’s default instead of the SQL Server instance level default.

Categories:
SQL Server 2012

Theme design by Nukeation based on Jelle Druyts