Mostrando postagens com marcador Indexes. Mostrar todas as postagens
Mostrando postagens com marcador Indexes. Mostrar todas as postagens

sexta-feira, 30 de novembro de 2012

[SQLServerDF] Material do encontro XV - ColumnStore Index

Ontem tivemos novamente um bom público para nossa sessão do SQLServerDF, mesmo véspera de feriado em Brasília. Dessa vez o Luan quem foi o palestrante e mandou muito bem, falando sobre columnstore index. E de quebra ainda distribuiu artigos e sorteou um arc mouse da Microsoft, top!
Ele me encaminhou o conteúdo da palestra que publico no skydrive (https://skydrive.live.com/redir?resid=E145F7753042D628!414).
E não deixe de dar uma olhada no seu blog: http://luanmorenodba.wordpress.com/.
Agora é aguardar a próxima palestra que acontecerá em Janeiro/2013, onde vamos ouvir um pouco sobre AlwaysOn, focando em Availability Groups.
Abraços
sr. Nimbus Serviços em Tecnologia - www.srnimbus.com.br

terça-feira, 29 de maio de 2012

Filtered index, query hints e QO error 8622


Para ler o PDF e baixar o script utilizado, acesse o skydrive do Luti.
Já não foi a primeira vez que eu esbarro no erro 8622 e ontem aconteceu novamente, então aproveito para escrever um pequeno post sobre o assunto.
Para quem nunca viu o erro seu detalhamento é: “Msg 8622, Level 16, State 1, Line 3 - Query processor could not produce a query plan because of the hints defined in this query. Resubmit the query without specifying any hints and without using SET FORCEPLAN.”
A mensagem é bem clara, estamos utilizando uma hint que impede o query optimizer produzir um plano de execução, ou seja, nossa hint é algo que viola as regras do QO para geração do plano, por ser uma contradição ou uma dica que se seguida pode gerar resultados incorretos.
Para gerar um cenário semelhante ao que encontrei o problema basta utilizar o script 01, guardadas as devidas proporções, pois no meu caso eram 100 milhões de registros.
Script 01 – Criando a tabela RegistroProblema
USE Internals
GO

IF OBJECT_ID('dbo.RegistroProblema', 'U') IS NOT NULL
      DROP TABLE dbo.RegistroProblema
GO

CREATE TABLE dbo.RegistroProblema (
      ID INT IDENTITY NOT NULL PRIMARY KEY
      , Nome VARCHAR(100) NOT NULL DEFAULT ('Sr. Nimbus')
      , DataRegistro DATETIME2 NOT NULL DEFAULT(SYSDATETIME())
      , Problema VARCHAR(100) NULL
, Resolvido BIT NULL DEFAULT (0) 
)
GO

INSERT INTO dbo.RegistroProblema DEFAULT VALUES
GO 100000

UPDATE dbo.RegistroProblema
      SET
            Problema = 'Problema' + CAST((ID % 13) AS VARCHAR)
            , Resolvido = IIF((ID % 73) < 72, 1, 0)
go

Continuando com o detalhamento do cenário, imagine que você está fazendo um tratamento pontual para o problema 01 e com o intuito de suportar a consulta decide criar um índice filtrado exatamente como a cláusula where que é utilizada.
Script 02 – Utilizando o índice com filtro
SELECT *
FROM dbo.RegistroProblema
WHERE Problema = 'Problema1'
      AND Resolvido = 0
ORDER BY DataRegistro ASC
go

CREATE NONCLUSTERED INDEX idxNCL_RegistroProblema_FiltroProblema1
ON dbo.RegistroProblema (DataRegistro)
WHERE Resolvido = 0
      AND Problema = 'Problema1'
GO

SELECT *
FROM dbo.RegistroProblema
WHERE Problema = 'Problema1'
      AND Resolvido = 0
ORDER BY DataRegistro ASC
go

No segundo plano de execução já podemos ver que o índice é utilizado, então ao invés de um cluster index scan temos um non-cluster index scan, conforme esperado.
Porém no procedimento em que eu estava utilizando essa consulta duas vezes, o código original utilizava a variável @NomeProblema para organizar o código T-SQL, só que essa abordagem faz com que o QO não consiga garantir o valor da variável e consequentemente não possa utilizar o índice filtrado, voltando ao índice cluster. Isso gera um plano bem mais caro (73% vs. 27%) mesmo para uma massa de dados menor, em produção a diferença era bem mais sensível.
Script 03 – Comparando planos de execução
DECLARE @NomeProblema VARCHAR(100) = 'Problema1'

SELECT *
FROM dbo.RegistroProblema
WHERE Problema = @NomeProblema
      AND Resolvido = 0
ORDER BY DataRegistro ASC
go

SELECT *
FROM dbo.RegistroProblema
WHERE Problema = 'Problema1'
      AND Resolvido = 0
ORDER BY DataRegistro ASC
go

Então para resolver o problema do plano de execução você poderia tentar forçar a utilização de um índice (script 04). Só que essa ação vai levar ao erro 8622, pois o SQL Server não pode utilizar o índice criado, pois o valor da variável não é conhecido e caso a pesquisa seja pelo “Problema2” o índice não retornaria nenhum registro, então o QO não pode utilizar essa hint para gerar o plano.
Script 04 – Recebendo o erro 8622
DECLARE @NomeProblema VARCHAR(100) = 'Problema1'

SELECT *
FROM dbo.RegistroProblema WITH(INDEX(idxNCL_RegistroProblema_FiltroProblema1))
WHERE Problema = @NomeProblema
      AND Resolvido = 0
ORDER BY DataRegistro ASC
go

Nesse caso não adianta trabalharmos com a hint OPTIMIZE FOR ou plan guides, o problema é o mesmo é persiste. Mas vamos então trabalhar com uma stored procedure e para evitar o SQL Server não saber o valor da variável em tempo de compilação, vou criar o parâmetro @NomeProblema e utilizar o mesmo diretamente na consulta, isto é, o SQL Server consegue fazer o sniff e sabe que o valor passado é o “Problema1”.
Script 05 – Encapsulando em uma SP
CREATE PROCEDURE proc_TesteSniffing @NomeProblema VARCHAR(100)
AS
      SELECT *
      FROM dbo.RegistroProblema
      WHERE Problema = @NomeProblema
            AND Resolvido = 0
      ORDER BY DataRegistro ASC
go

EXEC proc_TesteSniffing @NomeProblema = 'Problema1'

Ao executar o procedimento qual plano de execução você espera? Scan no cluster ou não-cluster? Dica: ao analisar o plano de execução conseguimos ver que o QO sabe do valor passado ParameterCompiledValue = "'Problema1'".
Se você respondeu scan no índice cluster, acertou! O SQL Server não pode colocar em cache um plano que não possa gerar resultados incorretos para outras chamadas, então ele não pode utilizar o índice com filtro. Se fizermos um outro teste (script 06) com o OPTION RECOMPILE, podemos ver que como a instrução é recompilada a toda chamada, o SQL Server pode efetivamente utilizar o índice filtrado quando o problema 1 é especificado.
Script 06 – OPTION RECOMPILE
ALTER PROCEDURE proc_TesteSniffing @NomeProblema VARCHAR(100)
AS
SELECT *
FROM dbo.RegistroProblema
WHERE Problema = @NomeProblema
      AND Resolvido = 0
ORDER BY DataRegistro ASC

SELECT *
FROM dbo.RegistroProblema
WHERE Problema = @NomeProblema
      AND Resolvido = 0
ORDER BY DataRegistro ASC
OPTION (RECOMPILE)
go

EXEC proc_TesteSniffing 'Problema1'
EXEC proc_TesteSniffing 'Problema2'

Neste pequeno artigo nós falamos um pouco sobre o erro 8622 que pode acontecer em diversos cenários e também resvalamos em outro detalhe muito interessante, que é a geração de planos de execução não ótimos dentro de procedures devido à reutilização do plano.
Abraços,
sr. Nimbus Serviços em Tecnologia - www.srnimbus.com.br

sexta-feira, 2 de março de 2012

Otimizando o Data Cache

Bom dia pessoal.

No dia 01/02/2012 eu publiquei uma pesquisa no meu blog para analisarmos como está o desperdício no data cache do seu SQL Server (http://luticm.blogspot.com/2012/01/pesquisa-desperdicio-no-data-cache.html).

Como vocês podem ver pelas repostas, é usual termos um desperdício de 8% a 15%, o que isso significa? Se você tem um data cache com 30 GB de tamanho, então algo entre 2,4GB e 4,5GB está sendo desperdiçado.

É claro que não tenho ilusão de que 100% do data cache estará ocupado, pelo contrário, temos fragmentação normal dos índices, níveis não folha e outras páginas de controle usualmente possuem espaço livre, então parte desse espaço em memória vai ser desperdiçado sim! Mas você como DBA deve cuidar para que o desperdício seja o menor possível. Como?

  • Modelando corretamente seu banco de dados
  • Usando outros recursos, como compressão de dados (cuidado sempre com os trade-offs).
  • Garantindo a manutenção dos seus índices
  • Tomando muito cuidado com thresholds globais    Fillfactor? PADIndex? Fragmentação para reorganize e Rebuild?    Na boa, quem já fez treinamento comigo sabe que sou bem ácido em relação a essas recomendações universalmente aceitas, usualmente elas podem te prejudicar muito.

Todos vocês podem ver o que os outros publicaram e tirar suas próprias conclusões, então dentro do que foi mostrado eu gostaria de propor uma nova verificação para todos os ambientes: tentar minimizar o desperdício do data cache!

Qual o threshold? (Opa, acabei de falar mal de thresholds globais! Shame on me :-))
Se o seu negócio permitir (normalmente sim), vamos com uma meta ambiciosa: manter o desperdício do data cache sempre abaixo de 10%.

E claro que nem sempre a coisa é simples e muitos detalhes podem tornar a vida do DBA mais complexa (e divertida, porque não?). Imagine o seguinte... Seu índice apresenta fragmentação interna de 3% mas o desperdício dela no data cache para este objeto é de 50%!

Sim, isso pode acontecer com você, é o que eu chamo de fragmentação de um ramo da árvore (B-tree+), principalmente quando suas tabelas começam a crescer demais e uma fragmentação em um ramo mais utilizado pode passar despercebida, quando analisado todo o índice.

Esse seria um post longo, mas acabei escolhendo por publicar o artigo no SimpleTalk (http://www.simple-talk.com/sql/database-administration/no-significant-fragmentation-look-closer%E2%80%A6/) para ver se tinha aceitação também lá fora.

Então você somente pode considerar que leu esse post depois de ler o artigo inteiro e deixar lá seu comentário e avaliação, seja boa ou ruim.


[]s
Luciano Caixeta Moreira - {Luti}
luciano.moreira@srnimbus.com.br
www.twitter.com/luticm
www.srnimbus.com.br

quinta-feira, 2 de junho de 2011

Primeiro treinamento online da Sr. Nimbus

Ladies and gentlemen!

************************************************************************************
!!UPDATE!!

Recebemos questionamentos sobre o formato do curso e algumas pessoas querem testar o Live Meeting, então hoje (08/06/2011), entre 21:00 e 21:30 eu estarei online no LM para tirar dúvidas com relação ao formato do curso, ementa, material, certificados, além claro, de já testar a conectividade do LM.

Link para sessão aberta: https://www.livemeeting.com/cc/mvp/join?id=9WM6CF&role=attend&pw=AlunoNimbus20110608

Perto do horário eu publicarei aqui e no twitter o endereço para vocês acessarem a sessão do Live Meeting.

IMPORTANTE: hoje de noite eu estarei com uma conexão de modem 3G que NÃO é a conexão do treinamento, mas atende o propósito pontual. Para o treinamento temos dedicado um link de 10MB da GVT, e por todos os testes que conduzimos podemos dizer que a conectividade ficou muito boa.
************************************************************************************

Colocamos no nosso site a inscrição para o primeiro treinamento online oferecido pela Sr. Nimbus. O treinamento será o novo SQL10 - Indexação no SQL Server 2008 e eu serei o instrutor dessa turma.

Nesse treinamento vamos focar em um dos assuntos mais fascinantes e importantes do SQL Server: índices! Ficaremos 16 horas discutindo sua estrutura, tipos, melhores práticas de utilização e otimização.

A ideia é nos aproximarmos ao máximo uma aula presencial, com um moderador repassando as perguntas para o instrutor, que responderá perguntas ao vivo, apresentaremos PPTs, demos e, em paralelo, o aluno terá acesso a todos os scripts que poderá ir executando confortávelmente em sua casa. Antes de cada aula também ficaremos por 30 minutos na sala de aula, tirando dúvidas dos alunos por chat ou respondendo em voz alta para todos.

Temos muita expectativa no excelente aproveitamento dos alunos e na satisfação de todos, pois é um formato mais democrático, que possibilita a participação de todos, independente de onde você esteja.

Gostou?! Veja todos os detalhes aqui: http://intranet.srnimbus.com.br/treinamento/paginas/inscricoes/SQL10-201101.aspx

Estou contando os dias para o treinamento.

[]s
Luciano Caixeta Moreira - {Luti}
luciano.moreira@srnimbus.com.br
www.twitter.com/luticm
www.srnimbus.com.br

terça-feira, 10 de maio de 2011

Treinamentos Sr. Nimbus e curso online

Bom dia pessoal.
Recentemente temos recebido vários questionamentos sobre calendário e pedidos para que nossos treinamentos sejam executados em outros estados. Vamos a um rápido posicionamento:
  • Não fechamos turma para os treinamentos de querying e programming por conta dos horários. Tínhamos demanda, mas as datas/horas não coincidiam para os alunos.
  • Por conta dos nossos recursos, mais especificamente eu (sim, sou o culpado), tivemos que alterar o calendário por conta de alguns projetos em que estamos trabalhando. O que nos forçou a postergar algumas turmas para não fazer uma entrega corrida.
    • Estamos trabalhando em um novo calendário para os próximos meses, em breve publicarei no site da empresa e farei o anúncio aqui também.
  • O treinamento de database mirroring com o Gustavo Aguiar vai ser ministrado provavelmente em Junho. Estamos negociando uma parceria e trabalhando forte no conteúdo e, devo confessar, está espetacular.
Com relação a oferecer treinamentos em outros locais, a dificuldade de deslocamento ainda é grande e não conseguimos cobrir um espaço muito grande, então estamos estruturando uma oferta para um treinamento online de SQL Server, focado em indexação.
Para nos ajudar, montei uma pequena enquete e gostaria MMMUUIITTTOOO que você nos ajudasse. Na enquete estão os detalhes para o primeiro treinamento.


Responda a pesquisa!


[]s
Luciano Caixeta Moreira - {Luti}
luciano.moreira@srnimbus.com.br
www.twitter.com/luticm
http://www.srnimbus.com.br/

quarta-feira, 15 de setembro de 2010

Non-SARG que nada! Enganei o SQL Server...

Bom dia pessoal.

Prefere ler em PDF?




Ontem eu citei em uma palestra do Teched 2010 que um index seek exibido no plano de execução pode esconder na verdade um range scan ou até um full scan, então vou pegar um gancho de uma pergunta que apareceu recentemente em um treinamento que estava ministrando.

Explicando sobre non-search arguments eu demonstrei a consulta 01, que busca as vendas de um determinado mês. Porém sendo um non-sarg, o SQL Server utiliza o índice OrderDate (NCL) mas não faz um seek, e sim um NonClustered Index Scan, varrendo 4 páginas (o índice é muito pequeno).


Para resolver o problema, vamos alterar a consulta removendo o non-sarg, conforme a consulta 02, fazendo com que o SQL Server faça um seek (lendo duas páginas - raiz e folha) ao invés de um scan.


-- Consulta 01
SELECT orderdate, OrderID FROM Orders WHERE MONTH(OrderDate) = 07 and YEAR(OrderDate) = 1996


-- Consulta 02
SELECT OrderDate, OrderID FROM Orders WHERE OrderDate between '19960701' and '19960731 23:59:59.997'

Nesse momento um DBA fez uma observação muito curiosa: “eu posso enganar o SQL Server!” E para isso basta executar a consulta 03.

-- Consulta 03
SELECT OrderDate, OrderId FROM Orders WHERE MONTH(OrderDate) = 07 and YEAR(OrderDate) = 1996 and OrderDate > 0


Prontinho, se você olhar o plano gerado pelo SQL Server (figura 01) verá que ele está fazendo um index seek, ao invés do index scan gerado originalmente pelo non-sarg. Resolvido? No no no meu caro, na verdade não estamos enganando o SQL Server, infelizmente estamos sendo enganados.



(Figura 01)

Se olharmos com calma, o seek predicate é somente o “orderdate > 1990-01-01”, isto é, um belo scan no nosso índice não-cluster e enquanto o SQL Server está fazendo esse “seek”, ele vai tentando aplicar o predicado com MONTH e YEAR, que é o nosso non-sarg.


Putz, como eu sei que o SQL Server está fazendo um index scan? Dê uma olhada no STATISTICS IO e você verá o seguinte: “Table 'Orders'. Scan count 1, logical reads 4”. Hhhuummm, scan count = 1 (o SQL Server está fazendo um scan!) e logical reads = 4 é o mesmo que vimos durante a execução da consulta 01. Houve então alguma diferença efetiva no plano de execução? NÃO! Se você executar lado a lado a consulta 01 e a 03, verá um custo relativo de 50% para cada.


O que me incentivou a finalmente escrever esse post? Hoje cedo em vi que o time de CSS postou um artigo bem legal, chamado “SCAN COUNT meaning in SET STATISTICS IO output”, que explica um pouco sobre o scan count e serve de base para esse post, em que o “falso index seek” que é um full scan ou um range scan.

Espero que seja útil, e não deixe esses detalhes do SQL Server te enganar, ok? :-)
Abraços e até um próximo artigo.


[]s
Luciano Caixeta Moreira - {Luti}

terça-feira, 23 de fevereiro de 2010

Artigo: Como o Query Optimizer utiliza (ou não) constraints unique e filtered indexes

Bom dia pessoal.

Aproveitando o gancho do último post, escrevi hoje um artigo com duas intenções:


  1. Mostrar como o query optimizer do SQL Server se beneficia de constraints unique
  2. Mostrar uma característica do optimizer com filtered indexes que me parece um bug
Como o artigo ficou grande e acho que a leitura será mais agradável com a formatação do PDF, não vou colocar ele inteiro como um post, deixo somente o link para download do PDF.

Como o Query Optimizer utiliza (ou não) constraints unique e filtered indexes




Espero que vocês gostem e deixem aqui seus comentários...

[]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, 19 de fevereiro de 2010

Colunas UNIQUE e NULLs

Se quiser baixar o PDF e o script que utilizei, clique aqui.

Durante o último treinamento do SQL Server 2008 Internals, tive mais uma vez o prazer de usufruir de uma das grandes vantagens de ser instrutor, que é aprender com os alunos, então compartilho com vocês.
Estávamos discutindo sobre a utilização do NULL e eu joguei na sala a pergunta: Como fazemos para manter a unicidade de uma coluna e ainda permitirmos diversos valores nulos?

(Pausa para respirar e pensar um pouquinho)

No SQL Server a unicidade dos valores em uma coluna é garantida através de índices marcados como UNIQUE (cluster ou não) e uma vez inserido um NULL, nenhum outro NULL pode ser adicionado a tabela, pois é um valor duplicado.
É interessante ver esse comportamento de igualdade de nulos em uma constraint, pois se testarmos a igualdade de um nulo através de uma consulta, veremos que NULL é diferente de NULL (ele é desconhecido).

SELECT 'Comparando'
WHERE 1 = 1

SELECT 'Comparando'
WHERE NULL = NULL
go

Então como você resolve esse problema?

- Uma abordagem seria trabalhar com triggers na tabela, garantindo a unicidade dos valores não nulos.
- Particularmente não gosto dessa abordagem, por prolongar a transação e, se necessário, efetuar um rollback da mesma.

- Poderíamos garantir a unicidade através da aplicação ou SPs, mas aí temos que garantir que ninguém vai conseguir inserir um registro "por fora".
- Essa é uma abordagem interessante por evita o rollback, mas a falta de controle e de informações para o query optimizer (como no primeiro caso) não é legal.

- Outra abordagem que mostro no treinamento, seria criarmos uma coluna computada que em combinação com a coluna original (que precisa garantir unicidade para não-nulos) deve ser única. Essa coluna computada condicionalmente recebe um valor único (o campo da PK, por exemplo) caso o campo original seja nulo ou recebe NULL caso ele não seja nulo. Dessa forma poderíamos criar uma constraint UNIQUE nas colunas original e calculada, garantindo assim a unicidade não-nula.
- Gosto dessa abordagem porque trabalhamos com constraints e não preciso confiar em terceiros para que a regra seja respeitada.


Entendeu a explicação da terceira solução? Bem, eu já li vinte vezes o que escrevi e não entendi nada, então segue um exemplo para exemplificar melhor o que eu disse. :-)

