Rails / Sql - порядок / группа результатов поиска, так что повторение сущностей происходит только после появления других - PullRequest
4 голосов
/ 06 мая 2011

В моем приложении, скажем, у животных много фотографий. Я запрашиваю фотографии животных так, что я хочу, чтобы все фотографии всех животных отображались. Тем не менее, я хочу, чтобы каждое животное появилось в виде фотографии, прежде чем произойдет повторение.

Example: 

animal instance 1, 'cat', has four photos, 
animal instance 2, 'dog', has two photos:

photos should appear ordered as so:

#photo         belongs to     #animal

tiddles.jpg ,                  cat 
fido.jpg                       dog 
meow.jpg                       cat 
rover.jpg                      dog 
puss.jpg                       cat 
felix.jpg,                     cat  (no more dogs so two consecutive cats)
  • Требуется нумерация страниц, поэтому я не могу порядок на массиве.
  • Имя файла структура / соглашение не обеспечивает помогите, хотя animal_id существует на каждая фотография.
  • Хотя есть два типы животных в этом примере это активная модель записи с сотни записей.
  • Животные могут быть выборочно запрашивается.

Если это не возможно с active_record, тогда я с радостью использую sql; Я использую postgresql.

Мой мозг измотан, поэтому, если кто-то может придумать лучший заголовок, пожалуйста, отредактируйте его или предложите в комментариях.

Ответы [ 6 ]

3 голосов
/ 08 мая 2011

Вот решение для PostgreSQL:

batch_id_sql = "RANK() OVER (PARTITION BY animal_id ORDER BY id ASC)"

Photo.paginate(
  :select => "DISTINCT photos.*, (#{batch_id_sql}) batch_id",
  :order  => "batch_id ASC, photos.animal_id ASC",
  :page   => 1)

Вот решение, не зависящее от БД:

batch_id_sql = "
 SELECT COUNT(bm.*) 
 FROM   photos bm 
 WHERE  bm.animal_id = photos.animal_id AND
        bm.id <= photos.id 
"

Photo.paginate(
  :select => "photos.*, (#{batch_id_sql}) batch_id",
  :order  => "batch_id ASC, photos.animal_id ASC",
  :page   => 1)

Оба запроса работают, даже если у вас есть условие where.Выполните оценку запроса, используя ожидаемый набор данных, чтобы проверить, соответствует ли он ожидаемым требованиям к пропускной способности и задержке.

Ссылка

Функция окна PostgreSQL

1 голос
/ 08 мая 2011

Я бы порекомендовал что-то гибридное / исправленное на основе ввода Кандада Боггу.

Прежде всего, правильный способ сделать это на бумаге с row_number() over (partition by animal_id order by id). Предлагаемый rank() сгенерирует глобальный номер строки, но вам нужен номер в его разделе.

Использование оконной функции также является наиболее гибким решением (фактически единственным решением), если вы хотите планировать изменение порядка сортировки здесь и там.

Обратите внимание, что это не всегда хорошо масштабируется, потому что для сортировки результатов вам необходимо:

  • получить весь набор результатов, который соответствует вашим критериям
  • отсортировать весь набор результатов, чтобы создать разделы и получить rank_id
  • top-n сортирует / ограничивает набор результатов во второй раз, чтобы получить их в окончательном порядке

Правильный способ сделать это на практике, если ваш порядок сортировки неизменен, - это поддерживать предварительно вычисленный rank_id. В этом смысле другие предложения Кандада Боггу указывают в правильном направлении.

Когда дело доходит до удалений (и, возможно, обновлений, если вы не хотите, чтобы они сортировались по идентификатору), вы можете столкнуться с проблемами, поскольку в итоге вы торгуете более быстрым чтением для медленных записей. Если удаление кошки с индексом 1 приведет к обновлению следующих 50 тыс. Кошек, у вас будут проблемы.

Если у вас очень маленькие наборы, накладные расходы могут быть очень приемлемыми (не забудьте индексировать animal_id).

Если нет, то есть обходной путь, если вы обнаружите, что порядок появления определенных животных не имеет значения. Это выглядит так:

  1. Начать транзакцию.

  2. Если rank_id собирается измениться (то есть вставить или удалить), получите консультативную блокировку, чтобы гарантировать, что две сессии не могут повлиять на rank_id одного и того же класса животных, например ::101033

    SELECT pg_try_advisory_lock('the_table'::regclass, the_animal_id);
    

    (Сон в течение 0,05 с, если вы его не получаете.)

  3. При вставке найдите max (rank_id) для этого animal_id. Присвойте ему rank_id + 1. Затем вставьте его.

    При удалении выберите животное с тем же animal_id и наибольшим rank_id. Удалите свое животное и назначьте его старый rank_id извлеченному животному (если, конечно, вы не удаляли последнее).

  4. Снять защитную блокировку.

  5. Совершить работу.

Обратите внимание, что вышеупомянутое будет хорошо использовать индекс для (animal_id, rank_id) и может быть выполнено с помощью триггеров plpgsql:

create trigger "__animals_rank_id__ins"
before insert on animals
for each row execute procedure lock_animal_id_and_assign_rank_id();

