Написание SQL-запроса на основе множеств без операций над множествами (EXCEPT, INTERSECT) - PullRequest
1 голос
/ 10 февраля 2011

У меня есть три таблицы: UserObjects, UserObjectsRelations, UserClasses, которые моделируют отношения M: N между UserObjects и UserClasses.

Теперь мне нужно выбрать это:

All(UserObjects) - Intersect(UserObjectRelations -> UserObjects).Where(UserObjectRelation -> UserClassId IN (some list))

Это означает, что у меня есть список UserClassIds, которые я использую для фильтрации UserClasses (или отношений напрямую), и мне нужно найти все UserObjects, которые не назначены all этим UserClasses.

Пример. Предположим, что у меня есть UserObjectRelations, отфильтрованные по UserClassId IN (1,2):

UserClassId |  UserObjectId 
--------------------------
     1      |        1
     2      |        1
     2      |        2

У меня также есть много других пользовательских объектов. Результатом моего запроса должны быть все UserObjects, не упомянутые в этом наборе результатов + UserObject с Id = 2, поскольку он не связан со всеми запрошенными UserClasses.

Проблема в том, что SQL-запрос генерируется Entity Framework (у нас нет полного контроля над сгенерированным SQL), поэтому наш первоначальный подход с INTERSECT не удался - со многими классами UserClass он создает слишком сложный запрос, а иногда SQL Server вызывает ошибку из-за глубокого вложения.

Он создает такой запрос (но очень большой, потому что EF не использует * нотацию и ему действительно нравится много вложенных SELECT):

SELECT Unsused.* 
FROM dbo.UserObjects AS Unsused
WHERE Unsused.IsDeleted = 0
EXCEPT (
    SELECT U.* 
    FROM dbo.UserObjects AS U
    INNER JOIN dbo.UserObjectRelations AS UR ON UR.UserObjectId = U.Id
    WHERE UR.UserClassId = 1
    INTERSECT (
        SELECT U.* 
        FROM dbo.UserObjects AS U
        INNER JOIN dbo.UserObjectRelations AS UR ON UR.UserObjectId = U.Id
        WHERE UR.UserClassId = 2    
    ))

Сейчас я переписываю запрос - сначала в SQL, затем я попытаюсь определить его в Linq-To-Entities. Я придумываю это:

SELECT Unused.*
FROM dbo.UserObjects AS Unused
LEFT JOIN (
    SELECT UsageReport.Id
    FROM (
        SELECT Tmp.Id, COUNT(*) AS Usage
        FROM dbo.UserObjects AS Tmp 
        INNER JOIN dbo.UserObjectRelations AS DefiningRelations ON
            Tmp.Id = DefiningRelations.UserObjectId
        WHERE DefiningRelations.UserClassId IN (1, 2)
        GROUP BY Tmp.Id) AS UsageReport
    WHERE UsageReport.Usage = 2
) AS Used ON Used.Id = Unused.Id
WHERE Unused.IsDeleted = 0 AND Used.Id IS NULL

Запрос, вероятно, выглядит не очень хорошо, но я уже пытаюсь избегать конструкций, которые я не знаю, как преобразовать в Linq-To-Entities.

Я все еще не доволен запросом. Мне не нравится эта часть: WHERE UsageReport.Usage = 2, которая фильтрует внутренний выбор только для пользовательских объектов, которые используются обоими пользовательскими классами. Этот параметр должен быть динамическим и всегда представлять количество идентификаторов, переданных в предложение IN.

У вас есть идея, как лучше написать такой запрос?

Ответы [ 2 ]

1 голос
/ 10 февраля 2011

Еще один, который также использует COUNT ():

SELECT u.*
FROM UserObjects
  LEFT JOIN (
    SELECT UserObjectId
    FROM UserObjectRelations
    WHERE UserClassId IN (1, 2)
    GROUP BY UserObjectId
    HAVING COUNT(DISTINCT UserClassId) = 2
  ) r ON u.Id = r.UserObjectId
WHERE r.UserObjectId IS NULL
  AND u.IsDeleted = 0

Я использую COUNT (DISTINCT) здесь, но если есть уверенность, что дубликаты там невозможны, то COUNT (*), вероятно, будет лучше.

Однако, если вы действительно сильно против такого COUNT, я бы порекомендовал вам пересмотреть подход INTERSECT, но не так, как вы это показали.

Вот как бы я это использовал:

SELECT u.*
FROM UserObjects
  LEFT JOIN (
    SELECT UserObjectId FROM UserObjectRelations WHERE UserClassId = 1
    INTERSECT
    SELECT UserObjectId FROM UserObjectRelations WHERE UserClassId = 2
  ) r ON u.Id = r.UserObjectId
WHERE r.UserObjectId IS NULL
  AND u.IsDeleted = 0

Как видите, здесь нет СЧЕТА, и он не выглядит слишком тяжелым. Я полагаю, что таким образом вы можете включить много классов, и вам не нужно использовать круглые скобки там.

1 голос
/ 10 февраля 2011

Это работает?Тем не менее, он все еще использует счетчик списка.Я не уверен, есть ли способ обойти это без хранимой процедуры ...

SELECT o.* FROM UserObjects o
           LEFT JOIN UserObjectsRelations r ON o.id = r.UserObjectId
WHERE r.UserClassId IN (1,2) OR r.UserClassId IS NULL
GROUP BY o.id HAVING COUNT(o.id) < 2

Обновление: Извините, раньше не думал должным образом.Не уверен, что это лучший способ сделать это, но вы избавляетесь от количества идентификаторов в предложении IN (и я сделал это с MySQL, так что извините, если это не кошерно в TSQL).Вот что я придумал:

SELECT o.* FROM UserObjects o, 
                (SELECT o.id oid, c.id cid FROM UserObjects o, UserClasses c
                 WHERE c.id IN (1,2)
                ) sub
           LEFT JOIN UserObjectsRelations r ON sub.oid = r.UserObjectId AND
                                               sub.cid = r.UserClassId
WHERE o.id = sub.oid AND r.UserClassId IS NULL
GROUP BY o.id
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...