Summary
LEFT JOIN drops left-hand rows that find no partner instead of emitting them
with NULLs. The result is identical to the equivalent INNER JOIN. This makes
anti-joins (LEFT JOIN ... WHERE right.col IS NULL) impossible to express, which
is the natural way to ask the most common corpus question there is: which
documents are missing X?
Reproduction
mkdir -p /tmp/mqbug/docs && cd /tmp/mqbug
printf '# Alpha\n\nIntro paragraph.\n\n## Install\n\nRun the installer.\n' > docs/a.md
printf '# Beta\n\nAnother paragraph.\n\n## Usage\n\nUse it well.\n' > docs/b.md
mq-db index docs -o s.mq-db
docs/a.md has an Install heading; docs/b.md does not.
$ mq-db sql -d s.mq-db -F json \
"SELECT d.path, b.id FROM documents d JOIN blocks b ON b.document_id = d.id AND b.content = 'Install'"
[{"path":"docs/a.md","id":6}]
$ mq-db sql -d s.mq-db -F json \
"SELECT d.path, b.id FROM documents d LEFT JOIN blocks b ON b.document_id = d.id AND b.content = 'Install'"
[{"path":"docs/a.md","id":6}]
Expected from the LEFT JOIN: two rows, the second being
{"path":"docs/b.md","id":null}. Actual: byte-identical to the INNER JOIN.
Consequently the anti-join returns nothing:
$ mq-db sql -d s.mq-db -F json \
"SELECT d.path FROM documents d LEFT JOIN blocks b ON b.document_id = d.id AND b.content = 'Install' WHERE b.id IS NULL"
[]
Expected docs/b.md.
It isn't IS NULL, and it isn't join resolution
IS NULL / IS NOT NULL evaluate correctly on ordinary columns:
$ mq-db sql -d s.mq-db -F json "SELECT path FROM documents WHERE title IS NOT NULL"
[{"path":"docs/b.md"},{"path":"docs/a.md"}]
And the join itself resolves fine when every left row happens to match — all 8
pairs come back:
$ mq-db sql -d s.mq-db -F json "SELECT d.path, b.id FROM documents d LEFT JOIN blocks b ON b.document_id = d.id"
[{"path":"docs/b.md","id":0}, ... 8 rows ... ,{"path":"docs/a.md","id":7}]
So the missing piece is specifically the generation of NULL-extended rows for
unmatched left tuples.
The same happens when the right side is a CTE, so it isn't specific to base
tables:
$ mq-db sql -d s.mq-db -F json \
"WITH h AS (SELECT document_id FROM blocks WHERE content = 'Install') SELECT d.path FROM documents d LEFT JOIN h ON h.document_id = d.id WHERE h.document_id IS NULL"
[]
(The INNER JOIN form of that CTE query works correctly and returns docs/a.md.)
EXPLAIN
$ mq-db sql -d s.mq-db "EXPLAIN SELECT d.path, b.id FROM documents d LEFT JOIN blocks b ON b.document_id = d.id AND b.content = 'Install'"
│ query:from │ documents (documents) │
│ query:where │ none — full scan │
│ query:join[0] │ blocks: hash join on b.document_id = d.id — join partner always full-scanned │
The plan records only hash join, with no left/outer marker — the join type
appears not to survive into the plan. (Minor, separate observation: the extra
AND b.content = 'Install' conjunct in the ON clause is absent from the plan
detail too, though it clearly is applied to the results.)
Environment
mq-db 0.2.1, installed via cargo install
- macOS 26.6.2 (build 25G83), arm64
Summary
LEFT JOINdrops left-hand rows that find no partner instead of emitting themwith NULLs. The result is identical to the equivalent
INNER JOIN. This makesanti-joins (
LEFT JOIN ... WHERE right.col IS NULL) impossible to express, whichis the natural way to ask the most common corpus question there is: which
documents are missing X?
Reproduction
docs/a.mdhas anInstallheading;docs/b.mddoes not.Expected from the
LEFT JOIN: two rows, the second being{"path":"docs/b.md","id":null}. Actual: byte-identical to theINNER JOIN.Consequently the anti-join returns nothing:
Expected
docs/b.md.It isn't
IS NULL, and it isn't join resolutionIS NULL/IS NOT NULLevaluate correctly on ordinary columns:And the join itself resolves fine when every left row happens to match — all 8
pairs come back:
So the missing piece is specifically the generation of NULL-extended rows for
unmatched left tuples.
The same happens when the right side is a CTE, so it isn't specific to base
tables:
(The
INNER JOINform of that CTE query works correctly and returnsdocs/a.md.)EXPLAINThe plan records only
hash join, with no left/outer marker — the join typeappears not to survive into the plan. (Minor, separate observation: the extra
AND b.content = 'Install'conjunct in theONclause is absent from the plandetail too, though it clearly is applied to the results.)
Environment
mq-db 0.2.1, installed viacargo install