Viele Messwerte schnell speichern

Hi ihr,

Ich nutze aktuelles Debian mit PHP und Postgres Datenbank.

Also meine Problemstellung ist:

  • Viele Messwerte (doubles) in einer Postgres Datenbank speichern (viele heißt dabei >30 pro Sekunde)

Ich hab das wie folgt gelöst:

  • Eine Tabelle “values”: Messwert, Sensor_ID, Uhrzeit
  • Eine Tabelle “values_avg”: in der die Druchschnittswerte pro Min/Std/Tag gespeichert werden. (Damit Diagramme ohne Berechnung gezeichnet werden können)
  • Eine plpgsql Function die eben diesen Messwert/Sensor_ID als Input bekommt, und in “values” direkt reinschreibt, und bei “values_avg” jeweils den aktuellen Wert dort einer TMP Spalte addiert, und durch die Anzahl messwerte teilt, also ein schneller Durchschnittswert.(jeweils für Tag/Std/Min)
  • Habe auch alle möglcihen Schrauben in der Config von postgres gredreht, damit er mehr RAM nutzt etc. (geändert ist nun eigentlich nur: shared_buffers = 700MB und effective_cache_size = 1280MB)
  • Indexe sind passende angelegt (jeweils für die Sensor_IDs, und den Tag/Std/Min bei “values_avg”)

Alles läuft soweit auch gut, nur zu langsam, bzw. die Festplatte bremst alles aus (erkennbar am 50% “wait” Wert bei top).

Momentan dauert ein Aufruf der plpgsql Function ca. 70-100ms, und so bekomme ich ja max. 15 Werte/Sekunde hin.

Habt ihr vielleicht einen Tipp wie ich das (Softwaremäßig) beschleunigen könnte?

Ideen:

  • Kann man Postgres sagen, dass er nicht sofort, sondern verzögert, dann in einem Rutsch erst auf die Festplatte schreiben soll, und nicht jeden Einzelwert?
  • Ist es vielleicht sinnvoll in der Anwendung die Daten ein paar Sekunden zu sammeln und dann als “Liste” an eine plpgsql zu geben?
  • Ist vielleicht eine ganz andere Tabellenstruktur besser?
  • Vielleicht eine “Memory” (?) Tabelle im Hauptspeicher, die nur alle par Minunte weggeschrieben wird?

Vielen Dank fürs durchlesen:-)

Gruß
David

das sind ueber 2,5 mio / tag …

da gibt es einiges was man machen kann

  • prepared statements verwenden

  • päckchen deiner messwerte in transaktionen sammeln
    oder vorher in einem string zeilenweise (meinetwegen sowas CSV artiges) und dann mit einem mal auf den server schicken

  • daneben kann man auch asynchrone statements absetzen - ob das aber mit php geht weiss ich nicht. also waerend der server deine daten verarbeitet und aufbereitet koennte ein paralleler prozess/thread bereits neue daten sammeln …

  • und noch mehr …

müssen alle messwerte erhalten bleiben? oder sind nur die aggregierten werte wichtig?

Ich habe ähnliche Projekte mit wesentlich mehr Daten in dem Zeitraum bereits durchgeführt.

Vorneweg: der Ansatz mit einem einzelnen Funktionsaufruf pro Messdatensatz wird nicht funktionieren. Das ist ja noch schlechter und langsamer als die Idee, jeden Datensatz einzeln zu schreiben :wink:

Zu allem weiteren kann ich aber nur etwas sagen, wenn ich (wesentlich) mehr Details kenne und wenn du bereit bist, das gesamte Konzept zu überdenken.

Hi,

erstmal danke an euch beide!

@symbiont:

  • Ja es sind jede Menge Messwerte:-)

  • Prepared statements denke ich brauche ich nicht, da meine SQL Anweisungen ja doch alle in einer “plpgsql” Funktion ablaufen?

  • Transaktionen klingt schonmal nach er guten Idee, ich könnte 10-100 Messwerte sammeln und in einer Transaktion dann senden.

  • Asynchrone Statement klingt gut, aber so wie cih das bei php im handbuch lesen arbeiten die noch dran: “… um zukünftig asynchrone Verbindungen zu unterstützen…”

  • Jein, erhalten werden müssen die Messwerte sccon, aber mein gedanke ist, (die Rohdaten) nach ein paar Monaten dann in CSV oder sowas gezippt zu exportieren und irgendwo zu archivieren. Dann störts die Datenbank auch nicht mehr:-) Die Mittelwerte würde ich aber gerne alle behalten.

