Insérer SQL Server s'il n'existe pas de bonnes pratiques

152

J'ai un Competitionstableau de résultats qui contient les noms des membres de l'équipe et leur classement d'une part.

D'autre part, je dois maintenir un tableau des noms de concurrents uniques :

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

Maintenant, j'ai environ 200000 résultats dans la 1ère table et lorsque la table des concurrents est vide, je peux effectuer ceci:

INSERT INTO Competitors SELECT DISTINCT Name FROM CompResults

Et la requête ne prend que 5 secondes pour insérer environ 11 000 noms.

Jusqu'à présent, ce n'est pas une application critique, donc je peux envisager de tronquer le tableau des concurrents une fois par mois, lorsque je reçois les nouveaux résultats du concours avec environ 10 000 lignes.

Mais quelle est la meilleure pratique lorsque de nouveaux résultats sont ajoutés, avec des concurrents nouveaux ET existants? Je ne veux pas tronquer la table des concurrents existante

Je dois effectuer l'instruction INSERT pour les nouveaux concurrents uniquement et ne rien faire s'ils existent.

Didier Levy
la source
70
S'il vous plaît, ne faites pas d' une NVARCHAR(64)colonne votre clé primaire (et donc: clustering) !! Tout d'abord - c'est une clé très large - jusqu'à 128 octets; et deuxièmement, sa taille est variable - encore une fois: pas optimale ... C'est à peu près le pire choix que vous puissiez avoir - vos performances seront d'enfer, et la fragmentation des tables et des index sera à 99,9% tout le temps .....
marc_s
4
Marc a un bon point. N'utilisez pas le nom comme pk. Utilisez un identifiant, de préférence int ou quelque chose de léger.
richard
6
Voir l'article de blog de Kimberly Tripp sur ce qui fait une bonne clé de clustering: unique, étroite, statique, en constante augmentation. Votre cNameéchec dans trois des quatre catégories ... (ce n'est pas étroit, ce n'est probablement pas statique, et ce n'est certainement pas en augmentation constante)
marc_s
Je ne vois pas l'intérêt d'ajouter une clé primaire INT à la table Nom d'un concurrent où TOUTES les requêtes seront sur le nom, comme 'WHERE nom comme'% xxxxx% '' donc j'ai toujours besoin d'un index unique sur le nom. Mais oui, je peux voir l'intérêt de NE PAS faire de longueur variable ..
Didier Levy
3
a) éviter la fragmentation et b) s'il s'agit de la clé étrangère dans d'autres tables, les données dupliquées sont plus grandes que nécessaire (ce qui est une considération de vitesse)
JamesRyan

Réponses:

214

Sémantiquement, vous demandez "insérer des concurrents là où n'existe pas déjà":

INSERT Competitors (cName)
SELECT DISTINCT Name
FROM CompResults cr
WHERE
   NOT EXISTS (SELECT * FROM Competitors c
              WHERE cr.Name = c.cName)
gbn
la source
2
Eh bien, c'est ce que j'aurais fait avant de poser la question sur SO. Mais le cœur de ma pensée est: dans quelle mesure cela fonctionnera-t-il par rapport à la reconstruction de la table des noms à partir de zéro une fois par semaine environ? (rappelez-vous que cela ne prend que quelques secondes)
Didier Levy
3
@Didier Levy: efficacité? Pourquoi tronquer, recréer lorsque vous pouvez mettre à jour avec les différences uniquement. C'est-à-dire: BEGIN TRAN DELETE CompResults INSERT CompResults .. COMMIT TRAN = plus de travail.
gbn
@gbn - Y a-t-il un moyen d'utiliser la logique if-else en toute sécurité ici au lieu de votre réponse? J'ai une question connexe. Pouvez-vous m'aider avec ça? stackoverflow.com/questions/21889843/…
Steam
53

Une autre option est de quitter votre table de résultats avec votre table de concurrents existante et de trouver les nouveaux concurrents en filtrant les enregistrements distincts qui ne correspondent pas à la jointure:

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 nouvelle syntaxe MERGE offre également un moyen compact, élégant et efficace de le faire:

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);
pcofre
la source
1
Merge est génial dans ce cas, il fait exactement ce qu'il dit.
VorobeY1326
Je pense vraiment que c'est la bonne voie à suivre, donnant à SQL Server les meilleurs conseils possibles pour l'optimisation, contrairement à l'approche de sous-requête.
Mads Nielsen
4
La déclaration MERGE a encore beaucoup de problèmes. Il suffit de google "Problèmes de fusion SQL" - de nombreux blogueurs en ont longuement discuté.
David Wilson
pourquoi y a-t-il As Target dans l'instruction MERGE, mais pas Target dans l'instruction INSERT? Il y a plus de différences qui rendent difficile la compréhension de l'équivalence.
Peter
32

Je ne sais pas pourquoi quelqu'un d'autre n'a pas encore dit cela;

NORMALISER.

Vous avez une table qui modélise les compétitions? Les compétitions sont constituées de concurrents? Vous avez besoin d'une liste distincte de concurrents dans une ou plusieurs compétitions ......

