quinta-feira, 27 de maio de 2010

Forward pointer do forward pointer?

Prefere o PDF? Baixe aqui.



(...)

Nova semana de internals, muitas dúvidas e discussões para lá de profundas, então vou dormir menos do que as parcas horas tradicionais para escrever um artigo rápido.

Básico: em uma tabela heap quando um registro é modificado e ele não cabe mais na página em que se encontra, o SQL Server move o registro para outra página e deixa na página original um ponteiro para a onde o registro foi levado. Esse registro é o forward pointer ou forward stub, e ajuda o SQL Server a evitar uma série de alterações mantendo o RID (fileId, PageId e SlotId) sem ser alterado.

Pergunta: Se eu tiver um forward pointer e o registro na nova página sofrer outra atualização que o faz crescer mais uma vez, novamente não cabendo na página atual, quando ele for colocado em outra página, teremos um forward pointer de um forward pointer?

Resposta de sala: a resposta by the book é não, seria extremamente ineficiente se o SQL Server mantivesse uma listagem encadeada de forward pointers para um único registro. Porém, nunca fiz esse teste (não que eu me lembre), então fica a resposta teórica.

Verificando se eu falei besteira dentro da sala de aula...

Para manter minha sanidade e conseguir publicar o post sem deixá-lo pela metade (como eu fiz com os últimos seis), vou tentar ser breve e usar comentário no próprio script, então qualquer dúvida grite!

/*
Forward pointer de Forward pointer?
*/
USE Inside
go

IF Exists (SELECT * FROM sys.tables WHERE [name] = 'RegistroGrande')
DROP TABLE RegistroGrande
go

CREATE TABLE RegistroGrande
( ColA int IDENTITY,
ColB varchar(1600),
ColC varchar(1600),
ColD varchar(1600));
GO

INSERT INTO RegistroGrande
VALUES (REPLICATE('a', 1600), '', '')
INSERT INTO RegistroGrande
VALUES (REPLICATE('b', 1600), '', '')
INSERT INTO RegistroGrande
VALUES (REPLICATE('c', 1600), '', '')
INSERT INTO RegistroGrande
VALUES (REPLICATE('d', 1600), '', '')
INSERT INTO RegistroGrande
VALUES (REPLICATE('e', 1600), '', '')
GO

INSERT INTO RegistroGrande
VALUES (REPLICATE('f', 1600), '', '')
INSERT INTO RegistroGrande
VALUES (REPLICATE('g', 1600), '', '')
GO

/*
Primeiro inserimos alguns registros e nesse momento temos duas páginas alocadas, uma praticamente cheia e outra parcialmente cheia.
*/

DBCC IND(Inside, RegistroGrande, -1)
/*
PageFID PagePID IAMFID IAMPID
1 173 NULL NULL
1 151 1 173
1 174 1 173
*/

-- Analisando as páginas de dados (não me importa a IAM), podemos ver os registros inseridos e
-- no cabeçalho a quantidade de espaço livre por página
DBCC TRACEON(3604)
DBCC PAGE(5, 1, 151, 3)
DBCC PAGE(5, 1, 174, 3)

-- Quando fazemos um update em um registro que está na página 151 (lotada) e ele não cabe mais na página atual,
-- temos um forward record.
UPDATE RegistroGrande
SET ColC = REPLICATE('X', 1200)
WHERE ColA = 3

-- Em qual página que esse registro vai parar?
DBCC PAGE(5, 1, 151, 3)

/*
Slot 2 Offset 0x1feb Length 9

Record Type = FORWARDING_STUB Record Attributes = Record Size = 9

Memory Dump @0x000000000E54BFEB

0000000000000000: 04ae0000 00010002 00†††††††††††††††††.®.......
Forwarding to = file 1 page 174 slot 2
*/

-- Muito bem SQL Server, viu que existe espaço na página 174 e mandou o registro grande para ela
-- E de quebra o modo de vizualização 3 do DBCC PAGE já mostra a página, nada fazer essa tarefa complicadíssima
-- de converter hexa para decimal.

-- Vamos ver se o registro apareceu por lá?
DBCC PAGE(5, 1, 174, 3)

-- Lindo, olhando o offset do slot 2 encontramos um registro cheio de CCCCCCCCs e XXXXXXs.
-- MAASSSS, como o m_freeCnt está com o valor 2031, quero inserir um registro para diminuir o espaço
-- livre nessa página.

INSERT INTO RegistroGrande
VALUES (REPLICATE('h', 1000), '', '')
GO

-- Beleza, o registro dos hhhhhhhhs está na página 174 e m_freeCnt em 1014.
DBCC PAGE(5, 1, 174, 3)

-- De quebra, olhando para o DBCC IND vemos que as mesmas páginas de dados originais 151 e 174
-- continuam alocadas, e mais nenhuma!
DBCC IND(Inside, RegistroGrande, -1)