@ads:

  • Warum ist EIN Funktionaufruf pro Messwert schlechter, als 5 einzelne Datenbank INSERTs? Ich dachte gerade dafür wären die Functions da, und die hatten das auch sichtlich beschleunigt damals…

  • ich gebe dir gerne mehr Details, und bin auch gerne bereit für mehr geschwindigkeit alles umzustellen!

Hier genaueres:

  • Quelle der Daten ist eine Socket Anwendung, die die Daten ziemlich zügig weitergibt. (Output in eine TXT Datei oder Telnet Konsole sind problemlos >100 Werte/Sek. möglich, daran haperts also wohl nicht)

  • In PHP habe ich einen Socket Client geschrieben (ja ich könnts ja auch in C++ machen…, aber dessen Geschwindigkeit drosselt auch nicht, und läuft bisher sehr stabil, das ganze ist an sich eine Webanwendung, daher sind die meisten Teile in PHP). Jede Datenzeile die ich dort empfange, sende ich dann mittels:

SELECT insert_value('2009-10-12 12:12:00'::timestamp without time zone,1234,12.45::double precision) as value

an die plpgsql Funktion. Die beiden Tabellen sind wie folgt aufgebaut:

CREATE TABLE values
(
  sensor_id bigint NOT NULL,
  "value" double precision,
  value_text character varying(255),
  value_time timestamp without time zone NOT NULL,
  CONSTRAINT values_pkey PRIMARY KEY (sensor_id, value_time)
)
WITH (OIDS=FALSE);


CREATE TABLE values_avg
(
  sensor_id bigint,
  "day" date,
  "hour" smallint,
  value_avg double precision,
  value_min double precision,
  value_max double precision,
  value_count bigint,
  value_variance character varying,
  value_stddev double precision,
  "minute" smallint,
  temp_avg double precision,
  CONSTRAINT values_avg_sensor_id_key UNIQUE (sensor_id, day, hour, minute)
)
WITH (OIDS=FALSE);

CREATE TABLE sensors
(
  id integer NOT NULL,
  value_time timestamp without time zone,
  value_cur double precision,
  value_min double precision,
  value_max double precision,
  is_scalar smallint DEFAULT 0,
  visible smallint DEFAULT 1,
  value_before double precision,
  value_beforetime timestamp without time zone,
  CONSTRAINT sensors_pkey PRIMARY KEY (id)
)
WITH (OIDS=FALSE);

und die Funktion:

CREATE OR REPLACE FUNCTION insert_value(date2 timestamp without time zone, sensor2 integer, 

value2 double precision)
  RETURNS integer AS
$BODY$DECLARE 
 tmpmin double precision;
 tmpmax double precision;
 tmpval double precision; 
 tmpbefore double precision;
 tmpdatebefore timestamp without time zone; 
 comma2 integer; 

 hour2 integer;
 minute2 integer;
 temp_avg2 double precision;
 day2 date;
BEGIN
 -- Hole alte Daten
 SELECT value_min, value_max, value_cur, value_time INTO tmpmin, tmpmax, tmpbefore, 

tmpdatebefore FROM sensors WHERE id=sensor2;

 tmpval := value2;

 -- Suche min, max
 IF (tmpval<tmpmin) OR (tmpmin is NULL) THEN tmpmin := tmpval; END IF;
 IF (tmpval>tmpmax) OR (tmpmax is NULL) THEN tmpmax := tmpval; END IF;

 -- Wenn es der neuste ist Wert, dann Speicher ihn ab
 IF ((tmpdatebefore is null OR tmpdatebefore<=date2)) THEN
  UPDATE sensors SET value_time = date2, value_min = tmpmin, value_max = tmpmax, value_cur = 

tmpval, value_before=tmpbefore, value_beforetime=tmpdatebefore WHERE id=sensor2; 
 END IF; 
 
