La manipulation de plusieurs enregistrements dans un MS SQL déclencheur
Je suis d'avoir à utiliser des déclencheurs dans MSSQL pour la première fois, bien déclenche en général. Après avoir lu autour et testé moi-même je me rends compte maintenant que le déclencheur est activé par commande et non par ligne insérée, supprimé ou mis à jour.
La chose entière est quelques statistiques pour un système de publicité. Notre principal stat table est assez grande et ne contiennent pas de données d'une manière qui fait sens dans la plupart des cas. Il contient une ligne par annonce cliqué, vu et etc. En tant qu'utilisateur on est plus enclin à vouloir voir ce que le jour X a Y nombre de clics et le montant de Z de points de vue et ainsi de suite. Nous l'avons fait purement basé sur une requête SQL jusqu'à présent, cette sorte de rapport de la table principale, mais que la table est devenue tellement le temps pour que la requête à exécuter. En raison de cela, nous avons opté pour l'utilisation des déclencheurs de garder un autre tableau mis à jour et donc ce qui en fait un peu plus facile sur le serveur SQL.
Mon problème est maintenant de le faire fonctionner avec plusieurs enregistrements. Ce que j'ai fait est de créer 2 procédures stockées, l'un pour la gestion de l'exploitation d'un insert, et une pour les supprimer. Mon déclencheur d'insertion (écrit pour travailler avec un seul enregistrement) puis graps les données de la table Inserted, et l'envoie directement à la procédure stockée. Le déclencheur de suppression fonctionne de la même manière, et (évidemment?) le déclencheur de mise à jour fonctionne de la même comme un delete + insert.
Mon problème est maintenant la meilleure façon de faire cela avec plusieurs enregistrements. J'ai essayé à l'aide d'un curseur, mais comme ce que j'ai pu lire et voir moi-même, ce réalise vraiment mal. J'ai envisagé d'écrire certains des "contrôles" - comme dans la vérification pour voir SI il y a plusieurs enregistrements dans les commandes, et puis aller avec le curseur, et sinon, il suffit juste d'éviter cela. De toute façon, voici ma solution avec un curseur, et im me demandais si il y a moyen de faire mieux?
CREATE TRIGGER [dbo].[TR_STAT_INSERT]
ON [iqdev].[dbo].[Stat]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Date DATE
DECLARE @CampaignId BIGINT
DECLARE @CampaignName varchar(500)
DECLARE @AdvertiserId BIGINT
DECLARE @PublisherId BIGINT
DECLARE @Unique BIT
DECLARE @Approved BIT
DECLARE @PublisherEarning money
DECLARE @AdvertiserCost money
DECLARE @Type smallint
DECLARE InsertCursor CURSOR FOR SELECT Id FROM Inserted
DECLARE @curId bigint
OPEN InsertCursor
FETCH NEXT FROM InsertCursor INTO @curId
WHILE @@FETCH_STATUS = 0
BEGIN
SELECT @Date = [Date], @PublisherId = [PublisherCustomerId], @Approved = [Approved], @Unique = [Unique], @Type = [Type], @AdvertiserCost = AdvertiserCost, @PublisherEarning = PublisherEarning
FROM Inserted
WHERE Id = @curId
SELECT @CampaignId = T1.CampaignId, @CampaignName = T2.Name, @AdvertiserId = T2.CustomerId
FROM Advert AS T1
INNER JOIN Campaign AS T2 on T1.CampaignId = T2.Id
WHERE T1.Id = (SELECT AdvertId FROM Inserted WHERE Id = @curId)
EXEC ProcStatInsertTrigger @Date, @CampaignId, @CampaignName, @AdvertiserId, @PublisherId, @Unique, @Approved, @PublisherEarning, @AdvertiserCost, @Type
FETCH NEXT FROM InsertCursor INTO @curId
END
CLOSE InsertCursor
DEALLOCATE InsertCursor
END
La procédure stockée est plutôt grand et intense et je ne pense pas qu'il y a un moyen d'avoir à éviter de boucle à travers les enregistrements de la table Insérée d'une manière ou d'une autre (ok, peut-être qu'il est, mais je voudrais être capable de lire le code de trop :p), donc je ne vais pas vous ennuyer avec celui-là (sauf si vous aimez à penser le contraire). Donc, à peu près, est-il une meilleure façon de faire cela, et si oui, comment?
EDIT: bon après la demande de plusieurs, voici la procédure stockée
CREATE PROCEDURE ProcStatInsertTrigger
@Date DATE,
@CampaignId BIGINT,
@CampaignName varchar(500),
@AdvertiserId BIGINT,
@PublisherId BIGINT,
@Unique BIT,
@Approved BIT,
@PublisherEarning money,
@AdvertiserCost money,
@Type smallint
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
IF @Approved = 1
BEGIN
DECLARE @test bit
SELECT @test = 1 FROM CachedStats WHERE [Date] = @Date AND CampaignId = @CampaignId AND CustomerId = @PublisherId
IF @test IS NULL
BEGIN
INSERT INTO CachedStats ([Date], CustomerId, CampaignId, CampaignName) VALUES (@Date, @PublisherId, @CampaignId, @CampaignName)
END
SELECT @test = NULL
DECLARE @Clicks int
DECLARE @TotalAdvertiserCost money
DECLARE @TotalPublisherEarning money
DECLARE @PublisherCPC money
DECLARE @AdvertiserCPC money
SELECT @Clicks = Clicks, @TotalAdvertiserCost = AdvertiserCost + @AdvertiserCost, @TotalPublisherEarning = PublisherEarning + @PublisherEarning FROM CachedStats
WHERE [Date] = @Date AND CustomerId = @PublisherId AND CampaignId = @CampaignId
IF @Type = 0 -- If click add one to the calculation
BEGIN
SELECT @Clicks = @Clicks + 1
END
IF @Clicks > 0
BEGIN
SELECT @PublisherCPC = @TotalPublisherEarning / @Clicks, @AdvertiserCPC = @TotalAdvertiserCost / @Clicks
END
ELSE
BEGIN
SELECT @PublisherCPC = 0, @AdvertiserCPC = 0
END
IF @Type = 0
BEGIN
UPDATE CachedStats SET
Clicks = @Clicks,
UniqueClicks = UniqueClicks + @Unique,
PublisherEarning = @TotalPublisherEarning,
AdvertiserCost = @TotalAdvertiserCost,
PublisherCPC = @PublisherCPC,
AdvertiserCPC = @AdvertiserCPC
WHERE [Date] = @Date AND CustomerId = @PublisherId AND CampaignId = @CampaignId
END
ELSE IF @Type = 1 OR @Type = 4 -- lead or coreg
BEGIN
UPDATE CachedStats SET
Leads = Leads + 1,
PublisherEarning = @TotalPublisherEarning,
AdvertiserCost = @TotalAdvertiserCost,
AdvertiserCPC = @AdvertiserCPC,
PublisherCPC = @AdvertiserCPC
WHERE [Date] = @Date AND CustomerId = @PublisherId AND CampaignId = @CampaignId
END
ELSE IF @Type = 3 -- Isale
BEGIN
UPDATE CachedStats SET
Leads = Leads + 1,
PublisherEarning = @TotalPublisherEarning,
AdvertiserCost = @TotalAdvertiserCost,
AdvertiserCPC = @AdvertiserCPC,
PublisherCPC = @AdvertiserCPC,
AdvertiserOrderValue = @AdvertiserCost,
PublisherOrderValue = @PublisherEarning
WHERE [Date] = @Date AND CustomerId = @PublisherId AND CampaignId = @CampaignId
END
ELSE IF @Type = 2 -- View
BEGIN
UPDATE CachedStats SET
[Views] = [Views] + 1,
UniqueViews = UniqueViews + @Unique,
PublisherEarning = @TotalPublisherEarning,
AdvertiserCost = @TotalAdvertiserCost,
PublisherCPC = @PublisherCPC,
AdvertiserCPC = @AdvertiserCPC
WHERE [Date] = @Date AND CustomerId = @PublisherId AND CampaignId = @CampaignId
END
END
END
Après l'aide, voici mon résultat final, posté dans le cas où d'autres ont un problème similaire
CREATE TRIGGER [dbo].[TR_STAT_INSERT]
ON [iqdev].[dbo].[Stat]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON
-- insert all missing "CachedStats" rows
INSERT INTO
CachedStats ([Date], AdvertId, CustomerId, CampaignId, CampaignName)
SELECT DISTINCT
CONVERT(Date, i.[Date]), i.AdvertId, i.[PublisherCustomerId], c.Id, c.Name
FROM
Inserted i
INNER JOIN Advert AS a ON a.Id = i.AdvertId
INNER JOIN Campaign AS c ON c.Id = a.CampaignId
WHERE
i.[Approved] = 1
AND NOT EXISTS (
SELECT 1
FROM CachedStats as t
WHERE
[Date] = CONVERT(Date, i.[Date])
AND CampaignId = c.Id
AND CustomerId = i.[PublisherCustomerId]
AND t.AdvertId = i.AdvertId
)
-- update all affected records at once
UPDATE
CachedStats
SET
Clicks =
Clicks + (
SELECT COUNT(*) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 0
),
UniqueClicks =
UniqueClicks + (
SELECT COUNT(*) FROM Inserted s
WHERE s.Approved = 1
AND s.[Unique] = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 0
),
[Views] =
[Views] + (
SELECT COUNT(*) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 2
),
UniqueViews =
UniqueViews + (
SELECT COUNT(*) FROM Inserted s
WHERE s.Approved = 1
AND s.[Unique] = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 2
),
Leads =
Leads + (
SELECT COUNT(*) FROM Inserted s
WHERE s.Approved = 1
AND s.[Unique] = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] IN (1,3,4)
),
PublisherEarning =
CachedStats.PublisherEarning + ISNULL((
SELECT SUM(PublisherEarning) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
), 0),
AdvertiserCost =
CachedStats.AdvertiserCost + ISNULL((
SELECT SUM(AdvertiserCost) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
), 0),
PublisherOrderValue =
PublisherOrderValue + ISNULL((
SELECT SUM(PublisherEarning) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 3
), 0),
AdvertiserOrderValue =
AdvertiserOrderValue + ISNULL((
SELECT SUM(AdvertiserCost) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 3
), 0),
PublisherCPC =
CASE WHEN (Clicks + (
SELECT COUNT(*) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 0
)) > 0 THEN
(CachedStats.PublisherEarning + ISNULL((
SELECT SUM(PublisherEarning) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
), 0)) -- COST ^
/ (
Clicks + (
SELECT COUNT(*) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 0
)
) --- Clicks ^
ELSE
0
END,
AdvertiserCPC =
CASE WHEN (Clicks + (
SELECT COUNT(*) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 0
)) > 0 THEN
(CachedStats.AdvertiserCost + ISNULL((
SELECT SUM(AdvertiserCost) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
), 0)) -- COST ^
/ (
Clicks + (
SELECT COUNT(*) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 0
)
) --- Clicks ^
ELSE
0
END
FROM
Inserted i
WHERE
i.Approved = 1 AND
CachedStats.Advertid = i.AdvertId AND
CachedStats.[Date] = Convert(Date, i.[Date]) AND
CachedStats.CustomerId = i.PublisherCustomerId
SET NOCOUNT OFF
END
Il ressemble un peu différent maintenant, parce que j'ai eu à l'index par la publicité trop mais merci beaucoup pour l'aide - accéléré de 30hour+ de 30 sec pour générer le CachedStats de mon propre développement Stat table 🙂
- Pouvez-vous nous dire ce que "ProcStatInsertTrigger" le fait? (BTW: Vous ne devriez pas le nom d'une procédure stockée "Déclencheur", plutôt pour des raisons évidentes). Si ce n'est pas beaucoup plus que d'insérer des données dans une table, puis il y a un moyen de simplifier considérablement l'approche.
- Merci de poster votre version finale. 🙂 Cependant, je ne suis pas sûr, il est optimal - vous faire beaucoup apparemment redondante sous-sélectionne que peut être établi et calculé à partir de la jointure résultat, à mon humble avis.
- J'aimerais m'en débarrasser ainsi. Cependant comme je ne suis pas SQL Gourou, je n'ai aucune idée de comment. Si vous pouviez me montrer le chemin que j'aimerais l'optimiser un peu plus. Aussi serait-il préférable de performance sage de vérifier pour voir si il y a plusieurs lignes et si pas juste faire comme avant?
- Oh, et aussi, en allant de la FAST_FORWARD méthode de curseur pour ce - la méthode de curseur a pris environ 17 minutes pour faire de l'INSERTION de la partie (comme mentionné, j'ai une SUPPRESSION en partie de trop, qui a peu près juste - au lieu de + dans la requête ci-dessus) - celui-ci: ~30 secondes.
- Si je trouve le temps demain je regarde en elle, peut-être que je peux envelopper ma tête autour de lui. Un premier aperçu de votre code l'indique, il ne sera pas facile. De toute façon, le temps d'exécution d'amélioration déjà réalisé des sons assez impressionnant.
- L'amélioration est significative oui - cependant, j'ai été tellement convaincu d'être " ok " que j'ai essayé de le mettre en ligne sur notre système en ligne maintenant (qui a environ 181 k $ lignes à la place de mes données de test de 25k) et qui a mâché sur la même commande pour environ 1 heure et c'est pas encore fait. J'aimerais de l'aide! 🙂
- Oh d'ailleurs, si vous NE le regardez, ne vous inquiétez pas à propos de ces CPC colonnes, il s'est avéré qu'ils seraient assez inutile d'avoir des pré calculé, donc j'ai pris ceux de la table, ce qui facilite également la requête un peu.
- Je n'ai pas bien gérer vendredi dernier. Jetez un oeil à mon code modifié, je pense que nous sommes proches de l'optimum maintenant.
Vous devez vous connecter pour publier un commentaire.
Le truc avec ce genre de situation est à son tour le fonctionnement séquentiel (pour chaque enregistrement ne xyz) dans un ensemble de base de fonctionnement (une instruction de mise à JOUR).
J'ai analysé votre procédure stockée et de fusion de votre mise à JOUR séparée consolidés en un seul. Cette unique phrase peut ensuite être transformé en une version qui peut être appliquée à tous les enregistrements insérés à la fois, éliminant le besoin d'une procédure stockée et, partant, la nécessité d'un curseur.
EDIT: ci-Dessous le code que nous avons finalement obtenu de travail. Le temps d'exécution pour l'ensemble de l'opération est passé de "presque jamais" (pour la solution d'origine) pour quelque chose sous un deuxième, selon l'OP de la rétroaction. Globale de la taille du code a également diminué sensiblement.
Les opérations impliquant la
CachedStats
table bénéficieront grandement d'un index multi-colonnes sur(Advertid, CustomerId, [Date])
(confirmée par l'OP).En fonction de la version de MYSQL que vous utilisez, vous devriez également envisager d'utiliser des Vues Indexées pour cela ainsi. Cela pourrait très bien être une approche plus simple que vos déclencheurs, selon ce que le rapport de la requête ressemble. Voir ici pour plus d'info.
Aussi, dans votre déclencheur, vous devriez essayer d'écrire vos mises à jour pour la matérialisée les résultats de la table comme un ensemble de base de fonctionnement, pas d'un curseur. L'écriture d'un curseur en fonction de déclenchement pourrait éventuellement être simplement en déplaçant votre problème dans le rapport de la requête à votre table insère à la place.
Première chose que je voudrais faire est d'utiliser un FAST_FORWARD curseur à la place. Comme vous allez seulement à partir d'un enregistrement à l'autre et ne pas faire les mises à jour, ce sera beaucoup mieux pour la performance.
DÉCLARER le CURSEUR de la syntaxe
Vous pouvez légèrement optimiser votre curseur variation en faisant FAST_FORWARD, READ_ONLY et options LOCALES sur le curseur. Aussi, vous êtes en tirant l'Id de votre curseur, puis en boucle de retour pour obtenir les valeurs. Soit utiliser CURRENT_OF ou les jeter tous dans des variables. Mais, je ne m'attends pas à ces changements pour vous acheter beaucoup.
Vous avez vraiment besoin de se déplacer à un jeu basé sur l'approche. Cette procédure stockée est certainement faisable en un ensemble de base du modèle - mais il peut prendre 3 ou 4 mises à jour différents états. Mais même 3 ou 4 différents déclencheurs (1 pour les vues, 1 pour les clics, etc.) serait mieux que le curseur approche.
Votre meilleur pari est de trouver un ensemble de base de fonctionnement de la gâchette. Je ne vais pas vous écrire pour vous à 100%, mais permettez-moi de vous obtenir a commencé, et nous pouvons voir où nous allons partir de là. Gardez à l'esprit que j'écris cela sans tableaux /schémas et donc je ne vais pas valider. Attendre Les Fautes De Frappe:-)
Passons à votre mise à jour des déclarations d'abord, De ce que je peux vous dire que la mise à jour de la même table avec la même clause where la seule différence est que les colonnes. Vous pouvez consolider cette ressemble à:
Je ne aggree avec vous que cela pourrait devenir laid mais c'est une décision que vous aurez à faire.
Que pour l'insertion de la clause, je réagirais de la même façon que vous êtes déjà il suffit de l'insérer dans la table de la table Inserted quelle que soit n'existent pas déjà.