One more comment. If you are sure that right table does not have duplicates you can do:
julia> leftjoin!(df, df2, on=[:a, :c])
4×4 DataFrame
Row │ a b c value
│ Int64 Int64 Int64 String?
─────┼──────────────────────────────
1 │ 1 5 9 result1
2 │ 2 6 10 missing
3 │ 3 7 11 result2
4 │ 1 8 9 result1
which maintains row order (it does in-place update so if you want a fresh data frame do leftjoin!(copy(df), df2, on=[:a, :c]))