--Prüfe ob zu dieser Uhrzeit schon ein Wert viorhanden ist (aus Backups etc.)
 SELECT value INTO temp_avg2 FROM values WHERE sensor_id=sensor2 AND value_time=date2;
 IF FOUND THEN
  UPDATE values SET value=tmpval WHERE sensor_id=sensor2 AND value_time=date2;
 ELSE
  INSERT INTO values (sensor_id, value, value_time) VALUES (sensor2, tmpval, date2);
 END IF;

 -- Bilde Mittelwerte für diese Minute
 day2 := date2::date;
 hour2 := date_part('hour',date2);
 minute2 := date_part('minute',date2); 
 SELECT value_min, value_max, temp_avg INTO tmpmin, tmpmax, temp_avg2 FROM values_avg WHERE 

day=day2 AND hour=hour2 AND minute=minute2 AND sensor_id=sensor2;
 IF FOUND THEN
  temp_avg2 := temp_avg2 + tmpval;
  IF (tmpval<tmpmin) OR (tmpmin is NULL) THEN tmpmin := tmpval; END IF;
  IF (tmpval>tmpmax) OR (tmpmax is NULL) THEN tmpmax := tmpval; END IF;
  UPDATE values_avg SET temp_avg=temp_avg2, value_min = tmpmin, value_max = tmpmax, 

value_count=value_count+1, value_avg = (temp_avg/value_count) WHERE sensor_id=sensor2 AND 

day=day2 AND hour=hour2 AND minute=minute2;
 ELSE
  INSERT INTO values_avg (sensor_id, value_count, value_min, value_max, temp_avg, value_avg, 

day, hour, minute) VALUES (sensor2, 1, tmpval, tmpval, tmpval, tmpval, day2, hour2, minute2);
 END IF;

 -- Bilde Mittelwerte für diese Stunde
 minute2 := -1; 
 SELECT value_min, value_max, temp_avg INTO tmpmin, tmpmax, temp_avg2 FROM values_avg WHERE 

day=day2 AND hour=hour2 AND minute=minute2 AND sensor_id=sensor2;
 IF FOUND THEN
  temp_avg2 := temp_avg2 + tmpval;
  IF (tmpval<tmpmin) OR (tmpmin is NULL) THEN tmpmin := tmpval; END IF;
  IF (tmpval>tmpmax) OR (tmpmax is NULL) THEN tmpmax := tmpval; END IF;
  UPDATE values_avg SET temp_avg=temp_avg2, value_min = tmpmin, value_max = tmpmax, 

value_count=value_count+1, value_avg = (temp_avg/value_count) WHERE sensor_id=sensor2 AND 

day=day2 AND hour=hour2 AND minute=minute2;
 ELSE
  INSERT INTO values_avg (sensor_id, value_count, value_min, value_max, temp_avg, value_avg, 

day, hour, minute) VALUES (sensor2, 1, tmpval, tmpval, tmpval, tmpval, day2, hour2, minute2);
 END IF;

 -- Bilde Mittelwerte für diesen Tag
 hour2 := -1; 
 SELECT value_min, value_max, temp_avg INTO tmpmin, tmpmax, temp_avg2 FROM values_avg WHERE 

day=day2 AND hour=hour2 AND minute=minute2 AND sensor_id=sensor2;
 IF FOUND THEN
  temp_avg2 := temp_avg2 + tmpval;
  IF (tmpval<tmpmin) OR (tmpmin is NULL) THEN tmpmin := tmpval; END IF;
  IF (tmpval>tmpmax) OR (tmpmax is NULL) THEN tmpmax := tmpval; END IF;
  UPDATE values_avg SET temp_avg=temp_avg2, value_min = tmpmin, value_max = tmpmax, 

value_count=value_count+1, value_avg = (temp_avg/value_count) WHERE sensor_id=sensor2 AND 

day=day2 AND hour=hour2 AND minute=minute2;
 ELSE
  INSERT INTO values_avg (sensor_id, value_count, value_min, value_max, temp_avg, value_avg, 

day, hour, minute) VALUES (sensor2, 1, tmpval, tmpval, tmpval, tmpval, day2, hour2, minute2);
 END IF;
 
 RETURN 0;
END; $BODY$
  LANGUAGE 'plpgsql' VOLATILE
  COST 100;

Nochmla gesagt, ich baue auch gern alels um, wenns dann merklich besser geht:-) Wichtig waren mir halt eben die Mittelwertbildung, damit ich einfach Diagramme zeichen kann.

Gruß David

