Как мне перевести простое логическое выражение в SQL? - PullRequest
6 голосов
/ 09 июля 2010

У меня есть следующая таблица базы данных с информацией о людях, болезнях и наркотиках:

PERSON_T              DISEASE_T               DRUG_T
=========             ==========              ========
PERSON_ID             DISEASE_ID              DRUG_ID
GENDER                PERSON_ID               PERSON_ID
NAME                  DISEASE_START_DATE      DRUG_START_DATE
                      DISEASE_END_DATE        DRUG_END_DATE

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

( (Drug 234 = false AND Drug 474 = true AND Drug 26 = false) OR 
  (Drug 395 = false AND Drug 791 = false AND Drug 371 = true) )

Редактировать: Вот еще один пример:

( (Drug 234 = true AND Drug 474 = true AND Drug 26 = false) OR 
      (Drug 395 = false AND Drug 791 = false AND Drug 371 = true) )

Теперь я хочу преобразовать этот шаблон в SQL-запрос и найти всех людей, которые соответствуют этому шаблону.
Например, я хочу найти всех людей в PERSON_T, у которых было заболевание и ((которые не принимали наркотик 234и 26 до проявления симптомов, но принимал наркотик 474 до проявления симптомов) или (который принимал наркотик 371 до проявления симптомов, но не наркотик 791 и 395 до проявления симптомов))

Как мне поступить с переводом этого паттернаобратно в исходный запрос?

Вот моя первая попытка, но я застрял на первом члене:

