Piszę zapytanie, które zostanie użyte do wyświetlenia strony dla kanału społecznościowego. Koncepcja polega na tym, że aplikacja mobilna zażąda N elementów i zapewni początkową datę i godzinę, którą zadzwoniłem @CutoffTime
poniżej. Celem czasu odcięcia jest ustalenie, kiedy powinno się rozpocząć okno stronicowania. Powodem, dla którego używamy znacznika czasu zamiast przesunięcia wiersza jest to, że znacznik czasu pozwoli nam przeglądać strony w spójnym miejscu podczas pobierania starszych postów, nawet jeśli dodawane są nowsze treści społecznościowe.
Ponieważ elementy kanału społecznościowego mogą pochodzić od ciebie lub twoich znajomych, używam a UNION
do połączenia wyników z tych dwóch grup. Oryginalnie próbowałem TheQuery_CTE
logiki bez UNION
i było to bardzo wolne.
Oto co zrobiłem (w tym odpowiedni schemat tabeli):
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
Zdaję sobie sprawę, mogę pozbyć się UNION
w GetCounts_CTE
ale nie sądzę, będzie to naprawdę rozwiązać żadnych problemów widzę poniżej.
Widzę kilka potencjalnych problemów:
- To dużo zduplikowanej logiki, więc prawdopodobnie utrudniam sobie życie.
- Jeśli wstawka zdarzy się między obliczeniem liczby a wybraniem danych, będę wyłączony. Nie sądzę, by zdarzało się to często, ale prowadziłoby do dziwnych / trudnych do debugowania błędów.
- Wszystkie problemy mądrzejsze / bardziej doświadczone przez ludzi przy powyższej konfiguracji.
Jaki jest najlepszy sposób na napisanie tego zapytania? Punkty bonusowe rozwiązanie upraszcza moje życie.
Edytować:
Nie chcę wybierać wszystkich danych i pozwolić klientowi leniwie wyświetlać elementy, ponieważ nie chcę nadużywać planów danych ludzi, zmuszając ich do pobierania elementów, których nigdy nie zobaczą. Trzeba przyznać, że dane prawdopodobnie nie będą tak duże w wielkim schemacie rzeczy, ale grosze nie będą działać ...
Edycja 2:
Podejrzewam, że nie jest to optymalne rozwiązanie, ale jest to najlepsze, jakie do tej pory wymyśliłem.
Przenoszenie mojego UNION
zapytania do VIEW
jak Greg zaproponował pracował dobrze się ukryć, że logika i dać bardziej zwięzły kwerendy w moim procedury przechowywanej. Widok uwydatnia także brzydotę / komplikację związku, co jest miłe, ponieważ używam go dwa razy w mojej selekcji. Oto kod widoku:
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]
Korzystając z tego widoku, skróciłem moje zapytanie do następującego. Uwaga: OFFSET
Ustawiam za pomocą podzapytania.
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
Widok ładnie oddziela złożoność UNION
od filtrowania. Myślę, że podzapytanie w OFFSET
klauzuli zapobiegnie problemom współbieżności, o które martwiłem się, czyniąc całe zapytanie atomowym.
Jednym z problemów, które właśnie znalazłem podczas pisania jest: w powyższym kodzie, jeśli dwa zdjęcia z tą samą datą utworzenia znajdują się na różnych „stronach”, to zdjęcia na kolejnych stronach zostaną odfiltrowane. Rozważ następujące dane:
PhotoId | Created | ...
------------------------
1 | 2015-08-26 01:00.00
2 | 2015-08-26 01:00.00
3 | 2015-08-26 01:00.00
Rozmiar strony 1 na stronie początkowej PhotoId 1
zostanie zwrócony. Przy takim samym rozmiarze strony na drugiej stronie wyniki nie będą zwracane. Myślę, że aby rozwiązać ten problem, muszę dodać Key
Guid jako parametr ....