Stored Procedures und “plpgsql”-Funktionen sind zwei Namen für ein und die gleiche Sache.

Letztendlich läuft es darauf hinaus, dass ein einzelner Insert so ziemlich das langsamste ist, wie man Daten schreiben kann. Du verlangsamst das halt noch, indem du jeweils eine Funktion aufrufst.


Konzeptionsmäßig würde ich Schreiben und Auslesen trennen. Nur weil das eine Webanwendung ist, bedeutet das nicht, dass man das alles zusammenlegen muss. Außerdem weiß ich aus vergangenen Projekten, dass ein gewisser Fallback für Zeiten ohne Datenbank (z. B. Wartung) sehr hilfreich ist. Dadurch muss man keine einhundertprozentige Verfügbarkeit der Datenbank voraussetzen.

Das Berechnen der Werte würde ich - wenn möglich - nachträglich ausführen. Dadurch kann man die Daten ggf. schneller schreiben und die benötigten Werte für mehrere Zeilen/Datensätze auf einen Schlaf ausrechnen.


David: vielleicht sollten wir zu dem Projekt mal telefonieren?

Hm. Noch sehr früh und ohne Kaffee.

Warum speicherst Du einen Timestamp in 3 Feldern (Tag, Stunde, Minute)?

Ist es nötig, bei jedem Insert auch gleich die values_avg zu aktualisieren oder würde auch ein Trigger reichen, bei bei Insert in die values -Tabelle prüft, ob z.B. eine neue Minute oder Stunde angefangen hat und erst dann die Mittelwerte berechnet und speichert? Das würde enorm die Anzahl der Updates auf die Mittelwerttabelle reduzieren.


Andreas

@ne menge messwerte
nunja ich wollte nur daran erinnern, dass die db sehr schnell stark anwaechst … ich kenne sowas auch von QS systemen die ich mal gebaut habe - mit ner datenbank die man heute schon fast nicht mehr kennt g

aggregationen (avg) beim einfuegen zu berechnen muss man sich halt ueberlegen ob man das unbedingt moechte … es ist prinzipiell machbar (auch schnell) - dann aber eben nicht nur mit avg(feld), sondern summen und anzahl mitschreiben -> redundanz mehr daten …

ist halt immer so ne sache entweder performance oder platz :wink:

wenn man das alles nicht sofort braucht, dann kann man die messwerte auch gleich in eine textdatei appenden … pragmatisch CSV -> und dann per bulk copy bei bedarf in die DB importieren … aber ich denke schon, dass er moeglichst sofort ergebnisse sehen will … hmm muss man halt entscheiden

das nadelör bei sovielen transaktionen ist die latenz von client zu dbserver … 50 - 100 inserts sind ja nicht wirklich eine großenordnung fuer einen modernen rechner - wenn man halt nicht ueber irgednwelche netzwerke operiert und bei jedem aufruf von irgendwas mehrere millisekunden vergeuded … da sollte man zusammenpacken …

vielleicht kann ja die software am messgeraet auch diese messwerte in einer textdatei zusammenfassen … und dann per upload dem php uebergeben, das php koennte die datei dann mit einem rutsch in den dbserver schieben, wo sie zerflueckt wird - postgres hat sehr maechtige und elegante string-funktionen, oder auch einen sehr schoenen copy befehl

@ads:
Das Stored Procedures und “plpgsql” das gleiche sind wusste ich, wir hatten vorher von “prepared statements” und “asynchrone statements” geredet:-)

@akretschmer:

  • Den Timestamp speicher ich in 3 Felder, da ich der Meinung war das es schneller geht, einen Index auf drei Intergerwert zu suchen, als bei einem Timstamp größer kleiner Vergleiche. Wie würde ich denn auch den Tagesdurchschnittswert als Timestamp Speichern? Die minunte sind klar, das geht, aber für Stunde und Tag müsste cih ja dann doch wieder zwei Spalten einfügen is_dayavg und is_houravg oder fällt dir eine einfache Lösung ein?

  • Stimmt, ich könnte nur jede Minute den Mittelwert bilden, aber ich habe mal nachgesehen, da es soviele verschiedene Messwerte gibt, wird der Mittelwert/Minute/Messwert meist eh nur aus 1-2 Messwerten gebildet, ein Trigger ändert da also nicht viel, oder? Und der Stundenmittelwert sollte schon recht zeitnah berechnet werden (für LIVE Diagramme etc.)

