Wie zerlege ich ctid in Seiten- und Zeilennummern?

16

Jede Zeile in einer Tabelle verfügt über eine Systemspalte ctid des Typs tid, der den physischen Speicherort der Zeile darstellt:

create table t(id serial);
insert into t default values;
insert into t default values;
select ctid
     , id
from t;
ctid | Ich würde
: ---- | -:
(0,1) | 1
(0,2) | 2

dbfiddle hier

Was ist der beste Weg , nur die Seitenzahl wie von der des Erhalten ctidin dem am besten geeigneten Typ (zB integer, bigintoder numeric(1000,0))?

Der einzige Weg, den ich mir vorstellen kann, ist sehr hässlich.

Jack Douglas
quelle
1
IIRC ist ein Vektortyp, für den wir keine Zugriffsmethoden haben. Ich bin nicht sicher, ob Sie es von einer C-Funktion aus tun können. Craig wird es mit Sicherheit sagen :)
Dezember
2
Kannst du als POINT besetzen? Z.B. select ct[0], ct[1] from (select ctid::text::point as ct from pg_class where ...) y;
bma
1
Der Titel weist darauf hin, dass Sie sowohl nach der Seitenzahl als auch nach dem Tupelindex stehen. Später werden Sie auf die Seitenzahl eingegrenzt. Ich ging mit der Version in den Körper, Tupel-Index ist eine triviale Erweiterung.
Erwin Brandstetter

Antworten:

21
SELECT (ctid::text::point)[0]::bigint AS page_number FROM t;

Ihre Geige mit meiner Lösung.

@bma hat bereits in einem Kommentar etwas Ähnliches angedeutet. Hier ist ein ...

Begründung für den Typ

ctidist vom Typ tid(Tupelidentifikator), der ItemPointerim C-Code aufgerufen wird . Per Dokumentation:

Dies ist der Datentyp der Systemspalte ctid. Eine Tupel-ID ist ein Paar ( Blocknummer , Tupelindex innerhalb des Blocks ), das die physische Position der Zeile in ihrer Tabelle angibt.

Meine kühne Betonung. Und:

( ItemPointerauch bekannt als CTID)

Ein Block hat in Standardinstallationen eine Größe von 8 KB . Die maximale Tabellengröße beträgt 32 TB . Es folgt logischerweise, dass Blocknummern mindestens ein Maximum von (Berechnung gemäß Kommentar von @Daniel festgelegt) aufnehmen müssen:

SELECT (2^45 / 2^13)::int      -- = 2^32 = 4294967294

Welches würde in ein unsigniertes passen integer. Bei weiteren Nachforschungen stellte ich im Quellcode fest, dass ...

Blöcke werden fortlaufend von 0 bis 0xFFFFFFFE nummeriert .

Meine kühne Betonung. Was die erste Berechnung bestätigt:

SELECT 'xFFFFFFFE'::bit(32)::int8 -- max page number: 4294967294

Postgres verwendet eine Ganzzahl mit Vorzeichen und ist daher ein bisschen kurz. Ich konnte noch nicht festlegen, ob die Textdarstellung verschoben wird, um eine Ganzzahl mit Vorzeichen aufzunehmen. Bis jemand dies aufklären kann, würde ich zurück fallen bigint, was auf jeden Fall funktioniert.

Besetzung

Es ist keine Besetzung für den tidTyp in Postgres 9.3 registriert :

SELECT *
FROM   pg_cast
WHERE  castsource = 'tid'::regtype
OR     casttarget = 'tid'::regtype;

 castsource | casttarget | castfunc | castcontext | castmethod
------------+------------+----------+-------------+------------
(0 rows)

Sie können immer noch zu besetzen text. Es gibt eine Textdarstellung für alles in Postgres :

Eine weitere wichtige Ausnahme ist, dass "automatische E / A-Konvertierungskonvertierungen", bei denen die E / A-Funktionen eines Datentyps zum Konvertieren in oder aus Text oder anderen Zeichenfolgentypen verwendet werden, in nicht explizit dargestellt werden pg_cast.

Die Textdarstellung entspricht der eines Punktes, der aus zwei float8Zahlen besteht, die verlustfrei gewirkt werden.

Sie können auf die erste Nummer eines Punktes mit Index 0 zugreifen bigint. Voilá.

Performance

Ich habe einen Kurztest an einer Tabelle mit 30.000 Zeilen (Best-of-5) mit ein paar alternativen Ausdrücken durchgeführt, die mir in den Sinn kamen, einschließlich Ihres Originals:

SELECT (ctid::text::point)[0]::int                              --  25 ms
      ,right(split_part(ctid::text, ',', 1), -1)::int           --  28 ms
      ,ltrim(split_part(ctid::text, ',', 1), '(')::int          --  29 ms
      ,(ctid::text::t_tid).page_number                          --  31 ms
      ,(translate(ctid::text,'()', '{}')::int[])[1]             --  45 ms
      ,(replace(replace(ctid::text,'(','{'),')','}')::int[])[1] --  51 ms
      ,substring(right(ctid::text, -1), '^\d+')::int            --  52 ms
      ,substring(ctid::text, '^\((\d+),')::int                  -- 143 ms
FROM tbl;

intstatt biginthier, meist irrelevant für den Testzweck. Ich habe es nicht wiederholt bigint.
Die Besetzung t_tidbaut auf einem benutzerdefinierten zusammengesetzten Typ auf, wie beispielsweise @Jake commented.
Das Wesentliche dabei: Das Casting ist in der Regel schneller als die Manipulation von Saiten. Reguläre Ausdrücke sind teuer. Die obige Lösung ist am kürzesten und schnellsten.

Erwin Brandstetter
quelle
1
Danke Erwin, nützliches Zeug. Ab hier sieht es so aus, als wären ctides 6 Bytes mit 4 für die Seite und 2 für die Zeile. Ich war besorgt über das Casting, floataber ich denke, ich muss nicht von dem sprechen, was du hier sagst. Es sieht so aus, als wäre ein benutzerdefinierter zusammengesetzter Typ viel langsamer als der verwendete point. Findest du das auch?
Jack Douglas
@JackDouglas: Bei weiteren Nachforschungen bin ich auf zurückgefallen bigint. Betrachten Sie das Update.
Erwin Brandstetter
1
@ JackDouglas: Ich mag deine Vorstellung von einer Besetzung in einen zusammengesetzten Typ. Es ist sauber und funktioniert sehr gut - auch wenn das pointHin- und Hergehen int8noch schneller ist. Das Umwandeln in vordefinierte Typen ist immer etwas schneller. Ich habe es zu meinem Test hinzugefügt, um es zu vergleichen. Ich würde das machen, (page_number bigint, row_number integer)um sicher zu sein.
Erwin Brandstetter
1
2^40ist nur 1 TB, nicht 32 TB, 2^45was dividiert durch 2^13ergibt 2^32, daher sind die vollen 32 Bits für die Seitenzahl notwendig.
Daniel Vérité
1
Auch vielleicht bemerkenswert ist , dass pg_freespacemap Anwendungen bigintfür blkno
Jack Douglas