SQL-запрос для сопоставления списка значений со списком полей в любом порядке без повторения

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

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

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

знач1 = "а", знач2 = "б", знач3 = "с"

Это запрос, который я сгенерировал:

SELECT * FROM table WHERE
(fieldA = :val1 AND fieldB = :val2 AND fieldC = :val3) OR
(fieldA = :val1 AND fieldB = :val3 AND fieldC = :val2) OR
(fieldA = :val2 AND fieldB = :val1 AND fieldC = :val3) OR
(fieldA = :val2 AND fieldB = :val3 AND fieldC = :val1) OR
(fieldA = :val3 AND fieldB = :val1 AND fieldC = :val2) OR
(fieldA = :val3 AND fieldB = :val2 AND fieldC = :val1)

Что мне нужно было сделать, так это сгенерировать запрос, имитирующий перестановку без повторения. Есть ли лучший способ сделать этот тип запроса?

Это нормально для 3x3, но если мне нужно сделать то же самое с чем-то большим, например 9x9, то генерация запроса будет огромным беспорядком.

Я использую MariaDB, но могу принимать ответы, которые могут работать на PostgreSQL. (Я хочу узнать, есть ли умный способ написания запросов такого типа без «грубой силы»)


person Gabriel Mazetto    schedule 05.03.2014    source источник
comment
Каков тип даты ваших столбцов? Потому что для целых чисел есть более быстрое решение...   -  person Erwin Brandstetter    schedule 06.03.2014
comment
Они все "струны"   -  person Gabriel Mazetto    schedule 06.03.2014


Ответы (5)


Нет намного способа лучше, но вы можете использовать in:

SELECT *
FROM table
WHERE :val1 in (fieldA, fieldB, fieldC) and
      :val2 in (fieldA, fieldB, fieldC) and
      :val3 in (fieldA, fieldB, fieldC)

По крайней мере он короче. И это стандартный SQL, поэтому он должен работать в любой базе данных.

person Gordon Linoff    schedule 05.03.2014
comment
это позволило бы fieldA и fieldB удовлетворяться :val1, когда только 1 val может удовлетворять 1 полю за раз. - person T McKeown; 06.03.2014
comment
@TMcKeown . . . Я просто изменил его на то, как я сначала подумал об этом (проверка значений в переменных). Я думал, что изменение порядка было просто косметическим, но, как вы заметили, это явно не так. - person Gordon Linoff; 06.03.2014
comment
я думаю мой способ тоже сработает... подсказка подсказка... нет красивого способа - person T McKeown; 06.03.2014
comment
Большое спасибо... ваша альтернатива чище. - person Gabriel Mazetto; 06.03.2014

... Я могу принять ответы, которые могут работать на PostgreSQL. (Я хочу узнать, есть ли умный способ написания запросов такого типа без «грубой силы»)

В Postgres есть «умный способ» с отсортированными массивами.

Целое число

Для значений integer используйте sort_asc() дополнительного модуль intarray.

SELECT * FROM tbl
WHERE  sort_asc(ARRAY[id1, id2, id3]) = '{1,2,3}' -- compare sorted arrays

Работает для любого количества элементов.

Другие типы

Как поясняется в комментарии, мы имеем дело со строками.
Создайте вариант sort_asc(), который работает для любого типа, который можно сортировать. :

CREATE OR REPLACE FUNCTION sort_asc(anyarray)
  RETURNS anyarray LANGUAGE sql IMMUTABLE AS
'SELECT array_agg(x ORDER BY x COLLATE "C") FROM unnest($1) AS x';

Не так быстро, как брат из intarray, но достаточно быстро.

  • Сделайте его IMMUTABLE, чтобы разрешить его использование в индексах.
  • Используйте COLLATE "C", чтобы игнорировать правила сортировки текущей локали: быстрее, неизменно .
  • Чтобы функция работала для любого типа, который можно сортировать, используйте полиморфный параметр.

Запрос тот же:

SELECT * FROM tbl
WHERE  sort_asc(ARRAY[val1, val2, val3]) = '{bar,baz,foo}';

Или, если вы не уверены в порядке сортировки в локали "C"...

SELECT * FROM tbl
WHERE  sort_asc(ARRAY[val1, val2, val3]) = sort_asc('{bar,baz,foo}'::text[]);

Показатель

Для лучшей производительности чтения создайте функциональный указатель (за некоторую плату написать производительность):