@symbiont:
“software am messgeraet auch diese messwerte in einer textdatei”

  • Ich denke eher ist es schneller, wenn ich 50-100 Messwerte im RAM zwischenspeichern und dann zusammen einfüge.

Ich habs heute Nacht mal mit Transaktionen versucht:
Also mit:

BEGIN WORK
SELECT insert_value('2009-10-12 12:12:00'::timestamp without time zone,1234,12.45::double precision) as value
SELECT insert_value('2009-10-12 12:12:00'::timestamp without time zone,1235,1.45::double precision) as value
SELECT insert_value('2009-10-12 12:12:00'::timestamp without time zone,1236,2.45::double precision) as value
... 50x
COMMIT

Allerdings hat sich für mich da kein großer Unterschied ergeben, im Endeffekt war die Festplatte immernoch total am routieren, und pro Messwert hat eher sogar länger gedauert. Oder sollte ich mla versuchen, nicht alle paar MilliSekunden ein insert zu machen, sondern zuerst die Sachen zu sammeln und als EIN großer Datenbankrequet dann abzusenden? Aber macht ein WORK nicht genau das?

Wie würdet ihr denn so die Tabellengrundstruktur für so ne Sache anlegen?

Dein Problem sind immer noch die Funktionsaufrufe, die machen das langsam.

er koennte ja diese aufrufe im speicher zusammenbauen (wie ers oben darstellt) und miteinem mal an den db-server schicken … also nicht jede zeile einzeln … sondern den ganzen block mit execBatch(…) [oder was weiss ich wie so eine fkt in PHP heisst] ausfuehren

dann waere das ne recht fixe geschichte … gut … die funktion koennte man ueberarbeiten - die ist nicht ganz huebsch - aber das ist ein anderes thema

Ja, da kann man vieles verbessern.

Auch fehlt immer noch eine Absicherung gegen eine nicht erreichbare Datenbank. Die Tabelle mit den Daten wächst mit der Zeit unendlich an - das ist auch unschön. Maintenance (löschen alter Daten) ist damit auch nur begrenzt möglich.

@batch - ich habe gerade nachgesehen - und bin mir nichtmehr sicher ob so ein execBatch vom verwendeten DB-Treiber zerlegt und einzeln zur Datenbank geschickt wird - das wuerde dann natuerlich wenig bringen …

dann würde ich als alternative ein

INSERT INTO messwerte (a, b, c) 
VALUES (1, 2, 3), [color="Silver"]-- Messung 1[/color]
  (4, 5, 6),   [color="Silver"]-- Messung 2[/color]
  (7, 2, 2),   [color="Silver"]-- Messung 3[/color]
  (4, 2, 3),
  .... 
  (4, 2, 3);   [color="Silver"]-- Messung n[/color]

vorschlagen.

Das ist auch EINE Transaktion - und definitiv EIN Aufruf :wink:

der schlüssel zu seinen hauptproblemen sind die vielen aufrufe (egal ob funktionen oder viele inserts) - wie du auch schon dargestellt hat - daran sollte er zuerst arbeiten

… dann wuerde ich die messwerte-tabelle als rohdaten betrachten - und tatsaechlich nix weiter berechnen - auch auf indizes jenseits eines primary keys (ID) verzichten - und alle gewuenschten statistiken bei bedarf berechnen … man kann sich ja irgendwo merken, bis zu welcher ID aus den Messwerten die Statistiken Aktuell sind …

[Nachtrag @ Nicht erreichbare DB]
Naja - kommt drauf an - irgendwas kann immer mal “nicht funktionieren” … ob das php dingens ne mail verschickt wenn es die db nicht connecten kann ist ja durchaus denkbar …

Ja.

Angenommen, Du speicherst so:

test=# select * from messwerte ;
         ts          | avg_minute
---------------------+------------
 2009-10-09 10:00:00 |        100
 2009-10-09 10:01:00 |        150
 2009-10-09 10:02:00 |        122
 2009-10-09 11:11:00 |        133
(4 rows)

Und willst nun das als stündliche Zusammenfassung:

test=# create view messwerte_stuendlich as select to_char(ts, 'yyyy-mm-dd hh24 Uhr'), avg(avg_minute) as avg_stuendlich from messwerte group by 1 order by 1;
CREATE VIEW
test=*# select * from messwerte_stuendlich ;
      to_char      | avg_stuendlich
-------------------+----------------
 2009-10-09 10 Uhr |            124
 2009-10-09 11 Uhr |            133
(2 rows)

Andreas

@akretschmer:

create view messwerte_stuendlich as select to_char(ts, 'yyyy-mm-dd hh24 Uhr'), avg(avg_minute) as avg_stuendlich from messwerte group by 1 order by 1;

Sieht ja schön aus, aber ich vermute bei mehreren 10000 Messwerte dauert ein solcher View dann auch recht lange… zumindest länger als meine Lösung, oder?

Oder cacht/berechnet er die Daten für den View schon vorab?

@ads:
Das mit der Fehleranfälligkeit ist anders gelöst, der Sensor schreibt ALLE Daten zusätzlich in eine TEXT Datei, sollte der Server, die Datenbank oder irgendwas nicht erreichbar sein (im Besten Fall Wartung:-)), dann lade ich mir beim nächsten mal alle TEXT Dateien seit dem letzten Eintrag in die Datenbank.

@symbiont:
Die Idee mit dem langen Insert gefällt mir, die Mittelwertbildung könnte ich dann zeitlich davon auskoppeln. Das werd ich mal versuchen.

Nun, alle Daten musst du nicht in eine Text-Datei extra schreiben, das ist extra I/O-Overhead und wird dadurch nicht schneller. Aber Sinn macht das als Fallback schon.

Nun, wir doktoren hier immer noch an Kleinigkeiten und Einzelheiten herum. Ich glaube nicht, dass du auf die Art auf den nächsten drei Forumsseiten zu einer vernünftigen Gesamtlösung kommst.

Nein, das nicht, aber ein passender Index und ein WHERE bei der Abfrage würde da nur das berechnen, was nötig ist.

Dein Problem scheint ja eher das speichern der Datenmenge zu sein, die Auswertung ist eher nicht zeitkritisch, oder?

Andreas

Nein … du kannst aber aus diesem VIEW direkt eine Tabelle erzeugen - oder in einer Tabelle mit gleicher Signatur inserten …

es ging ja nur um das berechnen - weil du meintest du muesstest den timestamp in seinen komponenten speichern

SELECT ... INTO table FROM view WHERE ...

erzeugt dir eine Table mit genau dem inhalt und der struktur

INSERT INTO table SELECT ... FROM view WHERE messungsnummer > letzebearbeitet

fuegt die daten des virew in eine existierende table ein.

wenn du sowas wie ne prozessnummer hast (id z.B.) kannst du das view sehr performant benutzen … und die aggregat-tabelle zum anzeigen der diagramme immer haeppchenweise (on demand) erweitern … alle weiteren abfragen sind dann immer sehr schnell

auf der neuen tabelle kannst dann rumreiten wie du willst ohne die inserts bei der anderen zu belasten - da wuerde ich naemlich mit indizes sparsam bleiben

der ansatz ist ja schon richtig - fuer kompakte diagramme aus sehr vielen messdaten diese vorberechnen …

Hi,

der I/O Overhead ist mir egal, die Sensoren sind ja entfernte geräte, mit eigenem Speicher, und dort werden ALLE Messwerte gespeichert. Ich lade mir nur per SCP die passenden Dateien, wenn ich Lücken habe.

Ich erwarte ja kein komplettes Gesamtkonzept, nur eine Idee für eine (neue) Struktur.

1.) So wie ich das bisher sehe, wäre eine “reine” Messwerttabelle, die mit LANGEN Inserts gefüllt wird gut. Darauf aufbauend dann minütlich oder so die Mittelwertbildung, zB. als Cronjob. Oder?

2.) Da manche Sensoren auch STRINGs liefern (ca. 10%), ist es besser eine zweite Messwerttabelle zu machen, oder eine, mit DOUBLE und character varying?

@akretschmer:

Jein, es geht mir primär um die “Speicher” Geschwindigkeit, allerdings darf ein Diagramm (zb. vom ganzen Jahr) nicht länger als 2 Sek, zur Berechnung der werte dauern:-) Daher fällt ein “on-the-fly” berechnen weg.

Das sind ja schon wieder neue Bedingungen :wink: