Динамически генерировать критерии в SQL - PullRequest
4 голосов
/ 12 октября 2011

У меня есть таблица «Пользователи», которая содержит десятки столбцов, таких как дата рождения, год автомобиля, марка и модель автомобиля, цвет и многие другие личные поля, не связанные с автомобилем.

Есть также 2-ойтаблица под названием «Купоны», которая должна быть разработана таким образом, чтобы поддерживать квалификацию, например «пользователь соответствует требованиям, если возраст менее 30 лет», «пользователь соответствует требованиям, если автомобиль старше 10 лет», «пользователь соответствует требованиям, если цвет транспортного средства зеленый».

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

На данный момент мое единственное решение - сохранить фактическиеСтрока sql в одном из столбцов таблицы купонов, например

select * from Users where UserId = SOME_PLACEHOLDER and VehicleYear < 10

Тогда я мог бы выполнить sql для каждой строки купона и вернуть true или false.Кажется, очень неэффективно, так как мне, возможно, придется выполнять 1000 SQL-выражений для каждого кода купона.

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

Спасибо.

Ответы [ 4 ]

1 голос
/ 13 октября 2011

Очень сложная проблема.Похоже, что пользователи будут добавляться с большой скоростью, с купонами с довольно регулярной частотой.

Добавление SQL в таблицу для динамического использования - работоспособно - по крайней мере, вы получите новый план выполнения - НО вашкэш плана может увеличиться.

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

В любом случае, дело касается всех купонов для всех пользователей (или в любом случае, действительно) будет выглядеть примерно так:

SELECT user.id, coupon.id
FROM user
INNER JOIN coupon
ON (
    CASE WHEN <coupon.criteria> THEN <coupon.id> -- code generated from the coupon rules table
    CASE WHEN <coupon.criteria> THEN <coupon.id> -- etc.
    ELSE NULL
) = coupon.id

Чтобы сгенерировать правила купона, вы можете относительно легко выполнить объединение строк одним движением (и вы можете объединить дизайн отдельных строк правил для купона сИ с дополнительным внутренним шаблоном):

DECLARE @outer_template AS varchar(max) = 'SELECT user.id, coupon.id
    FROM user
    INNER JOIN coupon
    ON (
        {template}
        ELSE NULL
    ) = coupon.id
';

DECLARE @template AS varchar(max) = 'CASE WHEN {coupon.rule} THEN {coupon.id}{crlf}';

DECLARE @coupon AS TABLE (id INT, [rule] varchar(max));
INSERT INTO @coupon VALUES
    (1, 'user.Age BETWEEN 20 AND 29')
    ,(2, 'user.Color = ''Yellow''');


DECLARE @sql AS varchar(MAX) = REPLACE(
           @outer_template
           ,'{template}',
REPLACE((
SELECT REPLACE(REPLACE(
           @template
           ,'{coupon.rule}', coupon.[rule])
           , '{coupon.id}', coupon.id)
FROM @coupon AS coupon
FOR XML PATH('')
), '{crlf}', CHAR(13) + CHAR(10)));

PRINT @sql;
// EXEC (@sql);

Есть способы сделать это - поиграйте с этим здесь: http://data.stackexchange.com/stackoverflow/q/115098/

Я бы рассмотрел добавление вычисляемых столбцов (возможно, сохранившихся и проиндексированных)помогать.Например, вычисляемый столбец age - non-persisted, вероятно, будет работать лучше, чем скалярная функция.

Я бы посоветовал объединить это с таблицей, в которой указано, действителен ли купон для пользователя и когда он был последний раз подтвержден.

Похоже, возраст может измениться, и пользователь может стать действительным или недействительным.для купона в день рождения.

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

Всего несколько идей.

Я бы тожедобавьте, что у вас должен быть способ протестировать купон до его утверждения, чтобы убедиться в отсутствии синтаксических ошибок (поскольку SQL является произвольным или произвольным) - это можно сделать относительно легко - возможно, с помощью тестовой пользовательской таблицы (test_user в качестве пользователя ввместо этого необходимо, чтобы сгенерированный шаблон кода) содержал строки «годен» и «нет», и правило купона указывает на них.Мало того, что EXEC должен работать - возвращаемые строки должны быть ожидаемыми и только ожидаемыми строками для этого купона.

0 голосов
/ 12 октября 2011

Моей первой мыслью о подходе (аналогичном подходу Хогана) будет проверка применимости купона во время его создания.Сохраните эти результаты в таблице (например, User_Coupons).Если какие-либо пользовательские данные будут изменены, ваша система затем проведет повторную проверку любых измененных пользователей, для которых к ним применимы купоны.Во время создания (или изменения) купона он будет сравниваться только с этим купоном.Во время создания (или изменения) использования он будет проверяться только против этого пользователя.

Критерии купона должны быть из известного набора возможных критериев и в любое время, когда вы хотите добавить новый тип * 1005.* критериев, это может повлечь за собой изменение кода.Например, предположим, что у вас есть таблица, настроенная примерно так:

