LEFT OUTER JOIN с 'field IS NULL' в WHERE работает как INNER JOIN
Сегодня я столкнулся с необъяснимым (для меня) поведением PostgreSQL - LEFT OUTER JOIN
не возвращает записи для основной таблицы (с нулями для объединенных полей) в случае, если поля объединенной таблицы используются в выражении WHERE
.
Чтобы было легче понять детали дела, я приведу пример. Итак, допустим, у нас есть 2 таблицы: item
с некоторыми товарами, и price
, ссылаясь на item
, с ценами на товары в разные годы:
CREATE TABLE item(
id INTEGER PRIMARY KEY,
name VARCHAR(50)
);
CREATE TABLE price(
id INTEGER PRIMARY KEY,
item_id INTEGER NOT NULL,
year INTEGER NOT NULL,
value INTEGER NOT NULL,
CONSTRAINT goods_fk FOREIGN KEY (item_id) REFERENCES item(id)
);
Таблица item
имеет 2 записи (телевизоры и видеомагнитофоны), а таблица price
имеет 3 записи, цену на телевизор в 2000 и 2010 годах и цену на видеомагнитофон только за 2000 год:
INSERT INTO item(id, name)
VALUES
(1, 'TV set'),
(2, 'VCR');
INSERT INTO price(id, item_id, year, value)
VALUES
(1, 1, 2000, 290),
(2, 1, 2010, 270),
(3, 2, 2000, 770);
-- no price of VCR for 2010
Теперь составим запрос LEFT OUTER JOIN
, чтобы получить цены на все товары за 2010 год:
SELECT
i.*,
p.year,
p.value
FROM item i
LEFT OUTER JOIN price p ON i.id = p.item_id
WHERE p.year = 2010 OR p.year IS NULL;
По какой-то причине этот запрос вернет результаты только для телевизора, у которого есть цена на этот год. Запись для видеомагнитофона отсутствует в результатах:
id | name | year | value
----+--------+------+-------
1 | TV set | 2010 | 270
(1 row)
После некоторых экспериментов я нашел способ заставить запрос возвращать нужные мне результаты (все записи для item
таблицы, с нулями в полях объединенной таблицы в случае отсутствия математических записей для года. Это было достигнуто путем переноса фильтрации года в условие JOIN
:
SELECT
i.*,
p.year,
p.value
FROM item i
LEFT OUTER JOIN (
SELECT * FROM price
WHERE year = 2010 -- <= here I filter a year
) p ON i.id = p.item_id;
И теперь результат таков:
id | name | year | value
----+--------+------+-------
1 | TV set | 2010 | 270
2 | VCR | |
(2 rows)
Мой главный вопрос заключается в том - почему первый запрос (с фильтрацией по году в WHERE
) не работает так, как ожидалось, а превращается в нечто вроде INNER JOIN
?
Я сильно заблокирован этой проблемой в моем текущем проекте, поэтому я буду благодарен за советы/подсказки по следующим связанным вопросам тоже:
- Are there any other options to achieve the proper results?
- ... especially — easily translatable to Django's ORM queryset?
Обновление: @astentx предложил перенести условие фильтрации непосредственно в JOIN
(и это тоже работает):
SELECT
i.*,
p.year,
p.value
FROM item i
LEFT OUTER JOIN price p
ON
i.id = p.item_id
AND p.year = 2010;
Так же, как и мое первое решение, я не вижу, как выразить его в терминах Django ORM querysets. Есть ли другие предложения?
Первый запрос не работает так, как ожидалось, потому что ожидание неверно. Он также не работает как INNER JOIN. Запрос возвращает запись для VCR только в том случае, если для VCR вообще нет цены.
SELECT
i.*,
y.year,
p.value
FROM item i
CROSS JOIN (SELECT 2010 AS year) y -- here could be a table
LEFT OUTER JOIN price p
ON (p.item_id = i.id
AND p.year = y.year);