USE tempdb
go

-- Criando a tabela de teste
IF (OBJECT_ID('Funcionario') IS NOT NULL)
DROP TABLE Funcionario
go

CREATE TABLE Funcionario
(
Codigo INT IDENTITY NOT NULL,
Nome VARCHAR(200) NOT NULL,
CNPJ CHAR(14) NULL)
go

ALTER TABLE Funcionario
ADD CONSTRAINT UNQ_Funcionario_CNPJ
UNIQUE (CNPJ)
go

ALTER TABLE Funcionario
ADD CONSTRAINT PK_Funcionario
PRIMARY KEY (Codigo)
go

-- Inserts OK
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Ronaldo Fenômeno', NULL)
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Nilmar', '000.000.000-00')
go

-- Ambos os INSERTs abaixo irão trazer problema por conta da constraint UNIQUE
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Ronaldo Fenômeno 2', NULL)
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Nilmar 2', '000.000.000-00')
go

-- Reconstruindo a tabela com a coluna computada
IF (OBJECT_ID('Funcionario') IS NOT NULL)
DROP TABLE Funcionario
go

CREATE TABLE Funcionario
(
Codigo INT IDENTITY NOT NULL,
Nome VARCHAR(200) NOT NULL,
CNPJ CHAR(14) NULL,
CNPJNulo AS (CASE WHEN CNPJ IS NULL THEN Codigo ELSE -1 END)
)
go

