SQL дизайн подход для поиска в таблице с неограниченным количеством битовых полей - PullRequest
7 голосов
/ 01 октября 2009

Рассмотрите возможность поиска в таблице, содержащей информацию об аренде квартиры: клиент, использующий интерфейс, выбирает ряд критериев, которые представлены как битовые поля в БД, например:

  • ПозволяетПетс
  • HasParking
  • HasDeck
  • ModernKitchen

и т.д ..

Мы сталкиваемся с ситуацией, когда у каждого нового клиента нашего программного обеспечения есть дополнительные поля, в которых они хотят, чтобы их конечные пользователи могли выполнять поиск. Количество битовых полей может достигать сотен.

У меня есть три подхода, которые я рассматриваю и надеюсь на ввод и / или другой подход.

  • Текущий подход: добавить больше битовых полей, SQL-запросы строятся динамически и выполняются с использованием EXEC: SET @SQL = @SQL + 'l.[NumUnits],' exec(@SQL))

Продолжать добавлять дополнительные битовые поля. (таблица с 300 столбцами?)

  • Представлять данные в виде последовательности битов в одном поле. Мне неясно, сработает ли этот подход, рассмотрим 4 примера битовых полей, которые я предложил выше. Поле может выглядеть так: 1011, который будет означать ложь для «hasparking», но верно для всех остальных. Что мне неясно, так это то, как вы бы структурировали запрос, если вам было все равно, был ли он ложным или истинным, например, 1–11, где ищущему нужны значения 1,3 и 4, чтобы быть правдой, но не волнует, «HasParking» имеет значение true или false.

  • Перейдите к подходу на основе атрибутов, где у вас есть таблица AttributeTypeID и таблица PropertyAttributes, которая соединяет PropertyID с AttributeTypeId, новые битовые поля - это просто строки в таблице AttributeTypeID.

какой-то другой подход? Это хорошо известный шаблон проектирования SQL?

Спасибо за любую помощь

KM- РЕДАКТИРОВАТЬ В КОММЕНТАРИИ


attribute table has a few other rows in it and is called listingattributes