create trigger "_00_animals_rank_id__ins"
after insert on animals
for each row execute procedure unlock_animal_id();

create trigger "__animals_rank_id__del"
before delete on animals
for each row execute procedure lock_animal_id();

create trigger "_00_animals_rank_id__del"
after delete on animals
for each row execute procedure reassign_rank_id_and_unlock_animal_id();

Затем вы можете создать многостолбцовый индекс для ваших критериев сортировки, если вы не объединяете их во всех местах, например (rank_id, имя). И у вас получится быстрый сайт для чтения и записи.

1 голос
/ 08 мая 2011

Не имея опыта активной записи. Используя обычный PostgreSQL, я бы попробовал что-то вроде этого:

Определить оконную функцию для всех предыдущих строк, которая подсчитывает, сколько раз появилось текущее животное, а затем упорядочить по этому количеству.

SELECT
   filename,
   animal_id,
   COUNT(*) OVER (PARTITION BY animal_id ORDER BY filename) AS cnt
FROM
   photos
ORDER BY
   cnt,
   animal_id,
   filename

Будет работать фильтрация для определенных animal_id. Это всегда будет заказывать одинаково. Я не знаю, хотите ли вы что-нибудь случайное, но это должно быть легко добавлено.

1 голос
/ 06 мая 2011

Новое решение

Добавьте столбец integer с именем batch_id в таблицу animals.

class AddBatchIdToPhotos < ActiveRecord::Migration
  def self.up
    add_column    :photos,   :batch_id, :integer
    set_batch_id
    change_column :photos,   :batch_id, :integer, :nil => false
    add_index     :photos,   :batch_id
  end

  def self.down
    remove_column :photos,   :batch_id
  end

  def self.set_batch_id
    # set the batch id to existing rows
    # implement this
  end
end

Теперь добавьте before_create на модель Photo для установки идентификатора партии.

class Photo
  belongs_to     :animal
  before_create  :batch_photo_add
  after_update   :batch_photo_update
  after_destroy  :batch_photo_remove

private

  def batch_photo_add
    self.batch_id = next_batch_id_for_animal(animal_id)
    true
  end

  def batch_photo_update
    return true unless animal_id_changed?
    batch_photo_remove(batch_id, animal_id_was)
    batch_photo_add
  end

  def batch_photo_remove(b_id=batch_id, a_id=animal_id)
    Photo.update_all("batch_id = batch_id- 1", 
      ["animal_id = ? AND batch_id > ?", a_id, b_id])
    true
  end

  def next_batch_id_for_animal(a_id)
    (Photo.maximum(:batch_id, :conditions => {:animal_id => a_id}) || 0) + 1
  end
end

Теперь вы можете получить желаемый результат, введя простую команду paginate

@animal_photos = Photo.paginate(:page => 1, :per_page => 10, 
                     :order => :batch_id)

Как это работает?

Давайте рассмотрим, есть ли у нас набор данных, как указано ниже:

id  Photo Description    Batch Id
1   Cat_photo_1          1
2   Cat_photo_2          2
3   Dog_photo_1          1
2   Cat_photo_3          3
4   Dog_photo_2          2
5   Lion_photo_1         1
6   Cat_photo_4          4

Теперь, если бы мывыполнить запрос, упорядоченный по batch_id мы получим это

# batch 1 (cat, dog, lion)
Cat_photo_1
Dog_photo_1
Lion_photo_1

# batch 2 (cat, dog)
Cat_photo_2
Dog_photo_2

# batch 3,4 (cat)
Cat_photo_3
Cat_photo_4

Распределение по партиям не случайно, животные наполняются сверху.Количество животных, отображаемых на странице, определяется параметром per_page, переданным методу paginate (не размером партии).

Старое решение

Есть ли у васпробовал это?

Если вы используете гем will_paginate:

# assuming you want to order by animal name
animal_photos = Photo.paginate(:include => :animal, :page => 1, 
                  :order => "animals.name")

animal_photos.each do |animal_photo|
  puts animal_photo.file_name
  puts animal_photo.animal.name
end
0 голосов
/ 06 мая 2011

Вы можете запустить две сортировки и построить один массив следующим образом:

result1 = Первый только для каждого типа животных. используйте метод ruby ​​"find" для этого поиска.

result2 = Все животные, отсортированные по группам. Используйте «find», чтобы снова найти первое вхождение каждого животного, а затем «drop», чтобы удалить эти «первые вхождения» из result2.

Тогда: markCustomResult = result1 + result2

Тогда: Вы можете использовать willpaginate на markCustomResult

0 голосов
/ 06 мая 2011

Вы должны быть в состоянии получить изображения (или имена файлов, в любом случае), используя ActiveRecord, упорядоченный по имени.

Тогда вы можете использовать Enumerable#group_by и Enumerable#zip чтобы сжать все массивы вместе.

Если вы дадите мне больше информации о том, как на самом деле устроены ваши имена файлов (т. Е. Все ли они точно с подчеркиванием перед числом и постоянным именем перед подчеркиванием)для каждого "типа"? и т. д.), тогда я могу привести пример.Я на мгновение напишу, как ты это сделаешь для своего текущего примера.

...