SELECT * FROM PERSON_T, DRUG_T, DISEASE_T 
  WHERE DISEASE_ID = 52 AND 
    PERSON_T.PERSON_ID = DISEASE_T.PERSON_ID AND 
    PERSON_T.PERSON_ID = DRUG_T.PERSON_ID  AND 
    (DRUG_T.DRUG_ID=234 AND (DRUG_T.DRUG_START_DATE>DISEASE_T.END_DATE || ???)

Мне нужно это для работы в PostgreSql, но я предполагаю, что любой ответможет быть переведен из данной базы данных в PostgreSql.

Ответ на комментарии

  1. Я исправил форматирование таблиц базы данных.Спасибо.
  2. Мне нужно иметь возможность принять произвольное логическое выражение и перевести его на SQL.Логические выражения, которые мы на самом деле создаем, намного длиннее, чем пример, который я привел.Любые новые таблицы, которые я создаю, будут в новой базе данных и должны иметь ту же схему, что и исходные таблицы.Таким образом, конечный пользователь может запустить один и тот же код в новых таблицах, и он будет работать так же, как если бы он работал в исходных таблицах.Это требование от клиента.Я надеюсь, что смогу создать представление, которое является просто запросом к исходным таблицам.Если мы не можем заставить это работать, я могу создать копию таблиц и отфильтровать данные, поскольку я копирую их в новую таблицу.Мы не используем нейронные сети для анализа.Мы используем наши собственные пользовательские алгоритмы, которые масштабируются намного лучше, чем нейронные сети.
  3. Disease_Start_Date - это дата, когда у человека возникают заболевания, которые вероятны, когда начинают появляться симптомы.Disease_End_Date - это когда человек выздоравливает, что, вероятно, происходит, когда симптомы исчезают.
  4. Drug_start_date - это когда человек начинает принимать наркотики.Drug_end_date - это когда человек прекращает принимать наркотики.

Редактировать Я добавил свой собственный ответ.Кто-нибудь может придумать более простой ответ?

Ответы [ 10 ]

4 голосов
/ 09 июля 2010

Для меня простое (если некрасивое) решение - использовать предложения EXISTS, а NOT EXISTS:

SELECT *
FROM PERSON_T INNER JOIN DISEASE_T
     USING (PERSON_ID)
WHERE DISEASE_ID = 52
  AND EXISTS (SELECT 1 FROM DRUG_T
              WHERE DRUG_T.PERSON_ID = PERSON_T.PERSON_ID
                AND DRUG_ID = 474
                AND [time condition])
  AND NOT EXISTS (SELECT 1 FROM DRUG_T
              WHERE DRUG_T.PERSON_ID = PERSON_T.PERSON_ID
                AND DRUG_ID = 234
                AND [time condition])

... и так далее.В этом примере мы просим людей, которые принимали наркотики 474, но не 234. Очевидно, что вы можете сгруппировать предложения с AND и OR в соответствии с вашими потребностями.

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

1 голос
/ 23 июля 2010

Вот запрос, который обрабатывает ( (Drug 234 = true AND Drug 474 = true AND Drug 26 = false) OR (Drug 395 = false AND Drug 791 = false AND Drug 371 = true) ), как вы отправили.

/*
-- AS DEFINED BY JOINS
-- All "person_id"'s match
-- Drug 1 is not Drug 2
-- Drug 1 is not Drug 3
-- Drug 2 is not Drug 3
-- All Drugs are optional as far as the SELECT statement is concerned (left join)
   -- Drug IDs will be defined in the WHERE clause
-- All Diseases for "person_id"

-- AS DEFINED IN WHERE STATEMENT
-- Disease IS 52
-- AND ONE OF THE FOLLOWING:
--   1) Disease started AFTER Drug 1
--      Disease started AFTER Drug 2
--      Drug 1 IS 234
--      Drug 2 IS 474
--      Drug 3 IS NOT 26 (AND NOT 234 or 474, as defined in JOINs)
--   2) Disease started AFTER Drug 3
--      Drug 1 IS NOT 395
--      Drug 2 IS NOT 791
--      Drug 3 IS 371
*/

SELECT p.person_id, p.gender FROM person_t as p
LEFT JOIN drug_t    AS dr1 ON (p.person_id = dr1.person_id)
LEFT JOIN drug_t    AS dr2 ON (p.person_id = dr2.person_id AND dr1.drug_id != dr2.drug_id)
LEFT JOIN drug_t    AS dr3 ON (p.person_id = dr3.person_id AND dr1.drug_id != dr3.drug_id AND dr2.drug_id != dr3.drug_id)
JOIN      disease_t AS ds  ON (p.person_id = ds.person_id)
WHERE ds.disease_id = 52
AND (   (    (dr1.drug_start_date < ds.disease_start_date AND dr2.drug_start_date < ds.disease_start_date)
        AND (dr1.drug_id = 234 AND dr2.drug_id = 474 AND dr3.drug_id != 26)
        )
    OR
        (    (dr3.drug_start_date < ds.disease_start_date)
        AND (dr1.drug_id != 395 AND dr2.drug_id != 791 AND dr3.drug_id = 371)
        )
    )
1 голос
/ 23 июля 2010

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

Пример 1:

SELECT dis.*
FROM disease_t dis
LEFT JOIN drug d1 ON d1.person_id = dis.person_id AND d1.drug_id = 234
LEFT JOIN drug d2 ON d2.person_id = dis.person_id AND d2.drug_id = 474
LEFT JOIN drug d3 ON d3.person_id = dis.person_id AND d3.drug_id = 26
LEFT JOIN drug d4 ON d4.person_id = dis.person_id AND d4.drug_id = 395
LEFT JOIN drug d5 ON d5.person_id = dis.person_id AND d5.drug_id = 791
LEFT JOIN drug d6 ON d6.person_id = dis.person_id AND d6.drug_id = 371
WHERE dis.disease_id = 52
AND (((d1.person_id IS NULL OR dis.startdate < d1.startdate) AND
      (d2.person_id IS NOT NULL AND d2.startdate < dis.startdate) AND
      (d3.person_id IS NULL OR dis.startdate < d3.startdate)) 
     OR
     ((d4.person_id IS NULL OR dis.startdate < d4.startdate) AND
      (d5.person_id IS NULL OR dis.startdate < d5.startdate) AND
      (d6.person_id IS NOT NULL AND d6.startdate < dis.startdate)))

Пример 2:

SELECT dis.*
FROM disease_t dis
LEFT JOIN drug d1 ON d1.person_id = dis.person_id AND d1.drug_id = 234
LEFT JOIN drug d2 ON d2.person_id = dis.person_id AND d2.drug_id = 474
LEFT JOIN drug d3 ON d3.person_id = dis.person_id AND d3.drug_id = 26
LEFT JOIN drug d4 ON d4.person_id = dis.person_id AND d4.drug_id = 395
LEFT JOIN drug d5 ON d5.person_id = dis.person_id AND d5.drug_id = 791
LEFT JOIN drug d6 ON d6.person_id = dis.person_id AND d6.drug_id = 371
WHERE dis.disease_id = 52
AND (((d1.person_id IS NOT NULL AND d1.startdate < dis.startdate) AND
      (d2.person_id IS NOT NULL AND d2.startdate < dis.startdate) AND
      (d3.person_id IS NULL OR dis.startdate < d3.startdate)) 
     or
     ((d4.person_id IS NULL OR dis.startdate < d4.startdate) AND
      (d5.person_id IS NULL OR dis.startdate < d5.startdate) AND
      (d6.person_id IS NOT NULL AND d6.startdate < dis.startdate)))
0 голосов
/ 24 июля 2010

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

Во-первых, можно представить три таблицы (Person_T, Drugs_T, Disease_T), как показано на рисунке 1.0:

Человек может иметь несколько лекарств и несколько болезней .Каждое лекарство и заболевание имеют дату начала и дату окончания.

Поэтому сначала я бы нормализовал три таблицы в одну таблицу (Table_dn), таким образом:

dnId | PersonId | DrugId | DiseaseId | DgSt | DgEn | DiSt | DiEn
----   --------   ------   ---------   ----   ----   ----   ----

Эта нормализованная таблицаможет быть временной таблицей, если необходимо, независимо от того, что Table_dn теперь содержит весь глобальный набор данных, как показано на рис. 2.0 (обозначено как G).

Из моего понимания вашего описания я могу видеть, по сути, двухслойный фильтр.

Фильтр 1

Этот фильтр является просто булевым набором лекарств Комбинации , как вы уже указали в описании вопроса.Например:

(drug a = 1 & drug b = 0 & etc) OR (.....

Фильтр 2

Этот фильтр немного сложнее, чем первый, это критерии диапазона дат.Рис. 3.0 показывает этот диапазон дат в RED .Желтый представляет даты записи, которые охватывают несколько способов:

  • до периода RED
  • После периода RED
  • Между периодом RED
  • Конец до концаКРАСНОГО периода
  • Начиная с начала КРАСНОГО периода

Теперь ЖЕЛТЫМИ периодами даты могут быть период лекарств или период болезни ИЛИ комбинация обоих.

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

Конечно, в зависимости от вашего точного вопроса, эти два фильтра могут нуждаться в обратном направлении (например, сначала f2, а затем f1).

SQL псевдокод:

Select sub.*
From    
      (select    * 
       from      Table_dn 
       where     [Filter 1]
      ) as sub

where [Filter 2]

alt text

0 голосов
/ 24 июля 2010

Если я правильно понял, вы хотите:

  • Выберите этих людей
  • Кто был инфицирован одним (1) конкретным заболеванием
  • Кто лечился одним или несколькими указанными препаратами
  • А кто НЕ лечился одним или несколькими указанными другими препаратами

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

Разбивая шаги:

Сначала , вот как выбираются нужные пациенты. Мы будем использовать это как подзапрос позже:

SELECT [PersonData]
 from DISEASE_T di
  inner join PERSON_T pe
   on pe.Person_Id = di.Person_Id
 where di.Disease_Id = [TargetDisease]
  and [TimeConstraints]

Секунда , для каждого набора «целевых» препаратов, которые вы вместе AND, создали временную таблицу следующим образом (это синтаксис SQL Server, у Postgres должно быть что-то похожее):

CREATE TABLE #DrugSet
 (
   Drug_Id  [KeyDataType]
  ,Include  int   not null
 )

Заполните его одной строкой для каждого рассматриваемого вами препарата:

  • Drug_Id = препарат, который вы проверяете
  • Включите = 1, если человек принимал препарат, и 0, если он не принимал его

и вычислим два значения:

@ GoodDrugs, количество лекарств, которые вы хотите, чтобы пациент принял
@BadDrugs, количество лекарств, которые вы хотите, чтобы пациент не принимал

Теперь сошиваем все вышеперечисленное вместе в следующем запросе:

SELECT pe.[PersonData]  --  All the desired columns from PERSON_T and elsewhere
 from DRUG_T dr
  --  Filter to only include "persons of interest"
  inner join (select [PersonData]
               from DISEASE_T di
                inner join PERSON_T pe
                 on pe.Person_Id = di.Person_Id
               where di.Disease_Id = [TargetDisease]
                and [TimeConstraints]) pe
   on pe.Person_Id = dr.Person_ID
 --  Join with any of the drugs we are intersted in
 left outer join #DrugSet ta  
  on ta.Drug_Id = dr.Drug_Id
 group by pe.[PersonData]  --  Same as in the SELECT clause
 having sum(case ta.Include
              when 1 then 1  --  This patient has been given a drug that we're looking to match
              else 0         --  This patient has not been given this drug (catches NULLs, too)
            end) = @GoodDrugs
  and  sum(case ta.Include
              when 0 then 1  --  This patient has been given this drug that we're NOT looking to match
              else 0         --  This patient has not been given this drug (catches NULLs, too)
            end) = @BadDrugs

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

Вам нужно будет выполнить это один раз для каждого «набора лекарств» (то есть наборов ИСТИННЫХ или ЛОЖНЫХ лекарств И вместе взятых), объединяя список с каждым проходом. Возможно, вы могли бы расширить #DrugSet, чтобы учитывать каждый набор препаратов, который вы проверяете, но я не хочу пытаться код, который без каких-либо серьезных данных для проверки.

* /

0 голосов
/ 24 июля 2010

Я, вероятно, подхожу к этой проблеме с некоторого направления, подобного этому.Это довольно гибкий.

DRUG_DISEASE_CORRELATION_QUERY
===============================
DRUG_DISEASE_CORRELATION_QUERY_ID
DISEASE_ID
DESCRIPTION

(1, 52, 'What this query does.')
(2, 52, 'Add some more results.')

DRUG_DISEASE_CORRELATION_QUERY_INCLUDE_DRUG
===========================================
DRUG_DISEASE_CORRELATION_QUERY_ID
DRUG_ID

(1, 234)
(1, 474)
(2, 371)

DRUG_DISEASE_CORRELATION_QUERY_EXCLUDE_DRUG
===========================================
DRUG_DISEASE_CORRELATION_QUERY_ID
DRUG_ID

(1, 26)
(2, 395)
(2, 791)



CREATE VIEW DRUG_DISEASE_CORRELATION
AS
SELECT 
    p.*,
    q.DRUG_DISEASE_CORRELATION_QUERY_ID
FROM 
    DRUG_DISEASE_CORRELATION_QUERY q
    INNER JOIN DISEASE_T ds on ds.DISEASE_ID = q.DISEASE_ID
    INNER JOIN PERSON_T p ON p.PERSON_ID = ds.PERSON_ID
  WHERE 
    AND EXISTS (SELECT * FROM DRUG_T dr WHERE dr.PERSON_ID = p.PERSON_ID AND dr.DRUG_ID IN
        (SELECT qid.DRUG_ID FROM DRUG_DISEASE_CORRELATION_QUERY_INCLUDE_DRUG qid WHERE 
        qid.DRUG_DISEASE_CORRELATION_QUERY_ID = q.DRUG_DISEASE_CORRELATION_QUERY_ID)
        AND DRUG_START_DATE < ds.DISEASE_START_DATE)
   AND NOT EXISTS (SELECT * FROM DRUG_T dr WHERE dr.PERSON_ID = p.PERSON_ID AND dr.DRUG_ID IN
        (SELECT qed.DRUG_ID FROM DRUG_DISEASE_CORRELATION_QUERY_EXCLUDE_DRUG qed WHERE 
        qed.DRUG_DISEASE_CORRELATION_QUERY_ID = q.DRUG_DISEASE_CORRELATION_QUERY_ID)
        AND DRUG_START_DATE < ds.DISEASE_START_DATE)
GO


SELECT * FROM DRUG_DISEASE_CORRELATION WHERE DRUG_DISEASE_CORRELATION_QUERY_ID = 1
UNION
SELECT * FROM DRUG_DISEASE_CORRELATION WHERE DRUG_DISEASE_CORRELATION_QUERY_ID = 2
0 голосов
/ 24 июля 2010

У меня нет действительно удобных тестовых данных, чтобы попробовать это, но я думаю, вы могли бы сделать что-то вроде:

SELECT *
FROM DISEASE_T D
INNER JOIN DRUG_T DR ON D.PERSON_ID = DR.PERSON_ID AND D.DRUG_ID=52
INNER JOIN PERSON_T P ON P.PERSON_ID = D.PERSON_ID
GROUP BY PERSON_ID
HAVING SUM(
    CASE WHEN DRUG_ID=234 AND DRUG_START_DATE<DISEASE_START_DATE THEN -1 
    WHEN DRUG_ID=474 AND DRUG_START_DATE<DISEASE_START_DATE THEN 1 
    WHEN DRUG_ID=26 AND DRUG_START_DATE<DISEASE_START_DATE THEN -1 
    ELSE 0 END) = 1
    OR
    SUM(
    CASE WHEN DRUG_ID=395 AND DRUG_START_DATE<DISEASE_START_DATE THEN -1 
    WHEN DRUG_ID=791 AND DRUG_START_DATE<DISEASE_START_DATE THEN -1 
    WHEN DRUG_ID=371 AND DRUG_START_DATE<DISEASE_START_DATE THEN 1 
    ELSE 0 END) = 1

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

(SUM(CASE WHEN DRUG_ID=234 AND DRUG_START_DATE<DISEASE_START_DATE THEN 1 ELSE 0 END) = 0
AND SUM(CASE WHEN DRUG_ID=474 AND DRUG_START_DATE<DISEASE_START_DATE THEN 1 ELSE 0 END) > 0
AND SUM(CASE WHEN DRUG_ID=26 AND DRUG_START_DATE<DISEASE_START_DATE THEN 1 ELSE 0 END) = 0)
OR
(SUM(CASE WHEN DRUG_ID=395 AND DRUG_START_DATE<DISEASE_START_DATE THEN 1 ELSE 0 END) = 0
AND SUM(CASE WHEN DRUG_ID=791 AND DRUG_START_DATE<DISEASE_START_DATE THEN 1 ELSE 0 END) = 0
AND SUM(CASE WHEN DRUG_ID=371 AND DRUG_START_DATE<DISEASE_START_DATE THEN 1 ELSE 0 END) > 0)
0 голосов
/ 23 июля 2010

Ни один из приведенных ответов не работает.Опять-таки, вот схема, которую я хочу реализовать: ((препарат 234 = истина И препарат 474 = истина И препарат 26 = ложь) ИЛИ (препарат 395 = ложь И препарат 791 = ложь И препарат 371 = правда))

Я считаю, что следующий запрос будет работать (препарат 234 = истина И препарат 474 = истина И препарат 26 = ложь).Учитывая это, довольно легко добавить вторую половину запроса.

SELECT  p.person_id, p.gender FROM person_t as p 
    join drug_t as dr on dr.person_id = p.person_id 
    join disease_t as ds on ds.person_id=p.person_id 
    WHERE dr.drug_start_date < ds.disease_start_date AND disease_id = 52 AND dr.drug_id=234
INTERSECT
SELECT  p.person_id, p.gender FROM person_t as p 
    join drug_t as dr on dr.person_id = p.person_id 
    join disease_t as ds on ds.person_id=p.person_id 
    WHERE dr.drug_start_date < ds.disease_start_date AND disease_id = 52 AND dr.drug_id=474
INTERSECT (
SELECT p.person_id, p.gender
    FROM person_t as p 
    JOIN disease_t as ds on ds.person_id = p.person_id 
    LEFT JOIN drug_t as dr ON dr.person_id = p.person_id  AND dr.drug_id = 26
    WHERE disease_id = 52 AND dr.person_id is null 
UNION 
SELECT p.person_id, p.gender
    FROM person_t as p 
    JOIN disease_t as ds on ds.person_id = p.person_id 
    JOIN drug_t as dr ON dr.person_id = p.person_id  AND dr.drug_id = 26
    WHERE disease_id = 52 AND dr.drug_start_date > ds.disease_start_date)

Этот запрос работает, но довольно уродливо.Я также подозреваю, что это будет очень медленно, когда у меня будет производственная база данных со 100 миллионами человек.Что я могу сделать, чтобы упростить / оптимизировать этот запрос?

0 голосов
/ 09 июля 2010
SELECT per.person_id, per.name, per.gender
FROM person_t per
INNER JOIN disease_t dis
USING (person_id)
INNER JOIN drug_t drug
USING (person_id)
WHERE dis.disease_id = 52 AND drug.drug_start_date < dis.disease_start_date AND ((drug.drug_id IN (234, 474) AND drug.drug_id NOT IN (26)) OR (drug.drug_id IN (371) AND drug.drug_id NOT IN (395, 791)));

Это сделает то, что вы просите.Операторы IN в конце довольно понятны.

0 голосов
/ 09 июля 2010

Прошу прощения за любые ошибки, но я думаю, что-то вроде этого будет работать (в T-SQL):

SELECT col1, col2, col3...<br> FROM PERSON_T AS P, DRUG_T AS DR, DISEASE_T AS DI<br> WHERE disease_id = 52<br> AND P.person_id = DI.person_id<br> AND P.person_id = DR.person_id<br> AND drug_id NOT IN(234, 26)<br> AND drug_id = 474<br> AND disease_start_date < drug_start_date<br> UNION<br> SELECT col1, col2, col3...<br> FROM PERSON_T AS P, DRUG_T AS DR, DISEASE_T AS DI<br> WHERE disease_id = 52<br> AND P.person_id = DI.person_id<br> AND P.person_id = DR.person_id<br> AND drug_id NOT IN(791, 395)<br> AND drug_id = 371<br> AND disease_start_date < drug_start_date

Теперь это не нужно делать с UNION, ноудобочитаемость Я думал, что это было самым легким, учитывая ваши условия.Может быть, это приведет вас в правильном направлении.

...