CREATE TABLE Coupon_Criteria (
    coupon_id        INT         NOT NULL,
    age_minimum      SMALLINT    NULL,
    age_maximum      SMALLINT    NULL,
    vehicle_color    VARCHAR(20) NULL,
    ...
    CONSTRAINT PK_Coupon_Criteria PRIMARY KEY CLUSTERED (coupon_id)
)

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

Пример запроса для приведенной выше таблицы:

SELECT
    CC.coupon_id
FROM
    Users U
INNER JOIN Coupon_Criteria CC ON
    (CC.age_maximum IS NULL OR dbo.f_GetAge(U.birthday) <= age_maximum) AND
    (CC.age_minimum IS NULL OR dbo.f_GetAge(U.birthday) >= age_minimum) AND
    (CC.vehicle_color IS NULL OR U.vehicle_color = CC.vehicle_color) AND
    ...

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

Другой возможностью было бы сохранение критериев купона в XML и использование бизнес-объекта для вашего приложения, чтобы определить приемлемость.Он может использовать XML для генерации правильного запроса к таблице User (и любым другим необходимым таблицам).

0 голосов
/ 12 октября 2011

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

CREATE TABLE Coupons (
    coupon_id    INT              NOT NULL,
    description  VARCHAR(2000)    NOT NULL,
    ...
    CONSTRAINT PK_Coupons PRIMARY KEY CLUSTERED (coupon_id)
)

CREATE TABLE Coupon_Criteria (
    coupon_id        INT             NOT NULL,
    criteria_num     SMALLINT        NOT NULL,
    description      VARCHAR(50)     NOT NULL,
    code_template    VARCHAR(500)    NOT NULL,
    CONSTRAINT PK_Coupon_Criteria PRIMARY KEY CLUSTERED (coupon_id, criteria_num),
    CONSTRAINT FK_Coupon_Criteria_Coupon FOREIGN KEY (coupon_id) REFERENCES Coupons (coupon_id)
)

INSERT INTO Coupons (coupon_id, description)
VALUES (1, 'Young people save $200 on yellow vehicles!')

INSERT INTO Coupon_Criteria (coupon_id, criteria_num, description, code_template)
VALUES (1, 1, 'Young people', 'dbo.Get_Age(U.birthday) <= 20')
INSERT INTO Coupon_Criteria (coupon_id, criteria_num, description, code_template)
VALUES (1, 2, 'Yellow Vehicles', U.vehicle_color = ''Yellow''')

Затем вы можете построить запрос, просто объединив все критерии для любого заданного купона.Недостатком big является то, что он только однонаправленный.Получив купон, вы легко можете найти, кто на него претендует, но, учитывая пользователя, вы не сможете найти все купоны, на которые они имеют право, кроме как, пройдя все купоны.Я предполагаю, что второе - это то, что вас, вероятно, больше всего интересует, к сожалению.Возможно, это даст вам некоторые другие идеи.

Например, вы могли бы потенциально заставить его работать по-другому, если бы в таблице было задано определенное количество критериев, а в таблице связей купонов / критериев указывалось, стоит лиэтот критерий активен.При запросе вы можете включить это в свой запрос.Другими словами, запрос будет выглядеть примерно так:

WHERE
    (CC.is_active = 0 OR <code from the code column>) AND

Запрос становится очень сложным, поскольку вам нужно либо присоединиться один раз для каждого возможного критерия, либо выполнить запрос, чтобы сравнить количество активных требований длякупон против числа, которое выполнено.Это возможно в SQL, но это похоже на работу с моделью EAV - что, в сущности, превращает это в: вариацию модели EAV (гадость)

0 голосов
/ 12 октября 2011

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

  1. Ограничьте тип критериев, по которым вы будете фильтровать, чтобы вы могли использовать динамический или нединамический sql для их эффективного выполнения. Например, если в качестве критерия у вас будут только целые числа между диапазоном минимальных и максимальных значений, проблема станет проще. (Вам нужно знать только имя поля и минимальные максимальные значения, чтобы описать критерий, а не полный оператор where.)

  2. Создайте несколько представлений, которые помогают раскрыть атрибуты. Затем выполните запросы к этим представлениям - или предварительно выберите эти представления каким-либо образом. Например, представление возрастной группы, имеющее поле, которое может содержать значения < 21, 21-30, 30-45, >45. Тогда вашему выбору нужно просто вернуть строки из этого представления, которые соответствуют этим строкам.

  3. Создайте таблицу, в которой будут храниться результаты выполнения вашего запроса соответствия критериев (это может быть запущено автономно с помощью фонового процесса). Затем для данного пользователя проверьте членство, посмотрев, где в таблице существует идентификатор этого пользователя.

Думая об этом еще немного, я понимаю, что все мои предложения основаны на одной идее.

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

Надеюсь, это поможет - прокомментируйте, если эти идеи не ясны.

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