Vous devriez avoir les tableaux suivants .....

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

Avec des contraintes sur CompetitionCompetitors.CompetitionID et CompetitorID pointant vers les autres tables.

Avec ce type de structure de table - vos clés sont toutes de simples INTS - il ne semble pas y avoir de bonne CLE NATURELLE qui conviendrait au modèle, donc je pense qu'une CLÉ SURROGATE convient bien ici.

Donc, si vous aviez ceci, pour obtenir la liste distincte des concurrents dans un concours particulier, vous pouvez émettre une requête comme celle-ci:

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
        )

Et si vous vouliez le score de chaque compétition à laquelle participe un concurrent:

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]

Et lorsque vous avez un nouveau concours avec de nouveaux concurrents, il vous suffit de vérifier ceux qui existent déjà dans le tableau des concurrents. S'ils existent déjà, vous n'insérez pas dans Competitor pour ces concurrents et insérez pour les nouveaux.

Ensuite, vous insérez le nouveau Concours en Compétition et enfin il vous suffit de faire tous les liens dans CompetitionCompetitors.

Transacter Charlie
la source
2
En supposant que l'OP a la légèreté à ce stade de restructurer toutes ses tables pour obtenir un résultat mis en cache. Réécrire votre base de données et votre application, au lieu de résoudre le problème dans une certaine portée définie, chaque fois que quelque chose ne se met pas en place facilement, est une recette pour un désastre.
Jeffrey Vest
1
Peut-être que dans le cas de l'OP comme le mien, vous n'avez pas toujours accès pour modifier la base de données .. ET réécrire / normaliser une ancienne base de données n'est pas toujours dans le budget ou le temps alloué.
eaglei22
10

Vous devrez rejoindre les tables ensemble et obtenir une liste de concurrents uniques qui n'existent pas déjà Competitors.

Cela insérera des enregistrements uniques.

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

Il peut arriver un moment où cette insertion doit être faite rapidement sans pouvoir attendre la sélection de noms uniques. Dans ce cas, vous pouvez insérer les noms uniques dans une table temporaire, puis utiliser cette table temporaire pour l'insérer dans votre table réelle. Cela fonctionne bien car tout le traitement se produit au moment où vous insérez dans une table temporaire, donc cela n'affecte pas votre table réelle. Ensuite, lorsque vous avez terminé le traitement, vous effectuez une insertion rapide dans la table réelle. Je pourrais même envelopper la dernière partie, où vous insérez dans la vraie table, à l'intérieur d'une transaction.

Richard
la source
4

Les réponses ci-dessus qui parlent de normalisation sont excellentes! Mais que faire si vous vous trouvez dans une position comme moi où vous n'êtes pas autorisé à toucher le schéma ou la structure de la base de données en l'état? Par exemple, les DBA sont des «dieux» et toutes les révisions suggérées vont dans / dev / null?

À cet égard, je pense que cela a également été répondu avec cette publication de Stack Overflow en ce qui concerne tous les utilisateurs ci-dessus donnant des échantillons de code.

Je republie le code de INSERT VALUES WHERE NOT EXISTS, ce qui m'a le plus aidé car je ne peux pas modifier les tables de base de données sous-jacentes:

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

Le code ci-dessus utilise des champs différents de ceux que vous avez, mais vous obtenez l'essentiel des différentes techniques.

Notez que selon la réponse originale sur Stack Overflow, ce code a été copié à partir d'ici .

Quoi qu'il en soit, mon argument est que les «meilleures pratiques» se résument souvent à ce que vous pouvez et ne pouvez pas faire aussi bien que la théorie.

  • Si vous êtes capable de normaliser et de générer des index / clés, c'est parfait!
  • Si ce n'est pas le cas et que vous avez recours à des hacks de code comme moi, j'espère que ce qui précède vous aidera.

Bonne chance!


la source
Au cas où ce ne serait pas clair, il s'agit de quatre approches différentes du problème, alors choisissez-en une.
nasch
3

La normalisation de vos tables opérationnelles comme suggéré par Transact Charlie est une bonne idée et vous évitera de nombreux maux de tête et problèmes au fil du temps - mais il existe des éléments tels que les tables d' interface , qui prennent en charge l'intégration avec des systèmes externes, et les tableaux de rapport , qui prennent en charge des choses comme l'analyse En traitement; et ces types de tableaux ne doivent pas nécessairement être normalisés - en fait, très souvent, il est beaucoup, beaucoup plus pratique et performant pour eux de ne pas être .

Dans ce cas, je pense que la proposition de Transact Charlie pour vos tables opérationnelles est bonne.

Mais j'ajouterais un index (pas nécessairement unique) à CompetitorName dans la table Competitors pour prendre en charge des jointures efficaces sur CompetitorName à des fins d'intégration (chargement de données à partir de sources externes), et je mettrais une table d'interface dans le mélange: CompetitionResults.

Les résultats de la compétition doivent contenir toutes les données de vos résultats de compétition. Le but d'une table d'interface comme celle-ci est de rendre aussi rapide et facile que possible la tronquer et la recharger à partir d'une feuille Excel ou d'un fichier CSV, ou quelle que soit la forme dans laquelle vous avez ces données.

