Estou procurando substituir minha solução baseada em cursor, se possível, em um determinado procedimento armazenado. Se fizer alguma diferença, isso está sendo executado no SQL Server 2008 R2. Estou procurando mais um algoritmo do que um código preciso.
Fundo:
O SP faz parte de um sistema para uma empresa que envia correspondências por mala direta ou e-mail. As correspondências contêm um código personalizado que o destinatário pode inserir ao visitar o comerciante para obter descontos ou ofertas especiais. O uso do código é rastreado e relatórios agregados sobre a resposta a várias correspondências são fornecidos aos comerciantes. Um "cliente" é definido como o alvo de uma das correspondências com um nome, sobrenome e endereço exclusivos; se não houver endereço, o e-mail substituirá o endereço.
As tabelas em causa são as seguintes (versões simplificadas):
CREATE TABLE job (
id INT PRIMARY KEY IDENTITY (1,1),
job_num VARCHAR(32) NOT NULL,
mailing_id INT NOT NULL,
personal_code NVARCHAR(50) NOT NULL,
fname NVARCHAR(50) NOT NULL,
lname NVARCHAR(50) NOT NULL,
email NVARCHAR(50),
address NVARCHAR(50),
city NVARCHAR(50),
state CHAR(2),
zip NVARCHAR(10),
extra NVARCHAR(150)
);
CREATE TABLE customer (
id INT PRIMARY KEY IDENTITY (1,1),
fname NVARCHAR(50) NOT NULL,
lname NVARCHAR(50) NOT NULL,
email NVARCHAR(50),
address NVARCHAR(50),
city NVARCHAR(50),
state CHAR(2),
zip NVARCHAR(10)
);
CREATE TABLE personal_code (
id INT PRIMARY KEY IDENTITY (1,1),
customer_id INT NOT NULL,
mailing_id INT NOT NULL,
personal_code NVARCHAR(50) NOT NULL,
email NVARCHAR(50),
FOREIGN KEY (customer_id) REFERENCES customer(id)
);
CREATE TABLE personal_code_extra (
personal_code_id INT PRIMARY KEY,
extra NVARCHAR(150),
FOREIGN KEY (personal_code_id) REFERENCES personal_code(id)
);
A tabela de tarefas é preenchida por um processo externo que está envolvido na criação das listas de endereços para as quais a correspondência será enviada. Em seguida, chama o SP que desejo otimizar (espero), passando o job_num. O SP então lê todos os registros da tabela de trabalho com esse job_num e insere os dados armazenados nas outras três tabelas.
A tabela de clientes é, como esperado, onde os dados dos vários destinatários de correspondência são armazenados.
A tabela personal_code é um armazenamento para dados nos códigos pessoais associados às correspondências. Cada código está vinculado a uma correspondência específica, bem como a um cliente específico. Atualmente, o SP abre um cursor para a tabela de tarefas. Em seguida, itera por cada linha e, para cada uma, faz o seguinte:
- Se address não for nulo, ele definirá @customer_id igual a customer.id onde fname, lname e address correspondem; caso contrário, ele o define igual a customer.id onde fname, lname e email correspondem e o endereço é nulo ou vazio.
- Neste ponto, se @customer_id for nulo, não houve registro correspondente e um novo registro será adicionado ao cliente. @customer_id é definido como scope_identity().
- Um registro é inserido em personal_code.
- Se job.extra não for nulo, um registro é inserido em personal_code_extra com (é claro) o id gerado pela inserção na Etapa 3.
customer e personal_code são as maiores tabelas do banco de dados, com 40 milhões e 64 milhões de registros, respectivamente. Mesmo que as consultas no cliente tenham índices de cobertura, fazer uma pesquisa separada para cada linha no trabalho deve atrasar as coisas. Eu gostaria muito de abandonar o cursor e substituir essa abordagem RBAR por uma abordagem baseada em conjunto. O que está me impedindo de fazer isso é ter que usar um customer_id recém-criado para a inserção de personal_code, bem como um personal_code_id recém-criado para a tabela "extras". Se não fosse por isso, eu poderia fazer algo como
INSERT INTO personal_code (fields)
SELECT (fields)
FROM job j
INNER JOIN customer c ON j.fname = c.fname AND j.lname = c.lname AND j.address = c.address
WHERE j.address IS NOT NULL;
INSERT INTO personal_code (fields)
SELECT (fields)
FROM job j
INNER JOIN customer c ON j.fname = c.fname AND j.lname = c.lname AND j.email = c.email
WHERE j.address IS NULL;
INSERT INTO personal_code (fields)
SELECT (fields) -- but won't have a value for customer_id !
FROM job j
LEFT JOIN customer c ON either_address_or_email
WHERE c.id IS NULL;
E como lidar com a inserção do material "extra" em uma abordagem baseada em conjunto, não tenho ideia no momento. Agradeço antecipadamente quaisquer ideias.
EDIT: Código do cursor adicionado por solicitação. Isso é simplificado, mas tem todos os elementos essenciais - espero que minhas edições sejam precisas.
DECLARE job_cur CURSOR FOR
SELECT mailing_id, personal_code, email, fname, lname, address, city, state, zip, extra
FROM job
WHERE job_num = @job_no;
OPEN job_cur;
FETCH NEXT FROM job_cur INTO @mailing_id, @personal_code, @email, @fname, @lname, @address, @city, @state, @zip, @extra;
WHILE @@FETCH_STATUS = 0
BEGIN
IF ISNULL(@address, '') != ''
SET @customer_id = (
SELECT id
FROM customer
WHERE fname = @fname
AND lname = @lname
AND address = @address
);
ELSE
SET @customer_id = (
SELECT id
FROM customer
WHERE fname = @fname
AND lname = @lname
AND email = @email
AND (address IS NULL OR address = '')
);
IF @customer_id IS NULL
BEGIN
INSERT INTO customer (
fname, lname, address, city, state, zip, email
)
VALUES (
@fname, @lname, @address, @city, @state, @zip, @email
);
SET @customer_id = SCOPE_IDENTITY();
END
INSERT INTO personal_code (
customer_id, mailing_id, personal_code, email
)
VALUES (
@customer_id, @mailing_id, @personal_code, @email
);
SET @personal_code_id = SCOPE_IDENTITY();
IF @extra IS NOT NULL
INSERT INTO personal_code_extra (
personal_code_id, extra
)
VALUES (
@personal_code_id, @extra
);
FETCH NEXT FROM job_cur INTO @mailing_id, @personal_code, @email, @fname, @lname, @address, @city, @state, @zip, @extra;
END
Eu provavelmente faria isso da maneira de força bruta e adicionaria índices para dar suporte a essas junções onde elas não existem. Não há muito ganho em tratar novos clientes e clientes antigos de maneira diferente, uma vez que você inseriu todos os clientes que ainda não existem:
Acho que você precisa da cláusula OUTPUT para retornar quaisquer novos recordids. Além disso, dependendo do que você deseja fazer, você pode estar interessado em usar uma instrução MERGE em vez de apenas uma inserção. Por exemplo, alguém cujo e-mail permaneceu o mesmo, mas cujo endereço mudou, pode ser atualizado em vez de ter um novo registro adicionado, como parece ser o caso no que você está fazendo.