2016-03-30 23 views
8

Sử dụng Rails. Tôi có đoạn mã sau:Truy vấn phức tạp Rails để đếm các bản ghi duy nhất dựa trên bảng chân lý

class TypeOfBlock < ActiveRecord::Base 
    has_and_belongs_to_many :patients 
end 

class Patient < ActiveRecord::Base 
    has_and_belongs_to_many :type_of_blocks, dependent: :destroy 
end 

Với những bộ bàn:

╔══════════════╗ 
║type_of_blocks║ 
╠══════╦═══════╣ 
║ id ║ name ║ 
╠══════╬═══════╣ 
║ 1 ║ UP ║ 
║ 2 ║ LL ║ 
║ 3 ║ T  ║ 
╚══════╩═══════╝ 

╔═══════════════════════════════╗ 
║ patients_type_of_blocks ║ 
╠══════════════════╦════════════╣ 
║ type_of_block_id ║ patient_id ║ 
╠══════════════════╬════════════╣ 
║    1 ║   1 ║ 
║    1 ║   2 ║ 
║    2 ║   2 ║ 
║    3 ║   3 ║ 
║    2 ║   4 ║ 
║    1 ║   5 ║ 
║    1 ║   6 ║ 
║    2 ║   6 ║ 
║    3 ║   6 ║ 
╚══════════════════╩════════════╝ 

Tôi muốn đếm số lượng bệnh nhân độc đáo phụ thuộc vào loại kết hợp khối, đây là kết quả mong đợi:

# Expected results (just like a truth table) 
UP (patient with type_of_block_id 1 only) = 2 patient 
UP + LL (patient with type_of_block_ids 1 and 2) = 1 patient 
UP + T (patient with type_of_block_ids 1 and 3) = 0 patient 
LL (patient with type_of_block_id 2 only) = 1 patient 
LL + T (patient with type_of_block_ids 2 and 3) = 0 patient 
T (patient with type_of_block_id 3 only) = 1 patient 
UP + LL + T (patient with type_of_block_ids 1, 2 and 3) = 1 patient 

Tôi đã cố gắng tham gia các bảng như sau:

up_ll = 
    TypeOfBlock. 
    joins("join patients_type_of_blocks on patients_type_of_blocks.type_of_block_id = type_of_blocks.id"). 
    where("patients_type_of_blocks.type_of_block_id = 1 and patients_type_of_blocks.type_of_block_id = 2"). 
    size 

Nhưng có quá nhiều phức tạp, và con số là sai. Tôi muốn thử SQL thô, nhưng Rails 4 không dùng nó và yêu cầu tôi phải làm ModelClass.find_by_sql.

Làm cách nào để tạo kết quả mong đợi ở trên?

Trả lời

5

Giải pháp duy nhất mà tôi nghĩ đến là sử dụng SQL thô và tận dụng group_concat function, như được hiển thị here.

SQL cần thiết là thế này:

SELECT 
    combination, 
    count(*) as cnt 
FROM (
     SELECT 
     ptb.patient_id, 
     group_concat(tb.name ORDER BY tb.name) AS combination 
     FROM type_of_blocks tb 
     INNER JOIN patients_type_of_blocks ptb ON ptb.type_of_block_id = tb.id 
     GROUP BY ptb.patient_id) patient_combinations 
GROUP BY combination; 

Các chọn nhóm bên trong của bệnh nhân và lựa chọn các kết hợp của các loại khối mỗi bệnh nhân có. Việc lựa chọn bên ngoài sau đó chỉ đơn giản là đếm các bệnh nhân trong mỗi kết hợp.

Truy vấn trả về sau (xem SQL fiddle):

combination  cnt 
LL    1 
LL,T,UP   1 
LL,UP   1 
T    1 
UP    2 

Như bạn có thể thấy, các truy vấn không trả lại không đếm, điều này đã được giải quyết trong mã ruby ​​(có lẽ khởi tạo một hash với tất cả kết hợp với zeroes và sau đó hợp nhất với số truy vấn).

Để tích hợp truy vấn này để ruby, chỉ cần sử dụng phương pháp find_by_sql trên bất kỳ mô hình (và ví dụ chuyển đổi kết quả cho một hash):

sql = <<-EOF 
     ...the query from above... 
     EOF 

TypeOfBlock.find_by_sql(sql).to_a.reduce({}) { |h, u| h[u.combination] = u.cnt; h } 
# => { "LL" => 1, "LL,T,UP" => 1, "LL,UP" => 1, "T" => 1, "UP" => 2 } 
+1

này là siêu tuyệt vời. Chỉ đánh dấu điều này là câu trả lời không làm công lý của nó. Tôi sẽ đợi thêm 1 ngày nữa, sau đó cho bạn tiền thưởng cho việc này. Cám ơn rất nhiều. – Victor

+0

Cảm ơn bạn, niềm vui của tôi! :) Và tuyệt vời mà bạn bắt gặp lỗi đánh máy (tôi đã thử nghiệm nó trên các mô hình khác nhau tại địa phương). – BoraMa

+3

**** Nó có thể được mở rộng để xử lý số không chỉ sử dụng MySQL. Nếu quan tâm kiểm tra câu trả lời của tôi :) – lad2025

5

Câu trả lời được cung cấp bởi BoraMa là đúng. Tôi chỉ muốn giải quyết:

Như bạn có thể thấy, các truy vấn không trả lại không đếm, này phải được giải quyết trong mã ruby ​​(có lẽ khởi tạo một hash với tất cả các tổ hợp với zero và sau đó hợp nhất với số lượng truy vấn).

Nó có thể đạt được bằng cách sử dụng MySQL tinh khiết:

SELECT sub.combination, COALESCE(cnt, 0) AS cnt 
FROM (SELECT GROUP_CONCAT(Name ORDER BY Name SEPARATOR ' + ') AS combination 
     FROM (SELECT p.Name, p.rn, LPAD(BIN(u.N + t.N * 10), size, '0') bitmap 
      FROM (SELECT @rownum := @rownum + 1 rn, id, Name 
        FROM type_of_blocks, (SELECT @rownum := 0) r) p 
      CROSS JOIN (SELECT 0 N UNION ALL SELECT 1 
        UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 
        UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 
        UNION ALL SELECT 8 UNION ALL SELECT 9) u 
      CROSS JOIN (SELECT 0 N UNION ALL SELECT 1 
        UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 
        UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 
        UNION ALL SELECT 8 UNION ALL SELECT 9) t 
      CROSS JOIN (SELECT COUNT(*) AS size FROM type_of_blocks) o 
      WHERE u.N + t.N * 10 < POW(2, size) 
      ) b 
     WHERE SUBSTRING(bitmap, rn, 1) = '1' 
     GROUP BY bitmap 
) AS sub 
LEFT JOIN (
    SELECT combination, COUNT(*) AS cnt 
    FROM (SELECT ptb.patient_id, 
       GROUP_CONCAT(tb.name ORDER BY tb.name SEPARATOR ' + ') AS combination 
      FROM type_of_blocks tb 
      JOIN patients_type_of_blocks ptb 
      ON ptb.type_of_block_id = tb.id 
      GROUP BY ptb.patient_id) patient_combinations 
    GROUP BY combination 
) AS sub2 
    ON sub.combination = sub2.combination 
ORDER BY LENGTH(sub.combination), sub.combination; 

SQLFiddleDemo

Output:

╔══════════════╦═════╗ 
║ combination ║ cnt ║ 
╠══════════════╬═════╣ 
║ T   ║ 1 ║ 
║ LL   ║ 1 ║ 
║ UP   ║ 2 ║ 
║ LL + T  ║ 0 ║ 
║ T + UP  ║ 0 ║ 
║ LL + UP  ║ 1 ║ 
║ LL + T + UP ║ 1 ║ 
╚══════════════╩═════╝ 

Cách hoạt động:

  1. Tạo tất cả các kết hợp có thể sử dụng phương pháp mô tả bởi Serpiton (với những cải tiến nhỏ)
  2. Tính sẵn kết hợp
  3. Kết hợp cả kết quả

Để hiểu rõ hơn về cách thức hoạt động Postgresql phiên bản của tạo tất cả các sự kiện:

WITH all_combinations AS (
    SELECT string_agg(b.Name ,' + ' ORDER BY b.Name) AS combination 
    FROM (SELECT p.Name, p.rn, RIGHT(o.n::bit(16)::text, size) AS bitmap 
      FROM (SELECT *, ROW_NUMBER() OVER(ORDER BY id)::int AS rn 
       FROM type_of_blocks)AS p 
      CROSS JOIN generate_series(1, 100000) AS o(n)  
      ,LATERAL(SELECT COUNT(*)::int AS size FROM type_of_blocks) AS s 
      WHERE o.n < 2^size 
     ) b 
    WHERE SUBSTRING(b.bitmap, b.rn, 1) = '1' 
    GROUP BY b.bitmap 
) 
SELECT sub.combination, COALESCE(sub2.cnt, 0) AS cnt 
FROM all_combinations sub 
LEFT JOIN (SELECT combination, COUNT(*) AS cnt 
      FROM (SELECT ptb.patient_id, 
       string_agg(tb.name,' + ' ORDER BY tb.name) AS combination 
       FROM type_of_blocks tb 
       JOIN patients_type_of_blocks ptb 
        ON ptb.type_of_block_id = tb.id 
       GROUP BY ptb.patient_id) patient_combinations 
      GROUP BY combination) AS sub2 
    ON sub.combination = sub2.combination 
ORDER BY LENGTH(sub.combination), sub.combination; 

SqlFiddleDemo2

+0

Rất cám ơn @ lad2025. TIL. Thật không may, tôi đã tặng tiền thưởng cho BoraMa. Cảm ơn một lần nữa. – Victor

+0

Thật tuyệt vời! Tôi vẫn tìm thấy giải pháp ruby ​​dễ đọc hơn nhưng điều này thật tuyệt! – BoraMa

+0

@Victor Bạn luôn có thể bắt đầu tiền thưởng thứ hai nếu bạn nghĩ rằng câu trả lời của tôi đáng giá – lad2025

0

Bạn cũng có thể chỉ làm việc với đường ray

Dự kiến ​​

class PatientsTypeOfBlock < ActiveRecord::Base 
    belongs_to :patient 
    belongs_to :type_of_block 
end 

Query là một ví dụ

PatientsTypeOfBlock.joins(:type_of_block, :patient).where("type_of_blocks.name = ?", "UP").count 
Các vấn đề liên quan