Cette table d'interface ne doit pas être considérée comme faisant partie de l'ensemble normalisé de tables opérationnelles.Ensuite, vous pouvez vous joindre à CompetitionResults comme suggéré par Richard, pour insérer des enregistrements dans des concurrents qui n'existent pas déjà, et mettre à jour ceux qui existent (par exemple si vous avez réellement plus d'informations sur les concurrents, comme leur numéro de téléphone ou leur adresse e-mail).

Une chose que je voudrais noter - en réalité, il me semble que le nom du concurrent est très peu susceptible d'être unique dans vos données . Dans 200 000 concurrents, vous pouvez très bien avoir 2 David Smith ou plus, par exemple. Je vous recommande donc de collecter plus d'informations auprès des concurrents, telles que leur numéro de téléphone ou une adresse e-mail, ou quelque chose qui est plus susceptible d'être unique.

Votre table opérationnelle, Concurrents, doit avoir une seule colonne pour chaque élément de données qui contribue à une clé naturelle composite; par exemple, il doit avoir une colonne pour une adresse e-mail principale. Mais la table d'interface doit avoir un emplacement pour l' ancien et le nouveau valeurs pour une adresse e-mail principale, afin que l'ancienne valeur puisse être utilisée pour rechercher l'enregistrement dans Concurrents et mettre à jour cette partie avec la nouvelle valeur.

Donc CompetitionResults devrait avoir quelques champs "anciens" et "nouveaux" - oldEmail, newEmail, oldPhone, newPhone, etc. De cette façon, vous pouvez former une clé composite, dans Competitors, à partir de CompetitorName, Email et Phone.

Ensuite, lorsque vous avez des résultats de compétition, vous pouvez tronquer et recharger votre table CompetitionResults à partir de votre feuille Excel ou de tout ce que vous avez, et exécuter une seule insertion efficace pour insérer tous les nouveaux concurrents dans la table des concurrents, et une mise à jour unique et efficace pour mettre à jour toutes les informations sur les concurrents existants à partir des résultats du concours. Et vous pouvez faire une seule insertion pour insérer de nouvelles lignes dans le tableau CompetitionCompetitors. Ces opérations peuvent être effectuées dans une procédure stockée ProcessCompetitionResults, qui pourrait être exécutée après le chargement de la table CompetitionResults.

C'est une sorte de description rudimentaire de ce que j'ai vu faire à maintes reprises dans le monde réel avec Oracle Applications, SAP, PeopleSoft et une longue liste d'autres suites logicielles d'entreprise.

Un dernier commentaire que je ferais est celui que j'ai déjà fait sur SO: Si vous créez une clé étrangère qui garantit qu'un concurrent existe dans la table des concurrents avant de pouvoir ajouter une ligne avec ce concurrent à CompetitionCompetitors, assurez-vous que la clé étrangère est définie pour mettre en cascade les mises à jour et les suppressions . De cette façon, si vous devez supprimer un concurrent, vous pouvez le faire et toutes les lignes associées à ce concurrent seront automatiquement supprimées. Sinon, par défaut, la clé étrangère vous demandera de supprimer toutes les lignes associées de CompetitionCompetitors avant de vous permettre de supprimer un concurrent.

(Certaines personnes pensent que les clés étrangères non en cascade sont une bonne précaution de sécurité, mais mon expérience est qu'elles sont juste une douleur effrayante dans les fesses qui sont le plus souvent simplement le résultat d'un oubli et qu'elles créent un tas de travail pour les administrateurs de base de données. Le fait que des personnes suppriment accidentellement des éléments est la raison pour laquelle vous avez des boîtes de dialogue "êtes-vous sûr" et divers types de sauvegardes régulières et de sources de données redondantes. Il est beaucoup plus courant de vouloir supprimer un concurrent, dont les données sont toutes foiré par exemple, c'est d'en supprimer accidentellement un puis de dire "Oh non! Je ne voulais pas faire ça! Et maintenant je n'ai pas leurs résultats de compétition! Aaaahh!" Ce dernier est certainement assez courant, donc , vous devez y être préparé, mais le premier est beaucoup plus courant,Donc, le moyen le plus simple et le meilleur de se préparer à l'ancien, imo, est simplement de faire en sorte que les clés étrangères mettent en cascade les mises à jour et les suppressions.)

Shavais
la source
1

Ok, cela a été demandé il y a 7 ans, mais je pense que la meilleure solution ici est de renoncer complètement à la nouvelle table et de le faire simplement comme une vue personnalisée. De cette façon, vous ne dupliquez pas de données, vous ne vous inquiétez pas des données uniques et cela ne touche pas la structure réelle de la base de données. Quelque chose comme ça:

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

D'autres éléments peuvent être ajoutés ici comme des jointures sur d'autres tables, des clauses WHERE, etc. C'est probablement la solution la plus élégante à ce problème, car vous pouvez maintenant simplement interroger la vue:

SELECT *
FROM vw_competitions

... et ajoutez les clauses WHERE, IN ou EXISTS à la requête de vue.

Beervenger
la source