NHÓM THEO một cột, trong khi sắp xếp theo một cột khác trong PostgreSQL


8

Làm thế nào tôi có thể GROUP BYmột cột, trong khi sắp xếp chỉ bằng một cột khác.

Tôi đang cố gắng làm như sau:

SELECT dbId,retreivalTime 
    FROM FileItems 
    WHERE sourceSite='something' 
    GROUP BY seriesName 
    ORDER BY retreivalTime DESC 
    LIMIT 100 
    OFFSET 0;

Tôi muốn chọn cuối cùng / n / mục từ FileItems, theo thứ tự giảm dần, với các hàng được lọc theo DISTINCTcác giá trị của seriesName. Các lỗi truy vấn trên ERROR: column "fileitems.dbid" must appear in the GROUP BY clause or be used in an aggregate function. Tôi cần dbidgiá trị để sau đó lấy đầu ra của truy vấn này và JOINnó trên bảng nguồn để lấy phần còn lại của các cột mà tôi đã chọn.

Lưu ý rằng về cơ bản đây là dấu hiệu của câu hỏi dưới đây, với rất nhiều chi tiết không liên quan được loại bỏ cho rõ ràng.


Câu hỏi gốc

Tôi có một hệ thống tôi đang chuyển từ sqlite3 sang PostgreSQL, vì tôi đã vượt xa sqlite:

    SELECT
            d.dbId,
            d.dlState,
            d.sourceSite,
        [snip a bunch of rows]
            d.note

    FROM FileItems AS d
        JOIN
            ( SELECT dbId
                FROM FileItems
                WHERE sourceSite='{something}'
                GROUP BY seriesName
                ORDER BY MAX(retreivalTime) DESC
                LIMIT 100
                OFFSET 0
            ) AS di
            ON  di.dbId = d.dbId
    ORDER BY d.retreivalTime DESC;

Về cơ bản, tôi muốn chọn n mục cuối cùngDISTINCT trong cơ sở dữ liệu, trong đó ràng buộc riêng biệt nằm trên một cột và thứ tự sắp xếp nằm trên một cột khác.

Thật không may, truy vấn trên, trong khi nó hoạt động tốt trong sqlite, lỗi trong PostgreQuery với lỗi psycopg2.ProgrammingError: column "fileitems.dbid" must appear in the GROUP BY clause or be used in an aggregate function.

Thật không may, trong khi thêm dbIdvào mệnh đề GROUP BY khắc phục sự cố (ví dụ GROUP BY seriesName,dbId), điều đó có nghĩa là bộ lọc riêng biệt trên kết quả truy vấn không còn hoạt động, vì dbidlà khóa chính của cơ sở dữ liệu và vì vậy tất cả các giá trị đều khác biệt.

Từ việc đọc tài liệu Postgres , có SELECT DISTINCT ON ({nnn}), nhưng điều đó đòi hỏi các kết quả trả về phải được sắp xếp theo {nnn}.

Vì vậy, để làm những gì tôi muốn qua SELECT DISTINCT ON, tôi sẽ phải truy vấn cho tất cả DISTINCT {nnn}và họ MAX(retreivalTime), sắp xếp lại bằng cách retreivalTimethay sau đó {nnn}, sau đó lấy lớn nhất 100 và truy vấn sử dụng những chống lại bảng để có được phần còn lại của các hàng, mà tôi Tôi muốn tránh, vì cơ sở dữ liệu có ~ 175K hàng và ~ 14K giá trị riêng biệt trong seriesNamecột, tôi chỉ muốn 100 mới nhất và truy vấn này có phần quan trọng về hiệu suất (tôi cần thời gian truy vấn <1/2 giây).