ALTER TABLE Funcionario
ADD CONSTRAINT PK_Funcionario
PRIMARY KEY (Codigo)
go

ALTER TABLE Funcionario
ADD CONSTRAINT UNQ_Funcionario_CNPJ
UNIQUE (CNPJ, CNPJNulo)
go

-- Inserts OK
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Ronaldo Fenômeno', NULL)
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Nilmar', '000.000.000-00')
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Caio do Botafogo', '000.000.000-01')
go

-- Vai funcionar
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Ronaldo Fenômeno 2', NULL)
go

-- Não vai funcionar
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Nilmar 2', '000.000.000-00')
go


Melhorou?
Dessa forma conseguimos garantir a unicidade antes que o valor seja inserido na tabela, sem a necessidade de criação de triggers.


SQL Server 2008

Agora que vem a sacada, enquanto estava falando sobre isso o amigo Burgos me perguntou: eu não conseguiria resolver esse problema utilizando índices com filtro?

(Momento de silêncio na sala)

Caramba! Se a unicidade é garantida através de índices e eu posso criar um índice com o predicado "IS NOT NULL", então provavelmente filtered index deve resolver o problema! Testamos e bang! Na mosca.

Eu tinha ficado tão focado nos ganhos de desempenho e tamanho dos índices com filtro que nunca tinha parado para pensar nessa utilização! Nada melhor do que dar aula e aprender também = Doscendo discimus.

