web-dev-qa-db-ja.com

PostgreSQLでは別の列でソートしながら1つの列をGROUP BY

onlyを別の列でソートしながら、どのようにして1つの列を_GROUP BY_できますか?.

私は次のことをしようとしています:

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

last/ n /アイテムをFileItemsから降順で選択し、DISTINCTseriesNameの値で行をフィルタリングします。上記のクエリは_ERROR: column "fileitems.dbid" must appear in the GROUP BY clause or be used in an aggregate function_でエラーになります。このクエリの出力を取得するためにdbidの値が必要であり、ソーステーブルでJOINの値を取得して、残りの列を取得しました。

これは基本的に以下の質問のゲシュタルトであり、明確にするために多くの無関係な詳細が削除されていることに注意してください。


元の質問

私はsqlite3からPostgreSQLに移行しているシステムを持っています。

_    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;
_

基本的に、データベース内のlastn DISTINCTアイテムを選択します。ここで、個別の制約が1つの列にあり、並べ替え順序が異なるカラム。

残念ながら、上記のクエリはsqliteでは正常に動作しますが、PostgreSQLではエラー_psycopg2.ProgrammingError: column "fileitems.dbid" must appear in the GROUP BY clause or be used in an aggregate function_でエラーが発生します。

残念ながら、dbIdをGROUP BY句に追加すると問題が修正されますが(_GROUP BY seriesName,dbId_など)、dbidはデータベースの主キーであり、すべての値が異なるため、クエリ結果に対する個別のフィルタリングは機能しなくなります。

Postgresのドキュメント を読むとSELECT DISTINCT ON ({nnn})がありますが、返される結果は_{nnn}_でソートする必要があります。

したがって、_SELECT DISTINCT ON_を使用してやりたいことを行うには、すべての_DISTINCT {nnn}_とそのMAX(retreivalTime)に対してクエリを実行し、再度ソートする必要があります_{nnn}_ではなくretreivalTimeを使用して、最大の100を取得し、テーブルに対してそれらを使用してクエリを実行し、残りの行を取得します。 seriesName列に〜175Kの行と〜14Kの個別の値があり、最新の100だけが必要です。このクエリはパフォーマンスがやや重要です(クエリ時間は1/2秒未満が必要です)。

ここでの単純な仮定は、基本的に、DBはretreivalTimeの降順で各行を反復処理し、LIMIT項目が表示されたら停止する必要があるため、完全なテーブルクエリは理想的ではありませんが、実際にどのように理解するつもりはありませんデータベースシステムは内部的に最適化されますが、私はこれに完全に間違ったアプローチをしている可能性があります。

FWIW、Idoは、異なるOFFSET値を使用することがありますが、offset>〜500が完全に許容される場合、クエリ時間が長くなります。基本的に、OFFSETは、スクロールカーソルを各接続専用にする必要なしに逃げることができる、くだらないページングメカニズムです。おそらく、いつか再訪するでしょう。


参照- このクエリにつながる1か月前に尋ねた質問


OK、もっとメモ:

_    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;
_

説明されているようにクエリに対して正しく機能しますが、_GROUP BY_句をremoveすると、失敗します(アプリケーションではオプションです)。

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

私は、PostgreSQLでサブクエリがどのように機能するかを根本的に理解していないと思います。どこがいけないの?サブクエリは基本的にインライン関数であり、結果がメインクエリにフィードされるだけであるという印象を受けました。

8
Fake Name

一貫した行

まだあなたのレーダーにはないように見える重要な質問:
同じseriesNameの各行セットから、one行の列、またはany複数の行の値(一緒に移動する場合としない場合があります)?

あなたの答えは後者です、あなたは最大のdbidを最大のretreivaltimeと組み合わせます。これは別の行から来るかもしれません。

consistent行を取得するには、_DISTINCT ON_を使用してサブクエリにラップし、結果を異なる順序で並べます。

_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;
_

_DISTINCT ON_の詳細:

余談ですが、おそらくretrievalTimeである必要があります。さらに良いのは_retrieval_time_です。引用符で囲まれていない大文字と小文字の識別子は、Postgresでの混乱の一般的な原因です。

RCTEによるパフォーマンスの向上

ここでは大きなテーブルを扱っているため、インデックスを使用できるクエリが必要です。これは、上記のクエリには当てはまりません(_WHERE sourceSite = 'mk'_を除く)。

よく調べてみると、問題はルーズインデックススキャンの特殊なケースのようです。 Postgresはネイティブではルーズインデックススキャンをサポートしていませんが、再帰CTEでエミュレートできます。 Postgres Wikiの単純なケースのコード例 があります。

SOに関する関連回答、より高度なソリューション、説明、フィドル:

しかし、あなたのケースはもっと複雑です。しかし、私はそれをあなたのために機能させるための変種を見つけたと思います。このインデックスに基づく(_WHERE sourceSite = 'mk'_なし)

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

または(_WHERE sourceSite = 'mk'_を使用)

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

最初のインデックスは両方のクエリに使用できますが、追加のWHERE条件では完全に効率的ではありません。 2番目のインデックスは、最初のクエリに対して非常に限定的に使用されます。クエリの両方のバリアントがあるので、bothインデックスを作成することを検討してください。