Giả định ngây thơ của tôi ở đây về cơ bản là DB cần lặp lại theo từng hàng theo thứ tự giảm dần retreivalTimevà chỉ dừng lại một khi nó nhìn thấy LIMITcác mục, vì vậy một truy vấn bảng đầy đủ là không lý tưởng, nhưng tôi không giả vờ hiểu cơ sở dữ liệu như thế nào hệ thống tối ưu hóa trong nội bộ, và tôi có thể đang tiếp cận điều này hoàn toàn sai.

FWIW, đôi khi tôi sử dụng các OFFSETgiá trị khác nhau , nhưng thời gian truy vấn dài cho các trường hợp có độ lệch> ~ 500 là hoàn toàn chấp nhận được. Về cơ bản, OFFSETlà một cơ chế phân trang xảo quyệt cho phép tôi thoát ra mà không cần phải dành các con trỏ cuộn cho mỗi kết nối, và có lẽ tôi sẽ xem lại nó vào một lúc nào đó.


Tham khảo - Câu hỏi tôi đã hỏi một tháng trước dẫn đến truy vấn này .


Ok, ghi chú thêm:

    SELECT
            d.dbId,
            d.dlState,
            d.sourceSite,
        [snip a bunch of rows]
            d.note

    FROM FileItems AS d
        JOIN
            ( SELECT seriesName, MAX(retreivalTime) AS max_retreivalTime
                FROM FileItems
                WHERE sourceSite='{something}'
                GROUP BY seriesName
                ORDER BY max_retreivalTime DESC
                LIMIT %s
                OFFSET %s
            ) AS di
            ON  di.seriesName = d.seriesName AND di.max_retreivalTime = d.retreivalTime
    ORDER BY d.retreivalTime DESC;

Hoạt động chính xác cho truy vấn như mô tả, nhưng nếu tôi loại bỏ các GROUP BYđiều khoản, nó không thành công (đó là tùy chọn trong ứng dụng của tôi).

psycopg2.ProgrammingError: column "FileItems.seriesname" must appear in the GROUP BY clause or be used in an aggregate function

Tôi nghĩ rằng về cơ bản tôi không hiểu cách thức các truy vấn con hoạt động trong PostgreSQL. Tôi làm sai ở đâu? Tôi có ấn tượng rằng một truy vấn con về cơ bản chỉ là một hàm nội tuyến, trong đó các kết quả chỉ được đưa vào truy vấn chính.

Câu trả lời:


9

Hàng nhất quán

Câu hỏi quan trọng dường như chưa có trên radar của bạn:
Từ mỗi bộ hàng giống nhau seriesName, bạn có muốn các cột của một hàng hoặc chỉ bất kỳ giá trị nào từ nhiều hàng (có thể có hoặc không đi cùng nhau)?

Câu trả lời của bạn thực hiện sau, bạn kết hợp tối đa dbidvới tối đa retreivaltime, có thể đến từ một hàng khác nhau.

Để có được các hàng nhất quán , hãy sử dụng DISTINCT ONvà bọc nó trong một truy vấn con để sắp xếp kết quả khác nhau:

SELECT * FROM (
   SELECT DISTINCT ON (seriesName)
          dbid, seriesName, retreivaltime
   FROM   FileItems
   WHERE  sourceSite = 'mk' 
   ORDER  BY seriesName, retreivaltime DESC NULLS LAST  -- latest retreivaltime
   ) sub
ORDER BY retreivaltime DESC NULLS LAST
LIMIT  100;

Chi tiết cho DISTINCT ON:

Ngoài ra: có lẽ nên retrievalTime, hoặc tốt hơn nữa : retrieval_time. Số nhận dạng trường hợp hỗn hợp không được trích dẫn là một nguồn gây nhầm lẫn phổ biến trong Postgres.

Hiệu suất tốt hơn với rCTE

Vì chúng tôi đang xử lý một bảng lớn ở đây, chúng tôi cần một truy vấn có thể sử dụng một chỉ mục, đây không phải là trường hợp của truy vấn trên (ngoại trừ WHERE sourceSite = 'mk')

