OK. I have data to share. I would like some help interpreting it because I’m a little lost. I ran a bunch of cases, but I’m going to share 2: without any filter and with a filter that basically does an OR on all of the fields in the ListView, including the annotations. I also individually tried a single filter on each field individually as a sanity check and each one completed in roughly under 2 seconds.
I generated the effective ORM code that is executed in each case. I’m working with my Study model.
Here is the fast case (no filter). It does all the same counts. It returns all 11 Study records (I’m on a different machine today that doesn’t have as many studies loaded) in roughly 2 seconds:
# ORM query that generates the QuerySet:
Study.objects
.prefetch_related(
'animals__infusate__tracer_links__tracer__compound',
'animals__samples__msrun_samples__msrun_sequence',
'animals__samples__tissue',
'animals__treatment'
)
.annotate(
**{
'name_bstrowsort': Lower(F(name)),
'total_infusates': Count(F(animals__infusate), distinct=True),
'animals_infusate_tracer_links_mm_count': Count(F(animals__infusate__tracer_links), distinct=True),
'animals_mm_count': Count(F(animals), distinct=True),
'total_tissues': Count(F(animals__samples__tissue), distinct=True),
'animals_samples_mm_count': Count(F(animals__samples), distinct=True),
'animals_samples_msrun_samples_mm_count': Count(F(animals__samples__msrun_samples), distinct=True)
}
)
.order_by(OrderBy(F(name_bstrowsort), descending=False))
.distinct()
# EXPLAIN:
Unique (cost=749.97..753.82 rows=140 width=632)
-> Sort (cost=749.97..750.32 rows=140 width=632)
Sort Key: "DataRepo_study".name, "DataRepo_study".id, "DataRepo_study".description, (lower(("DataRepo_study".name)::text)), (count(DISTINCT "DataRepo_animal".infusate_id)), (count(DISTINCT "DataRepo_infusatetracer".id)), (count(DISTINCT "DataRepo_animal_studies".animal_id)), (count(DISTINCT "DataRepo_sample".tissue_id)), (count(DISTINCT "DataRepo_sample".id)), (count(DISTINCT "DataRepo_msrunsample".id))
-> GroupAggregate (cost=624.05..744.98 rows=140 width=632)
Group Key: "DataRepo_study".id, (lower(("DataRepo_study".name)::text))
-> Sort (cost=624.05..637.29 rows=5297 width=608)
Sort Key: "DataRepo_study".id, (lower(("DataRepo_study".name)::text))
-> Hash Right Join (cost=162.75..296.41 rows=5297 width=608)
Hash Cond: ("DataRepo_sample".animal_id = "DataRepo_animal".id)
-> Hash Right Join (cost=106.19..159.32 rows=2861 width=16)
Hash Cond: ("DataRepo_msrunsample".sample_id = "DataRepo_sample".id)
-> Seq Scan on "DataRepo_msrunsample" (cost=0.00..45.61 rows=2861 width=8)
-> Hash (cost=72.75..72.75 rows=2675 width=12)
-> Seq Scan on "DataRepo_sample" (cost=0.00..72.75 rows=2675 width=12)
-> Hash (cost=49.85..49.85 rows=537 width=568)
-> Hash Left Join (cost=33.74..49.85 rows=537 width=568)
Hash Cond: ("DataRepo_animal".infusate_id = "DataRepo_infusate".id)
-> Hash Left Join (cost=27.68..36.94 rows=408 width=564)
Hash Cond: ("DataRepo_animal_studies".animal_id = "DataRepo_animal".id)
-> Hash Right Join (cost=13.15..21.33 rows=408 width=556)
Hash Cond: ("DataRepo_animal_studies".study_id = "DataRepo_study".id)
-> Seq Scan on "DataRepo_animal_studies" (cost=0.00..7.08 rows=408 width=8)
-> Hash (cost=11.40..11.40 rows=140 width=552)
-> Seq Scan on "DataRepo_study" (cost=0.00..11.40 rows=140 width=552)
-> Hash (cost=10.90..10.90 rows=290 width=8)
-> Seq Scan on "DataRepo_animal" (cost=0.00..10.90 rows=290 width=8)
-> Hash (cost=4.87..4.87 rows=96 width=8)
-> Hash Right Join (cost=2.64..4.87 rows=96 width=8)
Hash Cond: ("DataRepo_infusatetracer".infusate_id = "DataRepo_infusate".id)
-> Seq Scan on "DataRepo_infusatetracer" (cost=0.00..1.96 rows=96 width=8)
-> Hash (cost=1.73..1.73 rows=73 width=4)
-> Seq Scan on "DataRepo_infusate" (cost=0.00..1.73 rows=73 width=4)
And this is the slow case (filtering for any field that contains “24”). It returns 4 Study records. I didn’t time it on this machine, but it’s definitely very slow. The only difference other than the filter is that one of the annotations not needed for the filter is applies after the filter:
# ORM query that generates the QuerySet:
Study.objects
.prefetch_related(
'animals__infusate__tracer_links__tracer__compound',
'animals__samples__msrun_samples__msrun_sequence',
'animals__samples__tissue',
'animals__treatment'
)
.annotate(
**{
'total_infusates': Count(F(animals__infusate), distinct=True),
'animals_infusate_tracer_links_mm_count': Count(F(animals__infusate__tracer_links), distinct=True),
'animals_mm_count': Count(F(animals), distinct=True),
'total_tissues': Count(F(animals__samples__tissue), distinct=True),
'animals_samples_mm_count': Count(F(animals__samples), distinct=True),
'animals_samples_msrun_samples_mm_count': Count(F(animals__samples__msrun_samples), distinct=True)
}
)
.filter(
# This is how the Q object prints
(OR:
('name__icontains', '24'),
('description__icontains', '24'),
('animals__genotype__iexact', '24'),
('total_infusates__icontains', '24'),
('animals__infusate__name__icontains', '24'),
('animals_infusate_tracer_links_mm_count__iexact', '24'),
('animals__infusate__tracer_links__tracer__name__icontains', '24'),
('animals__infusate__tracer_links__tracer__compound__name__icontains', '24'),
('animals__label_combo__iexact', '24'),
('animals__treatment__name__icontains', '24'),
('animals__samples__researcher__iexact', '24'),
('animals__samples__msrun_samples__msrun_sequence__researcher__iexact', '24'),
('animals_mm_count__iexact', '24'),
('total_tissues__icontains', '24'),
('animals_samples_mm_count__iexact', '24'),
('animals_samples_msrun_samples_mm_count__iexact', '24'))
.annotate(**{'name_bstrowsort': Lower(F(name))})
.order_by(OrderBy(F(name_bstrowsort), descending=False))
.distinct()
# EXPLAIN:
Unique (cost=472909.82..477825.97 rows=178769 width=1751)
-> Sort (cost=472909.82..473356.75 rows=178769 width=1751)
Sort Key: "DataRepo_study".name, "DataRepo_study".id, "DataRepo_study".description, (count(DISTINCT "DataRepo_animal".infusate_id)), (count(DISTINCT "DataRepo_infusatetracer".id)), (count(DISTINCT "DataRepo_animal_studies".animal_id)), (count(DISTINCT "DataRepo_sample".tissue_id)), (count(DISTINCT "DataRepo_sample".id)), (count(DISTINCT "DataRepo_msrunsample".id)), (lower(("DataRepo_study".name)::text))
-> GroupAggregate (cost=1689.45..186015.25 rows=178769 width=1751)
Group Key: "DataRepo_study".id, (lower(("DataRepo_study".name)::text)), t10.genotype, t11.name, "DataRepo_tracer".name, "DataRepo_compound".name, t10.label_combo, "DataRepo_protocol".name, t16.researcher, "DataRepo_msrunsequence".researcher
Filter: ((upper(("DataRepo_study".name)::text) ~~ '%24%'::text) OR (upper("DataRepo_study".description) ~~ '%24%'::text) OR (upper((t10.genotype)::text) = '24'::text) OR (upper((count(DISTINCT "DataRepo_animal".infusate_id))::text) ~~ '%24%'::text) OR (upper((t11.name)::text) ~~ '%24%'::text) OR (upper((count(DISTINCT "DataRepo_infusatetracer".id))::text) = '24'::text) OR (upper(("DataRepo_tracer".name)::text) ~~ '%24%'::text) OR (upper(("DataRepo_compound".name)::text) ~~ '%24%'::text) OR (upper((t10.label_combo)::text) = '24'::text) OR (upper(("DataRepo_protocol".name)::text) ~~ '%24%'::text) OR (upper((t16.researcher)::text) = '24'::text) OR (upper(("DataRepo_msrunsequence".researcher)::text) = '24'::text) OR (upper((count(DISTINCT "DataRepo_animal_studies".animal_id))::text) = '24'::text) OR (upper((count(DISTINCT "DataRepo_sample".tissue_id))::text) ~~ '%24%'::text) OR (upper((count(DISTINCT "DataRepo_sample".id))::text) = '24'::text) OR (upper((count(DISTINCT "DataRepo_msrunsample".id))::text) = '24'::text))
-> Incremental Sort (cost=1689.45..150519.33 rows=200280 width=1727)
Sort Key: "DataRepo_study".id, (lower(("DataRepo_study".name)::text)), t10.genotype, t11.name, "DataRepo_tracer".name, "DataRepo_compound".name, t10.label_combo, "DataRepo_protocol".name, t16.researcher, "DataRepo_msrunsequence".researcher
Presorted Key: "DataRepo_study".id
-> Nested Loop Left Join (cost=638.56..131138.43 rows=200280 width=1727)
Join Filter: (t17.msrun_sequence_id = "DataRepo_msrunsequence".id)
-> Nested Loop Left Join (cost=638.56..121797.41 rows=200280 width=1183)
-> Nested Loop Left Join (cost=638.28..56094.88 rows=187259 width=1183)
-> Nested Loop Left Join (cost=638.00..29427.06 rows=20301 width=1170)
Join Filter: (t12.tracer_id = "DataRepo_tracer".id)
-> Nested Loop Left Join (cost=636.01..16003.67 rows=20301 width=1139)
-> Nested Loop Left Join (cost=635.87..10841.79 rows=15437 width=1139)
Join Filter: (t10.treatment_id = "DataRepo_protocol".id)
-> Nested Loop Left Join (cost=635.87..8022.13 rows=15437 width=627)
-> Nested Loop Left Join (cost=635.73..4925.35 rows=15437 width=598)
-> Merge Left Join (cost=635.58..869.17 rows=15437 width=580)
Merge Cond: ("DataRepo_study".id = "DataRepo_animal_studies".study_id)
-> Sort (cost=39.03..40.05 rows=408 width=556)
Sort Key: "DataRepo_study".id
-> Hash Right Join (cost=13.15..21.33 rows=408 width=556)
Hash Cond: (t9.study_id = "DataRepo_study".id)
-> Seq Scan on "DataRepo_animal_studies" t9 (cost=0.00..7.08 rows=408 width=8)
-> Hash (cost=11.40..11.40 rows=140 width=552)
-> Seq Scan on "DataRepo_study" (cost=0.00..11.40 rows=140 width=552)
-> Sort (cost=596.55..609.80 rows=5297 width=28)
Sort Key: "DataRepo_animal_studies".study_id
-> Hash Right Join (cost=148.49..268.91 rows=5297 width=28)
Hash Cond: ("DataRepo_sample".animal_id = "DataRepo_animal".id)
-> Hash Right Join (cost=106.19..159.32 rows=2861 width=16)
Hash Cond: ("DataRepo_msrunsample".sample_id = "DataRepo_sample".id)
-> Seq Scan on "DataRepo_msrunsample" (cost=0.00..45.61 rows=2861 width=8)
-> Hash (cost=72.75..72.75 rows=2675 width=12)
-> Seq Scan on "DataRepo_sample" (cost=0.00..72.75 rows=2675 width=12)
-> Hash (cost=35.59..35.59 rows=537 width=20)
-> Hash Left Join (cost=20.59..35.59 rows=537 width=20)
Hash Cond: ("DataRepo_animal".infusate_id = "DataRepo_infusate".id)
-> Hash Left Join (cost=14.53..22.69 rows=408 width=16)
Hash Cond: ("DataRepo_animal_studies".animal_id = "DataRepo_animal".id)
-> Seq Scan on "DataRepo_animal_studies" (cost=0.00..7.08 rows=408 width=8)
-> Hash (cost=10.90..10.90 rows=290 width=8)
-> Seq Scan on "DataRepo_animal" (cost=0.00..10.90 rows=290 width=8)
-> Hash (cost=4.87..4.87 rows=96 width=8)
-> Hash Right Join (cost=2.64..4.87 rows=96 width=8)
Hash Cond: ("DataRepo_infusatetracer".infusate_id = "DataRepo_infusate".id)
-> Seq Scan on "DataRepo_infusatetracer" (cost=0.00..1.96 rows=96 width=8)
-> Hash (cost=1.73..1.73 rows=73 width=4)
-> Seq Scan on "DataRepo_infusate" (cost=0.00..1.73 rows=73 width=4)
-> Index Scan using "DataRepo_animal_pkey" on "DataRepo_animal" t10 (cost=0.15..0.26 rows=1 width=22)
Index Cond: (id = t9.animal_id)
-> Index Scan using "DataRepo_infusate_pkey" on "DataRepo_infusate" t11 (cost=0.14..0.20 rows=1 width=33)
Index Cond: (id = t10.infusate_id)
-> Materialize (cost=0.00..3.19 rows=13 width=520)
-> Seq Scan on "DataRepo_protocol" (cost=0.00..3.13 rows=13 width=520)
-> Index Only Scan using unique_infusate_tracer on "DataRepo_infusatetracer" t12 (cost=0.14..0.32 rows=1 width=8)
Index Cond: (infusate_id = t11.id)
-> Materialize (cost=1.99..24.85 rows=44 width=39)
-> Hash Right Join (cost=1.99..24.63 rows=44 width=39)
Hash Cond: ("DataRepo_compound".id = "DataRepo_tracer".compound_id)
-> Seq Scan on "DataRepo_compound" (cost=0.00..17.89 rows=689 width=21)
-> Hash (cost=1.44..1.44 rows=44 width=26)
-> Seq Scan on "DataRepo_tracer" (cost=0.00..1.44 rows=44 width=26)
-> Index Scan using "DataRepo_sample_animal_id_d0c92479" on "DataRepo_sample" t16 (cost=0.28..1.21 rows=10 width=21)
Index Cond: (animal_id = t10.id)
-> Index Scan using "DataRepo_msrunsample_sample_id_145999d5" on "DataRepo_msrunsample" t17 (cost=0.28..0.34 rows=1 width=8)
Index Cond: (sample_id = t16.id)
-> Materialize (cost=0.00..1.04 rows=3 width=520)
-> Seq Scan on "DataRepo_msrunsequence" (cost=0.00..1.03 rows=3 width=520)
I have a few thoughts about little improvements that could improve performance, though I’m not sure it would account for the dramatic difference. I can fix things like the type conversion that is happening automatically and I could probably exclude some fields from the search based on the search term.
This performance issue only affects the Studies page though. Other views have multiple many-related columns and don’t have this issue with the whole-table search feature (worst case they take 4 seconds). So my guess is that its the number of many-related steps in the path that doesn’t cooperate well with the filtering… The Study:Animal step (Many:Many) really increases the number of records involved and while the other views contain multiple many-related steps, they don’t have that big step from Study to Animal. I might disable the search on just the fields that start with animals__samples__msrun_samples and see if that affects performance.