SQL Server inserisci se non esiste la migliore procedura


152

Ho una Competitionstabella dei risultati che contiene i nomi dei membri del team e la loro classifica da un lato.

D'altra parte, devo mantenere una tabella con nomi di concorrenti unici :

CREATE TABLE Competitors (cName nvarchar(64) primary key)

Ora ho circa 200.000 risultati nel 1 ° tavolo e quando il tavolo dei concorrenti è vuoto posso eseguire questo:

INSERT INTO Competitors SELECT DISTINCT Name FROM CompResults

E la query richiede solo circa 5 secondi per inserire circa 11.000 nomi.

Finora questa non è un'applicazione critica, quindi posso considerare di troncare la tabella dei concorrenti una volta al mese, quando ricevo i nuovi risultati della competizione con circa 10.000 righe.

Ma qual è la migliore pratica quando vengono aggiunti nuovi risultati, con concorrenti nuovi e esistenti? Non voglio troncare la tabella dei concorrenti esistenti

Devo eseguire la dichiarazione INSERT solo per i nuovi concorrenti e non fare nulla se esistono.


70
Per favore, non fare di una NVARCHAR(64)colonna la tua chiave primaria (e quindi: il clustering) !! Prima di tutto - è una chiave molto ampia - fino a 128 byte; e in secondo luogo ha dimensioni variabili - di nuovo: non ottimale ... Questa è la scelta peggiore che puoi avere - le tue prestazioni saranno infernali e la frammentazione di tabelle e indici sarà sempre al 99,9% .....
marc_s

4
Marc ha un buon punto. Non usare il nome come pk. Usa un ID, preferibilmente int o qualcosa di leggero.
Richard

6
Vedi il post sul blog di Kimberly Tripp su ciò che rende una buona chiave di clustering: unica, stretta, statica, in costante aumento. I tuoi cNamefallimenti in tre delle quattro categorie .... (non è stretto, probabilmente non è statico e sicuramente non è in
costante

Non riesco a vedere il punto nell'aggiunta di una chiave primaria INT alla tabella Nome di un concorrente in cui TUTTE le query saranno sul nome, come "DOVE nome come"% xxxxx% "", quindi ho sempre bisogno di un indice univoco sul nome. Ma sì, posso vedere il punto di NON renderlo di lunghezza variabile ..
Didier Levy

3
a) evitare la frammentazione eb) se è la chiave esterna in altre tabelle i dati duplicati sono più grandi di quelli necessari (il che è una considerazione rapida)
JamesRyan

Risposte:


214

Semanticamente stai chiedendo "inserisci concorrenti dove non esiste già":

INSERT Competitors (cName)
SELECT DISTINCT Name
FROM CompResults cr
WHERE
   NOT EXISTS (SELECT * FROM Competitors c
              WHERE cr.Name = c.cName)

2
Bene, questo è quello che avrei fatto prima di porre la domanda su SO. Ma il nocciolo del mio pensiero è: quanto funzionerà bene contro la ricostruzione della tabella dei nomi da zero una volta alla settimana? (ricorda che bastano pochi secondi)
Didier Levy

3
@Didier Levy: efficienza? Perché troncare, ricreare quando è possibile aggiornare solo con le differenze. Cioè: INIZIA TRAN ELIMINA CompResults INSERISCI CompResults .. COMMIT TRAN = più lavoro.
gbn

@gbn - C'è un modo per usare la logica if-else in modo sicuro qui invece della tua risposta? Ho una domanda in merito. Potete per favore aiutarmi con quello? stackoverflow.com/questions/21889843/…
Steam,

53

Un'altra opzione è quella di unire la tabella dei risultati con la tabella dei concorrenti esistenti e trovare i nuovi concorrenti filtrando i record distinti che non corrispondono al join:

INSERT Competitors (cName)
SELECT  DISTINCT cr.Name
FROM    CompResults cr left join
        Competitors c on cr.Name = c.cName
where   c.cName is null

La nuova sintassi MERGE offre anche un modo compatto, elegante ed efficiente per farlo:

MERGE INTO Competitors AS Target
USING (SELECT DISTINCT Name FROM CompResults) AS Source ON Target.Name = Source.Name
WHEN NOT MATCHED THEN
    INSERT (Name) VALUES (Source.Name);

1
Unire è fantastico in questo caso, fa esattamente quello che dice.
VorobeY1326,

Credo sicuramente che questo sia il modo giusto di procedere, fornendo a SQL Server i migliori suggerimenti possibili per l'ottimizzazione, in contrasto con l'approccio delle sub query.
Mads Nielsen,