CREATE INDEX tbl_arr_idx ON tbl (sort_asc(ARRAY[val1, val2, val3]));

SQL Fiddle демонстрирует все.

person Erwin Brandstetter    schedule 05.03.2014
comment
Я искал модуль куба, если это может помочь. Но пропустил функции сортировки в модуле intarray. Очень хорошо. - person a_horse_with_no_name; 06.03.2014

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

Это «должно» дать вам список ключей, соответствующих критериям

SELECT F.KEY
FROM (
SELECT DISTINCT L.Key, L.POS
FROM (
  SELECT Key, 'A' AS POS, FieldA AS FIELD FROM table AS A
  UNION ALL
  SELECT Key, 'B' AS POS, FieldB AS FIELD FROM table AS A
  UNION ALL
  SELECT Key, 'C' AS POS, FieldC AS FIELD FROM table AS A ) AS L
WHERE L.FIELD IN(:VAL1, :VAL2, :VAL3)
) AS F
GROUP BY F.KEY
HAVING COUNT(*) = 3
person T McKeown    schedule 05.03.2014
comment
это столбец псевдонима, однако вы указываете псевдоним или вычисляемый столбец - person T McKeown; 06.03.2014
comment
Скобочное цитирование ([identifier]) — это вещь SQL Server, не так ли? - person mu is too short; 06.03.2014
comment
@muistooshort: да SQL Server, но не (стандартный) SQL (и определенно не Postgres или MySQL). - person a_horse_with_no_name; 06.03.2014
comment
спасибо, что поделились этим, хотя для конкретных констант тот, за который я проголосовал, выглядит чище, я вижу другие ситуации, когда этот способ реализации может быть лучшим выбором. - person Gabriel Mazetto; 06.03.2014

Хотя ответ Гордона определенно короче и почти наверняка быстрее, я играл с идеей о том, как минимизировать изменение кода при увеличении количества комбинаций.

И я могу придумать что-то для Postgres, которое ни в коем случае не короче, но более «дружественно к изменениям»:

with recursive params (val) as (
   values (1),(2),(3) -- these are the input values
), all_combinations as (
   select array[val] as elements
   from params
   union all
   select ac.elements||p.val
   from params p
     join all_combinations ac 
       on array_length(ac.elements,1) < (select count(*) from params)
)
select *
from the_table
where array[id1,id2,id3] = any (select elements from all_combinations);

Что оно делает?

Сначала мы создаем CTE, содержащий искомые значения, затем рекурсивный CTE строит список всех возможных перестановок из этих значений. Этот список будет включать слишком много элементов, потому что он также будет содержать массивы с 1 или двумя элементами.

Окончательный выбор, который помещает столбцы, которые необходимо сравнить, в массив и сравнивает их с перестановками, сгенерированными CTE.

Вот пример SQLFiddle: http://sqlfiddle.com/#!15/43066/1

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

person a_horse_with_no_name    schedule 05.03.2014
comment
Я читал о рекурсии в PostgreSQL и задавался вопросом, как ее использовать для выполнения этой работы. Спасибо, что поделился. - person Gabriel Mazetto; 06.03.2014
comment
@GabrielMazetto Здесь действительно помогает не только CTE, но и эффективность массивов в Postgres. Я бы, вероятно, поместил генерацию перестановки (два CTE) в функцию, если бы это было чем-то, что используется очень часто. Затем вы можете просто написать: select * from the_table where array[id1,id2,id3] = any (select elements from generate_permutation(1,2,3) - person a_horse_with_no_name; 06.03.2014
comment
Если вы отсортируете массив, вы можете сократить его до одной проверки на равенство. - person Erwin Brandstetter; 06.03.2014

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

Есть также некоторые оптимизации, которые вы могли бы сделать.

Сначала вы можете исключить последнее поле, так как:

A <> B, A <> C
A <> B, B <> C, 

Также означает, что:

C <> B,  C <> A

Кроме того, для следующих запросов не требуется ранее запрошенное поле, поскольку:

A <> B == B <> A

Запрос будет записан так:

SELECT * FROM table
WHERE :val1 in (fieldA, fieldB, fieldC) and
  :val2 in (fieldA, fieldB, fieldC) and
  :val3 in (fieldA, fieldB, fieldC) and
  fieldA not in (fieldB, fieldC) and
  fieldB <> fieldC

Это наивный подход, возможно, есть и другие, использующие MySQL API, но этот работает.

person Mateus Dubiela Oliveira    schedule 05.03.2014