CREATE TABLE [dbo].[ListingAttributes](
    [ListingID] [bigint] NOT NULL,
    [AttributeID] [int] IDENTITY(1,1) NOT NULL,
    [AttributeType] [smallint] NOT NULL,
    [BoardID] [int] NOT NULL,
    [ListingMLS] [varchar](30) NOT NULL,
    [PropertyTypeID] [char](3) NOT NULL,
    [StatusID] [varchar](2) NOT NULL,
PRIMARY KEY CLUSTERED 
(
    [AttributeID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON, FILLFACTOR = 80) ON [PRIMARY]
) ON [PRIMARY]





;WITH GetMatchingAttributes AS
(
SELECT
    ListingID,COUNT(AttributeID) AS CountOfMatches
    FROM ListingAttributes

    WHERE 
    BoardID = 1
    AND
    StatusID IN ('A')
    AND
    --PropertyTypeID in (select * from @PropertyType)
    --AND
    AttributeType IN (2,3,6)
    GROUP BY ListingID
    HAVING COUNT(AttributeID)=(3)
)

SELECT 
    count(l.listingid)  
    FROM Listing l
        INNER JOIN GetMatchingAttributes m ON l.ListingID=m.ListingID
    --   where
    --   StatusID IN (select * from @Status)
    --AND
    --PropertyTypeID in (select * from @PropertyType)     1           1           0           NULL                           NULL                           1                                                                                                                                                                                                                                                                                                                                                                                                                                                                          NULL                                                                                                                                                                                                         1             NULL          NULL          NULL        0.1934759        NULL                                                                                                                                                                                                         NULL     SELECT                                                           0        NULL
  |--Compute Scalar(DEFINE:([Expr1006]=CONVERT_IMPLICIT(int,[Expr1012],0)))                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     1           2           1           Compute Scalar                 Compute Scalar                 DEFINE:([Expr1006]=CONVERT_IMPLICIT(int,[Expr1012],0))                                                                                                                                                                                                                                                                                                                                                                                                                     [Expr1006]=CONVERT_IMPLICIT(int,[Expr1012],0)                                                                                                                                                                1             0             0.001483165   11          0.1934759        [Expr1006]                                                                                                                                                                                                   NULL     PLAN_ROW                                                         0        1
       |--Stream Aggregate(DEFINE:([Expr1012]=Count(*)))                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        1           3           2           Stream Aggregate               Aggregate                      NULL                                                                                                                                                                                                                                                                                                                                                                                                                                                                       [Expr1012]=Count(*)                                                                                                                                                                                          1             0             0.001483165   11          0.1934759        [Expr1012]                                                                                                                                                                                                   NULL     PLAN_ROW                                                         0        1
            |--Filter(WHERE:([Expr1005]=(3)))                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   1           4           3           Filter                         Filter                         WHERE:([Expr1005]=(3))                                                                                                                                                                                                                                                                                                                                                                                                                                                     NULL                                                                                                                                                                                                         2471.109      0             0.00440886    9           0.1919928        NULL                                                                                                                                                                                                         NULL     PLAN_ROW                                                         0        1
                 |--Compute Scalar(DEFINE:([Expr1005]=CONVERT_IMPLICIT(int,[Expr1011],0)))                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      1           5           4           Compute Scalar                 Compute Scalar                 DEFINE:([Expr1005]=CONVERT_IMPLICIT(int,[Expr1011],0))                                                                                                                                                                                                                                                                                                                                                                                                                     [Expr1005]=CONVERT_IMPLICIT(int,[Expr1011],0)                                                                                                                                                                9185.126      0             0.01422281    11          0.1875839        [Expr1005]                                                                                                                                                                                                   NULL     PLAN_ROW                                                         0        1
                      |--Stream Aggregate(GROUP BY:(.[dbo].[ListingAttributes].[ListingID]) DEFINE:([Expr1011]=Count(*)))                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             1           6           5           Stream Aggregate               Aggregate                      GROUP BY:(.[dbo].[ListingAttributes].[ListingID])                                                                                                                                                                                                                                                                                                                                                                                                                [Expr1011]=Count(*)                                                                                                                                                                                          9185.126      0             0.01422281    11          0.1875839        [Expr1011]                                                                                                                                                                                                   NULL     PLAN_ROW                                                         0        1
                           |--Index Seek(OBJECT:(.[dbo].[ListingAttributes].[_dta_index_ListingAttributes_BoardID_ListingID__AttributeType_PropertyTypeID_StatusID_6_7]), SEEK:(.[dbo].[ListingAttributes].[BoardID]=(1)),  WHERE:(.[dbo].[ListingAttributes].[StatusID]='A' AND (.[dbo].[ListingAttributes].[AttributeType]=(2) OR .[dbo].[ListingAttributes].[AttributeType]=(3) OR .[dbo].[ListingAttributes].[AttributeType]=(6))) ORDERED FORWARD)                                                                                                                             1           7           6           Index Seek                     Index Seek                     OBJECT:(.[dbo].[ListingAttributes].[_dta_index_ListingAttributes_BoardID_ListingID__AttributeType_PropertyTypeID_StatusID_6_7]), SEEK:(.[dbo].[ListingAttributes].[BoardID]=(1)),  WHERE:(.[dbo].[ListingAttributes].[StatusID]='A' AND (.[dbo].[ListingAttributes].[AttributeType]=(2) OR .[dbo].[ListingAttributes].[AttributeType]=(3) OR .[dbo].[ListingAttributes].[AttributeType]=(6))) ORDERED FORWARD  .[dbo].[ListingAttributes].[ListingID], .[dbo].[ListingAttributes].[AttributeID], .[dbo].[ListingAttributes].[AttributeType], .[dbo].[ListingAttributes].[StatusID]  16050.41      0.09677318    0.0315279     26          0.1283011        .[dbo].[ListingAttributes].[ListingID], .[dbo].[ListingAttributes].[AttributeID], .[dbo].[ListingAttributes].[AttributeType], .[dbo].[ListingAttributes].[StatusID]  NULL     PLAN_ROW                                                         0        1

(7 row(s) affected)



Ответы [ 5 ]

9 голосов
/ 01 октября 2009

что-то вроде этого может работать для вас:

определить таблицы:

CREATE TABLE #Apartments
(
     ApartmentID    int          not null primary key identity(1,1)
    ,ApartmentName  varchar(500) not null
    ,Status         char(1)      not null default ('A') 
    --....
)

CREATE TABLE #AttributeTypes
(
    AttributeType         smallint     not null primary key
    ,AttributeDescription varchar(500) not null
)

CREATE TABLE #Attributes  --boolean attributes, if row exists apartment has this attribute 
(
     ApartmentID     int not null --FK to Apartments.ApartmentID    
    ,AttributeID     int not null primary key identity(1,1)
    ,AttributeType   smallint  not null --fk to AttributeTypes
)

вставить пример данных:

SET NO COUNT ON
INSERT INTO #Apartments VALUES ('one','A')
INSERT INTO #Apartments VALUES ('two','A')
INSERT INTO #Apartments VALUES ('three','I')
INSERT INTO #Apartments VALUES ('four','I')

INSERT INTO #AttributeTypes VALUES (1,'dishwasher')
INSERT INTO #AttributeTypes VALUES (2,'deck')
INSERT INTO #AttributeTypes VALUES (3,'pool')
INSERT INTO #AttributeTypes VALUES (4,'pets allowed')
INSERT INTO #AttributeTypes VALUES (5,'washer/dryer')
INSERT INTO #AttributeTypes VALUES (6,'Pets Alowed')
INSERT INTO #AttributeTypes VALUES (7,'No Pets')

INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (1,1)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (1,2)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (1,3)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (1,4)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (1,5)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (1,6)

INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (2,1)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (2,2)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (2,3)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (2,4)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (2,7)

INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (3,1)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (3,2)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (3,3)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (3,4)

INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (4,1)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (4,2)
SET NOCOUNT OFF

пример поискового запроса:

;WITH GetMatchingAttributes AS
(
SELECT
    ApartmentID,COUNT(AttributeID) AS CountOfMatches
    FROM #Attributes
    WHERE AttributeType IN (1,2,3)  --<<change dynamically or split a CSV string and join in
    GROUP BY ApartmentID
    HAVING COUNT(AttributeID)=3--<<change dynamically or split a CSV string and use COUNT(*) from resulting table
)
SELECT
    a.*
    FROM #Apartments                      a
        INNER JOIN GetMatchingAttributes m ON a.ApartmentID=m.ApartmentID
    WHERE a.Status='A'
    ORDER BY m.CountOfMatches DESC

ВЫВОД:

ApartmentID ApartmentName 
----------- --------------
1           one           
2           two           

(2 row(s) affected)

В приведенном выше поисковом запросе я просто включил CSV-строку идентификаторов атрибутов для поиска. В действительности вы можете создать хранимую процедуру поиска, в которой вы передаете параметр CSV, содержащий идентификаторы для поиска. Вы можете посмотреть этот ответ , чтобы узнать о разбиении этих циклов CSV без таблицы на таблицы, к которым вы можете присоединиться. Это привело бы к тому, что не нужно использовать какой-либо динамический SQL.

РЕДАКТИРОВАТЬ на основе множества комментариев:

если вы добавите несколько столбцов в таблицу #AttributeTypes, вы можете динамически построить страницу поиска. Вот несколько предложений:

  • Статус: "A" ctive "I" nactive
  • ListOrder: можно использовать это для сортировки, чтобы построить экран
  • ColumnNumber: может помочь организовать поля в одной строке экрана
  • AttributeGroupID: чтобы сгруппировать поля, см. Ниже
  • и т.д..

Вы можете установить флажки для всех полей или добавить другую таблицу с именем #AttributesGroups, сгруппировать их вместе и использовать переключатели. Например, поскольку «Домашние животные разрешены» и «Домашние животные не разрешены» являются исключительными, добавьте строку в таблицу #AttributesGroups «Домашние животные». Приложение будет группировать атрибуты в интерфейсе. Атрибуты в группах будут работать так же, как обычные разгруппированные атрибуты, просто соберите выбранные идентификаторы и передайте их в процедуру поиска. Однако для каждой группы необходимо, чтобы приложение включало переключатель «без предпочтений» и включало его по умолчанию. Эта опция не будет иметь идентификатора атрибута и не будет передана, так как вы не хотите рассматривать атрибут.

В моем примере я показываю пример "супер атрибута", который находится в #Apartments стол "Статус". Вы должны учитывать только основные атрибуты этой таблицы. Если вы начнете использовать их, вы можете изменить CTE на FROM #Apartments с фильтрацией по этим полям, а затем присоединиться к #Attributes. Однако вы столкнетесь с проблемами Условия динамического поиска, поэтому прочитайте эту статью Эрланда Соммарскога .

РЕДАКТИРОВАТЬ в последних комментариях:

вот код для списка исключаемых атрибутов:

;WITH GetMatchingAttributes AS
(
SELECT
    ApartmentID,COUNT(AttributeID) AS CountOfMatches
    FROM #Attributes
    WHERE AttributeType IN (1,2,3)  --<<change dynamically or split an include CSV string and join in
    GROUP BY ApartmentID
    HAVING COUNT(AttributeID)=3--<<change dynamically or split a CSV string and use COUNT(*) from resulting include table
)
, SomeRemoved AS
(
SELECT
    m.ApartmentID
    FROM GetMatchingAttributes      m
        LEFT OUTER JOIN #Attributes a ON m.ApartmentID=a.ApartmentID 
            AND a.AttributeType IN (5,6)   --<<change dynamically or split an exclude CSV string and join in
    WHERE a.ApartmentID IS NULL
)
SELECT
    a.*
    FROM #Apartments           a
        INNER JOIN SomeRemoved m ON a.ApartmentID=m.ApartmentID
    WHERE a.Status='A'

Я не думаю, что пошел бы по этому пути, хотя. Я бы пошел с подходом, который я изложил в моем предыдущем EDIT выше. Когда необходимо включить / исключить атрибут, я бы просто добавил атрибут для каждого: «Домашние животные разрешены» и «Домашние животные не разрешены».

Я обновил пример данных из исходного поста, чтобы показать это.

Запустите исходный запрос с помощью:

  • (.., .., 6, ..), чтобы найти квартиры, в которых разрешено проживание с домашними животными
  • (.., .., 7, ..) для поиска квартир, в которых домашние животные не допускаются
  • (.., .., ..), если нет предпочтений.

Я думаю, что это лучший подход. В сочетании с идеей группировки и динамически создаваемой страницей поиска, описанной в последнем редакторе, я думаю, что это будет лучше и будет работать быстрее.

2 голосов
/ 01 октября 2009

Я предлагаю вам воспользоваться вторым подходом, известным как Модель значения атрибута объекта . Вероятно, это единственный подход, который будет масштабироваться по мере необходимости.

Вы также можете выполнить два поиска: основной и расширенный. Вы сохраняете атрибуты для основного поиска в одной таблице, а все расширенные атрибуты - в другой таблице. Таким образом, по крайней мере, основной поиск останется быстрым, так как количество атрибутов будет расти со временем.

0 голосов
/ 01 октября 2009

Я несколько раз шел по этому пути, пытаясь сохранить маркеры состояния здоровья!

Когда я впервые начал (в 2000 году?), Я попробовал подход с позицией персонажа (ваш # 2) и обнаружил, что он быстро стал довольно громоздким, когда я снова и снова боролся с одними и теми же вопросами: «какая позиция занимала« Позволяет домашних животных » снова?" или, что еще хуже, "как долго эта строка сейчас? / на какой позиции я нахожусь?" Можете ли вы обойти эту проблему - разрабатывать объекты для управления вещами для вас? Ну да, в некоторой степени. Но я действительно не оценил, сколько дополнительной работы это стоило по сравнению с полевыми идентификаторами , управляемыми для меня базой данных.

Во второй раз я использовал подход пары атрибут / значение, аналогичный вашему решению №3. Это в основном сработало, и для специальных нужд я все еще генерирую пары атрибут / значение, используя PIVOT. Кроме того, мой опыт в искусственном интеллекте, и мы все время использовали пары атрибут / значение в механическом доказательстве теорем, поэтому для меня это было очень естественно.

Тем не менее, существует огромная проблема с этим подходом: вытащить любой один факт («Покажите мне квартиры, в которых разрешено проживание с домашними животными») легко, но вытащить все записи, отвечающие множественным ограничениям, быстро становится очень очень некрасиво (см. мой пример ниже).

** ТАК ... ** Я закончил тем, что добавил поля в таблицу. Я понимаю теоретические причины, по которым Джон, «Неизвестно» и «Новый город» предпочитают другие подходы, и я бы согласился с одним или обоими в один момент. Но опыт довольно суровый учитель ...

Еще пара вещей

Во-первых, я не согласен с тем, что добавление большего количества битовых полей является кошмаром обслуживания - по крайней мере по сравнению с символьно-битным подходом (ваш № 2). То есть наличие отдельного поля для каждого атрибута гарантирует, что не существует «управления», необходимого для определения, какой слот принадлежит какому атрибуту.

Во-вторых, наличие 300 полей не является проблемой - любая приличная база данных может сделать это без проблем.

В-третьих, ваша реальная проблема и источник боли - это действительно вопрос динамической генерации ваших запросов. Если вы похожи на меня, то этот вопрос на самом деле весь о том, «Нужно ли мне иметь эту массивную, грязную и неэлегатную цепочку операторов« IF »для построения запроса?»

Ответ, к сожалению, Да. Все три подхода, которые вы предлагаете, все равно будут сводиться к цепочке утверждений IF.

При подходе к битовым полям базы данных вы получите последовательность операторов IF, в которую должны быть добавлены все ваши столбцы, например:

string SQL = "Select X,Y,Z Where ";

if (AllowsPets == 0)
  SQL += "(AllowsPets = 0) AND ";
else if (AllowsPets == )
  SQL += "(AllowsPets = 1) AND ";  // Else AllowsPets not in query
.
.
.
SQL = SQL.Substring(SQL.Length - 4);  // Get rid of trailing 'AND' / alternatively append '(1=1)'

При подходе с позицией символов вы будете делать то же самое, но ваши «добавления» добавят «0», «1» или «_» к вашему SQL. Вы также, конечно, столкнетесь с проблемами обслуживания, решая, какую из них я обсуждал выше (перечисления помогают, но не полностью решают проблему).

Как упоминалось выше, подход Attribute-Value на самом деле является худшим. Вам придется либо создать неприятную цепочку подзапросов (что, несомненно, приведет к переполнению стека с 300 предложениями), либо вам нужно иметь IF-THEN, например:

// Kill any previously stored selections.
SQLObject.Execute("Delete From SelectedApts Where SessionKey=X");
// Start with your first *known* attr/value and fill a table with the results.
.
.
Logic to pick first known attr/value pair
.
.
SQLObject.Execute("Insert Into SelectedApts Select X as SessionKey, AptID From AttrValue Where AllowsPets=1");

// Now you have the widest set that meets your criteria. Time to whittle it down.
if (HasParking == 1)
  SQLObject.Execute("Delete From SelectedApts Where AptID not in (Select AptID From AttrValue Where AllowsChildren=1));
if (AllowsChildren == 0)
  SQLObject.Execute("Delete From SelectedApts Where AptID not in (Select AptID From AttrValue Where AllowsChildren=0));
.
.
.
// Perform 2-300 more queries to keep whittling down your set to the actual match.

Теперь вы можете немного оптимизировать этот процесс, чтобы выполнить меньше запросов (PIVOT, наборы подзапросов или использовать оператор UNION), но факт в том, что это становится ОЧЕНЬ дорого по сравнению с единственным запросом, который вы можете использовать (но надо строить) используя другие подходы.

Таким образом, это болезненная проблема, независимо от того, какой подход вы используете - на самом деле нет магии, которая поможет вам избежать ее. Но, побывав там раньше, я бы абсолютно рекомендовал подход № 1.

Обновление: Если вы действительно сосредоточены на получении прямых критериев соответствия («Все квартиры, в которых есть A, B и C») и не нуждаетесь в других запросах (например, «... Sum (AllowsPets) ), Sum (AllowsChildren) ... "или" ... (AllowsPets = 1) ИЛИ (AllowsChildren = 1) ... ") тогда мне действительно нравится ответ КМ, чем больше я на него смотрю. Это очень умно и выглядит довольно быстро.

0 голосов
/ 01 октября 2009

Я никогда не проверял это, но что если вы создадите поля varchar (256), в которых все ваши флаги хранятся в виде одной длинной строки из 0 и 1.

Например,

  • AllowsPets = 1
  • HasParking = 0
  • HasDeck = 1
  • ModernKitchen = 1

будет:

  • PropertyFlags = 1011

и если вы ищете что-то, что позволяет AllowsPets и HasDeck, то поисковый запрос будет выглядеть примерно так:

WHERE PropertyFlags LIKE '1_1_' (подчеркивание представляет один символ подстановки в предложении like)

это решит ваши проблемы с добавлением дополнительных столбцов к поиску в будущем, но я не уверен, как это повлияет на производительность.

кто-нибудь пробовал что-нибудь подобное?

0 голосов
/ 01 октября 2009

Создайте таблицу, в которой хранятся атрибуты или столбцы поиска в зависимости от квартиры. Определенно не продолжайте добавлять больше столбцов битовых полей .. обслуживание кошмара и кошмарное кодирование. И, конечно, не надо, пожалуйста, не генерируйте динамически оператор where и не используйте exec.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...