Sto scrivendo una query che verrà utilizzata per sfogliare i risultati per un feed social. Il concetto è che l'app per dispositivi mobili richiederà N articoli e fornirà un datetime iniziale che ho chiamato di @CutoffTime
seguito. Lo scopo del tempo di interruzione è stabilire quando deve iniziare la finestra di paging. Il motivo per cui stiamo usando un timestamp anziché uno scostamento di riga è che il timestamp ci consentirà di passare da una posizione coerente quando otteniamo post più vecchi anche se vengono aggiunti nuovi contenuti social.
Poiché gli elementi dei feed social possono provenire da te o dai tuoi amici, sto usando a UNION
per combinare i risultati di questi due gruppi. Inizialmente ho provato la TheQuery_CTE
logica senza ilUNION
ed era un cane lento.
Questo è quello che ho fatto (incluso lo schema di tabella pertinente):
CREATE TABLE [Content].[Photo]
(
[PhotoId] INT NOT NULL PRIMARY KEY IDENTITY (1, 1),
[Key] UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
[FullResolutionUrl] NVARCHAR(255) NOT NULL,
[Description] NVARCHAR(255) NULL,
[Created] DATETIME2(2) NOT NULL DEFAULT SYSUTCDATETIME(),
);
CREATE TABLE [Content].[UserPhotoAssociation]
(
[PhotoId] INT NOT NULL,
[UserId] INT NOT NULL,
[ShowInSocialFeed] BIT NOT NULL DEFAULT 0,
CONSTRAINT [PK_UserPhotos] PRIMARY KEY ([PhotoId], [UserId]),
CONSTRAINT [FK_UserPhotos_User] FOREIGN KEY ([UserId])
REFERENCES [User].[User]([UserId]),
CONSTRAINT [FK_UserPhotos_Photo] FOREIGN KEY ([PhotoId])
REFERENCES [Content].[Photo]([PhotoId])
);
CREATE TABLE [Content].[FlaggedPhoto]
(
[FlaggedPhotoId] INT NOT NULL PRIMARY KEY IDENTITY(1,1),
[PhotoId] INT NOT NULL,
[FlaggedBy] INT NOT NULL,
[FlaggedOn] DATETIME2(0) NOT NULL DEFAULT SYSDATETIME(),
[FlaggedStatus] INT NOT NULL DEFAULT 1,
[ReviewedBy] INT NULL,
[ReviewedAt] DATETIME2(0) NULL
CONSTRAINT [FK_Photos_PhotoId_to_FlaggedPhotos_PhotoId] FOREIGN KEY ([PhotoId])
REFERENCES [Content].[Photo]([PhotoId]),
CONSTRAINT [FK_FlaggedPhotoStatus_FlaggedPhotoStatusId_to_FlaggedPhotos_FlaggedStatus] FOREIGN KEY ([FlaggedStatus])
REFERENCES [Content].[FlaggedContentStatus]([FlaggedContentStatusId]),
CONSTRAINT [FK_User_UserId_to_FlaggedPhotos_FlaggedBy] FOREIGN KEY ([FlaggedBy])
REFERENCES [User].[User]([UserId]),
CONSTRAINT [FK_User_UserId_to_FlaggedPhotos_ReviewedBy] FOREIGN KEY ([ReviewedBy])
REFERENCES [User].[User]([UserId])
);
CREATE TABLE [User].[CurrentConnections]
(
[MonitoringId] INT NOT NULL PRIMARY KEY IDENTITY,
[Monitor] INT NOT NULL,
[Monitored] INT NOT NULL,
[ShowInSocialFeed] BIT NOT NULL DEFAULT 1,
CONSTRAINT [FK_Monitoring_Monitor_to_User_UserId] FOREIGN KEY ([Monitor])
REFERENCES [dbo].[User]([UserId]),
CONSTRAINT [FK_Monitoring_Monitored_to_User_UserId] FOREIGN KEY ([Monitored])
REFERENCES [dbo].[User]([UserId])
);
CREATE TABLE [Content].[PhotoLike]
(
[PhotoLikeId] INT NOT NULL PRIMARY KEY IDENTITY,
[PhotoId] INT NOT NULL,
[UserId] INT NOT NULL,
[Created] DATETIME2(2) NOT NULL DEFAULT SYSUTCDATETIME(),
[Archived] DATETIME2(2) NULL,
CONSTRAINT [FK_PhotoLike_PhotoId_to_Photo_PhotoId] FOREIGN KEY ([PhotoId])
REFERENCES [Content].[Photo]([PhotoId]),
CONSTRAINT [FK_PhotoLike_UserId_to_User_UserId] FOREIGN KEY ([UserId])
REFERENCES [User].[User]([UserId])
);
CREATE TABLE [Content].[Comment]
(
[CommentId] INT NOT NULL PRIMARY KEY IDENTITY,
[PhotoId] INT NOT NULL,
[UserId] INT NOT NULL,
[Comment] NVARCHAR(255) NOT NULL,
[Created] DATETIME2(2) NOT NULL DEFAULT SYSUTCDATETIME(),
[CommentOrder] DATETIME2(2) NOT NULL DEFAULT SYSUTCDATETIME(),
[Archived] DATETIME2(2) NULL,
CONSTRAINT [FK_Comment_PhotoId_to_Photo_PhotoId] FOREIGN KEY ([PhotoId])
REFERENCES [Content].[Photo]([PhotoId]),
CONSTRAINT [FK_Comment_UserId_to_User_UserId] FOREIGN KEY ([UserId])
REFERENCES [User].[User]([UserId])
);
/*
End table schema
*/
DECLARE @UserId INT,
@NumberOfItems INT,
@CutoffTime DATETIME2(2) = NULL -- Stored Proc input params
-- Make the joins and grab the social data we need once since they are used in subsequent queries that aren't shown
DECLARE @SocialFeed TABLE ([Key] UNIQUEIDENTIFIER, [PhotoId] INT
, [Description] NVARCHAR(255), [FullResolutionUrl] NVARCHAR(255)
, [Created] DATETIME2(2), [CreatorId] INT, [LikeCount] INT
, [CommentCount] INT, [UserLiked] BIT);
-- Offset might be different for each group
DECLARE @OffsetMine INT = 0, @OffsetTheirs INT = 0;
IF @CutoffTime IS NOT NULL
BEGIN
-- Get the offsets
;WITH [GetCounts_CTE] AS
(
SELECT
[P].[PhotoId] -- INT
, 1 AS [MyPhotos]
FROM [Content].[Photo] [P]
INNER JOIN [Content].[UserPhotoAssociation] [UPA] ON
[UPA].[PhotoId] = [P].[PhotoId]
AND
[UPA].[ShowInSocialFeed] = 1
LEFT JOIN [Content].[FlaggedPhoto] [FP] ON
[FP].[PhotoId] = [P].[PhotoId]
AND
[FP].[FlaggedStatus] = 3 -- Flagged photos that are confirmed apply to everyone
WHERE
[FP].[FlaggedPhotoId] IS NULL -- Filter out flagged photos
AND
[UPA].[UserId] = @UserId -- Show the requesting user
AND
[P].[Created] >= @CutoffTime -- Get the newer items
UNION
SELECT
[P].[PhotoId] -- INT
, 0 AS [MyPhotos]
FROM [Content].[Photo] [P]
INNER JOIN [Content].[UserPhotoAssociation] [UPA] ON
[UPA].[PhotoId] = [P].[PhotoId]
AND
[UPA].[ShowInSocialFeed] = 1
INNER JOIN [User].[CurrentConnections] [M] ON
[M].[Monitored] = [UPA].[UserId]
AND
[M].[Monitor] = @UserId AND [M].[ShowInSocialFeed] = 1 -- this join isn't present above
LEFT JOIN [Content].[FlaggedPhoto] [FP] ON
[FP].[PhotoId] = [P].[PhotoId]
AND
(
[FP].[FlaggedStatus] = 3
OR
([FP].[FlaggedBy] = @UserId AND [FP].[FlaggedStatus] = 1)
) -- Flagged photos that are confirmed apply to everyone, pending flags apply to the user
WHERE
[FP].[FlaggedPhotoId] IS NULL -- Filter out flagged photos
AND
[P].[Created] >= @CutoffTime -- Get the newer items
)
SELECT
@OffsetMine = SUM(CASE WHEN [MyPhotos] = 1 THEN 1 ELSE 0 END)
, @OffsetTheirs = SUM(CASE WHEN [MyPhotos] = 0 THEN 1 ELSE 0 END)
FROM [GetCounts_CTE]
END
-- Prevent absence of social data from throwing an error below.
SET @OffsetMine = ISNULL(@OffsetMine, 0);
SET @OffsetTheirs = ISNULL(@OffsetTheirs, 0);
-- Actually select the data I want
;WITH TheQuery_CTE AS
(
SELECT
[P].[Key]
, [P].[PhotoId]
, [P].[Description]
, [P].[FullResolutionUrl]
, [P].[Created]
, [UPA].[UserId]
, COUNT(DISTINCT [PL].[PhotoLikeId]) AS [LikeCount] -- Count distinct used due to common join key
, COUNT(DISTINCT [C].[CommentId]) AS [CommentCount]
, CAST(ISNULL(MAX(CASE WHEN [PL].[UserId] = @UserId THEN 1 END), 0) AS BIT) AS [UserLiked]
FROM [Content].[Photo] [P]
INNER JOIN [Content].[UserPhotoAssociation] [UPA] ON
[UPA].[PhotoId] = [P].[PhotoId]
AND
[UPA].[ShowInSocialFeed] = 1
LEFT JOIN [Content].[PhotoLike] [PL] ON
[PL].[PhotoId] = [P].[PhotoId]
AND
[PL].[Archived] IS NULL
LEFT JOIN [Content].[Comment] [C] ON
[C].[PhotoId] = [P].[PhotoId]
AND
[C].[Archived] IS NULL
LEFT JOIN [Content].[FlaggedPhoto] [FP] ON
[FP].[PhotoId] = [P].[PhotoId]
AND
[FP].[FlaggedStatus] = 3 -- Flagged photos that are confirmed apply to everyone
WHERE
[FP].[FlaggedPhotoId] IS NULL -- Filter out flagged photos
AND
[UPA].[UserId] = @UserId -- Show the requesting user
GROUP BY
[P].[Key]
, [P].[PhotoId]
, [P].[Description]
, [P].[FullResolutionUrl]
, [P].[Created]
, [UPA].[UserId]
ORDER BY
[P].[Created] DESC
, [P].[Key] -- Ensure consistent order in case of duplicate timestamps
OFFSET @OffsetMine ROWS FETCH NEXT @NumberOfItems ROWS ONLY
UNION
SELECT
[P].[Key]
, [P].[PhotoId]
, [P].[Description]
, [P].[FullResolutionUrl]
, [P].[Created]
, [UPA].[UserId]
, COUNT(DISTINCT [PL].[PhotoLikeId]) AS [LikeCount]
, COUNT(DISTINCT [C].[CommentId]) AS [CommentCount]
, CAST(ISNULL(MAX(CASE WHEN [PL].[UserId] = @UserId THEN 1 END), 0) AS BIT) AS [UserLiked]
FROM [Content].[Photo] [P]
INNER JOIN [Content].[UserPhotoAssociation] [UPA] ON
[UPA].[PhotoId] = [P].[PhotoId]
AND
[UPA].[ShowInSocialFeed] = 1
INNER JOIN [User].[CurrentConnections] [M] ON
[M].[Monitored] = [UPA].[UserId]
AND
[M].[Monitor] = @UserId AND [M].[ShowInSocialFeed] = 1
LEFT JOIN [Content].[PhotoLike] [PL] ON
[PL].[PhotoId] = [P].[PhotoId]
AND
[PL].[Archived] IS NULL
LEFT JOIN [Content].[Comment] [C] ON
[C].[PhotoId] = [P].[PhotoId]
AND
[C].[Archived] IS NULL
LEFT JOIN [Content].[FlaggedPhoto] [FP] ON
[FP].[PhotoId] = [P].[PhotoId]
AND
(
[FP].[FlaggedStatus] = 3
OR
([FP].[FlaggedBy] = @UserId AND [FP].[FlaggedStatus] = 1)
) -- Flagged photos that are confirmed apply to everyone, pending flags apply to the user
WHERE
[FP].[FlaggedPhotoId] IS NULL -- Filter out flagged photos
GROUP BY
[P].[Key]
, [P].[PhotoId]
, [P].[Description]
, [P].[FullResolutionUrl]
, [P].[Created]
, [UPA].[UserId]
ORDER BY
[P].[Created] DESC
, [P].[Key] -- Ensure consistant order in case of duplicate timestamps
OFFSET @OffsetTheirs ROWS FETCH NEXT @NumberOfItems ROWS ONLY
)
INSERT INTO @SocialFeed ([Key], [PhotoId], [Description], [FullResolutionUrl]
, [Created], [CreatorId], [LikeCount], [CommentCount], [UserLiked])
SELECT TOP (@NumberOfItems)
[Key]
, [PhotoId]
, [Description]
, [FullResolutionUrl]
, [Created]
, [UserId]
, [LikeCount]
, [CommentCount]
, [UserLiked]
FROM [TheQuery_CTE]
ORDER BY -- Order here so the top works properly
[Created] DESC
, [Key] -- Ensure consistent order in case of duplicate timestamps
-- Output the social feed
SELECT
[P].[Key]
, [P].[PhotoId]
, [P].[Description] AS [PhotoDescription]
, [P].[FullResolutionUrl]
, [P].[Created] AS [Posted]
, [P].[CreatorId]
, [LikeCount]
, [CommentCount]
, [UserLiked]
FROM @Photos [P]
-- Select other data needed to build the object tree in the application layer
Mi rendo conto che posso sbarazzarmi di UNION
in GetCounts_CTE
ma non credo che risolverà davvero nessuno dei problemi che vedo di seguito.
Vedo alcuni potenziali problemi:
- Questa è molta logica duplicata, quindi probabilmente sto rendendo la vita più difficile per me stesso.
- Se si verifica un inserimento tra il calcolo del conteggio e la selezione dei dati, sarò spento. Non penso che ciò accada di frequente, ma porterebbe a bug strani / difficili da eseguire il debug.
- Tutti i problemi più intelligenti / più esperienza che le persone potrebbero trovare con la configurazione di cui sopra.
Qual è il modo migliore per scrivere questa query? Punti bonus che la soluzione mi semplifica la vita.
Modificare:
Non voglio selezionare tutti i dati e lasciare che il cliente mostri pigramente gli articoli, perché non voglio abusare dei piani dati delle persone costringendoli a scaricare elementi che non vedranno mai. Certo, i dati probabilmente non saranno così grandi nel grande schema delle cose, ma accumuli di penny ....
Modifica 2:
Sospetto fortemente che questa non sia la soluzione ottimale, ma è la migliore che abbia mai inventato finora.
Spostare la mia UNION
query in un modo VIEW
simile a quello suggerito da Greg ha funzionato bene per nascondere quella logica e fornire una query più concisa nella mia procedura memorizzata. La vista astrae anche la bruttezza / complicanza dell'unione, il che è bello perché lo sto usando due volte nella mia selezione. Ecco il codice per la vista:
CREATE VIEW [Social].[EverFeed]
AS
SELECT
[P].[Key]
, [P].[PhotoId]
, [P].[Description]
, [P].[FullResolutionUrl]
, [P].[Created]
, [UPA].[UserId]
, COUNT(DISTINCT [PL].[PhotoLikeId]) AS [LikeCount] -- Distinct due to common join key
, COUNT(DISTINCT [C].[CommentId]) AS [CommentCount]
, CAST(ISNULL(
MAX(CASE WHEN [PL].[UserId] = [UPA].[UserId] THEN 1 END), 0) AS BIT) AS [UserLiked]
, NULL AS [Monitor]
FROM [Content].[Photo] [P]
INNER JOIN [Content].[UserPhotoAssociation] [UPA] ON
[UPA].[PhotoId] = [P].[PhotoId]
AND
[UPA].[ShowInSocialFeed] = 1
LEFT JOIN [Content].[PhotoLike] [PL] ON
[PL].[PhotoId] = [P].[PhotoId]
AND
[PL].[Archived] IS NULL
LEFT JOIN [Content].[Comment] [C] ON
[C].[PhotoId] = [P].[PhotoId]
AND
[C].[Archived] IS NULL
LEFT JOIN [Content].[FlaggedPhoto] [FP] ON
[FP].[PhotoId] = [P].[PhotoId]
AND
[FP].[FlaggedStatus] = 3 -- Flagged photos that are confirmed apply to everyone
WHERE
[FP].[FlaggedPhotoId] IS NULL -- Filter out flagged photos
GROUP BY
[P].[Key]
, [P].[PhotoId]
, [P].[Description]
, [P].[FullResolutionUrl]
, [P].[Created]
, [UPA].[UserId]
UNION
SELECT
[P].[Key]
, [P].[PhotoId]
, [P].[Description]
, [P].[FullResolutionUrl]
, [P].[Created]
, [UPA].[UserId]
, COUNT(DISTINCT [PL].[PhotoLikeId]) AS [LikeCount]
, COUNT(DISTINCT [C].[CommentId]) AS [CommentCount]
, CAST(ISNULL(
MAX(CASE WHEN [PL].[UserId] = [M].[Monitor] THEN 1 END), 0) AS BIT) AS [UserLiked]
, [M].[Monitor]
FROM [Content].[Photo] [P]
INNER JOIN [Content].[UserPhotoAssociation] [UPA] ON
[UPA].[PhotoId] = [P].[PhotoId]
AND
[UPA].[ShowInSocialFeed] = 1
INNER JOIN [User].[CurrentConnections] [M] ON
[M].[Monitored] = [UPA].[UserId]
AND
[M].[ShowInSocialFeed] = 1
LEFT JOIN [Content].[PhotoLike] [PL] ON
[PL].[PhotoId] = [P].[PhotoId]
AND
[PL].[Archived] IS NULL
LEFT JOIN [Content].[Comment] [C] ON
[C].[PhotoId] = [P].[PhotoId]
AND
[C].[Archived] IS NULL
LEFT JOIN [Content].[FlaggedPhoto] [FP] ON
[FP].[PhotoId] = [P].[PhotoId]
AND
(
[FP].[FlaggedStatus] = 3
OR
([FP].[FlaggedBy] = [M].[Monitor] AND [FP].[FlaggedStatus] = 1)
) -- Flagged photos that are confirmed (3) apply to everyone
-- , pending flags (1) apply to the user
WHERE
[FP].[FlaggedPhotoId] IS NULL -- Filter out flagged photos
GROUP BY
[P].[Key]
, [P].[PhotoId]
, [P].[Description]
, [P].[FullResolutionUrl]
, [P].[Created]
, [UPA].[UserId]
, [M].[Monitor]
Usando quella vista ho abbreviato la mia domanda nel modo seguente. Nota Sto impostando il OFFSET
con una subquery.
DECLARE @UserId INT, @NumberOfItems INT, @CutoffTime DATETIME2(2);
SELECT
[Key]
, [PhotoId]
, [Description]
, [FullResolutionUrl]
, [Created]
, [UserId]
, [LikeCount]
, [CommentCount]
, [UserLiked]
FROM [Social].[EverFeed] [EF]
WHERE
(
([EF].[UserId] = @UserId AND [EF].[Monitor] IS NULL)
OR
[EF].[Monitor] = @UserId
)
ORDER BY -- Order here so the top works properly
[Created] DESC
, [Key] -- Ensure consistant order in case of duplicate timestamps
OFFSET CASE WHEN @CutoffTime IS NULL THEN 0 ELSE
(
SELECT
COUNT([PhotoId])
FROM [Social].[EverFeed] [EF]
WHERE
(
([EF].[UserId] = @UserId AND [EF].[Monitor] IS NULL)
OR
[EF].[Monitor] = @UserId
)
AND
[EF].[Created] >= @CutoffTime -- Get the newer items
) END
ROWS FETCH NEXT @NumberOfItems ROWS ONLY
La vista separa bene la complessità del UNION
filtro. Penso che la subquery nella OFFSET
clausola prevenga problemi di concorrenza di cui ero preoccupato rendendo atomica l'intera query.
Un problema che ho appena riscontrato durante la digitazione è: nel codice sopra se due foto con la stessa data di creazione si trovano su "pagine" diverse, le foto nelle pagine successive verranno filtrate. Considera i seguenti dati:
PhotoId | Created | ...
------------------------
1 | 2015-08-26 01:00.00
2 | 2015-08-26 01:00.00
3 | 2015-08-26 01:00.00
Con una dimensione della pagina pari a 1 nella pagina iniziale, PhotoId 1
verrà restituito. Con le stesse dimensioni della pagina nella seconda pagina non verranno restituiti risultati. Penso che per risolvere questo problema dovrò aggiungere il Key
Guid come parametro ....