ash-project / ash

A declarative, extensible framework for building Elixir applications.
https://www.ash-hq.org
MIT License
1.63k stars 218 forks source link

Significant latency in Ash.load #1565

Open nallwhy opened 3 weeks ago

nallwhy commented 3 weeks ago

Describe the bug

Ash.load exhibits significant performance issues when loading large resources -> large has_many associations.

For example, there 5000 Bills and 10000 Pays.

defmodule Bill do
  use Ash.Resource, ...

  relationships do
     has_many :pays, Pay
  end

  actions do
    defaults [:read]
  end
end

defmodule Pay do
  use Ash.Resource, ...

  relationships do
    belongs_to :bill, Bill
  end

  actions do
    default [:read]

    read :list_by_bill_ids do
      argument :bill_ids, {:array, :integer}

      filter expr(bill_id in ^arg(:bill_ids))
    end
  end
end

# too slow (>= 10s), but db query is not slow. (~= 200ms)

Bill.read!() |> Ash.load([:pays])

# much faster (~= 200ms)

bills = Bill.read!()
bill_ids = bills |> Enum.map(& &1.id)
bill_id_pays_map = Pay.list_by_bill_ids(%{bill_ids: bill_ids}) |> Enum.group_by(& &1.bill_id)

bills
|> Enum.map(fn bill -> %Bill{bill | pays: bill_id_pays_map |> Map.get(bill.id, [])} end)

To Reproduce

I haven’t yet replicated the issue, but I plan to set up a minimal test environment within a few days. We use PostgreSQL and multitenancy, which may affect the behavior.

Expected behavior

Ash.load is not too slow.

Runtime

zachdaniel commented 3 weeks ago

Let me know once you have a reproduction. Something to try to see if it impacts anything is setting this:

# config/test.exs
config :ash, :disable_async?, true
zachdaniel commented 3 weeks ago

Did some benchmarking of reading 5k records each with 10 related records, and we perform only slightly worse than ecto (would still be worth optimizing of course): CleanShot 2024-10-30 at 19 02 38@2x

Do you perhaps have any calculations or preparations on the relevant read actions that could be coming into play?