4
L'affermazione MERGE ha ancora molti problemi. Google "SQL Merge Problems": molti blogger ne hanno discusso a lungo.
David Wilson

perché c'è come Target nell'istruzione MERGE, ma non c'è Target nell'istruzione INSERT? Ci sono più differenze che rendono difficile comprendere l'equivalenza.
Peter

32

Non so perché qualcun altro non l'abbia ancora detto;

NORMALIZZARE.

Hai un tavolo che modella le competizioni? Le competizioni sono composte da concorrenti? È necessario un elenco distinto di concorrenti in una o più competizioni ......

Dovresti avere le seguenti tabelle .....

CREATE TABLE Competitor (
    [CompetitorID] INT IDENTITY(1,1) PRIMARY KEY
    , [CompetitorName] NVARCHAR(255)
    )

CREATE TABLE Competition (
    [CompetitionID] INT IDENTITY(1,1) PRIMARY KEY
    , [CompetitionName] NVARCHAR(255)
    )

CREATE TABLE CompetitionCompetitors (
    [CompetitionID] INT
    , [CompetitorID] INT
    , [Score] INT

    , PRIMARY KEY (
        [CompetitionID]
        , [CompetitorID]
        )
    )

Con vincoli su CompetitionCompetitors.CompetitionID e CompetitorID che puntano alle altre tabelle.

Con questo tipo di struttura del tavolo - le tue chiavi sono tutte semplici INTS - non sembra esserci una buona CHIAVE NATURALE adatta al modello, quindi penso che una CHIAVE SURROGATE sia adatta qui.

Quindi, se hai avuto questo, quindi per ottenere l'elenco distinto dei concorrenti in una determinata competizione, puoi inviare una query come questa:

DECLARE @CompetitionName VARCHAR(50) SET @CompetitionName = 'London Marathon'

    SELECT
        p.[CompetitorName] AS [CompetitorName]
    FROM
        Competitor AS p
    WHERE
        EXISTS (
            SELECT 1
            FROM
                CompetitionCompetitor AS cc
                JOIN Competition AS c ON c.[ID] = cc.[CompetitionID]
            WHERE
                cc.[CompetitorID] = p.[CompetitorID]
                AND cc.[CompetitionName] = @CompetitionNAme
        )

E se volevi il punteggio per ogni competizione un concorrente è in:

SELECT
    p.[CompetitorName]
    , c.[CompetitionName]
    , cc.[Score]
FROM
    Competitor AS p
    JOIN CompetitionCompetitor AS cc ON cc.[CompetitorID] = p.[CompetitorID]
    JOIN Competition AS c ON c.[ID] = cc.[CompetitionID]

E quando hai una nuova competizione con nuovi concorrenti, allora controlli semplicemente quelli già esistenti nella tabella dei concorrenti. Se esistono già, non inserirli nel concorrente per quei concorrenti e inserirli per quelli nuovi.

Quindi si inserisce il nuovo Concorso in Concorso e infine si creano tutti i collegamenti in Concorrenti Competizione.


2
Supponendo che in questo momento l'OP abbia il vantaggio di ristrutturare tutte le sue tabelle per ottenere un risultato memorizzato nella cache. Riscrivere il tuo db e la tua app, invece di risolvere il problema in un ambito definito, ogni volta che qualcosa non si adatta facilmente, è una ricetta per il disastro.
Jeffrey Vest

1
Forse nel caso del PO come il mio, non sempre si ha accesso per modificare il database .. E riscrivere / normalizzare un vecchio database non è sempre nel budget o nel tempo assegnato.
eaglei22,

10

Dovrai unirti ai tavoli insieme e ottenere un elenco di concorrenti unici in cui non esiste già Competitors.

Ciò inserirà record univoci.

INSERT Competitors (cName) 
SELECT DISTINCT Name
FROM CompResults cr LEFT JOIN Competitors c ON cr.Name = c.cName
WHERE c.Name IS NULL

Potrebbe venire un momento in cui questo inserto deve essere eseguito rapidamente senza poter attendere la selezione di nomi univoci. In tal caso, è possibile inserire i nomi univoci in una tabella temporanea, quindi utilizzare quella tabella temporanea per inserirla nella tabella reale. Funziona bene perché tutte le elaborazioni avvengono al momento dell'inserimento in una tabella temporanea, quindi non influisce sulla tabella reale. Quindi, una volta terminata l'elaborazione, fai un rapido inserimento nella tabella reale. Potrei anche avvolgere l'ultima parte, in cui si inserisce nella tabella reale, all'interno di una transazione.