Index Onlyscans を許可するために、最後にdbidを追加しました。

再帰CTEを使用したこのクエリは、インデックスを使用します。 Postgres 9.3でテストしましたが、動作します:順次スキャンなし、すべてindex-onlyスキャン:

_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;
_

seriesNameは一意ではないため、retreivaltimeを_ORDER BY_に含めるには、needします。 「ほぼ」の一意性は、依然としてnot一意です。

説明する

  • 非再帰クエリは、最新の行から始まります。

  • 再帰クエリは、100行になるまで、リストにないseriesNameを含む次の最新の行を追加します。

  • 重要な部分は、JOIN条件_(b.retreivaltime, b.seriesName) < (c.retreivaltime, c.seriesName)_および_ORDER BY_句_ORDER BY retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST_です。どちらもインデックスのソート順と一致しているため、マジックが発生します。

  • 重複を除外するために配列でseriesNameを収集します。 b.seriesName <> ALL(c.foo_arr)のコストは、行の数に応じて徐々に増加しますが、100行だけの場合でも、まだ安価です。

  • コメントで明記されているようにdbidを返すだけです。

部分的なインデックスを持つ代替:

以前にも同様の問題を扱ってきました。以下は、部分インデックスとループ関数に基づいた高度に最適化された完全なソリューションです。

正しく行われた場合、おそらく最もマテリアライズドビューを除いて最も速い方法です。しかし、もっと複雑です。

マテリアライズドビュー

コメントに記載されているように、書き込み操作は多くなく、パフォーマンスも重要ではないため(質問に含める必要があります)、saveマテリアライズドビューの上位n個の事前計算された行。基になるテーブルに関連する変更を加えた後、更新します。代わりに、パフォーマンス重視のクエリをマテリアライズドビューに基づいてください。

  • 最新の1000 dbid程度の「薄い」mvの場合もあります。クエリで、元のテーブルに結合します。たとえば、コンテンツが時々更新されても、上位n行は変更されないままである場合があります。

  • または、返す行全体を含む「太い」mv。まだ高速です。明らかに、より頻繁に更新する必要があります。

マニュアルの詳細 here および here

9

わかりました、私はドキュメントをもっと読みました、そして今、私は問題を少なくとも少しよく理解します。

基本的には、GROUP BY seriesName集計の結果として、dbidには複数の値が存在する可能性があります。 SQLiteとMySQLでは、どうやらDBエンジンが1つをランダムに選択するだけです(これは私のアプリケーションではまったく問題ありません)。

ただし、PostgreSQLははるかに保守的であるため、ランダムな値を選択するのではなく、エラーをスローします。

このクエリを機能させる簡単な方法は、関連する値に集計関数を適用することです。

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;

これにより、クエリ出力が完全に修飾され、クエリが機能するようになります。

5
Fake Name

ええと、私は実際に、データベースの外でいくつかの手続き型ロジックを使用して、やりたいことを達成するために仕上げました。

基本的に、99%の時間、lastが欲しい 100 200件クエリプランナーはこれを最適化していないようで、OFFSETの値が大きい場合、手続き型フィルターの処理速度が大幅に低下します。

とにかく、名前付きカーソルを使用して手動でデータベースの行を反復処理し、数百のグループで行を取得しました。次に、アプリケーションコードでそれらを区別するためにそれらをフィルター処理し、必要な明確な結果の数を累積した直後にカーソルを閉じます。

makoコード(基本的にはpython)。たくさんのデバッグステートメントが残っています。

<%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>

これは現在、最新の 100 200の異なるシリーズアイテム 115 〜80ミリ秒(より短い時間は、TCPソケット)ではなく、ローカル接続を使用している場合)、約1500行の処理中。

コメントを来てください:

  • 行は250のチャンクで読み取られます。
  • buildWhereQueryは私自身の動的クエリビルダーです。はい、これは恐ろしい考えです。はい、SQLalchemyなどについて知っています。 A.これは私が自宅のLANの外で使用することを期待していない個人的なプロジェクトであり、B。それはSQLを学ぶための素晴らしい方法であるため、独自に作成しました。
  • オフセットの値に応じて、2つのクエリメカニズムを切り替えることを検討します。オフセット> 1000で、個別のアイテムをフィルタリングしている場合、このアプローチは、@ ErwinBrandstetterの回答にあるような手順に必要な時間を超え始めます。
  • @ErwinBrandstetterの答えは、まだはるかに優れていますgeneralソリューションです。これは、非常に特殊な1つの場合にのみ優れています。
  • なんらかの理由で、2つのカーソルを使用する必要がありました。トランザクションを使用していない限り、名前付きカーソルを作成することはできませんが、カーソルなしではトランザクションを開始できません(注-これはautocommitモードoffを使用する場合)。匿名カーソルをインスタンス化し、SQL(ここではBEGINのみ)を発行し、名前付きカーソルを作成し、それを使用して閉じ、最後に匿名カーソルでコミットする必要があります。
  • これはおそらく完全にPL/pgSQLで行うことができ、結果はおそらくさらに速くなりますが、私はpythonより良いことを知っています。
1
Fake Name