Estou vendo algum comportamento inesperado com colunas de carimbo de data/hora (rowversion). Criei uma tabela de teste:
create table Test
(
Test_Key int identity(1,1) primary key clustered,
Test_Value int,
Test_Thread int,
ts timestamp
)
create nonclustered index IX_Test_Value on Test (Test_Value) -- probably irrelevant
Comecei dois threads executando inserções nesta tabela ao mesmo tempo. O primeiro thread está executando o seguinte código:
declare @i int = 0
while @i < 100
begin
insert into Test (Test_Value, Test_Thread) select n, 1 from dbo.fn_GenerateNumbers(10000)
set @i = @i + 1
end
O segundo encadeamento está executando um código idêntico, exceto pelo fato de que está fazendo select n, 2
a partir da função inserir seu ID de encadeamento.
Primeiro, uma palavra sobre a função. Isso usa uma série de expressões de tabela comuns cruzadas com um ROW_NUMBER() para retornar muitos números em sequência muito rapidamente. Aprendi esse truque com um artigo de Itzik Ben-Gan , então o crédito vai para ele. Não acho que a implementação da função seja importante, mas vou incluí-la de qualquer maneira:
CREATE FUNCTION dbo.fn_GenerateNumbers(@count int)
RETURNS TABLE WITH SCHEMABINDING
AS
RETURN
WITH
Nbrs_4( n ) AS ( SELECT 1 UNION SELECT 0 ),
Nbrs_3( n ) AS ( SELECT 1 FROM Nbrs_4 n1 CROSS JOIN Nbrs_4 n2 ),
Nbrs_2( n ) AS ( SELECT 1 FROM Nbrs_3 n1 CROSS JOIN Nbrs_3 n2 ),
Nbrs_1( n ) AS ( SELECT 1 FROM Nbrs_2 n1 CROSS JOIN Nbrs_2 n2 ),
Nbrs_0( n ) AS ( SELECT 1 FROM Nbrs_1 n1 CROSS JOIN Nbrs_1 n2 ),
Nbrs ( n ) AS ( SELECT 1 FROM Nbrs_0 n1 CROSS JOIN Nbrs_0 n2 )
SELECT n
FROM ( SELECT ROW_NUMBER() OVER (ORDER BY n) FROM Nbrs ) D ( n )
WHERE n <= @count ;
Esta tabela tem uma identity
coluna nela. Eu esperava que, quando selecionasse os valores da tabela por essa chave primária monotonicamente crescente, também veria os carimbos de data e hora na mesma ordem. Os timestamps podem não ser sequenciais, porque pode ter havido outras atualizações, mas pelo menos estariam em ordem.
No entanto, o que estou vendo é diferente. As inserções são intercaladas por chave primária, mas os timestamps são sequenciais por thread .
Test_Key Test_Value Test_Thread ts
-------- ---------- ----------- ------------------
20227 227 1 0x000000006EDF3BC5
20228 228 1 0x000000006EDF3BC6
20229 229 1 0x000000006EDF3BC7
20230 230 1 0x000000006EDF3BC8
20231 1 2 0x000000006EDF41E9 -- thread 2 starts with a new ts
20232 2 2 0x000000006EDF41EB
20233 3 2 0x000000006EDF41EC
20234 4 2 0x000000006EDF41ED
--<snip lots of thread 2 inserts>
21538 1308 2 0x000000006EDF4710
21539 1309 2 0x000000006EDF4711
21540 1310 2 0x000000006EDF4712
21541 1311 2 0x000000006EDF4713
21542 231 1 0x000000006EDF3BC9 -- This is less than the prior row!
21543 232 1 0x000000006EDF3BCA -- Thread 1 is inserting
21544 233 1 0x000000006EDF3BCB -- from its last ts value
21545 234 1 0x000000006EDF3BCC
Minha pergunta é:
1) Por que o timestamp nem sempre aumenta com inserções simultâneas?
Pontos de bônus se você puder responder a esta pergunta:
2) Por que as inserções simultâneas se sobrepõem à chave primária em vez de todas serem inseridas de uma vez? Cada inserção está executando sua própria transação implícita, portanto, esperava que as chaves primárias estivessem em ordem para a inserção de um único encadeamento. Eu não esperava que as chaves primárias fossem intercaladas.
Não sei o suficiente sobre replicação para responder a esta:
3) Ter timestamps fora de ordem causa um problema com a replicação? No exemplo acima, e se o thread 2 confirmar seus dados primeiro? Quando o thread 1 é concluído, seus timestamps são todos menores do que os registros inseridos pelo thread 2.
Examinei as solicitações em execução e verifiquei que elas não estão sendo executadas em paralelo, portanto, não acho que o paralelismo seja o problema.
Observe que esta consulta estava sendo executada no nível de isolamento padrão (READ COMMITTED). Se eu aumentar o nível de isolamento para SERIALIZABLE, ainda obterei carimbos de data/hora na ordem inversa quando os threads mudarem.
Estou testando isso no SQL Server 2008 R2.
Para verificar os pedidos de carimbo de data/hora, eu estava fazendo um select * from Test
, e também estava usando as seguintes consultas:
-- find timestamps out of sequential order
select t1.*, t2.*
from Test t1
inner join Test t2
on t2.Test_Key = t1.Test_Key + 1
where
t2.ts <> t1.ts + 1
-- find timestamps that are less than the prior timestamp
select t1.*, t2.*
from Test t1
inner join Test t2
on t2.Test_Key = t1.Test_Key + 1
where
t2.ts < t1.ts
O gerador de IDENTIDADE não está bem documentado. Existem alguns comportamentos, no entanto, que podem ser observados e que parecem relevantes:
A geração de identidade não é afetada por transações. Isso significa que, uma vez que um valor tenha sido usado, ele não será reutilizado, mesmo que a transação que está causando seu uso seja revertida.
Nem todo uso causa uma atualização da posição da sequência sendo gravada no banco de dados. Você pode ver isso, por exemplo, após uma falha. Freqüentemente, o próximo valor usado após uma falha é vários números mais altos que o anterior.
Embora não haja prova (o que significa documentação), pode-se supor que, por motivos de desempenho, uma inserção de várias linhas captura um bloco de valores de identidade e os usa até que se esgote. Outro thread simultâneo obterá o próximo bloco de números. Neste ponto, o valor de identidade não reflete mais a ordem das inserções.
O tipo de dados rowversion, por outro lado, é um número cada vez maior que refletiria a ordem de inserção. (timestamp é um sinônimo obsoleto para rowversion.)
Portanto, no seu caso, você pode assumir que as linhas foram inseridas na ordem da coluna rowversion e que o valor de identidade fora de ordem é causado por otimizações de desempenho de memória.
A propósito, embora o gerador de IDENTIDADE não esteja muito bem documentado, a nova
SEQUENCE
funcionalidade de 2012 está. Aqui você pode ler tudo sobre os comportamentos descritos acima em sequências.Quanto à sua preocupação com a replicação:
A replicação transacional está usando o log do banco de dados e não depende de valores de coluna específicos.
A replicação de mesclagem usa uma coluna rowguid para identificar uma linha. Esta é uma coluna que é valorizada uma vez e não muda ao longo da vida da linha. A replicação de mesclagem não usa uma coluna de versão de linha. A consistência transacional é reforçada pelo fato de que, no momento de uma sincronização, o bloqueio normal é usado, portanto, uma transação é completamente visível para o agente de mesclagem ou completamente invisível.
A replicação de instantâneo não procura alterações. Ele apenas pega os dados confirmados de sincronização no momento e os copia.