-- O que acontecerá quando eu atualizar o registro novamente, de forma que ele não caiba mais na página atual?
-- Uma coisa é certa: O SQL Server vai precisar de uma nova página para esse registro!
-- Agora, OU ele atualiza o forward pointer para o novo RID OU ele mantém um ponteiro de ponteiro.

update RegistroGrande
set ColD = REPLICATE('Y', 1200)
where ColA = 3
go

-- Tem que aparecer uma nova página...
DBCC IND(Inside, RegistroGrande, -1)
/*
PageFID PagePID IAMFID IAMPID
1 173 NULL NULL
1 151 1 173
1 174 1 173
1 175 1 173
*/

-- Beleza, se essa página não aparecesse algo estaria muito errado... :-)

-- Primeiro, vamos verificar que o registro que está na nova página (a 175) é realmente o registro forwardado
-- (eu ia até substituir essa palavra, mas ela ficou tão ridícula que vou manter - FORWARDADO!)

-- Forwarded record??
DBCC PAGE(5, 1, 175, 3)

-- SIM! Um registro cheio de CCCCs, XXXXXs e YYYYYs em uma página só com o slot 0.

-- E agora a resposta da pergunta.... que toquem os tambores!!!!
DBCC PAGE(5, 1, 151, 3)
/*
Slot 2 Offset 0x199c Length 9

Record Type = FORWARDING_STUB Record Attributes = Record Size = 9

Memory Dump @0x000000000DF5B99C

0000000000000000: 04af0000 00010000 00†††††††††††††††††.¯.......
Forwarding to = file 1 page 175 slot 0
*/

/*
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAÊÊÊÊÊÊÊÊÊÊÊÊÊÊÊÊÊÊÊÊÊÊÊÊÊÊÊÊÊÊ!!!!!!
Uma salva de palmas para o SQL Server!

Ao invés de manter um forward pointer de um forward pointer, o que potencialmente poderia virar um "FP -> FP -> FP" ou
"FP -> FP -> FP -> FP -> FP -> FP -> FP ..." (entendeu, né?) o SQL Server atualizou o ponteiro na página de origem.

O que antes era um RID para 1:174:2 virou 1:175:0.
*/

-- Por curiosidade, o que ficou no antigo slot do registro?
DBCC PAGE(5, 1, 174, 3)

-- Com essa visualização, nada!
-- Mas com a visualização 2...
DBCC PAGE(5, 1, 174, 2)

-- Vemos que o conteúdo do antigo registro continua lá dentro da página, mas o espaço livre no cabeçalho é atualizado
-- e pelo slot array vemos que o slot 2 não está mais sendo utilizado.
/*
OFFSET TABLE:

Row - Offset
3 (0x3) - 6155 (0x180b)
2 (0x2) - 0 (0x0)
1 (0x1) - 1711 (0x6af)
0 (0x0) - 96 (0x60)
*/

/*
Por fim eu respondi certo a pergunta do aluno e agora está comprovado!
Curtiu, mano? (aproveitando que estou em SP)
*/

[]s
Luciano Caixeta Moreira - {Luti}
Chief Innovation Officer
Sr. Nimbus Serviços em Tecnologia Ltda
luciano.moreira@srnimbus.com.br
www.twitter.com/luticm

sexta-feira, 7 de maio de 2010

Internals em São Paulo - nova data

Depois de tanto tempo sem escrever eu até fico sem graça de voltar aqui sem um post técnico, mas não posso deixar de fazer esse anúncio. Curiosamente, esse é o post número 100 deste blog e fico orgulhoso de ser sobre uma grande paixão, internals do SQL Server.

Está confirmado o treinamento SQL03 - SQL Server 2008 Internals que ocorrerá em São Paulo, nos dias:

18/05 – De 14:00h a 18:00h
19/05 – De 8:30 a 18:00h (1:30h de parada para almoço)
20/05 – De 8:30 a 18:00h (1:30h de parada para almoço)
25/05 – De 14:00h a 18:00h
26/05 – De 8:30 a 18:00h (1:30h de parada para almoço)
27/05 – De 8:30 a 18:00h (1:30h de parada para almoço)

Dessa vez o treinamento será na Paulista, perto do Metrô, então deve facilitar a vida de muitos.
Ainda temos algumas vagas, se você é um profissional que ir além da média e procura por um treinamento profundo e diferenciado de SQL Server (nada de MOCs superficiais!), não pode deixar de participar.

[]s

Luciano Caixeta Moreira - {Luti}
Chief Innovation Officer
Sr. Nimbus Serviços em Tecnologia Ltda - www.srnimbus.com.br
luciano.moreira@srnimbus.com.br
www.twitter.com/luticm