Khi kiểm tra kỹ hơn, vấn đề của bạn dường như là một trường hợp đặc biệt của việc quét chỉ mục lỏng lẻo . Postgres không hỗ trợ quét chỉ mục lỏng lẻo, nhưng nó có thể được mô phỏng bằng CTE đệ quy . Có một ví dụ mã cho trường hợp đơn giản trong Postgres Wiki.

Câu trả lời liên quan về SO với các giải pháp, giải thích, fiddle nâng cao hơn:

Trường hợp của bạn phức tạp hơn, mặc dù. Nhưng tôi nghĩ rằng tôi đã tìm thấy một biến thể để làm cho nó hoạt động cho bạn. Dựa trên chỉ số này (không có WHERE sourceSite = 'mk')

CREATE INDEX mi_special_full_idx ON MangaItems
(retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST, dbid)

Hoặc (với WHERE sourceSite = 'mk')

CREATE INDEX mi_special_granulated_idx ON MangaItems
(sourceSite, retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST, dbid)

Chỉ mục đầu tiên có thể được sử dụng cho cả hai truy vấn, nhưng không hoàn toàn hiệu quả với điều kiện WHERE bổ sung. Chỉ mục thứ hai được sử dụng rất hạn chế cho truy vấn đầu tiên. Vì bạn có cả hai biến thể của truy vấn, hãy xem xét việc tạo cả hai chỉ mục.

Tôi đã thêm dbidvào cuối để cho phép quét Chỉ mục .

Truy vấn này với CTE đệ quy sử dụng chỉ mục. Tôi đã thử nghiệm với Postgres 9.3 và nó hoạt động với tôi: không quét liên tiếp, tất cả các lần quét chỉ mục :

WITH RECURSIVE cte AS (
   (
   SELECT dbid, seriesName, retreivaltime, 1 AS rn, ARRAY[seriesName] AS arr
   FROM   MangaItems
   WHERE  sourceSite = 'mk'
   ORDER  BY retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST
   LIMIT  1
   )
   UNION ALL
   SELECT i.dbid, i.seriesName, i.retreivaltime, c.rn + 1, c.arr || i.seriesName
   FROM   cte c
   ,      LATERAL (
      SELECT dbid, seriesName, retreivaltime
      FROM   MangaItems
      WHERE (retreivaltime, seriesName) < (c.retreivaltime, c.seriesName)
      AND    sourceSite = 'mk'  -- repeat condition!
      AND    seriesName <> ALL(c.arr)
      ORDER  BY retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST
      LIMIT  1
      ) i
   WHERE  c.rn < 101
   )
SELECT dbid
FROM   cte
ORDER  BY rn;

Bạn cần bao gồm seriesName trong ORDER BY, vì retreivaltimekhông phải là duy nhất. "Hầu như" duy nhất vẫn không phải là duy nhất.

Giải thích

  • Truy vấn không đệ quy bắt đầu với hàng mới nhất.

  • Truy vấn đệ quy thêm hàng mới nhất tiếp theo với một seriesNamehàng không có trong danh sách, v.v., cho đến khi chúng tôi có 100 hàng.

  • Phần thiết yếu là JOINđiều kiện (b.retreivaltime, b.seriesName) < (c.retreivaltime, c.seriesName)ORDER BYmệnh đề ORDER BY retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST. Cả hai đều khớp với thứ tự sắp xếp của chỉ số, cho phép phép màu xảy ra.

  • Thu thập seriesNametrong một mảng để loại trừ trùng lặp. Chi phí để b.seriesName <> ALL(c.foo_arr)tăng dần theo số lượng hàng, nhưng chỉ với 100 hàng thì vẫn còn rẻ.

  • Chỉ cần trở lại dbidnhư được làm rõ trong các ý kiến.

Thay thế bằng chỉ mục một phần:

Chúng tôi đã được xử lý các vấn đề tương tự trước đây. Đây là một giải pháp hoàn chỉnh được tối ưu hóa cao dựa trên các chỉ mục một phần và chức năng lặp:

Có lẽ là cách nhanh nhất (ngoại trừ một quan điểm cụ thể hóa) nếu được thực hiện đúng. Nhưng phức tạp hơn.

Chế độ xem cụ thể

Vì bạn không có nhiều thao tác ghi và chúng không quan trọng về hiệu năng như đã nêu trong các nhận xét (nên có trong câu hỏi), hãy lưu n hàng đầu được tính toán trước trong chế độ xem cụ thể hóa và làm mới nó sau khi thay đổi có liên quan đến bảng bên dưới. Thay vào đó, hãy dựa vào các truy vấn quan trọng về hiệu suất của bạn trên chế độ xem cụ thể hóa.

  • Có thể chỉ là một mv "mỏng" trong số 1000 mới nhất dbid. Trong truy vấn, tham gia vào bảng gốc. Chẳng hạn, nếu nội dung đôi khi được cập nhật, nhưng n hàng trên cùng có thể không thay đổi.

  • Hoặc một mv "béo" với toàn bộ hàng để trả về. Nhanh hơn, chưa. Cần phải được làm mới thường xuyên hơn, rõ ràng.

Chi tiết trong hướng dẫn ở đâyở đây .


5

Ok, tôi đã đọc các tài liệu nhiều hơn và bây giờ tôi hiểu vấn đề ít nhất là tốt hơn một chút.

Về cơ bản, những gì đang diễn ra là có nhiều giá trị có thể dbidlà kết quả của GROUP BY seriesNametổng hợp. Với SQLite và MySQL, rõ ràng công cụ DB chỉ chọn ngẫu nhiên một cái (điều này hoàn toàn tốt trong ứng dụng của tôi).

Tuy nhiên, PostgreSQL bảo thủ hơn nhiều, vì vậy sau đó chọn một giá trị ngẫu nhiên, nó sẽ đưa ra một lỗi.

Một cách đơn giản để làm cho truy vấn này hoạt động là áp dụng hàm tổng hợp cho giá trị liên quan:

SELECT MAX(dbid) AS mdbid, seriesName, MAX(retreivaltime) AS mrt
    FROM MangaItems 
    WHERE sourceSite='mk' 
    GROUP BY seriesName
    ORDER BY mrt DESC 
    LIMIT 100 
    OFFSET 0;

Điều này làm cho đầu ra truy vấn đủ điều kiện và truy vấn hiện hoạt động.


1

Chà, tôi thực sự đã sử dụng một số logic thủ tục bên ngoài cơ sở dữ liệu để thực hiện những gì tôi muốn làm.

Về cơ bản, 99% thời gian, tôi muốn 100 200 kết quả cuối cùng . Công cụ lập kế hoạch truy vấn dường như không tối ưu hóa cho điều này và nếu giá trị lớn, bộ lọc thủ tục của tôi sẽ chậm hơn nhiều.OFFSET

Dù sao, tôi đã sử dụng một con trỏ có tên để lặp lại các hàng trong cơ sở dữ liệu theo cách thủ công, truy xuất lại các hàng theo nhóm vài trăm. Sau đó, tôi lọc chúng để phân biệt mã ứng dụng của mình và đóng con trỏ ngay sau khi tôi tích lũy được số lượng kết quả khác biệt tôi muốn.

Các makomã (về cơ bản python). Rất nhiều báo cáo gỡ lỗi còn lại.

<%def name="fetchMangaItems(flags='', limit=100, offset=0, distinct=False, tableKey=None, seriesName=None)">
    <%
        if distinct and seriesName:
            raise ValueError("Cannot filter for distinct on a single series!")

        if flags:
            raise ValueError("TODO: Implement flag filtering!")

        whereStr, queryAdditionalArgs = buildWhereQuery(tableKey, None, seriesName=seriesName)
        params = tuple(queryAdditionalArgs)


        anonCur = sqlCon.cursor()
        anonCur.execute("BEGIN;")

        cur = sqlCon.cursor(name='test-cursor-1')
        cur.arraysize = 250
        query = '''

            SELECT
                    dbId,
                    dlState,
                    sourceSite,
                    sourceUrl,
                    retreivalTime,
                    sourceId,
                    seriesName,
                    fileName,
                    originName,
                    downloadPath,
                    flags,
                    tags,
                    note

            FROM MangaItems
            {query}
            ORDER BY retreivalTime DESC;'''.format(query=whereStr)

        start = time.time()
        print("time", start)
        print("Query = ", query)
        print("params = ", params)
        print("tableKey = ", tableKey)

        ret = cur.execute(query, params)
        print("Cursor ret = ", ret)
        # for item in cur:
        #   print("Row", item)

        seenItems = []
        rowsBuf = cur.fetchmany()

        rowsRead = 0

        while len(seenItems) < offset:
            if not rowsBuf:
                rowsBuf = cur.fetchmany()
            row = rowsBuf.pop(0)
            rowsRead += 1
            if row[6] not in seenItems or not distinct:
                seenItems.append(row[6])

        retRows = []

        while len(seenItems) < offset+limit:
            if not rowsBuf:
                rowsBuf = cur.fetchmany()
            row = rowsBuf.pop(0)
            rowsRead += 1
            if row[6] not in seenItems or not distinct:
                retRows.append(row)
                seenItems.append(row[6])

        cur.close()
        anonCur.execute("COMMIT;")

        print("duration", time.time()-start)
        print("Rows used", rowsRead)
        print("Query complete!")

        return retRows
    %>

</%def>

Điều này hiện đang truy xuất 100 200 mục khác nhau mới nhất trong 115 ~ 80 mili giây (thời gian thấp hơn là khi sử dụng kết nối cục bộ, thay vì ổ cắm TCP), trong khi xử lý khoảng 1500 hàng.

Hãy bình luận:

  • Hàng được đọc trong khối 250.
  • buildWhereQuerylà trình xây dựng truy vấn động của riêng tôi. Vâng, đây là một ý tưởng khủng khiếp. Vâng, tôi biết về SQLalchemy et al. Tôi đã tự viết vì A. đây là một dự án cá nhân mà tôi không bao giờ sử dụng ngoài mạng LAN gia đình của mình và B. Đó là một cách tuyệt vời để học SQL.
  • Tôi có thể xem xét chuyển đổi giữa hai cơ chế truy vấn là phụ thuộc vào giá trị của offset. Có vẻ như khi bù> 1000 và tôi đang lọc các mục riêng biệt, cách tiếp cận này bắt đầu vượt quá thời gian cần thiết cho các thủ tục như các câu hỏi trong câu trả lời của @ ErwinBrandstetter.
  • Câu trả lời của @ ErwinBrandstetter vẫn là một giải pháp chung tốt hơn nhiều . Điều này chỉ tốt hơn trong một trường hợp rất cụ thể.
  • Tôi đã phải sử dụng hai con trỏ, vì một số lý do kỳ lạ. Bạn không thể tạo một con trỏ được đặt tên trừ khi bạn tham gia giao dịch, nhưng bạn không thể bắt đầu giao dịch mà không có con trỏ (lưu ý - đây là autocommitchế độ tắt ). Tôi phải khởi tạo một con trỏ ẩn danh, phát hành một số SQL (chỉ là a BEGIN, ở đây), tạo con trỏ có tên của tôi, sử dụng nó, đóng nó và cuối cùng là cam kết với con trỏ ẩn danh.
  • Điều này có thể được thực hiện hoàn toàn trong PL / pgSQL và kết quả có thể sẽ nhanh hơn nữa, nhưng tôi biết python tốt hơn nhiều.
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.