Vamos ao exemplo…

-- Somente para SQL Server 2008
-- Resolução com filtered indexes
IF (OBJECT_ID('Funcionario') IS NOT NULL)
DROP TABLE Funcionario
go

CREATE TABLE Funcionario
(
Codigo INT IDENTITY NOT NULL,
Nome VARCHAR(200) NOT NULL,
CNPJ CHAR(14) NULL)
go

ALTER TABLE Funcionario
ADD CONSTRAINT PK_Funcionario
PRIMARY KEY (Codigo)
go

CREATE UNIQUE NONCLUSTERED INDEX idx_CNPF
ON Funcionario (CNPJ)
WHERE CNPJ IS NOT NULL
go

-- Inserts OK
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Ronaldo Fenômeno', NULL)
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Nilmar', '000.000.000-00')
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Caio do Botafogo', '000.000.000-01')
go

-- Vai funcionar
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Ronaldo Fenômeno 2', NULL)
go

-- Não vai funcionar
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Nilmar 2', '000.000.000-00')
go


Viu que realmente funciona?!
Espero que a solução pré-SQL Server 2008 e a nova abordagem possam ajudar você no dia-a-dia.

Só fiquei agoniado com uma coisa nessa abordagem, relacionado com o Query Optimizer, mas vou fazer alguns testes e depois coloco aqui minhas considerações.

[]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