4

Le risposte sopra le quali parlano di normalizzazione sono fantastiche! Ma cosa succede se ti trovi in ​​una posizione come me in cui non ti è permesso toccare lo schema o la struttura del database così com'è? Ad esempio, i DBA sono "dei" e tutte le revisioni suggerite vanno a / dev / null?

A tale proposito, mi sembra che sia stato risposto anche con questo post Stack Overflow per quanto riguarda tutti gli utenti sopra che forniscono esempi di codice.

Sto ripubblicando il codice da INSERT VALUES WHERE NOT EXISTS che mi ha aiutato di più in quanto non posso modificare alcuna tabella di database sottostante:

INSERT INTO #table1 (Id, guidd, TimeAdded, ExtraData)
SELECT Id, guidd, TimeAdded, ExtraData
FROM #table2
WHERE NOT EXISTS (Select Id, guidd From #table1 WHERE #table1.id = #table2.id)
-----------------------------------
MERGE #table1 as [Target]
USING  (select Id, guidd, TimeAdded, ExtraData from #table2) as [Source]
(id, guidd, TimeAdded, ExtraData)
    on [Target].id =[Source].id
WHEN NOT MATCHED THEN
    INSERT (id, guidd, TimeAdded, ExtraData)
    VALUES ([Source].id, [Source].guidd, [Source].TimeAdded, [Source].ExtraData);
------------------------------
INSERT INTO #table1 (id, guidd, TimeAdded, ExtraData)
SELECT id, guidd, TimeAdded, ExtraData from #table2
EXCEPT
SELECT id, guidd, TimeAdded, ExtraData from #table1
------------------------------
INSERT INTO #table1 (id, guidd, TimeAdded, ExtraData)
SELECT #table2.id, #table2.guidd, #table2.TimeAdded, #table2.ExtraData
FROM #table2
LEFT JOIN #table1 on #table1.id = #table2.id
WHERE #table1.id is null

Il codice sopra usa campi diversi da quelli che hai, ma ottieni l'essenza generale con le varie tecniche.

Si noti che secondo la risposta originale su Stack Overflow, questo codice è stato copiato da qui .

Comunque il mio punto è che "la migliore pratica" spesso dipende da ciò che si può e non si può fare e dalla teoria.

  • Se sei in grado di normalizzare e generare indici / chiavi - fantastico!
  • Altrimenti e hai la possibilità di codificare hack come me, spero che quanto sopra aiuti.

In bocca al lupo!


Nel caso in cui non sia chiaro, si tratta di quattro approcci diversi al problema, quindi scegline uno.
nasch,

3

La normalizzazione delle tabelle operative come suggerito da Transact Charlie, è una buona idea e salverà molti mal di testa e problemi nel tempo - ma ci sono cose come tabelle di interfaccia , che supportano l'integrazione con sistemi esterni e tabelle di report , che supportano cose come analitiche in lavorazione; e questi tipi di tabelle non dovrebbero necessariamente essere normalizzati - in effetti, molto spesso è molto, molto più conveniente e performante per loro non essere .

In questo caso, penso che la proposta di Transact Charlie per i tuoi tavoli operativi sia valida.

Ma aggiungerei un indice (non necessariamente univoco) a CompetitorName nella tabella Competitors per supportare join efficienti su CompetitorName ai fini dell'integrazione (caricamento di dati da fonti esterne) e aggiungerei una tabella di interfaccia nel mix: CompetitionResults.

I risultati della competizione devono contenere tutti i dati contenuti nei risultati della competizione. Lo scopo di una tabella di interfaccia come questa è rendere il più semplice e veloce possibile troncare e ricaricare da un foglio Excel o un file CSV, o qualunque sia il modulo in cui si trovano quei dati.

Tale tabella di interfaccia non dovrebbe essere considerata parte dell'insieme normalizzato di tabelle operative.Quindi puoi unirti a CompetitionResults come suggerito da Richard, per inserire record in concorrenti che non esistono già e aggiornare quelli che lo fanno (ad esempio se in realtà hai più informazioni sui concorrenti, come il loro numero di telefono o indirizzo e-mail).

Una cosa che vorrei notare - in realtà, il nome del concorrente, mi sembra, è molto improbabile che sia unico nei tuoi dati . In 200.000 concorrenti, potresti benissimo avere 2 o più David Smith, per esempio. Quindi ti consiglio di raccogliere maggiori informazioni dai concorrenti, come il loro numero di telefono o un indirizzo e-mail o qualcosa che è più probabile che sia univoco.

La tua tabella operativa, concorrenti, dovrebbe avere solo una colonna per ogni elemento di dati che contribuisce a una chiave naturale composita; ad esempio dovrebbe avere una colonna per un indirizzo email principale. Ma la tabella di interfaccia dovrebbe avere uno slot per vecchi e nuovi valori per un indirizzo di posta elettronica primario, in modo che il vecchio valore possa essere utilizzato per cercare il record in Concorrenti e aggiornare quella parte di esso con il nuovo valore.

Quindi CompetitionResults dovrebbe avere alcuni campi "vecchi" e "nuovi" - oldEmail, newEmail, oldPhone, newPhone, ecc. In questo modo puoi formare una chiave composita, in Concorrenti, da Nome concorrente, Email e Telefono.

Quindi, quando si ottengono alcuni risultati della competizione, è possibile troncare e ricaricare la tabella dei risultati della concorrenza dal foglio Excel o qualsiasi altra cosa, ed eseguire un singolo inserto efficiente per inserire tutti i nuovi concorrenti nella tabella dei concorrenti e un singolo aggiornamento efficiente da aggiornare tutte le informazioni sui concorrenti esistenti dai risultati della competizione. E puoi fare un singolo inserto per inserire nuove righe nella tabella CompetitionCompetitors. Queste cose possono essere fatte in una procedura memorizzata ProcessCompetitionResults, che può essere eseguita dopo aver caricato la tabella CompetitionResults.

Questa è una sorta di rudimentale descrizione di ciò che ho visto fare più volte nel mondo reale con Oracle Applications, SAP, PeopleSoft e un elenco di altre suite di software aziendali.

Un ultimo commento che farei è quello che ho già fatto in precedenza su SO: Se crei una chiave esterna che assicuri che un concorrente esista nella tabella dei concorrenti prima di poter aggiungere una riga con quel concorrente in esso ai concorrenti, assicurati che la chiave esterna è impostata per gli aggiornamenti e le eliminazioni in cascata . In questo modo, se è necessario eliminare un concorrente, è possibile farlo e tutte le righe associate a quel concorrente verranno eliminate automaticamente. Altrimenti, per impostazione predefinita, la chiave esterna richiederà di eliminare tutte le righe correlate da Competitori concorrenti prima di consentire l'eliminazione di un concorrente.

(Alcune persone pensano che le chiavi esterne non a cascata siano una buona precauzione di sicurezza, ma la mia esperienza è che sono solo un tremendo dolore nel sedere che sono più spesso che non semplicemente un risultato di una svista e creano un sacco di lavoro per i DBA. Avere a che fare con la gente che elimina accidentalmente cose è il motivo per cui si hanno dialoghi come "sei sicuro" e vari tipi di backup regolari e origini dati ridondanti. È molto, molto più comune voler effettivamente eliminare un concorrente, i cui dati sono tutti incasinato, ad esempio, che è quello di cancellarne accidentalmente uno e poi andare "Oh no! Non intendevo farlo! E ora non ho i risultati della competizione! Aaaahh!" Quest'ultimo è certamente abbastanza comune, quindi , devi essere preparato per questo, ma il primo è molto più comune,quindi il modo più semplice e migliore per prepararsi al primo, imo, è semplicemente fare aggiornamenti ed eliminazioni a cascata delle chiavi esterne.)


1

Ok, questo è stato chiesto 7 anni fa, ma penso che la soluzione migliore qui sia quella di rinunciare del tutto al nuovo tavolo e farlo come una vista personalizzata. In questo modo non stai duplicando i dati, non devi preoccuparti di dati univoci e non tocca la struttura effettiva del database. Qualcosa come questo:

CREATE VIEW vw_competitions
  AS
  SELECT
   Id int
   CompetitionName nvarchar(75)
   CompetitionType nvarchar(50)
   OtherField1 int
   OtherField2 nvarchar(64)  --add the fields you want viewed from the Competition table
  FROM Competitions
GO

Altri elementi possono essere aggiunti qui come join su altre tabelle, clausole WHERE, ecc. Questa è probabilmente la soluzione più elegante a questo problema, dato che ora puoi semplicemente interrogare la vista:

SELECT *
FROM vw_competitions

... e aggiungi eventuali clausole WHERE, IN o EXISTS alla query di visualizzazione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.